@yansigit/opencodex 2.36.1-dev.20260829.47 → 2.36.1-dev.20260829.50

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 (112) hide show
  1. package/gui/dist/assets/{ApiKeys-DAuYQsGJ.js → ApiKeys-BLPOvK4z.js} +1 -1
  2. package/gui/dist/assets/{Claude-bN_80RT8.js → Claude-BbHv3EU7.js} +1 -1
  3. package/gui/dist/assets/{CodexSet-D3dWX_ay.js → CodexSet-hgu5hES-.js} +1 -1
  4. package/gui/dist/assets/{FileIntegrationPage-BGyzkPs2.js → FileIntegrationPage-RCmDvvsx.js} +1 -1
  5. package/gui/dist/assets/{Grok-CuaKd1TT.js → Grok-DH-ggRqA.js} +1 -1
  6. package/gui/dist/assets/{Integrations-C7NGQoxR.js → Integrations-D6m5E2-T.js} +2 -2
  7. package/gui/dist/assets/{IntegrationsOverview-DjVorYO5.js → IntegrationsOverview-C1rbnXV7.js} +1 -1
  8. package/gui/dist/assets/{Logs-B21a-9xe.js → Logs-Bqb0vYRk.js} +1 -1
  9. package/gui/dist/assets/{Models-CN7Q3kuK.js → Models-gMnzHf0c.js} +1 -1
  10. package/gui/dist/assets/{NumberStepper-BWg90tEs.js → NumberStepper-awaoZNlj.js} +1 -1
  11. package/gui/dist/assets/{Providers-CP_i-7LS.js → Providers-Bo6wuQMa.js} +1 -1
  12. package/gui/dist/assets/{RestoreDialog-3M0NqCiD.js → RestoreDialog-DX7SGWIk.js} +1 -1
  13. package/gui/dist/assets/{Startup-BVPFoP2s.js → Startup-B1htiLU9.js} +1 -1
  14. package/gui/dist/assets/{Storage-BdsG9ypz.js → Storage-B3MfosmS.js} +1 -1
  15. package/gui/dist/assets/{Subagents-CguR_bGQ.js → Subagents-L6Q2V9PZ.js} +1 -1
  16. package/gui/dist/assets/{Usage-C0kKi9_H.js → Usage-xbZnkDoj.js} +1 -1
  17. package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
  18. package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
  19. package/gui/dist/assets/{data-surface-BCycbObp.js → data-surface-BbBMp0Wj.js} +1 -1
  20. package/gui/dist/assets/{index-CU1jE0st.js → index-JEC_6CUW.js} +3 -3
  21. package/gui/dist/assets/{model-display-D2elXkuE.js → model-display-BvLWy9D5.js} +1 -1
  22. package/gui/dist/assets/{provider-payload-DfkXWE7v.js → provider-payload-BRfSsonS.js} +1 -1
  23. package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
  24. package/gui/dist/assets/shared-DI18-uF7.js +69 -0
  25. package/gui/dist/index.html +2 -2
  26. package/package.json +3 -1
  27. package/src/adapters/base.ts +26 -0
  28. package/src/adapters/cursor/catalog.ts +541 -0
  29. package/src/adapters/cursor/cursor-errors.ts +15 -0
  30. package/src/adapters/cursor/discovery.ts +34 -41
  31. package/src/adapters/cursor/envelope-echo.ts +128 -0
  32. package/src/adapters/cursor/request-builder.ts +19 -12
  33. package/src/adapters/cursor/tool-definitions.ts +2 -1
  34. package/src/adapters/cursor/tool-result-normalize.ts +23 -31
  35. package/src/adapters/cursor.ts +21 -2
  36. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  37. package/src/adapters/google-antigravity-replay.ts +71 -2
  38. package/src/adapters/google.ts +19 -4
  39. package/src/adapters/kiro-constants.ts +12 -0
  40. package/src/adapters/kiro.ts +128 -11
  41. package/src/adapters/openai-chat.ts +16 -2
  42. package/src/adapters/openai-responses.ts +15 -2
  43. package/src/adapters/tool-catalog-nudge.ts +2 -1
  44. package/src/adapters/xai-web-search.ts +10 -14
  45. package/src/claude/outbound.ts +14 -3
  46. package/src/cli/access.ts +46 -3
  47. package/src/cli/account-api.ts +93 -16
  48. package/src/cli/account-extended.ts +262 -36
  49. package/src/cli/account-main.ts +12 -12
  50. package/src/cli/account.ts +40 -10
  51. package/src/cli/agent.ts +8 -1
  52. package/src/cli/capabilities-command.ts +94 -0
  53. package/src/cli/capabilities.ts +535 -0
  54. package/src/cli/claude-desktop.ts +31 -11
  55. package/src/cli/config-command.ts +7 -3
  56. package/src/cli/dispatch.ts +195 -27
  57. package/src/cli/doctor.ts +100 -1
  58. package/src/cli/help.ts +11 -2
  59. package/src/cli/index.ts +19 -3
  60. package/src/cli/init.ts +2 -2
  61. package/src/cli/inspect.ts +230 -0
  62. package/src/cli/observe.ts +11 -3
  63. package/src/cli/provider.ts +58 -74
  64. package/src/cli/registry.ts +34 -2
  65. package/src/cli/runtime-api.ts +51 -7
  66. package/src/cli/status.ts +16 -0
  67. package/src/cli/storage.ts +234 -0
  68. package/src/cli/system-command.ts +16 -0
  69. package/src/cli/usage-report.ts +52 -2
  70. package/src/cli/version-skew.ts +46 -0
  71. package/src/codex/account-label.ts +21 -0
  72. package/src/codex/catalog/provider-fetch.ts +4 -0
  73. package/src/codex/convergence.ts +110 -15
  74. package/src/codex/transition-state.ts +12 -3
  75. package/src/compatibility/openai-responses.ts +9 -1
  76. package/src/config.ts +209 -11
  77. package/src/generated/compatibility-version.json +115 -75
  78. package/src/integrations/ownership-policy.ts +24 -5
  79. package/src/integrations/ownership.ts +36 -2
  80. package/src/integrations/state.ts +40 -7
  81. package/src/integrations/writer.ts +21 -3
  82. package/src/lib/admin-secrets.ts +24 -0
  83. package/src/lib/errors.ts +25 -1
  84. package/src/lib/service-secrets.ts +15 -0
  85. package/src/oauth/index.ts +143 -82
  86. package/src/oauth/login-cli.ts +53 -11
  87. package/src/oauth/store.ts +16 -6
  88. package/src/providers/alibaba-region-startup.ts +31 -11
  89. package/src/providers/api-keys.ts +58 -34
  90. package/src/providers/key-failover.ts +59 -46
  91. package/src/providers/label.ts +34 -1
  92. package/src/providers/model-rename-startup.ts +25 -7
  93. package/src/providers/openai-tier-startup.ts +38 -14
  94. package/src/responses/turn-termination.ts +107 -0
  95. package/src/server/index.ts +10 -0
  96. package/src/server/management/context.ts +10 -3
  97. package/src/server/management/logs-usage-routes.ts +24 -43
  98. package/src/server/management/model-routes.ts +284 -165
  99. package/src/server/management/oauth-account-routes.ts +93 -15
  100. package/src/server/management/provider-routes.ts +200 -87
  101. package/src/server/management/route-registry.ts +317 -0
  102. package/src/server/proxy-liveness.ts +27 -4
  103. package/src/server/request-log.ts +37 -17
  104. package/src/server/responses/core.ts +80 -4
  105. package/src/server/responses/policy-fallback.ts +1 -1
  106. package/src/service.ts +34 -0
  107. package/src/storage/policy-input.ts +166 -0
  108. package/src/storage/policy-job.ts +14 -4
  109. package/src/storage/policy.ts +115 -221
  110. package/src/usage/log.ts +32 -6
  111. package/src/usage/summary.ts +22 -34
  112. package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
