This commit is contained in:
knowhy 2026-08-27 17:19:49 -04:00 • committed by GitHub
commit cb9ac4089d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 184 additions and 1 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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)