fix(proxy): add /openai/responses router routes for Azure SDK Responses API (Fixes #45033)

- Register /openai/responses, /compact, /input_tokens, /{id}, /cancel, and /{id}/input_items endpoints on the proxy router
- Update RESPONSES_API_PATHS, _MANAGED_MODEL_ROUTES, _UNBILLED_ROUTES, and LiteLLMRoutes
- Prevent AzureOpenAI Responses API calls from falling into wildcard pass-through
- Add unit test coverage in tests/proxy_unit_tests/test_openai_responses_routes.py
This commit is contained in:
Chau Vu / CPF-FAMILY 2026-10-08 20:26:13 +07:00
parent d8c0e2c715
commit 449b37364b
6 changed files with 74 additions and 1 deletions

View file

@ -454,18 +454,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

@ -37,6 +37,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

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

@ -213,6 +213,12 @@ async def _resolve_cursor_model_variant_before_auth(request: Request) -> None:
tags=["responses"],
responses=RESPONSES_API_CREATE_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,
@ -696,6 +702,12 @@ async def cursor_chat_completions(
tags=["responses"],
responses=RESPONSES_API_RESPONSE_SCHEMAS,
)
@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,
@ -812,6 +824,12 @@ async def get_response(
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,
@ -921,6 +939,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,
@ -989,6 +1013,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,
@ -1144,6 +1173,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,
@ -1217,6 +1251,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

@ -236,6 +236,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

@ -0,0 +1,27 @@
from litellm.proxy._types import LiteLLMRoutes
from litellm.proxy.agent_endpoints.auth.managed_authorization import _MANAGED_MODEL_ROUTES
from litellm.proxy.common_utils.custom_openapi_spec import CustomOpenAPISpec
from litellm.proxy.response_api_endpoints.endpoints import router
from litellm.proxy.spend_tracking.budget_reservation import _UNBILLED_ROUTES
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