@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.
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-CMKZnkG9.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-DyBPh28A.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.7.29",
3
+ "version": "2.7.30",
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",
@@ -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: parsed.context.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
- ...(parsed.context.tools?.length ? { tools: parsed.context.tools } : {}),
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 && toolChoiceAllows(tool, toolChoice)) ?? false;
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 => toolChoiceAllows(tool, toolChoice))) return tools;
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 toolChoiceAllows(tool: Pick<OcxTool, "namespace" | "name">, toolChoice: OcxRequestOptions["toolChoice"] | undefined): boolean {
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 => toolChoiceAllows(tool, toolChoice))
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 => toolChoiceAllows(tool, toolChoice)).map(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
+ }
@@ -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) return []; // not logged in → skip
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 { QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL } from "./base-url-choices";
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.
@@ -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].filter(t => {
530
- const k = namespacedToolName(t.namespace, t.name);
531
- if (seenTools.has(k)) return false;
532
- seenTools.add(k);
533
- return true;
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), knownModelIdsForProvider(provName, prov)));
263
+ return routeResult(provName, prov, decodeRoutedModelId(modelId.slice(slash + 1), known));
259
264
  }
260
265
  }
261
266
 
@@ -1,8 +1,18 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { Database } from "bun:sqlite";
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 a lock-safe readonly open (the same secondary-reader contract as
125
- * codex/history-provider.ts): short busy timeout, and any lock/corruption/schema
126
- * error degrades to null — "unknown", never a crash and never a write.
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
- const db = new Database(dbPath, { readonly: true });
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
  }