@@ -147,7 +147,7 @@ import type {
147
147
 
148
148
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
149
149
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
150
- import { saveManagementConfig, type ManagementContext } from "./context";
150
+ import { mutateManagementConfig, saveManagementConfig, type ManagementContext } from "./context";
151
151
  import { listManagementModelRows, loadExportModels } from "./model-rows";
152
152
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
153
153
  import {
@@ -170,6 +170,123 @@ function summarizeExportedModels(client: ExportClientId, document: unknown): { m
170
170
  return EXPORT_CLIENTS[client].summarize(document);
171
171
  }
172
172
 
173
+ type ModelMutationValue =
174
+ | { config: OcxConfig; alias?: string | null; aliases?: Record<string, string>; selected?: string[] }
175
+ | { error: string; conflicts?: Array<{ alias: string; heldBy: string }>; status?: number };
176
+
177
+ function providerDiscoveryFingerprint(provider: OcxProviderConfig): string {
178
+ return JSON.stringify(provider);
179
+ }
180
+
181
+ function adoptCommittedConfig(target: OcxConfig, source: OcxConfig): void {
182
+ for (const key of Object.keys(target)) delete (target as unknown as Record<string, unknown>)[key];
183
+ Object.assign(target, structuredClone(source));
184
+ }
185
+
186
+ function unavailableMutationResponse(reason: "missing" | "invalid" | "conflict", req: Request, config: OcxConfig): Response {
187
+ const message = reason === "conflict"
188
+ ? "config changed while applying this update; retry"
189
+ : `config is ${reason}`;
190
+ return jsonResponse({ error: message }, reason === "conflict" ? 409 : 500, req, config);
191
+ }
192
+
193
+ function applyModelVisibility(
194
+ config: OcxConfig,
195
+ scope: "models" | "provider",
196
+ provider: string,
197
+ enabled: boolean,
198
+ rawTargets: unknown[],
199
+ ): { ok: true; disabled: string[] } | { ok: false; error: string } {
200
+ const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
201
+ const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
202
+ if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
203
+ return { ok: false, error: "unknown model visibility provider" };
204
+ }
205
+ const accountNativeQualified = shouldIncludeAccountBoundNativeOpenAi(config)
206
+ ? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
207
+ slugs.filter(slug => !nativeModelRows(config).some(row => row.slug === slug)).map(slug => `${selector}/${slug}`))
208
+ : [];
209
+ const supportedNative = new Set([
210
+ ...nativeModelRows(config).map(row => row.slug),
211
+ ...accountNativeQualified,
212
+ ]);
213
+ const targets: Array<{ id: string; native: boolean }> = [];
214
+ const seen = new Set<string>();
215
+ for (const value of rawTargets) {
216
+ if (!isPlainRecord(value) || typeof value.id !== "string" || (value.native !== undefined && typeof value.native !== "boolean")) {
217
+ return { ok: false, error: "invalid model visibility target" };
218
+ }
219
+ const id = value.id.trim();
220
+ const native = value.native === true;
221
+ if (!id || (provider === "openai") !== native || (native && !supportedNative.has(id))) {
222
+ return { ok: false, error: "invalid model visibility target" };
223
+ }
224
+ const key = `${native ? "native" : "routed"}:${id}`;
225
+ if (!seen.has(key)) {
226
+ seen.add(key);
227
+ targets.push({ id, native });
228
+ }
229
+ }
230
+ if (targets.length === 0) return { ok: false, error: "model visibility targets required" };
231
+
232
+ const knownComboSelectors = new Set(
233
+ Object.entries(config.combos ?? {}).flatMap(([id, combo]) => comboDisabledModelSelectors(id, combo)),
234
+ );
235
+ const targetComboSelectors = new Map<string, Set<string>>();
236
+ if (isVirtualComboNamespace) {
237
+ for (const target of targets) {
238
+ const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
239
+ if (!combo) return { ok: false, error: "invalid model visibility target" };
240
+ targetComboSelectors.set(target.id, new Set(comboDisabledModelSelectors(target.id, combo)));
241
+ }
242
+ }
243
+ const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
244
+ ? stored === target.id
245
+ : isVirtualComboNamespace
246
+ ? targetComboSelectors.get(target.id)!.has(stored)
247
+ : slugEquals(stored, provider, target.id);
248
+
249
+ let disabled = [...new Set(config.disabledModels ?? [])];
250
+ if (enabled) {
251
+ if (scope === "provider") {
252
+ if (providerConfig && !isVirtualComboNamespace) delete providerConfig.selectedModels;
253
+ if (isVirtualComboNamespace) {
254
+ disabled = disabled.filter(stored => !knownComboSelectors.has(stored));
255
+ } else {
256
+ const nativeIds = provider === "openai" ? disabledNativeSlugs({ disabledModels: disabled }) : new Set<string>();
257
+ const accountNativeIds = provider === "openai" ? new Set(accountNativeQualified) : new Set<string>();
258
+ const nativeAliasSlugs = provider === "openai" ? configuredNativeAliasSlugs(config) : new Set<string>();
259
+ disabled = disabled.filter(stored => (
260
+ knownComboSelectors.has(stored)
261
+ || nativeAliasSlugs.has(stored)
262
+ || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored) && !accountNativeIds.has(stored))
263
+ ));
264
+ }
265
+ } else {
266
+ if (!isVirtualComboNamespace && providerConfig?.selectedModels && providerConfig.selectedModels.length > 0) {
267
+ const additions = targets.filter(target => !target.native).map(target => target.id);
268
+ providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
269
+ }
270
+ disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
271
+ const arrivals = config.modelDiscovery?.recentArrivals?.[provider];
272
+ if (arrivals) config.modelDiscovery!.recentArrivals![provider] = arrivals.filter(row => (
273
+ !targets.some(target => !target.native && target.id === row.id)
274
+ ));
275
+ }
276
+ } else {
277
+ for (const target of targets) {
278
+ const canonical = target.native
279
+ ? target.id
280
+ : isVirtualComboNamespace
281
+ ? comboModelId(target.id)
282
+ : routedSlug(provider, target.id);
283
+ if (!disabled.some(stored => matchesTarget(stored, target))) disabled.push(canonical);
284
+ }
285
+ }
286
+ config.disabledModels = disabled;
287
+ return { ok: true, disabled };
288
+ }
289
+
173
290
  export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
