@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.
- package/README.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BGdxwydf.js +34 -0
- package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +56 -10
- package/src/adapters/cursor/effort-map.ts +35 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -6
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +32 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +168 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +209 -0
- package/src/types.ts +28 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- 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
|
-
|
|
66
|
-
|
|
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
|
|
87
|
-
*
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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":
|
|
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"
|
|
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,
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -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: "
|
|
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,
|
|
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(
|
|
117
|
+
const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
|
|
97
118
|
if (!codexEffort) return undefined;
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -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
|
-
|
|
468
|
-
|
|
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"
|
|
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. */
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
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.
|
|
142
|
-
vision: { model: vs.model ?? "gpt-5.
|
|
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.
|
|
164
|
-
vision: { model: vs.model ?? "gpt-5.
|
|
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
|
-
|
|
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") {
|