@yansigit/opencodex 2.33.1 → 2.35.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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -39,7 +39,7 @@ function requestForHost(request: AdapterRequest, host: string): AdapterRequest {
39
39
  return { ...request, url: target.toString() };
40
40
  }
41
41
 
42
- type CcaSseProbe = "empty" | "candidate" | "unavailable" | "quota_exhausted" | "geo_blocked" | "terminal";
42
+ type CcaSseProbe = "empty" | "candidate" | "unavailable" | "quota_exhausted" | "rate_limit" | "geo_blocked" | "terminal";
43
43
 
44
44
  function probeCcaSseEvent(bytes: Uint8Array): CcaSseProbe {
45
45
  const text = new TextDecoder().decode(bytes);
@@ -67,6 +67,7 @@ function probeCcaSseEvent(bytes: Uint8Array): CcaSseProbe {
67
67
  }
68
68
  const serialized = JSON.stringify(frame);
69
69
  if (isQuotaExhaustedBody(serialized)) return "quota_exhausted";
70
+ if (/rate[- ]limit|too many requests|per[- ]minute|requests per minute|concurrent request/i.test(serialized)) return "rate_limit";
70
71
  if (isAntigravityGeoBlockedBody(serialized)) return "geo_blocked";
71
72
  return "terminal";
72
73
  }
@@ -209,15 +210,15 @@ async function prepareCcaSseResponse(
209
210
  if (probe === "unavailable") {
210
211
  return failoverOrPassthrough();
211
212
  }
212
- if (probe === "quota_exhausted" || probe === "geo_blocked") {
213
+ if (probe === "quota_exhausted" || probe === "rate_limit" || probe === "geo_blocked") {
213
214
  if (accountId) {
214
215
  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",
216
+ code: probe === "geo_blocked" ? 403 : 429,
217
+ status: probe === "geo_blocked" ? "PERMISSION_DENIED" : "RESOURCE_EXHAUSTED",
218
+ message: probe === "quota_exhausted" ? "quota exceeded" : probe === "rate_limit" ? "rate limit exceeded" : "user location is not supported",
218
219
  });
219
220
  }
220
- const status = probe === "quota_exhausted" ? 429 : 403;
221
+ const status = probe === "geo_blocked" ? 403 : 429;
221
222
  return passthrough(undefined, status);
222
223
  }
223
224
  }
@@ -244,15 +245,15 @@ async function prepareCcaSseResponse(
244
245
  }
245
246
  return failoverOrPassthrough();
246
247
  }
247
- if (probe === "quota_exhausted" || probe === "geo_blocked") {
248
+ if (probe === "quota_exhausted" || probe === "rate_limit" || probe === "geo_blocked") {
248
249
  if (accountId) {
249
250
  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",
251
+ code: probe === "geo_blocked" ? 403 : 429,
252
+ status: probe === "geo_blocked" ? "PERMISSION_DENIED" : "RESOURCE_EXHAUSTED",
253
+ message: probe === "quota_exhausted" ? "quota exceeded" : probe === "rate_limit" ? "rate limit exceeded" : "user location is not supported",
253
254
  });
254
255
  }
255
- const status = probe === "quota_exhausted" ? 429 : 403;
256
+ const status = probe === "geo_blocked" ? 403 : 429;
256
257
  return passthrough(overflow, status);
257
258
  }
258
259
  if (probe === "terminal") return passthrough(overflow);
@@ -37,7 +37,7 @@ import {
37
37
  } from "../web-search/gemini-executor";
38
38
  import type { WebSearchSource } from "../web-search/parse";
