feat(llm): add first-class llmtr/ provider prefix for the LLMTR gateway

Make LLMTR (https://llmtr.com) a first-class option alongside OpenRouter:
users set STRIX_LLM="llmtr/<model>" + LLM_API_KEY (llmtr-...) and nothing
else. LLMTR is a Turkey-hosted OpenAI-compatible gateway, so StrixProvider
resolves llmtr/<model> to LiteLLM's openai/<model> route, and
_configure_llmtr_routing supplies the gateway base URL (an explicit
LLM_API_BASE still wins) plus Strix attribution headers. Attribution is
also sent per-request via _request_headers, mirroring OpenRouter.

Adds unit tests for prefix detection, routing resolution, base-URL/header
configuration, explicit-base override, the non-LLMTR no-op path, and
frontier-model detection through the llmtr/ prefix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag2XdAsvrR7XKMmQsAjqSN
This commit is contained in:
knowhycodata 2026-08-22 15:24:43 +03:00
parent 1c499c5b2d
commit 60f54a5363
3 changed files with 146 additions and 0 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,
@ -271,6 +273,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

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