diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index e53b5d4ad52..326548dd72d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -352,18 +352,23 @@ class LiteLLMRoutes(enum.Enum): "/responses", "/v1/responses", "/openai/v1/responses", + "/openai/responses", "/responses/{response_id}", "/v1/responses/{response_id}", "/openai/v1/responses/{response_id}", + "/openai/responses/{response_id}", "/responses/{response_id}/input_items", "/v1/responses/{response_id}/input_items", "/openai/v1/responses/{response_id}/input_items", + "/openai/responses/{response_id}/input_items", "/responses/{response_id}/cancel", "/v1/responses/{response_id}/cancel", "/openai/v1/responses/{response_id}/cancel", + "/openai/responses/{response_id}/cancel", "/responses/input_tokens", "/v1/responses/input_tokens", "/openai/v1/responses/input_tokens", + "/openai/responses/input_tokens", # vector stores "/vector_stores", "/v1/vector_stores", diff --git a/litellm/proxy/agent_endpoints/auth/managed_authorization.py b/litellm/proxy/agent_endpoints/auth/managed_authorization.py index 57e99edecba..0b3e11b19ea 100644 --- a/litellm/proxy/agent_endpoints/auth/managed_authorization.py +++ b/litellm/proxy/agent_endpoints/auth/managed_authorization.py @@ -47,6 +47,7 @@ _MANAGED_MODEL_ROUTES: Final = frozenset( ) | frozenset( ( "/openai/v1/responses", + "/openai/responses", "/v2/rerank", "/claude_code_gateway/v1/messages", "/claude_code_gateway/v1/messages/count_tokens", diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 24602dec677..e16ae590c0b 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1033,6 +1033,9 @@ def normalize_request_route(route: str) -> str: (r"^(/(?:openai/)?v1/responses)/([^/]+)(/input_items)$", r"\1/{response_id}\3"), (r"^(/(?:openai/)?v1/responses)/([^/]+)(/cancel)$", r"\1/{response_id}\3"), (r"^(/(?:openai/)?v1/responses)/([^/]+)$", r"\1/{response_id}"), + (r"^(/openai/responses)/([^/]+)(/input_items)$", r"\1/{response_id}\3"), + (r"^(/openai/responses)/([^/]+)(/cancel)$", r"\1/{response_id}\3"), + (r"^(/openai/responses)/([^/]+)$", r"\1/{response_id}"), (r"^(/responses)/([^/]+)(/input_items)$", r"\1/{response_id}\3"), (r"^(/responses)/([^/]+)(/cancel)$", r"\1/{response_id}\3"), (r"^(/responses)/([^/]+)$", r"\1/{response_id}"), diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 0aa2528d46b..30ae9efafa2 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -103,6 +103,7 @@ _UNBILLED_ROUTES: Final[frozenset[str]] = frozenset( "/responses/input_tokens", "/v1/responses/input_tokens", "/openai/v1/responses/input_tokens", + "/openai/responses/input_tokens", } ) _TOKEN_COUNTING_SEGMENTS: Final[frozenset[str]] = frozenset({"count_tokens", "count-tokens"}) diff --git a/litellm/proxy/common_utils/custom_openapi_spec.py b/litellm/proxy/common_utils/custom_openapi_spec.py index 45ca509b7b7..16fb458a321 100644 --- a/litellm/proxy/common_utils/custom_openapi_spec.py +++ b/litellm/proxy/common_utils/custom_openapi_spec.py @@ -31,7 +31,12 @@ class CustomOpenAPISpec: "/openai/deployments/{model}/embeddings", ] - RESPONSES_API_PATHS = ["/v1/responses", "/responses", "/openai/v1/responses"] + RESPONSES_API_PATHS = [ + "/v1/responses", + "/responses", + "/openai/v1/responses", + "/openai/responses", + ] @staticmethod def _as_object(node: JsonValue) -> JsonObject: diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index c4cd7c2d411..28b57d047f1 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -312,6 +312,12 @@ async def _resolve_cursor_model_variant_before_auth(request: Request) -> None: tags=["responses"], responses=RESPONSES_API_RESPONSE_SCHEMAS, ) +@router.post( + "/openai/responses", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], + responses=RESPONSES_API_CREATE_RESPONSE_SCHEMAS, +) async def responses_api( request: Request, fastapi_response: Response, @@ -809,6 +815,12 @@ async def cursor_chat_completions( responses=RESPONSES_API_RESPONSE_SCHEMAS, openapi_extra=_RETRIEVE_QUERY_OPENAPI, ) +@router.get( + "/openai/responses/{response_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], + responses=RESPONSES_API_RESPONSE_SCHEMAS, +) async def get_response( response_id: str, request: Request, @@ -1240,6 +1252,12 @@ async def _bill_finished_stream( tags=["responses"], responses=DELETE_RESPONSE_SCHEMAS, ) +@router.delete( + "/openai/responses/{response_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], + responses=DELETE_RESPONSE_SCHEMAS, +) async def delete_response( response_id: str, request: Request, @@ -1356,6 +1374,12 @@ async def delete_response( tags=["responses"], responses=RESPONSE_ITEM_LIST_SCHEMAS, ) +@router.get( + "/openai/responses/{response_id}/input_items", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], + responses=RESPONSE_ITEM_LIST_SCHEMAS, +) async def get_response_input_items( response_id: str, request: Request, @@ -1424,6 +1448,11 @@ async def get_response_input_items( dependencies=[Depends(user_api_key_auth)], tags=["responses"], ) +@router.post( + "/openai/responses/compact", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], +) async def compact_response( request: Request, fastapi_response: Response, @@ -1736,6 +1765,11 @@ def _responses_input_as_token_count_messages( dependencies=(_user_api_key_auth_dep,), tags=_RESPONSES_TAGS, ) +@router.post( + "/openai/responses/input_tokens", + dependencies=(_user_api_key_auth_dep,), + tags=_RESPONSES_TAGS, +) async def responses_input_tokens( request: Request, token_counter: _TokenCounter = _token_counter_dep, @@ -1868,6 +1902,11 @@ async def responses_input_tokens( dependencies=[Depends(user_api_key_auth)], tags=["responses"], ) +@router.post( + "/openai/responses/{response_id}/cancel", + dependencies=[Depends(user_api_key_auth)], + tags=["responses"], +) async def cancel_response( response_id: str, request: Request, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 3ddeb118c33..98aa66da271 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1143,12 +1143,15 @@ API_ROUTE_TO_CALL_TYPES: Final[Mapping[str, Sequence[CallTypes]]] = { "/responses": (CallTypes.aresponses, CallTypes.responses), "/v1/responses": (CallTypes.aresponses, CallTypes.responses), "/openai/v1/responses": (CallTypes.aresponses, CallTypes.responses), + "/openai/responses": (CallTypes.aresponses, CallTypes.responses), "/responses/{response_id}": (CallTypes.aresponses, CallTypes.responses), "/v1/responses/{response_id}": (CallTypes.aresponses, CallTypes.responses), "/openai/v1/responses/{response_id}": (CallTypes.aresponses, CallTypes.responses), + "/openai/responses/{response_id}": (CallTypes.aresponses, CallTypes.responses), "/responses/{response_id}/input_items": (CallTypes.alist_input_items,), "/v1/responses/{response_id}/input_items": (CallTypes.alist_input_items,), "/openai/v1/responses/{response_id}/input_items": (CallTypes.alist_input_items,), + "/openai/responses/{response_id}/input_items": (CallTypes.alist_input_items,), # Realtime API "/realtime": (CallTypes.arealtime,), "/v1/realtime": (CallTypes.arealtime,), diff --git a/tests/unit/proxy/test_openai_responses_routes.py b/tests/unit/proxy/test_openai_responses_routes.py new file mode 100644 index 00000000000..3c4595c88ce --- /dev/null +++ b/tests/unit/proxy/test_openai_responses_routes.py @@ -0,0 +1,43 @@ +from litellm.proxy._types import LiteLLMRoutes +from litellm.proxy.agent_endpoints.auth.managed_authorization import _MANAGED_MODEL_ROUTES +from litellm.proxy.auth.auth_utils import normalize_request_route +from litellm.proxy.auth.route_checks import _UNBILLED_ROUTES +from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec +from litellm.proxy.response_api_endpoints.endpoints import router +from litellm.types.utils import API_ROUTE_TO_CALL_TYPES, CallTypes + + +def test_openai_responses_routes_registered(): + """Verify that Azure-style /openai/responses endpoints are registered on the router.""" + routes = {getattr(r, "path", None) for r in router.routes} + + expected_routes = [ + "/openai/responses", + "/openai/responses/{response_id}", + "/openai/responses/{response_id}/input_items", + "/openai/responses/compact", + "/openai/responses/input_tokens", + "/openai/responses/{response_id}/cancel", + ] + + for expected in expected_routes: + assert expected in routes, f"Route {expected} missing from router.routes!" + + assert "/openai/responses" in CustomOpenAPISpec.RESPONSES_API_PATHS + assert "/openai/responses" in _MANAGED_MODEL_ROUTES + assert "/openai/responses/input_tokens" in _UNBILLED_ROUTES + assert "/openai/responses" in LiteLLMRoutes.openai_routes.value + + +def test_openai_responses_api_route_to_call_types(): + """Verify that /openai/responses mappings exist in API_ROUTE_TO_CALL_TYPES for guardrails/tools.""" + assert API_ROUTE_TO_CALL_TYPES.get("/openai/responses") == (CallTypes.aresponses, CallTypes.responses) + assert API_ROUTE_TO_CALL_TYPES.get("/openai/responses/{response_id}") == (CallTypes.aresponses, CallTypes.responses) + assert API_ROUTE_TO_CALL_TYPES.get("/openai/responses/{response_id}/input_items") == (CallTypes.alist_input_items,) + + +def test_openai_responses_metrics_normalization(): + """Verify that dynamic response IDs are normalized to prevent Prometheus high-cardinality metrics.""" + assert normalize_request_route("/openai/responses/resp-123456") == "/openai/responses/{response_id}" + assert normalize_request_route("/openai/responses/resp-123456/cancel") == "/openai/responses/{response_id}/cancel" + assert normalize_request_route("/openai/responses/resp-123456/input_items") == "/openai/responses/{response_id}/input_items"