Files
tradingagents/tests/test_openai_compatible_provider.py
Yijia-Xiao 20d3b0782f feat(llm): unify OpenAI-compatible providers behind a registry + generic endpoint
The OpenAI-compatible family (openai, xAI, DeepSeek, Qwen, GLM, MiniMax,
OpenRouter, Ollama) all speak the same Chat Completions API and differ only by
base_url, key, and two narrow wire-format quirks already isolated in subclasses.
Replace the scattered base-URL dict, key handling, and client-class branches with
one ProviderSpec registry that get_llm and the factory drive off; provider quirks
stay in their subclasses. Add a generic "openai_compatible" provider for any
OpenAI-compatible server (vLLM, LM Studio, llama.cpp, relays) via backend_url +
optional key — adding a provider is now one registry row. Native Anthropic/Google
keep their own clients (genuinely different APIs). Also fixes the env backend URL
being ignored when the provider was chosen interactively (#978).
2026-06-14 03:22:24 +00:00

76 lines
3.3 KiB
Python

"""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 os
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"