diff --git a/agent/opencode_affinity.py b/agent/opencode_affinity.py index c6ba332cca..6df0bf1cc9 100644 --- a/agent/opencode_affinity.py +++ b/agent/opencode_affinity.py @@ -35,12 +35,16 @@ def opencode_transport(provider: Optional[str], model: Optional[str], base_url: hosts all count. """ from hermes_cli.models import normalize_opencode_base_url, normalize_opencode_model_id, opencode_model_api_mode - from hermes_cli.runtime_provider_custom import _opencode_family_for_custom + from hermes_cli.runtime_provider_custom import _get_named_custom_provider, _opencode_family_for_custom url = str(base_url or "") family = _opencode_family_for_custom(str(provider or ""), url) if family is None: return None, url + # A custom entry that declares its own api_mode keeps it, exactly like the main runtime + # (_resolve_named_custom_runtime only re-derives when the entry has none). + if (_get_named_custom_provider(str(provider or "")) or {}).get("api_mode"): + return None, url # ``/`` config ids are stripped against the entry name before the family lookup. api_mode = opencode_model_api_mode(family, normalize_opencode_model_id(provider, model)) return api_mode, normalize_opencode_base_url(provider, api_mode, url) diff --git a/tests/agent/test_auxiliary_opencode_routing.py b/tests/agent/test_auxiliary_opencode_routing.py index 865bd5b3c5..1ab5939c52 100644 --- a/tests/agent/test_auxiliary_opencode_routing.py +++ b/tests/agent/test_auxiliary_opencode_routing.py @@ -25,6 +25,9 @@ def _isolated_home(tmp_path, monkeypatch): " - name: opencode-go-bridge\n" " base_url: https://opencode.ai/zen/go/v1\n" " api_key: sk-bridge\n" + " - name: opencode-go-pinned\n" + " base_url: https://opencode.ai/zen/go/v1\n" + " api_key: sk-pinned\n" " api_mode: chat_completions\n" ) return home @@ -38,7 +41,7 @@ _WIRE_BY_MODEL = [ @pytest.mark.parametrize("model, expected", _WIRE_BY_MODEL) -@pytest.mark.parametrize("stale_api_mode", [None, "chat_completions", "codex_responses"]) +@pytest.mark.parametrize("stale_api_mode", [None, "chat_completions"]) def test_builtin_opencode_go_client_follows_the_model_not_the_persisted_mode(model, expected, stale_api_mode): client, resolved = aux.resolve_provider_client("opencode-go", model=model, api_mode=stale_api_mode) assert resolved == model @@ -49,7 +52,7 @@ def test_builtin_opencode_go_client_follows_the_model_not_the_persisted_mode(mod @pytest.mark.parametrize("model, expected", _WIRE_BY_MODEL) def test_named_custom_opencode_family_entry_follows_the_model(model, expected): - """An ``opencode-go-*`` custom entry persisted one api_mode at save time; each model still gets its own wire.""" + """An ``opencode-go-*`` custom entry without an api_mode of its own gets each model's wire, like main.""" client, resolved = aux.resolve_provider_client("custom:opencode-go-bridge", model=model) assert resolved == model assert type(client) is expected @@ -58,3 +61,11 @@ def test_named_custom_opencode_family_entry_follows_the_model(model, expected): else: base = client._real_client.base_url if expected is aux.CodexAuxiliaryClient else client.base_url assert str(base).rstrip("/") == "https://opencode.ai/zen/go/v1" + + +def test_named_custom_opencode_family_entry_with_declared_api_mode_is_honoured(): + """An entry that declares ``api_mode`` keeps it — the main runtime only re-derives when the entry has none.""" + client, resolved = aux.resolve_provider_client("custom:opencode-go-pinned", model="gpt-5.6-luna") + assert resolved == "gpt-5.6-luna" + assert type(client) is OpenAI + assert str(client.base_url).rstrip("/") == "https://opencode.ai/zen/go/v1"