Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,20 @@ label. A management client can set or reset one label with
`{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }`; send `displayName: null` to reset it.
Provider `PATCH` does not edit this map. Use this dedicated `PUT` endpoint to change or remove labels.

The dashboard exposes the same durable setting on **Models**. Expand the provider, find a
discovered model, and choose **Name**. The dialog keeps the exact `provider/model` selector visible
while you save a friendly label. Choose **Reset name** to return to provider metadata or the normal
selector fallback. **Name** changes presentation only; the separate alias pencil changes the
short routing alias and is not a display name editor. Native OpenAI and custom model rows keep their
existing controls.

If the change is saved but refreshing fails, the dialog reflects the saved override and keeps
**Retry** available. Retry repeats catalog convergence when the server reported it failed, or
reloads the list when only the list request failed. Reset recovery keeps the reset operation;
it does not restore the old name. Requests have a 60-second deadline covering the write and its
follow-up list refresh. A timeout does not undo a write: use **Retry** to check the current name
before making another change.

## Codex catalog and root `config.toml` settings

These settings belong in the root of `$CODEX_HOME/config.toml`, alongside
Expand Down
175 changes: 175 additions & 0 deletions gui/src/components/ModelDisplayNameDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { useEffect, useId, useRef, useState } from "react";
import { useT, type TKey } from "../i18n/shared";
import {
modelDisplayNameValidationKey,
type ModelRow,
} from "../pages/models-shared";

interface ModelDisplayNameDialogProps {
model: ModelRow;
saving: boolean;
requestError: string | null;
currentNamePending?: boolean;
onRetry?: () => void;
onEdit?: () => void;
onSave: (displayName: string) => void;
onReset: () => void;
onClose: () => void;
}

const SOURCE_LABEL_KEYS: Record<NonNullable<ModelRow["displayNameSource"]>, TKey> = {
operator: "models.displayNameSourceOperator",
provider: "models.displayNameSourceProvider",
fallback: "models.displayNameSourceFallback",
};

export default function ModelDisplayNameDialog({
model,
saving,
requestError,
currentNamePending = false,
onRetry,
onEdit,
onSave,
onReset,
onClose,
}: ModelDisplayNameDialogProps) {
const t = useT();
const dialogRef = useRef<HTMLDialogElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const wasSavingRef = useRef(saving);
const titleId = useId();
const helpId = useId();
const errorId = useId();
const [draftSnapshot, setDraftSnapshot] = useState(model);
const [draft, setDraft] = useState(model.displayNameOverride ?? "");
const [validationKey, setValidationKey] = useState<TKey | null>(null);

useEffect(() => {
const dialog = dialogRef.current;
if (dialog && !dialog.open) dialog.showModal();
inputRef.current?.focus();
return () => { if (dialog?.open) dialog.close(); };
}, []);

useEffect(() => {
const saveFailed = wasSavingRef.current && !saving && Boolean(requestError);
wasSavingRef.current = saving;
if (saveFailed) inputRef.current?.focus();
}, [requestError, saving]);

// Parent replaces this snapshot only after a confirmed mutation, not typing or polling.
// Adjust before committing children, preserving the mounted dialog and its focus refs.
if (draftSnapshot !== model) {
setDraftSnapshot(model);
setDraft(model.displayNameOverride ?? "");
setValidationKey(null);
}

const validationError = validationKey ? t(validationKey) : null;
const visibleError = validationError ?? requestError;
const sourceKey = model.displayNameSource
? SOURCE_LABEL_KEYS[model.displayNameSource]
: "models.displayNameSourceFallback";

const requestClose = () => {
if (!saving) onClose();
};

return (
<dialog
ref={dialogRef}
className="modal-overlay"
aria-labelledby={titleId}
onCancel={event => {
event.preventDefault();
requestClose();
}}
>
<button
type="button"
className="modal-backdrop-dismiss"
aria-label={t("common.close")}
tabIndex={-1}
disabled={saving}
onClick={requestClose}
/>
<form
className="modal-card model-display-name-dialog"
role="document"
onClick={event => event.stopPropagation()}
onSubmit={event => {
event.preventDefault();
if (saving) return;
if (onRetry) { onRetry(); return; }
const nextValidationKey = modelDisplayNameValidationKey(draft);
setValidationKey(nextValidationKey);
if (!nextValidationKey) onSave(draft.trim());
}}
>
<div className="modal-head">
<h3 id={titleId}>{t("models.displayNameTitle")}</h3>
<button type="button" className="btn btn-ghost btn-sm" disabled={saving} onClick={requestClose}>
{t("common.close")}
</button>
</div>

<div className="model-display-name-identity">
<span className="muted text-label">{t("models.displayNameModelId")}</span>
<code className="mono text-control">{model.namespaced}</code>
</div>

<div className="model-display-name-current">
<span className="muted text-label">{t("models.displayNameCurrent")}</span>
<strong>{currentNamePending ? t("models.displayNameCurrentUnavailable") : model.displayName ?? model.namespaced}</strong>
{!currentNamePending && <span className="models-chip muted text-caption">{t(sourceKey)}</span>}
</div>

<label className="field-label" htmlFor={`${titleId}-input`}>
{t("models.displayNameField")}
</label>
<input
ref={inputRef}
id={`${titleId}-input`}
className="input"
value={draft}
maxLength={129}
placeholder={t("models.displayNamePlaceholder")}
aria-describedby={`${helpId}${visibleError ? ` ${errorId}` : ""}`}
aria-invalid={validationError ? true : undefined}
disabled={saving}
onChange={event => {
onEdit?.();
setDraft(event.target.value);
setValidationKey(null);
}}
/>
<p id={helpId} className="muted small">
{t("models.displayNameHelp", { model: model.namespaced })}
</p>
{visibleError && (
<p id={errorId} className="model-display-name-error" role="alert">
{visibleError}
</p>
)}

<div className="modal-actions">
<button
type="button"
className="btn btn-ghost btn-sm"
disabled={saving || !model.displayNameOverride}
onClick={onReset}
>
{t("models.displayNameReset")}
</button>
<button type="button" className="btn btn-sm" disabled={saving} onClick={requestClose}>
{t("common.cancel")}
</button>
<button type="submit" className="btn btn-primary btn-sm" disabled={saving}>
{saving ? t("common.saving") : onRetry ? t("common.retry") : t("common.save")}
</button>
</div>
</form>
</dialog>
);
}
23 changes: 23 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2554,4 +2554,27 @@ export const de: Record<TKey, string> = {
"integrations.cursor.colReasoning": "Reasoning-Aufwand",
"integrations.cursor.colContext": "Kontext",
"integrations.cursor.guide": "Anleitung zu Cursor Private Inference öffnen",
"models.displayNameSavedRefreshFailed": "Die Änderung wurde gespeichert, aber die Modellliste konnte nicht aktualisiert werden. Versuchen Sie es erneut.",
"models.displayNameOutcomeUnknown": "Die Anfrage wurde nicht abgeschlossen. Die Änderung wurde möglicherweise gespeichert. Prüfen Sie den aktuellen Namen durch erneutes Versuchen, bevor Sie ihn weiter ändern.",
"models.displayNameCurrentUnavailable": "Aktueller Name erst nach Aktualisierung verfügbar",
"models.displayNameReloaded": "Modellliste aktualisiert",
"models.displayNameAction": "Name",
"models.displayNameActionLabel": "Anzeigenamen für {model} bearbeiten",
"models.displayNameTitle": "Anzeigename",
"models.displayNameModelId": "Modell-ID",
"models.displayNameCurrent": "Aktueller Name",
"models.displayNameSourceOperator": "Ihr Name",
"models.displayNameSourceProvider": "Anbietername",
"models.displayNameSourceFallback": "Modell-ID als Ersatz",
"models.displayNameField": "Anzeigename",
"models.displayNamePlaceholder": "z. B. Grok 4.6",
"models.displayNameHelp": "Ändert nur die Anzeige. Das Routing bleibt {model}.",
"models.displayNameReset": "Name zurücksetzen",
"models.displayNameSaved": "Anzeigename gespeichert",
"models.displayNameResetDone": "Anzeigename zurückgesetzt",
"models.displayNameSaveFailed": "Anzeigename konnte nicht gespeichert werden",
"models.displayNameRequired": "Geben Sie einen Anzeigenamen ein oder verwenden Sie Name zurücksetzen.",
"models.displayNameTooLong": "Der Anzeigename darf höchstens 128 Zeichen lang sein.",
"models.displayNameNoSlash": "Der Anzeigename darf kein / enthalten.",
"models.displayNameNoControl": "Der Anzeigename darf keine Steuerzeichen enthalten.",
};
23 changes: 23 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2588,6 +2588,29 @@ export const en = {
"usage.scope.machine": "This machine",
"usage.scope.hub": "Hub-wide",
"usage.hubOffline": "Hub usage is unavailable. Local usage was not substituted.",
"models.displayNameSavedRefreshFailed": "The change was saved, but the model list could not be refreshed. Retry to refresh it.",
"models.displayNameOutcomeUnknown": "The request did not finish. The change may have been saved. Retry to check the current name before making another change.",
"models.displayNameCurrentUnavailable": "Current name unavailable until refresh",
"models.displayNameReloaded": "Model list refreshed",
"models.displayNameAction": "Name",
"models.displayNameActionLabel": "Edit friendly name for {model}",
"models.displayNameTitle": "Friendly name",
"models.displayNameModelId": "Model ID",
"models.displayNameCurrent": "Current name",
"models.displayNameSourceOperator": "Your name",
"models.displayNameSourceProvider": "Provider name",
"models.displayNameSourceFallback": "Model ID fallback",
"models.displayNameField": "Friendly name",
"models.displayNamePlaceholder": "e.g. Grok 4.6",
"models.displayNameHelp": "Changes presentation only. Routing remains {model}.",
"models.displayNameReset": "Reset name",
"models.displayNameSaved": "Display name saved",
"models.displayNameResetDone": "Display name reset",
"models.displayNameSaveFailed": "Failed to save display name",
"models.displayNameRequired": "Enter a friendly name, or use Reset name.",
"models.displayNameTooLong": "Friendly name must be 128 characters or fewer.",
"models.displayNameNoSlash": "Friendly name cannot contain /.",
"models.displayNameNoControl": "Friendly name cannot contain control characters.",
} as const;

export type TKey = keyof typeof en;
23 changes: 23 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2541,4 +2541,27 @@ export const fr: Record<TKey, string> = {
"integrations.cursor.colReasoning": "Raisonnement",
"integrations.cursor.colContext": "Contexte",
"integrations.cursor.guide": "Ouvrir le guide de Cursor Private Inference",
"models.displayNameSavedRefreshFailed": "La modification a été enregistrée, mais la liste des modèles n’a pas pu être actualisée. Réessayez.",
"models.displayNameOutcomeUnknown": "La requête n’a pas abouti. La modification a peut-être été enregistrée. Réessayez pour vérifier le nom actuel avant toute autre modification.",
"models.displayNameCurrentUnavailable": "Nom actuel indisponible avant actualisation",
"models.displayNameReloaded": "Liste des modèles actualisée",
"models.displayNameAction": "Nom",
"models.displayNameActionLabel": "Modifier le nom d’affichage de {model}",
"models.displayNameTitle": "Nom d’affichage",
"models.displayNameModelId": "ID du modèle",
"models.displayNameCurrent": "Nom actuel",
"models.displayNameSourceOperator": "Votre nom d’affichage",
"models.displayNameSourceProvider": "Nom du fournisseur",
"models.displayNameSourceFallback": "ID du modèle par défaut",
"models.displayNameField": "Nom d’affichage",
"models.displayNamePlaceholder": "p. ex. Grok 4.6",
"models.displayNameHelp": "Modifie uniquement l’affichage. Le routage reste {model}.",
"models.displayNameReset": "Réinitialiser le nom",
"models.displayNameSaved": "Nom d’affichage enregistré",
"models.displayNameResetDone": "Nom d’affichage réinitialisé",
"models.displayNameSaveFailed": "Impossible d’enregistrer le nom d’affichage",
"models.displayNameRequired": "Saisissez un nom d’affichage ou utilisez Réinitialiser le nom.",
"models.displayNameTooLong": "Le nom d’affichage doit contenir au maximum 128 caractères.",
"models.displayNameNoSlash": "Le nom d’affichage ne peut pas contenir /.",
"models.displayNameNoControl": "Le nom d’affichage ne peut pas contenir de caractères de contrôle.",
};
23 changes: 23 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2575,4 +2575,27 @@ export const ja: Record<TKey, string> = {
"integrations.cursor.colReasoning": "推論",
"integrations.cursor.colContext": "コンテキスト",
"integrations.cursor.guide": "Cursor Private Inference のガイドを開く",
"models.displayNameSavedRefreshFailed": "変更は保存されましたが、モデル一覧を更新できませんでした。再試行してください。",
"models.displayNameOutcomeUnknown": "リクエストが完了しませんでした。変更が保存されている可能性があります。再度変更する前に再試行して現在の名前を確認してください。",
"models.displayNameCurrentUnavailable": "更新するまで現在の名前を確認できません",
"models.displayNameReloaded": "モデル一覧を更新しました",
"models.displayNameAction": "名前",
"models.displayNameActionLabel": "{model} の表示名を編集",
"models.displayNameTitle": "表示名",
"models.displayNameModelId": "モデル ID",
"models.displayNameCurrent": "現在の名前",
"models.displayNameSourceOperator": "設定した名前",
"models.displayNameSourceProvider": "プロバイダー名",
"models.displayNameSourceFallback": "モデル ID の既定値",
"models.displayNameField": "表示名",
"models.displayNamePlaceholder": "例: Grok 4.6",
"models.displayNameHelp": "表示だけを変更します。ルーティングは {model} のままです。",
"models.displayNameReset": "名前をリセット",
"models.displayNameSaved": "表示名を保存しました",
"models.displayNameResetDone": "表示名をリセットしました",
"models.displayNameSaveFailed": "表示名を保存できませんでした",
"models.displayNameRequired": "表示名を入力するか、名前をリセットしてください。",
"models.displayNameTooLong": "表示名は 128 文字以内にしてください。",
"models.displayNameNoSlash": "表示名に / は使用できません。",
"models.displayNameNoControl": "表示名に制御文字は使用できません。",
};
23 changes: 23 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2576,4 +2576,27 @@ export const ko: Record<TKey, string> = {
"integrations.cursor.colReasoning": "추론",
"integrations.cursor.colContext": "컨텍스트",
"integrations.cursor.guide": "Cursor Private Inference 가이드 열기",
"models.displayNameSavedRefreshFailed": "변경 사항은 저장되었지만 모델 목록을 새로 고치지 못했습니다. 다시 시도해 주세요.",
"models.displayNameOutcomeUnknown": "요청이 완료되지 않았습니다. 변경 사항이 저장되었을 수 있습니다. 다시 변경하기 전에 재시도하여 현재 이름을 확인하세요.",
"models.displayNameCurrentUnavailable": "새로 고침 전까지 현재 이름을 확인할 수 없음",
"models.displayNameReloaded": "모델 목록을 새로 고쳤습니다",
"models.displayNameAction": "이름",
"models.displayNameActionLabel": "{model}의 표시 이름 편집",
"models.displayNameTitle": "표시 이름",
"models.displayNameModelId": "모델 ID",
"models.displayNameCurrent": "현재 이름",
"models.displayNameSourceOperator": "운영자 지정 이름",
"models.displayNameSourceProvider": "프로바이더 제공 이름",
"models.displayNameSourceFallback": "모델 ID 기본값",
"models.displayNameField": "표시 이름",
"models.displayNamePlaceholder": "예: Grok 4.6",
"models.displayNameHelp": "표시 방식만 변경합니다. 라우팅은 {model}로 유지됩니다.",
"models.displayNameReset": "이름 초기화",
"models.displayNameSaved": "표시 이름이 저장되었습니다",
"models.displayNameResetDone": "표시 이름이 초기화되었습니다",
"models.displayNameSaveFailed": "표시 이름을 저장하지 못했습니다",
"models.displayNameRequired": "표시 이름을 입력하거나 이름 초기화를 사용하세요.",
"models.displayNameTooLong": "표시 이름은 128자 이하여야 합니다.",
"models.displayNameNoSlash": "표시 이름에 /를 사용할 수 없습니다.",
"models.displayNameNoControl": "표시 이름에 제어 문자를 사용할 수 없습니다.",
};
23 changes: 23 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2577,4 +2577,27 @@ export const ru: Record<TKey, string> = {
"integrations.cursor.colReasoning": "Рассуждения",
"integrations.cursor.colContext": "Контекст",
"integrations.cursor.guide": "Открыть руководство по Cursor Private Inference",
"models.displayNameSavedRefreshFailed": "Изменение сохранено, но список моделей не удалось обновить. Повторите попытку.",
"models.displayNameOutcomeUnknown": "Запрос не завершён. Изменение могло сохраниться. Повторите попытку, чтобы проверить текущее имя перед следующим изменением.",
"models.displayNameCurrentUnavailable": "Текущее имя недоступно до обновления",
"models.displayNameReloaded": "Список моделей обновлён",
"models.displayNameAction": "Имя",
"models.displayNameActionLabel": "Изменить понятное имя для {model}",
"models.displayNameTitle": "Понятное имя",
"models.displayNameModelId": "ID модели",
"models.displayNameCurrent": "Текущее имя",
"models.displayNameSourceOperator": "Ваше имя",
"models.displayNameSourceProvider": "Имя провайдера",
"models.displayNameSourceFallback": "ID модели по умолчанию",
"models.displayNameField": "Понятное имя",
"models.displayNamePlaceholder": "например, Grok 4.6",
"models.displayNameHelp": "Меняет только отображение. Маршрут остаётся {model}.",
"models.displayNameReset": "Сбросить имя",
"models.displayNameSaved": "Понятное имя сохранено",
"models.displayNameResetDone": "Понятное имя сброшено",
"models.displayNameSaveFailed": "Не удалось сохранить понятное имя",
"models.displayNameRequired": "Введите понятное имя или используйте Сбросить имя.",
"models.displayNameTooLong": "Понятное имя должно содержать не более 128 символов.",
"models.displayNameNoSlash": "Понятное имя не может содержать /.",
"models.displayNameNoControl": "Понятное имя не может содержать управляющие символы.",
};
Loading
Loading