from __future__ import annotations import io import json import urllib.error from unittest.mock import patch import click import pytest from headroom.providers.copilot.wrap import ( build_launch_env, copilot_model_from_args, default_wire_api_for_model, detect_running_proxy_backend, is_auto_model, model_configured, model_prefers_responses_api, provider_key_source, query_proxy_config, resolve_provider_type, strip_auto_model_args, validate_configuration, ) def test_query_proxy_config_handles_success_and_invalid_payload() -> None: payload = io.BytesIO(json.dumps({"config": {"backend": "anyllm"}}).encode("utf-8")) payload_missing = io.BytesIO(json.dumps({"status": "ok"}).encode("utf-8")) with patch("urllib.request.urlopen", return_value=payload): assert query_proxy_config(8787) == {"backend": "anyllm"} with patch("urllib.request.urlopen", return_value=payload_missing): assert query_proxy_config(8787) is None with patch("urllib.request.urlopen", side_effect=urllib.error.URLError("down")): assert query_proxy_config(8787) is None def test_detect_running_proxy_backend_requires_string_backend(monkeypatch) -> None: monkeypatch.setattr( "headroom.providers.copilot.wrap.query_proxy_config", lambda port: {"backend": 123} if port == 8787 else None, ) assert detect_running_proxy_backend(8787) is None assert detect_running_proxy_backend(9999) is None def test_resolve_provider_type_prefers_explicit_and_env() -> None: assert ( resolve_provider_type( "anthropic", "openai", {"COPILOT_PROVIDER_TYPE": "anthropic", "HEADROOM_BACKEND": "anthropic"}, ) == "openai" ) assert ( resolve_provider_type( "anthropic", "auto", {"COPILOT_PROVIDER_TYPE": "openai", "HEADROOM_BACKEND": "anthropic"}, ) == "openai" ) assert ( resolve_provider_type( None, "auto", {"COPILOT_PROVIDER_TYPE": "not-a-provider", "HEADROOM_BACKEND": "anthropic"}, ) == "anthropic" ) assert resolve_provider_type(None, "auto", {"HEADROOM_BACKEND": "anthropic"}) == "anthropic" assert resolve_provider_type(None, "auto", {"HEADROOM_BACKEND": "anyllm"}) == "openai" def test_validate_configuration_accepts_supported_combinations() -> None: validate_configuration(provider_type="openai", wire_api="responses", backend=None) validate_configuration(provider_type="openai", wire_api="completions", backend="anyllm") def test_validate_configuration_rejects_invalid_combinations() -> None: with pytest.raises(click.ClickException, match="--wire-api is only valid"): validate_configuration(provider_type="anthropic", wire_api="responses", backend=None) with pytest.raises(click.ClickException, match="not supported with translated backends"): validate_configuration(provider_type="openai", wire_api="responses", backend="anyllm") @pytest.mark.parametrize( ("model", "expected"), [ ("gpt-5.5", True), ("gpt-5-codex", True), ("gpt-5.4", True), ("openai/gpt-5.4", True), ("o1", True), ("o3-mini", True), ("gpt-4.1", False), ("claude-sonnet-4.6", False), (None, False), ], ) def test_model_prefers_responses_api_for_reasoning_models( model: str | None, expected: bool, ) -> None: assert model_prefers_responses_api(model) is expected assert default_wire_api_for_model(model) == ("responses" if expected else "completions") def test_copilot_model_from_args_prefers_cli_over_environment() -> None: assert ( copilot_model_from_args( ("--model", "gpt-5.5"), {"COPILOT_MODEL": "gpt-4.1"}, ) == "gpt-5.5" ) assert ( copilot_model_from_args( ("--model=gpt-5-codex",), {"COPILOT_PROVIDER_MODEL_ID": "gpt-4.1"}, ) == "gpt-5-codex" ) assert copilot_model_from_args((), {"COPILOT_PROVIDER_MODEL_ID": "gpt-4.1"}) == "gpt-4.1" def test_provider_key_source_and_build_launch_env_cover_anthropic_and_openai() -> None: assert provider_key_source("anthropic") == "ANTHROPIC_API_KEY" assert provider_key_source("openai") == "OPENAI_API_KEY" anthropic_env, anthropic_lines = build_launch_env( port=8787, provider_type="anthropic", wire_api="responses", environ={ "ANTHROPIC_API_KEY": "sk-ant-test", "COPILOT_PROVIDER_WIRE_API": "stale", }, ) openai_env, openai_lines = build_launch_env( port=8787, provider_type="openai", wire_api=None, environ={"OPENAI_API_KEY": "sk-proj-test"}, ) assert anthropic_env["COPILOT_PROVIDER_TYPE"] == "anthropic" assert anthropic_env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787" assert anthropic_env["COPILOT_PROVIDER_API_KEY"] == "sk-ant-test" assert "COPILOT_PROVIDER_WIRE_API" not in anthropic_env assert anthropic_lines == [ "COPILOT_PROVIDER_TYPE=anthropic", "COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:8787", ] assert openai_env["COPILOT_PROVIDER_TYPE"] == "openai" assert openai_env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1" assert openai_env["COPILOT_PROVIDER_WIRE_API"] == "completions" assert openai_env["COPILOT_PROVIDER_API_KEY"] == "sk-proj-test" assert openai_lines[-1] == "COPILOT_PROVIDER_WIRE_API=completions" def test_build_launch_env_keeps_existing_provider_key_and_allows_missing_source_key() -> None: existing_env, _existing_lines = build_launch_env( port=8787, provider_type="openai", wire_api="responses", environ={ "COPILOT_PROVIDER_API_KEY": "existing-provider-key", "OPENAI_API_KEY": "sk-proj-test", }, ) missing_env, _missing_lines = build_launch_env( port=8787, provider_type="openai", wire_api="responses", environ={}, ) assert existing_env["COPILOT_PROVIDER_API_KEY"] == "existing-provider-key" assert "COPILOT_PROVIDER_API_KEY" not in missing_env def test_model_configured_detects_env_and_cli_variants() -> None: assert model_configured((), {"COPILOT_MODEL": "gpt-4o"}) is True assert model_configured(("--model", "gpt-4o"), {}) is True assert model_configured(("--model=gpt-4o",), {}) is True assert model_configured(("--other", "value"), {}) is False # ``auto`` is not a valid BYOK model — must be treated as unconfigured. assert model_configured(("--model", "auto"), {}) is False assert model_configured(("--model=auto",), {}) is False assert model_configured((), {"COPILOT_MODEL": "auto"}) is False assert model_configured((), {"COPILOT_PROVIDER_MODEL_ID": "auto"}) is False @pytest.mark.parametrize( ("model", "expected"), [ ("auto", True), ("Auto", True), ("AUTO", True), (" auto ", True), ("gpt-4o", False), ("gpt-5", False), ("claude-sonnet-4.6", False), (None, False), ("", False), ], ) def test_is_auto_model(model: str | None, expected: bool) -> None: assert is_auto_model(model) is expected @pytest.mark.parametrize( ("args", "expected"), [ # Strips --model auto (space-separated) ( ("--model", "auto", "--port", "8788"), ("--port", "8788"), ), # Strips --model=auto (equals form) ( ("--model=auto", "-p", "hello"), ("-p", "hello"), ), # Case-insensitive stripping ( ("--model", "AUTO", "--allow-all-tools"), ("--allow-all-tools",), ), # Leaves concrete models untouched ( ("--model", "gpt-4o", "--port", "8788"), ("--model", "gpt-4o", "--port", "8788"), ), # Leaves --model=gpt-4o untouched ( ("--model=gpt-4o",), ("--model=gpt-4o",), ), # Empty args unchanged ((), ()), # --model at end with no value (malformed) — leave as-is, don't crash (("--model",), ("--model",)), ], ) def test_strip_auto_model_args( args: tuple[str, ...], expected: tuple[str, ...], ) -> None: assert strip_auto_model_args(args) == expected def test_build_launch_env_applies_project_path_prefix() -> None: anthropic_env, _ = build_launch_env( port=8787, provider_type="anthropic", wire_api=None, environ={"ANTHROPIC_API_KEY": "sk-ant-test"}, project="api server", ) openai_env, openai_lines = build_launch_env( port=8787, provider_type="openai", wire_api=None, environ={"OPENAI_API_KEY": "sk-proj-test"}, project="api server", ) assert anthropic_env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/p/api%20server" assert openai_env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/p/api%20server/v1" assert "COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:8787/p/api%20server/v1" in openai_lines plain_env, _ = build_launch_env( port=8787, provider_type="openai", wire_api=None, environ={"OPENAI_API_KEY": "sk-proj-test"}, ) assert plain_env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"