"""Generic OpenAI-compatible provider (vLLM / LM Studio / llama.cpp / relays). Verifies the user-supplied base_url is required and honored, the key is optional (keyless local default), Chat Completions (not the Responses API) is used, any model name is accepted, and the env backend URL precedence (#978). """ import pytest from tradingagents.llm_clients.api_key_env import get_api_key_env from tradingagents.llm_clients.factory import create_llm_client from tradingagents.llm_clients.validators import validate_model # Note: assert by class NAME, not isinstance — other tests reload the # openai_client module, which would otherwise create a second class identity. @pytest.mark.unit def test_factory_routes_to_openai_client(): client = create_llm_client( provider="openai_compatible", model="my-model", base_url="http://localhost:8000/v1" ) assert type(client).__name__ == "OpenAIClient" @pytest.mark.unit def test_base_url_required(monkeypatch): monkeypatch.delenv("OPENAI_COMPATIBLE_API_KEY", raising=False) with pytest.raises(ValueError, match="requires a base_url"): create_llm_client(provider="openai_compatible", model="m").get_llm() @pytest.mark.unit def test_keyless_local_uses_placeholder_and_chat_completions(monkeypatch): monkeypatch.delenv("OPENAI_COMPATIBLE_API_KEY", raising=False) llm = create_llm_client( provider="openai_compatible", model="qwen2.5", base_url="http://localhost:8000/v1" ).get_llm() assert type(llm).__name__ == "NormalizedChatOpenAI" assert str(llm.openai_api_base) == "http://localhost:8000/v1" # keyless local servers: a placeholder key is sent key = llm.openai_api_key.get_secret_value() if hasattr(llm.openai_api_key, "get_secret_value") else llm.openai_api_key assert key == "EMPTY" # must use Chat Completions, not OpenAI's Responses API assert getattr(llm, "use_responses_api", False) in (False, None) @pytest.mark.unit def test_optional_key_from_env(monkeypatch): monkeypatch.setenv("OPENAI_COMPATIBLE_API_KEY", "sk-relay-123") llm = create_llm_client( provider="openai_compatible", model="m", base_url="https://relay.example/v1" ).get_llm() key = llm.openai_api_key.get_secret_value() if hasattr(llm.openai_api_key, "get_secret_value") else llm.openai_api_key assert key == "sk-relay-123" @pytest.mark.unit def test_any_model_accepted_no_forced_key(): assert validate_model("openai_compatible", "literally-anything") is True # The key env exists (read for keyed relays) but the provider is marked # key-optional, so the CLI never forces a prompt and keyless servers work. assert get_api_key_env("openai_compatible") == "OPENAI_COMPATIBLE_API_KEY" from tradingagents.llm_clients.openai_client import OPENAI_COMPATIBLE_PROVIDERS assert OPENAI_COMPATIBLE_PROVIDERS["openai_compatible"].key_optional is True @pytest.mark.unit def test_env_backend_url_precedence(): # #978: explicit env URL wins over the menu/default regardless of provider source. from cli.utils import resolve_backend_url assert resolve_backend_url("openai", "https://api.openai.com/v1", env_url="http://proxy/v1") == "http://proxy/v1" assert resolve_backend_url("openai", "https://api.openai.com/v1", env_url=None) == "https://api.openai.com/v1" assert resolve_backend_url("deepseek", None, None) == "https://api.deepseek.com"