@jsonstudio/llms 0.6.1449 → 0.6.1643

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
  2. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
  3. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
  4. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
  5. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
  7. package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
  8. package/dist/conversion/compat/antigravity-session-signature.js +833 -21
  9. package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
  10. package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
  12. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
  13. package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
  14. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
  16. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
  17. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
  18. package/dist/conversion/hub/process/chat-process.js +300 -67
  19. package/dist/conversion/hub/response/provider-response.js +4 -3
  20. package/dist/conversion/shared/gemini-tool-utils.js +134 -9
  21. package/dist/conversion/shared/text-markup-normalizer.js +90 -1
  22. package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
  23. package/dist/conversion/shared/thought-signature-validator.js +2 -1
  24. package/dist/quota/apikey-reset.d.ts +17 -0
  25. package/dist/quota/apikey-reset.js +43 -0
  26. package/dist/quota/index.d.ts +2 -0
  27. package/dist/quota/index.js +1 -0
  28. package/dist/quota/quota-manager.d.ts +44 -0
  29. package/dist/quota/quota-manager.js +491 -0
  30. package/dist/quota/quota-state.d.ts +6 -0
  31. package/dist/quota/quota-state.js +167 -0
  32. package/dist/quota/types.d.ts +61 -0
  33. package/dist/quota/types.js +1 -0
  34. package/dist/router/virtual-router/bootstrap.js +103 -6
  35. package/dist/router/virtual-router/engine-health.js +104 -0
  36. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
  37. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
  38. package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
  39. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
  40. package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
  41. package/dist/router/virtual-router/engine-selection.js +2 -2
  42. package/dist/router/virtual-router/engine.d.ts +16 -1
  43. package/dist/router/virtual-router/engine.js +320 -42
  44. package/dist/router/virtual-router/features.js +20 -2
  45. package/dist/router/virtual-router/success-center.d.ts +10 -0
  46. package/dist/router/virtual-router/success-center.js +32 -0
  47. package/dist/router/virtual-router/types.d.ts +48 -0
  48. package/dist/servertool/clock/config.d.ts +2 -0
  49. package/dist/servertool/clock/config.js +10 -2
  50. package/dist/servertool/clock/daemon.js +3 -0
  51. package/dist/servertool/clock/ntp.d.ts +18 -0
  52. package/dist/servertool/clock/ntp.js +318 -0
  53. package/dist/servertool/clock/paths.d.ts +1 -0
  54. package/dist/servertool/clock/paths.js +3 -0
  55. package/dist/servertool/clock/state.d.ts +2 -0
  56. package/dist/servertool/clock/state.js +15 -2
  57. package/dist/servertool/clock/tasks.d.ts +1 -0
  58. package/dist/servertool/clock/tasks.js +24 -1
  59. package/dist/servertool/clock/types.d.ts +21 -0
  60. package/dist/servertool/engine.js +105 -1
  61. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
  62. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
  63. package/dist/servertool/handlers/clock-auto.js +39 -4
  64. package/dist/servertool/handlers/clock.js +145 -16
  65. package/dist/servertool/handlers/followup-request-builder.js +84 -0
  66. package/dist/servertool/handlers/stop-message-auto.js +1 -1
  67. package/dist/servertool/server-side-tools.d.ts +1 -0
  68. package/dist/servertool/server-side-tools.js +1 -0
  69. package/dist/servertool/types.d.ts +2 -0
  70. package/dist/tools/apply-patch/execution-capturer.js +24 -3
  71. package/package.json +3 -2
@@ -0,0 +1,61 @@
1
+ export type QuotaReason = 'ok' | 'cooldown' | 'blacklist' | 'quotaDepleted' | 'fatal' | 'authVerify';
2
+ export type QuotaAuthType = 'apikey' | 'oauth' | 'unknown';
3
+ export type QuotaAuthIssue = {
4
+ kind: 'google_account_verification';
5
+ url?: string | null;
6
+ message?: string | null;
7
+ } | null;
8
+ export interface StaticQuotaConfig {
9
+ priorityTier?: number | null;
10
+ authType?: QuotaAuthType | null;
11
+ /**
12
+ * Daily reset time for apikey quota exhaustion (HTTP 402).
13
+ * Format:
14
+ * - "HH:mm" => local time
15
+ * - "HH:mmZ" => UTC time
16
+ * If not set, defaults to 12:00 local.
17
+ */
18
+ apikeyDailyResetTime?: string | null;
19
+ }
20
+ export type ErrorSeries = 'E429' | 'E5XX' | 'ENET' | 'EFATAL' | 'EOTHER';
21
+ export interface QuotaState {
22
+ providerKey: string;
23
+ inPool: boolean;
24
+ reason: QuotaReason;
25
+ authType: QuotaAuthType;
26
+ authIssue?: QuotaAuthIssue;
27
+ priorityTier: number;
28
+ cooldownUntil: number | null;
29
+ blacklistUntil: number | null;
30
+ lastErrorSeries: ErrorSeries | null;
31
+ lastErrorCode: string | null;
32
+ lastErrorAtMs: number | null;
33
+ consecutiveErrorCount: number;
34
+ }
35
+ export interface ErrorEventForQuota {
36
+ providerKey: string;
37
+ code?: string | null;
38
+ httpStatus?: number | null;
39
+ fatal?: boolean | null;
40
+ timestampMs?: number;
41
+ /**
42
+ * Optional upstream resetAt ISO string for HTTP 402 quota depletion.
43
+ */
44
+ resetAt?: string | null;
45
+ /**
46
+ * Optional auth issue hint extracted by host/core adapters.
47
+ */
48
+ authIssue?: QuotaAuthIssue;
49
+ }
50
+ export interface SuccessEventForQuota {
51
+ providerKey: string;
52
+ timestampMs?: number;
53
+ }
54
+ export type QuotaStoreSnapshot = {
55
+ savedAtMs: number;
56
+ providers: Record<string, QuotaState>;
57
+ };
58
+ export interface QuotaStore {
59
+ load(): Promise<QuotaStoreSnapshot | null>;
60
+ save(snapshot: QuotaStoreSnapshot): Promise<void>;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -13,6 +13,19 @@ const DEFAULT_CONTEXT_ROUTING = {
13
13
  warnRatio: 0.9,
14
14
  hardLimit: false
15
15
  };
16
+ const CLAUDE_CODE_DEFAULT_USER_AGENT = 'claude-cli/2.0.76 (external, cli)';
17
+ const CLAUDE_CODE_DEFAULT_X_APP = 'claude-cli';
18
+ // Claude Code upstream gates may require anthropic-beta to be present (value is service-specific).
19
+ const CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA = 'claude-code';
20
+ function parseClaudeCodeAppVersionFromUserAgent(userAgent) {
21
+ const ua = typeof userAgent === 'string' ? userAgent.trim() : '';
22
+ if (!ua) {
23
+ return null;
24
+ }
25
+ const match = /claude-cli\/([0-9][^ )]*)/i.exec(ua);
26
+ const version = match?.[1]?.trim() ?? '';
27
+ return version ? version : null;
28
+ }
16
29
  /**
17
30
  * 将用户提供的 Virtual Router 配置(或包含 virtualrouter 字段的整体配置)
18
31
  * 规范化为 VirtualRouterConfig,供 HubPipeline / VirtualRouterEngine 直接使用。
@@ -32,7 +45,7 @@ export function bootstrapVirtualRouterConfig(input) {
32
45
  const execCommandGuard = normalizeExecCommandGuard(section.execCommandGuard);
33
46
  const clock = normalizeClock(section.clock);
34
47
  const { runtimeEntries, aliasIndex, modelIndex } = buildProviderRuntimeEntries(providersSource);
35
- const { routing, targetKeys } = expandRoutingTable(routingSource, aliasIndex);
48
+ const { routing, targetKeys } = expandRoutingTable(routingSource, aliasIndex, modelIndex);
36
49
  if (!routing.default || routing.default.length === 0) {
37
50
  throw new VirtualRouterError('Virtual Router default route must contain at least one provider target', VirtualRouterErrorCode.CONFIG_ERROR);
38
51
  }
@@ -42,7 +55,7 @@ export function bootstrapVirtualRouterConfig(input) {
42
55
  // so that "direct model" and "prefer model" routing can bypass route pools when needed.
43
56
  const expandedTargetKeys = new Set(targetKeys);
44
57
  for (const [providerId, aliases] of aliasIndex.entries()) {
45
- const models = modelIndex.get(providerId) ?? [];
58
+ const models = modelIndex.get(providerId)?.models ?? [];
46
59
  if (!models.length || !aliases.length) {
47
60
  continue;
48
61
  }
@@ -116,6 +129,14 @@ function normalizeClock(raw) {
116
129
  if (typeof record.tickMs === 'number' && Number.isFinite(record.tickMs) && record.tickMs >= 0) {
117
130
  out.tickMs = Math.floor(record.tickMs);
118
131
  }
132
+ if (record.holdNonStreaming === true ||
133
+ (typeof record.holdNonStreaming === 'string' && record.holdNonStreaming.trim().toLowerCase() === 'true') ||
134
+ (typeof record.holdNonStreaming === 'number' && record.holdNonStreaming === 1)) {
135
+ out.holdNonStreaming = true;
136
+ }
137
+ if (typeof record.holdMaxMs === 'number' && Number.isFinite(record.holdMaxMs) && record.holdMaxMs >= 0) {
138
+ out.holdMaxMs = Math.floor(record.holdMaxMs);
139
+ }
119
140
  return out;
120
141
  }
121
142
  function buildProviderRuntimeEntries(providers) {
@@ -124,8 +145,10 @@ function buildProviderRuntimeEntries(providers) {
124
145
  const modelIndex = new Map();
125
146
  for (const [providerId, providerRaw] of Object.entries(providers)) {
126
147
  const normalizedProvider = normalizeProvider(providerId, providerRaw);
127
- const modelsNode = asRecord(providerRaw?.models);
128
- modelIndex.set(providerId, modelsNode ? Object.keys(modelsNode).filter(Boolean) : []);
148
+ const rawModelsNode = providerRaw?.models;
149
+ const modelsDeclared = rawModelsNode !== undefined;
150
+ const modelsNode = asRecord(rawModelsNode);
151
+ modelIndex.set(providerId, { declared: modelsDeclared, models: Object.keys(modelsNode).filter(Boolean) });
129
152
  const authEntries = extractProviderAuthEntries(providerId, providerRaw);
130
153
  if (!authEntries.length) {
131
154
  throw new VirtualRouterError(`Provider ${providerId} requires at least one auth entry`, VirtualRouterErrorCode.CONFIG_ERROR);
@@ -179,7 +202,7 @@ function buildProviderRuntimeEntries(providers) {
179
202
  }
180
203
  return { runtimeEntries, aliasIndex, modelIndex };
181
204
  }
182
- function expandRoutingTable(routingSource, aliasIndex) {
205
+ function expandRoutingTable(routingSource, aliasIndex, modelIndex) {
183
206
  const routing = {};
184
207
  const targetKeys = new Set();
185
208
  for (const [routeName, pools] of Object.entries(routingSource)) {
@@ -195,6 +218,19 @@ function expandRoutingTable(routingSource, aliasIndex) {
195
218
  if (!aliasIndex.has(parsed.providerId)) {
196
219
  throw new VirtualRouterError(`Route "${routeName}" references unknown provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
197
220
  }
221
+ const modelInfo = modelIndex.get(parsed.providerId);
222
+ if (modelInfo?.declared) {
223
+ if (!parsed.modelId) {
224
+ throw new VirtualRouterError(`Route "${routeName}" references empty model id for provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
225
+ }
226
+ const knownModels = modelInfo.models ?? [];
227
+ if (!knownModels.length) {
228
+ throw new VirtualRouterError(`Route "${routeName}" references provider "${parsed.providerId}" but provider declares no models`, VirtualRouterErrorCode.CONFIG_ERROR);
229
+ }
230
+ if (!knownModels.includes(parsed.modelId)) {
231
+ throw new VirtualRouterError(`Route "${routeName}" references unknown model "${parsed.modelId}" for provider "${parsed.providerId}"`, VirtualRouterErrorCode.CONFIG_ERROR);
232
+ }
233
+ }
198
234
  const aliases = parsed.keyAlias ? [parsed.keyAlias] : aliasIndex.get(parsed.providerId);
199
235
  if (!aliases.length) {
200
236
  throw new VirtualRouterError(`Provider ${parsed.providerId} has no auth aliases but is referenced in routing`, VirtualRouterErrorCode.CONFIG_ERROR);
@@ -476,8 +512,8 @@ function normalizeProvider(providerId, raw) {
476
512
  : typeof provider.baseUrl === 'string' && provider.baseUrl.trim()
477
513
  ? provider.baseUrl.trim()
478
514
  : '';
479
- const headers = normalizeHeaders(provider.headers);
480
515
  const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
516
+ const headers = maybeInjectClaudeCodeHeaders(providerId, providerType, compatibilityProfile, normalizeHeaders(provider.headers));
481
517
  const responsesNode = asRecord(provider.responses);
482
518
  const responsesConfig = normalizeResponsesConfig(provider, responsesNode);
483
519
  const processMode = normalizeProcessMode(provider.process);
@@ -506,6 +542,50 @@ function normalizeProvider(providerId, raw) {
506
542
  ...(serverToolsDisabled ? { serverToolsDisabled: true } : {})
507
543
  };
508
544
  }
545
+ function hasHeader(headers, name) {
546
+ if (!headers) {
547
+ return false;
548
+ }
549
+ const lowered = name.trim().toLowerCase();
550
+ if (!lowered) {
551
+ return false;
552
+ }
553
+ for (const key of Object.keys(headers)) {
554
+ if (key.trim().toLowerCase() === lowered) {
555
+ const value = headers[key];
556
+ if (typeof value === 'string' && value.trim()) {
557
+ return true;
558
+ }
559
+ }
560
+ }
561
+ return false;
562
+ }
563
+ function maybeInjectClaudeCodeHeaders(_providerId, providerType, compatibilityProfile, headers) {
564
+ const profile = typeof compatibilityProfile === 'string' ? compatibilityProfile.trim().toLowerCase() : '';
565
+ if (!profile || (profile !== 'anthropic:claude-code' && profile !== 'chat:claude-code')) {
566
+ return headers;
567
+ }
568
+ if (!String(providerType).toLowerCase().includes('anthropic')) {
569
+ return headers;
570
+ }
571
+ const base = { ...(headers ?? {}) };
572
+ if (!hasHeader(base, 'User-Agent')) {
573
+ base['User-Agent'] = CLAUDE_CODE_DEFAULT_USER_AGENT;
574
+ }
575
+ if (!hasHeader(base, 'X-App')) {
576
+ base['X-App'] = CLAUDE_CODE_DEFAULT_X_APP;
577
+ }
578
+ if (!hasHeader(base, 'X-App-Version')) {
579
+ const version = parseClaudeCodeAppVersionFromUserAgent(base['User-Agent'] ?? '');
580
+ if (version) {
581
+ base['X-App-Version'] = version;
582
+ }
583
+ }
584
+ if (!hasHeader(base, 'anthropic-beta')) {
585
+ base['anthropic-beta'] = CLAUDE_CODE_DEFAULT_ANTHROPIC_BETA;
586
+ }
587
+ return base;
588
+ }
509
589
  function normalizeModelStreaming(provider) {
510
590
  const modelsNode = asRecord(provider.models);
511
591
  if (!modelsNode) {
@@ -1221,6 +1301,21 @@ function normalizeAliasSelection(raw) {
1221
1301
  const record = raw;
1222
1302
  const enabled = typeof record.enabled === 'boolean' ? record.enabled : undefined;
1223
1303
  const defaultStrategy = coerceAliasSelectionStrategy(record.defaultStrategy);
1304
+ const sessionLeaseCooldownMs = typeof record.sessionLeaseCooldownMs === 'number' && Number.isFinite(record.sessionLeaseCooldownMs)
1305
+ ? Math.max(0, Math.floor(record.sessionLeaseCooldownMs))
1306
+ : typeof record.sessionLease_cooldown_ms === 'number' && Number.isFinite(record.sessionLease_cooldown_ms)
1307
+ ? Math.max(0, Math.floor(record.sessionLease_cooldown_ms))
1308
+ : undefined;
1309
+ const antigravitySessionBindingRaw = typeof record.antigravitySessionBinding === 'string'
1310
+ ? record.antigravitySessionBinding.trim().toLowerCase()
1311
+ : typeof record.antigravity_session_binding === 'string'
1312
+ ? String(record.antigravity_session_binding).trim().toLowerCase()
1313
+ : '';
1314
+ const antigravitySessionBinding = antigravitySessionBindingRaw === 'strict'
1315
+ ? 'strict'
1316
+ : antigravitySessionBindingRaw === 'lease'
1317
+ ? 'lease'
1318
+ : undefined;
1224
1319
  const providersRaw = asRecord(record.providers);
1225
1320
  const providers = {};
1226
1321
  for (const [providerId, value] of Object.entries(providersRaw)) {
@@ -1232,6 +1327,8 @@ function normalizeAliasSelection(raw) {
1232
1327
  const out = {
1233
1328
  ...(enabled !== undefined ? { enabled } : {}),
1234
1329
  ...(defaultStrategy ? { defaultStrategy } : {}),
1330
+ ...(sessionLeaseCooldownMs !== undefined ? { sessionLeaseCooldownMs } : {}),
1331
+ ...(antigravitySessionBinding ? { antigravitySessionBinding } : {}),
1235
1332
  ...(Object.keys(providers).length ? { providers } : {})
1236
1333
  };
1237
1334
  return Object.keys(out).length ? out : undefined;
@@ -105,6 +105,7 @@ const ANTIGRAVITY_RISK_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY
105
105
  const ANTIGRAVITY_RISK_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_COOLDOWN', 5 * 60_000);
106
106
  const ANTIGRAVITY_RISK_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_BAN', 24 * 60 * 60_000);
107
107
  const ANTIGRAVITY_AUTH_VERIFY_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_AUTH_VERIFY_BAN', 24 * 60 * 60_000);
108
+ const ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN', 5 * 60_000);
108
109
  function isAntigravityEvent(event) {
109
110
  const runtime = event?.runtime;
110
111
  if (!runtime || typeof runtime !== 'object') {
@@ -145,6 +146,11 @@ function isGoogleAccountVerificationRequired(event) {
145
146
  }
146
147
  const lowered = sources.join(' | ').toLowerCase();
147
148
  return (lowered.includes('verify your account') ||
149
+ // Antigravity-Manager alignment: 403 validation gating keywords.
150
+ lowered.includes('validation_required') ||
151
+ lowered.includes('validation required') ||
152
+ lowered.includes('validation_url') ||
153
+ lowered.includes('validation url') ||
148
154
  lowered.includes('accounts.google.com/signin/continue') ||
149
155
  lowered.includes('support.google.com/accounts?p=al_alert'));
150
156
  }
@@ -187,6 +193,68 @@ function computeAntigravityRiskSignature(event) {
187
193
  const parts = [status, code, stage].filter((p) => p.length > 0);
188
194
  return parts.length ? parts.join(':') : 'unknown';
189
195
  }
196
+ function isAntigravityThoughtSignatureMissing(event) {
197
+ const code = typeof event.code === 'string' ? event.code.trim().toLowerCase() : '';
198
+ if (code.includes('thought') && code.includes('signature')) {
199
+ return true;
200
+ }
201
+ const sources = [];
202
+ const message = typeof event.message === 'string' ? event.message : '';
203
+ if (message)
204
+ sources.push(message);
205
+ const details = event.details;
206
+ if (details && typeof details === 'object' && !Array.isArray(details)) {
207
+ const upstreamMessage = details.upstreamMessage;
208
+ if (typeof upstreamMessage === 'string' && upstreamMessage.trim()) {
209
+ sources.push(upstreamMessage);
210
+ }
211
+ const meta = details.meta;
212
+ if (meta && typeof meta === 'object' && !Array.isArray(meta)) {
213
+ const metaUpstream = meta.upstreamMessage;
214
+ if (typeof metaUpstream === 'string' && metaUpstream.trim()) {
215
+ sources.push(metaUpstream);
216
+ }
217
+ const metaMessage = meta.message;
218
+ if (typeof metaMessage === 'string' && metaMessage.trim()) {
219
+ sources.push(metaMessage);
220
+ }
221
+ }
222
+ }
223
+ if (sources.length === 0) {
224
+ return false;
225
+ }
226
+ for (const source of sources) {
227
+ const lowered = source.toLowerCase();
228
+ // Match both "thoughtSignature" and "thought signature" variants, as well as "reasoning signature".
229
+ const mentionsSignature = lowered.includes('thoughtsignature') ||
230
+ lowered.includes('thought signature') ||
231
+ lowered.includes('reasoning_signature') ||
232
+ lowered.includes('reasoning signature');
233
+ if (!mentionsSignature) {
234
+ continue;
235
+ }
236
+ // Match common failure patterns in upstream error messages.
237
+ if (lowered.includes('missing') ||
238
+ lowered.includes('required') ||
239
+ lowered.includes('invalid') ||
240
+ lowered.includes('not provided') ||
241
+ lowered.includes('签名') ||
242
+ lowered.includes('缺少') ||
243
+ lowered.includes('无效')) {
244
+ return true;
245
+ }
246
+ }
247
+ return false;
248
+ }
249
+ function shouldApplyGeminiSeriesSignatureFreeze(event) {
250
+ const status = typeof event.status === 'number' ? event.status : undefined;
251
+ // Signature missing is a request-shape contract failure; treat as 4xx.
252
+ if (status !== undefined && status >= 400 && status < 500) {
253
+ return true;
254
+ }
255
+ const code = typeof event.code === 'string' ? event.code.trim().toUpperCase() : '';
256
+ return /^HTTP_4\d\d$/.test(code) || code.includes('INVALID_ARGUMENT') || code.includes('FAILED_PRECONDITION');
257
+ }
190
258
  export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
191
259
  if (!event) {
192
260
  return;
@@ -228,6 +296,42 @@ export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthMa
228
296
  });
229
297
  return;
230
298
  }
299
+ // Thought signature missing is a deterministic incompatibility for Gemini-family flows that require it.
300
+ // If we keep routing into Antigravity Gemini models without a signature, we create a request storm.
301
+ // Freeze the *Gemini* series (pro/flash) immediately so the router falls back to non-Antigravity providers.
302
+ if (isAntigravityThoughtSignatureMissing(event) && shouldApplyGeminiSeriesSignatureFreeze(event)) {
303
+ const allProviderKeys = providerRegistry.listProviderKeys('antigravity');
304
+ const providerKeys = runtimeKey
305
+ ? allProviderKeys.filter((key) => typeof key === 'string' && key.startsWith(`${runtimeKey}.`))
306
+ : allProviderKeys;
307
+ const affected = [];
308
+ for (const key of providerKeys) {
309
+ try {
310
+ if (!healthManager.isAvailable(key)) {
311
+ continue;
312
+ }
313
+ const profile = providerRegistry.get(key);
314
+ const modelSeries = resolveModelSeries(profile.modelId);
315
+ if (modelSeries !== 'gemini-pro' && modelSeries !== 'gemini-flash') {
316
+ continue;
317
+ }
318
+ healthManager.tripProvider(key, 'signature_missing', ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS);
319
+ markProviderCooldown(key, ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS);
320
+ affected.push(key);
321
+ }
322
+ catch {
323
+ // ignore lookup failures
324
+ }
325
+ }
326
+ if (affected.length) {
327
+ debug?.log?.('[virtual-router] antigravity thoughtSignature missing: freeze gemini series', {
328
+ ...(runtimeKey ? { runtimeKey } : {}),
329
+ cooldownMs: ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS,
330
+ affected
331
+ });
332
+ }
333
+ return;
334
+ }
231
335
  const signature = computeAntigravityRiskSignature(event);
232
336
  if (!signature || signature === 'unknown') {
233
337
  return;
@@ -13,6 +13,24 @@ export type SelectionDeps = {
13
13
  resolveStickyKey: (metadata: RouterMetadataInput) => string | undefined;
14
14
  quotaView?: ProviderQuotaView;
15
15
  aliasQueueStore?: Map<string, string[]>;
16
+ /**
17
+ * Antigravity alias session lease (session isolation) store.
18
+ * Key: runtimeKey (providerId.keyAlias), e.g. "antigravity.aliasA".
19
+ */
20
+ antigravityAliasLeaseStore?: Map<string, {
21
+ sessionKey: string;
22
+ lastSeenAt: number;
23
+ }>;
24
+ /**
25
+ * Session → runtimeKey mapping for Antigravity alias leases.
26
+ * Key: session scope key, e.g. "session:abc" / "conversation:xyz".
27
+ * Value: runtimeKey (providerId.keyAlias)
28
+ */
29
+ antigravitySessionAliasStore?: Map<string, string>;
30
+ /**
31
+ * Cooldown window (ms) before an Antigravity alias can be reused by a different session.
32
+ */
33
+ antigravityAliasReuseCooldownMs?: number;
16
34
  };
17
35
  export type TrySelectFromTierOptions = {
18
36
  disabledProviders?: Set<string>;
@@ -1,10 +1,9 @@
1
- import type { ProviderHealthManager } from '../health-manager.js';
2
1
  import type { ProviderRegistry } from '../provider-registry.js';
3
2
  export declare function pickPriorityGroup(opts: {
4
3
  candidates: string[];
5
4
  orderedTargets: string[];
6
5
  providerRegistry: ProviderRegistry;
7
- healthManager: ProviderHealthManager;
6
+ availabilityCheck: (key: string) => boolean;
8
7
  penalties?: Record<string, number>;
9
8
  }): {
10
9
  groupId: string;
@@ -31,12 +31,12 @@ function resolvePriorityMeta(orderedTargets, providerRegistry) {
31
31
  return meta;
32
32
  }
33
33
  export function pickPriorityGroup(opts) {
34
- const { candidates, orderedTargets, providerRegistry, healthManager, penalties } = opts;
34
+ const { candidates, orderedTargets, providerRegistry, availabilityCheck, penalties } = opts;
35
35
  const meta = resolvePriorityMeta(orderedTargets, providerRegistry);
36
36
  let bestGroupId = null;
37
37
  let bestScore = Number.NEGATIVE_INFINITY;
38
38
  for (const key of candidates) {
39
- if (!healthManager.isAvailable(key))
39
+ if (!availabilityCheck(key))
40
40
  continue;
41
41
  const m = meta.get(key);
42
42
  if (!m)
@@ -67,7 +67,7 @@ function applyAliasStickyQueuePinning(opts) {
67
67
  excludedProviderKeys: excludedKeys,
68
68
  aliasOfKey: extractKeyAlias,
69
69
  modelIdOfKey: (key) => getProviderModelId(key, deps.providerRegistry),
70
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
70
+ availabilityCheck: (key) => deps.healthManager.isAvailable(key) || Boolean(deps.quotaView?.(key))
71
71
  });
72
72
  if (pinned && pinned.length) {
73
73
  pinnedByGroup.set(groupId, new Set(pinned));
@@ -137,9 +137,33 @@ function preferAntigravityAliasesOnRetry(opts) {
137
137
  export function selectProviderKeyFromCandidatePool(opts) {
138
138
  const { routeName, tier, stickyKey, candidates, isSafePool, deps, options, contextResult, warnRatio, excludedKeys, isRecoveryAttempt, now, nowForWeights, healthWeightedCfg, contextWeightedCfg } = opts;
139
139
  const quotaView = deps.quotaView;
140
+ const isAvailable = (key) => {
141
+ if (!quotaView) {
142
+ return deps.healthManager.isAvailable(key);
143
+ }
144
+ const entry = quotaView(key);
145
+ if (!entry) {
146
+ // When quotaView is present, quota is the source of truth for availability.
147
+ // Treat unknown entries as "in pool" so routing does not depend on router-local health.
148
+ return true;
149
+ }
150
+ if (entry.inPool === false) {
151
+ return false;
152
+ }
153
+ if (entry.cooldownUntil && entry.cooldownUntil > now) {
154
+ return false;
155
+ }
156
+ if (entry.blacklistUntil && entry.blacklistUntil > now) {
157
+ return false;
158
+ }
159
+ // When quotaView is injected, quota is the source of truth for availability.
160
+ // Do not let router-local health snapshots (which may be stale or intentionally disabled)
161
+ // prevent selection for in-pool targets.
162
+ return true;
163
+ };
140
164
  const selectFirstAvailable = (keys) => {
141
165
  for (const key of keys) {
142
- if (deps.healthManager.isAvailable(key)) {
166
+ if (isAvailable(key)) {
143
167
  return key;
144
168
  }
145
169
  }
@@ -171,7 +195,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
171
195
  candidates: pinnedCandidates,
172
196
  orderedTargets: tier.targets,
173
197
  providerRegistry: deps.providerRegistry,
174
- healthManager: deps.healthManager
198
+ availabilityCheck: isAvailable
175
199
  });
176
200
  if (!group) {
177
201
  return null;
@@ -198,7 +222,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
198
222
  candidates: group.groupCandidates,
199
223
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
200
224
  weights,
201
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
225
+ availabilityCheck: isAvailable
202
226
  }, 'round-robin');
203
227
  }
204
228
  const weights = (() => {
@@ -223,7 +247,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
223
247
  candidates: pinnedCandidates,
224
248
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
225
249
  weights,
226
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
250
+ availabilityCheck: isAvailable
227
251
  }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
228
252
  }
229
253
  const buckets = new Map();
@@ -313,7 +337,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
313
337
  candidates: bucketCandidates,
314
338
  orderedTargets: tier.targets,
315
339
  providerRegistry: deps.providerRegistry,
316
- healthManager: deps.healthManager,
340
+ availabilityCheck: isAvailable,
317
341
  penalties: bucketPenaltyMap
318
342
  });
319
343
  if (!group) {
@@ -328,7 +352,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
328
352
  candidates: group.groupCandidates,
329
353
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
330
354
  weights: groupWeights,
331
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
355
+ availabilityCheck: isAvailable
332
356
  }, 'round-robin');
333
357
  if (selected) {
334
358
  return selected;
@@ -339,7 +363,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
339
363
  let best = null;
340
364
  let bestM = Number.NEGATIVE_INFINITY;
341
365
  for (const key of bucketCandidates) {
342
- if (!deps.healthManager.isAvailable(key))
366
+ if (!isAvailable(key))
343
367
  continue;
344
368
  const m = bucketMultipliers[key] ?? 1;
345
369
  if (m > bestM) {
@@ -365,7 +389,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
365
389
  let best = null;
366
390
  let bestM = Number.NEGATIVE_INFINITY;
367
391
  for (const key of bucketCandidates) {
368
- if (!deps.healthManager.isAvailable(key))
392
+ if (!isAvailable(key))
369
393
  continue;
370
394
  const m = bucketMultipliers[key] ?? 1;
371
395
  if (m > bestM) {
@@ -389,7 +413,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
389
413
  candidates: bucketCandidates,
390
414
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
391
415
  weights: bucketWeights,
392
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
416
+ availabilityCheck: isAvailable
393
417
  }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
394
418
  if (selected) {
395
419
  return selected;