Repository navigation
Expand file tree
/
Copy pathModelClientRouter.cs
More file actions
73 lines (64 loc) · 2.85 KB
/
Copy pathModelClientRouter.cs
File metadata and controls
73 lines (64 loc) · 2.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MissionController.Core.Interfaces;
namespace MissionController.Infrastructure.Network.Ai;
/// <summary>
/// Routes each mission to whichever governed provider client actually has credentials.
///
/// Selection policy (a product decision, kept in exactly one place):
/// 1. Claude is the product default - it wins whenever an Anthropic key is configured.
/// 2. Gemini engages only when it holds the sole configured key, so a user who pastes either
/// vendor's key gets live analysis instead of the simulated placeholder.
/// 3. With no keys at all, the Claude client's honest SIMULATED path handles messaging.
///
/// The router owns no HTTP and no egress pinning - each concrete client pins its own host
/// through the Layer-4 firewall, so routing can never widen the whitelist.
/// </summary>
public sealed class ModelClientRouter : IModelStreamClient
{
private readonly ClaudeStreamClient _claude;
private readonly GeminiStreamClient _gemini;
public ModelClientRouter(ClaudeStreamClient claude, GeminiStreamClient gemini)
{
_claude = claude;
_gemini = gemini;
}
public async Task<ModelAvailability> CheckAvailabilityAsync(
string requestedModel,
CancellationToken cancellationToken = default)
{
var claude = await _claude.CheckAvailabilityAsync(requestedModel, cancellationToken);
if (claude.IsLive)
return claude;
var gemini = await _gemini.CheckAvailabilityAsync(requestedModel, cancellationToken);
if (gemini.IsLive)
return gemini;
// Neither provider is configured - report the full recovery path, not just one vendor's.
return claude with
{
UnavailableReason =
"No AI provider key configured (Anthropic or Gemini). Add either key in Settings to enable live analysis."
};
}
public async IAsyncEnumerable<string> StreamAnalysisAsync(
string requestedModel,
string systemPrompt,
string userPrompt,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var client = await SelectAsync(requestedModel, cancellationToken);
await foreach (var chunk in client.StreamAnalysisAsync(requestedModel, systemPrompt, userPrompt, cancellationToken))
{
yield return chunk;
}
}
private async Task<IModelStreamClient> SelectAsync(string requestedModel, CancellationToken cancellationToken)
{
var claude = await _claude.CheckAvailabilityAsync(requestedModel, cancellationToken);
if (claude.IsLive)
return _claude;
var gemini = await _gemini.CheckAvailabilityAsync(requestedModel, cancellationToken);
return gemini.IsLive ? _gemini : _claude;
}
}