@bitkyc08/opencodex 2.7.29 → 2.7.30
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-B-UauL1p.css +1 -0
- package/gui/dist/assets/index-avcinRsG.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +7 -2
- package/src/adapters/cursor/request-builder.ts +90 -6
- package/src/adapters/cursor/tool-definitions.ts +23 -6
- package/src/codex/catalog.ts +6 -1
- package/src/lib/errors.ts +4 -0
- package/src/providers/base-url-choices.ts +18 -0
- package/src/providers/registry.ts +95 -2
- package/src/responses/parser.ts +11 -6
- package/src/router.ts +6 -1
- package/src/storage/scanner.ts +18 -6
- package/src/types.ts +2 -0
- package/gui/dist/assets/index-CMKZnkG9.js +0 -40
- package/gui/dist/assets/index-DyBPh28A.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-avcinRsG.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B-UauL1p.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -48,7 +48,10 @@ export function classifyCursorError(message: string): string {
|
|
|
48
48
|
|
|
49
49
|
if (
|
|
50
50
|
lower.includes("resource_exhausted") ||
|
|
51
|
-
lower.includes("resource exhausted")
|
|
51
|
+
lower.includes("resource exhausted")
|
|
52
|
+
) return "Cursor resource limit exceeded";
|
|
53
|
+
|
|
54
|
+
if (
|
|
52
55
|
lower.includes("rate limit") ||
|
|
53
56
|
lower.includes("rate-limit") ||
|
|
54
57
|
lower.includes("too many requests") ||
|
|
@@ -106,7 +109,9 @@ export function classifyCursorError(message: string): string {
|
|
|
106
109
|
* Mirrors `safeKiroErrorMessage` / `safeKiroHttpErrorMessage` in kiro-errors.ts.
|
|
107
110
|
*/
|
|
108
111
|
export function safeCursorErrorMessage(rawMessage: string): string {
|
|
109
|
-
const detail = sanitize(rawMessage).slice(0, 500);
|
|
110
112
|
const prefix = classifyCursorError(rawMessage);
|
|
113
|
+
const detail = sanitize(rawMessage)
|
|
114
|
+
.replace(/resource[_ ]exhausted/gi, "resource limit exceeded")
|
|
115
|
+
.slice(0, 500);
|
|
111
116
|
return detail ? `${prefix}: ${detail}` : prefix;
|
|
112
117
|
}
|
|
@@ -6,10 +6,89 @@ import type {
|
|
|
6
6
|
OcxToolCall,
|
|
7
7
|
OcxToolResultMessage,
|
|
8
8
|
} from "../../types";
|
|
9
|
-
import { namespacedToolName } from "../../types";
|
|
9
|
+
import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
|
|
10
10
|
import type { CursorRequestMessage, CursorRunRequest } from "./types";
|
|
11
11
|
import { cursorCodexToWireModelId } from "./discovery";
|
|
12
12
|
import { cursorEffortSuffix } from "./effort-map";
|
|
13
|
+
import {
|
|
14
|
+
cursorMcpToolEncodedSize,
|
|
15
|
+
cursorMcpToolsEncodedSize,
|
|
16
|
+
cursorToolAllowedByChoice,
|
|
17
|
+
cursorToolWireName,
|
|
18
|
+
cursorToolsForActivePrompt,
|
|
19
|
+
} from "./tool-definitions";
|
|
20
|
+
|
|
21
|
+
/** Probe-verified Cursor Connect boundaries, with byte headroom for the enclosing field. */
|
|
22
|
+
export const CURSOR_TOOL_COUNT_LIMIT = 330;
|
|
23
|
+
export const CURSOR_TOOL_BYTES_LIMIT = 120_000;
|
|
24
|
+
|
|
25
|
+
interface CursorToolBudgetResult {
|
|
26
|
+
tools: OcxTool[];
|
|
27
|
+
omitted: OcxTool[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function explicitlySelectedNames(choice: OcxToolChoice | undefined): Set<string> {
|
|
31
|
+
if (!choice || choice === "auto" || choice === "none" || choice === "required") return new Set();
|
|
32
|
+
return new Set("name" in choice ? [choice.name] : isAllowedToolChoice(choice) ? choice.allowedTools : []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toolPriority(tool: OcxTool, selectedNames: ReadonlySet<string>): number {
|
|
36
|
+
if (toolChoiceAliases(tool).some(name => selectedNames.has(name))) return 0;
|
|
37
|
+
if (tool.loadedFromToolSearch) return 1;
|
|
38
|
+
if (!tool.namespace) return 2;
|
|
39
|
+
return 3;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Select one catalog used by both Cursor protobuf registration and call recognition.
|
|
44
|
+
* Actual McpTools serialization is measured after every candidate so descriptions,
|
|
45
|
+
* names, provider identifiers, and schemas all count toward the byte ceiling.
|
|
46
|
+
*/
|
|
47
|
+
export function applyCursorToolBudget(
|
|
48
|
+
tools: readonly OcxTool[] | undefined,
|
|
49
|
+
toolChoice: OcxToolChoice | undefined,
|
|
50
|
+
): CursorToolBudgetResult {
|
|
51
|
+
const eligible = (tools ?? []).filter(tool => cursorToolAllowedByChoice(tool, toolChoice));
|
|
52
|
+
if (
|
|
53
|
+
eligible.length <= CURSOR_TOOL_COUNT_LIMIT
|
|
54
|
+
&& cursorMcpToolsEncodedSize(eligible, toolChoice) <= CURSOR_TOOL_BYTES_LIMIT
|
|
55
|
+
) return { tools: [...eligible], omitted: [] };
|
|
56
|
+
|
|
57
|
+
const selectedNames = explicitlySelectedNames(toolChoice);
|
|
58
|
+
const candidates = eligible
|
|
59
|
+
.map((tool, index) => ({ tool, index, priority: toolPriority(tool, selectedNames) }))
|
|
60
|
+
.sort((a, b) => a.priority - b.priority || a.index - b.index);
|
|
61
|
+
const kept: OcxTool[] = [];
|
|
62
|
+
const keptSet = new Set<OcxTool>();
|
|
63
|
+
let keptBytes = 0;
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (kept.length >= CURSOR_TOOL_COUNT_LIMIT) continue;
|
|
67
|
+
// Repeated protobuf message fields serialize as concatenated tag/length/value entries,
|
|
68
|
+
// so each one-entry wrapper size is the exact additive contribution to McpTools.
|
|
69
|
+
const candidateBytes = cursorMcpToolEncodedSize(candidate.tool, toolChoice);
|
|
70
|
+
if (keptBytes + candidateBytes > CURSOR_TOOL_BYTES_LIMIT) continue;
|
|
71
|
+
kept.push(candidate.tool);
|
|
72
|
+
keptSet.add(candidate.tool);
|
|
73
|
+
keptBytes += candidateBytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
tools: eligible.filter(tool => keptSet.has(tool)),
|
|
78
|
+
omitted: eligible.filter(tool => !keptSet.has(tool)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[]): string | undefined {
|
|
83
|
+
if (omitted.length === 0) return undefined;
|
|
84
|
+
const recoverable = kept.some(tool => tool.toolSearch || cursorToolWireName(tool) === "tool_search");
|
|
85
|
+
const names = omitted.slice(0, 12).map(cursorToolWireName);
|
|
86
|
+
const remainder = omitted.length - names.length;
|
|
87
|
+
const omittedSummary = `${names.join(", ")}${remainder > 0 ? `, and ${remainder} more` : ""}`;
|
|
88
|
+
return recoverable
|
|
89
|
+
? `[opencodex] Cursor's transport limit allows ${kept.length} of ${kept.length + omitted.length} client tools this turn. Omitted: ${omittedSummary}. Use tool_search for a needed omitted tool; tools returned by tool_search are prioritized on the next turn.`
|
|
90
|
+
: `[opencodex] Cursor's transport limit allows ${kept.length} of ${kept.length + omitted.length} client tools this turn. Omitted and unavailable this turn: ${omittedSummary}.`;
|
|
91
|
+
}
|
|
13
92
|
|
|
14
93
|
/**
|
|
15
94
|
* Resolve a `cursor/<model>` selection + Codex reasoning effort to the actual Cursor model id. Cursor
|
|
@@ -80,6 +159,13 @@ export function generatedCursorConversationId(): string {
|
|
|
80
159
|
}
|
|
81
160
|
|
|
82
161
|
export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest {
|
|
162
|
+
const messages = parsed.context.messages
|
|
163
|
+
.map(requestMessage)
|
|
164
|
+
.filter((message): message is CursorRequestMessage => !!message && message.content.length > 0);
|
|
165
|
+
const activeText = [...messages].reverse().find(message => message.role === "user" || message.role === "developer")?.content ?? "";
|
|
166
|
+
const visibleTools = cursorToolsForActivePrompt(parsed.context.tools, activeText, parsed.options.toolChoice);
|
|
167
|
+
const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
|
|
168
|
+
const limitNote = catalogLimitNote(budget.tools, budget.omitted);
|
|
83
169
|
return {
|
|
84
170
|
modelId: normalizeCursorModelId(parsed.modelId, parsed.options.reasoning),
|
|
85
171
|
// The Cursor conversation id comes ONLY from remembered state (_cursorConversationId). Do NOT fall
|
|
@@ -87,12 +173,10 @@ export function createCursorRequest(parsed: OcxParsedRequest): CursorRunRequest
|
|
|
87
173
|
// different namespace and would start an unrelated Cursor conversation, breaking tool-result
|
|
88
174
|
// continuation. If we have no remembered Cursor conversation, start a fresh one.
|
|
89
175
|
conversationId: parsed._cursorConversationId ?? generatedCursorConversationId(),
|
|
90
|
-
system: [...(parsed.context.systemPrompt ?? [])],
|
|
91
|
-
messages
|
|
92
|
-
.map(requestMessage)
|
|
93
|
-
.filter((message): message is CursorRequestMessage => !!message && message.content.length > 0),
|
|
176
|
+
system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
|
|
177
|
+
messages,
|
|
94
178
|
rawMessages: parsed.context.messages,
|
|
95
|
-
...(
|
|
179
|
+
...(budget.tools.length ? { tools: budget.tools } : {}),
|
|
96
180
|
...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
|
|
97
181
|
...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
|
|
98
182
|
};
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { namespacedToolName } from "../../types";
|
|
5
|
-
import { McpToolDefinitionSchema, type McpToolDefinition } from "./gen/agent_pb";
|
|
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";
|
|
@@ -50,7 +50,7 @@ export function cursorRequestAdvertisesApplyPatch(
|
|
|
50
50
|
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
51
51
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
52
52
|
): boolean {
|
|
53
|
-
return tools?.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true &&
|
|
53
|
+
return tools?.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice)) ?? false;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
|
|
@@ -176,7 +176,7 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
|
|
|
176
176
|
): readonly T[] | undefined {
|
|
177
177
|
if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools;
|
|
178
178
|
const execTools = tools?.filter(isBareCodexExecCommandTool);
|
|
179
|
-
if (execTools?.length && !execTools.some(tool =>
|
|
179
|
+
if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice))) return tools;
|
|
180
180
|
return execTools && execTools.length > 0 ? execTools : tools;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -199,7 +199,7 @@ export function appendCursorShellAliasHint(
|
|
|
199
199
|
return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${CURSOR_SHELL_ALIAS_USER_HINT}`;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
function
|
|
202
|
+
export function cursorToolAllowedByChoice(tool: Pick<OcxTool, "namespace" | "name">, toolChoice: OcxRequestOptions["toolChoice"] | undefined): boolean {
|
|
203
203
|
if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true;
|
|
204
204
|
if (toolChoice === "none") return false;
|
|
205
205
|
if ("allowedTools" in toolChoice) {
|
|
@@ -232,7 +232,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
232
232
|
if (!tools?.length) return undefined;
|
|
233
233
|
const wireNames = [...new Set(
|
|
234
234
|
tools
|
|
235
|
-
.filter(tool =>
|
|
235
|
+
.filter(tool => cursorToolAllowedByChoice(tool, toolChoice))
|
|
236
236
|
.map(tool => cursorToolWireName(tool)),
|
|
237
237
|
)];
|
|
238
238
|
if (wireNames.length === 0) return undefined;
|
|
@@ -291,7 +291,7 @@ export function buildCursorToolDefinitions(
|
|
|
291
291
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
292
292
|
): McpToolDefinition[] {
|
|
293
293
|
if (!tools?.length) return [];
|
|
294
|
-
return tools.filter(tool =>
|
|
294
|
+
return tools.filter(tool => cursorToolAllowedByChoice(tool, toolChoice)).map(tool => {
|
|
295
295
|
const wireName = cursorToolWireName(tool);
|
|
296
296
|
return create(McpToolDefinitionSchema, {
|
|
297
297
|
name: wireName,
|
|
@@ -302,3 +302,20 @@ export function buildCursorToolDefinitions(
|
|
|
302
302
|
});
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
|
+
|
|
306
|
+
/** Exact byte size of the protobuf field value Cursor receives for client tool registration. */
|
|
307
|
+
export function cursorMcpToolsEncodedSize(
|
|
308
|
+
tools: readonly OcxTool[] | undefined,
|
|
309
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
310
|
+
): number {
|
|
311
|
+
const definitions = buildCursorToolDefinitions(tools, toolChoice);
|
|
312
|
+
return toBinary(McpToolsSchema, create(McpToolsSchema, { mcpTools: definitions })).byteLength;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Exact additive contribution of one repeated McpToolDefinition entry. */
|
|
316
|
+
export function cursorMcpToolEncodedSize(
|
|
317
|
+
tool: OcxTool,
|
|
318
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
319
|
+
): number {
|
|
320
|
+
return cursorMcpToolsEncodedSize([tool], toolChoice);
|
|
321
|
+
}
|
package/src/codex/catalog.ts
CHANGED
|
@@ -1266,7 +1266,12 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1266
1266
|
const staleCursor = getStaleCached(name);
|
|
1267
1267
|
return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured;
|
|
1268
1268
|
}
|
|
1269
|
-
if (prov.authMode === "oauth" && !apiKey)
|
|
1269
|
+
if (prov.authMode === "oauth" && !apiKey) {
|
|
1270
|
+
// No usable token (logged out, or account marked needsReauth). Still surface the
|
|
1271
|
+
// configured static catalog so the GUI Models tab / rail counts are not empty —
|
|
1272
|
+
// matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
|
|
1273
|
+
return configured;
|
|
1274
|
+
}
|
|
1270
1275
|
if (prov.liveModels === false) {
|
|
1271
1276
|
return configured;
|
|
1272
1277
|
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -99,6 +99,9 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
99
99
|
) {
|
|
100
100
|
return { message, type: "invalid_request_error", code: "context_length_exceeded" };
|
|
101
101
|
}
|
|
102
|
+
if (text.includes("cursor resource limit exceeded")) {
|
|
103
|
+
return { message, type: "invalid_request_error", code: "tool_catalog_too_large" };
|
|
104
|
+
}
|
|
102
105
|
if (
|
|
103
106
|
text.includes("insufficient_quota") ||
|
|
104
107
|
text.includes("exceeded your current quota") ||
|
|
@@ -199,6 +202,7 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
199
202
|
const lower = message.toLowerCase();
|
|
200
203
|
// Client aborts (e.g. mid web-search loop) must not look like upstream 502s in /api/logs.
|
|
201
204
|
if (isClientClosedMessage(lower)) return 499;
|
|
205
|
+
if (lower.includes("cursor resource limit exceeded")) return 400;
|
|
202
206
|
if (
|
|
203
207
|
lower.includes("resource_exhausted") ||
|
|
204
208
|
lower.includes("resource exhausted") ||
|
|
@@ -21,6 +21,24 @@ export const QWEN_CLOUD_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
|
21
21
|
{ id: "custom", label: "Custom" },
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Alibaba Token Plan International (ap-southeast-1) endpoint presets.
|
|
26
|
+
* Same product as the Beijing Token Plan but for international accounts.
|
|
27
|
+
* Note: ALIBABA_INTL_TOKEN_PLAN_BASE_URL intentionally duplicates
|
|
28
|
+
* QWEN_CLOUD_TOKEN_PLAN_BASE_URL — same host, different product branding
|
|
29
|
+
* and model lineup. Kept as a separate constant for clarity.
|
|
30
|
+
*/
|
|
31
|
+
export const ALIBABA_INTL_TOKEN_PLAN_BASE_URL =
|
|
32
|
+
"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
|
|
33
|
+
export const ALIBABA_INTL_PAYG_BASE_URL =
|
|
34
|
+
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
|
35
|
+
|
|
36
|
+
export const ALIBABA_INTL_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
37
|
+
{ id: "token-plan", label: "Token plan", baseUrl: ALIBABA_INTL_TOKEN_PLAN_BASE_URL },
|
|
38
|
+
{ id: "payg", label: "Pay as you go", baseUrl: ALIBABA_INTL_PAYG_BASE_URL },
|
|
39
|
+
{ id: "custom", label: "Custom" },
|
|
40
|
+
];
|
|
41
|
+
|
|
24
42
|
/** Match a saved baseUrl to a known choice id (`custom` when it does not match). */
|
|
25
43
|
export function matchBaseUrlChoice(
|
|
26
44
|
choices: readonly ProviderBaseUrlChoice[],
|
|
@@ -2,7 +2,10 @@ import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
3
|
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
|
|
4
4
|
import type { ProviderBaseUrlChoice } from "./base-url-choices";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
|
|
7
|
+
ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL,
|
|
8
|
+
} from "./base-url-choices";
|
|
6
9
|
import {
|
|
7
10
|
CURSOR_STATIC_MODELS,
|
|
8
11
|
cursorModelContextWindows,
|
|
@@ -199,6 +202,33 @@ const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
|
199
202
|
"glm-5.2": ["text"],
|
|
200
203
|
"deepseek-v4-pro": ["text"],
|
|
201
204
|
};
|
|
205
|
+
|
|
206
|
+
// 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore).
|
|
207
|
+
// Multi-vendor lineup distinct from Beijing — includes DeepSeek V4 flash, Kimi K2.7, MiniMax.
|
|
208
|
+
// Evidence: https://www.alibabacloud.com/help/en/model-studio/token-plan-overview
|
|
209
|
+
const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
|
|
210
|
+
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
211
|
+
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2",
|
|
212
|
+
"kimi-k2.7-code",
|
|
213
|
+
"glm-5.2",
|
|
214
|
+
"MiniMax-M2.5",
|
|
215
|
+
];
|
|
216
|
+
const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
|
|
217
|
+
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
218
|
+
];
|
|
219
|
+
const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
220
|
+
"qwen3.7-max": ["text"],
|
|
221
|
+
"qwen3.7-plus": ["text", "image"],
|
|
222
|
+
"qwen3.6-plus": ["text", "image"],
|
|
223
|
+
"qwen3.6-flash": ["text", "image"],
|
|
224
|
+
"deepseek-v4-pro": ["text"],
|
|
225
|
+
"deepseek-v4-flash": ["text"],
|
|
226
|
+
"deepseek-v3.2": ["text"],
|
|
227
|
+
"kimi-k2.7-code": ["text"],
|
|
228
|
+
"glm-5.2": ["text"],
|
|
229
|
+
"MiniMax-M2.5": ["text"],
|
|
230
|
+
};
|
|
231
|
+
|
|
202
232
|
// 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both
|
|
203
233
|
// entitlement tiers. Bare `k3` advertises the Moderato 256K ceiling; the local `[1m]`
|
|
204
234
|
// alias advertises Allegretto's 1M ceiling and is stripped before the upstream request.
|
|
@@ -564,6 +594,39 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
564
594
|
preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
|
|
565
595
|
},
|
|
566
596
|
{ id: "openrouter", label: "OpenRouter", adapter: "openai-chat", baseUrl: "https://openrouter.ai/api/v1", authKind: "key", featured: true, dashboardUrl: "https://openrouter.ai/keys", jawcodeBundle: "openrouter", models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS], modelContextWindows: { "anthropic/claude-sonnet-5": 1_000_000, ...OPENROUTER_GPT56_CONTEXT_WINDOWS } },
|
|
597
|
+
{
|
|
598
|
+
// OrcaRouter: OpenAI-compatible adaptive router (api.orcarouter.ai). Model ids are
|
|
599
|
+
// vendor-namespaced (`<vendor>/<model>`) and pass through to the upstream as-is.
|
|
600
|
+
// The default pins a tool-capable model; the adaptive `orcarouter/auto` router is also
|
|
601
|
+
// selectable. Live-verified 2026-07-20: /v1/chat/completions accepts the `tools` field
|
|
602
|
+
// and routes to a function-calling-capable upstream.
|
|
603
|
+
id: "orcarouter", label: "OrcaRouter", adapter: "openai-chat", baseUrl: "https://api.orcarouter.ai/v1",
|
|
604
|
+
authKind: "key", dashboardUrl: "https://www.orcarouter.ai/console",
|
|
605
|
+
defaultModel: "openai/gpt-5.5",
|
|
606
|
+
models: [
|
|
607
|
+
"openai/gpt-5.5",
|
|
608
|
+
"anthropic/claude-opus-4.8",
|
|
609
|
+
"google/gemini-3.5-flash",
|
|
610
|
+
"deepseek/deepseek-v4-pro",
|
|
611
|
+
"orcarouter/auto",
|
|
612
|
+
],
|
|
613
|
+
// Text-only models → the vision sidecar describes images instead.
|
|
614
|
+
noVisionModels: ["deepseek/deepseek-v4-pro"],
|
|
615
|
+
// Reasoning/temperature behavior verified live 2026-07-20 against api.orcarouter.ai:
|
|
616
|
+
// - openai/gpt-5.5 accepts reasoning_effort none|low|medium|high|xhigh but rejects `max` (400),
|
|
617
|
+
// so advertise up to xhigh and let mapReasoningEffort clamp a `max`/`ultra` request to xhigh.
|
|
618
|
+
// - deepseek/deepseek-v4-pro mirrors the direct-DeepSeek wiring (thinking-effort map +
|
|
619
|
+
// reasoning_content history replay) so the namespaced selection behaves identically.
|
|
620
|
+
// - temperature is accepted by every seeded model (gpt-5.5, claude-opus-4.8, deepseek-v4-pro all
|
|
621
|
+
// returned 200), so no noTemperatureModels entry is warranted here.
|
|
622
|
+
modelReasoningEfforts: {
|
|
623
|
+
"openai/gpt-5.5": ["low", "medium", "high", "xhigh"],
|
|
624
|
+
"deepseek/deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
|
|
625
|
+
},
|
|
626
|
+
modelReasoningEffortMap: { "deepseek/deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP },
|
|
627
|
+
preserveReasoningContentModels: ["deepseek/deepseek-v4-pro"],
|
|
628
|
+
note: "OpenAI-compatible adaptive router. Default is a tool-capable model; orcarouter/auto (adaptive routing) is also selectable. Full catalog: https://www.orcarouter.ai/models",
|
|
629
|
+
},
|
|
567
630
|
{ id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
|
|
568
631
|
// 2026-07-10 Gemini API refresh: Tier-2 ai.google.dev evidence recorded in
|
|
569
632
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
@@ -691,7 +754,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
691
754
|
id: "alibaba-token-plan",
|
|
692
755
|
label: "Alibaba Token Plan (Beijing)",
|
|
693
756
|
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
|
|
694
|
-
allowBaseUrlOverride: true,
|
|
695
757
|
adapter: "openai-chat",
|
|
696
758
|
authKind: "key",
|
|
697
759
|
dashboardUrl: "https://bailian.console.aliyun.com/cn-beijing?tab=plan",
|
|
@@ -709,6 +771,37 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
709
771
|
thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS,
|
|
710
772
|
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max-preview"],
|
|
711
773
|
},
|
|
774
|
+
{
|
|
775
|
+
id: "alibaba-token-plan-intl",
|
|
776
|
+
label: "Alibaba Token Plan (International)",
|
|
777
|
+
baseUrl: ALIBABA_INTL_TOKEN_PLAN_BASE_URL,
|
|
778
|
+
adapter: "openai-chat",
|
|
779
|
+
authKind: "key",
|
|
780
|
+
allowBaseUrlOverride: true,
|
|
781
|
+
baseUrlChoices: ALIBABA_INTL_BASE_URL_CHOICES,
|
|
782
|
+
dashboardUrl: "https://modelstudio.console.alibabacloud.com/?tab=api#/api",
|
|
783
|
+
defaultModel: "qwen3.7-max",
|
|
784
|
+
models: ALIBABA_INTL_TOKEN_PLAN_MODELS,
|
|
785
|
+
liveModels: false,
|
|
786
|
+
note: "Token Plan Team Edition · Singapore (ap-southeast-1)",
|
|
787
|
+
metadataModelIdNormalize: "case-insensitive",
|
|
788
|
+
modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES,
|
|
789
|
+
modelContextWindows: { "deepseek-v4-pro": 1_000_000, "deepseek-v4-flash": 1_000_000, "glm-5.2": 1_000_000 },
|
|
790
|
+
modelReasoningEfforts: {
|
|
791
|
+
...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
792
|
+
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
793
|
+
"deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
|
|
794
|
+
"deepseek-v4-flash": DEEPSEEK_THINKING_EFFORTS,
|
|
795
|
+
},
|
|
796
|
+
modelReasoningEffortMap: {
|
|
797
|
+
"deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP,
|
|
798
|
+
"deepseek-v4-flash": DEEPSEEK_THINKING_REASONING_MAP,
|
|
799
|
+
},
|
|
800
|
+
thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS,
|
|
801
|
+
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.7-max"],
|
|
802
|
+
noVisionModels: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2", "kimi-k2.7-code", "glm-5.2", "MiniMax-M2.5", "qwen3.7-max"],
|
|
803
|
+
noReasoningModels: ["kimi-k2.7-code", "deepseek-v3.2", "MiniMax-M2.5"],
|
|
804
|
+
},
|
|
712
805
|
// NEEDS_HUMAN 2026-07-10: kept for config compatibility, but this is a dashboard URL,
|
|
713
806
|
// no /models endpoint is documented, and tools are silently ignored upstream per docs.parallel.ai.
|
|
714
807
|
// Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
|
package/src/responses/parser.ts
CHANGED
|
@@ -525,13 +525,18 @@ export function parseRequest(body: unknown): OcxParsedRequest {
|
|
|
525
525
|
|
|
526
526
|
const declaredTools = buildTools(data.tools as unknown[] | undefined) ?? [];
|
|
527
527
|
const loadedTools = buildTools(loadedToolSpecs) ?? [];
|
|
528
|
+
const loadedToolNames = new Set(loadedTools.map(t => namespacedToolName(t.namespace, t.name)));
|
|
528
529
|
const seenTools = new Set<string>();
|
|
529
|
-
const mergedTools = [...declaredTools, ...loadedTools]
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
const mergedTools = [...declaredTools, ...loadedTools]
|
|
531
|
+
.filter(t => {
|
|
532
|
+
const k = namespacedToolName(t.namespace, t.name);
|
|
533
|
+
if (seenTools.has(k)) return false;
|
|
534
|
+
seenTools.add(k);
|
|
535
|
+
return true;
|
|
536
|
+
})
|
|
537
|
+
.map(t => loadedToolNames.has(namespacedToolName(t.namespace, t.name))
|
|
538
|
+
? { ...t, loadedFromToolSearch: true }
|
|
539
|
+
: t);
|
|
535
540
|
const context: OcxContext = {
|
|
536
541
|
...(systemPrompt.length > 0 ? { systemPrompt } : {}),
|
|
537
542
|
messages,
|
package/src/router.ts
CHANGED
|
@@ -253,9 +253,14 @@ export function routeModel(config: OcxConfig, modelId: string): RouteResult {
|
|
|
253
253
|
if (hasOwnProvider(config.providers, provName)) {
|
|
254
254
|
const prov = config.providers[provName];
|
|
255
255
|
if (prov.disabled === true) throw new Error(`Provider is disabled: ${provName}`);
|
|
256
|
+
const known = knownModelIdsForProvider(provName, prov);
|
|
257
|
+
// Self-namespaced native id — the vendor segment equals the provider id, so the FULL ref is
|
|
258
|
+
// itself a known model (e.g. orcarouter/auto). Route it whole instead of stripping to the
|
|
259
|
+
// remainder, which would send a bare `auto` the upstream cannot resolve.
|
|
260
|
+
if (known.includes(modelId)) return routeResult(provName, prov, modelId);
|
|
256
261
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
257
262
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
258
|
-
return routeResult(provName, prov, decodeRoutedModelId(modelId.slice(slash + 1),
|
|
263
|
+
return routeResult(provName, prov, decodeRoutedModelId(modelId.slice(slash + 1), known));
|
|
259
264
|
}
|
|
260
265
|
}
|
|
261
266
|
|
package/src/storage/scanner.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { Database, constants } from "bun:sqlite";
|
|
4
5
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
5
6
|
|
|
7
|
+
// SQLITE_OPEN_READONLY alone is not filesystem-read-only for a WAL-mode DB: Bun's
|
|
8
|
+
// `{ readonly: true }` can still materialize *.sqlite-wal/-shm sidecars the first time a
|
|
9
|
+
// checkpointed WAL database (no live sidecars yet) is opened and queried. `immutable=1`
|
|
10
|
+
// (via a file: URI, which requires the SQLITE_OPEN_URI flag) tells SQLite the file will
|
|
11
|
+
// never change for this connection's lifetime, so it skips WAL/shm entirely — the
|
|
12
|
+
// tradeoff is reading the last-checkpointed snapshot instead of blocking on a live writer,
|
|
13
|
+
// which is the right tradeoff for a passive diagnostics scan that must never write.
|
|
14
|
+
const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
* Read-only CODEX_HOME storage scanner — Phase 1 of the Storage page epic
|
|
8
18
|
* (devlog/_plan/500_storage-page-session-cleanup). Pure measurement: sizes via
|
|
@@ -121,15 +131,17 @@ function buildBucket(key: StorageBucketKey, files: FileEntry[]): StorageBucket {
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
/**
|
|
124
|
-
* Row count via
|
|
125
|
-
*
|
|
126
|
-
*
|
|
134
|
+
* Row count via an immutable readonly open — guarantees zero writes under CODEX_HOME even
|
|
135
|
+
* for a checkpointed WAL-mode DB with no sidecars yet. Any error (corruption, a file that
|
|
136
|
+
* vanished mid-scan, a future schema change) degrades to null — "unknown", never a crash.
|
|
127
137
|
*/
|
|
128
138
|
function countRowsReadonly(dbPath: string, table: string): number | null {
|
|
129
139
|
try {
|
|
130
|
-
|
|
140
|
+
// pathToFileURL percent-encodes reserved characters (space, #, ?, %) that a naive
|
|
141
|
+
// `file:${dbPath}` concatenation would misparse as a URI fragment/query/escape.
|
|
142
|
+
const uri = `${pathToFileURL(dbPath).href}?immutable=1`;
|
|
143
|
+
const db = new Database(uri, IMMUTABLE_READONLY_FLAGS);
|
|
131
144
|
try {
|
|
132
|
-
db.exec("PRAGMA busy_timeout = 100");
|
|
133
145
|
const row = db.query<{ n: number }, []>(`SELECT count(*) AS n FROM "${table}"`).get();
|
|
134
146
|
return row?.n ?? null;
|
|
135
147
|
} finally {
|
package/src/types.ts
CHANGED
|
@@ -122,6 +122,8 @@ export interface OcxTool {
|
|
|
122
122
|
freeform?: boolean;
|
|
123
123
|
/** Client-executed tool discovery (tool_search): the model's call must be relayed as a tool_search_call. */
|
|
124
124
|
toolSearch?: boolean;
|
|
125
|
+
/** Tool definition restored from a prior tool_search output; transports may prioritize it when catalogs are bounded. */
|
|
126
|
+
loadedFromToolSearch?: boolean;
|
|
125
127
|
/** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
|
|
126
128
|
webSearch?: boolean;
|
|
127
129
|
}
|