mirror of
https://github.com/usestrix/strix.git
synced 2026-08-28 05:25:00 +00:00
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:
parent
1c499c5b2d
commit
60f54a5363
3 changed files with 146 additions and 0 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,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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