@bitkyc08/opencodex 2.6.32 → 2.7.0

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 (55) 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-BGdxwydf.js +34 -0
  5. package/gui/dist/assets/index-DANCQ2Jt.css +1 -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 +56 -10
  11. package/src/adapters/cursor/effort-map.ts +35 -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 -6
  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 +32 -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 +168 -7
  47. package/src/server/relay.ts +2 -2
  48. package/src/server/request-log.ts +78 -0
  49. package/src/server/responses.ts +209 -0
  50. package/src/types.ts +28 -1
  51. package/src/usage/debug.ts +32 -5
  52. package/src/usage/summary.ts +6 -6
  53. package/src/web-search/index.ts +1 -1
  54. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  55. 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,12 +1,14 @@
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);
@@ -46,11 +48,25 @@ export function sanitizeCodexReasoningEfforts(efforts: readonly string[] | undef
46
48
  export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId: string): string[] | undefined {
47
49
  if (modelInList(provider.noReasoningModels, modelId)) return [];
48
50
  const modelEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId);
49
- if (modelEfforts !== undefined) return sanitizeCodexReasoningEfforts(modelEfforts) ?? [];
50
- if (provider.reasoningEfforts !== undefined) return sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? [];
51
+ if (modelEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(modelEfforts) ?? []);
52
+ if (provider.reasoningEfforts !== undefined) return healMaxTier(provider, modelId, sanitizeCodexReasoningEfforts(provider.reasoningEfforts) ?? []);
51
53
  return undefined;
52
54
  }
53
55
 
56
+ /**
57
+ * Stale-ladder self-heal: saved configs seeded before `max` became a native Codex level can
58
+ * advertise a ladder that stops at `xhigh` while the wire map already routes xhigh -> max
59
+ * (e.g. opencode-go glm-5.2, deepseek thinking models). When the map proves the provider
60
+ * accepts wire `max`, append `max` so the picker actually shows the top tier. Thinking-toggle
61
+ * maps (xhigh -> "enabled") never match, so binary-toggle models stay two-step.
62
+ */
63
+ function healMaxTier(provider: OcxProviderConfig, modelId: string, efforts: string[]): string[] {
64
+ if (efforts.includes("max") || !efforts.includes("xhigh")) return efforts;
65
+ const wireMap = reasoningEffortMapFor(provider, modelId);
66
+ if (wireMap?.xhigh !== "max" && wireMap?.max !== "max") return efforts;
67
+ return sanitizeCodexReasoningEfforts([...efforts, "max"]) ?? efforts;
68
+ }
69
+
54
70
  function requestToCodexEffort(requested: string): string | undefined {
55
71
  if (requested === "none") return undefined;
56
72
  if (requested === "minimal") return "low";
@@ -89,13 +105,20 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
89
105
  if (!requested) return undefined;
90
106
  if (modelInList(provider.noReasoningModels, modelId)) return undefined;
91
107
 
108
+ // Upstream codex-rs converts ultra -> max before ANY provider request (core/src/client.rs
109
+ // `reasoning_effort_for_request`), so "ultra" must never influence the provider wire — not even
110
+ // through a raw alias. Apply the boundary before alias/clamp resolution.
111
+ const boundary = requested === "ultra" ? "max" : requested;
112
+
92
113
  const wireMap = reasoningEffortMapFor(provider, modelId);
93
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, requested)) return wireMap[requested];
114
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) return wireMap[boundary];
94
115
 
95
116
  const supported = configuredReasoningEfforts(provider, modelId);
96
- const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(requested, supported) : requestToCodexEffort(requested);
117
+ const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
97
118
  if (!codexEffort) return undefined;
98
119
 
99
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, codexEffort)) return wireMap[codexEffort];
100
- return codexEffort;
120
+ // Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
121
+ const wire = codexEffort === "ultra" ? "max" : codexEffort;
122
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) return wireMap[wire];
123
+ return wire;
101
124
  }
@@ -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);
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import type { CatalogModel } from "../codex/catalog";
3
- import { invalidateCodexModelsCache } from "../codex/catalog";
3
+ import { invalidateCodexModelsCache, nativeModelRows } from "../codex/catalog";
4
4
  import {
5
5
  DEFAULT_SUBAGENT_MODELS,
6
6
  codexAutoStartEnabled,
@@ -24,8 +24,17 @@ import { deriveProviderPresets } from "../providers/derive";
24
24
  import { fetchProviderQuotaReports } from "../providers/quota";
25
25
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
26
26
  import { readUsageEntries } from "../usage/log";
27
+ import { getUsageDebugLogEntries } from "../usage/debug";
27
28
  import { parseRange, summarizeUsage } from "../usage/summary";
28
29
  import { stripCodexRuntimeProviderFields } from "../codex/auth-context";
30
+ import { getDebugLogEntries } from "../lib/debug-log-buffer";
31
+ import {
32
+ clearDebugSettings,
33
+ clearDebugSetting,
34
+ getDebugSettings,
35
+ setDebugSettings,
36
+ type DebugFlag,
37
+ } from "../lib/debug-settings";
29
38
  import type { OcxConfig, OcxProviderConfig } from "../types";
30
39
  import { drainAndShutdown } from "./lifecycle";
31
40
  import { filterRequestLogs, getRequestLogEntries } from "./request-log";
@@ -41,6 +50,15 @@ export const VERSION = (() => {
41
50
  }
42
51
  })();
43
52
 
53
+ function parseDebugLogQuery(url: URL): { after: number; limit: number } {
54
+ const after = Number(url.searchParams.get("after") ?? url.searchParams.get("since") ?? "0");
55
+ const limit = Number(url.searchParams.get("limit") ?? "500");
56
+ return {
57
+ after: Number.isFinite(after) && after > 0 ? after : 0,
58
+ limit: Number.isFinite(limit) && limit > 0 ? Math.min(limit, 2000) : 500,
59
+ };
60
+ }
61
+
44
62
  export async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): Promise<Response | null> {
45
63
  if (!isAllowedRequestOrigin(req, config)) {
46
64
  return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
@@ -89,6 +107,12 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
89
107
  return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config) });
90
108
  }
91
109
 
110
+ if (url.pathname === "/api/diagnostics/project-config" && req.method === "GET") {
111
+ const { getCachedProjectConfigDiagnostics } = await import("../codex/project-config-warnings");
112
+ const { warnings, grouped } = getCachedProjectConfigDiagnostics();
113
+ return jsonResponse({ warnings, grouped });
114
+ }
115
+
92
116
  if (url.pathname === "/api/sync" && req.method === "POST") {
93
117
  const { syncModelsToCodex } = await import("../codex/sync");
94
118
  const result = await syncModelsToCodex(undefined, config, null);
@@ -138,8 +162,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
138
162
  const ws = config.webSearchSidecar ?? {};
139
163
  const vs = config.visionSidecar ?? {};
140
164
  return jsonResponse({
141
- webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
142
- vision: { model: vs.model ?? "gpt-5.4-mini" },
165
+ webSearch: { model: ws.model ?? "gpt-5.6-luna", reasoning: ws.reasoning ?? "low" },
166
+ vision: { model: vs.model ?? "gpt-5.6-luna" },
143
167
  });
144
168
  }
145
169
 
@@ -160,8 +184,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
160
184
  const vs = config.visionSidecar ?? {};
161
185
  return jsonResponse({
162
186
  ok: true,
163
- webSearch: { model: ws.model ?? "gpt-5.4-mini", reasoning: ws.reasoning ?? "low" },
164
- vision: { model: vs.model ?? "gpt-5.4-mini" },
187
+ webSearch: { model: ws.model ?? "gpt-5.6-luna", reasoning: ws.reasoning ?? "low" },
188
+ vision: { model: vs.model ?? "gpt-5.6-luna" },
165
189
  });
166
190
  }
167
191
 
@@ -169,6 +193,38 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
169
193
  return jsonResponse(filterRequestLogs(getRequestLogEntries(), url.searchParams));
170
194
  }
171
195
 
196
+ if (url.pathname === "/api/debug" && req.method === "GET") {
197
+ return jsonResponse(getDebugSettings());
198
+ }
199
+
200
+ if (url.pathname === "/api/debug/logs" && req.method === "GET") {
201
+ const { after, limit } = parseDebugLogQuery(url);
202
+ return jsonResponse(getDebugLogEntries({ after, limit }));
203
+ }
204
+
205
+ if (url.pathname === "/api/debug/usage-logs" && req.method === "GET") {
206
+ const { after, limit } = parseDebugLogQuery(url);
207
+ return jsonResponse(getUsageDebugLogEntries({ after, limit }));
208
+ }
209
+
210
+ if (url.pathname === "/api/debug" && req.method === "PUT") {
211
+ let body: { debug?: unknown; usage?: unknown; reset?: unknown };
212
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
213
+ if (body.reset === true) return jsonResponse(clearDebugSettings());
214
+ if (body.reset === "debug" || body.reset === "provider") return jsonResponse(clearDebugSetting("debug"));
215
+ if (body.reset === "usage") return jsonResponse(clearDebugSetting("usage"));
216
+ const partial: Partial<Record<DebugFlag, boolean>> = {};
217
+ for (const key of ["debug", "usage"] as const) {
218
+ if (body[key] === undefined) continue;
219
+ if (typeof body[key] !== "boolean") return jsonResponse({ error: `${key} must be a boolean` }, 400);
220
+ partial[key] = body[key];
221
+ }
222
+ if (Object.keys(partial).length === 0) {
223
+ return jsonResponse({ error: "provide debug/usage booleans or reset:true" }, 400);
224
+ }
225
+ return jsonResponse(setDebugSettings(partial));
226
+ }
227
+
172
228
  if (url.pathname === "/api/usage" && req.method === "GET") {
173
229
  const range = parseRange(url.searchParams.get("range"));
174
230
  const now = Date.now();
@@ -284,7 +340,17 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
284
340
  if (url.pathname === "/api/models" && req.method === "GET") {
285
341
  const models = await fetchAllModels(config);
286
342
  const disabled = new Set(config.disabledModels ?? []);
287
- return jsonResponse(models.map(m => {
343
+ // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
344
+ // from the static supported set so a disabled model stays listed and re-enableable.
345
+ const native = nativeModelRows(config).map(row => ({
346
+ provider: "openai",
347
+ id: row.slug,
348
+ namespaced: row.slug,
349
+ disabled: row.disabled,
350
+ native: true,
351
+ ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
352
+ }));
353
+ return jsonResponse([...native, ...models.map(m => {
288
354
  const namespaced = `${m.provider}/${m.id}`;
289
355
  const contextCap = providerContextCap(config, m.provider);
290
356
  return {
@@ -293,7 +359,7 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
293
359
  disabled: disabled.has(namespaced),
294
360
  ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
295
361
  };
296
- }));
362
+ })]);
297
363
  }
298
364
 
299
365
  if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
@@ -365,6 +431,76 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
365
431
  return jsonResponse({ ok: true, disabled });
366
432
  }
367
433
 
434
+ // multi_agent_v2 surface toggle. GET reports the flag + the agents.max_threads
435
+ // boot conflict; PUT flips it via the official `codex features` CLI and RESYNCS
436
+ // the catalog so multi-agent surface metadata stays fresh. The catalog build
437
+ // itself never writes config — this endpoint is the only server-side mutation
438
+ // surface for the flag.
439
+ if (url.pathname === "/api/v2" && req.method === "GET") {
440
+ const { isMultiAgentV2Enabled, hasAgentsMaxThreads, getMaxConcurrentThreads } = await import("../codex/features");
441
+ return jsonResponse({
442
+ enabled: isMultiAgentV2Enabled(),
443
+ agentsMaxThreadsConflict: hasAgentsMaxThreads(),
444
+ maxConcurrentThreadsPerSession: getMaxConcurrentThreads(),
445
+ multiAgentMode: config.multiAgentMode ?? "default",
446
+ });
447
+ }
448
+ if (url.pathname === "/api/v2" && req.method === "PUT") {
449
+ let body: { enabled?: unknown; maxConcurrentThreadsPerSession?: unknown; multiAgentMode?: unknown };
450
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
451
+ const wantsFlag = body.enabled !== undefined;
452
+ const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
453
+ const wantsMode = body.multiAgentMode !== undefined;
454
+ if (!wantsFlag && !wantsThreads && !wantsMode) return jsonResponse({ error: "body must set enabled, multiAgentMode, and/or maxConcurrentThreadsPerSession" }, 400);
455
+ if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
456
+ if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
457
+ return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
458
+ }
459
+ if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
460
+ return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
461
+ }
462
+ const { isMultiAgentV2Enabled, hasAgentsMaxThreads, getMaxConcurrentThreads, setMaxConcurrentThreads } = await import("../codex/features");
463
+ const warnings: string[] = [];
464
+ if (wantsFlag && isMultiAgentV2Enabled() !== body.enabled) {
465
+ const { execFileSync } = await import("node:child_process");
466
+ const command = process.env.CODEX_CLI_PATH?.trim() || "codex";
467
+ try {
468
+ execFileSync(command, ["features", body.enabled ? "enable" : "disable", "multi_agent_v2"],
469
+ { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true });
470
+ } catch (err) {
471
+ return jsonResponse({ error: `codex features ${body.enabled ? "enable" : "disable"} failed: ${err instanceof Error ? err.message : String(err)}` }, 502);
472
+ }
473
+ await refreshCodexCatalogBestEffort();
474
+ }
475
+ if (wantsThreads) {
476
+ // setMaxConcurrentThreads is idempotent (equal value -> no write) and refuses
477
+ // when the [features.multi_agent_v2] table is missing, so a threads-only PUT
478
+ // against a never-enabled config fails loudly instead of inventing state.
479
+ const result = setMaxConcurrentThreads(body.maxConcurrentThreadsPerSession as number);
480
+ if (!result.ok) return jsonResponse({ error: result.error }, 409);
481
+ if (result.changed) warnings.push("Thread limit applies to new sessions.");
482
+ }
483
+ if (wantsMode) {
484
+ const mode = body.multiAgentMode as "v1" | "default" | "v2";
485
+ if (mode === "default") delete config.multiAgentMode;
486
+ else config.multiAgentMode = mode;
487
+ saveConfig(config);
488
+ await refreshCodexCatalogBestEffort();
489
+ warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
490
+ }
491
+ if ((wantsFlag ? body.enabled === true : isMultiAgentV2Enabled()) && hasAgentsMaxThreads()) {
492
+ warnings.push("[agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled; remove it (features.multi_agent_v2.max_concurrent_threads_per_session replaces it).");
493
+ }
494
+ if (wantsFlag) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
495
+ return jsonResponse({
496
+ ok: true,
497
+ enabled: isMultiAgentV2Enabled(),
498
+ maxConcurrentThreadsPerSession: getMaxConcurrentThreads(),
499
+ multiAgentMode: config.multiAgentMode ?? "default",
500
+ warnings,
501
+ });
502
+ }
503
+
368
504
  // Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
369
505
  if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
370
506
  return jsonResponse({ providers: listOAuthProviders() });
@@ -381,6 +517,31 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
381
517
  return jsonResponse({ providers: deriveProviderPresets() });
382
518
  }
383
519
 
520
+ // Subagent prompt injection model: single native or routed model whose info is
521
+ // dynamically injected into the v1 proactive prompt. GET returns the current pick
522
+ // + available models; PUT sets or clears the pick.
523
+ if (url.pathname === "/api/injection-model" && req.method === "GET") {
524
+ const models = await fetchAllModels(config);
525
+ const disabled = new Set(config.disabledModels ?? []);
526
+ const { listCatalogNativeSlugs } = await import("../codex/catalog");
527
+ const nativeModels = listCatalogNativeSlugs()
528
+ .filter(slug => !disabled.has(slug))
529
+ .map(slug => ({ provider: "openai", model: slug, namespaced: slug }));
530
+ const routedModels = models
531
+ .map(m => ({ provider: m.provider, model: m.id, namespaced: `${m.provider}/${m.id}` }))
532
+ .filter(m => !disabled.has(m.namespaced));
533
+ return jsonResponse({ model: config.injectionModel ?? null, available: [...nativeModels, ...routedModels] });
534
+ }
535
+ if (url.pathname === "/api/injection-model" && req.method === "PUT") {
536
+ let body: { model?: unknown };
537
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
538
+ const model = typeof body.model === "string" && body.model.length > 0 ? body.model : undefined;
539
+ if (model) config.injectionModel = model;
540
+ else delete config.injectionModel;
541
+ saveConfig(config);
542
+ return jsonResponse({ ok: true, model: config.injectionModel ?? null });
543
+ }
544
+
384
545
  // Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
385
546
  // first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
386
547
  if (url.pathname === "/api/subagent-models" && req.method === "GET") {