@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
- package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
- package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
- package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
- package/dist/conversion/compat/antigravity-session-signature.js +833 -21
- package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
- package/dist/conversion/hub/process/chat-process.js +300 -67
- package/dist/conversion/hub/response/provider-response.js +4 -3
- package/dist/conversion/shared/gemini-tool-utils.js +134 -9
- package/dist/conversion/shared/text-markup-normalizer.js +90 -1
- package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
- package/dist/conversion/shared/thought-signature-validator.js +2 -1
- package/dist/quota/apikey-reset.d.ts +17 -0
- package/dist/quota/apikey-reset.js +43 -0
- package/dist/quota/index.d.ts +2 -0
- package/dist/quota/index.js +1 -0
- package/dist/quota/quota-manager.d.ts +44 -0
- package/dist/quota/quota-manager.js +491 -0
- package/dist/quota/quota-state.d.ts +6 -0
- package/dist/quota/quota-state.js +167 -0
- package/dist/quota/types.d.ts +61 -0
- package/dist/quota/types.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +103 -6
- package/dist/router/virtual-router/engine-health.js +104 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
- package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
- package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
- package/dist/router/virtual-router/engine-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +16 -1
- package/dist/router/virtual-router/engine.js +320 -42
- package/dist/router/virtual-router/features.js +20 -2
- package/dist/router/virtual-router/success-center.d.ts +10 -0
- package/dist/router/virtual-router/success-center.js +32 -0
- package/dist/router/virtual-router/types.d.ts +48 -0
- package/dist/servertool/clock/config.d.ts +2 -0
- package/dist/servertool/clock/config.js +10 -2
- package/dist/servertool/clock/daemon.js +3 -0
- package/dist/servertool/clock/ntp.d.ts +18 -0
- package/dist/servertool/clock/ntp.js +318 -0
- package/dist/servertool/clock/paths.d.ts +1 -0
- package/dist/servertool/clock/paths.js +3 -0
- package/dist/servertool/clock/state.d.ts +2 -0
- package/dist/servertool/clock/state.js +15 -2
- package/dist/servertool/clock/tasks.d.ts +1 -0
- package/dist/servertool/clock/tasks.js +24 -1
- package/dist/servertool/clock/types.d.ts +21 -0
- package/dist/servertool/engine.js +105 -1
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
- package/dist/servertool/handlers/clock-auto.js +39 -4
- package/dist/servertool/handlers/clock.js +145 -16
- package/dist/servertool/handlers/followup-request-builder.js +84 -0
- package/dist/servertool/handlers/stop-message-auto.js +1 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +1 -0
- package/dist/servertool/types.d.ts +2 -0
- package/dist/tools/apply-patch/execution-capturer.js +24 -3
- 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
|
|
128
|
-
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
416
|
+
availabilityCheck: isAvailable
|
|
393
417
|
}, tier.mode === 'round-robin' ? 'round-robin' : undefined);
|
|
394
418
|
if (selected) {
|
|
395
419
|
return selected;
|