mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
fix(codex): route OpenCode Zen reasoning effort through the platform config (#6123)
* fix(codex): route OpenCode Zen reasoning effort through the platform config Zen 网关的合法 reasoning_effort 档位是逐模型的(models.dev reasoning_options:glm-5.2 仅 high|max、deepseek-v4-flash 为 low|high|max、kimi-k3 仅 max,glm-5.1 等为 toggle 型无 effort), opencode 客户端也严格按模型声明发值。此前统一并集映射会把 Codex 默认的 medium 发给只声明 high|max 的 glm-5.2(默认路径必现), 也会把 low 发给不支持 low 的 deepseek-v4-pro(回归)。 - infer_aggregator_platform_config 新增 opencode.ai 平台条目(仅按 name+base_url 判定,不看模型名),zen 模式:顶层 reasoning_effort + reasoning_content,不发厂商原生 thinking 形状 - CodexChatReasoningConfig 新增运行时字段 effort_levels(#[serde(skip)] 不持久化);resolve 末端对 zen 模式按请求模型从供应商 settings_config.modelCatalog 的 reasoningLevels(复用 #6228 引入的 逐模型声明,camelCase/snake_case 双格式兼容)查表附上,查不到则 None —— 转换层完全不发 reasoning_effort - transform_codex_chat 的 zen 映射改为逐模型钳制:取「不小于请求的 最近合法档」,超出最高档取最高档;请求值无法识别则丢弃字段 - OpenCode Go 预设按 models.dev 补上逐模型 reasoningLevels 表; 表单加载侧提取纯函数 mapCodexCatalogModelForForm 并保住 reasoningLevels 的 load→save 回环(含 snake_case 旧数据),保存侧 trim 档位值 存量供应商不会自动获得档位表(其 settings_config 无 reasoningLevels), 行为退化为不发 effort 字段(与改动前一致的安全默认);重新添加一次 OpenCode Go 预设即可获得逐模型表。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(codex): move OpenCode Go to filled entries in reasoning-level expectations 上游 #6228 的 deliberately-unfilled 清单把 OpenCode Go 整体列为不填, 依据是「无 codexChatReasoning 声明、逐模型语义未经网关验证」——两个 前提均被本 PR 改变:预设已补 codexChatReasoning(effortValueMode:"zen"), 且逐模型档位有据(opencode 客户端 variants() 严格按 models.dev reasoning_options 发值)。glm-5.2 / deepseek-v4-pro / deepseek-v4-flash 移入 EXPECTED(不填 none:effort 档位非思考开关);glm-5.1(toggle 型)、 kimi-k2.7-code(官方标注不支持 effort)、mimo-v2.5-pro(models.dev 未 收录)保持不填并更新注释依据。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46f19a1583
commit
d01eab979e
10 files changed
+585
-82
No files matched your search
@@ -406,6 +406,12 @@ pub struct CodexChatReasoningConfig {
|
||||
/// 靠穷举字段提取、并不读取本字段;保留作文档说明与未来按格式分发(如 think_tags)的预留。
|
||||
#[serde(rename = "outputFormat", skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
/// 运行时字段(不持久化、不进 meta):当前请求模型在平台侧声明的合法 effort
|
||||
/// 档位,由 resolve 按请求模型从供应商 `settings_config.modelCatalog` 的
|
||||
/// `reasoningLevels`(逐模型声明,见 #6228)查表填充。仅 "zen" 值映射消费:
|
||||
/// Some → 钳到合法档;None → 不发 effort 字段(模型未收录或为 toggle 型)。
|
||||
#[serde(skip)]
|
||||
pub effort_levels: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Local proxy request overrides applied after route/protocol transforms.
|
||||
|
||||
@@ -339,15 +339,57 @@ pub fn resolve_codex_chat_reasoning_config(
|
||||
provider: &Provider,
|
||||
body: &JsonValue,
|
||||
) -> Option<CodexChatReasoningConfig> {
|
||||
if let Some(config) = provider
|
||||
let mut config = if let Some(config) = provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.codex_chat_reasoning.clone())
|
||||
{
|
||||
return Some(normalize_codex_chat_reasoning_config(config));
|
||||
normalize_codex_chat_reasoning_config(config)
|
||||
} else {
|
||||
infer_codex_chat_reasoning_config(provider, body)?
|
||||
};
|
||||
|
||||
// zen 的合法 effort 档位是逐模型的(models.dev:glm-5.2 仅 high|max、
|
||||
// kimi-k3 仅 max、qwen/glm-5.1 等为 toggle 型无 effort),opencode 客户端
|
||||
// 也严格按模型声明发值。按请求模型从 modelCatalog 的 reasoningLevels
|
||||
// (#6228 引入的逐模型声明)查表附上;查不到(模型未收录 / 条目未声明
|
||||
// effort)→ None,转换层将完全不发 reasoning_effort。
|
||||
if config.effort_value_mode.as_deref() == Some("zen") {
|
||||
config.effort_levels = zen_catalog_effort_levels(provider, body);
|
||||
}
|
||||
|
||||
infer_codex_chat_reasoning_config(provider, body)
|
||||
Some(config)
|
||||
}
|
||||
|
||||
/// 按请求模型从供应商 modelCatalog 查 Zen 合法 effort 档位(逐模型数据镜像
|
||||
/// models.dev 的 reasoning_options effort values)。仅做档位查表,不参与平台
|
||||
/// 判定——平台身份仍只由 name/base_url 决定(见 infer_aggregator_platform_config)。
|
||||
/// DB SSOT 为 camelCase,手写/旧数据可能为 snake_case,双格式兼容(与表单加载侧一致)。
|
||||
fn zen_catalog_effort_levels(provider: &Provider, body: &JsonValue) -> Option<Vec<String>> {
|
||||
let model = body.get("model")?.as_str()?.trim();
|
||||
if model.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entries = provider
|
||||
.settings_config
|
||||
.get("modelCatalog")?
|
||||
.get("models")?
|
||||
.as_array()?;
|
||||
let entry = entries.iter().find(|entry| {
|
||||
entry
|
||||
.get("model")
|
||||
.and_then(|value| value.as_str())
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(model))
|
||||
})?;
|
||||
let levels_value = entry
|
||||
.get("reasoningLevels")
|
||||
.or_else(|| entry.get("reasoning_levels"))?;
|
||||
let levels: Vec<String> = levels_value
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|level| level.as_str().map(str::to_string))
|
||||
.collect();
|
||||
(!levels.is_empty()).then_some(levels)
|
||||
}
|
||||
|
||||
fn normalize_codex_chat_reasoning_config(
|
||||
@@ -403,6 +445,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("deepseek".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,6 +471,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
.to_string(),
|
||||
),
|
||||
output_format: Some("reasoning".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -439,6 +483,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -450,6 +495,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -461,6 +507,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -472,6 +519,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_details".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,6 +531,7 @@ fn infer_codex_chat_reasoning_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -512,6 +561,7 @@ fn infer_aggregator_platform_config(
|
||||
effort_param: Some("reasoning.effort".to_string()),
|
||||
effort_value_mode: Some("openrouter".to_string()),
|
||||
output_format: Some("auto".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -526,6 +576,7 @@ fn infer_aggregator_platform_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -542,6 +593,26 @@ fn infer_aggregator_platform_config(
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
// OpenCode Zen(opencode.ai 网关,issue #6112):其自家客户端对该传输发顶层
|
||||
// `reasoning_effort`(provider/transform.ts),平台归一参数;不发厂商原生
|
||||
// thinking 形状(glm 模型走 zen 时套智谱 thinking:{type} 网关不认)。
|
||||
// 合法档位逐模型(models.dev 的 reasoning_options,opencode 客户端同样严格
|
||||
// 按模型声明发值):具体档位表见供应商 modelCatalog 各条目的 reasoningLevels,
|
||||
// 代理由此按请求模型查表钳制(resolve 处附上 effort_levels),无表不发字段。
|
||||
// 匹配域名而非裸 "opencode",避免误伤名字含 opencode 的无关供应商。
|
||||
if platform.contains("opencode.ai") {
|
||||
return Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1526,6 +1597,7 @@ wire_api = "chat"
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("auto".to_string()),
|
||||
effort_levels: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -1617,6 +1689,139 @@ wire_api = "chat"
|
||||
assert_eq!(config.output_format.as_deref(), Some("reasoning_content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_opencode_zen_platform_overrides_model_vendor() {
|
||||
let provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "opencode"
|
||||
model = "glm-5.2"
|
||||
|
||||
[model_providers.opencode]
|
||||
name = "OpenCode Go"
|
||||
base_url = "https://opencode.ai/zen/go/v1"
|
||||
wire_api = "chat"
|
||||
"#
|
||||
}));
|
||||
|
||||
// 模型是 GLM(智谱自家用 thinking:{type} 方言),但平台是 OpenCode Zen ——
|
||||
// 必须走平台配置:顶层 reasoning_effort + reasoning_content,
|
||||
// 不得注入智谱 thinking 形状。
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "glm-5.2" })).unwrap();
|
||||
|
||||
assert_eq!(config.supports_thinking, Some(true));
|
||||
assert_eq!(config.supports_effort, Some(true));
|
||||
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("zen"));
|
||||
assert_eq!(config.output_format.as_deref(), Some("reasoning_content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_zen_attaches_per_model_effort_levels() {
|
||||
let provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "opencode"
|
||||
model = "glm-5.2"
|
||||
|
||||
[model_providers.opencode]
|
||||
name = "OpenCode Go"
|
||||
base_url = "https://opencode.ai/zen/go/v1"
|
||||
wire_api = "chat"
|
||||
"#,
|
||||
"modelCatalog": {
|
||||
"models": [
|
||||
{ "model": "glm-5.2", "reasoningLevels": ["high", "max"] },
|
||||
{ "model": "deepseek-v4-flash", "reasoningLevels": ["low", "high", "max"] },
|
||||
{ "model": "glm-5.1" }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
// 逐模型查表:声明了 reasoningLevels 的模型附上各自档位(模型名大小写不敏感)。
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "GLM-5.2" })).unwrap();
|
||||
assert_eq!(
|
||||
config.effort_levels,
|
||||
Some(vec!["high".to_string(), "max".to_string()])
|
||||
);
|
||||
|
||||
let config = resolve_codex_chat_reasoning_config(
|
||||
&provider,
|
||||
&json!({ "model": "deepseek-v4-flash" }),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
config.effort_levels,
|
||||
Some(vec![
|
||||
"low".to_string(),
|
||||
"high".to_string(),
|
||||
"max".to_string()
|
||||
])
|
||||
);
|
||||
|
||||
// toggle 型模型(目录条目未声明 reasoningLevels)→ None:转换层不发 effort 字段。
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "glm-5.1" })).unwrap();
|
||||
assert_eq!(config.effort_value_mode.as_deref(), Some("zen"));
|
||||
assert!(config.effort_levels.is_none());
|
||||
|
||||
// 目录未收录的模型 → 同样 None。
|
||||
let config =
|
||||
resolve_codex_chat_reasoning_config(&provider, &json!({ "model": "kimi-k3" })).unwrap();
|
||||
assert!(config.effort_levels.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_codex_chat_reasoning_zen_levels_attach_on_explicit_meta_too() {
|
||||
let mut provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "opencode"
|
||||
model = "deepseek-v4-flash"
|
||||
|
||||
[model_providers.opencode]
|
||||
name = "OpenCode Go"
|
||||
base_url = "https://opencode.ai/zen/go/v1"
|
||||
wire_api = "chat"
|
||||
"#,
|
||||
"modelCatalog": {
|
||||
"models": [
|
||||
{ "model": "deepseek-v4-flash", "reasoning_levels": ["low", "high", "max"] }
|
||||
]
|
||||
}
|
||||
}));
|
||||
// 显式 meta(表单手选 zen 模式)同样要在 resolve 末端按请求模型附表;
|
||||
// 且手写/旧数据可能是 snake_case 的 reasoning_levels(加载侧双格式兼容)。
|
||||
provider.meta = Some(crate::provider::ProviderMeta {
|
||||
codex_chat_reasoning: Some(CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let config = resolve_codex_chat_reasoning_config(
|
||||
&provider,
|
||||
&json!({ "model": "deepseek-v4-flash" }),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.effort_value_mode.as_deref(), Some("zen"));
|
||||
assert_eq!(
|
||||
config.effort_levels,
|
||||
Some(vec![
|
||||
"low".to_string(),
|
||||
"high".to_string(),
|
||||
"max".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infer_codex_chat_reasoning_stepfun_per_model_effort() {
|
||||
let provider = create_provider(json!({
|
||||
|
||||
@@ -424,7 +424,11 @@ fn apply_reasoning_options(
|
||||
let Some(effort) = body.pointer("/reasoning/effort").and_then(|v| v.as_str()) else {
|
||||
return;
|
||||
};
|
||||
let Some(mapped) = map_reasoning_effort(effort, config.effort_value_mode.as_deref()) else {
|
||||
let Some(mapped) = map_reasoning_effort(
|
||||
effort,
|
||||
config.effort_value_mode.as_deref(),
|
||||
config.effort_levels.as_deref(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -455,7 +459,11 @@ fn reasoning_requested(body: &Value) -> Option<bool> {
|
||||
body.get("reasoning").map(|value| !value.is_null())
|
||||
}
|
||||
|
||||
fn map_reasoning_effort(effort: &str, mode: Option<&str>) -> Option<&'static str> {
|
||||
fn map_reasoning_effort<'a>(
|
||||
effort: &str,
|
||||
mode: Option<&str>,
|
||||
effort_levels: Option<&'a [String]>,
|
||||
) -> Option<&'a str> {
|
||||
let effort = effort.trim().to_ascii_lowercase();
|
||||
if matches!(effort.as_str(), "none" | "off" | "disabled") {
|
||||
return None;
|
||||
@@ -486,6 +494,31 @@ fn map_reasoning_effort(effort: &str, mode: Option<&str>) -> Option<&'static str
|
||||
"minimal" => Some("minimal"),
|
||||
_ => None,
|
||||
},
|
||||
// OpenCode Zen:合法档位逐模型(表数据 = 供应商 modelCatalog 各条目的
|
||||
// reasoningLevels,镜像 models.dev——glm-5.2 仅 high|max、deepseek-v4-flash
|
||||
// 为 low|high|max、kimi-k3 仅 max),opencode 客户端也严格按模型声明发值,
|
||||
// 故不能用统一并集映射。无表(模型未收录目录、或为 toggle/budget 型未声明
|
||||
// effort)→ None,完全不发 reasoning_effort;有表 → 钳到「不小于请求的
|
||||
// 最近合法档」,请求超出最高档则取最高合法档;请求值本身无法识别 → None
|
||||
// (同其他模式的未知值丢弃策略)。
|
||||
"zen" => {
|
||||
let levels = effort_levels?;
|
||||
let requested = zen_effort_rank(&effort)?;
|
||||
levels
|
||||
.iter()
|
||||
.filter_map(|level| zen_effort_rank(level).map(|rank| (rank, level.as_str())))
|
||||
.filter(|(rank, _)| *rank >= requested)
|
||||
.min_by_key(|(rank, _)| *rank)
|
||||
.or_else(|| {
|
||||
levels
|
||||
.iter()
|
||||
.filter_map(|level| {
|
||||
zen_effort_rank(level).map(|rank| (rank, level.as_str()))
|
||||
})
|
||||
.max_by_key(|(rank, _)| *rank)
|
||||
})
|
||||
.map(|(_, level)| level)
|
||||
}
|
||||
_ => match effort.as_str() {
|
||||
"minimal" => Some("minimal"),
|
||||
"low" => Some("low"),
|
||||
@@ -499,6 +532,21 @@ fn map_reasoning_effort(effort: &str, mode: Option<&str>) -> Option<&'static str
|
||||
}
|
||||
}
|
||||
|
||||
/// Codex 规范档位序(minimal < low < medium < high < xhigh < max < ultra),供 zen
|
||||
/// 逐模型钳制做大小比较;目录里的非法/扩展值(如 "none")返回 None,查表时被滤掉。
|
||||
fn zen_effort_rank(effort: &str) -> Option<u8> {
|
||||
match effort.trim().to_ascii_lowercase().as_str() {
|
||||
"minimal" => Some(0),
|
||||
"low" => Some(1),
|
||||
"medium" => Some(2),
|
||||
"high" => Some(3),
|
||||
"xhigh" => Some(4),
|
||||
"max" => Some(5),
|
||||
"ultra" => Some(6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// MiniMax 严格要求 messages 中只能首条出现 `role=system`,
|
||||
/// 否则返回 `invalid params, chat content has invalid message role: system (2013)`。
|
||||
/// 把所有 system 消息合并到首位,避免中间 system(如 Codex 的 `developer` 指令)触发该约束;
|
||||
@@ -2029,19 +2077,22 @@ mod tests {
|
||||
fn map_reasoning_effort_handles_ultra_per_mode() {
|
||||
// passthrough 面向枚举未知的通用上游:ultra 与 max/xhigh 一样原值透传,
|
||||
// 让声明了该档位的上游能收到用户选择。
|
||||
assert_eq!(map_reasoning_effort("ultra", None), Some("ultra"));
|
||||
assert_eq!(map_reasoning_effort("ultra", None, None), Some("ultra"));
|
||||
// 已知枚举的专用模式钳到自身最高合法档,而不是走 None 被静默丢弃。
|
||||
assert_eq!(map_reasoning_effort("ultra", Some("deepseek")), Some("max"));
|
||||
assert_eq!(
|
||||
map_reasoning_effort("ultra", Some("low_high")),
|
||||
map_reasoning_effort("ultra", Some("deepseek"), None),
|
||||
Some("max")
|
||||
);
|
||||
assert_eq!(
|
||||
map_reasoning_effort("ultra", Some("low_high"), None),
|
||||
Some("high")
|
||||
);
|
||||
assert_eq!(
|
||||
map_reasoning_effort("ultra", Some("openrouter")),
|
||||
map_reasoning_effort("ultra", Some("openrouter"), None),
|
||||
Some("xhigh")
|
||||
);
|
||||
// 真正的未知值仍然丢弃,防上游 400。
|
||||
assert_eq!(map_reasoning_effort("turbo", None), None);
|
||||
assert_eq!(map_reasoning_effort("turbo", None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2535,6 +2586,7 @@ mod tests {
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("deepseek".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let result = responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
@@ -2576,6 +2628,7 @@ mod tests {
|
||||
effort_param: Some("reasoning.effort".to_string()),
|
||||
effort_value_mode: Some("openrouter".to_string()),
|
||||
output_format: Some("auto".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
// max 不在 OpenRouter 枚举内(见 openclaw#77350),必须钳成 xhigh,
|
||||
@@ -2617,6 +2670,7 @@ mod tests {
|
||||
effort_param: Some("reasoning.effort".to_string()),
|
||||
effort_value_mode: Some("openrouter".to_string()),
|
||||
output_format: Some("auto".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let input = json!({
|
||||
@@ -2644,6 +2698,7 @@ mod tests {
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("deepseek".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let input = json!({
|
||||
@@ -2659,6 +2714,138 @@ mod tests {
|
||||
assert!(result.get("reasoning").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_to_chat_clamps_zen_effort_to_model_declared_levels() {
|
||||
// OpenCode Zen 平台形态 + 逐模型档位钳制(表数据镜像 models.dev:glm-5.2
|
||||
// 仅声明 high|max)。锁定:统一并集映射会把 Codex 默认的 medium 发给只声明
|
||||
// high|max 的 glm-5.2(恰好是预设默认模型),严格校验的网关会报错;
|
||||
// 低档一律上钳到最近合法档,超出最高档(含 Codex 扩展档 ultra)取最高合法档。
|
||||
let config = CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: Some(vec!["high".to_string(), "max".to_string()]),
|
||||
};
|
||||
|
||||
for (input_effort, expected) in [
|
||||
("minimal", "high"),
|
||||
("low", "high"),
|
||||
("medium", "high"),
|
||||
("high", "high"),
|
||||
("xhigh", "max"),
|
||||
("max", "max"),
|
||||
("ultra", "max"),
|
||||
] {
|
||||
let input = json!({
|
||||
"model": "glm-5.2",
|
||||
"input": "hello",
|
||||
"reasoning": {"effort": input_effort}
|
||||
});
|
||||
let result =
|
||||
responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
assert_eq!(
|
||||
result["reasoning_effort"], expected,
|
||||
"effort={input_effort}"
|
||||
);
|
||||
// 写成顶层 reasoning_effort;不发任何 thinking 字段
|
||||
// (网关只认平台归一参数,不认厂商 thinking 形状)。
|
||||
assert!(result.get("thinking").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_to_chat_zen_preserves_low_when_model_declares_it() {
|
||||
// deepseek-v4-flash 声明 low|high|max:low 合法原样透传,medium 上钳 high,
|
||||
// xhigh 上钳 max——回归锁:旧 deepseek 厂商分支把非 max 一律归 high,
|
||||
// 逐模型钳制不得比那更差,也不得发出声明外的值。
|
||||
let config = CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: Some(vec![
|
||||
"low".to_string(),
|
||||
"high".to_string(),
|
||||
"max".to_string(),
|
||||
]),
|
||||
};
|
||||
|
||||
for (input_effort, expected) in [
|
||||
("minimal", "low"),
|
||||
("low", "low"),
|
||||
("medium", "high"),
|
||||
("xhigh", "max"),
|
||||
] {
|
||||
let input = json!({
|
||||
"model": "deepseek-v4-flash",
|
||||
"input": "hello",
|
||||
"reasoning": {"effort": input_effort}
|
||||
});
|
||||
let result =
|
||||
responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
assert_eq!(
|
||||
result["reasoning_effort"], expected,
|
||||
"effort={input_effort}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_to_chat_zen_single_level_model_clamps_everything_to_max() {
|
||||
// kimi-k3 仅声明 max(全模型交集不存在,统一映射覆盖不了它——必须逐模型):
|
||||
// 任何请求档都钳到 max。
|
||||
let config = CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: Some(vec!["max".to_string()]),
|
||||
};
|
||||
|
||||
for input_effort in ["minimal", "medium", "high", "max"] {
|
||||
let input = json!({
|
||||
"model": "kimi-k3",
|
||||
"input": "hello",
|
||||
"reasoning": {"effort": input_effort}
|
||||
});
|
||||
let result =
|
||||
responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
assert_eq!(result["reasoning_effort"], "max", "effort={input_effort}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_to_chat_omits_zen_effort_without_model_levels() {
|
||||
// 模型未在目录声明 effort(toggle/budget 型如 glm-5.1、qwen 系,或目录未收录)
|
||||
// → 完全不发 reasoning_effort,避免给严格校验的网关送无法核实的值。
|
||||
let config = CodexChatReasoningConfig {
|
||||
supports_thinking: Some(true),
|
||||
supports_effort: Some(true),
|
||||
thinking_param: Some("none".to_string()),
|
||||
effort_param: Some("reasoning_effort".to_string()),
|
||||
effort_value_mode: Some("zen".to_string()),
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let input = json!({
|
||||
"model": "glm-5.1",
|
||||
"input": "hello",
|
||||
"reasoning": {"effort": "medium"}
|
||||
});
|
||||
let result = responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
|
||||
assert!(result.get("reasoning_effort").is_none());
|
||||
assert!(result.get("thinking").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_request_to_chat_maps_thinking_only_provider_without_effort() {
|
||||
let input = json!({
|
||||
@@ -2673,6 +2860,7 @@ mod tests {
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let result = responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
@@ -2695,6 +2883,7 @@ mod tests {
|
||||
effort_param: Some("none".to_string()),
|
||||
effort_value_mode: None,
|
||||
output_format: Some("reasoning_content".to_string()),
|
||||
effort_levels: None,
|
||||
};
|
||||
|
||||
let result = responses_to_chat_completions_with_reasoning(input, Some(&config)).unwrap();
|
||||
|
||||
@@ -178,9 +178,9 @@ export const normalizeCodexCatalogModelsForSave = (
|
||||
);
|
||||
|
||||
const baseInstructions = item.baseInstructions?.trim();
|
||||
const reasoningLevels = item.reasoningLevels?.filter(
|
||||
(level) => typeof level === "string" && level.trim(),
|
||||
);
|
||||
const reasoningLevels = item.reasoningLevels
|
||||
?.filter((level) => typeof level === "string" && level.trim())
|
||||
.map((level) => level.trim());
|
||||
const defaultReasoningLevel = item.defaultReasoningLevel?.trim();
|
||||
|
||||
normalized.push({
|
||||
|
||||
@@ -29,6 +29,71 @@ function pickCodexApiKey(
|
||||
return extractCodexExperimentalBearerToken(configText) || "";
|
||||
}
|
||||
|
||||
// 目录行 load 映射(隐藏字段白名单重建)。抽成可导出的纯函数,与
|
||||
// normalizeCodexCatalogModelsForSave 成对做 load→save 回环测试——
|
||||
// 任一侧丢字段都会静默清空供应商的逐模型声明(reasoningLevels、
|
||||
// baseInstructions 等),且 UI 无可察觉。
|
||||
export const mapCodexCatalogModelForForm = (item: any): CodexCatalogModel => {
|
||||
// 隐藏字段(原生 Responses profile 用)不在行 UI 暴露,但必须 load→save
|
||||
// 原样保留,否则编辑保存 MiMo/MiniMax 等会丢官方 base_instructions、
|
||||
// 并行工具、图像模态。DB SSOT 为 camelCase、live 反解兜底可能为 snake_case,
|
||||
// 双格式兼容(与 displayName/contextWindow 一致)。
|
||||
const supportsParallelToolCalls =
|
||||
typeof item?.supportsParallelToolCalls === "boolean"
|
||||
? item.supportsParallelToolCalls
|
||||
: typeof item?.supports_parallel_tool_calls === "boolean"
|
||||
? item.supports_parallel_tool_calls
|
||||
: undefined;
|
||||
const inputModalities = Array.isArray(item?.inputModalities)
|
||||
? item.inputModalities
|
||||
: Array.isArray(item?.input_modalities)
|
||||
? item.input_modalities
|
||||
: undefined;
|
||||
const baseInstructions =
|
||||
typeof item?.baseInstructions === "string"
|
||||
? item.baseInstructions
|
||||
: typeof item?.base_instructions === "string"
|
||||
? item.base_instructions
|
||||
: undefined;
|
||||
const reasoningLevels = Array.isArray(item?.reasoningLevels)
|
||||
? item.reasoningLevels
|
||||
: Array.isArray(item?.reasoning_levels)
|
||||
? item.reasoning_levels
|
||||
: undefined;
|
||||
const defaultReasoningLevel =
|
||||
typeof item?.defaultReasoningLevel === "string"
|
||||
? item.defaultReasoningLevel
|
||||
: typeof item?.default_reasoning_level === "string"
|
||||
? item.default_reasoning_level
|
||||
: undefined;
|
||||
return {
|
||||
model: typeof item?.model === "string" ? item.model : "",
|
||||
displayName:
|
||||
typeof item?.displayName === "string"
|
||||
? item.displayName
|
||||
: typeof item?.display_name === "string"
|
||||
? item.display_name
|
||||
: "",
|
||||
contextWindow:
|
||||
typeof item?.contextWindow === "string" ||
|
||||
typeof item?.contextWindow === "number"
|
||||
? item.contextWindow
|
||||
: typeof item?.context_window === "string" ||
|
||||
typeof item?.context_window === "number"
|
||||
? item.context_window
|
||||
: "",
|
||||
...(supportsParallelToolCalls !== undefined
|
||||
? { supportsParallelToolCalls }
|
||||
: {}),
|
||||
...(inputModalities ? { inputModalities } : {}),
|
||||
...(baseInstructions ? { baseInstructions } : {}),
|
||||
...(reasoningLevels && reasoningLevels.length > 0
|
||||
? { reasoningLevels }
|
||||
: {}),
|
||||
...(defaultReasoningLevel ? { defaultReasoningLevel } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 管理 Codex 配置状态
|
||||
* Codex 配置包含两部分:auth.json (JSON) 和 config.toml (TOML 字符串)
|
||||
@@ -70,66 +135,7 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
|
||||
: [];
|
||||
setCodexCatalogModels(
|
||||
rawCatalogModels
|
||||
.map((item: any) => {
|
||||
// 隐藏字段(原生 Responses profile 用)不在行 UI 暴露,但必须 load→save
|
||||
// 原样保留,否则编辑保存 MiMo/MiniMax 等会丢官方 base_instructions、
|
||||
// 并行工具、图像模态。DB SSOT 为 camelCase、live 反解兜底可能为 snake_case,
|
||||
// 双格式兼容(与 displayName/contextWindow 一致)。
|
||||
const supportsParallelToolCalls =
|
||||
typeof item?.supportsParallelToolCalls === "boolean"
|
||||
? item.supportsParallelToolCalls
|
||||
: typeof item?.supports_parallel_tool_calls === "boolean"
|
||||
? item.supports_parallel_tool_calls
|
||||
: undefined;
|
||||
const inputModalities = Array.isArray(item?.inputModalities)
|
||||
? item.inputModalities
|
||||
: Array.isArray(item?.input_modalities)
|
||||
? item.input_modalities
|
||||
: undefined;
|
||||
const baseInstructions =
|
||||
typeof item?.baseInstructions === "string"
|
||||
? item.baseInstructions
|
||||
: typeof item?.base_instructions === "string"
|
||||
? item.base_instructions
|
||||
: undefined;
|
||||
const reasoningLevels = Array.isArray(item?.reasoningLevels)
|
||||
? item.reasoningLevels
|
||||
: Array.isArray(item?.reasoning_levels)
|
||||
? item.reasoning_levels
|
||||
: undefined;
|
||||
const defaultReasoningLevel =
|
||||
typeof item?.defaultReasoningLevel === "string"
|
||||
? item.defaultReasoningLevel
|
||||
: typeof item?.default_reasoning_level === "string"
|
||||
? item.default_reasoning_level
|
||||
: undefined;
|
||||
return {
|
||||
model: typeof item?.model === "string" ? item.model : "",
|
||||
displayName:
|
||||
typeof item?.displayName === "string"
|
||||
? item.displayName
|
||||
: typeof item?.display_name === "string"
|
||||
? item.display_name
|
||||
: "",
|
||||
contextWindow:
|
||||
typeof item?.contextWindow === "string" ||
|
||||
typeof item?.contextWindow === "number"
|
||||
? item.contextWindow
|
||||
: typeof item?.context_window === "string" ||
|
||||
typeof item?.context_window === "number"
|
||||
? item.context_window
|
||||
: "",
|
||||
...(supportsParallelToolCalls !== undefined
|
||||
? { supportsParallelToolCalls }
|
||||
: {}),
|
||||
...(inputModalities ? { inputModalities } : {}),
|
||||
...(baseInstructions ? { baseInstructions } : {}),
|
||||
...(reasoningLevels && reasoningLevels.length > 0
|
||||
? { reasoningLevels }
|
||||
: {}),
|
||||
...(defaultReasoningLevel ? { defaultReasoningLevel } : {}),
|
||||
};
|
||||
})
|
||||
.map(mapCodexCatalogModelForForm)
|
||||
.filter((item: CodexCatalogModel) => item.model.trim()),
|
||||
);
|
||||
|
||||
|
||||
@@ -1840,8 +1840,25 @@ requires_openai_auth = true`,
|
||||
),
|
||||
endpointCandidates: ["https://opencode.ai/zen/go/v1"],
|
||||
apiFormat: "openai_chat",
|
||||
// OpenCode Zen 网关:统一接受顶层 reasoning_effort(其自家客户端同款参数),
|
||||
// 但合法档位逐模型(见各条目 reasoningLevels,镜像 models.dev;opencode
|
||||
// 客户端同样严格按模型声明发值)——代理转换层按表钳制,未声明 effort 的
|
||||
// 模型(toggle 型如 glm-5.1)不发该字段。不发厂商原生 thinking 字段。
|
||||
codexChatReasoning: {
|
||||
supportsThinking: true,
|
||||
supportsEffort: true,
|
||||
thinkingParam: "none",
|
||||
effortParam: "reasoning_effort",
|
||||
effortValueMode: "zen",
|
||||
outputFormat: "reasoning_content",
|
||||
},
|
||||
modelCatalog: modelCatalog([
|
||||
{ model: "glm-5.2", displayName: "GLM 5.2", contextWindow: 204800 },
|
||||
{
|
||||
model: "glm-5.2",
|
||||
displayName: "GLM 5.2",
|
||||
contextWindow: 204800,
|
||||
reasoningLevels: ["high", "max"],
|
||||
},
|
||||
{ model: "glm-5.1", displayName: "GLM 5.1", contextWindow: 204800 },
|
||||
{
|
||||
model: "kimi-k2.7-code",
|
||||
@@ -1852,11 +1869,13 @@ requires_openai_auth = true`,
|
||||
model: "deepseek-v4-pro",
|
||||
displayName: "DeepSeek V4 Pro",
|
||||
contextWindow: 1048576,
|
||||
reasoningLevels: ["high", "max"],
|
||||
},
|
||||
{
|
||||
model: "deepseek-v4-flash",
|
||||
displayName: "DeepSeek V4 Flash",
|
||||
contextWindow: 1048576,
|
||||
reasoningLevels: ["low", "high", "max"],
|
||||
},
|
||||
{
|
||||
model: "mimo-v2.5-pro",
|
||||
|
||||
+4
-1
@@ -142,7 +142,10 @@ export type CodexChatEffortValueMode =
|
||||
| "low_high"
|
||||
| "deepseek"
|
||||
// OpenRouter effort 枚举 xhigh|high|medium|low|minimal(无 max,max 钳到 xhigh)
|
||||
| "openrouter";
|
||||
| "openrouter"
|
||||
// OpenCode Zen 网关:合法档位逐模型,见 modelCatalog 各条目 reasoningLevels
|
||||
// (镜像 models.dev);代理转换层按请求模型查表钳制,无表不发 effort 字段
|
||||
| "zen";
|
||||
|
||||
export type CodexChatReasoningOutputFormat =
|
||||
| "auto"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeCodexCatalogModelsForSave } from "@/components/providers/forms/ProviderForm";
|
||||
import { mapCodexCatalogModelForForm } from "@/components/providers/forms/hooks/useCodexConfigState";
|
||||
|
||||
describe("ProviderForm Codex catalog helpers", () => {
|
||||
it("normalizes catalog rows and removes empty or duplicate models", () => {
|
||||
@@ -76,4 +77,43 @@ describe("ProviderForm Codex catalog helpers", () => {
|
||||
{ model: "plain-model" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("round-trips reasoning levels through load and save without loss", () => {
|
||||
// load→save 回环:加载映射(mapCodexCatalogModelForForm)与保存归一化
|
||||
// (normalizeCodexCatalogModelsForSave)各锁半边时,回环丢字段两边都测不出——
|
||||
// 而编辑保存丢表会让依赖逐模型档位的功能(zen 钳制)静默失效且 UI 无可察觉。
|
||||
const stored = [
|
||||
{
|
||||
model: "glm-5.2",
|
||||
displayName: "GLM 5.2",
|
||||
reasoningLevels: ["high", "max"],
|
||||
},
|
||||
// 手写/旧数据可能是 snake_case,加载侧兼容后保存侧同样要留住
|
||||
{ model: "deepseek-v4-flash", reasoning_levels: ["low", "high", "max"] },
|
||||
{ model: "glm-5.1" }, // toggle 型:无表,全程不得凭空造表
|
||||
];
|
||||
|
||||
const roundTripped = normalizeCodexCatalogModelsForSave(
|
||||
stored.map(mapCodexCatalogModelForForm),
|
||||
);
|
||||
|
||||
expect(roundTripped).toEqual([
|
||||
{
|
||||
model: "glm-5.2",
|
||||
displayName: "GLM 5.2",
|
||||
reasoningLevels: ["high", "max"],
|
||||
},
|
||||
{ model: "deepseek-v4-flash", reasoningLevels: ["low", "high", "max"] },
|
||||
{ model: "glm-5.1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("trims reasoning level values on save", () => {
|
||||
// 手编 JSON 里的 " high " 不得原样落库/发给上游。
|
||||
expect(
|
||||
normalizeCodexCatalogModelsForSave([
|
||||
{ model: "glm-5.2", reasoningLevels: [" high ", "max"] },
|
||||
]),
|
||||
).toEqual([{ model: "glm-5.2", reasoningLevels: ["high", "max"] }]);
|
||||
});
|
||||
});
|
||||
@@ -242,4 +242,29 @@ describe("Codex Chat provider presets", () => {
|
||||
expect(preset?.codexChatReasoning).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("ships per-model reasoningLevels for OpenCode Go mirroring models.dev", () => {
|
||||
// Zen 网关的合法 effort 档位是逐模型的(models.dev reasoning_options,
|
||||
// 2026-08):统一并集映射会把 Codex 默认的 medium 发给只声明 high|max 的
|
||||
// glm-5.2(默认路径),此测试锁住逐模型表,防回退。
|
||||
const preset = codexProviderPresets.find((item) => item.name === "OpenCode Go");
|
||||
|
||||
expect(preset, "OpenCode Go preset").toBeDefined();
|
||||
expect(preset?.codexChatReasoning?.effortValueMode).toBe("zen");
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
(preset?.modelCatalog ?? []).map((model) => [
|
||||
model.model,
|
||||
model.reasoningLevels ?? null,
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
"glm-5.2": ["high", "max"],
|
||||
"glm-5.1": null, // toggle 型,无 effort 声明
|
||||
"kimi-k2.7-code": null,
|
||||
"deepseek-v4-pro": ["high", "max"],
|
||||
"deepseek-v4-flash": ["low", "high", "max"],
|
||||
"mimo-v2.5-pro": null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -103,6 +103,15 @@ describe("Codex preset pre-filled reasoning levels", () => {
|
||||
["Kimi For Coding", "kimi-for-coding-highspeed", ["high"]],
|
||||
["Kimi For Coding", "k3", ["low", "high", "max"], "high"],
|
||||
["Kimi For Coding", "k3-256k", ["low", "high", "max"], "high"],
|
||||
// OpenCode Go(Zen 网关):opencode 客户端 variants() 严格按各模型在
|
||||
// models.dev 的 reasoning_options 声明发 reasoning_effort(provider/
|
||||
// transform.ts)——glm-5.2 / deepseek-v4-pro 仅 high|max、v4-flash 另
|
||||
// 声明 low(均无 none:effort 档位非思考开关,none 会是假开关)。本 PR
|
||||
// 已为该预设补上 codexChatReasoning(effortValueMode:"zen"),代理转换
|
||||
// 层按同一张表逐模型钳制,表即下发生效的依据
|
||||
["OpenCode Go", "glm-5.2", ["high", "max"]],
|
||||
["OpenCode Go", "deepseek-v4-pro", ["high", "max"]],
|
||||
["OpenCode Go", "deepseek-v4-flash", ["low", "high", "max"]],
|
||||
];
|
||||
|
||||
it.each(EXPECTED)(
|
||||
@@ -117,15 +126,16 @@ describe("Codex preset pre-filled reasoning levels", () => {
|
||||
);
|
||||
|
||||
it("keeps deliberately-unfilled presets unfilled", () => {
|
||||
// Bailian qwen3-coder-plus 无 per-model 档位证据;OpenCode Go 是多厂商
|
||||
// Chat 网关且无 codexChatReasoning 声明(思考开关不生效,填 none 会是假
|
||||
// 开关),逐模型语义未经网关验证前保持不填。SiliconFlow .cn 的 M2.5 能否
|
||||
// 真正关思考无官方明文、ModelScope 是否透传思考字段未证实——真机验证前
|
||||
// 不造两态假开关(2026-08-15 盘点结论)
|
||||
// Bailian qwen3-coder-plus 无 per-model 档位证据。OpenCode Go 的
|
||||
// toggle/未收录模型保持不填:glm-5.1 是 toggle 型(models.dev 无 effort
|
||||
// 声明)、kimi-k2.7-code 官方标注不支持 effort、mimo-v2.5-pro 未收录
|
||||
// models.dev——与 opencode 客户端一致(代理侧无表不发 reasoning_effort
|
||||
// 字段)。SiliconFlow .cn 的 M2.5 能否真正关思考无官方明文、ModelScope
|
||||
// 是否透传思考字段未证实——真机验证前不造两态假开关(2026-08-15 盘点结论)
|
||||
const UNFILLED: Array<[string, string]> = [
|
||||
["Bailian", "qwen3-coder-plus"],
|
||||
["OpenCode Go", "glm-5.2"],
|
||||
["OpenCode Go", "deepseek-v4-flash"],
|
||||
["OpenCode Go", "glm-5.1"],
|
||||
["OpenCode Go", "kimi-k2.7-code"],
|
||||
["OpenCode Go", "mimo-v2.5-pro"],
|
||||
["SiliconFlow", "Pro/MiniMaxAI/MiniMax-M2.5"],
|
||||
["ModelScope", "ZhipuAI/GLM-5.2"],
|
||||
|
||||
Reference in new issue
Block a user