Reach OpenCode Go from the command line

test-key accepts opencode and reports the model count its /models
endpoint offers, ask --provider accepts it and maps --model to its own
setting, doctor reports its key for cleanup on it, and config list
masks the key like the other two.
This commit is contained in:
sudoeren
2026-08-25 21:05:23 +03:00
parent 3c336e7178
commit e6b3fcf48f
3 changed files with 46 additions and 5 deletions
+18 -5
View File
@@ -227,7 +227,8 @@ def cmd_ask(opts):
conf["assistant_provider"] = opts.provider conf["assistant_provider"] = opts.provider
if opts.model: if opts.model:
key = {"claude": "assistant_model", "codex": "assistant_codex_model", key = {"claude": "assistant_model", "codex": "assistant_codex_model",
"openrouter": "assistant_openrouter_model"}[assistant.provider(conf)] "openrouter": "assistant_openrouter_model",
"opencode": "assistant_opencode_model"}[assistant.provider(conf)]
conf[key] = opts.model conf[key] = opts.model
if opts.dir: if opts.dir:
conf["assistant_dir"] = opts.dir conf["assistant_dir"] = opts.dir
@@ -519,7 +520,7 @@ def cmd_history_clear(opts):
# --- settings --------------------------------------------------------------- # --- settings ---------------------------------------------------------------
SECRET_KEYS = ("openai_api_key", "openrouter_api_key") SECRET_KEYS = ("openai_api_key", "openrouter_api_key", "opencode_api_key")
def _mask(key, value): def _mask(key, value):
@@ -709,6 +710,14 @@ def cmd_test_key(opts):
results[name] = {"ok": True, "message": message} results[name] = {"ok": True, "message": message}
except api.ApiError as exc: except api.ApiError as exc:
results[name] = {"ok": False, "message": str(exc)} results[name] = {"ok": False, "message": str(exc)}
if opts.which in ("opencode", "all"):
try:
count = len(api.openai_models(conf.opencode_key(),
conf["opencode_base_url"], "OpenCode Go"))
results["opencode"] = {"ok": True,
"message": f"connection works, {count} models visible"}
except api.ApiError as exc:
results["opencode"] = {"ok": False, "message": str(exc)}
everything_ok = all(item["ok"] for item in results.values()) everything_ok = all(item["ok"] for item in results.values())
lines = [f"{'' if item['ok'] else ''} {name}: {item['message']}" lines = [f"{'' if item['ok'] else ''} {name}: {item['message']}"
for name, item in results.items()] for name, item in results.items()]
@@ -875,7 +884,9 @@ def cmd_doctor(opts):
"key": bool(target.api_key)}, "key": bool(target.api_key)},
"cleanup": {"enabled": conf["cleanup_enabled"], "provider": cleaner, "cleanup": {"enabled": conf["cleanup_enabled"], "provider": cleaner,
"model": cleanup.model(conf), "model": cleanup.model(conf),
"key": bool(conf.openrouter_key())}, "key": (bool(conf.openrouter_key()) if cleaner == "openrouter"
else bool(conf.opencode_key())
if cleaner == "opencode" else None)},
"agent": {"provider": assistant.provider(conf), "agent": {"provider": assistant.provider(conf),
"directory": assistant.working_dir(conf)}, "directory": assistant.working_dir(conf)},
"running": ipc.send("status") is not None, "running": ipc.send("status") is not None,
@@ -888,6 +899,8 @@ def cmd_doctor(opts):
# Cleanup on a CLI needs no key, so what is checked is the program. # Cleanup on a CLI needs no key, so what is checked is the program.
(f"{'' if conf.openrouter_key() else ''} OpenRouter key, cleaning up on " (f"{'' if conf.openrouter_key() else ''} OpenRouter key, cleaning up on "
f"{conf['cleanup_model']}") if cleaner == "openrouter" else f"{conf['cleanup_model']}") if cleaner == "openrouter" else
(f"{'' if conf.opencode_key() else ''} OpenCode Go key, cleaning up on "
f"{conf['cleanup_opencode_model']}") if cleaner == "opencode" else
(f"{'' if programs[cleanup.executable(cleaner)] else ''} " (f"{'' if programs[cleanup.executable(cleaner)] else ''} "
f"{cleanup.executable(cleaner)}, cleaning up on {cleanup.model(conf)}"), f"{cleanup.executable(cleaner)}, cleaning up on {cleanup.model(conf)}"),
f"{'' if checks['running'] else '·'} application " f"{'' if checks['running'] else '·'} application "
@@ -977,7 +990,7 @@ def build_parser():
ask = leaf(subs, "ask", "put a command to the agent") ask = leaf(subs, "ask", "put a command to the agent")
ask.add_argument("text", nargs="*", help="the command; read from stdin, or " ask.add_argument("text", nargs="*", help="the command; read from stdin, or "
"recorded when there is none") "recorded when there is none")
ask.add_argument("--provider", choices=("claude", "codex", "openrouter"), ask.add_argument("--provider", choices=("claude", "codex", "openrouter", "opencode"),
help="just for this run") help="just for this run")
ask.add_argument("--model", help="just for this run") ask.add_argument("--model", help="just for this run")
ask.add_argument("--dir", help="working directory, just for this run") ask.add_argument("--dir", help="working directory, just for this run")
@@ -1100,7 +1113,7 @@ def build_parser():
models.set_defaults(func=cmd_models) models.set_defaults(func=cmd_models)
test = leaf(subs, "test-key", "check the API keys") test = leaf(subs, "test-key", "check the API keys")
test.add_argument("which", nargs="?", default="all", test.add_argument("which", nargs="?", default="all",
choices=("all", *cfg.TRANSCRIBERS)) choices=("all", *cfg.TRANSCRIBERS, "opencode"))
test.set_defaults(func=cmd_test_key) test.set_defaults(func=cmd_test_key)
leaf(subs, "doctor", "keys, programs, and what is missing").set_defaults(func=cmd_doctor) leaf(subs, "doctor", "keys, programs, and what is missing").set_defaults(func=cmd_doctor)
+19
View File
@@ -423,6 +423,16 @@ class Providers(DikteTest):
self.assertIn("groq", out) self.assertIn("groq", out)
self.assertIn("Groq", out) self.assertIn("Groq", out)
def test_opencode_is_a_choice_and_reports_under_its_own_name(self):
parser = cli.build_parser()
self.assertEqual(
parser.parse_args(["test-key", "opencode"]).which, "opencode")
self.write_config({"opencode_api_key": "opencode-test"})
with fake_urlopen({"data": [{"id": "deepseek-v4-flash"}]}):
code, out, _ = self.run_cmd(cli.cmd_test_key, which="opencode")
self.assertEqual(code, 0)
self.assertIn("opencode: connection works, 1 models visible", out)
class Updates(DikteTest): class Updates(DikteTest):
"""`dikte update` looks, says what it found, and installs nothing.""" """`dikte update` looks, says what it found, and installs nothing."""
@@ -503,6 +513,15 @@ class Doctor(DikteTest):
self.assertIn("OpenRouter key, cleaning up on some/model", self.assertIn("OpenRouter key, cleaning up on some/model",
self.run_doctor(as_json=False, cleanup_model="some/model")) self.run_doctor(as_json=False, cleanup_model="some/model"))
def test_cleanup_on_opencode_is_a_question_about_its_own_key(self):
reply = self.run_doctor(cleanup_provider="opencode",
cleanup_opencode_model="glm-5.3")
self.assertEqual(reply["cleanup"]["provider"], "opencode")
self.assertEqual(reply["cleanup"]["model"], "glm-5.3")
self.assertIn("OpenCode Go key, cleaning up on glm-5.3",
self.run_doctor(as_json=False, cleanup_provider="opencode",
cleanup_opencode_model="glm-5.3"))
def test_it_asks_after_the_programs_this_desktop_actually_uses(self): def test_it_asks_after_the_programs_this_desktop_actually_uses(self):
"""A missing ydotool on a Mac is a red mark with nothing behind it.""" """A missing ydotool on a Mac is a red mark with nothing behind it."""
with mock.patch.object(cli.paste, "desktop", return_value=paste.MACOS): with mock.patch.object(cli.paste, "desktop", return_value=paste.MACOS):
+9
View File
@@ -134,6 +134,8 @@ class Keys(DikteTest):
def test_every_provider_falls_back_to_the_variable_of_its_own_name(self): def test_every_provider_falls_back_to_the_variable_of_its_own_name(self):
with mock.patch.dict(os.environ, {"GROQ_API_KEY": "gsk-env"}): with mock.patch.dict(os.environ, {"GROQ_API_KEY": "gsk-env"}):
self.assertEqual(cfg.Config().groq_key(), "gsk-env") self.assertEqual(cfg.Config().groq_key(), "gsk-env")
with mock.patch.dict(os.environ, {"OPENCODE_API_KEY": "opencode-env"}):
self.assertEqual(cfg.Config().opencode_key(), "opencode-env")
class TranscribeTarget(DikteTest): class TranscribeTarget(DikteTest):
@@ -449,6 +451,13 @@ class Defaults(unittest.TestCase):
def test_the_keys_ship_empty(self): def test_the_keys_ship_empty(self):
self.assertEqual(cfg.DEFAULTS["openai_api_key"], "") self.assertEqual(cfg.DEFAULTS["openai_api_key"], "")
self.assertEqual(cfg.DEFAULTS["openrouter_api_key"], "") self.assertEqual(cfg.DEFAULTS["openrouter_api_key"], "")
self.assertEqual(cfg.DEFAULTS["opencode_api_key"], "")
def test_opencode_ships_on_its_own_endpoint(self):
self.assertEqual(cfg.DEFAULTS["opencode_base_url"],
"https://opencode.ai/zen/go/v1")
self.assertEqual(cfg.DEFAULTS["cleanup_opencode_model"], "deepseek-v4-flash")
self.assertEqual(cfg.DEFAULTS["assistant_opencode_model"], "deepseek-v4-flash")
def test_every_language_specific_prompt_has_both_languages(self): def test_every_language_specific_prompt_has_both_languages(self):
for name in ("CLEANUP_PROMPT", "FILE_CLEANUP_PROMPT", "MEETING_PROMPT", for name in ("CLEANUP_PROMPT", "FILE_CLEANUP_PROMPT", "MEETING_PROMPT",