From 7b6581f1c3cb6f7d8b74d269f3f89365dcb85d2e Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:54:30 +0800 Subject: [PATCH 1/2] Add Atlas Cloud as built-in provider --- README.md | 4 +-- README.zh-CN.md | 4 +-- apps/desktop/src/main/onboarding-ipc.test.ts | 19 ++++++++++++++ .../src/main/onboarding/provider-parsers.ts | 2 +- packages/core/src/agent.ts | 1 + packages/core/src/errors.test.ts | 7 +++++ packages/core/src/errors.ts | 1 + packages/providers/src/index.test.ts | 8 +++++- packages/providers/src/index.ts | 1 + packages/providers/src/validate.test.ts | 14 ++++++++++ packages/providers/src/validate.ts | 9 ++++++- packages/shared/src/base-url.test.ts | 6 +++++ packages/shared/src/config.test.ts | 26 ++++++++++++++++++- packages/shared/src/config.ts | 25 ++++++++++++++++++ packages/shared/src/index.ts | 1 + 15 files changed, 120 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b32bdca0..2cef8784 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 14de979a..f8e547e8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 中。除非你选择的模型入口本身需要联网,请求内容不会额外离开你的机器。 @@ -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 都会展示真实模型列表,而不是一小撮写死的选项 diff --git a/apps/desktop/src/main/onboarding-ipc.test.ts b/apps/desktop/src/main/onboarding-ipc.test.ts index 686e67c8..fff71c5c 100644 --- a/apps/desktop/src/main/onboarding-ipc.test.ts +++ b/apps/desktop/src/main/onboarding-ipc.test.ts @@ -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(); diff --git a/apps/desktop/src/main/onboarding/provider-parsers.ts b/apps/desktop/src/main/onboarding/provider-parsers.ts index 65b2dfa2..cacdc1e8 100644 --- a/apps/desktop/src/main/onboarding/provider-parsers.ts +++ b/apps/desktop/src/main/onboarding/provider-parsers.ts @@ -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, ); } diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index d4e2121a..2b630113 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -268,6 +268,7 @@ function supportsImageInput(wire: WireApi | undefined, modelId: string): boolean const BUILTIN_PUBLIC_BASE_URLS: Record = { anthropic: 'https://api.anthropic.com', openai: 'https://api.openai.com/v1', + atlascloud: 'https://api.atlascloud.ai/v1', openrouter: 'https://openrouter.ai/api/v1', }; diff --git a/packages/core/src/errors.test.ts b/packages/core/src/errors.test.ts index 03208a71..1fdb4d60 100644 --- a/packages/core/src/errors.test.ts +++ b/packages/core/src/errors.test.ts @@ -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'); diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 5a99b5df..3b32f708 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -21,6 +21,7 @@ import { CodesignError, ERROR_CODES } from '@open-codesign/shared'; export const PROVIDER_KEY_HELP_URL: Partial> = { 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', diff --git a/packages/providers/src/index.test.ts b/packages/providers/src/index.test.ts index c2ec14ec..8c4eb05a 100644 --- a/packages/providers/src/index.test.ts +++ b/packages/providers/src/index.test.ts @@ -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' }; @@ -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({ diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 34cfdf70..afc62f56 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -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'; diff --git a/packages/providers/src/validate.test.ts b/packages/providers/src/validate.test.ts index d693fc2c..22f1e8be 100644 --- a/packages/providers/src/validate.test.ts +++ b/packages/providers/src/validate.test.ts @@ -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; + 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'); diff --git a/packages/providers/src/validate.ts b/packages/providers/src/validate.ts index 0336b283..117ba0c4 100644 --- a/packages/providers/src/validate.ts +++ b/packages/providers/src/validate.ts @@ -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 { @@ -97,7 +104,7 @@ export async function pingProvider( ): Promise { 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, ); } diff --git a/packages/shared/src/base-url.test.ts b/packages/shared/src/base-url.test.ts index c38456bb..736f66a1 100644 --- a/packages/shared/src/base-url.test.ts +++ b/packages/shared/src/base-url.test.ts @@ -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'), diff --git a/packages/shared/src/config.test.ts b/packages/shared/src/config.test.ts index 129396be..f8ddee5c 100644 --- a/packages/shared/src/config.test.ts +++ b/packages/shared/src/config.test.ts @@ -6,6 +6,7 @@ import { detectWireFromBaseUrl, hydrateConfig, migrateLegacyToV3, + PROVIDER_SHORTLIST, parseConfigFlexible, resolveProviderCapabilities, SUPPORTED_ONBOARDING_PROVIDERS, @@ -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, @@ -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, diff --git a/packages/shared/src/config.ts b/packages/shared/src/config.ts index 12aac774..4c73bc2d 100644 --- a/packages/shared/src/config.ts +++ b/packages/shared/src/config.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; const ProviderIdEnum = z.enum([ 'anthropic', 'openai', + 'atlascloud', 'google', 'openrouter', 'groq', @@ -19,6 +20,7 @@ const ProviderIdEnum = z.enum([ export const SUPPORTED_ONBOARDING_PROVIDERS = [ 'anthropic', 'openai', + 'atlascloud', 'openrouter', 'ollama', ] as const; @@ -263,6 +265,22 @@ export const BUILTIN_PROVIDERS: Readonly Date: Thu, 16 Jul 2026 13:58:08 +0800 Subject: [PATCH 2/2] Add changeset for Atlas Cloud provider --- .changeset/atlascloud-provider.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/atlascloud-provider.md diff --git a/.changeset/atlascloud-provider.md b/.changeset/atlascloud-provider.md new file mode 100644 index 00000000..4eb08a2d --- /dev/null +++ b/.changeset/atlascloud-provider.md @@ -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.