@bitkyc08/opencodex 2.11.0 → 2.11.1

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 (92) hide show
  1. package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
  2. package/gui/dist/assets/index-DDZpgzKk.js +70 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +6 -4
  5. package/src/adapters/anthropic.ts +10 -2
  6. package/src/adapters/cursor/native-exec-common.ts +6 -2
  7. package/src/adapters/google-antigravity-replay.ts +3 -2
  8. package/src/adapters/google-antigravity-wire.ts +38 -6
  9. package/src/adapters/google.ts +57 -10
  10. package/src/adapters/openai-chat.ts +19 -2
  11. package/src/claude/model-info.ts +1 -1
  12. package/src/cli/account-auth.ts +3 -1
  13. package/src/cli/account-catalog-refresh.ts +14 -0
  14. package/src/cli/account-extended.ts +15 -1
  15. package/src/cli/codex-shim-readiness.ts +69 -0
  16. package/src/cli/combo.ts +5 -0
  17. package/src/cli/config-command.ts +33 -4
  18. package/src/cli/doctor.ts +21 -0
  19. package/src/cli/export-command.ts +6 -6
  20. package/src/cli/help.ts +6 -6
  21. package/src/cli/index.ts +22 -5
  22. package/src/cli/models-runtime.ts +13 -1
  23. package/src/cli/provider.ts +7 -0
  24. package/src/clients/config-export.ts +67 -5
  25. package/src/codex/account-lifecycle.ts +99 -10
  26. package/src/codex/auth-api.ts +222 -34
  27. package/src/codex/catalog/account-models.ts +9 -4
  28. package/src/codex/catalog/aggregation.ts +41 -8
  29. package/src/codex/catalog/bundled.ts +54 -22
  30. package/src/codex/catalog/effort.ts +47 -20
  31. package/src/codex/catalog/kinds.ts +2 -0
  32. package/src/codex/catalog/metadata.ts +70 -18
  33. package/src/codex/catalog/native-models.ts +7 -0
  34. package/src/codex/catalog/parsing.ts +29 -11
  35. package/src/codex/catalog/provider-fetch.ts +335 -50
  36. package/src/codex/catalog/sync.ts +588 -126
  37. package/src/codex/catalog-refresh-status.ts +87 -0
  38. package/src/codex/catalog-write-serialization.ts +2 -1
  39. package/src/codex/catalog.ts +4 -3
  40. package/src/codex/convergence-types.ts +1 -1
  41. package/src/codex/convergence.ts +190 -52
  42. package/src/codex/custom-model-catalog-migration.ts +176 -0
  43. package/src/codex/features.ts +79 -1
  44. package/src/codex/history-job.ts +81 -1
  45. package/src/codex/history-lock.ts +2 -1
  46. package/src/codex/history-provider.ts +4 -3
  47. package/src/codex/inject.ts +56 -12
  48. package/src/codex/model-cache.ts +50 -10
  49. package/src/codex/transition-state.ts +10 -2
  50. package/src/codex/user-identity.ts +110 -2
  51. package/src/combos/index.ts +3 -0
  52. package/src/combos/types.ts +75 -9
  53. package/src/config.ts +26 -12
  54. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
  55. package/src/integrations/registry.ts +7 -0
  56. package/src/integrations/writer.ts +1 -1
  57. package/src/lib/pinned-http.ts +40 -9
  58. package/src/lib/process-control.ts +4 -1
  59. package/src/lib/provider-outbound.ts +42 -9
  60. package/src/oauth/cursor.ts +25 -5
  61. package/src/oauth/index.ts +59 -45
  62. package/src/providers/antigravity-models.ts +115 -3
  63. package/src/providers/context-cap.ts +13 -5
  64. package/src/providers/model-discovery-limits.ts +16 -0
  65. package/src/providers/model-discovery.ts +14 -15
  66. package/src/providers/provider-id-rewrite.ts +29 -0
  67. package/src/providers/quota.ts +93 -19
  68. package/src/providers/registry.ts +16 -5
  69. package/src/providers/slug-codec.ts +13 -6
  70. package/src/reasoning-effort.ts +22 -0
  71. package/src/router.ts +0 -20
  72. package/src/routing/history/indexer.ts +54 -39
  73. package/src/routing/quota.ts +77 -56
  74. package/src/server/index.ts +32 -7
  75. package/src/server/management/combo-routes.ts +43 -19
  76. package/src/server/management/config-routes.ts +115 -20
  77. package/src/server/management/model-routes.ts +10 -7
  78. package/src/server/management/model-rows.ts +6 -2
  79. package/src/server/management/oauth-account-routes.ts +12 -0
  80. package/src/server/management/provider-routes.ts +114 -40
  81. package/src/server/management/routing-profile-routes.ts +0 -22
  82. package/src/server/management-api.ts +7 -25
  83. package/src/server/responses/core.ts +3 -3
  84. package/src/server/responses/policy-fallback.ts +152 -0
  85. package/src/server/responses.ts +3 -2
  86. package/src/types.ts +15 -2
  87. package/src/usage/cost.ts +0 -0
  88. package/src/vision/describe.ts +3 -1
  89. package/src/vision/index.ts +17 -6
  90. package/src/vision/reasoning.ts +55 -0
  91. package/src/web-search/parse.ts +15 -3
  92. package/gui/dist/assets/index-BynIEIV-.js +0 -70
