mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
feat(proxy): serve the OpenAI model list at /cursor/models for BYOK base URLs
This commit is contained in:
parent
19c875fa82
commit
96916f29a6
4 changed files with 149 additions and 0 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
92
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
92
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue