@bitkyc08/opencodex 2.7.39 → 2.7.40
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-CMip1DzF.css +1 -0
- package/gui/dist/assets/index-cydcmbzC.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- 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
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-cydcmbzC.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CMip1DzF.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.40",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"type": "git",
|
|
90
90
|
"url": "git+https://github.com/lidge-jun/opencodex.git"
|
|
91
91
|
},
|
|
92
|
-
"homepage": "https://
|
|
92
|
+
"homepage": "https://opencodex.me/",
|
|
93
93
|
"bugs": {
|
|
94
94
|
"url": "https://github.com/lidge-jun/opencodex/issues"
|
|
95
95
|
},
|
|
@@ -60,8 +60,9 @@ function schemaPropertyNames(schema: unknown): Set<string> | undefined {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Normalize argument keys against the tool's declared schema. Keys not in the schema that have a
|
|
63
|
-
* known alias pointing to a schema-declared key are renamed.
|
|
64
|
-
*
|
|
63
|
+
* known alias pointing to a schema-declared key are renamed. Explicitly supplied canonical keys
|
|
64
|
+
* always win over aliases, regardless of object property insertion order — conflicting aliases
|
|
65
|
+
* are discarded rather than left beside the canonical key.
|
|
65
66
|
*
|
|
66
67
|
* Returns the original object reference if no changes were needed (cheap identity check for callers).
|
|
67
68
|
*/
|
|
@@ -69,6 +70,11 @@ export function normalizeArgKeys(args: Record<string, unknown>, toolSchema: unkn
|
|
|
69
70
|
const declared = schemaPropertyNames(toolSchema);
|
|
70
71
|
if (!declared || declared.size === 0) return args;
|
|
71
72
|
|
|
73
|
+
const suppliedCanonical = new Set<string>();
|
|
74
|
+
for (const key of Object.keys(args)) {
|
|
75
|
+
if (declared.has(key)) suppliedCanonical.add(key);
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
let changed = false;
|
|
73
79
|
const result: Record<string, unknown> = {};
|
|
74
80
|
for (const [key, value] of Object.entries(args)) {
|
|
@@ -77,12 +83,22 @@ export function normalizeArgKeys(args: Record<string, unknown>, toolSchema: unkn
|
|
|
77
83
|
continue;
|
|
78
84
|
}
|
|
79
85
|
const canonical = KEY_ALIASES.get(key.toLowerCase());
|
|
80
|
-
if (canonical && declared.has(canonical)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
if (canonical && declared.has(canonical)) {
|
|
87
|
+
// Canonical was explicitly supplied (any order) — drop the alias entirely.
|
|
88
|
+
if (suppliedCanonical.has(canonical)) {
|
|
89
|
+
changed = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// First alias fills the canonical slot; later aliases for the same key are discarded.
|
|
93
|
+
if (!(canonical in result)) {
|
|
94
|
+
result[canonical] = value;
|
|
95
|
+
changed = true;
|
|
96
|
+
} else {
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
85
100
|
}
|
|
101
|
+
result[key] = value;
|
|
86
102
|
}
|
|
87
103
|
return changed ? result : args;
|
|
88
104
|
}
|
|
@@ -2,7 +2,7 @@ import http2 from "node:http2";
|
|
|
2
2
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
3
|
import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
|
|
4
4
|
import { CONNECT_FLAG_END_STREAM, decodeAvailableConnectFrames, encodeConnectFrame } from "./framing";
|
|
5
|
-
import { activePromptText,
|
|
5
|
+
import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
|
|
6
6
|
import {
|
|
7
7
|
createCursorContextUsageTracker,
|
|
8
8
|
createCursorProtobufEventState,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
mapCursorProtobufServerMessage,
|
|
11
11
|
mapSyntheticMcpExecToToolEvents,
|
|
12
12
|
reportableContextTokens,
|
|
13
|
+
resolvedTurnUsage,
|
|
13
14
|
usageFromContextTokens,
|
|
14
15
|
} from "./protobuf-events";
|
|
15
16
|
import {
|
|
@@ -50,7 +51,7 @@ import {
|
|
|
50
51
|
buildCursorToolDefinitions,
|
|
51
52
|
cursorRequestAdvertisesApplyPatch,
|
|
52
53
|
cursorRequestHasShellAlias,
|
|
53
|
-
|
|
54
|
+
cursorToolArgNormalizeSchema,
|
|
54
55
|
cursorToolWireName,
|
|
55
56
|
cursorToolsForActivePrompt,
|
|
56
57
|
isGenericToolUseCountDemoPrompt,
|
|
@@ -506,21 +507,33 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
506
507
|
const cursorToolNameMap = new Map<string, string>();
|
|
507
508
|
for (const tool of cursorVisibleTools ?? []) {
|
|
508
509
|
const cursorWireName = cursorToolWireName(tool);
|
|
509
|
-
|
|
510
|
+
// Normalize against Responses/Codex field names, not the Cursor advertisement schema.
|
|
511
|
+
// Advertising `cmd` while also storing that schema here left `cmd` unmapped and Codex
|
|
512
|
+
// rejected shell_command with "missing field `command`" (#399).
|
|
513
|
+
toolSchemas.set(cursorWireName, cursorToolArgNormalizeSchema(tool));
|
|
510
514
|
cursorToolNameMap.set(cursorWireName, namespacedToolName(tool.namespace, tool.name));
|
|
511
515
|
}
|
|
516
|
+
const contextUsage = cursorContextUsageTracker.controlsForConversation(request.conversationId, {
|
|
517
|
+
clearPrior: request.contextUsageReset === true,
|
|
518
|
+
storeCheckpoints: request.contextUsageStoreCheckpoints !== false,
|
|
519
|
+
});
|
|
520
|
+
// Build the payload once. The estimate is only worth deriving when there is no
|
|
521
|
+
// carry-forward to fall back on — with a carry present it would never be used (#373).
|
|
522
|
+
const prepared = prepareCursorRunRequest(request, {
|
|
523
|
+
estimateInputTokens: contextUsage.carryForwardTokens === undefined,
|
|
524
|
+
});
|
|
512
525
|
state = createCursorProtobufEventState({
|
|
513
526
|
clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
|
|
514
527
|
parallelToolCalls: request.parallelToolCalls,
|
|
515
528
|
toolSchemas,
|
|
516
529
|
cursorToolNameMap,
|
|
517
|
-
contextUsage
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
530
|
+
contextUsage,
|
|
531
|
+
...(prepared.estimatedInputTokens !== undefined
|
|
532
|
+
? { estimatedInputTokens: prepared.estimatedInputTokens }
|
|
533
|
+
: {}),
|
|
521
534
|
});
|
|
522
535
|
|
|
523
|
-
this.open(
|
|
536
|
+
this.open(prepared.bytes, signal, state, push, err => {
|
|
524
537
|
failure = err;
|
|
525
538
|
wake();
|
|
526
539
|
}, () => {
|
|
@@ -629,7 +642,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
629
642
|
}
|
|
630
643
|
|
|
631
644
|
private open(
|
|
632
|
-
|
|
645
|
+
encodedRequest: Uint8Array,
|
|
633
646
|
signal: AbortSignal | undefined,
|
|
634
647
|
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
635
648
|
push: (message: CursorServerMessage) => void,
|
|
@@ -795,7 +808,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
795
808
|
failAndClear(new Error("Cursor request was aborted"));
|
|
796
809
|
}, { once: true });
|
|
797
810
|
|
|
798
|
-
this.stream.write(encodeConnectFrame(
|
|
811
|
+
this.stream.write(encodeConnectFrame(encodedRequest));
|
|
799
812
|
this.heartbeat = setInterval(() => {
|
|
800
813
|
this.stream?.write(encodeClientMessage({
|
|
801
814
|
message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
|
|
@@ -885,10 +898,9 @@ export function partialUsageFromEventState(state: ReturnType<typeof createCursor
|
|
|
885
898
|
// usage math after this turn emits output, but cannot by itself prove that a first-frame failure
|
|
886
899
|
// consumed anything.
|
|
887
900
|
if (!hasCurrentCheckpoint && !hasCurrentOutput) return undefined;
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
: { ...state.usage, estimated: true };
|
|
901
|
+
// Same resolution order as a clean turn, so a failed turn does not silently drop
|
|
902
|
+
// back to inputTokens=0 when only the request-local estimate is available (#373).
|
|
903
|
+
return { ...resolvedTurnUsage(state), estimated: true };
|
|
892
904
|
}
|
|
893
905
|
|
|
894
906
|
/**
|
|
@@ -44,7 +44,7 @@ function codexNativeMutationRefusal(operation: "write" | "delete"): string {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const NATIVE_LOCAL_EXEC_DISABLED =
|
|
47
|
-
"
|
|
47
|
+
"Route filesystem work through the Codex Responses bridge shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with equivalent shell commands (cat, head, ls, rg, grep), or `apply_patch` for file edits. Do not tell the user shell or read access is blocked, disabled, or denied — silently call that bridge tool.";
|
|
48
48
|
|
|
49
49
|
export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
50
50
|
if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
|
|
@@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const NATIVE_FETCH_DISABLED =
|
|
10
|
-
"
|
|
10
|
+
"Route this through the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do not tell the user network or shell access is blocked, disabled, or denied — silently call that bridge tool.";
|
|
11
11
|
|
|
12
12
|
export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
|
|
13
13
|
if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
|
|
@@ -24,7 +24,7 @@ const backgroundShells = new Map<number, { child: ChildProcessWithoutNullStreams
|
|
|
24
24
|
let nextShellId = 1;
|
|
25
25
|
|
|
26
26
|
const NATIVE_SHELL_DISABLED =
|
|
27
|
-
"
|
|
27
|
+
"Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool with the same command.";
|
|
28
28
|
|
|
29
29
|
function rejectedShellResult(command: string, cwd: string, started: number) {
|
|
30
30
|
return create(ShellResultSchema, {
|
|
@@ -2,7 +2,12 @@ import type { OcxUsage } from "../../types";
|
|
|
2
2
|
import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
|
|
3
3
|
import { decodeCursorArgsMap } from "./arg-codec";
|
|
4
4
|
import { normalizeArgKeys } from "./arg-normalize";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
normalizeCursorWireName,
|
|
7
|
+
OCX_RESPONSES_TOOL_PROVIDER,
|
|
8
|
+
resolveShellBridgeAliasKey,
|
|
9
|
+
responsesToolNameFromCursorWire,
|
|
10
|
+
} from "./tool-definitions";
|
|
6
11
|
import type { CursorServerMessage } from "./types";
|
|
7
12
|
|
|
8
13
|
const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
|
|
@@ -125,6 +130,13 @@ export interface CursorProtobufEventState {
|
|
|
125
130
|
* share one field, or Codex double-counts (e.g. 10000 then 10300 surfacing as 20300).
|
|
126
131
|
*/
|
|
127
132
|
contextTokens?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Request-local input estimate derived from the payload actually sent to Cursor.
|
|
135
|
+
* Used only when neither a checkpoint nor a carry-forward is available — a restart
|
|
136
|
+
* clears the tracker, and reporting inputTokens=0 makes Codex see an almost-empty
|
|
137
|
+
* context (#373). Never written back into the tracker: only real checkpoints are.
|
|
138
|
+
*/
|
|
139
|
+
estimatedInputTokens?: number;
|
|
128
140
|
/**
|
|
129
141
|
* Session-level last-known absolute context size for this Cursor conversation. This is a fallback
|
|
130
142
|
* for no-checkpoint client-tool finalize turns only; any checkpoint observed during the current
|
|
@@ -152,6 +164,12 @@ export function createCursorProtobufEventState(options: {
|
|
|
152
164
|
toolSchemas?: Map<string, unknown>;
|
|
153
165
|
cursorToolNameMap?: Map<string, string>;
|
|
154
166
|
contextUsage?: CursorContextUsageControls;
|
|
167
|
+
/**
|
|
168
|
+
* Request-local input estimate derived from the payload actually sent. Used only
|
|
169
|
+
* when neither a checkpoint nor a carry-forward is available; never recorded into
|
|
170
|
+
* the checkpoint tracker (#373).
|
|
171
|
+
*/
|
|
172
|
+
estimatedInputTokens?: number;
|
|
155
173
|
} = {}): CursorProtobufEventState {
|
|
156
174
|
return {
|
|
157
175
|
// Cursor provides no authoritative usage frame; token counts are heuristic estimates from
|
|
@@ -166,6 +184,11 @@ export function createCursorProtobufEventState(options: {
|
|
|
166
184
|
...(options.cursorToolNameMap ? { cursorToolNameMap: options.cursorToolNameMap } : {}),
|
|
167
185
|
...(options.contextUsage?.carryForwardTokens !== undefined ? { contextCarryForwardTokens: options.contextUsage.carryForwardTokens } : {}),
|
|
168
186
|
...(options.contextUsage?.recordContextTokens ? { recordContextTokens: options.contextUsage.recordContextTokens } : {}),
|
|
187
|
+
...(typeof options.estimatedInputTokens === "number"
|
|
188
|
+
&& Number.isFinite(options.estimatedInputTokens)
|
|
189
|
+
&& options.estimatedInputTokens > 0
|
|
190
|
+
? { estimatedInputTokens: options.estimatedInputTokens }
|
|
191
|
+
: {}),
|
|
169
192
|
};
|
|
170
193
|
}
|
|
171
194
|
|
|
@@ -212,12 +235,26 @@ function decodeMcpArgs(args: McpArgs | undefined): string {
|
|
|
212
235
|
return JSON.stringify(decodeCursorArgsMap(args?.args));
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
/** Resolve an advertised client-tool wire name, including shell_command/exec_command aliases (#399). */
|
|
239
|
+
function resolveAdvertisedClientToolName(
|
|
240
|
+
state: CursorProtobufEventState,
|
|
241
|
+
cursorWireName: string,
|
|
242
|
+
): string | undefined {
|
|
243
|
+
const normalized = normalizeCursorWireName(cursorWireName);
|
|
244
|
+
if (!state.clientToolNames) return normalized;
|
|
245
|
+
return resolveShellBridgeAliasKey(normalized, alias => (state.clientToolNames!.has(alias) ? alias : undefined));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function toolSchemaForWireName(state: CursorProtobufEventState, toolName: string | undefined): unknown | undefined {
|
|
249
|
+
if (!toolName || !state.toolSchemas) return undefined;
|
|
250
|
+
return resolveShellBridgeAliasKey(toolName, alias => state.toolSchemas!.get(alias));
|
|
251
|
+
}
|
|
252
|
+
|
|
215
253
|
function decodeMcpArgsNormalized(args: McpArgs | undefined, state: CursorProtobufEventState): string {
|
|
216
254
|
const decoded = decodeCursorArgsMap(args?.args);
|
|
217
255
|
const toolName = mcpWireNameFromArgs(args);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
256
|
+
const schema = toolSchemaForWireName(state, toolName);
|
|
257
|
+
if (schema) return JSON.stringify(normalizeArgKeys(decoded, schema));
|
|
221
258
|
return JSON.stringify(decoded);
|
|
222
259
|
}
|
|
223
260
|
|
|
@@ -237,11 +274,12 @@ function isCompleteJson(text: string): boolean {
|
|
|
237
274
|
|
|
238
275
|
/** Schema-normalize a JSON-text argument blob for a named tool, if a schema is known. */
|
|
239
276
|
function normalizeJsonText(text: string, toolName: string | undefined, state: CursorProtobufEventState): string {
|
|
240
|
-
|
|
277
|
+
const schema = toolSchemaForWireName(state, toolName);
|
|
278
|
+
if (!schema) return text;
|
|
241
279
|
try {
|
|
242
280
|
const parsed = JSON.parse(text);
|
|
243
281
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
244
|
-
return JSON.stringify(normalizeArgKeys(parsed as Record<string, unknown>,
|
|
282
|
+
return JSON.stringify(normalizeArgKeys(parsed as Record<string, unknown>, schema));
|
|
245
283
|
}
|
|
246
284
|
} catch {
|
|
247
285
|
// Not parseable as an object: leave as-is.
|
|
@@ -305,10 +343,14 @@ export function mapSyntheticMcpExecToToolEvents(
|
|
|
305
343
|
function recordToolCall(state: CursorProtobufEventState, callId: string, cursorWireName: string): CursorServerMessage[] {
|
|
306
344
|
if (state.completedToolCalls.has(callId)) return [];
|
|
307
345
|
if (state.openToolCalls.has(callId)) return [];
|
|
308
|
-
|
|
346
|
+
const advertisedName = resolveAdvertisedClientToolName(state, cursorWireName);
|
|
347
|
+
if (state.clientToolNames && !advertisedName) {
|
|
309
348
|
return [{ type: "error", message: `Cursor requested unknown Responses tool: ${cursorWireName}` }];
|
|
310
349
|
}
|
|
311
|
-
|
|
350
|
+
// Prefer the advertised catalog name for Responses mapping so shell_command/exec_command aliases
|
|
351
|
+
// land on the tool Codex actually exposed this turn (#399).
|
|
352
|
+
const mapKey = advertisedName ?? normalizeCursorWireName(cursorWireName);
|
|
353
|
+
state.openToolCalls.set(callId, { name: responsesToolNameFromCursorWire(mapKey, state.cursorToolNameMap), args: "" });
|
|
312
354
|
state.startedClientToolCalls++;
|
|
313
355
|
return [];
|
|
314
356
|
}
|
|
@@ -428,6 +470,27 @@ export function mapCursorProtobufServerMessage(
|
|
|
428
470
|
}
|
|
429
471
|
}
|
|
430
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Resolve the usage to report for a turn, in order of trustworthiness: a checkpoint
|
|
475
|
+
* observed this turn, then the session carry-forward, then the request-local estimate,
|
|
476
|
+
* then the raw per-turn counters. Shared with the partial-usage path in live-transport
|
|
477
|
+
* so a failed turn reports the same input side as a clean one (#373).
|
|
478
|
+
*/
|
|
479
|
+
export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
|
|
480
|
+
const contextTokens = reportableContextTokens(state);
|
|
481
|
+
if (contextTokens !== undefined) return usageFromContextTokens(state, contextTokens);
|
|
482
|
+
const estimate = state.estimatedInputTokens;
|
|
483
|
+
if (estimate !== undefined) {
|
|
484
|
+
return {
|
|
485
|
+
...state.usage,
|
|
486
|
+
inputTokens: estimate,
|
|
487
|
+
totalTokens: estimate + state.usage.outputTokens,
|
|
488
|
+
estimated: true,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { ...state.usage };
|
|
492
|
+
}
|
|
493
|
+
|
|
431
494
|
/**
|
|
432
495
|
* Finalize a Cursor turn. If any client tool call is still open (started but never completed),
|
|
433
496
|
* the stream was truncated and the partial tool call must not reach Codex as a completed call
|
|
@@ -447,9 +510,5 @@ export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServe
|
|
|
447
510
|
// render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
|
|
448
511
|
// first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
|
|
449
512
|
// input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
|
|
450
|
-
|
|
451
|
-
const usage: OcxUsage = contextTokens !== undefined
|
|
452
|
-
? usageFromContextTokens(state, contextTokens)
|
|
453
|
-
: { ...state.usage };
|
|
454
|
-
return [{ type: "done", usage }];
|
|
513
|
+
return [{ type: "done", usage: resolvedTurnUsage(state) }];
|
|
455
514
|
}
|
|
@@ -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 {
|