@@ -111,6 +111,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
111
111
  clearComboSelectionState,
112
112
  clearComboTargetCooldowns,
113
113
  comboConfigError,
114
+ comboDisabledModelId,
115
+ comboDisabledModelSelectors,
114
116
  comboModelId,
115
117
  comboPublicModelId,
116
118
  normalizeComboConfig,
@@ -122,13 +124,31 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
122
124
  });
123
125
  if (error) return jsonResponse({ error }, 400);
124
126
  const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
125
- const stored: import("../../types").OcxComboConfig = normalized.alias === null
126
- ? (({ alias: _alias, ...rest }) => rest)(normalized)
127
- : normalized;
127
+ const {
128
+ alias: normalizedAlias,
129
+ nativeAlias: normalizedNativeAlias,
130
+ displayName: normalizedDisplayName,
131
+ ...normalizedBase
132
+ } = normalized;
133
+ const stored: import("../../types").OcxComboConfig = {
134
+ ...normalizedBase,
135
+ ...(normalizedAlias ? { alias: normalizedAlias } : {}),
136
+ ...(normalizedNativeAlias ? { nativeAlias: true } : {}),
137
+ ...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
138
+ };
128
139
  const sourceId = renameFrom ?? id;
129
140
  const previous = config.combos?.[sourceId];
130
141
  const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
131
142
  const newPublicModel = comboPublicModelId(id, normalized);
143
+ const disabledIdentityChanged = previous !== undefined && (
144
+ renameFrom !== undefined
145
+ || oldPublicModel !== newPublicModel
146
+ || (previous.nativeAlias === true) !== normalized.nativeAlias
147
+ );
148
+ const oldDisabledSelectors = disabledIdentityChanged
149
+ ? new Set(comboDisabledModelSelectors(sourceId, previous))
150
+ : new Set<string>();
151
+ const newDisabledModel = comboDisabledModelId(id, normalized);
132
152
  if (codexAccountNamespaceForModel(config.codexAccountNamespaces, newPublicModel)) {
133
153
  return jsonResponse({ error: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR }, 409);
134
154
  }
@@ -137,36 +157,35 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
137
157
  nextCombos[id] = stored;
138
158
  config.combos = nextCombos;
139
159
  let shouldSyncClaudeAgentDefs = false;