39
39
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
40
- import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
40
+ import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
41
41
  import {
42
42
  isTranslatorBudgetExceededError,
43
43
  releaseTranslatedEvent,
@@ -49,9 +49,8 @@ import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
49
49
  import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
50
50
  import { normalizeAntigravityProviderError } from "../oauth/antigravity-routing";
51
51
  import { buildAiStudioHeaders, parseGoogleCookieJar } from "../oauth/google-aistudio-auth";
52
- import { cookieHeaderFromSession, loadAiStudioSession } from "../oauth/aistudio-session-sync";
52
+ import { resolveAiStudioCredentials } from "../oauth/aistudio-credentials";
53
53
  import { parseMakerSuiteChunk } from "./google-aistudio-parser";
54
- import { globalAiStudioRelayHub } from "../server/aistudio-ws-hub";
55
54
 
56
55
  const INLINE_ERROR_URL_USERINFO = /https?:\/\/[^\s"'<>]*@/gi;
57
56
 
@@ -75,6 +74,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
75
74
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
76
75
  ].join("\n");
77
76
 
77
+ /**
78
+ * Documented output ceiling for a Google-surface model, or `undefined` when the id is not
79
+ * recognized.
80
+ *
81
+ * Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
82
+ * anything unmatched, which silently truncated aliases, gateway ids, and any model added after
83
+ * this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
84
+ * we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
85
+ * that an explicit request value wins, so an unrecognized model passes through untouched and the
86
+ * upstream remains the authority on its own limit.
87
+ *
88
+ * Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
89
+ * matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
90
+ * containing "oss" (`crossover-v2`).
91
+ */
92
+ export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
93
+ const lower = modelId.toLowerCase().trim();
94
+ if (lower.startsWith("gemini")) {
95
+ // Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
96
+ return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
97
+ }
98
+ if (lower.startsWith("claude")) return 64000;
99
+ if (lower.startsWith("gpt-oss")) return 32768;
100
+ return undefined;
101
+ }
102
+
103
+ export function clampGoogleMaxOutputTokens(
104
+ modelId: string,
105
+ requestedTokens?: number,
106
+ ): number | undefined {
107
+ if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
108
+ const modelMax = maxOutputTokensForGoogleModel(modelId);
109
+ // Unknown model: honour the request as-is rather than inventing a ceiling for it.
110
+ if (modelMax === undefined) return requestedTokens;
111
+ return Math.min(requestedTokens, modelMax);
112
+ }
113
+
78
114
  /**
79
115
  * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
80
116
  * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
@@ -181,7 +217,7 @@ function geminiTextPart(text: unknown): { text: string } | undefined {
181
217
  */
182
218
  function geminiToolResultText(content: string | OcxContentPart[]): string {
183
219
  if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
184
- const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
220
+ const hasContent = content.some(p => p.type !== "text" || p.text.length > 0);
185
221
  return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
186
222
  }
187
223
 
@@ -225,7 +261,7 @@ function messagesToGeminiFormat(
225
261
  parsed: OcxParsedRequest,
226
262
  identityModelId: string,
227
263
  repairToolPairs: boolean,
228
- ): { systemInstruction?: unknown; contents: unknown[] } {
264
+ ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
229
265
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
230
266
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
231
267
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
@@ -237,6 +273,7 @@ function messagesToGeminiFormat(
237
273
  const systemInstruction = { parts: [{ text: systemText }] };
238
274
 
239
275
  const contents: unknown[] = [];
276
+ const replayedCallIds: string[] = [];
240
277
  let userMergeBarrier = false;
241
278
  const appendContent = (next: { role: string; parts: unknown[] }): void => {
242
279
  appendGeminiContent(contents, next, !userMergeBarrier);
@@ -271,6 +308,13 @@ function messagesToGeminiFormat(
271
308
  parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
272
309
  continue;
273
310
  }
311
+ if (p.type === "video") {
312
+ const data = parseDataUrl(p.videoUrl);
313
+ // Gemini accepts inline video bytes in the same Part union as images. Arbitrary
314
+ // remote URLs are not valid fileData references, so retain only a short marker.
315
+ parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
316
+ continue;
317
+ }
274
318
  // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
275
319
  const textPart = geminiTextPart(p.text);
276
320
  if (textPart) parts.push(textPart);
@@ -318,7 +362,10 @@ function messagesToGeminiFormat(
318
362
  const signature = tc.providerMetadata?.google?.thoughtSignature
319
363
  ?? tc.thoughtSignature
320
364
  ?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
321
- if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
365
+ if (isLikelyRealThoughtSignature(signature)) {
366
+ part.thoughtSignature = signature;
367
+ replayedCallIds.push(tc.id);
368
+ }
322
369
  parts.push(part);
323
370
  }
324
371
  }
@@ -375,7 +422,7 @@ function messagesToGeminiFormat(
375
422
  }
376
423
  }
377
424
 
378
- return { systemInstruction, contents };
425
+ return { systemInstruction, contents, replayedCallIds };
379
426
  }
