@bitkyc08/opencodex 2.6.32 → 2.7.1-preview.20260710

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.
Files changed (61) hide show
  1. package/README.ko.md +9 -5
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +8 -4
  4. package/gui/dist/assets/index-BUAMcKFd.css +1 -0
  5. package/gui/dist/assets/index-KorpEKW8.js +34 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic.ts +62 -1
  9. package/src/adapters/cursor/cursor-errors.ts +28 -1
  10. package/src/adapters/cursor/discovery.ts +60 -10
  11. package/src/adapters/cursor/effort-map.ts +38 -7
  12. package/src/adapters/cursor/live-models.ts +3 -0
  13. package/src/adapters/cursor/live-transport.ts +136 -7
  14. package/src/adapters/cursor/protobuf-request.ts +24 -1
  15. package/src/adapters/cursor/request-builder.ts +6 -5
  16. package/src/adapters/cursor/transport-retry.ts +22 -3
  17. package/src/adapters/cursor.ts +2 -1
  18. package/src/adapters/openai-chat.ts +75 -26
  19. package/src/bridge.ts +42 -3
  20. package/src/cli/debug.ts +203 -0
  21. package/src/cli/doctor.ts +11 -0
  22. package/src/cli/help.ts +11 -0
  23. package/src/cli/index.ts +10 -0
  24. package/src/cli/v2.ts +131 -0
  25. package/src/codex/auth-api.ts +7 -3
  26. package/src/codex/catalog.ts +334 -31
  27. package/src/codex/data/upstream-models.json +830 -0
  28. package/src/codex/features.ts +178 -0
  29. package/src/codex/project-config-warnings.ts +388 -0
  30. package/src/codex/sync.ts +8 -0
  31. package/src/codex/warmup.ts +62 -7
  32. package/src/config.ts +7 -5
  33. package/src/lib/debug-log-buffer.ts +42 -0
  34. package/src/lib/debug-settings.ts +84 -0
  35. package/src/lib/debug.ts +18 -9
  36. package/src/lib/errors.ts +104 -1
  37. package/src/oauth/cursor.ts +35 -12
  38. package/src/oauth/store.ts +4 -3
  39. package/src/providers/derive.ts +8 -0
  40. package/src/providers/registry.ts +56 -21
  41. package/src/reasoning-effort.ts +37 -9
  42. package/src/responses/parser.ts +7 -2
  43. package/src/router.ts +5 -0
  44. package/src/server/adapter-resolve.ts +1 -1
  45. package/src/server/index.ts +27 -3
  46. package/src/server/management-api.ts +189 -7
  47. package/src/server/relay.ts +2 -2
  48. package/src/server/request-decompress.ts +8 -2
  49. package/src/server/request-log.ts +78 -0
  50. package/src/server/responses.ts +241 -9
  51. package/src/types.ts +34 -1
  52. package/src/usage/debug.ts +32 -5
  53. package/src/usage/summary.ts +6 -6
  54. package/src/vision/describe.ts +4 -0
  55. package/src/web-search/executor.ts +4 -0
  56. package/src/web-search/format-result.ts +11 -3
  57. package/src/web-search/index.ts +31 -2
  58. package/src/web-search/loop.ts +112 -61
  59. package/src/web-search/parse.ts +4 -1
  60. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  61. package/gui/dist/assets/index-D_JZzI0r.js +0 -15