140
- const migratedModels = new Set<string>();
141
- if (oldPublicModel && oldPublicModel !== newPublicModel) {
142
- migratedModels.add(oldPublicModel);
160
+ const migratedModels = new Map<string, string>();
161
+ if (oldPublicModel && oldPublicModel !== newPublicModel && previous?.nativeAlias !== true) {
162
+ migratedModels.set(oldPublicModel, newPublicModel);
143
163
  }
144
- if (renameFrom) migratedModels.add(comboModelId(renameFrom));
145
- if (migratedModels.size > 0) {
146
- const migrateReference = (model: string): string => (
147
- migratedModels.has(model) ? newPublicModel : model
164
+ if (renameFrom) {
165
+ // A bare native id is ambiguous after the alias changes. Preserve it as a native route,
166
+ // while the unambiguous canonical combo reference follows the renamed combo.
167
+ migratedModels.set(
168
+ comboModelId(renameFrom),
169
+ previous?.nativeAlias === true ? comboModelId(id) : newPublicModel,
148
170
  );
171
+ }
172
+ if (migratedModels.size > 0) {
173
+ const migrateReference = (model: string): string => migratedModels.get(model) ?? model;
149
174
  const migrateAgentReference = (model: string): string => {
150
175
  const migrated = migrateReference(model);
151
176
  if (migrated !== model) shouldSyncClaudeAgentDefs = true;
152
177
  return migrated;
153
178
  };
154
- const migrateReferences = (models: string[]): string[] => [
155
- ...new Set(models.map(migrateReference)),
156
- ];
157
- if (config.disabledModels) {
158
- config.disabledModels = migrateReferences(config.disabledModels);
159
- }
160
179
  if (config.subagentModels) {
161
180
  config.subagentModels = [...new Set(config.subagentModels.map(migrateAgentReference))];
162
181
  }
163
182
  if (config.injectionModel && migratedModels.has(config.injectionModel)) {
164
- config.injectionModel = newPublicModel;
183
+ config.injectionModel = migrateReference(config.injectionModel);
165
184
  }
166
185
  if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
167
186
  config.shadowCallIntercept = {
168
187
  ...config.shadowCallIntercept,
169
- model: newPublicModel,
188
+ model: migrateReference(config.shadowCallIntercept.model),
170
189
  };
171
190
  }
172
191
  if (config.claudeCode) {
@@ -187,6 +206,11 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
187
206
  config.claudeCode = claudeCode;
188
207
  }
189
208
  }
209
+ if (oldDisabledSelectors.size > 0 && config.disabledModels) {
210
+ config.disabledModels = [...new Set(config.disabledModels.map(model => (
211
+ oldDisabledSelectors.has(model) ? newDisabledModel : model
212
+ )))];
213
+ }
190
214
  saveConfigPreservingClaudeCode(config);
191
215
  reconcileLiveStateStores();
192
216
  clearComboSelectionState(id);
@@ -197,7 +221,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
197
221
  }
198
222
  const catalogRefresh = await convergeCodexCatalog();
199
223
  if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
200
- return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized, catalogRefresh });
224
+ return jsonResponse({ success: true, id, model: newPublicModel, combo: stored, catalogRefresh });
201
225
  }
202
226
 
