feat(provider-form): consolidate codex form into advanced options section

- Fold local routing toggle, model mapping, reasoning overrides and custom
  User-Agent into a single collapsible advanced section, mirroring the
  Claude form (auto-expands when UA is set or local routing is enabled)
- Custom User-Agent becomes configurable for native Responses providers;
  it was previously reachable only when openai_chat routing was on
- Collapsed hint names local routing as the entry point for Chat
  Completions / non-GPT providers
- Backfill all missing codexConfig keys in zh-TW locale
This commit is contained in:
Jason
2026-06-10 21:32:26 +08:00
Unverified
parent 596019505f
commit e776160912
5 changed files with 299 additions and 220 deletions
+262 -213
View File
@@ -26,6 +26,7 @@ import {
type FetchedModel,
} from "@/lib/api/model-fetch";
import { CustomUserAgentField } from "./CustomUserAgentField";
import { cn } from "@/lib/utils";
import type {
CodexApiFormat,
CodexCatalogModel,
@@ -140,7 +141,6 @@ export function CodexFormFields({
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
const [reasoningExpanded, setReasoningExpanded] = useState(false);
const needsLocalRouting = apiFormat === "openai_chat";
const canEditCatalog = Boolean(onCatalogModelsChange);
const canEditReasoning = Boolean(onCodexChatReasoningChange);
@@ -149,6 +149,17 @@ export function CodexFormFields({
codexChatReasoning.supportsEffort === true;
const supportsEffort = codexChatReasoning.supportsEffort === true;
// needsLocalRouting 非默认值说明预设/用户动过路由配置,需要让模型映射保持可见
const hasAnyAdvancedValue = !!customUserAgent || needsLocalRouting;
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
// 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠)
useEffect(() => {
if (hasAnyAdvancedValue) {
setAdvancedExpanded(true);
}
}, [hasAnyAdvancedValue]);
const [catalogRows, setCatalogRows] = useState<CodexCatalogRow[]>(() =>
catalogModels.map((m) => createCatalogRow(m)),
);
@@ -334,42 +345,11 @@ export function CodexFormFields({
/>
)}
{shouldShowSpeedTest && (
<div className="space-y-3 rounded-lg border border-border-default bg-muted/20 p-4">
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{needsLocalRouting
? t("codexConfig.localRoutingOnHint", {
defaultValue:
"Codex 目前仅原生支持 OpenAI Responses API 与 GPT 系列模型;如果您的供应商使用 Chat Completions 协议或非 GPT 模型(如 DeepSeek、Kimi),则需要打开本开关,并在使用过程中保持本地路由开启。",
})
: t("codexConfig.localRoutingOffHint", {
defaultValue:
"如果您的供应商不是原生 OpenAI Responses API,或者模型名不是 Codex 默认的 GPT 系列,请打开此开关。",
})}
</p>
</div>
<Switch
checked={needsLocalRouting}
onCheckedChange={handleLocalRoutingChange}
aria-label={t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
/>
</div>
</div>
)}
{needsLocalRouting && canEditReasoning && (
{/* 高级选项 —— 本地路由映射/模型映射/思考能力/自定义 UA;预设供应商通常无需展开 */}
{category !== "official" && (
<Collapsible
open={reasoningExpanded}
onOpenChange={setReasoningExpanded}
open={advancedExpanded}
onOpenChange={setAdvancedExpanded}
className="rounded-lg border border-border-default p-4"
>
<CollapsibleTrigger asChild>
@@ -379,213 +359,282 @@ export function CodexFormFields({
size="sm"
className="h-8 w-full justify-start gap-1.5 px-0 text-sm font-medium text-foreground hover:opacity-70"
>
{reasoningExpanded ? (
{advancedExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
{t("codexConfig.reasoningSectionToggle", {
defaultValue: "思考能力(高级·通常自动识别)",
{t("providerForm.advancedOptionsToggle", {
defaultValue: "高级选项",
})}
</Button>
</CollapsibleTrigger>
{!reasoningExpanded && (
{!advancedExpanded && (
<p className="mt-1 ml-1 text-xs text-muted-foreground">
{t("codexConfig.reasoningSectionHint", {
{t("codexConfig.advancedSectionHint", {
defaultValue:
"预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需展开手动覆盖。",
"包含本地路由映射、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。",
})}
</p>
)}
<CollapsibleContent className="space-y-3 pt-3">
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.reasoningModeToggle", {
defaultValue: "支持思考模式",
{/* 本地路由映射开关 —— 沿用 shouldShowSpeedTest 门控,cloud_provider 保持不可切换 */}
{shouldShowSpeedTest && (
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{needsLocalRouting
? t("codexConfig.localRoutingOnHint", {
defaultValue:
"Codex 目前仅原生支持 OpenAI Responses API 与 GPT 系列模型;如果您的供应商使用 Chat Completions 协议或非 GPT 模型(如 DeepSeek、Kimi),则需要打开本开关,并在使用过程中保持本地路由开启。",
})
: t("codexConfig.localRoutingOffHint", {
defaultValue:
"如果您的供应商不是原生 OpenAI Responses API,或者模型名不是 Codex 默认的 GPT 系列,请打开此开关。",
})}
</p>
</div>
<Switch
checked={needsLocalRouting}
onCheckedChange={handleLocalRoutingChange}
aria-label={t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.reasoningModeHint", {
defaultValue:
"上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
})}
</p>
/>
</div>
<Switch
checked={supportsThinking}
onCheckedChange={handleReasoningThinkingChange}
aria-label={t("codexConfig.reasoningModeToggle", {
defaultValue: "支持思考模式",
})}
/>
</div>
)}
<div className="flex items-center justify-between gap-4 border-t border-border-default pt-3">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.reasoningEffortToggle", {
defaultValue: "支持思考等级",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.reasoningEffortHint", {
defaultValue:
"上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
})}
</p>
{needsLocalRouting && canEditReasoning && (
<div
className={cn(
"space-y-3",
shouldShowSpeedTest && "border-t border-border-default pt-3",
)}
>
<div className="space-y-1">
<FormLabel>
{t("codexConfig.reasoningGroupTitle", {
defaultValue: "思考能力",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.reasoningSectionHint", {
defaultValue:
"预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需手动覆盖。",
})}
</p>
</div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.reasoningModeToggle", {
defaultValue: "支持思考模式",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.reasoningModeHint", {
defaultValue:
"上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
})}
</p>
</div>
<Switch
checked={supportsThinking}
onCheckedChange={handleReasoningThinkingChange}
aria-label={t("codexConfig.reasoningModeToggle", {
defaultValue: "支持思考模式",
})}
/>
</div>
<div className="flex items-center justify-between gap-4 border-t border-border-default pt-3">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.reasoningEffortToggle", {
defaultValue: "支持思考等级",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.reasoningEffortHint", {
defaultValue:
"上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
})}
</p>
</div>
<Switch
checked={supportsEffort}
onCheckedChange={handleReasoningEffortChange}
aria-label={t("codexConfig.reasoningEffortToggle", {
defaultValue: "支持思考等级",
})}
/>
</div>
</div>
<Switch
checked={supportsEffort}
onCheckedChange={handleReasoningEffortChange}
aria-label={t("codexConfig.reasoningEffortToggle", {
defaultValue: "支持思考等级",
})}
/>
</div>
)}
<div className="border-t border-border-default pt-3">
<div
className={cn(
(shouldShowSpeedTest ||
(needsLocalRouting && canEditReasoning)) &&
"border-t border-border-default pt-3",
)}
>
<CustomUserAgentField
id="codex-custom-user-agent"
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
</div>
</CollapsibleContent>
</Collapsible>
)}
{/* Codex 模型映射 —— 仅在本地路由 + 可编辑时显示 */}
{needsLocalRouting && canEditCatalog && (
<div className="space-y-4 rounded-lg border border-border-default p-4">
<div className="space-y-1">
<div className="flex items-center justify-between gap-3">
<FormLabel>
{t("codexConfig.modelMappingTitle", {
defaultValue: "模型映射",
})}
</FormLabel>
{renderCatalogActionButtons(
handleAddCatalogRow,
t("codexConfig.addCatalogModel", {
defaultValue: "添加模型",
}),
)}
</div>
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.modelMappingHint", {
defaultValue:
"选择模型角色后,CC Switch 会自动生成 Codex 兼容路由;菜单显示名可以填 DeepSeek、Kimi 等品牌模型,实际请求模型按右侧填写内容发送。",
})}
</p>
</div>
{catalogRows.length > 0 && (
<div className="space-y-2">
{/* 列头:md+ 显示 */}
<div className="hidden grid-cols-[1fr_1fr_140px_36px] gap-2 px-1 text-xs font-medium text-muted-foreground md:grid">
<span>
{t("codexConfig.catalogColumnDisplay", {
defaultValue: "菜单显示名",
})}
</span>
<span>
{t("codexConfig.catalogColumnModel", {
defaultValue: "实际请求模型",
})}
</span>
<span>
{t("codexConfig.catalogColumnContext", {
defaultValue: "上下文窗口",
})}
</span>
<span />
</div>
{catalogRows.map((row, index) => (
<div
key={row.rowId}
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_140px_36px]"
>
<Input
value={row.displayName ?? ""}
onChange={(event) =>
handleUpdateCatalogRow(index, {
displayName: event.target.value,
})
}
placeholder={t(
"codexConfig.catalogDisplayNamePlaceholder",
{
defaultValue: "例如: DeepSeek V4 Flash",
},
)}
aria-label={t("codexConfig.catalogColumnDisplay", {
defaultValue: "菜单显示名",
})}
/>
<div className="flex gap-1">
<Input
value={row.model}
onChange={(event) =>
handleUpdateCatalogRow(index, {
model: event.target.value,
})
}
placeholder={t("codexConfig.catalogModelPlaceholder", {
defaultValue: "例如: deepseek-v4-flash",
{/* 模型映射 —— 仅在本地路由 + 可编辑时显示;上方恒有 UA 字段,分隔线无需条件 */}
{needsLocalRouting && canEditCatalog && (
<div className="space-y-4 border-t border-border-default pt-3">
<div className="space-y-1">
<div className="flex items-center justify-between gap-3">
<FormLabel>
{t("codexConfig.modelMappingTitle", {
defaultValue: "模型映射",
})}
aria-label={t("codexConfig.catalogColumnModel", {
defaultValue: "实际请求模型",
})}
className="flex-1"
/>
{fetchedModels.length > 0 && (
<ModelDropdown
models={fetchedModels}
onSelect={(id) =>
handleUpdateCatalogRow(index, {
model: id,
displayName: row.displayName?.trim()
? row.displayName
: id,
})
}
/>
</FormLabel>
{renderCatalogActionButtons(
handleAddCatalogRow,
t("codexConfig.addCatalogModel", {
defaultValue: "添加模型",
}),
)}
</div>
<Input
type="number"
min={1}
inputMode="numeric"
value={row.contextWindow ?? ""}
onChange={(event) =>
handleUpdateCatalogRow(index, {
contextWindow: event.target.value.replace(/[^\d]/g, ""),
})
}
placeholder={t("codexConfig.contextWindowPlaceholder", {
defaultValue: "例如: 128000",
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.modelMappingHint", {
defaultValue:
"选择模型角色后,CC Switch 会自动生成 Codex 兼容路由;菜单显示名可以填 DeepSeek、Kimi 等品牌模型,实际请求模型按右侧填写内容发送。",
})}
aria-label={t("codexConfig.catalogColumnContext", {
defaultValue: "上下文窗口",
})}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-muted-foreground hover:text-destructive"
onClick={() => handleRemoveCatalogRow(index)}
title={t("common.delete", { defaultValue: "删除" })}
>
<Trash2 className="h-4 w-4" />
</Button>
</p>
</div>
))}
</div>
)}
</div>
{catalogRows.length > 0 && (
<div className="space-y-2">
{/* 列头:md+ 显示 */}
<div className="hidden grid-cols-[1fr_1fr_140px_36px] gap-2 px-1 text-xs font-medium text-muted-foreground md:grid">
<span>
{t("codexConfig.catalogColumnDisplay", {
defaultValue: "菜单显示名",
})}
</span>
<span>
{t("codexConfig.catalogColumnModel", {
defaultValue: "实际请求模型",
})}
</span>
<span>
{t("codexConfig.catalogColumnContext", {
defaultValue: "上下文窗口",
})}
</span>
<span />
</div>
{catalogRows.map((row, index) => (
<div
key={row.rowId}
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_140px_36px]"
>
<Input
value={row.displayName ?? ""}
onChange={(event) =>
handleUpdateCatalogRow(index, {
displayName: event.target.value,
})
}
placeholder={t(
"codexConfig.catalogDisplayNamePlaceholder",
{
defaultValue: "例如: DeepSeek V4 Flash",
},
)}
aria-label={t("codexConfig.catalogColumnDisplay", {
defaultValue: "菜单显示名",
})}
/>
<div className="flex gap-1">
<Input
value={row.model}
onChange={(event) =>
handleUpdateCatalogRow(index, {
model: event.target.value,
})
}
placeholder={t(
"codexConfig.catalogModelPlaceholder",
{
defaultValue: "例如: deepseek-v4-flash",
},
)}
aria-label={t("codexConfig.catalogColumnModel", {
defaultValue: "实际请求模型",
})}
className="flex-1"
/>
{fetchedModels.length > 0 && (
<ModelDropdown
models={fetchedModels}
onSelect={(id) =>
handleUpdateCatalogRow(index, {
model: id,
displayName: row.displayName?.trim()
? row.displayName
: id,
})
}
/>
)}
</div>
<Input
type="number"
min={1}
inputMode="numeric"
value={row.contextWindow ?? ""}
onChange={(event) =>
handleUpdateCatalogRow(index, {
contextWindow: event.target.value.replace(
/[^\d]/g,
"",
),
})
}
placeholder={t(
"codexConfig.contextWindowPlaceholder",
{
defaultValue: "例如: 128000",
},
)}
aria-label={t("codexConfig.catalogColumnContext", {
defaultValue: "上下文窗口",
})}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-muted-foreground hover:text-destructive"
onClick={() => handleRemoveCatalogRow(index)}
title={t("common.delete", { defaultValue: "删除" })}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
)}
</CollapsibleContent>
</Collapsible>
)}
{/* 端点测速弹窗 - Codex */}
+3 -2
View File
@@ -1261,8 +1261,9 @@
"reasoningModeHint": "Enable when the upstream Chat Completions API supports toggling thinking on/off. Providers like Kimi, GLM and Qwen usually fall into this category.",
"reasoningEffortToggle": "Supports Reasoning Effort",
"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.",
"reasoningSectionToggle": "Reasoning Capability (Advanced · usually auto-detected)",
"reasoningSectionHint": "Preset providers are configured automatically; custom providers are inferred from name/URL. Expand to override manually only when auto-detection is wrong."
"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."
},
"geminiConfig": {
"envFile": "Environment Variables (.env)",
+3 -2
View File
@@ -1261,8 +1261,9 @@
"reasoningModeHint": "上流の Chat Completions API が思考(thinking)のオン/オフ切り替えに対応している場合に有効化します。Kimi、GLM、Qwen などが通常このタイプに該当します。",
"reasoningEffortToggle": "思考レベルに対応",
"reasoningEffortHint": "上流が low/high/max などの思考の深さの制御に対応している場合に有効化します。有効にすると思考モードも自動的にオンになり、Codex の reasoning.effort を上流の Chat パラメータに変換します。",
"reasoningSectionToggle": "思考能力(詳細・通常は自動識別)",
"reasoningSectionHint": "プリセットの供給元は自動的に設定され、カスタム供給元は名前/URL から自動推論されます。自動識別が正しくない場合のみ展開して手動で上書きしてください。"
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "プリセットの供給元は自動的に設定され、カスタム供給元は名前/URL から自動推論されます。自動識別が正しくない場合のみ手動で上書きしてください。",
"advancedSectionHint": "ローカルルーティング、モデルマッピング、思考能力、カスタム User-Agent の設定を含みます。供給元が Chat Completions プロトコルまたは GPT 以外のモデルを使用する場合は、ここでローカルルーティングを有効にしてください。"
},
"geminiConfig": {
"envFile": "環境変数 (.env)",
+28 -1
View File
@@ -1207,7 +1207,34 @@
"modelNamePlaceholder": "例如: gpt-5-codex",
"contextWindow1M": "1M 上下文視窗",
"autoCompactLimit": "壓縮閾值",
"autoCompactLimitHint": "上下文 token 數達到此閾值時自動壓縮歷史"
"autoCompactLimitHint": "上下文 token 數達到此閾值時自動壓縮歷史",
"autoCompactLimitPlaceholder": "例如: 90000",
"localRoutingToggle": "需要本地路由映射",
"localRoutingOnHint": "Codex 目前僅原生支援 OpenAI Responses API 與 GPT 系列模型;如果您的供應商使用 Chat Completions 協定或非 GPT 模型(如 DeepSeek、Kimi),則需要打開本開關,並在使用過程中保持本地路由開啟。",
"localRoutingOffHint": "如果您的供應商不是原生 OpenAI Responses API,或者模型名不是 Codex 預設的 GPT 系列,請打開此開關。",
"modelMappingTitle": "模型映射",
"modelMappingHint": "產生 Codex model_catalog_json,讓 /model 指令顯示這些第三方模型名;表中條目按填寫內容原樣儲存。修改後需要重新啟動 Codex 才能重新整理模型清單。",
"addCatalogModel": "新增模型",
"catalogDisplayName": "顯示名稱",
"catalogModelId": "模型 ID",
"catalogColumnDisplay": "選單顯示名",
"catalogColumnModel": "實際請求模型",
"catalogColumnContext": "上下文視窗",
"catalogDisplayNamePlaceholder": "例如: DeepSeek V4 Flash",
"catalogModelPlaceholder": "例如: deepseek-v4-flash",
"contextWindow": "上下文視窗",
"contextWindowPlaceholder": "例如: 128000",
"upstreamModelName": "上游模型名稱",
"upstreamModelNameHint": "Chat 格式下這裡填寫真實上游模型;路由會把 Codex 的 Responses 請求轉換為 Chat Completions 並保持該模型。",
"modelMetadataAdvanced": "模型中繼資料進階選項",
"modelMetadataAdvancedHint": "用於未知模型的 Codex 中繼資料覆寫;清空欄位會從 config.toml 刪除對應設定。",
"reasoningModeToggle": "支援思考模式",
"reasoningModeHint": "上游 Chat Completions 介面支援開啟或關閉 thinking 時啟用。Kimi、GLM、Qwen 等通常屬於這一類。",
"reasoningEffortToggle": "支援思考等級",
"reasoningEffortHint": "上游支援 low/high/max 等思考深度控制時啟用。啟用後會自動啟用思考模式,並把 Codex 的 reasoning.effort 轉成上游 Chat 參數。",
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "預設供應商已自動設定;自訂供應商會按名稱/位址自動推斷。僅當自動識別不準時才需手動覆寫。",
"advancedSectionHint": "包含本地路由映射、模型映射、思考能力與自訂 User-Agent。供應商使用 Chat Completions 協定或非 GPT 模型時,需在此開啟本地路由映射。"
},
"geminiConfig": {
"envFile": "環境變數 (.env)",
+3 -2
View File
@@ -1261,8 +1261,9 @@
"reasoningModeHint": "上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
"reasoningEffortToggle": "支持思考等级",
"reasoningEffortHint": "上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
"reasoningSectionToggle": "思考能力(高级·通常自动识别)",
"reasoningSectionHint": "预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需展开手动覆盖。"
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需手动覆盖。",
"advancedSectionHint": "包含本地路由映射、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。"
},
"geminiConfig": {
"envFile": "环境变量 (.env)",