174
291
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
175
292
  // A handler persists the exact config object passed in. Production defaults to
@@ -202,16 +319,25 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
202
319
  let body: { policy?: unknown; provider?: unknown };
203
320
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
204
321
  if (body.policy !== "on" && body.policy !== "off") return jsonResponse({ error: "policy must be on or off" }, 400);
322
+ const policy = body.policy;
205
323
  const provider = typeof body.provider === "string" && body.provider.trim() ? body.provider.trim() : null;
206
324
  let baselineBootstrapped = false;
207
325
  if (provider) {
208
326
  if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "unknown provider" }, 404);
209
- config.providers[provider].newModelPolicy = body.policy;
327
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
328
+ const target = fresh.providers[provider];
329
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
330
+ target.newModelPolicy = policy;
331
+ return { changed: true, value: { config: structuredClone(fresh) } };
332
+ });
333
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
334
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 404);
335
+ adoptCommittedConfig(config, outcome.value.config);
210
336
  } else {
211
337
  const wasAbsent = config.modelDiscovery?.newModelPolicy === undefined;
212
338
  config.modelDiscovery ??= {};
213
- config.modelDiscovery.newModelPolicy = body.policy;
214
- if (body.policy === "off" && wasAbsent) {
339
+ config.modelDiscovery.newModelPolicy = policy;
340
+ if (policy === "off" && wasAbsent) {
215
341
  const models = await fetchAllModels(config);
216
342
  const known = config.modelDiscovery.knownModels ??= {};
217
343
  const at = new Date().toISOString();
@@ -221,9 +347,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
221
347
  baselineBootstrapped = true;
222
348
  }
223
349
  }
224
- persistConfig(config);
350
+ if (!provider) persistConfig(config);
225
351
  const catalogRefresh = await convergeCodexCatalog();
226
- return jsonResponse({ ok: true, policy: body.policy, provider, ...(baselineBootstrapped ? { baselineBootstrapped } : {}), catalogRefresh });
352
+ return jsonResponse({ ok: true, policy, provider, ...(baselineBootstrapped ? { baselineBootstrapped } : {}), catalogRefresh });
227
353
  }