203
227
  if (url.pathname === "/api/combos" && req.method === "DELETE") {
@@ -40,6 +40,11 @@ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../provid
40
40
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
41
41
  import { clearThreadAccountMap } from "../../codex/routing";
42
42
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
43
+ import {
44
+ codexAccountPickerEnabled,
45
+ initializeDefaultCodexAccountNamespaces,
46
+ } from "../../codex/account-namespaces";
47
+ import { catalogRefreshIsPending } from "../../codex/catalog-refresh-status";
43
48
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
44
49
  import { resolveCodexHomeDir } from "../../codex/home";
45
50
  import { readUsageEntries } from "../../usage/log";
@@ -47,6 +52,8 @@ import { getUsageDebugLogEntries } from "../../usage/debug";
47
52
  import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
48
53
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
49
54
  import { getProviderRegistryEntry } from "../../providers/registry";
55
+ import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
56
+ import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
50
57
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
51
58
  import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
52
59
  import {
@@ -74,7 +81,7 @@ import type { ManagementContext } from "./context";
74
81
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
75
82
 
76
83
  export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
77
- const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
84
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
78
85
  if (url.pathname === "/api/config" && req.method === "GET") {
79
86
  return jsonResponse(safeConfigDTO(config));
80
87
  }
@@ -129,6 +136,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
129
136
  hostname: config.hostname ?? "127.0.0.1",
130
137
  streamMode: config.streamMode ?? "auto",
131
138
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
139
+ codexAccountPickerEnabled: codexAccountPickerEnabled(config),
132
140
  startupHealth: await getCachedStartupHealth(config),
133
141
  codexRuntime: {
134
142
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -205,10 +213,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
205
213
  // (a Windows service does not inherit shell env). A stream-shape
206
214
  // change applies to NEW turns only — the config object is shared by
207
215
  // reference with the request handlers, no restart needed.
208
- let body: { codexAutoStart?: unknown; streamMode?: unknown; appOwnedMemoryBudgetMb?: unknown };
209
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
210
- if (body.codexAutoStart === undefined && body.streamMode === undefined && body.appOwnedMemoryBudgetMb === undefined) {
211
- return jsonResponse({ error: "provide codexAutoStart, streamMode, or appOwnedMemoryBudgetMb" }, 400);
216
+ let parsedBody: unknown;
217
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
218
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "settings body must be an object" }, 400);
219
+ const body = parsedBody as {
220
+ codexAutoStart?: unknown;
221
+ streamMode?: unknown;
222
+ appOwnedMemoryBudgetMb?: unknown;
223
+ codexAccountPickerEnabled?: unknown;
224
+ };
225
+ if (body.codexAutoStart === undefined
226
+ && body.streamMode === undefined
227
+ && body.appOwnedMemoryBudgetMb === undefined
228
+ && body.codexAccountPickerEnabled === undefined) {
229
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, or codexAccountPickerEnabled" }, 400);
212
230
  }
213
231
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
214
232
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -216,6 +234,10 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
216
234
  if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
217
235
  return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
218
236
  }
237
+ if (body.codexAccountPickerEnabled !== undefined
238
+ && typeof body.codexAccountPickerEnabled !== "boolean") {
239
+ return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400);
240
+ }
219
241
  if (body.appOwnedMemoryBudgetMb !== undefined && (
220
242
  typeof body.appOwnedMemoryBudgetMb !== "number"
221
243
  || !Number.isInteger(body.appOwnedMemoryBudgetMb)
@@ -224,30 +246,76 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
224
246
  )) {
225
247
  return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
226
248
  }
