diff --git a/core/llm/llms/OpenAI-compatible.vitest.ts b/core/llm/llms/OpenAI-compatible.vitest.ts index 402fb7e7585..318f06657a1 100644 --- a/core/llm/llms/OpenAI-compatible.vitest.ts +++ b/core/llm/llms/OpenAI-compatible.vitest.ts @@ -33,6 +33,7 @@ import NCompass from "./NCompass.js"; import LlamaStack from "./LlamaStack.js"; import Nebius from "./Nebius.js"; import OVHcloud from "./OVHcloud.js"; +import SaladCloud from "./SaladCloud.js"; // Base OpenAI tests import { afterEach, describe, expect, test, vi } from "vitest"; @@ -457,3 +458,13 @@ createOpenAISubclassTests(OVHcloud, { providerName: "ovhcloud", defaultApiBase: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/", }); + +createOpenAISubclassTests(SaladCloud, { + providerName: "saladcloud", + defaultApiBase: "https://ai.salad.cloud/v1/", + customBodyOptions: { + chat_template_kwargs: { + enable_thinking: false, + }, + }, +}); diff --git a/core/llm/llms/SaladCloud.ts b/core/llm/llms/SaladCloud.ts new file mode 100644 index 00000000000..f49919e0ab7 --- /dev/null +++ b/core/llm/llms/SaladCloud.ts @@ -0,0 +1,50 @@ +import { ChatCompletionCreateParams } from "openai/resources/index"; +import { ChatMessage, CompletionOptions, LLMOptions } from "../.."; + +import OpenAI from "./OpenAI"; + +class SaladCloud extends OpenAI { + static providerName = "saladcloud"; + static defaultOptions: Partial = { + apiBase: "https://ai.salad.cloud/v1/", + useLegacyCompletionsEndpoint: false, + }; + + protected _convertArgs( + options: CompletionOptions, + messages: ChatMessage[], + ): ChatCompletionCreateParams { + return this.addDefaultChatTemplateKwargs( + super._convertArgs(options, messages), + ); + } + + protected modifyChatBody( + body: ChatCompletionCreateParams, + ): ChatCompletionCreateParams { + return this.addDefaultChatTemplateKwargs(super.modifyChatBody(body)); + } + + private addDefaultChatTemplateKwargs( + body: ChatCompletionCreateParams, + ): ChatCompletionCreateParams { + const extendedBody = body as ChatCompletionCreateParams & { + chat_template_kwargs?: Record; + }; + const existingKwargs = + extendedBody.chat_template_kwargs && + typeof extendedBody.chat_template_kwargs === "object" && + !Array.isArray(extendedBody.chat_template_kwargs) + ? extendedBody.chat_template_kwargs + : {}; + + extendedBody.chat_template_kwargs = { + enable_thinking: false, + ...existingKwargs, + }; + + return extendedBody; + } +} + +export default SaladCloud; diff --git a/core/llm/llms/SaladCloud.vitest.ts b/core/llm/llms/SaladCloud.vitest.ts new file mode 100644 index 00000000000..e946a9f3417 --- /dev/null +++ b/core/llm/llms/SaladCloud.vitest.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; + +import SaladCloud from "./SaladCloud.js"; + +describe("SaladCloud", () => { + it("disables Qwen thinking output by default", () => { + const saladCloud = new SaladCloud({ + apiKey: "test-api-key", + model: "qwen3.6-35b-a3b", + }); + + const body = (saladCloud as any)._convertArgs( + { + model: "qwen3.6-35b-a3b", + maxTokens: 128, + }, + [{ role: "user", content: "hello" }], + ); + + expect(body.chat_template_kwargs).toEqual({ + enable_thinking: false, + }); + }); + + it("preserves explicitly provided chat template kwargs", () => { + const saladCloud = new SaladCloud({ + apiKey: "test-api-key", + model: "qwen3.6-35b-a3b", + }); + + const body = (saladCloud as any).modifyChatBody({ + model: "qwen3.6-35b-a3b", + messages: [], + chat_template_kwargs: { + enable_thinking: true, + custom: "value", + }, + }); + + expect(body.chat_template_kwargs).toEqual({ + enable_thinking: true, + custom: "value", + }); + }); +}); diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts index 453b2d90cd8..a33deee97ed 100644 --- a/core/llm/llms/index.ts +++ b/core/llm/llms/index.ts @@ -55,6 +55,7 @@ import OVHcloud from "./OVHcloud"; import { Relace } from "./Relace"; import Replicate from "./Replicate"; import SageMaker from "./SageMaker"; +import SaladCloud from "./SaladCloud"; import SambaNova from "./SambaNova"; import Scaleway from "./Scaleway"; import SiliconFlow from "./SiliconFlow"; @@ -115,6 +116,7 @@ export const LLMClasses = [ ClawRouter, Nvidia, Vllm, + SaladCloud, SambaNova, MockLLM, TestLLM, diff --git a/core/llm/toolSupport.test.ts b/core/llm/toolSupport.test.ts index af980c3f1d8..d34792e0808 100644 --- a/core/llm/toolSupport.test.ts +++ b/core/llm/toolSupport.test.ts @@ -125,6 +125,20 @@ describe("PROVIDER_TOOL_SUPPORT", () => { }); }); + describe("saladcloud", () => { + const supportsFn = PROVIDER_TOOL_SUPPORT["saladcloud"]; + + it("should return true for SaladCloud Qwen models", () => { + expect(supportsFn("qwen3.6-35b-a3b")).toBe(true); + expect(supportsFn("qwen3.6-27b")).toBe(true); + expect(supportsFn("qwen3.5-9b")).toBe(true); + }); + + it("should return false for unverified SaladCloud models", () => { + expect(supportsFn("gemma-4-26b-a4b-instruct")).toBe(false); + }); + }); + describe("cohere", () => { const supportsFn = PROVIDER_TOOL_SUPPORT["cohere"]; diff --git a/core/llm/toolSupport.ts b/core/llm/toolSupport.ts index 1b74d6188b6..9d782f6c50d 100644 --- a/core/llm/toolSupport.ts +++ b/core/llm/toolSupport.ts @@ -79,6 +79,10 @@ export const PROVIDER_TOOL_SUPPORT: Record boolean> = return false; }, + saladcloud: (model) => { + const lower = model.toLowerCase(); + return lower.startsWith("qwen3.5-") || lower.startsWith("qwen3.6-"); + }, cohere: (model) => { const lower = model.toLowerCase(); if (lower.startsWith("command-a-vision")) { diff --git a/docs/customize/model-providers/more/saladcloud.mdx b/docs/customize/model-providers/more/saladcloud.mdx new file mode 100644 index 00000000000..1fea12bcfec --- /dev/null +++ b/docs/customize/model-providers/more/saladcloud.mdx @@ -0,0 +1,53 @@ +--- +title: "SaladCloud" +description: "Configure SaladCloud AI Gateway with Continue" +--- + +You can get an API key from the [SaladCloud portal](https://portal.salad.com/api-key) + +## Available Models + +Available models can be found on the [SaladCloud AI Gateway pricing page](https://docs.salad.com/ai-gateway/reference/pricing). Continue includes presets for the Qwen models currently returned by SaladCloud's `/v1/models` endpoint: `qwen3.6-35b-a3b`, `qwen3.6-27b`, and `qwen3.5-9b`. + +## Chat Model + + + + ```yaml title="config.yaml" + name: My Config + version: 0.0.1 + schema: v1 + + models: + - name: Qwen 3.6 35B A3B + provider: saladcloud + model: qwen3.6-35b-a3b + apiKey: + ``` + + + ```json title="config.json" + { + "models": [ + { + "title": "Qwen 3.6 35B A3B", + "provider": "saladcloud", + "model": "qwen3.6-35b-a3b", + "apiKey": "" + } + ] + } + ``` + + + + + Continue disables Qwen thinking output by default with + `chat_template_kwargs.enable_thinking: false`. To enable thinking, add + `requestOptions.extraBodyProperties.chat_template_kwargs.enable_thinking` and + set it to `true` in your model configuration. + + +## Embeddings Model + +SaladCloud AI Gateway currently focuses on chat/completion models. diff --git a/gui/src/pages/AddNewModel/configs/models.ts b/gui/src/pages/AddNewModel/configs/models.ts index 2482b1af241..8a825064c0b 100644 --- a/gui/src/pages/AddNewModel/configs/models.ts +++ b/gui/src/pages/AddNewModel/configs/models.ts @@ -3310,6 +3310,52 @@ export const models: { [key: string]: ModelPackage } = { isOpenSource: false, }, + // SaladCloud models + saladQwen36_35bA3B: { + title: "Qwen 3.6 35B A3B", + description: + "Qwen 3.6 35B A3B is a fast mixture-of-experts model with 35B total parameters and 3B active, available via SaladCloud AI Gateway.", + refUrl: "https://docs.salad.com/ai-gateway/explanation/overview", + params: { + title: "Qwen 3.6 35B A3B", + model: "qwen3.6-35b-a3b", + contextLength: 262_144, + capabilities: { tools: true }, + }, + icon: "qwen.png", + providerOptions: ["saladcloud"], + isOpenSource: true, + }, + saladQwen36_27b: { + title: "Qwen 3.6 27B", + description: + "Qwen 3.6 27B is a capable instruction-tuned model available via SaladCloud AI Gateway with a 256K context window.", + refUrl: "https://docs.salad.com/ai-gateway/explanation/overview", + params: { + title: "Qwen 3.6 27B", + model: "qwen3.6-27b", + contextLength: 262_144, + capabilities: { tools: true }, + }, + icon: "qwen.png", + providerOptions: ["saladcloud"], + isOpenSource: true, + }, + saladQwen35_9b: { + title: "Qwen 3.5 9B", + description: + "Qwen 3.5 9B is a lightweight and efficient model available via SaladCloud AI Gateway.", + refUrl: "https://docs.salad.com/ai-gateway/explanation/overview", + params: { + title: "Qwen 3.5 9B", + model: "qwen3.5-9b", + contextLength: 262_144, + capabilities: { tools: true }, + }, + icon: "qwen.png", + providerOptions: ["saladcloud"], + isOpenSource: true, + }, // Xiaomi Mimo models mimoV2Flash: { title: "mimo-v2-flash", diff --git a/gui/src/pages/AddNewModel/configs/providers.ts b/gui/src/pages/AddNewModel/configs/providers.ts index 8e2826e4fdd..0065c3bb5c8 100644 --- a/gui/src/pages/AddNewModel/configs/providers.ts +++ b/gui/src/pages/AddNewModel/configs/providers.ts @@ -996,6 +996,33 @@ To get started, [register](https://dataplatform.cloud.ibm.com/registration/stepo models.MetaLlama3, ], }, + saladcloud: { + title: "SaladCloud", + provider: "saladcloud", + refPage: "saladcloud", + description: "Use SaladCloud AI Gateway to access open-source models", + longDescription: `SaladCloud AI Gateway provides OpenAI-compatible access to open-source models at flat-rate pricing. To get started:\n1. Sign up at [salad.com](https://salad.com)\n2. Obtain an API key from the [SaladCloud portal](https://portal.salad.com)\n3. Paste below\n4. Select a model preset`, + tags: [ModelProviderTags.RequiresApiKey, ModelProviderTags.OpenSource], + params: { + apiKey: "", + }, + collectInputFor: [ + { + inputType: "text", + key: "apiKey", + label: "API Key", + placeholder: "Enter your SaladCloud API key", + required: true, + }, + ...completionParamsInputsConfigs, + ], + packages: [ + models.saladQwen36_35bA3B, + models.saladQwen36_27b, + models.saladQwen35_9b, + ], + apiKeyUrl: "https://portal.salad.com/api-key", + }, sambanova: { title: "SambaNova", provider: "sambanova",