From cd15e9e8c3f073d6f0502a3413ae9c9b040dcce1 Mon Sep 17 00:00:00 2001 From: Alex Schapiro Date: Mon, 10 Aug 2026 13:42:20 +0000 Subject: [PATCH] fix: build fallback-specific model settings for the denial fallback --- strix/core/agents.py | 14 ++++++++++- strix/core/execution.py | 6 ++++- strix/core/runner.py | 13 +++++++++- tests/test_denied_retry_fallback.py | 38 +++++++++++++++++++++++------ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/strix/core/agents.py b/strix/core/agents.py index fc98c076..bc29e9ba 100644 --- a/strix/core/agents.py +++ b/strix/core/agents.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from agents.items import TResponseInputItem from agents.memory import Session + from agents.model_settings import ModelSettings logger = logging.getLogger(__name__) @@ -56,6 +57,7 @@ class AgentCoordinator: self.denial_counts: dict[str, int] = {} self.denial_fallback: set[str] = set() self._denial_fallback_model: str | None = None + self._denial_fallback_model_settings: ModelSettings | None = None self._denied_retries: int = 3 self.wait_kinds: dict[str, WaitKind] = {} self.runtimes: dict[str, AgentRuntime] = {} @@ -71,15 +73,25 @@ class AgentCoordinator: def set_snapshot_path(self, path: Path) -> None: self._snapshot_path = path - def configure_denial_fallback(self, model: str | None, denied_retries: int) -> None: + def configure_denial_fallback( + self, + model: str | None, + denied_retries: int, + model_settings: ModelSettings | None = None, + ) -> None: """Configure the per-agent content denial fallback.""" self._denial_fallback_model = model + self._denial_fallback_model_settings = model_settings self._denied_retries = denied_retries @property def denial_fallback_model(self) -> str | None: return self._denial_fallback_model + @property + def denial_fallback_model_settings(self) -> ModelSettings | None: + return self._denial_fallback_model_settings + @property def denied_retries(self) -> int: return self._denied_retries diff --git a/strix/core/execution.py b/strix/core/execution.py index 09bb5559..b2d03b93 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -648,7 +648,11 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 active_run_config = run_config if coordinator.denial_fallback_model and await coordinator.is_on_denial_fallback(agent_id): active_run_config = dataclasses.replace( - run_config, model=coordinator.denial_fallback_model + run_config, + model=coordinator.denial_fallback_model, + model_settings=( + coordinator.denial_fallback_model_settings or run_config.model_settings + ), ) stream: Any = None pre_run_items: list[Any] = [] diff --git a/strix/core/runner.py b/strix/core/runner.py index b008810f..ccbeb851 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -175,9 +175,20 @@ async def run_strix_scan( if coordinator is None: coordinator = AgentCoordinator() coordinator.set_snapshot_path(agents_path) + fallback_model = settings.llm.fallback_model coordinator.configure_denial_fallback( - settings.llm.fallback_model, + fallback_model, settings.llm.denied_retries, + model_settings=make_model_settings( + settings.llm.reasoning_effort, + model_name=fallback_model, + force_required_tool_choice=settings.llm.force_required_tool_choice, + request_timeout=settings.llm.timeout, + prompt_cache=settings.llm.prompt_cache, + extra_headers=settings.llm.extra_headers, + ) + if fallback_model + else None, ) from strix.tools.notes.tools import hydrate_notes_from_disk diff --git a/tests/test_denied_retry_fallback.py b/tests/test_denied_retry_fallback.py index 084d5678..8b27f97c 100644 --- a/tests/test_denied_retry_fallback.py +++ b/tests/test_denied_retry_fallback.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any import pytest -from agents import RunConfig, Runner +from agents import ModelSettings, RunConfig, Runner from strix.config import codex from strix.core import execution @@ -39,13 +39,14 @@ async def _run_once( fallback_model: str | None = None, denied_retries: int = 3, primary_model: str = "openai/gpt-5.6-sol", -) -> tuple[Any, list[str | None], AgentCoordinator]: + fallback_model_settings: ModelSettings | None = None, +) -> tuple[Any, list[tuple[str | None, ModelSettings]], AgentCoordinator]: _patch_fast_backoff(monkeypatch) - calls: list[str | None] = [] + calls: list[tuple[str | None, ModelSettings]] = [] def _fake_run_streamed(*_args: Any, **kwargs: Any) -> _FakeStream: run_config = kwargs["run_config"] - calls.append(run_config.model) + calls.append((run_config.model, run_config.model_settings)) return streams[len(calls) - 1] monkeypatch.setattr(Runner, "run_streamed", _fake_run_streamed) @@ -53,14 +54,16 @@ async def _run_once( coordinator = AgentCoordinator() await coordinator.register("root", "strix", parent_id=None) if fallback_model is not None: - coordinator.configure_denial_fallback(fallback_model, denied_retries) + coordinator.configure_denial_fallback( + fallback_model, denied_retries, model_settings=fallback_model_settings + ) result = await execution._run_cycle( object(), coordinator, "root", input_data="task", - run_config=RunConfig(model=primary_model), + run_config=RunConfig(model=primary_model, model_settings=ModelSettings()), context={}, max_turns=5, session=None, @@ -83,7 +86,7 @@ async def test_run_cycle_falls_back_after_repeated_content_denials( ) assert result is streams[3] - assert models == ["openai/gpt-5.6-sol"] * 3 + ["openai/gpt-5.4"] + assert [model for model, _ in models] == ["openai/gpt-5.6-sol"] * 3 + ["openai/gpt-5.4"] assert await coordinator.is_on_denial_fallback("root") is True @@ -109,10 +112,29 @@ async def test_run_cycle_switches_on_first_denial_at_boundary( ) assert result is streams[1] - assert models == ["openai/gpt-5.6-sol", "openai/gpt-5.4"] + assert [model for model, _ in models] == ["openai/gpt-5.6-sol", "openai/gpt-5.4"] assert await coordinator.is_on_denial_fallback("root") is True +@pytest.mark.asyncio +async def test_fallback_uses_its_own_model_settings( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fallback_settings = ModelSettings(parallel_tool_calls=True) + streams = [_guardrail_stream(), _FakeStream()] + result, calls, _coordinator = await _run_once( + monkeypatch, + streams, + fallback_model="openai/gpt-5.4", + denied_retries=1, + fallback_model_settings=fallback_settings, + ) + + assert result is streams[1] + assert calls[0][1] is not fallback_settings + assert calls[1][1] is fallback_settings + + @pytest.mark.asyncio async def test_denial_fallback_state_round_trips_through_snapshot() -> None: coordinator = AgentCoordinator()