@jsonstudio/llms 0.6.1403 → 0.6.1435
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 +10 -4
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.d.ts +3 -0
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +60 -0
- package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -1
- package/dist/conversion/compat/actions/gemini-cli-request.js +71 -2
- package/dist/conversion/compat/antigravity-session-signature.d.ts +13 -0
- package/dist/conversion/compat/antigravity-session-signature.js +245 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +3 -1
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +82 -17
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +7 -1
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/response/provider-response.js +10 -4
- package/dist/router/virtual-router/engine-health.d.ts +1 -0
- package/dist/router/virtual-router/engine-health.js +100 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +40 -3
- package/dist/router/virtual-router/engine.js +4 -1
- package/dist/servertool/engine.js +2 -57
- package/dist/servertool/handlers/stop-message-auto.js +18 -1
- package/package.json +1 -1
|
@@ -5,7 +5,6 @@ import { mapBridgeToolsToChat } from '../shared/tool-mapping.js';
|
|
|
5
5
|
import { prepareGeminiToolsForBridge } from '../shared/gemini-tool-utils.js';
|
|
6
6
|
import { registerResponsesReasoning, consumeResponsesReasoning, registerResponsesOutputTextMeta, consumeResponsesOutputTextMeta, consumeResponsesPayloadSnapshot, registerResponsesPayloadSnapshot, consumeResponsesPassthrough, registerResponsesPassthrough } from '../shared/responses-reasoning-registry.js';
|
|
7
7
|
import { ProviderProtocolError } from '../shared/errors.js';
|
|
8
|
-
const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
|
9
8
|
function isObject(v) {
|
|
10
9
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
11
10
|
}
|
|
@@ -229,9 +228,13 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
229
228
|
// 5. Function call (tool call)
|
|
230
229
|
if (pObj.functionCall && typeof pObj.functionCall === 'object') {
|
|
231
230
|
const fc = pObj.functionCall;
|
|
232
|
-
|
|
231
|
+
let name = typeof fc.name === 'string' ? String(fc.name) : undefined;
|
|
233
232
|
if (!name)
|
|
234
233
|
continue;
|
|
234
|
+
// Gemini "websearch" is a transport alias for the canonical server-side tool "web_search".
|
|
235
|
+
if (name === 'websearch' || name.startsWith('websearch_')) {
|
|
236
|
+
name = 'web_search';
|
|
237
|
+
}
|
|
235
238
|
let id = typeof fc.id === 'string' && fc.id.trim().length ? String(fc.id).trim() : undefined;
|
|
236
239
|
const argsRaw = (fc.args ?? fc.arguments);
|
|
237
240
|
let argsStr;
|
|
@@ -268,7 +271,10 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
|
|
|
268
271
|
if (pObj.functionResponse && typeof pObj.functionResponse === 'object') {
|
|
269
272
|
const fr = pObj.functionResponse;
|
|
270
273
|
const callId = typeof fr.id === 'string' && fr.id.trim().length ? String(fr.id) : undefined;
|
|
271
|
-
|
|
274
|
+
let name = typeof fr.name === 'string' && fr.name.trim().length ? String(fr.name) : undefined;
|
|
275
|
+
if (name && (name === 'websearch' || name.startsWith('websearch_'))) {
|
|
276
|
+
name = 'web_search';
|
|
277
|
+
}
|
|
272
278
|
const resp = fr.response;
|
|
273
279
|
let contentStr = '';
|
|
274
280
|
if (typeof resp === 'string') {
|
|
@@ -568,7 +574,7 @@ export function buildGeminiFromOpenAIChat(chatResp) {
|
|
|
568
574
|
const id = typeof tc.id === 'string' ? String(tc.id) : undefined;
|
|
569
575
|
if (id)
|
|
570
576
|
functionCall.id = id;
|
|
571
|
-
const thoughtSignature = extractThoughtSignatureFromToolCall(tc)
|
|
577
|
+
const thoughtSignature = extractThoughtSignatureFromToolCall(tc);
|
|
572
578
|
const partEntry = { functionCall };
|
|
573
579
|
if (thoughtSignature) {
|
|
574
580
|
partEntry.thoughtSignature = thoughtSignature;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AdapterContext } from '../../hub/types/chat-envelope.js';
|
|
2
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
3
|
+
export declare function cacheAntigravityThoughtSignatureFromGeminiResponse(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cacheAntigravitySessionSignature, getAntigravityRequestSessionId } from '../antigravity-session-signature.js';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function shouldEnableForAdapter(adapterContext) {
|
|
6
|
+
if (!adapterContext) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const protocol = typeof adapterContext.providerProtocol === 'string' ? adapterContext.providerProtocol.trim().toLowerCase() : '';
|
|
10
|
+
if (protocol !== 'gemini-chat') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const providerIdOrKey = typeof adapterContext.providerId === 'string' ? adapterContext.providerId.trim().toLowerCase() : '';
|
|
14
|
+
const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
|
|
15
|
+
return effectiveProviderId === 'antigravity';
|
|
16
|
+
}
|
|
17
|
+
export function cacheAntigravityThoughtSignatureFromGeminiResponse(payload, adapterContext) {
|
|
18
|
+
if (!shouldEnableForAdapter(adapterContext)) {
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
const ctxAny = adapterContext;
|
|
22
|
+
const payloadAny = payload;
|
|
23
|
+
const keyCandidates = [
|
|
24
|
+
adapterContext?.requestId,
|
|
25
|
+
typeof ctxAny.clientRequestId === 'string' ? String(ctxAny.clientRequestId) : '',
|
|
26
|
+
typeof ctxAny.groupRequestId === 'string' ? String(ctxAny.groupRequestId) : '',
|
|
27
|
+
typeof payloadAny.request_id === 'string' ? String(payloadAny.request_id) : '',
|
|
28
|
+
typeof payloadAny.requestId === 'string' ? String(payloadAny.requestId) : ''
|
|
29
|
+
].filter((k) => typeof k === 'string' && k.trim().length);
|
|
30
|
+
let sessionId = '';
|
|
31
|
+
for (const key of keyCandidates) {
|
|
32
|
+
const resolved = getAntigravityRequestSessionId(key);
|
|
33
|
+
if (resolved && resolved.trim().length) {
|
|
34
|
+
sessionId = resolved.trim();
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!sessionId) {
|
|
39
|
+
return payload;
|
|
40
|
+
}
|
|
41
|
+
const messageCount = 1;
|
|
42
|
+
const candidatesRaw = payload.candidates;
|
|
43
|
+
const candidates = Array.isArray(candidatesRaw) ? candidatesRaw : [];
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
const content = isRecord(candidate.content) ? candidate.content : undefined;
|
|
46
|
+
const partsRaw = content?.parts;
|
|
47
|
+
const parts = Array.isArray(partsRaw) ? partsRaw : [];
|
|
48
|
+
for (const part of parts) {
|
|
49
|
+
if (!isRecord(part.functionCall)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const sig = typeof part.thoughtSignature === 'string' ? String(part.thoughtSignature) : '';
|
|
53
|
+
if (!sig.trim().length) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
cacheAntigravitySessionSignature(sessionId, sig.trim(), messageCount);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
-
|
|
2
|
+
import type { AdapterContext } from '../../hub/types/chat-envelope.js';
|
|
3
|
+
export declare function wrapGeminiCliRequest(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cacheAntigravityRequestSessionId, extractAntigravityGeminiSessionId, getAntigravitySessionSignature, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
|
|
1
2
|
const REQUEST_FIELDS = [
|
|
2
3
|
'contents',
|
|
3
4
|
'systemInstruction',
|
|
@@ -368,7 +369,6 @@ function normalizeFunctionCallArgs(node) {
|
|
|
368
369
|
const fnCall = part.functionCall;
|
|
369
370
|
if (!isRecord(fnCall))
|
|
370
371
|
continue;
|
|
371
|
-
part.thoughtSignature = 'skip_thought_signature_validator';
|
|
372
372
|
const rawName = typeof fnCall.name === 'string' ? fnCall.name.trim() : '';
|
|
373
373
|
const normalizedName = (TOOL_NAME_ALIASES[rawName] ?? rawName).trim();
|
|
374
374
|
fnCall.name = normalizedName;
|
|
@@ -453,7 +453,50 @@ function stripWebSearchTools(requestNode) {
|
|
|
453
453
|
delete requestNode.tools;
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
|
-
|
|
456
|
+
function shouldEnableAntigravitySignature(adapterContext) {
|
|
457
|
+
// Prefer adapterContext (canonical) but fall back to transport hints for cases where
|
|
458
|
+
// host doesn't populate providerId (e.g. certain v1/v2 bridges).
|
|
459
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
const protocol = typeof adapterContext.providerProtocol === 'string' ? adapterContext.providerProtocol.trim().toLowerCase() : '';
|
|
463
|
+
if (protocol !== 'gemini-chat') {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
// NOTE: In Hub adapterContext, `providerId` is historically populated with providerKey
|
|
467
|
+
// (e.g. "antigravity.<alias>.<model>") rather than the bare provider id.
|
|
468
|
+
// Treat the first segment as the effective provider id for compatibility checks.
|
|
469
|
+
const providerIdOrKey = typeof adapterContext.providerId === 'string' ? adapterContext.providerId.trim().toLowerCase() : '';
|
|
470
|
+
const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
|
|
471
|
+
return effectiveProviderId === 'antigravity';
|
|
472
|
+
}
|
|
473
|
+
function injectAntigravityThoughtSignature(requestNode) {
|
|
474
|
+
const sessionId = extractAntigravityGeminiSessionId(requestNode);
|
|
475
|
+
const signature = getAntigravitySessionSignature(sessionId);
|
|
476
|
+
if (!signature) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const contents = requestNode.contents;
|
|
480
|
+
if (!Array.isArray(contents)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
for (const item of contents) {
|
|
484
|
+
if (!isRecord(item))
|
|
485
|
+
continue;
|
|
486
|
+
const parts = Array.isArray(item.parts) ? item.parts : [];
|
|
487
|
+
for (const part of parts) {
|
|
488
|
+
if (!isRecord(part))
|
|
489
|
+
continue;
|
|
490
|
+
if (!isRecord(part.functionCall))
|
|
491
|
+
continue;
|
|
492
|
+
const existing = part.thoughtSignature;
|
|
493
|
+
if (shouldTreatAsMissingThoughtSignature(existing)) {
|
|
494
|
+
part.thoughtSignature = signature;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
export function wrapGeminiCliRequest(payload, adapterContext) {
|
|
457
500
|
const root = { ...payload };
|
|
458
501
|
const existingRequest = isRecord(root.request) ? normalizeRequestNode(root.request) : undefined;
|
|
459
502
|
const requestNode = existingRequest ?? {};
|
|
@@ -471,6 +514,32 @@ export function wrapGeminiCliRequest(payload) {
|
|
|
471
514
|
stripWebSearchTools(requestNode);
|
|
472
515
|
normalizeToolDeclarations(requestNode);
|
|
473
516
|
normalizeFunctionCallArgs(requestNode);
|
|
517
|
+
const enableSignature = shouldEnableAntigravitySignature(adapterContext) ||
|
|
518
|
+
(typeof requestNode.userAgent === 'string' && requestNode.userAgent.trim().toLowerCase() === 'antigravity') ||
|
|
519
|
+
(typeof requestNode.requestId === 'string' && requestNode.requestId.trim().toLowerCase().startsWith('agent-')) ||
|
|
520
|
+
(typeof root.userAgent === 'string' && root.userAgent.trim().toLowerCase() === 'antigravity') ||
|
|
521
|
+
(typeof root.requestId === 'string' && root.requestId.trim().toLowerCase().startsWith('agent-'));
|
|
522
|
+
if (enableSignature) {
|
|
523
|
+
// Antigravity-Manager alignment:
|
|
524
|
+
if (adapterContext) {
|
|
525
|
+
try {
|
|
526
|
+
const sessionId = extractAntigravityGeminiSessionId(requestNode);
|
|
527
|
+
const ctxAny = adapterContext;
|
|
528
|
+
const keys = [
|
|
529
|
+
adapterContext.requestId,
|
|
530
|
+
typeof ctxAny.clientRequestId === 'string' ? String(ctxAny.clientRequestId) : '',
|
|
531
|
+
typeof ctxAny.groupRequestId === 'string' ? String(ctxAny.groupRequestId) : ''
|
|
532
|
+
].filter((k) => typeof k === 'string' && k.trim().length);
|
|
533
|
+
for (const key of keys) {
|
|
534
|
+
cacheAntigravityRequestSessionId(key, sessionId);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// best-effort only
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
injectAntigravityThoughtSignature(requestNode);
|
|
542
|
+
}
|
|
474
543
|
// Cloud Code Assist request wrapper should not carry metadata/action/web_search/stream.
|
|
475
544
|
delete requestNode.metadata;
|
|
476
545
|
delete requestNode.action;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DUMMY_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
|
|
2
|
+
export declare function cacheAntigravityRequestSessionId(requestId: string, sessionId: string): void;
|
|
3
|
+
export declare function getAntigravityRequestSessionId(requestId: string): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Antigravity-Manager alignment: derive a stable session fingerprint for Gemini native requests.
|
|
6
|
+
* - sha256(first user text parts joined), if len>10 and no "<system-reminder>"
|
|
7
|
+
* - else sha256(JSON body)
|
|
8
|
+
* - sid = "sid-" + first 16 hex chars
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractAntigravityGeminiSessionId(payload: unknown): string;
|
|
11
|
+
export declare function cacheAntigravitySessionSignature(sessionId: string, signature: string, messageCount?: number): void;
|
|
12
|
+
export declare function getAntigravitySessionSignature(sessionId: string): string | undefined;
|
|
13
|
+
export declare function shouldTreatAsMissingThoughtSignature(value: unknown): boolean;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const DUMMY_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
|
3
|
+
// Antigravity-Manager alignment: Node proxy uses 2 hours TTL.
|
|
4
|
+
const SIGNATURE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
5
|
+
const MIN_SIGNATURE_LENGTH = 50;
|
|
6
|
+
const SESSION_CACHE_LIMIT = 1000;
|
|
7
|
+
const GLOBAL_SIGNATURE_CACHE_KEY = '__LLMSWITCH_ANTIGRAVITY_SESSION_SIGNATURE_CACHE__';
|
|
8
|
+
const GLOBAL_REQUEST_SESSION_CACHE_KEY = '__LLMSWITCH_ANTIGRAVITY_REQUEST_SESSION_ID_CACHE__';
|
|
9
|
+
function getGlobalSignatureCache() {
|
|
10
|
+
const g = globalThis;
|
|
11
|
+
const existing = g[GLOBAL_SIGNATURE_CACHE_KEY];
|
|
12
|
+
if (existing instanceof Map) {
|
|
13
|
+
return existing;
|
|
14
|
+
}
|
|
15
|
+
const created = new Map();
|
|
16
|
+
g[GLOBAL_SIGNATURE_CACHE_KEY] = created;
|
|
17
|
+
return created;
|
|
18
|
+
}
|
|
19
|
+
const sessionSignatures = getGlobalSignatureCache();
|
|
20
|
+
function getGlobalRequestSessionCache() {
|
|
21
|
+
const g = globalThis;
|
|
22
|
+
const existing = g[GLOBAL_REQUEST_SESSION_CACHE_KEY];
|
|
23
|
+
if (existing instanceof Map) {
|
|
24
|
+
return existing;
|
|
25
|
+
}
|
|
26
|
+
const created = new Map();
|
|
27
|
+
g[GLOBAL_REQUEST_SESSION_CACHE_KEY] = created;
|
|
28
|
+
return created;
|
|
29
|
+
}
|
|
30
|
+
const requestSessionIds = getGlobalRequestSessionCache();
|
|
31
|
+
function nowMs() {
|
|
32
|
+
return Date.now();
|
|
33
|
+
}
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function stableStringify(value) {
|
|
38
|
+
if (value === null || value === undefined) {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== 'object') {
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
46
|
+
}
|
|
47
|
+
const record = value;
|
|
48
|
+
const keys = Object.keys(record).sort();
|
|
49
|
+
const entries = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
50
|
+
return `{${entries.join(',')}}`;
|
|
51
|
+
}
|
|
52
|
+
function jsonStringifyFallback(value) {
|
|
53
|
+
// Antigravity-Manager uses `serde_json::Value::to_string()` for the fallback seed.
|
|
54
|
+
// In JS, JSON.stringify preserves insertion order and is the closest equivalent.
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(value ?? null) ?? 'null';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return stableStringify(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function sha256Hex(value) {
|
|
63
|
+
return createHash('sha256').update(value).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
function isExpired(entry, ts) {
|
|
66
|
+
return ts - entry.timestamp > SIGNATURE_TTL_MS;
|
|
67
|
+
}
|
|
68
|
+
function isRequestSessionExpired(entry, ts) {
|
|
69
|
+
return ts - entry.timestamp > SIGNATURE_TTL_MS;
|
|
70
|
+
}
|
|
71
|
+
function pruneExpired() {
|
|
72
|
+
const ts = nowMs();
|
|
73
|
+
for (const [key, entry] of sessionSignatures.entries()) {
|
|
74
|
+
if (isExpired(entry, ts)) {
|
|
75
|
+
sessionSignatures.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function ensureCacheLimit() {
|
|
80
|
+
if (sessionSignatures.size <= SESSION_CACHE_LIMIT) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
pruneExpired();
|
|
84
|
+
if (sessionSignatures.size <= SESSION_CACHE_LIMIT) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Best-effort: remove the oldest entries until within limit.
|
|
88
|
+
const entries = Array.from(sessionSignatures.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
89
|
+
const overflow = sessionSignatures.size - SESSION_CACHE_LIMIT;
|
|
90
|
+
for (let i = 0; i < overflow; i++) {
|
|
91
|
+
const key = entries[i]?.[0];
|
|
92
|
+
if (key) {
|
|
93
|
+
sessionSignatures.delete(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function cacheAntigravityRequestSessionId(requestId, sessionId) {
|
|
98
|
+
const rid = typeof requestId === 'string' ? requestId.trim() : '';
|
|
99
|
+
const sid = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
100
|
+
if (!rid || !sid) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const ts = nowMs();
|
|
104
|
+
requestSessionIds.set(rid, { sessionId: sid, timestamp: ts });
|
|
105
|
+
if (requestSessionIds.size <= SESSION_CACHE_LIMIT) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Best-effort cleanup to avoid unbounded growth.
|
|
109
|
+
const entries = Array.from(requestSessionIds.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
110
|
+
const overflow = requestSessionIds.size - SESSION_CACHE_LIMIT;
|
|
111
|
+
for (let i = 0; i < overflow; i++) {
|
|
112
|
+
const key = entries[i]?.[0];
|
|
113
|
+
if (key) {
|
|
114
|
+
requestSessionIds.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function getAntigravityRequestSessionId(requestId) {
|
|
119
|
+
const rid = typeof requestId === 'string' ? requestId.trim() : '';
|
|
120
|
+
if (!rid) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const entry = requestSessionIds.get(rid);
|
|
124
|
+
if (!entry) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const ts = nowMs();
|
|
128
|
+
if (isRequestSessionExpired(entry, ts)) {
|
|
129
|
+
requestSessionIds.delete(rid);
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return entry.sessionId;
|
|
133
|
+
}
|
|
134
|
+
function findGeminiContentsNode(payload) {
|
|
135
|
+
if (!isRecord(payload)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(payload.contents)) {
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
const nested = payload.request;
|
|
142
|
+
if (isRecord(nested) && Array.isArray(nested.contents)) {
|
|
143
|
+
return nested;
|
|
144
|
+
}
|
|
145
|
+
const data = payload.data;
|
|
146
|
+
if (isRecord(data)) {
|
|
147
|
+
return findGeminiContentsNode(data);
|
|
148
|
+
}
|
|
149
|
+
return payload;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Antigravity-Manager alignment: derive a stable session fingerprint for Gemini native requests.
|
|
153
|
+
* - sha256(first user text parts joined), if len>10 and no "<system-reminder>"
|
|
154
|
+
* - else sha256(JSON body)
|
|
155
|
+
* - sid = "sid-" + first 16 hex chars
|
|
156
|
+
*/
|
|
157
|
+
export function extractAntigravityGeminiSessionId(payload) {
|
|
158
|
+
const node = findGeminiContentsNode(payload) ?? {};
|
|
159
|
+
let seed;
|
|
160
|
+
const contentsRaw = node.contents;
|
|
161
|
+
const contents = Array.isArray(contentsRaw) ? contentsRaw : [];
|
|
162
|
+
for (const content of contents) {
|
|
163
|
+
if (!isRecord(content))
|
|
164
|
+
continue;
|
|
165
|
+
if (typeof content.role !== 'string' || content.role !== 'user')
|
|
166
|
+
continue;
|
|
167
|
+
const partsRaw = content.parts;
|
|
168
|
+
const parts = Array.isArray(partsRaw) ? partsRaw : [];
|
|
169
|
+
const texts = [];
|
|
170
|
+
for (const part of parts) {
|
|
171
|
+
if (!isRecord(part))
|
|
172
|
+
continue;
|
|
173
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
174
|
+
if (text) {
|
|
175
|
+
texts.push(text);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const combined = texts.join(' ').trim();
|
|
179
|
+
if (combined.length > 10 && !combined.includes('<system-reminder>')) {
|
|
180
|
+
seed = combined;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!seed) {
|
|
185
|
+
seed = jsonStringifyFallback(node);
|
|
186
|
+
}
|
|
187
|
+
const hash = sha256Hex(seed);
|
|
188
|
+
return `sid-${hash.slice(0, 16)}`;
|
|
189
|
+
}
|
|
190
|
+
export function cacheAntigravitySessionSignature(sessionId, signature, messageCount = 1) {
|
|
191
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (typeof signature !== 'string' || signature.length < MIN_SIGNATURE_LENGTH) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const key = sessionId.trim();
|
|
198
|
+
const ts = nowMs();
|
|
199
|
+
const existing = sessionSignatures.get(key);
|
|
200
|
+
let shouldStore = false;
|
|
201
|
+
if (!existing) {
|
|
202
|
+
shouldStore = true;
|
|
203
|
+
}
|
|
204
|
+
else if (isExpired(existing, ts)) {
|
|
205
|
+
shouldStore = true;
|
|
206
|
+
}
|
|
207
|
+
else if (messageCount < existing.messageCount) {
|
|
208
|
+
// Rewind detected: allow overwrite.
|
|
209
|
+
shouldStore = true;
|
|
210
|
+
}
|
|
211
|
+
else if (messageCount === existing.messageCount) {
|
|
212
|
+
shouldStore = signature.length > existing.signature.length;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
shouldStore = true;
|
|
216
|
+
}
|
|
217
|
+
if (!shouldStore) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
sessionSignatures.set(key, { signature, messageCount, timestamp: ts });
|
|
221
|
+
ensureCacheLimit();
|
|
222
|
+
}
|
|
223
|
+
export function getAntigravitySessionSignature(sessionId) {
|
|
224
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const key = sessionId.trim();
|
|
228
|
+
const entry = sessionSignatures.get(key);
|
|
229
|
+
if (!entry) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const ts = nowMs();
|
|
233
|
+
if (isExpired(entry, ts)) {
|
|
234
|
+
sessionSignatures.delete(key);
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return entry.signature;
|
|
238
|
+
}
|
|
239
|
+
export function shouldTreatAsMissingThoughtSignature(value) {
|
|
240
|
+
if (typeof value !== 'string') {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const trimmed = value.trim();
|
|
244
|
+
return trimmed.length === 0 || trimmed === DUMMY_THOUGHT_SIGNATURE;
|
|
245
|
+
}
|
|
@@ -108,6 +108,25 @@ function hasFunctionDeclarations(tools) {
|
|
|
108
108
|
return Array.isArray(record.functionDeclarations) && record.functionDeclarations.length > 0;
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
function hasNonNetworkingFunctionDeclarations(tools) {
|
|
112
|
+
if (!Array.isArray(tools))
|
|
113
|
+
return false;
|
|
114
|
+
for (const tool of tools) {
|
|
115
|
+
if (!tool || typeof tool !== 'object')
|
|
116
|
+
continue;
|
|
117
|
+
const record = tool;
|
|
118
|
+
const decls = Array.isArray(record.functionDeclarations)
|
|
119
|
+
? record.functionDeclarations
|
|
120
|
+
: [];
|
|
121
|
+
for (const decl of decls) {
|
|
122
|
+
const name = typeof decl?.name === 'string' ? String(decl.name) : '';
|
|
123
|
+
if (name && !isNetworkingToolName(name)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
111
130
|
function injectGoogleSearchTool(request) {
|
|
112
131
|
const toolsRaw = request.tools;
|
|
113
132
|
if (!Array.isArray(toolsRaw)) {
|
|
@@ -137,7 +156,8 @@ function pruneSearchFunctionDeclarations(request) {
|
|
|
137
156
|
const record = tool;
|
|
138
157
|
if (!Array.isArray(record.functionDeclarations))
|
|
139
158
|
continue;
|
|
140
|
-
|
|
159
|
+
const decls = record.functionDeclarations;
|
|
160
|
+
const filtered = decls.filter((decl) => {
|
|
141
161
|
if (!decl || typeof decl !== 'object')
|
|
142
162
|
return false;
|
|
143
163
|
const name = typeof decl.name === 'string'
|
|
@@ -145,7 +165,18 @@ function pruneSearchFunctionDeclarations(request) {
|
|
|
145
165
|
: '';
|
|
146
166
|
return name ? !isNetworkingToolName(name) : true;
|
|
147
167
|
});
|
|
168
|
+
if (filtered.length === 0) {
|
|
169
|
+
delete record.functionDeclarations;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
record.functionDeclarations = filtered;
|
|
173
|
+
}
|
|
148
174
|
}
|
|
175
|
+
request.tools = toolsRaw.filter((tool) => {
|
|
176
|
+
if (!tool || typeof tool !== 'object')
|
|
177
|
+
return true;
|
|
178
|
+
return Object.keys(tool).length > 0;
|
|
179
|
+
});
|
|
149
180
|
}
|
|
150
181
|
function deepCleanUndefined(value) {
|
|
151
182
|
if (Array.isArray(value)) {
|
|
@@ -244,9 +275,19 @@ function resolveAntigravityRequestConfig(options) {
|
|
|
244
275
|
imageConfig: parsed.imageConfig
|
|
245
276
|
};
|
|
246
277
|
}
|
|
247
|
-
|
|
278
|
+
// Gemini v1internal constraint (Antigravity manager alignment):
|
|
279
|
+
// - "web_search" requests must NOT mix googleSearch with functionDeclarations.
|
|
280
|
+
// - If the request includes any non-networking function declarations (e.g. MCP/local tools),
|
|
281
|
+
// we must disable networking and keep the request as "agent" to avoid upstream schema errors.
|
|
282
|
+
const wantsNetworking = original.endsWith('-online') || detectsNetworkingTool(options.tools);
|
|
283
|
+
const hasLocalFunctions = hasNonNetworkingFunctionDeclarations(options.tools);
|
|
284
|
+
const enableNetworking = wantsNetworking && !hasLocalFunctions;
|
|
248
285
|
let finalModel = stripOnlineSuffix(mapped);
|
|
249
286
|
finalModel = normalizePreviewAlias(finalModel);
|
|
287
|
+
// Only gemini-2.5-flash reliably supports googleSearch in v1internal; downgrade for networking.
|
|
288
|
+
if (enableNetworking && finalModel !== 'gemini-2.5-flash') {
|
|
289
|
+
finalModel = 'gemini-2.5-flash';
|
|
290
|
+
}
|
|
250
291
|
return {
|
|
251
292
|
requestType: enableNetworking ? 'web_search' : 'agent',
|
|
252
293
|
injectGoogleSearch: enableNetworking,
|
|
@@ -640,7 +681,33 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
640
681
|
const semanticsNode = readGeminiSemantics(chat);
|
|
641
682
|
const systemTextBlocksFromSemantics = readSystemTextBlocksFromSemantics(chat);
|
|
642
683
|
let antigravityRequestType;
|
|
684
|
+
// Gemini series alignment:
|
|
685
|
+
// - Client/tool surface uses canonical name: "web_search"
|
|
686
|
+
// - Gemini upstream receives a function tool name: "websearch" (no underscore)
|
|
687
|
+
// then ServerTool intercepts and executes the web_search route.
|
|
688
|
+
const mapToolNameForGemini = (nameRaw) => {
|
|
689
|
+
const name = typeof nameRaw === 'string' ? nameRaw.trim() : '';
|
|
690
|
+
if (!name)
|
|
691
|
+
return undefined;
|
|
692
|
+
if (name === 'web_search' || name.startsWith('web_search_')) {
|
|
693
|
+
return 'websearch';
|
|
694
|
+
}
|
|
695
|
+
return name;
|
|
696
|
+
};
|
|
643
697
|
const bridgeDefs = chat.tools && chat.tools.length ? mapChatToolsToBridge(chat.tools) : undefined;
|
|
698
|
+
if (bridgeDefs && bridgeDefs.length) {
|
|
699
|
+
for (const def of bridgeDefs) {
|
|
700
|
+
if (!def || typeof def !== 'object')
|
|
701
|
+
continue;
|
|
702
|
+
const mapped = mapToolNameForGemini(def.name);
|
|
703
|
+
if (mapped && mapped !== def.name) {
|
|
704
|
+
def.name = mapped;
|
|
705
|
+
if (def.function && typeof def.function === 'object') {
|
|
706
|
+
def.function.name = mapped;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
644
711
|
const toolSchemaKeys = bridgeDefs ? buildToolSchemaKeyMap(bridgeDefs) : new Map();
|
|
645
712
|
const sourceMessages = chat.messages;
|
|
646
713
|
// 收集当前 ChatEnvelope 中 assistant/tool_calls 的 id,用于过滤孤立的 tool_result:
|
|
@@ -670,6 +737,7 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
670
737
|
if (allowFunctionCallingProtocol) {
|
|
671
738
|
const toolOutput = convertToolMessageToOutput(message, assistantToolCallIds);
|
|
672
739
|
if (toolOutput) {
|
|
740
|
+
toolOutput.name = mapToolNameForGemini(toolOutput.name);
|
|
673
741
|
contents.push(buildFunctionResponseEntry(toolOutput, { includeCallId: includeToolCallIds }));
|
|
674
742
|
emittedToolOutputs.add(toolOutput.tool_call_id);
|
|
675
743
|
}
|
|
@@ -699,7 +767,7 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
699
767
|
continue;
|
|
700
768
|
}
|
|
701
769
|
const fn = tc.function || {};
|
|
702
|
-
const name = typeof fn.name === 'string' ? fn.name : undefined;
|
|
770
|
+
const name = mapToolNameForGemini(typeof fn.name === 'string' ? fn.name : undefined);
|
|
703
771
|
if (!name)
|
|
704
772
|
continue;
|
|
705
773
|
let argsStruct;
|
|
@@ -726,11 +794,10 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
726
794
|
if (includeToolCallIds && typeof tc.id === 'string' && tc.id.trim().length) {
|
|
727
795
|
part.functionCall.id = String(tc.id).trim();
|
|
728
796
|
}
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
797
|
+
// Antigravity-Manager alignment:
|
|
798
|
+
// - Do NOT invent a dummy thoughtSignature in conversion.
|
|
799
|
+
// - Only inject a real thoughtSignature when the compat layer has a cached signature.
|
|
800
|
+
// This avoids sending a placeholder that may be treated as an invalid fingerprint upstream.
|
|
734
801
|
entry.parts.push(part);
|
|
735
802
|
}
|
|
736
803
|
if (entry.parts.length) {
|
|
@@ -756,6 +823,7 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
756
823
|
if (emittedToolOutputs.has(output.tool_call_id)) {
|
|
757
824
|
continue;
|
|
758
825
|
}
|
|
826
|
+
output.name = mapToolNameForGemini(output.name);
|
|
759
827
|
contents.push(buildFunctionResponseEntry(output, { includeCallId: includeToolCallIds }));
|
|
760
828
|
emittedToolOutputs.add(output.tool_call_id);
|
|
761
829
|
}
|
|
@@ -856,7 +924,9 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
856
924
|
if (isAntigravityProvider && typeof request.model === 'string') {
|
|
857
925
|
const requestPayload = request;
|
|
858
926
|
const original = requestPayload.model;
|
|
859
|
-
|
|
927
|
+
// Antigravity v1internal model IDs are tiered (e.g. gemini-3-pro-high/low) and must be preserved.
|
|
928
|
+
// Align with fetchAvailableModels: do NOT strip "-high"/"-low" suffixes for upstream requests.
|
|
929
|
+
const mapped = stripOnlineSuffix(original);
|
|
860
930
|
const size = typeof chat.parameters?.size === 'string' ? String(chat.parameters.size) : undefined;
|
|
861
931
|
const quality = typeof chat.parameters?.quality === 'string' ? String(chat.parameters.quality) : undefined;
|
|
862
932
|
const config = resolveAntigravityRequestConfig({
|
|
@@ -941,14 +1011,9 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
941
1011
|
if (!Array.isArray(parts))
|
|
942
1012
|
continue;
|
|
943
1013
|
const first = parts[0];
|
|
944
|
-
const firstIsThinking = isJsonObject(first) &&
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
content.parts = [
|
|
948
|
-
{ text: '...', thoughtSignature: 'skip_thought_signature_validator' },
|
|
949
|
-
...parts
|
|
950
|
-
];
|
|
951
|
-
}
|
|
1014
|
+
const firstIsThinking = isJsonObject(first) && ('thought' in first || 'thoughtSignature' in first);
|
|
1015
|
+
// Antigravity-Manager alignment: do not inject placeholder thoughtSignature blocks.
|
|
1016
|
+
// If the upstream requires a signature, it must come from cached candidate parts.
|
|
952
1017
|
break;
|
|
953
1018
|
}
|
|
954
1019
|
gc.thinkingConfig = thinkingConfig;
|
|
@@ -18,6 +18,7 @@ import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image
|
|
|
18
18
|
import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
|
|
19
19
|
import { applyClaudeThinkingToolSchemaCompat } from '../../../compat/actions/claude-thinking-tools.js';
|
|
20
20
|
import { wrapGeminiCliRequest } from '../../../compat/actions/gemini-cli-request.js';
|
|
21
|
+
import { cacheAntigravityThoughtSignatureFromGeminiResponse } from '../../../compat/actions/antigravity-thought-signature-cache.js';
|
|
21
22
|
const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
|
|
22
23
|
const INTERNAL_STATE = Symbol('compat.internal_state');
|
|
23
24
|
export function runRequestCompatPipeline(profileId, payload, options) {
|
|
@@ -203,7 +204,12 @@ function applyMapping(root, mapping, state) {
|
|
|
203
204
|
break;
|
|
204
205
|
case 'gemini_cli_request_wrap':
|
|
205
206
|
if (state.direction === 'request') {
|
|
206
|
-
replaceRoot(root, wrapGeminiCliRequest(root));
|
|
207
|
+
replaceRoot(root, wrapGeminiCliRequest(root, state.adapterContext));
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case 'antigravity_thought_signature_cache':
|
|
211
|
+
if (state.direction === 'response') {
|
|
212
|
+
replaceRoot(root, cacheAntigravityThoughtSignatureFromGeminiResponse(root, state.adapterContext));
|
|
207
213
|
}
|
|
208
214
|
break;
|
|
209
215
|
case 'glm_image_content':
|
|
@@ -118,6 +118,8 @@ export type MappingInstruction = {
|
|
|
118
118
|
action: 'claude_thinking_tool_schema';
|
|
119
119
|
} | {
|
|
120
120
|
action: 'gemini_cli_request_wrap';
|
|
121
|
+
} | {
|
|
122
|
+
action: 'antigravity_thought_signature_cache';
|
|
121
123
|
};
|
|
122
124
|
export type FilterInstruction = {
|
|
123
125
|
action: 'rate_limit_text';
|
|
@@ -261,10 +261,16 @@ export async function convertProviderResponse(options) {
|
|
|
261
261
|
const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
|
|
262
262
|
// 是否跳过 ServerTool 编排:
|
|
263
263
|
// - Provider 不支持 ServerTool(没有 invoker/reenterPipeline)时跳过;
|
|
264
|
-
// - 对于 serverToolFollowup=true
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
const
|
|
264
|
+
// - 对于 serverToolFollowup=true 的二/三跳内部请求,通常需要跳过以防嵌套;
|
|
265
|
+
// - 例外:stop_message_flow 允许链式触发(同一 flow 内重复检查直到 maxRepeats)。
|
|
266
|
+
const rt = readRuntimeMetadata(options.context);
|
|
267
|
+
const loopState = rt?.serverToolLoopState;
|
|
268
|
+
const flowId = loopState && typeof loopState === 'object' && !Array.isArray(loopState)
|
|
269
|
+
? String(loopState.flowId || '').trim()
|
|
270
|
+
: '';
|
|
271
|
+
const isStopMessageFlow = flowId === 'stop_message_flow';
|
|
272
|
+
// stop_message_flow 的 followup 允许再次触发(直到 maxRepeats),其他 followup 跳过。
|
|
273
|
+
const skipServerTools = (isFollowup && !isStopMessageFlow) || !hasServerToolSupport;
|
|
268
274
|
// 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
|
|
269
275
|
// 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
|
|
270
276
|
// __sse_responses 可读流。
|
|
@@ -5,6 +5,7 @@ type DebugLike = {
|
|
|
5
5
|
log?: (...args: unknown[]) => void;
|
|
6
6
|
} | Console | undefined;
|
|
7
7
|
export declare function resetRateLimitBackoffForProvider(providerKey: string): void;
|
|
8
|
+
export declare function applyAntigravityRiskPolicyImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
8
9
|
export declare function handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
|
|
9
10
|
export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
|
|
10
11
|
export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
|
|
@@ -100,6 +100,106 @@ export function resetRateLimitBackoffForProvider(providerKey) {
|
|
|
100
100
|
}
|
|
101
101
|
rateLimitBackoffByProvider.delete(providerKey);
|
|
102
102
|
}
|
|
103
|
+
const antigravityRiskBySignature = new Map();
|
|
104
|
+
const ANTIGRAVITY_RISK_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_RESET_WINDOW', 30 * 60_000);
|
|
105
|
+
const ANTIGRAVITY_RISK_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_COOLDOWN', 5 * 60_000);
|
|
106
|
+
const ANTIGRAVITY_RISK_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_BAN', 24 * 60 * 60_000);
|
|
107
|
+
function isAntigravityEvent(event) {
|
|
108
|
+
const runtime = event?.runtime;
|
|
109
|
+
if (!runtime || typeof runtime !== 'object') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const providerId = typeof runtime.providerId === 'string' ? runtime.providerId.trim().toLowerCase() : '';
|
|
113
|
+
if (providerId === 'antigravity') {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const providerKey = typeof runtime.providerKey === 'string' ? runtime.providerKey.trim().toLowerCase() : '';
|
|
117
|
+
return providerKey.startsWith('antigravity.');
|
|
118
|
+
}
|
|
119
|
+
function shouldTriggerAntigravityRiskPolicy(event) {
|
|
120
|
+
const status = typeof event.status === 'number' ? event.status : undefined;
|
|
121
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
122
|
+
// Focus on "dirty request" / auth / permission class issues. Avoid 429 which already has backoff.
|
|
123
|
+
return status >= 400 && status < 500 && status !== 429;
|
|
124
|
+
}
|
|
125
|
+
// If status is missing, fall back to known HTTP-ish error codes.
|
|
126
|
+
return typeof event.code === 'string' && /^HTTP_4\d\d$/.test(event.code.trim());
|
|
127
|
+
}
|
|
128
|
+
function computeAntigravityRiskSignature(event) {
|
|
129
|
+
const status = typeof event.status === 'number' && Number.isFinite(event.status) ? String(event.status) : '';
|
|
130
|
+
const code = typeof event.code === 'string' && event.code.trim() ? event.code.trim() : '';
|
|
131
|
+
const stage = typeof event.stage === 'string' && event.stage.trim() ? event.stage.trim() : '';
|
|
132
|
+
const parts = [status, code, stage].filter((p) => p.length > 0);
|
|
133
|
+
return parts.length ? parts.join(':') : 'unknown';
|
|
134
|
+
}
|
|
135
|
+
export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
|
|
136
|
+
if (!event) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isAntigravityEvent(event)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!shouldTriggerAntigravityRiskPolicy(event)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const signature = computeAntigravityRiskSignature(event);
|
|
146
|
+
if (!signature || signature === 'unknown') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const prev = antigravityRiskBySignature.get(signature);
|
|
151
|
+
let count = 1;
|
|
152
|
+
if (prev) {
|
|
153
|
+
const elapsed = now - prev.lastAt;
|
|
154
|
+
if (Number.isFinite(elapsed) && elapsed >= 0 && elapsed < ANTIGRAVITY_RISK_RESET_WINDOW_MS) {
|
|
155
|
+
count = prev.count + 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
antigravityRiskBySignature.set(signature, { count, lastAt: now });
|
|
159
|
+
// Escalation ladder (Antigravity account safety):
|
|
160
|
+
// 1) First/second occurrence: normal retry/fallback logic handles per-request behavior.
|
|
161
|
+
// 2) Third occurrence: cooldown all Antigravity providerKeys for 5 minutes.
|
|
162
|
+
// 3) Fourth+ occurrence: effectively remove Antigravity from routing (long ban window).
|
|
163
|
+
const providerKeys = providerRegistry.listProviderKeys('antigravity');
|
|
164
|
+
if (providerKeys.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (count === 3) {
|
|
168
|
+
for (const key of providerKeys) {
|
|
169
|
+
try {
|
|
170
|
+
healthManager.tripProvider(key, 'risk_cooldown', ANTIGRAVITY_RISK_COOLDOWN_MS);
|
|
171
|
+
markProviderCooldown(key, ANTIGRAVITY_RISK_COOLDOWN_MS);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// ignore lookup failures
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
debug?.log?.('[virtual-router] antigravity risk cooldown', {
|
|
178
|
+
signature,
|
|
179
|
+
count,
|
|
180
|
+
cooldownMs: ANTIGRAVITY_RISK_COOLDOWN_MS,
|
|
181
|
+
affected: providerKeys
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else if (count >= 4) {
|
|
185
|
+
const ttl = Math.max(ANTIGRAVITY_RISK_BAN_MS, ANTIGRAVITY_RISK_COOLDOWN_MS);
|
|
186
|
+
for (const key of providerKeys) {
|
|
187
|
+
try {
|
|
188
|
+
healthManager.tripProvider(key, 'risk_blacklist', ttl);
|
|
189
|
+
markProviderCooldown(key, ttl);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// ignore lookup failures
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
debug?.log?.('[virtual-router] antigravity risk blacklist', {
|
|
196
|
+
signature,
|
|
197
|
+
count,
|
|
198
|
+
cooldownMs: ttl,
|
|
199
|
+
affected: providerKeys
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
103
203
|
export function handleProviderFailureImpl(event, healthManager, healthConfig, markProviderCooldown) {
|
|
104
204
|
if (!event || !event.providerKey) {
|
|
105
205
|
return;
|
|
@@ -4,6 +4,34 @@ import { resolveHealthWeightedConfig } from '../health-weighted.js';
|
|
|
4
4
|
import { pinCandidatesByAliasQueue, resolveAliasSelectionStrategy } from './alias-selection.js';
|
|
5
5
|
import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './key-parsing.js';
|
|
6
6
|
import { selectProviderKeyFromCandidatePool } from './tier-selection-select.js';
|
|
7
|
+
function shouldAvoidAntigravityAfterRepeatedError(metadata) {
|
|
8
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const rtRaw = metadata.__rt;
|
|
12
|
+
if (!rtRaw || typeof rtRaw !== 'object' || Array.isArray(rtRaw)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const rt = rtRaw;
|
|
16
|
+
const signature = typeof rt.antigravityRetryErrorSignature === 'string' ? rt.antigravityRetryErrorSignature.trim() : '';
|
|
17
|
+
const consecutive = typeof rt.antigravityRetryErrorConsecutive === 'number' && Number.isFinite(rt.antigravityRetryErrorConsecutive)
|
|
18
|
+
? Math.max(0, Math.floor(rt.antigravityRetryErrorConsecutive))
|
|
19
|
+
: 0;
|
|
20
|
+
return signature.length > 0 && signature !== 'unknown' && consecutive >= 2;
|
|
21
|
+
}
|
|
22
|
+
function preferNonAntigravityWhenPossible(candidates) {
|
|
23
|
+
if (!Array.isArray(candidates) || candidates.length < 2) {
|
|
24
|
+
return candidates;
|
|
25
|
+
}
|
|
26
|
+
const nonAntigravity = candidates.filter((key) => (extractProviderId(key) ?? '') !== 'antigravity');
|
|
27
|
+
return nonAntigravity.length > 0 ? nonAntigravity : candidates;
|
|
28
|
+
}
|
|
29
|
+
function extractNonAntigravityTargets(targets) {
|
|
30
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return targets.filter((key) => (extractProviderId(key) ?? '') !== 'antigravity');
|
|
34
|
+
}
|
|
7
35
|
export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features, deps, options) {
|
|
8
36
|
const { disabledProviders, disabledKeysMap, allowedProviders, disabledModels, requiredProviderKeys } = options;
|
|
9
37
|
let targets = Array.isArray(tier.targets) ? tier.targets : [];
|
|
@@ -124,19 +152,28 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
|
|
|
124
152
|
}
|
|
125
153
|
const contextResult = deps.contextAdvisor.classify(targets, estimatedTokens, (key) => deps.providerRegistry.get(key));
|
|
126
154
|
const prioritizedPools = buildContextCandidatePools(contextResult);
|
|
155
|
+
const avoidAntigravityOnRetry = isRecoveryAttempt && shouldAvoidAntigravityAfterRepeatedError(features.metadata);
|
|
156
|
+
const nonAntigravityTargets = avoidAntigravityOnRetry ? extractNonAntigravityTargets(targets) : [];
|
|
157
|
+
const poolsToTry = avoidAntigravityOnRetry && nonAntigravityTargets.length > 0
|
|
158
|
+
? [nonAntigravityTargets, ...prioritizedPools]
|
|
159
|
+
: prioritizedPools;
|
|
127
160
|
const quotaView = deps.quotaView;
|
|
128
161
|
const now = quotaView ? Date.now() : 0;
|
|
129
162
|
const healthWeightedCfg = resolveHealthWeightedConfig(deps.loadBalancer.getPolicy().healthWeighted);
|
|
130
163
|
const contextWeightedCfg = resolveContextWeightedConfig(deps.loadBalancer.getPolicy().contextWeighted);
|
|
131
164
|
const warnRatio = deps.contextAdvisor.getConfig().warnRatio;
|
|
132
165
|
const nowForWeights = Date.now();
|
|
133
|
-
for (const candidatePool of
|
|
166
|
+
for (const candidatePool of poolsToTry) {
|
|
167
|
+
const isSafePool = candidatePool === contextResult.safe;
|
|
168
|
+
const candidatesForSelect = avoidAntigravityOnRetry
|
|
169
|
+
? preferNonAntigravityWhenPossible(candidatePool)
|
|
170
|
+
: candidatePool;
|
|
134
171
|
const providerKey = selectProviderKeyFromCandidatePool({
|
|
135
172
|
routeName,
|
|
136
173
|
tier,
|
|
137
174
|
stickyKey,
|
|
138
|
-
candidates:
|
|
139
|
-
isSafePool
|
|
175
|
+
candidates: candidatesForSelect,
|
|
176
|
+
isSafePool,
|
|
140
177
|
deps,
|
|
141
178
|
options,
|
|
142
179
|
contextResult,
|
|
@@ -10,7 +10,7 @@ import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRo
|
|
|
10
10
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from './sticky-session-store.js';
|
|
11
11
|
import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
|
|
12
12
|
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine-selection.js';
|
|
13
|
-
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
|
|
13
|
+
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
|
|
14
14
|
import { mergeStopMessageFromPersisted } from './stop-message-state-sync.js';
|
|
15
15
|
export class VirtualRouterEngine {
|
|
16
16
|
routing = {};
|
|
@@ -623,6 +623,9 @@ export class VirtualRouterEngine {
|
|
|
623
623
|
// ignore persistence errors
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
|
+
// Antigravity account safety policy should apply even when quotaView is enabled.
|
|
627
|
+
// It uses router-local cooldown TTLs (not quotaView) to temporarily remove Antigravity from selection.
|
|
628
|
+
applyAntigravityRiskPolicyImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
626
629
|
// 当 Host 注入 quotaView 时,VirtualRouter 的入池/优先级决策应以 quota 为准;
|
|
627
630
|
// 此时不再在 engine-health 内部进行 429/backoff/series cooldown 等健康决策,
|
|
628
631
|
// 以避免与 daemon/quota-center 的长期熔断策略重复维护并导致日志噪声。
|
|
@@ -428,8 +428,8 @@ export async function runServerToolOrchestration(options) {
|
|
|
428
428
|
// stopMessage 是一种“状态型” servertool:一旦触发,我们需要尽量避免因 followup 失败而把状态留在可继续触发的位置,
|
|
429
429
|
// 否则会出现下一轮仍然自动触发 → 再次失败 → 客户端永远 502 的死循环。
|
|
430
430
|
//
|
|
431
|
-
//
|
|
432
|
-
const stopMessageReservation =
|
|
431
|
+
// stop_message_flow 的计数器递增由 handler 在决定触发时处理,engine 不再提前递增。
|
|
432
|
+
const stopMessageReservation = null;
|
|
433
433
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
434
434
|
try {
|
|
435
435
|
followup = await withTimeout(options.reenterPipeline({
|
|
@@ -521,61 +521,6 @@ export async function runServerToolOrchestration(options) {
|
|
|
521
521
|
flowId: engineResult.execution.flowId
|
|
522
522
|
};
|
|
523
523
|
}
|
|
524
|
-
function reserveStopMessageUsage(adapterContext) {
|
|
525
|
-
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
const sessionId = typeof adapterContext.sessionId === 'string'
|
|
529
|
-
? adapterContext.sessionId.trim()
|
|
530
|
-
: '';
|
|
531
|
-
const conversationId = typeof adapterContext.conversationId === 'string'
|
|
532
|
-
? adapterContext.conversationId.trim()
|
|
533
|
-
: '';
|
|
534
|
-
const stickyKey = sessionId ? `session:${sessionId}` : conversationId ? `conversation:${conversationId}` : '';
|
|
535
|
-
if (!stickyKey) {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
let state = loadRoutingInstructionStateSync(stickyKey);
|
|
539
|
-
if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
|
|
540
|
-
const rt = readRuntimeMetadata(adapterContext);
|
|
541
|
-
const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
|
|
542
|
-
if (!fallback) {
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
state = createStopMessageState(fallback);
|
|
546
|
-
}
|
|
547
|
-
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
548
|
-
const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
549
|
-
? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
|
|
550
|
-
: 0;
|
|
551
|
-
if (!text || maxRepeats <= 0) {
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
const previousState = cloneRoutingInstructionState(state);
|
|
555
|
-
const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
|
|
556
|
-
? Math.max(0, Math.floor(state.stopMessageUsed))
|
|
557
|
-
: 0;
|
|
558
|
-
const nextUsed = used + 1;
|
|
559
|
-
state.stopMessageUsed = nextUsed;
|
|
560
|
-
const now = Date.now();
|
|
561
|
-
state.stopMessageLastUsedAt = now;
|
|
562
|
-
if (nextUsed >= maxRepeats) {
|
|
563
|
-
// Auto-clear after reaching max repeats. This avoids leaving an "exhausted" stopMessage
|
|
564
|
-
// stuck in sticky state and ensures a fresh `<**stopMessage:...**>` can re-arm cleanly.
|
|
565
|
-
state.stopMessageText = undefined;
|
|
566
|
-
state.stopMessageMaxRepeats = undefined;
|
|
567
|
-
state.stopMessageUsed = undefined;
|
|
568
|
-
state.stopMessageSource = undefined;
|
|
569
|
-
// Keep monotonic timestamps as a tombstone to prevent accidental re-application from replayed history.
|
|
570
|
-
state.stopMessageUpdatedAt = now;
|
|
571
|
-
state.stopMessageLastUsedAt = now;
|
|
572
|
-
}
|
|
573
|
-
saveRoutingInstructionStateSync(stickyKey, state);
|
|
574
|
-
return { stickyKey, previousState };
|
|
575
|
-
}
|
|
576
|
-
function rollbackStopMessageUsage(reservation) {
|
|
577
|
-
saveRoutingInstructionStateSync(reservation.stickyKey, reservation.previousState);
|
|
578
|
-
}
|
|
579
524
|
function disableStopMessageAfterFailedFollowup(adapterContext, reservation) {
|
|
580
525
|
try {
|
|
581
526
|
const key = reservation && typeof reservation.stickyKey === 'string' && reservation.stickyKey.trim()
|
|
@@ -148,7 +148,24 @@ const handler = async (ctx) => {
|
|
|
148
148
|
debugLog('skip_failed_build_followup', { stickyKey });
|
|
149
149
|
return null;
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
// Extract assistant message for potential followup injection (no-op today; keeps compat).
|
|
152
|
+
void extractAssistantMessageForFollowup(ctx.base);
|
|
153
|
+
// Increment stopMessage usage counter when we decide to trigger followup.
|
|
154
|
+
const nextUsed = used + 1;
|
|
155
|
+
state.stopMessageUsed = nextUsed;
|
|
156
|
+
state.stopMessageLastUsedAt = Date.now();
|
|
157
|
+
// If this will be the last allowed trigger, mark it for cleanup.
|
|
158
|
+
// We still return the followup plan for this trigger, but clear the config
|
|
159
|
+
// so the next response won't trigger again.
|
|
160
|
+
if (nextUsed >= maxRepeats) {
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
state.stopMessageText = undefined;
|
|
163
|
+
state.stopMessageMaxRepeats = undefined;
|
|
164
|
+
state.stopMessageUsed = undefined;
|
|
165
|
+
state.stopMessageSource = undefined;
|
|
166
|
+
state.stopMessageUpdatedAt = now;
|
|
167
|
+
}
|
|
168
|
+
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
152
169
|
return {
|
|
153
170
|
flowId: FLOW_ID,
|
|
154
171
|
finalize: async () => ({
|