@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.
@@ -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
- const name = typeof fc.name === 'string' ? String(fc.name) : undefined;
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
- const name = typeof fr.name === 'string' && fr.name.trim().length ? String(fr.name) : undefined;
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) ?? DUMMY_THOUGHT_SIGNATURE;
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
- export declare function wrapGeminiCliRequest(payload: JsonObject): JsonObject;
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
- export function wrapGeminiCliRequest(payload) {
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
+ }
@@ -29,6 +29,8 @@
29
29
  ]
30
30
  },
31
31
  "response": {
32
- "mappings": []
32
+ "mappings": [
33
+ { "action": "antigravity_thought_signature_cache" }
34
+ ]
33
35
  }
34
36
  }
@@ -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
- record.functionDeclarations = record.functionDeclarations.filter((decl) => {
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
- const enableNetworking = original.endsWith('-online') || detectsNetworkingTool(options.tools);
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
- // gcli2api alignment: Gemini CLI / antigravity functionCall parts always include a thoughtSignature
730
- // (Cloud Code Assist expects it even when no real signature exists).
731
- if (requiresThoughtSignature && !Object.prototype.hasOwnProperty.call(part, 'thoughtSignature')) {
732
- part.thoughtSignature = 'skip_thought_signature_validator';
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
- const mapped = stripTierSuffix(original);
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
- ('thought' in first || 'thoughtSignature' in first);
946
- if (!firstIsThinking) {
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
- // followup 的语义是“本轮 ServerTool 已决定并发起下一跳”,下一跳响应不应再次触发 ServerTool,
266
- // 否则会形成嵌套/循环(例如 stop_message_flow 反复触发)。
267
- const skipServerTools = isFollowup || !hasServerToolSupport;
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 prioritizedPools) {
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: candidatePool,
139
- isSafePool: candidatePool === contextResult.safe,
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
- // 因此这里对 stop_message_flow 做一次性 reservation,并在最终判定 followup 为空时清理 stopMessage 状态。
432
- const stopMessageReservation = isStopMessageFlow ? reserveStopMessageUsage(options.adapterContext) : null;
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
- const assistantMessage = extractAssistantMessageForFollowup(ctx.base);
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 () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.1403",
3
+ "version": "0.6.1435",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",