228
354
 
229
355
  if (url.pathname === "/api/model-discovery/acknowledge" && req.method === "POST") {
@@ -275,16 +401,25 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
275
401
  if (!isPlainRecord(raw) || (raw.alias !== null && typeof raw.alias !== "string")) return jsonResponse({ error: "alias must be a string or null" }, 400, req, config);
276
402
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : null;
277
403
  if (alias && !isValidProviderName(alias)) return jsonResponse({ error: "invalid provider alias" }, 400, req, config);
278
- const lower = alias?.toLowerCase();
279
- const collision = lower && Object.entries(config.providers).find(([other, p]) =>
280
- other !== name && (other.toLowerCase() === lower || p.alias?.toLowerCase() === lower));
281
- const comboCollision = lower && Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower);
282
- const accountCollision = lower && Object.keys(config.codexAccountNamespaces ?? {}).find(value => value.toLowerCase() === lower);
283
- if (collision || comboCollision || accountCollision) return jsonResponse({ error: `alias conflicts with '${collision?.[0] ?? comboCollision?.[0] ?? accountCollision}'` }, 409, req, config);
284
- if (alias) provider.alias = alias; else delete provider.alias;
285
- persistConfig(config);
404
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
405
+ const provider = fresh.providers[name];
406
+ if (!provider) return { changed: false, value: { error: `provider '${name}' not found`, status: 404 } };
407
+ const lower = alias?.toLowerCase();
408
+ const collision = lower && Object.entries(fresh.providers).find(([other, p]) =>
409
+ other !== name && (other.toLowerCase() === lower || p.alias?.toLowerCase() === lower));
410
+ const comboCollision = lower && Object.entries(fresh.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower);
411
+ const accountCollision = lower && Object.keys(fresh.codexAccountNamespaces ?? {}).find(value => value.toLowerCase() === lower);
412
+ if (collision || comboCollision || accountCollision) {
413
+ return { changed: false, value: { error: `alias conflicts with '${collision?.[0] ?? comboCollision?.[0] ?? accountCollision}'`, status: 409 } };
414
+ }
415
+ if (alias) provider.alias = alias; else delete provider.alias;
416
+ return { changed: true, value: { config: structuredClone(fresh), alias } };
417
+ });
418
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
419
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, outcome.value.status ?? 400, req, config);
420
+ adoptCommittedConfig(config, outcome.value.config);
286
421
  const catalogRefresh = await convergeCodexCatalog();
