|
|
| import { ProviderId, ProviderConfig, ProviderModel, CodexAuthData, HFAuthData, GitHubAuthData } from '@/lib/llm/providers/types'; |
| import { getDefaultModel } from '@/lib/llm/providers/registry'; |
| import { |
| getCustomProviders, |
| setCustomProviders, |
| saveCustomProvider as saveCustomProviderConfig, |
| removeCustomProvider as removeCustomProviderConfig, |
| generateCustomProviderId as generateCustomProviderIdHelper, |
| buildCustomProviderConfig as buildCustomProviderConfigHelper, |
| } from '@/lib/llm/providers/custom-providers'; |
| import { UsageInfo } from '@/lib/llm/types'; |
| import type { ModelTemplate, ModelAssignment } from '@/lib/llm/models/assignment'; |
| import { BUILT_IN_MODEL_TEMPLATES, isBuiltInTemplateId } from '@/lib/llm/models/registry'; |
| import { logger } from '@/lib/utils'; |
| import type { PermissionMode, GateDecision } from '@/lib/llm/permissions'; |
|
|
| export interface SessionCost { |
| sessionId: string; |
| startTime: Date; |
| totalCost: number; |
| messageCount: number; |
| providerBreakdown: Record<string, { |
| cost: number; |
| tokenUsage: { |
| input: number; |
| output: number; |
| }; |
| requestCount: number; |
| }>; |
| } |
|
|
| export interface CostSettings { |
| showCosts?: boolean; |
| dailyLimit?: number; |
| projectLimit?: number; |
| warningThreshold?: number; |
| } |
|
|
| interface ModelCacheEntry { |
| models: ProviderModel[]; |
| timestamp: string; |
| expiresAt: string; |
| } |
|
|
| export interface ProviderPricingEntry { |
| input: number; |
| output: number; |
| reasoning?: number; |
| } |
|
|
| export interface AppSettings { |
| openRouterApiKey?: string; |
| defaultModel?: string; |
| selectedProvider?: ProviderId; |
| providerKeys?: Partial<Record<ProviderId, string>>; |
| providerModels?: Partial<Record<ProviderId, string>>; |
| theme?: 'light' | 'dark' | 'system'; |
| costSettings?: CostSettings; |
| currentSession?: SessionCost; |
| lifetimeCosts?: { |
| total: number; |
| byProvider: Record<string, number>; |
| lastReset?: Date; |
| }; |
| hasSeenAboutModal?: boolean; |
| hasSeenGuidedTour?: boolean; |
| |
| pacingNoticeDismissed?: boolean; |
| modelCache?: Partial<Record<ProviderId, ModelCacheEntry>>; |
| modelPricing?: Partial<Record<ProviderId, Record<string, ProviderPricingEntry>>>; |
| reasoningEnabled?: Record<string, boolean>; |
| |
| compactionEnabled?: Partial<Record<ProviderId, boolean>>; |
| |
| compactionLimits?: Partial<Record<ProviderId, number>>; |
| codexAuth?: CodexAuthData; |
| hfAuth?: HFAuthData; |
| githubAuth?: GitHubAuthData; |
| telemetryOptIn?: boolean; |
| |
| debugStreamEnabled?: boolean; |
| modelTemplates?: Record<string, ModelTemplate>; |
| defaultTemplateId?: string; |
| |
| activeAssignment?: ModelAssignment; |
| permissionMode?: 'auto' | 'ask' | 'custom'; |
| permissionOverrides?: Record<string, 'ask' | 'allow'>; |
| webSearch?: { |
| provider?: 'tavily' | 'firecrawl' | 'brave' | 'searxng'; |
| keys?: Partial<Record<'tavily' | 'firecrawl' | 'brave', string>>; |
| searxngUrl?: string; |
| }; |
| |
| modelPickerCollapsed?: string[]; |
| } |
|
|
| |
| |
| |
| |
| |
| function hydrateDate(value: Date | string | undefined): Date | undefined { |
| if (!value) return undefined; |
| const d = new Date(value); |
| return isNaN(d.getTime()) ? undefined : d; |
| } |
|
|
| function hydrateModelTemplate(t: ModelTemplate): ModelTemplate { |
| return { |
| ...t, |
| builtin: false, |
| updatedAt: hydrateDate(t.updatedAt), |
| lastSyncedAt: hydrateDate(t.lastSyncedAt), |
| serverUpdatedAt: hydrateDate(t.serverUpdatedAt), |
| }; |
| } |
|
|
| class ConfigManager { |
| private readonly STORAGE_KEY = 'osw-studio-settings'; |
|
|
| getSettings(): AppSettings { |
| if (typeof window === 'undefined') { |
| return {}; |
| } |
| const stored = localStorage.getItem(this.STORAGE_KEY); |
| if (!stored) return {}; |
| |
| const settings = JSON.parse(stored); |
| |
| if ('autoSave' in settings || 'autoSaveInterval' in settings) { |
| delete settings.autoSave; |
| delete settings.autoSaveInterval; |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings)); |
| } |
| |
| return settings; |
| } |
|
|
| setSetting<K extends keyof AppSettings>( |
| key: K, |
| value: AppSettings[K] |
| ): void { |
| if (typeof window === 'undefined') { |
| return; |
| } |
| const settings = this.getSettings(); |
| settings[key] = value; |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings)); |
| } |
|
|
| hasSeenTour(): boolean { |
| return Boolean(this.getSettings().hasSeenGuidedTour); |
| } |
|
|
| setHasSeenTour(seen: boolean): void { |
| this.setSetting('hasSeenGuidedTour', seen); |
| } |
|
|
| hasDismissedPacingNotice(): boolean { |
| return this.getSettings().pacingNoticeDismissed === true; |
| } |
|
|
| setPacingNoticeDismissed(): void { |
| this.setSetting('pacingNoticeDismissed', true); |
| } |
|
|
| getApiKey(): string | null { |
| const provider = this.getSelectedProvider(); |
| if (provider) { |
| return this.getProviderApiKey(provider); |
| } |
| return this.getSettings().openRouterApiKey || null; |
| } |
|
|
| setApiKey(key: string): void { |
| const provider = this.getSelectedProvider(); |
| if (provider) { |
| this.setProviderApiKey(provider, key); |
| } |
| this.setSetting('openRouterApiKey', key); |
| } |
|
|
| getDefaultModel(): string { |
| const provider = this.getSelectedProvider(); |
| if (provider) { |
| return this.getProviderModel(provider) || this.getProviderDefaultModel(provider); |
| } |
| return this.getSettings().defaultModel || 'deepseek/deepseek-chat'; |
| } |
|
|
| setDefaultModel(model: string): void { |
| const provider = this.getSelectedProvider(); |
| if (provider) { |
| this.setProviderModel(provider, model); |
| } |
| this.setSetting('defaultModel', model); |
| } |
|
|
| getSelectedProvider(): ProviderId { |
| return this.getSettings().selectedProvider |
| || (process.env.NEXT_PUBLIC_DEFAULT_PROVIDER as ProviderId) |
| || 'openrouter'; |
| } |
|
|
| setSelectedProvider(provider: ProviderId): void { |
| this.setSetting('selectedProvider', provider); |
| this.emitModelConfigChanged(); |
| } |
|
|
| getProviderApiKey(provider: ProviderId): string | null { |
| const settings = this.getSettings(); |
| if (settings.providerKeys?.[provider]) { |
| return settings.providerKeys[provider]; |
| } |
| if (provider === 'openrouter' && settings.openRouterApiKey) { |
| return settings.openRouterApiKey; |
| } |
| return null; |
| } |
|
|
| setProviderApiKey(provider: ProviderId, key: string): void { |
| const settings = this.getSettings(); |
| const providerKeys = settings.providerKeys || {}; |
| providerKeys[provider] = key; |
| this.setSetting('providerKeys', providerKeys); |
| |
| if (provider === 'openrouter') { |
| this.setSetting('openRouterApiKey', key); |
| } |
| } |
|
|
| getProviderModel(provider: ProviderId): string | null { |
| const settings = this.getSettings(); |
| if (settings.providerModels?.[provider]) { |
| return settings.providerModels[provider]; |
| } |
| if (provider === 'openrouter' && settings.defaultModel) { |
| return settings.defaultModel; |
| } |
| return null; |
| } |
|
|
| setProviderModel(provider: ProviderId, model: string): void { |
| const settings = this.getSettings(); |
| const providerModels = settings.providerModels || {}; |
| providerModels[provider] = model; |
| this.setSetting('providerModels', providerModels); |
| |
| if (provider === 'openrouter') { |
| this.setSetting('defaultModel', model); |
| } |
| this.emitModelConfigChanged(); |
| } |
|
|
| getModelPricing(provider: ProviderId, model: string): ProviderPricingEntry | null { |
| const settings = this.getSettings(); |
| const providerPricing = settings.modelPricing?.[provider]; |
| if (!providerPricing) { |
| return null; |
| } |
|
|
| return ( |
| providerPricing[model] || |
| providerPricing[`${provider}/${model}`] || |
| (model.includes('/') ? providerPricing[model.split('/').pop() ?? ''] : null) |
| ) || null; |
| } |
|
|
| setProviderPricing(provider: ProviderId, pricingMap: Record<string, ProviderPricingEntry>): void { |
| if (typeof window === 'undefined') { |
| return; |
| } |
|
|
| if (!pricingMap || Object.keys(pricingMap).length === 0) { |
| return; |
| } |
|
|
| const settings = this.getSettings(); |
| const modelPricing = { ...(settings.modelPricing || {}) }; |
| const providerPricing = { ...(modelPricing[provider] || {}) }; |
|
|
| for (const [model, pricing] of Object.entries(pricingMap)) { |
| providerPricing[model] = pricing; |
| } |
|
|
| modelPricing[provider] = providerPricing; |
| this.setSetting('modelPricing', modelPricing); |
| } |
|
|
| private getProviderDefaultModel(provider: ProviderId): string { |
| return getDefaultModel(provider); |
| } |
|
|
| getTheme(): 'light' | 'dark' | 'system' { |
| return this.getSettings().theme || 'dark'; |
| } |
|
|
| setTheme(theme: 'light' | 'dark' | 'system'): void { |
| this.setSetting('theme', theme); |
| } |
|
|
| clearSettings(): void { |
| if (typeof window !== 'undefined') { |
| localStorage.removeItem(this.STORAGE_KEY); |
| } |
| } |
|
|
| getCostSettings(): CostSettings { |
| return this.getSettings().costSettings || { |
| showCosts: true, |
| warningThreshold: 80 |
| }; |
| } |
|
|
| setCostSettings(settings: CostSettings): void { |
| this.setSetting('costSettings', settings); |
| |
| if (typeof window !== 'undefined') { |
| window.dispatchEvent(new CustomEvent('osw-studio-cost-settings-changed')); |
| } |
| } |
|
|
| getCurrentSession(): SessionCost | null { |
| const session = this.getSettings().currentSession; |
| if (!session) { |
| return null; |
| } |
| return { |
| ...session, |
| startTime: new Date(session.startTime) |
| }; |
| } |
|
|
| startNewSession(): SessionCost { |
| const session: SessionCost = { |
| sessionId: Date.now().toString(), |
| startTime: new Date(), |
| totalCost: 0, |
| messageCount: 0, |
| providerBreakdown: {} |
| }; |
| this.setSetting('currentSession', session); |
| return session; |
| } |
|
|
| updateSessionCost(usage: UsageInfo, cost: number): void { |
| let session = this.getCurrentSession(); |
| if (!session) { |
| session = this.startNewSession(); |
| } |
|
|
| session.totalCost += cost; |
| session.messageCount += 1; |
|
|
| const provider = usage.provider || 'unknown'; |
| if (!session.providerBreakdown[provider]) { |
| session.providerBreakdown[provider] = { |
| cost: 0, |
| tokenUsage: { input: 0, output: 0 }, |
| requestCount: 0 |
| }; |
| } |
|
|
| session.providerBreakdown[provider].cost += cost; |
| session.providerBreakdown[provider].tokenUsage.input += usage.promptTokens; |
| session.providerBreakdown[provider].tokenUsage.output += usage.completionTokens; |
| session.providerBreakdown[provider].requestCount += 1; |
|
|
| const lifetimeCosts = this.getSettings().lifetimeCosts || { |
| total: 0, |
| byProvider: {} |
| }; |
| lifetimeCosts.total += cost; |
| lifetimeCosts.byProvider[provider] = (lifetimeCosts.byProvider[provider] || 0) + cost; |
|
|
| this.setSetting('currentSession', session); |
| this.setSetting('lifetimeCosts', lifetimeCosts); |
| } |
|
|
| getLifetimeCosts() { |
| return this.getSettings().lifetimeCosts || { |
| total: 0, |
| byProvider: {} |
| }; |
| } |
|
|
| resetLifetimeCosts(): void { |
| this.setSetting('lifetimeCosts', { |
| total: 0, |
| byProvider: {}, |
| lastReset: new Date() |
| }); |
| } |
|
|
| |
| getCachedModels(provider: ProviderId): ModelCacheEntry | null { |
| const settings = this.getSettings(); |
| const cache = settings.modelCache?.[provider]; |
| |
| if (!cache) return null; |
| |
| |
| const now = new Date(); |
| const expiresAt = new Date(cache.expiresAt); |
| |
| if (now > expiresAt) { |
| |
| this.clearModelCache(provider); |
| return null; |
| } |
| |
| return cache; |
| } |
|
|
| setCachedModels(provider: ProviderId, models: ProviderModel[]): void { |
| const settings = this.getSettings(); |
| const now = new Date(); |
| const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); |
| |
| const cache = settings.modelCache || {}; |
| cache[provider] = { |
| models, |
| timestamp: now.toISOString(), |
| expiresAt: expiresAt.toISOString() |
| }; |
| |
| this.setSetting('modelCache', cache); |
| } |
|
|
| clearModelCache(provider?: ProviderId): void { |
| if (provider) { |
| const settings = this.getSettings(); |
| const cache = settings.modelCache || {}; |
| delete cache[provider]; |
| this.setSetting('modelCache', cache); |
| } else { |
| |
| this.setSetting('modelCache', {}); |
| } |
| } |
|
|
| |
| getCodexAuth(): CodexAuthData | null { |
| return this.getSettings().codexAuth || null; |
| } |
|
|
| setCodexAuth(auth: CodexAuthData): void { |
| this.setSetting('codexAuth', auth); |
| |
| this.setProviderApiKey('openai-codex', auth.access_token); |
| } |
|
|
| clearCodexAuth(): void { |
| const settings = this.getSettings(); |
| delete settings.codexAuth; |
| if (typeof window !== 'undefined') { |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings)); |
| } |
| |
| const providerKeys = settings.providerKeys || {}; |
| delete providerKeys['openai-codex']; |
| this.setSetting('providerKeys', providerKeys); |
| } |
|
|
| isCodexTokenExpired(): boolean { |
| const auth = this.getCodexAuth(); |
| if (!auth) return true; |
| |
| return Date.now() / 1000 >= auth.expires_at - 60; |
| } |
|
|
| |
| getHFAuth(): HFAuthData | null { |
| return this.getSettings().hfAuth || null; |
| } |
|
|
| setHFAuth(auth: HFAuthData): void { |
| this.setSetting('hfAuth', auth); |
| |
| this.setProviderApiKey('huggingface', auth.access_token); |
| } |
|
|
| clearHFAuth(): void { |
| const settings = this.getSettings(); |
| delete settings.hfAuth; |
| if (typeof window !== 'undefined') { |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings)); |
| } |
| |
| const providerKeys = settings.providerKeys || {}; |
| delete providerKeys['huggingface']; |
| this.setSetting('providerKeys', providerKeys); |
| } |
|
|
| |
| getGitHubAuth(): GitHubAuthData | null { |
| const stored = this.getSettings().githubAuth; |
| if (stored) return stored; |
|
|
| |
| const envToken = process.env.GITHUB_TOKEN || process.env.NEXT_PUBLIC_GITHUB_TOKEN; |
| if (envToken) { |
| return { access_token: envToken }; |
| } |
|
|
| return null; |
| } |
|
|
| setGitHubAuth(auth: GitHubAuthData): void { |
| this.setSetting('githubAuth', auth); |
| |
| this.setProviderApiKey('github', auth.access_token); |
| } |
|
|
| clearGitHubAuth(): void { |
| const settings = this.getSettings(); |
| delete settings.githubAuth; |
| if (typeof window !== 'undefined') { |
| localStorage.setItem(this.STORAGE_KEY, JSON.stringify(settings)); |
| } |
| |
| const providerKeys = settings.providerKeys || {}; |
| delete providerKeys['github']; |
| this.setSetting('providerKeys', providerKeys); |
| } |
|
|
| getModelContextLengthFromCache(provider: ProviderId, modelId: string): number | undefined { |
| const cache = this.getCachedModels(provider); |
| if (!cache?.models) return undefined; |
| const model = cache.models.find(m => m.id === modelId); |
| return model?.contextLength; |
| } |
|
|
| isCompactionEnabled(provider: ProviderId): boolean { |
| const settings = this.getSettings(); |
| return settings.compactionEnabled?.[provider] ?? true; |
| } |
|
|
| setCompactionEnabled(provider: ProviderId, enabled: boolean): void { |
| const settings = this.getSettings(); |
| const map = { ...settings.compactionEnabled }; |
| if (enabled) { |
| delete map[provider]; |
| } else { |
| map[provider] = false; |
| } |
| this.setSetting('compactionEnabled', map); |
| } |
|
|
| getCompactionLimit(provider: ProviderId): number | undefined { |
| const settings = this.getSettings(); |
| return settings.compactionLimits?.[provider]; |
| } |
|
|
| setCompactionLimit(provider: ProviderId, limit: number | undefined): void { |
| const settings = this.getSettings(); |
| const limits = { ...settings.compactionLimits }; |
| if (limit === undefined) { |
| delete limits[provider]; |
| } else { |
| limits[provider] = limit; |
| } |
| this.setSetting('compactionLimits', limits); |
| } |
|
|
| |
| getReasoningEnabled(modelId: string): boolean { |
| const settings = this.getSettings(); |
| return settings.reasoningEnabled?.[modelId] ?? false; |
| } |
|
|
| getDebugStreamEnabled(): boolean { |
| const settings = this.getSettings(); |
| return settings.debugStreamEnabled ?? false; |
| } |
|
|
| setDebugStreamEnabled(enabled: boolean): void { |
| this.setSetting('debugStreamEnabled', enabled); |
| } |
|
|
| setReasoningEnabled(modelId: string, enabled: boolean): void { |
| const settings = this.getSettings(); |
| const reasoningEnabled = { ...(settings.reasoningEnabled || {}) }; |
| reasoningEnabled[modelId] = enabled; |
| this.setSetting('reasoningEnabled', reasoningEnabled); |
|
|
| |
| if (typeof window !== 'undefined') { |
| window.dispatchEvent(new CustomEvent('osw-studio-reasoning-changed', { |
| detail: { modelId, enabled } |
| })); |
| } |
| } |
|
|
| migrateModels(): void { |
| if (this.getSettings().modelTemplates) return; |
| const provider = this.getSelectedProvider(); |
| let model = this.getProviderModel(provider) || this.getDefaultModel(); |
| if (typeof window !== 'undefined') { |
| if (localStorage.getItem(`osw-studio-use-separate-chat-model-${provider}`) === 'true') { |
| model = localStorage.getItem(`osw-studio-code-model-${provider}`) || model; |
| } |
| const stale: string[] = []; |
| for (let i = 0; i < localStorage.length; i++) { |
| const k = localStorage.key(i); |
| if (k && /^osw-studio-(use-separate-chat-model|chat-model|code-model)-/.test(k)) stale.push(k); |
| } |
| stale.forEach((k) => localStorage.removeItem(k)); |
| } |
| const def: ModelTemplate = { |
| id: 'default', name: 'Default', builtin: false, |
| assignment: { |
| agent: { provider, model }, |
| imageGen: null, voiceInput: null, |
| autoCompact: this.isCompactionEnabled(provider), |
| compactLimit: this.getCompactionLimit(provider) ?? null, |
| }, |
| }; |
| this.saveModelTemplate(def); |
| this.setDefaultTemplateId('default'); |
| } |
|
|
| |
| |
| |
| |
| getModelTemplates(): Record<string, ModelTemplate> { |
| const merged: Record<string, ModelTemplate> = {}; |
| const stored = this.getSettings().modelTemplates || {}; |
| for (const [id, t] of Object.entries(stored)) { |
| |
| |
| |
| if (!isBuiltInTemplateId(id)) merged[id] = hydrateModelTemplate(t); |
| } |
| for (const t of BUILT_IN_MODEL_TEMPLATES) merged[t.id] = t; |
| return merged; |
| } |
| getModelTemplate(id: string): ModelTemplate | null { return this.getModelTemplates()[id] || null; } |
| saveModelTemplate(t: ModelTemplate): void { |
| if (isBuiltInTemplateId(t.id)) return; |
| const stored = this.getSettings().modelTemplates || {}; |
| |
| const saved: ModelTemplate = { ...t, builtin: false, updatedAt: new Date() }; |
| this.setSetting('modelTemplates', { ...stored, [t.id]: saved }); |
| |
| |
| |
| if (t.id === this.getDefaultTemplateId()) this.setSetting('activeAssignment', t.assignment); |
| if (typeof window !== 'undefined') { |
| import('@/lib/vfs/auto-sync').then(({ autoSyncModelTemplate }) => autoSyncModelTemplate(saved)).catch((e) => logger.debug('[ConfigManager] model-template auto-sync trigger failed', e)); |
| this.emitModelConfigChanged(); |
| } |
| } |
| deleteModelTemplate(id: string): void { |
| if (isBuiltInTemplateId(id)) return; |
| const all = { ...(this.getSettings().modelTemplates || {}) }; |
| delete all[id]; |
| this.setSetting('modelTemplates', all); |
| if (typeof window !== 'undefined') { |
| import('@/lib/vfs/auto-sync').then(({ autoDeleteModelTemplate }) => autoDeleteModelTemplate(id)).catch((e) => logger.debug('[ConfigManager] model-template auto-delete trigger failed', e)); |
| } |
| } |
| |
| importModelTemplateFromServer(t: ModelTemplate): void { |
| if (isBuiltInTemplateId(t.id)) return; |
| const stored = this.getSettings().modelTemplates || {}; |
| const now = new Date(); |
| const serverUpdated = t.updatedAt ? new Date(t.updatedAt) : now; |
| this.setSetting('modelTemplates', { |
| ...stored, |
| [t.id]: { ...t, builtin: false, updatedAt: serverUpdated, lastSyncedAt: now, serverUpdatedAt: serverUpdated }, |
| }); |
| |
| |
| |
| if (t.id === this.getDefaultTemplateId()) this.setSetting('activeAssignment', t.assignment); |
| this.emitModelConfigChanged(); |
| } |
| |
| updateModelTemplateSyncMetadata(id: string, lastSyncedAt: Date, serverUpdatedAt: Date): void { |
| const stored = this.getSettings().modelTemplates || {}; |
| const t = stored[id]; |
| if (!t) return; |
| this.setSetting('modelTemplates', { ...stored, [id]: { ...t, lastSyncedAt, serverUpdatedAt } }); |
| } |
| getDefaultTemplateId(): string { return this.getSettings().defaultTemplateId || 'default'; } |
| setDefaultTemplateId(id: string): void { |
| this.setSetting('defaultTemplateId', id); |
| |
| |
| |
| const t = this.getModelTemplate(id); |
| if (t) this.setSetting('activeAssignment', t.assignment); |
| this.emitModelConfigChanged(); |
| } |
|
|
| |
| private emitModelConfigChanged(): void { |
| if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('modelConfigChanged')); |
| } |
|
|
| |
| |
| |
| |
| |
| getActiveModelTemplate(): ModelTemplate { |
| const id = this.getDefaultTemplateId(); |
| let t = this.getModelTemplate(id); |
| if (!t) { |
| this.migrateModels(); |
| t = this.getModelTemplate(id); |
| } |
| if (!t) t = this.getModelTemplate('default') ?? this.getModelTemplate('or-recommended'); |
| if (!t) throw new Error('No model template available (default and built-ins missing)'); |
| return t; |
| } |
|
|
| |
| |
| |
| |
| |
| getActiveAssignment(): ModelAssignment { |
| const stored = this.getSettings().activeAssignment; |
| const assignment = stored ?? this.getActiveModelTemplate().assignment; |
| return JSON.parse(JSON.stringify(assignment)) as ModelAssignment; |
| } |
| setActiveAssignment(a: ModelAssignment): void { |
| this.setSetting('activeAssignment', a); |
| this.emitModelConfigChanged(); |
| } |
|
|
| |
| getPermissionMode(): PermissionMode { |
| return this.getSettings().permissionMode ?? 'ask'; |
| } |
| setPermissionMode(mode: PermissionMode): void { |
| this.setSetting('permissionMode', mode); |
| } |
| getPermissionOverrides(): Record<string, GateDecision> { |
| return this.getSettings().permissionOverrides ?? {}; |
| } |
| setPermissionOverride(key: string, decision: GateDecision): void { |
| const next = { ...this.getPermissionOverrides(), [key]: decision }; |
| this.setSetting('permissionOverrides', next); |
| } |
| setPermissionOverrides(map: Record<string, GateDecision>): void { |
| this.setSetting('permissionOverrides', map); |
| } |
|
|
| |
| getWebSearchProvider(): 'tavily' | 'firecrawl' | 'brave' | 'searxng' | null { |
| return this.getSettings().webSearch?.provider ?? null; |
| } |
| setWebSearchProvider(p: 'tavily' | 'firecrawl' | 'brave' | 'searxng' | null): void { |
| const ws = { ...this.getSettings().webSearch }; |
| if (p === null) { delete ws.provider; } else { ws.provider = p; } |
| this.setSetting('webSearch', ws); |
| } |
| getWebSearchKey(p: 'tavily' | 'firecrawl' | 'brave'): string | null { |
| return this.getSettings().webSearch?.keys?.[p] ?? null; |
| } |
| setWebSearchKey(p: 'tavily' | 'firecrawl' | 'brave', key: string): void { |
| const ws = this.getSettings().webSearch ?? {}; |
| this.setSetting('webSearch', { ...ws, keys: { ...ws.keys, [p]: key } }); |
| } |
| getSearxngUrl(): string | null { |
| return this.getSettings().webSearch?.searxngUrl ?? null; |
| } |
| setSearxngUrl(url: string): void { |
| this.setSetting('webSearch', { ...this.getSettings().webSearch, searxngUrl: url }); |
| } |
| isWebSearchConfigured(): boolean { |
| const ws = this.getSettings().webSearch; |
| if (!ws?.provider) return false; |
| if (ws.provider === 'searxng') return !!ws.searxngUrl; |
| return !!ws.keys?.[ws.provider]; |
| } |
|
|
| getCollapsedModelGroups(): string[] { |
| return this.getSettings().modelPickerCollapsed ?? []; |
| } |
| setModelGroupCollapsed(groupId: string, collapsed: boolean): void { |
| const set = new Set(this.getCollapsedModelGroups()); |
| if (collapsed) set.add(groupId); else set.delete(groupId); |
| this.setSetting('modelPickerCollapsed', [...set]); |
| } |
|
|
| |
| getCustomProviders(): Record<string, ProviderConfig> { |
| return getCustomProviders(); |
| } |
|
|
| setCustomProviders(providers: Record<string, ProviderConfig>): void { |
| setCustomProviders(providers); |
| } |
|
|
| saveCustomProvider(id: string, config: ProviderConfig): void { |
| saveCustomProviderConfig(id, config); |
| if (typeof window !== 'undefined') { |
| import('@/lib/vfs/auto-sync').then(({ autoSyncConnection }) => autoSyncConnection(config)).catch((e) => logger.debug('[ConfigManager] connection auto-sync trigger failed', e)); |
| } |
| } |
|
|
| removeCustomProvider(id: string): void { |
| removeCustomProviderConfig(id); |
| if (typeof window !== 'undefined') { |
| import('@/lib/vfs/auto-sync').then(({ autoDeleteConnection }) => autoDeleteConnection(id)).catch((e) => logger.debug('[ConfigManager] connection auto-delete trigger failed', e)); |
| } |
| } |
|
|
| generateCustomProviderId(base: string): string { |
| return generateCustomProviderIdHelper(base); |
| } |
|
|
| buildCustomProviderConfig( |
| id: string, |
| name: string, |
| baseUrl: string, |
| apiKeyRequired: boolean |
| ): ProviderConfig { |
| return buildCustomProviderConfigHelper(id, name, baseUrl, apiKeyRequired); |
| } |
| } |
|
|
| export const configManager = new ConfigManager(); |
|
|
| |
| |
| |
| export function getLoginUrl(): string { |
| return process.env.NEXT_PUBLIC_GATEWAY_URL |
| ? `${process.env.NEXT_PUBLIC_GATEWAY_URL}/login` |
| : '/admin/login'; |
| } |
|
|
| |
| |
| |
| |
| export function migrateBackendKey(projectId: string): boolean { |
| if (typeof window === 'undefined') return true; |
| const legacyKey = `osw-server-features-${projectId}`; |
| const newKey = `osw-backend-${projectId}`; |
| if (localStorage.getItem(legacyKey) && !localStorage.getItem(newKey)) { |
| localStorage.setItem(newKey, localStorage.getItem(legacyKey)!); |
| localStorage.removeItem(legacyKey); |
| } |
| return localStorage.getItem(newKey) !== 'false'; |
| } |
|
|