-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add OpenRouter as alternative LLM provider #7
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ __pycache__/ | |
| .env | ||
| .env.* | ||
| secrets.env | ||
| .venv/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -220,6 +220,23 @@ def init_ui(self) -> None: | |
|
|
||
| self._refresh_api_key_status() | ||
|
|
||
| self.combo_llm_provider = QComboBox() | ||
| self.combo_llm_provider.addItem("OpenAI", "openai") | ||
| self.combo_llm_provider.addItem("OpenRouter", "openrouter") | ||
| self.combo_llm_provider.addItem("Eigener Endpunkt", "custom") | ||
| provider_index = self.combo_llm_provider.findData(self.config.llm_provider) | ||
| self.combo_llm_provider.setCurrentIndex(provider_index if provider_index >= 0 else 0) | ||
| self.combo_llm_provider.currentIndexChanged.connect(lambda *_: self._on_llm_provider_changed()) | ||
|
|
||
| self.edit_base_url = QLineEdit() | ||
| self.edit_base_url.setText(self.config.llm_base_url) | ||
| self.edit_base_url.setPlaceholderText("https://openrouter.ai/api/v1") | ||
| self.edit_base_url.setEnabled(self.config.llm_provider != "openai") | ||
|
|
||
| self.edit_llm_model = QLineEdit() | ||
| self.edit_llm_model.setText(self.config.llm_model) | ||
| self.edit_llm_model.setPlaceholderText("gpt-4o-mini") | ||
|
|
||
| self.combo_tone = QComboBox() | ||
| self.combo_tone.addItems(["formal", "neutral", "locker"]) | ||
| self.combo_tone.setCurrentText(self.config.text_improver_tone) | ||
|
|
@@ -260,8 +277,15 @@ def init_ui(self) -> None: | |
| custom_terms_widget = QWidget() | ||
| custom_terms_widget.setLayout(custom_terms_layout) | ||
|
|
||
| form_llm.addRow("OpenAI API-Key-Umgebung:", api_key_layout) | ||
| form_llm.addRow("", create_help_label("Nur der Name der Umgebungsvariable wird gespeichert. Der Schlüssel selbst wird aus os.environ gelesen.")) | ||
| form_llm.addRow("API-Key-Umgebung:", api_key_layout) | ||
| form_llm.addRow("", create_help_label("Nur der Name der Umgebungsvariable wird gespeichert. Der Schlüssel selbst wird aus os.environ gelesen (secrets.env). Für OpenRouter z. B. OPENROUTER_API_KEY.")) | ||
|
|
||
| form_llm.addRow("LLM-Anbieter:", self.combo_llm_provider) | ||
| form_llm.addRow("", create_help_label("OpenAI = Standard. OpenRouter und 'Eigener Endpunkt' nutzen das OpenAI-kompatible API über eine eigene Basis-URL und ein eigenes Modell.")) | ||
| form_llm.addRow("Basis-URL (base_url):", self.edit_base_url) | ||
| form_llm.addRow("", create_help_label("Leer = OpenAI-Standard. Für OpenRouter: https://openrouter.ai/api/v1. Muss mit http:// oder https:// beginnen.")) | ||
| form_llm.addRow("LLM-Modell:", self.edit_llm_model) | ||
| form_llm.addRow("", create_help_label("Modellname beim Anbieter, z. B. 'gpt-4o-mini' (OpenAI) oder 'openai/gpt-4o' (OpenRouter).")) | ||
|
|
||
| form_llm.addRow("Text-Verbesserer Tonfall:", self.combo_tone) | ||
| form_llm.addRow("Schreibstil-Vorlage:", self.combo_writing_preset) | ||
|
|
@@ -326,6 +350,18 @@ def _refresh_api_key_status(self) -> None: | |
| status = "gesetzt" if env_value else "nicht gesetzt" | ||
| self.lbl_api_key_status.setText(f"Status: {status} ({env_name})") | ||
|
|
||
| def _on_llm_provider_changed(self) -> None: | ||
| provider = self.combo_llm_provider.currentData() | ||
| if provider == "openrouter": | ||
| if not self.edit_base_url.text().strip(): | ||
| self.edit_base_url.setText("https://openrouter.ai/api/v1") | ||
| self.edit_base_url.setEnabled(True) | ||
| elif provider == "openai": | ||
| self.edit_base_url.setText("") | ||
| self.edit_base_url.setEnabled(False) | ||
| else: # custom / eigener Endpunkt | ||
| self.edit_base_url.setEnabled(True) | ||
|
|
||
| def _open_config_file(self) -> None: | ||
| """Open the config.json in the desktop's default editor. | ||
|
|
||
|
|
@@ -390,6 +426,9 @@ def save_settings(self) -> None: | |
| self.config.transcription_hotkey = self.combo_transcription_key.currentText() | ||
|
|
||
| self.config.openai_api_key_env = self.edit_api_key_env.text().strip() | ||
| self.config.llm_provider = self.combo_llm_provider.currentData() | ||
| self.config.llm_base_url = self.edit_base_url.text().strip() | ||
| self.config.llm_model = self.edit_llm_model.text().strip() | ||
| self.config.text_improver_tone = self.combo_tone.currentText() | ||
| self.config.writing_preset = self.combo_writing_preset.currentData() | ||
| self.config.emoji_density = self.combo_emoji.currentText() | ||
|
|
@@ -499,15 +538,7 @@ def __init__(self, app: QApplication) -> None: | |
| self.app = app | ||
| self.config = Config.load() | ||
|
|
||
| self.llm_service = LLMService( | ||
| api_key=self.config.resolve_openai_api_key(), | ||
| tone=self.config.text_improver_tone, | ||
| emoji_density=self.config.emoji_density, | ||
| dampf_system_prompt=self.config.dampf_system_prompt, | ||
| custom_terms=self.config.custom_terms, | ||
| api_key_env=self.config.openai_api_key_env, | ||
| writing_preset=self.config.writing_preset, | ||
| ) | ||
| self.llm_service = self._build_llm_service() | ||
| self.audio_recorder = AudioRecorder() | ||
| self.paste_service = PasteService(autopaste=self.config.autopaste) | ||
|
|
||
|
|
@@ -531,6 +562,27 @@ def __init__(self, app: QApplication) -> None: | |
| self.hotkey_thread: Optional[QThread] = None | ||
| self.start_hotkey_worker() | ||
|
|
||
| def _build_llm_service(self) -> LLMService: | ||
| """Baut den LLMService aus der aktuellen Config. | ||
|
|
||
| Einziger Konstruktionsort, damit Init und Settings-Save nicht | ||
| auseinanderlaufen (z. B. base_url/model vergessen). Der Provider ist | ||
| autoritativ: bei "openai" wird eine evtl. gespeicherte base_url ignoriert, | ||
| damit der OpenAI-Standardendpunkt genutzt wird (OpenRouter nur bei Auswahl). | ||
| """ | ||
| base_url = "" if self.config.llm_provider == "openai" else self.config.llm_base_url | ||
|
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.
For Useful? React with 👍 / 👎. |
||
| return LLMService( | ||
| api_key=self.config.resolve_openai_api_key(), | ||
| tone=self.config.text_improver_tone, | ||
| emoji_density=self.config.emoji_density, | ||
| dampf_system_prompt=self.config.dampf_system_prompt, | ||
| custom_terms=self.config.custom_terms, | ||
| api_key_env=self.config.openai_api_key_env, | ||
| writing_preset=self.config.writing_preset, | ||
| base_url=base_url, | ||
| model=self.config.llm_model, | ||
| ) | ||
|
|
||
| def setup_tray(self) -> None: | ||
| self.tray_icon = QSystemTrayIcon(self) | ||
| self._tray_icons = { | ||
|
|
@@ -675,15 +727,7 @@ def show_settings_dialog(self) -> None: | |
| dialog = SettingsDialog(self.config) | ||
| if dialog.exec() == QDialog.DialogCode.Accepted: | ||
| # Update LLM Service parameters from saved configuration | ||
| self.llm_service = LLMService( | ||
| api_key=self.config.resolve_openai_api_key(), | ||
| tone=self.config.text_improver_tone, | ||
| emoji_density=self.config.emoji_density, | ||
| dampf_system_prompt=self.config.dampf_system_prompt, | ||
| custom_terms=self.config.custom_terms, | ||
| api_key_env=self.config.openai_api_key_env, | ||
| writing_preset=self.config.writing_preset, | ||
| ) | ||
| self.llm_service = self._build_llm_service() | ||
| self.update_menu_availability() | ||
|
|
||
| # Restart hotkey listener if mode or key changed | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user with an existing
OPENAI_API_KEYswitches to OpenRouter, this branch only fillsbase_url;edit_api_key_envremains at the default unless the user notices and changes it manually._build_llm_service()then resolves that OpenAI token and the SDK sends it as the Authorization header toopenrouter.ai, so the common provider-switch path both fails and discloses the user's OpenAI key to the new provider. Please either switch/clear the env field for OpenRouter or block saving until a provider-appropriate key env is selected.Useful? React with 👍 / 👎.