mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Offer OpenCode Go in the settings window
A key row under Keys, a model box in the cleanup tab and a box in the agent tab, each with its own model list of the models OpenCode Go serves over /chat/completions. Fetch model list reads whichever cleanup provider is on screen, and the Turkish strings cover the new rows.
This commit is contained in:
+27
-2
@@ -40,8 +40,10 @@ def t(text, /, **kwargs):
|
||||
# by the sentence, so it arrives already inflected. English takes the name as it
|
||||
# is and puts the preposition in the sentence, where it belongs.
|
||||
_TR_CASES = {
|
||||
"dative": {"Claude": "Claude'a", "Codex": "Codex'e", "OpenRouter": "OpenRouter'a"},
|
||||
"accusative": {"Claude": "Claude'u", "Codex": "Codex'i", "OpenRouter": "OpenRouter'ı"},
|
||||
"dative": {"Claude": "Claude'a", "Codex": "Codex'e", "OpenRouter": "OpenRouter'a",
|
||||
"OpenCode Go": "OpenCode Go'ya"},
|
||||
"accusative": {"Claude": "Claude'u", "Codex": "Codex'i",
|
||||
"OpenRouter": "OpenRouter'ı", "OpenCode Go": "OpenCode Go'yu"},
|
||||
}
|
||||
|
||||
|
||||
@@ -221,11 +223,15 @@ TR = {
|
||||
"sk-… (falls back to OPENAI_API_KEY)": "sk-… (boşsa OPENAI_API_KEY kullanılır)",
|
||||
"gsk_… (falls back to GROQ_API_KEY)": "gsk_… (boşsa GROQ_API_KEY kullanılır)",
|
||||
"sk-or-… (falls back to OPENROUTER_API_KEY)": "sk-or-… (boşsa OPENROUTER_API_KEY kullanılır)",
|
||||
"(falls back to OPENCODE_API_KEY)": "(boşsa OPENCODE_API_KEY kullanılır)",
|
||||
"Test": "Test et",
|
||||
"Trying…": "Deneniyor…",
|
||||
"Runs on OpenRouter.": "OpenRouter üzerinde çalışır.",
|
||||
"Runs on OpenCode Go.": "OpenCode Go üzerinde çalışır.",
|
||||
"Connection works. {count} audio models visible.":
|
||||
"Bağlantı tamam. {count} ses modeli görünüyor.",
|
||||
"Connection works. {count} models visible.":
|
||||
"Bağlantı tamam. {count} model görünüyor.",
|
||||
"Clean the transcript with a model": "Transkripti bir modelle temizle",
|
||||
"OpenRouter is the quickest and the only one that needs nothing installed. "
|
||||
"Claude Code and Codex clean up on the subscription you already have, "
|
||||
@@ -235,6 +241,14 @@ TR = {
|
||||
"Claude Code ile Codex, temizliği hâlihazırda ödediğin abonelik üzerinden "
|
||||
"yapar, ikinci bir anahtar istemez; her biri bunun için bir oturum açtığından "
|
||||
"birkaç saniye daha uzun sürer.",
|
||||
"OpenRouter and OpenCode Go are the quickest and need nothing installed. "
|
||||
"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.":
|
||||
"En hızlıları OpenRouter ve OpenCode Go'dur; kurulu bir program "
|
||||
"istemezler. Claude Code ile Codex, temizliği hâlihazırda ödediğin "
|
||||
"abonelik üzerinden yapar, ikinci bir anahtar istemez; her biri bunun "
|
||||
"için bir oturum açtığından birkaç saniye daha uzun sürer.",
|
||||
"{binary} is not on your PATH, so cleanup would fail and the raw transcript "
|
||||
"would be pasted. Install it, or pick another one above.":
|
||||
"{binary} PATH'te değil; temizleme başarısız olur ve ham transkript "
|
||||
@@ -541,6 +555,17 @@ TR = {
|
||||
"Yukarıdaki çalışma dizini ve izinler burada bir şey ifade etmez.",
|
||||
"Needs no program installed, only the OpenRouter key.":
|
||||
"Kurulu bir programa değil, yalnızca OpenRouter anahtarına ihtiyaç duyar.",
|
||||
"A plain question and a plain answer, over the OpenCode Go key you already "
|
||||
"have. It runs no commands, opens no files and reaches none of your "
|
||||
"services, so it can tell you what the capital of Peru is but not what is "
|
||||
"in your calendar. Working directory and permissions above mean nothing "
|
||||
"here.":
|
||||
"Elindeki OpenCode Go anahtarı üzerinden düz bir soru ve düz bir cevap. "
|
||||
"Komut çalıştırmaz, dosya açmaz, servislerinin hiçbirine erişmez; yani "
|
||||
"Peru'nun başkentini söyler ama takviminde ne olduğunu söyleyemez. "
|
||||
"Yukarıdaki çalışma dizini ve izinler burada bir şey ifade etmez.",
|
||||
"Needs no program installed, only an OpenCode Go key.":
|
||||
"Kurulu bir programa değil, yalnızca bir OpenCode Go anahtarına ihtiyaç duyar.",
|
||||
"{binary} is not on your PATH, so this cannot run yet. Install it, or pick "
|
||||
"another one above.":
|
||||
"{binary} PATH'te değil, dolayısıyla bu henüz çalışamaz. Kur ya da "
|
||||
|
||||
+113
-17
@@ -61,7 +61,8 @@ CLEANUP_MODELS = [
|
||||
# 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.
|
||||
CLEANUP_PROVIDERS = [
|
||||
("OpenRouter", "openrouter"), ("This machine (llama.cpp)", "local"),
|
||||
("OpenRouter", "openrouter"), ("OpenCode Go", "opencode"),
|
||||
("This machine (llama.cpp)", "local"),
|
||||
("Claude Code", "claude"), ("Codex", "codex"),
|
||||
]
|
||||
# Cleaning up a sentence is the lightest thing either of them will ever be
|
||||
@@ -74,7 +75,8 @@ MEETING_MODELS = [
|
||||
"anthropic/claude-sonnet-5", "openai/gpt-5.4", "x-ai/grok-4.5",
|
||||
]
|
||||
ASSISTANT_PROVIDERS = [
|
||||
("Claude Code", "claude"), ("Codex", "codex"), ("OpenRouter", "openrouter"),
|
||||
("Claude Code", "claude"), ("Codex", "codex"),
|
||||
("OpenRouter", "openrouter"), ("OpenCode Go", "opencode"),
|
||||
]
|
||||
# Aliases resolve to the newest model of that name, so they age better than an
|
||||
# id does; a full id can be typed in when a particular one is wanted.
|
||||
@@ -85,6 +87,14 @@ ASSISTANT_OR_MODELS = [
|
||||
"google/gemini-3.5-flash", "anthropic/claude-sonnet-5", "openai/gpt-5.4",
|
||||
"x-ai/grok-4.5", "google/gemini-3.1-pro-preview",
|
||||
]
|
||||
# The models OpenCode Go serves over /chat/completions. The ones its catalog
|
||||
# lists under /responses or /messages (Grok, GPT-5.6 Luna, MiniMax, Qwen) are
|
||||
# not offered here, because Dikte speaks only the chat endpoint.
|
||||
OPENCODE_MODELS = [
|
||||
"deepseek-v4-flash", "deepseek-v4-pro", "glm-5.3", "glm-5.2", "glm-5.1",
|
||||
"kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "longcat-2.0",
|
||||
"mimo-v2.5", "mimo-v2.5-pro", "hy3", "ox-alpha-free",
|
||||
]
|
||||
# What Claude Code may do without being able to ask. It cannot ask: there is no
|
||||
# window to answer in, so a mode that would have prompted denies instead.
|
||||
PERMISSION_MODES = [
|
||||
@@ -519,7 +529,7 @@ class SettingsWindow(QDialog):
|
||||
# hears about it from here rather than waiting for its own next check.
|
||||
update_found = pyqtSignal(object)
|
||||
|
||||
_models_loaded = pyqtSignal(list, str)
|
||||
_models_loaded = pyqtSignal(list, str, str)
|
||||
_transcribe_models_loaded = pyqtSignal(list, str)
|
||||
# Which key was tested, whether it worked, and what to write under it.
|
||||
_test_done = pyqtSignal(str, bool, str)
|
||||
@@ -746,6 +756,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.opencode_key = self._key_row(
|
||||
keys_form, "opencode", t("(falls back to OPENCODE_API_KEY)"),
|
||||
self._test_opencode, service="OpenCode Go")
|
||||
outer.addWidget(keys)
|
||||
|
||||
stt = QGroupBox(t("Speech to text"))
|
||||
@@ -818,7 +831,7 @@ 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 and the only one that needs nothing "
|
||||
"OpenRouter and OpenCode Go are the quickest and need nothing "
|
||||
"installed. 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 "
|
||||
@@ -848,6 +861,11 @@ class SettingsWindow(QDialog):
|
||||
self.cleanup_codex_model.addItems([t("Codex's own default")] + CODEX_MODELS)
|
||||
orr_form.addRow(t("Model"), self.cleanup_codex_model)
|
||||
|
||||
self.cleanup_opencode_model = QComboBox()
|
||||
self.cleanup_opencode_model.setEditable(True)
|
||||
self.cleanup_opencode_model.addItems(OPENCODE_MODELS)
|
||||
orr_form.addRow(t("Model"), self.cleanup_opencode_model)
|
||||
|
||||
self.cleanup_reasoning = QComboBox()
|
||||
for label, value in REASONING_LEVELS:
|
||||
self.cleanup_reasoning.addItem(t(label), value)
|
||||
@@ -1044,6 +1062,23 @@ class SettingsWindow(QDialog):
|
||||
or_form.addRow(or_note)
|
||||
layout.addWidget(self.openrouter_box)
|
||||
|
||||
self.opencode_box = QGroupBox("OpenCode Go")
|
||||
og_form = QFormLayout(self.opencode_box)
|
||||
self.assistant_opencode_model = QComboBox()
|
||||
self.assistant_opencode_model.setEditable(True)
|
||||
self.assistant_opencode_model.addItems(OPENCODE_MODELS)
|
||||
og_form.addRow(t("Model"), self.assistant_opencode_model)
|
||||
og_note = QLabel(t(
|
||||
"A plain question and a plain answer, over the OpenCode Go key you "
|
||||
"already have. It runs no commands, opens no files and reaches none "
|
||||
"of your services, so it can tell you what the capital of Peru is "
|
||||
"but not what is in your calendar. Working directory and permissions "
|
||||
"above mean nothing here."
|
||||
))
|
||||
og_note.setWordWrap(True)
|
||||
og_form.addRow(og_note)
|
||||
layout.addWidget(self.opencode_box)
|
||||
|
||||
thread = QGroupBox(t("The conversation"))
|
||||
thread_form = QFormLayout(thread)
|
||||
self.assistant_session_minutes = QSpinBox()
|
||||
@@ -1515,7 +1550,7 @@ class SettingsWindow(QDialog):
|
||||
box.lineEdit().setPlaceholderText(placeholder)
|
||||
return box
|
||||
|
||||
def _key_row(self, form, provider, placeholder, tester):
|
||||
def _key_row(self, form, provider, placeholder, tester, service=""):
|
||||
"""A key field, its Test button and the line the answer lands on.
|
||||
|
||||
The field and the pair the answer needs are filed under the provider's
|
||||
@@ -1529,7 +1564,8 @@ class SettingsWindow(QDialog):
|
||||
button.clicked.connect(tester)
|
||||
answer = QLabel("")
|
||||
answer.setWordWrap(True)
|
||||
form.addRow(cfg.TRANSCRIBERS[provider].service, self._row(field, button))
|
||||
form.addRow(service or cfg.TRANSCRIBERS[provider].service,
|
||||
self._row(field, button))
|
||||
form.addRow("", answer)
|
||||
self._key_fields[provider] = field
|
||||
self._testers[provider] = (button, answer)
|
||||
@@ -1603,6 +1639,7 @@ 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._shown_provider = ""
|
||||
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||
self._provider_changed() # selecting index 0 fires no signal
|
||||
@@ -1617,6 +1654,7 @@ class SettingsWindow(QDialog):
|
||||
self.cleanup_codex_model.setCurrentText(
|
||||
conf["cleanup_codex_model"] or t("Codex'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
|
||||
self._select_data(self.cleanup_reasoning, conf["cleanup_reasoning"])
|
||||
@@ -1636,6 +1674,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_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"])
|
||||
self.assistant_dir.setText(conf["assistant_dir"])
|
||||
@@ -1701,6 +1740,7 @@ 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["local_model"] = self.local_whisper.selected()
|
||||
conf["local_gpu"] = self.local_gpu.isChecked()
|
||||
conf["local_preload"] = self.local_preload.isChecked()
|
||||
@@ -1715,6 +1755,10 @@ class SettingsWindow(QDialog):
|
||||
conf["cleanup_codex_model"] = (
|
||||
"" if codex_cleanup_model == t("Codex's own default") else codex_cleanup_model
|
||||
)
|
||||
conf["cleanup_opencode_model"] = (
|
||||
self.cleanup_opencode_model.currentText().strip()
|
||||
or cfg.DEFAULTS["cleanup_opencode_model"]
|
||||
)
|
||||
conf["cleanup_reasoning"] = self.cleanup_reasoning.currentData() or ""
|
||||
conf["local_llm_model"] = self.local_llm.selected()
|
||||
conf["local_llm_repo"] = self.local_llm.repository()
|
||||
@@ -1748,6 +1792,10 @@ class SettingsWindow(QDialog):
|
||||
self.assistant_openrouter_model.currentText().strip()
|
||||
or cfg.DEFAULTS["assistant_openrouter_model"]
|
||||
)
|
||||
conf["assistant_opencode_model"] = (
|
||||
self.assistant_opencode_model.currentText().strip()
|
||||
or cfg.DEFAULTS["assistant_opencode_model"]
|
||||
)
|
||||
conf["assistant_reasoning"] = self.assistant_reasoning.currentData() or ""
|
||||
conf["assistant_dir"] = self.assistant_dir.text().strip()
|
||||
conf["assistant_timeout"] = self.assistant_timeout.value()
|
||||
@@ -1854,26 +1902,52 @@ class SettingsWindow(QDialog):
|
||||
def _load_models(self):
|
||||
self.refresh_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), "")
|
||||
self._models_loaded.emit(api.openrouter_models(key), "", provider)
|
||||
except api.ApiError as exc:
|
||||
self._models_loaded.emit([], str(exc))
|
||||
self._models_loaded.emit([], str(exc), provider)
|
||||
|
||||
threading.Thread(target=work, daemon=True).start()
|
||||
|
||||
def _on_models_loaded(self, models, error):
|
||||
def _on_models_loaded(self, models, error, provider):
|
||||
self.refresh_models.setEnabled(True)
|
||||
if error:
|
||||
self.models_label.setText(t("Could not fetch the list: {error}", error=error))
|
||||
return
|
||||
for combo in (self.cleanup_model, self.meeting_model):
|
||||
current = combo.currentText()
|
||||
combo.clear()
|
||||
combo.addItems(models)
|
||||
combo.setCurrentText(current)
|
||||
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)
|
||||
self.models_label.setText(t("{count} models loaded.", count=len(models)))
|
||||
|
||||
def _test_openai(self):
|
||||
@@ -1894,8 +1968,20 @@ class SettingsWindow(QDialog):
|
||||
key, _ = self._typed_key("openrouter")
|
||||
self._test_key("openrouter", lambda: api.openrouter_key_status(key))
|
||||
|
||||
def _test_opencode(self):
|
||||
key, base = self._typed_key("opencode")
|
||||
self._test_key("opencode", lambda: t(
|
||||
"Connection works. {count} models visible.",
|
||||
count=len(api.openai_models(key, base, "OpenCode Go")),
|
||||
))
|
||||
|
||||
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]
|
||||
typed = self._key_fields[provider].text().strip()
|
||||
return typed or self.conf.api_key(who.key), self.conf[who.url]
|
||||
@@ -2118,6 +2204,8 @@ class SettingsWindow(QDialog):
|
||||
provider == "claude")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_codex_model,
|
||||
provider == "codex")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_opencode_model,
|
||||
provider == "opencode")
|
||||
self.cleanup_form.setRowVisible(self.cleanup_reasoning,
|
||||
provider != "local")
|
||||
self.cleanup_form.setRowVisible(self.local_llm, provider == "local")
|
||||
@@ -2126,6 +2214,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 == "opencode":
|
||||
self.models_label.setText(t("Runs on OpenCode Go."))
|
||||
elif not binary:
|
||||
self.models_label.setText(t("Runs on OpenRouter."))
|
||||
elif found:
|
||||
@@ -2142,6 +2232,7 @@ class SettingsWindow(QDialog):
|
||||
self.claude_box.setVisible(provider == "claude")
|
||||
self.codex_box.setVisible(provider == "codex")
|
||||
self.openrouter_box.setVisible(provider == "openrouter")
|
||||
self.opencode_box.setVisible(provider == "opencode")
|
||||
self._refresh_assistant_status()
|
||||
|
||||
def _refresh_assistant_status(self):
|
||||
@@ -2149,9 +2240,14 @@ class SettingsWindow(QDialog):
|
||||
binary = assistant.executable(provider)
|
||||
found = shutil.which(binary) if binary else ""
|
||||
if not binary:
|
||||
self.assistant_found.setText(
|
||||
t("Needs no program installed, only the OpenRouter key.")
|
||||
)
|
||||
if provider == "opencode":
|
||||
self.assistant_found.setText(
|
||||
t("Needs no program installed, only an OpenCode Go key.")
|
||||
)
|
||||
else:
|
||||
self.assistant_found.setText(
|
||||
t("Needs no program installed, only the OpenRouter key.")
|
||||
)
|
||||
elif found:
|
||||
self.assistant_found.setText(t("Found: {path}", path=found))
|
||||
else:
|
||||
|
||||
@@ -244,6 +244,7 @@ class Settings(DikteTest):
|
||||
"""An OpenRouter id and a Claude alias are not the same field."""
|
||||
window = self.window(cfg.Config())
|
||||
boxes = {"openrouter": window.cleanup_model_row,
|
||||
"opencode": window.cleanup_opencode_model,
|
||||
"claude": window.cleanup_claude_model,
|
||||
"codex": window.cleanup_codex_model}
|
||||
for provider, box in boxes.items():
|
||||
|
||||
Reference in New Issue
Block a user