feat(codex): decouple upstream format selector from model-mapping toggle

The Codex provider form tied Chat-format conversion and route takeover (model
mapping) to one toggle, so a provider serving a native Responses API could not
use model mapping without forcing Chat Completions conversion.

- Promote the upstream format (Chat Completions / Responses) to an independent,
  always-visible selector that triggers no sub-menus on its own.
- The local-routing toggle now solely gates the advanced sub-sections: model
  mapping catalog, plus reasoning capability when the format is Chat.
- Persist modelCatalog / codexChatReasoning based on the takeover toggle and
  derive its initial state from saved catalog presence (no new persisted field).
- Refresh codexConfig i18n (zh/en/ja/zh-TW): add upstreamFormat* keys and reword
  the routing/advanced hints to reflect the decoupling.
- Fix codexChatProviderPresets test expectation for the Doubao Seed 2.1 Pro rename.
This commit is contained in:
Jason
2026-06-24 23:35:42 +08:00
parent 213f55a6d2
commit a4eb5f3778
7 changed files with 168 additions and 67 deletions
+41 -18
View File
@@ -150,6 +150,16 @@ const codexApiFormatFromWireApi = (
}
};
// 从已保存的 settingsConfig 推断 Codex 模型映射条目数(用于决定本地路由初始开关)。
const codexCatalogCountFromSettings = (settingsConfig: unknown): number => {
if (settingsConfig && typeof settingsConfig === "object") {
const models = (settingsConfig as { modelCatalog?: { models?: unknown } })
.modelCatalog?.models;
return Array.isArray(models) ? models.length : 0;
}
return 0;
};
export const normalizeCodexCatalogModelsForSave = (
models: CodexCatalogModel[],
): CodexCatalogModel[] => {
@@ -568,24 +578,29 @@ function ProviderFormFull({
resetCodexConfig,
} = useCodexConfigState({ initialData });
const initialCodexApiFormat: CodexApiFormat =
initialData?.meta?.apiFormat === "openai_chat"
? "openai_chat"
: initialData?.meta?.apiFormat === "openai_responses"
? "openai_responses"
: (codexApiFormatFromWireApi(
extractCodexWireApi(
typeof initialData?.settingsConfig?.config === "string"
? initialData.settingsConfig.config
: "",
),
) ?? "openai_responses");
const [localCodexApiFormat, setLocalCodexApiFormat] =
useState<CodexApiFormat>(() => {
if (initialData?.meta?.apiFormat === "openai_chat") {
return "openai_chat";
}
if (initialData?.meta?.apiFormat === "openai_responses") {
return "openai_responses";
}
return (
codexApiFormatFromWireApi(
extractCodexWireApi(
typeof initialData?.settingsConfig?.config === "string"
? initialData.settingsConfig.config
: "",
),
) ?? "openai_responses"
);
});
useState<CodexApiFormat>(initialCodexApiFormat);
// 本地路由(接管)开关 —— 纯模型映射门控,与上游格式完全独立。
// 没有独立持久化字段,初值仅按「是否已配置模型映射」推断(有 catalog 即视为
// 接管已开)。只在 useState 初始化与预设重置点设置,跟 localCodexApiFormat
// 对称,避免漂移。
const [codexTakeoverEnabled, setCodexTakeoverEnabled] = useState<boolean>(
() => codexCatalogCountFromSettings(initialData?.settingsConfig) > 0,
);
const { configError: codexConfigError, debouncedValidate } =
useCodexTomlValidation();
@@ -616,6 +631,7 @@ function ProviderFormFull({
const template = getCodexCustomTemplate();
resetCodexConfig(template.auth, template.config);
setCodexChatReasoning({});
setCodexTakeoverEnabled(false);
}
}, [appId, initialData, selectedPresetId, resetCodexConfig]);
@@ -1262,7 +1278,7 @@ function ProviderFormFull({
? setCodexWireApi(codexConfig ?? "", "responses")
: (codexConfig ?? "");
const normalizedCatalogModels =
category !== "official" && localCodexApiFormat === "openai_chat"
category !== "official" && codexTakeoverEnabled
? normalizeCodexCatalogModelsForSave(codexCatalogModels)
: [];
// Sync first catalog row's model into config.toml so Codex uses it as default
@@ -1458,6 +1474,7 @@ function ProviderFormFull({
codexChatReasoning:
appId === "codex" &&
category !== "official" &&
codexTakeoverEnabled &&
localCodexApiFormat === "openai_chat"
? normalizeCodexChatReasoningForSave(codexChatReasoning)
: undefined,
@@ -1610,6 +1627,8 @@ function ProviderFormFull({
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
"openai_responses",
);
// 自定义模板无模型映射,路由默认关闭
setCodexTakeoverEnabled(false);
}
if (appId === "gemini") {
resetGeminiConfig({}, {});
@@ -1652,6 +1671,8 @@ function ProviderFormFull({
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
"openai_responses",
);
// 路由开关与格式无关,仅按预设是否带模型映射决定
setCodexTakeoverEnabled((preset.modelCatalog?.length ?? 0) > 0);
form.reset({
name: preset.nameKey ? t(preset.nameKey) : preset.name,
@@ -2124,6 +2145,8 @@ function ProviderFormFull({
}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
takeoverEnabled={codexTakeoverEnabled}
onTakeoverEnabledChange={setCodexTakeoverEnabled}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
codexChatReasoning={codexChatReasoning}