mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/usestrix/strix.git
synced 2026-09-20 16:13:44 +08:00
* Add api_type field to LlmSettings Added 'api_type' field to LlmSettings for API path selection. * Refactor API type handling in models.py * Implement test for LlmSettings API type Add test for API type override settings in LlmSettings. * fix(tests): lint api_type test, cover the api_base override route, document STRIX_API_TYPE * fix(models): keep LiteLLM chat-completions tool schema when STRIX_API_TYPE=responses --------- Co-authored-by: RAJVARDHAN <95933896+vardhans07@users.noreply.github.com>
221 lines
7.8 KiB
Python
221 lines
7.8 KiB
Python
"""Tests for LLM model recommendation helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import litellm
|
|
import pytest
|
|
from agents.extensions.models.litellm_model import LitellmModel
|
|
from agents.model_settings import ModelSettings
|
|
from agents.models import _openai_shared
|
|
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
|
from agents.models.openai_responses import OpenAIResponsesModel
|
|
|
|
from strix.config.models import (
|
|
RECOMMENDED_MODEL_NAMES,
|
|
StrixProvider,
|
|
_NonStreamingModel,
|
|
_TurnGuardModel,
|
|
configure_sdk_model_defaults,
|
|
is_recommended_or_frontier_model,
|
|
request_timeout_extra_args,
|
|
routes_through_litellm,
|
|
supports_strict_tool_schemas,
|
|
uses_chat_completions_tool_schema,
|
|
)
|
|
from strix.config.settings import Settings
|
|
|
|
|
|
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
|
|
def test_recommended_models_are_accepted(model_name: str) -> None:
|
|
assert is_recommended_or_frontier_model(model_name)
|
|
|
|
|
|
def test_request_timeout_extra_args_positive() -> None:
|
|
assert request_timeout_extra_args(300) == {"timeout": 300}
|
|
assert request_timeout_extra_args(10) == {"timeout": 10}
|
|
|
|
|
|
def test_request_timeout_extra_args_survives_model_settings_json_dump() -> None:
|
|
"""The Chat Completions and LiteLLM paths pydantic-serialize ModelSettings for
|
|
their tracing span; a non-JSON-serializable timeout fails every turn there."""
|
|
settings = ModelSettings(extra_args=request_timeout_extra_args(300))
|
|
assert settings.to_json_dict()["extra_args"] == {"timeout": 300}
|
|
|
|
|
|
@pytest.mark.parametrize("value", [None, 0, -1])
|
|
def test_request_timeout_extra_args_disabled(value: float | None) -> None:
|
|
assert request_timeout_extra_args(value) is None
|
|
|
|
|
|
def test_recommended_models_are_matched_case_insensitively() -> None:
|
|
assert is_recommended_or_frontier_model("Vertex_AI/Gemini-3-Pro-Preview")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_name",
|
|
[
|
|
"gpt-5.5",
|
|
"chatgpt/gpt-5.4",
|
|
"litellm/openai/gpt-5.4-pro",
|
|
"azure_ai/gpt-5.5-pro",
|
|
"bedrock_mantle/openai.gpt-5.5",
|
|
"anthropic/claude-opus-5",
|
|
"anthropic/claude-opus-4-8",
|
|
"anthropic.claude-opus-4-8",
|
|
"anthropic/claude-opus-4-7",
|
|
"anthropic/claude-fable-5",
|
|
"anthropic/claude-sonnet-5",
|
|
"vertex_ai/claude-sonnet-5@default",
|
|
"vertex_ai/claude-sonnet-4-6@default",
|
|
"any-llm/anthropic/claude-sonnet-4-6",
|
|
"vertex_ai/gemini-3.1-pro-preview",
|
|
"openrouter/google/gemini-3.1-pro-preview",
|
|
"deepseek/deepseek-v4-pro",
|
|
"deepseek/deepseek-r1-0528",
|
|
"deepseek/deepseek-reasoner",
|
|
"dashscope/qwen3-max-2026-01-23",
|
|
"qwen3.7-max",
|
|
"dashscope/qwen3.8-max",
|
|
"moonshot/kimi-k2.6",
|
|
"kimi-k2.7-code",
|
|
"moonshot/kimi-k3",
|
|
"anthropic/claude-fable-5-1",
|
|
"vertex_ai/claude-fable-5-1@default",
|
|
"gemini/gemini-3.7-flash",
|
|
"glm-5.3",
|
|
"zai/glm-5.3-flash",
|
|
"openrouter/z-ai/glm-5.3",
|
|
"novita/zai-org/glm-5.2",
|
|
"openai/glm-5.3",
|
|
"openai/zai-org/glm-5.3",
|
|
"hosted_vllm/glm-5.3",
|
|
"openai/claude-opus-4-8",
|
|
"openai/deepseek-v4-pro",
|
|
"custom-ollama/gpt-5-mini-local",
|
|
"custom-provider/claude-opus-4-local",
|
|
"custom-provider/glm-5.3-local",
|
|
],
|
|
)
|
|
def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
|
assert is_recommended_or_frontier_model(model_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_name",
|
|
[
|
|
"",
|
|
"openai/gpt-4.1",
|
|
"anthropic/claude-3-5-sonnet-latest",
|
|
"ollama/llama3.1",
|
|
"deepseek/deepseek-chat",
|
|
"xai/grok-4.5",
|
|
"openrouter/x-ai/grok-4",
|
|
"mistral/mistral-medium-3-5",
|
|
"mistral/magistral-medium-latest",
|
|
"zai/glm-4.7",
|
|
"openai/glm-4.7",
|
|
"openrouter/z-ai/glm-5",
|
|
],
|
|
)
|
|
def test_non_frontier_models_are_rejected(model_name: str) -> None:
|
|
assert not is_recommended_or_frontier_model(model_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_name",
|
|
[
|
|
"anthropic/claude-sonnet-4-6",
|
|
"bedrock/anthropic.claude-opus-4-8-v1:0",
|
|
"vertex_ai/claude-sonnet-5",
|
|
"Sonnet-5",
|
|
],
|
|
)
|
|
def test_claude_routes_reject_strict_tool_schemas(model_name: str) -> None:
|
|
assert not supports_strict_tool_schemas(model_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_name",
|
|
["openai/gpt-5.4", "gpt-5.4", "gemini/gemini-3.1-pro-preview", "deepseek/deepseek-v4"],
|
|
)
|
|
def test_other_routes_keep_strict_tool_schemas(model_name: str) -> None:
|
|
assert supports_strict_tool_schemas(model_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model_name", "litellm"),
|
|
[
|
|
("claude-sonnet-4-5", False),
|
|
("openai/claude-sonnet-4-5", False),
|
|
("any-llm/anthropic/claude-sonnet-4-5", False),
|
|
("anthropic/claude-sonnet-4-5", True),
|
|
("litellm/anthropic/claude-sonnet-4-5", True),
|
|
("bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0", True),
|
|
("ollama/llama3", True),
|
|
],
|
|
)
|
|
def test_routes_through_litellm_matches_the_provider(
|
|
monkeypatch: pytest.MonkeyPatch, model_name: str, litellm: bool
|
|
) -> None:
|
|
"""The helper must agree with what StrixProvider actually builds.
|
|
|
|
Callers use it to decide whether a LiteLLM-only request field is safe to
|
|
attach; on the SDK's own clients such a field raises TypeError mid-turn, so
|
|
drift here breaks every request on that route.
|
|
"""
|
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
|
assert routes_through_litellm(model_name) is litellm
|
|
try:
|
|
model = StrixProvider().get_model(model_name)
|
|
except ImportError:
|
|
# any-llm's client is an optional dependency; reaching it at all already
|
|
# proves the route is not LiteLLM's.
|
|
assert not litellm
|
|
return
|
|
while isinstance(model, _NonStreamingModel | _TurnGuardModel):
|
|
model = model._inner
|
|
assert isinstance(model, LitellmModel) is litellm
|
|
|
|
|
|
def test_api_type_override_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("STRIX_LLM", "gpt-4")
|
|
monkeypatch.setenv("STRIX_API_TYPE", "chat_completions")
|
|
assert uses_chat_completions_tool_schema("gpt-4", Settings()) is True
|
|
monkeypatch.setenv("STRIX_LLM", "openai/gpt-4")
|
|
monkeypatch.setenv("STRIX_API_TYPE", "responses")
|
|
assert uses_chat_completions_tool_schema("openai/gpt-4", Settings()) is False
|
|
monkeypatch.setenv("STRIX_LLM", "anthropic/claude-sonnet-4-5")
|
|
assert uses_chat_completions_tool_schema("anthropic/claude-sonnet-4-5", Settings()) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("api_type", "expected"),
|
|
[
|
|
(None, OpenAIChatCompletionsModel),
|
|
("chat_completions", OpenAIChatCompletionsModel),
|
|
("responses", OpenAIResponsesModel),
|
|
],
|
|
)
|
|
def test_api_type_overrides_the_api_base_route(
|
|
monkeypatch: pytest.MonkeyPatch, api_type: str | None, expected: type
|
|
) -> None:
|
|
"""``LLM_API_BASE`` defaults to chat completions. ``STRIX_API_TYPE`` must win."""
|
|
monkeypatch.setattr(_openai_shared, "_use_responses_by_default", True)
|
|
monkeypatch.setattr(_openai_shared, "_default_openai_client", None)
|
|
monkeypatch.setattr(_openai_shared, "_default_openai_key", None)
|
|
monkeypatch.setattr(litellm, "api_key", None)
|
|
monkeypatch.setattr(litellm, "api_base", None)
|
|
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
|
|
monkeypatch.setenv("OPENAI_BASE_URL", "")
|
|
monkeypatch.setenv("STRIX_LLM", "gpt-5")
|
|
monkeypatch.setenv("LLM_API_KEY", "test-key")
|
|
monkeypatch.setenv("LLM_API_BASE", "https://gateway.example/v1")
|
|
monkeypatch.delenv("STRIX_API_TYPE", raising=False)
|
|
if api_type is not None:
|
|
monkeypatch.setenv("STRIX_API_TYPE", api_type)
|
|
configure_sdk_model_defaults(Settings())
|
|
model = StrixProvider().get_model("gpt-5")
|
|
while isinstance(model, _NonStreamingModel | _TurnGuardModel):
|
|
model = model._inner
|
|
assert isinstance(model, expected)
|