@@ -37,9 +37,12 @@ export interface ProviderRegistryEntry {
37
37
  noTemperatureModels?: string[];
38
38
  noTopPModels?: string[];
39
39
  noPenaltyModels?: string[];
40
+ /** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */
41
+ parallelToolCalls?: boolean;
40
42
  autoToolChoiceOnlyModels?: string[];
41
43
  preserveReasoningContentModels?: string[];
42
44
  thinkingToggleModels?: string[];
45
+ thinkingBudgetModels?: string[];
43
46
  escapeBuiltinToolNames?: boolean;
44
47
  oauthId?: string;
45
48
  jawcodeBundle?: string;
@@ -56,14 +59,16 @@ export type ProviderConfigSeed = Pick<
56
59
  | "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
57
60
  | "reasoningEfforts" | "modelReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap"
58
61
  | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
59
- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "escapeBuiltinToolNames"
62
+ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames"
60
63
  | "googleMode" | "project" | "location"
61
64
  >;
62
65
 
63
66
  // Shared between the OAuth (Claude account) and API-key Anthropic entries so both expose the
64
67
  // same static model seed.
65
- const ANTHROPIC_MODELS = ["claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
66
- const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000 };
68
+ // 260709 refresh: claude-fable-5 added (official models overview); evidence in
69
+ // devlog/model_update/260709_model_refresh/002_cursor_registry_drift.md.
70
+ const ANTHROPIC_MODELS = ["claude-fable-5", "claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
71
+ const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000 };
67
72
 
68
73
  const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
69
74
  const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
@@ -83,11 +88,11 @@ const OPENROUTER_GPT56_CONTEXT_WINDOWS = {
83
88
 
84
89
  /**
85
90
  * Vendor thinking-toggle models (MiMo v2.x, GLM 5/5.1 on Zen Go): the wire knob is
86
- * `thinking: {type: enabled|disabled}` — a binary. Advertise a two-step Codex ladder
87
- * (low = thinking off, high = thinking on) and map efforts onto the toggle. Zen Go
91
+ * `thinking: {type: enabled|disabled}` — a binary. Advertise the full Codex picker ladder
92
+ * and map efforts onto the toggle. Zen Go
88
93
  * pass-through probed live 2026-07-07 (glm-5.2 toggle verified; mimo/minimax accept shape).
89
94
  */
90
- const THINKING_TOGGLE_EFFORTS = ["low", "high"];
95
+ const THINKING_TOGGLE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
91
96
  const THINKING_TOGGLE_MAP: Record<string, string> = {
92
97
  none: "disabled",
93
98
  minimal: "disabled",
@@ -100,8 +105,16 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
100
105
  const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
101
106
  "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-omni", "mimo-v2-pro", "glm-5", "glm-5.1",
102
107
  ];
108
+ const THINKING_BUDGET_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
109
+ const THINKING_BUDGET_MODELS = [
110
+ "qwen3.5-397b", "qwen3.6-35b",
111
+ "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus",
112
+ ];
113
+ const OPENCODE_GO_THINKING_BUDGET_MODELS = ["qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"];
103
114
  const DEEPSEEK_THINKING_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"];
104
- const DEEPSEEK_THINKING_EFFORTS = ["high", "xhigh"];
115
+ // "max" is advertised too: the wire map routes xhigh->max and max->max, so the picker
116
+ // should surface the max tier instead of hiding it behind xhigh.
117
+ const DEEPSEEK_THINKING_EFFORTS = ["high", "xhigh", "max"];
105
118
  const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
106
119
  low: "high",
107
120
  medium: "high",
@@ -109,7 +122,7 @@ const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
109
122
  xhigh: "max",
110
123
  max: "max",
111
124
  };
112
- const KIMI_THINKING_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"];
125
+ const KIMI_THINKING_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
113
126
  const KIMI_LOCKED_PARAMETER_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
114
127
  const NEURALWATT_REASONING_HISTORY_MODELS = [
115
128
  "glm-5.2",
@@ -119,7 +132,6 @@ const NEURALWATT_REASONING_HISTORY_MODELS = [
119
132
  const UMANS_MODELS = [
120
133
  "umans-coder",
121
134
  "umans-kimi-k2.7",
122
- "umans-kimi-k2.6",
123
135
  "umans-flash",
124
136
  "umans-glm-5.2",
125
137
  "umans-glm-5.1",
@@ -131,7 +143,6 @@ const UMANS_TEXT_ONLY_MODELS = ["umans-glm-5.2", "umans-glm-5.1"];
131
143
  const UMANS_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
132
144
  "umans-coder": 262_144,
133
145
  "umans-kimi-k2.7": 262_144,
134
- "umans-kimi-k2.6": 262_144,
135
146
  "umans-flash": 262_144,
136
147
  "umans-glm-5.2": 405_504,
137
148
  "umans-glm-5.1": 202_752,
@@ -159,7 +170,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
159
170
  authKind: "oauth",
160
171
  featured: false,
161
172
  dashboardPreset: true,
162
- note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution stays disabled unless provider.unsafeAllowNativeLocalExec is explicitly set for a trusted local experiment.",
173
+ note: "Experimental Cursor bridge. Live transport and live model discovery are enabled after a standalone PKCE browser login via 'ocx login cursor'; native read/write/delete/shell/fetch execution stays disabled unless you set \"unsafeAllowNativeLocalExec\": true on providers.cursor in ~/.opencodex/config.json (dashboard: Providers → Cursor → Edit JSON) for a trusted local experiment.",
163
174
  models: cursorModelIds(CURSOR_STATIC_MODELS),
164
175
  liveModels: true,
165
176
  defaultModel: "auto",
@@ -182,9 +193,29 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
182
193
  oauthId: "xai",
183
194
  jawcodeBundle: "xai",
184
195
  note: "Log in with your Grok account",
185
- models: ["grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
186
- defaultModel: "grok-4.3",
187
- noReasoningModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
196
+ // Parallel tool calls: officially supported and default-on per docs.x.ai function-calling
197
+ // (verified 260709, devlog/_plan/260709_parallel_tool_calls). Streamed calls arrive whole
198
+ // per chunk, so the buffered parser assembles them losslessly.
199
+ parallelToolCalls: true,
200
+ // Live /v1/models discovery is the authoritative lineup (verified 260709: returns grok-4.5);
201
+ // the static list below is the logged-out fallback seed.
202
+ liveModels: true,
203
+ // 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
204
+ // grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
205
+ // devlog/model_update/260709_model_refresh/001_xai_lineup.md.
206
+ models: ["grok-4.5", "grok-4.3", "grok-4.20-multi-agent-0309", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
207
+ defaultModel: "grok-4.5",
208
+ noReasoningModels: ["grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
209
+ // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
210
+ modelReasoningEfforts: { "grok-4.5": ["low", "medium", "high"] },
211
+ modelContextWindows: {
212
+ "grok-4.5": 500_000,
213
+ "grok-4.3": 1_000_000,
214
+ "grok-4.20-multi-agent-0309": 1_000_000,
215
+ "grok-4.20-0309-reasoning": 1_000_000,
216
+ "grok-4.20-0309-non-reasoning": 1_000_000,
217
+ "grok-build-0.1": 256_000,
218
+ },
188
219
  noVisionModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
189
220
  },
190
221
  {
@@ -199,7 +230,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
199
230
  note: "Log in with your Claude account",
200
231
  models: [...ANTHROPIC_MODELS],
201
232
  modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
202
- defaultModel: "claude-sonnet-4-6",
233
+ defaultModel: "claude-sonnet-5",
203
234
  },
204
235
  {
205
236
  id: "anthropic-apikey",
@@ -215,7 +246,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
215
246
  models: [...ANTHROPIC_MODELS],
216
247
  liveModels: true,
217
248
  modelContextWindows: { ...ANTHROPIC_MODEL_CONTEXT_WINDOWS },
218
- defaultModel: "claude-sonnet-4-6",
249
+ defaultModel: "claude-sonnet-5",
219
250
  },
220
251
  {
221
252
  id: "kimi",
@@ -252,7 +283,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
252
283
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
253
284
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
254
285
  },
255
- { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5", models: ["gpt-5.5", ...OPENAI_GPT56_MODELS], modelContextWindows: OPENAI_GPT56_CONTEXT_WINDOWS },
286
+ { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5", models: ["gpt-5.5", ...OPENAI_GPT56_MODELS], liveModels: true, modelContextWindows: OPENAI_GPT56_CONTEXT_WINDOWS },
256
287
  {
257
288
  id: "umans",
258
289
  label: "Umans AI Coding Plan",
@@ -269,7 +300,6 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
269
300
  modelReasoningEfforts: {
270
301
  "umans-coder": UMANS_REASONING_EFFORTS,
271
302
  "umans-kimi-k2.7": UMANS_REASONING_EFFORTS,
272
- "umans-kimi-k2.6": UMANS_REASONING_EFFORTS,
273
303
  "umans-flash": UMANS_REASONING_EFFORTS,
274
304
  "umans-glm-5.2": UMANS_GLM_REASONING_EFFORTS,
275
305
  "umans-glm-5.1": UMANS_GLM_REASONING_EFFORTS,
@@ -287,6 +317,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
287
317
  "kimi-k2.7-code": [],
288
318
  "kimi-k2.7-code-highspeed": [],
289
319
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_EFFORTS])),
320
+ ...Object.fromEntries(OPENCODE_GO_THINKING_BUDGET_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
290
321
  },
291
322
  // glm-5.2 uses identity labels now that `max` is a native Codex level (no alias map);
292
323
  // the thinking-toggle map is a REAL wire alias (effort -> enabled/disabled) and stays.
@@ -294,6 +325,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
294
325
  ...Object.fromEntries(OPENCODE_GO_THINKING_TOGGLE_MODELS.map(id => [id, THINKING_TOGGLE_MAP])),
295
326
  },
296
327
  thinkingToggleModels: OPENCODE_GO_THINKING_TOGGLE_MODELS,
328
+ thinkingBudgetModels: THINKING_BUDGET_MODELS,
297
329
  noReasoningModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
298
330
  // Text-only Zen Go models (jawcode metadata) — the vision sidecar describes images for
299
331
  // every model listed here (and the catalog advertises image input on their behalf).
@@ -334,11 +366,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
334
366
  "kimi-k2.6": [],
335
367
  "kimi-k2.6-fast": [],
336
368
  "kimi-k2.7-code": [],
337
- "qwen3.5-397b": ["low", "medium", "high", "xhigh", "max"],
369
+ // Qwen3.x uses thinking_budget, NOT graded reasoning_effort; the adapter maps the five
370
+ // Codex picker levels onto budget fractions.
371
+ "qwen3.5-397b": THINKING_BUDGET_EFFORTS,
338
372
  "qwen3.5-397b-fast": [],
339
- "qwen3.6-35b": ["low", "medium", "high", "xhigh", "max"],
373
+ "qwen3.6-35b": THINKING_BUDGET_EFFORTS,
340
374
  "qwen3.6-35b-fast": [],
341
375
  },
376
+ thinkingBudgetModels: THINKING_BUDGET_MODELS,
342
377
  noReasoningModels: ["glm-5.2-fast", "kimi-k2.5-fast", "kimi-k2.6-fast", "qwen3.5-397b-fast", "qwen3.6-35b-fast"],
343
378
  noVisionModels: ["glm-5.2", "glm-5.2-fast", "qwen3.5-397b", "qwen3.5-397b-fast"],
344
379
  noTemperatureModels: ["kimi-k2.7-code"],
@@ -381,7 +416,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
381
416
  {
382
417
  id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: "https://api.moonshot.ai/v1", adapter: "openai-chat", authKind: "key",
383
418
  dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
384
- models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", "kimi-k2-0905-preview"],
419
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"],
385
420
  noReasoningModels: KIMI_THINKING_MODELS,
386
421
  modelReasoningEfforts: Object.fromEntries(KIMI_THINKING_MODELS.map(id => [id, []])),
387
422
  noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
@@ -1,17 +1,24 @@
1
1
  import type { OcxProviderConfig } from "./types";
2
2
  import { modelInList } from "./types";
3
3
 
4
+ // Descriptions mirror the upstream bundled models.json canonical wording (openai/codex PR #31684).
4
5
  export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] = [
5
6
  { effort: "low", description: "Fast responses with lighter reasoning" },
6
- { effort: "medium", description: "Balances speed and reasoning depth" },
7
+ { effort: "medium", description: "Balances speed and reasoning depth for everyday tasks" },
7
8
  { effort: "high", description: "Greater reasoning depth for complex problems" },
8
- { effort: "xhigh", description: "Extended reasoning for the hardest problems" },
9
- { effort: "max", description: "Maximum reasoning for the hardest problems" },
9
+ { effort: "xhigh", description: "Extra high reasoning depth for complex problems" },
10
+ { effort: "max", description: "Maximum reasoning depth for the hardest problems" },
11
+ { effort: "ultra", description: "Maximum reasoning with automatic task delegation" },
10
12
  ];
11
13
 
12
14
  const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
13
15
  const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
14
16
 
17
+ /** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */
18
+ export function isCodexReasoningEffort(effort: string): boolean {
19
+ return CODEX_REASONING_SET.has(effort);
20
+ }
21
+
15
22
  export function modelRecordValue<T>(record: Record<string, T> | undefined, modelId: string): T | undefined {
16
23
  if (!record) return undefined;
17
24
  if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
@@ -46,11 +53,25 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
46
53
  export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
47
54
  if (modelInList(provider.noReasoningModels, modelId)) return [];
48
55
  const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
49
- if (modelEfforts !== undefined) return sanitizeCodexReasoningEfforts(modelEfforts) ?? [];
50
- if (provider.reasoningEfforts !== undefined) return sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? [];
56
+ if (modelEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
57
+ if (provider.reasoningEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
51
58
  return undefined;
52
59
  }
53
60
 
61
+ /**
62
+ * Stale-ladder self-heal: saved configs seeded before `max` became a native Codex level can
63
+ * advertise a ladder that stops at `xhigh` while the wire map already routes xhigh -> max
64
+ * (e.g. opencode-go glm-5.2, deepseek thinking models). When the map proves the provider
65
+ * accepts wire `max`, append `max` so the picker actually shows the top tier. Thinking-toggle
66
+ * maps (xhigh -> "enabled") never match, so binary-toggle models stay two-step.
67
+ */
68
+ function healMaxTier(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
69
+ if (efforts.includes("max") || !efforts.includes("xhigh")) return efforts;
70
+ const wireMap = reasoningEffortMapFor(provider, modelId);
71
+ if (wireMap?.xhigh !== "max" && wireMap?.max !== "max") return efforts;
72
+ return sanitizeCodexReasoningEfforts([...efforts, "max"]) ?? efforts;
73
+ }
74
+
54
75
  function requestToCodexEffort(requested: string): string | undefined {
55
76
  if (requested === "none") return undefined;
56
77
  if (requested === "minimal") return "low";
@@ -89,13 +110,20 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
89
110
  if (!requested) return undefined;
90
111
  if (modelInList(provider.noReasoningModels, modelId)) return undefined;
91
112
 
113
+ // Upstream codex-rs converts ultra -> max before ANY provider request (core/src/client.rs
114
+ // `reasoning_effort_for_request`), so "ultra" must never influence the provider wire — not even
115
+ // through a raw alias. Apply the boundary before alias/clamp resolution.
116
+ const boundary = requested === "ultra" ? "max" : requested;
117
+
92
118
  const wireMap = reasoningEffortMapFor(provider, modelId);
93
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, requested)) return wireMap[requested];
119
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) return wireMap[boundary];
94
120
 
95
121
  const supported = configuredReasoningEfforts(provider, modelId);
96
- const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(requested, supported) : requestToCodexEffort(requested);
122
+ const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
97
123
  if (!codexEffort) return undefined;
98
124
 
99
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, codexEffort)) return wireMap[codexEffort];
100
- return codexEffort;
125
+ // Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
126
+ const wire = codexEffort === "ultra" ? "max" : codexEffort;
127
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) return wireMap[wire];
128
+ return wire;
101
129
  }
@@ -464,8 +464,13 @@ export function parseRequest(body: unknown): OcxParsedRequest {
464
464
  const tc = mapToolChoice(data.tool_choice);
465
465
  if (tc !== undefined) options.toolChoice = tc;
466
466
  if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
467
- if (data.reasoning?.effort && REASONING_EFFORTS.has(data.reasoning.effort)) {
468
- options.reasoning = data.reasoning.effort;
467
+ // Upstream codex-rs converts "ultra" to "max" at the inference boundary (core/src/client.rs
468
+ // `reasoning_effort_for_request`), so current clients never send it — but a catalog that
469
+ // advertises ultra plus an older/direct caller can. Degrade it to max like upstream instead of
470
+ // silently dropping reasoning altogether.
471
+ const requestedEffort = data.reasoning?.effort === "ultra" ? "max" : data.reasoning?.effort;
472
+ if (requestedEffort && REASONING_EFFORTS.has(requestedEffort)) {
473
+ options.reasoning = requestedEffort;
469
474
  }
470
475
  const summaryMode = data.reasoning?.summary;
471
476
  if (!summaryMode || summaryMode === "none") options.hideThinkingSummary = true;
package/src/router.ts CHANGED
@@ -96,6 +96,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
96
96
  const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
97
97
  const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
98
98
  const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
99
+ const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
99
100
 
100
101
  return {
101
102
  ...provider,
@@ -112,6 +113,9 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
112
113
  ...(provider.contextWindow === undefined && registryEntry.contextWindow !== undefined ? { contextWindow: registryEntry.contextWindow } : {}),
113
114
  ...(provider.reasoningEfforts === undefined && registryEntry.reasoningEfforts !== undefined ? { reasoningEfforts: registryEntry.reasoningEfforts } : {}),
114
115
  ...(provider.escapeBuiltinToolNames === undefined && registryEntry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: registryEntry.escapeBuiltinToolNames } : {}),
116
+ // Scalar backfill: a persisted config created before the flag shipped inherits the registry
117
+ // opt-in, while an explicit user `false` keeps overriding registry `true`.
118
+ ...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
115
119
  ...(modelContextWindows ? { modelContextWindows } : {}),
116
120
  ...(modelInputModalities ? { modelInputModalities } : {}),
117
121
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
@@ -125,6 +129,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
125
129
  ...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
126
130
  ...(preserveReasoningContentModels ? { preserveReasoningContentModels } : {}),
127
131
  ...(thinkingToggleModels ? { thinkingToggleModels } : {}),
132
+ ...(thinkingBudgetModels ? { thinkingBudgetModels } : {}),
128
133
  };
129
134
  }
130
135
 
@@ -10,7 +10,7 @@ import type { OcxProviderConfig } from "../types";
10
10
  /** Providers whose listed model ids must be driven over the Anthropic wire even if the provider's
11
11
  * configured adapter is something else (the upstream only speaks Anthropic for these models). */
12
12
  const ANTHROPIC_WIRE_MODELS: Record<string, Set<string>> = {
13
- "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3", "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]),
13
+ "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
14
14
  };
15
15
 
16
16
  /** Return a provider config whose adapter is forced to "anthropic" when the model id is wire-pinned. */
@@ -69,6 +69,7 @@ export {
69
69
  addFinalRequestLog,
70
70
  filterRequestLogs,
71
71
  httpStatusForTerminalStatus,
72
+ httpStatusFromTerminalError,
72
73
  nextRequestLogId,
73
74
  requestLogErrorCode,
74
75
  requestLogSpeedLabel,
@@ -157,6 +158,24 @@ export function startServer(port?: number) {
157
158
  config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
158
159
  saveConfig(config);
159
160
  }
161
+ // Sidecar model migration (KST 2026-07-10 06:00 = UTC 2026-07-09 21:00): auto-migrate the old
162
+ // gpt-5.4-mini default to gpt-5.6-luna for both search and vision sidecars. Only touches configs
163
+ // still on the old default — explicit user choices are preserved.
164
+ {
165
+ const SIDECAR_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0); // July 9 21:00 UTC = KST July 10 06:00
166
+ if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
167
+ let migrated = false;
168
+ if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
169
+ config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
170
+ migrated = true;
171
+ }
172
+ if (config.visionSidecar?.model === "gpt-5.4-mini") {
173
+ config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
174
+ migrated = true;
175
+ }
176
+ if (migrated) saveConfig(config);
177
+ }
178
+ }
160
179
  invalidateCodexModelsCache();
