diff --git a/packages/providers/src/deepseekTextProvider.ts b/packages/providers/src/deepseekTextProvider.ts new file mode 100644 index 0000000..e6cc8f8 --- /dev/null +++ b/packages/providers/src/deepseekTextProvider.ts @@ -0,0 +1,55 @@ +import type { ModelProvider, ProviderKind, GenParams, GenResult } from "@executagent/core"; + +export const TIER_DEEPSEEK_MODEL: Record = { + S: "deepseek-chat", + M: "deepseek-chat", + L: "deepseek-chat", +}; + +const API_URL = "https://api.deepseek.com/v1/chat/completions"; + +export class DeepSeekTextProvider implements ModelProvider { + readonly name = "deepseek"; + constructor(private readonly apiKey: string) {} + + supports(kind: ProviderKind): boolean { + return kind === "text"; + } + + async generate(prompt: string, params: GenParams): Promise { + const model = TIER_DEEPSEEK_MODEL[params.tier]; + const start = performance.now(); + const res = await fetch(API_URL, { + method: "POST", + headers: { + "content-type": "application/json", + "Authorization": `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + model, + max_tokens: params.maxTokens ?? 1024, + messages: [{ role: "user", content: prompt }], + }), + }); + if (!res.ok) { + throw new Error(`DeepSeek API ${res.status}: ${await res.text()}`); + } + const data = await res.json() as { + choices: Array<{ message: { content: string } }>; + usage?: { prompt_tokens?: number; completion_tokens?: number }; + }; + const text = data.choices[0]?.message?.content ?? ""; + return { + kind: "text", + text, + contentType: "text/plain", + model, + provider: this.name, + usage: { + inputTokens: data.usage?.prompt_tokens ?? 0, + outputTokens: data.usage?.completion_tokens ?? 0, + latencyMs: Math.round(performance.now() - start), + }, + }; + } +} diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index a11c187..a08ea1b 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -5,6 +5,7 @@ export * from "./types.ts"; export * from "./registry.ts"; export { MockProvider } from "./mockProvider.ts"; export { ClaudeTextProvider, TIER_TEXT_MODEL } from "./claudeTextProvider.ts"; +export { DeepSeekTextProvider, TIER_DEEPSEEK_MODEL } from "./deepseekTextProvider.ts"; export { ImageProvider, TIER_IMAGE_PARAMS } from "./imageProvider.ts"; export { SelfHostedProvider } from "./selfHostedStub.ts"; export { HashingEmbeddingProvider, lexicalEmbed } from "./embed.ts"; diff --git a/packages/providers/src/registry.test.ts b/packages/providers/src/registry.test.ts index 864e336..c259b46 100644 --- a/packages/providers/src/registry.test.ts +++ b/packages/providers/src/registry.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { resolveProvider, configFromEnv, TIER_TEXT_MODEL } from "./registry.ts"; import { MockProvider } from "./mockProvider.ts"; import { ClaudeTextProvider } from "./claudeTextProvider.ts"; +import { DeepSeekTextProvider } from "./deepseekTextProvider.ts"; describe("registry", () => { it("maps text tiers to distinct Claude models S { @@ -29,4 +30,13 @@ describe("registry", () => { expect(cfg.anthropicApiKey).toBe("k"); expect(cfg.forceMock).toBe(true); }); + + it("uses DeepSeek when only deepseekApiKey is configured", () => { + expect(resolveProvider("text", { deepseekApiKey: "sk-test" })).toBeInstanceOf(DeepSeekTextProvider); + }); + + it("reads DEEPSEEK_API_KEY from env bag", () => { + const cfg = configFromEnv({ DEEPSEEK_API_KEY: "sk-ds-test" }); + expect(cfg.deepseekApiKey).toBe("sk-ds-test"); + }); }); diff --git a/packages/providers/src/registry.ts b/packages/providers/src/registry.ts index 026365d..142e5b5 100644 --- a/packages/providers/src/registry.ts +++ b/packages/providers/src/registry.ts @@ -4,6 +4,7 @@ import type { ModelProvider, ProviderKind } from "@executagent/core"; import { MockProvider } from "./mockProvider.ts"; import { ClaudeTextProvider, TIER_TEXT_MODEL } from "./claudeTextProvider.ts"; +import { DeepSeekTextProvider } from "./deepseekTextProvider.ts"; import { ImageProvider } from "./imageProvider.ts"; import { HashingEmbeddingProvider } from "./embed.ts"; @@ -12,6 +13,7 @@ export { TIER_IMAGE_PARAMS } from "./imageProvider.ts"; export interface ProviderConfig { anthropicApiKey?: string; + deepseekApiKey?: string; imageApiKey?: string; imageApiUrl?: string; imageModel?: string; @@ -24,7 +26,9 @@ export function resolveProvider(kind: ProviderKind, cfg: ProviderConfig): ModelP switch (kind) { case "text": - return cfg.anthropicApiKey ? new ClaudeTextProvider(cfg.anthropicApiKey) : new MockProvider(); + if (cfg.anthropicApiKey) return new ClaudeTextProvider(cfg.anthropicApiKey); + if (cfg.deepseekApiKey) return new DeepSeekTextProvider(cfg.deepseekApiKey); + return new MockProvider(); case "image": return cfg.imageApiKey && cfg.imageApiUrl ? new ImageProvider({ apiKey: cfg.imageApiKey, apiUrl: cfg.imageApiUrl, ...(cfg.imageModel ? { model: cfg.imageModel } : {}) }) @@ -41,6 +45,7 @@ export function resolveProvider(kind: ProviderKind, cfg: ProviderConfig): ModelP export function configFromEnv(env: Record): ProviderConfig { const cfg: ProviderConfig = {}; if (env.ANTHROPIC_API_KEY) cfg.anthropicApiKey = env.ANTHROPIC_API_KEY; + if (env.DEEPSEEK_API_KEY) cfg.deepseekApiKey = env.DEEPSEEK_API_KEY; if (env.IMAGE_API_KEY) cfg.imageApiKey = env.IMAGE_API_KEY; if (env.IMAGE_API_URL) cfg.imageApiUrl = env.IMAGE_API_URL; if (env.IMAGE_MODEL) cfg.imageModel = env.IMAGE_MODEL;