@yansigit/opencodex 2.32.0 → 2.33.1
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/README.md +2 -2
- package/gui/dist/assets/index-CIDo4y4k.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/adapters/anthropic.ts +20 -6
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +3 -1
- package/src/adapters/command-code.ts +131 -17
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-transport.ts +4 -2
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +13 -25
- package/src/adapters/cursor/native-exec.ts +18 -10
- package/src/adapters/cursor/protobuf-events.ts +186 -9
- package/src/adapters/cursor/protobuf-request.ts +53 -18
- package/src/adapters/cursor/request-builder.ts +11 -3
- package/src/adapters/cursor/tool-definitions.ts +49 -2
- package/src/adapters/cursor/tool-result-normalize.ts +21 -8
- package/src/adapters/cursor/types.ts +11 -3
- package/src/adapters/cursor.ts +123 -0
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +242 -28
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +112 -9
- package/src/chat/inbound.ts +15 -0
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/doctor.ts +2 -2
- package/src/cli/index.ts +10 -3
- package/src/cli/provider.ts +6 -0
- package/src/cli/status.ts +23 -0
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -12
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +39 -12
- package/src/codex/catalog/parsing.ts +38 -27
- package/src/codex/catalog/provider-fetch.ts +232 -133
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/convergence.ts +5 -0
- package/src/codex/shim.ts +56 -3
- package/src/config/provider-validation.ts +37 -0
- package/src/config.ts +78 -2
- package/src/generated/compatibility-version.json +187 -119
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +37 -6
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/oauth/aistudio-native-daemon.ts +62 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/index.ts +15 -3
- package/src/oauth/key-providers.ts +8 -0
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/login-cli.ts +66 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +5 -1
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +148 -50
- package/src/providers/registry.ts +27 -4
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +11 -0
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +11 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/server/aistudio-ws-hub.ts +295 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +2 -0
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +94 -0
- package/src/server/management/agent-settings-routes.ts +205 -15
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +31 -5
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/oauth-account-routes.ts +38 -7
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/request-log-conversation.ts +12 -0
- package/src/server/request-log.ts +23 -1
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +30 -1
- package/src/server/responses/core.ts +363 -156
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +18 -5
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-undeclared-tool-guard.ts +90 -8
- package/src/server/ws-bridge.ts +2 -1
- package/src/service.ts +1 -1
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +18 -1
- package/src/types/request.ts +30 -0
- package/src/types/tools.ts +51 -0
- package/src/types.ts +6 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +83 -0
- package/src/usage/log.ts +2 -2
- package/src/usage/summary.ts +34 -12
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-BG43zwVe.js +0 -102
- package/gui/dist/assets/index-CiSI-jrP.css +0 -1
|
@@ -48,6 +48,24 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
|
48
48
|
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
49
49
|
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
50
50
|
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
51
|
+
sandbox_permissions: {
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: ["use_default", "require_escalated"],
|
|
54
|
+
description: "Per-command sandbox override. Defaults to use_default; use require_escalated for unsandboxed execution.",
|
|
55
|
+
},
|
|
56
|
+
justification: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "User-facing approval question for require_escalated; omit otherwise.",
|
|
59
|
+
},
|
|
60
|
+
prefix_rule: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: { type: "string" },
|
|
63
|
+
description: "Reusable approval prefix for cmd, only with sandbox_permissions: require_escalated; for example [\"git\", \"pull\"].",
|
|
64
|
+
},
|
|
65
|
+
login: {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
description: "True runs the shell with -l/-i semantics; false disables them. Defaults to true.",
|
|
68
|
+
},
|
|
51
69
|
},
|
|
52
70
|
required: ["cmd"],
|
|
53
71
|
additionalProperties: false,
|
|
@@ -94,6 +112,13 @@ export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
|
|
|
94
112
|
additionalProperties: false,
|
|
95
113
|
} as const;
|
|
96
114
|
|
|
115
|
+
/** Cursor requires freeform custom tools to advertise their body as one string input. */
|
|
116
|
+
export const CURSOR_FREEFORM_INPUT_SCHEMA = {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: { input: { type: "string" } },
|
|
119
|
+
required: ["input"],
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
97
122
|
/**
|
|
98
123
|
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
99
124
|
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
@@ -109,6 +134,10 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
|
|
|
109
134
|
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
110
135
|
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
111
136
|
max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
|
|
137
|
+
sandbox_permissions: { type: "string" },
|
|
138
|
+
justification: { type: "string" },
|
|
139
|
+
prefix_rule: { type: "array" },
|
|
140
|
+
login: { type: "boolean" },
|
|
112
141
|
},
|
|
113
142
|
required: ["command"],
|
|
114
143
|
} as const;
|
|
@@ -364,6 +393,7 @@ export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?
|
|
|
364
393
|
|
|
365
394
|
/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
|
|
366
395
|
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
396
|
+
if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
|
|
367
397
|
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
368
398
|
}
|
|
369
399
|
|
|
@@ -373,6 +403,7 @@ export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
|
373
403
|
* treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
|
|
374
404
|
*/
|
|
375
405
|
export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
|
|
406
|
+
if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
|
|
376
407
|
if (isBareCodexShellBridgeTool(tool)) {
|
|
377
408
|
return shellBridgeArgNormalizeSchema(tool);
|
|
378
409
|
}
|
|
@@ -651,11 +682,17 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
651
682
|
// Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
|
|
652
683
|
// model probes for a top-level shell tool that is not there.
|
|
653
684
|
codeMode
|
|
654
|
-
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
|
|
685
|
+
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. When commands require network access, file writes outside workspace, or fail due to sandbox/permission restrictions, pass \`sandbox_permissions: "require_escalated"\` and a clear \`justification: "..."\` to \`tools.exec_command\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Your tool list may display tools under a longer \`mcp_opencodex-responses_*\` name; call whichever your list shows. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
|
|
655
686
|
: undefined,
|
|
656
687
|
codeMode
|
|
657
688
|
? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
|
|
658
689
|
: undefined,
|
|
690
|
+
codeMode
|
|
691
|
+
? "To read, search, or inspect files or run CLI commands, call `await tools.exec_command({ cmd: '...' })` inside `exec`. Do not write Node.js scripts or `require('fs')`."
|
|
692
|
+
: undefined,
|
|
693
|
+
codeMode
|
|
694
|
+
? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
|
|
695
|
+
: undefined,
|
|
659
696
|
hasBareExec
|
|
660
697
|
? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
|
|
661
698
|
: undefined,
|
|
@@ -663,7 +700,13 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
663
700
|
? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
|
|
664
701
|
: undefined,
|
|
665
702
|
hasBareExec
|
|
666
|
-
? `
|
|
703
|
+
? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
|
|
704
|
+
: undefined,
|
|
705
|
+
hasBareExec
|
|
706
|
+
? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
|
|
707
|
+
: undefined,
|
|
708
|
+
hasBareExec
|
|
709
|
+
? 'When a command requires network access, file writes outside workspace, or fails due to sandbox restrictions, include `sandbox_permissions: "require_escalated"` and `justification: "..."`.'
|
|
667
710
|
: undefined,
|
|
668
711
|
hostShellNote,
|
|
669
712
|
"Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
|
|
@@ -675,6 +718,9 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
675
718
|
? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.`
|
|
676
719
|
: "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
677
720
|
: undefined,
|
|
721
|
+
hasApplyPatch
|
|
722
|
+
? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat <<EOF`, `sed -i`) is forbidden while apply_patch or the structured edit tools are advertised — use those edit tools so the change is reviewable. Shell output redirection is fine for logs/scratch pipes that are not the deliverable file."
|
|
723
|
+
: undefined,
|
|
678
724
|
hasBareExec
|
|
679
725
|
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
680
726
|
: undefined,
|
|
@@ -686,6 +732,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
686
732
|
? `Use ${discoveryTools} only for explicit discovery/resource tasks, not generic tool-count demos.`
|
|
687
733
|
: undefined,
|
|
688
734
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
735
|
+
"When pursuing a multi-step task, check, or verification, do not stop or narrate intended future actions in plain text; immediately call the tool to execute the next step until the task is complete.",
|
|
689
736
|
hasBareExec
|
|
690
737
|
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
|
|
691
738
|
: undefined,
|
|
@@ -44,8 +44,24 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
|
|
|
44
44
|
guidance: "The node_repl session keeps earlier declarations; rename the variable or use var/reassignment instead of redeclaring.",
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
marker: "unsupported import in exec",
|
|
48
|
+
guidance: "Imports are not available in this exec context; use the injected globals instead.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
marker: "require is not defined",
|
|
52
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
marker: "fs is not defined",
|
|
56
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
marker: "process is not defined",
|
|
60
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
marker: "module is not defined",
|
|
64
|
+
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
49
65
|
},
|
|
50
66
|
];
|
|
51
67
|
|
|
@@ -80,13 +96,10 @@ export function normalizeCursorToolResultText(
|
|
|
80
96
|
changed: true,
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
87
|
-
}
|
|
99
|
+
for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
|
|
100
|
+
if (text.includes(marker) && !text.includes("[recovery:") && !text.includes(guidance)) {
|
|
101
|
+
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
return { text, isError, changed: false };
|
|
91
105
|
}
|
|
92
|
-
|
|
@@ -15,6 +15,13 @@ export interface CursorRunRequest {
|
|
|
15
15
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
16
16
|
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|
|
17
17
|
routingLevel?: CursorRoutingLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
|
|
20
|
+
* When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
|
|
21
|
+
* the userMessageAction text instead of the standard continuation text; rawMessages stay
|
|
22
|
+
* untouched so history replay is unchanged.
|
|
23
|
+
*/
|
|
24
|
+
echoRetryContinuationText?: string;
|
|
18
25
|
conversationId: string;
|
|
19
26
|
system: string[];
|
|
20
27
|
messages: CursorRequestMessage[];
|
|
@@ -25,9 +32,10 @@ export interface CursorRunRequest {
|
|
|
25
32
|
* hydration). History stays text-only. data: URLs only in this slice.
|
|
26
33
|
*/
|
|
27
34
|
selectedImages?: readonly ResolvedCursorImage[];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
tools?: OcxTool[];
|
|
36
|
+
toolChoice?: OcxRequestOptions["toolChoice"];
|
|
37
|
+
textFormat?: OcxRequestOptions["textFormat"];
|
|
38
|
+
parallelToolCalls?: boolean;
|
|
31
39
|
/**
|
|
32
40
|
* Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
|
|
33
41
|
* newly observed compacted context epoch, so pre-compaction totals are not over-reported while
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -41,6 +41,15 @@ import {
|
|
|
41
41
|
shouldSurfaceCursorOverflowFirst,
|
|
42
42
|
} from "./cursor/thread-continuity";
|
|
43
43
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
44
|
+
import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
|
|
45
|
+
import {
|
|
46
|
+
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
|
|
47
|
+
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
|
|
48
|
+
CursorEnvelopeEchoSniffer,
|
|
49
|
+
CursorRoutingCommentaryError,
|
|
50
|
+
CursorRoutingCommentarySniffer,
|
|
51
|
+
CursorToolResultEchoError,
|
|
52
|
+
} from "./cursor/envelope-echo";
|
|
44
53
|
import {
|
|
45
54
|
createDisabledCursorTransport,
|
|
46
55
|
CursorTransportDisabledError,
|
|
@@ -208,6 +217,38 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
208
217
|
};
|
|
209
218
|
|
|
210
219
|
const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
|
|
220
|
+
const effort = _parsed.options.reasoning;
|
|
221
|
+
const isHeavyReasoning = effort === "high"
|
|
222
|
+
|| effort === "max"
|
|
223
|
+
|| effort === "xhigh"
|
|
224
|
+
|| activeRequest.modelId.includes("grok-4.6")
|
|
225
|
+
|| activeRequest.modelId.includes("kimi-k3")
|
|
226
|
+
|| activeRequest.modelId.includes("opus-4-8");
|
|
227
|
+
const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
|
|
228
|
+
const armEchoSniffer =
|
|
229
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
230
|
+
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
231
|
+
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
232
|
+
const armRoutingCommentarySniffer =
|
|
233
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
234
|
+
&& (
|
|
235
|
+
cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
|
|
236
|
+
|| cursorRequestHasShellAlias(activeRequest.tools)
|
|
237
|
+
);
|
|
238
|
+
const routingCommentarySniffer = armRoutingCommentarySniffer
|
|
239
|
+
? new CursorRoutingCommentarySniffer()
|
|
240
|
+
: undefined;
|
|
241
|
+
let guardHeld: AdapterEvent[] = [];
|
|
242
|
+
const releaseGuardHeld = () => {
|
|
243
|
+
for (const held of guardHeld) {
|
|
244
|
+
if (held.type !== "heartbeat") emittedOutput = true;
|
|
245
|
+
emit(held);
|
|
246
|
+
}
|
|
247
|
+
guardHeld = [];
|
|
248
|
+
};
|
|
249
|
+
const guardsSettled = () =>
|
|
250
|
+
(!echoSniffer || echoSniffer.settled)
|
|
251
|
+
&& (!routingCommentarySniffer || routingCommentarySniffer.settled);
|
|
211
252
|
await runCursorTurnWithRetry(
|
|
212
253
|
makeTransport,
|
|
213
254
|
{
|
|
@@ -216,6 +257,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
216
257
|
translatorBudget: incoming.translatorBudget,
|
|
217
258
|
requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
|
|
218
259
|
sessionId: activeRequest.conversationId,
|
|
260
|
+
streamHeartbeatOnlyFailMs: heartbeatOnlyMs,
|
|
219
261
|
...(incoming.providerFetch ? { fetch: incoming.providerFetch } : {}),
|
|
220
262
|
},
|
|
221
263
|
activeRequest,
|
|
@@ -237,6 +279,43 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
237
279
|
},
|
|
238
280
|
});
|
|
239
281
|
for (const event of events) {
|
|
282
|
+
if (!guardsSettled()) {
|
|
283
|
+
if (event.type === "text_delta") {
|
|
284
|
+
guardHeld.push(event);
|
|
285
|
+
if (echoSniffer && !echoSniffer.settled) {
|
|
286
|
+
const decision = echoSniffer.feed(event.text);
|
|
287
|
+
if (decision.kind === "echo") {
|
|
288
|
+
guardHeld = [];
|
|
289
|
+
throw new CursorToolResultEchoError(decision.marker);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
|
|
293
|
+
const decision = routingCommentarySniffer.feed(event.text);
|
|
294
|
+
if (decision.kind === "hallucination") {
|
|
295
|
+
guardHeld = [];
|
|
296
|
+
throw new CursorRoutingCommentaryError();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (guardsSettled()) releaseGuardHeld();
|
|
300
|
+
continue;
|
|
301
|
+
} else if (event.type === "thinking_delta" || event.type === "heartbeat") {
|
|
302
|
+
// Reasoning before first text stays ordered; liveness still passes through.
|
|
303
|
+
if (event.type === "thinking_delta") {
|
|
304
|
+
guardHeld.push(event);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// A tool call, done, or error closes the text quarantine. Re-check the full
|
|
309
|
+
// held first line before release so a fragmented routing claim cannot leak.
|
|
310
|
+
echoSniffer?.finish();
|
|
311
|
+
const routingDecision = routingCommentarySniffer?.finish();
|
|
312
|
+
if (routingDecision?.kind === "hallucination") {
|
|
313
|
+
guardHeld = [];
|
|
314
|
+
throw new CursorRoutingCommentaryError();
|
|
315
|
+
}
|
|
316
|
+
releaseGuardHeld();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
240
319
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
241
320
|
if (event.type === "done") {
|
|
242
321
|
commitCapturedCheckpoint(activeRequest);
|
|
@@ -290,6 +369,50 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
290
369
|
await runOnce(request);
|
|
291
370
|
break;
|
|
292
371
|
} catch (err) {
|
|
372
|
+
const outputGuardRetryText =
|
|
373
|
+
err instanceof CursorToolResultEchoError
|
|
374
|
+
? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
|
|
375
|
+
: err instanceof CursorRoutingCommentaryError
|
|
376
|
+
? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
|
|
377
|
+
: undefined;
|
|
378
|
+
// One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
|
|
379
|
+
// The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
|
|
380
|
+
// retry is safe. A second rejection propagates as an error rather than looping.
|
|
381
|
+
if (
|
|
382
|
+
outputGuardRetryText
|
|
383
|
+
&& !emittedOutput
|
|
384
|
+
&& !replayUnsafe
|
|
385
|
+
&& !incoming.abortSignal?.aborted
|
|
386
|
+
) {
|
|
387
|
+
debugProviderDiagnostic(
|
|
388
|
+
"cursor",
|
|
389
|
+
err instanceof CursorToolResultEchoError
|
|
390
|
+
? "envelope-echo-retry"
|
|
391
|
+
: "routing-commentary-retry",
|
|
392
|
+
{
|
|
393
|
+
wireModel: request.modelId,
|
|
394
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
const echoedConversationId = request.conversationId;
|
|
398
|
+
lastTransport = undefined;
|
|
399
|
+
_parsed._cursorConversationId = undefined;
|
|
400
|
+
request = {
|
|
401
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
402
|
+
echoRetryContinuationText: outputGuardRetryText,
|
|
403
|
+
};
|
|
404
|
+
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
405
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
406
|
+
if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
|
|
407
|
+
rememberCursorThreadConversation(
|
|
408
|
+
_parsed._clientThreadId,
|
|
409
|
+
request.conversationId,
|
|
410
|
+
_parsed._cursorIdentityScope,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
await runOnce(request);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
293
416
|
const overflowRemintSafe =
|
|
294
417
|
!lastRawIsToolResult
|
|
295
418
|
&& !emittedOutput
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient streaming chunk parser for Google AI Studio MakerSuite private RPC protocol.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MakerSuiteParsedResult {
|
|
6
|
+
text: string;
|
|
7
|
+
thought?: string;
|
|
8
|
+
thoughtSignature?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseMakerSuiteChunk(raw: string): MakerSuiteParsedResult {
|
|
12
|
+
const result: MakerSuiteParsedResult = { text: "" };
|
|
13
|
+
if (!raw || typeof raw !== "string") return result;
|
|
14
|
+
|
|
15
|
+
// 1. Extract regular model response text: [null, "escaped_text"]
|
|
16
|
+
const textMatches = raw.matchAll(/\[\s*null\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
17
|
+
for (const m of textMatches) {
|
|
18
|
+
if (!m[1]) continue;
|
|
19
|
+
try {
|
|
20
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
21
|
+
if (decoded) result.text += decoded;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
void err;
|
|
24
|
+
result.text += m[1];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Extract model reasoning / thoughts: [true, "escaped_thought"]
|
|
29
|
+
const thoughtMatches = raw.matchAll(/\[\s*true\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
30
|
+
for (const m of thoughtMatches) {
|
|
31
|
+
if (!m[1]) continue;
|
|
32
|
+
try {
|
|
33
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
34
|
+
if (decoded) result.thought = (result.thought ?? "") + decoded;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
void err;
|
|
37
|
+
result.thought = (result.thought ?? "") + m[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Extract Gemini thought signature token if present
|
|
42
|
+
const sigMatch = raw.match(/"(ErQ[A-Za-z0-9+/=_-]{30,})"/);
|
|
43
|
+
if (sigMatch?.[1]) {
|
|
44
|
+
result.thoughtSignature = sigMatch[1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -38,9 +38,9 @@ const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
|
38
38
|
export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
|
|
39
39
|
const REPLAY_EVICT_BATCH = 128;
|
|
40
40
|
const REPLAY_MAX_CALLS_PER_SESSION = 256;
|
|
41
|
-
export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION =
|
|
41
|
+
export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 8 * 1024 * 1024;
|
|
42
42
|
export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
43
|
-
const REPLAY_MAX_SIGNATURE_BYTES =
|
|
43
|
+
const REPLAY_MAX_SIGNATURE_BYTES = 1024 * 1024;
|
|
44
44
|
/** Fixed 64-hex outer key length, counted once per session entry. */
|
|
45
45
|
const REPLAY_SESSION_KEY_BYTES = 64;
|
|
46
46
|
const REPLAY_SNAPSHOT_FILE = "antigravity-replay.json";
|
|
@@ -11,6 +11,13 @@ import { antigravityUserAgent } from "./client-fingerprint";
|
|
|
11
11
|
*/
|
|
12
12
|
export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Bypass sentinel for Google Cloud Code Assist (Antigravity).
|
|
16
|
+
* When a historical functionCall part lacks a thought_signature, Antigravity rejects
|
|
17
|
+
* the request with HTTP 400 unless this sentinel is provided.
|
|
18
|
+
*/
|
|
19
|
+
export const ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL = "skip_thought_signature_validator";
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
|
|
16
23
|
* foreign id that must not be forwarded to Gemini/Antigravity.
|
|
@@ -2,7 +2,10 @@ import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErro
|
|
|
2
2
|
|
|
3
3
|
/** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
|
|
4
4
|
function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
|
|
5
|
-
|
|
5
|
+
let trimmed = payloadText.trim();
|
|
6
|
+
if (trimmed.startsWith("data:")) {
|
|
7
|
+
trimmed = trimmed.replace(/^data:\s*/, "").trim();
|
|
8
|
+
}
|
|
6
9
|
if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
|
|
7
10
|
return { message: trimmed || undefined };
|
|
8
11
|
}
|
|
@@ -18,7 +21,8 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
|
|
|
18
21
|
const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";
|
|
19
22
|
|
|
20
23
|
export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
|
|
21
|
-
|
|
24
|
+
const lower = payloadText.toLowerCase();
|
|
25
|
+
return lower.includes(ANTIGRAVITY_GEO_BLOCKED_MARKER) || lower.includes("location is not supported");
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./google-errors";
|
|
8
8
|
import { isGoogleMixedBuiltinToolError, repairGoogleInvalidRequestBody, stripGoogleBuiltinToolsFromWireBody } from "./google-wire-compiler";
|
|
9
9
|
import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
|
|
10
|
-
import { recordAntigravityCooldown } from "../oauth/antigravity-routing";
|
|
10
|
+
import { recordAntigravityCooldown, recordAntigravitySyntheticFailure } from "../oauth/antigravity-routing";
|
|
11
11
|
import {
|
|
12
12
|
antigravityHostCandidates,
|
|
13
13
|
canonicalAntigravityHttpsHost,
|
|
@@ -178,6 +178,7 @@ function responseWithBufferedBody(
|
|
|
178
178
|
async function prepareCcaSseResponse(
|
|
179
179
|
response: Response,
|
|
180
180
|
fetchPeer: (() => Promise<Response>) | undefined,
|
|
181
|
+
accountId?: string,
|
|
181
182
|
): Promise<Response> {
|
|
182
183
|
if (!response.body) return fetchPeer ? fetchPeer() : response;
|
|
183
184
|
const reader = response.body.getReader();
|
|
@@ -209,6 +210,13 @@ async function prepareCcaSseResponse(
|
|
|
209
210
|
return failoverOrPassthrough();
|
|
210
211
|
}
|
|
211
212
|
if (probe === "quota_exhausted" || probe === "geo_blocked") {
|
|
213
|
+
if (accountId) {
|
|
214
|
+
recordAntigravitySyntheticFailure(accountId, {
|
|
215
|
+
code: probe === "quota_exhausted" ? 429 : 403,
|
|
216
|
+
status: probe === "quota_exhausted" ? "RESOURCE_EXHAUSTED" : "PERMISSION_DENIED",
|
|
217
|
+
message: probe === "quota_exhausted" ? "quota exceeded" : "user location is not supported",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
212
220
|
const status = probe === "quota_exhausted" ? 429 : 403;
|
|
213
221
|
return passthrough(undefined, status);
|
|
214
222
|
}
|
|
@@ -237,6 +245,13 @@ async function prepareCcaSseResponse(
|
|
|
237
245
|
return failoverOrPassthrough();
|
|
238
246
|
}
|
|
239
247
|
if (probe === "quota_exhausted" || probe === "geo_blocked") {
|
|
248
|
+
if (accountId) {
|
|
249
|
+
recordAntigravitySyntheticFailure(accountId, {
|
|
250
|
+
code: probe === "quota_exhausted" ? 429 : 403,
|
|
251
|
+
status: probe === "quota_exhausted" ? "RESOURCE_EXHAUSTED" : "PERMISSION_DENIED",
|
|
252
|
+
message: probe === "quota_exhausted" ? "quota exceeded" : "user location is not supported",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
240
255
|
const status = probe === "quota_exhausted" ? 429 : 403;
|
|
241
256
|
return passthrough(overflow, status);
|
|
242
257
|
}
|
|
@@ -262,6 +277,8 @@ function isUnavailableResponse(response: Response): boolean {
|
|
|
262
277
|
export interface GoogleRetryOptions {
|
|
263
278
|
/** Repair-and-replay structurally invalid 400 bodies (Vertex/Antigravity behavior). */
|
|
264
279
|
repairInvalid400?: boolean;
|
|
280
|
+
/** Let the Responses layer own Antigravity's single same-account 429 replay. */
|
|
281
|
+
retry429?: boolean;
|
|
265
282
|
}
|
|
266
283
|
|
|
267
284
|
async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
|
|
@@ -291,13 +308,15 @@ async function recordAntigravityHttpCooldown(
|
|
|
291
308
|
if (response.status === 429) {
|
|
292
309
|
recordAntigravityCooldown(
|
|
293
310
|
accountId,
|
|
294
|
-
|
|
295
|
-
|
|
311
|
+
response.headers.get("retry-after"),
|
|
312
|
+
Date.now(),
|
|
313
|
+
isQuotaExhaustedBody(payloadText) ? "quota" : "rate-limit",
|
|
314
|
+
"synthetic",
|
|
296
315
|
);
|
|
297
316
|
return true;
|
|
298
317
|
}
|
|
299
|
-
if (isAntigravityGeoBlockedBody(payloadText)) {
|
|
300
|
-
recordAntigravityCooldown(accountId, "
|
|
318
|
+
if (response.status === 403 || isAntigravityGeoBlockedBody(payloadText)) {
|
|
319
|
+
recordAntigravityCooldown(accountId, response.headers.get("retry-after"), Date.now(), "geoblock", "synthetic");
|
|
301
320
|
return true;
|
|
302
321
|
}
|
|
303
322
|
return false;
|
|
@@ -318,6 +337,7 @@ async function fetchGoogleWithRetryInternal(
|
|
|
318
337
|
opts: GoogleRetryOptions = {},
|
|
319
338
|
): Promise<Response> {
|
|
320
339
|
const repairInvalid400 = opts.repairInvalid400 ?? true;
|
|
340
|
+
const retry429 = opts.retry429 ?? true;
|
|
321
341
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
322
342
|
const executor = ctx.executor ?? globalThis.fetch;
|
|
323
343
|
let activeRequest = request;
|
|
@@ -369,7 +389,7 @@ async function fetchGoogleWithRetryInternal(
|
|
|
369
389
|
opts,
|
|
370
390
|
)
|
|
371
391
|
: undefined;
|
|
372
|
-
return prepareCcaSseResponse(res, fetchPeer);
|
|
392
|
+
return prepareCcaSseResponse(res, fetchPeer, ctx.accountId);
|
|
373
393
|
}
|
|
374
394
|
if (res.status === 400 && repairInvalid400 && !compatibilityReplayUsed) {
|
|
375
395
|
let payloadText = "";
|
|
@@ -397,6 +417,9 @@ async function fetchGoogleWithRetryInternal(
|
|
|
397
417
|
continue;
|
|
398
418
|
}
|
|
399
419
|
}
|
|
420
|
+
if (res.status === 429 && !retry429) {
|
|
421
|
+
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
422
|
+
}
|
|
400
423
|
if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
|
|
401
424
|
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
402
425
|
}
|
|
@@ -466,5 +489,5 @@ export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchC
|
|
|
466
489
|
|
|
467
490
|
/** Antigravity (Cloud Code Assist) retry wrapper. */
|
|
468
491
|
export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
469
|
-
return fetchGoogleWithRetry("Antigravity", request, ctx);
|
|
492
|
+
return fetchGoogleWithRetry("Antigravity", request, ctx, { retry429: false });
|
|
470
493
|
}
|
|
@@ -12,6 +12,11 @@ export function vertexTruncationErrorMessage(reason?: string): string {
|
|
|
12
12
|
return `Vertex AI response truncated upstream before the turn completed${suffix}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function googleTruncationErrorMessage(reason?: string): string {
|
|
16
|
+
const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
|
|
17
|
+
return `Google AI Studio response truncated upstream before the turn completed${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Whether a finished turn must fail closed. A truncation reason arriving mid tool call always
|
|
17
22
|
* does. MALFORMED_FUNCTION_CALL fails closed even with zero started calls: the malformed call
|
|
@@ -5,6 +5,15 @@ type JsonObject = Record<string, unknown>;
|
|
|
5
5
|
|
|
6
6
|
const GOOGLE_TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
7
7
|
const GOOGLE_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
|
|
8
|
+
const GOOGLE_SAFETY_CATEGORIES = new Set([
|
|
9
|
+
"HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
10
|
+
"HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_CIVIC_INTEGRITY", "HARM_CATEGORY_JAILBREAK",
|
|
11
|
+
]);
|
|
12
|
+
const GOOGLE_SAFETY_THRESHOLDS = new Set([
|
|
13
|
+
"HARM_BLOCK_THRESHOLD_UNSPECIFIED", "BLOCK_LOW_AND_ABOVE", "BLOCK_MEDIUM_AND_ABOVE",
|
|
14
|
+
"BLOCK_ONLY_HIGH", "BLOCK_NONE", "OFF",
|
|
15
|
+
]);
|
|
16
|
+
const GOOGLE_CACHED_CONTENT = /^(?:cachedContents\/[^/?#\s]+|projects\/[^/?#\s]+\/locations\/[^/?#\s]+\/cachedContents\/[^/?#\s]+)$/;
|
|
8
17
|
|
|
9
18
|
function isObject(value: unknown): value is JsonObject {
|
|
10
19
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -158,12 +167,18 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
|
|
|
158
167
|
))].slice(0, 5);
|
|
159
168
|
if (stopSequences.length > 0) out.stopSequences = stopSequences;
|
|
160
169
|
}
|
|
161
|
-
if (isObject(value.thinkingConfig)
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
170
|
+
if (isObject(value.thinkingConfig)) {
|
|
171
|
+
const config: JsonObject = {};
|
|
172
|
+
if (typeof value.thinkingConfig.thinkingBudget === "number"
|
|
173
|
+
&& Number.isSafeInteger(value.thinkingConfig.thinkingBudget)
|
|
174
|
+
&& value.thinkingConfig.thinkingBudget >= -1) config.thinkingBudget = value.thinkingConfig.thinkingBudget;
|
|
175
|
+
if (typeof value.thinkingConfig.includeThoughts === "boolean") config.includeThoughts = value.thinkingConfig.includeThoughts;
|
|
176
|
+
if (typeof value.thinkingConfig.thinkingLevel === "string") {
|
|
177
|
+
const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
|
|
178
|
+
const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) ? raw : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
|
|
179
|
+
if (thinkingLevel && config.thinkingBudget === undefined) config.thinkingLevel = thinkingLevel;
|
|
180
|
+
}
|
|
181
|
+
if (Object.keys(config).length > 0) out.thinkingConfig = config;
|
|
167
182
|
}
|
|
168
183
|
if (Array.isArray(value.responseModalities)) {
|
|
169
184
|
const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
|
|
@@ -180,6 +195,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
|
|
|
180
195
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
181
196
|
}
|
|
182
197
|
|
|
198
|
+
function compileSafetySettings(value: unknown): unknown[] | undefined {
|
|
199
|
+
if (!Array.isArray(value) || value.length > 16) return undefined;
|
|
200
|
+
const seen = new Set<string>();
|
|
201
|
+
const out: JsonObject[] = [];
|
|
202
|
+
for (const setting of value) {
|
|
203
|
+
if (!isObject(setting) || typeof setting.category !== "string" || !GOOGLE_SAFETY_CATEGORIES.has(setting.category)
|
|
204
|
+
|| typeof setting.threshold !== "string" || !GOOGLE_SAFETY_THRESHOLDS.has(setting.threshold)
|
|
205
|
+
|| seen.has(setting.category)) return undefined;
|
|
206
|
+
seen.add(setting.category);
|
|
207
|
+
out.push({ category: setting.category, threshold: setting.threshold });
|
|
208
|
+
}
|
|
209
|
+
return out.length > 0 ? out : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
183
212
|
function compileToolConfig(value: unknown, toWireName: (name: string) => string): JsonObject | undefined {
|
|
184
213
|
if (!isObject(value) || !isObject(value.functionCallingConfig)) return undefined;
|
|
185
214
|
const raw = value.functionCallingConfig;
|
|
@@ -216,6 +245,9 @@ export function compileGoogleWireBody(input: unknown): {
|
|
|
216
245
|
if (generationConfig) body.generationConfig = generationConfig;
|
|
217
246
|
const toolConfig = compileToolConfig(source.toolConfig, names.toWire);
|
|
218
247
|
if (toolConfig) body.toolConfig = toolConfig;
|
|
248
|
+
const safetySettings = compileSafetySettings(source.safetySettings);
|
|
249
|
+
if (safetySettings) body.safetySettings = safetySettings;
|
|
250
|
+
if (typeof source.cachedContent === "string" && GOOGLE_CACHED_CONTENT.test(source.cachedContent)) body.cachedContent = source.cachedContent;
|
|
219
251
|
if (typeof source.sessionId === "string" && source.sessionId.length > 0) body.sessionId = source.sessionId;
|
|
220
252
|
return { body, restoreToolName: names.fromWire };
|
|
221
253
|
}
|