@bitkyc08/opencodex 2.24.1 → 2.25.0-preview.20260818

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 (58) hide show
  1. package/gui/dist/assets/{index-C3FiAveG.js → index-TFd4xi1L.js} +8 -8
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic.ts +42 -0
  5. package/src/adapters/client-fingerprint.ts +9 -5
  6. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  7. package/src/adapters/command-code.ts +17 -0
  8. package/src/adapters/cursor/cursor-errors.ts +49 -0
  9. package/src/adapters/cursor/live-models.ts +36 -2
  10. package/src/adapters/cursor/live-transport.ts +55 -4
  11. package/src/adapters/cursor/native-exec.ts +9 -0
  12. package/src/adapters/cursor/protobuf-request.ts +160 -9
  13. package/src/adapters/cursor/request-builder.ts +9 -1
  14. package/src/adapters/cursor/tool-definitions.ts +7 -2
  15. package/src/adapters/google-antigravity-wire.ts +1 -1
  16. package/src/adapters/google.ts +30 -12
  17. package/src/adapters/openai-responses-url.ts +5 -3
  18. package/src/adapters/registry.ts +3 -1
  19. package/src/adapters/tool-catalog-nudge.ts +76 -9
  20. package/src/bridge.ts +53 -9
  21. package/src/claude/context-windows.ts +2 -2
  22. package/src/claude/desktop-3p.ts +6 -6
  23. package/src/claude/model-info.ts +2 -2
  24. package/src/cli/claude-desktop.ts +2 -3
  25. package/src/codex/app-server-processes.ts +69 -35
  26. package/src/codex/catalog/metadata.ts +29 -10
  27. package/src/codex/catalog/provider-fetch.ts +21 -11
  28. package/src/codex/catalog.ts +1 -1
  29. package/src/codex/injected-marker.ts +9 -3
  30. package/src/codex/user-identity.ts +88 -6
  31. package/src/config.ts +1 -0
  32. package/src/generated/compatibility-version.json +61 -53
  33. package/src/grok/sync.ts +2 -4
  34. package/src/lab/projection/rebuild.ts +36 -18
  35. package/src/lib/windows-elevation.ts +18 -3
  36. package/src/lib/windows-secret-acl.ts +49 -19
  37. package/src/oauth/google-antigravity.ts +7 -2
  38. package/src/providers/antigravity-models.ts +126 -17
  39. package/src/providers/derive.ts +11 -1
  40. package/src/responses/parser.ts +4 -0
  41. package/src/responses/reasoning-replay-cache.ts +16 -1
  42. package/src/responses/thought-signature-replay.ts +17 -1
  43. package/src/responses/truncated-stop-reason.ts +60 -0
  44. package/src/router.ts +2 -10
  45. package/src/routing/capability.ts +5 -6
  46. package/src/server/index.ts +3 -4
  47. package/src/server/management/agent-settings-routes.ts +5 -5
  48. package/src/server/management/config-routes.ts +2 -2
  49. package/src/server/management/context.ts +2 -0
  50. package/src/server/management/native-integration-routes.ts +3 -3
  51. package/src/server/management/provider-routes.ts +22 -0
  52. package/src/server/management/shared.ts +4 -4
  53. package/src/server/management-api.ts +2 -2
  54. package/src/server/request-log.ts +11 -3
  55. package/src/server/responses/core.ts +4 -1
  56. package/src/server/responses/input-admission.ts +13 -10
  57. package/src/server/system-env.ts +3 -3
  58. package/src/types.ts +13 -1
@@ -8,12 +8,14 @@ import { isCursorExternalWireModel } from "./discovery";
8
8
  import { debugProviderDiagnostic } from "../../lib/debug";
9
9
  import {
10
10
  createCursorBlobRequestScope,
11
+ cursorBlobMaxEntryBytes,
11
12
  releaseCursorBlobRequestScope,
12
13
  sealCursorBlobRequestScope,
13
14
  storeCursorBlob,
14
15
  type CursorBlobRequestScopeToken,
15
16
  } from "./native-exec";
