From 36c53fe051ebc2509de737f184cd27eb301cc9d0 Mon Sep 17 00:00:00 2001 From: Siraj637909 Date: Fri, 14 Aug 2026 16:38:38 +0530 Subject: [PATCH 1/4] fix(router): add NotFoundErrorRetries to RetryPolicy (gh-36896) Add a `NotFoundErrorRetries` field to `RetryPolicy` and wire it into `get_num_retries_from_retry_policy()`. Previously a 404 (litellm.NotFoundError) could not be pinned to 0 retries and fell back to `num_retries`, so the router retried 404s. Each retry also triggers an immediate cooldown, so retrying across the deployment pool cools every deployment and can cascade into a fleet-wide 5xx storm. Operators can now set `NotFoundErrorRetries: 0` to surface 404s to the client immediately. Fixes #36896 --- litellm/router_utils/get_retry_from_policy.py | 3 ++ litellm/types/router.py | 1 + .../test_router_helper_utils.py | 42 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/litellm/router_utils/get_retry_from_policy.py b/litellm/router_utils/get_retry_from_policy.py index 7cf55e80e0c..ce6b6c7c122 100644 --- a/litellm/router_utils/get_retry_from_policy.py +++ b/litellm/router_utils/get_retry_from_policy.py @@ -8,6 +8,7 @@ from litellm.exceptions import ( AuthenticationError, BadRequestError, ContentPolicyViolationError, + NotFoundError, RateLimitError, Timeout, ) @@ -50,6 +51,8 @@ def get_num_retries_from_retry_policy( return retry_policy.ContentPolicyViolationErrorRetries if isinstance(exception, BadRequestError) and retry_policy.BadRequestErrorRetries is not None: return retry_policy.BadRequestErrorRetries + if isinstance(exception, NotFoundError) and retry_policy.NotFoundErrorRetries is not None: + return retry_policy.NotFoundErrorRetries def reset_retry_policy() -> RetryPolicy: diff --git a/litellm/types/router.py b/litellm/types/router.py index 217364c48b7..44985a28c66 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -100,6 +100,7 @@ class RetryPolicy(BaseModel): RateLimitErrorRetries: int | None = None ContentPolicyViolationErrorRetries: int | None = None InternalServerErrorRetries: int | None = None + NotFoundErrorRetries: int | None = None class UpdateRouterConfig(BaseModel): diff --git a/tests/router_unit_tests/test_router_helper_utils.py b/tests/router_unit_tests/test_router_helper_utils.py index c883890f5f6..16b898064e2 100644 --- a/tests/router_unit_tests/test_router_helper_utils.py +++ b/tests/router_unit_tests/test_router_helper_utils.py @@ -1375,6 +1375,48 @@ def test_get_num_retries_from_retry_policy( assert calc_num_retries == num_retries +def test_get_num_retries_from_retry_policy_notfounderror_zero(model_list): + """gh-36896: NotFoundErrorRetries lets operators pin 404s to 0 retries + so the router surfaces a 404 to the client immediately instead of retrying + it across the deployment pool (which would cool every deployment).""" + from litellm.router import RetryPolicy + from litellm.router_utils.get_retry_from_policy import ( + get_num_retries_from_retry_policy, + ) + + router = Router( + model_list=model_list, + retry_policy=RetryPolicy(NotFoundErrorRetries=0), + ) + calc_num_retries = router.get_num_retries_from_retry_policy( + exception=litellm.exceptions.NotFoundError( + message="test", llm_provider="openai", model="gpt-5-mini" + ) + ) + assert calc_num_retries == 0 + + +def test_get_num_retries_from_retry_policy_notfounderror_falls_back_to_num_retries(model_list): + """gh-36896: when NotFoundErrorRetries is unset, a 404 still falls back to + the router's default num_retries (unchanged behavior).""" + from litellm.router import RetryPolicy + from litellm.router_utils.get_retry_from_policy import ( + get_num_retries_from_retry_policy, + ) + + router = Router( + model_list=model_list, + retry_policy=RetryPolicy(), + num_retries=3, + ) + calc_num_retries = router.get_num_retries_from_retry_policy( + exception=litellm.exceptions.NotFoundError( + message="test", llm_provider="openai", model="gpt-5-mini" + ) + ) + assert calc_num_retries is None # no policy-set count; falls back to num_retries + + @pytest.mark.parametrize( "exception_type, exception_name, allowed_fails", [ From 325228e143a4944045760882004abc2399169689 Mon Sep 17 00:00:00 2001 From: Siraj637909 Date: Fri, 14 Aug 2026 21:21:19 +0530 Subject: [PATCH 2/4] test: add coverage for NotFoundError retry policy branch --- .../test_router_helper_utils.py | 49 ++++++------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/tests/router_unit_tests/test_router_helper_utils.py b/tests/router_unit_tests/test_router_helper_utils.py index 16b898064e2..fcdcb8a0526 100644 --- a/tests/router_unit_tests/test_router_helper_utils.py +++ b/tests/router_unit_tests/test_router_helper_utils.py @@ -1352,6 +1352,7 @@ def test_track_deployment_metrics(model_list): "ContentPolicyViolationError", 7, ), + (litellm.exceptions.NotFoundError, "NotFoundError", 0), ], ) def test_get_num_retries_from_retry_policy( @@ -1375,46 +1376,24 @@ def test_get_num_retries_from_retry_policy( assert calc_num_retries == num_retries -def test_get_num_retries_from_retry_policy_notfounderror_zero(model_list): - """gh-36896: NotFoundErrorRetries lets operators pin 404s to 0 retries - so the router surfaces a 404 to the client immediately instead of retrying - it across the deployment pool (which would cool every deployment).""" +def test_get_num_retries_from_retry_policy_not_found_error(model_list): + """Test NotFoundError retry policy mapping.""" from litellm.router import RetryPolicy - from litellm.router_utils.get_retry_from_policy import ( - get_num_retries_from_retry_policy, + from litellm.router_utils.get_retry_from_policy import get_num_retries_from_retry_policy + + backup_retry_policy = RetryPolicy(NotFoundErrorRetries=9) + exception = litellm.NotFoundError( + message="test", + llm_provider="openai", + model="gpt-5-mini", ) - router = Router( - model_list=model_list, - retry_policy=RetryPolicy(NotFoundErrorRetries=0), - ) - calc_num_retries = router.get_num_retries_from_retry_policy( - exception=litellm.exceptions.NotFoundError( - message="test", llm_provider="openai", model="gpt-5-mini" - ) - ) - assert calc_num_retries == 0 - - -def test_get_num_retries_from_retry_policy_notfounderror_falls_back_to_num_retries(model_list): - """gh-36896: when NotFoundErrorRetries is unset, a 404 still falls back to - the router's default num_retries (unchanged behavior).""" - from litellm.router import RetryPolicy - from litellm.router_utils.get_retry_from_policy import ( - get_num_retries_from_retry_policy, + calc_num_retries = get_num_retries_from_retry_policy( + exception=exception, + retry_policy=backup_retry_policy, ) - router = Router( - model_list=model_list, - retry_policy=RetryPolicy(), - num_retries=3, - ) - calc_num_retries = router.get_num_retries_from_retry_policy( - exception=litellm.exceptions.NotFoundError( - message="test", llm_provider="openai", model="gpt-5-mini" - ) - ) - assert calc_num_retries is None # no policy-set count; falls back to num_retries + assert calc_num_retries == 9 @pytest.mark.parametrize( From 061dcb20c15be2ff96210b31c6ae3ebb0f77061c Mon Sep 17 00:00:00 2001 From: Siraj637909 Date: Sat, 15 Aug 2026 12:22:22 +0530 Subject: [PATCH 3/4] fix(router): add NotFoundErrorRetries to retry policy (#36896) --- litellm/router_utils/get_retry_from_policy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/router_utils/get_retry_from_policy.py b/litellm/router_utils/get_retry_from_policy.py index ce6b6c7c122..3496e7fce81 100644 --- a/litellm/router_utils/get_retry_from_policy.py +++ b/litellm/router_utils/get_retry_from_policy.py @@ -27,6 +27,7 @@ def get_num_retries_from_retry_policy( TimeoutErrorRetries: Optional[int] = None RateLimitErrorRetries: Optional[int] = None ContentPolicyViolationErrorRetries: Optional[int] = None + NotFoundErrorRetries: Optional[int] = None """ # if we can find the exception then in the retry policy -> return the number of retries From 7980ef731acc013dca72af76dfde4b58b6fe78b2 Mon Sep 17 00:00:00 2001 From: Siraj637909 Date: Sat, 15 Aug 2026 12:57:24 +0530 Subject: [PATCH 4/4] fix(mcp): add GitHub MCP server config --- mcp_servers.json | 7 +++++++ .../mcp_server/test_mcp_discovery.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/mcp_servers.json b/mcp_servers.json index 3dd21c0d65f..88d454e1dff 100644 --- a/mcp_servers.json +++ b/mcp_servers.json @@ -1,4 +1,11 @@ { + "github": { + "http_url": "https://api.githubcopilot.com/mcp/", + "description": "GitHub MCP server for repository operations, issues, pull requests, and workflow management.", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" + } + }, "zapier": { "sse_url": "https://mcp.zapier.com/api/mcp" }, diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py index 9a741a3f861..ac7a782307e 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py @@ -103,6 +103,21 @@ class TestMCPRegistryFile: missing = expected - names assert not missing, f"Missing well-known servers: {missing}" + def test_root_mcp_servers_file_contains_github(self): + repo_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "..") + ) + config_path = os.path.join(repo_root, "mcp_servers.json") + assert os.path.exists(config_path), f"Config not found at {config_path}" + + with open(config_path, "r") as f: + data = json.load(f) + + assert "github" in data, "Root MCP config should include a GitHub server entry" + github_config = data["github"] + assert github_config.get("http_url") or github_config.get("sse_url") + assert "github" in (github_config.get("description") or "").lower() + def test_env_vars_structure(self, registry_path): with open(registry_path, "r") as f: data = json.load(f)