@bitkyc08/opencodex 2.25.0 → 2.26.0
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/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
|
@@ -17,8 +17,15 @@ export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI
|
|
|
17
17
|
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
18
18
|
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
|
|
19
19
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
20
|
-
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell
|
|
20
|
+
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.';
|
|
21
21
|
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
22
|
+
const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = {
|
|
23
|
+
Read: ["read", "read_file"],
|
|
24
|
+
Grep: ["grep"],
|
|
25
|
+
Glob: ["glob", "find"],
|
|
26
|
+
Bash: ["bash", "shell"],
|
|
27
|
+
LS: ["ls"],
|
|
28
|
+
};
|
|
22
29
|
|
|
23
30
|
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
24
31
|
"For generic tool-use/count demos, satisfy the request with repeated Codex shell bridge calls (`shell_command` or `exec_command`) for harmless commands.",
|
|
@@ -29,8 +36,7 @@ export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
|
29
36
|
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
|
|
30
37
|
"If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.",
|
|
31
38
|
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
|
|
32
|
-
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names.",
|
|
33
|
-
"Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Do not narrate Cursor-native Shell/Read routing.",
|
|
39
|
+
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names or an equivalent listed client tool.",
|
|
34
40
|
].join(" ");
|
|
35
41
|
|
|
36
42
|
export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
@@ -563,9 +569,14 @@ function quotedNames(names: readonly string[]): string {
|
|
|
563
569
|
return names.map(name => `\`${name}\``).join(", ");
|
|
564
570
|
}
|
|
565
571
|
|
|
572
|
+
function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeof NEIGHBOR_AGENT_TOOL_NAMES)[number]): boolean {
|
|
573
|
+
const advertised = new Set(wireNames.map(name => name.toLowerCase()));
|
|
574
|
+
if (advertised.has(neighbor.toLowerCase())) return true;
|
|
575
|
+
return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase()));
|
|
576
|
+
}
|
|
577
|
+
|
|
566
578
|
function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] {
|
|
567
|
-
|
|
568
|
-
return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertised.has(name));
|
|
579
|
+
return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertisedCoversNeighbor(wireNames, name));
|
|
569
580
|
}
|
|
570
581
|
|
|
571
582
|
function discoveryToolLabel(wireNames: readonly string[]): string | undefined {
|
|
@@ -632,7 +643,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
632
643
|
? "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."
|
|
633
644
|
: undefined,
|
|
634
645
|
hasBareExec
|
|
635
|
-
?
|
|
646
|
+
? `Prefer the Codex shell bridge over Cursor-native Shell/Read. If a Cursor-native file read, directory listing, grep, or shell operation is rejected, continue with the listed catalog tool ${shellBridgeLabel}.`
|
|
636
647
|
: undefined,
|
|
637
648
|
hostShellNote,
|
|
638
649
|
"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.",
|
|
@@ -656,7 +667,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
656
667
|
: undefined,
|
|
657
668
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
658
669
|
hasBareExec
|
|
659
|
-
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime,
|
|
670
|
+
? `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.`
|
|
660
671
|
: undefined,
|
|
661
672
|
].filter((note): note is string => typeof note === "string");
|
|
662
673
|
return notes.join(" ");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor tool-result normalization for Computer Use / node_repl surfaces (#1920/#1866).
|
|
3
|
+
*
|
|
4
|
+
* Scoped re-implementation of PR #1920 per the 260818 campaign disposition
|
|
5
|
+
* (REDESIGN-SMALL: "apply formatted.text at native toolResultPart + decode test").
|
|
6
|
+
* Only empty-output and known-failure-state normalization ships here; screenshot
|
|
7
|
+
* stripping and AXTree text compaction from the original PR are deliberately out
|
|
8
|
+
* of scope (the native path already bounds step size by real serialized bytes,
|
|
9
|
+
* dropping images oldest-first — see toolCallStep in protobuf-request.ts).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const COMPUTER_USE_TOOL_NAMES = new Set([
|
|
13
|
+
"node_repl",
|
|
14
|
+
"node_repl__js",
|
|
15
|
+
"mcp__node_repl__js",
|
|
16
|
+
"get_app_state",
|
|
17
|
+
"list_apps",
|
|
18
|
+
"screenshot",
|
|
19
|
+
"computer_use",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function isNodeReplOrComputerUseTool(toolName?: string, toolNamespace?: string): boolean {
|
|
23
|
+
if (toolNamespace && (toolNamespace === "mcp__node_repl" || toolNamespace.includes("node_repl") || toolNamespace.includes("computer_use"))) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (!toolName) return false;
|
|
27
|
+
const lower = toolName.toLowerCase();
|
|
28
|
+
if (COMPUTER_USE_TOOL_NAMES.has(lower)) return true;
|
|
29
|
+
return lower.startsWith("mcp__node_repl") || lower.startsWith("mcp__computer_use");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Failure states the Computer Use / node_repl runtime reports as PLAIN TEXT inside a non-error result. */
|
|
33
|
+
const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [
|
|
34
|
+
{
|
|
35
|
+
marker: "SkyComputerUseError",
|
|
36
|
+
guidance: "The Computer Use runtime rejected this action. Re-check application state with get_app_state before retrying.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
marker: "sky is not defined",
|
|
40
|
+
guidance: "The sky binding is unavailable in this context; Computer Use calls only work inside the privileged node_repl session.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
marker: "has already been declared",
|
|
44
|
+
guidance: "The node_repl session keeps earlier declarations; rename the variable or use var/reassignment instead of redeclaring.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
marker: "unsupported import in exec",
|
|
48
|
+
guidance: "Imports are not available in this exec context; use the injected globals instead.",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Matches exec wrappers whose only payload is an empty-output marker. */
|
|
53
|
+
const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Script failed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
|
|
54
|
+
|
|
55
|
+
export interface NormalizedToolResultText {
|
|
56
|
+
text: string;
|
|
57
|
+
isError: boolean;
|
|
58
|
+
/** True when normalization changed either field (lets callers skip work on the common path). */
|
|
59
|
+
changed: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a Cursor-bound tool-result TEXT payload:
|
|
64
|
+
* - blank / empty-exec-wrapper output on Computer Use or node_repl tools becomes an
|
|
65
|
+
* actionable error instead of an empty string the model silently accepts;
|
|
66
|
+
* - known runtime failure states reported as plain text are marked isError with a
|
|
67
|
+
* one-line recovery hint appended.
|
|
68
|
+
* Everything else passes through byte-identical.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeCursorToolResultText(
|
|
71
|
+
text: string,
|
|
72
|
+
options: { toolName?: string; toolNamespace?: string; isError?: boolean } = {},
|
|
73
|
+
): NormalizedToolResultText {
|
|
74
|
+
const isError = options.isError === true;
|
|
75
|
+
const computerUse = isNodeReplOrComputerUseTool(options.toolName, options.toolNamespace);
|
|
76
|
+
if (computerUse && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
|
|
77
|
+
return {
|
|
78
|
+
text: "[empty output: the tool ran but produced no stdout or return value. Verify application state with get_app_state, or make the script emit output.]",
|
|
79
|
+
isError: true,
|
|
80
|
+
changed: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!isError) {
|
|
84
|
+
for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
|
|
85
|
+
if (text.includes(marker)) {
|
|
86
|
+
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { text, isError, changed: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
@@ -18,6 +18,8 @@ export interface CursorTransportFactoryInput {
|
|
|
18
18
|
provider: OcxProviderConfig;
|
|
19
19
|
translatorBudget: TranslatorBudget;
|
|
20
20
|
headers?: Headers;
|
|
21
|
+
/** Router-prepared fetch that preserves provider overrides and per-request pacing. */
|
|
22
|
+
fetch?: typeof globalThis.fetch;
|
|
21
23
|
/** Pre-first-frame deadline (dial + first server frame). Defaults to 30s when omitted. */
|
|
22
24
|
firstFrameTimeoutMs?: number;
|
|
23
25
|
/** Grace (ms) between close() and the force-destroy fallback after a first-frame timeout. Defaults to 1s. */
|
|
@@ -33,6 +35,11 @@ export interface CursorTransportFactoryInput {
|
|
|
33
35
|
* native local exec authorization because the text is caller-controlled.
|
|
34
36
|
*/
|
|
35
37
|
requestDeclaresFullAccess?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Stable Cursor Connect `x-session-id` across transport rebuilds for the same
|
|
40
|
+
* client thread. Distinct from the per-transport native-exec/shell owner.
|
|
41
|
+
*/
|
|
42
|
+
sessionId?: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -121,6 +121,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
121
121
|
headers: incoming.headers,
|
|
122
122
|
translatorBudget: incoming.translatorBudget,
|
|
123
123
|
requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
|
|
124
|
+
sessionId: activeRequest.conversationId,
|
|
125
|
+
...(incoming.providerFetch ? { fetch: incoming.providerFetch } : {}),
|
|
124
126
|
},
|
|
125
127
|
activeRequest,
|
|
126
128
|
incoming.abortSignal,
|
|
@@ -14,6 +14,11 @@ const GOOGLE_RETRY_ATTEMPTS = 3;
|
|
|
14
14
|
const GOOGLE_RETRY_BASE_MS = 250;
|
|
15
15
|
const GOOGLE_RETRY_MAX_MS = 2_000;
|
|
16
16
|
|
|
17
|
+
export interface GoogleRetryOptions {
|
|
18
|
+
/** Repair-and-replay structurally invalid 400 bodies (Vertex/Antigravity behavior). */
|
|
19
|
+
repairInvalid400?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
|
|
18
23
|
return normalizeUpstreamHttpErrorResponse(res, {
|
|
19
24
|
signal,
|
|
@@ -22,13 +27,21 @@ async function normalizeFinalGoogleError(label: string, res: Response, signal?:
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
|
-
* Fetch a Google-family upstream
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* error body. `label` is the provider-facing prefix used in error
|
|
30
|
+
* Fetch a Google-family upstream with Kiro-style hardening: per-attempt timeout
|
|
31
|
+
* (`AbortSignal.any([parent, timeout])`), bounded retry on transient status / network errors,
|
|
32
|
+
* `Retry-After` honoring, jittered exponential backoff, and (unless raw mode is used) a
|
|
33
|
+
* classified + redacted final error body. `label` is the provider-facing prefix used in error
|
|
34
|
+
* messages.
|
|
29
35
|
*/
|
|
30
|
-
export async function fetchGoogleWithRetry(
|
|
36
|
+
export async function fetchGoogleWithRetry(
|
|
37
|
+
label: string,
|
|
38
|
+
request: AdapterRequest,
|
|
39
|
+
ctx: AdapterFetchContext = {},
|
|
40
|
+
opts: GoogleRetryOptions = {},
|
|
41
|
+
): Promise<Response> {
|
|
42
|
+
const repairInvalid400 = opts.repairInvalid400 ?? true;
|
|
31
43
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
44
|
+
const executor = ctx.executor ?? globalThis.fetch;
|
|
32
45
|
let lastError: unknown;
|
|
33
46
|
let activeRequest = request;
|
|
34
47
|
let compatibilityReplayUsed = false;
|
|
@@ -39,8 +52,8 @@ export async function fetchGoogleWithRetry(label: string, request: AdapterReques
|
|
|
39
52
|
method: activeRequest.method,
|
|
40
53
|
headers: activeRequest.headers,
|
|
41
54
|
body: activeRequest.body,
|
|
42
|
-
}, timeoutMs, ctx.abortSignal, ctx.stream);
|
|
43
|
-
if (res.status === 400 && !compatibilityReplayUsed) {
|
|
55
|
+
}, timeoutMs, ctx.abortSignal, ctx.stream, executor);
|
|
56
|
+
if (res.status === 400 && repairInvalid400 && !compatibilityReplayUsed) {
|
|
44
57
|
let payloadText = "";
|
|
45
58
|
try {
|
|
46
59
|
payloadText = await readDisplaySafeErrorPayloadText(res.clone(), ctx.abortSignal);
|
|
@@ -61,10 +74,11 @@ export async function fetchGoogleWithRetry(label: string, request: AdapterReques
|
|
|
61
74
|
}
|
|
62
75
|
// A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
|
|
63
76
|
// it won't recover for hours and burns retries). Peek the body to tell them apart.
|
|
64
|
-
if (res.status === 429
|
|
65
|
-
const
|
|
77
|
+
if (res.status === 429) {
|
|
78
|
+
const peekTarget = ctx.returnRawErrors ? res.clone() : res;
|
|
79
|
+
const peek = await readDisplaySafeErrorPayloadText(peekTarget, ctx.abortSignal);
|
|
66
80
|
if (isQuotaExhaustedBody(peek)) {
|
|
67
|
-
return normalizeUpstreamHttpErrorResponse(res, {
|
|
81
|
+
return ctx.returnRawErrors ? res : normalizeUpstreamHttpErrorResponse(res, {
|
|
68
82
|
signal: ctx.abortSignal,
|
|
69
83
|
formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText || peek),
|
|
70
84
|
});
|
|
@@ -89,6 +103,20 @@ export async function fetchGoogleWithRetry(label: string, request: AdapterReques
|
|
|
89
103
|
throw lastError ?? new Error(`${label} fetch failed`);
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
/**
|
|
107
|
+
* AI Studio direct (`generativelanguage.googleapis.com`) retry wrapper.
|
|
108
|
+
*
|
|
109
|
+
* Direct requests keep the default server error surface — the raw `Provider error <status>:
|
|
110
|
+
* <body>` text the shared Responses path formats — and keep single-shot 400 semantics (no
|
|
111
|
+
* request-shape compatibility replay). The wrapper exists for the failure mode observed in
|
|
112
|
+
* production: AI Studio's transient `503 UNAVAILABLE` "model is currently experiencing high
|
|
113
|
+
* demand" spikes, plus plain rate-limit 429s, both of which previously failed immediately
|
|
114
|
+
* because the default server fetch path only retries connection resets.
|
|
115
|
+
*/
|
|
116
|
+
export function fetchDirectGeminiWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
117
|
+
return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, { repairInvalid400: false });
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
/** Vertex AI retry wrapper. */
|
|
93
121
|
export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
94
122
|
return fetchGoogleWithRetry("Vertex AI", request, ctx);
|
package/src/adapters/google.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { identifyRoutedModel } from "./identity";
|
|
|
26
26
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
27
27
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
28
28
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
29
|
+
import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
29
30
|
import {
|
|
30
31
|
isTranslatorBudgetExceededError,
|
|
31
32
|
retainTranslatedEventBatch,
|
|
@@ -215,7 +216,13 @@ function messagesToGeminiFormat(
|
|
|
215
216
|
const part: Record<string, unknown> = { functionCall };
|
|
216
217
|
// Prefer the metadata that travelled with this exact call; fall back to the legacy
|
|
217
218
|
// field for callers that have not been migrated. Never merge or synthesize.
|
|
218
|
-
|
|
219
|
+
// Final fallback (#1926): the durable store, read AT SERIALIZATION TIME. The
|
|
220
|
+
// Responses parser runs before the route/credential scope is bound, so its
|
|
221
|
+
// parse-time lookup can never hit; by the time this adapter serializes, the
|
|
222
|
+
// credential-scoped identity is bound and the durable lookup is meaningful.
|
|
223
|
+
const signature = tc.providerMetadata?.google?.thoughtSignature
|
|
224
|
+
?? tc.thoughtSignature
|
|
225
|
+
?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
|
|
219
226
|
if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
|
|
220
227
|
parts.push(part);
|
|
221
228
|
}
|
|
@@ -377,8 +384,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
377
384
|
return {
|
|
378
385
|
name: "google",
|
|
379
386
|
|
|
380
|
-
// Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
|
|
381
|
-
//
|
|
387
|
+
// Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
|
|
388
|
+
// Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
|
|
389
|
+
// retries transient 5xx responses through providerFetch while preserving multi-key pool
|
|
390
|
+
// 429 rotation and raw error formatting.
|
|
382
391
|
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
383
392
|
? {
|
|
384
393
|
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
@@ -496,6 +505,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
496
505
|
} else {
|
|
497
506
|
sanitizeAntigravityClaudeSignatures(contents);
|
|
498
507
|
}
|
|
508
|
+
// Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
|
|
509
|
+
// as prefill: "This model does not support assistant message prefill. The conversation
|
|
510
|
+
// must end with a user message." Context compaction, previous_response_id expansion,
|
|
511
|
+
// and interrupted-turn replay can all produce a model-tail history. Append a user
|
|
512
|
+
// "(continue)" nudge, mirroring the anthropic adapter's tail guard (src/adapters/anthropic.ts).
|
|
513
|
+
if (/claude/i.test(wireModelId)) {
|
|
514
|
+
const last = contents.length > 0 ? contents[contents.length - 1] as { role?: string } : undefined;
|
|
515
|
+
if (!last || last.role === "model") {
|
|
516
|
+
contents.push({ role: "user", parts: [{ text: "(continue)" }] });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
499
519
|
}
|
|
500
520
|
const envelope = {
|
|
501
521
|
model: wireModelId,
|
|
@@ -12,7 +12,14 @@ import { identifyRoutedModel } from "./identity";
|
|
|
12
12
|
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
13
13
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
14
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
canForwardForeignServiceTierForChatModel,
|
|
17
|
+
supportsServiceTierForModel,
|
|
18
|
+
} from "../providers/service-tier";
|
|
19
|
+
import {
|
|
20
|
+
canonicalFastTierMarker,
|
|
21
|
+
createAdapterTierMetadata,
|
|
22
|
+
} from "../providers/fastwire";
|
|
16
23
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
17
24
|
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
18
25
|
import {
|
|
@@ -100,13 +107,20 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
100
107
|
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
111
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
112
|
+
|
|
103
113
|
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
104
114
|
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
105
115
|
if (modelInList(provider.noPenaltyModels, modelId)) {
|
|
106
116
|
delete body.presence_penalty;
|
|
107
117
|
delete body.frequency_penalty;
|
|
108
118
|
}
|
|
109
|
-
|
|
119
|
+
// Exact match, unlike the gates above: `noStructuredOutputModels` is documented as
|
|
120
|
+
// "only an exact requested-model match omits the field" (#1424), and the Responses
|
|
121
|
+
// ingress enforces exactly that. A prefix match here would strip response_format from
|
|
122
|
+
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
123
|
+
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
110
124
|
|
|
111
125
|
if (provider.chatServiceTier && rawBody.service_tier !== undefined) {
|
|
112
126
|
body.service_tier = rawBody.service_tier;
|
|
@@ -1287,17 +1301,20 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1287
1301
|
messages,
|
|
1288
1302
|
stream: parsed.stream,
|
|
1289
1303
|
};
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1304
|
+
// A policy-produced canonical decision has already passed capability validation. Without
|
|
1305
|
+
// that decision, a canonical caller value still requires an explicit true capability;
|
|
1306
|
+
// unclassified Chat routes remain behind the caller-forwarding opt-in.
|
|
1307
|
+
const serviceTier = parsed.options.serviceTier;
|
|
1308
|
+
const tierDecision = parsed.options.tierDecision;
|
|
1309
|
+
const callerCanonicalFast = canonicalFastTierMarker(serviceTier) !== undefined;
|
|
1310
|
+
const callerTierForwardAllowed = canForwardForeignServiceTierForChatModel(provider, parsed.modelId);
|
|
1311
|
+
const canonicalFastCapability = callerCanonicalFast
|
|
1312
|
+
&& supportsServiceTierForModel(provider, parsed.modelId) === true;
|
|
1313
|
+
const canSerializeServiceTier = tierDecision?.kind === "set"
|
|
1314
|
+
|| tierDecision?.kind === "forward-caller"
|
|
1315
|
+
|| (tierDecision === undefined && (callerTierForwardAllowed || canonicalFastCapability));
|
|
1316
|
+
if (canSerializeServiceTier && serviceTier !== undefined) {
|
|
1317
|
+
body.service_tier = serviceTier;
|
|
1301
1318
|
}
|
|
1302
1319
|
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
1303
1320
|
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
@@ -1430,6 +1447,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1430
1447
|
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
1431
1448
|
|
|
1432
1449
|
const bodyJson = JSON.stringify(body);
|
|
1450
|
+
const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
|
|
1451
|
+
const tierLog = createAdapterTierMetadata(
|
|
1452
|
+
parsed.options.tierObservation,
|
|
1453
|
+
parsed.options.tierDecision,
|
|
1454
|
+
actualServiceTier === null ? null : "service-tier",
|
|
1455
|
+
actualServiceTier,
|
|
1456
|
+
);
|
|
1433
1457
|
if (isDebugEnabled()) {
|
|
1434
1458
|
let host = "upstream";
|
|
1435
1459
|
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
@@ -1450,6 +1474,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1450
1474
|
headers,
|
|
1451
1475
|
body: bodyJson,
|
|
1452
1476
|
...(reasoningLog ? { reasoningLog } : {}),
|
|
1477
|
+
...(tierLog ? { tierLog } : {}),
|
|
1453
1478
|
};
|
|
1454
1479
|
},
|
|
1455
1480
|
|