Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/commands/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ async function runBtcaAuth(providerId: string): Promise<boolean> {
}

if (
providerId === 'atlascloud' ||
providerId === 'opencode' ||
providerId === 'openrouter' ||
providerId === 'anthropic' ||
Expand Down
20 changes: 20 additions & 0 deletions apps/cli/src/connect/constants.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
11 changes: 11 additions & 0 deletions apps/cli/src/connect/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const CURATED_MODELS: Record<string, { id: string; label: string }[]> = {
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' }
Expand Down Expand Up @@ -35,6 +36,7 @@ export const CURATED_MODELS: Record<string, { id: string; label: string }[]> = {
};

export const PROVIDER_INFO: Record<string, { label: string; requiresAuth: boolean }> = {
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 },
Expand All @@ -46,6 +48,7 @@ export const PROVIDER_INFO: Record<string, { label: string; requiresAuth: boolea
};

export const PROVIDER_AUTH_GUIDANCE: Record<string, string> = {
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.',
Expand All @@ -57,6 +60,10 @@ export const PROVIDER_AUTH_GUIDANCE: Record<string, string> = {
};

export const PROVIDER_MODEL_DOCS: Record<string, { label: string; url: string }> = {
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',
Expand All @@ -82,6 +89,10 @@ export const PROVIDER_MODEL_WARNINGS: Record<string, string> = {
};

export const PROVIDER_SETUP_LINKS: Record<string, { label: string; url: string }> = {
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',
Expand Down
5 changes: 4 additions & 1 deletion apps/docs/btca.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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`)
Expand All @@ -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 <provider>`.

**OpenAI-compatible provider inputs (and why):**
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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`)
Expand Down
33 changes: 33 additions & 0 deletions apps/server/src/providers/atlascloud.test.ts
Original file line number Diff line number Diff line change
@@ -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;
});
Comment on lines +8 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the previous environment value after the test.

afterEach always deletes process.env.ATLASCLOUD_API_KEY. If the test process already contains this variable, later tests lose shared state. Capture the previous value and restore it after each test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/providers/atlascloud.test.ts` around lines 8 - 10, Update the
test cleanup around the ATLASCLOUD_API_KEY environment variable: capture its
pre-test value and have afterEach restore that value when it existed, deleting
the variable only when it was previously unset. Preserve isolation without
discarding shared process state.


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');
});
});
26 changes: 26 additions & 0 deletions apps/server/src/providers/atlascloud.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
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);
}
4 changes: 4 additions & 0 deletions apps/server/src/providers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AuthStatus =
| { status: 'invalid'; authType: AuthType };

const PROVIDER_AUTH_TYPES: Record<string, readonly AuthType[]> = {
atlascloud: ['api'],
opencode: ['api'],
'github-copilot': ['oauth'],
openrouter: ['api'],
Expand All @@ -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');
Expand Down Expand Up @@ -141,6 +143,8 @@ export const getAuthStatus = async (providerId: string): Promise<AuthStatus> =>

export const getProviderAuthHint = (providerId: string) => {
switch (providerId) {
case 'atlascloud':
return 'Set ATLASCLOUD_API_KEY or run "btca connect -p atlascloud".';
Comment on lines +146 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the provider-only authentication command effective.

The hint advertises btca connect -p atlascloud. In apps/cli/src/commands/connect.ts, runConnectCommand only uses args.provider in the args.provider && args.model branch at Lines 212-213. Without a model, the command ignores the provider and opens the generic provider selector. Either honor provider-only arguments or change this hint to the supported invocation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/providers/auth.ts` around lines 146 - 147, Update the
atlascloud authentication hint in the provider error-message switch to use a
supported connect invocation, or modify runConnectCommand so args.provider is
honored without requiring args.model; ensure the advertised btca connect -p
atlascloud command directly selects Atlas Cloud instead of opening the generic
provider selector.

case 'github-copilot':
return 'Run "btca connect -p github-copilot" and complete device flow OAuth.';
case 'openai':
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +32,9 @@ export type ProviderFactory = (options?: any) => {

// Registry of all supported providers
export const PROVIDER_REGISTRY: Record<string, ProviderFactory> = {
// Atlas Cloud (OpenAI-compatible gateway)
atlascloud: createAtlasCloud as ProviderFactory,

// OpenCode Zen (curated models gateway)
opencode: createOpenCodeZen as ProviderFactory,

Expand Down