mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-12 03:16:19 +00:00
Merge master into the OpenCode Go branch
Master grew Google AI Studio and Antigravity as providers, a doctor that names each provider's own key, and settings that fetch every hosted model list as the window opens. OpenCode Go is folded into each: its name joins SERVICES and the doctor's key table, its key row sits beside Google's, its Fetch button follows the per-provider pattern Google's uses, and _load_hosted_models fetches its catalog at open when a key is on file, filling the cleanup and agent boxes alike.
This commit is contained in:
+283
-94
@@ -60,13 +60,24 @@ CLEANUP_MODELS = [
|
||||
"google/gemini-2.5-flash-lite", "anthropic/claude-haiku-4.5",
|
||||
"openai/gpt-5-mini", "meta-llama/llama-3.3-70b-instruct",
|
||||
]
|
||||
# In the order they answer in. A request to OpenRouter is over in a second, a
|
||||
# model here takes a little longer and costs nothing, and the two CLIs the agent
|
||||
# can run on open a whole session to do the smaller job.
|
||||
GEMINI_MODELS = [
|
||||
"gemini-3.5-flash-lite", "gemini-3.1-flash-lite",
|
||||
"gemini-2.5-flash-lite", "gemini-3.5-flash", "gemini-2.5-flash",
|
||||
]
|
||||
# agy's model ids carry the reasoning effort in their suffix, which is why one
|
||||
# model appears here at more than one level. The same list seeds two boxes:
|
||||
# cleanup, which wants the bottom rung, and the agent, which sometimes does not.
|
||||
AGY_MODELS = [
|
||||
"gemini-3.7-flash-low", "gemini-3.7-flash-medium", "gemini-3.7-flash-high",
|
||||
"gemini-3.5-flash-low", "gemini-3.1-pro-low",
|
||||
]
|
||||
# In the order they answer in. The hosted requests are over in a second, a
|
||||
# model here takes a little longer and costs nothing, and the three CLIs the
|
||||
# agent can run on open a whole session to do the smaller job.
|
||||
CLEANUP_PROVIDERS = [
|
||||
("OpenRouter", "openrouter"), ("OpenCode Go", "opencode"),
|
||||
("This machine (llama.cpp)", "local"),
|
||||
("Claude Code", "claude"), ("Codex", "codex"),
|
||||
("OpenRouter", "openrouter"), ("Google AI Studio", "gemini"),
|
||||
("OpenCode Go", "opencode"), ("This machine (llama.cpp)", "local"),
|
||||
("Claude Code", "claude"), ("Codex", "codex"), ("Antigravity", "agy"),
|
||||
]
|
||||
# Cleaning up a sentence is the lightest thing either of them will ever be
|
||||
# asked, so the small model comes first.
|
||||
@@ -78,7 +89,7 @@ MEETING_MODELS = [
|
||||
"anthropic/claude-sonnet-5", "openai/gpt-5.4", "x-ai/grok-4.5",
|
||||
]
|
||||
ASSISTANT_PROVIDERS = [
|
||||
("Claude Code", "claude"), ("Codex", "codex"),
|
||||
("Claude Code", "claude"), ("Codex", "codex"), ("Antigravity", "agy"),
|
||||
("OpenRouter", "openrouter"), ("OpenCode Go", "opencode"),
|
||||
]
|
||||
# Aliases resolve to the newest model of that name, so they age better than an
|
||||
@@ -566,10 +577,14 @@ class SettingsWindow(QDialog):
|
||||
self._sources = audio.list_sources()
|
||||
return self._sources
|
||||
|
||||
_models_loaded = pyqtSignal(list, str, str)
|
||||
_models_loaded = pyqtSignal(list, str)
|
||||
_gemini_models_loaded = pyqtSignal(list, str)
|
||||
_opencode_models_loaded = pyqtSignal(list, str)
|
||||
_transcribe_models_loaded = pyqtSignal(list, str)
|
||||
_codex_models_loaded = pyqtSignal(list)
|
||||
_opencode_models_loaded = pyqtSignal(list)
|
||||
_agy_models_loaded = pyqtSignal(list)
|
||||
# Which hosted provider's list arrived on its own at open, and the list.
|
||||
_hosted_models_loaded = pyqtSignal(str, list)
|
||||
# Which key was tested, whether it worked, and what to write under it.
|
||||
_test_done = pyqtSignal(str, bool, str)
|
||||
# The release that was found, or None, and what went wrong instead.
|
||||
@@ -604,6 +619,7 @@ class SettingsWindow(QDialog):
|
||||
|
||||
tabs = self.tabs = QTabWidget(self)
|
||||
tabs.addTab(self._scrolled(self._general_tab()), t("General"))
|
||||
tabs.addTab(self._scrolled(self._display_tab()), t("Display"))
|
||||
self.api_tab_index = tabs.addTab(
|
||||
self._scrolled(self._api_tab()), t("API and models"))
|
||||
tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules"))
|
||||
@@ -627,9 +643,12 @@ class SettingsWindow(QDialog):
|
||||
self._size_to_screen(680, 640)
|
||||
|
||||
self._models_loaded.connect(self._on_models_loaded)
|
||||
self._gemini_models_loaded.connect(self._on_gemini_models_loaded)
|
||||
self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded)
|
||||
self._codex_models_loaded.connect(self._on_codex_models_loaded)
|
||||
self._opencode_models_loaded.connect(self._on_opencode_models_loaded)
|
||||
self._agy_models_loaded.connect(self._on_agy_models_loaded)
|
||||
self._hosted_models_loaded.connect(self._on_hosted_models_loaded)
|
||||
self._test_done.connect(self._on_test_done)
|
||||
self._update_checked.connect(self._on_update_checked)
|
||||
self.transcriber.progress.connect(self._on_file_progress)
|
||||
@@ -641,7 +660,8 @@ class SettingsWindow(QDialog):
|
||||
self.meetings.failed.connect(self._on_minutes_failed)
|
||||
self._load()
|
||||
self._load_codex_models()
|
||||
self._load_opencode_models()
|
||||
self._load_agy_models()
|
||||
self._load_hosted_models()
|
||||
# Connected after the load, so that filling the boxes in is not taken
|
||||
# for the user ticking them.
|
||||
self.file_timestamps.toggled.connect(self._remember_file_choices)
|
||||
@@ -726,11 +746,6 @@ class SettingsWindow(QDialog):
|
||||
self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting"))
|
||||
form.addRow("", self.restore_clipboard)
|
||||
|
||||
self.corner = QComboBox()
|
||||
for value in CORNERS:
|
||||
self.corner.addItem(t(value), value)
|
||||
form.addRow(t("Indicator corner"), self.corner)
|
||||
|
||||
self.max_seconds = QSpinBox()
|
||||
self.max_seconds.setRange(10, 3600)
|
||||
self.max_seconds.setSuffix(t(" s"))
|
||||
@@ -781,6 +796,31 @@ class SettingsWindow(QDialog):
|
||||
self.update_now))
|
||||
return page
|
||||
|
||||
def _display_tab(self):
|
||||
page = QWidget()
|
||||
form = QFormLayout(page)
|
||||
|
||||
self.indicator_screen = QComboBox()
|
||||
self.indicator_screen.addItem(t("Follow the mouse pointer"), "")
|
||||
for screen in QGuiApplication.screens():
|
||||
# The native resolution, so that a scaled 4K screen reads
|
||||
# 3840 × 2160 and not the 1920 × 1080 Qt sees through the scale.
|
||||
area = screen.geometry()
|
||||
ratio = screen.devicePixelRatio()
|
||||
self.indicator_screen.addItem(
|
||||
t("{name} ({width} × {height})", name=screen.name(),
|
||||
width=round(area.width() * ratio),
|
||||
height=round(area.height() * ratio)),
|
||||
screen.name(),
|
||||
)
|
||||
form.addRow(t("Indicator screen"), self.indicator_screen)
|
||||
|
||||
self.corner = QComboBox()
|
||||
for value in CORNERS:
|
||||
self.corner.addItem(t(value), value)
|
||||
form.addRow(t("Indicator corner"), self.corner)
|
||||
return page
|
||||
|
||||
def _api_tab(self):
|
||||
page = QWidget()
|
||||
outer = QVBoxLayout(page)
|
||||
@@ -798,6 +838,9 @@ class SettingsWindow(QDialog):
|
||||
self.openrouter_key = self._key_row(
|
||||
keys_form, "openrouter", t("sk-or-… (falls back to OPENROUTER_API_KEY)"),
|
||||
self._test_openrouter)
|
||||
self.gemini_key = self._key_row(
|
||||
keys_form, "gemini", t("(falls back to GEMINI_API_KEY)"),
|
||||
self._test_gemini, service="Google AI Studio")
|
||||
self.opencode_key = self._key_row(
|
||||
keys_form, "opencode", t("(falls back to OPENCODE_API_KEY)"),
|
||||
self._test_opencode, service="OpenCode Go")
|
||||
@@ -873,11 +916,11 @@ class SettingsWindow(QDialog):
|
||||
for label, value in CLEANUP_PROVIDERS:
|
||||
self.cleanup_provider.addItem(t(label), value)
|
||||
self.cleanup_provider.setToolTip(t(
|
||||
"OpenRouter is the quickest; OpenCode Go needs nothing installed "
|
||||
"either. llama.cpp runs here, on a model downloaded below. Claude "
|
||||
"Code and Codex clean up on the subscription you already have, "
|
||||
"without a second key, and take a few seconds longer because each "
|
||||
"one opens a session to do it."
|
||||
"OpenRouter, Google AI Studio and OpenCode Go are the quick ones "
|
||||
"that need nothing installed. llama.cpp runs here, on a model "
|
||||
"downloaded below. Claude Code, Codex and Antigravity clean up on "
|
||||
"a subscription you already have, without a second key, and take a "
|
||||
"few seconds longer because each opens a session to do it."
|
||||
))
|
||||
self.cleanup_provider.currentIndexChanged.connect(self._cleanup_provider_changed)
|
||||
orr_form.addRow(t("Runs on"), self.cleanup_provider)
|
||||
@@ -890,6 +933,15 @@ class SettingsWindow(QDialog):
|
||||
self.cleanup_model_row = self._row(self.cleanup_model, self.refresh_models)
|
||||
orr_form.addRow(t("Model"), self.cleanup_model_row)
|
||||
|
||||
self.cleanup_gemini_model = QComboBox()
|
||||
self.cleanup_gemini_model.setEditable(True)
|
||||
self.cleanup_gemini_model.addItems(GEMINI_MODELS)
|
||||
self.refresh_gemini_models = QPushButton(t("Fetch model list"))
|
||||
self.refresh_gemini_models.clicked.connect(self._load_gemini_models)
|
||||
self.cleanup_gemini_model_row = self._row(
|
||||
self.cleanup_gemini_model, self.refresh_gemini_models)
|
||||
orr_form.addRow(t("Model"), self.cleanup_gemini_model_row)
|
||||
|
||||
# One row per provider rather than one box that means a different thing
|
||||
# in each: an OpenRouter id and a Claude alias do not belong in the same
|
||||
# field, and only the row of whoever is chosen is on screen.
|
||||
@@ -909,14 +961,17 @@ class SettingsWindow(QDialog):
|
||||
self.cleanup_opencode_model.setEditable(True)
|
||||
self.cleanup_opencode_model.addItems(OPENCODE_MODELS)
|
||||
self.cleanup_opencode_model.setToolTip(_typed_model_note("OpenCode Go"))
|
||||
# Its own button, because a widget lives in one row and the OpenRouter
|
||||
# button is hidden along with its box whenever OpenCode Go is chosen.
|
||||
self.refresh_opencode_models = QPushButton(t("Fetch model list"))
|
||||
self.refresh_opencode_models.clicked.connect(self._load_models)
|
||||
self.refresh_opencode_models.clicked.connect(self._load_opencode_models)
|
||||
self.cleanup_opencode_model_row = self._row(self.cleanup_opencode_model,
|
||||
self.refresh_opencode_models)
|
||||
orr_form.addRow(t("Model"), self.cleanup_opencode_model_row)
|
||||
|
||||
self.cleanup_agy_model = QComboBox()
|
||||
self.cleanup_agy_model.setEditable(True)
|
||||
self.cleanup_agy_model.addItems([t("Antigravity's own default")] + AGY_MODELS)
|
||||
orr_form.addRow(t("Model"), self.cleanup_agy_model)
|
||||
|
||||
self.cleanup_reasoning = QComboBox()
|
||||
for label, value in REASONING_LEVELS:
|
||||
self.cleanup_reasoning.addItem(t(label), value)
|
||||
@@ -1000,9 +1055,9 @@ class SettingsWindow(QDialog):
|
||||
"This shortcut records the same way dictation does, but the "
|
||||
"transcript is not what gets pasted. It goes to an agent as a "
|
||||
"command, and what comes back is pasted instead: the answer to a "
|
||||
"question, or a sentence saying what was done. Claude Code and "
|
||||
"Codex run as the session you would have opened yourself, with your "
|
||||
"skills, your connected services and your account."
|
||||
"question, or a sentence saying what was done. Claude Code, Codex "
|
||||
"and Antigravity run as the session you would have opened yourself, "
|
||||
"with your skills, your connected services and your account."
|
||||
))
|
||||
intro.setWordWrap(True)
|
||||
layout.addWidget(intro)
|
||||
@@ -1040,7 +1095,7 @@ class SettingsWindow(QDialog):
|
||||
dir_note.setWordWrap(True)
|
||||
how_form.addRow(dir_note)
|
||||
|
||||
# One scale for all three: how hard to think is one thing to want, and
|
||||
# One scale for all four: how hard to think is one thing to want, and
|
||||
# each provider is handed the nearest rung it actually has.
|
||||
self.assistant_reasoning = QComboBox()
|
||||
for label, value in REASONING_LEVELS:
|
||||
@@ -1063,7 +1118,7 @@ class SettingsWindow(QDialog):
|
||||
layout.addWidget(how)
|
||||
|
||||
# One box per provider, only the chosen one on screen: they have nothing
|
||||
# in common past the model, and three sets of half-relevant fields would
|
||||
# in common past the model, and four sets of half-relevant fields would
|
||||
# be worse than none.
|
||||
self.claude_box = QGroupBox(t("Claude Code"))
|
||||
claude_form = QFormLayout(self.claude_box)
|
||||
@@ -1114,6 +1169,24 @@ class SettingsWindow(QDialog):
|
||||
or_form.addRow(or_note)
|
||||
layout.addWidget(self.openrouter_box)
|
||||
|
||||
self.agy_box = QGroupBox(t("Antigravity"))
|
||||
agy_form = QFormLayout(self.agy_box)
|
||||
self.assistant_agy_model = QComboBox()
|
||||
self.assistant_agy_model.setEditable(True)
|
||||
self.assistant_agy_model.addItem(t("Antigravity's own default"), "")
|
||||
for name in AGY_MODELS:
|
||||
self.assistant_agy_model.addItem(name, name)
|
||||
agy_form.addRow(t("Model"), self.assistant_agy_model)
|
||||
agy_note = QLabel(t(
|
||||
"Antigravity has neither a permission mode nor a sandbox to hand it, "
|
||||
"so what it may do without asking is whatever its own allow-rules "
|
||||
"say. The Permissions and Sandbox boxes above belong to the other "
|
||||
"two; the working directory still applies."
|
||||
))
|
||||
agy_note.setWordWrap(True)
|
||||
agy_form.addRow(agy_note)
|
||||
layout.addWidget(self.agy_box)
|
||||
|
||||
self.opencode_box = QGroupBox("OpenCode Go")
|
||||
og_form = QFormLayout(self.opencode_box)
|
||||
self.assistant_opencode_model = QComboBox()
|
||||
@@ -1616,8 +1689,8 @@ class SettingsWindow(QDialog):
|
||||
button.clicked.connect(tester)
|
||||
answer = QLabel("")
|
||||
answer.setWordWrap(True)
|
||||
form.addRow(service or cfg.TRANSCRIBERS[provider].service,
|
||||
self._row(field, button))
|
||||
label = service or cfg.TRANSCRIBERS[provider].service
|
||||
form.addRow(label, self._row(field, button))
|
||||
form.addRow("", answer)
|
||||
self._key_fields[provider] = field
|
||||
self._testers[provider] = (button, answer)
|
||||
@@ -1679,6 +1752,10 @@ class SettingsWindow(QDialog):
|
||||
self.auto_paste.setChecked(conf["auto_paste"])
|
||||
self.paste_shortcut.setCurrentText(conf["paste_shortcut"])
|
||||
self.restore_clipboard.setChecked(conf["restore_clipboard"])
|
||||
screen_name = conf["overlay_screen"]
|
||||
if screen_name and self.indicator_screen.findData(screen_name) < 0:
|
||||
self.indicator_screen.addItem(t("{name} (not connected)", name=screen_name), screen_name)
|
||||
self._select_data(self.indicator_screen, screen_name)
|
||||
self._select_data(self.corner, conf["overlay_corner"])
|
||||
self.max_seconds.setValue(conf["max_seconds"])
|
||||
self.skip_silent.setChecked(conf["skip_silent"])
|
||||
@@ -1691,7 +1768,8 @@ class SettingsWindow(QDialog):
|
||||
for name, who in cfg.TRANSCRIBERS.items():
|
||||
self._key_fields[name].setText(conf[who.key])
|
||||
self._models[name] = conf[who.model]
|
||||
self._key_fields["opencode"].setText(conf["opencode_api_key"])
|
||||
self.gemini_key.setText(conf["gemini_api_key"])
|
||||
self.opencode_key.setText(conf["opencode_api_key"])
|
||||
self._shown_provider = ""
|
||||
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||
self._provider_changed() # selecting index 0 fires no signal
|
||||
@@ -1702,10 +1780,16 @@ class SettingsWindow(QDialog):
|
||||
|
||||
self.cleanup_enabled.setChecked(conf["cleanup_enabled"])
|
||||
self.cleanup_model.setCurrentText(conf["cleanup_model"])
|
||||
self.cleanup_gemini_model.setCurrentText(
|
||||
conf["cleanup_gemini_model"] or cfg.DEFAULTS["cleanup_gemini_model"]
|
||||
)
|
||||
self.cleanup_claude_model.setCurrentText(conf["cleanup_claude_model"])
|
||||
self.cleanup_codex_model.setCurrentText(
|
||||
conf["cleanup_codex_model"] or t("Codex's own default")
|
||||
)
|
||||
self.cleanup_agy_model.setCurrentText(
|
||||
conf["cleanup_agy_model"] or t("Antigravity's own default")
|
||||
)
|
||||
self.cleanup_opencode_model.setCurrentText(conf["cleanup_opencode_model"])
|
||||
self._select_data(self.cleanup_provider, conf["cleanup_provider"])
|
||||
self._cleanup_provider_changed() # selecting index 0 fires no signal
|
||||
@@ -1737,6 +1821,7 @@ class SettingsWindow(QDialog):
|
||||
self.assistant_codex_model.setCurrentText(conf["assistant_codex_model"])
|
||||
self._select_data(self.assistant_codex_sandbox, conf["assistant_codex_sandbox"])
|
||||
self.assistant_openrouter_model.setCurrentText(conf["assistant_openrouter_model"])
|
||||
self.assistant_agy_model.setCurrentText(conf["assistant_agy_model"])
|
||||
self.assistant_opencode_model.setCurrentText(conf["assistant_opencode_model"])
|
||||
self._assistant_provider_changed() # selecting index 0 fires no signal
|
||||
self._select_data(self.assistant_reasoning, conf["assistant_reasoning"])
|
||||
@@ -1789,6 +1874,7 @@ class SettingsWindow(QDialog):
|
||||
conf["auto_paste"] = self.auto_paste.isChecked()
|
||||
conf["paste_shortcut"] = self.paste_shortcut.currentText().strip()
|
||||
conf["restore_clipboard"] = self.restore_clipboard.isChecked()
|
||||
conf["overlay_screen"] = self.indicator_screen.currentData() or ""
|
||||
conf["overlay_corner"] = self.corner.currentData() or "bottom-left"
|
||||
conf["max_seconds"] = self.max_seconds.value()
|
||||
conf["skip_silent"] = self.skip_silent.isChecked()
|
||||
@@ -1804,7 +1890,8 @@ class SettingsWindow(QDialog):
|
||||
for name, who in cfg.TRANSCRIBERS.items():
|
||||
conf[who.key] = self._key_fields[name].text().strip()
|
||||
conf[who.model] = self._models[name].strip() or cfg.DEFAULTS[who.model]
|
||||
conf["opencode_api_key"] = self._key_fields["opencode"].text().strip()
|
||||
conf["gemini_api_key"] = self.gemini_key.text().strip()
|
||||
conf["opencode_api_key"] = self.opencode_key.text().strip()
|
||||
conf["local_model"] = self.local_whisper.selected()
|
||||
conf["local_gpu"] = self.local_gpu.isChecked()
|
||||
conf["local_preload"] = self.local_preload.isChecked()
|
||||
@@ -1813,12 +1900,21 @@ class SettingsWindow(QDialog):
|
||||
conf["cleanup_enabled"] = self.cleanup_enabled.isChecked()
|
||||
conf["cleanup_provider"] = self.cleanup_provider.currentData() or "openrouter"
|
||||
conf["cleanup_model"] = self.cleanup_model.currentText().strip()
|
||||
conf["cleanup_gemini_model"] = (
|
||||
self.cleanup_gemini_model.currentText().strip()
|
||||
or cfg.DEFAULTS["cleanup_gemini_model"]
|
||||
)
|
||||
conf["cleanup_claude_model"] = (self.cleanup_claude_model.currentText().strip()
|
||||
or cfg.DEFAULTS["cleanup_claude_model"])
|
||||
codex_cleanup_model = self.cleanup_codex_model.currentText().strip()
|
||||
conf["cleanup_codex_model"] = (
|
||||
"" if codex_cleanup_model == t("Codex's own default") else codex_cleanup_model
|
||||
)
|
||||
agy_cleanup_model = self.cleanup_agy_model.currentText().strip()
|
||||
conf["cleanup_agy_model"] = (
|
||||
"" if agy_cleanup_model == t("Antigravity's own default")
|
||||
else agy_cleanup_model
|
||||
)
|
||||
conf["cleanup_opencode_model"] = (
|
||||
self.cleanup_opencode_model.currentText().strip()
|
||||
or cfg.DEFAULTS["cleanup_opencode_model"]
|
||||
@@ -1861,6 +1957,10 @@ class SettingsWindow(QDialog):
|
||||
self.assistant_openrouter_model.currentText().strip()
|
||||
or cfg.DEFAULTS["assistant_openrouter_model"]
|
||||
)
|
||||
agy_model = self.assistant_agy_model.currentText().strip()
|
||||
conf["assistant_agy_model"] = (
|
||||
"" if agy_model == t("Antigravity's own default") else agy_model
|
||||
)
|
||||
conf["assistant_opencode_model"] = (
|
||||
self.assistant_opencode_model.currentText().strip()
|
||||
or cfg.DEFAULTS["assistant_opencode_model"]
|
||||
@@ -2005,55 +2105,51 @@ class SettingsWindow(QDialog):
|
||||
|
||||
def _load_models(self):
|
||||
self.refresh_models.setEnabled(False)
|
||||
self.refresh_opencode_models.setEnabled(False)
|
||||
self.models_label.setText(t("Fetching model list…"))
|
||||
# Whichever provider is selected for cleanup is the one whose models are
|
||||
# fetched, so the list lands in the box of the provider on screen.
|
||||
provider = self.cleanup_provider.currentData() or "openrouter"
|
||||
if provider == "opencode":
|
||||
key = (self.opencode_key.text().strip() or self.conf.opencode_key())
|
||||
base = self.conf["opencode_base_url"]
|
||||
|
||||
def work():
|
||||
try:
|
||||
self._models_loaded.emit(
|
||||
api.openai_models(key, base, "OpenCode Go"), "", provider)
|
||||
except api.ApiError as exc:
|
||||
self._models_loaded.emit([], str(exc), provider)
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
return
|
||||
key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
||||
|
||||
def work():
|
||||
try:
|
||||
self._models_loaded.emit(api.openrouter_models(key), "", provider)
|
||||
self._models_loaded.emit(api.openrouter_models(key), "")
|
||||
except api.ApiError as exc:
|
||||
self._models_loaded.emit([], str(exc), provider)
|
||||
self._models_loaded.emit([], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_models_loaded(self, models, error, provider):
|
||||
def _on_models_loaded(self, models, error):
|
||||
self.refresh_models.setEnabled(True)
|
||||
self.refresh_opencode_models.setEnabled(True)
|
||||
if error:
|
||||
self.models_label.setText(t("Could not fetch the list: {error}", error=error))
|
||||
return
|
||||
if provider == "opencode":
|
||||
combo = self.cleanup_opencode_model
|
||||
else:
|
||||
combo = self.cleanup_model
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItems(models)
|
||||
combo.setCurrentText(current)
|
||||
if provider == "openrouter":
|
||||
# The minutes summary runs on the same key, so the meeting box is
|
||||
# filled from the same list.
|
||||
current = self.meeting_model.currentText()
|
||||
self.meeting_model.clear()
|
||||
self.meeting_model.addItems(models)
|
||||
self.meeting_model.setCurrentText(current)
|
||||
for combo in (self.cleanup_model, self.meeting_model):
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItems(models)
|
||||
combo.setCurrentText(current)
|
||||
self.models_label.setText(t("{count} models loaded.", count=len(models)))
|
||||
|
||||
def _load_gemini_models(self):
|
||||
self.refresh_gemini_models.setEnabled(False)
|
||||
self.models_label.setText(t("Fetching model list…"))
|
||||
key, base = self._typed_key("gemini")
|
||||
|
||||
def work():
|
||||
try:
|
||||
self._gemini_models_loaded.emit(api.gemini_models(key, base), "")
|
||||
except api.ApiError as exc:
|
||||
self._gemini_models_loaded.emit([], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_gemini_models_loaded(self, models, error):
|
||||
self.refresh_gemini_models.setEnabled(True)
|
||||
if error:
|
||||
self.models_label.setText(t("Could not fetch the list: {error}", error=error))
|
||||
return
|
||||
current = self.cleanup_gemini_model.currentText()
|
||||
self.cleanup_gemini_model.clear()
|
||||
self.cleanup_gemini_model.addItems(models)
|
||||
self.cleanup_gemini_model.setCurrentText(current)
|
||||
self.models_label.setText(t("{count} models loaded.", count=len(models)))
|
||||
|
||||
def _load_codex_models(self):
|
||||
@@ -2084,36 +2180,111 @@ class SettingsWindow(QDialog):
|
||||
combo.setCurrentText(current)
|
||||
|
||||
def _load_opencode_models(self):
|
||||
"""Ask OpenCode Go for its catalog of the day, off the interface thread.
|
||||
|
||||
The same courtesy Codex gets: the built-in list is only a starting
|
||||
point, so the boxes are refreshed from the source as the window opens.
|
||||
Skipped without a key, so a machine that never touched OpenCode Go
|
||||
sends it nothing; the Fetch button stays for a key typed in just now.
|
||||
"""
|
||||
key = self.conf.opencode_key()
|
||||
if not key:
|
||||
return
|
||||
self.refresh_opencode_models.setEnabled(False)
|
||||
self.models_label.setText(t("Fetching model list…"))
|
||||
key, base = self._typed_key("opencode")
|
||||
|
||||
def work():
|
||||
try:
|
||||
found = api.openai_models(key, self.conf["opencode_base_url"],
|
||||
"OpenCode Go")
|
||||
except api.ApiError:
|
||||
# The window is only opening; the Test button says what failed.
|
||||
return
|
||||
if found:
|
||||
self._opencode_models_loaded.emit(found)
|
||||
self._opencode_models_loaded.emit(
|
||||
api.openai_models(key, base, "OpenCode Go"), "")
|
||||
except api.ApiError as exc:
|
||||
self._opencode_models_loaded.emit([], str(exc))
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_opencode_models_loaded(self, models):
|
||||
def _on_opencode_models_loaded(self, models, error):
|
||||
self.refresh_opencode_models.setEnabled(True)
|
||||
if error:
|
||||
self.models_label.setText(t("Could not fetch the list: {error}", error=error))
|
||||
return
|
||||
self._fill_opencode_boxes(models)
|
||||
self.models_label.setText(t("{count} models loaded.", count=len(models)))
|
||||
|
||||
def _fill_opencode_boxes(self, models):
|
||||
# The agent runs on the same key and catalog, so its box is refilled
|
||||
# from the same list.
|
||||
for combo in (self.cleanup_opencode_model, self.assistant_opencode_model):
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItems(models)
|
||||
combo.setCurrentText(current)
|
||||
|
||||
def _load_agy_models(self):
|
||||
"""Ask Antigravity which models it offers, off the interface thread.
|
||||
|
||||
The same arrangement as Codex, except agy answers over the network
|
||||
rather than from a cache, so the couple of seconds it takes are spent
|
||||
where nobody is waiting. Skipped when agy is not installed, which is
|
||||
also when the built-in list stays on screen and nobody is running
|
||||
Antigravity anyway.
|
||||
"""
|
||||
if not shutil.which("agy"):
|
||||
return
|
||||
|
||||
def work():
|
||||
found = assistant.agy_models()
|
||||
if found:
|
||||
self._agy_models_loaded.emit(found)
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_agy_models_loaded(self, models):
|
||||
for combo in (self.cleanup_agy_model, self.assistant_agy_model):
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItem(t("Antigravity's own default"), "")
|
||||
for name in models:
|
||||
combo.addItem(name, name)
|
||||
combo.setCurrentText(current)
|
||||
|
||||
def _load_hosted_models(self):
|
||||
"""Fetch the hosted model lists at open, without being asked.
|
||||
|
||||
The Fetch buttons stay: they are the retry, and the place a failure is
|
||||
worth explaining. Here nobody asked, so an error changes nothing on
|
||||
screen and the built-in lists remain, and a provider whose key has not
|
||||
been given yet is not called at all.
|
||||
"""
|
||||
jobs = []
|
||||
openrouter_key = self.conf.openrouter_key()
|
||||
if openrouter_key:
|
||||
jobs.append(("openrouter",
|
||||
lambda: api.openrouter_models(openrouter_key)))
|
||||
gemini_key = self.conf.gemini_key()
|
||||
gemini_base = self.conf["gemini_base_url"]
|
||||
if gemini_key:
|
||||
jobs.append(("gemini",
|
||||
lambda: api.gemini_models(gemini_key, gemini_base)))
|
||||
opencode_key = self.conf.opencode_key()
|
||||
opencode_base = self.conf["opencode_base_url"]
|
||||
if opencode_key:
|
||||
jobs.append(("opencode",
|
||||
lambda: api.openai_models(opencode_key, opencode_base,
|
||||
"OpenCode Go")))
|
||||
for provider, fetch in jobs:
|
||||
def work(provider=provider, fetch=fetch):
|
||||
try:
|
||||
found = fetch()
|
||||
except api.ApiError:
|
||||
return
|
||||
if found:
|
||||
self._hosted_models_loaded.emit(provider, found)
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_hosted_models_loaded(self, provider, models):
|
||||
if provider == "opencode":
|
||||
self._fill_opencode_boxes(models)
|
||||
return
|
||||
combos = ((self.cleanup_model, self.meeting_model)
|
||||
if provider == "openrouter" else (self.cleanup_gemini_model,))
|
||||
for combo in combos:
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItems(models)
|
||||
combo.setCurrentText(current)
|
||||
|
||||
def _test_openai(self):
|
||||
key, base = self._typed_key("openai")
|
||||
self._test_key("openai", lambda: t(
|
||||
@@ -2132,6 +2303,13 @@ class SettingsWindow(QDialog):
|
||||
key, _ = self._typed_key("openrouter")
|
||||
self._test_key("openrouter", lambda: api.openrouter_key_status(key))
|
||||
|
||||
def _test_gemini(self):
|
||||
key, base = self._typed_key("gemini")
|
||||
self._test_key("gemini", lambda: t(
|
||||
"Connection works. {count} models visible.",
|
||||
count=len(api.gemini_models(key, base)),
|
||||
))
|
||||
|
||||
def _test_opencode(self):
|
||||
key, base = self._typed_key("opencode")
|
||||
self._test_key("opencode", lambda: t(
|
||||
@@ -2141,14 +2319,14 @@ class SettingsWindow(QDialog):
|
||||
|
||||
def _typed_key(self, provider):
|
||||
"""(key, base URL) for a provider, preferring what is in the field now."""
|
||||
if provider == "opencode":
|
||||
# The one key that is not in the TRANSCRIBERS table: it pays for
|
||||
# cleanup and the agent rather than for speech to text.
|
||||
typed = self._key_fields["opencode"].text().strip()
|
||||
return typed or self.conf.opencode_key(), self.conf["opencode_base_url"]
|
||||
who = cfg.TRANSCRIBERS[provider]
|
||||
if provider in cfg.TRANSCRIBERS:
|
||||
who = cfg.TRANSCRIBERS[provider]
|
||||
key_setting, url_setting = who.key, who.url
|
||||
else:
|
||||
key_setting = f"{provider}_api_key"
|
||||
url_setting = f"{provider}_base_url"
|
||||
typed = self._key_fields[provider].text().strip()
|
||||
return typed or self.conf.api_key(who.key), self.conf[who.url]
|
||||
return typed or self.conf.api_key(key_setting), self.conf[url_setting]
|
||||
|
||||
def _test_key(self, provider, ask):
|
||||
"""Run `ask` off the interface thread and write its answer under the key.
|
||||
@@ -2372,12 +2550,16 @@ class SettingsWindow(QDialog):
|
||||
provider = self.cleanup_provider.currentData() or "openrouter"
|
||||
self.cleanup_form.setRowVisible(self.cleanup_model_row,
|
||||
provider == "openrouter")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_gemini_model_row,
|
||||
provider == "gemini")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_claude_model,
|
||||
provider == "claude")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_codex_model,
|
||||
provider == "codex")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_opencode_model_row,
|
||||
provider == "opencode")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_agy_model,
|
||||
provider == "agy")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_reasoning,
|
||||
provider != "local")
|
||||
self.cleanup_form.setRowVisible(self.local_llm, provider == "local")
|
||||
@@ -2386,6 +2568,8 @@ class SettingsWindow(QDialog):
|
||||
found = shutil.which(binary) if binary else ""
|
||||
if provider == "local":
|
||||
self.models_label.setText(t("Runs on this machine, on llama.cpp."))
|
||||
elif provider == "gemini":
|
||||
self.models_label.setText(t("Runs on Google AI Studio."))
|
||||
elif provider == "opencode":
|
||||
self.models_label.setText(t("Runs on OpenCode Go."))
|
||||
elif not binary:
|
||||
@@ -2404,6 +2588,7 @@ class SettingsWindow(QDialog):
|
||||
self.claude_box.setVisible(provider == "claude")
|
||||
self.codex_box.setVisible(provider == "codex")
|
||||
self.openrouter_box.setVisible(provider == "openrouter")
|
||||
self.agy_box.setVisible(provider == "agy")
|
||||
self.opencode_box.setVisible(provider == "opencode")
|
||||
self._refresh_assistant_status()
|
||||
|
||||
@@ -2538,7 +2723,11 @@ class SettingsWindow(QDialog):
|
||||
# The text of an answer says nothing about what was asked, and
|
||||
# out of that context half of them read like non sequiturs.
|
||||
asked = (row.get("question") or row.get("raw") or "").replace("\n", " ")
|
||||
header += t(" · asked Claude: {question}",
|
||||
# Rows written before the provider was recorded are all Claude's,
|
||||
# because it was the only one the history could name.
|
||||
who = assistant.SERVICES.get(row.get("assistant"), "Claude")
|
||||
header += t(" · asked {who}: {question}",
|
||||
who=i18n.name(who, "dative"),
|
||||
question=asked[:60] + ("…" if len(asked) > 60 else ""))
|
||||
item = QListWidgetItem(f"{header}\n{preview}")
|
||||
item.setData(Qt.ItemDataRole.UserRole, row)
|
||||
|
||||
Reference in New Issue
Block a user