This commit is contained in:
Chau Vu 2026-11-06 19:29:24 -08:00 • committed by GitHub
commit ebfdda44bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 101 additions and 1 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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}"),

View file

@ -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"})

View file

@ -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:

View file

@ -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,

View file

@ -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,),

View file

@ -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"