feat: adaptive reasoning detection for Codex Chat providers

Auto-detect each Chat-routed Codex provider's reasoning interface from
its name, base URL, and model, then inject the matching thinking
parameter without manual configuration:

- Platform-first inference (OpenRouter, SiliconFlow) overrides model
  rules, since the same model exposes different reasoning controls
  depending on the hosting platform.
- Effort tiers are forwarded only to providers that support them
  (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
  providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
  instead of sending a field the upstream rejects.
- OpenRouter uses the native reasoning:{effort} object, clamps max to
  xhigh (its enum has no max), and forwards an explicit effort:"none" so
  reasoning can be turned off.
- StepFun falls back to inference so per-model effort support is honored
  (the static preset would have forced effort on step-3.5-flash too).

Includes the Codex provider-form reasoning controls, i18n strings
(zh/en/ja), and response-side reasoning extraction.
This commit is contained in:
Jason
2026-05-21 22:29:18 +08:00
parent 5048ed632d
commit 44d9aabbf3
13 changed files with 1058 additions and 16 deletions
@@ -4,8 +4,20 @@ 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 {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { toast } from "sonner";
import { Download, Loader2, Plus, Trash2 } from "lucide-react";
import {
ChevronDown,
ChevronRight,
Download,
Loader2,
Plus,
Trash2,
} from "lucide-react";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField, ModelDropdown } from "./shared";
import {
@@ -16,6 +28,7 @@ import {
import type {
CodexApiFormat,
CodexCatalogModel,
CodexChatReasoning,
ProviderCategory,
} from "@/types";
@@ -50,6 +63,8 @@ interface CodexFormFieldsProps {
// Note: wire_api is always "responses" for Codex; apiFormat controls proxy-layer conversion
apiFormat: CodexApiFormat;
onApiFormatChange: (format: CodexApiFormat) => void;
codexChatReasoning?: CodexChatReasoning;
onCodexChatReasoningChange?: (value: CodexChatReasoning) => void;
// Model Catalog
catalogModels?: CodexCatalogModel[];
@@ -108,6 +123,8 @@ export function CodexFormFields({
onAutoSelectChange,
apiFormat,
onApiFormatChange,
codexChatReasoning = {},
onCodexChatReasoningChange,
catalogModels = [],
onCatalogModelsChange,
speedTestEndpoints,
@@ -116,8 +133,14 @@ 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);
const supportsThinking =
codexChatReasoning.supportsThinking === true ||
codexChatReasoning.supportsEffort === true;
const supportsEffort = codexChatReasoning.supportsEffort === true;
const [catalogRows, setCatalogRows] = useState<CodexCatalogRow[]>(() =>
catalogModels.map((m) => createCatalogRow(m)),
@@ -157,6 +180,33 @@ export function CodexFormFields({
[onApiFormatChange],
);
const handleReasoningThinkingChange = useCallback(
(checked: boolean) => {
if (!onCodexChatReasoningChange) return;
onCodexChatReasoningChange({
...codexChatReasoning,
supportsThinking: checked,
supportsEffort: checked ? codexChatReasoning.supportsEffort : false,
});
},
[codexChatReasoning, onCodexChatReasoningChange],
);
const handleReasoningEffortChange = useCallback(
(checked: boolean) => {
if (!onCodexChatReasoningChange) return;
onCodexChatReasoningChange({
...codexChatReasoning,
supportsThinking: checked ? true : codexChatReasoning.supportsThinking,
supportsEffort: checked,
effortParam: checked
? (codexChatReasoning.effortParam ?? "reasoning_effort")
: "none",
});
},
[codexChatReasoning, onCodexChatReasoningChange],
);
const handleFetchModels = useCallback(() => {
if (!codexBaseUrl || !codexApiKey) {
showFetchModelsError(null, t, {
@@ -303,6 +353,87 @@ export function CodexFormFields({
</div>
)}
{needsLocalRouting && canEditReasoning && (
<Collapsible
open={reasoningExpanded}
onOpenChange={setReasoningExpanded}
className="rounded-lg border border-border-default p-4"
>
<CollapsibleTrigger asChild>
<Button
type="button"
variant={null}
size="sm"
className="h-8 w-full justify-start gap-1.5 px-0 text-sm font-medium text-foreground hover:opacity-70"
>
{reasoningExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
{t("codexConfig.reasoningSectionToggle", {
defaultValue: "思考能力(高级·通常自动识别)",
})}
</Button>
</CollapsibleTrigger>
{!reasoningExpanded && (
<p className="mt-1 ml-1 text-xs text-muted-foreground">
{t("codexConfig.reasoningSectionHint", {
defaultValue:
"预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需展开手动覆盖。",
})}
</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: "支持思考模式",
})}
</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>
</CollapsibleContent>
</Collapsible>
)}
{/* Codex 模型映射 —— 仅在本地路由 + 可编辑时显示 */}
{needsLocalRouting && canEditCatalog && (
<div className="space-y-4 rounded-lg border border-border-default p-4">
@@ -16,6 +16,7 @@ import type {
ClaudeApiFormat,
CodexApiFormat,
CodexCatalogModel,
CodexChatReasoning,
ClaudeApiKeyField,
} from "@/types";
import {
@@ -174,6 +175,41 @@ export const normalizeCodexCatalogModelsForSave = (
return normalized;
};
const normalizeCodexChatReasoningForSave = (
value?: CodexChatReasoning,
): CodexChatReasoning | undefined => {
const supportsEffort = value?.supportsEffort === true;
const supportsThinking = value?.supportsThinking === true || supportsEffort;
const hasExplicitConfig = value && Object.keys(value).length > 0;
if (!supportsThinking && !supportsEffort) {
return hasExplicitConfig
? {
supportsThinking: false,
supportsEffort: false,
thinkingParam: "none",
effortParam: "none",
outputFormat: value?.outputFormat ?? "auto",
}
: undefined;
}
return {
supportsThinking,
supportsEffort,
thinkingParam: supportsThinking
? (value?.thinkingParam ?? "thinking")
: "none",
effortParam: supportsEffort
? (value?.effortParam ?? "reasoning_effort")
: "none",
effortValueMode: supportsEffort
? (value?.effortValueMode ?? "passthrough")
: undefined,
outputFormat: value?.outputFormat ?? "auto",
};
};
export interface ProviderFormProps {
appId: AppId;
providerId?: string;
@@ -316,6 +352,7 @@ function ProviderFormFull({
initialData?.meta?.pricingModelSource,
),
});
setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {});
}, [appId, initialData, supportsFullUrl]);
const defaultValues: ProviderFormData = useMemo(
@@ -466,6 +503,10 @@ function ProviderFormFull({
const [codexFastMode, setCodexFastMode] = useState<boolean>(
() => initialData?.meta?.codexFastMode ?? false,
);
const [codexChatReasoning, setCodexChatReasoning] =
useState<CodexChatReasoning>(
() => initialData?.meta?.codexChatReasoning ?? {},
);
const {
codexAuth,
@@ -530,6 +571,7 @@ function ProviderFormFull({
if (appId === "codex" && !initialData && selectedPresetId === "custom") {
const template = getCodexCustomTemplate();
resetCodexConfig(template.auth, template.config);
setCodexChatReasoning({});
}
}, [appId, initialData, selectedPresetId, resetCodexConfig]);
@@ -1336,6 +1378,12 @@ function ProviderFormFull({
? selectedGitHubAccountId
: undefined,
codexFastMode: isCodexOauthProvider ? codexFastMode : undefined,
codexChatReasoning:
appId === "codex" &&
category !== "official" &&
localCodexApiFormat === "openai_chat"
? normalizeCodexChatReasoningForSave(codexChatReasoning)
: undefined,
testConfig: testConfig.enabled ? testConfig : undefined,
costMultiplier: pricingConfig.enabled
? pricingConfig.costMultiplier
@@ -1473,6 +1521,7 @@ function ProviderFormFull({
if (appId === "codex") {
const template = getCodexCustomTemplate();
resetCodexConfig(template.auth, template.config);
setCodexChatReasoning({});
setLocalCodexApiFormat(
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
"openai_responses",
@@ -1513,6 +1562,7 @@ function ProviderFormFull({
const config = preset.config ?? "";
resetCodexConfig(auth, config, preset.modelCatalog ?? []);
setCodexChatReasoning(preset.codexChatReasoning ?? {});
setLocalCodexApiFormat(
preset.apiFormat ??
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
@@ -1984,6 +2034,8 @@ function ProviderFormFull({
onAutoSelectChange={setEndpointAutoSelect}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
codexChatReasoning={codexChatReasoning}
onCodexChatReasoningChange={setCodexChatReasoning}
catalogModels={codexCatalogModels}
onCatalogModelsChange={setCodexCatalogModels}
speedTestEndpoints={speedTestEndpoints}
+92 -1
View File
@@ -2,7 +2,11 @@
* Codex 预设供应商配置模板
*/
import { ProviderCategory } from "../types";
import type { CodexApiFormat, CodexCatalogModel } from "../types";
import type {
CodexApiFormat,
CodexCatalogModel,
CodexChatReasoning,
} from "../types";
import type { PresetTheme } from "./claudeProviderPresets";
export interface CodexProviderPreset {
@@ -29,6 +33,8 @@ export interface CodexProviderPreset {
apiFormat?: CodexApiFormat;
// Codex Chat 本地路由模式下的模型目录
modelCatalog?: CodexCatalogModel[];
// Codex Responses -> Chat Completions reasoning capability defaults
codexChatReasoning?: CodexChatReasoning;
}
/**
@@ -261,6 +267,14 @@ requires_openai_auth = true`,
contextWindow: 1000000,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: true,
thinkingParam: "thinking",
effortParam: "reasoning_effort",
effortValueMode: "deepseek",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "deepseek",
iconColor: "#1E88E5",
@@ -280,6 +294,13 @@ requires_openai_auth = true`,
modelCatalog: modelCatalog([
{ model: "glm-5", displayName: "GLM-5", contextWindow: 200000 },
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "zhipu",
iconColor: "#0F62FE",
@@ -299,6 +320,13 @@ requires_openai_auth = true`,
modelCatalog: modelCatalog([
{ model: "glm-5", displayName: "GLM-5", contextWindow: 200000 },
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "zhipu",
iconColor: "#0F62FE",
@@ -347,6 +375,13 @@ requires_openai_auth = true`,
},
{ model: "qwen3-max", displayName: "Qwen3 Max", contextWindow: 262144 },
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "enable_thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "bailian",
iconColor: "#624AFF",
@@ -366,6 +401,13 @@ requires_openai_auth = true`,
modelCatalog: modelCatalog([
{ model: "kimi-k2.6", displayName: "Kimi K2.6", contextWindow: 262144 },
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "kimi",
iconColor: "#6366F1",
@@ -445,6 +487,13 @@ requires_openai_auth = true`,
contextWindow: 200000,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "aggregator",
icon: "modelscope",
iconColor: "#624AFF",
@@ -491,6 +540,13 @@ requires_openai_auth = true`,
contextWindow: 200000,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "reasoning_split",
effortParam: "none",
outputFormat: "reasoning_details",
},
category: "cn_official",
isPartner: true,
partnerPromotionKey: "minimax_cn",
@@ -520,6 +576,13 @@ requires_openai_auth = true`,
contextWindow: 200000,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "reasoning_split",
effortParam: "none",
outputFormat: "reasoning_details",
},
category: "cn_official",
isPartner: true,
partnerPromotionKey: "minimax_en",
@@ -570,6 +633,13 @@ requires_openai_auth = true`,
contextWindow: 1048576,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "xiaomimimo",
iconColor: "#000000",
@@ -593,6 +663,13 @@ requires_openai_auth = true`,
contextWindow: 1048576,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "cn_official",
icon: "xiaomimimo",
iconColor: "#000000",
@@ -662,6 +739,13 @@ requires_openai_auth = true`,
modelCatalog: modelCatalog([
{ model: "zai-org/glm-5", displayName: "GLM-5", contextWindow: 202800 },
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "aggregator",
icon: "novita",
iconColor: "#000000",
@@ -685,6 +769,13 @@ requires_openai_auth = true`,
contextWindow: 262144,
},
]),
codexChatReasoning: {
supportsThinking: true,
supportsEffort: false,
thinkingParam: "thinking",
effortParam: "none",
outputFormat: "reasoning_content",
},
category: "aggregator",
icon: "nvidia",
iconColor: "#000000",
+7 -1
View File
@@ -1103,7 +1103,13 @@
"catalogModelPlaceholder": "e.g., deepseek-v4-flash",
"catalogColumnDisplay": "Menu Display Name",
"catalogColumnModel": "Actual Request Model",
"catalogColumnContext": "Context Window"
"catalogColumnContext": "Context Window",
"reasoningModeToggle": "Supports Thinking Mode",
"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."
},
"geminiConfig": {
"envFile": "Environment Variables (.env)",
+7 -1
View File
@@ -1103,7 +1103,13 @@
"catalogModelPlaceholder": "例: deepseek-v4-flash",
"catalogColumnDisplay": "メニュー表示名",
"catalogColumnModel": "実際のリクエストモデル",
"catalogColumnContext": "コンテキストウィンドウ"
"catalogColumnContext": "コンテキストウィンドウ",
"reasoningModeToggle": "思考モードに対応",
"reasoningModeHint": "上流の Chat Completions API が思考(thinking)のオン/オフ切り替えに対応している場合に有効化します。Kimi、GLM、Qwen などが通常このタイプに該当します。",
"reasoningEffortToggle": "思考レベルに対応",
"reasoningEffortHint": "上流が low/high/max などの思考の深さの制御に対応している場合に有効化します。有効にすると思考モードも自動的にオンになり、Codex の reasoning.effort を上流の Chat パラメータに変換します。",
"reasoningSectionToggle": "思考能力(詳細・通常は自動識別)",
"reasoningSectionHint": "プリセットの供給元は自動的に設定され、カスタム供給元は名前/URL から自動推論されます。自動識別が正しくない場合のみ展開して手動で上書きしてください。"
},
"geminiConfig": {
"envFile": "環境変数 (.env)",
+7 -1
View File
@@ -1103,7 +1103,13 @@
"catalogModelPlaceholder": "例如: deepseek-v4-flash",
"catalogColumnDisplay": "菜单显示名",
"catalogColumnModel": "实际请求模型",
"catalogColumnContext": "上下文窗口"
"catalogColumnContext": "上下文窗口",
"reasoningModeToggle": "支持思考模式",
"reasoningModeHint": "上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
"reasoningEffortToggle": "支持思考等级",
"reasoningEffortHint": "上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
"reasoningSectionToggle": "思考能力(高级·通常自动识别)",
"reasoningSectionHint": "预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需展开手动覆盖。"
},
"geminiConfig": {
"envFile": "环境变量 (.env)",
+38
View File
@@ -137,6 +137,42 @@ export interface ClaudeDesktopModelRoute {
supports1m?: boolean;
}
export type CodexChatThinkingParam =
| "none"
| "thinking"
| "enable_thinking"
| "reasoning_split";
export type CodexChatEffortParam =
| "none"
| "reasoning_effort"
// OpenRouter 原生归一化对象 reasoning:{effort}(区别于顶层 OpenAI 别名 reasoning_effort
| "reasoning.effort";
export type CodexChatEffortValueMode =
| "passthrough"
| "low_high"
| "deepseek"
// OpenRouter effort 枚举 xhigh|high|medium|low|minimal(无 maxmax 钳到 xhigh
| "openrouter";
export type CodexChatReasoningOutputFormat =
| "auto"
| "reasoning_content"
| "reasoning"
| "reasoning_details"
| "think_tags";
export interface CodexChatReasoning {
supportsThinking?: boolean;
supportsEffort?: boolean;
thinkingParam?: CodexChatThinkingParam;
effortParam?: CodexChatEffortParam;
effortValueMode?: CodexChatEffortValueMode;
// 声明性字段:标注上游 reasoning 回传位置。当前提取靠穷举字段,未读取此值(think_tags 尚未接线)。
outputFormat?: CodexChatReasoningOutputFormat;
}
// 供应商元数据(字段名与后端一致,保持 snake_case
export interface ProviderMeta {
// 自定义端点:以 URL 为键,值为端点信息
@@ -181,6 +217,8 @@ export interface ProviderMeta {
promptCacheKey?: string;
// Codex OAuth FAST mode: injects service_tier="priority" on ChatGPT Codex requests
codexFastMode?: boolean;
// Codex Responses -> Chat Completions reasoning capability metadata
codexChatReasoning?: CodexChatReasoning;
// 供应商类型(用于识别 Copilot 等特殊供应商)
providerType?: string;
// GitHub Copilot 关联账号 ID(旧字段,保留兼容读取)