feat(codex): add default model field to provider form

Expose the top-level `model` key of config.toml as an editable field in
the Codex provider form, so users can switch to newly released models
(e.g. gpt-5.6) without waiting for a preset update — preset updates only
affect newly added providers, existing ones keep their saved TOML.

The field syncs bidirectionally with the TOML editor (mirroring the
base-URL pattern), suggests models from the mapping catalog plus the
provider's /models endpoint, and offers a one-click "add to mapping"
action when the value is outside a non-empty catalog. Hidden for
official providers.

Details:

- Save-time catalog sync now only backfills the first mapping row into
  the top-level model when the field was left empty, so the explicit
  field wins over the implicit row-0 sync
- Escape model names (and base_url) with TOML basic-string escaping and
  strip control characters from field input: /models ids are remote
  data and unescaped interpolation could inject config.toml lines
  (e.g. a forged [mcp_servers.*] command)
- The strict model-line matcher now recognizes escaped output, empty
  strings and single-quoted literals, keeping extract/set round-trips
  stable; deliberately no key-only loose matching (it would misfire on
  assignment-looking text inside multiline strings)
- Invalidate the fetched model list whenever the request identity
  changes (base URL, full-URL toggle, API key, custom UA) and discard
  in-flight responses via a sequence guard, so the dropdown never shows
  a previous provider's models
- i18n for zh/en/ja/zh-TW
This commit is contained in:
Jason
2026-07-10 10:47:07 +08:00
parent 62e44c4838
commit 7479d10db7
9 changed files with 350 additions and 24 deletions
@@ -63,6 +63,7 @@ import {
codexApiFormatFromWireApi,
extractCodexWireApi,
setCodexWireApi,
extractCodexModelName,
setCodexModelName as setCodexModelNameInConfig,
} from "@/utils/providerConfigUtils";
import { isNonNegativeDecimalString } from "@/types/usage";
@@ -554,6 +555,7 @@ function ProviderFormFull({
codexConfig,
codexApiKey,
codexBaseUrl,
codexModel,
codexCatalogModels,
codexAuthError,
setCodexAuth,
@@ -561,6 +563,7 @@ function ProviderFormFull({
setCodexCatalogModels,
handleCodexApiKeyChange,
handleCodexBaseUrlChange,
handleCodexModelChange,
handleCodexConfigChange: originalHandleCodexConfigChange,
resetCodexConfig,
} = useCodexConfigState({ initialData });
@@ -1286,8 +1289,13 @@ function ProviderFormFull({
category !== "official"
? normalizeCodexCatalogModelsForSave(codexCatalogModels)
: [];
// Sync first catalog row's model into config.toml so Codex uses it as default
if (normalizedCatalogModels.length > 0) {
// The default-model field writes the top-level `model` into the TOML
// as the user types; only when it was left empty fall back to the
// first catalog row so "fill mapping only" keeps its old behavior.
if (
normalizedCatalogModels.length > 0 &&
!extractCodexModelName(normalizedCodexConfig)
) {
normalizedCodexConfig = setCodexModelNameInConfig(
normalizedCodexConfig,
normalizedCatalogModels[0].model,
@@ -2168,6 +2176,8 @@ function ProviderFormFull({
}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
codexModel={codexModel}
onModelChange={handleCodexModelChange}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
anthropicAuthField={localCodexAnthropicAuthField}