diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b98e0c032..ced6f1c2d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added DeepSeek to the built-in API-key login provider catalog so `omp login deepseek` stores a reusable `DEEPSEEK_API_KEY` credential for the bundled DeepSeek models. + ## [15.2.4] - 2026-05-22 ### Fixed diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index 654e54f23..2d1a62168 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -29,6 +29,7 @@ import { kimiUsageProvider } from "./usage/kimi"; import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex"; import { zaiUsageProvider } from "./usage/zai"; import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./utils/oauth"; +import { loginDeepSeek } from "./utils/oauth/deepseek"; import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex"; import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types"; @@ -1378,6 +1379,11 @@ export class AuthStorage { await saveApiKeyCredential(apiKey); return; } + case "deepseek": { + const apiKey = await loginDeepSeek(ctrl); + await saveApiKeyCredential(apiKey); + return; + } case "fireworks": { const { loginFireworks } = await import("./utils/oauth/fireworks"); const apiKey = await loginFireworks(ctrl); diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index c5f4789f2..66c5c69db 100755 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -109,6 +109,7 @@ Providers: kagi Kagi tavily Tavily zai Z.AI (GLM Coding Plan) + deepseek DeepSeek nanogpt NanoGPT minimax-code MiniMax Coding Plan (International) minimax-code-cn MiniMax Coding Plan (China) diff --git a/packages/ai/src/utils/oauth/deepseek.ts b/packages/ai/src/utils/oauth/deepseek.ts new file mode 100644 index 000000000..b98074283 --- /dev/null +++ b/packages/ai/src/utils/oauth/deepseek.ts @@ -0,0 +1,16 @@ +/** DeepSeek login flow (API key paste against https://api.deepseek.com). */ +import { createApiKeyLogin } from "./api-key-login"; + +export const loginDeepSeek = createApiKeyLogin({ + providerLabel: "DeepSeek", + authUrl: "https://platform.deepseek.com/api_keys", + instructions: "Create or copy your API key from the DeepSeek dashboard", + promptMessage: "Paste your DeepSeek API key", + placeholder: "sk-...", + validation: { + kind: "chat-completions", + provider: "deepseek", + baseUrl: "https://api.deepseek.com/v1", + model: "deepseek-v4-pro", + }, +}); diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 6213514b1..f114cfa63 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -55,6 +55,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [ name: "Cerebras", available: true, }, + { + id: "deepseek", + name: "DeepSeek", + available: true, + }, { id: "fireworks", name: "Fireworks", diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 3789b4188..2d8d0fe4d 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -14,6 +14,7 @@ export type OAuthProvider = | "cerebras" | "cloudflare-ai-gateway" | "cursor" + | "deepseek" | "fireworks" | "firepass" | "github-copilot" diff --git a/packages/ai/test/auth-storage-api-key-login.test.ts b/packages/ai/test/auth-storage-api-key-login.test.ts index 62e3a5a7d..16be149b6 100644 --- a/packages/ai/test/auth-storage-api-key-login.test.ts +++ b/packages/ai/test/auth-storage-api-key-login.test.ts @@ -5,6 +5,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { AuthStorage, SqliteAuthCredentialStore } from "../src/auth-storage"; +import * as deepseekModule from "../src/utils/oauth/deepseek"; import * as kagiModule from "../src/utils/oauth/kagi"; import * as ollamaCloudModule from "../src/utils/oauth/ollama-cloud"; @@ -25,6 +26,7 @@ describe("AuthStorage api-key login replacement", () => { let dbPath = ""; let store: SqliteAuthCredentialStore | null = null; let authStorage: AuthStorage | null = null; + let loginDeepSeekSpy: Mock; let loginKagiSpy: Mock; let loginOllamaCloudSpy: Mock; @@ -33,6 +35,7 @@ describe("AuthStorage api-key login replacement", () => { dbPath = path.join(tempDir, "agent.db"); store = await SqliteAuthCredentialStore.open(dbPath); authStorage = new AuthStorage(store); + loginDeepSeekSpy = vi.spyOn(deepseekModule, "loginDeepSeek"); loginKagiSpy = vi.spyOn(kagiModule, "loginKagi"); loginOllamaCloudSpy = vi.spyOn(ollamaCloudModule, "loginOllamaCloud"); }); @@ -100,4 +103,30 @@ describe("AuthStorage api-key login replacement", () => { expect(store.getApiKey("ollama-cloud")).toBe("same-ollama-cloud-key"); expect(await authStorage.getApiKey("ollama-cloud", "session-ollama-cloud-relogin")).toBe("same-ollama-cloud-key"); }); + + it("stores DeepSeek login credentials as a reusable api-key credential", async () => { + if (!store || !authStorage || !dbPath) throw new Error("test setup failed"); + + loginDeepSeekSpy.mockResolvedValueOnce("same-deepseek-key").mockResolvedValueOnce("same-deepseek-key"); + + const controller = { + onAuth: () => {}, + onPrompt: async () => "", + }; + + await authStorage.login("deepseek", controller); + await authStorage.login("deepseek", controller); + + expect(countCredentialRows(dbPath, "deepseek")).toBe(1); + const credentials = store.listAuthCredentials("deepseek"); + expect(credentials).toHaveLength(1); + const [stored] = credentials; + expect(stored?.credential.type).toBe("api_key"); + if (!stored || stored.credential.type !== "api_key") { + throw new Error("expected stored api-key credential"); + } + expect(stored.credential.key).toBe("same-deepseek-key"); + expect(store.getApiKey("deepseek")).toBe("same-deepseek-key"); + expect(await authStorage.getApiKey("deepseek", "session-deepseek-relogin")).toBe("same-deepseek-key"); + }); }); diff --git a/packages/ai/test/issue-830-repro.test.ts b/packages/ai/test/issue-830-repro.test.ts index 24d5deaab..b0f9e1d91 100644 --- a/packages/ai/test/issue-830-repro.test.ts +++ b/packages/ai/test/issue-830-repro.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "../src/provide import { MODELS_DEV_PROVIDER_DESCRIPTORS } from "../src/provider-models/openai-compat"; import { getEnvApiKey } from "../src/stream"; import type { OpenAICompat } from "../src/types"; +import { getOAuthProviders } from "../src/utils/oauth"; describe("deepseek built-in provider (issue #830)", () => { test("registers built-in runtime descriptor with DEEPSEEK_API_KEY env discovery", () => { @@ -13,6 +14,12 @@ describe("deepseek built-in provider (issue #830)", () => { expect(DEFAULT_MODEL_PER_PROVIDER.deepseek).toBe("deepseek-v4-pro"); }); + test("registers DeepSeek as an API-key login provider", () => { + const provider = getOAuthProviders().find(item => item.id === "deepseek"); + expect(provider?.name).toBe("DeepSeek"); + expect(provider?.available).toBe(true); + }); + test("resolves DEEPSEEK_API_KEY via env", () => { const previous = Bun.env.DEEPSEEK_API_KEY; Bun.env.DEEPSEEK_API_KEY = "deepseek-test-key";