diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 97e9b1500..546516567 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -3110,6 +3110,83 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, + { + name: "SiliconFlow", + family: "siliconflow", + regionKey: "cn", + websiteUrl: "https://siliconflow.cn", + apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve", + settingsConfig: { + npm: "@ai-sdk/openai-compatible", + name: "SiliconFlow", + options: { + baseURL: "https://api.siliconflow.cn/v1", + apiKey: "", + setCacheKey: true, + }, + models: { + "deepseek-ai/DeepSeek-V4-Flash": { + name: "DeepSeek V4 Flash", + reasoning: true, + limit: { context: 1048576, output: 384000 }, + }, + }, + }, + category: "aggregator", + isPartner: true, + partnerPromotionKey: "siliconflow", + icon: "siliconflow", + iconColor: "#6E29F6", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + }, + { + name: "SiliconFlow en", + family: "siliconflow", + regionKey: "intl", + websiteUrl: "https://siliconflow.com", + apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve", + settingsConfig: { + npm: "@ai-sdk/openai-compatible", + name: "SiliconFlow en", + options: { + baseURL: "https://api.siliconflow.com/v1", + apiKey: "", + setCacheKey: true, + }, + models: { + "MiniMaxAI/MiniMax-M3": { + name: "MiniMax M3", + reasoning: true, + limit: { context: 1048576, output: 131072 }, + modalities: { input: ["text", "image"], output: ["text"] }, + }, + "deepseek-ai/DeepSeek-V4.1-Flash": { + name: "DeepSeek V4.1 Flash", + reasoning: true, + limit: { context: 1048576, output: 384000 }, + modalities: { input: ["text", "image"], output: ["text"] }, + }, + }, + }, + category: "aggregator", + isPartner: true, + partnerPromotionKey: "siliconflow", + icon: "siliconflow", + iconColor: "#000000", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "sk-...", + editorValue: "", + }, + }, + }, { name: "AiHubMix", websiteUrl: "https://aihubmix.com", diff --git a/tests/config/opencodeProviderPresets.test.ts b/tests/config/opencodeProviderPresets.test.ts index 543e7873a..5336e68c3 100644 --- a/tests/config/opencodeProviderPresets.test.ts +++ b/tests/config/opencodeProviderPresets.test.ts @@ -4,6 +4,12 @@ import { 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", () => { @@ -161,3 +167,82 @@ describe("OpenCode preset reasoning flags", () => { } }); }); + +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", + }); + }); +});