diff --git a/apps/cli/src/commands/connect.ts b/apps/cli/src/commands/connect.ts index 6945117f..a78adcc9 100644 --- a/apps/cli/src/commands/connect.ts +++ b/apps/cli/src/commands/connect.ts @@ -151,6 +151,7 @@ async function runBtcaAuth(providerId: string): Promise { } if ( + providerId === 'atlascloud' || providerId === 'opencode' || providerId === 'openrouter' || providerId === 'anthropic' || diff --git a/apps/cli/src/connect/constants.test.ts b/apps/cli/src/connect/constants.test.ts new file mode 100644 index 00000000..dbc2d683 --- /dev/null +++ b/apps/cli/src/connect/constants.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'bun:test'; + +import { + CURATED_MODELS, + PROVIDER_INFO, + PROVIDER_MODEL_DOCS, + PROVIDER_SETUP_LINKS +} from './constants.ts'; + +describe('Atlas Cloud connect metadata', () => { + test('provides authenticated setup and a default model', () => { + expect(PROVIDER_INFO.atlascloud).toEqual({ label: 'Atlas Cloud', requiresAuth: true }); + expect(CURATED_MODELS.atlascloud).toContainEqual({ + id: 'deepseek-ai/deepseek-v4-pro', + label: 'DeepSeek V4 Pro' + }); + expect(PROVIDER_SETUP_LINKS.atlascloud?.url).toBe('https://www.atlascloud.ai/console/api-keys'); + expect(PROVIDER_MODEL_DOCS.atlascloud?.url).toBe('https://api.atlascloud.ai/api/v1/models'); + }); +}); diff --git a/apps/cli/src/connect/constants.ts b/apps/cli/src/connect/constants.ts index c3beab37..b1ad6da1 100644 --- a/apps/cli/src/connect/constants.ts +++ b/apps/cli/src/connect/constants.ts @@ -1,4 +1,5 @@ export const CURATED_MODELS: Record = { + atlascloud: [{ id: 'deepseek-ai/deepseek-v4-pro', label: 'DeepSeek V4 Pro' }], openai: [ { id: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' }, { id: 'gpt-5.4', label: 'GPT-5.4' } @@ -35,6 +36,7 @@ export const CURATED_MODELS: Record = { }; export const PROVIDER_INFO: Record = { + atlascloud: { label: 'Atlas Cloud', requiresAuth: true }, opencode: { label: 'OpenCode Zen', requiresAuth: true }, 'github-copilot': { label: 'GitHub Copilot', requiresAuth: true }, anthropic: { label: 'Anthropic (Claude)', requiresAuth: true }, @@ -46,6 +48,7 @@ export const PROVIDER_INFO: Record = { + atlascloud: 'Atlas Cloud uses API keys: paste your Atlas Cloud API key to continue.', 'github-copilot': 'GitHub Copilot uses device flow OAuth: follow the browser prompt.', openai: 'OpenAI requires OAuth: btca will open a browser to sign in.', minimax: 'MiniMax uses API keys: paste your MiniMax API key to continue.', @@ -57,6 +60,10 @@ export const PROVIDER_AUTH_GUIDANCE: Record = { }; export const PROVIDER_MODEL_DOCS: Record = { + atlascloud: { + label: 'Model catalog', + url: 'https://api.atlascloud.ai/api/v1/models' + }, openai: { label: 'Model docs', url: 'https://platform.openai.com/docs/models' }, 'openai-compat': { label: 'OpenAI-compatible docs', @@ -82,6 +89,10 @@ export const PROVIDER_MODEL_WARNINGS: Record = { }; export const PROVIDER_SETUP_LINKS: Record = { + atlascloud: { + label: 'Get Atlas Cloud API key', + url: 'https://www.atlascloud.ai/console/api-keys' + }, opencode: { label: 'Get OpenCode Zen API key', url: 'https://opencode.ai/zen' }, minimax: { label: 'Get MiniMax API key', diff --git a/apps/docs/btca.spec.md b/apps/docs/btca.spec.md index 9f3471ed..9669996f 100644 --- a/apps/docs/btca.spec.md +++ b/apps/docs/btca.spec.md @@ -43,6 +43,7 @@ btca reads credentials from OpenCode’s auth storage: Supported providers: +- `atlascloud` — API key - `opencode` — API key - `openrouter` — API key - `openai` — OAuth (no API keys) @@ -53,6 +54,8 @@ Supported providers: Environment variable overrides: +- `ATLASCLOUD_API_KEY` (for provider `atlascloud`) +- `ATLASCLOUD_BASE_URL` (optional Atlas Cloud endpoint override) - `OPENCODE_API_KEY` (for provider `opencode`) - `OPENROUTER_API_KEY` (for provider `openrouter`) - `MINIMAX_API_KEY` (for provider `minimax`) @@ -63,7 +66,7 @@ Environment variable overrides: - If provider is `openai`, runs local OAuth flow (PKCE) and writes tokens into OpenCode auth. - If provider is `openai-compat`, prompts for base URL, provider name, model ID, and optional API key. -- If provider is `opencode`, `openrouter`, `anthropic`, `google`, or `minimax`, prompts for API key and writes into OpenCode auth. +- If provider is `atlascloud`, `opencode`, `openrouter`, `anthropic`, `google`, or `minimax`, prompts for API key and writes into OpenCode auth. - If provider is not handled directly, falls back to `opencode auth --provider `. **OpenAI-compatible provider inputs (and why):** diff --git a/apps/docs/guides/authentication.mdx b/apps/docs/guides/authentication.mdx index 8def3d5d..d720865f 100644 --- a/apps/docs/guides/authentication.mdx +++ b/apps/docs/guides/authentication.mdx @@ -12,6 +12,7 @@ btca reads credentials from OpenCode's auth storage: Supported providers: +- `atlascloud` (API key) - `opencode` (API key) - `openrouter` (API key) - `openai` (OAuth, no API keys) @@ -23,6 +24,8 @@ Supported providers: Environment variable overrides: +- `ATLASCLOUD_API_KEY` (for provider `atlascloud`) +- `ATLASCLOUD_BASE_URL` (optional Atlas Cloud endpoint override) - `OPENCODE_API_KEY` (for provider `opencode`) - `OPENROUTER_API_KEY` (for provider `openrouter`) - `MINIMAX_API_KEY` (for provider `minimax`) diff --git a/apps/server/src/providers/atlascloud.test.ts b/apps/server/src/providers/atlascloud.test.ts new file mode 100644 index 00000000..ba8e03f6 --- /dev/null +++ b/apps/server/src/providers/atlascloud.test.ts @@ -0,0 +1,33 @@ +import { afterEach, describe, expect, test } from 'bun:test'; + +import { getAuthStatus, getProviderAuthHint } from './auth.ts'; +import { ATLAS_CLOUD_BASE_URL, createAtlasCloud } from './atlascloud.ts'; +import { getProviderFactory, isProviderSupported } from './registry.ts'; + +describe('Atlas Cloud provider', () => { + afterEach(() => { + delete process.env.ATLASCLOUD_API_KEY; + }); + + test('registers an OpenAI-compatible chat model', () => { + const factory = getProviderFactory('atlascloud'); + expect(isProviderSupported('atlascloud')).toBe(true); + expect(factory).toBeDefined(); + + const model = createAtlasCloud({ apiKey: 'test-key' })('deepseek-ai/deepseek-v4-pro'); + expect(model.provider).toBe('atlascloud.chat'); + expect(model.modelId).toBe('deepseek-ai/deepseek-v4-pro'); + expect(ATLAS_CLOUD_BASE_URL).toBe('https://api.atlascloud.ai/v1'); + }); + + test('reads ATLASCLOUD_API_KEY for authentication', async () => { + process.env.ATLASCLOUD_API_KEY = 'test-atlas-key'; + + expect(await getAuthStatus('atlascloud')).toEqual({ + status: 'ok', + authType: 'api', + apiKey: 'test-atlas-key' + }); + expect(getProviderAuthHint('atlascloud')).toContain('ATLASCLOUD_API_KEY'); + }); +}); diff --git a/apps/server/src/providers/atlascloud.ts b/apps/server/src/providers/atlascloud.ts new file mode 100644 index 00000000..046699c4 --- /dev/null +++ b/apps/server/src/providers/atlascloud.ts @@ -0,0 +1,26 @@ +import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; + +export const ATLAS_CLOUD_BASE_URL = 'https://api.atlascloud.ai/v1'; + +const readEnv = (key: string) => { + const value = process.env[key]; + return value && value.trim().length > 0 ? value.trim() : undefined; +}; + +export function createAtlasCloud( + options: { + apiKey?: string; + baseURL?: string; + headers?: Record; + name?: string; + } = {} +) { + const provider = createOpenAICompatible({ + name: options.name ?? 'atlascloud', + apiKey: options.apiKey ?? readEnv('ATLASCLOUD_API_KEY'), + baseURL: options.baseURL ?? readEnv('ATLASCLOUD_BASE_URL') ?? ATLAS_CLOUD_BASE_URL, + headers: options.headers + }); + + return (modelId: string) => provider.chatModel(modelId); +} diff --git a/apps/server/src/providers/auth.ts b/apps/server/src/providers/auth.ts index 4f04294f..fff479fc 100644 --- a/apps/server/src/providers/auth.ts +++ b/apps/server/src/providers/auth.ts @@ -19,6 +19,7 @@ export type AuthStatus = | { status: 'invalid'; authType: AuthType }; const PROVIDER_AUTH_TYPES: Record = { + atlascloud: ['api'], opencode: ['api'], 'github-copilot': ['oauth'], openrouter: ['api'], @@ -35,6 +36,7 @@ const readEnv = (key: string) => { }; const getEnvApiKey = (providerId: string) => { + if (providerId === 'atlascloud') return readEnv('ATLASCLOUD_API_KEY'); if (providerId === 'openrouter') return readEnv('OPENROUTER_API_KEY'); if (providerId === 'opencode') return readEnv('OPENCODE_API_KEY'); if (providerId === 'minimax') return readEnv('MINIMAX_API_KEY'); @@ -141,6 +143,8 @@ export const getAuthStatus = async (providerId: string): Promise => export const getProviderAuthHint = (providerId: string) => { switch (providerId) { + case 'atlascloud': + return 'Set ATLASCLOUD_API_KEY or run "btca connect -p atlascloud".'; case 'github-copilot': return 'Run "btca connect -p github-copilot" and complete device flow OAuth.'; case 'openai': diff --git a/apps/server/src/providers/registry.ts b/apps/server/src/providers/registry.ts index 5ffd5a26..9a7e6219 100644 --- a/apps/server/src/providers/registry.ts +++ b/apps/server/src/providers/registry.ts @@ -5,6 +5,7 @@ import { createAnthropic } from '@ai-sdk/anthropic'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createAtlasCloud } from './atlascloud.ts'; import { createCopilotProvider } from './copilot.ts'; import { createOpenCodeZen } from './opencode.ts'; import { createOpenAICodex } from './openai.ts'; @@ -31,6 +32,9 @@ export type ProviderFactory = (options?: any) => { // Registry of all supported providers export const PROVIDER_REGISTRY: Record = { + // Atlas Cloud (OpenAI-compatible gateway) + atlascloud: createAtlasCloud as ProviderFactory, + // OpenCode Zen (curated models gateway) opencode: createOpenCodeZen as ProviderFactory,