Skip to content

fix(agents): wire FallbackModel so a primary 503 retries the fallback (#183)#184

Merged
w7-mgfcode merged 2 commits into
devfrom
fix/agents-wire-model-fallback
May 18, 2026
Merged

fix(agents): wire FallbackModel so a primary 503 retries the fallback (#183)#184
w7-mgfcode merged 2 commits into
devfrom
fix/agents-wire-model-fallback

Conversation

@w7-mgfcode
Copy link
Copy Markdown
Owner

@w7-mgfcode w7-mgfcode commented May 18, 2026

Summary

An agent chat against gemini-3-flash-preview died on a transient 503 UNAVAILABLE from Google. The configured agent_fallback_model never engaged — get_fallback_model() was dead code, and both agents built Agent(model=<primary only>).

This wires PydanticAI's FallbackModel into both agents, so a ModelAPIError (HTTP 5xx, rate limit) on the primary transparently retries agent_fallback_model.

Closes #183.

Changes

File Change
app/features/agents/agents/base.py New build_agent_model_with_fallback() — returns FallbackModel(primary, fallback), or the primary alone when no fallback is configured / it equals the primary / its provider has no API key (logged)
app/features/agents/agents/experiment.py Build the model via the new helper
app/features/agents/agents/rag_assistant.py Build the model via the new helper
app/features/agents/tests/test_base.py 3 new tests: wraps both models; primary-only on missing fallback key; primary-only when fallback == primary

Behaviour

  • FallbackModel's default fallback_on=(ModelAPIError,) already covers the 503 — no custom predicate needed.
  • Graceful degradation: a missing fallback API key logs agents.fallback_disabled and runs primary-only rather than crashing at construction.
  • Ollama primaries/fallbacks (Model objects) and cloud identifiers (strings) both pass through.
  • With the default config, a Gemini outage now falls back to openai:gpt-4o.

Verification

  • ruff check + ruff format --check — clean
  • mypy + pyright (changed modules) — 0 errors
  • pytest -m "not integration" — 1093 passed (incl. 3 new)

Note: an unrelated pre-existing PydanticAIDeprecationWarning surfaces (openai: → Responses API in pydantic-ai v2.0). Out of scope here; worth a separate follow-up to pin openai-chat:.

Summary by Sourcery

Wire agents to use a primary model wrapped with an optional fallback model to improve resilience to transient provider failures.

Bug Fixes:

  • Ensure configured agent_fallback_model is actually used so that transient 5xx errors on the primary model retry against the fallback instead of failing the run.

Enhancements:

  • Add centralized helper to construct the agent model with an optional FallbackModel wrapper, including graceful handling when the fallback model is unset, identical to the primary, or missing an API key.
  • Add structured logging when fallback support is enabled or disabled due to configuration, improving observability of agent model behavior.

Tests:

  • Add unit tests covering FallbackModel wrapping of primary and fallback, and primary-only behavior when the fallback is missing an API key or identical to the primary.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f53198df-b90a-41de-b2cc-abfe49aea197

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agents-wire-model-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 18, 2026

Reviewer's Guide

Introduces a shared helper to construct the agent model as a PydanticAI FallbackModel (primary + fallback) and wires it into both agents, with API-key validation, graceful degradation when the fallback is unusable, and tests to cover the main behaviors.

Sequence diagram for primary model failure falling back to agent_fallback_model

sequenceDiagram
    actor User
    participant ExperimentAgent
    participant FallbackModel
    participant PrimaryProvider
    participant FallbackProvider

    User->>ExperimentAgent: create_experiment_agent()
    ExperimentAgent->>ExperimentAgent: build_agent_model_with_fallback()
    ExperimentAgent->>FallbackModel: Agent(model=FallbackModel)

    User->>ExperimentAgent: agent.run()
    ExperimentAgent->>FallbackModel: call
    FallbackModel->>PrimaryProvider: request
    alt primary succeeds
        PrimaryProvider-->>FallbackModel: response
        FallbackModel-->>ExperimentAgent: result
    else ModelAPIError (HTTP 5xx, rate limit)
        PrimaryProvider-->>FallbackModel: ModelAPIError
        FallbackModel->>FallbackProvider: retry_request
        FallbackProvider-->>FallbackModel: response
        FallbackModel-->>ExperimentAgent: result
    end
Loading

Flow diagram for build_agent_model_with_fallback construction logic

flowchart TD
    A[build_agent_model_with_fallback] --> B[get_model_identifier]
    B --> C[validate_api_key_for_model primary_id]
    C --> D[build_agent_model primary_id]
    D --> E[get_fallback_model]

    E --> F{fallback_id is empty<br/>or fallback_id == primary_id}
    F -->|yes| G[return primary]
    F -->|no| H[validate_api_key_for_model fallback_id]

    H --> I[build_agent_model fallback_id]
    I --> J[logger.info agents.fallback_enabled]
    J --> K[return FallbackModel primary fallback]

    H -->|ValueError| L[logger.warning agents.fallback_disabled]
    L --> G
Loading

File-Level Changes

Change Details Files
Add a central helper that builds a primary agent model optionally wrapped in a FallbackModel with validation and logging.
  • Implement build_agent_model_with_fallback() that validates the primary provider API key, builds the primary model, and conditionally wraps it in FallbackModel with a fallback model.
  • Return the primary model alone when no fallback is configured, the fallback identifier matches the primary, or the fallback provider lacks an API key.
  • Log a warning when a configured fallback is disabled due to missing API key and an info event when a fallback is successfully enabled.
app/features/agents/agents/base.py
Wire the new fallback-aware model construction into both experiment and RAG assistant agents.
  • Replace direct calls to get_model_identifier() + validate_api_key_for_model() + build_agent_model() with build_agent_model_with_fallback().
  • Document in comments that the agent model is now wrapped so transient provider errors on the primary transparently retry against the fallback.
app/features/agents/agents/experiment.py
app/features/agents/agents/rag_assistant.py
Add tests to verify FallbackModel wiring and primary-only behavior in edge cases.
  • Add a test asserting that distinct, key-backed primary and fallback models yield a FallbackModel instance.
  • Add a test asserting that a missing fallback API key results in returning the primary identifier with no FallbackModel wrapping.
  • Add a test asserting that configuring the fallback equal to the primary returns the primary identifier alone.
app/features/agents/tests/test_base.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/features/agents/tests/test_base.py" line_range="67-76" />
<code_context>
     validate_api_key_for_model("ollama:llama3.1")


+def test_build_agent_model_with_fallback_wraps_primary_and_fallback():
+    """A distinct, key-backed fallback yields a FallbackModel(primary, fallback)."""
+    settings = get_settings()
+    settings.agent_default_model = "anthropic:claude-sonnet-4-5"
+    settings.agent_fallback_model = "openai:gpt-4o"
+    settings.anthropic_api_key = "test-anthropic-key"
+    settings.openai_api_key = "test-openai-key"
+
+    model = build_agent_model_with_fallback()
+
+    assert isinstance(model, FallbackModel)
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the FallbackModel test by asserting that the primary and fallback are wired as expected, not just that a FallbackModel instance is returned.

This test could still pass if the primary and fallback models were swapped or misconfigured. To make it more robust, assert the internal configuration of the `FallbackModel` via its public API so that the primary maps to `settings.agent_default_model` and the fallback to `settings.agent_fallback_model`. If that isn’t possible, add a behavior-focused test using stub models that confirms the primary is tried first and the fallback is used when the primary raises `ModelAPIError`.

Suggested implementation:

```python
def test_build_agent_model_with_fallback_wraps_primary_and_fallback():
    """A distinct, key-backed fallback yields a FallbackModel with correctly wired primary and fallback."""
    settings = get_settings()
    settings.agent_default_model = "anthropic:claude-sonnet-4-5"
    settings.agent_fallback_model = "openai:gpt-4o"
    settings.anthropic_api_key = "test-anthropic-key"
    settings.openai_api_key = "test-openai-key"

    model = build_agent_model_with_fallback()

    # Still ensure the wrapper type is correct
    assert isinstance(model, FallbackModel)

    # Verify that the primary model corresponds to the agent_default_model
    primary = model.primary
    # Different model classes may expose their identifier differently; prefer a public attribute if available.
    primary_id = getattr(primary, "model", None) or getattr(primary, "model_name", None) or getattr(primary, "model_id", None)
    assert primary_id == settings.agent_default_model

    # Verify that the fallback model corresponds to the agent_fallback_model
    fallback = model.fallback
    fallback_id = getattr(fallback, "model", None) or getattr(fallback, "model_name", None) or getattr(fallback, "model_id", None)
    assert fallback_id == settings.agent_fallback_model

```

To make this test pass robustly, ensure that:
1. `FallbackModel` exposes the wrapped models via public attributes or properties named `primary` and `fallback`. If different names are used, update the test accordingly.
2. The underlying model instances on `primary` and `fallback` expose a public identifier attribute such as `model`, `model_name`, or `model_id` that matches the configured `"provider:model"` string (e.g., `"anthropic:claude-sonnet-4-5"` and `"openai:gpt-4o"`). If the public API uses a different attribute or a method (e.g., `primary.name` or `primary.info.model`), adjust the `primary_id` / `fallback_id` extraction to match that API.
3. If no such public attributes exist, consider adding a small, public, read-only property on the model classes (or on `FallbackModel` itself) that exposes the configured model string so tests can assert wiring without relying on internals.
</issue_to_address>

### Comment 2
<location path="app/features/agents/tests/test_base.py" line_range="75-77" />
<code_context>
+    assert isinstance(model, FallbackModel)
+
+
+def test_build_agent_model_with_fallback_primary_only_when_fallback_key_missing():
+    """With no API key for the fallback provider, the primary is returned alone."""
+    settings = get_settings()
+    settings.agent_default_model = "anthropic:claude-sonnet-4-5"
+    settings.agent_fallback_model = "openai:gpt-4o"
+    settings.anthropic_api_key = "test-anthropic-key"
+    settings.openai_api_key = ""
+
+    model = build_agent_model_with_fallback()
+
+    assert model == "anthropic:claude-sonnet-4-5"
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the fail-fast behavior when the primary model API key is missing in build_agent_model_with_fallback.

The current tests cover a missing fallback key and the primary==fallback case. Since `build_agent_model_with_fallback()` also calls `validate_api_key_for_model(primary_id)`, please add a test that sets `agent_default_model` to a non-Ollama provider, clears its API key, and asserts that `build_agent_model_with_fallback()` raises `ValueError`. This will lock in the fail-fast behavior and protect against future removal of the primary key validation.

```suggestion
    model = build_agent_model_with_fallback()

    assert isinstance(model, FallbackModel)


def test_build_agent_model_with_fallback_raises_when_primary_api_key_missing():
    """Fail fast when primary model uses non-Ollama provider and its API key is missing."""
    settings = get_settings()
    settings.agent_default_model = "anthropic:claude-sonnet-4-5"
    settings.agent_fallback_model = "openai:gpt-4o"
    settings.anthropic_api_key = ""
    settings.openai_api_key = "test-openai-key"

    with pytest.raises(ValueError):
        build_agent_model_with_fallback()
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +67 to +76
def test_build_agent_model_with_fallback_wraps_primary_and_fallback():
"""A distinct, key-backed fallback yields a FallbackModel(primary, fallback)."""
settings = get_settings()
settings.agent_default_model = "anthropic:claude-sonnet-4-5"
settings.agent_fallback_model = "openai:gpt-4o"
settings.anthropic_api_key = "test-anthropic-key"
settings.openai_api_key = "test-openai-key"

model = build_agent_model_with_fallback()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Strengthen the FallbackModel test by asserting that the primary and fallback are wired as expected, not just that a FallbackModel instance is returned.

This test could still pass if the primary and fallback models were swapped or misconfigured. To make it more robust, assert the internal configuration of the FallbackModel via its public API so that the primary maps to settings.agent_default_model and the fallback to settings.agent_fallback_model. If that isn’t possible, add a behavior-focused test using stub models that confirms the primary is tried first and the fallback is used when the primary raises ModelAPIError.

Suggested implementation:

def test_build_agent_model_with_fallback_wraps_primary_and_fallback():
    """A distinct, key-backed fallback yields a FallbackModel with correctly wired primary and fallback."""
    settings = get_settings()
    settings.agent_default_model = "anthropic:claude-sonnet-4-5"
    settings.agent_fallback_model = "openai:gpt-4o"
    settings.anthropic_api_key = "test-anthropic-key"
    settings.openai_api_key = "test-openai-key"

    model = build_agent_model_with_fallback()

    # Still ensure the wrapper type is correct
    assert isinstance(model, FallbackModel)

    # Verify that the primary model corresponds to the agent_default_model
    primary = model.primary
    # Different model classes may expose their identifier differently; prefer a public attribute if available.
    primary_id = getattr(primary, "model", None) or getattr(primary, "model_name", None) or getattr(primary, "model_id", None)
    assert primary_id == settings.agent_default_model

    # Verify that the fallback model corresponds to the agent_fallback_model
    fallback = model.fallback
    fallback_id = getattr(fallback, "model", None) or getattr(fallback, "model_name", None) or getattr(fallback, "model_id", None)
    assert fallback_id == settings.agent_fallback_model

To make this test pass robustly, ensure that:

  1. FallbackModel exposes the wrapped models via public attributes or properties named primary and fallback. If different names are used, update the test accordingly.
  2. The underlying model instances on primary and fallback expose a public identifier attribute such as model, model_name, or model_id that matches the configured "provider:model" string (e.g., "anthropic:claude-sonnet-4-5" and "openai:gpt-4o"). If the public API uses a different attribute or a method (e.g., primary.name or primary.info.model), adjust the primary_id / fallback_id extraction to match that API.
  3. If no such public attributes exist, consider adding a small, public, read-only property on the model classes (or on FallbackModel itself) that exposes the configured model string so tests can assert wiring without relying on internals.

Comment on lines +75 to +77
model = build_agent_model_with_fallback()

assert isinstance(model, FallbackModel)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test for the fail-fast behavior when the primary model API key is missing in build_agent_model_with_fallback.

The current tests cover a missing fallback key and the primary==fallback case. Since build_agent_model_with_fallback() also calls validate_api_key_for_model(primary_id), please add a test that sets agent_default_model to a non-Ollama provider, clears its API key, and asserts that build_agent_model_with_fallback() raises ValueError. This will lock in the fail-fast behavior and protect against future removal of the primary key validation.

Suggested change
model = build_agent_model_with_fallback()
assert isinstance(model, FallbackModel)
model = build_agent_model_with_fallback()
assert isinstance(model, FallbackModel)
def test_build_agent_model_with_fallback_raises_when_primary_api_key_missing():
"""Fail fast when primary model uses non-Ollama provider and its API key is missing."""
settings = get_settings()
settings.agent_default_model = "anthropic:claude-sonnet-4-5"
settings.agent_fallback_model = "openai:gpt-4o"
settings.anthropic_api_key = ""
settings.openai_api_key = "test-openai-key"
with pytest.raises(ValueError):
build_agent_model_with_fallback()

@w7-mgfcode w7-mgfcode merged commit f229be9 into dev May 18, 2026
8 checks passed
@w7-mgfcode w7-mgfcode deleted the fix/agents-wire-model-fallback branch May 20, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant