@yansigit/opencodex 2.32.0 → 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 (108) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  3. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +6 -5
  6. package/src/adapters/anthropic.ts +20 -6
  7. package/src/adapters/azure.ts +20 -4
  8. package/src/adapters/base.ts +3 -1
  9. package/src/adapters/command-code.ts +40 -7
  10. package/src/adapters/cursor/live-transport.ts +1 -1
  11. package/src/adapters/cursor/protobuf-events.ts +158 -7
  12. package/src/adapters/cursor/protobuf-request.ts +33 -15
  13. package/src/adapters/cursor/request-builder.ts +4 -3
  14. package/src/adapters/cursor/tool-definitions.ts +27 -1
  15. package/src/adapters/cursor/types.ts +4 -3
  16. package/src/adapters/cursor.ts +9 -0
  17. package/src/adapters/google-antigravity-replay.ts +2 -2
  18. package/src/adapters/google-antigravity-wire.ts +7 -0
  19. package/src/adapters/google-errors.ts +6 -2
  20. package/src/adapters/google-http.ts +30 -7
  21. package/src/adapters/google-truncation.ts +5 -0
  22. package/src/adapters/google-wire-compiler.ts +38 -6
  23. package/src/adapters/google.ts +148 -26
  24. package/src/adapters/kiro-tools.ts +20 -9
  25. package/src/adapters/openai-chat.ts +9 -0
  26. package/src/adapters/openai-responses.ts +1 -1
  27. package/src/bridge.ts +112 -9
  28. package/src/claude/context-windows.ts +16 -9
  29. package/src/cli/doctor.ts +2 -2
  30. package/src/cli/index.ts +9 -2
  31. package/src/cli/provider.ts +6 -0
  32. package/src/cli/status.ts +23 -0
  33. package/src/codex/auth-api.ts +4 -2
  34. package/src/codex/autostart-health.ts +16 -0
  35. package/src/codex/catalog/aggregation.ts +12 -12
  36. package/src/codex/catalog/effort.ts +18 -3
  37. package/src/codex/catalog/metadata.ts +27 -1
  38. package/src/codex/catalog/model-metadata.ts +39 -12
  39. package/src/codex/catalog/parsing.ts +38 -27
  40. package/src/codex/catalog/provider-fetch.ts +198 -133
  41. package/src/codex/catalog/sync.ts +1 -1
  42. package/src/codex/convergence.ts +5 -0
  43. package/src/codex/shim.ts +56 -3
  44. package/src/config/provider-validation.ts +37 -0
  45. package/src/config.ts +78 -2
  46. package/src/generated/compatibility-version.json +120 -96
  47. package/src/images/loop.ts +37 -6
  48. package/src/lib/azure-identity.ts +154 -0
  49. package/src/lib/debug.ts +42 -0
  50. package/src/lib/errors.ts +14 -0
  51. package/src/lib/provider-outbound.ts +45 -33
  52. package/src/lib/provider-tls-profile.ts +309 -0
  53. package/src/lib/proxy-env.ts +49 -0
  54. package/src/lib/redact.ts +10 -1
  55. package/src/oauth/antigravity-routing.ts +282 -236
  56. package/src/oauth/callback-server.ts +22 -2
  57. package/src/oauth/command-code.ts +5 -16
  58. package/src/oauth/google-antigravity.ts +42 -5
  59. package/src/oauth/index.ts +15 -3
  60. package/src/oauth/kimi.ts +9 -1
  61. package/src/oauth/open-browser-choice.ts +26 -0
  62. package/src/oauth/store.ts +6 -0
  63. package/src/providers/antigravity-quota.ts +3 -1
  64. package/src/providers/api-keys.ts +2 -1
  65. package/src/providers/auto-compact-budget.ts +65 -0
  66. package/src/providers/derive.ts +4 -0
  67. package/src/providers/key-failover.ts +5 -1
  68. package/src/providers/openai-tiers.ts +5 -0
  69. package/src/providers/provider-id-rewrite.ts +1 -0
  70. package/src/providers/quota.ts +59 -13
  71. package/src/providers/registry.ts +3 -1
  72. package/src/providers/request-pacing.ts +33 -6
  73. package/src/providers/xai-transport.ts +21 -0
  74. package/src/responses/google-provider-options.ts +36 -0
  75. package/src/responses/namespace-tool-compat.ts +84 -4
  76. package/src/responses/parser.ts +11 -0
  77. package/src/responses/provider-opaque-metadata.ts +3 -3
  78. package/src/responses/schema.ts +37 -0
  79. package/src/responses/state.ts +94 -4
  80. package/src/router.ts +8 -2
  81. package/src/server/auth-cors.ts +28 -0
  82. package/src/server/images.ts +19 -35
  83. package/src/server/management/agent-settings-routes.ts +205 -15
  84. package/src/server/management/combo-routes.ts +6 -0
  85. package/src/server/management/config-routes.ts +31 -5
  86. package/src/server/management/model-rows.ts +4 -0
  87. package/src/server/management/oauth-account-routes.ts +25 -4
  88. package/src/server/management/provider-routes.ts +113 -15
  89. package/src/server/management/routing-profile-routes.ts +3 -0
  90. package/src/server/request-log.ts +21 -0
  91. package/src/server/responses/agent-task-recovery.ts +1 -1
  92. package/src/server/responses/compact.ts +30 -1
  93. package/src/server/responses/core.ts +359 -153
  94. package/src/server/responses/empty-completion-guard.ts +35 -6
  95. package/src/server/responses/fetch-helpers.ts +18 -5
  96. package/src/server/responses/v2-native-parent-override.ts +59 -0
  97. package/src/server/responses/ws-upstream.ts +75 -2
  98. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  99. package/src/service.ts +1 -1
  100. package/src/types/config.ts +16 -1
  101. package/src/types/provider.ts +16 -0
  102. package/src/types/request.ts +28 -0
  103. package/src/types/tools.ts +27 -0
  104. package/src/types.ts +6 -0
  105. package/src/web-search/gemini-executor.ts +6 -4
  106. package/src/web-search/loop.ts +42 -6
  107. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  108. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -7,7 +7,7 @@ import {
7
7
  } from "./google-errors";
8
8
  import { isGoogleMixedBuiltinToolError, repairGoogleInvalidRequestBody, stripGoogleBuiltinToolsFromWireBody } from "./google-wire-compiler";
9
9
  import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
10
- import { recordAntigravityCooldown } from "../oauth/antigravity-routing";
10
+ import { recordAntigravityCooldown, recordAntigravitySyntheticFailure } from "../oauth/antigravity-routing";
11
11
  import {
12
12
  antigravityHostCandidates,
13
13
  canonicalAntigravityHttpsHost,
@@ -178,6 +178,7 @@ function responseWithBufferedBody(
178
178
  async function prepareCcaSseResponse(
179
179
  response: Response,
180
180
  fetchPeer: (() => Promise<Response>) | undefined,
181
+ accountId?: string,
181
182
  ): Promise<Response> {
182
183
  if (!response.body) return fetchPeer ? fetchPeer() : response;
183
184
  const reader = response.body.getReader();
@@ -209,6 +210,13 @@ async function prepareCcaSseResponse(
209
210
  return failoverOrPassthrough();
210
211
  }
211
212
  if (probe === "quota_exhausted" || probe === "geo_blocked") {
213
+ if (accountId) {
214
+ recordAntigravitySyntheticFailure(accountId, {
215
+ code: probe === "quota_exhausted" ? 429 : 403,
216
+ status: probe === "quota_exhausted" ? "RESOURCE_EXHAUSTED" : "PERMISSION_DENIED",
217
+ message: probe === "quota_exhausted" ? "quota exceeded" : "user location is not supported",
218
+ });
219
+ }
212
220
  const status = probe === "quota_exhausted" ? 429 : 403;
213
221
  return passthrough(undefined, status);
214
222
  }
@@ -237,6 +245,13 @@ async function prepareCcaSseResponse(
237
245
  return failoverOrPassthrough();
238
246
  }
239
247
  if (probe === "quota_exhausted" || probe === "geo_blocked") {
248
+ if (accountId) {
249
+ recordAntigravitySyntheticFailure(accountId, {
250
+ code: probe === "quota_exhausted" ? 429 : 403,
251
+ status: probe === "quota_exhausted" ? "RESOURCE_EXHAUSTED" : "PERMISSION_DENIED",
252
+ message: probe === "quota_exhausted" ? "quota exceeded" : "user location is not supported",
253
+ });
254
+ }
240
255
  const status = probe === "quota_exhausted" ? 429 : 403;
241
256
  return passthrough(overflow, status);
242
257
  }
@@ -262,6 +277,8 @@ function isUnavailableResponse(response: Response): boolean {
262
277
  export interface GoogleRetryOptions {
263
278
  /** Repair-and-replay structurally invalid 400 bodies (Vertex/Antigravity behavior). */
264
279
  repairInvalid400?: boolean;
280
+ /** Let the Responses layer own Antigravity's single same-account 429 replay. */
281
+ retry429?: boolean;
265
282
  }
266
283
 
267
284
  async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
@@ -291,13 +308,15 @@ async function recordAntigravityHttpCooldown(
291
308
  if (response.status === 429) {
292
309
  recordAntigravityCooldown(
293
310
  accountId,
294
- isQuotaExhaustedBody(payloadText) ? "quota_exhausted" : "rate_limited",
295
- retryAfterMs(response.headers.get("retry-after")),
311
+ response.headers.get("retry-after"),
312
+ Date.now(),
313
+ isQuotaExhaustedBody(payloadText) ? "quota" : "rate-limit",
314
+ "synthetic",
296
315
  );
297
316
  return true;
298
317
  }
299
- if (isAntigravityGeoBlockedBody(payloadText)) {
300
- recordAntigravityCooldown(accountId, "geo_blocked");
318
+ if (response.status === 403 || isAntigravityGeoBlockedBody(payloadText)) {
319
+ recordAntigravityCooldown(accountId, response.headers.get("retry-after"), Date.now(), "geoblock", "synthetic");
301
320
  return true;
302
321
  }
303
322
  return false;
@@ -318,6 +337,7 @@ async function fetchGoogleWithRetryInternal(
318
337
  opts: GoogleRetryOptions = {},
319
338
  ): Promise<Response> {
320
339
  const repairInvalid400 = opts.repairInvalid400 ?? true;
340
+ const retry429 = opts.retry429 ?? true;
321
341
  const timeoutMs = ctx.timeoutMs ?? 200_000;
322
342
  const executor = ctx.executor ?? globalThis.fetch;
323
343
  let activeRequest = request;
@@ -369,7 +389,7 @@ async function fetchGoogleWithRetryInternal(
369
389
  opts,
370
390
  )
371
391
  : undefined;
372
- return prepareCcaSseResponse(res, fetchPeer);
392
+ return prepareCcaSseResponse(res, fetchPeer, ctx.accountId);
373
393
  }
374
394
  if (res.status === 400 && repairInvalid400 && !compatibilityReplayUsed) {
375
395
  let payloadText = "";
@@ -397,6 +417,9 @@ async function fetchGoogleWithRetryInternal(
397
417
  continue;
398
418
  }
399
419
  }
420
+ if (res.status === 429 && !retry429) {
421
+ return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
422
+ }
400
423
  if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
401
424
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
402
425
  }
@@ -466,5 +489,5 @@ export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchC
466
489
 
467
490
  /** Antigravity (Cloud Code Assist) retry wrapper. */
468
491
  export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
469
- return fetchGoogleWithRetry("Antigravity", request, ctx);
492
+ return fetchGoogleWithRetry("Antigravity", request, ctx, { retry429: false });
470
493
  }
@@ -12,6 +12,11 @@ export function vertexTruncationErrorMessage(reason?: string): string {
12
12
  return `Vertex AI response truncated upstream before the turn completed${suffix}`;
13
13
  }
14
14
 
15
+ export function googleTruncationErrorMessage(reason?: string): string {
16
+ const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
17
+ return `Google AI Studio response truncated upstream before the turn completed${suffix}`;
18
+ }
19
+
15
20
  /**
16
21
  * Whether a finished turn must fail closed. A truncation reason arriving mid tool call always
17
22
  * does. MALFORMED_FUNCTION_CALL fails closed even with zero started calls: the malformed call
@@ -5,6 +5,15 @@ type JsonObject = Record<string, unknown>;
5
5
 
6
6
  const GOOGLE_TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
7
7
  const GOOGLE_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
8
+ const GOOGLE_SAFETY_CATEGORIES = new Set([
9
+ "HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT",
10
+ "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_CIVIC_INTEGRITY", "HARM_CATEGORY_JAILBREAK",
11
+ ]);
12
+ const GOOGLE_SAFETY_THRESHOLDS = new Set([
13
+ "HARM_BLOCK_THRESHOLD_UNSPECIFIED", "BLOCK_LOW_AND_ABOVE", "BLOCK_MEDIUM_AND_ABOVE",
14
+ "BLOCK_ONLY_HIGH", "BLOCK_NONE", "OFF",
15
+ ]);
16
+ const GOOGLE_CACHED_CONTENT = /^(?:cachedContents\/[^/?#\s]+|projects\/[^/?#\s]+\/locations\/[^/?#\s]+\/cachedContents\/[^/?#\s]+)$/;
8
17
 
9
18
  function isObject(value: unknown): value is JsonObject {
10
19
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -158,12 +167,18 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
158
167
  ))].slice(0, 5);
159
168
  if (stopSequences.length > 0) out.stopSequences = stopSequences;
160
169
  }
161
- if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") {
162
- const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
163
- const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
164
- ? raw
165
- : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
166
- if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
170
+ if (isObject(value.thinkingConfig)) {
171
+ const config: JsonObject = {};
172
+ if (typeof value.thinkingConfig.thinkingBudget === "number"
173
+ && Number.isSafeInteger(value.thinkingConfig.thinkingBudget)
174
+ && value.thinkingConfig.thinkingBudget >= -1) config.thinkingBudget = value.thinkingConfig.thinkingBudget;
175
+ if (typeof value.thinkingConfig.includeThoughts === "boolean") config.includeThoughts = value.thinkingConfig.includeThoughts;
176
+ if (typeof value.thinkingConfig.thinkingLevel === "string") {
177
+ const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
178
+ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) ? raw : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
179
+ if (thinkingLevel && config.thinkingBudget === undefined) config.thinkingLevel = thinkingLevel;
180
+ }
181
+ if (Object.keys(config).length > 0) out.thinkingConfig = config;
167
182
  }
168
183
  if (Array.isArray(value.responseModalities)) {
169
184
  const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
@@ -180,6 +195,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
180
195
  return Object.keys(out).length > 0 ? out : undefined;
181
196
  }
182
197
 
198
+ function compileSafetySettings(value: unknown): unknown[] | undefined {
199
+ if (!Array.isArray(value) || value.length > 16) return undefined;
200
+ const seen = new Set<string>();
201
+ const out: JsonObject[] = [];
202
+ for (const setting of value) {
203
+ if (!isObject(setting) || typeof setting.category !== "string" || !GOOGLE_SAFETY_CATEGORIES.has(setting.category)
204
+ || typeof setting.threshold !== "string" || !GOOGLE_SAFETY_THRESHOLDS.has(setting.threshold)
205
+ || seen.has(setting.category)) return undefined;
206
+ seen.add(setting.category);
207
+ out.push({ category: setting.category, threshold: setting.threshold });
208
+ }
209
+ return out.length > 0 ? out : undefined;
210
+ }
211
+
183
212
  function compileToolConfig(value: unknown, toWireName: (name: string) => string): JsonObject | undefined {
184
213
  if (!isObject(value) || !isObject(value.functionCallingConfig)) return undefined;
185
214
  const raw = value.functionCallingConfig;
@@ -216,6 +245,9 @@ export function compileGoogleWireBody(input: unknown): {
216
245
  if (generationConfig) body.generationConfig = generationConfig;
217
246
  const toolConfig = compileToolConfig(source.toolConfig, names.toWire);
218
247
  if (toolConfig) body.toolConfig = toolConfig;
248
+ const safetySettings = compileSafetySettings(source.safetySettings);
249
+ if (safetySettings) body.safetySettings = safetySettings;
250
+ if (typeof source.cachedContent === "string" && GOOGLE_CACHED_CONTENT.test(source.cachedContent)) body.cachedContent = source.cachedContent;
219
251
  if (typeof source.sessionId === "string" && source.sessionId.length > 0) body.sessionId = source.sessionId;
220
252
  return { body, restoreToolName: names.fromWire };
221
253
  }
@@ -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,
@@ -19,8 +20,9 @@ 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
  }
@@ -395,6 +428,13 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
395
428
  };
396
429
  }
397
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
+
398
438
  /**
399
439
  * Cap on the buffered non-streaming response body (100 MiB), matching
400
440
  * IMAGES_RESPONSE_MAX_BYTES in src/server/images.ts. Enforced by streaming the
@@ -672,6 +712,7 @@ function invalidGoogleShapeEvent(
672
712
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
673
713
  // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
674
714
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
715
+ let observeProviderError: IncomingMeta["onProviderError"];
675
716
  let antigravityModel: string | undefined;
676
717
  let antigravitySession: string | undefined;
677
718
  // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace
@@ -681,8 +722,23 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
681
722
  let vertexReplaySession: string | undefined;
682
723
  let restoreGoogleToolName = (name: string): string => name;
683
724
  const emitInTurnGroundingSourcesQueue: boolean[] = [];
725
+ const truncationErrorMessage = provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
726
+ ? vertexTruncationErrorMessage
727
+ : googleTruncationErrorMessage;
684
728
  return {
685
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
+ },
686
742
 
687
743
  // Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors.
688
744
  // Direct AI-Studio uses the canonical server transport (fetchWithTransientRetry), which
@@ -697,7 +753,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
697
753
  }
698
754
  : {}),
699
755
 
700
- async buildRequest(parsed: OcxParsedRequest) {
756
+ async buildRequest(parsed: OcxParsedRequest, options?: IncomingMeta) {
757
+ observeProviderError = options?.onProviderError;
701
758
  const routedModelId = provider.googleMode === "cloud-code-assist"
702
759
  ? resolveAntigravityEffortWireModel(
703
760
  parsed.modelId,
@@ -723,6 +780,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
723
780
  // catalog is a guaranteed upstream 400.
724
781
  const toolConfig = tools ? toolChoiceToGeminiToolConfig(parsed) : undefined;
725
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;
726
786
 
727
787
  const generationConfig: Record<string, unknown> = {};
728
788
  if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
@@ -744,7 +804,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
744
804
  const thinkingLevel = thinkingEligible
745
805
  ? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
746
806
  : undefined;
747
- 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
+ }
748
814
  if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
749
815
  generationConfig.responseModalities = ["TEXT", "IMAGE"];
750
816
  }
@@ -816,6 +882,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
816
882
  const strippedModelTail = /claude/i.test(wireModelId) ? stripTrailingClaudePrefill(contents) : false;
817
883
  if (antigravityUsesReplayCache(wireModelId)) {
818
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
+ }
819
898
  } else {
820
899
  sanitizeAntigravityClaudeSignatures(contents);
821
900
  }
@@ -970,17 +1049,31 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
970
1049
 
971
1050
  // Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
972
1051
  if (chunk.error) {
973
- 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" };
974
1061
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
975
1062
  // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
976
1063
  const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
977
1064
  const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
978
1065
  if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
979
1066
  && replayModel && replaySession
1067
+ && !/missing.*thought_signature/i.test(err?.message ?? "")
980
1068
  && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
981
1069
  clearAntigravityReplay(replayModel, replaySession);
982
1070
  }
983
- 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
+ };
984
1077
  return "terminate";
985
1078
  }
986
1079
 
@@ -1010,7 +1103,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1010
1103
  // absent key and an empty array are already skipped here; `null` joins them. A non-null
1011
1104
  // non-array container is still claimed structure the parser cannot read, and stays
1012
1105
  // terminal.
1013
- 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
+ }
1014
1114
  if (!Array.isArray(rawCandidates)) {
1015
1115
  yield invalidGoogleShapeEvent({
1016
1116
  reason: "candidates_not_array",
@@ -1018,7 +1118,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1018
1118
  });
1019
1119
  return "terminate";
1020
1120
  }
1021
- 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
+ }
1022
1129
  const rawCandidate = rawCandidates[0];
1023
1130
  if (!isGoogleRecord(rawCandidate)) {
1024
1131
  // Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
@@ -1086,7 +1193,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1086
1193
  if (parts) {
1087
1194
  for (const part of parts) {
1088
1195
  const sig = googlePartThoughtSignature(part);
1089
- if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1196
+ if (sig && isLikelyRealThoughtSignature(sig) && (part.thought === true || !part.functionCall)) {
1090
1197
  pendingStreamThoughtSig = sig;
1091
1198
  }
1092
1199
  const textEvent = googlePartTextEvent(part, filterCcaSearchSuggestionHtml);
@@ -1191,9 +1298,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1191
1298
  }
1192
1299
  // Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
1193
1300
  // an error instead of a silently-incomplete done. Mirrors kiro-truncation.
1194
- if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
1195
- && isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
1196
- yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
1301
+ if (isVertexTruncatedTurn(lastFinishReason, toolCallsStarted)) {
1302
+ yield { type: "error", message: truncationErrorMessage(lastFinishReason) };
1197
1303
  return;
1198
1304
  }
1199
1305
  if (!sawAnyFrame || !sawTerminalSignal) {
@@ -1237,7 +1343,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1237
1343
  // Cloud Code Assist exposes only the SSE transport. Unary callers still use this
1238
1344
  // buffered adapter entry point, so collect the exact same events parseStream emits
1239
1345
  // instead of maintaining a second CCA JSON parser.
1240
- 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) {
1241
1348
  const events: AdapterEvent[] = [];
1242
1349
  let previousTail: AdapterEvent | undefined;
1243
1350
  try {
@@ -1326,10 +1433,24 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1326
1433
  return events;
1327
1434
  };
1328
1435
  if (raw.error) {
1329
- const err = raw.error as { message?: string };
1330
- 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
+ }]);
1331
1450
  }
1332
- 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;
1333
1454
  const events: AdapterEvent[] = [];
1334
1455
 
1335
1456
  const rawCandidates: unknown = json.candidates;
@@ -1345,6 +1466,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1345
1466
  }
1346
1467
  const candidates = rawCandidates as { finishReason?: string }[] | undefined;
1347
1468
  if (!candidates?.length) {
1469
+ const feedbackError = googlePromptFeedbackError(json);
1470
+ if (feedbackError) return finish([feedbackError]);
1348
1471
  return finish([{ type: "error", message: "google response contained no candidates" }]);
1349
1472
  }
1350
1473
  const rawCandidate: unknown = candidates[0];
@@ -1417,9 +1540,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1417
1540
 
1418
1541
  // Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
1419
1542
  // (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
1420
- if (provider.googleMode === "vertex"
1421
- && isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1422
- return finish([{ type: "error", message: vertexTruncationErrorMessage(candidate.finishReason) }]);
1543
+ if (isVertexTruncatedTurn(candidate.finishReason, toolCallsStarted)) {
1544
+ return finish([{ type: "error", message: truncationErrorMessage(candidate.finishReason) }]);
1423
1545
  }
1424
1546
 
1425
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)] : [],
@@ -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
 
@@ -1686,7 +1686,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1686
1686
  let convertedRoutedCustomToolNames: Set<string> | undefined;
1687
1687
  let routedCustomToolRepairNames: Set<string> | undefined;
1688
1688
  let convertedRoutedToolSearchNames: Set<string> | undefined;
1689
- let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
1689
+ let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
1690
1690
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
1691
1691
  let outBody = stripPreviousResponseId(
1692
1692
  parsed._rawBody,