-
Notifications
You must be signed in to change notification settings - Fork 864
feat(catalog): operator display labels for live-discovered models #2299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
17efd01
a6f8167
866dda2
48326fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /** | ||
| * Operator-supplied display labels for live-discovered provider models (#2201). | ||
| * | ||
| * A discovered row's label is its routed slug, so NVIDIA NIM surfaces as | ||
| * `nvidia/deepseek-ai-deepseek-v4-flash-0731` in the picker, the dashboard, | ||
| * `/v1/models`, and client exports. `customModels[].displayName` and combo | ||
| * display labels already relabel their rows display-only; live discovery is the | ||
| * one row source with no equivalent. | ||
| * | ||
| * The single invariant: a display label is never routing identity. Nothing here | ||
| * touches `provider`, `id`, or the routed slug — the resolved label lands on | ||
| * `CatalogModel.displayName`, which `applyCatalogModelMetadata` already treats | ||
| * as display-only, and which client exports already read. | ||
| */ | ||
|
|
||
| import { COMBO_NAMESPACE } from "../../combos/types"; | ||
| import type { OcxConfig } from "../../types/config"; | ||
| import { CODEX_CUSTOM_MODEL_CATALOG_KIND } from "./parsing"; | ||
| import type { CatalogModel } from "./parsing"; | ||
|
|
||
| /** Same bound as the combo display label (src/combos/types.ts) so every label surface agrees. */ | ||
| export const MAX_DISPLAY_LABEL_LENGTH = 128; | ||
|
|
||
| // Control characters corrupt picker rendering, so a label carrying one is rejected. | ||
| // The range is C0, DEL, and C1 (U+0080-U+009F). C1 was originally missing, which let | ||
| // a label such as `Label<U+0085>More` through — U+0085 is NEL, a line break, and | ||
| // `trim()` does not touch a mid-string one. U+2028/U+2029 are added for the same | ||
| // reason: they are line and paragraph separators, so a label carrying one is not | ||
| // single-line whatever its width. | ||
| const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; | ||
|
|
||
| /** | ||
| * Checked against the *untrimmed* value, unlike `CONTROL_CHARS`. | ||
| * | ||
| * `trim()` counts U+2028/U+2029 as whitespace and would strip an edge one, so a | ||
| * trailing line separator would otherwise be normalised away and reported as | ||
| * valid. A stray space or newline is plausible slop in a hand-edited config and | ||
| * is still forgiven; a Unicode line separator is not, so it is rejected wherever | ||
| * it appears rather than quietly removed. | ||
| */ | ||
| const CONTROLS_TRIM_WOULD_HIDE = /[\u0080-\u009f\u2028\u2029]/; | ||
|
|
||
| /** | ||
| * A label is usable when it is a non-empty single-line string within the shared bound. | ||
| * | ||
| * Slashes are rejected, matching the `customModels[].displayName` rule: a label | ||
| * containing `/` reads as a routed slug, and this field must never be mistaken | ||
| * for one. | ||
| */ | ||
| export function isValidDisplayLabel(value: unknown): value is string { | ||
| if (typeof value !== "string") return false; | ||
| if (CONTROLS_TRIM_WOULD_HIDE.test(value)) return false; | ||
| const trimmed = value.trim(); | ||
| if (trimmed.length === 0 || trimmed.length > MAX_DISPLAY_LABEL_LENGTH) return false; | ||
| if (CONTROL_CHARS.test(trimmed)) return false; | ||
| return !trimmed.includes("/"); | ||
| } | ||
|
|
||
| /** | ||
| * The precedence chain, in one place: | ||
| * | ||
| * 1. operator override — `providers[<provider>].modelDisplayNames[<native id>]` | ||
| * 2. trusted discovery metadata — a label discovery already attached | ||
| * 3. undefined — caller keeps its derived slug, i.e. today's behaviour | ||
| * | ||
| * Rows that already own an operator-supplied label keep it, and the provider map | ||
| * must not outrank them: | ||
| * - combo rows validate their own bounded label independently, so an entry under | ||
| * the combo namespace is never relabelled from provider config; | ||
| * - an explicit `customModels[]` row carries the label the operator typed there, | ||
| * and #2201 requires those to continue unchanged. Matching on `catalogKind` | ||
| * rather than on the provider name is what makes that hold, because a custom | ||
| * model shares its provider with the discovered rows this function exists for. | ||
| * | ||
| * Native OpenAI rows never reach here at all — they come from the pinned snapshot | ||
| * path with no `CatalogModel` — so upstream marketing names stay untouched. | ||
| */ | ||
| export function resolveModelDisplayLabel( | ||
| config: OcxConfig, | ||
| model: CatalogModel, | ||
| ): string | undefined { | ||
| if (model.provider === COMBO_NAMESPACE || model.catalogKind === CODEX_CUSTOM_MODEL_CATALOG_KIND) { | ||
| return isValidDisplayLabel(model.displayName) ? model.displayName.trim() : undefined; | ||
| } | ||
| const override = config.providers?.[model.provider]?.modelDisplayNames?.[model.id]; | ||
| if (isValidDisplayLabel(override)) return override.trim(); | ||
| if (isValidDisplayLabel(model.displayName)) return model.displayName.trim(); | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve labels across a discovered model list. | ||
| * | ||
| * Returns the input array unchanged when nothing resolves, and otherwise a new | ||
| * array of new objects — the input models are never mutated, so a caller holding | ||
| * the pre-label list keeps it intact. | ||
| */ | ||
| export function applyOperatorDisplayLabels( | ||
| models: CatalogModel[], | ||
| config: OcxConfig, | ||
| ): CatalogModel[] { | ||
| let changed = false; | ||
| const labeled = models.map(model => { | ||
| const label = resolveModelDisplayLabel(config, model); | ||
| if (label === undefined || label === model.displayName) return model; | ||
| changed = true; | ||
| return { ...model, displayName: label }; | ||
| }); | ||
| return changed ? labeled : models; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,7 @@ import { | |
| resolveCodexModelEntitlements, | ||
| type CodexModelEntitlementSnapshot, | ||
| } from "./model-entitlements"; | ||
| import { applyOperatorDisplayLabels } from "./catalog/display-labels"; | ||
| import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models"; | ||
| import { providerCodexAccountMode } from "../providers/registry"; | ||
| import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; | ||
|
|
@@ -237,7 +238,11 @@ function prepareCatalog( | |
| const template = findNativeTemplate(catalog); | ||
| const enabled = filterCatalogVisibleModels(routedModels, config); | ||
| const featured = config.subagentModels ?? []; | ||
| const ordered = orderForSubagents(enabled, featured); | ||
| // #2201: resolve operator display labels before ordering. Display-only — the | ||
| // routed slug, provider id and native model id are all unchanged, so ordering, | ||
| // featuring and spawn-candidate derivation below see the same identities. | ||
| const labeled = applyOperatorDisplayLabels(enabled, config); | ||
| const ordered = orderForSubagents(labeled, featured); | ||
|
Comment on lines
+241
to
+245
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add a regression test for the prepared catalog output. The current tests stop at This test will detect integration regressions if As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” 🤖 Prompt for AI AgentsSource: Path instructions |
||
| const modelPickerOrder = config.modelPickerOrder ?? []; | ||
| const multiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" | ||
| ? config.multiAgentMode : "default"; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,7 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| MAIN_CODEX_ACCOUNT_NAMESPACE_TARGET, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "./codex/account-namespace-match"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isCodexAccountPriorityKey } from "./codex/account-priority"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MAX_DISPLAY_LABEL_LENGTH, isValidDisplayLabel } from "./codex/catalog/display-labels"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "./codex/upstream-host-health"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| adoptCustomModelCatalogMigration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -714,6 +715,25 @@ const providerConfigSchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| fastWire: fastWireSchema.nullable().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| supportsServiceTier: z.boolean().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Display-only labels for discovered models, keyed by native model id — the same | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // key space as `modelAdapters`. Declared rather than left to `.passthrough()` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // below, for the reason the `codexToolMode` comment gives: an undeclared key is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // accepted, persisted, and then silently ignored (#2106). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Salvaged entry by entry rather than validated strictly, following `apiKeys`: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // one hand-edited label must not send the whole config through the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // backup-and-defaults repair path, and must not take the operator's other | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // labels down with it. Writes go through displayLabelRecordConfigError instead, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so an invalid label is a 400 at the API and a dropped entry on load. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelDisplayNames: z.unknown().optional().transform(value => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value === undefined || value === null) return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value !== "object" || Array.isArray(value)) return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const kept = Object.entries(value as Record<string, unknown>) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(([id, label]) => id.trim().length > 0 && isValidDisplayLabel(label)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(0, MAX_MODEL_DISPLAY_NAMES) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(([id, label]) => [id.trim(), (label as string).trim()] as const); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return kept.length > 0 ? Object.fromEntries(kept) : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| preserveResponsesReasoningContent: z.boolean().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decodesNativeCompactionBlobs: z.boolean().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowPrivateNetwork: z.boolean().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -947,6 +967,48 @@ export function booleanRecordConfigError(value: unknown, field: string): string | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Bound on how many labels one provider may carry. A display map is a convenience, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * not a catalogue, so an unbounded hand-edited map is a mistake rather than a use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * case — and every entry is walked on each convergence. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const MAX_MODEL_DISPLAY_NAMES = 512; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Strict diagnostic for `providers[<name>].modelDisplayNames`, mirroring | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `booleanRecordConfigError`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This is the *write* rule, used by the provider editor so a bad label is a 400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * rather than something that lands on disk. The load path is deliberately more | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * forgiving — see the schema entry, which drops a bad entry instead of failing — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * because the two paths answer different questions: "is this a valid edit?" and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * "can this file still be served?". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * `null` is accepted as an explicit clear, matching `upstreamHttpVersion`: the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * management API says null means "remove this", so rejecting it here would refuse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the documented way to take a label back off. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function displayLabelRecordConfigError(value: unknown, field = "modelDisplayNames"): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value === undefined) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prototype = Object.getPrototypeOf(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = Object.entries(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entries.length > MAX_MODEL_DISPLAY_NAMES) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${field} must hold at most ${MAX_MODEL_DISPLAY_NAMES} entries`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [key, label] of entries) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!key.trim()) return `${field} keys must be nonblank model ids`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (label === null) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof label !== "string") return `${field}.${key} must be a string`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+991
to
+1003
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Accept a null Line 993 rejects Return Proposed fix export function displayLabelRecordConfigError(value: unknown, field = "modelDisplayNames"): string | null {
- if (value === undefined) return null;
+ if (value === undefined || value === null) return null;
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.1)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isValidDisplayLabel(label)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${field}.${key} must be a nonblank single-line label of at most ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| + `${MAX_DISPLAY_LABEL_LENGTH} characters, and must not contain '/'`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const REASONING_SUMMARY_DELIVERY_SET = new Set<string>(REASONING_SUMMARY_DELIVERY_VALUES); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function reasoningSummaryDeliveryRecordConfigError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,7 +75,8 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ". | |
| import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; | ||
| import type { PersistedUsageAttempt } from "../../usage/log"; | ||
| import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; | ||
| import { providerServiceTierConfigError } from "./provider-capability-config"; | ||
| import { MAX_DISPLAY_LABEL_LENGTH, isValidDisplayLabel } from "../../codex/catalog/display-labels"; | ||
| import { providerDisplayNamesConfigError, providerServiceTierConfigError } from "./provider-capability-config"; | ||
| import { applySystemEnvToggle } from "../system-env"; | ||
| import { | ||
| LOCAL_PROVIDER_RELOAD_NAME_HEADER, | ||
|
|
@@ -266,6 +267,34 @@ function applyProviderPatchFields( | |
| } | ||
| touched = true; | ||
| } | ||
| if (Object.hasOwn(rawBody, "modelDisplayNames")) { | ||
| const value = rawBody.modelDisplayNames; | ||
| if (value === null) { | ||
| delete next.modelDisplayNames; | ||
| } else { | ||
| if (!isPlainRecord(value)) return { error: "modelDisplayNames must be a plain object or null" }; | ||
| const labels: Record<string, string> = { ...(next.modelDisplayNames ?? {}) }; | ||
| for (const [model, label] of Object.entries(value)) { | ||
| if (!model.trim()) return { error: "modelDisplayNames keys must be nonblank model ids" }; | ||
| // Per-key null clears one label, matching `modelContextWindows`, so an operator can | ||
| // take a single label back off without resubmitting the rest of the map. | ||
| if (label === null) { | ||
| delete labels[model.trim()]; | ||
| continue; | ||
| } | ||
| if (!isValidDisplayLabel(label)) { | ||
| return { | ||
| error: "modelDisplayNames values must be a nonblank single-line label of at most " | ||
| + `${MAX_DISPLAY_LABEL_LENGTH} characters without '/', or null`, | ||
| }; | ||
| } | ||
| labels[model.trim()] = label.trim(); | ||
| } | ||
| if (Object.keys(labels).length > 0) next.modelDisplayNames = labels; | ||
| else delete next.modelDisplayNames; | ||
| } | ||
| touched = true; | ||
| } | ||
| if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) { | ||
| const value = rawBody.modelSupportsServiceTier; | ||
| if (value === null) { | ||
|
|
@@ -500,6 +529,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| if (providerError) return jsonResponse({ error: providerError }, 400); | ||
| const serviceTierError = providerServiceTierConfigError(name, body.provider); | ||
| if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400); | ||
| const displayNamesError = providerDisplayNamesConfigError(name, body.provider); | ||
| if (displayNamesError) return jsonResponse({ error: displayNamesError }, 400); | ||
| const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined; | ||
| // PATCH already clears on null; POST persisted the body as submitted, so a `null` here | ||
| // reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what | ||
|
|
@@ -537,6 +568,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| const submittedContextWindow = Object.hasOwn(prov, "contextWindow"); | ||
| const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows"); | ||
| const submittedRequestPacing = Object.hasOwn(prov, "requestPacing"); | ||
| const submittedModelDisplayNames = Object.hasOwn(prov, "modelDisplayNames"); | ||
| enrichProviderFromCatalog(name, prov); | ||
| const { saveConfigPreservingClaudeCode: save } = await import("../../config"); | ||
| // Overwriting an existing provider must not drop its multi-key pool: carry it over, then | ||
|
|
@@ -568,6 +600,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| ? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) } | ||
| : { ...existing.modelContextWindows }; | ||
| } | ||
| // ...and to operator display labels, for the same structural reason: `ProviderPayload` | ||
| // has no member for `modelDisplayNames` either, and this change deliberately leaves the | ||
| // dashboard editor to a follow-up, so the add/edit form cannot round-trip the field at | ||
| // all. Without this, saving an unrelated setting on the provider silently erases every | ||
| // label the operator set. Deletion goes through PATCH with an explicit null. | ||
| if (existing?.modelDisplayNames) { | ||
| prov.modelDisplayNames = submittedModelDisplayNames | ||
| ? { ...existing.modelDisplayNames, ...(prov.modelDisplayNames ?? {}) } | ||
| : { ...existing.modelDisplayNames }; | ||
| } | ||
| config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov); | ||
| if (body.setDefault === true) config.defaultProvider = name; | ||
| save(config); | ||
|
|
@@ -657,6 +699,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| if (providerError) return jsonResponse({ error: providerError }, 400); | ||
| const serviceTierError = providerServiceTierConfigError(name, next); | ||
| if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400); | ||
| const displayNamesError = providerDisplayNamesConfigError(name, next); | ||
| if (displayNamesError) return jsonResponse({ error: displayNamesError }, 400); | ||
|
Comment on lines
+702
to
+703
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Apply
Add As per path instructions: flag changes that bypass the shared routing/config layers. Also applies to: 699-703 🤖 Prompt for AI AgentsSource: Path instructions |
||
| const resolvedError = await providerDestinationResolvedError(name, next); | ||
| if (resolvedError) return jsonResponse({ error: resolvedError }, 400); | ||
| } else if (applied.enablingOpenAi) { | ||
|
|
@@ -692,6 +736,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| replayError = serviceTierError; | ||
| return; | ||
| } | ||
| const displayNamesError = providerDisplayNamesConfigError(name, replay.next); | ||
| if (displayNamesError) { | ||
| replayError = displayNamesError; | ||
| return; | ||
| } | ||
| } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) { | ||
| replayError = "provider openai must be the canonical built-in provider"; | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject control characters before trimming the label.
trim()removes leading or trailing tabs, newlines, andU+2028/U+2029beforeCONTROL_CHARSruns. The current regex also permits C1 controls such asU+0085.An invalid operator override can therefore win over valid discovery metadata. Validate the original string with the complete control and line-separator set.
Proposed fix
Add cases for
"Label\n","\tLabel","Label\u0085", and"Label\u2028".📝 Committable suggestion
🤖 Prompt for AI Agents