From acc375a2a9d2110847597406968c2e6cb7697b4a Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 17 Sep 2026 18:00:51 -0700 Subject: [PATCH 1/4] fix(proxy): forward every method on the typesafe pass-through route #41607 registered the typesafe pass-through with a route that only accepted GET and POST, so a PUT, DELETE or PATCH to /typesafe/... came back 405 before reaching the upstream. CircleCI's pass-through method test caught it, but that lane does not run on the PR gate, so the mapped unit test now covers the same invariant for typesafe The same CircleCI run also failed test_models_by_provider because typesafe is not a key of models_by_provider. Registering it there would satisfy the assertion without changing behaviour: typesafe has no LlmProviders member, so a typesafe/* deployment never loads and get_valid_models returns nothing, and its spend is priced straight from model_cost. The test already skips search-mode providers for that reason, so it now skips evaluation mode too --- .../proxy/pass_through_endpoints/llm_passthrough_endpoints.py | 2 +- tests/litellm_utils_tests/test_utils.py | 3 +-- .../pass_through_endpoints/test_llm_pass_through_endpoints.py | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index f251b3b052c..1c763db2146 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -527,7 +527,7 @@ async def mistral_proxy_route( @router.api_route( "/typesafe/{endpoint:path}", - methods=["GET", "POST"], # mutable-ok: FastAPI route metadata requires a list + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route metadata requires a list tags=["TypeSafe AI Pass-through", "pass-through"], # mutable-ok: FastAPI route metadata requires a list ) async def typesafe_proxy_route( diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index 0ccfae55290..b68c2cb3d65 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -1360,8 +1360,7 @@ def test_models_by_provider(): or v["litellm_provider"] == "bedrock_converse" ): continue - elif v.get("mode") == "search": - # Skip search providers as they don't have traditional models + elif v.get("mode") in ("search", "evaluation"): continue else: providers.add(v["litellm_provider"]) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 901c5318442..07936e7a9e9 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -6149,6 +6149,10 @@ class TestTypeSafePassthroughRoute: request.json = AsyncMock(return_value=body) return request + @pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE", "PATCH"]) + def test_route_serves_every_method(self, method: str): + assert _resolve_route_name(method, "/typesafe/v1/systemone") == "typesafe_proxy_route" + @pytest.mark.asyncio async def test_forwards_target_auth_headers_provider_and_query(self, monkeypatch): monkeypatch.setenv("TYPESAFE_API_KEY", "typesafe-test-key") From 685ac115caa78171a864806f08af05797757c59e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 17 Sep 2026 18:09:50 -0700 Subject: [PATCH 2/4] chore(proxy): regenerate the OpenAPI snapshot and dashboard types for the typesafe methods --- litellm/proxy/_lazy_openapi_snapshot.json | 134 +++++++++++++++++- ui/litellm-dashboard/src/lib/http/schema.d.ts | 113 ++++++++++++++- 2 files changed, 241 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 473b21186e8..fa046ef0a72 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -19346,7 +19346,7 @@ } } }, - "description": "\n Unified rate-limit error.\n\n Every rate-limit condition surfaced by litellm \u2014 whether it originated from\n an upstream LLM provider, a vendor batch endpoint, or one of litellm's own\n proxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\n max-iterations, etc.) \u2014 is raised as an instance of this class.\n\n The :attr:`category` attribute lets callers distinguish the source. See\n :class:`RateLimitErrorCategory` for the available values.\n " + "description": "\nUnified rate-limit error.\n\nEvery rate-limit condition surfaced by litellm \u2014 whether it originated from\nan upstream LLM provider, a vendor batch endpoint, or one of litellm's own\nproxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\nmax-iterations, etc.) \u2014 is raised as an instance of this class.\n\nThe :attr:`category` attribute lets callers distinguish the source. See\n:class:`RateLimitErrorCategory` for the available values.\n" }, "500": { "content": { @@ -20374,6 +20374,50 @@ } }, "/typesafe/{endpoint}": { + "delete": { + "description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)", + "operationId": "typesafe_proxy_route_typesafe__endpoint__delete", + "parameters": [ + { + "in": "path", + "name": "endpoint", + "required": true, + "schema": { + "title": "Endpoint", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Typesafe Proxy Route", + "tags": [ + "llm_passthrough" + ] + }, "get": { "description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)", "operationId": "typesafe_proxy_route_typesafe__endpoint__get", @@ -20418,6 +20462,50 @@ "llm_passthrough" ] }, + "patch": { + "description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)", + "operationId": "typesafe_proxy_route_typesafe__endpoint__patch", + "parameters": [ + { + "in": "path", + "name": "endpoint", + "required": true, + "schema": { + "title": "Endpoint", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Typesafe Proxy Route", + "tags": [ + "llm_passthrough" + ] + }, "post": { "description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)", "operationId": "typesafe_proxy_route_typesafe__endpoint__post", @@ -20461,6 +20549,50 @@ "tags": [ "llm_passthrough" ] + }, + "put": { + "description": "[Docs](https://docs.litellm.ai/docs/pass_through/typesafe)", + "operationId": "typesafe_proxy_route_typesafe__endpoint__put", + "parameters": [ + { + "in": "path", + "name": "endpoint", + "required": true, + "schema": { + "title": "Endpoint", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Typesafe Proxy Route", + "tags": [ + "llm_passthrough" + ] } }, "/vertex_ai/discovery/{endpoint}": { diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index d9bc9827d47..7f341b58d6e 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -16488,16 +16488,28 @@ export interface paths { * @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe) */ get: operations["typesafe_proxy_route_typesafe__endpoint__get"]; - put?: never; + /** + * Typesafe Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe) + */ + put: operations["typesafe_proxy_route_typesafe__endpoint__put"]; /** * Typesafe Proxy Route * @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe) */ post: operations["typesafe_proxy_route_typesafe__endpoint__post"]; - delete?: never; + /** + * Typesafe Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe) + */ + delete: operations["typesafe_proxy_route_typesafe__endpoint__delete"]; options?: never; head?: never; - patch?: never; + /** + * Typesafe Proxy Route + * @description [Docs](https://docs.litellm.ai/docs/pass_through/typesafe) + */ + patch: operations["typesafe_proxy_route_typesafe__endpoint__patch"]; trace?: never; }; "/update/default_team_settings": { @@ -42720,8 +42732,6 @@ export interface operations { object_team_id?: string | null; /** @description Filter by token (key hash) present in before_value or updated_values JSON (PostgreSQL only) */ object_key_hash?: string | null; - /** @description Match a row whose id, object_id, changed_by, or changed_by_api_key equals this value */ - search?: string | null; /** @description Column to sort by (e.g. 'updated_at', 'action', 'table_name') */ sort_by?: string | null; /** @description Sort order ('asc' or 'desc') */ @@ -61695,6 +61705,37 @@ export interface operations { }; }; }; + typesafe_proxy_route_typesafe__endpoint__put: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; typesafe_proxy_route_typesafe__endpoint__post: { parameters: { query?: never; @@ -61726,6 +61767,68 @@ export interface operations { }; }; }; + typesafe_proxy_route_typesafe__endpoint__delete: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + typesafe_proxy_route_typesafe__endpoint__patch: { + parameters: { + query?: never; + header?: never; + path: { + endpoint: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; update_default_team_settings_update_default_team_settings_patch: { parameters: { query?: never; From b9e468100b32c2e4e10a72105e91e9b4cb9d1d7e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 17 Sep 2026 18:15:55 -0700 Subject: [PATCH 3/4] chore(proxy): regenerate the OpenAPI snapshot with the CI Python version The previous regeneration ran on Python 3.13, which strips docstring indentation at compile time, so one description and one query field came out different from what the Python 3.12 sync check produces. Regenerated on 3.12 so only the typesafe route entries differ from main --- litellm/proxy/_lazy_openapi_snapshot.json | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index fa046ef0a72..b244678e201 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -19346,7 +19346,7 @@ } } }, - "description": "\nUnified rate-limit error.\n\nEvery rate-limit condition surfaced by litellm \u2014 whether it originated from\nan upstream LLM provider, a vendor batch endpoint, or one of litellm's own\nproxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\nmax-iterations, etc.) \u2014 is raised as an instance of this class.\n\nThe :attr:`category` attribute lets callers distinguish the source. See\n:class:`RateLimitErrorCategory` for the available values.\n" + "description": "\n Unified rate-limit error.\n\n Every rate-limit condition surfaced by litellm \u2014 whether it originated from\n an upstream LLM provider, a vendor batch endpoint, or one of litellm's own\n proxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\n max-iterations, etc.) \u2014 is raised as an instance of this class.\n\n The :attr:`category` attribute lets callers distinguish the source. See\n :class:`RateLimitErrorCategory` for the available values.\n " }, "500": { "content": { diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 7f341b58d6e..fd882937e79 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -42732,6 +42732,8 @@ export interface operations { object_team_id?: string | null; /** @description Filter by token (key hash) present in before_value or updated_values JSON (PostgreSQL only) */ object_key_hash?: string | null; + /** @description Match a row whose id, object_id, changed_by, or changed_by_api_key equals this value */ + search?: string | null; /** @description Column to sort by (e.g. 'updated_at', 'action', 'table_name') */ sort_by?: string | null; /** @description Sort order ('asc' or 'desc') */ From 349e8b93583d5517d0855504b3ff67a857e592dc Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 17 Sep 2026 18:19:42 -0700 Subject: [PATCH 4/4] test(proxy): forward each method through the typesafe route to a mocked upstream The route test only resolved route names. It now sends every method through the proxy with a virtual key and asserts the upstream receives that method, the proxy's TypeSafe key and the caller's body --- .../test_llm_pass_through_endpoints.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 07936e7a9e9..9394a13fee4 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -6149,9 +6149,41 @@ class TestTypeSafePassthroughRoute: request.json = AsyncMock(return_value=body) return request - @pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE", "PATCH"]) - def test_route_serves_every_method(self, method: str): - assert _resolve_route_name(method, "/typesafe/v1/systemone") == "typesafe_proxy_route" + @pytest.fixture + def client(self, monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]: + from litellm.proxy.proxy_server import app + + monkeypatch.setenv("TYPESAFE_API_KEY", "typesafe-test-key") + monkeypatch.setenv("TYPESAFE_API_BASE", "https://typesafe.example/base") + monkeypatch.delenv("SERVER_ROOT_PATH", raising=False) + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + litellm.in_memory_llm_clients_cache.flush_cache() + monkeypatch.setitem(app.dependency_overrides, user_api_key_auth, lambda: UserAPIKeyAuth(api_key="sk-virtual")) + yield TestClient(app) + + @pytest.mark.parametrize( + "method, body", + [ + ("GET", None), + ("POST", {"state": "x"}), + ("PUT", {"state": "x"}), + ("DELETE", None), + ("PATCH", {"state": "x"}), + ], + ) + def test_forwards_every_method_and_body_upstream( + self, client: TestClient, method: str, body: dict[str, str] | None + ) -> None: + with respx.mock(assert_all_called=True) as upstream: + route = upstream.request(method, "https://typesafe.example/base/v1/systemone").mock( + return_value=httpx.Response(200, json={"id": "upstream_123"}) + ) + response = client.request(method, "/typesafe/v1/systemone", json=body) + + assert (response.status_code, response.json()) == (200, {"id": "upstream_123"}) + sent: Final = route.calls.last.request + assert sent.headers["authorization"] == "Bearer typesafe-test-key" + assert json.loads(sent.content or b"{}") == (body or {}) @pytest.mark.asyncio async def test_forwards_target_auth_headers_provider_and_query(self, monkeypatch):