161
180
 
162
181
  const listenPort = port ?? config.port ?? 10100;
@@ -247,7 +266,7 @@ export function startServer(port?: number) {
247
266
  return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
248
267
  }
249
268
  const goModels = await fetchAllModels(config);
250
- const { buildCatalogEntries, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels } = await import("../codex/catalog");
269
+ const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
251
270
  const nativeSlugs = nativeOpenAiSlugs();
252
271
  const goEnabled = filterCatalogVisibleModels(goModels, config);
253
272
  const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
@@ -255,11 +274,16 @@ export function startServer(port?: number) {
255
274
  // Codex client → Codex catalog shape: native gpt + namespaced routed models,
256
275
  // cloned from a native template so required fields (base_instructions, etc.) are present.
257
276
  // Pass the subagent picks so featured models lead by priority (matches the on-disk file).
258
- return jsonResponse({ models: buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config)) }, 200, req, config);
277
+ // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
278
+ // on-disk sync; codex-rs keeps them out of the picker itself).
279
+ const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
280
+ const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2");
281
+ return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
259
282
  }
260
283
  // OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
284
+ // (pure availability list — disabled natives are omitted entirely).
261
285
  const data = [
262
- ...nativeSlugs.map(id => ({ id, object: "model", created: 0, owned_by: "openai" })),
286
+ ...visibleNativeSlugs(config).map(id => ({ id, object: "model", created: 0, owned_by: "openai" })),
263
287
  ...goOrdered.map(m => ({ id: `${m.provider}/${m.id}`, object: "model", created: 0, owned_by: m.owned_by ?? m.provider })),
264
288
  ];
265
289
  return jsonResponse({ object: "list", data }, 200, req, config);