mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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:
@@ -6,7 +6,7 @@
|
||||
//! 支持检测官方 Codex 客户端 (codex_vscode, codex_cli_rs)
|
||||
|
||||
use super::{AuthInfo, AuthStrategy, ProviderAdapter};
|
||||
use crate::provider::Provider;
|
||||
use crate::provider::{CodexChatReasoningConfig, Provider};
|
||||
use crate::proxy::error::ProxyError;
|
||||
use regex::Regex;
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -147,6 +147,192 @@ pub fn apply_codex_chat_upstream_model(
|
||||
Some(upstream_model)
|
||||
}
|
||||
|
||||
pub fn resolve_codex_chat_reasoning_config(
|
||||
provider: &Provider,
|
||||
body: &JsonValue,
|
||||
) -> Option<CodexChatReasoningConfig> {
|
||||
if let Some(config) = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.codex_chat_reasoning.clone())
|
||||
{
|
||||
return Some(normalize_codex_chat_reasoning_config(config));
|
||||
}
|
||||
|
||||
infer_codex_chat_reasoning_config(provider, body)
|
||||
}
|
||||
|
||||
fn normalize_codex_chat_reasoning_config(
|
||||
mut config: CodexChatReasoningConfig,
|
||||
) -> CodexChatReasoningConfig {
|
||||
if config.supports_effort.unwrap_or(false) && config.supports_thinking.is_none() {
|
||||
config.supports_thinking = Some(true);
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
fn infer_codex_chat_reasoning_config(
|
||||
provider: &Provider,
|
||||
body: &JsonValue,
|
||||
) -> Option<CodexChatReasoningConfig> {
|
||||
let model = body
|
||||
.get("model")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| codex_provider_upstream_model(provider))
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let base_url = provider
|
||||
.settings_config
|
||||
.get("base_url")
|
||||
.or_else(|| provider.settings_config.get("baseURL"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| {
|
||||
provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(extract_codex_base_url_from_toml)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let name = provider.name.to_ascii_lowercase();
|
||||
|
||||
// 平台优先:聚合 / 托管平台的 reasoning 接口由平台的推理框架决定,而非模型官方实现,
|
||||
// 因此先按平台标识(仅 name + base_url,不含 model 名)判定并覆盖模型规则。
|
||||
if let Some(config) = infer_aggregator_platform_config(&name, &base_url) {
|
||||
return Some(config);
|
||||
}
|
||||
|
||||
let haystack = format!("{name} {base_url} {model}");
|
||||
|
||||
if haystack.contains("deepseek") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("thinking".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("deepseek".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// StepFun:仅 step-3.5-flash-2603 这一版支持 reasoning effort(low/high 两档),
|
||||
// 其余 step 模型不暴露 effort,故 supports_effort 仅对含 "2603" 的模型置真。
|
||||
// 第二个 OR 分支覆盖「经中转/聚合跑该模型、但平台 name/base_url 不含 stepfun」的情况。
|
||||
if haystack.contains("stepfun") || haystack.contains("step-3.5-flash-2603") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(model.contains("2603")),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("low_high".to_string()),
|
||||
output_format: Some("reasoning".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if haystack.contains("kimi") || haystack.contains("moonshot") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("thinking".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if haystack.contains("glm") || haystack.contains("zhipu") || haystack.contains("z.ai") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("thinking".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if haystack.contains("qwen") || haystack.contains("dashscope") || haystack.contains("bailian") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("enable_thinking".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if haystack.contains("minimax") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("reasoning_split".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_details".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if haystack.contains("mimo") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("thinking".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 聚合 / 托管平台的 reasoning 接口由平台决定:同一个模型在不同平台参数可能完全不同
|
||||
/// (DeepSeek 官方用 `thinking:{type}`、SiliconFlow 用 `enable_thinking`、
|
||||
/// OpenRouter 用原生 `reasoning:{effort}` 对象)。仅以平台标识(name / base_url)判定,
|
||||
/// 绝不掺入 model 名——model 名属于模型厂商,会把托管平台误判成模型官方接口。
|
||||
fn infer_aggregator_platform_config(
|
||||
name: &str,
|
||||
base_url: &str,
|
||||
) -> Option<CodexChatReasoningConfig> {
|
||||
let platform = format!("{name} {base_url}");
|
||||
|
||||
// OpenRouter:用原生归一化对象 `reasoning: { effort }`(由 OpenRouter 翻译成各底层
|
||||
// 模型的正确推理参数,比顶层 OpenAI 别名 reasoning_effort 覆盖面更全)。effort 走
|
||||
// "openrouter" 值映射:枚举为 xhigh|high|medium|low|minimal,无 max——max 会触发
|
||||
// `400 reasoning_effort: Invalid option`(见 openclaw#77350),故钳到 xhigh。
|
||||
// 安全降级:不发 `thinking:{type}`(OpenRouter 不认该字段),避免误配导致请求被拒。
|
||||
if platform.contains("openrouter") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(false),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning.effort".to_string()),
|
||||
effort_value_mode: Some("openrouter".to_string()),
|
||||
output_format: Some("auto".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// SiliconFlow:平台级统一 `enable_thinking`,思维回传 reasoning_content。
|
||||
// 安全降级:不按 reasoning_effort 发 effort(平台用 thinking_budget 控制深度,
|
||||
// 发 reasoning_effort 反而可能不被接受)。
|
||||
if platform.contains("siliconflow") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("enable_thinking".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_chat_wire_api(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
@@ -642,4 +828,114 @@ wire_api = "responses"
|
||||
assert_eq!(upstream_model.as_deref(), Some("kimi-k2"));
|
||||
assert_eq!(body.get("model").and_then(|v| v.as_str()), Some("kimi-k2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_infers_deepseek_effort_support() {
|
||||
let provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "deepseek"
|
||||
model = "deepseek-v4-pro"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.com"
|
||||
wire_api = "chat"
|
||||
"#
|
||||
}));
|
||||
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "deepseek-v4-pro" }))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.supports_thinking, Some(true));
|
||||
assert_eq!(config.supports_effort, Some(true));
|
||||
assert_eq!(config.effort_value_mode.as_deref(), Some("deepseek"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_explicit_meta_overrides_inference() {
|
||||
let mut provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "deepseek"
|
||||
model = "deepseek-v4-pro"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.com"
|
||||
wire_api = "chat"
|
||||
"#
|
||||
}));
|
||||
provider.meta = Some(crate::provider::ProviderMeta {
|
||||
codex_chat_reasoning: Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(false),
|
||||
supports_effort: Some(false),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("auto".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "deepseek-v4-pro" }))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.supports_thinking, Some(false));
|
||||
assert_eq!(config.supports_effort, Some(false));
|
||||
assert_eq!(config.thinking_param.as_deref(), Some("none"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_openrouter_platform_overrides_model() {
|
||||
let provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "openrouter"
|
||||
model = "deepseek/deepseek-chat-v3.1"
|
||||
|
||||
[model_providers.openrouter]
|
||||
name = "OpenRouter"
|
||||
base_url = "https://openrouter.ai/api/v1"
|
||||
wire_api = "chat"
|
||||
"#
|
||||
}));
|
||||
|
||||
// 模型名含 "deepseek",但平台是 OpenRouter —— 平台规则必须覆盖模型规则。
|
||||
let config = resolve_codex_chat_reasoning_config(
|
||||
&provider,
|
||||
&json!({ "model": "deepseek/deepseek-chat-v3.1" }),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.thinking_param.as_deref(), Some("none"));
|
||||
assert_eq!(config.effort_param.as_deref(), Some("reasoning.effort"));
|
||||
assert_eq!(config.effort_value_mode.as_deref(), Some("openrouter"));
|
||||
assert_eq!(config.supports_effort, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_siliconflow_platform_overrides_minimax() {
|
||||
let provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "siliconflow"
|
||||
model = "MiniMaxAI/MiniMax-M2.7"
|
||||
|
||||
[model_providers.siliconflow]
|
||||
name = "SiliconFlow"
|
||||
base_url = "https://api.siliconflow.cn/v1"
|
||||
wire_api = "chat"
|
||||
"#
|
||||
}));
|
||||
|
||||
// 模型是 MiniMax(官方用 reasoning_split),但平台是 SiliconFlow —— 应走平台的 enable_thinking。
|
||||
let config = resolve_codex_chat_reasoning_config(
|
||||
&provider,
|
||||
&json!({ "model": "MiniMaxAI/MiniMax-M2.7" }),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.thinking_param.as_deref(), Some("enable_thinking"));
|
||||
assert_eq!(config.supports_effort, Some(false));
|
||||
assert_eq!(config.output_format.as_deref(), Some("reasoning_content"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user