diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 172b46f..81ad7bd 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import { streamClaude } from '@/lib/claude-client'; -import { addMessage, getSession, updateSessionTitle, updateSdkSessionId, updateSessionModel, updateSessionProvider, getSetting, getActiveProvider } from '@/lib/db'; +import { addMessage, getSession, updateSessionTitle, updateSdkSessionId, updateSessionModel, updateSessionProvider, updateSessionProviderId, getSetting, getProvider, getDefaultProviderId } from '@/lib/db'; import type { SendMessageRequest, SSEEvent, TokenUsage, MessageContentBlock, FileAttachment } from '@/types'; import fs from 'fs'; import path from 'path'; @@ -10,8 +10,8 @@ export const dynamic = 'force-dynamic'; export async function POST(request: NextRequest) { try { - const body: SendMessageRequest & { files?: FileAttachment[]; toolTimeout?: number } = await request.json(); - const { session_id, content, model, mode, files, toolTimeout } = body; + const body: SendMessageRequest & { files?: FileAttachment[]; toolTimeout?: number; provider_id?: string } = await request.json(); + const { session_id, content, model, mode, files, toolTimeout, provider_id } = body; if (!session_id || !content) { return new Response(JSON.stringify({ error: 'session_id and content are required' }), { @@ -62,11 +62,36 @@ export async function POST(request: NextRequest) { if (effectiveModel && effectiveModel !== session.model) { updateSessionModel(session_id, effectiveModel); } - const activeProvider = getActiveProvider(); - const providerName = activeProvider?.name || ''; + + // Resolve provider: explicit provider_id > default_provider_id > environment variables + let resolvedProvider: import('@/types').ApiProvider | undefined; + const effectiveProviderId = provider_id || session.provider_id || ''; + if (effectiveProviderId && effectiveProviderId !== 'env') { + resolvedProvider = getProvider(effectiveProviderId); + if (!resolvedProvider) { + // Requested provider not found, try default + const defaultId = getDefaultProviderId(); + if (defaultId) { + resolvedProvider = getProvider(defaultId); + } + } + } else if (!effectiveProviderId) { + // No provider specified, try default + const defaultId = getDefaultProviderId(); + if (defaultId) { + resolvedProvider = getProvider(defaultId); + } + } + // effectiveProviderId === 'env' → resolvedProvider stays undefined → uses env vars + + const providerName = resolvedProvider?.name || ''; if (providerName !== (session.provider_name || '')) { updateSessionProvider(session_id, providerName); } + const persistProviderId = effectiveProviderId || provider_id || ''; + if (persistProviderId !== (session.provider_id || '')) { + updateSessionProviderId(session_id, persistProviderId); + } // Determine permission mode from chat mode: code → acceptEdits, plan → plan, ask → default (no tools) const effectiveMode = mode || session.mode || 'code'; @@ -122,6 +147,7 @@ export async function POST(request: NextRequest) { permissionMode, files: fileAttachments, toolTimeoutSeconds: toolTimeout || 300, + provider: resolvedProvider, }); // Tee the stream: one for client, one for collecting the response diff --git a/src/app/api/providers/models/route.ts b/src/app/api/providers/models/route.ts index 05d494a..feb3712 100644 --- a/src/app/api/providers/models/route.ts +++ b/src/app/api/providers/models/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { getActiveProvider } from '@/lib/db'; -import type { ErrorResponse } from '@/types'; +import { getAllProviders, getDefaultProviderId } from '@/lib/db'; +import type { ErrorResponse, ProviderModelGroup } from '@/types'; // Default Claude model options const DEFAULT_MODELS = [ @@ -53,31 +53,67 @@ const PROVIDER_MODEL_LABELS: Record ], }; +/** + * Deduplicate models: if multiple aliases map to the same label, keep only the first one. + */ +function deduplicateModels(models: { value: string; label: string }[]): { value: string; label: string }[] { + const seen = new Set(); + const result: { value: string; label: string }[] = []; + for (const m of models) { + if (!seen.has(m.label)) { + seen.add(m.label); + result.push(m); + } + } + return result; +} + export async function GET() { try { - const activeProvider = getActiveProvider(); + const providers = getAllProviders(); + const groups: ProviderModelGroup[] = []; - // No active provider or anthropic type -> default Claude models - if (!activeProvider || activeProvider.provider_type === 'anthropic') { - return NextResponse.json({ + // Check for environment variables + const hasEnvKey = !!(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN); + if (hasEnvKey) { + groups.push({ + provider_id: 'env', + provider_name: 'Environment', + provider_type: 'anthropic', models: DEFAULT_MODELS, - provider_name: activeProvider?.name || 'Anthropic', }); } - // Custom/other provider -> match by base_url - const matched = PROVIDER_MODEL_LABELS[activeProvider.base_url]; - if (matched) { - return NextResponse.json({ - models: matched, - provider_name: activeProvider.name, + // Build a group for each configured provider + for (const provider of providers) { + const matched = PROVIDER_MODEL_LABELS[provider.base_url]; + const rawModels = matched || DEFAULT_MODELS; + const models = deduplicateModels(rawModels); + + groups.push({ + provider_id: provider.id, + provider_name: provider.name, + provider_type: provider.provider_type, + models, }); } - // Unknown provider -> return defaults with provider name + // If no groups at all (no env, no providers), show default Anthropic group + if (groups.length === 0) { + groups.push({ + provider_id: 'env', + provider_name: 'Anthropic', + provider_type: 'anthropic', + models: DEFAULT_MODELS, + }); + } + + // Determine default provider + const defaultProviderId = getDefaultProviderId() || groups[0].provider_id; + return NextResponse.json({ - models: DEFAULT_MODELS, - provider_name: activeProvider.name, + groups, + default_provider_id: defaultProviderId, }); } catch (error) { return NextResponse.json( diff --git a/src/app/api/providers/route.ts b/src/app/api/providers/route.ts index 7d87b17..1650b5f 100644 --- a/src/app/api/providers/route.ts +++ b/src/app/api/providers/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getAllProviders, createProvider } from '@/lib/db'; +import { getAllProviders, createProvider, getSetting } from '@/lib/db'; import type { ProviderResponse, ErrorResponse, CreateProviderRequest, ApiProvider } from '@/types'; function maskApiKey(provider: ApiProvider): ApiProvider { @@ -38,7 +38,11 @@ export async function GET() { try { const providers = getAllProviders().map(maskApiKey); const envDetected = detectEnvVars(); - return NextResponse.json({ providers, env_detected: envDetected }); + return NextResponse.json({ + providers, + env_detected: envDetected, + default_provider_id: getSetting('default_provider_id') || '', + }); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : 'Failed to get providers' }, diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 474a8fa..c3e6858 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -23,6 +23,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { const [error, setError] = useState(null); const [sessionTitle, setSessionTitle] = useState(''); const [sessionModel, setSessionModel] = useState(''); + const [sessionProviderId, setSessionProviderId] = useState(''); const [sessionMode, setSessionMode] = useState(''); const [projectName, setProjectName] = useState(''); const [sessionWorkingDir, setSessionWorkingDir] = useState(''); @@ -94,6 +95,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { setSessionTitle(title); setPanelSessionTitle(title); setSessionModel(data.session.model || ''); + setSessionProviderId(data.session.provider_id || ''); setSessionMode(data.session.mode || 'code'); setProjectName(data.session.project_name || ''); } @@ -233,7 +235,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { )} )} - + ); } diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 4df2f44..68476ff 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -30,6 +30,7 @@ export default function NewChatPage() { const [workingDir, setWorkingDir] = useState(''); const [mode, setMode] = useState('code'); const [currentModel, setCurrentModel] = useState('sonnet'); + const [currentProviderId, setCurrentProviderId] = useState(''); const [pendingPermission, setPendingPermission] = useState(null); const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null); const [streamingToolOutput, setStreamingToolOutput] = useState(''); @@ -143,7 +144,7 @@ export default function NewChatPage() { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ session_id: session.id, content, mode, model: currentModel }), + body: JSON.stringify({ session_id: session.id, content, mode, model: currentModel, provider_id: currentProviderId }), signal: controller.signal, }); @@ -310,7 +311,7 @@ export default function NewChatPage() { abortControllerRef.current = null; } }, - [isStreaming, router, workingDir, mode, currentModel, setPendingApprovalSessionId] + [isStreaming, router, workingDir, mode, currentModel, currentProviderId, setPendingApprovalSessionId] ); const handleCommand = useCallback((command: string) => { @@ -369,6 +370,11 @@ export default function NewChatPage() { isStreaming={isStreaming} modelName={currentModel} onModelChange={setCurrentModel} + providerId={currentProviderId} + onProviderModelChange={(pid, model) => { + setCurrentProviderId(pid); + setCurrentModel(model); + }} workingDirectory={workingDir} mode={mode} onModeChange={setMode} diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 6220bef..a6f5d2f 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -24,9 +24,10 @@ interface ChatViewProps { initialHasMore?: boolean; modelName?: string; initialMode?: string; + providerId?: string; } -export function ChatView({ sessionId, initialMessages = [], initialHasMore = false, modelName, initialMode }: ChatViewProps) { +export function ChatView({ sessionId, initialMessages = [], initialHasMore = false, modelName, initialMode, providerId }: ChatViewProps) { const { setStreamingSessionId, workingDirectory, setWorkingDirectory, setPanelOpen, setPendingApprovalSessionId } = usePanel(); const [messages, setMessages] = useState(initialMessages); const [hasMore, setHasMore] = useState(initialHasMore); @@ -39,6 +40,7 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal const [statusText, setStatusText] = useState(); const [mode, setMode] = useState(initialMode || 'code'); const [currentModel, setCurrentModel] = useState(modelName || (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') : null) || 'sonnet'); + const [currentProviderId, setCurrentProviderId] = useState(providerId || (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') : null) || ''); const [pendingPermission, setPendingPermission] = useState(null); const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null); const [streamingToolOutput, setStreamingToolOutput] = useState(''); @@ -64,6 +66,12 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal }).catch(() => { /* silent — will apply on next message */ }); } }, [sessionId]); + + const handleProviderModelChange = useCallback((newProviderId: string, model: string) => { + setCurrentProviderId(newProviderId); + setCurrentModel(model); + }, []); + const abortControllerRef = useRef(null); // Abort active stream on unmount (e.g., session switch via key={id} remount). @@ -266,6 +274,7 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal content, mode, model: currentModel, + provider_id: currentProviderId, ...(files && files.length > 0 ? { files } : {}), }), signal: controller.signal, @@ -498,7 +507,7 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal window.dispatchEvent(new CustomEvent('refresh-file-tree')); } }, - [sessionId, isStreaming, setStreamingSessionId, setPendingApprovalSessionId, mode, currentModel] + [sessionId, isStreaming, setStreamingSessionId, setPendingApprovalSessionId, mode, currentModel, currentProviderId] ); // Keep sendMessageRef in sync so timeout auto-retry can call it @@ -605,6 +614,8 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal sessionId={sessionId} modelName={currentModel} onModelChange={setCurrentModel} + providerId={currentProviderId} + onProviderModelChange={handleProviderModelChange} workingDirectory={workingDirectory} mode={mode} onModeChange={handleModeChange} diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 9e7fc97..d94ba2c 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -33,7 +33,7 @@ import { usePromptInputAttachments, } from '@/components/ai-elements/prompt-input'; import type { ChatStatus } from 'ai'; -import type { FileAttachment } from '@/types'; +import type { FileAttachment, ProviderModelGroup } from '@/types'; import { nanoid } from 'nanoid'; // Accepted file types for upload @@ -58,6 +58,8 @@ interface MessageInputProps { sessionId?: string; modelName?: string; onModelChange?: (model: string) => void; + providerId?: string; + onProviderModelChange?: (providerId: string, model: string) => void; workingDirectory?: string; mode?: string; onModeChange?: (mode: string) => void; @@ -331,6 +333,8 @@ export function MessageInput({ sessionId, modelName, onModelChange, + providerId, + onProviderModelChange, workingDirectory, mode = 'code', onModeChange, @@ -349,40 +353,53 @@ export function MessageInput({ const [modelMenuOpen, setModelMenuOpen] = useState(false); const [inputValue, setInputValue] = useState(''); const [badge, setBadge] = useState(null); - const [dynamicModels, setDynamicModels] = useState<{ value: string; label: string }[]>(DEFAULT_MODEL_OPTIONS); - const [activeProviderName, setActiveProviderName] = useState(null); + const [providerGroups, setProviderGroups] = useState([]); + const [defaultProviderId, setDefaultProviderId] = useState(''); const [aiSuggestions, setAiSuggestions] = useState([]); const [aiSearchLoading, setAiSearchLoading] = useState(false); const aiSearchAbortRef = useRef(null); const aiSearchTimerRef = useRef | null>(null); - // Fetch models from API - const fetchModels = useCallback(() => { + // Fetch provider groups from API + const fetchProviderModels = useCallback(() => { fetch('/api/providers/models') .then((r) => r.json()) .then((data) => { - if (data.models && data.models.length > 0) { - setDynamicModels(data.models); + if (data.groups && data.groups.length > 0) { + setProviderGroups(data.groups); } else { - setDynamicModels(DEFAULT_MODEL_OPTIONS); + setProviderGroups([{ + provider_id: 'env', + provider_name: 'Anthropic', + provider_type: 'anthropic', + models: DEFAULT_MODEL_OPTIONS, + }]); } - setActiveProviderName(data.provider_name || null); + setDefaultProviderId(data.default_provider_id || ''); }) .catch(() => { - setDynamicModels(DEFAULT_MODEL_OPTIONS); - setActiveProviderName(null); + setProviderGroups([{ + provider_id: 'env', + provider_name: 'Anthropic', + provider_type: 'anthropic', + models: DEFAULT_MODEL_OPTIONS, + }]); + setDefaultProviderId(''); }); }, []); // Load models on mount and listen for provider changes useEffect(() => { - fetchModels(); - const handler = () => fetchModels(); + fetchProviderModels(); + const handler = () => fetchProviderModels(); window.addEventListener('provider-changed', handler); return () => window.removeEventListener('provider-changed', handler); - }, [fetchModels]); + }, [fetchProviderModels]); - const MODEL_OPTIONS = dynamicModels; + // Derive flat model list for current provider (used by currentModelOption lookup) + const currentProviderIdValue = providerId || defaultProviderId || (providerGroups[0]?.provider_id ?? ''); + const currentGroup = providerGroups.find(g => g.provider_id === currentProviderIdValue) || providerGroups[0]; + const MODEL_OPTIONS = currentGroup?.models || DEFAULT_MODEL_OPTIONS; // Fetch files for @ mention const fetchFiles = useCallback(async (filter: string) => { @@ -1066,40 +1083,50 @@ export function MessageInput({ onClick={() => setModelMenuOpen((prev) => !prev)} > {currentModelOption.label} - {activeProviderName && activeProviderName !== 'Anthropic' && ( - {activeProviderName} + {currentGroup && currentGroup.provider_name !== 'Anthropic' && currentGroup.provider_name !== 'Environment' && ( + {currentGroup.provider_name} )} {modelMenuOpen && ( -
- {activeProviderName && ( -
- {activeProviderName} +
+ {providerGroups.map((group, groupIdx) => ( +
+ {/* Group header */} +
0 && "border-t" + )}> + {group.provider_name} +
+ {/* Models in group */} +
+ {group.models.map((opt) => { + const isActive = opt.value === currentModelValue && group.provider_id === currentProviderIdValue; + return ( + + ); + })} +
- )} -
- {MODEL_OPTIONS.map((opt) => { - const isActive = opt.value === currentModelValue; - return ( - - ); - })} -
+ ))}
)}
diff --git a/src/components/settings/ProviderManager.tsx b/src/components/settings/ProviderManager.tsx index 56b0e5a..5a97c77 100644 --- a/src/components/settings/ProviderManager.tsx +++ b/src/components/settings/ProviderManager.tsx @@ -19,9 +19,7 @@ import { Loading02Icon, Delete02Icon, PencilEdit01Icon, - Tick01Icon, ServerStack01Icon, - Cancel01Icon, } from "@hugeicons/core-free-icons"; import { ProviderForm } from "./ProviderForm"; import type { ProviderFormData } from "./ProviderForm"; @@ -59,9 +57,6 @@ export function ProviderManager() { const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); - // Activating state - const [activatingId, setActivatingId] = useState(null); - const fetchProviders = useCallback(async () => { try { setError(null); @@ -119,6 +114,7 @@ export function ProviderManager() { setProviders((prev) => prev.map((p) => (p.id === editingProvider.id ? result.provider : p)) ); + window.dispatchEvent(new Event('provider-changed')); } else { const res = await fetch("/api/providers", { method: "POST", @@ -131,6 +127,7 @@ export function ProviderManager() { } const result = await res.json(); setProviders((prev) => [...prev, result.provider]); + window.dispatchEvent(new Event('provider-changed')); } }; @@ -143,6 +140,7 @@ export function ProviderManager() { }); if (res.ok) { setProviders((prev) => prev.filter((p) => p.id !== deleteTarget.id)); + window.dispatchEvent(new Event('provider-changed')); } } catch { // ignore @@ -152,50 +150,6 @@ export function ProviderManager() { } }; - const handleActivate = async (provider: ApiProvider) => { - setActivatingId(provider.id); - try { - const res = await fetch(`/api/providers/${provider.id}/activate`, { - method: "POST", - }); - if (res.ok) { - // Mark the activated provider as active, deactivate others - setProviders((prev) => - prev.map((p) => ({ - ...p, - is_active: p.id === provider.id ? 1 : 0, - })) - ); - window.dispatchEvent(new Event('provider-changed')); - } - } catch { - // ignore - } finally { - setActivatingId(null); - } - }; - - const handleDeactivate = async (provider: ApiProvider) => { - setActivatingId(provider.id); - try { - const res = await fetch(`/api/providers/${provider.id}/activate`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ active: false }), - }); - if (res.ok) { - setProviders((prev) => - prev.map((p) => ({ ...p, is_active: 0 })) - ); - window.dispatchEvent(new Event('provider-changed')); - } - } catch { - // ignore - } finally { - setActivatingId(null); - } - }; - const sorted = [...providers].sort((a, b) => a.sort_order - b.sort_order); return ( @@ -212,7 +166,7 @@ export function ProviderManager() { )}

- Manage API providers for Claude Code. The active provider will be used for all sessions. + Configure API providers. Select provider and model from the chat input.

- ) : ( - - )} - - - + {/* Actions */} +
+ +
- ); - })} + + ))} )} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index e9f6d9d..ab57dab 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -135,6 +135,9 @@ const en = { 'provider.saving': 'Saving...', 'provider.update': 'Update', 'provider.envDetected': 'Detected from environment', + 'provider.default': 'Default', + 'provider.setDefault': 'Set as Default', + 'provider.environment': 'Environment', // ── Right panel / Files ───────────────────────────────────── 'panel.files': 'Files', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 5149c30..33b8e0f 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -132,6 +132,9 @@ const zh: Record = { 'provider.saving': '保存中...', 'provider.update': '更新', 'provider.envDetected': '从环境变量检测到', + 'provider.default': '默认', + 'provider.setDefault': '设为默认', + 'provider.environment': '环境变量', // ── Right panel / Files ───────────────────────────────────── 'panel.files': '文件', diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts index a68cff9..658b3cb 100644 --- a/src/lib/claude-client.ts +++ b/src/lib/claude-client.ts @@ -15,7 +15,7 @@ import type { NotificationHookInput, PostToolUseHookInput, } from '@anthropic-ai/claude-agent-sdk'; -import type { ClaudeStreamOptions, SSEEvent, TokenUsage, MCPServerConfig, PermissionRequestEvent, FileAttachment } from '@/types'; +import type { ClaudeStreamOptions, SSEEvent, TokenUsage, MCPServerConfig, PermissionRequestEvent, FileAttachment, ApiProvider } from '@/types'; import { isImageFile } from '@/types'; import { registerPendingPermission } from './permission-registry'; import { registerConversation, unregisterConversation } from './conversation-registry'; @@ -242,7 +242,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream({ async start(controller) { // Hoist activeProvider so it's accessible in the catch block for error messages - const activeProvider = getActiveProvider(); + const activeProvider: ApiProvider | undefined = options.provider ?? getActiveProvider(); try { // Build env for the Claude Code subprocess. diff --git a/src/lib/db.ts b/src/lib/db.ts index 049345c..1c85127 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -153,6 +153,20 @@ function migrateDb(db: Database.Database): void { if (!colNames.includes('provider_name')) { db.exec("ALTER TABLE chat_sessions ADD COLUMN provider_name TEXT NOT NULL DEFAULT ''"); } + if (!colNames.includes('provider_id')) { + db.exec("ALTER TABLE chat_sessions ADD COLUMN provider_id TEXT NOT NULL DEFAULT ''"); + } + + // Migrate is_active provider to default_provider_id setting + const defaultProviderSetting = db.prepare("SELECT value FROM settings WHERE key = 'default_provider_id'").get() as { value: string } | undefined; + if (!defaultProviderSetting) { + const activeProvider = db.prepare('SELECT id FROM api_providers WHERE is_active = 1 LIMIT 1').get() as { id: string } | undefined; + if (activeProvider) { + db.prepare( + "INSERT INTO settings (key, value) VALUES ('default_provider_id', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value" + ).run(activeProvider.id); + } + } const msgColumns = db.prepare("PRAGMA table_info(messages)").all() as { name: string }[]; const msgColNames = msgColumns.map(c => c.name); @@ -274,6 +288,19 @@ export function updateSessionProvider(id: string, providerName: string): void { db.prepare('UPDATE chat_sessions SET provider_name = ? WHERE id = ?').run(providerName, id); } +export function updateSessionProviderId(id: string, providerId: string): void { + const db = getDb(); + db.prepare('UPDATE chat_sessions SET provider_id = ? WHERE id = ?').run(providerId, id); +} + +export function getDefaultProviderId(): string | undefined { + return getSetting('default_provider_id') || undefined; +} + +export function setDefaultProviderId(id: string): void { + setSetting('default_provider_id', id); +} + export function updateSessionWorkingDirectory(id: string, workingDirectory: string): void { const db = getDb(); const projectName = path.basename(workingDirectory); diff --git a/src/types/index.ts b/src/types/index.ts index 36c6766..cb0a968 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,6 +16,7 @@ export interface ChatSession { mode?: 'code' | 'plan' | 'ask'; needs_approval?: boolean; provider_name: string; + provider_id: string; } // ========================================== @@ -112,6 +113,13 @@ export interface ApiProvider { updated_at: string; } +export interface ProviderModelGroup { + provider_id: string; // provider DB id, or 'env' for environment variables + provider_name: string; + provider_type: string; + models: Array<{ value: string; label: string }>; +} + export interface CreateProviderRequest { name: string; provider_type?: string; @@ -168,6 +176,7 @@ export interface SendMessageRequest { content: string; model?: string; mode?: string; + provider_id?: string; } export interface UpdateMCPConfigRequest { @@ -438,4 +447,5 @@ export interface ClaudeStreamOptions { permissionMode?: string; files?: FileAttachment[]; toolTimeoutSeconds?: number; + provider?: ApiProvider; }