mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 10:21:16 +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:
@@ -3,6 +3,8 @@ use serde_json::{json, Map, Value};
|
||||
const THINK_OPEN_TAG: &str = "<think>";
|
||||
const THINK_CLOSE_TAG: &str = "</think>";
|
||||
|
||||
// 穷举上游可能的 reasoning 回传字段,优先级:reasoning_content > reasoning(字符串/对象) > reasoning_details。
|
||||
// 不依赖 provider meta 的 outputFormat 声明,因此对各家 Chat 兼容接口都能兜底提取。
|
||||
pub(crate) fn extract_reasoning_field_text(value: &Value) -> Option<String> {
|
||||
for key in ["reasoning_content", "reasoning"] {
|
||||
if let Some(text) = value.get(key).and_then(|v| v.as_str()) {
|
||||
@@ -12,15 +14,61 @@ pub(crate) fn extract_reasoning_field_text(value: &Value) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
let reasoning = value.get("reasoning")?;
|
||||
for key in ["content", "text", "summary"] {
|
||||
if let Some(text) = reasoning.get(key).and_then(|v| v.as_str()) {
|
||||
if let Some(reasoning) = value.get("reasoning") {
|
||||
for key in ["content", "text", "summary"] {
|
||||
if let Some(text) = reasoning.get(key).and_then(|v| v.as_str()) {
|
||||
if !text.is_empty() {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(details) = value.get("reasoning_details") {
|
||||
if let Some(text) = extract_reasoning_details_text(details) {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_reasoning_details_text(value: &Value) -> Option<String> {
|
||||
match value {
|
||||
Value::String(text) => (!text.is_empty()).then(|| text.to_string()),
|
||||
Value::Array(parts) => {
|
||||
let text = parts
|
||||
.iter()
|
||||
.filter_map(extract_reasoning_detail_part_text)
|
||||
.filter(|text| !text.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
(!text.is_empty()).then_some(text)
|
||||
}
|
||||
Value::Object(_) => extract_reasoning_detail_part_text(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_reasoning_detail_part_text(value: &Value) -> Option<String> {
|
||||
for key in ["text", "content", "summary"] {
|
||||
if let Some(text) = value.get(key).and_then(|v| v.as_str()) {
|
||||
if !text.is_empty() {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parts) = value.get("parts").and_then(|v| v.as_array()) {
|
||||
let text = parts
|
||||
.iter()
|
||||
.filter_map(extract_reasoning_detail_part_text)
|
||||
.filter(|text| !text.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
return (!text.is_empty()).then_some(text);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user