16
17
  import { estimateTokens } from "../../lib/token-estimate";
18
+ import { parseDataUrl } from "../image";
17
19
  import {
18
20
  AgentClientMessageSchema,
19
21
  AgentConversationTurnStructureSchema,
@@ -26,6 +28,7 @@ import {
26
28
  McpArgsSchema,
27
29
  McpSuccessSchema,
28
30
  McpTextContentSchema,
31
+ McpImageContentSchema,
29
32
  McpToolCallSchema,
30
33
  McpToolResultContentItemSchema,
31
34
  McpToolResultSchema,
@@ -318,7 +321,7 @@ function contentText(message: OcxMessage): string {
318
321
  .map(part => {
319
322
  if (part.type === "text") return part.text;
320
323
  if (part.type === "thinking") return part.thinking;
321
- if (part.type === "image") return `[image input unsupported by Cursor adapter phase 3: ${part.detail ?? "auto"}]`;
324
+ if (part.type === "image") return `[image produced by this tool, omitted from Cursor text replay: ${part.detail ?? "auto"}]`;
322
325
  return undefined;
323
326
  })
324
327
  .filter((value): value is string => typeof value === "string" && value.length > 0)
@@ -328,10 +331,146 @@ function contentText(message: OcxMessage): string {
328
331
  function contentToText(content: OcxToolResultMessage["content"]): string {
329
332
  if (typeof content === "string") return content;
330
333
  return content
331
- .map(part => part.type === "text" ? part.text : `[image input unsupported by Cursor adapter phase 3: ${part.detail ?? "auto"}]`)
334
+ .map(part => part.type === "text" ? part.text : `[image produced by this tool, omitted from Cursor text replay: ${part.detail ?? "auto"}]`)
332
335
  .join("\n");
333
336
  }
334
337
 
338
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
339
+
340
+ /**
341
+ * Decode a Codex inline image into Cursor wire bytes.
342
+ *
343
+ * `OcxImageContent.imageUrl` is either a `data:` URL or a remote https URL, so this cannot reuse
344
+ * the MCP helper (which takes bare base64 plus a separate mime). It layers strict validation over
345
+ * the shared `parseDataUrl` rather than tightening it, because Anthropic, Google, and Command Code
346
+ * share that parser. `Buffer.from(x, "base64")` accepts many invalid strings silently, so the
347
+ * charset is checked explicitly. Remote URLs are out of scope: `McpImageContent` needs bytes, and
348
+ * fetching here would put network IO inside request construction.
349
+ */
350
+ function decodeInlineImage(imageUrl: string): { bytes: Uint8Array; mimeType: string } | undefined {
351
+ const parsed = parseDataUrl(imageUrl);
352
+ if (!parsed) return undefined;
353
+ const base64 = parsed.base64.trim();
354
+ if (base64.length === 0 || base64.length % 4 !== 0 || !BASE64_PATTERN.test(base64)) return undefined;
355
+ try {
356
+ const bytes = Uint8Array.from(Buffer.from(base64, "base64"));
357
+ if (bytes.length === 0) return undefined;
358
+ return { bytes, mimeType: parsed.mediaType || "application/octet-stream" };
359
+ } catch {
360
+ return undefined;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * A degraded image must never make a step LARGER than the legacy encoding did, or this change
366
+ * could fail admission for a request that previously fit. The old placeholder was
367
+ * `[image input unsupported by Cursor adapter phase 3: <detail>]`; anything we emit in its place
368
+ * is truncated to that budget so the zero-image case is byte-bounded by the pre-change behavior.
369
+ */
370
+ const LEGACY_IMAGE_PLACEHOLDER_BUDGET =
371
+ "[image input unsupported by Cursor adapter phase 3: auto]".length;
372
+
373
+ function imagePlaceholder(reason: string): string {
374
+ const text = `[image omitted: ${reason}]`;
375
+ return text.length <= LEGACY_IMAGE_PLACEHOLDER_BUDGET
376
+ ? text
377
+ : `${text.slice(0, LEGACY_IMAGE_PLACEHOLDER_BUDGET - 1)}]`;
378
+ }
379
+
380
+ type DecodedResultPart =
381
+ | { kind: "text"; text: string }
382
+ | { kind: "image"; bytes: Uint8Array; mimeType: string }
383
+ | { kind: "undecodable" };
384
+
385
+ /**
386
+ * Decode a tool result's parts ONCE. `toolCallStep` may re-serialize a step several times while
387
+ * shrinking it to fit blob admission, and decoding base64 on every attempt made that loop
388
+ * quadratic (an audit measured ~3s for 100 images).
389
+ */
390
+ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] | undefined {
391
+ const content = message.content;
392
+ if (typeof content === "string") return undefined;
393
+ return content.map((part): DecodedResultPart => {
394
+ if (part.type === "text") return { kind: "text", text: part.text };
395
+ const decoded = decodeInlineImage(part.imageUrl);
396
+ return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
397
+ });
398
+ }
399
+
400
+ function countImages(parts: DecodedResultPart[] | undefined): number {
401
+ return parts ? parts.filter(p => p.kind === "image").length : 0;
402
+ }
403
+
404
+ /**
405
+ * Build the wire content items for a tool result, preserving part order.
406
+ *
407
+ * Images become real `McpImageContent` — the Cursor schema has an image case on
408
+ * `McpToolResultContentItem`, and `native-exec-mcp.ts` already uses it for MCP-invoked tools.
409
+ * Flattening them to placeholder text blinded every screenshot-returning tool (Computer Use,
410
+ * browser QA) that Codex routes through this path.
411
+ */
412
+ function toolResultContentItems(
413
+ message: OcxToolResultMessage,
414
+ decoded?: DecodedResultPart[],
415
+ maxImages = Number.POSITIVE_INFINITY,
416
+ ) {
417
+ const parts = decoded ?? decodeResultParts(message);
418
+ if (!parts) {
419
+ const text = typeof message.content === "string" ? message.content : "";
420
+ return [create(McpToolResultContentItemSchema, {
421
+ content: { case: "text" as const, value: create(McpTextContentSchema, { text }) },
422
+ })];
423
+ }
424
+ // Images are dropped OLDEST first when the step must shrink: the most recent screenshot is the
425
+ // one the model is reasoning about, so it is the last to go.
426
+ const totalImages = countImages(parts);
427
+ const allowed = Math.max(0, Math.min(totalImages, maxImages));
428
+ let seen = 0;
429
+ // Consecutive text runs are newline-joined into ONE item, exactly as the legacy encoding did.
430
+ // Emitting one protobuf item per part adds per-item framing, which was enough to push a
431
+ // previously admissible step past the blob ceiling (round-3 audit: 1020 -> 1025 bytes at a
432
+ // 1024 limit). A result with no images must serialize identically to before this feature.
433
+ const items: ReturnType<typeof create<typeof McpToolResultContentItemSchema>>[] = [];
434
+ let pendingText: string[] = [];
435
+ const flushText = () => {
436
+ if (pendingText.length === 0) return;
437
+ const text = pendingText.join("\n");
438
+ pendingText = [];
439
+ items.push(create(McpToolResultContentItemSchema, {
440
+ content: { case: "text" as const, value: create(McpTextContentSchema, { text }) },
441
+ }));
442
+ };
443
+ for (const part of parts) {
444
+ if (part.kind === "text") {
445
+ pendingText.push(part.text);
446
+ continue;
447
+ }
448
+ if (part.kind === "undecodable") {
449
+ pendingText.push(imagePlaceholder("no inline data"));
450
+ continue;
451
+ }
452
+ seen++;
453
+ if (seen <= totalImages - allowed) {
454
+ pendingText.push(imagePlaceholder(`${part.bytes.byteLength}B over step limit`));
455
+ continue;
456
+ }
457
+ flushText();
458
+ items.push(create(McpToolResultContentItemSchema, {
459
+ content: { case: "image" as const, value: create(McpImageContentSchema, {
460
+ data: part.bytes,
461
+ mimeType: part.mimeType,
462
+ }) },
463
+ }));
464
+ }
465
+ flushText();
466
+ if (items.length === 0) {
467
+ items.push(create(McpToolResultContentItemSchema, {
468
+ content: { case: "text" as const, value: create(McpTextContentSchema, { text: "" }) },
469
+ }));
470
+ }
471
+ return items;
472
+ }
473
+
335
474
  function toolResultToText(message: OcxToolResultMessage): string {
336
475
  return [
337
476
  "[tool_result]",
@@ -359,7 +498,8 @@ function toolCallStep(
359
498
  const args: Record<string, Uint8Array> = {};
360
499
  for (const [key, value] of Object.entries(part.arguments ?? {})) args[key] = argBytes(value);
361
500
  const toolName = namespacedToolName(part.namespace, part.name);
362
- return storeCursorBlob(toBinary(ConversationStepSchema, create(ConversationStepSchema, {
501
+ const decodedResult = result ? decodeResultParts(result) : undefined;
502
+ const serialize = (maxImages: number): Uint8Array => toBinary(ConversationStepSchema, create(ConversationStepSchema, {
363
503
  message: {
364
504
  case: "toolCall",
365
505
  value: create(ToolCallSchema, {
@@ -373,23 +513,34 @@ function toolCallStep(
373
513
  providerIdentifier: OCX_RESPONSES_TOOL_PROVIDER,
374
514
  args,
375
515
  }),
376
- ...(result ? { result: toolResultPart(result) } : {}),
516
+ ...(result ? { result: toolResultPart(result, decodedResult, maxImages) } : {}),
377
517
  }),
378
518
  },
379
519
  }),
380
520
  },
381
- })), requestScope);
521
+ }));
522
+
523
+ // A step is stored as ONE blob, so its images share an entry with the call's arguments, text,
524
+ // mime strings, and protobuf framing. A byte budget over decoded images alone cannot bound that
525
+ // (an audit reproduced a 448-byte-argument call whose 460-byte image pushed a previously
526
+ // admitted step past the ceiling). Measure the real serialized size instead, then drop images —
527
+ // oldest first, so the most recent screenshot survives — until the step fits.
528
+ const limit = cursorBlobMaxEntryBytes();
529
+ const imageCount = countImages(decodedResult);
530
+ let encoded = serialize(imageCount);
531
+ for (let allowed = imageCount - 1; allowed >= 0 && encoded.byteLength > limit; allowed--) {
532
+ encoded = serialize(allowed);
533
+ }
534
+ return storeCursorBlob(encoded, requestScope);
382
535
  }
383
536
 
384
- function toolResultPart(message: OcxToolResultMessage) {
537
+ function toolResultPart(message: OcxToolResultMessage, decoded?: DecodedResultPart[], maxImages?: number) {
385
538
  return create(McpToolResultSchema, {
386
539
  result: {
387
540
  case: "success",
388
541
  value: create(McpSuccessSchema, {
389
542
  isError: message.isError,
390
- content: [create(McpToolResultContentItemSchema, {
391
- content: { case: "text", value: create(McpTextContentSchema, { text: contentToText(message.content) }) },
392
- })],
543
+ content: toolResultContentItems(message, decoded, maxImages),
393
544
  }),
394
545
  },
395
546
  });
@@ -205,7 +205,15 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
205
205
  case "thinking":
206
206
  return part.thinking;
207
207
  case "image":
208
- return `[image input unsupported by Cursor adapter phase 3: ${part.detail ?? "auto"}]`;
208
+ // User-message images are still flattened here: this path builds the plain-text prompt, and
209
+ // the schema slot that could carry them (UserMessage.selectedContext.selectedImages) is not
210
+ // populated by this adapter. The tool-result ENCODER does build real McpImageContent
211
+ // (see protobuf-request.ts), so the old "unsupported by Cursor adapter" wording is no
212
+ // longer true of the encoder — but note that nothing reaches Cursor today either way:
213
+ // every Cursor model is in noVisionModels (providers/registry.ts), so the vision sidecar
214
+ // describes or strips images before this adapter runs. Kept the same length to avoid
215
+ // shifting any byte-budgeted prompt path.
216
+ return `[image omitted from this Cursor text prompt: ${part.detail ?? "auto"}]`;
209
217
  case "toolCall":
210
218
  // Cursor does not accept OpenAI Responses assistant tool-call parts as native history here.
211
219
  // Rendering them as visible "[tool_call]" text leaks synthetic protocol markers back into
@@ -525,7 +525,12 @@ export function nonEmptyShellBridgeCommandFromArgs(
525
525
  }
526
526
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
527
527
  const record = parsed as Record<string, unknown>;
528
- for (const key of shellBridgeRequiredCommandKeys(toolName, schema)) {
528
+ const requiredKeys = shellBridgeRequiredCommandKeys(toolName, schema);
529
+ const candidateKeys = new Set<"cmd" | "command">([
530
+ ...requiredKeys,
531
+ requiredKeys.includes("cmd") ? "command" : "cmd",
532
+ ]);
533
+ for (const key of candidateKeys) {
529
534
  const value = record[key];
530
535
  if (typeof value === "string" && value.trim().length > 0) return value.trim();
531
536
  }
@@ -615,7 +620,7 @@ export function buildCursorToolGuidanceSystemNote(
615
620
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
616
621
  // model probes for a top-level shell tool that is not there.
617
622
  codeMode
618
- ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description for the exact nested helpers this turn provides. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\`, \`shell_command\`, or \`apply_patch\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}.`
623
+ ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}.`
619
624
  : undefined,
620
625
  codeMode
621
626
  ? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
@@ -9,7 +9,7 @@ import { antigravityUserAgent } from "./client-fingerprint";
9
9
  * sends. The IDE client family is also required to unlock newer agent models (the backend 404s
10
10
  * CLI-shaped UAs for `gemini-3.7-*`). A `GOOGLE_ANTIGRAVITY_USER_AGENT` override still wins.
11
11
  */
12
- export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT || antigravityUserAgent();
12
+ export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
13
13
 
14
14
  /**
15
15
  * Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
@@ -48,19 +48,17 @@ const GOOGLE_BREVITY_INSTRUCTION = [
48
48
  ].join("\n");
49
49
 
50
50
  /**
51
- * Google renamed the current Gemini Flash generations on the Generative Language API,
52
- * appending a `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). The
53
- * old `gemini-3.7-flash` path 404s, so a saved config or registry entry naming the base
54
- * id must be resolved here before it reaches the URL. The user-facing id is deliberately
55
- * left alone: the picker, the catalog, the usage log and the price overlays all stay
56
- * keyed on the base id, and only the wire path learns the new spelling.
51
+ * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
52
+ * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
53
+ * stable and make the mapping configurable for deployments that still serve the bare id.
57
54
  */
58
55
  const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
59
56
  "gemini-3.7-flash": "gemini-3.7-flash-tiered",
60
57
  "gemini-3.6-flash": "gemini-3.6-flash-tiered",
61
58
  };
62
59
 
63
- function resolveDirectGeminiWireModelId(modelId: string): string {
60
+ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string {
61
+ if (!applyRenames) return modelId;
64
62
  return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
65
63
  ? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
66
64
  : modelId;
@@ -147,7 +145,7 @@ function geminiToolResultText(content: string | OcxContentPart[]): string {
147
145
 
148
146
  function messagesToGeminiFormat(
149
147
  parsed: OcxParsedRequest,
150
- routedModelId = parsed.modelId,
148
+ identityModelId: string,
151
149
  ): { systemInstruction?: unknown; contents: unknown[] } {
152
150
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
153
151
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
@@ -156,7 +154,7 @@ function messagesToGeminiFormat(
156
154
  ...(parsed.context.systemPrompt ?? []),
157
155
  ...(toolCatalogNudge ? [toolCatalogNudge] : []),
158
156
  GOOGLE_BREVITY_INSTRUCTION,
159
- ].join("\n\n"), routedModelId);
157
+ ].join("\n\n"), identityModelId);
160
158
  const systemInstruction = { parts: [{ text: systemText }] };
161
159
 
162
160
  const contents: unknown[] = [];
@@ -395,9 +393,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
395
393
  ? resolveAntigravityEffortWireModel(
396
394
  parsed.modelId,
397
395
  mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
396
+ provider.baseUrl,
398
397
  ).wireModelId
399
- : resolveDirectGeminiWireModelId(parsed.modelId);
400
- const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId);
398
+ : provider.googleMode === "vertex"
399
+ ? parsed.modelId
400
+ : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
401
+ // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
402
+ const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
403
+ const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
401
404
  const tools = toolsToGeminiFormat(parsed);
402
405
 
403
406
  const body: Record<string, unknown> = { contents };
@@ -450,7 +453,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
450
453
  if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
451
454
  const sessionId = antigravitySessionId(parsed);
452
455
  const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
453
- const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
456
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
457
+ parsed.modelId,
458
+ mappedEffort,
459
+ provider.baseUrl,
460
+ );
454
461
  antigravityModel = wireModelId;
455
462
  antigravitySession = sessionId;
456
463
  // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
@@ -939,9 +946,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
939
946
  }
940
947
 
941
948
  const usage = json.usageMetadata as Record<string, number> | undefined;
949
+ // Mirror the streaming path: a buffered turn cut off by the token limit or a content filter
950
+ // must carry its stop reason, or the bridge sees a clean `done` and reports the truncated
951
+ // turn as completed — and, on a compaction turn, installs the half-written summary as
952
+ // replacement history (#422).
953
+ const finishReason = candidates?.[0]?.finishReason as string | undefined;
954
+ const stopReason = finishReason === "MAX_TOKENS"
955
+ ? "max_tokens"
956
+ : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(finishReason ?? "")
957
+ ? "content_filter"
958
+ : undefined;
942
959
  events.push({
943
960
  type: "done",
944
961
  usage: usageFromGemini(usage),
962
+ ...(stopReason ? { stopReason } : {}),
945
963
  });
946
964
  return finish(events);
947
965
  },
@@ -7,8 +7,10 @@ const TRAILING_V1 = /\/v1\/?$/;
7
7
  * Custom `responsesPath` stays on the adapter; this helper is only the legacy /v1/responses branch.
8
8
  */
9
9
  export function openaiResponsesUrl(baseUrl: string): string {
10
- const trimmed = baseUrl.trim().replace(TRAILING_SLASHES, "");
11
- const withoutEndpoint = trimmed.replace(TRAILING_RESPONSES, "");
10
+ const url = new URL(baseUrl.trim());
11
+ const trimmedPath = url.pathname.replace(TRAILING_SLASHES, "");
12
+ const withoutEndpoint = trimmedPath.replace(TRAILING_RESPONSES, "");
12
13
  const withoutV1 = withoutEndpoint.replace(TRAILING_V1, "");
13
- return `${withoutV1}/v1/responses`;
14
+ url.pathname = `${withoutV1}/v1/responses`;
15
+ return url.toString();
14
16
  }
@@ -1,6 +1,7 @@
1
1
  import { createAnthropicAdapter } from "./anthropic";
2
2
  import { createAzureAdapter } from "./azure";
3
3
  import type { ProviderAdapter } from "./base";
4
+ import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
4
5
  import { createCommandCodeAdapter } from "./command-code";
5
6
  import { createCursorAdapter } from "./cursor";
6
7
  import { createGoogleAdapter } from "./google";
@@ -57,7 +58,8 @@ export const ADAPTER_REGISTRY = {
57
58
  "openai-chat": {
58
59
  wire: "openai-chat",
59
60
  mutation: "codex-owned",
60
- create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createOpenAIChatAdapter(provider),
61
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) =>
62
+ withClinePassDeepSeekV4ToolReplayCompatibility(createOpenAIChatAdapter(provider)),
61
63
  },
62
64
  anthropic: {
63
65
  wire: "anthropic",
@@ -17,8 +17,40 @@ import {
17
17
  // included it either.
18
18
  const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
19
19
 
20
+ /**
21
+ * The two halves of the code-mode shape, kept provider-neutral here.
22
+ *
23
+ * `./cursor/tool-definitions.ts` owns the Cursor-scoped versions of these
24
+ * (`isCursorCodeModeExecTool` / `isBareCodexShellBridgeTool`), but those additionally require
25
+ * the Cursor Responses namespace. This nudge is shared by Anthropic, Google, Kiro,
26
+ * OpenAI-chat and command-code, so it needs the same semantics without that provider gate.
27
+ */
28
+ const CODEX_UNIFIED_EXEC_TOOL_NAME = "exec";
29
+ const CODEX_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
30
+
31
+ function isCodexCodeModeExecTool(tool: Pick<OcxTool, "namespace" | "name" | "freeform">): boolean {
32
+ return !tool.namespace && tool.name === CODEX_UNIFIED_EXEC_TOOL_NAME && tool.freeform === true;
33
+ }
34
+
35
+ /**
36
+ * BARE means un-namespaced, and the word is load-bearing.
37
+ *
38
+ * An MCP server can advertise its own `exec_command` or `shell_command` — a docker, k8s or ssh
39
+ * server plausibly does — and those arrive namespaced (`mcp__docker__exec_command`). They are
40
+ * not Codex's shell bridge, so they must not cancel code mode: a genuine code-mode turn that
41
+ * merely happens to sit beside an MCP shell tool would lose its guidance and fall back to the
42
+ * generic sentence.
43
+ *
44
+ * The Cursor original this was ported from (`isBareCodexShellBridgeTool`) carries the same
45
+ * `!tool.namespace` requirement; dropping it here made the name assert a check the body did not
46
+ * perform.
47
+ */
48
+ function isBareShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
49
+ return !tool.namespace && (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(tool.name);
50
+ }
51
+
20
52
  function quoteNames(names: readonly string[]): string {
21
- return names.map(name => `\`${name}\``).join(", ");
53
+ return names.map(name => "`" + name + "`").join(", ");
22
54
  }
23
55
 
24
56
  function uniqueNames(names: readonly string[]): string[] {
@@ -40,9 +72,33 @@ export function shouldInjectNonOpenAIToolCatalogNudge(provider: Pick<OcxProvider
40
72
  }
41
73
  }
42
74
 
75
+ /**
76
+ * Codex code mode is a SEMANTIC property, not a name.
77
+ *
78
+ * The tool that carries it is a `freeform` `exec` whose body is JavaScript evaluated in a V8
79
+ * isolate, advertised alongside no bare shell bridge. A provider is free to advertise an
80
+ * ordinary structured tool called `exec` that runs a shell string — and a catalog can list
81
+ * `exec` next to `exec_command`/`shell_command`, which is the flat-bridge shape, not code mode.
82
+ *
83
+ * Classifying on the name alone would tell those turns that `exec` takes JavaScript and that
84
+ * shell is only reachable as a nested `tools.*` helper. Both are false there, and a model that
85
+ * believes them sends the wrong arguments or avoids a legitimate execution tool entirely.
86
+ *
87
+ * So callers that HAVE the tool objects decide with the semantic predicate and pass the verified
88
+ * wire name in; the name-only entry point cannot decide it and does not try.
89
+ */
90
+ function codeModeExecWireName(
91
+ advertised: ReadonlySet<string>,
92
+ verifiedName: string | undefined,
93
+ ): string | undefined {
94
+ if (!verifiedName) return undefined;
95
+ return advertised.has(verifiedName) ? verifiedName : undefined;
96
+ }
97
+
43
98
  export function buildNonOpenAIToolCatalogNudgeFromNames(
44
99
  wireNames: readonly string[] | undefined,
45
100
  toWireName: (name: string) => string = name => name,
101
+ codeModeExecName?: string,
46
102
  ): string | undefined {
47
103
  const names = uniqueNames(wireNames ?? []);
48
104
  if (names.length === 0) return undefined;
@@ -50,21 +106,24 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
50
106
  const advertised = new Set(names);
51
107
  // Compare in the catalog's own coordinate system. `advertised` holds WIRE names, so a
52
108
  // provider that rewrites them (Claude OAuth `custom_`, Anthropic compat `cx_`) would never
53
- // match a bare neighbor name and would forbid tools the turn actually advertises the
109
+ // match a bare neighbor name and would forbid tools the turn actually advertises -- the
54
110
  // catalog would list `custom_apply_patch` while the same sentence banned `apply_patch`.
55
111
  const unavailableNeighborNames = NEIGHBOR_AGENT_TOOL_NAMES.filter(
56
112
  name => !advertised.has(name) && !advertised.has(toWireName(name)),
57
113
  );
114
+ const verifiedCodeModeExecName = codeModeExecWireName(advertised, codeModeExecName);
58
115
 
59
116
  return [
60
117
  "Tool contract: use the current tool catalog as ground truth.",
61
- `Valid tool names for this turn are exactly ${quoteNames(names)}.`,
118
+ "Valid tool names for this turn are exactly " + quoteNames(names) + ".",
62
119
  "These listed names are the complete top-level tool-call surface for this turn.",
63
120
  "Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
64
121
  "Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
65
- "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.",
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."
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.",
66
125
  unavailableNeighborNames.length > 0
67
- ? `Do not use neighboring-agent tool names ${quoteNames(unavailableNeighborNames)} unless this turn's catalog lists those exact names.`
126
+ ? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
68
127
  : undefined,
69
128
  "If you need shell, file search, file read, edit, or discovery behavior, choose the listed tool that provides that capability.",
70
129
  "Count a tool call only after its tool result returns; batch independent read-only calls when the runtime supports it.",
@@ -72,16 +131,24 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
72
131
  }
73
132
 
74
133
  export function buildNonOpenAIToolCatalogNudgeForTools(
75
- tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
134
+ tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
76
135
  toolChoice?: OcxRequestOptions["toolChoice"],
77
136
  toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
78
137
  ): string | undefined {
79
- const visibleNames = tools
80
- ?.filter(toolChoiceToolPredicate(toolChoice))
81
- .map(toWireName);
138
+ const visible = tools?.filter(toolChoiceToolPredicate(toolChoice));
139
+ const visibleNames = visible?.map(toWireName);
140
+ // Decide code mode from the tool OBJECTS, while the `freeform` flag still exists — reducing
141
+ // to wire names first throws away the only thing that distinguishes Codex's JavaScript
142
+ // `exec` from an ordinary structured tool that happens to share the name.
143
+ const codeModeExecTool = visible?.find(isCodexCodeModeExecTool);
144
+ const codeModeExecName = codeModeExecTool
145
+ && !visible?.some(isBareShellBridgeTool)
146
+ ? toWireName(codeModeExecTool)
147
+ : undefined;
82
148
  // Neighbor names are bare and un-namespaced, so probe the same transform with a bare tool.
83
149
  return buildNonOpenAIToolCatalogNudgeFromNames(
84
150
  visibleNames,
85
151
  name => toWireName({ name }),
152
+ codeModeExecName,
86
153
  );
87
154
  }