@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -1,7 +1,8 @@
1
- import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
1
+ import type { AdapterFetchContext, AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
2
2
  import { debugDroppedFrame } from "../lib/debug";
3
3
  import { createToolCallIdAllocator } from "./tool-call-id";
4
4
  import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
5
+ import { OcxRequestValidationError } from "../lib/errors";
5
6
  import type {
6
7
  AdapterEvent,
7
8
  OcxAssistantMessage,
@@ -14,13 +15,14 @@ import type {
14
15
  OcxToolResultMessage,
15
16
  OcxUsage,
16
17
  } from "../types";
17
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
18
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
18
19
  import { contentPartsToText, parseDataUrl } from "./image";
19
20
  import { getVertexAccessToken } from "../lib/gcp-adc";
20
21
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
21
22
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
22
- import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
23
- import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
23
+ import { sanitizeUpstreamErrorText } from "./upstream-http-error";
24
+ import { googleTruncationErrorMessage, isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-truncation";
25
+ import { ANTIGRAVITY_REQUEST_UA, ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
24
26
  import { repairGoogleToolPairs, stripTrailingClaudePrefill } from "./google-antigravity-tools";
25
27
  import { canonicalAntigravityHttpsHost, isAntigravityHttpsHost } from "./google-antigravity-hosts";
26
28
  import { compileGoogleWireBody } from "./google-wire-compiler";
@@ -45,6 +47,16 @@ import {
45
47
  } from "../lib/translator-budget";
46
48
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
47
49
  import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";
50
+ import { normalizeAntigravityProviderError } from "../oauth/antigravity-routing";
51
+
52
+ const INLINE_ERROR_URL_USERINFO = /https?:\/\/[^\s"'<>]*@/gi;
53
+
54
+ function safeAntigravityInlineErrorMessage(value: unknown): string | undefined {
55
+ if (typeof value !== "string") return undefined;
56
+ return sanitizeUpstreamErrorText(value)
57
+ .replace(INLINE_ERROR_URL_USERINFO, "[REDACTED_URL]")
58
+ .slice(0, 500);
59
+ }
48
60
 
49
61
  // Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
50
62
  // tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
@@ -139,6 +151,19 @@ const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
139
151
  const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
140
152
  const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
141
153
 
154
+ function appendGeminiContent(
155
+ contents: unknown[],
156
+ next: { role: string; parts: unknown[] },
157
+ mergeAdjacentUsers = true,
158
+ ): void {
159
+ const previous = contents.at(-1) as { role?: unknown; parts?: unknown[] } | undefined;
160
+ if (mergeAdjacentUsers && previous?.role === "user" && next.role === "user" && Array.isArray(previous.parts)) {
161
+ previous.parts.push(...next.parts);
162
+ } else {
163
+ contents.push(next);
164
+ }
165
+ }
166
+
142
167
  /** A Gemini text part, or undefined when the value cannot form a valid non-empty text block. */
143
168
  function geminiTextPart(text: unknown): { text: string } | undefined {
144
169
  return typeof text === "string" && text.length > 0 ? { text } : undefined;
@@ -208,6 +233,11 @@ function messagesToGeminiFormat(
208
233
  const systemInstruction = { parts: [{ text: systemText }] };
209
234
 
210
235
  const contents: unknown[] = [];
236
+ let userMergeBarrier = false;
237
+ const appendContent = (next: { role: string; parts: unknown[] }): void => {
238
+ appendGeminiContent(contents, next, !userMergeBarrier);
239
+ userMergeBarrier = false;
240
+ };
211
241
  const messages = repairGoogleToolPairs(parsed.context.messages, { dropUnmatchedCalls: repairToolPairs });
212
242
 
213
243
  const callIds = createToolCallIdAllocator();
@@ -226,7 +256,7 @@ function messagesToGeminiFormat(
226
256
  case "user":
227
257
  case "developer": {
228
258
  if (typeof msg.content === "string") {
229
- contents.push({ role: "user", parts: [{ text: msg.content || GEMINI_EMPTY_PLACEHOLDER }] });
259
+ appendContent({ role: "user", parts: [{ text: msg.content || GEMINI_EMPTY_PLACEHOLDER }] });
230
260
  } else {
231
261
  const parts: unknown[] = [];
232
262
  for (const p of msg.content as OcxContentPart[]) {
@@ -241,7 +271,7 @@ function messagesToGeminiFormat(
241
271
  const textPart = geminiTextPart(p.text);
242
272
  if (textPart) parts.push(textPart);
243
273
  }
244
- contents.push({ role: "user", parts: parts.length > 0 ? parts : [{ text: GEMINI_EMPTY_PLACEHOLDER }] });
274
+ appendContent({ role: "user", parts: parts.length > 0 ? parts : [{ text: GEMINI_EMPTY_PLACEHOLDER }] });
245
275
  }
246
276
  break;
247
277
  }
@@ -291,8 +321,11 @@ function messagesToGeminiFormat(
291
321
  // A turn with nothing Gemini can represent (e.g. thinking-only) would serialize as
292
322
  // `parts: []`, which the Anthropic translation rejects. Skip it, as the Anthropic
293
323
  // adapter does for its own empty assistant content.
294
- if (parts.length === 0) break;
295
- contents.push({ role: "model", parts });
324
+ if (parts.length === 0) {
325
+ userMergeBarrier = true;
326
+ break;
327
+ }
328
+ appendContent({ role: "model", parts });
296
329
  if (toolCalls.length > 0) {
297
330
  // Gemini/Claude-on-Antigravity requires one adjacent response batch for the whole
298
331
  // function-call turn. Replayed histories can be interrupted, reversed, duplicated, or
@@ -321,7 +354,7 @@ function messagesToGeminiFormat(
321
354
  for (const orphan of orphanResults) {
322
355
  responseParts.push(...geminiOrphanToolResultParts(orphan));
323
356
  }
324
- contents.push({ role: "user", parts: responseParts });
357
+ appendContent({ role: "user", parts: responseParts });
325
358
  i = j - 1;
326
359
  }
327
360
  break;
@@ -332,7 +365,7 @@ function messagesToGeminiFormat(
332
365
  // batch never reach here (the assistant branch consumes them). Standalone or
333
366
  // barrier-delayed results still have to stay visible — especially image-bearing
334
367
  // screenshots — as explicit user text rather than vanishing or 400ing CCA.
335
- contents.push({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
368
+ appendContent({ role: "user", parts: geminiOrphanToolResultParts(msg as OcxToolResultMessage) });
336
369
  break;
337
370
  }
338
371
  }
@@ -346,11 +379,8 @@ function toolsToGeminiFormat(
346
379
  wireModelId: string,
347
380
  ): unknown[] | undefined {
348
381
  const grounding = parsed._ccaInTurnGrounding;
349
- const allowed = isAllowedToolChoice(parsed.options.toolChoice)
350
- ? new Set(parsed.options.toolChoice.allowedTools)
351
- : undefined;
352
- const tools = allowed
353
- ? parsed.context.tools?.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools)) ?? []
382
+ const tools = isAllowedToolChoice(parsed.options.toolChoice)
383
+ ? parsed.context.tools?.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools)) ?? []
354
384
  : parsed.context.tools ?? [];
355
385
  const functionDeclarations = tools.map(t => ({
356
386
  name: namespacedToolName(t.namespace, t.name),
@@ -398,6 +428,13 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
398
428
  };
399
429
  }
400
430
 
431
+ function googlePromptFeedbackError(root: Record<string, unknown>): Extract<AdapterEvent, { type: "error" }> | undefined {
432
+ const feedback = root.promptFeedback;
433
+ if (!isGoogleRecord(feedback) || typeof feedback.blockReason !== "string" || !feedback.blockReason.trim()) return undefined;
434
+ const reason = sanitizeUpstreamErrorText(feedback.blockReason).slice(0, 160);
435
+ return { type: "error", message: `google response blocked by prompt feedback: ${reason}` };
436
+ }
437
+
401
438
  /**
402
439
  * Cap on the buffered non-streaming response body (100 MiB), matching
403
440
  * IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
@@ -675,6 +712,7 @@ function invalidGoogleShapeEvent(
675
712
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
676
713
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
677
714
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
715
+ let observeProviderError: IncomingMeta["onProviderError"];
678
716
  let antigravityModel: string | undefined;
679
717
  let antigravitySession: string | undefined;
680
718
  // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
@@ -684,8 +722,23 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
684
722
  let vertexReplaySession: string | undefined;
685
723
  let restoreGoogleToolName = (name: string): string => name;
686
724
  const emitInTurnGroundingSourcesQueue: boolean[] = [];
725
+ const truncationErrorMessage = provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
726
+ ? vertexTruncationErrorMessage
727
+ : googleTruncationErrorMessage;
687
728
  return {
688
729
  name: "google",
730
+ validateRequest(parsed: OcxParsedRequest) {
731
+ if (provider.googleMode === "cloud-code-assist" && parsed.options.providerOptions?.google) {
732
+ throw new OcxRequestValidationError("provider_options.google is not supported on Google Cloud Code Assist routes");
733
+ }
734
+ const googleOptions = parsed.options.providerOptions?.google;
735
+ if (isImageCapableModel(parsed.modelId)
736
+ && (googleOptions?.thinkingBudget !== undefined || googleOptions?.includeThoughts !== undefined)) {
737
+ throw new OcxRequestValidationError(
738
+ "provider_options.google thinking_budget and include_thoughts are not supported for image-capable Gemini models",
739
+ );
740
+ }
741
+ },
689
742
 
690
743
  // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
691
744
  // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
@@ -700,7 +753,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
700
753
  }
701
754
  : {}),
702
755
 
703
- async buildRequest(parsed: OcxParsedRequest) {
756
+ async buildRequest(parsed: OcxParsedRequest, options?: IncomingMeta) {
757
+ observeProviderError = options?.onProviderError;
704
758
  const routedModelId = provider.googleMode === "cloud-code-assist"
705
759
  ? resolveAntigravityEffortWireModel(
706
760
  parsed.modelId,
@@ -726,6 +780,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
726
780
  // catalog is a guaranteed upstream 400.
727
781
  const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
728
782
  if (toolConfig) body.toolConfig = toolConfig;
783
+ const googleOptions = parsed.options.providerOptions?.google;
784
+ if (googleOptions?.safetySettings) body.safetySettings = googleOptions.safetySettings;
785
+ if (googleOptions?.cachedContent) body.cachedContent = googleOptions.cachedContent;
729
786
 
730
787
  const generationConfig: Record<string, unknown> = {};
731
788
  if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
@@ -747,7 +804,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
747
804
  const thinkingLevel = thinkingEligible
748
805
  ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
749
806
  : undefined;
750
- if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
807
+ if (!isImageCapableModel(parsed.modelId)) {
808
+ const thinkingConfig: Record<string, unknown> = {};
809
+ if (googleOptions?.thinkingBudget !== undefined) thinkingConfig.thinkingBudget = googleOptions.thinkingBudget;
810
+ else if (thinkingLevel) thinkingConfig.thinkingLevel = thinkingLevel;
811
+ if (googleOptions?.includeThoughts !== undefined) thinkingConfig.includeThoughts = googleOptions.includeThoughts;
812
+ if (Object.keys(thinkingConfig).length > 0) generationConfig.thinkingConfig = thinkingConfig;
813
+ }
751
814
  if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
752
815
  generationConfig.responseModalities = ["TEXT", "IMAGE"];
753
816
  }
@@ -819,6 +882,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
819
882
  const strippedModelTail = /claude/i.test(wireModelId) ? stripTrailingClaudePrefill(contents) : false;
820
883
  if (antigravityUsesReplayCache(wireModelId)) {
821
884
  applyAntigravityReplay(wireModelId, sessionId, contents);
885
+ // If any functionCall still lacks a thoughtSignature on Gemini Antigravity,
886
+ // supply the bypass sentinel so Antigravity does not reject the turn with HTTP 400.
887
+ for (const c of contents as { role?: string; parts?: unknown[] }[]) {
888
+ if (c?.role !== "model" || !Array.isArray(c.parts)) continue;
889
+ for (const p of c.parts) {
890
+ if (p && typeof p === "object") {
891
+ const partObj = p as Record<string, unknown>;
892
+ if (partObj.functionCall && !partObj.thoughtSignature && !partObj.thought_signature) {
893
+ partObj.thoughtSignature = ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL;
894
+ }
895
+ }
896
+ }
897
+ }
822
898
  } else {
823
899
  sanitizeAntigravityClaudeSignatures(contents);
824
900
  }
@@ -973,17 +1049,31 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
973
1049
 
974
1050
  // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
975
1051
  if (chunk.error) {
976
- const err = chunk.error as { message?: string } | undefined;
1052
+ const rawError = chunk.error as { code?: unknown; status?: unknown; message?: unknown };
1053
+ const safeMessage = safeAntigravityInlineErrorMessage(rawError.message);
1054
+ const error = normalizeAntigravityProviderError({
1055
+ code: rawError.code,
1056
+ status: rawError.status,
1057
+ message: safeMessage,
1058
+ });
1059
+ if (provider.googleMode === "cloud-code-assist" && error) observeProviderError?.(error);
1060
+ const err = { ...(error ?? {}), message: error?.message ?? safeMessage ?? "upstream error" };
977
1061
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
978
1062
  // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
979
1063
  const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
980
1064
  const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
981
1065
  if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
982
1066
  && replayModel && replaySession
1067
+ && !/missing.*thought_signature/i.test(err?.message ?? "")
983
1068
  && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
984
1069
  clearAntigravityReplay(replayModel, replaySession);
985
1070
  }
986
- yield { type: "error", message: err?.message ?? "upstream error" };
1071
+ yield {
1072
+ type: "error",
1073
+ ...(error?.status !== undefined ? { status: error.status } : {}),
1074
+ ...(error?.code ? { code: error.code } : {}),
1075
+ message: err.message ?? "upstream error",
1076
+ };
987
1077
  return "terminate";
988
1078
  }
989
1079
 
@@ -1013,7 +1103,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1013
1103
  // absent key and an empty array are already skipped here; `null` joins them. A non-null
1014
1104
  // non-array container is still claimed structure the parser cannot read, and stays
1015
1105
  // terminal.
1016
- if (rawCandidates === undefined || rawCandidates === null) return "continue";
1106
+ if (rawCandidates === undefined || rawCandidates === null) {
1107
+ const feedbackError = googlePromptFeedbackError(root);
1108
+ if (feedbackError) {
1109
+ yield feedbackError;
1110
+ return "terminate";
1111
+ }
1112
+ return "continue";
1113
+ }
1017
1114
  if (!Array.isArray(rawCandidates)) {
1018
1115
  yield invalidGoogleShapeEvent({
1019
1116
  reason: "candidates_not_array",
@@ -1021,7 +1118,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1021
1118
  });
1022
1119
  return "terminate";
1023
1120
  }
1024
- if (rawCandidates.length === 0) return "continue";
1121
+ if (rawCandidates.length === 0) {
1122
+ const feedbackError = googlePromptFeedbackError(root);
1123
+ if (feedbackError) {
1124
+ yield feedbackError;
1125
+ return "terminate";
1126
+ }
1127
+ return "continue";
1128
+ }
1025
1129
  const rawCandidate = rawCandidates[0];
1026
1130
  if (!isGoogleRecord(rawCandidate)) {
1027
1131
  // Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
@@ -1077,7 +1181,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1077
1181
  const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
1078
1182
  if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
1079
1183
  && parts && replayModel && replaySession) {
1080
- pendingStreamThoughtSig = observeAntigravityReplay(
1184
+ // Observation may scan the whole frame, so use it only for replay-cache side effects.
1185
+ // The source-order loop below exclusively owns stream carry and cannot pair backwards.
1186
+ observeAntigravityReplay(
1081
1187
  replayModel,
1082
1188
  replaySession,
1083
1189
  parts as unknown[],
@@ -1087,7 +1193,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1087
1193
  if (parts) {
1088
1194
  for (const part of parts) {
1089
1195
  const sig = googlePartThoughtSignature(part);
1090
- if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1196
+ if (sig && isLikelyRealThoughtSignature(sig) && (part.thought === true || !part.functionCall)) {
1091
1197
  pendingStreamThoughtSig = sig;
1092
1198
  }
1093
1199
  const textEvent = googlePartTextEvent(part, filterCcaSearchSuggestionHtml);
@@ -1192,9 +1298,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1192
1298
  }
1193
1299
  // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
1194
1300
  // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
1195
- if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
1196
- && isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
1197
- yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
1301
+ if (isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
1302
+ yield { type: "error", message: truncationErrorMessage(lastFinishReason) };
1198
1303
  return;
1199
1304
  }
1200
1305
  if (!sawAnyFrame || !sawTerminalSignal) {
@@ -1238,7 +1343,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1238
1343
  // Cloud Code Assist exposes only the SSE transport. Unary callers still use this
1239
1344
  // buffered adapter entry point, so collect the exact same events parseStream emits
1240
1345
  // instead of maintaining a second CCA JSON parser.
1241
- if (provider.googleMode === "cloud-code-assist") {
1346
+ const isSse = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
1347
+ if (provider.googleMode === "cloud-code-assist" && isSse) {
1242
1348
  const events: AdapterEvent[] = [];
1243
1349
  let previousTail: AdapterEvent | undefined;
1244
1350
  try {
@@ -1327,10 +1433,24 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1327
1433
  return events;
1328
1434
  };
1329
1435
  if (raw.error) {
1330
- const err = raw.error as { message?: string };
1331
- return finish([{ type: "error", message: err.message ?? "upstream error" }]);
1436
+ const rawError = raw.error as { code?: unknown; status?: unknown; message?: unknown };
1437
+ const safeMessage = safeAntigravityInlineErrorMessage(rawError.message);
1438
+ const error = normalizeAntigravityProviderError({
1439
+ code: rawError.code,
1440
+ status: rawError.status,
1441
+ message: safeMessage,
1442
+ });
1443
+ if (error) observeProviderError?.(error);
1444
+ return finish([{
1445
+ type: "error",
1446
+ ...(error?.status !== undefined ? { status: error.status } : {}),
1447
+ ...(error?.code ? { code: error.code } : {}),
1448
+ message: error?.message ?? safeMessage ?? "upstream error",
1449
+ }]);
1332
1450
  }
1333
- const json = raw;
1451
+ const json = (provider.googleMode === "cloud-code-assist" && raw.response && typeof raw.response === "object" && !Array.isArray(raw.response))
1452
+ ? (raw.response as Record<string, unknown>)
1453
+ : raw;
1334
1454
  const events: AdapterEvent[] = [];
1335
1455
 
1336
1456
  const rawCandidates: unknown = json.candidates;
@@ -1346,6 +1466,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1346
1466
  }
1347
1467
  const candidates = rawCandidates as { finishReason?: string }[] | undefined;
1348
1468
  if (!candidates?.length) {
1469
+ const feedbackError = googlePromptFeedbackError(json);
1470
+ if (feedbackError) return finish([feedbackError]);
1349
1471
  return finish([{ type: "error", message: "google response contained no candidates" }]);
1350
1472
  }
1351
1473
  const rawCandidate: unknown = candidates[0];
@@ -1418,9 +1540,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1418
1540
 
1419
1541
  // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
1420
1542
  // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
1421
- if (provider.googleMode === "vertex"
1422
- && isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1423
- return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
1543
+ if (isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1544
+ return finish([{ type: "error", message: truncationErrorMessage(candidate.finishReason) }]);
1424
1545
  }
1425
1546
 
1426
1547
  const usage = json.usageMetadata as Record<string, number> | undefined;
@@ -172,6 +172,12 @@ function omittedToolCatalogNotice(kept: number, omitted: readonly OcxTool[], reg
172
172
  return `[opencodex] Kiro's outbound catalog budget allows ${kept} of ${kept + omitted.length} client tools this turn. Omitted and unavailable this turn: ${summary}.`;
173
173
  }
174
174
 
175
+ function boundedCatalogPriority(tool: OcxTool): number {
176
+ if (tool.loadedFromToolSearch) return 0;
177
+ if (tool.toolSearch) return 1;
178
+ return 2;
179
+ }
180
+
175
181
  export function convertKiroToolContext(
176
182
  parsed: OcxParsedRequest,
177
183
  registry: KiroToolNameRegistry = createKiroToolNameRegistry(),
@@ -181,9 +187,7 @@ export function convertKiroToolContext(
181
187
  // Validate every listed name even when tool_choice:none emulates a tool-free turn.
182
188
  for (const tool of tools) registry.alias(namespacedToolName(tool.namespace, tool.name));
183
189
  const effectiveTools = parsed.options.toolChoice === "none" ? [] : tools;
184
- const convertedTools: unknown[] = [];
185
- let omittedAt = effectiveTools.length;
186
- for (const [index, tool] of effectiveTools.entries()) {
190
+ const convertedEntries = effectiveTools.map((tool, index) => {
187
191
  const description = tool.description || `Tool: ${tool.name}`;
188
192
  // Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
189
193
  // it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
@@ -198,19 +202,26 @@ export function convertKiroToolContext(
198
202
  inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(tool.parameters ?? {})) },
199
203
  },
200
204
  };
201
- // Preserve declaration order and only omit a suffix. Ranking tools would make a catalog change
202
- // silently alter which capability disappears; this deterministic policy is paired with a
203
- // model-visible omission notice so unavailable tools are explicit rather than assumed absent.
205
+ return { tool, index, converted };
206
+ });
207
+ const exceedsBudget = convertedEntries.length > MAX_KIRO_TOOL_COUNT
208
+ || serializedToolCatalogBytes(convertedEntries.map(entry => entry.converted)) > MAX_KIRO_TOOL_CATALOG_BYTES;
209
+ const candidates = exceedsBudget
210
+ ? convertedEntries.toSorted((a, b) => boundedCatalogPriority(a.tool) - boundedCatalogPriority(b.tool) || a.index - b.index)
211
+ : convertedEntries;
212
+ const convertedTools: unknown[] = [];
213
+ let omittedAt = candidates.length;
214
+ for (const [index, entry] of candidates.entries()) {
204
215
  if (
205
216
  convertedTools.length >= MAX_KIRO_TOOL_COUNT
206
- || serializedToolCatalogBytes([...convertedTools, converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
217
+ || serializedToolCatalogBytes([...convertedTools, entry.converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
207
218
  ) {
208
219
  omittedAt = index;
209
220
  break;
210
221
  }
211
- convertedTools.push(converted);
222
+ convertedTools.push(entry.converted);
212
223
  }
213
- const omittedTools = effectiveTools.slice(omittedAt);
224
+ const omittedTools = candidates.slice(omittedAt).map(entry => entry.tool);
214
225
  return {
215
226
  tools: convertedTools,
216
227
  systemAdditions: omittedTools.length > 0 ? [omittedToolCatalogNotice(convertedTools.length, omittedTools, registry)] : [],
@@ -318,9 +318,6 @@ function validateKiroCapabilities(parsed: OcxParsedRequest): void {
318
318
  if (choice !== undefined && choice !== "auto" && choice !== "none") {
319
319
  throw new Error("Kiro supports only automatic tool choice or tool_choice:none");
320
320
  }
321
- if (parsed.options.parallelToolCalls === true) {
322
- throw new Error("Kiro does not support parallel tool calls");
323
- }
324
321
  if (parsed.options.serviceTier !== undefined) {
325
322
  throw new Error("Kiro does not support service tiers");
326
323
  }
@@ -1244,6 +1244,15 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
1244
1244
  },
1245
1245
  }];
1246
1246
  });
1247
+ if (xaiTarget) {
1248
+ const omitted = tools.length - formatted.length;
1249
+ debugProviderDiagnostic("openai-chat", "tool-catalog", {
1250
+ declared: tools.length,
1251
+ emitted: formatted.length,
1252
+ omitted,
1253
+ ...(omitted > 0 ? { omissionCause: "xai_schema_not_lossless" } : {}),
1254
+ });
1255
+ }
1247
1256
  return formatted.length > 0 ? formatted : undefined;
1248
1257
  }
1249
1258
 
@@ -1684,8 +1684,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1684
1684
 
1685
1685
  const forward = provider.authMode === "forward";
1686
1686
  let convertedRoutedCustomToolNames: Set<string> | undefined;
1687
+ let routedCustomToolRepairNames: Set<string> | undefined;
1687
1688
  let convertedRoutedToolSearchNames: Set<string> | undefined;
1688
- let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
1689
+ let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
1689
1690
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
1690
1691
  let outBody = stripPreviousResponseId(
1691
1692
  parsed._rawBody,
@@ -1741,6 +1742,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1741
1742
  );
1742
1743
  outBody = rewritten.body;
1743
1744
  convertedRoutedCustomToolNames = rewritten.names;
1745
+ routedCustomToolRepairNames = rewritten.repairNames;
1744
1746
  }
1745
1747
  if (!isCanonicalOpenAiForwardProvider(provider)) {
1746
1748
  // Run after custom-tool lowering so the search compatibility layer can choose a
@@ -1811,6 +1813,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1811
1813
  body,
1812
1814
  releaseBodyObservation,
1813
1815
  ...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
1816
+ ...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
1814
1817
  ...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
1815
1818
  ...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
1816
1819
  ...(tierLog ? { tierLog } : {}),
@@ -120,7 +120,7 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
120
120
  "Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
121
121
  "Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
122
122
  verifiedCodeModeExecName
123
- ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names."
123
+ ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch` (no trailing `***` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated `*** Begin Patch ***` envelope is rejected by Codex before the file is touched."
124
124
  : "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
125
125
  unavailableNeighborNames.length > 0
126
126
  ? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
@@ -60,10 +60,15 @@ function normalizeToolGroup(tools: unknown[]): ToolGroupRewrite {
60
60
  : undefined;
61
61
  const enableImageSearch = searchContentTypes?.includes("image") === true;
62
62
  const next: Record<string, unknown> = { ...tool, type: CODEX_WEB_SEARCH_TOOL };
63
+ // Only the two fields xAI actually refuses are removed. Probed 2026-08-22, one field per
64
+ // request, against BOTH xAI destinations (api.x.ai and cli-chat-proxy.grok.com): they behave
65
+ // identically — `external_web_access` 400s on every value including `true`, and
66
+ // `search_context_size` 400s, while `user_location`, `search_content_types`, `filters` and
67
+ // `enable_image_search` are all accepted. Deleting the accepted ones was a silent capability
68
+ // loss, and it contradicted the sibling layer, whose own probe note already records
69
+ // user_location/filters as accepted (tests/responses-routed-web-search-fields.test.ts).
63
70
  delete next.external_web_access;
64
71
  delete next.search_context_size;
65
- delete next.search_content_types;
66
- delete next.user_location;
67
72
  if (enableImageSearch && !Object.hasOwn(next, "enable_image_search")) {
68
73
  next.enable_image_search = true;
69
74
  }