Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
# Get your API key from: https://makersuite.google.com/app/apikey
GEMINI_API_KEY=your_gemini_api_key_here

# LLM Provider Configuration
# Choose which AI backend to use: gemini | openrouter
# Changing this requires an app restart (the provider is selected at startup).
LLM_PROVIDER=gemini
# OpenRouter API key — required only when LLM_PROVIDER=openrouter
# Get your key from: https://openrouter.ai/keys
OPENROUTER_API_KEY=your_openrouter_key_here
# Vision-capable model to use with OpenRouter (can also be set in Settings UI)
OPENROUTER_MODEL=anthropic/claude-sonnet-4

# Speech Recognition Configuration
# Choose one provider: azure or whisper
SPEECH_PROVIDER=whisper
Expand Down
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@
<i class="fas fa-circle-info"></i>
</div>
<div class="command-separator"></div>
<div class="command-item" id="quitButton" title="Quit OpenCluely">
<i class="fas fa-xmark"></i>
</div>
<div class="command-separator"></div>
<div class="status-dot non-interactive" id="statusDot"></div>
</div>

Expand Down
48 changes: 44 additions & 4 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -392,6 +393,7 @@ class ApplicationController {
const shortcuts = {
"CommandOrControl+Shift+S": () => this.triggerScreenshotOCR(),
"CommandOrControl+Shift+V": () => windowManager.toggleVisibility(),
"CommandOrControl+Shift+A": () => windowManager.toggleAssessmentMode(),
"CommandOrControl+Shift+I": () => windowManager.toggleInteraction(),
"CommandOrControl+Shift+C": () => windowManager.switchToWindow("chat"),
"CommandOrControl+Shift+\\": () => this.clearSessionMemory(),
Expand Down Expand Up @@ -1087,7 +1089,7 @@ class ApplicationController {
// Use image directly with LLM and active skill; do not send chat messages here
const sessionHistory = sessionManager.getOptimizedHistory();

const skillsRequiringProgrammingLanguage = ['dsa'];
const skillsRequiringProgrammingLanguage = ['dsa', 'code-explanation'];
const needsProgrammingLanguage = skillsRequiringProgrammingLanguage.includes(this.activeSkill);

this._responseSeq = (this._responseSeq || 0) + 1;
Expand Down Expand Up @@ -1153,7 +1155,7 @@ class ApplicationController {
sessionManager.addUserInput(text, 'llm_input');

// Check if current skill needs programming language context
const skillsRequiringProgrammingLanguage = ['dsa'];
const skillsRequiringProgrammingLanguage = ['dsa', 'code-explanation'];
const needsProgrammingLanguage = skillsRequiringProgrammingLanguage.includes(this.activeSkill);

this._responseSeq = (this._responseSeq || 0) + 1;
Expand Down Expand Up @@ -1322,7 +1324,7 @@ class ApplicationController {
});

// Check if current skill needs programming language context
const skillsRequiringProgrammingLanguage = ['dsa'];
const skillsRequiringProgrammingLanguage = ['dsa', 'code-explanation'];
const needsProgrammingLanguage = skillsRequiringProgrammingLanguage.includes(this.activeSkill);

// Stream the answer progressively to the configured speech target.
Expand Down Expand Up @@ -1608,6 +1610,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
};
Expand Down Expand Up @@ -1679,6 +1686,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) {
envUpdates.OPENROUTER_MODEL = String(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
Expand All @@ -1703,6 +1721,28 @@ class ApplicationController {
}
}

// If the OpenRouter key or model was saved and the current runtime service is
// OpenRouter, reinitialize its client so changes take effect immediately.
if (envUpdates.OPENROUTER_API_KEY !== undefined || envUpdates.OPENROUTER_MODEL !== undefined) {
try {
if (llmService.constructor && llmService.constructor.name === 'OpenRouterService') {
llmService.initializeClient();
logger.info("OpenRouter service reinitialized after settings update");
}
} catch (e) {
logger.warn("Failed to reinitialize OpenRouter service after settings update", { error: e.message });
}
}

// 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
Expand Down
Loading