@yansigit/opencodex 2.36.1-dev.20260829.42 → 2.36.1-dev.20260829.48
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/gui/dist/assets/{ApiKeys-C_cz6Rww.js → ApiKeys-LOOFiZfv.js} +1 -1
- package/gui/dist/assets/{Claude-ofGguLfF.js → Claude-BwKzpXe3.js} +1 -1
- package/gui/dist/assets/{CodexSet-fhGiVwxe.js → CodexSet-B5qi9KeE.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B-dI69Kn.js → FileIntegrationPage-B0sUef6W.js} +1 -1
- package/gui/dist/assets/{Grok-CgkBu_Sm.js → Grok-bsb4n-f4.js} +1 -1
- package/gui/dist/assets/{Integrations-CTWMNzkD.js → Integrations-CSMwFDvM.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-BKZ0dZnj.js → IntegrationsOverview-CTORdKJA.js} +1 -1
- package/gui/dist/assets/{Logs-6r90C0x4.js → Logs-BWYbfELf.js} +1 -1
- package/gui/dist/assets/{Models-CpcS1zZr.js → Models-CexEtdT1.js} +1 -1
- package/gui/dist/assets/{NumberStepper-C70ttiJY.js → NumberStepper-BapSFQnW.js} +1 -1
- package/gui/dist/assets/{Providers-BROTzuSF.js → Providers-BwUFeAgA.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-DR5vgFXB.js → RestoreDialog-CD8piq90.js} +1 -1
- package/gui/dist/assets/{Startup-BSoZDYbS.js → Startup-BgX731C2.js} +1 -1
- package/gui/dist/assets/{Storage-CUvBwbeB.js → Storage-Bgq7HphP.js} +1 -1
- package/gui/dist/assets/{Subagents-CYu3_DBt.js → Subagents-ChxItbeF.js} +1 -1
- package/gui/dist/assets/{Usage-BV36sd7V.js → Usage-BkylzP50.js} +1 -1
- package/gui/dist/assets/{data-surface-DhcMe0J_.js → data-surface-pCXChiBf.js} +1 -1
- package/gui/dist/assets/{index-DnuNKc0D.js → index-CNopOid3.js} +2 -2
- package/gui/dist/assets/{model-display-BPrQ7F3P.js → model-display-CiUpg8T9.js} +1 -1
- package/gui/dist/assets/{provider-payload-CMb37zWF.js → provider-payload-b2jlS-On.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli/agent.ts +19 -2
- package/src/cli/config-command.ts +7 -3
- package/src/cli/init.ts +2 -2
- package/src/cli/provider.ts +58 -74
- package/src/cli/registry.ts +1 -1
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence.ts +110 -15
- package/src/codex/inject.ts +2 -0
- package/src/codex/subagent-defaults.ts +1 -8
- package/src/codex/subagent-model-authority.ts +189 -0
- package/src/config.ts +209 -11
- package/src/generated/compatibility-version.json +35 -27
- package/src/oauth/index.ts +143 -82
- package/src/oauth/login-cli.ts +53 -11
- package/src/providers/alibaba-region-startup.ts +31 -11
- package/src/providers/api-keys.ts +58 -34
- package/src/providers/key-failover.ts +59 -46
- package/src/providers/model-rename-startup.ts +25 -7
- package/src/providers/openai-tier-startup.ts +38 -14
- package/src/server/index.ts +10 -0
- package/src/server/management/agent-settings-routes.ts +12 -0
- package/src/server/management/context.ts +10 -3
- package/src/server/management/logs-usage-routes.ts +19 -16
- package/src/server/management/model-routes.ts +284 -165
- package/src/server/management/oauth-account-routes.ts +93 -15
- package/src/server/management/provider-routes.ts +200 -87
- package/src/server/responses/collaboration.ts +1 -1
- package/src/storage/policy-input.ts +166 -0
- package/src/storage/policy.ts +59 -211
|
@@ -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
|
-
|
|
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 =
|
|
214
|
-
if (
|
|
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
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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:
|
|
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
|
|
325
|
-
|
|
326
|
-
provider
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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:
|
|
889
|
+
selected: outcome.value.selected,
|
|
789
890
|
});
|
|
790
891
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
}
|