Compare commits

..
Author SHA1 Message Date
Alex Schapiro d8ad8e3572 feat(models): STRIX_STREAM_MODE opt-in non-streaming for custom endpoints
Replace the api_base-based non-streaming heuristic with an explicit
STRIX_STREAM_MODE=auto|always|never setting. auto/always stream (unchanged
default); never routes through the non-streaming wrapper for endpoints whose
streamed responses drop tool calls.
2026-07-30 00:51:43 +00:00
Alex Schapiro 788a5393db docs(models): generalize non-streaming wrapper docstring 2026-07-29 15:18:26 +00:00
Alex Schapiro 7b82ff8432 fix(models): run custom OpenAI-compatible endpoints non-streamed
Some OpenAI-compatible endpoints return valid tool_calls for a non-streamed
completion but, when streamed, emit the tool call as plain text or drop it and
close the stream, leaving Strix's tool-driven loop with nothing to execute.

Wrap the model for custom (api_base) endpoints so the request is made
non-streamed (where tool calling works) while still presenting the streaming
interface the runner consumes. Hosted providers are unchanged. Opt back into
streaming with STRIX_STREAM_CUSTOM_ENDPOINT=1.
2026-07-29 14:45:37 +00:00
6 changed files with 294 additions and 376 deletions
+115 -3
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import contextlib
import inspect
import os
import time
from typing import TYPE_CHECKING, Any
from agents import (
@@ -13,6 +14,8 @@ from agents import (
set_tracing_disabled,
)
from agents.model_settings import ModelSettings
from agents.models.fake_id import FAKE_RESPONSES_ID
from agents.models.interface import Model
from agents.models.multi_provider import MultiProvider
from agents.models.openai_responses import OpenAIResponsesModel
from agents.retry import (
@@ -21,6 +24,12 @@ from agents.retry import (
RetryPolicyContext,
retry_policies,
)
from openai.types.responses import (
Response,
ResponseCompletedEvent,
ResponseOutputItemDoneEvent,
ResponseUsage,
)
from openai.types.shared import Reasoning
from strix.config import codex
@@ -30,8 +39,14 @@ from strix.config.loader import load_settings
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from agents.models.interface import Model, ModelProvider
from agents.agent_output import AgentOutputSchemaBase
from agents.handoffs import Handoff
from agents.items import ModelResponse, TResponseInputItem, TResponseStreamEvent
from agents.models.interface import ModelProvider, ModelTracing
from agents.tool import Tool
from agents.usage import Usage
from openai import AsyncOpenAI
from openai.types.responses import ResponsePromptParam
from strix.config.settings import ReasoningEffort, Settings
@@ -135,6 +150,99 @@ class _CodexResponsesModel(OpenAIResponsesModel):
await result
def _to_response_usage(usage: Usage | None) -> ResponseUsage | None:
if usage is None:
return None
return ResponseUsage(
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
total_tokens=usage.total_tokens,
input_tokens_details=usage.input_tokens_details,
output_tokens_details=usage.output_tokens_details,
)
class _NonStreamingModel(Model):
"""Run a model non-streamed but expose the streaming interface the runner uses.
Some OpenAI-compatible endpoints (notably gateways serving reasoning models)
return valid ``tool_calls`` for a non-streamed completion but, when streamed,
emit the tool call as plain text or drop it and close the stream — leaving
Strix's tool-driven loop with nothing to execute. Selecting
``STRIX_STREAM_MODE=never`` routes through this wrapper, which makes the real
request non-streamed (where tool calling works) and synthesizes the minimal
event sequence the runner consumes from a stream, so the rest of the pipeline
is unchanged. The only user-visible difference is no token-by-token output.
"""
def __init__(self, inner: Model) -> None:
self._inner = inner
async def get_response(self, *args: Any, **kwargs: Any) -> ModelResponse:
return await self._inner.get_response(*args, **kwargs)
async def stream_response(
self,
system_instructions: str | None,
input: str | list[TResponseInputItem], # noqa: A002
model_settings: ModelSettings,
tools: list[Tool],
output_schema: AgentOutputSchemaBase | None,
handoffs: list[Handoff],
tracing: ModelTracing,
*,
previous_response_id: str | None = None,
conversation_id: str | None = None,
prompt: ResponsePromptParam | None = None,
) -> AsyncIterator[TResponseStreamEvent]:
model_response = await self._inner.get_response(
system_instructions,
input,
model_settings,
tools,
output_schema,
handoffs,
tracing,
previous_response_id=previous_response_id,
conversation_id=conversation_id,
prompt=prompt,
)
sequence = 0
for index, item in enumerate(model_response.output):
yield ResponseOutputItemDoneEvent(
item=item,
output_index=index,
type="response.output_item.done",
sequence_number=sequence,
)
sequence += 1
response = Response(
id=model_response.response_id or FAKE_RESPONSES_ID,
created_at=time.time(),
model=str(getattr(self._inner, "model", "")),
object="response",
output=model_response.output,
tool_choice="auto",
tools=[],
parallel_tool_calls=False,
usage=_to_response_usage(model_response.usage),
)
yield ResponseCompletedEvent(
response=response,
type="response.completed",
sequence_number=sequence,
)
def get_retry_advice(self, request: Any) -> Any:
return self._inner.get_retry_advice(request)
def _should_run_non_streamed(settings: Settings) -> bool:
return settings.llm.stream_mode == "never"
class StrixProvider(MultiProvider):
"""Route any non-OpenAI prefix through LiteLLM with the prefix preserved,
so users type ``deepseek/deepseek-chat`` rather than
@@ -159,14 +267,18 @@ class StrixProvider(MultiProvider):
return self._get_fallback_provider("litellm"), original_model_name
def get_model(self, model_name: str | None) -> Model:
settings = load_settings()
slug = codex.subscription_model(model_name)
if slug:
return _CodexResponsesModel(
slug,
codex.get_subscription_client(),
reasoning_effort=load_settings().llm.reasoning_effort,
reasoning_effort=settings.llm.reasoning_effort,
)
return super().get_model(model_name)
model = super().get_model(model_name)
if _should_run_non_streamed(settings):
return _NonStreamingModel(model)
return model
DEFAULT_MODEL_RETRY = ModelRetrySettings(
+6 -3
View File
@@ -9,6 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
StreamMode = Literal["auto", "always", "never"]
_BASE_CONFIG = SettingsConfigDict(
case_sensitive=False,
@@ -40,9 +41,11 @@ class LlmSettings(BaseSettings):
default=False,
alias="STRIX_FORCE_REQUIRED_TOOL_CHOICE",
)
skip_tool_call_probe: bool = Field(
default=False,
alias="STRIX_SKIP_TOOL_CALL_PROBE",
# auto/always stream; never runs non-streamed, for endpoints that stream tool
# calls incorrectly (some OpenAI-compatible gateways serving reasoning models).
stream_mode: StreamMode = Field(
default="auto",
alias="STRIX_STREAM_MODE",
)
prompt_cache: bool = Field(
default=True,
-190
View File
@@ -1,190 +0,0 @@
"""Preflight probe: verify the model emits structured tool calls when streamed.
Strix is entirely tool-driven and runs every agent turn as a streamed request.
Some OpenAI-compatible endpoints return a valid ``tool_calls`` response when a
completion is requested non-streamed, but under streaming they emit the tool
call as plain assistant text (or drop it entirely) and close the stream. The
Agents SDK then sees a normal final message, the scan makes no progress, and it
either stalls waiting for input or burns turns on empty output.
There is no safe client-side way to execute a tool call the endpoint never
streamed, so we detect the missing capability up front — using the same
streaming path the scan uses — and fail loudly with actionable guidance.
"""
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING, Any
from agents import ModelSettings, ModelTracing
from agents.tool import FunctionTool
from openai.types.responses import ResponseFunctionToolCall
from strix.config.models import StrixProvider
if TYPE_CHECKING:
from strix.config.settings import Settings
logger = logging.getLogger(__name__)
_PROBE_RETRIES = 2
_GUIDANCE = (
"The configured LLM endpoint did not return a structured tool call when "
"streamed.\n\n"
"Strix drives every action through native `tool_calls`, and it streams every "
"turn. Some OpenAI-compatible servers return tool calls correctly for a "
"non-streamed request but, when streamed, emit the tool call as plain text "
"(or omit it) and end the response — so Strix can never act.\n\n"
"Fixes:\n"
" - llama.cpp / llama-server: start with `--jinja` so the chat template "
"produces streamed `tool_calls` deltas.\n"
" - Ollama: use a model whose template wires tool calling, and disable "
"'thinking' if the template can't stream tools alongside it.\n"
" - vLLM: set a matching `--tool-call-parser` (and `--enable-auto-tool-choice`) "
"for the served model.\n"
" - Other gateways: confirm streamed tool calling works for this model "
"(a non-streamed test is not enough).\n\n"
"If you know the endpoint streams tool calls correctly, set "
"STRIX_SKIP_TOOL_CALL_PROBE=1 to skip this check."
)
_TOOL_CONFIG_ERROR_MARKERS = (
"jinja",
"tool call parser",
"tool-call-parser",
"tool_choice",
"does not support tools",
"tools param",
"tool use is not supported",
)
class ToolCallingUnsupportedError(RuntimeError):
"""The endpoint cannot return structured tool calls over a streamed request."""
async def _noop_invoke(_ctx: Any, _args: str) -> str:
return "ok"
_PROBE_TOOL = FunctionTool(
name="strix_ready_check",
description="Report readiness. Call this to acknowledge you can use tools.",
params_json_schema={
"type": "object",
"additionalProperties": False,
"properties": {"status": {"type": "string"}},
"required": ["status"],
},
on_invoke_tool=_noop_invoke,
strict_json_schema=True,
)
_PROBE_SYSTEM = (
"You are a setup probe. You can only respond by calling the provided tool. "
"Do not produce any other output."
)
_PROBE_INPUT = 'Call the `strix_ready_check` tool now with {"status": "ok"}.'
def requires_tool_call_probe(model_name: str, settings: Settings) -> bool:
"""Only self-hosted / OpenAI-compatible routes, where the streaming leak happens."""
return model_name.startswith("ollama/") or bool(settings.llm.api_base)
def _is_tool_config_error(exc: Exception) -> bool:
text = str(exc).lower()
return any(marker in text for marker in _TOOL_CONFIG_ERROR_MARKERS)
async def _stream_saw_tool_call(
model_name: str, model_settings: ModelSettings, *, timeout: float | None
) -> bool:
model = StrixProvider().get_model(model_name)
async def _run() -> bool:
stream = model.stream_response(
system_instructions=_PROBE_SYSTEM,
input=_PROBE_INPUT,
model_settings=model_settings,
tools=[_PROBE_TOOL],
output_schema=None,
handoffs=[],
tracing=ModelTracing.DISABLED,
previous_response_id=None,
conversation_id=None,
prompt=None,
)
# Drain the whole stream rather than returning early: the SDK wraps it in
# a span that must be closed in the context it was opened in.
saw_tool_call = False
async for event in stream:
item = getattr(event, "item", None)
if isinstance(item, ResponseFunctionToolCall):
saw_tool_call = True
response = getattr(event, "response", None)
if response is not None and any(
isinstance(out, ResponseFunctionToolCall)
for out in getattr(response, "output", []) or []
):
saw_tool_call = True
return saw_tool_call
if timeout is not None:
return await asyncio.wait_for(_run(), timeout=timeout)
return await _run()
async def probe_tool_calling(
model_name: str,
settings: Settings,
*,
request_timeout: float | None = None,
) -> None:
"""Fail fast if a streamed request to ``model_name`` yields no structured tool call.
No-op for hosted providers and when ``STRIX_SKIP_TOOL_CALL_PROBE`` is set.
"""
if settings.llm.skip_tool_call_probe or not requires_tool_call_probe(model_name, settings):
return
model_settings = ModelSettings(
parallel_tool_calls=False,
include_usage=True,
)
last_exc: Exception | None = None
for attempt in range(_PROBE_RETRIES + 1):
try:
saw_tool_call = await _stream_saw_tool_call(
model_name, model_settings, timeout=request_timeout
)
except ToolCallingUnsupportedError:
raise
except Exception as exc:
if _is_tool_config_error(exc):
logger.debug("Tool-call probe hit a tool-config error", exc_info=True)
raise ToolCallingUnsupportedError(_GUIDANCE) from exc
last_exc = exc
logger.debug(
"Tool-call probe attempt %d/%d failed transiently",
attempt + 1,
_PROBE_RETRIES + 1,
exc_info=True,
)
continue
if saw_tool_call:
logger.info("Tool-call probe passed for model %s", model_name)
return
raise ToolCallingUnsupportedError(_GUIDANCE)
# All attempts raised transient errors; surface the last one unchanged so the
# caller's existing connection-error handling reports it.
if last_exc is not None:
raise last_exc
-22
View File
@@ -33,7 +33,6 @@ from strix.config.models import (
)
from strix.core.inputs import DEFAULT_MAX_TURNS
from strix.core.paths import run_dir_for, runtime_state_dir
from strix.core.warmup import ToolCallingUnsupportedError, probe_tool_calling
from strix.interface.cli import run_cli
from strix.interface.tui import run_tui
from strix.interface.update_check import (
@@ -423,27 +422,6 @@ async def warm_up_llm(show_model_warning: bool = True) -> None:
)
logger.info("LLM warm-up succeeded for dedupe model %s", dedupe_model)
raw_model = (llm.model or "").strip()
await probe_tool_calling(raw_model, settings, request_timeout=llm.timeout)
except ToolCallingUnsupportedError as e:
logger.debug("Tool-call probe failed", exc_info=True)
error_text = Text()
error_text.append("TOOL CALLING NOT SUPPORTED", style="bold red")
error_text.append("\n\n", style="white")
error_text.append(str(e), style="white")
console.print("\n")
console.print(
Panel(
error_text,
title="[bold white]STRIX",
title_align="left",
border_style="red",
padding=(1, 2),
),
)
console.print()
sys.exit(1)
except Exception as e:
logger.debug("LLM warm-up failed", exc_info=True)
error_text = Text()
+173
View File
@@ -0,0 +1,173 @@
"""Tests for the non-streaming wrapper used on custom OpenAI-compatible endpoints."""
from __future__ import annotations
from types import SimpleNamespace
from typing import TYPE_CHECKING, cast
from unittest.mock import patch
import pytest
from agents.items import ModelResponse
from agents.model_settings import ModelSettings
from agents.models.interface import Model, ModelTracing
from agents.usage import Usage
from openai.types.responses import (
ResponseCompletedEvent,
ResponseFunctionToolCall,
ResponseOutputItemDoneEvent,
ResponseStreamEvent,
)
from strix.config.models import StrixProvider, _NonStreamingModel, _to_response_usage
if TYPE_CHECKING:
from collections.abc import AsyncIterator
def _tool_call() -> ResponseFunctionToolCall:
return ResponseFunctionToolCall(
arguments='{"command": "ls"}',
call_id="call_1",
name="terminal_execute",
type="function_call",
)
class _FakeModel(Model):
def __init__(self, response: ModelResponse) -> None:
self.model = "fake-model"
self._response = response
self.get_response_calls = 0
async def get_response(self, *_args: object, **_kwargs: object) -> ModelResponse:
self.get_response_calls += 1
return self._response
async def stream_response( # pragma: no cover
self, *_args: object, **_kwargs: object
) -> AsyncIterator[ResponseStreamEvent]:
for _ in range(0):
yield cast("ResponseStreamEvent", None)
raise AssertionError("inner stream_response must never be called")
def _settings(*, api_base: str | None, stream_mode: str = "auto") -> SimpleNamespace:
return SimpleNamespace(
llm=SimpleNamespace(
model="openai/glm",
api_base=api_base,
stream_mode=stream_mode,
reasoning_effort="high",
)
)
def test_to_response_usage_maps_token_details() -> None:
usage = Usage(requests=1, input_tokens=10, output_tokens=5, total_tokens=15)
usage.input_tokens_details.cached_tokens = 4
usage.output_tokens_details.reasoning_tokens = 3
mapped = _to_response_usage(usage)
assert mapped is not None
assert (mapped.input_tokens, mapped.output_tokens, mapped.total_tokens) == (10, 5, 15)
assert mapped.input_tokens_details.cached_tokens == 4
assert mapped.output_tokens_details.reasoning_tokens == 3
def test_to_response_usage_none() -> None:
assert _to_response_usage(None) is None
@pytest.mark.asyncio
async def test_stream_response_synthesizes_tool_call_from_non_streamed() -> None:
tool_call = _tool_call()
inner = _FakeModel(
ModelResponse(
output=[tool_call],
usage=Usage(requests=1, input_tokens=10, output_tokens=5, total_tokens=15),
response_id="resp_123",
)
)
wrapper = _NonStreamingModel(inner)
events = [
event
async for event in wrapper.stream_response(
"sys",
"hi",
ModelSettings(),
[],
None,
[],
ModelTracing.DISABLED,
)
]
assert inner.get_response_calls == 1
item_done = [e for e in events if isinstance(e, ResponseOutputItemDoneEvent)]
completed = [e for e in events if isinstance(e, ResponseCompletedEvent)]
assert len(item_done) == 1
assert item_done[0].item == tool_call
assert len(completed) == 1
final = completed[0].response
assert final.output == [tool_call]
assert final.id == "resp_123"
assert final.usage is not None
assert final.usage.total_tokens == 15
# sequence numbers are strictly increasing
assert [e.sequence_number for e in events] == list(range(len(events)))
@pytest.mark.asyncio
async def test_stream_response_delegates_get_response() -> None:
inner = _FakeModel(
ModelResponse(output=[], usage=Usage(), response_id=None),
)
wrapper = _NonStreamingModel(inner)
result = await wrapper.get_response(
"sys", "hi", ModelSettings(), [], None, [], ModelTracing.DISABLED
)
assert result is inner._response
assert inner.get_response_calls == 1
def test_get_model_auto_streams_custom_endpoint() -> None:
sentinel = _FakeModel(ModelResponse(output=[], usage=Usage(), response_id=None))
with (
patch("strix.config.models.load_settings", return_value=_settings(api_base="http://x/v1")),
patch(
"agents.models.multi_provider.MultiProvider.get_model",
return_value=sentinel,
),
):
model = StrixProvider().get_model("openai/glm")
assert model is sentinel
def test_get_model_auto_streams_hosted() -> None:
sentinel = _FakeModel(ModelResponse(output=[], usage=Usage(), response_id=None))
with (
patch("strix.config.models.load_settings", return_value=_settings(api_base="")),
patch(
"agents.models.multi_provider.MultiProvider.get_model",
return_value=sentinel,
),
):
model = StrixProvider().get_model("openai/gpt-4o")
assert model is sentinel
def test_get_model_stream_mode_never_wraps() -> None:
sentinel = _FakeModel(ModelResponse(output=[], usage=Usage(), response_id=None))
with (
patch(
"strix.config.models.load_settings",
return_value=_settings(api_base="http://x/v1", stream_mode="never"),
),
patch(
"agents.models.multi_provider.MultiProvider.get_model",
return_value=sentinel,
),
):
model = StrixProvider().get_model("openai/glm")
assert isinstance(model, _NonStreamingModel)
-158
View File
@@ -1,158 +0,0 @@
from __future__ import annotations
import types
from typing import TYPE_CHECKING, Any
import pytest
from openai.types.responses import ResponseFunctionToolCall
from strix.core import warmup
from strix.core.warmup import (
ToolCallingUnsupportedError,
probe_tool_calling,
requires_tool_call_probe,
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator
def _settings(*, api_base: str | None = None, skip: bool = False) -> Any:
return types.SimpleNamespace(
llm=types.SimpleNamespace(api_base=api_base, skip_tool_call_probe=skip),
)
def _tool_call_event() -> Any:
return types.SimpleNamespace(
item=ResponseFunctionToolCall(
arguments='{"status": "ok"}',
call_id="call_1",
name="strix_ready_check",
type="function_call",
),
)
def _text_event() -> Any:
# A completed response whose output is a plain message, no tool call.
return types.SimpleNamespace(
item=None,
response=types.SimpleNamespace(output=[types.SimpleNamespace(type="message")]),
)
class _FakeModel:
def __init__(self, events: list[Any] | None = None, raises: Exception | None = None) -> None:
self._events = events or []
self._raises = raises
def stream_response(self, **_kwargs: Any) -> AsyncIterator[Any]:
events = self._events
raises = self._raises
async def _gen() -> AsyncIterator[Any]:
if raises is not None:
raise raises
for event in events:
yield event
return _gen()
def _patch_model(monkeypatch: pytest.MonkeyPatch, model: _FakeModel) -> None:
monkeypatch.setattr(
warmup, "StrixProvider", lambda: types.SimpleNamespace(get_model=lambda _m: model)
)
def test_requires_probe_only_for_custom_endpoints_and_ollama() -> None:
assert requires_tool_call_probe("openai/glm-5.2", _settings(api_base="http://x")) is True
assert requires_tool_call_probe("ollama/llama3", _settings()) is True
assert requires_tool_call_probe("openai/gpt-4o", _settings()) is False
assert requires_tool_call_probe("anthropic/claude", _settings()) is False
@pytest.mark.asyncio
async def test_probe_skipped_for_hosted_provider(monkeypatch: pytest.MonkeyPatch) -> None:
# Would raise if it tried to stream; gating must short-circuit first.
_patch_model(monkeypatch, _FakeModel(raises=RuntimeError("should not be called")))
await probe_tool_calling("openai/gpt-4o", _settings())
@pytest.mark.asyncio
async def test_probe_skipped_when_setting_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_model(monkeypatch, _FakeModel(raises=RuntimeError("should not be called")))
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x", skip=True))
@pytest.mark.asyncio
async def test_probe_passes_on_streamed_tool_call(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_model(monkeypatch, _FakeModel(events=[_tool_call_event()]))
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x"))
@pytest.mark.asyncio
async def test_probe_passes_when_tool_call_only_in_final_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
completed = types.SimpleNamespace(
item=None,
response=types.SimpleNamespace(
output=[
ResponseFunctionToolCall(
arguments="{}",
call_id="c",
name="strix_ready_check",
type="function_call",
)
]
),
)
_patch_model(monkeypatch, _FakeModel(events=[completed]))
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x"))
@pytest.mark.asyncio
async def test_probe_aborts_when_only_text_streamed(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_model(monkeypatch, _FakeModel(events=[_text_event()]))
with pytest.raises(ToolCallingUnsupportedError):
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x"))
@pytest.mark.asyncio
async def test_probe_aborts_on_tool_config_error(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_model(monkeypatch, _FakeModel(raises=RuntimeError("tools param requires --jinja flag")))
with pytest.raises(ToolCallingUnsupportedError):
await probe_tool_calling("ollama/llama3", _settings(api_base="http://x"))
@pytest.mark.asyncio
async def test_probe_retries_transient_then_passes(monkeypatch: pytest.MonkeyPatch) -> None:
calls = {"n": 0}
good = _FakeModel(events=[_tool_call_event()])
class _Flaky:
def stream_response(self, **kwargs: Any) -> AsyncIterator[Any]:
calls["n"] += 1
if calls["n"] == 1:
async def _boom() -> AsyncIterator[Any]:
for _ in range(0): # make this a generator without an unreachable yield
yield None
raise ConnectionError("transient")
return _boom()
return good.stream_response(**kwargs)
_patch_model(monkeypatch, _Flaky()) # type: ignore[arg-type]
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x"))
assert calls["n"] == 2
@pytest.mark.asyncio
async def test_probe_surfaces_persistent_transient_error(monkeypatch: pytest.MonkeyPatch) -> None:
_patch_model(monkeypatch, _FakeModel(raises=ConnectionError("down")))
with pytest.raises(ConnectionError):
await probe_tool_calling("openai/glm-5.2", _settings(api_base="http://x"))