227
- if (typeof body.codexAutoStart === "boolean") {
228
- config.codexAutoStart = body.codexAutoStart;
229
- }
230
- if (body.streamMode !== undefined) {
231
- if (body.streamMode === "auto") {
232
- delete config.streamMode;
233
- } else {
234
- config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
249
+ const previousSettings = {
250
+ codexAutoStart: config.codexAutoStart,
251
+ hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
252
+ streamMode: config.streamMode,
253
+ hasStreamMode: Object.hasOwn(config, "streamMode"),
254
+ appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb,
255
+ hasAppOwnedMemoryBudgetMb: Object.hasOwn(config, "appOwnedMemoryBudgetMb"),
256
+ codexAccountNamespaces: config.codexAccountNamespaces,
257
+ hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
258
+ codexAccountPickerEnabled: config.codexAccountPickerEnabled,
259
+ hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
260
+ };
261
+ const pickerWasEnabled = codexAccountPickerEnabled(config);
262
+ let pickerIsEnabled = pickerWasEnabled;
263
+ try {
264
+ if (typeof body.codexAutoStart === "boolean") {
265
+ config.codexAutoStart = body.codexAutoStart;
235
266
  }
267
+ if (body.streamMode !== undefined) {
268
+ if (body.streamMode === "auto") {
269
+ delete config.streamMode;
270
+ } else {
271
+ config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
272
+ }
273
+ }
274
+ if (typeof body.appOwnedMemoryBudgetMb === "number") {
275
+ config.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
276
+ }
277
+ if (body.codexAccountPickerEnabled === true) {
278
+ config.codexAccountPickerEnabled = true;
279
+ initializeDefaultCodexAccountNamespaces(config);
280
+ } else if (body.codexAccountPickerEnabled === false) {
281
+ config.codexAccountPickerEnabled = false;
282
+ }
283
+ pickerIsEnabled = codexAccountPickerEnabled(config);
284
+ (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
285
+ } catch (error) {
286
+ if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart;
287
+ else delete config.codexAutoStart;
288
+ if (previousSettings.hasStreamMode) config.streamMode = previousSettings.streamMode;
289
+ else delete config.streamMode;
290
+ if (previousSettings.hasAppOwnedMemoryBudgetMb) {
291
+ config.appOwnedMemoryBudgetMb = previousSettings.appOwnedMemoryBudgetMb;
292
+ } else delete config.appOwnedMemoryBudgetMb;
293
+ if (previousSettings.hasCodexAccountNamespaces) {
294
+ config.codexAccountNamespaces = previousSettings.codexAccountNamespaces;
295
+ } else delete config.codexAccountNamespaces;
296
+ if (previousSettings.hasCodexAccountPickerEnabled) {
297
+ config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
298
+ } else delete config.codexAccountPickerEnabled;
299
+ throw error;
236
300
  }
237
- if (typeof body.appOwnedMemoryBudgetMb === "number") {
238
- config.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
239
- }
240
- saveConfigPreservingClaudeCode(config);
241
301
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
242
302
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
243
303
  enforceAppOwnedMemoryBudget();
244
304
  }
305
+ const catalogRefresh = pickerWasEnabled !== pickerIsEnabled
306
+ ? await convergeCodexCatalog()
307
+ : undefined;
308
+ const catalogRefreshPending = catalogRefresh
309
+ ? catalogRefreshIsPending(catalogRefresh)
310
+ : false;
245
311
  invalidateStartupHealthCache();
246
312
  return jsonResponse({
247
313
  ok: true,
248
314
  codexAutoStart: codexAutoStartEnabled(config),
249
315
  streamMode: config.streamMode ?? "auto",
250
316
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
317
+ codexAccountPickerEnabled: pickerIsEnabled,
318
+ catalogRefreshPending,
251
319
  startupHealth: await getCachedStartupHealth(config),
252
320
  });
253
321
  }
@@ -312,11 +380,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
312
380
  if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
313
381
  const ws = config.webSearchSidecar ?? {};
314
382
  const vs = config.visionSidecar ?? {};
383
+ const visionModel = vs.model || "gpt-5.4-mini";
384
+ const visionReasoning = normalizeVisionReasoningForModel(visionModel, vs.reasoning) ?? "low";
315
385
  return jsonResponse({
316
386
  webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
317
387
  vision: {
318
- model: vs.model ?? "gpt-5.6-luna",
388
+ model: visionModel,
319
389
  backend: vs.backend,
390
+ reasoning: visionReasoning,
320
391
  maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
321
392
  },
322
393
  });
@@ -332,7 +403,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
332
403
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
333
404
  const body = raw as {
334
405
  webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown };
335
- vision?: { model?: unknown; backend?: unknown; maxDescriptionsPerTurn?: unknown };
406
+ vision?: { model?: unknown; backend?: unknown; reasoning?: unknown; maxDescriptionsPerTurn?: unknown };
336
407
  };
337
408
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
338
409
  && body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