287
- return jsonResponse({ ok: true, provider: name, alias, catalogRefresh });
422
+ return jsonResponse({ ok: true, provider: name, alias: outcome.value.alias ?? null, catalogRefresh });
288
423
  }
289
424
 
290
425
  const modelAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-aliases$/);
@@ -296,36 +431,58 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
296
431
  let raw: unknown;
297
432
  try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
298
433
  if (!isPlainRecord(raw) || (raw.set !== undefined && !isPlainRecord(raw.set)) || (raw.remove !== undefined && !Array.isArray(raw.remove))) return jsonResponse({ error: "invalid model alias update" }, 400, req, config);
299
- const next = { ...(provider.modelAliases ?? {}) };
300
- for (const id of (raw.remove ?? []) as unknown[]) if (typeof id === "string") delete next[id];
301
- const conflicts: Array<{ alias: string; heldBy: string }> = [];
302
- const known = knownModelIdsForProvider(name, provider, config);
303
- for (const [id, value] of Object.entries((raw.set ?? {}) as Record<string, unknown>)) {
304
- if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value)) return jsonResponse({ error: `invalid model alias for '${id}'` }, 400, req, config);
305
- const lower = value.toLowerCase();
306
- const heldBy = Object.entries(next).find(([other, alias]) => other !== id && alias.toLowerCase() === lower)?.[0]
307
- ?? known.find(native => native.toLowerCase() === lower)
308
- ?? Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower)?.[0];
309
- if (heldBy || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) conflicts.push({ alias: value, heldBy: heldBy ?? "native OpenAI family" });
310
- else next[id] = value;
311
- }
312
- if (conflicts.length) return jsonResponse({ error: "model alias collision", conflicts }, 409, req, config);
313
- provider.modelAliases = next;
314
- persistConfig(config);
434
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
435
+ const provider = fresh.providers[name];
436
+ if (!provider) return { changed: false, value: { error: `provider '${name}' not found` } };
437
+ const next = { ...(provider.modelAliases ?? {}) };
438
+ for (const id of (raw.remove ?? []) as unknown[]) if (typeof id === "string") delete next[id];
439
+ const conflicts: Array<{ alias: string; heldBy: string }> = [];
440
+ const known = knownModelIdsForProvider(name, provider, fresh);
441
+ for (const [id, value] of Object.entries((raw.set ?? {}) as Record<string, unknown>)) {
442
+ if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value)) return { changed: false, value: { error: `invalid model alias for '${id}'`, status: 400 } };
443
+ const lower = value.toLowerCase();
444
+ const heldBy = Object.entries(next).find(([other, alias]) => other !== id && alias.toLowerCase() === lower)?.[0]
445
+ ?? known.find(native => native.toLowerCase() === lower)
446
+ ?? Object.entries(fresh.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower)?.[0];
447
+ if (heldBy || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) conflicts.push({ alias: value, heldBy: heldBy ?? "native OpenAI family" });
448
+ else next[id] = value;
449
+ }
450
+ if (conflicts.length) return { changed: false, value: { error: "model alias collision", conflicts, status: 409 } };
451
+ provider.modelAliases = next;
452
+ return { changed: true, value: { config: structuredClone(fresh), aliases: next } };
453
+ });
454
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
455
+ if ("error" in outcome.value) return jsonResponse({
456
+ error: outcome.value.error,
457
+ ...(outcome.value.conflicts ? { conflicts: outcome.value.conflicts } : {}),
458
+ }, outcome.value.status ?? 404, req, config);
459
+ adoptCommittedConfig(config, outcome.value.config);
315
460
  const catalogRefresh = await convergeCodexCatalog();
