feat(proxy): serve the OpenAI model list at /cursor/models for BYOK base URLs

This commit is contained in:
Tin Chi Lo 2026-07-20 16:36:57 -07:00
parent 19c875fa82
commit 96916f29a6
4 changed files with 149 additions and 0 deletions

View file

@ -287,6 +287,8 @@ class LiteLLMRoutes(enum.Enum):
"/chat/completions",
"/v1/chat/completions",
"/cursor/chat/completions",
"/cursor/models",
"/cursor/v1/models",
# completions
"/engines/{model}/completions",
"/openai/deployments/{model}/completions",

View file

@ -22,6 +22,8 @@ from litellm.types.responses.main import DeleteResponseResult
router = APIRouter()
_user_api_key_auth_dep = Depends(user_api_key_auth)
@router.post(
"/v1/responses",
@ -283,6 +285,33 @@ async def responses_api(
)
@router.get(
"/cursor/models",
dependencies=[Depends(user_api_key_auth)],
tags=["responses"],
)
@router.get(
"/cursor/v1/models",
dependencies=[Depends(user_api_key_auth)],
tags=["responses"],
)
async def cursor_model_list(
user_api_key_dict: UserAPIKeyAuth = _user_api_key_auth_dep,
):
"""
OpenAI-compatible model listing for the Cursor BYOK base URL.
Clients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and
verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
route those requests fall through to the Cursor Cloud Agents passthrough, which
demands a Cursor API key and 401s, so key verification silently fails before any
chat request is ever sent. Delegates to the standard `/v1/models` handler.
"""
from litellm.proxy.proxy_server import model_list
return await model_list(user_api_key_dict=user_api_key_dict)
@router.post(
"/cursor/chat/completions",
dependencies=[Depends(user_api_key_auth)],

View file

@ -881,3 +881,29 @@ def test_cursor_chat_completions_input_body_uses_responses_pipeline_and_strips_s
followup_body = asyncio.run(real_read_request_body(request=captured_requests[0]))
assert followup_body.get("stream_options") == {"include_usage": True}
assert followup_body.get("input") == [{"role": "user", "content": "hello"}]
def test_cursor_models_route_delegates_to_model_list():
"""Clients pointed at <proxy>/cursor as an OpenAI-compatible base URL resolve and
verify keys via GET {base}/models (the OpenAI SDK contract). Without a dedicated
route those requests fall through to the Cursor Cloud Agents passthrough and 401
for lack of a Cursor API key, so BYOK verification fails before any chat request
is sent. Both /cursor/models and /cursor/v1/models must serve the standard model
list instead."""
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
import litellm.proxy.proxy_server as ps
model_payload = {"data": [{"id": "gpt-5.6", "object": "model"}], "object": "list"}
app.dependency_overrides[user_api_key_auth] = _auth_override
try:
with patch.object(ps, "model_list", AsyncMock(return_value=model_payload)) as mock_model_list:
client = TestClient(app)
for path in ("/cursor/models", "/cursor/v1/models"):
response = client.get(path, headers={"Authorization": "Bearer sk-test-cursor"})
assert response.status_code == 200, f"{path}: {response.text}"
assert response.json() == model_payload
assert mock_model_list.call_count == 2
finally:
app.dependency_overrides.pop(user_api_key_auth, None)

View file

@ -2645,6 +2645,58 @@ export interface paths {
patch?: never;
trace?: never;
};
"/cursor/models": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Cursor Model List
* @description OpenAI-compatible model listing for the Cursor BYOK base URL.
*
* Clients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and
* verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
* route those requests fall through to the Cursor Cloud Agents passthrough, which
* demands a Cursor API key and 401s, so key verification silently fails before any
* chat request is ever sent. Delegates to the standard `/v1/models` handler.
*/
get: operations["cursor_model_list_cursor_models_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/cursor/v1/models": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Cursor Model List
* @description OpenAI-compatible model listing for the Cursor BYOK base URL.
*
* Clients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and
* verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
* route those requests fall through to the Cursor Cloud Agents passthrough, which
* demands a Cursor API key and 401s, so key verification silently fails before any
* chat request is ever sent. Delegates to the standard `/v1/models` handler.
*/
get: operations["cursor_model_list_cursor_v1_models_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/cursor/{endpoint}": {
parameters: {
query?: never;
@ -38506,6 +38558,46 @@ export interface operations {
};
};
};
cursor_model_list_cursor_models_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
};
};
cursor_model_list_cursor_v1_models_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
};
};
cursor_proxy_route_cursor__endpoint__get: {
parameters: {
query?: never;