-
Notifications
You must be signed in to change notification settings - Fork 0
fix(agents): wire FallbackModel so a primary 503 retries the fallback (#183) #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,13 +8,15 @@ | |||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||
| from pydantic_ai import ModelRetry | ||||||||||||||||||||||||||||||||||||||
| from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart | ||||||||||||||||||||||||||||||||||||||
| from pydantic_ai.models.fallback import FallbackModel | ||||||||||||||||||||||||||||||||||||||
| from pydantic_ai.models.function import AgentInfo, FunctionModel | ||||||||||||||||||||||||||||||||||||||
| from pydantic_ai.models.openai import OpenAIChatModel | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from app.core.config import get_settings | ||||||||||||||||||||||||||||||||||||||
| from app.features.agents.agents.base import ( | ||||||||||||||||||||||||||||||||||||||
| TOOL_USAGE_INSTRUCTIONS, | ||||||||||||||||||||||||||||||||||||||
| build_agent_model, | ||||||||||||||||||||||||||||||||||||||
| build_agent_model_with_fallback, | ||||||||||||||||||||||||||||||||||||||
| get_agent_retries, | ||||||||||||||||||||||||||||||||||||||
| recoverable, | ||||||||||||||||||||||||||||||||||||||
| validate_api_key_for_model, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,6 +64,66 @@ def test_validate_api_key_for_model_ollama_skips_key_check(): | |||||||||||||||||||||||||||||||||||||
| 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 wired primary-then-fallback. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Asserts the *order* via the public ``FallbackModel.models`` list — ``models[0]`` | ||||||||||||||||||||||||||||||||||||||
| must be the primary (``agent_default_model``) and ``models[1]`` the fallback | ||||||||||||||||||||||||||||||||||||||
| (``agent_fallback_model``) — so a swap or misconfiguration is caught, not just | ||||||||||||||||||||||||||||||||||||||
| the wrapper type. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| # Each member model exposes its provider via `.system` and name via | ||||||||||||||||||||||||||||||||||||||
| # `.model_name`; recombine to the `provider:model` identifier we configured. | ||||||||||||||||||||||||||||||||||||||
| wired = [f"{m.system}:{m.model_name}" for m in model.models] | ||||||||||||||||||||||||||||||||||||||
| assert wired == [settings.agent_default_model, settings.agent_fallback_model] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_build_agent_model_with_fallback_raises_when_primary_api_key_missing(): | ||||||||||||||||||||||||||||||||||||||
| """Fail fast: a non-Ollama primary with no API key raises before any wrapping.""" | ||||||||||||||||||||||||||||||||||||||
| 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, match="Anthropic API key not configured"): | ||||||||||||||||||||||||||||||||||||||
| build_agent_model_with_fallback() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_build_agent_model_with_fallback_primary_only_when_fallback_equals_primary(): | ||||||||||||||||||||||||||||||||||||||
| """A fallback identical to the primary adds no resilience — primary returned alone.""" | ||||||||||||||||||||||||||||||||||||||
| settings = get_settings() | ||||||||||||||||||||||||||||||||||||||
| settings.agent_default_model = "anthropic:claude-sonnet-4-5" | ||||||||||||||||||||||||||||||||||||||
| settings.agent_fallback_model = "anthropic:claude-sonnet-4-5" | ||||||||||||||||||||||||||||||||||||||
| settings.anthropic_api_key = "test-anthropic-key" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| model = build_agent_model_with_fallback() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| assert model == "anthropic:claude-sonnet-4-5" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_prompts_only_reference_registered_tool_names() -> None: | ||||||||||||||||||||||||||||||||||||||
| """Every `tool_*` name in the agent prompts must be an actually-registered tool. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
FallbackModelvia its public API so that the primary maps tosettings.agent_default_modeland the fallback tosettings.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 raisesModelAPIError.Suggested implementation:
To make this test pass robustly, ensure that:
FallbackModelexposes the wrapped models via public attributes or properties namedprimaryandfallback. If different names are used, update the test accordingly.primaryandfallbackexpose a public identifier attribute such asmodel,model_name, ormodel_idthat 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.nameorprimary.info.model), adjust theprimary_id/fallback_idextraction to match that API.FallbackModelitself) that exposes the configured model string so tests can assert wiring without relying on internals.