diff --git a/CLAUDE.md b/CLAUDE.md index 570891f9..8ccfd2bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,7 +206,7 @@ A code this build knows but an older backend does not is resolved back to Englis `configStore.getConfig()` resolves the language on the way *out*, not on the way in. The disk holds whatever some build wrote - a code a later release dropped, or one an older release never knew - and every consumer reads through `getConfig`, so that is the single place an unknown code can be stopped before it reaches the ASR URL and three request bodies. `test/language.test.mjs` pins it. -**The picker stays live mid-interview**, unlike Model, because an interview that switches language is the case it exists for and not one the candidate can prepare for by restarting. The two halves of the setting move at different speeds and `useInterviewLanguage` is where that is reconciled. Suggestions need nothing: every request reads the config store as it is built, so the next one already follows. The ASR carries its language as a *connection* parameter, so `liveTranscriptionService.setLanguage()` tears both sockets down and re-opens them - a second or two of gap, and whatever utterance was mid-flight is orphaned, which is why the button shows a spinner rather than pretending the change was instant and why the menu says so before the user commits. +**The picker stays live mid-interview**, because an interview that switches language is the case it exists for and not one the candidate can prepare for by restarting. The two halves of the setting move at different speeds and `useInterviewLanguage` is where that is reconciled. Suggestions need nothing: every request reads the config store as it is built, so the next one already follows. The ASR carries its language as a *connection* parameter, so `liveTranscriptionService.setLanguage()` tears both sockets down and re-opens them - a second or two of gap, and whatever utterance was mid-flight is orphaned, which is why the button shows a spinner rather than pretending the change was instant and why the menu says so before the user commits. Three guards in `AudioWsStream` make that safe, and all three protect against the same failure - two sockets on one channel, one of them orphaned and still relaying audio into a dead session. `ws.onclose` ignores a close from a socket that is no longer `this.ws`, since that is the tail of a replacement rather than a disconnect; and the `switching` flag suppresses the ordinary backoff reconnect for the close `setLanguage` causes itself, which it then handles immediately instead of after `WS_RETRY_BASE_DELAY_MS`. `connectWebSocket` rebuilds the URL per attempt rather than capturing it, which is what lets a reconnect pick up the new language at all. diff --git a/src/main/api/llm.ts b/src/main/api/llm.ts index 60020c95..bffbd7fc 100644 --- a/src/main/api/llm.ts +++ b/src/main/api/llm.ts @@ -7,27 +7,10 @@ import { GenerateActionSuggestionRequest, GenerateLiveSuggestionRequest, GenerateSummarizeRequest, - LLMConfigValidationResult, - LLMModelInfo, - LLMRequest, } from '../types/llm.js'; import { ApiClient, ApiResponse } from './client.js'; export class LLMApi extends ApiClient { - /** - * Validate LLM Config - */ - async validate(request: LLMRequest): Promise> { - return this.post('/api/llm/validate', request); - } - - /** - * List Supported Models - */ - async listModels(): Promise> { - return this.get('/api/llm/models'); - } - /** * Generate Live Suggestions */ diff --git a/src/main/index.ts b/src/main/index.ts index 5f5a1bec..7e5112ae 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,7 +14,6 @@ import { registerAuthHandlers } from './ipc/auth.js'; import { registerAutoUpdaterHandlers } from './ipc/auto-updater.js'; import { registerConfigHandlers } from './ipc/config.js'; import { registerExternalHandlers } from './ipc/external.js'; -import { registerLLMHandlers } from './ipc/llm.js'; import { registerPaymentHandlers } from './ipc/payment.js'; import { registerPermissionHandlers } from './ipc/permissions.js'; import { registerActionSuggestionHandlers } from './ipc/suggestion-action.js'; @@ -206,7 +205,6 @@ app.whenReady().then(async () => { registerAuthHandlers(); registerAccountHandlers(); registerPaymentHandlers(); - registerLLMHandlers(); registerPermissionHandlers(); registerTranscriptHandlers(); registerLiveSuggestionHandlers(); diff --git a/src/main/ipc/llm.ts b/src/main/ipc/llm.ts deleted file mode 100644 index e0cd021c..00000000 --- a/src/main/ipc/llm.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ipcMain } from 'electron'; - -import { LLMApi } from '../api/llm.js'; -import { LLMConfig } from '../types/llm.js'; - -const llmApi = new LLMApi(); - -export function registerLLMHandlers(): void { - ipcMain.handle('llm:list-models', async () => { - try { - const response = await llmApi.listModels(); - if (response.error) { - return { success: false, error: response.error.message }; - } - return { success: true, data: response.data ?? [] }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to list models', - }; - } - }); - - ipcMain.handle('llm:validate', async (_event, config: LLMConfig | null) => { - try { - const response = await llmApi.validate({ config }); - if (response.error) { - return { success: false, error: response.error.message }; - } - return { success: true, data: response.data }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to validate llm config', - }; - } - }); -} diff --git a/src/main/preload.cts b/src/main/preload.cts index b9b320f1..c6696f96 100644 --- a/src/main/preload.cts +++ b/src/main/preload.cts @@ -89,12 +89,6 @@ const electronApi = { getCredits: () => ipcRenderer.invoke('payment:get-credits'), }, - llm: { - listModels: () => ipcRenderer.invoke('llm:list-models'), - validate: (config: Record | null) => - ipcRenderer.invoke('llm:validate', config), - }, - appState: { get: () => ipcRenderer.invoke('app:get-state'), update: (updates: Record) => ipcRenderer.invoke('app:update-state', updates), diff --git a/src/main/services/suggestion-action.service.ts b/src/main/services/suggestion-action.service.ts index 182bdce5..e03520d1 100644 --- a/src/main/services/suggestion-action.service.ts +++ b/src/main/services/suggestion-action.service.ts @@ -212,7 +212,6 @@ export class ActionSuggestionService { const interviewConfig = appStateService.getState().interviewConfig; const payload: GenerateActionSuggestionRequest = { - config: conf.llmConf, profile_data: interviewConfig.profileData, context: interviewConfig.context, transcripts: transcripts.slice(-TRANSCRIPT_UPLOAD_LIMIT), diff --git a/src/main/services/suggestion-live.service.ts b/src/main/services/suggestion-live.service.ts index aa23f553..cb012f39 100644 --- a/src/main/services/suggestion-live.service.ts +++ b/src/main/services/suggestion-live.service.ts @@ -119,7 +119,6 @@ class LiveSuggestionService { try { const interviewConfig = appStateService.getState().interviewConfig; const requestBody: GenerateLiveSuggestionRequest = { - config: conf.llmConf, profile_data: interviewConfig.profileData, context: interviewConfig.context, transcripts: transcripts.slice(-TRANSCRIPT_UPLOAD_LIMIT), diff --git a/src/main/services/tools.service.ts b/src/main/services/tools.service.ts index 42d9ca67..471fb4ba 100644 --- a/src/main/services/tools.service.ts +++ b/src/main/services/tools.service.ts @@ -37,7 +37,6 @@ class ToolsService { // Call the API to generate the summary text const conf = configStore.getConfig(); const response = await this.llmApi.generateSummary({ - config: conf.llmConf, username, transcripts, // The exported report is written in the interview's language too. A Spanish interview diff --git a/src/main/store/config.store.ts b/src/main/store/config.store.ts index 10c2ccc6..f60255f9 100644 --- a/src/main/store/config.store.ts +++ b/src/main/store/config.store.ts @@ -7,7 +7,6 @@ import ElectronStore from 'electron-store'; import { OPACITY_DEFAULT } from '../consts.js'; import { DEFAULT_LANGUAGE, Language, resolveLanguage } from '../types/language.js'; -import { LLMConfig } from '../types/llm.js'; // Runtime configuration (matches Config type in frontend) export interface RuntimeConfig { @@ -19,8 +18,6 @@ export interface RuntimeConfig { password: string; audioInputDeviceName: string; - llmConf: LLMConfig | null; - // panel auto-scroll preferences autoScrollLiveSuggestions: boolean; autoScrollActionSuggestions: boolean; @@ -54,8 +51,6 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = { password: '', audioInputDeviceName: '', - llmConf: null, - // default autoscroll preferences are enabled autoScrollLiveSuggestions: true, autoScrollActionSuggestions: true, @@ -275,6 +270,18 @@ export const configStore = new ConfigStore(); } })(); // migration block +// One-time cleanup: `llmConf` backed the removed bring-your-own-API-key feature and could hold a +// real provider key in plaintext on disk. `RuntimeConfig` no longer declares it, but every read +// and write here spreads the raw stored object through - nothing strips a key TypeScript no +// longer knows about - so a leftover value would otherwise survive on an upgraded install forever. +(() => { + const raw = configStore.getStoredRuntime() as (StoredRuntime & { llmConf?: unknown }) | undefined; + if (raw && 'llmConf' in raw) { + delete raw.llmConf; + configStore.setStoredRuntime(raw); + } +})(); // llmConf scrub + // Read (but do not yet delete) any leftover local copy, so AccountService can migrate it // onto the account. Deleting here unconditionally would destroy the only copy whenever the // first launch after upgrade happens to be offline. diff --git a/src/main/types/llm.ts b/src/main/types/llm.ts index 8513e254..b8c82547 100644 --- a/src/main/types/llm.ts +++ b/src/main/types/llm.ts @@ -1,55 +1,7 @@ import { Transcript } from './app-state.js'; import { Language } from './language.js'; -export enum LLMProvider { - OPENAI = 'openai', - ANTHROPIC = 'anthropic', - GROQ = 'groq', - GOOGLE = 'google', -} - -export enum LLMModality { - TEXT_INPUT = 'text_input', - IMAGE_INPUT = 'image_input', - TEXT_OUTPUT = 'text_output', - IMAGE_OUTPUT = 'image_output', - AUDIO_INPUT = 'audio_input', - AUDIO_OUTPUT = 'audio_output', -} - -export interface LLMModelInfo { - id: string; - provider: LLMProvider; - name: string; - description: string; - modalities: LLMModality[]; - vision_capable: boolean; - context_window: number; - max_output_tokens: number; - pricing_input: number; - pricing_output: number; - supports_streaming: boolean; - supports_function_calling: boolean; - supports_json_mode: boolean; - release_date: string | null; -} - -export interface LLMConfig { - provider: LLMProvider; - apikey: string; - model: string; -} - -export interface LLMConfigValidationResult { - provider_ok: boolean; - apikey_ok: boolean; - model_ok: boolean; - error: string; -} - export interface LLMRequest { - config: LLMConfig | null; - /** * Interview language. Carried on the shared base so the three request kinds cannot drift, and * defaulted server-side, so omitting it against an older deployment still means English. diff --git a/src/renderer/components/custom/control-panel/index.tsx b/src/renderer/components/custom/control-panel/index.tsx index cea314b1..d28ae7ed 100644 --- a/src/renderer/components/custom/control-panel/index.tsx +++ b/src/renderer/components/custom/control-panel/index.tsx @@ -18,7 +18,6 @@ import PermissionGateDialog from '../permission-gate-dialog'; import ZoomControl from '../zoom-control'; import { AudioGroup } from './audio-group'; import { LanguageGroup } from './language-group'; -import { LLMGroup } from './llm-group'; import { MainGroup } from './main-group'; import { ProfessionalModeGroup } from './professional-mode-group'; import { ToolsGroup } from './tools-group'; @@ -203,11 +202,11 @@ export default function ControlPanel() {