Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions strix/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,11 @@ def _resolve_prefixed_model(
)
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}"
Comment on lines +469 to +473

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 LLMTR prefix bypasses reasoning detection

When an LLMTR-hosted reasoning model is used with STRIX_REASONING_EFFORT, capability detection examines the original llmtr/<provider>/<model> name without removing the new prefix, so make_model_settings silently omits the configured reasoning effort.

Knowledge Base Used: Configuration and Telemetry

Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/config/models.py
Line: 469-473

Comment:
**LLMTR prefix bypasses reasoning detection**

When an LLMTR-hosted reasoning model is used with `STRIX_REASONING_EFFORT`, capability detection examines the original `llmtr/<provider>/<model>` name without removing the new prefix, so `make_model_settings` silently omits the configured reasoning effort.

**Knowledge Base Used:** [Configuration and Telemetry](https://app.greptile.com/strix-org-3/-/custom-context/knowledge-base/usestrix/strix/-/docs/telemetry-and-config.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

return self._get_fallback_provider("litellm"), original_model_name

def get_model(self, model_name: str | None) -> Model:
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Global routing ignores dedupe model

When the main and deduplication models use different providers and exactly one uses llmtr/, _configure_llmtr_routing configures LiteLLM's process-global endpoint from only the main model before the independently selected deduplication model is resolved, causing deduplication requests to reach the wrong gateway and fail authentication or model lookup.

Knowledge Base Used: Configuration and Telemetry

Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/config/models.py
Line: 568

Comment:
**Global routing ignores dedupe model**

When the main and deduplication models use different providers and exactly one uses `llmtr/`, `_configure_llmtr_routing` configures LiteLLM's process-global endpoint from only the main model before the independently selected deduplication model is resolved, causing deduplication requests to reach the wrong gateway and fail authentication or model lookup.

**Knowledge Base Used:** [Configuration and Telemetry](https://app.greptile.com/strix-org-3/-/custom-context/knowledge-base/usestrix/strix/-/docs/telemetry-and-config.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

if llm.api_key:
set_default_openai_key(llm.api_key, use_for_tracing=False)
_configure_litellm_default("api_key", llm.api_key)
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions strix/core/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@

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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions strix/report/dedupe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

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
Expand Down Expand Up @@ -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


Expand Down
31 changes: 30 additions & 1 deletion tests/test_dedupe_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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:
Expand Down Expand Up @@ -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",
Expand Down
103 changes: 103 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)