316
- return jsonResponse({ ok: true, aliases: next, catalogRefresh });
461
+ return jsonResponse({ ok: true, aliases: outcome.value.aliases ?? {}, catalogRefresh });
317
462
  }
318
463
 
319
464
  if (url.pathname === "/api/default-aliases" && req.method === "PUT") {
320
465
  let raw: unknown;
321
466
  try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
322
467
  if (!isPlainRecord(raw) || typeof raw.enabled !== "boolean" || (raw.provider !== undefined && typeof raw.provider !== "string")) return jsonResponse({ error: "enabled must be boolean" }, 400, req, config);
468
+ const enabled = raw.enabled;
323
469
  if (typeof raw.provider === "string") {
324
- const provider = config.providers[raw.provider];
325
- if (!provider) return jsonResponse({ error: `provider '${raw.provider}' not found` }, 404, req, config);
326
- provider.defaultAliases = raw.enabled;
327
- } else config.defaultModelAliases = raw.enabled;
328
- persistConfig(config);
470
+ const providerName = raw.provider;
471
+ const provider = config.providers[providerName];
472
+ if (!provider) return jsonResponse({ error: `provider '${providerName}' not found` }, 404, req, config);
473
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
474
+ const target = fresh.providers[providerName];
475
+ if (!target) return { changed: false, value: { error: `provider '${providerName}' not found` } };
476
+ target.defaultAliases = enabled;
477
+ return { changed: true, value: { config: structuredClone(fresh) } };
478
+ });
479
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
480
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 404, req, config);
481
+ adoptCommittedConfig(config, outcome.value.config);
482
+ } else {
483
+ config.defaultModelAliases = enabled;
484
+ persistConfig(config);
485
+ }
329
486
  const catalogRefresh = await convergeCodexCatalog();
330
487
  return jsonResponse({ ok: true, catalogRefresh });
331
488
  }
@@ -452,102 +609,20 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
452
609
  return jsonResponse({ error: "invalid model visibility request" }, 400);
453
610
  }
454
611
 
