Fetch OpenCode Go's catalog as the settings window opens

Codex already refreshes its boxes from the source at open, so the
built-in list is never the whole truth for longer than a window takes to
build. OpenCode Go now gets the same courtesy: one request to /models on
a background thread, both of its boxes refilled with the picked model
kept, skipped entirely when there is no key to send.
This commit is contained in:
2026-08-27 16:04:30 +03:00
parent 1ff18e46ab
commit fa3d72f5a7
2 changed files with 55 additions and 1 deletions
+34
View File
@@ -569,6 +569,7 @@ class SettingsWindow(QDialog):
_models_loaded = pyqtSignal(list, str, str) _models_loaded = pyqtSignal(list, str, str)
_transcribe_models_loaded = pyqtSignal(list, str) _transcribe_models_loaded = pyqtSignal(list, str)
_codex_models_loaded = pyqtSignal(list) _codex_models_loaded = pyqtSignal(list)
_opencode_models_loaded = pyqtSignal(list)
# Which key was tested, whether it worked, and what to write under it. # Which key was tested, whether it worked, and what to write under it.
_test_done = pyqtSignal(str, bool, str) _test_done = pyqtSignal(str, bool, str)
# The release that was found, or None, and what went wrong instead. # The release that was found, or None, and what went wrong instead.
@@ -628,6 +629,7 @@ class SettingsWindow(QDialog):
self._models_loaded.connect(self._on_models_loaded) self._models_loaded.connect(self._on_models_loaded)
self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded) self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded)
self._codex_models_loaded.connect(self._on_codex_models_loaded) self._codex_models_loaded.connect(self._on_codex_models_loaded)
self._opencode_models_loaded.connect(self._on_opencode_models_loaded)
self._test_done.connect(self._on_test_done) self._test_done.connect(self._on_test_done)
self._update_checked.connect(self._on_update_checked) self._update_checked.connect(self._on_update_checked)
self.transcriber.progress.connect(self._on_file_progress) self.transcriber.progress.connect(self._on_file_progress)
@@ -639,6 +641,7 @@ class SettingsWindow(QDialog):
self.meetings.failed.connect(self._on_minutes_failed) self.meetings.failed.connect(self._on_minutes_failed)
self._load() self._load()
self._load_codex_models() self._load_codex_models()
self._load_opencode_models()
# Connected after the load, so that filling the boxes in is not taken # Connected after the load, so that filling the boxes in is not taken
# for the user ticking them. # for the user ticking them.
self.file_timestamps.toggled.connect(self._remember_file_choices) self.file_timestamps.toggled.connect(self._remember_file_choices)
@@ -2080,6 +2083,37 @@ class SettingsWindow(QDialog):
combo.addItem(name, name) combo.addItem(name, name)
combo.setCurrentText(current) 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
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)
threading.Thread(target=work, daemon=True).start()
def _on_opencode_models_loaded(self, models):
for combo in (self.cleanup_opencode_model, self.assistant_opencode_model):
current = combo.currentText()
combo.clear()
combo.addItems(models)
combo.setCurrentText(current)
def _test_openai(self): def _test_openai(self):
key, base = self._typed_key("openai") key, base = self._typed_key("openai")
self._test_key("openai", lambda: t( self._test_key("openai", lambda: t(
+21 -1
View File
@@ -135,6 +135,8 @@ class Settings(DikteTest):
"_load_transcribe_models")) "_load_transcribe_models"))
self.enterContext(mock.patch.object(settings_ui.SettingsWindow, self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
"_load_codex_models")) "_load_codex_models"))
self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
"_load_opencode_models"))
# The local model boxes fetch their own list the moment they are shown, # The local model boxes fetch their own list the moment they are shown,
# from a thread, which is nobody's test failing but a real request. # from a thread, which is nobody's test failing but a real request.
self.enterContext(mock.patch.object(settings_ui.LocalModelBox, self.enterContext(mock.patch.object(settings_ui.LocalModelBox,
@@ -285,6 +287,20 @@ class Settings(DikteTest):
self.assertEqual(window.cleanup_codex_model.currentText(), self.assertEqual(window.cleanup_codex_model.currentText(),
"my-own-model") "my-own-model")
def test_opencode_answering_refills_both_of_its_boxes(self):
"""The catalog fetched at open replaces the built-in list in the
cleanup and agent boxes alike, and neither loses what was picked."""
conf = self.config(cleanup_opencode_model="my-own-model")
window = self.window(conf)
window._on_opencode_models_loaded(["glm-9", "kimi-k9"])
for combo in (window.cleanup_opencode_model,
window.assistant_opencode_model):
with self.subTest(combo=combo.objectName() or "combo"):
offered = [combo.itemText(i) for i in range(combo.count())]
self.assertEqual(offered, ["glm-9", "kimi-k9"])
self.assertEqual(window.cleanup_opencode_model.currentText(),
"my-own-model")
def test_a_fetched_opencode_list_lands_in_opencode_s_own_box(self): def test_a_fetched_opencode_list_lands_in_opencode_s_own_box(self):
"""The OpenRouter and meeting boxes are not refilled by another """The OpenRouter and meeting boxes are not refilled by another
provider's catalog, and the picked model survives the refill.""" provider's catalog, and the picked model survives the refill."""
@@ -1026,7 +1042,9 @@ class MeetingSources(DikteTest):
mock.patch.object(settings_ui.SettingsWindow, mock.patch.object(settings_ui.SettingsWindow,
"_load_transcribe_models"), \ "_load_transcribe_models"), \
mock.patch.object(settings_ui.SettingsWindow, mock.patch.object(settings_ui.SettingsWindow,
"_load_codex_models"): "_load_codex_models"), \
mock.patch.object(settings_ui.SettingsWindow,
"_load_opencode_models"):
window = settings_ui.SettingsWindow(cfg.Config()) window = settings_ui.SettingsWindow(cfg.Config())
self.addCleanup(window.deleteLater) self.addCleanup(window.deleteLater)
self.addCleanup(window.close) self.addCleanup(window.close)
@@ -1058,6 +1076,8 @@ class LocalModels(DikteTest):
# And one with Codex on it would ask it for its model list. # And one with Codex on it would ask it for its model list.
self.enterContext(mock.patch.object(settings_ui.SettingsWindow, self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
"_load_codex_models")) "_load_codex_models"))
self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
"_load_opencode_models"))
def window(self, conf): def window(self, conf):
window = settings_ui.SettingsWindow(conf) window = settings_ui.SettingsWindow(conf)