import { app } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import logManager from '@electron/service/logger'; import { PROVIDER_TYPE_INFO } from '@lib/providers'; import type { ProviderAccount, ProviderVendorInfo, ProviderWithKeyInfo, } from '@lib/providers'; interface ProviderStore { accounts: ProviderAccount[]; defaultAccountId: string | null; } const defaultStore: ProviderStore = { accounts: [], defaultAccountId: null, }; const storePath = path.join(app.getPath('userData'), 'provider-accounts.json'); const keysPath = path.join(app.getPath('userData'), 'provider-keys.json'); function readJson(filePath: string, defaultValue: T): T { try { if (fs.existsSync(filePath)) { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } } catch (e) { logManager.error(`Failed to read ${filePath}:`, e); } return defaultValue; } function writeJson(filePath: string, data: unknown) { try { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); } catch (e) { logManager.error(`Failed to write ${filePath}:`, e); } } function getStore(): ProviderStore { return readJson(storePath, defaultStore); } function saveStore(store: ProviderStore) { writeJson(storePath, store); } function getKeys(): Record { return readJson(keysPath, {}); } function saveKeys(keys: Record) { writeJson(keysPath, keys); } function mapToProviderWithKeyInfo(account: ProviderAccount): ProviderWithKeyInfo { const keys = getKeys(); const hasKey = !!keys[account.id]; return { id: account.id, name: account.label, type: account.vendorId as any, baseUrl: account.baseUrl, apiProtocol: account.apiProtocol, headers: account.headers, model: account.model, fallbackModels: account.fallbackModels, fallbackProviderIds: account.fallbackAccountIds, enabled: account.enabled, createdAt: account.createdAt, updatedAt: account.updatedAt, hasKey, keyMasked: hasKey ? '••••••••' : null, }; } type ProviderChangeListener = () => void; const listeners: ProviderChangeListener[] = []; export function onProviderChange(listener: ProviderChangeListener): () => void { listeners.push(listener); return () => { const idx = listeners.indexOf(listener); if (idx > -1) listeners.splice(idx, 1); }; } function notifyChange() { listeners.forEach((l) => l()); } function mapToVendorInfo(info: typeof PROVIDER_TYPE_INFO[number]): ProviderVendorInfo { return { ...info, category: info.id === 'ollama' ? 'local' : info.id === 'custom' ? 'custom' : 'compatible', supportedAuthModes: info.requiresApiKey ? info.isOAuth ? ['api_key', 'oauth_browser'] : ['api_key'] : info.isOAuth ? ['local', 'oauth_browser'] : ['local'], defaultAuthMode: info.requiresApiKey ? 'api_key' : 'local', supportsMultipleAccounts: true, } as ProviderVendorInfo; } function sanitizeAccount(account: ProviderAccount): ProviderAccount { let model = account.model; if (model) { // Fix corrupted DeepSeek model IDs stored in legacy accounts if (model === 'deepseek-chat/deepseek-reasoner' || model.startsWith('deepseek-chat/')) { model = 'deepseek-chat'; } else if (model.startsWith('deepseek-reasoner/')) { model = 'deepseek-reasoner'; } } if (model !== account.model) { return { ...account, model }; } return account; } export const providerApiService = { getVendors(): ProviderVendorInfo[] { return PROVIDER_TYPE_INFO.map(mapToVendorInfo); }, getAccounts(): ProviderAccount[] { return getStore().accounts.map(sanitizeAccount); }, getProviders(): ProviderWithKeyInfo[] { return getStore().accounts.map(sanitizeAccount).map(mapToProviderWithKeyInfo); }, getDefault(): { accountId: string | null } { return { accountId: getStore().defaultAccountId }; }, createAccount(body: { account: ProviderAccount; apiKey?: string }) { const store = getStore(); const account = { ...body.account, updatedAt: new Date().toISOString() }; store.accounts.push(account); if (body.apiKey) { const keys = getKeys(); keys[account.id] = body.apiKey; saveKeys(keys); } saveStore(store); notifyChange(); return { success: true }; }, updateAccount( accountId: string, body: { updates: Partial; apiKey?: string } ) { const store = getStore(); const idx = store.accounts.findIndex((a) => a.id === accountId); if (idx === -1) return { success: false, error: 'Account not found' }; store.accounts[idx] = { ...store.accounts[idx], ...body.updates, updatedAt: new Date().toISOString(), }; if (body.apiKey) { const keys = getKeys(); keys[accountId] = body.apiKey; saveKeys(keys); } saveStore(store); notifyChange(); return { success: true }; }, deleteAccount(accountId: string) { const store = getStore(); store.accounts = store.accounts.filter((a) => a.id !== accountId); if (store.defaultAccountId === accountId) store.defaultAccountId = null; saveStore(store); const keys = getKeys(); delete keys[accountId]; saveKeys(keys); notifyChange(); return { success: true }; }, setDefault(body: { accountId: string }) { const store = getStore(); const accountExists = store.accounts.some((a) => a.id === body.accountId); if (!accountExists) { return { success: false, error: 'Account not found' }; } store.defaultAccountId = body.accountId; saveStore(store); notifyChange(); return { success: true }; }, validateApiKey(body: { providerId: string; apiKey: string; options?: { baseUrl?: string; apiProtocol?: string }; }) { if (!body.apiKey || body.apiKey.trim().length === 0) { return { valid: false, error: 'API key is required' }; } // TODO: perform real validation against provider endpoint return { valid: true }; }, getApiKey(providerId: string) { const keys = getKeys(); return { apiKey: keys[providerId] || null }; }, deleteApiKey(accountId: string) { const keys = getKeys(); delete keys[accountId]; saveKeys(keys); notifyChange(); return { success: true }; }, async getUsageHistory(limit?: number) { const { getRecentTokenUsageHistory } = await import('@electron/utils/token-usage'); return getRecentTokenUsageHistory(limit); }, };