mirror of
https://github.com/usestrix/strix.git
synced 2026-10-09 03:18:31 +00:00
Merge bcfa922cf5 into 717ffc8f4c
This commit is contained in:
commit
cb9ac4089d
5 changed files with 184 additions and 1 deletions
|
|
@ -466,6 +466,11 @@ class StrixProvider(MultiProvider):
|
|||
)
|
||||
if prefix == "ollama" and stripped_model_name:
|
||||
return self._get_fallback_provider("litellm"), f"ollama_chat/{stripped_model_name}"
|
||||
if prefix == "llmtr" and stripped_model_name:
|
||||
# LLMTR is an OpenAI-compatible gateway, so route llmtr/<model>
|
||||
# through LiteLLM's openai/ provider. The gateway base URL and
|
||||
# attribution headers are configured in _configure_llmtr_routing.
|
||||
return self._get_fallback_provider("litellm"), f"openai/{stripped_model_name}"
|
||||
return self._get_fallback_provider("litellm"), original_model_name
|
||||
|
||||
def get_model(self, model_name: str | None) -> Model:
|
||||
|
|
@ -560,6 +565,7 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
|||
return
|
||||
_configure_litellm_compatibility()
|
||||
_configure_openrouter_attribution(llm.model)
|
||||
_configure_llmtr_routing(llm.model, llm.api_base)
|
||||
if llm.api_key:
|
||||
set_default_openai_key(llm.api_key, use_for_tracing=False)
|
||||
_configure_litellm_default("api_key", llm.api_key)
|
||||
|
|
@ -679,6 +685,39 @@ def _configure_openrouter_attribution(model_name: str | None) -> None:
|
|||
litellm.headers = {**existing, **OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
|
||||
|
||||
|
||||
LLMTR_API_BASE = "https://llmtr.com/v1"
|
||||
|
||||
LLMTR_ATTRIBUTION_HEADERS = {
|
||||
"HTTP-Referer": "https://strix.ai",
|
||||
"X-Title": "Strix",
|
||||
}
|
||||
|
||||
|
||||
def is_llmtr_model(model_name: str | None) -> bool:
|
||||
return bool(model_name) and (model_name or "").strip().lower().startswith("llmtr/")
|
||||
|
||||
|
||||
def _configure_llmtr_routing(model_name: str | None, api_base: str | None) -> None:
|
||||
"""Make ``llmtr/`` a first-class prefix for the LLMTR gateway.
|
||||
|
||||
LLMTR (https://llmtr.com) is a Turkey-hosted OpenAI-compatible gateway, so
|
||||
``StrixProvider`` resolves ``llmtr/<model>`` to LiteLLM's ``openai/<model>``
|
||||
route. That route needs the gateway base URL, which is supplied here (an
|
||||
explicit ``LLM_API_BASE`` still wins, for a proxy in front of LLMTR), along
|
||||
with Strix attribution headers so scans are identifiable in the LLMTR
|
||||
dashboard. Users set only ``STRIX_LLM`` and ``LLM_API_KEY``.
|
||||
"""
|
||||
if not is_llmtr_model(model_name):
|
||||
return
|
||||
import litellm
|
||||
|
||||
if not api_base:
|
||||
_configure_litellm_default("api_base", LLMTR_API_BASE)
|
||||
current: object = litellm.headers
|
||||
existing: dict[str, str] = current if isinstance(current, dict) else {}
|
||||
litellm.headers = {**existing, **LLMTR_ATTRIBUTION_HEADERS} # type: ignore[assignment]
|
||||
|
||||
|
||||
def _configure_extra_headers(llm: LlmSettings) -> None:
|
||||
"""Send user-provided default headers on every LLM request.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ from openai.types.shared import Reasoning
|
|||
|
||||
from strix.config.models import (
|
||||
DEFAULT_MODEL_RETRY,
|
||||
LLMTR_ATTRIBUTION_HEADERS,
|
||||
OPENROUTER_ATTRIBUTION_HEADERS,
|
||||
bedrock_route_supports_prompt_caching,
|
||||
is_bedrock_route,
|
||||
is_claude_model,
|
||||
is_known_openai_bare_model,
|
||||
is_llmtr_model,
|
||||
is_openrouter_model,
|
||||
model_supports_reasoning,
|
||||
request_timeout_extra_args,
|
||||
|
|
@ -288,6 +290,8 @@ def _request_headers(
|
|||
headers: dict[str, str] = {}
|
||||
if is_openrouter_model(model_name):
|
||||
headers.update(OPENROUTER_ATTRIBUTION_HEADERS)
|
||||
if is_llmtr_model(model_name):
|
||||
headers.update(LLMTR_ATTRIBUTION_HEADERS)
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers or None
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ from openai.types.responses import ResponseOutputMessage
|
|||
|
||||
from strix.config import load_settings
|
||||
from strix.config.models import (
|
||||
LLMTR_API_BASE,
|
||||
StrixProvider,
|
||||
configure_sdk_model_defaults,
|
||||
is_llmtr_model,
|
||||
)
|
||||
from strix.core.inputs import make_model_settings
|
||||
from strix.report.state import get_global_report_state
|
||||
|
|
@ -45,6 +47,12 @@ def _dedupe_extra_args(dedupe: DedupeSettings) -> dict[str, str]:
|
|||
extra["api_key"] = dedupe.api_key.strip()
|
||||
if dedupe.api_base and dedupe.api_base.strip():
|
||||
extra["api_base"] = dedupe.api_base.strip()
|
||||
elif is_llmtr_model(dedupe.model):
|
||||
# A dedicated llmtr/ dedupe model reaches the LLMTR gateway only through
|
||||
# its endpoint. The global default is derived from the main model, which
|
||||
# may be a different provider, so pin the gateway per call here rather
|
||||
# than relying on the shared litellm.api_base.
|
||||
extra["api_base"] = LLMTR_API_BASE
|
||||
return extra
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from strix.config import loader
|
||||
from strix.config.settings import DedupeSettings
|
||||
from strix.report.dedupe import _dedupe_model_settings
|
||||
from strix.report.dedupe import _dedupe_extra_args, _dedupe_model_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -44,6 +44,35 @@ def test_dedupe_endpoint_sent_per_call() -> None:
|
|||
assert (settings.extra_args or {})["api_key"] == "dedupe-key"
|
||||
|
||||
|
||||
def test_llmtr_dedupe_model_pins_gateway_endpoint_per_call() -> None:
|
||||
# A dedicated llmtr/ dedupe model reaches the gateway only via its endpoint.
|
||||
# The global default is keyed to the main model, which may be a different
|
||||
# provider, so the endpoint must ride on the dedupe request itself.
|
||||
dedupe = DedupeSettings(
|
||||
STRIX_DEDUPE_MODEL="llmtr/openai/gpt-5.5",
|
||||
DEDUPE_LLM_API_KEY="llmtr-key",
|
||||
)
|
||||
extra = _dedupe_extra_args(dedupe)
|
||||
assert extra["api_base"] == "https://llmtr.com/v1"
|
||||
assert extra["api_key"] == "llmtr-key"
|
||||
|
||||
|
||||
def test_llmtr_dedupe_model_respects_explicit_endpoint() -> None:
|
||||
# An explicit DEDUPE_LLM_API_BASE (e.g. a proxy in front of LLMTR) wins over
|
||||
# the gateway default.
|
||||
dedupe = DedupeSettings(
|
||||
STRIX_DEDUPE_MODEL="llmtr/openai/gpt-5.5",
|
||||
DEDUPE_LLM_API_KEY="llmtr-key",
|
||||
DEDUPE_LLM_API_BASE="https://proxy.example/v1",
|
||||
)
|
||||
assert _dedupe_extra_args(dedupe)["api_base"] == "https://proxy.example/v1"
|
||||
|
||||
|
||||
def test_non_llmtr_dedupe_model_gets_no_injected_endpoint() -> None:
|
||||
dedupe = DedupeSettings(STRIX_DEDUPE_MODEL="openai/gpt-5.5", DEDUPE_LLM_API_KEY="k")
|
||||
assert "api_base" not in _dedupe_extra_args(dedupe)
|
||||
|
||||
|
||||
def test_dedicated_dedupe_model_uses_own_headers_not_main() -> None:
|
||||
dedupe = DedupeSettings(
|
||||
STRIX_DEDUPE_MODEL="deepseek/cheap",
|
||||
|
|
|
|||
|
|
@ -2,11 +2,18 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
from strix.config.models import (
|
||||
LLMTR_API_BASE,
|
||||
RECOMMENDED_MODEL_NAMES,
|
||||
StrixProvider,
|
||||
_configure_llmtr_routing,
|
||||
is_llmtr_model,
|
||||
is_recommended_or_frontier_model,
|
||||
request_timeout_extra_args,
|
||||
supports_strict_tool_schemas,
|
||||
|
|
@ -112,3 +119,99 @@ def test_claude_routes_reject_strict_tool_schemas(model_name: str) -> None:
|
|||
)
|
||||
def test_other_routes_keep_strict_tool_schemas(model_name: str) -> None:
|
||||
assert supports_strict_tool_schemas(model_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model_name", "expected"),
|
||||
[
|
||||
("llmtr/anthropic/claude-sonnet-5", True),
|
||||
("llmtr/openai/gpt-5.5", True),
|
||||
("llmtr/llmtr/gemma-4", True),
|
||||
("LLMTR/openai/gpt-5.5", True),
|
||||
(" llmtr/openai/gpt-5.5 ", True),
|
||||
("openrouter/anthropic/claude-sonnet-5", False),
|
||||
("openai/gpt-5.4", False),
|
||||
("anthropic/claude-sonnet-5", False),
|
||||
("", False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
def test_is_llmtr_model(model_name: str | None, expected: bool) -> None:
|
||||
assert bool(is_llmtr_model(model_name)) is expected
|
||||
|
||||
|
||||
def test_llmtr_prefix_routes_through_openai_compatible_litellm() -> None:
|
||||
provider = StrixProvider()
|
||||
|
||||
resolved_provider, resolved_model = provider._resolve_prefixed_model(
|
||||
original_model_name="llmtr/anthropic/claude-sonnet-5",
|
||||
prefix="llmtr",
|
||||
stripped_model_name="anthropic/claude-sonnet-5",
|
||||
)
|
||||
|
||||
assert resolved_model == "openai/anthropic/claude-sonnet-5"
|
||||
assert type(resolved_provider).__name__ == "LitellmProvider"
|
||||
|
||||
|
||||
def test_llmtr_prefix_preserves_nested_gateway_namespace() -> None:
|
||||
provider = StrixProvider()
|
||||
|
||||
_resolved_provider, resolved_model = provider._resolve_prefixed_model(
|
||||
original_model_name="llmtr/llmtr/gemma-4",
|
||||
prefix="llmtr",
|
||||
stripped_model_name="llmtr/gemma-4",
|
||||
)
|
||||
|
||||
assert resolved_model == "openai/llmtr/gemma-4"
|
||||
|
||||
|
||||
def test_configure_llmtr_routing_sets_base_url_and_attribution() -> None:
|
||||
saved_base, saved_headers = litellm.api_base, litellm.headers
|
||||
try:
|
||||
litellm.api_base = None
|
||||
litellm.headers = None
|
||||
_configure_llmtr_routing("llmtr/anthropic/claude-sonnet-5", None)
|
||||
assert litellm.api_base == LLMTR_API_BASE
|
||||
headers = cast("dict[str, str]", litellm.headers)
|
||||
assert headers["HTTP-Referer"] == "https://strix.ai"
|
||||
assert headers["X-Title"] == "Strix"
|
||||
finally:
|
||||
litellm.api_base, litellm.headers = saved_base, saved_headers
|
||||
|
||||
|
||||
def test_configure_llmtr_routing_respects_explicit_api_base() -> None:
|
||||
saved_base, saved_headers = litellm.api_base, litellm.headers
|
||||
try:
|
||||
litellm.api_base = None
|
||||
litellm.headers = None
|
||||
_configure_llmtr_routing("llmtr/anthropic/claude-sonnet-5", "https://proxy/v1")
|
||||
# An explicit LLM_API_BASE wins; the LLMTR default must not override it.
|
||||
assert litellm.api_base is None
|
||||
headers = cast("dict[str, str]", litellm.headers)
|
||||
assert headers["X-Title"] == "Strix"
|
||||
finally:
|
||||
litellm.api_base, litellm.headers = saved_base, saved_headers
|
||||
|
||||
|
||||
def test_configure_llmtr_routing_is_noop_for_other_providers() -> None:
|
||||
saved_base, saved_headers = litellm.api_base, litellm.headers
|
||||
try:
|
||||
litellm.api_base = None
|
||||
litellm.headers = None
|
||||
_configure_llmtr_routing("openrouter/anthropic/claude-sonnet-5", None)
|
||||
assert litellm.api_base is None
|
||||
assert litellm.headers is None
|
||||
finally:
|
||||
litellm.api_base, litellm.headers = saved_base, saved_headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"llmtr/anthropic/claude-opus-5",
|
||||
"llmtr/anthropic/claude-sonnet-5",
|
||||
"llmtr/openai/gpt-5.5",
|
||||
],
|
||||
)
|
||||
def test_llmtr_frontier_models_are_accepted(model_name: str) -> None:
|
||||
assert is_recommended_or_frontier_model(model_name)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue