fix: Codex model catalog WYSIWYG and config consolidation

- Remove mergeCodexDefaultCatalogModelForSave implicit injection (P1)
  The model mapping table is now the single source of truth; no hidden
  entries are prepended on save.

- Sync first catalog row model into config.toml on save
  Ensures Codex default request model matches the table's first entry
  instead of retaining a stale template value.

- Remove API Format selector from CodexFormFields (P3)
  wire_api is always 'responses'; the selector confused users into
  thinking they were changing the upstream protocol. Only the 'Needs
  Local Routing' toggle remains.

- Add restart hint to model mapping i18n text (P2)
  model_catalog_json is loaded at Codex startup; users are now informed
  that a restart is needed after changes.

- Unify write_codex_live_with_catalog helper (P4)
  Replaces three scattered prepare+write call sites in config.rs,
  provider/live.rs, and proxy.rs with a single entry point.

- Clean up useCodexConfigState dead state (P3 follow-up)
  Remove codexModelName, codexContextWindow, codexAutoCompactLimit and
  their handlers/effects since no component consumes them after the UI
  consolidation.
This commit is contained in:
Jason
2026-05-20 10:24:06 +08:00
parent 90b7f25111
commit 791ced0034
13 changed files with 959 additions and 207 deletions
+56 -10
View File
@@ -15,6 +15,7 @@ import type {
ProviderTestConfig,
ClaudeApiFormat,
CodexApiFormat,
CodexCatalogModel,
ClaudeApiKeyField,
} from "@/types";
import {
@@ -55,6 +56,7 @@ import { mergeProviderMeta } from "@/utils/providerMetaUtils";
import {
extractCodexWireApi,
setCodexWireApi,
setCodexModelName as setCodexModelNameInConfig,
} from "@/utils/providerConfigUtils";
import { isNonNegativeDecimalString } from "@/types/usage";
import { getCodexCustomTemplate } from "@/config/codexTemplates";
@@ -142,6 +144,36 @@ const codexApiFormatFromWireApi = (
}
};
export const normalizeCodexCatalogModelsForSave = (
models: CodexCatalogModel[],
): CodexCatalogModel[] => {
const seen = new Set<string>();
const normalized: CodexCatalogModel[] = [];
for (const item of models) {
const model = item.model.trim();
if (!model || seen.has(model)) continue;
seen.add(model);
const displayName = item.displayName?.trim();
const rawContextWindow = String(item.contextWindow ?? "").replace(
/[^\d]/g,
"",
);
const contextWindow = rawContextWindow
? Number.parseInt(rawContextWindow, 10)
: undefined;
normalized.push({
model,
...(displayName ? { displayName } : {}),
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
});
}
return normalized;
};
export interface ProviderFormProps {
appId: AppId;
providerId?: string;
@@ -440,13 +472,13 @@ function ProviderFormFull({
codexConfig,
codexApiKey,
codexBaseUrl,
codexModelName,
codexCatalogModels,
codexAuthError,
setCodexAuth,
setCodexConfig,
setCodexCatalogModels,
handleCodexApiKeyChange,
handleCodexBaseUrlChange,
handleCodexModelNameChange,
handleCodexConfigChange: originalHandleCodexConfigChange,
resetCodexConfig,
} = useCodexConfigState({ initialData });
@@ -476,10 +508,6 @@ function ProviderFormFull({
const handleCodexConfigChange = useCallback(
(value: string) => {
originalHandleCodexConfigChange(value);
const nextFormat = codexApiFormatFromWireApi(extractCodexWireApi(value));
if (nextFormat) {
setLocalCodexApiFormat(nextFormat);
}
debouncedValidate(value);
},
[originalHandleCodexConfigChange, debouncedValidate],
@@ -488,6 +516,7 @@ function ProviderFormFull({
const handleCodexApiFormatChange = useCallback(
(format: CodexApiFormat) => {
setLocalCodexApiFormat(format);
// wire_api is always "responses" for Codex; format controls proxy-layer conversion
setCodexConfig((prev) => {
const updated = setCodexWireApi(prev, "responses");
debouncedValidate(updated);
@@ -1109,14 +1138,32 @@ function ProviderFormFull({
if (appId === "codex") {
try {
const authJson = JSON.parse(codexAuth);
const normalizedCodexConfig =
let normalizedCodexConfig =
category !== "official" && (codexConfig ?? "").trim()
? setCodexWireApi(codexConfig ?? "", "responses")
: (codexConfig ?? "");
const normalizedCatalogModels =
category !== "official" && localCodexApiFormat === "openai_chat"
? normalizeCodexCatalogModelsForSave(codexCatalogModels)
: [];
// Sync first catalog row's model into config.toml so Codex uses it as default
if (normalizedCatalogModels.length > 0) {
normalizedCodexConfig = setCodexModelNameInConfig(
normalizedCodexConfig,
normalizedCatalogModels[0].model,
);
}
const configObj = {
auth: authJson,
config: normalizedCodexConfig,
} as {
auth: unknown;
config: string;
modelCatalog?: { models: CodexCatalogModel[] };
};
if (normalizedCatalogModels.length > 0) {
configObj.modelCatalog = { models: normalizedCatalogModels };
}
settingsConfig = JSON.stringify(configObj);
} catch (err) {
settingsConfig = values.settingsConfig.trim();
@@ -1937,9 +1984,8 @@ function ProviderFormFull({
onAutoSelectChange={setEndpointAutoSelect}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
shouldShowModelField={category !== "official"}
modelName={codexModelName}
onModelNameChange={handleCodexModelNameChange}
catalogModels={codexCatalogModels}
onCatalogModelsChange={setCodexCatalogModels}
speedTestEndpoints={speedTestEndpoints}
/>
)}