mirror of
https://github.com/usestrix/strix.git
synced 2026-10-16 04:28:23 +00:00
feat(llm): extra request-body fields via LLM_EXTRA_BODY
Some provider parameters, such as OpenAI's service_tier and safety_identifier, are read only from the request body, so LLM_EXTRA_HEADERS can't set them. LLM_EXTRA_BODY merges a JSON object into the body of every request to the configured model, on both routing paths. A dedicated dedupe model gets its own DEDUPE_LLM_EXTRA_BODY instead, as with headers.
This commit is contained in:
parent
4695290682
commit
0384d3c3c6
19 changed files with 322 additions and 13 deletions
|
|
@ -33,6 +33,18 @@ Configure Strix using environment variables or a config file.
|
|||
the LiteLLM and native OpenAI routing paths.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_EXTRA_BODY" type="string">
|
||||
Extra fields merged into the JSON body of every request to the main model, as a JSON object
|
||||
(e.g. `{"service_tier":"flex","safety_identifier":"9f86d081..."}`). Use it
|
||||
for provider parameters Strix doesn't expose, such as OpenAI's `service_tier`
|
||||
or `safety_identifier`. Applies to both the LiteLLM and native OpenAI routing
|
||||
paths. Fields are not validated, so the provider or gateway must accept them;
|
||||
unknown fields are usually rejected. Extra fields can override fields Strix
|
||||
sends and break requests, so don't set request-shaping fields such as `model`, `messages`, `input`,
|
||||
`tools` or `stream`, and use `STRIX_REASONING_EFFORT` rather than
|
||||
`reasoning_effort`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_TIMEOUT" default="300" type="integer">
|
||||
Request timeout in seconds for LLM calls.
|
||||
</ParamField>
|
||||
|
|
@ -88,6 +100,12 @@ affecting the agents that do the actual testing.
|
|||
inherits `LLM_EXTRA_HEADERS`; set this when its endpoint needs custom headers.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DEDUPE_LLM_EXTRA_BODY" type="string">
|
||||
Optional JSON object of extra request-body fields sent on every
|
||||
deduplication-model request. A dedicated dedupe model never inherits
|
||||
`LLM_EXTRA_BODY`; set this when its endpoint needs them too.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_DEDUPE_REASONING_EFFORT" type="string">
|
||||
Reasoning effort for the deduplication model. Defaults to the model's own
|
||||
baseline when unset.
|
||||
|
|
|
|||
|
|
@ -68,6 +68,21 @@ export LLM_API_KEY="your-bearer-token" # sent as Authorization: Bearer
|
|||
export LLM_EXTRA_HEADERS='{"X-Feature-Key":"value","X-Tenant":"acme"}'
|
||||
```
|
||||
|
||||
### Extra request-body fields
|
||||
|
||||
Some provider parameters go in the request body rather than a header, and
|
||||
Strix has no dedicated setting for them. Set them with `LLM_EXTRA_BODY` as a
|
||||
JSON object — they are merged into the body of every request to the model. For example, to
|
||||
use OpenAI's cheaper flex processing and attribute requests to a user with a
|
||||
safety identifier (a hash, never a raw email):
|
||||
|
||||
```bash
|
||||
export LLM_EXTRA_BODY='{"service_tier":"flex","safety_identifier":"9f86d081884c7d65"}'
|
||||
```
|
||||
|
||||
Fields aren't validated, so use only fields your provider accepts, and don't
|
||||
set fields Strix already sends, such as `model`, `messages` or `stream`.
|
||||
|
||||
For endpoints behind a private CA, point Strix at your certificate bundle with
|
||||
the standard `SSL_CERT_FILE=/path/to/ca-bundle.pem` — never disable TLS
|
||||
verification against a real endpoint.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import AliasChoices, Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
|
@ -55,6 +55,11 @@ class LlmSettings(BaseSettings):
|
|||
alias="LLM_EXTRA_HEADERS",
|
||||
repr=False,
|
||||
)
|
||||
extra_body: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
alias="LLM_EXTRA_BODY",
|
||||
repr=False,
|
||||
)
|
||||
reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT")
|
||||
force_required_tool_choice: bool = Field(
|
||||
default=False,
|
||||
|
|
@ -110,6 +115,11 @@ class DedupeSettings(BaseSettings):
|
|||
alias="DEDUPE_LLM_EXTRA_HEADERS",
|
||||
repr=False,
|
||||
)
|
||||
extra_body: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
alias="DEDUPE_LLM_EXTRA_BODY",
|
||||
repr=False,
|
||||
)
|
||||
|
||||
@field_validator("reasoning_effort", mode="before")
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ def make_model_settings(
|
|||
request_timeout: float | None = None,
|
||||
prompt_cache: bool = True,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
has_tools: bool = True,
|
||||
) -> ModelSettings:
|
||||
headers = _request_headers(model_name, extra_headers)
|
||||
|
|
@ -259,13 +260,16 @@ def make_model_settings(
|
|||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
extra_headers=headers,
|
||||
extra_body=dict(extra_body) if extra_body else None,
|
||||
)
|
||||
if (
|
||||
reasoning_effort is not None
|
||||
and reasoning_effort != "none"
|
||||
and model_supports_reasoning(model_name)
|
||||
):
|
||||
model_settings = model_settings.resolve(_reasoning_settings(reasoning_effort))
|
||||
model_settings = model_settings.resolve(
|
||||
_reasoning_settings(reasoning_effort, extra_body),
|
||||
)
|
||||
if force_required_tool_choice and _accepts_required_tool_choice(model_name):
|
||||
model_settings = model_settings.resolve(ModelSettings(tool_choice="required"))
|
||||
|
||||
|
|
@ -290,7 +294,9 @@ def _request_headers(
|
|||
return headers or None
|
||||
|
||||
|
||||
def _reasoning_settings(effort: ReasoningEffort) -> ModelSettings:
|
||||
def _reasoning_settings(
|
||||
effort: ReasoningEffort, extra_body: dict[str, Any] | None = None
|
||||
) -> ModelSettings:
|
||||
"""``max`` is not in the OpenAI SDK's ``Reasoning.effort`` enum, so send it as
|
||||
a raw body field instead — also keeping it clear of LiteLLM's DeepSeek mapping,
|
||||
which collapses every ``reasoning_effort`` level to plain thinking-enabled.
|
||||
|
|
@ -298,11 +304,13 @@ def _reasoning_settings(effort: ReasoningEffort) -> ModelSettings:
|
|||
|
||||
It goes in ``extra_body``, the field every model implementation forwards as the
|
||||
request's ``extra_body``; the same value under ``extra_args`` collides with that
|
||||
keyword and raises before a request is ever sent.
|
||||
keyword and raises before a request is ever sent. ``resolve`` replaces
|
||||
``extra_body`` wholesale, so the user's ``LLM_EXTRA_BODY`` fields are carried
|
||||
over, with ``reasoning_effort`` taking precedence.
|
||||
"""
|
||||
if effort != "max":
|
||||
return ModelSettings(reasoning=Reasoning(effort=effort))
|
||||
return ModelSettings(extra_body={"reasoning_effort": "max"})
|
||||
return ModelSettings(extra_body={**(extra_body or {}), "reasoning_effort": "max"})
|
||||
|
||||
|
||||
def _prompt_cache_extra_args(model_name: str) -> dict[str, Any] | None:
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ async def run_strix_scan(
|
|||
request_timeout=settings.llm.timeout,
|
||||
prompt_cache=settings.llm.prompt_cache,
|
||||
extra_headers=settings.llm.extra_headers,
|
||||
extra_body=settings.llm.extra_body,
|
||||
)
|
||||
run_config = RunConfig(
|
||||
model=resolved_model,
|
||||
|
|
|
|||
|
|
@ -164,12 +164,13 @@ async def warm_up_llm() -> None:
|
|||
raw_model = dedupe_model
|
||||
deduper = resolve_dedupe_model(settings.dedupe, dedupe_model)
|
||||
# A dedicated dedupe model may route to another provider, which must
|
||||
# never receive the main endpoint's headers; it has its own
|
||||
# DEDUPE_LLM_EXTRA_HEADERS.
|
||||
# never receive the main endpoint's headers or body fields; it has
|
||||
# its own DEDUPE_LLM_EXTRA_HEADERS and DEDUPE_LLM_EXTRA_BODY.
|
||||
await preflight_request(
|
||||
deduper,
|
||||
model_name=dedupe_model,
|
||||
extra_headers=settings.dedupe.extra_headers,
|
||||
extra_body=settings.dedupe.extra_body,
|
||||
timeout=llm.preflight_timeout,
|
||||
api_base_setting="DEDUPE_LLM_API_BASE",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ async def preflight_model_connection(
|
|||
model,
|
||||
model_name=model_name,
|
||||
extra_headers=resolved_settings.llm.extra_headers,
|
||||
extra_body=resolved_settings.llm.extra_body,
|
||||
timeout=resolved_settings.llm.preflight_timeout,
|
||||
api_base_setting="LLM_API_BASE",
|
||||
)
|
||||
|
|
@ -126,6 +127,7 @@ async def preflight_request(
|
|||
*,
|
||||
model_name: str,
|
||||
extra_headers: dict[str, str] | None,
|
||||
extra_body: dict[str, Any] | None,
|
||||
timeout: int,
|
||||
api_base_setting: str,
|
||||
) -> None:
|
||||
|
|
@ -144,6 +146,7 @@ async def preflight_request(
|
|||
request_timeout=timeout,
|
||||
prompt_cache=False,
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
has_tools=False,
|
||||
)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ async def _summarize(model: str, prompt: str, max_tokens: int) -> str | None:
|
|||
request_timeout=llm.timeout,
|
||||
prompt_cache=False,
|
||||
extra_headers=llm.extra_headers,
|
||||
extra_body=llm.extra_body,
|
||||
has_tools=False,
|
||||
).resolve(ModelSettings(max_tokens=max_tokens))
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -39,11 +39,13 @@ def _dedupe_model_settings(
|
|||
model_name=model_name,
|
||||
force_required_tool_choice=False,
|
||||
request_timeout=request_timeout,
|
||||
# The main model's headers apply only when dedupe falls back to the main
|
||||
# model; a dedicated dedupe model may route to another provider, which
|
||||
# must never receive the main endpoint's credentials. A dedicated model
|
||||
# gets its own DEDUPE_LLM_EXTRA_HEADERS instead.
|
||||
# The main model's headers and body fields apply only when dedupe falls
|
||||
# back to the main model; a dedicated dedupe model may route to another
|
||||
# provider, which must never receive the main endpoint's credentials or
|
||||
# identifiers. A dedicated model gets its own DEDUPE_LLM_EXTRA_HEADERS
|
||||
# and DEDUPE_LLM_EXTRA_BODY instead.
|
||||
extra_headers=dedupe.extra_headers if dedupe.model else llm.extra_headers,
|
||||
extra_body=dedupe.extra_body if dedupe.model else llm.extra_body,
|
||||
has_tools=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,9 @@ def _patch_budget(monkeypatch: pytest.MonkeyPatch, *, keep_tokens: int, window:
|
|||
context.auto_compact = True
|
||||
settings = SimpleNamespace(
|
||||
context=context,
|
||||
llm=SimpleNamespace(api_key=None, api_base=None, timeout=1, extra_headers=None),
|
||||
llm=SimpleNamespace(
|
||||
api_key=None, api_base=None, timeout=1, extra_headers=None, extra_body=None
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(compaction, "load_settings", lambda: settings)
|
||||
|
||||
|
|
@ -228,7 +230,11 @@ async def test_summarize_routes_through_provider_with_settings(
|
|||
"load_settings",
|
||||
lambda: SimpleNamespace(
|
||||
llm=SimpleNamespace(
|
||||
api_key=None, api_base=None, timeout=1, extra_headers={"X-Feature-Key": "svc"}
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
timeout=1,
|
||||
extra_headers={"X-Feature-Key": "svc"},
|
||||
extra_body={"service_tier": "flex"},
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
@ -239,6 +245,7 @@ async def test_summarize_routes_through_provider_with_settings(
|
|||
assert captured["model"] == "litellm/openai/some-model"
|
||||
settings = captured["model_settings"]
|
||||
assert settings.extra_headers == {"X-Feature-Key": "svc"}
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
assert settings.max_tokens == 64
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -148,3 +148,42 @@ def test_config_file_loads_dedupe_model(
|
|||
assert settings.dedupe.reasoning_effort == "minimal"
|
||||
# Main model stays independent of the dedupe override.
|
||||
assert settings.llm.model == "openai/root"
|
||||
|
||||
|
||||
def test_dedicated_dedupe_model_uses_own_body_fields_not_main(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
dedupe = DedupeSettings(
|
||||
STRIX_DEDUPE_MODEL="deepseek/cheap",
|
||||
DEDUPE_LLM_EXTRA_BODY={"service_tier": "flex"},
|
||||
)
|
||||
settings = _dedupe_model_settings(dedupe, "deepseek/cheap", 300)
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
finally:
|
||||
loader._cached = None
|
||||
|
||||
|
||||
def test_dedicated_dedupe_model_gets_no_main_body_fields_by_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
dedupe = DedupeSettings(STRIX_DEDUPE_MODEL="deepseek/cheap")
|
||||
settings = _dedupe_model_settings(dedupe, "deepseek/cheap", 300)
|
||||
assert settings.extra_body is None
|
||||
finally:
|
||||
loader._cached = None
|
||||
|
||||
|
||||
def test_fallback_dedupe_inherits_main_body_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
settings = _dedupe_model_settings(DedupeSettings(), "openai/main-model", 300)
|
||||
assert settings.extra_body == {"safety_identifier": "main"}
|
||||
finally:
|
||||
loader._cached = None
|
||||
|
|
|
|||
|
|
@ -361,6 +361,58 @@ def test_make_model_settings_extra_headers_survive_reasoning_resolve() -> None:
|
|||
assert settings.extra_headers == {"X-Feature-Key": "svc"}
|
||||
|
||||
|
||||
def test_make_model_settings_sets_extra_body() -> None:
|
||||
settings = make_model_settings(
|
||||
"none",
|
||||
model_name="openai/gpt-5",
|
||||
extra_body={"service_tier": "flex", "safety_identifier": "user-hash"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"service_tier": "flex", "safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
def test_make_model_settings_omits_extra_body_when_unset() -> None:
|
||||
assert make_model_settings("none", model_name="gpt-4o").extra_body is None
|
||||
|
||||
|
||||
def test_make_model_settings_extra_body_survives_reasoning_resolve() -> None:
|
||||
settings = make_model_settings(
|
||||
"high",
|
||||
model_name="openai/o3",
|
||||
extra_body={"service_tier": "flex"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
|
||||
|
||||
def test_make_model_settings_extra_body_merges_with_max_reasoning_field() -> None:
|
||||
settings = make_model_settings(
|
||||
"max",
|
||||
model_name="deepseek/deepseek-v4-flash",
|
||||
extra_body={"safety_identifier": "user-hash"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"safety_identifier": "user-hash", "reasoning_effort": "max"}
|
||||
|
||||
|
||||
def test_make_model_settings_max_reasoning_overrides_extra_body_reasoning_field() -> None:
|
||||
settings = make_model_settings(
|
||||
"max",
|
||||
model_name="deepseek/deepseek-v4-flash",
|
||||
extra_body={"reasoning_effort": "low"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"reasoning_effort": "max"}
|
||||
|
||||
|
||||
def test_make_model_settings_does_not_mutate_caller_extra_body() -> None:
|
||||
extra_body = {"safety_identifier": "user-hash"}
|
||||
|
||||
make_model_settings("max", model_name="deepseek/deepseek-v4-flash", extra_body=extra_body)
|
||||
|
||||
assert extra_body == {"safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
|
||||
# Reasoning is resolved via ModelSettings.resolve(); the timeout in extra_args
|
||||
# must not be dropped when a reasoning override is merged in.
|
||||
|
|
|
|||
97
tests/test_llm_extra_body.py
Normal file
97
tests/test_llm_extra_body.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"""Tests for LLM_EXTRA_BODY: extra fields merged into every LLM request body."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
import litellm
|
||||
import openai
|
||||
import pytest
|
||||
from agents.extensions.models.litellm_model import LitellmModel
|
||||
from agents.models.interface import ModelTracing
|
||||
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
||||
from agents.models.openai_responses import OpenAIResponsesModel
|
||||
|
||||
from strix.config import loader
|
||||
from strix.config.loader import load_settings
|
||||
from strix.core.inputs import make_model_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from agents.models.interface import Model
|
||||
|
||||
|
||||
_EXTRA_BODY = {"service_tier": "flex", "safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
monkeypatch.delenv("LLM_EXTRA_BODY", raising=False)
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
monkeypatch.setattr(loader, "_override", None)
|
||||
yield
|
||||
|
||||
|
||||
def test_extra_body_parsed_from_json_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"service_tier": "flex", "n": 1}))
|
||||
|
||||
assert load_settings().llm.extra_body == {"service_tier": "flex", "n": 1}
|
||||
|
||||
|
||||
async def _send(model: Model) -> None:
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input="Reply with just 'OK'.",
|
||||
model_settings=make_model_settings(None, model_name="gpt-5", extra_body=_EXTRA_BODY),
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
conversation_id=None,
|
||||
prompt=None,
|
||||
)
|
||||
|
||||
|
||||
def _capturing_client(captured: dict[str, Any]) -> openai.AsyncOpenAI:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
captured.update(json.loads(request.content))
|
||||
return httpx.Response(400, json={"error": {"message": "captured"}})
|
||||
|
||||
return openai.AsyncOpenAI(
|
||||
api_key="test",
|
||||
base_url="https://gateway.example/v1",
|
||||
max_retries=0,
|
||||
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model_cls", [OpenAIChatCompletionsModel, OpenAIResponsesModel])
|
||||
async def test_extra_body_reaches_native_openai_request_body(model_cls: type) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
with pytest.raises(openai.BadRequestError):
|
||||
await _send(model_cls(model="gpt-5", openai_client=_capturing_client(captured)))
|
||||
|
||||
assert {k: captured.get(k) for k in _EXTRA_BODY} == _EXTRA_BODY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_passed_to_litellm(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def fake_acompletion(**kwargs: Any) -> Any:
|
||||
captured.update(kwargs)
|
||||
raise RuntimeError("captured")
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
|
||||
with pytest.raises(RuntimeError, match="captured"):
|
||||
await _send(LitellmModel(model="openai/gpt-5", base_url="https://gateway.example/v1"))
|
||||
|
||||
assert captured["extra_body"] == _EXTRA_BODY
|
||||
|
|
@ -60,6 +60,7 @@ def test_preflight_request_fails_after_its_own_timeout_with_a_clear_message() ->
|
|||
model, # type: ignore[arg-type]
|
||||
model_name="openai/gpt-4o",
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
timeout=1,
|
||||
api_base_setting="DEDUPE_LLM_API_BASE",
|
||||
)
|
||||
|
|
@ -130,3 +131,52 @@ def test_warm_up_checks_the_dedupe_model_with_its_own_headers_and_the_preflight_
|
|||
"DEDUPE_LLM_API_BASE",
|
||||
]
|
||||
assert calls[1][0] is dedupe_model
|
||||
|
||||
|
||||
def test_warm_up_checks_each_model_with_its_own_body_fields(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-4o")
|
||||
monkeypatch.setenv("LLM_API_KEY", "sk-test")
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", '{"safety_identifier": "main"}')
|
||||
monkeypatch.setenv("STRIX_DEDUPE_MODEL", "anthropic/claude-sonnet-4-5")
|
||||
monkeypatch.setenv("DEDUPE_LLM_EXTRA_BODY", '{"service_tier": "flex"}')
|
||||
_fresh_settings(monkeypatch, tmp_path)
|
||||
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
async def record(_model: Any, **kwargs: Any) -> None:
|
||||
calls.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(scan_setup, "preflight_request", record)
|
||||
monkeypatch.setattr(cli_main, "preflight_request", record)
|
||||
monkeypatch.setattr("strix.config.models.configure_sdk_model_defaults", lambda _settings: None)
|
||||
monkeypatch.setattr("strix.report.dedupe.resolve_dedupe_model", lambda _dedupe, _name: object())
|
||||
|
||||
asyncio.run(cli_main.warm_up_llm())
|
||||
|
||||
assert [kwargs["extra_body"] for kwargs in calls] == [
|
||||
{"safety_identifier": "main"},
|
||||
{"service_tier": "flex"},
|
||||
]
|
||||
|
||||
|
||||
def test_preflight_request_sends_the_extra_body() -> None:
|
||||
seen: list[Any] = []
|
||||
|
||||
class _Recorder:
|
||||
async def get_response(self, *, model_settings: Any, **_: Any) -> None:
|
||||
seen.append(model_settings.extra_body)
|
||||
|
||||
asyncio.run(
|
||||
preflight_request(
|
||||
_Recorder(), # type: ignore[arg-type]
|
||||
model_name="openai/gpt-4o",
|
||||
extra_headers=None,
|
||||
extra_body={"safety_identifier": "main"},
|
||||
timeout=5,
|
||||
api_base_setting="LLM_API_BASE",
|
||||
)
|
||||
)
|
||||
|
||||
assert seen == [{"safety_identifier": "main"}]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ def _wire_runner(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ def _settings() -> Any:
|
|||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ async def test_persistent_rate_limit_stops_gracefully(
|
|||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ def _patch_engine_scaffold(
|
|||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ def _settings() -> Any:
|
|||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue