@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.
Files changed (99) hide show
  1. package/bin/ocx.mjs +59 -7
  2. package/gui/dist/assets/index-RL6b1bTV.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/base.ts +18 -0
  6. package/src/adapters/client-fingerprint.ts +0 -2
  7. package/src/adapters/cursor/http1-bidi.ts +361 -0
  8. package/src/adapters/cursor/live-models.ts +117 -30
  9. package/src/adapters/cursor/live-transport.ts +137 -56
  10. package/src/adapters/cursor/native-exec-fs.ts +1 -1
  11. package/src/adapters/cursor/native-exec-network.ts +1 -1
  12. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  13. package/src/adapters/cursor/protobuf-request.ts +89 -21
  14. package/src/adapters/cursor/tool-definitions.ts +18 -7
  15. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  16. package/src/adapters/cursor/transport.ts +7 -0
  17. package/src/adapters/cursor.ts +2 -0
  18. package/src/adapters/google-http.ts +38 -10
  19. package/src/adapters/google.ts +23 -3
  20. package/src/adapters/openai-chat.ts +38 -13
  21. package/src/adapters/openai-responses.ts +129 -9
  22. package/src/adapters/registry.ts +30 -1
  23. package/src/bridge.ts +50 -12
  24. package/src/chat/inbound.ts +1 -0
  25. package/src/claude/agents-inject.ts +3 -2
  26. package/src/cli/dispatch.ts +19 -6
  27. package/src/cli/help.ts +2 -1
  28. package/src/cli/integrations.ts +35 -0
  29. package/src/cli/minimax.ts +8 -2
  30. package/src/cli/registry.ts +13 -2
  31. package/src/clients/config-export.ts +120 -1
  32. package/src/codex/account-store.ts +17 -1
  33. package/src/codex/auth-api.ts +51 -17
  34. package/src/codex/catalog/effort.ts +8 -5
  35. package/src/codex/catalog/provider-fetch.ts +39 -27
  36. package/src/codex/catalog/sync.ts +30 -8
  37. package/src/codex/inject.ts +39 -13
  38. package/src/codex/main-account.ts +29 -1
  39. package/src/codex/plan-from-token.ts +140 -0
  40. package/src/codex/plan.ts +25 -0
  41. package/src/codex/prompt-journal.ts +6 -2
  42. package/src/codex/quota-rejection.ts +21 -7
  43. package/src/codex/refresh.ts +4 -2
  44. package/src/codex/sync.ts +106 -3
  45. package/src/codex/warmup.ts +187 -81
  46. package/src/config.ts +85 -46
  47. package/src/generated/compatibility-version.json +127 -83
  48. package/src/grok/inject.ts +1 -1
  49. package/src/images/loop.ts +1 -0
  50. package/src/integrations/registry.ts +7 -0
  51. package/src/lab/automation/config-persistence.ts +2 -2
  52. package/src/lab/automation/persistence.ts +2 -2
  53. package/src/lab/ledger/purge.ts +2 -2
  54. package/src/lab/subject/behavior-fingerprint.ts +2 -2
  55. package/src/lib/config-ownership.ts +5 -2
  56. package/src/lib/destination-policy.ts +18 -1
  57. package/src/lib/provider-outbound.ts +7 -0
  58. package/src/lib/redact.ts +11 -0
  59. package/src/lib/tool-argument-integers.ts +50 -6
  60. package/src/lib/upstream-http-version.ts +57 -0
  61. package/src/lib/upstream-retry.ts +2 -1
  62. package/src/lib/windows-atomic-replace.ts +155 -0
  63. package/src/lib/windows-service-wrappers.ts +72 -0
  64. package/src/oauth/google-antigravity.ts +2 -2
  65. package/src/oauth/index.ts +60 -8
  66. package/src/providers/antigravity-models.ts +58 -4
  67. package/src/providers/command-code-efforts.ts +36 -9
  68. package/src/providers/context-cap.ts +9 -0
  69. package/src/providers/derive.ts +4 -0
  70. package/src/providers/fastwire.ts +453 -0
  71. package/src/providers/registry.ts +21 -1
  72. package/src/providers/service-tier.ts +173 -89
  73. package/src/responses/parser.ts +25 -16
  74. package/src/responses/reasoning-replay-cache.ts +30 -0
  75. package/src/responses/thought-signature-replay.ts +74 -4
  76. package/src/router.ts +6 -0
  77. package/src/routing/compatibility/behavior.ts +27 -16
  78. package/src/server/index.ts +10 -1
  79. package/src/server/management/oauth-account-routes.ts +10 -2
  80. package/src/server/management/shared.ts +1 -1
  81. package/src/server/management/system-routes.ts +15 -0
  82. package/src/server/request-log.ts +57 -3
  83. package/src/server/responses/agent-task-recovery.ts +6 -1
  84. package/src/server/responses/core.ts +209 -40
  85. package/src/server/responses/fetch-helpers.ts +15 -36
  86. package/src/server/responses/responses-field-backfill.ts +173 -0
  87. package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
  88. package/src/service.ts +38 -36
  89. package/src/storage/cleanup.ts +2 -2
  90. package/src/tray/windows.ts +3 -2
  91. package/src/types.ts +101 -14
  92. package/src/update/job.ts +9 -18
  93. package/src/update/transactional-install.d.mts +22 -0
  94. package/src/update/transactional-install.mjs +259 -0
  95. package/src/usage/cost.ts +34 -5
  96. package/src/usage/log.ts +74 -4
  97. package/src/vision/anthropic-describe.ts +10 -6
  98. package/src/web-search/anthropic-executor.ts +8 -6
  99. 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; never say native shell is blocked.';
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
- const advertised = new Set(wireNames);
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
- ? "Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Prefer the bridge over Cursor-native Shell/Read; do not narrate phrases like \"Native shell access is blocked\" silently call `shell_command` / `exec_command`."
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, silently use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). Do not tell the user access is blocked. For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
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;
@@ -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 (Vertex / Antigravity) with Kiro-style hardening: per-attempt
26
- * timeout (`AbortSignal.any([parent, timeout])`), bounded retry on transient status / network
27
- * errors, `Retry-After` honoring, jittered exponential backoff, and a classified + redacted final
28
- * error body. `label` is the provider-facing prefix used in error messages.
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(label: string, request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
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 && !ctx.returnRawErrors) {
65
- const peek = await readDisplaySafeErrorPayloadText(res, ctx.abortSignal);
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);
@@ -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
- const signature = tc.providerMetadata?.google?.thoughtSignature ?? tc.thoughtSignature;
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. AI-Studio
381
- // Gemini keeps the default server fetch path (fetchResponse stays undefined so server.ts falls back).
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 { canSerializeServiceTierForChatModel } from "../providers/service-tier";
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
- if (modelInList(provider.noStructuredOutputModels, modelId)) delete body.response_format;
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
- // Preserve a caller-selected service tier for OpenAI-compatible chat gateways. The
1291
- // request pipeline deliberately does not inject fast mode for this adapter, but dropping
1292
- // an explicit value here makes the Responses parser's serviceTier projection ineffective.
1293
- //
1294
- // Opt-in, like `prompt_cache_key` directly below: `service_tier` is an OpenAI-specific
1295
- // extension and 66 registry providers share this adapter. A provider-wide Chat opt-in
1296
- // authorizes undeclared models; an exact model declaration can authorize or deny one
1297
- // model. Provider-level false remains fail-closed.
1298
- if (canSerializeServiceTierForChatModel(provider, parsed.modelId)
1299
- && parsed.options.serviceTier !== undefined) {
1300
- body.service_tier = parsed.options.serviceTier;
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