Files
cc-switch/tests/config/opencodeProviderPresets.test.ts
Jason 50a9d103d3 chore(presets): refresh preset models to the latest vendor lineups
Move first-party, sponsor and relay presets to current model IDs so a
pasted Key works without editing the model list.

- Claude: opus/sonnet 5 -> 5.5, haiku 4.5 -> 5.5, fable 5 -> 5.1
- GPT: gpt-5.6-sol / gpt-6-sol -> gpt-6.1-sol; add gpt-6-astra/luna
- Gemini 3.6 Flash -> 3.8 Flash, 3.1 Pro preview -> 4 Argon;
  Grok 4.5 -> 4.7; GLM 5.1/5.2 -> 5.3 (+5.3 Flash);
  DeepSeek V4 Flash -> V4.1 Flash; Doubao 2.1 Pro 260628 -> 260915;
  StepFun Step Plan -> step-5-preview; add Kimi K3.1 Pro, Qwen3.8 Max,
  MiniMax M3.1 Flash where the site serves them
- Drop retired models: NVIDIA moonshotai/kimi-k3 (EOL 2026-10-15,
  now kimi-k2.6 without unverified Codex reasoning levels),
  MiMo V2.5 / V2.5 Pro, Claude Sonnet 4.5, MiniMax M2.7 on Tencent
  Token Plan Enterprise Pro
- Relay presets follow each site's own ID spelling and catalog;
  DIT.ai, BuyToken and hao.ai are left as configured from their docs;
  Soshow, SiliconFlow CN, OpenCode Zen and the Ark rolling alias stay
- MoArk Codex keeps the tested Kimi-K2.7-Code; K3.1 Pro and Qwen3.8
  Max stay out of its Codex catalog until their levels are verified

Tests updated to the new IDs; cross-app invariants unchanged.
2026-10-24 22:56:15 +08:00

