diff --git a/app/api/validate-model/route.ts b/app/api/validate-model/route.ts index 9622b29..5d08fc8 100644 --- a/app/api/validate-model/route.ts +++ b/app/api/validate-model/route.ts @@ -372,12 +372,13 @@ export async function POST(req: Request) { break } - // GLM, Qwen, Kimi, Qiniu, Novita, MiMo - OpenAI compatible + // GLM, Qwen, Kimi, Qiniu, Novita, MiMo, Atlas Cloud - OpenAI compatible case "glm": case "qwen": case "kimi": case "qiniu": case "novita": + case "atlascloud": case "mimo": { const baseURL = baseUrl || diff --git a/env.example b/env.example index d1fafb5..da6ed9a 100644 --- a/env.example +++ b/env.example @@ -194,3 +194,8 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 # Get your API key from: https://platform.xiaomimimo.com/ # MIMO_API_KEY=your_mimo_api_key # MIMO_BASE_URL=https://api.xiaomimimo.com/v1 # Optional, default. Token Plan users: https://token-plan-cn.xiaomimimo.com/v1 + +# Atlas Cloud Configuration (Optional) +# Get your API key from: https://www.atlascloud.ai/console/api-keys +# ATLASCLOUD_API_KEY=your_atlascloud_api_key +# ATLASCLOUD_BASE_URL=https://api.atlascloud.ai/v1 # Optional, default. LLM chat endpoint; media generation uses a separate API. diff --git a/lib/ai-providers.ts b/lib/ai-providers.ts index 61e19e7..eb45831 100644 --- a/lib/ai-providers.ts +++ b/lib/ai-providers.ts @@ -118,6 +118,7 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [ "minimax", "novita", "mimo", + "atlascloud", ] // Bedrock provider options for Anthropic beta features @@ -543,6 +544,7 @@ function buildProviderOptions( case "kimi": case "qiniu": case "novita": + case "atlascloud": case "mimo": { // These providers don't have reasoning configs in AI SDK yet // Gateway passes through to underlying providers which handle their own configs @@ -581,6 +583,7 @@ export const PROVIDER_ENV_VARS: Record = { minimax: "MINIMAX_API_KEY", novita: "NOVITA_API_KEY", mimo: "MIMO_API_KEY", + atlascloud: "ATLASCLOUD_API_KEY", } /** @@ -1370,7 +1373,8 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { case "glm": case "qwen": case "qiniu": - case "novita": { + case "novita": + case "atlascloud": { const envVar = PROVIDER_ENV_VARS[provider] if (!envVar) { throw new Error( @@ -1414,7 +1418,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig { default: throw new Error( - `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita, mimo`, + `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita, mimo, atlascloud`, ) } diff --git a/lib/types/model-config.ts b/lib/types/model-config.ts index 810edd3..a1d9603 100644 --- a/lib/types/model-config.ts +++ b/lib/types/model-config.ts @@ -24,6 +24,7 @@ export type ProviderName = | "minimax" | "novita" | "mimo" + | "atlascloud" // Individual model configuration export interface ModelConfig { @@ -116,6 +117,7 @@ export const PROVIDER_LOGO_MAP: Record = { minimax: "minimax", novita: "novita", mimo: "xiaomi", + atlascloud: "openai", } // Provider metadata @@ -206,6 +208,10 @@ export const PROVIDER_INFO: Record< label: "MiMo (Xiaomi)", defaultBaseUrl: "https://api.xiaomimimo.com/v1", }, + atlascloud: { + label: "Atlas Cloud", + defaultBaseUrl: "https://api.atlascloud.ai/v1", + }, } // Suggested models per provider for quick add @@ -444,6 +450,7 @@ export const SUGGESTED_MODELS: Partial> = { "deepseek/deepseek-v4-flash", ], mimo: ["mimo-v2.5-pro", "mimo-v2.5"], + atlascloud: ["qwen/qwen3.5-flash", "deepseek-ai/deepseek-v4-pro"], } // Helper to generate UUID diff --git a/tests/unit/ai-providers.test.ts b/tests/unit/ai-providers.test.ts index 826cbb0..bba0ea1 100644 --- a/tests/unit/ai-providers.test.ts +++ b/tests/unit/ai-providers.test.ts @@ -206,6 +206,16 @@ vi.mock("@aihubmix/ai-sdk-provider", () => { return { aihubmix: mockAihubmix, createAihubmix: mockCreateAihubmix } }) +vi.mock("@ai-sdk/openai", () => { + const mockModel = { modelId: "test-model" } + const mockChat = vi.fn(() => mockModel) + const mockProviderFn = vi.fn(() => mockModel) as any + mockProviderFn.chat = mockChat + const mockCreateOpenAI = vi.fn(() => mockProviderFn) + const mockOpenai = vi.fn(() => mockModel) + return { createOpenAI: mockCreateOpenAI, openai: mockOpenai } +}) + describe("AIHubMix provider", () => { let createAihubmixMock: ReturnType const savedEnv: Record = {} @@ -262,6 +272,54 @@ describe("AIHubMix provider", () => { }) }) +describe("Atlas Cloud provider", () => { + let createOpenAIMock: ReturnType + const savedEnv: Record = {} + + beforeEach(async () => { + savedEnv.ATLASCLOUD_API_KEY = process.env.ATLASCLOUD_API_KEY + savedEnv.ATLASCLOUD_BASE_URL = process.env.ATLASCLOUD_BASE_URL + delete process.env.ATLASCLOUD_BASE_URL + + const mod = await import("@ai-sdk/openai") + createOpenAIMock = mod.createOpenAI as ReturnType + createOpenAIMock.mockClear() + }) + + afterEach(() => { + process.env.ATLASCLOUD_API_KEY = savedEnv.ATLASCLOUD_API_KEY + process.env.ATLASCLOUD_BASE_URL = savedEnv.ATLASCLOUD_BASE_URL + }) + + it("uses Atlas Cloud default endpoint with ATLASCLOUD_API_KEY", () => { + process.env.ATLASCLOUD_API_KEY = "server-atlas-key" + + getAIModel({ + provider: "atlascloud", + modelId: "qwen/qwen3.5-flash", + }) + + expect(createOpenAIMock).toHaveBeenCalledWith({ + apiKey: "server-atlas-key", + baseURL: "https://api.atlascloud.ai/v1", + }) + }) + + it("uses custom Atlas Cloud base URL when provided", () => { + getAIModel({ + provider: "atlascloud", + apiKey: "client-atlas-key", + baseUrl: "https://proxy.example.com/v1", + modelId: "deepseek-ai/deepseek-v4-pro", + }) + + expect(createOpenAIMock).toHaveBeenCalledWith({ + apiKey: "client-atlas-key", + baseURL: "https://proxy.example.com/v1", + }) + }) +}) + describe("Kimi provider uses createDeepSeek for reasoning_content support", () => { let createDeepSeekMock: ReturnType const savedEnv: Record = {} diff --git a/tests/unit/server-model-config.test.ts b/tests/unit/server-model-config.test.ts index 4dda82b..80d0c87 100644 --- a/tests/unit/server-model-config.test.ts +++ b/tests/unit/server-model-config.test.ts @@ -39,6 +39,22 @@ describe("ServerModelsConfigSchema", () => { expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow() }) + it("accepts Atlas Cloud provider names", () => { + const config: ServerModelsConfig = { + providers: [ + { + name: "Atlas Cloud Server", + provider: "atlascloud", + models: ["qwen/qwen3.5-flash"], + apiKeyEnv: "ATLASCLOUD_API_KEY", + baseUrlEnv: "ATLASCLOUD_BASE_URL", + }, + ], + } + + expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow() + }) + it("rejects invalid provider names", () => { const invalidConfig = { providers: [