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/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-...", 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..3eb6db4a5f --- /dev/null +++ b/src/browser/assets/icons/deepseek.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/browser/components/ProviderIcon.tsx b/src/browser/components/ProviderIcon.tsx index 1ed6999e81..c34f991f21 100644 --- a/src/browser/components/ProviderIcon.tsx +++ b/src/browser/components/ProviderIcon.tsx @@ -3,23 +3,30 @@ 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"; +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, google: GoogleIcon, xai: XAIIcon, + deepseek: DeepSeekIcon, bedrock: AWSIcon, "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; @@ -30,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, - 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", - 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 b48789a0cf..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) { @@ -791,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,