From 59a999654f321e882e0c1db7185cdef1de4f34c9 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 8 Dec 2025 15:36:06 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20DeepSeek=20pro?= =?UTF-8?q?vider=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DeepSeek to PROVIDER_REGISTRY with importDeepSeek function - Add DeepSeek display name and icon support - Implement DeepSeek handler in aiService.ts - Add @ai-sdk/deepseek package dependency - Add DeepSeek SVG logo icon Closes #973 --- bun.lock | 4 +++- package.json | 1 + src/browser/assets/icons/deepseek.svg | 1 + src/browser/components/ProviderIcon.tsx | 2 ++ src/common/constants/providers.ts | 9 +++++++++ src/node/services/aiService.ts | 19 +++++++++++++++++++ 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/browser/assets/icons/deepseek.svg diff --git a/bun.lock b/bun.lock index c319e107f8..2422a9d109 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,12 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "mux", "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.61", "@ai-sdk/anthropic": "^2.0.47", + "@ai-sdk/deepseek": "^1.0.31", "@ai-sdk/google": "^2.0.43", "@ai-sdk/mcp": "^0.0.11", "@ai-sdk/openai": "^2.0.72", @@ -172,6 +172,8 @@ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ih7NV+OFSNWZCF+tYYD7ovvvM+gv7TRKQblpVohg2ipIwC9Y0TirzocJVREzZa/v9luxUwFbsPji++DUDWWxsg=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.31", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Il7WJp8bA3CmlreYSl1YzCucGTn2e5P81IANYIIEeLtWrbK0Y9CLoOCROj8xKYyUSMKlINyGZX2uP79cKewtSg=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ=="], "@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="], diff --git a/package.json b/package.json index 1a1f6fcb0e..c6af4e1015 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.61", "@ai-sdk/anthropic": "^2.0.47", + "@ai-sdk/deepseek": "^1.0.31", "@ai-sdk/google": "^2.0.43", "@ai-sdk/mcp": "^0.0.11", "@ai-sdk/openai": "^2.0.72", diff --git a/src/browser/assets/icons/deepseek.svg b/src/browser/assets/icons/deepseek.svg new file mode 100644 index 0000000000..48b845e459 --- /dev/null +++ b/src/browser/assets/icons/deepseek.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/browser/components/ProviderIcon.tsx b/src/browser/components/ProviderIcon.tsx index 1ed6999e81..b29fbc625c 100644 --- a/src/browser/components/ProviderIcon.tsx +++ b/src/browser/components/ProviderIcon.tsx @@ -3,6 +3,7 @@ import AnthropicIcon from "@/browser/assets/icons/anthropic.svg?react"; import OpenAIIcon from "@/browser/assets/icons/openai.svg?react"; import GoogleIcon from "@/browser/assets/icons/google.svg?react"; import XAIIcon from "@/browser/assets/icons/xai.svg?react"; +import DeepSeekIcon from "@/browser/assets/icons/deepseek.svg?react"; import AWSIcon from "@/browser/assets/icons/aws.svg?react"; import { GatewayIcon } from "@/browser/components/icons/GatewayIcon"; import { PROVIDER_DISPLAY_NAMES, type ProviderName } from "@/common/constants/providers"; @@ -13,6 +14,7 @@ const PROVIDER_ICONS: Partial> = { openai: OpenAIIcon, google: GoogleIcon, xai: XAIIcon, + deepseek: DeepSeekIcon, bedrock: AWSIcon, "mux-gateway": GatewayIcon, }; diff --git a/src/common/constants/providers.ts b/src/common/constants/providers.ts index b3bd7ec558..a03d67647b 100644 --- a/src/common/constants/providers.ts +++ b/src/common/constants/providers.ts @@ -48,6 +48,13 @@ export function importXAI() { return import("@ai-sdk/xai"); } +/** + * Dynamically import the DeepSeek provider package + */ +export function importDeepSeek() { + return import("@ai-sdk/deepseek"); +} + /** * Dynamically import the Amazon Bedrock provider package */ @@ -80,6 +87,7 @@ export const PROVIDER_REGISTRY = { openai: importOpenAI, google: importGoogle, xai: importXAI, + deepseek: importDeepSeek, ollama: importOllama, openrouter: importOpenRouter, bedrock: importBedrock, @@ -104,6 +112,7 @@ export const PROVIDER_DISPLAY_NAMES: Record = { openai: "OpenAI", google: "Google", xai: "xAI", + deepseek: "DeepSeek", ollama: "Ollama", openrouter: "OpenRouter", bedrock: "Amazon Bedrock", diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index b48789a0cf..4f745f76e0 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -613,6 +613,25 @@ export class AIService extends EventEmitter { return Ok(provider(modelId)); } + // Handle DeepSeek provider + if (providerName === "deepseek") { + if (!providerConfig.apiKey) { + return Err({ + type: "api_key_not_found", + provider: providerName, + }); + } + const baseFetch = getProviderFetch(providerConfig); + + // Lazy-load DeepSeek provider to reduce startup time + const { createDeepSeek } = await PROVIDER_REGISTRY.deepseek(); + const provider = createDeepSeek({ + ...providerConfig, + fetch: baseFetch, + }); + return Ok(provider(modelId)); + } + // Handle Ollama provider if (providerName === "ollama") { // Ollama doesn't require API key - it's a local service From 48e56182cca4c502e3516a26831d85ce62481d81 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 8 Dec 2025 15:47:05 -0600 Subject: [PATCH 2/4] docs: add DeepSeek provider documentation --- docs/models.mdx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/models.mdx b/docs/models.mdx index e5d9ded20f..b4bd35c137 100644 --- a/docs/models.mdx +++ b/docs/models.mdx @@ -1,6 +1,6 @@ --- title: Models -description: Configure AI providers including Anthropic, OpenAI, Google, xAI, and more +description: Configure AI providers including Anthropic, OpenAI, Google, xAI, DeepSeek, and more --- See also: @@ -121,6 +121,26 @@ Frontier reasoning models from xAI with built-in search orchestration: Mux enables Grok's live search by default using `mode: "auto"` with citations. Add [`searchParameters`](https://docs.x.ai/docs/resources/search) to `providers.jsonc` if you want to customize the defaults (e.g., regional focus, time filters, or disabling search entirely per workspace). +#### DeepSeek (Cloud) + +Access DeepSeek's reasoning and chat models: + +- `deepseek:deepseek-chat` — Fast, cost-effective chat model +- `deepseek:deepseek-reasoner` — Advanced reasoning model with extended thinking + +**Setup:** + +1. Get your API key from [platform.deepseek.com](https://platform.deepseek.com/) +2. Add to `~/.mux/providers.jsonc`: + +```jsonc +{ + "deepseek": { + "apiKey": "sk-...", + }, +} +``` + #### OpenRouter (Cloud) Access 300+ models from multiple providers through a single API: @@ -322,6 +342,10 @@ All providers are configured in `~/.mux/providers.jsonc`. Example configurations "xai": { "apiKey": "sk-xai-...", }, + // Required for DeepSeek models + "deepseek": { + "apiKey": "sk-...", + }, // Required for OpenRouter models "openrouter": { "apiKey": "sk-or-v1-...", From 2ec12fe7c3f983e6c6b2a7af21d615fc29ce5136 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 8 Dec 2025 15:56:00 -0600 Subject: [PATCH 3/4] fix: convert DeepSeek icon to monochrome for CSS styling Extract just the whale shape from the DeepSeek logo and remove hardcoded fill colors so it works with fill-current CSS styling, matching the style of other provider icons. --- src/browser/assets/icons/deepseek.svg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/assets/icons/deepseek.svg b/src/browser/assets/icons/deepseek.svg index 48b845e459..3eb6db4a5f 100644 --- a/src/browser/assets/icons/deepseek.svg +++ b/src/browser/assets/icons/deepseek.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + From 90b6496c4d8f2bab5002d2dbb80f11add635c7b5 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 8 Dec 2025 19:16:10 -0600 Subject: [PATCH 4/4] refactor: consolidate provider definitions for easier additions - Unified PROVIDER_DEFINITIONS in providers.ts with all metadata: displayName, import fn, factoryName, requiresApiKey, strokeBasedIcon - PROVIDER_REGISTRY and PROVIDER_DISPLAY_NAMES now derived automatically - Added generic handler in aiService.ts for simple providers - Reordered providers: Mux Gateway, Anthropic, OpenAI, Google, xAI, DeepSeek, OpenRouter, Bedrock, Ollama (order controls UI display) Adding a new provider now requires: - Simple providers: edit providers.ts + ProviderIcon.tsx (+ SVG + package.json) - Complex providers: also add handler in aiService.ts --- src/browser/components/ProviderIcon.tsx | 20 ++- src/common/constants/providers.ts | 193 +++++++++++------------- src/node/services/aiService.ts | 77 +++++----- 3 files changed, 144 insertions(+), 146 deletions(-) diff --git a/src/browser/components/ProviderIcon.tsx b/src/browser/components/ProviderIcon.tsx index b29fbc625c..c34f991f21 100644 --- a/src/browser/components/ProviderIcon.tsx +++ b/src/browser/components/ProviderIcon.tsx @@ -6,9 +6,17 @@ import XAIIcon from "@/browser/assets/icons/xai.svg?react"; import DeepSeekIcon from "@/browser/assets/icons/deepseek.svg?react"; import AWSIcon from "@/browser/assets/icons/aws.svg?react"; import { GatewayIcon } from "@/browser/components/icons/GatewayIcon"; -import { PROVIDER_DISPLAY_NAMES, type ProviderName } from "@/common/constants/providers"; +import { + PROVIDER_DEFINITIONS, + PROVIDER_DISPLAY_NAMES, + type ProviderName, +} from "@/common/constants/providers"; import { cn } from "@/common/lib/utils"; +/** + * Provider icons mapped by provider name. + * When adding a new provider, add its icon import above and entry here. + */ const PROVIDER_ICONS: Partial> = { anthropic: AnthropicIcon, openai: OpenAIIcon, @@ -19,9 +27,6 @@ const PROVIDER_ICONS: Partial> = { "mux-gateway": GatewayIcon, }; -// Icons that use stroke instead of fill for their styling -const STROKE_BASED_ICONS = new Set(["mux-gateway"]); - export interface ProviderIconProps { provider: string; className?: string; @@ -32,10 +37,13 @@ export interface ProviderIconProps { * Icons are sized to 1em by default to match surrounding text. */ export function ProviderIcon(props: ProviderIconProps) { - const IconComponent = PROVIDER_ICONS[props.provider as keyof typeof PROVIDER_ICONS]; + const providerName = props.provider as ProviderName; + const IconComponent = PROVIDER_ICONS[providerName]; if (!IconComponent) return null; - const isStrokeBased = STROKE_BASED_ICONS.has(props.provider); + // Check if this provider uses stroke-based icon styling (from PROVIDER_DEFINITIONS) + const def = PROVIDER_DEFINITIONS[providerName] as { strokeBasedIcon?: boolean } | undefined; + const isStrokeBased = def?.strokeBasedIcon ?? false; return ( Promise; + /** Name of the factory function exported by the package */ + factoryName: string; + /** Whether provider requires an API key (false for local services like Ollama) */ + requiresApiKey: boolean; + /** Whether this provider uses stroke-based icon styling instead of fill */ + strokeBasedIcon?: boolean; } -/** - * Centralized provider registry mapping provider names to their import functions - * - * This is the single source of truth for supported providers. By mapping to import - * functions rather than package strings, we eliminate duplication while maintaining - * perfect type safety. - * - * When adding a new provider: - * 1. Create an importXxx() function above - * 2. Add entry mapping provider name to the import function - * 3. Implement provider handling in aiService.ts createModel() - * 4. Runtime check will fail if provider in registry but no handler - */ -export const PROVIDER_REGISTRY = { - anthropic: importAnthropic, - openai: importOpenAI, - google: importGoogle, - xai: importXAI, - deepseek: importDeepSeek, - ollama: importOllama, - openrouter: importOpenRouter, - bedrock: importBedrock, - "mux-gateway": importMuxGateway, -} as const; +// Order determines display order in UI (Settings, model selectors, etc.) +export const PROVIDER_DEFINITIONS = { + "mux-gateway": { + displayName: "Mux Gateway", + import: () => import("ai"), + factoryName: "createGateway", + requiresApiKey: true, // Uses couponCode + strokeBasedIcon: true, + }, + anthropic: { + displayName: "Anthropic", + import: () => import("@ai-sdk/anthropic"), + factoryName: "createAnthropic", + requiresApiKey: true, + }, + openai: { + displayName: "OpenAI", + import: () => import("@ai-sdk/openai"), + factoryName: "createOpenAI", + requiresApiKey: true, + }, + google: { + displayName: "Google", + import: () => import("@ai-sdk/google"), + factoryName: "createGoogleGenerativeAI", + requiresApiKey: true, + }, + xai: { + displayName: "xAI", + import: () => import("@ai-sdk/xai"), + factoryName: "createXai", + requiresApiKey: true, + }, + deepseek: { + displayName: "DeepSeek", + import: () => import("@ai-sdk/deepseek"), + factoryName: "createDeepSeek", + requiresApiKey: true, + }, + openrouter: { + displayName: "OpenRouter", + import: () => import("@openrouter/ai-sdk-provider"), + factoryName: "createOpenRouter", + requiresApiKey: true, + }, + bedrock: { + displayName: "Amazon Bedrock", + import: () => import("@ai-sdk/amazon-bedrock"), + factoryName: "createAmazonBedrock", + requiresApiKey: false, // Uses AWS credential chain + }, + ollama: { + displayName: "Ollama", + import: () => import("ollama-ai-provider-v2"), + factoryName: "createOllama", + requiresApiKey: false, // Local service + }, +} as const satisfies Record; /** * Union type of all supported provider names */ -export type ProviderName = keyof typeof PROVIDER_REGISTRY; +export type ProviderName = keyof typeof PROVIDER_DEFINITIONS; /** * Array of all supported provider names (for UI lists, iteration, etc.) */ -export const SUPPORTED_PROVIDERS = Object.keys(PROVIDER_REGISTRY) as ProviderName[]; +export const SUPPORTED_PROVIDERS = Object.keys(PROVIDER_DEFINITIONS) as ProviderName[]; /** * Display names for providers (proper casing for UI) + * Derived from PROVIDER_DEFINITIONS - do not edit directly + */ +export const PROVIDER_DISPLAY_NAMES: Record = Object.fromEntries( + Object.entries(PROVIDER_DEFINITIONS).map(([key, def]) => [key, def.displayName]) +) as Record; + +/** + * Legacy registry for backward compatibility with aiService.ts + * Maps provider names to their import functions */ -export const PROVIDER_DISPLAY_NAMES: Record = { - anthropic: "Anthropic", - openai: "OpenAI", - google: "Google", - xai: "xAI", - deepseek: "DeepSeek", - ollama: "Ollama", - openrouter: "OpenRouter", - bedrock: "Amazon Bedrock", - "mux-gateway": "Mux Gateway", -}; +export const PROVIDER_REGISTRY = Object.fromEntries( + Object.entries(PROVIDER_DEFINITIONS).map(([key, def]) => [key, def.import]) +) as { [K in ProviderName]: (typeof PROVIDER_DEFINITIONS)[K]["import"] }; /** * Type guard to check if a string is a valid provider name diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 4f745f76e0..297d5e8847 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -9,7 +9,11 @@ import { sanitizeToolInputs } from "@/browser/utils/messages/sanitizeToolInput"; import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; import type { WorkspaceMetadata } from "@/common/types/workspace"; -import { PROVIDER_REGISTRY } from "@/common/constants/providers"; +import { + PROVIDER_REGISTRY, + PROVIDER_DEFINITIONS, + type ProviderName, +} from "@/common/constants/providers"; import type { MuxMessage, MuxTextPart } from "@/common/types/message"; import { createMuxMessage } from "@/common/types/message"; @@ -558,24 +562,6 @@ export class AIService extends EventEmitter { return Ok(model); } - // Handle Google provider - if (providerName === "google") { - if (!providerConfig.apiKey) { - return Err({ - type: "api_key_not_found", - provider: providerName, - }); - } - - // Lazy-load Google provider to reduce startup time - const { createGoogleGenerativeAI } = await PROVIDER_REGISTRY.google(); - const provider = createGoogleGenerativeAI({ - ...providerConfig, - fetch: getProviderFetch(providerConfig), - }); - return Ok(provider(modelId)); - } - // Handle xAI provider if (providerName === "xai") { if (!providerConfig.apiKey) { @@ -613,25 +599,6 @@ export class AIService extends EventEmitter { return Ok(provider(modelId)); } - // Handle DeepSeek provider - if (providerName === "deepseek") { - if (!providerConfig.apiKey) { - return Err({ - type: "api_key_not_found", - provider: providerName, - }); - } - const baseFetch = getProviderFetch(providerConfig); - - // Lazy-load DeepSeek provider to reduce startup time - const { createDeepSeek } = await PROVIDER_REGISTRY.deepseek(); - const provider = createDeepSeek({ - ...providerConfig, - fetch: baseFetch, - }); - return Ok(provider(modelId)); - } - // Handle Ollama provider if (providerName === "ollama") { // Ollama doesn't require API key - it's a local service @@ -810,6 +777,40 @@ export class AIService extends EventEmitter { return Ok(gateway(modelId)); } + // Generic handler for simple providers (standard API key + factory pattern) + // Providers with custom logic (anthropic, openai, xai, ollama, openrouter, bedrock, mux-gateway) + // are handled explicitly above. New providers using the standard pattern need only be + // added to PROVIDER_DEFINITIONS - no code changes required here. + const providerDef = PROVIDER_DEFINITIONS[providerName as ProviderName]; + if (providerDef) { + // Check API key requirement + if (providerDef.requiresApiKey && !providerConfig.apiKey) { + return Err({ + type: "api_key_not_found", + provider: providerName, + }); + } + + // Lazy-load and create provider using factoryName from definition + const providerModule = (await providerDef.import()) as unknown as Record< + string, + (config: Record) => (modelId: string) => LanguageModel + >; + const factory = providerModule[providerDef.factoryName]; + if (!factory) { + return Err({ + type: "provider_not_supported", + provider: providerName, + }); + } + + const provider = factory({ + ...providerConfig, + fetch: getProviderFetch(providerConfig), + }); + return Ok(provider(modelId)); + } + return Err({ type: "provider_not_supported", provider: providerName,