-
Notifications
You must be signed in to change notification settings - Fork 211
feat: add OpenRouter as a selectable LLM provider (zero new npm deps) #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
1e593d4
bd1fa3a
816100c
9f7de69
adad520
08a7ba0
4979b14
8ce534d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,7 +102,8 @@ process.on("unhandledRejection", (reason) => { | |
| // Screen capture (image-based) | ||
| const captureService = require("./src/services/capture.service"); | ||
| const speechService = require("./src/services/speech.service"); | ||
| const llmService = require("./src/services/llm.service"); | ||
| // llm.factory selects openrouter.service or llm.service based on LLM_PROVIDER env var. | ||
| const llmService = require("./src/services/llm.factory"); | ||
|
|
||
| // Managers | ||
| const windowManager = require("./src/managers/window.manager"); | ||
|
|
@@ -1608,6 +1609,11 @@ class ApplicationController { | |
| whisperSegmentMs: process.env.WHISPER_SEGMENT_MS || "4000", | ||
| geminiKey: process.env.GEMINI_API_KEY || "", | ||
|
|
||
| // OpenRouter provider fields | ||
| llmProvider: process.env.LLM_PROVIDER || "gemini", | ||
| openrouterKey: process.env.OPENROUTER_API_KEY || "", | ||
| openrouterModel: process.env.OPENROUTER_MODEL || "anthropic/claude-sonnet-4", | ||
|
|
||
| azureConfigured: !!process.env.AZURE_SPEECH_KEY && !!process.env.AZURE_SPEECH_REGION, | ||
| speechAvailable: this.speechAvailable | ||
| }; | ||
|
|
@@ -1679,6 +1685,17 @@ class ApplicationController { | |
| envUpdates.GEMINI_API_KEY = settings.geminiKey; | ||
| } | ||
|
|
||
| // OpenRouter provider settings | ||
| if (settings.llmProvider === "openrouter" || settings.llmProvider === "gemini") { | ||
| envUpdates.LLM_PROVIDER = settings.llmProvider; | ||
| } | ||
| if (settings.openrouterKey !== undefined) { | ||
| envUpdates.OPENROUTER_API_KEY = settings.openrouterKey; | ||
| } | ||
| if (settings.openrouterModel !== undefined && settings.openrouterModel.trim()) { | ||
| envUpdates.OPENROUTER_MODEL = settings.openrouterModel.trim(); | ||
| } | ||
|
|
||
| // Capture the previous whisper command BEFORE persisting — persistEnvUpdates | ||
| // mutates process.env in place, so comparing afterwards would always read | ||
| // equal and skip the speech re-init below (the exact stale-mic-after-install | ||
|
|
@@ -1703,6 +1720,29 @@ class ApplicationController { | |
| } | ||
| } | ||
|
|
||
| // If the OpenRouter key was saved and the current runtime service is | ||
| // OpenRouter, reinitialize its client so the key is picked up immediately. | ||
| if (settings.openrouterKey !== undefined && envUpdates.OPENROUTER_API_KEY !== undefined) { | ||
| try { | ||
| if (typeof llmService.updateApiKey === 'function' && | ||
| llmService.constructor && llmService.constructor.name === 'OpenRouterService') { | ||
| llmService.updateApiKey(settings.openrouterKey); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When OpenRouter is already active and the user changes only the model, Useful? React with 👍 / 👎. |
||
| logger.info("OpenRouter service reinitialized after key update"); | ||
| } | ||
| } catch (e) { | ||
| logger.warn("Failed to reinitialize OpenRouter service after key update", { error: e.message }); | ||
| } | ||
| } | ||
|
ShlokNaidu marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Notify UI about provider change (restart still required for factory to switch) | ||
| if (settings.llmProvider !== undefined) { | ||
| windowManager.broadcastToAllWindows("llm-provider-changed", { | ||
| provider: settings.llmProvider, | ||
| requiresRestart: true | ||
| }); | ||
| logger.info("LLM provider setting updated; restart required for factory to reload", { provider: settings.llmProvider }); | ||
| } | ||
|
|
||
| // Reinitialize speech service when provider OR whisper command | ||
| // changes. Without the second check, the install flow (which | ||
| // writes a new whisperCommand after install but keeps the same | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,12 @@ class FirstRunManager { | |
| if (!fs.existsSync(this.sentinelPath)) return true; | ||
| if (!fs.existsSync(this.envPath)) return true; | ||
| const content = this._readEnv(); | ||
| const provider = (content.LLM_PROVIDER || 'gemini').trim(); | ||
| if (provider === 'openrouter') { | ||
| // When OpenRouter is selected, check its key instead of Gemini's | ||
| const orKey = (content.OPENROUTER_API_KEY || '').trim(); | ||
| return !orKey || orKey === 'your_openrouter_key_here'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
| const gemini = (content.GEMINI_API_KEY || '').trim(); | ||
| return !gemini || gemini === 'your_gemini_api_key_here'; | ||
| } | ||
|
|
@@ -80,10 +86,13 @@ class FirstRunManager { | |
| getStatus() { | ||
| const env = this._readEnv(); | ||
| const gemini = (env.GEMINI_API_KEY || '').trim(); | ||
| const openrouter = (env.OPENROUTER_API_KEY || '').trim(); | ||
| return { | ||
| envExists: fs.existsSync(this.envPath), | ||
| sentinelExists: fs.existsSync(this.sentinelPath), | ||
| geminiConfigured: !!gemini && gemini !== 'your_gemini_api_key_here', | ||
| openrouterConfigured: !!openrouter && openrouter !== 'your_openrouter_key_here', | ||
| llmProvider: (env.LLM_PROVIDER || 'gemini').trim(), | ||
| azureConfigured: !!(env.AZURE_SPEECH_KEY || '').trim() && !!(env.AZURE_SPEECH_REGION || '').trim(), | ||
| whisperConfigured: !!(env.WHISPER_COMMAND || '').trim(), | ||
| needsOnboarding: this.needsOnboarding() | ||
|
|
@@ -144,6 +153,11 @@ class FirstRunManager { | |
| '', | ||
| 'GEMINI_API_KEY=your_gemini_api_key_here', | ||
| '', | ||
| '# LLM Provider: gemini (default) | openrouter (restart required to switch)', | ||
| '# LLM_PROVIDER=gemini', | ||
| '# OPENROUTER_API_KEY=your_openrouter_key_here', | ||
| '# OPENROUTER_MODEL=anthropic/claude-sonnet-4', | ||
| '', | ||
| '# Speech provider: "whisper" (local) or "azure" (cloud).', | ||
| '# WHISPER_COMMAND is auto-set to the project-local venv when you', | ||
| '# install Whisper through the onboarding wizard, so no PATH change', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * LLM Provider Factory | ||
| * | ||
| * Selects the active LLM service based on config.get('llm.provider'). | ||
| * The value is read once at require() time from config, which itself reads | ||
| * the LLM_PROVIDER environment variable (set in .env before app start). | ||
| * | ||
| * Changing the provider via the settings UI updates .env and process.env, | ||
| * but takes effect only on the NEXT app restart because Node caches modules. | ||
| * | ||
| * Default: 'gemini' (backward-compatible). | ||
| */ | ||
| 'use strict'; | ||
|
|
||
| const config = require('../core/config'); | ||
| const provider = config.get('llm.provider') || 'gemini'; | ||
|
|
||
| if (provider === 'openrouter') { | ||
| module.exports = require('./openrouter.service'); | ||
| } else { | ||
| module.exports = require('./llm.service'); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.