Files
cc-switch/tests/components/ProviderForm.codexCatalog.test.ts
zayoka d01eab979e 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>
2026-08-16 18:59:09 +08:00

120 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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", () => {
expect(
normalizeCodexCatalogModelsForSave([
{ model: " deepseek-v4-flash ", displayName: " DeepSeek " },
{ model: "deepseek-v4-flash", displayName: "Duplicate" },
{ model: "", displayName: "Empty" },
{ model: "kimi-k2", contextWindow: "128000 tokens" },
]),
).toEqual([
{ model: "deepseek-v4-flash", displayName: "DeepSeek" },
{ model: "kimi-k2", contextWindow: 128000 },
]);
});
it("preserves native-profile overrides (parallel tool calls + input modalities + base instructions)", () => {
expect(
normalizeCodexCatalogModelsForSave([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions:
" You are Codex, a coding agent based on MiniMax-M3. ",
},
// false must be preserved (not dropped as falsy); empty modalities dropped;
// empty/whitespace baseInstructions dropped
{
model: "mimo-v2.5-pro",
supportsParallelToolCalls: false,
inputModalities: [],
baseInstructions: " ",
},
]),
).toEqual([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions: "You are Codex, a coding agent based on MiniMax-M3.",
},
{ model: "mimo-v2.5-pro", supportsParallelToolCalls: false },
]);
});
it("preserves per-model reasoning levels and default level", () => {
expect(
normalizeCodexCatalogModelsForSave([
{
model: "deepseek-v4-flash",
displayName: "DeepSeek V4 Flash",
reasoningLevels: ["none", "low", "medium", "high", "xhigh", "max"],
defaultReasoningLevel: " xhigh ",
},
// empty levels / whitespace default are dropped
{
model: "plain-model",
reasoningLevels: [],
defaultReasoningLevel: " ",
},
]),
).toEqual([
{
model: "deepseek-v4-flash",
displayName: "DeepSeek V4 Flash",
reasoningLevels: ["none", "low", "medium", "high", "xhigh", "max"],
defaultReasoningLevel: "xhigh",
},
{ 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"] }]);
});
});