@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.
- package/gui/dist/assets/{index-C3FiAveG.js → index-TFd4xi1L.js} +8 -8
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -0
- package/src/adapters/client-fingerprint.ts +9 -5
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +17 -0
- package/src/adapters/cursor/cursor-errors.ts +49 -0
- package/src/adapters/cursor/live-models.ts +36 -2
- package/src/adapters/cursor/live-transport.ts +55 -4
- package/src/adapters/cursor/native-exec.ts +9 -0
- package/src/adapters/cursor/protobuf-request.ts +160 -9
- package/src/adapters/cursor/request-builder.ts +9 -1
- package/src/adapters/cursor/tool-definitions.ts +7 -2
- package/src/adapters/google-antigravity-wire.ts +1 -1
- package/src/adapters/google.ts +30 -12
- package/src/adapters/openai-responses-url.ts +5 -3
- package/src/adapters/registry.ts +3 -1
- package/src/adapters/tool-catalog-nudge.ts +76 -9
- package/src/bridge.ts +53 -9
- package/src/claude/context-windows.ts +2 -2
- package/src/claude/desktop-3p.ts +6 -6
- package/src/claude/model-info.ts +2 -2
- package/src/cli/claude-desktop.ts +2 -3
- package/src/codex/app-server-processes.ts +69 -35
- package/src/codex/catalog/metadata.ts +29 -10
- package/src/codex/catalog/provider-fetch.ts +21 -11
- package/src/codex/catalog.ts +1 -1
- package/src/codex/injected-marker.ts +9 -3
- package/src/codex/user-identity.ts +88 -6
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +61 -53
- package/src/grok/sync.ts +2 -4
- package/src/lab/projection/rebuild.ts +36 -18
- package/src/lib/windows-elevation.ts +18 -3
- package/src/lib/windows-secret-acl.ts +49 -19
- package/src/oauth/google-antigravity.ts +7 -2
- package/src/providers/antigravity-models.ts +126 -17
- package/src/providers/derive.ts +11 -1
- package/src/responses/parser.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +16 -1
- package/src/responses/thought-signature-replay.ts +17 -1
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +2 -10
- package/src/routing/capability.ts +5 -6
- package/src/server/index.ts +3 -4
- package/src/server/management/agent-settings-routes.ts +5 -5
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +2 -0
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/shared.ts +4 -4
- package/src/server/management-api.ts +2 -2
- package/src/server/request-log.ts +11 -3
- package/src/server/responses/core.ts +4 -1
- package/src/server/responses/input-admission.ts +13 -10
- package/src/server/system-env.ts +3 -3
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
}))
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
package/src/adapters/google.ts
CHANGED
|
@@ -48,19 +48,17 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
48
48
|
].join("\n");
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Google
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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"),
|
|
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
|
-
:
|
|
400
|
-
|
|
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(
|
|
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
|
|
11
|
-
const
|
|
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
|
-
|
|
14
|
+
url.pathname = `${withoutV1}/v1/responses`;
|
|
15
|
+
return url.toString();
|
|
14
16
|
}
|
package/src/adapters/registry.ts
CHANGED
|
@@ -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) =>
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|