@bitkyc08/opencodex 2.7.39 → 2.7.40-preview.20260725
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/README.md +4 -4
- package/gui/dist/assets/index-BxQ8N_K5.js +52 -0
- package/gui/dist/assets/index-CMip1DzF.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/arg-normalize.ts +23 -7
- package/src/adapters/cursor/live-transport.ts +26 -14
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +1 -1
- package/src/adapters/cursor/protobuf-events.ts +72 -13
- package/src/adapters/cursor/protobuf-request.ts +82 -11
- package/src/adapters/cursor/request-builder.ts +35 -11
- package/src/adapters/cursor/tool-definitions.ts +175 -30
- package/src/adapters/openai-chat.ts +28 -7
- package/src/adapters/openai-responses.ts +150 -4
- package/src/bridge.ts +20 -1
- package/src/claude/outbound.ts +91 -6
- package/src/codex/auth-api.ts +12 -25
- package/src/codex/auth-context.ts +48 -3
- package/src/codex/catalog/provider-fetch.ts +56 -24
- package/src/codex/model-cache.ts +23 -0
- package/src/codex/quota.ts +120 -0
- package/src/codex/routing.ts +178 -9
- package/src/config.ts +56 -1
- package/src/providers/openai-sidecar.ts +8 -1
- package/src/providers/openai-tiers.ts +18 -0
- package/src/server/adapter-resolve.ts +24 -10
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +4 -0
- package/src/server/claude-messages.ts +4 -0
- package/src/server/index.ts +3 -1
- package/src/server/live.ts +56 -0
- package/src/server/memory-watchdog.ts +1 -1
- package/src/server/responses/compact.ts +40 -10
- package/src/server/responses/core.ts +180 -26
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/service.ts +113 -30
- package/src/types.ts +52 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/anthropic-executor.ts +3 -1
- package/src/web-search/index.ts +7 -1
- package/src/web-search/loop.ts +17 -3
- package/README.ja.md +0 -445
- package/README.ko.md +0 -435
- package/README.ru.md +0 -486
- package/README.zh-CN.md +0 -411
- package/gui/dist/assets/index-B-cheu55.js +0 -52
- package/gui/dist/assets/index-oOZcqVmj.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { create, toBinary } from "@bufbuild/protobuf";
|
|
1
|
+
import { create, fromBinary, toBinary, toJson } from "@bufbuild/protobuf";
|
|
2
2
|
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
3
3
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
4
4
|
import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
|
|
@@ -7,6 +7,7 @@ import type { CursorRunRequest } from "./types";
|
|
|
7
7
|
import { isCursorExternalWireModel } from "./discovery";
|
|
8
8
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
9
9
|
import { storeCursorBlob } from "./native-exec";
|
|
10
|
+
import { estimateTokens } from "../../lib/token-estimate";
|
|
10
11
|
import {
|
|
11
12
|
AgentClientMessageSchema,
|
|
12
13
|
AgentConversationTurnStructureSchema,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
McpToolResultContentItemSchema,
|
|
24
25
|
McpToolResultSchema,
|
|
25
26
|
McpToolsSchema,
|
|
27
|
+
type McpToolDefinition,
|
|
26
28
|
ModelDetailsSchema,
|
|
27
29
|
RequestedModelSchema,
|
|
28
30
|
RequestedModel_ModelParameterbytesSchema,
|
|
@@ -76,13 +78,20 @@ function buildRequestContext() {
|
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function jsonBlob(value: unknown): Uint8Array {
|
|
80
|
-
|
|
81
|
+
function jsonBlob(value: unknown): { data: Uint8Array; serialized: string } {
|
|
82
|
+
const serialized = JSON.stringify(value);
|
|
83
|
+
return { data: encoder.encode(serialized), serialized };
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
type StoredRootBlob = {
|
|
84
87
|
id: Uint8Array;
|
|
85
88
|
byteLength: number;
|
|
89
|
+
/**
|
|
90
|
+
* The exact JSON handed to storeCursorBlob(). Retained so a token estimate can read
|
|
91
|
+
* what the wire actually carries without re-serializing — and without drifting from
|
|
92
|
+
* it after pruning or truncation (#373).
|
|
93
|
+
*/
|
|
94
|
+
serialized: string;
|
|
86
95
|
role: "system" | "user" | "assistant" | "toolResult";
|
|
87
96
|
messageIndex?: number;
|
|
88
97
|
/** Original JSON text payload used when an active tool result must be truncated to fit. */
|
|
@@ -94,10 +103,11 @@ function storedRootBlob(
|
|
|
94
103
|
role: StoredRootBlob["role"],
|
|
95
104
|
opts?: { messageIndex?: number; text?: string },
|
|
96
105
|
): StoredRootBlob {
|
|
97
|
-
const data = jsonBlob(value);
|
|
106
|
+
const { data, serialized } = jsonBlob(value);
|
|
98
107
|
return {
|
|
99
108
|
id: storeCursorBlob(data),
|
|
100
109
|
byteLength: data.byteLength,
|
|
110
|
+
serialized,
|
|
101
111
|
role,
|
|
102
112
|
...(opts?.messageIndex !== undefined ? { messageIndex: opts.messageIndex } : {}),
|
|
103
113
|
...(opts?.text !== undefined ? { text: opts.text } : {}),
|
|
@@ -164,6 +174,8 @@ function rootPromptMessages(request: CursorRunRequest): {
|
|
|
164
174
|
ids: Uint8Array[];
|
|
165
175
|
byteLength: number;
|
|
166
176
|
historyMessageStart: number;
|
|
177
|
+
/** Serialized text of the roots that survived pruning, in wire order. */
|
|
178
|
+
serialized: string[];
|
|
167
179
|
} {
|
|
168
180
|
const entries = systemPromptBlobs(request);
|
|
169
181
|
const systemEntryCount = entries.length;
|
|
@@ -173,6 +185,7 @@ function rootPromptMessages(request: CursorRunRequest): {
|
|
|
173
185
|
ids: entries.map(entry => entry.id),
|
|
174
186
|
byteLength: entries.reduce((sum, entry) => sum + entry.byteLength, 0),
|
|
175
187
|
historyMessageStart: 0,
|
|
188
|
+
serialized: entries.map(entry => entry.serialized),
|
|
176
189
|
};
|
|
177
190
|
}
|
|
178
191
|
|
|
@@ -289,6 +302,7 @@ function rootPromptMessages(request: CursorRunRequest): {
|
|
|
289
302
|
ids: selected.map(entry => entry.id),
|
|
290
303
|
byteLength: selected.reduce((sum, entry) => sum + entry.byteLength, 0),
|
|
291
304
|
historyMessageStart,
|
|
305
|
+
serialized: selected.map(entry => entry.serialized),
|
|
292
306
|
};
|
|
293
307
|
}
|
|
294
308
|
|
|
@@ -503,7 +517,46 @@ export function activePromptText(request: CursorRunRequest): string {
|
|
|
503
517
|
return last?.role === "tool" ? last.content : "";
|
|
504
518
|
}
|
|
505
519
|
|
|
506
|
-
|
|
520
|
+
/**
|
|
521
|
+
* The model-visible text of one finalized tool definition. The schema travels as
|
|
522
|
+
* packed protobuf bytes, so it is decoded back to JSON to be counted the way the
|
|
523
|
+
* model reads it.
|
|
524
|
+
*/
|
|
525
|
+
function modelVisibleToolText(definition: McpToolDefinition): string {
|
|
526
|
+
let inputSchema: unknown;
|
|
527
|
+
try {
|
|
528
|
+
inputSchema = toJson(ValueSchema, fromBinary(ValueSchema, definition.inputSchema));
|
|
529
|
+
} catch {
|
|
530
|
+
inputSchema = undefined;
|
|
531
|
+
}
|
|
532
|
+
return JSON.stringify({
|
|
533
|
+
name: definition.toolName || definition.name,
|
|
534
|
+
description: definition.description,
|
|
535
|
+
...(inputSchema !== undefined ? { inputSchema } : {}),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface PreparedCursorRunRequest {
|
|
540
|
+
bytes: Uint8Array;
|
|
541
|
+
/** Only present when the caller asked for it; see prepareCursorRunRequest(). */
|
|
542
|
+
estimatedInputTokens?: number;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Build the wire payload once, and optionally derive a token estimate from the very
|
|
547
|
+
* same roots, action text, and tool definitions that produced it.
|
|
548
|
+
*
|
|
549
|
+
* Cursor only reports absolute context size in checkpoint frames, which live in a
|
|
550
|
+
* process-local map — so after a restart a turn with no checkpoint reports
|
|
551
|
+
* inputTokens=0 and Codex sees an almost-empty context (#373). The estimate fills
|
|
552
|
+
* that gap. Deriving it here, rather than from the original request, is what keeps
|
|
553
|
+
* it honest: history the pruner dropped and tools the filter removed are already
|
|
554
|
+
* gone by this point.
|
|
555
|
+
*/
|
|
556
|
+
export function prepareCursorRunRequest(
|
|
557
|
+
request: CursorRunRequest,
|
|
558
|
+
options?: { estimateInputTokens?: boolean },
|
|
559
|
+
): PreparedCursorRunRequest {
|
|
507
560
|
const rawText = activePromptText(request);
|
|
508
561
|
const lastRole = request.messages.at(-1)?.role;
|
|
509
562
|
const text = lastRole === "user" || lastRole === "developer"
|
|
@@ -536,6 +589,10 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
|
536
589
|
const rootPromptMessagesState = rootPromptMessages(request);
|
|
537
590
|
const rootPromptMessageIds = rootPromptMessagesState.ids;
|
|
538
591
|
const turnIds = conversationTurns(request, rootPromptMessagesState.historyMessageStart);
|
|
592
|
+
// Hoisted out of the mcp_tools spread below so the estimate can read the same
|
|
593
|
+
// filtered definitions the wire carries. Both helpers are pure.
|
|
594
|
+
const visibleTools = cursorToolsForActivePrompt(request.tools, rawText, request.toolChoice);
|
|
595
|
+
const mcpToolDefs = buildCursorToolDefinitions(visibleTools, request.toolChoice);
|
|
539
596
|
debugProviderDiagnostic("cursor", "run-request", {
|
|
540
597
|
wireModel: request.modelId,
|
|
541
598
|
action: actionCase,
|
|
@@ -598,15 +655,29 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
|
598
655
|
// the event-state `clientToolNames` use (live-transport.ts). Advertising the raw `request.tools`
|
|
599
656
|
// here would let mcp_tools expose a tool that the event state does not recognize for a generic
|
|
600
657
|
// tool-count prompt, so a call to it would be rejected as an unknown Responses tool.
|
|
601
|
-
...(()
|
|
602
|
-
const visibleTools = cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice);
|
|
603
|
-
const mcpToolDefs = buildCursorToolDefinitions(visibleTools, request.toolChoice);
|
|
604
|
-
return mcpToolDefs.length > 0 ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) } : {};
|
|
605
|
-
})(),
|
|
658
|
+
...(mcpToolDefs.length > 0 ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) } : {}),
|
|
606
659
|
});
|
|
607
660
|
|
|
608
661
|
const message = create(AgentClientMessageSchema, {
|
|
609
662
|
message: { case: "runRequest", value: runRequest },
|
|
610
663
|
});
|
|
611
|
-
|
|
664
|
+
const bytes = toBinary(AgentClientMessageSchema, message);
|
|
665
|
+
if (!options?.estimateInputTokens) return { bytes };
|
|
666
|
+
|
|
667
|
+
// Same instances that produced `bytes`, so the estimate cannot count history or
|
|
668
|
+
// tools the payload dropped — the defect that blocked PR #376.
|
|
669
|
+
const modelVisibleParts = [
|
|
670
|
+
...rootPromptMessagesState.serialized,
|
|
671
|
+
...(actionCase === "userMessageAction" ? [text] : []),
|
|
672
|
+
...mcpToolDefs.map(modelVisibleToolText),
|
|
673
|
+
];
|
|
674
|
+
return {
|
|
675
|
+
bytes,
|
|
676
|
+
estimatedInputTokens: estimateTokens(modelVisibleParts.join("\n"), request.modelId),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Back-compat wrapper: callers that only need the wire bytes. */
|
|
681
|
+
export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
|
|
682
|
+
return prepareCursorRunRequest(request).bytes;
|
|
612
683
|
}
|
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
cursorMcpToolEncodedSize,
|
|
16
16
|
cursorMcpToolsEncodedSize,
|
|
17
17
|
cursorToolAllowedByChoice,
|
|
18
|
+
cursorToolChoiceAliases,
|
|
18
19
|
cursorToolWireName,
|
|
19
20
|
cursorToolsForActivePrompt,
|
|
21
|
+
isBareCodexShellBridgeTool,
|
|
20
22
|
} from "./tool-definitions";
|
|
21
23
|
import { lookupCursorThreadConversation } from "./thread-continuity";
|
|
22
24
|
|
|
@@ -35,10 +37,18 @@ function explicitlySelectedNames(choice: OcxToolChoice | undefined): Set<string>
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function toolPriority(tool: OcxTool, selectedNames: ReadonlySet<string>): number {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
40
|
+
// Shell bridge and apply_patch outrank unrelated allowed_tools entries so a large
|
|
41
|
+
// selected filler cannot starve the Codex execution path during truncation (#399).
|
|
42
|
+
if (isBareCodexShellBridgeTool(tool)) return 0;
|
|
43
|
+
if (!tool.namespace && tool.name === "apply_patch") return 1;
|
|
44
|
+
if (cursorToolChoiceAliases(tool).some(name => selectedNames.has(name))) return 2;
|
|
45
|
+
if (tool.loadedFromToolSearch) return 3;
|
|
46
|
+
if (!tool.namespace) return 4;
|
|
47
|
+
return 5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPinnedCursorTool(tool: OcxTool, selectedNames: ReadonlySet<string>): boolean {
|
|
51
|
+
return toolPriority(tool, selectedNames) <= 2;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/**
|
|
@@ -50,7 +60,8 @@ export function applyCursorToolBudget(
|
|
|
50
60
|
tools: readonly OcxTool[] | undefined,
|
|
51
61
|
toolChoice: OcxToolChoice | undefined,
|
|
52
62
|
): CursorToolBudgetResult {
|
|
53
|
-
const
|
|
63
|
+
const catalog = tools ?? [];
|
|
64
|
+
const eligible = catalog.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
54
65
|
if (
|
|
55
66
|
eligible.length <= CURSOR_TOOL_COUNT_LIMIT
|
|
56
67
|
&& cursorMcpToolsEncodedSize(eligible, toolChoice) <= CURSOR_TOOL_BYTES_LIMIT
|
|
@@ -64,15 +75,28 @@ export function applyCursorToolBudget(
|
|
|
64
75
|
const keptSet = new Set<OcxTool>();
|
|
65
76
|
let keptBytes = 0;
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
if (kept.length >= CURSOR_TOOL_COUNT_LIMIT)
|
|
78
|
+
const tryKeep = (tool: OcxTool): boolean => {
|
|
79
|
+
if (keptSet.has(tool) || kept.length >= CURSOR_TOOL_COUNT_LIMIT) return keptSet.has(tool);
|
|
69
80
|
// Repeated protobuf message fields serialize as concatenated tag/length/value entries,
|
|
70
81
|
// so each one-entry wrapper size is the exact additive contribution to McpTools.
|
|
71
|
-
const candidateBytes = cursorMcpToolEncodedSize(
|
|
72
|
-
if (keptBytes + candidateBytes > CURSOR_TOOL_BYTES_LIMIT)
|
|
73
|
-
kept.push(
|
|
74
|
-
keptSet.add(
|
|
82
|
+
const candidateBytes = cursorMcpToolEncodedSize(tool, toolChoice);
|
|
83
|
+
if (keptBytes + candidateBytes > CURSOR_TOOL_BYTES_LIMIT) return false;
|
|
84
|
+
kept.push(tool);
|
|
85
|
+
keptSet.add(tool);
|
|
75
86
|
keptBytes += candidateBytes;
|
|
87
|
+
return true;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Phase 1: selected tools + shell bridge + apply_patch (priority <= 2).
|
|
91
|
+
// Pins are admitted before filler so a crowded catalog cannot drop the Codex execution path (#399).
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (!isPinnedCursorTool(candidate.tool, selectedNames)) continue;
|
|
94
|
+
tryKeep(candidate.tool);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Phase 2: remaining tools by priority.
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
tryKeep(candidate.tool);
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
return {
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import { create, fromJson, toBinary, type JsonValue } from "@bufbuild/protobuf";
|
|
2
2
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
3
3
|
import type { OcxRequestOptions, OcxTool } from "../../types";
|
|
4
|
-
import { namespacedToolName } from "../../types";
|
|
4
|
+
import { namespacedToolName, toolChoiceAliases } from "../../types";
|
|
5
5
|
import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from "./gen/agent_pb";
|
|
6
6
|
|
|
7
7
|
export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
|
|
8
8
|
export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
|
|
9
|
+
export const CODEX_SHELL_COMMAND_TOOL = "shell_command";
|
|
9
10
|
export const CODEX_APPLY_PATCH_TOOL = "apply_patch";
|
|
10
11
|
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
12
|
+
export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const;
|
|
13
|
+
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
14
|
+
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell; never say native shell is blocked.';
|
|
15
|
+
export const CURSOR_SHELL_ALIAS_USER_HINT =
|
|
16
|
+
"Use the Codex shell bridge tool listed this turn (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` name). Prefer that bridge over Cursor-native Shell/Read. Never say phrases like \"Native shell access is blocked\" or \"Shell access is blocked\" — silently use the bridge unless that bridge tool itself fails.";
|
|
13
17
|
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
14
18
|
|
|
15
19
|
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
16
|
-
"For generic tool-use/count demos, satisfy the request with repeated `exec_command`
|
|
17
|
-
"`exec_command`
|
|
20
|
+
"For generic tool-use/count demos, satisfy the request with repeated Codex shell bridge calls (`shell_command` or `exec_command`) for harmless commands.",
|
|
21
|
+
"`shell_command` / `exec_command` are the Codex Responses shell bridge exposed through Cursor's tool protocol; do not describe them as an external MCP server tool.",
|
|
18
22
|
"Do not use `run_shell` unless this turn's tool catalog lists it.",
|
|
19
|
-
"A request for N tools means N separate
|
|
20
|
-
"For independent read-only or output-only commands, emit all requested
|
|
23
|
+
"A request for N tools means N separate shell-bridge invocations/results; never satisfy it with one chained shell command such as `cmd1 && cmd2`.",
|
|
24
|
+
"For independent read-only or output-only commands, emit all requested shell-bridge calls in the same response before waiting when the runtime supports parallel tool calls.",
|
|
21
25
|
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
|
|
22
|
-
"If parallel emission is unavailable, continue with separate
|
|
26
|
+
"If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.",
|
|
23
27
|
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
|
|
24
28
|
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names.",
|
|
29
|
+
"Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Do not narrate Cursor-native Shell/Read routing.",
|
|
25
30
|
].join(" ");
|
|
26
31
|
|
|
27
32
|
export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
@@ -38,8 +43,91 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
|
38
43
|
additionalProperties: false,
|
|
39
44
|
} as const;
|
|
40
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
48
|
+
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
49
|
+
* `command`. Keeping `cmd` out of this schema lets `normalizeArgKeys` rewrite `cmd` → `command`.
|
|
50
|
+
*/
|
|
51
|
+
export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
command: { type: "string", description: "Shell command to execute." },
|
|
55
|
+
workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
|
|
56
|
+
shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
57
|
+
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
58
|
+
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
59
|
+
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
60
|
+
max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
|
|
61
|
+
},
|
|
62
|
+
required: ["command"],
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export function isCodexShellBridgeToolName(name: string): boolean {
|
|
66
|
+
return (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Direct key lookup, then shell_command/exec_command sibling aliases when the key is a bridge name.
|
|
71
|
+
* Used for catalog admission, schema normalize maps, and Responses name maps (#399).
|
|
72
|
+
*/
|
|
73
|
+
export function resolveShellBridgeAliasKey<T>(
|
|
74
|
+
key: string,
|
|
75
|
+
lookup: (name: string) => T | undefined,
|
|
76
|
+
): T | undefined {
|
|
77
|
+
const direct = lookup(key);
|
|
78
|
+
if (direct !== undefined) return direct;
|
|
79
|
+
if (!isCodexShellBridgeToolName(key)) return undefined;
|
|
80
|
+
for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) {
|
|
81
|
+
if (alias === key) continue;
|
|
82
|
+
const hit = lookup(alias);
|
|
83
|
+
if (hit !== undefined) return hit;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function cursorToolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
|
|
89
|
+
const aliases = new Set(toolChoiceAliases(tool));
|
|
90
|
+
if (isBareCodexShellBridgeTool(tool)) {
|
|
91
|
+
for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) aliases.add(alias);
|
|
92
|
+
}
|
|
93
|
+
return [...aliases];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function catalogHasBareCodexShellBridge(
|
|
97
|
+
catalog: readonly Pick<OcxTool, "namespace" | "name">[],
|
|
98
|
+
): boolean {
|
|
99
|
+
return catalog.some(isBareCodexShellBridgeTool);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Catalog-aware tool_choice matching for Cursor.
|
|
104
|
+
* When a bare Codex shell bridge is in the catalog, raw `shell_command` / `exec_command`
|
|
105
|
+
* choices select only that bridge (never a namespaced remote with the same raw name).
|
|
106
|
+
* When no bare bridge exists, raw bridge names may select a namespaced tool by raw name.
|
|
107
|
+
* Explicit wire names (`mcp__remote__exec_command`) always match the namespaced tool.
|
|
108
|
+
*/
|
|
109
|
+
function cursorToolChoiceMatches(
|
|
110
|
+
tool: Pick<OcxTool, "namespace" | "name">,
|
|
111
|
+
choiceName: string,
|
|
112
|
+
catalog: readonly Pick<OcxTool, "namespace" | "name">[],
|
|
113
|
+
): boolean {
|
|
114
|
+
if (isCodexShellBridgeToolName(choiceName)) {
|
|
115
|
+
if (catalogHasBareCodexShellBridge(catalog)) {
|
|
116
|
+
return isBareCodexShellBridgeTool(tool);
|
|
117
|
+
}
|
|
118
|
+
return tool.name === choiceName || cursorToolWireName(tool) === choiceName;
|
|
119
|
+
}
|
|
120
|
+
if (tool.name === choiceName || cursorToolWireName(tool) === choiceName) return true;
|
|
121
|
+
return cursorToolChoiceAliases(tool).includes(choiceName);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isBareCodexShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
|
|
125
|
+
return !tool.namespace && isCodexShellBridgeToolName(tool.name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @deprecated Prefer isBareCodexShellBridgeTool; kept for older call sites/tests. */
|
|
41
129
|
function isBareCodexExecCommandTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
|
|
42
|
-
return
|
|
130
|
+
return isBareCodexShellBridgeTool(tool);
|
|
43
131
|
}
|
|
44
132
|
|
|
45
133
|
export function cursorRequestHasShellAlias(tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined): boolean {
|
|
@@ -50,7 +138,8 @@ export function cursorRequestAdvertisesApplyPatch(
|
|
|
50
138
|
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
51
139
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
52
140
|
): boolean {
|
|
53
|
-
|
|
141
|
+
const catalog = tools ?? [];
|
|
142
|
+
return catalog.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice, catalog));
|
|
54
143
|
}
|
|
55
144
|
|
|
56
145
|
export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
|
|
@@ -60,8 +149,9 @@ export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): s
|
|
|
60
149
|
/**
|
|
61
150
|
* Cursor's harness shows MCP tools to the model as `mcp_<providerIdentifier>_<toolName>`; models
|
|
62
151
|
* sometimes call that display name verbatim instead of the advertised short name (live 20:41/21:00
|
|
63
|
-
* sessions: `mcp_opencodex-responses_exec_command`).
|
|
64
|
-
*
|
|
152
|
+
* sessions: `mcp_opencodex-responses_exec_command` / `mcp_opencodex-responses_shell_command`).
|
|
153
|
+
* Fold the display prefix back to the advertised wire name, and treat `shell_command` /
|
|
154
|
+
* `exec_command` as the same Codex shell bridge, so alias thrash does not become "tool not found".
|
|
65
155
|
*/
|
|
66
156
|
const CURSOR_MCP_DISPLAY_PREFIX = `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_`;
|
|
67
157
|
|
|
@@ -71,15 +161,60 @@ export function normalizeCursorWireName(name: string): string {
|
|
|
71
161
|
|
|
72
162
|
export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?: ReadonlyMap<string, string>): string {
|
|
73
163
|
const normalized = normalizeCursorWireName(name);
|
|
74
|
-
|
|
164
|
+
if (!cursorToolNameMap) return normalized;
|
|
165
|
+
return resolveShellBridgeAliasKey(normalized, alias => cursorToolNameMap.get(alias)) ?? normalized;
|
|
75
166
|
}
|
|
76
167
|
|
|
168
|
+
/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
|
|
77
169
|
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
78
170
|
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
79
171
|
}
|
|
80
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Schema used to normalize completed Cursor tool args back to Responses/Codex field names.
|
|
175
|
+
* Must NOT reuse `cursorToolInputSchema` for the shell bridge: advertising `cmd` while also
|
|
176
|
+
* treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
|
|
177
|
+
*/
|
|
178
|
+
export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
|
|
179
|
+
if (isBareCodexShellBridgeTool(tool)) {
|
|
180
|
+
return shellBridgeArgNormalizeSchema(tool);
|
|
181
|
+
}
|
|
182
|
+
return tool.parameters ?? {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown {
|
|
186
|
+
const parameters = tool.parameters;
|
|
187
|
+
if (!parameters || typeof parameters !== "object") return CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA;
|
|
188
|
+
const base = parameters as Record<string, unknown>;
|
|
189
|
+
const rawProps = base.properties && typeof base.properties === "object"
|
|
190
|
+
? { ...(base.properties as Record<string, unknown>) }
|
|
191
|
+
: {};
|
|
192
|
+
const required = Array.isArray(base.required) ? [...base.required as unknown[]] : [];
|
|
193
|
+
const requiresCommand = required.includes("command") || "command" in rawProps;
|
|
194
|
+
const requiresCmd = required.includes("cmd") || "cmd" in rawProps;
|
|
195
|
+
const shouldRewriteCmdToCommand = tool.name === CODEX_SHELL_COMMAND_TOOL || requiresCommand;
|
|
196
|
+
|
|
197
|
+
if (!shouldRewriteCmdToCommand && requiresCmd) {
|
|
198
|
+
return parameters;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Drop Cursor-preferred aliases so normalizeArgKeys can rewrite them to Responses keys.
|
|
202
|
+
delete rawProps.cmd;
|
|
203
|
+
const properties = {
|
|
204
|
+
...CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties,
|
|
205
|
+
...rawProps,
|
|
206
|
+
command: rawProps.command ?? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties.command,
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
...base,
|
|
210
|
+
type: "object",
|
|
211
|
+
properties,
|
|
212
|
+
required: requiresCommand ? required : ["command"],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
81
216
|
function activeTextMentionsExecCommand(text: string): boolean {
|
|
82
|
-
return /\
|
|
217
|
+
return /\b(?:exec_command|shell_command)\b/i.test(text);
|
|
83
218
|
}
|
|
84
219
|
|
|
85
220
|
function looksLikeShellCommandRequest(text: string): boolean {
|
|
@@ -127,9 +262,9 @@ function cursorGenericToolUseHint(text: string): string {
|
|
|
127
262
|
const count = requestedCursorToolUseCount(text);
|
|
128
263
|
if (!count) return CURSOR_GENERIC_TOOL_USE_USER_HINT;
|
|
129
264
|
return [
|
|
130
|
-
`This turn requests ${count} tool uses: emit exactly ${count} separate
|
|
131
|
-
`One
|
|
132
|
-
`Prefer one parallel tool-call batch containing all ${count} independent
|
|
265
|
+
`This turn requests ${count} tool uses: emit exactly ${count} separate Codex shell bridge function calls/results (\`shell_command\` or \`exec_command\`).`,
|
|
266
|
+
`One shell-bridge call containing chained commands counts as 1 tool call, not ${count}.`,
|
|
267
|
+
`Prefer one parallel tool-call batch containing all ${count} independent shell-bridge calls before waiting for results.`,
|
|
133
268
|
CURSOR_GENERIC_TOOL_USE_USER_HINT,
|
|
134
269
|
].join(" ");
|
|
135
270
|
}
|
|
@@ -176,7 +311,8 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
|
|
|
176
311
|
): readonly T[] | undefined {
|
|
177
312
|
if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools;
|
|
178
313
|
const execTools = tools?.filter(isBareCodexExecCommandTool);
|
|
179
|
-
|
|
314
|
+
const catalog = tools ?? [];
|
|
315
|
+
if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools;
|
|
180
316
|
return execTools && execTools.length > 0 ? execTools : tools;
|
|
181
317
|
}
|
|
182
318
|
|
|
@@ -199,13 +335,17 @@ export function appendCursorShellAliasHint(
|
|
|
199
335
|
return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${CURSOR_SHELL_ALIAS_USER_HINT}`;
|
|
200
336
|
}
|
|
201
337
|
|
|
202
|
-
export function cursorToolAllowedByChoice(
|
|
338
|
+
export function cursorToolAllowedByChoice(
|
|
339
|
+
tool: Pick<OcxTool, "namespace" | "name">,
|
|
340
|
+
toolChoice: OcxRequestOptions["toolChoice"] | undefined,
|
|
341
|
+
catalog: readonly Pick<OcxTool, "namespace" | "name">[] = [tool],
|
|
342
|
+
): boolean {
|
|
203
343
|
if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true;
|
|
204
344
|
if (toolChoice === "none") return false;
|
|
205
345
|
if ("allowedTools" in toolChoice) {
|
|
206
|
-
return toolChoice.allowedTools.
|
|
346
|
+
return toolChoice.allowedTools.some(choiceName => cursorToolChoiceMatches(tool, choiceName, catalog));
|
|
207
347
|
}
|
|
208
|
-
return tool
|
|
348
|
+
return cursorToolChoiceMatches(tool, toolChoice.name, catalog);
|
|
209
349
|
}
|
|
210
350
|
|
|
211
351
|
function quotedNames(names: readonly string[]): string {
|
|
@@ -232,13 +372,15 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
232
372
|
if (!tools?.length) return undefined;
|
|
233
373
|
const wireNames = [...new Set(
|
|
234
374
|
tools
|
|
235
|
-
.filter(tool => cursorToolAllowedByChoice(tool, toolChoice))
|
|
375
|
+
.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools))
|
|
236
376
|
.map(tool => cursorToolWireName(tool)),
|
|
237
377
|
)];
|
|
238
378
|
if (wireNames.length === 0) return undefined;
|
|
239
379
|
|
|
240
380
|
const listedNames = quotedNames(wireNames);
|
|
241
|
-
const
|
|
381
|
+
const shellBridgeNames = wireNames.filter(isCodexShellBridgeToolName);
|
|
382
|
+
const hasBareExec = shellBridgeNames.length > 0;
|
|
383
|
+
const shellBridgeLabel = quotedNames(shellBridgeNames.length > 0 ? shellBridgeNames : [...CODEX_SHELL_BRIDGE_TOOL_NAMES]);
|
|
242
384
|
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
|
|
243
385
|
const discoveryTools = discoveryToolLabel(wireNames);
|
|
244
386
|
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
|
|
@@ -249,31 +391,34 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
249
391
|
? `This turn does not expose neighboring-agent tool names ${quotedNames(unavailableNeighborNames)}; do not call or suggest them unless the catalog lists them.`
|
|
250
392
|
: undefined,
|
|
251
393
|
hasBareExec
|
|
252
|
-
?
|
|
394
|
+
? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
|
|
395
|
+
: undefined,
|
|
396
|
+
hasBareExec
|
|
397
|
+
? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
|
|
253
398
|
: undefined,
|
|
254
399
|
hasBareExec
|
|
255
|
-
? "
|
|
400
|
+
? "Never tell the user that shell or read access is blocked, disabled, or denied unless the Codex shell bridge tool itself fails. Prefer the bridge over Cursor-native Shell/Read; do not narrate phrases like \"Native shell access is blocked\" — silently call `shell_command` / `exec_command`."
|
|
256
401
|
: undefined,
|
|
257
402
|
"Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
|
|
258
403
|
hasBareExec
|
|
259
|
-
?
|
|
404
|
+
? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.`
|
|
260
405
|
: undefined,
|
|
261
406
|
hasApplyPatch
|
|
262
407
|
? "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
263
408
|
: undefined,
|
|
264
409
|
hasBareExec
|
|
265
|
-
? "For tool-count demos, each counted tool must be a separate
|
|
410
|
+
? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
|
|
266
411
|
: undefined,
|
|
267
412
|
"For independent read-only tool-count or batch requests, prefer one response containing multiple tool calls before waiting for results when the runtime supports parallel tool calls.",
|
|
268
413
|
hasBareExec
|
|
269
|
-
? "For bridge tool-count batches, emit sibling
|
|
414
|
+
? "For bridge tool-count batches, emit sibling shell-bridge calls together before any result is needed because the bridge may suspend after a returned tool call."
|
|
270
415
|
: undefined,
|
|
271
416
|
discoveryTools
|
|
272
417
|
? `Use ${discoveryTools} only for explicit discovery/resource tasks, not generic tool-count demos.`
|
|
273
418
|
: undefined,
|
|
274
419
|
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
275
420
|
hasBareExec
|
|
276
|
-
?
|
|
421
|
+
? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, silently use ${shellBridgeLabel} with the equivalent shell command instead (e.g. \`cat\`, \`ls\`, \`rg\`, \`grep\`). Do not tell the user access is blocked. For file edits, use \`apply_patch\` when available.`
|
|
277
422
|
: undefined,
|
|
278
423
|
].filter((note): note is string => typeof note === "string");
|
|
279
424
|
return notes.join(" ");
|
|
@@ -291,7 +436,7 @@ export function buildCursorToolDefinitions(
|
|
|
291
436
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
292
437
|
): McpToolDefinition[] {
|
|
293
438
|
if (!tools?.length) return [];
|
|
294
|
-
return tools.filter(tool => cursorToolAllowedByChoice(tool, toolChoice)).map(tool => {
|
|
439
|
+
return tools.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools)).map(tool => {
|
|
295
440
|
const wireName = cursorToolWireName(tool);
|
|
296
441
|
return create(McpToolDefinitionSchema, {
|
|
297
442
|
name: wireName,
|
|
@@ -61,6 +61,13 @@ function extractErrorDetail(parsed: unknown): string | undefined {
|
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function developerSystemText(message: OcxMessage): string | undefined {
|
|
65
|
+
if (message.role !== "developer") return undefined;
|
|
66
|
+
if (typeof message.content === "string") return message.content;
|
|
67
|
+
if (message.content.some(part => part.type === "image")) return undefined;
|
|
68
|
+
return message.content.map(part => (part as OcxTextContent).text).join("");
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
65
72
|
const out: unknown[] = [];
|
|
66
73
|
const { context, options } = parsed;
|
|
@@ -112,7 +119,20 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
112
119
|
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
113
120
|
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
114
121
|
: undefined;
|
|
115
|
-
|
|
122
|
+
// Chat templates used by LM Studio, llama.cpp, and other strict OpenAI-compatible
|
|
123
|
+
// backends require every system instruction to precede conversation history. Codex can
|
|
124
|
+
// append developer reminders after user turns, so fold text-only developer messages into
|
|
125
|
+
// the single leading system message instead of emitting role:"system" in place. Developer
|
|
126
|
+
// messages with images cannot be represented as system content and remain user-compatible
|
|
127
|
+
// vision messages at their original position below.
|
|
128
|
+
const developerSystemParts = context.messages
|
|
129
|
+
.map(developerSystemText)
|
|
130
|
+
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
131
|
+
const systemParts = [
|
|
132
|
+
...(context.systemPrompt ?? []),
|
|
133
|
+
...developerSystemParts,
|
|
134
|
+
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
135
|
+
];
|
|
116
136
|
if (systemParts.length > 0) {
|
|
117
137
|
// Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
|
|
118
138
|
// base_instructions is ignored at request time). Neutralize that one identity line
|
|
@@ -126,18 +146,19 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
126
146
|
switch (msg.role) {
|
|
127
147
|
case "user":
|
|
128
148
|
case "developer": {
|
|
129
|
-
const
|
|
149
|
+
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
150
|
+
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
151
|
+
if (msg.role === "developer" && !hasImages) break;
|
|
130
152
|
let chatMsg: Record<string, unknown>;
|
|
131
153
|
if (typeof msg.content === "string") {
|
|
132
|
-
chatMsg = { role, content: msg.content };
|
|
154
|
+
chatMsg = { role: "user", content: msg.content };
|
|
133
155
|
} else {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
chatMsg = { role, content: parts.map(p => (p as OcxTextContent).text).join("") };
|
|
156
|
+
if (!hasImages) {
|
|
157
|
+
chatMsg = { role: "user", content: parts!.map(p => (p as OcxTextContent).text).join("") };
|
|
137
158
|
} else {
|
|
138
159
|
// Vision: chat-completions content-parts array. Images are only valid on the user role,
|
|
139
160
|
// and the data URL goes straight into image_url.url (never the token-exploding text path).
|
|
140
|
-
const chatParts = parts
|
|
161
|
+
const chatParts = parts!.map(p => p.type === "image"
|
|
141
162
|
? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
|
|
142
163
|
: { type: "text", text: (p as OcxTextContent).text });
|
|
143
164
|
chatMsg = { role: "user", content: chatParts };
|