mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
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.
249 lines
8.9 KiB
TypeScript
249 lines
8.9 KiB
TypeScript
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",
|
||
});
|
||
});
|
||
});
|