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
8 changes: 8 additions & 0 deletions .changeset/atlascloud-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@open-codesign/shared": patch
"@open-codesign/providers": patch
"@open-codesign/core": patch
"@open-codesign/desktop": patch
---

Add Atlas Cloud as a built-in OpenAI-compatible provider for onboarding, validation, model discovery, and provider selection.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ After each stable tag push, CI syncs SHAs back into `packaging/` and publishes d
On first launch, Open CoDesign opens the Settings page. Pick the path that matches how you already use models:

- **ChatGPT subscription** — sign in with ChatGPT to use Codex models without pasting an API key.
- **API key** — paste Anthropic (`sk-ant-...`), OpenAI (`sk-...`), Google Gemini, OpenRouter, SiliconFlow, DeepSeek, or another supported provider key.
- **API key** — paste Anthropic (`sk-ant-...`), OpenAI (`sk-...`), Atlas Cloud, Google Gemini, OpenRouter, SiliconFlow, DeepSeek, or another supported provider key.
- **Local / keyless** — use Ollama or an IP-allowlisted OpenAI-compatible gateway.

Credentials stay in `~/.config/open-codesign/config.toml` and the ChatGPT OAuth token store under the app config directory. Nothing leaves your machine unless your chosen model route requires it.
Expand Down Expand Up @@ -222,7 +222,7 @@ Add a `SKILL.md` to any project to teach the model your own taste.
## What you get

### Models and providers
- **Unified provider model** — Anthropic, OpenAI, Gemini, DeepSeek, OpenRouter, SiliconFlow, local Ollama, or any OpenAI-compatible relay; keyless (IP-allowlisted) proxies supported
- **Unified provider model** — Anthropic, OpenAI, Atlas Cloud, Gemini, DeepSeek, OpenRouter, SiliconFlow, local Ollama, or any OpenAI-compatible relay; keyless (IP-allowlisted) proxies supported
- **One-click import and sign-in** — bring Claude Code / Codex API-key provider configs across, or sign in with ChatGPT subscription for Codex models
- **Dynamic model picker** — every provider exposes its real model catalogue, not a hardcoded shortlist

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ scoop install opencoworkai/open-codesign
首次启动时,Open CoDesign 会直接打开设置页。按你已有的模型入口选择即可:

- **ChatGPT 订阅登录**:直接登录 ChatGPT,使用 Codex 模型,无需粘贴 API Key。
- **API Key**:粘贴 Anthropic(`sk-ant-...`)、OpenAI(`sk-...`)、Google Gemini、OpenRouter、SiliconFlow、DeepSeek 或其他支持的 provider key。
- **API Key**:粘贴 Anthropic(`sk-ant-...`)、OpenAI(`sk-...`)、Atlas Cloud、Google Gemini、OpenRouter、SiliconFlow、DeepSeek 或其他支持的 provider key。
- **本地 / Keyless**:使用 Ollama,或使用 IP 白名单的 OpenAI 兼容网关。

凭证会保存在 `~/.config/open-codesign/config.toml`,ChatGPT OAuth token 会保存在应用配置目录下的 token store 中。除非你选择的模型入口本身需要联网,请求内容不会额外离开你的机器。
Expand Down Expand Up @@ -220,7 +220,7 @@ scoop install opencoworkai/open-codesign
## 你能得到什么

### 模型与提供商
- **统一的 provider 抽象**:支持 Anthropic、OpenAI、Gemini、DeepSeek、OpenRouter、SiliconFlow、本地 Ollama,以及任意 OpenAI-compatible relay;同时支持无 key 的 IP 白名单代理
- **统一的 provider 抽象**:支持 Anthropic、OpenAI、Atlas Cloud、Gemini、DeepSeek、OpenRouter、SiliconFlow、本地 Ollama,以及任意 OpenAI-compatible relay;同时支持无 key 的 IP 白名单代理
- **一键导入和登录**:Claude Code / Codex 的 API key provider 配置可以直接带进来,也可以用 ChatGPT 订阅登录使用 Codex 模型
- **动态模型选择器**:每个 provider 都会展示真实模型列表,而不是一小撮写死的选项

Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/main/onboarding-ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,25 @@ describe('registerOnboardingIpc — validate-key passes baseUrl to pingProvider'
expect(pingProvider).toHaveBeenCalledWith('anthropic', 'sk-ant-test', undefined);
});

it('forwards Atlas Cloud validation to the provider validator', async () => {
const { pingProvider } = await import('@open-codesign/providers');
vi.mocked(pingProvider).mockClear();
const handler = handlers.get('onboarding:validate-key');
expect(handler).toBeDefined();

await handler?.({} as unknown, {
provider: 'atlascloud',
apiKey: 'apikey-test',
baseUrl: 'https://api.atlascloud.ai/v1',
});

expect(pingProvider).toHaveBeenCalledWith(
'atlascloud',
'apikey-test',
'https://api.atlascloud.ai/v1',
);
});

it('allows explicitly keyless Ollama validation with an empty apiKey', async () => {
const { pingProvider } = await import('@open-codesign/providers');
vi.mocked(pingProvider).mockClear();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/onboarding/provider-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export function parseValidateKey(raw: unknown): ValidateKeyInput {
const providerId = provider.trim();
if (!isSupportedOnboardingProvider(providerId)) {
throw new CodesignError(
`Provider "${providerId}" is not supported. Only anthropic, openai, openrouter, ollama.`,
`Provider "${providerId}" is not supported. Only anthropic, openai, atlascloud, openrouter, ollama.`,
ERROR_CODES.PROVIDER_NOT_SUPPORTED,
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ function supportsImageInput(wire: WireApi | undefined, modelId: string): boolean
const BUILTIN_PUBLIC_BASE_URLS: Record<string, string> = {
anthropic: 'https://api.anthropic.com',
openai: 'https://api.openai.com/v1',
atlascloud: 'https://api.atlascloud.ai/v1',
openrouter: 'https://openrouter.ai/api/v1',
};

Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ describe('rewriteUpstreamMessage', () => {
expect(result.message).toContain('openrouter.ai/settings/keys');
});

it('rewrites leaked openai URL to Atlas Cloud URL', () => {
const result = rewriteUpstreamMessage(LEAKED, 'atlascloud', 401);
expect(result.rewritten).toBe(true);
expect(result.message).not.toContain('openai.com');
expect(result.message).toContain('atlascloud.ai');
});

it('rewrites to deepseek URL even though it is not in the typed enum', () => {
const result = rewriteUpstreamMessage(LEAKED, 'deepseek', 401);
expect(result.message).toContain('platform.deepseek.com/api_keys');
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CodesignError, ERROR_CODES } from '@open-codesign/shared';

export const PROVIDER_KEY_HELP_URL: Partial<Record<ProviderId, string>> = {
openai: 'https://platform.openai.com/account/api-keys',
atlascloud: 'https://atlascloud.ai/',
anthropic: 'https://console.anthropic.com/settings/keys',
openrouter: 'https://openrouter.ai/settings/keys',
google: 'https://aistudio.google.com/app/apikey',
Expand Down
8 changes: 7 additions & 1 deletion packages/providers/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ vi.mock('@mariozechner/pi-ai', () => ({
completeSimple: (...args: unknown[]) => completeSimpleMock(...args),
}));

import { complete, inferReasoning } from './index';
import { complete, detectProviderFromKey, inferReasoning } from './index';

const MODEL: ModelRef = { provider: 'openai', modelId: 'gpt-4o' };

Expand All @@ -23,6 +23,12 @@ afterEach(() => {
completeSimpleMock.mockReset();
});

describe('detectProviderFromKey', () => {
it('recognizes Atlas Cloud API keys', () => {
expect(detectProviderFromKey(' apikey-test ')).toBe('atlascloud');
});
});

describe('complete', () => {
it('adapts shared chat history into pi-ai context for follow-up turns', async () => {
getModelMock.mockReturnValue({
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ export function detectProviderFromKey(key: string): ModelRef['provider'] | null
const trimmed = key.trim();
if (trimmed.startsWith('sk-ant-')) return 'anthropic';
if (trimmed.startsWith('sk-or-')) return 'openrouter';
if (trimmed.startsWith('apikey-')) return 'atlascloud';
if (trimmed.startsWith('sk-')) return 'openai';
if (trimmed.startsWith('AIza')) return 'google';
if (trimmed.startsWith('xai-')) return 'xai';
Expand Down
14 changes: 14 additions & 0 deletions packages/providers/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ describe('pingProvider', () => {
expect(result).toEqual({ ok: true, modelCount: 0 });
});

it('uses the Atlas Cloud OpenAI-compatible models endpoint and Bearer auth', async () => {
mockFetch(async (url, init) => {
expect(url).toBe('https://api.atlascloud.ai/v1/models');
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers['authorization']).toBe('Bearer apikey-test');
return new Response(JSON.stringify({ data: [{ id: 'qwen/qwen3.5-flash' }] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
});
const result = await pingProvider('atlascloud', 'apikey-test');
expect(result).toEqual({ ok: true, modelCount: 1 });
});

it('respects custom baseUrl without /v1 suffix', async () => {
mockFetch(async (url) => {
expect(url).toBe('https://proxy.example/v1/models');
Expand Down
9 changes: 8 additions & 1 deletion packages/providers/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ function endpoint(provider: SupportedOnboardingProvider, baseUrl?: string): Prov
headers: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
};
}
case 'atlascloud': {
const root = baseUrl ? normalizeValidateBaseUrl(baseUrl) : 'https://api.atlascloud.ai';
return {
url: `${root}/v1/models`,
headers: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
};
}
case 'openrouter': {
const root = baseUrl ? normalizeValidateBaseUrl(baseUrl) : 'https://openrouter.ai/api';
return {
Expand Down Expand Up @@ -97,7 +104,7 @@ export async function pingProvider(
): Promise<ValidateResult> {
if (!isSupportedOnboardingProvider(provider)) {
throw new CodesignError(
`Provider "${provider}" is not supported by the first-run provider shortcut. Supported: anthropic, openai, openrouter, ollama. Add custom providers in Settings, or use ChatGPT subscription sign-in for chatgpt-codex.`,
`Provider "${provider}" is not supported by the first-run provider shortcut. Supported: anthropic, openai, atlascloud, openrouter, ollama. Add custom providers in Settings, or use ChatGPT subscription sign-in for chatgpt-codex.`,
ERROR_CODES.PROVIDER_NOT_SUPPORTED,
);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/base-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ describe('modelsEndpointUrl', () => {
);
});

it('openai-chat: Atlas Cloud versioned base + /models', () => {
expect(modelsEndpointUrl('https://api.atlascloud.ai/v1', 'openai-chat')).toBe(
'https://api.atlascloud.ai/v1/models',
);
});

it('openai-chat: GLM /api/paas/v4/models', () => {
expect(
modelsEndpointUrl('https://open.bigmodel.cn/api/paas/v4/chat/completions', 'openai-chat'),
Expand Down
26 changes: 25 additions & 1 deletion packages/shared/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
detectWireFromBaseUrl,
hydrateConfig,
migrateLegacyToV3,
PROVIDER_SHORTLIST,
parseConfigFlexible,
resolveProviderCapabilities,
SUPPORTED_ONBOARDING_PROVIDERS,
Expand Down Expand Up @@ -83,6 +84,29 @@ describe('config v3 schema', () => {
expect(caps.supportsReasoning).toBe(false);
});

it('includes Atlas Cloud as an OpenAI-compatible builtin provider', () => {
expect(SUPPORTED_ONBOARDING_PROVIDERS).toContain('atlascloud');
expect(BUILTIN_PROVIDERS.atlascloud).toMatchObject({
id: 'atlascloud',
name: 'Atlas Cloud',
wire: 'openai-chat',
baseUrl: 'https://api.atlascloud.ai/v1',
envKey: 'ATLASCLOUD_API_KEY',
defaultModel: 'qwen/qwen3.5-flash',
capabilities: {
supportsKeyless: false,
supportsModelsEndpoint: true,
modelDiscoveryMode: 'models',
},
});
expect(PROVIDER_SHORTLIST.atlascloud).toMatchObject({
provider: 'atlascloud',
label: 'Atlas Cloud',
defaultPrimary: 'qwen/qwen3.5-flash',
});
expect(PROVIDER_SHORTLIST.atlascloud.primary).toContain('deepseek-ai/deepseek-v4-pro');
});

it('rejects unknown wire values', () => {
const bad = {
version: 3,
Expand Down Expand Up @@ -302,7 +326,7 @@ describe('config v3 schema', () => {
});

describe('migrateLegacyToV3', () => {
it('seeds three builtin providers from an empty v2', () => {
it('seeds builtin providers from an empty v2', () => {
const legacy = {
version: 2 as const,
provider: 'anthropic' as const,
Expand Down
25 changes: 25 additions & 0 deletions packages/shared/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { z } from 'zod';
const ProviderIdEnum = z.enum([
'anthropic',
'openai',
'atlascloud',
'google',
'openrouter',
'groq',
Expand All @@ -19,6 +20,7 @@ const ProviderIdEnum = z.enum([
export const SUPPORTED_ONBOARDING_PROVIDERS = [
'anthropic',
'openai',
'atlascloud',
'openrouter',
'ollama',
] as const;
Expand Down Expand Up @@ -263,6 +265,22 @@ export const BUILTIN_PROVIDERS: Readonly<Record<SupportedOnboardingProvider, Pro
modelDiscoveryMode: 'models',
},
},
atlascloud: {
id: 'atlascloud',
name: 'Atlas Cloud',
builtin: true,
wire: 'openai-chat',
baseUrl: 'https://api.atlascloud.ai/v1',
envKey: 'ATLASCLOUD_API_KEY',
defaultModel: 'qwen/qwen3.5-flash',
capabilities: {
supportsKeyless: false,
supportsModelsEndpoint: true,
supportsReasoning: false,
requiresClaudeCodeIdentity: false,
modelDiscoveryMode: 'models',
},
},
openrouter: {
id: 'openrouter',
name: 'OpenRouter',
Expand Down Expand Up @@ -500,6 +518,13 @@ export const PROVIDER_SHORTLIST: Record<SupportedOnboardingProvider, ProviderSho
primary: ['gpt-4o', 'gpt-4.1'],
defaultPrimary: 'gpt-4o',
},
atlascloud: {
provider: 'atlascloud',
label: 'Atlas Cloud',
keyHelpUrl: 'https://atlascloud.ai/',
primary: ['qwen/qwen3.5-flash', 'deepseek-ai/deepseek-v4-pro'],
defaultPrimary: 'qwen/qwen3.5-flash',
},
openrouter: {
provider: 'openrouter',
label: 'OpenRouter',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';
export const ProviderId = z.enum([
'anthropic',
'openai',
'atlascloud',
'google',
'openrouter',
'groq',
Expand Down
Loading