380
427
 
381
428
  function toolsToGeminiFormat(
@@ -744,6 +791,47 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
744
791
  const truncationErrorMessage = provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
745
792
  ? vertexTruncationErrorMessage
746
793
  : googleTruncationErrorMessage;
794
+ let lastInjectedCallIds: string[] = [];
795
+ let lastReasoningReplayScope: OcxParsedRequest["_reasoningReplayScope"];
796
+
797
+ // Conservative batch invalidation: upstream Gemini/Antigravity errors (e.g.
798
+ // "Function call is missing a thought_signature in functionCall parts") do not specify which
799
+ // specific call_id was rejected. When a request containing replayed signatures is rejected,
800
+ // we evict all callIds injected in that turn (lastInjectedCallIds) from the durable store
801
+ // and clear the session replay cache, preventing poisoned-signature loops while allowing
802
+ // subsequent turns to re-accumulate valid signatures. Unrelated calls from other turns remain intact.
803
+ //
804
+ // Memory-cache clearing stays broad (any invalid-argument/signature error can poison the
805
+ // session replay cache), but durable-store eviction is intentionally narrower: it only runs
806
+ // when the error text explicitly mentions a signature, so a generic tool-schema
807
+ // INVALID_ARGUMENT does not destroy valid durable signatures.
808
+ function handleSignatureRejection(errorMessage?: string) {
809
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
810
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
811
+ const text = errorMessage ?? "";
812
+ const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
813
+ const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
814
+ // The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
815
+ // scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
816
+ if (
817
+ (provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
818
+ && replayModel && replaySession && (isInvalidArgument || isSignatureError)
819
+ ) {
820
+ clearAntigravityReplay(replayModel, replaySession);
821
+ }
822
+ // The DURABLE store is not mode-scoped: signatures are remembered through
823
+ // rememberAndSerializeExtraContent and read back by lookupReplayThoughtSignature on every
824
+ // Google mode, including AI Studio. Gating eviction on CCA/Vertex therefore left AI Studio
825
+ // with rejected signatures cached forever, replaying them into every subsequent turn — the
826
+ // store poisons itself and the request keeps failing. Eviction follows the same scope the
827
+ // write does.
828
+ if (isSignatureError) {
829
+ for (const callId of lastInjectedCallIds) {
830
+ forgetThoughtSignatureForReplay(callId, lastReasoningReplayScope);
831
+ }
832
+ }
833
+ }
834
+
747
835
  return {
748
836
  name: "google",
749
837
  validateRequest(parsed: OcxParsedRequest) {
@@ -763,46 +851,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
763
851
  // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
764
852
  // retries transient 5xx responses through providerFetch while preserving multi-key pool
765
853
  // 429 rotation and raw error formatting.
766
- ...(provider.googleMode === "ai-studio-web"
767
- ? {
768
- fetchResponse: async (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> => {
769
- if (globalAiStudioRelayHub.hasActiveSessions()) {
770
- const streamRes = await globalAiStudioRelayHub.dispatchStream(
771
- {
772
- url: request.url,
773
- method: request.method,
774
- headers: request.headers,
775
- body: request.body,
776
- },
777
- ctx?.abortSignal,
778
- );
779
- const encoder = new TextEncoder();
780
- const bodyStream = new ReadableStream({
781
- async start(controller) {
782
- try {
783
- for await (const chunk of streamRes.chunks) {
784
- controller.enqueue(encoder.encode(chunk));
785
- }
786
- controller.close();
787
- } catch (err) {
788
- controller.error(err);
789
- }
790
- },
791
- });
792
- return new Response(bodyStream, {
793
- status: 200,
794
- headers: { "Content-Type": "text/event-stream" },
795
- });
796
- }
797
- return fetch(request.url, {
798
- method: request.method,
799
- headers: request.headers,
800
- body: request.body,
801
- signal: ctx?.abortSignal,
802
- });
803
- },
804
- }
805
- : provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
854
+ ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
806
855
  ? {
807
856
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
808
857
  (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
@@ -824,11 +873,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
824
873
  : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
825
874
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
826
875
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
827
- const { systemInstruction, contents } = messagesToGeminiFormat(
876
+ const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
828
877
  parsed,
829
878
  identityModelId,
830
879
  provider.googleMode === "cloud-code-assist",
831
880
  );
881
+ lastInjectedCallIds = [...replayedCallIds];
882
+ lastReasoningReplayScope = parsed._reasoningReplayScope;
832
883
  const tools = toolsToGeminiFormat(parsed, routedModelId);
833
884
 
834
885
  const body: Record<string, unknown> = { contents };
@@ -843,7 +894,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
843
894
  if (googleOptions?.cachedContent) body.cachedContent = googleOptions.cachedContent;
844
895
 
845
896
  const generationConfig: Record<string, unknown> = {};
846
- if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
897
+ const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
898
+ if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
847
899
  if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
848
900
  if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
849
901
  if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
@@ -1019,8 +1071,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1019
1071
  if (provider.googleMode === "ai-studio-web") {
1020
1072
  const base = (provider.baseUrl || "https://alkalimakersuite-pa.clients6.google.com").replace(/\/+$/, "");
1021
1073
  const url = `${base}/v1internal:${method}${streamParam}`;
1022
- const cookieInput = provider.apiKey || provider.headers?.["Cookie"] || cookieHeaderFromSession(loadAiStudioSession()) || "";
1023
- const jar = parseGoogleCookieJar(cookieInput);
1074
+ const credentials = resolveAiStudioCredentials(provider);
1075
+ if (credentials.kind !== "ready") throw new Error(credentials.reason);
1076
+ const jar = parseGoogleCookieJar(credentials.cookieHeader);
1024
1077
  const aiStudioHeaders = await buildAiStudioHeaders(jar, "https://aistudio.google.com");
1025
1078
  Object.assign(headers, aiStudioHeaders);
1026
1079
  const compiled = compileGoogleWireBody({ ...body, model: routedModelId });
@@ -1051,6 +1104,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1051
1104
  const emitInTurnGroundingSources = emitInTurnGroundingSourcesQueue.shift() ?? false;
1052
1105
  const filterCcaSearchSuggestionHtml =
1053
1106
  provider.googleMode === "cloud-code-assist" && emitInTurnGroundingSources;
1107
+ if (provider.googleMode === "ai-studio-web" && (response.status === 401 || response.status === 403 || (response.status >= 300 && response.status < 400))) {
1108
+ try { await response.body?.cancel(); } catch { /* ignore */ }
1109
+ yield { type: "error", message: "Google AI Studio session expired — re-authentication required" };
1110
+ return;
1111
+ }
1054
1112
  if (!response.body) {
1055
1113
  yield { type: "error", message: "No response body" };
1056
1114
  return;
@@ -1063,6 +1121,17 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1063
1121
  const reader = response.body.getReader();
1064
1122
  const decoder = new TextDecoder();
1065
1123
  const budgetEncoder = new TextEncoder();
1124
+ const contentType = response.headers.get("content-type") ?? "";
1125
+ const isHtmlContentType = contentType.toLowerCase().includes("text/html");
1126
+ const isHtmlRedirect = (text: string) => {
1127
+ const lower = text.trim().toLowerCase();
1128
+ return lower.startsWith("<!doctype") || lower.includes("accounts.google.com/v3/signin");
1129
+ };
1130
+ const reauthError = "Google AI Studio session expired — re-authentication required";
1131
+ if (isHtmlContentType) {
1132
+ yield { type: "error", message: reauthError };
1133
+ return;
1134
+ }
1066
1135
  let buffer = "";
1067
1136
  let bufferBytes = 0;
1068
1137
  // Raw unterminated-line bytes, independent of TextDecoder's pending UTF-8
@@ -1124,15 +1193,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1124
1193
  if (provider.googleMode === "cloud-code-assist" && error) observeProviderError?.(error);
1125
1194
  const err = { ...(error ?? {}), message: error?.message ?? safeMessage ?? "upstream error" };
1126
1195
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
1127
- // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
1128
- const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
1129
- const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1130
- if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1131
- && replayModel && replaySession
1132
- && !/missing.*thought_signature/i.test(err?.message ?? "")
1133
- && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
1134
- clearAntigravityReplay(replayModel, replaySession);
1135
- }
1196
+ // Drop the cache entry and durable store entry for rejected calls so the next turn
1197
+ // starts clean instead of re-injecting a bad sig.
1198
+ handleSignatureRejection(err?.message);
1136
1199
  yield {
1137
1200
  type: "error",
1138
1201
  ...(error?.status !== undefined ? { status: error.status } : {}),
@@ -1321,6 +1384,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1321
1384
  }
1322
1385
  incompleteLineBytes = lineScan.residual;
1323
1386
  const nextBuffer = buffer + decoder.decode(value, { stream: true });
1387
+ if (isHtmlRedirect(nextBuffer)) {
1388
+ yield { type: "error", message: reauthError };
1389
+ try { await reader.cancel(); } catch { /* ignore */ }
1390
+ return;
1391
+ }
1324
1392
  const nextBufferBytes = budgetEncoder.encode(nextBuffer).byteLength;
1325
1393
  const appendReservation = budget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
1326
1394
  buffer = nextBuffer;
@@ -1384,6 +1452,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1384
1452
  return;
1385
1453
  }
1386
1454
  }
1455
+ if (isHtmlContentType || isHtmlRedirect(residual)) {
1456
+ yield { type: "error", message: reauthError };
1457
+ return;
1458
+ }
1387
1459
  yield { type: "error", message: `upstream non-SSE response: ${residual.slice(0, 300)}` };
1388
1460
  return;
1389
1461
  } else if ((yield* handleDataLine(residual)) === "terminate") return;
@@ -1509,7 +1581,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1509
1581
  let raw: Record<string, unknown>;
1510
1582
  let rawBytes = 0;
1511
1583
  try {
1512
- raw = JSON.parse(rawText) as Record<string, unknown>;
1584
+ const parsedRaw: unknown = JSON.parse(rawText);
1585
+ // `JSON.parse("null")` returns null instead of throwing, so the catch below cannot see it
1586
+ // and the `raw.error` read crashed the turn — #1219 at the buffered body root, which #1240
1587
+ // never reached because that audit swept SSE frame parsers only. There is no next frame to
1588
+ // recover into here, so unlike a stream frame this fails closed, matching the
1589
+ // unparseable-body branch just below and the buffered candidate guards added in #2232.
1590
+ if (!isGoogleRecord(parsedRaw)) {
1591
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
1592
+ const valueType = googleStructuralValueType(parsedRaw);
1593
+ return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
1594
+ }
1595
+ raw = parsedRaw;
1513
1596
  rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
1514
1597
  const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
1515
1598
  rawReservation.commitRetained();
@@ -1533,16 +1616,24 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1533
1616
  message: safeMessage,
1534
1617
  });
1535
1618
  if (error) observeProviderError?.(error);
1619
+ const message = error?.message ?? safeMessage ?? "upstream error";
1620
+ handleSignatureRejection(message);
1536
1621
  return finish([{
1537
1622
  type: "error",
1538
1623
  ...(error?.status !== undefined ? { status: error.status } : {}),
1539
1624
  ...(error?.code ? { code: error.code } : {}),
1540
- message: error?.message ?? safeMessage ?? "upstream error",
1625
+ message,
1541
1626
  }]);
1542
1627
  }
1543
- const json = (provider.googleMode === "cloud-code-assist" && raw.response && typeof raw.response === "object" && !Array.isArray(raw.response))
1544
- ? (raw.response as Record<string, unknown>)
1545
- : raw;
1628
+ // Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
1629
+ let json = raw;
1630
+ if (provider.googleMode === "cloud-code-assist") {
1631
+ const wrapped = raw.response;
1632
+ if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
1633
+ return finish([{ type: "error", message: "google-antigravity response missing response wrapper" }]);
1634
+ }
1635
+ json = wrapped as Record<string, unknown>;
1636
+ }
1546
1637
  const events: AdapterEvent[] = [];
1547
1638
 
1548
1639
  const rawCandidates: unknown = json.candidates;
@@ -18,6 +18,6 @@ export function parseDataUrl(url: string): { mediaType: string; base64: string }
18
18
  */
19
19
  export function contentPartsToText(content: string | OcxContentPart[]): string {
20
20
  if (typeof content === "string") return content;
21
- const text = content.map(p => (p.type === "text" ? p.text : "[image]")).join("");
21
+ const text = content.map(p => p.type === "text" ? p.text : p.type === "image" ? "[image]" : "[video]").join("");
22
22
  return text || "[image]";
23
23
  }
@@ -1,4 +1,19 @@
1
1
  export const KIRO_COMPLETION_TOOL_NAME = "codex_kiro_final_answer";
2
+
3
+ /**
4
+ * Request-scoped CodeWhisperer service profile for AWS Builder ID accounts.
5
+ *
6
+ * Builder ID is a personal identity with no AWS account behind it, so AWS never mints an
7
+ * account-scoped `profile/<id>` ARN for it. The Kiro CLI resolves this the same way: it carries
8
+ * this fixed service profile on Builder ID requests. The embedded account id is Amazon's own, not
9
+ * the user's, which is why sending it is not the same as synthesizing an account identity.
10
+ *
11
+ * Request-scoped is load-bearing. This value must never be persisted into `KiroOAuthMetadata`,
12
+ * never seed region inference (it is `us-east-1` and would pin every Builder ID account there),
13
+ * and never participate in account matching.
14
+ */
15
+ export const KIRO_BUILDER_ID_SERVICE_PROFILE_ARN =
16
+ "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
2
17
  export const KIRO_CONTINUATION_MESSAGE =
3
18
  "Continue from the prior conversation. Do not quote or mention this instruction.";
4
19
  export const KIRO_COMPLETION_RETRY_MESSAGE =
@@ -1,6 +1,7 @@
1
1
  import type { OcxParsedRequest, OcxTool } from "../types";
2
2
  import { namespacedToolName } from "../types";
3
3
  import { normalizeKiroModelId } from "../providers/kiro-models";
4
+ import { isCodexCodeModeExecTool } from "./tool-catalog-nudge";
4
5
  import { createKiroToolNameRegistry, type KiroToolNameRegistry } from "./kiro-wire";
5
6
 
6
7
  const MAX_KIRO_TOOL_DESCRIPTION_UNVERIFIED = 1024;
@@ -103,7 +104,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
103
104
  // Seed with the root's own properties/required so a schema like
104
105
  // { type:"object", properties:{path}, required:["path"], oneOf:[...] } keeps them.
105
106
  if (obj.properties && typeof obj.properties === "object") {
106
- Object.assign(props, sanitizeKiroSchema(obj.properties) as Record<string, unknown>);
107
+ Object.assign(props, sanitizeSchemaMap(obj.properties) as Record<string, unknown>);
107
108
  }
108
109
  if (Array.isArray(obj.required)) {
109
110
  for (const r of obj.required) if (typeof r === "string") required.add(r);
@@ -118,7 +119,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
118
119
  if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
119
120
  const v = variant as Record<string, unknown>;
120
121
  if (v.properties && typeof v.properties === "object") {
121
- Object.assign(props, sanitizeKiroSchema(v.properties) as Record<string, unknown>);
122
+ Object.assign(props, sanitizeSchemaMap(v.properties) as Record<string, unknown>);
122
123
  }
123
124
  if (mergeRequired && Array.isArray(v.required)) {
124
125
  for (const r of v.required) if (typeof r === "string") required.add(r);
@@ -174,8 +175,14 @@ function omittedToolCatalogNotice(kept: number, omitted: readonly OcxTool[], reg
174
175
 
175
176
  function boundedCatalogPriority(tool: OcxTool): number {
176
177
  if (tool.loadedFromToolSearch) return 0;
177
- if (tool.toolSearch) return 1;
178
- return 2;
178
+ // Codex code mode reaches shell, file edits, apply_patch and every MCP helper ONLY as nested
179
+ // `tools.<name>(...)` calls inside this one tool. Dropping it does not shrink the catalog, it
180
+ // makes the rest of the catalog uncallable -- so it outranks the search gateway and filler.
181
+ // It stays BEHIND `loadedFromToolSearch` because those are tools the model asked for by name
182
+ // this turn (#2475). Cursor pins its execution path the same way (request-builder.ts, #399).
183
+ if (isCodexCodeModeExecTool(tool)) return 1;
184
+ if (tool.toolSearch) return 2;
185
+ return 3;
179
186
  }
180
187
 
181
188
  export function convertKiroToolContext(
@@ -209,19 +216,40 @@ export function convertKiroToolContext(
209
216
  const candidates = exceedsBudget
210
217
  ? convertedEntries.toSorted((a, b) => boundedCatalogPriority(a.tool) - boundedCatalogPriority(b.tool) || a.index - b.index)
211
218
  : convertedEntries;
212
- const convertedTools: unknown[] = [];
213
- let omittedAt = candidates.length;
214
- for (const [index, entry] of candidates.entries()) {
219
+ // Reserve a seat for the code-mode execution path before filling the rest.
220
+ //
221
+ // Priority alone cannot save it. `loadedFromToolSearch` tools outrank it and arrive unbounded
222
+ // (the Responses parser pushes every `tool_search_output` spec), so a session that accumulated
223
+ // MAX_KIRO_TOOL_COUNT loaded tools would exhaust the budget before reaching tier 1 and drop the
224
+ // one tool through which all of them are actually callable.
225
+ //
226
+ // Reservation rather than eviction: this lowers the room the fill loop sees, so it admits one
227
+ // fewer tool. It never removes a tool that already fit, which is what keeps #2475's loaded-result
228
+ // guarantee intact -- Cursor's `evictNonExecutionPath` exempts only the execution path and could
229
+ // evict a loaded tool instead.
230
+ const reserved = candidates.find(entry => isCodexCodeModeExecTool(entry.tool));
231
+ const admitted = new Set<number>();
232
+ const filled: unknown[] = [];
233
+ for (const entry of candidates) {
234
+ if (entry === reserved) continue;
235
+ // Measure the projected FINAL array: the byte budget is computed over the serialized array, so
236
+ // subtracting a standalone size would misjudge it by the separators JSON adds between entries.
237
+ const projected = reserved ? [...filled, entry.converted, reserved.converted] : [...filled, entry.converted];
215
238
  if (
216
- convertedTools.length >= MAX_KIRO_TOOL_COUNT
217
- || serializedToolCatalogBytes([...convertedTools, entry.converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
218
- ) {
219
- omittedAt = index;
220
- break;
221
- }
222
- convertedTools.push(entry.converted);
239
+ projected.length > MAX_KIRO_TOOL_COUNT
240
+ || serializedToolCatalogBytes(projected) > MAX_KIRO_TOOL_CATALOG_BYTES
241
+ ) break;
242
+ filled.push(entry.converted);
243
+ admitted.add(entry.index);
223
244
  }
224
- const omittedTools = candidates.slice(omittedAt).map(entry => entry.tool);
245
+ if (reserved) admitted.add(reserved.index);
246
+ // Rebuild in sorted-candidate order so the wire order stays loaded -> exec -> gateway -> filler.
247
+ // Pushing the reserved entry after the loop would place it last instead.
248
+ const convertedTools = candidates.filter(entry => admitted.has(entry.index)).map(entry => entry.converted);
249
+ // Derive omissions by set difference. The old `candidates.slice(omittedAt)` assumed every
250
+ // candidate after the first rejection was omitted, which stops being true once one of them was
251
+ // reserved and admitted: the notice would name `exec` unavailable while it is on the wire.
252
+ const omittedTools = candidates.filter(entry => !admitted.has(entry.index)).map(entry => entry.tool);
225
253
  return {
226
254
  tools: convertedTools,
227
255
  systemAdditions: omittedTools.length > 0 ? [omittedToolCatalogNotice(convertedTools.length, omittedTools, registry)] : [],