diff --git a/.changeset/atlascloud-provider.md b/.changeset/atlascloud-provider.md new file mode 100644 index 000000000..532110f00 --- /dev/null +++ b/.changeset/atlascloud-provider.md @@ -0,0 +1,6 @@ +--- +"@voltagent/core": patch +"create-voltagent-app": patch +--- + +Add Atlas Cloud as an OpenAI-compatible model provider. diff --git a/packages/core/src/registries/model-provider-registry-atlascloud.spec.ts b/packages/core/src/registries/model-provider-registry-atlascloud.spec.ts new file mode 100644 index 000000000..882feb9fd --- /dev/null +++ b/packages/core/src/registries/model-provider-registry-atlascloud.spec.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +let createOpenAICompatibleCalls: unknown[][] = []; + +vi.mock("@voltagent/internal", async () => { + const actual = await vi.importActual("@voltagent/internal"); + return actual; +}); + +vi.mock("@ai-sdk/openai-compatible", () => ({ + createOpenAICompatible: (...args: unknown[]) => { + createOpenAICompatibleCalls.push(args); + const mockModel = { + modelId: "mock-model", + specificationVersion: "v1", + provider: "atlascloud", + }; + return { + languageModel: (modelId: string) => ({ ...mockModel, modelId }), + chatModel: (modelId: string) => ({ ...mockModel, modelId }), + }; + }, +})); + +describe("Atlas Cloud provider registry", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + createOpenAICompatibleCalls = []; + (globalThis as Record).___voltagent_model_provider_registry = undefined; + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + (globalThis as Record).___voltagent_model_provider_registry = undefined; + }); + + it("lists Atlas Cloud as a registered provider", async () => { + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + + expect(registry.listProviders()).toContain("atlascloud"); + }); + + it("loads Atlas Cloud through the OpenAI-compatible adapter", async () => { + process.env.ATLASCLOUD_API_KEY = "test-atlas-key"; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + const model = await registry.resolveLanguageModel("atlascloud/qwen/qwen3.5-flash"); + + expect(model).toMatchObject({ + modelId: "qwen/qwen3.5-flash", + provider: "atlascloud", + }); + + expect(createOpenAICompatibleCalls).toHaveLength(1); + const config = createOpenAICompatibleCalls[0]?.[0] as Record; + expect(config).toMatchObject({ + name: "atlascloud", + baseURL: "https://api.atlascloud.ai/v1", + apiKey: "test-atlas-key", + supportsStructuredOutputs: true, + }); + }); + + it("supports the conventional ATLASCLOUD_BASE_URL override", async () => { + process.env.ATLASCLOUD_API_KEY = "test-atlas-key"; + process.env.ATLASCLOUD_BASE_URL = "https://atlas-proxy.example/v1"; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + await registry.resolveLanguageModel("atlascloud/deepseek-ai/deepseek-v4-pro"); + + const config = createOpenAICompatibleCalls[0]?.[0] as Record; + expect(config.baseURL).toBe("https://atlas-proxy.example/v1"); + }); + + it("throws a helpful error when ATLASCLOUD_API_KEY is not set", async () => { + const { ATLASCLOUD_API_KEY: _atlascloudApiKey, ...envWithoutAtlasCloud } = process.env; + process.env = envWithoutAtlasCloud; + + const { ModelProviderRegistry } = await import("./model-provider-registry"); + const registry = ModelProviderRegistry.getInstance(); + + await expect(registry.resolveLanguageModel("atlascloud/qwen/qwen3.5-flash")).rejects.toThrow( + /ATLASCLOUD_API_KEY/, + ); + }); +}); diff --git a/packages/core/src/registries/model-provider-registry.ts b/packages/core/src/registries/model-provider-registry.ts index 5de3c0614..5f75319d5 100644 --- a/packages/core/src/registries/model-provider-registry.ts +++ b/packages/core/src/registries/model-provider-registry.ts @@ -144,6 +144,14 @@ const EXTRA_PROVIDER_REGISTRY: ModelProviderRegistryEntry[] = [ npm: "ollama-ai-provider-v2", doc: "https://ollama.com", }, + { + id: "atlascloud", + name: "Atlas Cloud", + npm: "@ai-sdk/openai-compatible", + api: "https://api.atlascloud.ai/v1", + env: ["ATLASCLOUD_API_KEY"], + doc: "https://docs.atlascloud.ai", + }, { id: "minimax", name: "MiniMax", diff --git a/packages/create-voltagent-app/src/cli.integration.spec.ts b/packages/create-voltagent-app/src/cli.integration.spec.ts index 1e6741c36..1bb3c5ecf 100644 --- a/packages/create-voltagent-app/src/cli.integration.spec.ts +++ b/packages/create-voltagent-app/src/cli.integration.spec.ts @@ -102,6 +102,13 @@ const scenarios: Scenario[] = [ aiProvider: "mistral", ide: "none", }, + { + name: "atlascloud-hono-pnpm-none", + server: "hono", + packageManager: "pnpm", + aiProvider: "atlascloud", + ide: "none", + }, { name: "ollama-elysia-bun-none", server: "elysia", @@ -317,7 +324,15 @@ describe.sequential("create-voltagent-app CLI option matrix", () => { expect(serverCoverage).toEqual(new Set(["hono", "elysia"])); expect(packageManagerCoverage).toEqual(new Set(["pnpm", "bun", "yarn", "npm"])); expect(providerCoverage).toEqual( - new Set(["openai", "anthropic", "google", "groq", "mistral", "ollama"]), + new Set([ + "openai", + "anthropic", + "google", + "groq", + "mistral", + "atlascloud", + "ollama", + ]), ); expect(ideCoverage).toEqual( new Set>(["none", "cursor", "windsurf", "vscode"]), diff --git a/packages/create-voltagent-app/src/cli.ts b/packages/create-voltagent-app/src/cli.ts index 54b3b31d4..f29e6a858 100644 --- a/packages/create-voltagent-app/src/cli.ts +++ b/packages/create-voltagent-app/src/cli.ts @@ -150,6 +150,10 @@ export const runCLI = async (): Promise => { { name: `Google (${AI_PROVIDER_CONFIG.google.modelName})`, value: "google" }, { name: `Groq (${AI_PROVIDER_CONFIG.groq.modelName})`, value: "groq" }, { name: `Mistral (${AI_PROVIDER_CONFIG.mistral.modelName})`, value: "mistral" }, + { + name: `Atlas Cloud (${AI_PROVIDER_CONFIG.atlascloud.modelName})`, + value: "atlascloud", + }, { name: `Ollama (${AI_PROVIDER_CONFIG.ollama.modelName} - Local)`, value: "ollama" }, ], default: "openai", diff --git a/packages/create-voltagent-app/src/types.ts b/packages/create-voltagent-app/src/types.ts index fa7513c99..fe2878542 100644 --- a/packages/create-voltagent-app/src/types.ts +++ b/packages/create-voltagent-app/src/types.ts @@ -1,4 +1,11 @@ -export type AIProvider = "openai" | "anthropic" | "google" | "groq" | "mistral" | "ollama"; +export type AIProvider = + | "openai" + | "anthropic" + | "google" + | "groq" + | "mistral" + | "atlascloud" + | "ollama"; export type ServerProvider = "hono" | "elysia"; export type PackageManager = "npm" | "bun" | "yarn" | "pnpm"; @@ -57,6 +64,13 @@ export const AI_PROVIDER_CONFIG = { modelName: "Mistral Large 2", apiKeyUrl: "https://console.mistral.ai/api-keys", }, + atlascloud: { + name: "Atlas Cloud", + envVar: "ATLASCLOUD_API_KEY", + model: "atlascloud/qwen/qwen3.5-flash", + modelName: "Qwen3.5 Flash", + apiKeyUrl: "https://docs.atlascloud.ai", + }, ollama: { name: "Ollama (Local)", envVar: null,