@@ -348,6 +419,23 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
348
419
  || body.vision.maxDescriptionsPerTurn <= 0)) {
349
420
  return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400);
350
421
  }
422
+ if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) {
423
+ return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400);
424
+ }
425
+
426
+ let normalizedVisionReasoning: ReturnType<typeof normalizeVisionReasoningForModel>;
427
+ let visionReasoningTouched = false;
428
+ if (body.vision && (body.vision.model !== undefined || body.vision.reasoning !== undefined)) {
429
+ visionReasoningTouched = true;
430
+ const model = typeof body.vision.model === "string"
431
+ ? (body.vision.model === "" ? "gpt-5.4-mini" : body.vision.model)
432
+ : (config.visionSidecar?.model || "gpt-5.4-mini");
433
+ const sourceReasoning = body.vision.reasoning ?? config.visionSidecar?.reasoning;
434
+ normalizedVisionReasoning = sourceReasoning === undefined
435
+ ? undefined
436
+ : normalizeVisionReasoningForModel(model, sourceReasoning);
437
+ }
438
+
351
439
  if (body.webSearch) {
352
440
  config.webSearchSidecar = { ...config.webSearchSidecar };
353
441
  if (typeof body.webSearch.model === "string") {
@@ -373,16 +461,23 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
373
461
  if (typeof body.vision.maxDescriptionsPerTurn === "number") {
374
462
  config.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
375
463
  }
464
+ if (visionReasoningTouched) {
465
+ if (normalizedVisionReasoning === undefined) delete config.visionSidecar.reasoning;
466
+ else config.visionSidecar.reasoning = normalizedVisionReasoning;
467
+ }
376
468
  }
377
469
  saveConfigPreservingClaudeCode(config);
378
470
  const ws = config.webSearchSidecar ?? {};
379
471
  const vs = config.visionSidecar ?? {};
472
+ const visionModel = vs.model || "gpt-5.4-mini";
473
+ const visionReasoning = normalizeVisionReasoningForModel(visionModel, vs.reasoning) ?? "low";
380
474
  return jsonResponse({
381
475
  ok: true,
382
476
  webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
383
477
  vision: {
384
- model: vs.model ?? "gpt-5.6-luna",
478
+ model: visionModel,
385
479
  backend: vs.backend,
480
+ reasoning: visionReasoning,
386
481
  maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
387
482
  },
388
483
  });
@@ -30,7 +30,7 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
30
30
  return { values: raw as string[] };
31
31
  }
32
32
  import type { CatalogModel } from "../../codex/catalog";
