From d6c3d54ebdad1cf0015b33320cae9cf668498c36 Mon Sep 17 00:00:00 2001 From: gorkii Date: Wed, 13 May 2026 07:56:49 -0400 Subject: [PATCH] Add a new provider SaladCloud --- docs/docs.json | 1 + .../integration-method/saladcloud.mdx | 39 +++++ .../registrySnapshots.test.ts.snap | 110 ++++++++++++- .../__tests__/cost/usageProcessor.test.ts | 5 + .../models/authors/alibaba/qwen3/endpoints.ts | 93 +++++++++++ .../models/authors/alibaba/qwen3/models.ts | 33 ++++ packages/cost/models/provider-helpers.ts | 10 ++ packages/cost/models/providers/index.ts | 7 +- packages/cost/models/providers/priorities.ts | 1 + packages/cost/models/providers/saladcloud.ts | 40 +++++ packages/cost/usage/getUsageProcessor.ts | 1 + web/data/providers.ts | 10 ++ .../assets/home/providers/saladcloud.svg | 34 ++++ .../test/ai-gateway/registry-alibaba.spec.ts | 154 ++++++++++++++++++ worker/test/setup.ts | 18 ++ 15 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 docs/getting-started/integration-method/saladcloud.mdx create mode 100644 packages/cost/models/providers/saladcloud.ts create mode 100644 web/public/assets/home/providers/saladcloud.svg diff --git a/docs/docs.json b/docs/docs.json index 2e7b990055..b3a02fe6fd 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -220,6 +220,7 @@ }, "getting-started/integration-method/openrouter", "getting-started/integration-method/perplexity", + "getting-started/integration-method/saladcloud", "getting-started/integration-method/together", { "group": "xAI", diff --git a/docs/getting-started/integration-method/saladcloud.mdx b/docs/getting-started/integration-method/saladcloud.mdx new file mode 100644 index 0000000000..d2eb2749f9 --- /dev/null +++ b/docs/getting-started/integration-method/saladcloud.mdx @@ -0,0 +1,39 @@ +--- +title: "SaladCloud Integration" +sidebarTitle: "SaladCloud" +description: "Use SaladCloud AI Gateway models through Helicone AI Gateway." +"twitter:title": "SaladCloud Integration - Helicone OSS LLM Observability" +--- + +SaladCloud AI Gateway is OpenAI-compatible. In Helicone AI Gateway, route to SaladCloud by saving your SaladCloud API key in Provider Keys and using a SaladCloud model route such as `qwen3.6-35b-a3b/saladcloud`. + + + + Create a Helicone API key from the [Developer](https://helicone.ai/developer) page and create a SaladCloud AI Gateway API key from SaladCloud. + + + In Helicone, add a new Provider Key for SaladCloud and paste your SaladCloud API key. + + + +```bash +curl https://ai-gateway.helicone.ai/v1/chat/completions \ + -H "Authorization: Bearer $HELICONE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "qwen3.6-35b-a3b/saladcloud", + "messages": [ + { + "role": "user", + "content": "Hello from SaladCloud through Helicone" + } + ] + }' +``` + + + + +Supported SaladCloud model routes include `qwen3.6-35b-a3b/saladcloud`, `qwen3.6-27b/saladcloud`, and `qwen3.5-9b/saladcloud`. + +For SaladCloud model and pricing details, see the [SaladCloud AI Gateway model reference](https://docs.salad.com/ai-gateway/reference/models) and [pricing reference](https://docs.salad.com/ai-gateway/reference/pricing). diff --git a/packages/__tests__/cost/__snapshots__/registrySnapshots.test.ts.snap b/packages/__tests__/cost/__snapshots__/registrySnapshots.test.ts.snap index ab023ddc00..d4ffdef9d3 100644 --- a/packages/__tests__/cost/__snapshots__/registrySnapshots.test.ts.snap +++ b/packages/__tests__/cost/__snapshots__/registrySnapshots.test.ts.snap @@ -298,6 +298,78 @@ exports[`Registry Snapshots endpoint configurations snapshot 1`] = ` "*", ], }, + "qwen3.5-9b:saladcloud": { + "context": 131072, + "crossRegion": false, + "maxTokens": 8192, + "modelId": "qwen3.5-9b", + "parameters": [ + "frequency_penalty", + "max_tokens", + "presence_penalty", + "response_format", + "seed", + "stop", + "stream", + "temperature", + "tool_choice", + "tools", + "top_p", + ], + "provider": "saladcloud", + "ptbEnabled": false, + "regions": [ + "*", + ], + }, + "qwen3.6-27b:saladcloud": { + "context": 131072, + "crossRegion": false, + "maxTokens": 16384, + "modelId": "qwen3.6-27b", + "parameters": [ + "frequency_penalty", + "max_tokens", + "presence_penalty", + "response_format", + "seed", + "stop", + "stream", + "temperature", + "tool_choice", + "tools", + "top_p", + ], + "provider": "saladcloud", + "ptbEnabled": false, + "regions": [ + "*", + ], + }, + "qwen3.6-35b-a3b:saladcloud": { + "context": 131072, + "crossRegion": false, + "maxTokens": 16384, + "modelId": "qwen3.6-35b-a3b", + "parameters": [ + "frequency_penalty", + "max_tokens", + "presence_penalty", + "response_format", + "seed", + "stop", + "stream", + "temperature", + "tool_choice", + "tools", + "top_p", + ], + "provider": "saladcloud", + "ptbEnabled": false, + "regions": [ + "*", + ], + }, }, "anthropic/claude-3-haiku-20240307": { "claude-3-haiku-20240307:anthropic": { @@ -7311,6 +7383,9 @@ exports[`Registry Snapshots model coverage snapshot 1`] = ` "novita", "novita", "openrouter", + "saladcloud", + "saladcloud", + "saladcloud", ], "anthropic/claude-3-haiku-20240307": [ "anthropic", @@ -7789,6 +7864,13 @@ exports[`Registry Snapshots pricing snapshot 1`] = ` "threshold": 0, }, ], + "saladcloud": [ + { + "input": 6e-8, + "output": 9e-8, + "threshold": 0, + }, + ], }, "anthropic/claude-3-haiku-20240307": { "anthropic": [ @@ -10813,6 +10895,24 @@ exports[`Registry Snapshots verify registry state 1`] = ` "novita", ], }, + { + "model": "qwen3.5-9b", + "providers": [ + "saladcloud", + ], + }, + { + "model": "qwen3.6-27b", + "providers": [ + "saladcloud", + ], + }, + { + "model": "qwen3.6-35b-a3b", + "providers": [ + "saladcloud", + ], + }, { "model": "sonar", "providers": [ @@ -10921,6 +11021,10 @@ exports[`Registry Snapshots verify registry state 1`] = ` "modelCount": 5, "provider": "perplexity", }, + { + "modelCount": 3, + "provider": "saladcloud", + }, { "modelCount": 23, "provider": "vertex", @@ -11049,9 +11153,9 @@ exports[`Registry Snapshots verify registry state 1`] = ` "claude-3.5-haiku:anthropic:*", ], "totalArchivedConfigs": 0, - "totalEndpoints": 329, - "totalModelProviderConfigs": 329, + "totalEndpoints": 332, + "totalModelProviderConfigs": 332, "totalModelsWithPtb": 108, - "totalProviders": 21, + "totalProviders": 22, } `; diff --git a/packages/__tests__/cost/usageProcessor.test.ts b/packages/__tests__/cost/usageProcessor.test.ts index 277d0056a9..6a03cda232 100644 --- a/packages/__tests__/cost/usageProcessor.test.ts +++ b/packages/__tests__/cost/usageProcessor.test.ts @@ -54,6 +54,11 @@ describe("getUsageProcessor", () => { expect(processor).toBeInstanceOf(OpenAIUsageProcessor); }); + it("should return OpenAIUsageProcessor for saladcloud provider", () => { + const processor = getUsageProcessor("saladcloud"); + expect(processor).toBeInstanceOf(OpenAIUsageProcessor); + }); + it("should return null for unsupported provider", () => { const processor = getUsageProcessor("unsupported-provider" as any); expect(processor).toBeNull(); diff --git a/packages/cost/models/authors/alibaba/qwen3/endpoints.ts b/packages/cost/models/authors/alibaba/qwen3/endpoints.ts index 955201011a..e919e318fe 100644 --- a/packages/cost/models/authors/alibaba/qwen3/endpoints.ts +++ b/packages/cost/models/authors/alibaba/qwen3/endpoints.ts @@ -74,6 +74,99 @@ export const endpoints = { "*": {}, }, }, + "qwen3.6-35b-a3b:saladcloud": { + providerModelId: "qwen3.6-35b-a3b", + provider: "saladcloud", + author: "qwen", + pricing: [ + { + threshold: 0, + input: 0.00000009, + output: 0.0000006, + }, + ], + contextLength: 131_072, + maxCompletionTokens: 16_384, + supportedParameters: [ + "tools", + "tool_choice", + "max_tokens", + "temperature", + "top_p", + "stop", + "frequency_penalty", + "presence_penalty", + "seed", + "response_format", + "stream", + ], + ptbEnabled: false, + endpointConfigs: { + "*": {}, + }, + }, + "qwen3.6-27b:saladcloud": { + providerModelId: "qwen3.6-27b", + provider: "saladcloud", + author: "qwen", + pricing: [ + { + threshold: 0, + input: 0.0000003, + output: 0.0000012, + }, + ], + contextLength: 131_072, + maxCompletionTokens: 16_384, + supportedParameters: [ + "tools", + "tool_choice", + "max_tokens", + "temperature", + "top_p", + "stop", + "frequency_penalty", + "presence_penalty", + "seed", + "response_format", + "stream", + ], + ptbEnabled: false, + endpointConfigs: { + "*": {}, + }, + }, + "qwen3.5-9b:saladcloud": { + providerModelId: "qwen3.5-9b", + provider: "saladcloud", + author: "qwen", + pricing: [ + { + threshold: 0, + input: 0.00000006, + output: 0.00000009, + }, + ], + contextLength: 131_072, + maxCompletionTokens: 8_192, + supportedParameters: [ + "tools", + "tool_choice", + "max_tokens", + "temperature", + "top_p", + "stop", + "frequency_penalty", + "presence_penalty", + "seed", + "response_format", + "stream", + ], + ptbEnabled: false, + endpointConfigs: { + "*": {}, + }, + }, "qwen3-30b-a3b:deepinfra": { providerModelId: "Qwen/Qwen3-30B-A3B", provider: "deepinfra", diff --git a/packages/cost/models/authors/alibaba/qwen3/models.ts b/packages/cost/models/authors/alibaba/qwen3/models.ts index be8f0811d9..4817f87774 100644 --- a/packages/cost/models/authors/alibaba/qwen3/models.ts +++ b/packages/cost/models/authors/alibaba/qwen3/models.ts @@ -12,6 +12,39 @@ export const models = { modality: { inputs: ["text"], outputs: ["text"] }, tokenizer: "GPT", }, + "qwen3.6-35b-a3b": { + name: "Qwen3.6 35B A3B", + author: "qwen", + description: + "Qwen3.6 35B A3B is a Mixture-of-Experts model available through SaladCloud AI Gateway, recommended for agentic tasks, complex multi-step reasoning, code generation, and instruction following.", + contextLength: 131_072, + maxOutputTokens: 16_384, + created: "2026-03-26T07:00:00.000Z", + modality: { inputs: ["text"], outputs: ["text"] }, + tokenizer: "Qwen", + }, + "qwen3.6-27b": { + name: "Qwen3.6 27B", + author: "qwen", + description: + "Qwen3.6 27B is a dense general-purpose model available through SaladCloud AI Gateway, balancing capability and speed for chat and assistant workloads.", + contextLength: 131_072, + maxOutputTokens: 16_384, + created: "2026-03-26T07:00:00.000Z", + modality: { inputs: ["text"], outputs: ["text"] }, + tokenizer: "Qwen", + }, + "qwen3.5-9b": { + name: "Qwen3.5 9B", + author: "qwen", + description: + "Qwen3.5 9B is a fast, lightweight model available through SaladCloud AI Gateway for high-volume queries, simple Q&A, and low-latency responses.", + contextLength: 131_072, + maxOutputTokens: 8_192, + created: "2026-03-26T07:00:00.000Z", + modality: { inputs: ["text"], outputs: ["text"] }, + tokenizer: "Qwen", + }, "qwen3-30b-a3b": { name: "Qwen3 30B A3B", author: "qwen", diff --git a/packages/cost/models/provider-helpers.ts b/packages/cost/models/provider-helpers.ts index ae00f70ee3..e4ee3a4b1f 100644 --- a/packages/cost/models/provider-helpers.ts +++ b/packages/cost/models/provider-helpers.ts @@ -62,6 +62,8 @@ export function heliconeProviderToModelProviderName( return "fireworks"; case "CANOPYWAVE": return "canopywave"; + case "saladcloud": + return "saladcloud"; // new registry does not have case "LOCAL": case "HELICONE": @@ -159,6 +161,14 @@ export const dbProviderToProvider = ( if (provider === "nebius" || provider === "Nebius") { return "nebius"; } + if ( + provider === "saladcloud" || + provider === "salad_cloud" || + provider === "SaladCloud" || + provider === "Salad Cloud" + ) { + return "saladcloud"; + } return null; }; diff --git a/packages/cost/models/providers/index.ts b/packages/cost/models/providers/index.ts index 963717d50d..1afc5e6b31 100644 --- a/packages/cost/models/providers/index.ts +++ b/packages/cost/models/providers/index.ts @@ -17,6 +17,7 @@ import { NovitaProvider } from "./novita"; import { OpenAIProvider } from "./openai"; import { OpenRouterProvider } from "./openrouter"; import { PerplexityProvider } from "./perplexity"; +import { SaladCloudProvider } from "./saladcloud"; import { VertexProvider } from "./vertex"; import { XAIProvider } from "./xai"; @@ -41,8 +42,9 @@ export const providers = { openai: new OpenAIProvider(), openrouter: new OpenRouterProvider(), perplexity: new PerplexityProvider(), + saladcloud: new SaladCloudProvider(), vertex: new VertexProvider(), - xai: new XAIProvider() + xai: new XAIProvider(), } as const; export type ModelProviderName = keyof typeof providers; @@ -84,12 +86,13 @@ export const ResponsesAPIEnabledProviders: ModelProviderName[] = [ "novita", "openrouter", "perplexity", + "saladcloud", "xai", "baseten", "fireworks", // anthropic and chat completions provider - "vertex" + "vertex", // anthropic only // none right now, need anthropic mapper diff --git a/packages/cost/models/providers/priorities.ts b/packages/cost/models/providers/priorities.ts index 38df67014c..1d49263b11 100644 --- a/packages/cost/models/providers/priorities.ts +++ b/packages/cost/models/providers/priorities.ts @@ -36,6 +36,7 @@ export const PROVIDER_PRIORITIES: Record = { novita: 4, perplexity: 4, + saladcloud: 4, vertex: 4, xai: 4, diff --git a/packages/cost/models/providers/saladcloud.ts b/packages/cost/models/providers/saladcloud.ts new file mode 100644 index 0000000000..6e07858aad --- /dev/null +++ b/packages/cost/models/providers/saladcloud.ts @@ -0,0 +1,40 @@ +import { BaseProvider } from "./base"; +import type { Endpoint, RequestBodyContext, RequestParams } from "../types"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export class SaladCloudProvider extends BaseProvider { + readonly displayName = "SaladCloud"; + readonly baseUrl = "https://ai.salad.cloud/"; + readonly auth = "api-key" as const; + readonly pricingPages = [ + "https://docs.salad.com/ai-gateway/reference/pricing", + ]; + readonly modelPages = ["https://docs.salad.com/ai-gateway/reference/models"]; + + buildUrl(endpoint: Endpoint, requestParams: RequestParams): string { + return `${this.baseUrl}v1/chat/completions`; + } + + buildRequestBody(endpoint: Endpoint, context: RequestBodyContext): string { + let updatedBody = context.parsedBody; + if (context.bodyMapping === "RESPONSES") { + updatedBody = context.toChatCompletions(updatedBody); + } + + const userChatTemplateKwargs = isRecord(updatedBody.chat_template_kwargs) + ? updatedBody.chat_template_kwargs + : {}; + + return JSON.stringify({ + ...updatedBody, + model: endpoint.providerModelId, + chat_template_kwargs: { + enable_thinking: false, + ...userChatTemplateKwargs, + }, + }); + } +} diff --git a/packages/cost/usage/getUsageProcessor.ts b/packages/cost/usage/getUsageProcessor.ts index 3dc612abf9..c7ff3f7246 100644 --- a/packages/cost/usage/getUsageProcessor.ts +++ b/packages/cost/usage/getUsageProcessor.ts @@ -27,6 +27,7 @@ export function getUsageProcessor( case "fireworks": case "cerebras": case "perplexity": + case "saladcloud": return new OpenAIUsageProcessor(); case "anthropic": return new AnthropicUsageProcessor(); diff --git a/web/data/providers.ts b/web/data/providers.ts index 8a22235279..230accb36b 100644 --- a/web/data/providers.ts +++ b/web/data/providers.ts @@ -174,6 +174,16 @@ export const providers: Provider[] = [ apiKeyPlaceholder: "...", relevanceScore: 40, }, + { + id: "saladcloud", + name: "SaladCloud", + logoUrl: "/assets/home/providers/saladcloud.svg", + description: "Configure your SaladCloud AI Gateway API keys", + docsUrl: "https://docs.salad.com/ai-gateway/explanation/overview", + apiKeyLabel: "SaladCloud API Key", + apiKeyPlaceholder: "...", + relevanceScore: 38, + }, { id: "openrouter", name: "OpenRouter", diff --git a/web/public/assets/home/providers/saladcloud.svg b/web/public/assets/home/providers/saladcloud.svg new file mode 100644 index 0000000000..a8ff3aa261 --- /dev/null +++ b/web/public/assets/home/providers/saladcloud.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/worker/test/ai-gateway/registry-alibaba.spec.ts b/worker/test/ai-gateway/registry-alibaba.spec.ts index e02f1bbe03..22f381ba05 100644 --- a/worker/test/ai-gateway/registry-alibaba.spec.ts +++ b/worker/test/ai-gateway/registry-alibaba.spec.ts @@ -35,6 +35,12 @@ const canopywaveAuthExpectations = { }, }; +const saladCloudAuthExpectations = { + headers: { + Authorization: /^Bearer /, + }, +}; + describe("Alibaba Registry Tests", () => { beforeEach(() => { // Clear all mocks between tests @@ -42,6 +48,154 @@ describe("Alibaba Registry Tests", () => { }); describe("BYOK Tests - Qwen Models", () => { + describe("SaladCloud Qwen models", () => { + it("should handle qwen3.6-35b-a3b with saladcloud provider", () => + runGatewayTest({ + model: "qwen3.6-35b-a3b/saladcloud", + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.6-35b-a3b", + data: createOpenAIMockResponse("qwen3.6-35b-a3b"), + expects: { + ...saladCloudAuthExpectations, + bodyContains: [ + '"chat_template_kwargs":{"enable_thinking":false}', + ], + }, + }, + ], + finalStatus: 200, + }, + })); + + it("should auto-select saladcloud provider when none specified", () => + runGatewayTest({ + model: "qwen3.6-35b-a3b", + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.6-35b-a3b", + data: createOpenAIMockResponse("qwen3.6-35b-a3b"), + expects: saladCloudAuthExpectations, + }, + ], + finalStatus: 200, + }, + })); + + it("should preserve explicit SaladCloud chat template kwargs", () => + runGatewayTest({ + model: "qwen3.6-35b-a3b/saladcloud", + request: { + body: { + chat_template_kwargs: { + enable_thinking: true, + }, + }, + }, + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.6-35b-a3b", + data: createOpenAIMockResponse("qwen3.6-35b-a3b"), + expects: { + ...saladCloudAuthExpectations, + bodyContains: [ + '"chat_template_kwargs":{"enable_thinking":true}', + ], + bodyDoesNotContain: [ + '"chat_template_kwargs":{"enable_thinking":false}', + ], + }, + }, + ], + finalStatus: 200, + }, + })); + + it("should handle qwen3.6-27b with saladcloud provider", () => + runGatewayTest({ + model: "qwen3.6-27b/saladcloud", + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.6-27b", + data: createOpenAIMockResponse("qwen3.6-27b"), + expects: saladCloudAuthExpectations, + }, + ], + finalStatus: 200, + }, + })); + + it("should handle qwen3.5-9b with saladcloud provider", () => + runGatewayTest({ + model: "qwen3.5-9b/saladcloud", + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.5-9b", + data: createOpenAIMockResponse("qwen3.5-9b"), + expects: saladCloudAuthExpectations, + }, + ], + finalStatus: 200, + }, + })); + + it("should forward streaming and tool parameters", () => + runGatewayTest({ + model: "qwen3.6-35b-a3b/saladcloud", + request: { + stream: true, + body: { + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the weather", + parameters: { + type: "object", + properties: { + location: { type: "string" }, + }, + }, + }, + }, + ], + tool_choice: "auto", + }, + }, + expected: { + providers: [ + { + url: "https://ai.salad.cloud/v1/chat/completions", + response: "success", + model: "qwen3.6-35b-a3b", + data: createOpenAIMockResponse("qwen3.6-35b-a3b"), + expects: { + ...saladCloudAuthExpectations, + bodyContains: ['"stream":true', "get_weather", "tool_choice"], + }, + }, + ], + finalStatus: 200, + }, + })); + }); + describe("qwen3-30b-a3b", () => { it("should handle deepinfra provider", () => runGatewayTest({ diff --git a/worker/test/setup.ts b/worker/test/setup.ts index ff949da294..64bb63b7d9 100644 --- a/worker/test/setup.ts +++ b/worker/test/setup.ts @@ -279,6 +279,15 @@ vi.mock("@supabase/supabase-js", () => ({ config: null, byok_enabled: isByokEnabled, }, + saladcloud: { + org_id: "test-org-id", + provider_name: "saladcloud", + decrypted_provider_key: "test-saladcloud-api-key", + decrypted_provider_secret_key: null, + auth_type: "api_key", + config: null, + byok_enabled: isByokEnabled, + }, openrouter: { org_id: "test-org-id", provider_name: "openrouter", @@ -446,6 +455,15 @@ vi.mock("@supabase/supabase-js", () => ({ config: null, byok_enabled: true, }, + saladcloud: { + org_id: "0afe3a6e-d095-4ec0-bc1e-2af6f57bd2a5", + provider_name: "saladcloud", + decrypted_provider_key: "helicone-saladcloud-api-key", + decrypted_provider_secret_key: null, + auth_type: "api_key", + config: null, + byok_enabled: true, + }, azure: { org_id: "0afe3a6e-d095-4ec0-bc1e-2af6f57bd2a5", provider_name: "azure",