@yansigit/opencodex 2.31.1 → 2.31.3
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/gui/dist/assets/{index-DJDp_XER.js → index-Cxt5fZMP.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/thread-continuity.ts +71 -0
- package/src/adapters/cursor.ts +90 -28
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +91 -2
- package/src/adapters/google.ts +83 -15
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +57 -25
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/oauth/anthropic-routing.ts +129 -219
- package/src/oauth/antigravity-routing.ts +165 -0
- package/src/oauth/cursor-routing.ts +252 -0
- package/src/oauth/index.ts +3 -0
- package/src/providers/cursor-pool.ts +3 -3
- package/src/routing/account-pool/affinity.ts +125 -0
- package/src/routing/account-pool/cooldown.ts +145 -0
- package/src/routing/account-pool/index.ts +45 -0
- package/src/routing/account-pool/resolve.ts +356 -0
- package/src/routing/account-pool/types.ts +32 -0
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/management/oauth-account-routes.ts +47 -12
- package/src/server/responses/core.ts +400 -72
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +13 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
|
@@ -65,3 +65,74 @@ export function lookupCursorThreadConversation(
|
|
|
65
65
|
export function clearCursorThreadContinuityForTests(): void {
|
|
66
66
|
overrides.clear();
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/** Max conversation-id remints after the first surfaced overflow (senpi cap). */
|
|
70
|
+
export const CURSOR_OVERFLOW_REMINT_MAX = 3;
|
|
71
|
+
|
|
72
|
+
type OverflowRemintState = {
|
|
73
|
+
surfaced: boolean;
|
|
74
|
+
remintCount: number;
|
|
75
|
+
skip: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const overflowRemintByScope = new Map<string, OverflowRemintState>();
|
|
79
|
+
|
|
80
|
+
function overflowRemintEntry(scopeKey: string): OverflowRemintState {
|
|
81
|
+
const existing = overflowRemintByScope.get(scopeKey);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
const fresh: OverflowRemintState = { surfaced: false, remintCount: 0, skip: false };
|
|
84
|
+
overflowRemintByScope.set(scopeKey, fresh);
|
|
85
|
+
return fresh;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stable scope for overflow remint accounting. Thread-identified clients key by
|
|
90
|
+
* thread + identity; conversation-only clients key by the base conversation id
|
|
91
|
+
* captured before any remint (wire id may rotate).
|
|
92
|
+
*/
|
|
93
|
+
export function cursorOverflowRemintScopeKey(
|
|
94
|
+
parsed: {
|
|
95
|
+
_clientThreadId?: string;
|
|
96
|
+
_cursorIdentityScope?: string;
|
|
97
|
+
},
|
|
98
|
+
baseConversationId?: string,
|
|
99
|
+
): string | null {
|
|
100
|
+
if (parsed._clientThreadId) {
|
|
101
|
+
return `overflow\0${cursorThreadScopeKey(parsed._clientThreadId, parsed._cursorIdentityScope)}`;
|
|
102
|
+
}
|
|
103
|
+
const base = baseConversationId?.trim();
|
|
104
|
+
if (!base) return null;
|
|
105
|
+
const scope = parsed._cursorIdentityScope?.trim() || "local";
|
|
106
|
+
return `overflow\0${scope}\0conv\0${base}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** True until the first overflow for this scope has been surfaced for Codex compact. */
|
|
110
|
+
export function shouldSurfaceCursorOverflowFirst(scopeKey: string): boolean {
|
|
111
|
+
return overflowRemintEntry(scopeKey).surfaced !== true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function markCursorOverflowSurfaced(scopeKey: string): void {
|
|
115
|
+
const entry = overflowRemintEntry(scopeKey);
|
|
116
|
+
entry.surfaced = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function shouldSkipCursorOverflowRemint(scopeKey: string): boolean {
|
|
120
|
+
const entry = overflowRemintByScope.get(scopeKey);
|
|
121
|
+
if (!entry) return false;
|
|
122
|
+
return entry.skip === true || entry.remintCount >= CURSOR_OVERFLOW_REMINT_MAX;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Record one overflow remint; returns false when the cap is exhausted. */
|
|
126
|
+
export function recordCursorOverflowRemint(scopeKey: string): boolean {
|
|
127
|
+
const entry = overflowRemintEntry(scopeKey);
|
|
128
|
+
if (entry.skip || entry.remintCount >= CURSOR_OVERFLOW_REMINT_MAX) {
|
|
129
|
+
entry.skip = true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
entry.remintCount += 1;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function clearCursorOverflowRemintForTests(): void {
|
|
137
|
+
overflowRemintByScope.clear();
|
|
138
|
+
}
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import type { AdapterEvent, OcxProviderConfig } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
5
5
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isCursorBenignCancelError,
|
|
8
|
+
isCursorInvalidArgumentError,
|
|
9
|
+
isCursorOverflowRemintCandidate,
|
|
10
|
+
safeCursorErrorMessage,
|
|
11
|
+
type CursorSizeContext,
|
|
12
|
+
} from "./cursor/cursor-errors";
|
|
7
13
|
import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
8
14
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
9
15
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
@@ -26,7 +32,14 @@ import {
|
|
|
26
32
|
} from "./cursor/checkpoint-store";
|
|
27
33
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
28
34
|
import { estimateTokens } from "../lib/token-estimate";
|
|
29
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
cursorOverflowRemintScopeKey,
|
|
37
|
+
markCursorOverflowSurfaced,
|
|
38
|
+
recordCursorOverflowRemint,
|
|
39
|
+
rememberCursorThreadConversation,
|
|
40
|
+
shouldSkipCursorOverflowRemint,
|
|
41
|
+
shouldSurfaceCursorOverflowFirst,
|
|
42
|
+
} from "./cursor/thread-continuity";
|
|
30
43
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
31
44
|
import {
|
|
32
45
|
createDisabledCursorTransport,
|
|
@@ -77,10 +90,18 @@ function cursorRequestSizeContext(request: { modelId: string; system: string[];
|
|
|
77
90
|
};
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
function assertCursorRequestSupported(parsed: OcxParsedRequest): void {
|
|
94
|
+
if (parsed.options.textFormat !== undefined || parsed._structuredOutput === true) {
|
|
95
|
+
throw new Error("Cursor does not support structured output");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAdapterDeps = {}): ProviderAdapter {
|
|
81
100
|
return {
|
|
82
101
|
name: "cursor",
|
|
83
102
|
|
|
103
|
+
validateRequest: assertCursorRequestSupported,
|
|
104
|
+
|
|
84
105
|
buildRequest() {
|
|
85
106
|
return {
|
|
86
107
|
url: provider.baseUrl || CURSOR_API_URL,
|
|
@@ -98,6 +119,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
98
119
|
},
|
|
99
120
|
|
|
100
121
|
async runTurn(_parsed, incoming, emit) {
|
|
122
|
+
assertCursorRequestSupported(_parsed);
|
|
101
123
|
if (incoming.abortSignal?.aborted) {
|
|
102
124
|
emit({ type: "error", message: "Cursor turn was aborted before start." });
|
|
103
125
|
return;
|
|
@@ -243,39 +265,79 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
243
265
|
);
|
|
244
266
|
};
|
|
245
267
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// resumes, local exec/MCP side effects, and already-emitted output fail closed.
|
|
252
|
-
if (
|
|
253
|
-
!isCursorInvalidArgumentError(err)
|
|
254
|
-
|| !isCursorExternalWireModel(request.modelId)
|
|
255
|
-
|| lastRawIsToolResult
|
|
256
|
-
|| emittedOutput
|
|
257
|
-
|| replayUnsafe
|
|
258
|
-
|| incoming.abortSignal?.aborted
|
|
259
|
-
) {
|
|
260
|
-
throw err;
|
|
261
|
-
}
|
|
262
|
-
const failedConversationId = request.conversationId;
|
|
268
|
+
const overflowRemintBaseId = _parsed._clientThreadId
|
|
269
|
+
? undefined
|
|
270
|
+
: (previousConversationId ?? _parsed._cursorConversationId);
|
|
271
|
+
|
|
272
|
+
const remintConversationId = (failedConversationId: string) => {
|
|
263
273
|
lastTransport = undefined;
|
|
264
274
|
_parsed._cursorConversationId = undefined;
|
|
265
|
-
|
|
266
|
-
rekeyContextUsage(failedConversationId,
|
|
267
|
-
_parsed._cursorConversationId =
|
|
268
|
-
// Persist recovery for store:false clients that only send a parent thread id, so the
|
|
269
|
-
// next turn does not recompute the stale deterministic thread hash. Isolated helper /
|
|
270
|
-
// compaction turns must not park their throwaway id under the parent thread key.
|
|
275
|
+
const next = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
276
|
+
rekeyContextUsage(failedConversationId, next.conversationId);
|
|
277
|
+
_parsed._cursorConversationId = next.conversationId;
|
|
271
278
|
if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
|
|
272
279
|
rememberCursorThreadConversation(
|
|
273
280
|
_parsed._clientThreadId,
|
|
274
|
-
|
|
281
|
+
next.conversationId,
|
|
275
282
|
_parsed._cursorIdentityScope,
|
|
276
283
|
);
|
|
277
284
|
}
|
|
278
|
-
|
|
285
|
+
return next;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
for (;;) {
|
|
289
|
+
try {
|
|
290
|
+
await runOnce(request);
|
|
291
|
+
break;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const overflowRemintSafe =
|
|
294
|
+
!lastRawIsToolResult
|
|
295
|
+
&& !emittedOutput
|
|
296
|
+
&& !replayUnsafe
|
|
297
|
+
&& !incoming.abortSignal?.aborted;
|
|
298
|
+
const overflowScopeKey = cursorOverflowRemintScopeKey(
|
|
299
|
+
_parsed,
|
|
300
|
+
overflowRemintBaseId ?? request.conversationId,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
overflowScopeKey
|
|
305
|
+
&& overflowRemintSafe
|
|
306
|
+
&& isCursorOverflowRemintCandidate(err, requestSizeContext)
|
|
307
|
+
) {
|
|
308
|
+
if (shouldSkipCursorOverflowRemint(overflowScopeKey)) {
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
if (shouldSurfaceCursorOverflowFirst(overflowScopeKey)) {
|
|
312
|
+
markCursorOverflowSurfaced(overflowScopeKey);
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
if (!recordCursorOverflowRemint(overflowScopeKey)) {
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
const failedConversationId = request.conversationId;
|
|
319
|
+
if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef);
|
|
320
|
+
request = remintConversationId(failedConversationId);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// One-shot fallback for external-model Connect invalid_argument before any
|
|
325
|
+
// non-heartbeat output. Retries apply only to safe plain-user turns; tool-result
|
|
326
|
+
// resumes, local exec/MCP side effects, and already-emitted output fail closed.
|
|
327
|
+
if (
|
|
328
|
+
!isCursorInvalidArgumentError(err)
|
|
329
|
+
|| !isCursorExternalWireModel(request.modelId)
|
|
330
|
+
|| lastRawIsToolResult
|
|
331
|
+
|| emittedOutput
|
|
332
|
+
|| replayUnsafe
|
|
333
|
+
|| incoming.abortSignal?.aborted
|
|
334
|
+
) {
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
request = remintConversationId(request.conversationId);
|
|
338
|
+
await runOnce(request);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
279
341
|
}
|
|
280
342
|
if (
|
|
281
343
|
request.checkpointInvalidationReason
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
retryableGoogleStatus,
|
|
6
6
|
safeGoogleHttpErrorMessage,
|
|
7
7
|
} from "./google-errors";
|
|
8
|
-
import { repairGoogleInvalidRequestBody } from "./google-wire-compiler";
|
|
8
|
+
import { isGoogleMixedBuiltinToolError, repairGoogleInvalidRequestBody, stripGoogleBuiltinToolsFromWireBody } from "./google-wire-compiler";
|
|
9
9
|
import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
|
|
10
10
|
import { recordAntigravityCooldown } from "../oauth/antigravity-routing";
|
|
11
11
|
import {
|
|
@@ -378,7 +378,17 @@ async function fetchGoogleWithRetryInternal(
|
|
|
378
378
|
} catch (error) {
|
|
379
379
|
if (ctx.abortSignal?.aborted) throw error;
|
|
380
380
|
}
|
|
381
|
-
|
|
381
|
+
// Mixed built-in + function tools must win over schema repair: those 400s often mention
|
|
382
|
+
// function_declarations, which would otherwise empty parameters and leave google_search attached.
|
|
383
|
+
let repairedBody: string | undefined;
|
|
384
|
+
if (isGoogleMixedBuiltinToolError(payloadText)) {
|
|
385
|
+
repairedBody = stripGoogleBuiltinToolsFromWireBody(activeRequest.body);
|
|
386
|
+
if (repairedBody === undefined) {
|
|
387
|
+
repairedBody = repairGoogleInvalidRequestBody(activeRequest.body, payloadText);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
repairedBody = repairGoogleInvalidRequestBody(activeRequest.body, payloadText);
|
|
391
|
+
}
|
|
382
392
|
if (repairedBody !== undefined) {
|
|
383
393
|
compatibilityReplayUsed = true;
|
|
384
394
|
activeRequest = { ...activeRequest, body: repairedBody };
|
|
@@ -94,10 +94,35 @@ function compileContents(value: unknown, toWireName: (name: string) => string):
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const GOOGLE_BUILTIN_TOOL_KEYS = new Set([
|
|
98
|
+
"googleSearch", "google_search",
|
|
99
|
+
"urlContext", "url_context",
|
|
100
|
+
"codeExecution", "code_execution",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
function isGoogleBuiltinToolObject(rawTool: unknown): boolean {
|
|
104
|
+
if (!isObject(rawTool)) return false;
|
|
105
|
+
const keys = Object.keys(rawTool);
|
|
106
|
+
return keys.length > 0 && keys.every(key => GOOGLE_BUILTIN_TOOL_KEYS.has(key));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function passthroughGoogleBuiltinTools(rawTool: unknown): unknown[] {
|
|
110
|
+
if (!isObject(rawTool) || Array.isArray(rawTool.functionDeclarations)) return [];
|
|
111
|
+
if (!isGoogleBuiltinToolObject(rawTool)) return [];
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
for (const key of Object.keys(rawTool)) {
|
|
114
|
+
if (key === "googleSearch" || key === "google_search") out.google_search = {};
|
|
115
|
+
else if (key === "urlContext" || key === "url_context") out.url_context = {};
|
|
116
|
+
else if (key === "codeExecution" || key === "code_execution") out.code_execution = rawTool[key] ?? {};
|
|
117
|
+
}
|
|
118
|
+
return Object.keys(out).length > 0 ? [out] : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
function compileTools(value: unknown, toWireName: (name: string) => string): unknown[] | undefined {
|
|
98
122
|
if (!Array.isArray(value)) return undefined;
|
|
99
123
|
const tools = value.flatMap(rawTool => {
|
|
100
|
-
|
|
124
|
+
const builtins = passthroughGoogleBuiltinTools(rawTool);
|
|
125
|
+
if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return builtins;
|
|
101
126
|
const functionDeclarations = rawTool.functionDeclarations.flatMap(rawDeclaration => {
|
|
102
127
|
if (!isObject(rawDeclaration) || typeof rawDeclaration.name !== "string") return [];
|
|
103
128
|
return [{
|
|
@@ -106,7 +131,10 @@ function compileTools(value: unknown, toWireName: (name: string) => string): unk
|
|
|
106
131
|
parameters: sanitizeGeminiToolParameters(rawDeclaration.parameters),
|
|
107
132
|
}];
|
|
108
133
|
});
|
|
109
|
-
return
|
|
134
|
+
return [
|
|
135
|
+
...builtins,
|
|
136
|
+
...(functionDeclarations.length > 0 ? [{ functionDeclarations }] : []),
|
|
137
|
+
];
|
|
110
138
|
});
|
|
111
139
|
return tools.length > 0 ? tools : undefined;
|
|
112
140
|
}
|
|
@@ -141,6 +169,14 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
|
|
|
141
169
|
const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
|
|
142
170
|
if (valid.length > 0) out.responseModalities = valid;
|
|
143
171
|
}
|
|
172
|
+
if (value.responseMimeType === "application/json") out.responseMimeType = value.responseMimeType;
|
|
173
|
+
if (isObject(value.responseSchema)) {
|
|
174
|
+
const responseSchema = sanitizeGeminiToolParameters(value.responseSchema);
|
|
175
|
+
const properties = isObject(responseSchema.properties) ? responseSchema.properties : undefined;
|
|
176
|
+
const hasProperties = properties !== undefined && Object.keys(properties).length > 0;
|
|
177
|
+
const hasRequired = Array.isArray(responseSchema.required) && responseSchema.required.length > 0;
|
|
178
|
+
if (hasProperties || hasRequired) out.responseSchema = responseSchema;
|
|
179
|
+
}
|
|
144
180
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
145
181
|
}
|
|
146
182
|
|
|
@@ -230,3 +266,56 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
|
|
|
230
266
|
}
|
|
231
267
|
return changed ? JSON.stringify(parsed) : undefined;
|
|
232
268
|
}
|
|
269
|
+
|
|
270
|
+
function stripBuiltinToolsFromRoot(root: JsonObject): boolean {
|
|
271
|
+
if (!Array.isArray(root.tools)) return false;
|
|
272
|
+
const before = root.tools.length;
|
|
273
|
+
const filtered = root.tools.filter(rawTool => !isGoogleBuiltinToolObject(rawTool));
|
|
274
|
+
root.tools = filtered;
|
|
275
|
+
return filtered.length !== before;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** One mixed-tool 400 replay: drop known built-in siblings and keep functionDeclarations. */
|
|
279
|
+
export function stripGoogleBuiltinToolsFromWireBody(body: string): string | undefined {
|
|
280
|
+
let parsed: unknown;
|
|
281
|
+
try {
|
|
282
|
+
parsed = JSON.parse(body) as unknown;
|
|
283
|
+
} catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
if (!isObject(parsed)) return undefined;
|
|
287
|
+
const root = isObject(parsed.request) ? parsed.request : parsed;
|
|
288
|
+
if (!stripBuiltinToolsFromRoot(root)) return undefined;
|
|
289
|
+
return JSON.stringify(parsed);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function isGoogleMixedBuiltinToolError(errorPayload: string): boolean {
|
|
293
|
+
const mentionsBuiltin = /\b(?:google[_ ]?search|url[_ ]?context|code[_ ]?execution|built[- ]?in(?:\s+tools?)?)\b/i.test(errorPayload);
|
|
294
|
+
if (!mentionsBuiltin) return false;
|
|
295
|
+
|
|
296
|
+
// A mixed-tool error names a builtin AND a function/tool term AND a coexistence verb.
|
|
297
|
+
// Builtin mention is gated above; each pattern below requires a coexistence verb to
|
|
298
|
+
// appear within a bounded window of a tool/function/declaration term (or the phrase
|
|
299
|
+
// itself implies coexistence, e.g. "mutually exclusive"). Bare "coexist"/"alongside"
|
|
300
|
+
// without a nearby tool/function term is NOT enough — a schema error can say fields
|
|
301
|
+
// "coexist" while merely mentioning a builtin.
|
|
302
|
+
const describesIncompatibleCoexistence = [
|
|
303
|
+
/\bmix\w*\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
304
|
+
/\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bmix\w*\b/i,
|
|
305
|
+
/\bcombin\w*\b[^\n.!?]{0,80}\bwith\b/i,
|
|
306
|
+
/\bcoexist\w*\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
307
|
+
/\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bcoexist\w*\b/i,
|
|
308
|
+
/\balongside\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
309
|
+
/\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\balongside\b/i,
|
|
310
|
+
/\buse\w*\b[^\n.!?]{0,80}\btogether\b/i,
|
|
311
|
+
/\btogether\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
312
|
+
/\buse\w*\b[^\n.!?]{0,80}\bwith\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
313
|
+
/\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bwith\b[^\n.!?]{0,80}\buse\w*\b/i,
|
|
314
|
+
/\bnot supported with\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
|
|
315
|
+
/\bmutually exclusive\b/i,
|
|
316
|
+
/\bincompatib\w*\b[^\n.!?]{0,80}\b(?:coexist\w*|combin\w*|alongside|used together|mutually exclusive)\b/i,
|
|
317
|
+
/\b(?:coexist\w*|combin\w*|alongside|used together|mutually exclusive)\b[^\n.!?]{0,80}\bincompatib\w*\b/i,
|
|
318
|
+
].some(pattern => pattern.test(errorPayload));
|
|
319
|
+
|
|
320
|
+
return describesIncompatibleCoexistence;
|
|
321
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -24,9 +24,16 @@ import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignat
|
|
|
24
24
|
import { repairGoogleToolPairs, stripTrailingClaudePrefill } from "./google-antigravity-tools";
|
|
25
25
|
import { canonicalAntigravityHttpsHost, isAntigravityHttpsHost } from "./google-antigravity-hosts";
|
|
26
26
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
27
|
+
import { sanitizeGeminiToolParameters } from "./google-tool-schema";
|
|
27
28
|
import { identifyRoutedModel } from "./identity";
|
|
28
29
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
29
30
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
31
|
+
import {
|
|
32
|
+
extractCcaGroundingSources,
|
|
33
|
+
formatCcaGroundingSourcesAppendix,
|
|
34
|
+
isCcaSearchSuggestionHtml,
|
|
35
|
+
} from "../web-search/gemini-executor";
|
|
36
|
+
import type { WebSearchSource } from "../web-search/parse";
|
|
30
37
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
31
38
|
import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
32
39
|
import {
|
|
@@ -334,22 +341,29 @@ function messagesToGeminiFormat(
|
|
|
334
341
|
return { systemInstruction, contents };
|
|
335
342
|
}
|
|
336
343
|
|
|
337
|
-
function toolsToGeminiFormat(
|
|
338
|
-
|
|
344
|
+
function toolsToGeminiFormat(
|
|
345
|
+
parsed: OcxParsedRequest,
|
|
346
|
+
wireModelId: string,
|
|
347
|
+
): unknown[] | undefined {
|
|
348
|
+
const grounding = parsed._ccaInTurnGrounding;
|
|
339
349
|
const allowed = isAllowedToolChoice(parsed.options.toolChoice)
|
|
340
350
|
? new Set(parsed.options.toolChoice.allowedTools)
|
|
341
351
|
: undefined;
|
|
342
352
|
const tools = allowed
|
|
343
|
-
? parsed.context.tools
|
|
344
|
-
: parsed.context.tools;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
+
? parsed.context.tools?.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools)) ?? []
|
|
354
|
+
: parsed.context.tools ?? [];
|
|
355
|
+
const functionDeclarations = tools.map(t => ({
|
|
356
|
+
name: namespacedToolName(t.namespace, t.name),
|
|
357
|
+
description: t.description,
|
|
358
|
+
parameters: t.parameters,
|
|
359
|
+
}));
|
|
360
|
+
const wireTools: unknown[] = [];
|
|
361
|
+
if (functionDeclarations.length > 0) wireTools.push({ functionDeclarations });
|
|
362
|
+
if (grounding && !/claude/i.test(wireModelId)) {
|
|
363
|
+
if (grounding.search) wireTools.push({ google_search: {} });
|
|
364
|
+
if (grounding.urlContext) wireTools.push({ url_context: {} });
|
|
365
|
+
}
|
|
366
|
+
return wireTools.length > 0 ? wireTools : undefined;
|
|
353
367
|
}
|
|
354
368
|
|
|
355
369
|
/**
|
|
@@ -430,6 +444,30 @@ function isImageCapableModel(modelId: string): boolean {
|
|
|
430
444
|
return IMAGE_CAPABLE_MODELS.has(modelId);
|
|
431
445
|
}
|
|
432
446
|
|
|
447
|
+
function applyGeminiStructuredOutput(
|
|
448
|
+
generationConfig: Record<string, unknown>,
|
|
449
|
+
parsed: OcxParsedRequest,
|
|
450
|
+
wireModelId: string,
|
|
451
|
+
): void {
|
|
452
|
+
const textFormat = parsed.options.textFormat;
|
|
453
|
+
if (!textFormat) return;
|
|
454
|
+
if (Array.isArray(parsed.context.tools) && parsed.context.tools.length > 0) return;
|
|
455
|
+
if (/claude/i.test(wireModelId) || /claude/i.test(parsed.modelId)) return;
|
|
456
|
+
if (isImageCapableModel(parsed.modelId)) return;
|
|
457
|
+
|
|
458
|
+
generationConfig.responseMimeType = "application/json";
|
|
459
|
+
if (textFormat.type !== "json_schema" || textFormat.schema === undefined) return;
|
|
460
|
+
|
|
461
|
+
const sanitized = sanitizeGeminiToolParameters(textFormat.schema);
|
|
462
|
+
const props = sanitized.properties;
|
|
463
|
+
const propKeys = props !== null && typeof props === "object" && !Array.isArray(props)
|
|
464
|
+
? Object.keys(props)
|
|
465
|
+
: [];
|
|
466
|
+
const required = Array.isArray(sanitized.required) ? sanitized.required : [];
|
|
467
|
+
if (propKeys.length === 0 && required.length === 0) return;
|
|
468
|
+
generationConfig.responseSchema = sanitized;
|
|
469
|
+
}
|
|
470
|
+
|
|
433
471
|
/**
|
|
434
472
|
* Model-visible markdown link for a materialized artifact. Uses the authenticated
|
|
435
473
|
* opaque HTTP route so remote/container clients can fetch the image without host
|
|
@@ -484,11 +522,12 @@ function googleToolCallMetadataFromPart(
|
|
|
484
522
|
* Keep that provider visibility bit authoritative here so the streaming and buffered parsers
|
|
485
523
|
* cannot accidentally expose the same hidden reasoning through different event types.
|
|
486
524
|
*/
|
|
487
|
-
function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
|
|
525
|
+
function googlePartTextEvent(part: GoogleResponsePart, filterCcaSearchSuggestionHtml = false): AdapterEvent | undefined {
|
|
488
526
|
// A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
|
|
489
527
|
// only this optional field preserves the rest of the part without inventing assistant output by
|
|
490
528
|
// coercion; an empty string keeps its existing no-event behavior.
|
|
491
529
|
if (typeof part.text !== "string" || part.text.length === 0) return undefined;
|
|
530
|
+
if (filterCcaSearchSuggestionHtml && isCcaSearchSuggestionHtml(part.text)) return undefined;
|
|
492
531
|
return part.thought === true
|
|
493
532
|
? { type: "reasoning_raw_delta", text: part.text }
|
|
494
533
|
: { type: "text_delta", text: part.text };
|
|
@@ -644,6 +683,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
644
683
|
let vertexReplayModel: string | undefined;
|
|
645
684
|
let vertexReplaySession: string | undefined;
|
|
646
685
|
let restoreGoogleToolName = (name: string): string => name;
|
|
686
|
+
const emitInTurnGroundingSourcesQueue: boolean[] = [];
|
|
647
687
|
return {
|
|
648
688
|
name: "google",
|
|
649
689
|
|
|
@@ -677,7 +717,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
677
717
|
identityModelId,
|
|
678
718
|
provider.googleMode === "cloud-code-assist",
|
|
679
719
|
);
|
|
680
|
-
const tools = toolsToGeminiFormat(parsed);
|
|
720
|
+
const tools = toolsToGeminiFormat(parsed, routedModelId);
|
|
681
721
|
|
|
682
722
|
const body: Record<string, unknown> = { contents };
|
|
683
723
|
if (systemInstruction) body.systemInstruction = systemInstruction;
|
|
@@ -711,6 +751,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
711
751
|
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
|
|
712
752
|
generationConfig.responseModalities = ["TEXT", "IMAGE"];
|
|
713
753
|
}
|
|
754
|
+
applyGeminiStructuredOutput(generationConfig, parsed, routedModelId);
|
|
714
755
|
if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig;
|
|
715
756
|
|
|
716
757
|
const ccaAlwaysSse = provider.googleMode === "cloud-code-assist";
|
|
@@ -810,6 +851,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
810
851
|
if (/claude/i.test(wireModelId)) {
|
|
811
852
|
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
812
853
|
}
|
|
854
|
+
emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
|
|
813
855
|
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
814
856
|
}
|
|
815
857
|
|
|
@@ -835,6 +877,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
835
877
|
if (apiKey) {
|
|
836
878
|
const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
837
879
|
headers["x-goog-api-key"] = apiKey;
|
|
880
|
+
emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
|
|
838
881
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
839
882
|
}
|
|
840
883
|
const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
@@ -847,6 +890,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
847
890
|
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
848
891
|
const token = await getVertexAccessToken();
|
|
849
892
|
headers["Authorization"] = `Bearer ${token}`;
|
|
893
|
+
emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
|
|
850
894
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
851
895
|
}
|
|
852
896
|
|
|
@@ -858,10 +902,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
858
902
|
|
|
859
903
|
const compiled = compileGoogleWireBody(body);
|
|
860
904
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
905
|
+
emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
|
|
861
906
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
862
907
|
},
|
|
863
908
|
|
|
864
909
|
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
910
|
+
const emitInTurnGroundingSources = emitInTurnGroundingSourcesQueue.shift() ?? false;
|
|
911
|
+
const filterCcaSearchSuggestionHtml =
|
|
912
|
+
provider.googleMode === "cloud-code-assist" && emitInTurnGroundingSources;
|
|
865
913
|
if (!response.body) {
|
|
866
914
|
yield { type: "error", message: "No response body" };
|
|
867
915
|
return;
|
|
@@ -886,6 +934,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
886
934
|
let sawAnyFrame = false;
|
|
887
935
|
let sawTerminalSignal = false;
|
|
888
936
|
let pendingStreamThoughtSig: string | undefined;
|
|
937
|
+
const groundingSources: WebSearchSource[] = [];
|
|
938
|
+
let groundingSourcesEmitted = false;
|
|
939
|
+
|
|
940
|
+
const mergeGroundingSources = (groundingMetadata: unknown): void => {
|
|
941
|
+
for (const source of extractCcaGroundingSources(groundingMetadata)) {
|
|
942
|
+
if (!groundingSources.some(existing => existing.url === source.url)) groundingSources.push(source);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
889
945
|
|
|
890
946
|
const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
|
|
891
947
|
const payload = line.slice(5).trim();
|
|
@@ -980,8 +1036,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
980
1036
|
const candidate = rawCandidate as {
|
|
981
1037
|
content?: unknown;
|
|
982
1038
|
finishReason?: string;
|
|
1039
|
+
groundingMetadata?: unknown;
|
|
983
1040
|
};
|
|
984
1041
|
|
|
1042
|
+
if (emitInTurnGroundingSources && candidate.groundingMetadata) {
|
|
1043
|
+
mergeGroundingSources(candidate.groundingMetadata);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
985
1046
|
if (typeof candidate.finishReason === "string" && candidate.finishReason) {
|
|
986
1047
|
lastFinishReason = candidate.finishReason;
|
|
987
1048
|
sawTerminalSignal = true;
|
|
@@ -1029,7 +1090,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1029
1090
|
if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
|
|
1030
1091
|
pendingStreamThoughtSig = sig;
|
|
1031
1092
|
}
|
|
1032
|
-
const textEvent = googlePartTextEvent(part);
|
|
1093
|
+
const textEvent = googlePartTextEvent(part, filterCcaSearchSuggestionHtml);
|
|
1033
1094
|
if (textEvent) {
|
|
1034
1095
|
emittedContentEvent = true;
|
|
1035
1096
|
yield textEvent;
|
|
@@ -1140,6 +1201,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1140
1201
|
yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
|
|
1141
1202
|
return;
|
|
1142
1203
|
}
|
|
1204
|
+
if (emitInTurnGroundingSources && !groundingSourcesEmitted && groundingSources.length > 0) {
|
|
1205
|
+
const appendix = formatCcaGroundingSourcesAppendix(groundingSources);
|
|
1206
|
+
if (appendix) {
|
|
1207
|
+
groundingSourcesEmitted = true;
|
|
1208
|
+
yield { type: "text_delta", text: appendix };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1143
1211
|
const stopReason = lastFinishReason === "MAX_TOKENS"
|
|
1144
1212
|
? "max_tokens"
|
|
1145
1213
|
: ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
|
package/src/config.ts
CHANGED
|
@@ -735,6 +735,8 @@ const providerConfigSchema = z.object({
|
|
|
735
735
|
// accepted, persisted, and then silently resolved to the `code_mode_only` default — the
|
|
736
736
|
// operator asked for shell mode, got code mode, and was told nothing (#2106).
|
|
737
737
|
codexToolMode: z.enum(["code_mode_only", "shell"]).optional(),
|
|
738
|
+
// Validated rather than passed through: same rationale as codexToolMode above.
|
|
739
|
+
projectContext: z.enum(["off", "on"]).optional(),
|
|
738
740
|
responsesItemIdRepair: z.object({
|
|
739
741
|
message: z.array(z.string().min(1)).optional(),
|
|
740
742
|
reasoning: z.array(z.string().min(1)).optional(),
|