455
- const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
456
- const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
457
- if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
458
- return jsonResponse({ error: "unknown model visibility provider" }, 400);
459
- }
460
- const accountNativeQualified = shouldIncludeAccountBoundNativeOpenAi(config)
461
- ? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
462
- slugs.filter(slug => !nativeModelRows(config).some(row => row.slug === slug)).map(slug => `${selector}/${slug}`))
463
- : [];
464
- const supportedNative = new Set([
465
- ...nativeModelRows(config).map(row => row.slug),
466
- ...accountNativeQualified,
467
- ]);
468
- const targets: Array<{ id: string; native: boolean }> = [];
469
- const seen = new Set<string>();
470
- for (const value of body.targets) {
471
- if (!isPlainRecord(value) || typeof value.id !== "string" || (value.native !== undefined && typeof value.native !== "boolean")) {
472
- return jsonResponse({ error: "invalid model visibility target" }, 400);
473
- }
474
- const id = value.id.trim();
475
- const native = value.native === true;
476
- if (!id || (provider === "openai") !== native || (native && !supportedNative.has(id))) {
477
- return jsonResponse({ error: "invalid model visibility target" }, 400);
478
- }
479
- const key = `${native ? "native" : "routed"}:${id}`;
480
- if (!seen.has(key)) {
481
- seen.add(key);
482
- targets.push({ id, native });
483
- }
484
- }
485
- if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
486
-
487
- const knownComboSelectors = new Set(
488
- Object.entries(config.combos ?? {}).flatMap(([id, combo]) => (
489
- comboDisabledModelSelectors(id, combo)
490
- )),
491
- );
492
- const targetComboSelectors = new Map<string, Set<string>>();
493
- if (isVirtualComboNamespace) {
494
- for (const target of targets) {
495
- const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
496
- if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
497
- targetComboSelectors.set(target.id, new Set(comboDisabledModelSelectors(target.id, combo)));
498
- }
499
- }
500
- const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
501
- ? stored === target.id
502
- : isVirtualComboNamespace
503
- ? targetComboSelectors.get(target.id)!.has(stored)
504
- : slugEquals(stored, provider, target.id);
505
-
506
- let disabled = [...new Set(config.disabledModels ?? [])];
507
- if (body.enabled) {
508
- if (scope === "provider") {
509
- if (providerConfig && !isVirtualComboNamespace) delete providerConfig.selectedModels;
510
- if (isVirtualComboNamespace) {
511
- disabled = disabled.filter(stored => !knownComboSelectors.has(stored));
512
- } else {
513
- const nativeIds = provider === "openai"
514
- ? disabledNativeSlugs({ disabledModels: disabled })
515
- : new Set<string>();
516
- const accountNativeIds = provider === "openai" ? new Set(accountNativeQualified) : new Set<string>();
517
- const nativeAliasSlugs = provider === "openai"
518
- ? configuredNativeAliasSlugs(config)
519
- : new Set<string>();
520
- disabled = disabled.filter(stored => (
521
- knownComboSelectors.has(stored)
522
- || nativeAliasSlugs.has(stored)
523
- || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored) && !accountNativeIds.has(stored))
524
- ));
525
- }
526
- } else {
527
- if (!isVirtualComboNamespace && providerConfig?.selectedModels && providerConfig.selectedModels.length > 0) {
528
- const additions = targets.filter(target => !target.native).map(target => target.id);
529
- providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
530
- }
531
- disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
532
- const arrivals = config.modelDiscovery?.recentArrivals?.[provider];
533
- if (arrivals) config.modelDiscovery!.recentArrivals![provider] = arrivals.filter(row => (
534
- !targets.some(target => !target.native && target.id === row.id)
535
- ));
536
- }
537
- } else {
538
- for (const target of targets) {
539
- const canonical = target.native
540
- ? target.id
541
- : isVirtualComboNamespace
542
- ? comboModelId(target.id)
543
- : routedSlug(provider, target.id);
544
- const alreadyDisabled = disabled.some(stored => matchesTarget(stored, target));
545
- if (!alreadyDisabled) disabled.push(canonical);
546
- }
547
- }
548
-
549
- config.disabledModels = disabled;
550
- persistConfig(config);
612
+ const outcome = mutateManagementConfig<
613
+ { config: OcxConfig; disabled: string[] } | { error: string }
614
+ >(deps, fresh => {
615
+ const applied = applyModelVisibility(fresh, scope, provider, body.enabled as boolean, body.targets as unknown[]);
616
+ if (!applied.ok) return { changed: false, value: { error: applied.error } };
617
+ return {
618
+ changed: true,
619
+ value: { config: structuredClone(fresh), disabled: applied.disabled },
620
+ };
621
+ });
622
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
623
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 400);
624
+ adoptCommittedConfig(config, outcome.value.config);
625
+ const disabled = outcome.value.disabled;
551
626
  const catalogRefresh = await convergeCodexCatalog();
552
627
  return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled, catalogRefresh });
553
628
  }
@@ -749,22 +824,37 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
749
824
  }
750
825
  const target = config.providers[provider];
751
826
  if (mode === "all") {
752
- // Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
753
- delete target.selectedModels;
754
- delete target.modelPreset;
755
- persistConfig(config);
827
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
828
+ const target = fresh.providers[provider];
829
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
830
+ // Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
831
+ delete target.selectedModels;
832
+ delete target.modelPreset;
833
+ return { changed: true, value: { config: structuredClone(fresh) } };
834
+ });
835
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
836
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 404);
837
+ adoptCommittedConfig(config, outcome.value.config);
756
838
  return jsonResponse({ ok: true, provider, mode, selected: [], catalogRefresh: await convergeCodexCatalog() });
757
839
  }
758
840
  if (mode === "custom") {
759
- // Keep whatever is selected; only the marker changes, so a user can pin their edits
760
- // without the proxy re-materializing over them.
761
- target.modelPreset = { ...(target.modelPreset ?? {}), mode: "custom" };
762
- persistConfig(config);
763
- return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
841
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
842
+ const target = fresh.providers[provider];
843
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
844
+ // Keep whatever is selected; only the marker changes, so a user can pin their edits
845
+ // without the proxy re-materializing over them.
846
+ target.modelPreset = { ...(target.modelPreset ?? {}), mode: "custom" };
847
+ return { changed: true, value: { config: structuredClone(fresh), selected: [...(target.selectedModels ?? [])] } };
848
+ });
849
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
850
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 404);
851
+ adoptCommittedConfig(config, outcome.value.config);
852
+ return jsonResponse({ ok: true, provider, mode, selected: outcome.value.selected });
764
853
  }
765
854
  if (!hasModelPreset(provider)) {
766
855
  return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
767
856
  }
857
+ const admittedProviderFingerprint = providerDiscoveryFingerprint(target);
768
858
  const models = await fetchAllModels(config);