33
- import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
33
+ import { catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
34
34
  import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
35
35
  import { getProviderLiveModelCount } from "../../codex/model-cache";
36
36
  import {
@@ -58,7 +58,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
58
58
  import { deriveProviderPresets } from "../../providers/derive";
59
59
  import { providerCodexAccountMode } from "../../providers/registry";
60
60
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
61
- import { COMBO_NAMESPACE, comboModelId, comboPublicModelId, preservesPhysicalComboProvider } from "../../combos";
61
+ import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
62
62
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
63
63
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
64
64
  import { clearThreadAccountMap } from "../../codex/routing";
@@ -255,17 +255,16 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
255
255
  if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
256
256
 
257
257
  const knownComboSelectors = new Set(
258
- Object.entries(config.combos ?? {}).flatMap(([id, combo]) => [
259
- comboModelId(id),
260
- comboPublicModelId(id, combo),
261
- ]),
258
+ Object.entries(config.combos ?? {}).flatMap(([id, combo]) => (
259
+ comboDisabledModelSelectors(id, combo)
260
+ )),
262
261
  );
263
262
  const targetComboSelectors = new Map<string, Set<string>>();
264
263
  if (isVirtualComboNamespace) {
265
264
  for (const target of targets) {
266
265
  const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
267
266
  if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
268
- targetComboSelectors.set(target.id, new Set([comboModelId(target.id), comboPublicModelId(target.id, combo)]));
267
+ targetComboSelectors.set(target.id, new Set(comboDisabledModelSelectors(target.id, combo)));
269
268
  }
270
269
  }
271
270
  const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
@@ -284,8 +283,12 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
284
283
  const nativeIds = provider === "openai"
285
284
  ? disabledNativeSlugs({ disabledModels: disabled })
286
285
  : new Set<string>();
286
+ const nativeAliasSlugs = provider === "openai"
287
+ ? configuredNativeAliasSlugs(config)
288
+ : new Set<string>();
287
289
  disabled = disabled.filter(stored => (
288
290
  knownComboSelectors.has(stored)
291
+ || nativeAliasSlugs.has(stored)
289
292
  || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
290
293
  ));
291
294
  }
@@ -9,9 +9,10 @@
9
9
  * Bodies are unchanged from their previous home; only `export` was added.
10
10
  */
11
11
  import type { CatalogModel } from "../../codex/catalog";
12
- import { catalogModelSlug, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
12
+ import { catalogModelSlug, nativeModelRows, nativeReasoningEfforts, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
13
13
  import type { ExportModel } from "../../clients/config-export";
14
14
  import { providerContextCap } from "../../providers/context-cap";
15
+ import { isVisionReasoningEffort } from "../../reasoning-effort";
15
16
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
16
17
  import type { OcxConfig } from "../../types";
17
18
  import { fetchAllModels } from "./shared";
@@ -47,6 +48,8 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
47
48
  namespaced: row.slug,
48
49
  disabled: row.disabled,
49
50
  native: true,
51
+ // The Codex catalog may advertise `ultra`, but the image describer accepts only low..max.
52
+ reasoningEfforts: nativeReasoningEfforts(row.slug).filter(isVisionReasoningEffort),
50
53
  ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
51
54
  }));
52
55
  const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
@@ -76,11 +79,12 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
76
79
  const namespaced = catalogModelSlug(m);
77
80
  if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
78
81
  const contextCap = providerContextCap(config, m.provider);
82
+ const nativeAlias = m.provider === "combo" && m.nativeAlias === true;
79
83
  return {
80
84
  ...m,
81
85
  namespaced,
82
86
  disabled: [...disabled].some(stored => (
83
- stored === namespaced || slugEquals(stored, m.provider, m.id)
87
+ (!nativeAlias && stored === namespaced) || slugEquals(stored, m.provider, m.id)
84
88
  )),
85
89
  ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
86
90
  };
@@ -210,6 +210,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
210
210
  await removeCredential(provider);
211
211
  reconcileLiveStateStores();
212
212
  clearLoginState(provider);
213
+ const { clearModelCache } = await import("../../codex/model-cache");
214
+ const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
215
+ clearModelCache(provider);
216
+ clearGatherRoutedModelsInflight();
213
217
  // Drop cached/last-good quota rows tied to the removed credential.
214
218
  const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
215
219
  clearProviderQuotaCache();
@@ -281,6 +285,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
281
285
  const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
282
286
  resetAnthropicRoutingForManualSelection(body.accountId);
283
287
  }
288
+ const { clearModelCache } = await import("../../codex/model-cache");
289
+ const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
290
+ clearModelCache(provider);
291
+ clearGatherRoutedModelsInflight();
284
292
  const { clearProviderQuotaCache } = await import("../../providers/quota");
285
293
  clearProviderQuotaCache();
286
294
  return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
@@ -404,6 +412,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
404
412
  clearAnthropicSessionAffinityForAccount(id);
405
413
  }
406
414
  if (!getAccountSet(provider)) clearLoginState(provider);
415
+ const { clearModelCache } = await import("../../codex/model-cache");
416
+ const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
417
+ clearModelCache(provider);
418
+ clearGatherRoutedModelsInflight();
407
419
  const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
408
420
  clearProviderQuotaCache();
409
421
  clearAccountQuotaCache(provider);