From 65d495bb7fabe46c54647ce4a38e56170517e5b6 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:13:30 -0400 Subject: [PATCH] feat(config): STRIX_API_TYPE forces responses vs chat completions (#1324) * Add api_type field to LlmSettings Added 'api_type' field to LlmSettings for API path selection. * Refactor API type handling in models.py * Implement test for LlmSettings API type Add test for API type override settings in LlmSettings. * fix(tests): lint api_type test, cover the api_base override route, document STRIX_API_TYPE * fix(models): keep LiteLLM chat-completions tool schema when STRIX_API_TYPE=responses --------- Co-authored-by: RAJVARDHAN <95933896+vardhans07@users.noreply.github.com> --- docs/advanced/configuration.mdx | 6 ++++ strix/config/models.py | 10 +++++-- strix/config/settings.py | 6 ++++ tests/test_models.py | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index 98a9d690..d2108e68 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -19,6 +19,12 @@ Configure Strix using environment variables or a config file. Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`. + + Select the OpenAI API path for the model: `responses` or `chat_completions`. + By default, a custom `LLM_API_BASE` uses chat completions. Set this variable + when your gateway requires the other API. Also accepts `STRIX_FORCE_API`. + + Extra HTTP headers sent on every LLM request, as a JSON object (e.g. `{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible diff --git a/strix/config/models.py b/strix/config/models.py index 3babbe37..d8444ee1 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -632,9 +632,11 @@ def configure_sdk_model_defaults(settings: Settings) -> None: if llm.api_base: os.environ["OPENAI_BASE_URL"] = llm.api_base _configure_litellm_default("api_base", llm.api_base) - set_default_openai_api("chat_completions") - else: - set_default_openai_api("responses") + api_type = llm.api_type + if api_type is None: + api_type = "chat_completions" if llm.api_base else "responses" + + set_default_openai_api(api_type) _configure_extra_headers(llm) @@ -809,6 +811,8 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo model = model_name.strip().lower() if "/" in model and not model.startswith("openai/"): return True + if settings.llm.api_type is not None: + return settings.llm.api_type == "chat_completions" if settings.llm.api_base: return True return not model_supports_reasoning(model_name) diff --git a/strix/config/settings.py b/strix/config/settings.py index 9309ac39..7bf1de7f 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -9,6 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"] +ApiType = Literal["responses", "chat_completions"] DEFAULT_MAX_TURNS = 500 @@ -23,6 +24,11 @@ class LlmSettings(BaseSettings): model_config = _BASE_CONFIG model: str | None = Field(default=None, alias="STRIX_LLM") + api_type: ApiType | None = Field( + default=None, + validation_alias=AliasChoices("STRIX_API_TYPE", "STRIX_FORCE_API"), + description="Force 'responses' or 'chat_completions' API path", + ) api_key: str | None = Field( default=None, validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"), diff --git a/tests/test_models.py b/tests/test_models.py index cd11819a..7078ca98 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,20 +2,27 @@ from __future__ import annotations +import litellm import pytest from agents.extensions.models.litellm_model import LitellmModel from agents.model_settings import ModelSettings +from agents.models import _openai_shared +from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from agents.models.openai_responses import OpenAIResponsesModel from strix.config.models import ( RECOMMENDED_MODEL_NAMES, StrixProvider, _NonStreamingModel, _TurnGuardModel, + configure_sdk_model_defaults, is_recommended_or_frontier_model, request_timeout_extra_args, routes_through_litellm, supports_strict_tool_schemas, + uses_chat_completions_tool_schema, ) +from strix.config.settings import Settings @pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES) @@ -168,3 +175,46 @@ def test_routes_through_litellm_matches_the_provider( while isinstance(model, _NonStreamingModel | _TurnGuardModel): model = model._inner assert isinstance(model, LitellmModel) is litellm + + +def test_api_type_override_settings(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("STRIX_LLM", "gpt-4") + monkeypatch.setenv("STRIX_API_TYPE", "chat_completions") + assert uses_chat_completions_tool_schema("gpt-4", Settings()) is True + monkeypatch.setenv("STRIX_LLM", "openai/gpt-4") + monkeypatch.setenv("STRIX_API_TYPE", "responses") + assert uses_chat_completions_tool_schema("openai/gpt-4", Settings()) is False + monkeypatch.setenv("STRIX_LLM", "anthropic/claude-sonnet-4-5") + assert uses_chat_completions_tool_schema("anthropic/claude-sonnet-4-5", Settings()) is True + + +@pytest.mark.parametrize( + ("api_type", "expected"), + [ + (None, OpenAIChatCompletionsModel), + ("chat_completions", OpenAIChatCompletionsModel), + ("responses", OpenAIResponsesModel), + ], +) +def test_api_type_overrides_the_api_base_route( + monkeypatch: pytest.MonkeyPatch, api_type: str | None, expected: type +) -> None: + """``LLM_API_BASE`` defaults to chat completions. ``STRIX_API_TYPE`` must win.""" + monkeypatch.setattr(_openai_shared, "_use_responses_by_default", True) + monkeypatch.setattr(_openai_shared, "_default_openai_client", None) + monkeypatch.setattr(_openai_shared, "_default_openai_key", None) + monkeypatch.setattr(litellm, "api_key", None) + monkeypatch.setattr(litellm, "api_base", None) + monkeypatch.setenv("OPENAI_API_KEY", "test-key") + monkeypatch.setenv("OPENAI_BASE_URL", "") + monkeypatch.setenv("STRIX_LLM", "gpt-5") + monkeypatch.setenv("LLM_API_KEY", "test-key") + monkeypatch.setenv("LLM_API_BASE", "https://gateway.example/v1") + monkeypatch.delenv("STRIX_API_TYPE", raising=False) + if api_type is not None: + monkeypatch.setenv("STRIX_API_TYPE", api_type) + configure_sdk_model_defaults(Settings()) + model = StrixProvider().get_model("gpt-5") + while isinstance(model, _NonStreamingModel | _TurnGuardModel): + model = model._inner + assert isinstance(model, expected)