769
859
  const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
770
860
  const presetIds = materializeModelPreset(provider, catalogIds);
@@ -773,28 +863,50 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
773
863
  // NEVER write an empty allowlist from a preset: empty means ALL, so it would silently
774
864
  // un-curate instead of curating. Keep the previous selection and record the fallback so
775
865
  // the next convergence can retry.
776
- target.modelPreset = {
777
- mode: "all",
778
- appliedVersion: preset.version,
779
- appliedAt: new Date().toISOString(),
780
- fallback: "preset-empty",
781
- };
782
- persistConfig(config);
866
+ const appliedAt = new Date().toISOString();
867
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
868
+ const target = fresh.providers[provider];
869
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
870
+ if (providerDiscoveryFingerprint(target) !== admittedProviderFingerprint) {
871
+ return { changed: false, value: { error: "provider changed during model discovery; retry", status: 409 } };
872
+ }
873
+ target.modelPreset = {
874
+ mode: "all",
875
+ appliedVersion: preset.version,
876
+ appliedAt,
877
+ fallback: "preset-empty",
878
+ };
879
+ return { changed: true, value: { config: structuredClone(fresh), selected: [...(target.selectedModels ?? [])] } };
880
+ });
881
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
882
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, outcome.value.status ?? 404);
883
+ adoptCommittedConfig(config, outcome.value.config);
783
884
  return jsonResponse({
784
885
  ok: true,
785
886
  provider,
786
887
  mode: "all",
787
888
  fallback: "preset-empty",
788
- selected: [...(target.selectedModels ?? [])],
889
+ selected: outcome.value.selected,
789
890
  });
790
891
  }
791
- target.selectedModels = presetIds;
792
- target.modelPreset = {
793
- mode: "preset",
794
- appliedVersion: preset.version,
795
- appliedAt: new Date().toISOString(),
796
- };
797
- persistConfig(config);
892
+ const appliedAt = new Date().toISOString();
893
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
894
+ const target = fresh.providers[provider];
895
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
896
+ if (providerDiscoveryFingerprint(target) !== admittedProviderFingerprint) {
897
+ return { changed: false, value: { error: "provider changed during model discovery; retry", status: 409 } };
898
+ }
899
+ target.selectedModels = presetIds;
900
+ target.modelPreset = {
901
+ mode: "preset",
902
+ appliedVersion: preset.version,
903
+ appliedAt,
904
+ };
905
+ return { changed: true, value: { config: structuredClone(fresh) } };
906
+ });
907
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
908
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, outcome.value.status ?? 404);
909
+ adoptCommittedConfig(config, outcome.value.config);
798
910
  return jsonResponse({
799
911
  ok: true,
800
912
  provider,
@@ -814,14 +926,21 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
814
926
  const models = Array.isArray(body.models)
815
927
  ? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
816
928
  : [];
817
- // Empty list clears the allowlist (provider reverts to exposing all models).
818
- if (models.length > 0) config.providers[provider].selectedModels = models;
819
- else delete config.providers[provider].selectedModels;
820
- // Divergence is detected at the WRITE path, not by diffing (#2465): a user edit while the
821
- // provider is in preset mode makes the selection theirs, and the proxy must never
822
- // re-materialize over it afterwards.
823
- markModelPresetDiverged(config.providers[provider]);
824
- persistConfig(config);
929
+ const outcome = mutateManagementConfig<ModelMutationValue>(deps, fresh => {
930
+ const target = fresh.providers[provider];
931
+ if (!target) return { changed: false, value: { error: "unknown provider" } };
932
+ // Empty list clears the allowlist (provider reverts to exposing all models).
933
+ if (models.length > 0) target.selectedModels = models;
934
+ else delete target.selectedModels;
935
+ // Divergence is detected at the WRITE path, not by diffing (#2465): a user edit while the
936
+ // provider is in preset mode makes the selection theirs, and the proxy must never
937
+ // re-materialize over it afterwards.
938
+ markModelPresetDiverged(target);
939
+ return { changed: true, value: { config: structuredClone(fresh) } };
940
+ });
941
+ if (outcome.status === "unavailable") return unavailableMutationResponse(outcome.reason, req, config);
942
+ if ("error" in outcome.value) return jsonResponse({ error: outcome.value.error }, 404);
943
+ adoptCommittedConfig(config, outcome.value.config);
825
944
  const catalogRefresh = await convergeCodexCatalog();
826
945
  return jsonResponse({ ok: true, provider, selected: models, catalogRefresh });
827
946
  }