249 lines
8.9 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 {
opencodeProviderPresets,
opencodeNpmPackages,
OPENCODE_PRESET_MODEL_VARIANTS,
} from "@/config/opencodeProviderPresets";
import { openclawProviderPresets } from "@/config/openclawProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import {
groupPresetRows,
presetVersionLayout,
} from "@/components/providers/forms/presetGroups";
describe("AWS Bedrock OpenCode Provider Presets", () => {
it("should include @ai-sdk/amazon-bedrock in npm packages", () => {
const bedrockPkg = opencodeNpmPackages.find(
(p) => p.value === "@ai-sdk/amazon-bedrock",
);
expect(bedrockPkg).toBeDefined();
expect(bedrockPkg!.label).toBe("Amazon Bedrock");
});
it("should include Bedrock model variants", () => {
const variants = OPENCODE_PRESET_MODEL_VARIANTS["@ai-sdk/amazon-bedrock"];
expect(variants).toBeDefined();
expect(variants.length).toBeGreaterThan(0);
const opusModel = variants.find((v) =>
v.id.includes("anthropic.claude-opus-5"),
);
expect(opusModel).toBeDefined();
});
const bedrockPreset = opencodeProviderPresets.find(
(p) => p.name === "AWS Bedrock",
);
it("should include AWS Bedrock preset", () => {
expect(bedrockPreset).toBeDefined();
});
it("Bedrock preset should use @ai-sdk/amazon-bedrock npm package", () => {
expect(bedrockPreset!.settingsConfig.npm).toBe("@ai-sdk/amazon-bedrock");
});
it("Bedrock preset should have region in options", () => {
expect(bedrockPreset!.settingsConfig.options).toHaveProperty("region");
});
it("Bedrock preset should have cloud_provider category", () => {
expect(bedrockPreset!.category).toBe("cloud_provider");
});
it("Bedrock preset should have template values for AWS credentials", () => {
expect(bedrockPreset!.templateValues).toBeDefined();
expect(bedrockPreset!.templateValues!.region).toBeDefined();
expect(bedrockPreset!.templateValues!.region.editorValue).toBe("us-west-2");
expect(bedrockPreset!.templateValues!.accessKeyId).toBeDefined();
expect(bedrockPreset!.templateValues!.secretAccessKey).toBeDefined();
});
it("Bedrock preset should include Claude models", () => {
const models = bedrockPreset!.settingsConfig.models;
expect(models).toBeDefined();
const modelIds = Object.keys(models!);
expect(modelIds.some((id) => id.includes("anthropic.claude"))).toBe(true);
});
it("Kimi For Coding preset should use Anthropic with the coding endpoint", () => {
const kimiForCodingPreset = opencodeProviderPresets.find(
(p) => p.name === "Kimi For Coding",
);
expect(kimiForCodingPreset).toBeDefined();
expect(kimiForCodingPreset!.settingsConfig.npm).toBe("@ai-sdk/anthropic");
expect(kimiForCodingPreset!.settingsConfig.options?.baseURL).toBe(
"https://api.kimi.com/coding/v1",
);
expect(kimiForCodingPreset!.templateValues?.baseURL.defaultValue).toBe(
"https://api.kimi.com/coding/v1",
);
});
it("Xiaomi MiMo presets should include official OpenCode model metadata", () => {
const presets = ["Xiaomi MiMo", "Xiaomi MiMo Token Plan (China)"].map(
(name) => opencodeProviderPresets.find((preset) => preset.name === name),
);
for (const preset of presets) {
expect(preset).toBeDefined();
expect(preset!.settingsConfig.models["mimo-v2.6-pro"]).toMatchObject({
limit: { context: 1048576, output: 131072 },
modalities: { input: ["text", "image"], output: ["text"] },
});
expect(preset!.settingsConfig.models["mimo-v2.6-flash"]).toMatchObject({
limit: { context: 1048576, output: 131072 },
modalities: { input: ["text", "image"], output: ["text"] },
});
}
});
it("Qwen presets should declare image/video input only for qwen3.8 models", () => {
const mediaPresets = [
"千问AI平台",
"千问AI平台 Token Plan",
"QwenCloud",
"QwenCloud Token Plan",
].map((name) =>
opencodeProviderPresets.find((preset) => preset.name === name),
);
for (const preset of mediaPresets) {
expect(preset).toBeDefined();
for (const modelId of ["qwen3.8-max", "qwen3.8-flash"]) {
expect(preset!.settingsConfig.models[modelId]).toMatchObject({
modalities: { input: ["text", "image", "video"], output: ["text"] },
});
}
}
// 显式声明会让代理停止剥离图片,因此不支持图片输入的模型必须留空。
for (const name of ["QwenCloud", "QwenCloud Token Plan"]) {
const preset = opencodeProviderPresets.find((p) => p.name === name);
expect(
preset!.settingsConfig.models["qwen3.7-max"]?.modalities,
).toBeUndefined();
}
const forCoding = opencodeProviderPresets.find(
(p) => p.name === "QwenCloud For Coding",
);
expect(forCoding).toBeDefined();
for (const model of Object.values(forCoding!.settingsConfig.models)) {
expect(model.modalities).toBeUndefined();
}
});
});
describe("OpenCode preset reasoning flags", () => {
// OpenCode 只给 reasoning 为真的模型生成思考档位;CC Switch 写的供应商 key
// 对不上 models.dev,不写就一个档位都没有。
const models = opencodeProviderPresets.flatMap((preset) =>
Object.entries(preset.settingsConfig.models ?? {}).map(([id, model]) => ({
preset: preset.name,
id,
model,
})),
);
it("marks Claude, GPT and DeepSeek V4 models as reasoning", () => {
const expected = models.filter(({ id }) =>
/claude-|gpt-[56]|deepseek-v4/.test(id),
);
expect(expected.length).toBeGreaterThan(0);
for (const { preset, id, model } of expected) {
expect(model.reasoning, `${preset} / ${id}`).toBe(true);
}
});
it("never claims reasoning on a model whose thinking is turned off", () => {
for (const { preset, id, model } of models) {
const thinking = (model.options as { thinking?: { type?: string } })
?.thinking;
if (thinking?.type === "disabled") {
expect(model.reasoning, `${preset} / ${id}`).not.toBe(true);
}
}
});
});
describe("OpenCode preset model limits", () => {
// 预设走 V1 格式(npm),OpenCode 要求 limit 同时带 context 和 output,
// 缺一个整份 opencode.json 报 Configuration is invalid,所有供应商一起不可用。
it("declares both context and output whenever a limit is set", () => {
for (const preset of opencodeProviderPresets) {
for (const [id, model] of Object.entries(
preset.settingsConfig.models ?? {},
)) {
if (model.limit === undefined) continue;
expect(model.limit.context, `${preset.name} / ${id}`).toBeTypeOf(
"number",
);
expect(model.limit.output, `${preset.name} / ${id}`).toBeTypeOf(
"number",
);
}
}
});
});
describe("SiliconFlow OpenCode provider presets", () => {
it.each(["SiliconFlow", "SiliconFlow en"])(
"%s includes the current Codex preset models at the matching endpoint",
(name) => {
const preset = opencodeProviderPresets.find(
(item) => item.name === name,
)!;
const openclaw = openclawProviderPresets.find(
(item) => item.name === name,
)!;
const codex = codexProviderPresets.find((item) => item.name === name)!;
expect(preset).toBeDefined();
expect(openclaw).toBeDefined();
expect(codex).toBeDefined();
expect(preset.settingsConfig.npm).toBe("@ai-sdk/openai-compatible");
expect(preset.settingsConfig.options.baseURL).toBe(
openclaw.settingsConfig.baseUrl,
);
expect(preset.settingsConfig.options.apiKey).toBe("");
const codexModels = codex.modelCatalog ?? [];
expect(codexModels.length).toBeGreaterThan(0);
expect(Object.keys(preset.settingsConfig.models)).toEqual(
expect.arrayContaining(codexModels.map((model) => model.model)),
);
for (const model of codexModels) {
expect(preset.settingsConfig.models[model.model].limit?.context).toBe(
model.contextWindow,
);
expect(preset.settingsConfig.models[model.model].reasoning).toBe(true);
}
expect(preset.category).toBe("aggregator");
expect(preset.partnerPromotionKey).toBe(openclaw.partnerPromotionKey);
expect(preset.icon).toBe(openclaw.icon);
},
);
it("groups both regions into one provider with a region selector", () => {
const entries = opencodeProviderPresets.map((preset, index) => ({
id: `opencode-${index}`,
preset,
}));
const rows = groupPresetRows(entries).filter((row) =>
row.versions.some(({ preset }) => preset.name.startsWith("SiliconFlow")),
);
expect(rows).toHaveLength(1);
expect(rows[0].family).toBe("siliconflow");
expect(rows[0].versions.map(({ preset }) => preset.regionKey)).toEqual([
"cn",
"intl",
]);
expect(presetVersionLayout(rows[0].versions)).toEqual({
kind: "single",
dimension: "region",
});
});
});