diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index 212660ba5..e165fb9b0 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -4,6 +4,13 @@ import { Button } from "@/components/ui/button"; import { FormLabel } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Collapsible, CollapsibleContent, @@ -62,6 +69,13 @@ interface CodexFormFieldsProps { autoSelect: boolean; onAutoSelectChange: (checked: boolean) => void; + // Local routing / takeover + // takeoverEnabled gates model mapping + reasoning visibility; it is decoupled + // from the wire format so a native Responses provider can use model mapping + // without Chat Completions conversion. + takeoverEnabled: boolean; + onTakeoverEnabledChange: (enabled: boolean) => void; + // API Format // Note: wire_api is always "responses" for Codex; apiFormat controls proxy-layer conversion apiFormat: CodexApiFormat; @@ -132,6 +146,8 @@ export function CodexFormFields({ onCustomEndpointsChange, autoSelect, onAutoSelectChange, + takeoverEnabled, + onTakeoverEnabledChange, apiFormat, onApiFormatChange, codexChatReasoning = {}, @@ -150,7 +166,9 @@ export function CodexFormFields({ const [fetchedModels, setFetchedModels] = useState([]); const [isFetchingModels, setIsFetchingModels] = useState(false); - const needsLocalRouting = apiFormat === "openai_chat"; + // takeoverEnabled 控制模型映射/思考能力的显示;isChatFormat 仅在选了 + // Chat Completions 上游格式时为真(思考能力是 Chat 专属)。 + const isChatFormat = apiFormat === "openai_chat"; const canEditCatalog = Boolean(onCatalogModelsChange); const canEditReasoning = Boolean(onCodexChatReasoningChange); const supportsThinking = @@ -158,12 +176,13 @@ export function CodexFormFields({ codexChatReasoning.supportsEffort === true; const supportsEffort = codexChatReasoning.supportsEffort === true; - // needsLocalRouting 非默认值说明预设/用户动过路由配置,需要让模型映射保持可见 + // takeoverEnabled 取代了旧的 needsLocalRouting:上游格式已与路由解耦。 + // takeoverEnabled 为真说明预设/用户启用了本地路由;请求头/请求体覆盖也算高级值。 const hasRequestOverrides = Boolean( localProxyHeadersOverride.trim() || localProxyBodyOverride.trim(), ); const hasAnyAdvancedValue = - !!customUserAgent || hasRequestOverrides || needsLocalRouting; + !!customUserAgent || hasRequestOverrides || takeoverEnabled; const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue); // 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠) @@ -204,13 +223,6 @@ export function CodexFormFields({ onCatalogModelsChange(next); }, [catalogRows, onCatalogModelsChange]); - const handleLocalRoutingChange = useCallback( - (checked: boolean) => { - onApiFormatChange(checked ? "openai_chat" : "openai_responses"); - }, - [onApiFormatChange], - ); - const handleReasoningThinkingChange = useCallback( (checked: boolean) => { if (!onCodexChatReasoningChange) return; @@ -391,38 +403,83 @@ export function CodexFormFields({

)} - {/* 本地路由映射开关 —— 沿用 shouldShowSpeedTest 门控,cloud_provider 保持不可切换 */} + {/* 上游格式 + 本地路由映射 —— 两个平级、相互独立的控件。 + 格式不依赖路由:Responses 原生供应商无需开启路由即可直连; + 沿用 shouldShowSpeedTest 门控,cloud_provider 保持不可切换。 */} {shouldShowSpeedTest && ( -
-
- - {t("codexConfig.localRoutingToggle", { - defaultValue: "需要本地路由映射", +
+ {/* 上游格式 —— 顶层独立选择,与路由开关解耦 */} +
+ + {t("codexConfig.upstreamFormatLabel", { + defaultValue: "上游格式", })} -

- {needsLocalRouting - ? t("codexConfig.localRoutingOnHint", { - defaultValue: - "Codex 目前仅原生支持 OpenAI Responses API 与 GPT 系列模型;如果您的供应商使用 Chat Completions 协议或非 GPT 模型(如 DeepSeek、Kimi),则需要打开本开关,并在使用过程中保持本地路由开启。", - }) - : t("codexConfig.localRoutingOffHint", { - defaultValue: - "如果您的供应商不是原生 OpenAI Responses API,或者模型名不是 Codex 默认的 GPT 系列,请打开此开关。", + +

+ {t("codexConfig.upstreamFormatHint", { + defaultValue: + "供应商原生是 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(转换为 Chat Completions)。", + })}

- + + {/* 需要本地路由映射 —— 纯模型映射门控,与上游格式无关 */} +
+
+ + {t("codexConfig.localRoutingToggle", { + defaultValue: "需要本地路由映射", + })} + +

+ {takeoverEnabled + ? t("codexConfig.localRoutingOnHint", { + defaultValue: + "打开后可在下方配置模型映射:让 Codex 的 /model 菜单显示自定义模型名,并把请求映射到真实上游模型。", + }) + : t("codexConfig.localRoutingOffHint", { + defaultValue: + "供应商模型名无需改写、也无需在 /model 菜单展示自定义名称时,可保持关闭;需要模型映射时打开。", + })} +

+
+ +
)} - {needsLocalRouting && canEditReasoning && ( + {takeoverEnabled && isChatFormat && canEditReasoning && (
@@ -514,8 +571,9 @@ export function CodexFormFields({
- {/* 模型映射 —— 仅在本地路由 + 可编辑时显示;上方恒有 UA 字段,分隔线无需条件 */} - {needsLocalRouting && canEditCatalog && ( + {/* 模型映射 —— 仅在本地路由开启 + 可编辑时显示(与上游格式解耦, + Responses 原生供应商同样可配置);上方恒有 UA 字段,分隔线无需条件 */} + {takeoverEnabled && canEditCatalog && (
diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 89aa92dcb..f28545385 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -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(() => { - 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(initialCodexApiFormat); + + // 本地路由(接管)开关 —— 纯模型映射门控,与上游格式完全独立。 + // 没有独立持久化字段,初值仅按「是否已配置模型映射」推断(有 catalog 即视为 + // 接管已开)。只在 useState 初始化与预设重置点设置,跟 localCodexApiFormat + // 对称,避免漂移。 + const [codexTakeoverEnabled, setCodexTakeoverEnabled] = useState( + () => 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} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 32c959a60..ff3c4911c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1262,8 +1262,12 @@ "modelNameHint": "Specify the model to use, will be auto-updated in config.toml", "modelName": "Model Name", "localRoutingToggle": "Needs Local Routing", - "localRoutingOffHint": "If your provider is not native OpenAI Responses API, or the model name is not Codex's default GPT series, please enable this switch.", - "localRoutingOnHint": "Codex currently only natively supports OpenAI Responses API and GPT series models. If your provider uses the Chat Completions protocol or non-GPT models (such as DeepSeek or Kimi), enable this switch and keep local routing running while in use.", + "localRoutingOffHint": "Keep off when the provider's model names need no rewriting and you don't need custom names shown in the /model menu; enable it when you need model mapping.", + "localRoutingOnHint": "When on, configure model mapping below: show custom model names in Codex's /model menu and map requests to the real upstream model.", + "upstreamFormatLabel": "Upstream Format", + "upstreamFormatHint": "Pick Responses when your provider is natively a Responses API (direct, no format conversion); pick Chat when it uses the Chat Completions protocol (converts to Chat Completions).", + "upstreamFormatChat": "Chat Completions (convert)", + "upstreamFormatResponses": "Responses (native)", "upstreamModelName": "Upstream Model Name", "upstreamModelNameHint": "For Chat format, enter the real upstream model here; routing converts Codex Responses requests to Chat Completions while keeping this model.", "modelNamePlaceholder": "e.g., gpt-5-codex", @@ -1291,7 +1295,7 @@ "reasoningEffortHint": "Enable when the upstream supports thinking-depth control such as low/high/max. Enabling this also turns on thinking mode and converts Codex's reasoning.effort into the upstream Chat parameter.", "reasoningGroupTitle": "Reasoning Capability", "reasoningSectionHint": "Preset providers are configured automatically; custom providers are inferred from name/URL. Override manually only when auto-detection is wrong.", - "advancedSectionHint": "Includes local routing, model mapping, reasoning overrides and custom User-Agent. Enable local routing here when your provider uses the Chat Completions protocol or non-GPT models." + "advancedSectionHint": "Includes local routing, upstream format, model mapping, reasoning overrides and custom User-Agent. Enable local routing here when your provider uses the Chat Completions protocol or non-GPT models." }, "geminiConfig": { "envFile": "Environment Variables (.env)", @@ -2212,6 +2216,7 @@ "importSuccess": "Successfully imported {{count}} skills", "importSelected": "Import Selected ({{count}})", "noUnmanagedFound": "No skills to import found. All skills are already managed by CC Switch.", + "unmanagedAvailable": "Skills available to import", "foundIn": "Found in", "local": "Local", "uninstallConfirm": "Are you sure you want to uninstall \"{{name}}\"? This will remove the skill from all apps and create a local backup first.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 2468232f5..1287fe1a0 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1262,8 +1262,12 @@ "modelNameHint": "使用するモデルを指定します。config.toml に自動更新されます", "modelName": "モデル名", "localRoutingToggle": "ローカルルーティングが必要", - "localRoutingOffHint": "プロバイダーがネイティブの OpenAI Responses API でない場合、またはモデル名が Codex デフォルトの GPT 系列でない場合は、このスイッチを有効にしてください。", - "localRoutingOnHint": "Codex は現在、OpenAI Responses API と GPT 系列モデルのみをネイティブにサポートしています。プロバイダーが Chat Completions プロトコルや非 GPT モデル(DeepSeek、Kimi など)を使用する場合は、このスイッチを有効にし、使用中はローカルルーティングを起動したままにしてください。", + "localRoutingOffHint": "プロバイダーのモデル名を書き換える必要がなく、/model メニューにカスタム名を表示する必要もない場合はオフのままで構いません。モデルマッピングが必要なときにオンにしてください。", + "localRoutingOnHint": "オンにすると下でモデルマッピングを設定できます。Codex の /model メニューにカスタムモデル名を表示し、リクエストを実際の上流モデルにマッピングします。", + "upstreamFormatLabel": "上流フォーマット", + "upstreamFormatHint": "プロバイダーがネイティブ Responses API なら Responses を選択(直結、フォーマット変換なし)。Chat Completions プロトコルなら Chat を選択(Chat Completions へ変換)。", + "upstreamFormatChat": "Chat Completions(変換)", + "upstreamFormatResponses": "Responses(ネイティブ)", "upstreamModelName": "上流モデル名", "upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。", "modelNamePlaceholder": "例: gpt-5-codex", @@ -1291,7 +1295,7 @@ "reasoningEffortHint": "上流が low/high/max などの思考の深さの制御に対応している場合に有効化します。有効にすると思考モードも自動的にオンになり、Codex の reasoning.effort を上流の Chat パラメータに変換します。", "reasoningGroupTitle": "思考能力", "reasoningSectionHint": "プリセットの供給元は自動的に設定され、カスタム供給元は名前/URL から自動推論されます。自動識別が正しくない場合のみ手動で上書きしてください。", - "advancedSectionHint": "ローカルルーティング、モデルマッピング、思考能力、カスタム User-Agent の設定を含みます。供給元が Chat Completions プロトコルまたは GPT 以外のモデルを使用する場合は、ここでローカルルーティングを有効にしてください。" + "advancedSectionHint": "ローカルルーティング、上流フォーマット、モデルマッピング、思考能力、カスタム User-Agent の設定を含みます。供給元が Chat Completions プロトコルまたは GPT 以外のモデルを使用する場合は、ここでローカルルーティングを有効にしてください。" }, "geminiConfig": { "envFile": "環境変数 (.env)", @@ -2212,6 +2216,7 @@ "importSuccess": "{{count}} 件のスキルをインポートしました", "importSelected": "選択をインポート ({{count}})", "noUnmanagedFound": "インポートするスキルが見つかりませんでした。すべてのスキルは CC Switch で管理されています。", + "unmanagedAvailable": "インポート可能なスキルがあります", "foundIn": "発見場所", "local": "ローカル", "uninstallConfirm": "「{{name}}」をアンインストールしますか?すべてのアプリからこのスキルが削除され、削除前にローカルバックアップが自動作成されます。", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 7f1436436..2184fdd6c 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1239,8 +1239,12 @@ "autoCompactLimitHint": "上下文 token 數達到此閾值時自動壓縮歷史", "autoCompactLimitPlaceholder": "例如: 90000", "localRoutingToggle": "需要本地路由映射", - "localRoutingOnHint": "Codex 目前僅原生支援 OpenAI Responses API 與 GPT 系列模型;如果您的供應商使用 Chat Completions 協定或非 GPT 模型(如 DeepSeek、Kimi),則需要打開本開關,並在使用過程中保持本地路由開啟。", - "localRoutingOffHint": "如果您的供應商不是原生 OpenAI Responses API,或者模型名不是 Codex 預設的 GPT 系列,請打開此開關。", + "localRoutingOnHint": "打開後可在下方設定模型映射:讓 Codex 的 /model 選單顯示自訂模型名,並把請求映射到真實上游模型。", + "localRoutingOffHint": "供應商模型名無需改寫、也無需在 /model 選單顯示自訂名稱時,可保持關閉;需要模型映射時打開。", + "upstreamFormatLabel": "上游格式", + "upstreamFormatHint": "供應商原生為 Responses API 就選 Responses(直連,不轉換格式);使用 Chat Completions 協定就選 Chat(轉換為 Chat Completions)。", + "upstreamFormatChat": "Chat Completions(轉換)", + "upstreamFormatResponses": "Responses(原生)", "modelMappingTitle": "模型映射", "modelMappingHint": "產生 Codex model_catalog_json,讓 /model 指令顯示這些第三方模型名;表中條目按填寫內容原樣儲存。修改後需要重新啟動 Codex 才能重新整理模型清單。", "addCatalogModel": "新增模型", @@ -1263,7 +1267,7 @@ "reasoningEffortHint": "上游支援 low/high/max 等思考深度控制時啟用。啟用後會自動啟用思考模式,並把 Codex 的 reasoning.effort 轉成上游 Chat 參數。", "reasoningGroupTitle": "思考能力", "reasoningSectionHint": "預設供應商已自動設定;自訂供應商會按名稱/位址自動推斷。僅當自動識別不準時才需手動覆寫。", - "advancedSectionHint": "包含本地路由映射、模型映射、思考能力與自訂 User-Agent。供應商使用 Chat Completions 協定或非 GPT 模型時,需在此開啟本地路由映射。" + "advancedSectionHint": "包含本地路由映射、上游格式、模型映射、思考能力與自訂 User-Agent。供應商使用 Chat Completions 協定或非 GPT 模型時,需在此開啟本地路由映射。" }, "geminiConfig": { "envFile": "環境變數 (.env)", @@ -2184,6 +2188,7 @@ "importSuccess": "成功匯入 {{count}} 個技能", "importSelected": "匯入已選 ({{count}})", "noUnmanagedFound": "未發現需要匯入的技能。所有技能已在 CC Switch 統一管理中。", + "unmanagedAvailable": "發現可匯入的技能", "foundIn": "發現於", "local": "本地", "uninstallConfirm": "確定要解除安裝技能 \"{{name}}\" 嗎?這將從所有應用程式中移除該技能,並在刪除前自動建立本地備份。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a6570f626..caff6ea5b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1262,8 +1262,12 @@ "modelNameHint": "指定使用的模型,将自动更新到 config.toml 中", "modelName": "模型名称", "localRoutingToggle": "需要本地路由映射", - "localRoutingOffHint": "如果您的供应商不是原生 OpenAI Responses API,或者模型名不是 Codex 默认的 GPT 系列,请打开此开关。", - "localRoutingOnHint": "Codex 目前仅原生支持 OpenAI Responses API 与 GPT 系列模型;如果您的供应商使用 Chat Completions 协议或非 GPT 模型(如 DeepSeek、Kimi),则需要打开本开关,并在使用过程中保持本地路由开启。", + "localRoutingOffHint": "供应商模型名无需改写、也无需在 /model 菜单展示自定义名称时,可保持关闭;需要模型映射时打开。", + "localRoutingOnHint": "打开后可在下方配置模型映射:让 Codex 的 /model 菜单显示自定义模型名,并把请求映射到真实上游模型。", + "upstreamFormatLabel": "上游格式", + "upstreamFormatHint": "供应商原生为 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(转换为 Chat Completions)。", + "upstreamFormatChat": "Chat Completions(转换)", + "upstreamFormatResponses": "Responses(原生)", "upstreamModelName": "上游模型名称", "upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。", "modelNamePlaceholder": "例如: gpt-5-codex", @@ -1291,7 +1295,7 @@ "reasoningEffortHint": "上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。", "reasoningGroupTitle": "思考能力", "reasoningSectionHint": "预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需手动覆盖。", - "advancedSectionHint": "包含本地路由映射、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。" + "advancedSectionHint": "包含本地路由映射、上游格式、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。" }, "geminiConfig": { "envFile": "环境变量 (.env)", @@ -2212,6 +2216,7 @@ "importSuccess": "成功导入 {{count}} 个技能", "importSelected": "导入已选 ({{count}})", "noUnmanagedFound": "未发现需要导入的技能。所有技能已在 CC Switch 统一管理中。", + "unmanagedAvailable": "发现可导入的技能", "foundIn": "发现于", "local": "本地", "uninstallConfirm": "确定要卸载技能 \"{{name}}\" 吗?这将从所有应用中移除该技能,并在删除前自动创建本地备份。", diff --git a/tests/config/codexChatProviderPresets.test.ts b/tests/config/codexChatProviderPresets.test.ts index 8986caf3c..506054cc9 100644 --- a/tests/config/codexChatProviderPresets.test.ts +++ b/tests/config/codexChatProviderPresets.test.ts @@ -28,7 +28,7 @@ const expectedChatPresets = new Map< "DouBaoSeed", { baseUrl: "https://ark.cn-beijing.volces.com/api/v3", - contextWindows: { "doubao-seed-2-0-code-preview-latest": 256000 }, + contextWindows: { "doubao-seed-2-1-pro": 256000 }, }, ], [