Development environment setup (#22432)

* feat: add Cursor Cloud Agents as a native pass-through provider

- Add CURSOR to LlmProviders enum
- Add /cursor/{endpoint:path} pass-through route with Basic Auth
- Add /cursor to mapped_pass_through_routes for proper routing
- Create CursorPassthroughLoggingHandler for Logs page visibility
  - Classifies operations (agent:create, agent:list, models:list, etc.)
  - Logs model as cursor/cursor:<operation> for clean Logs display
  - Tracks cost as $0 (subscription-based, no per-request pricing)
- Add Cursor to UI: provider enum, logo, credential fields
- Add provider_create_fields.json entry for LLM Credentials UI
- Add 18 unit tests covering route, auth, logging, and classification

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix: use correct Cursor logo from lobehub, add documentation page

- Replace placeholder Cursor logo with official hexagonal logo from lobehub
- Add docs/pass_through/cursor.md with full tutorial matching a2a_cost_tracking style
  - Quick Start: add creds on UI, start proxy, launch agent, view logs
  - Examples: all Cursor Cloud Agents API endpoints
  - Advanced: virtual key usage
  - Screenshots: credential form, logs page, log detail view
- Add Cursor to sidebars.js under Pass-through Endpoints
- Add screenshots to docs/my-website/img/

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* docs: simplify Cursor doc - UI-only flow, no config.yaml needed

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

* fix: cursor pass-through reads credentials from UI (litellm.credential_list)

The pass-through route now checks litellm.credential_list as a fallback
when CURSOR_API_KEY env var is not set. This means adding credentials
via the UI (Models + Endpoints → LLM Credentials) works without any
config.yaml or environment variable setup.

Credential lookup order:
1. passthrough_endpoint_router (config.yaml with use_in_pass_through)
2. litellm.credential_list (credentials added via UI)
3. CURSOR_API_KEY environment variable

Also respects api_base from UI credentials if set.

Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
Ishaan Jaff
2026-02-28 14:50:06 -08:00
committed by GitHub
co-authored by Cursor Agent Ishaan Jaff
parent bc9c28eb80
commit e3756252a8
15 changed files with 765 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
import Image from '@theme/IdealImage';
# Cursor Cloud Agents
Pass-through endpoints for the [Cursor Cloud Agents API](https://docs.cursor.com/account/api) — launch and manage cloud agents that work on your repositories, in native format (no translation).
| Feature | Supported | Notes |
|---------|-----------|-------|
| Cost Tracking | ✅ | Logged as $0.00 (subscription-based, no per-request pricing) |
| Logging | ✅ | All requests logged with operation classification |
| End-user Tracking | ❌ | [Tell us if you need this](https://github.com/BerriAI/litellm/issues/new) |
| Streaming | ❌ | Cursor API does not use streaming |
Just replace `https://api.cursor.com` with `LITELLM_PROXY_BASE_URL/cursor` 🚀
**Supported endpoints:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v0/agents` | GET | List agents |
| `/v0/agents` | POST | Launch an agent |
| `/v0/agents/{id}` | GET | Agent status |
| `/v0/agents/{id}` | DELETE | Delete an agent |
| `/v0/agents/{id}/conversation` | GET | Agent conversation |
| `/v0/agents/{id}/followup` | POST | Add follow-up |
| `/v0/agents/{id}/stop` | POST | Stop an agent |
| `/v0/me` | GET | API key info |
| `/v0/models` | GET | List models |
| `/v0/repositories` | GET | List GitHub repositories |
## Quick Start
### 1. Add Cursor API Key on the UI
Navigate to **Models + Endpoints → LLM Credentials** and click **Add Credential**. Select **Cursor** from the provider dropdown — you'll see the Cursor logo. Enter your API key from [cursor.com/settings](https://cursor.com/settings).
<Image img={require('../../img/cursor_add_credential.png')} alt="Add Cursor credential with logo" style={{maxWidth: '800px'}} />
### 2. Launch a Cursor Agent
```bash
curl -X POST http://0.0.0.0:4000/cursor/v0/agents \
-H "Authorization: Bearer <your-litellm-key>" \
-H "Content-Type: application/json" \
-d '{
"prompt": {
"text": "Add a README.md with installation instructions"
},
"source": {
"repository": "https://github.com/your-org/your-repo",
"ref": "main"
},
"target": {
"autoCreatePr": true
}
}'
```
**Expected Response:**
```json
{
"id": "bc_abc123",
"name": "Add README Documentation",
"status": "CREATING",
"source": {
"repository": "https://github.com/your-org/your-repo",
"ref": "main"
},
"target": {
"branchName": "cursor/add-readme-1234",
"url": "https://cursor.com/agents?id=bc_abc123",
"autoCreatePr": true
},
"createdAt": "2024-01-15T10:30:00Z"
}
```
### 3. View Logs
Navigate to **Logs** in the sidebar. Filter by "cursor" to see your agent requests. Each request shows the operation type (e.g., `cursor/cursor:agent:create`), status, duration, and cost.
<Image img={require('../../img/cursor_logs.png')} alt="Cursor requests in Logs page" style={{maxWidth: '800px'}} />
Click on any log entry to see full request details including provider, API base, and metadata.
<Image img={require('../../img/cursor_log_detail.png')} alt="Cursor log entry detail" style={{maxWidth: '800px'}} />
## Examples
Anything after `http://0.0.0.0:4000/cursor` is treated as a provider-specific route, and handled accordingly.
| **Original Endpoint** | **Replace With** |
|---|---|
| `https://api.cursor.com` | `http://0.0.0.0:4000/cursor` (LITELLM_PROXY_BASE_URL) |
| `-u YOUR_API_KEY:` (Basic Auth) | `-H "Authorization: Bearer <your-litellm-key>"` (LiteLLM Virtual Key) |
### List Available Models
```bash
curl http://0.0.0.0:4000/cursor/v0/models \
-H "Authorization: Bearer <your-litellm-key>"
```
### Check Agent Status
```bash
curl http://0.0.0.0:4000/cursor/v0/agents/bc_abc123 \
-H "Authorization: Bearer <your-litellm-key>"
```
### List All Agents
```bash
curl http://0.0.0.0:4000/cursor/v0/agents \
-H "Authorization: Bearer <your-litellm-key>"
```
### Add Follow-up to Agent
```bash
curl -X POST http://0.0.0.0:4000/cursor/v0/agents/bc_abc123/followup \
-H "Authorization: Bearer <your-litellm-key>" \
-H "Content-Type: application/json" \
-d '{
"prompt": {
"text": "Also add a section about troubleshooting"
}
}'
```
### Stop an Agent
```bash
curl -X POST http://0.0.0.0:4000/cursor/v0/agents/bc_abc123/stop \
-H "Authorization: Bearer <your-litellm-key>"
```
### Delete an Agent
```bash
curl -X DELETE http://0.0.0.0:4000/cursor/v0/agents/bc_abc123 \
-H "Authorization: Bearer <your-litellm-key>"
```
### Get API Key Info
```bash
curl http://0.0.0.0:4000/cursor/v0/me \
-H "Authorization: Bearer <your-litellm-key>"
```
## Related
- [Cursor Cloud Agents API Docs](https://docs.cursor.com/account/api)
- [Pass-through Endpoints Overview](./intro.md)
- [Virtual Keys](../proxy/virtual_keys.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+1
View File
@@ -635,6 +635,7 @@ const sidebars = {
"pass_through/bedrock",
"pass_through/azure_passthrough",
"pass_through/cohere",
"pass_through/cursor",
"pass_through/google_ai_studio",
"pass_through/langfuse",
"pass_through/mistral",
+1
View File
@@ -383,6 +383,7 @@ class LiteLLMRoutes(enum.Enum):
"/vertex-ai",
"/vertex_ai",
"/cohere",
"/cursor",
"/gemini",
"/anthropic",
"/langfuse",
@@ -2086,6 +2086,93 @@ class BaseOpenAIPassThroughHandler:
return joined_path_str
@router.api_route(
"/cursor/{endpoint:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
tags=["Cursor Pass-through", "pass-through"],
)
async def cursor_proxy_route(
endpoint: str,
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Pass-through endpoint for the Cursor Cloud Agents API.
Supports all Cursor Cloud Agents endpoints:
- GET /v0/agents List agents
- POST /v0/agents Launch an agent
- GET /v0/agents/{id} Agent status
- GET /v0/agents/{id}/conversation Agent conversation
- POST /v0/agents/{id}/followup Add follow-up
- POST /v0/agents/{id}/stop Stop an agent
- DELETE /v0/agents/{id} Delete an agent
- GET /v0/me API key info
- GET /v0/models List models
- GET /v0/repositories List GitHub repositories
Uses Basic Authentication (base64-encoded `API_KEY:`).
Credential lookup order:
1. passthrough_endpoint_router (config.yaml deployments with use_in_pass_through)
2. litellm.credential_list (credentials added via UI)
3. CURSOR_API_KEY environment variable
"""
import base64
base_target_url = os.getenv("CURSOR_API_BASE") or "https://api.cursor.com"
cursor_api_key = passthrough_endpoint_router.get_credentials(
custom_llm_provider="cursor",
region_name=None,
)
if cursor_api_key is None:
for credential in litellm.credential_list:
if (
credential.credential_info
and credential.credential_info.get("custom_llm_provider") == "cursor"
):
cursor_api_key = credential.credential_values.get("api_key")
credential_api_base = credential.credential_values.get("api_base")
if credential_api_base:
base_target_url = credential_api_base
break
if cursor_api_key is None:
raise HTTPException(
status_code=401,
detail="Cursor API key not found. Add Cursor credentials via the UI (Models + Endpoints → LLM Credentials) or set CURSOR_API_KEY environment variable.",
)
encoded_endpoint = httpx.URL(endpoint).path
if not encoded_endpoint.startswith("/"):
encoded_endpoint = "/" + encoded_endpoint
base_url = httpx.URL(base_target_url)
updated_url = base_url.copy_with(path=encoded_endpoint)
auth_value = base64.b64encode(
f"{cursor_api_key}:".encode("utf-8")
).decode("ascii")
endpoint_func = create_pass_through_route(
endpoint=endpoint,
target=str(updated_url),
custom_headers={"Authorization": f"Basic {auth_value}"},
custom_llm_provider="cursor",
)
received_value = await endpoint_func(
request,
fastapi_response,
user_api_key_dict,
)
return received_value
async def vertex_ai_live_websocket_passthrough(
websocket: WebSocket,
model: Optional[str] = None,
@@ -0,0 +1,141 @@
"""
Cursor Cloud Agents API - Pass-through Logging Handler
Transforms Cursor API responses into standardized logging payloads
so they appear cleanly in the LiteLLM Logs page.
"""
from datetime import datetime
from typing import Dict
import httpx
from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.litellm_logging import (
get_standard_logging_object_payload,
)
from litellm.proxy._types import PassThroughEndpointLoggingTypedDict
from litellm.types.utils import StandardPassThroughResponseObject
CURSOR_AGENT_ENDPOINTS: Dict[str, str] = {
"POST /v0/agents": "cursor:agent:create",
"GET /v0/agents": "cursor:agent:list",
"POST /v0/agents/{id}/followup": "cursor:agent:followup",
"POST /v0/agents/{id}/stop": "cursor:agent:stop",
"DELETE /v0/agents/{id}": "cursor:agent:delete",
"GET /v0/agents/{id}/conversation": "cursor:agent:conversation",
"GET /v0/agents/{id}": "cursor:agent:status",
"GET /v0/me": "cursor:account:info",
"GET /v0/models": "cursor:models:list",
"GET /v0/repositories": "cursor:repositories:list",
}
def _classify_cursor_request(method: str, path: str) -> str:
"""Classify a Cursor API request into a readable operation name."""
normalized = path.rstrip("/")
for pattern, operation in CURSOR_AGENT_ENDPOINTS.items():
pat_method, pat_path = pattern.split(" ", 1)
if method.upper() != pat_method:
continue
pat_parts = pat_path.strip("/").split("/")
req_parts = normalized.strip("/").split("/")
if len(pat_parts) != len(req_parts):
continue
match = True
for pp, rp in zip(pat_parts, req_parts):
if pp.startswith("{") and pp.endswith("}"):
continue
if pp != rp:
match = False
break
if match:
return operation
return f"cursor:{method.lower()}:{normalized}"
class CursorPassthroughLoggingHandler:
"""Handles logging for Cursor Cloud Agents pass-through requests."""
@staticmethod
def cursor_passthrough_handler(
httpx_response: httpx.Response,
response_body: dict,
logging_obj: LiteLLMLoggingObj,
url_route: str,
result: str,
start_time: datetime,
end_time: datetime,
cache_hit: bool,
request_body: dict,
**kwargs,
) -> PassThroughEndpointLoggingTypedDict:
"""
Transform a Cursor API response into a standard logging payload.
"""
try:
method = httpx_response.request.method
path = httpx.URL(url_route).path
operation = _classify_cursor_request(method, path)
agent_id = response_body.get("id", "")
agent_name = response_body.get("name", "")
agent_status = response_body.get("status", "")
model_name = f"cursor/{operation}"
summary_parts = []
if agent_id:
summary_parts.append(f"id={agent_id}")
if agent_name:
summary_parts.append(f"name={agent_name}")
if agent_status:
summary_parts.append(f"status={agent_status}")
response_summary = ", ".join(summary_parts) if summary_parts else result
kwargs["model"] = model_name
kwargs["response_cost"] = 0.0
logging_obj.model_call_details["model"] = model_name
logging_obj.model_call_details["custom_llm_provider"] = "cursor"
logging_obj.model_call_details["response_cost"] = 0.0
standard_logging_object = get_standard_logging_object_payload(
kwargs=kwargs,
init_response_obj=StandardPassThroughResponseObject(
response=response_summary
),
start_time=start_time,
end_time=end_time,
logging_obj=logging_obj,
status="success",
)
kwargs["standard_logging_object"] = standard_logging_object
verbose_proxy_logger.debug(
"Cursor passthrough logging: operation=%s, agent_id=%s",
operation,
agent_id,
)
return {
"result": StandardPassThroughResponseObject(
response=response_summary
),
"kwargs": kwargs,
}
except Exception as e:
verbose_proxy_logger.exception(
"Error in Cursor passthrough logging handler: %s", e
)
return {
"result": StandardPassThroughResponseObject(response=result),
"kwargs": kwargs,
}
@@ -22,6 +22,9 @@ from .llm_provider_handlers.assembly_passthrough_logging_handler import (
from .llm_provider_handlers.cohere_passthrough_logging_handler import (
CoherePassthroughLoggingHandler,
)
from .llm_provider_handlers.cursor_passthrough_logging_handler import (
CursorPassthroughLoggingHandler,
)
from .llm_provider_handlers.gemini_passthrough_logging_handler import (
GeminiPassthroughLoggingHandler,
)
@@ -60,6 +63,9 @@ class PassThroughEndpointLogging:
# Gemini
self.TRACKED_GEMINI_ROUTES = ["generateContent", "streamGenerateContent", "predictLongRunning"]
# Cursor Cloud Agents
self.TRACKED_CURSOR_ROUTES = ["/v0/agents", "/v0/me", "/v0/models", "/v0/repositories"]
# Vertex AI Live API WebSocket
self.TRACKED_VERTEX_AI_LIVE_ROUTES = ["/vertex_ai/live"]
@@ -223,6 +229,25 @@ class PassThroughEndpointLogging:
)
kwargs = openai_passthrough_logging_handler_result["kwargs"]
elif self.is_cursor_route(url_route, custom_llm_provider):
cursor_passthrough_logging_handler_result = (
CursorPassthroughLoggingHandler.cursor_passthrough_handler(
httpx_response=httpx_response,
response_body=response_body or {},
logging_obj=logging_obj,
url_route=url_route,
result=result,
start_time=start_time,
end_time=end_time,
cache_hit=cache_hit,
request_body=request_body,
**kwargs,
)
)
standard_logging_response_object = (
cursor_passthrough_logging_handler_result["result"]
)
kwargs = cursor_passthrough_logging_handler_result["kwargs"]
elif self.is_vertex_ai_live_route(url_route):
from .llm_provider_handlers.vertex_ai_live_passthrough_logging_handler import (
VertexAILivePassthroughLoggingHandler,
@@ -385,6 +410,22 @@ class PassThroughEndpointLogging:
return True
return False
def is_cursor_route(
self, url_route: str, custom_llm_provider: Optional[str] = None
):
"""Check if the URL route is a Cursor Cloud Agents API route."""
if custom_llm_provider == "cursor":
return True
parsed_url = urlparse(url_route)
if parsed_url.hostname and "api.cursor.com" in parsed_url.hostname:
return True
for route in self.TRACKED_CURSOR_ROUTES:
if route in url_route:
path = parsed_url.path if parsed_url.scheme else url_route
if path.startswith("/v0/"):
return custom_llm_provider == "cursor"
return False
def is_openai_route(self, url_route: str):
"""Check if the URL route is an OpenAI API route."""
if not url_route:
@@ -3025,5 +3025,33 @@
}
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "CURSOR",
"provider_display_name": "Cursor",
"litellm_provider": "cursor",
"credential_fields": [
{
"key": "api_key",
"label": "Cursor API Key",
"placeholder": "Your Cursor API key from cursor.com/settings",
"tooltip": "Get your API key from the Cursor Dashboard at cursor.com/settings",
"required": true,
"field_type": "password",
"options": null,
"default_value": null
},
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.cursor.com",
"tooltip": "Cursor API base URL (defaults to https://api.cursor.com)",
"required": false,
"field_type": "text",
"options": null,
"default_value": "https://api.cursor.com"
}
],
"default_model_placeholder": "cursor/claude-4-sonnet"
}
]
+1
View File
@@ -3185,6 +3185,7 @@ class LlmProviders(str, Enum):
CHUTES = "chutes"
XIAOMI_MIMO = "xiaomi_mimo"
LITELLM_AGENT = "litellm_agent"
CURSOR = "cursor"
# Create a set of all provider values for quick lookup
@@ -0,0 +1,116 @@
import os
import sys
from datetime import datetime
from unittest.mock import MagicMock
import httpx
import pytest
sys.path.insert(0, os.path.abspath("../../../../.."))
from litellm.proxy.pass_through_endpoints.llm_provider_handlers.cursor_passthrough_logging_handler import (
CursorPassthroughLoggingHandler,
_classify_cursor_request,
)
from litellm.proxy.pass_through_endpoints.success_handler import (
PassThroughEndpointLogging,
)
class TestClassifyCursorRequest:
def test_should_classify_create_agent(self):
assert _classify_cursor_request("POST", "/v0/agents") == "cursor:agent:create"
def test_should_classify_list_agents(self):
assert _classify_cursor_request("GET", "/v0/agents") == "cursor:agent:list"
def test_should_classify_agent_status(self):
assert (
_classify_cursor_request("GET", "/v0/agents/bc_abc123")
== "cursor:agent:status"
)
def test_should_classify_agent_conversation(self):
assert (
_classify_cursor_request("GET", "/v0/agents/bc_abc123/conversation")
== "cursor:agent:conversation"
)
def test_should_classify_agent_followup(self):
assert (
_classify_cursor_request("POST", "/v0/agents/bc_abc123/followup")
== "cursor:agent:followup"
)
def test_should_classify_agent_stop(self):
assert (
_classify_cursor_request("POST", "/v0/agents/bc_abc123/stop")
== "cursor:agent:stop"
)
def test_should_classify_agent_delete(self):
assert (
_classify_cursor_request("DELETE", "/v0/agents/bc_abc123")
== "cursor:agent:delete"
)
def test_should_classify_me_endpoint(self):
assert _classify_cursor_request("GET", "/v0/me") == "cursor:account:info"
def test_should_classify_models_endpoint(self):
assert _classify_cursor_request("GET", "/v0/models") == "cursor:models:list"
def test_should_classify_repositories_endpoint(self):
assert (
_classify_cursor_request("GET", "/v0/repositories")
== "cursor:repositories:list"
)
class TestCursorRouteDetection:
def test_should_detect_cursor_route_by_custom_llm_provider(self):
handler = PassThroughEndpointLogging()
assert handler.is_cursor_route("https://api.cursor.com/v0/agents", "cursor")
def test_should_detect_cursor_route_by_hostname(self):
handler = PassThroughEndpointLogging()
assert handler.is_cursor_route("https://api.cursor.com/v0/agents")
def test_should_not_detect_non_cursor_route(self):
handler = PassThroughEndpointLogging()
assert not handler.is_cursor_route("https://api.openai.com/v1/completions")
class TestCursorPassthroughHandler:
def test_should_log_agent_creation_response(self):
mock_response = MagicMock(spec=httpx.Response)
mock_response.request = MagicMock()
mock_response.request.method = "POST"
mock_response.text = '{"id": "bc_abc123"}'
mock_logging_obj = MagicMock()
mock_logging_obj.litellm_call_id = "test-call-id"
mock_logging_obj.model_call_details = {}
response_body = {
"id": "bc_abc123",
"name": "Test Agent",
"status": "CREATING",
}
result = CursorPassthroughLoggingHandler.cursor_passthrough_handler(
httpx_response=mock_response,
response_body=response_body,
logging_obj=mock_logging_obj,
url_route="https://api.cursor.com/v0/agents",
result='{"id": "bc_abc123"}',
start_time=datetime.now(),
end_time=datetime.now(),
cache_hit=False,
request_body={"prompt": {"text": "Add README"}},
)
assert result["result"] is not None
assert result["kwargs"]["model"] == "cursor/cursor:agent:create"
assert result["kwargs"]["response_cost"] == 0.0
assert mock_logging_obj.model_call_details["custom_llm_provider"] == "cursor"
@@ -20,6 +20,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
RouteChecks,
bedrock_llm_proxy_route,
create_pass_through_route,
cursor_proxy_route,
llm_passthrough_factory_proxy_route,
milvus_proxy_route,
openai_proxy_route,
@@ -2377,3 +2378,188 @@ class TestOpenAIPassthroughRoute:
# Verify result
assert result == {"id": "asst_123", "object": "assistant"}
class TestCursorProxyRoute:
"""Tests for the Cursor Cloud Agents pass-through route."""
@pytest.mark.asyncio
async def test_cursor_proxy_route_creates_pass_through_with_basic_auth(self):
"""should create a pass-through route with Basic Auth header for Cursor API"""
import base64
mock_request = MagicMock(spec=Request)
mock_request.method = "GET"
mock_request.query_params = {}
mock_request.headers = {}
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
test_api_key = "test-cursor-api-key-123"
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
return_value=test_api_key,
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route:
mock_endpoint_func = AsyncMock(
return_value={"agents": [], "nextCursor": None}
)
mock_create_route.return_value = mock_endpoint_func
result = await cursor_proxy_route(
endpoint="v0/agents",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
mock_create_route.assert_called_once()
call_args = mock_create_route.call_args[1]
assert call_args["target"] == "https://api.cursor.com/v0/agents"
expected_auth = base64.b64encode(
f"{test_api_key}:".encode("utf-8")
).decode("ascii")
assert call_args["custom_headers"]["Authorization"] == f"Basic {expected_auth}"
assert result == {"agents": [], "nextCursor": None}
@pytest.mark.asyncio
async def test_cursor_proxy_route_raises_on_missing_api_key(self):
"""should raise 401 when no Cursor API key is available"""
mock_request = MagicMock(spec=Request)
mock_request.method = "POST"
mock_request.query_params = {}
mock_request.headers = {}
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
return_value=None,
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.litellm.credential_list",
[],
):
with pytest.raises(Exception) as exc_info:
await cursor_proxy_route(
endpoint="v0/agents",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_cursor_proxy_route_uses_ui_credential(self):
"""should use credentials added via UI (litellm.credential_list) when env var is not set"""
from litellm.types.utils import CredentialItem
mock_request = MagicMock(spec=Request)
mock_request.method = "GET"
mock_request.query_params = {}
mock_request.headers = {}
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
ui_credential = CredentialItem(
credential_name="my-cursor-key",
credential_values={"api_key": "crsr_ui_test_key", "api_base": "https://api.cursor.com"},
credential_info={"custom_llm_provider": "cursor"},
)
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
return_value=None,
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.litellm.credential_list",
[ui_credential],
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route:
mock_endpoint_func = AsyncMock(return_value={"models": []})
mock_create_route.return_value = mock_endpoint_func
result = await cursor_proxy_route(
endpoint="v0/models",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
call_args = mock_create_route.call_args[1]
assert call_args["target"] == "https://api.cursor.com/v0/models"
import base64
expected_auth = base64.b64encode(b"crsr_ui_test_key:").decode("ascii")
assert call_args["custom_headers"]["Authorization"] == f"Basic {expected_auth}"
@pytest.mark.asyncio
async def test_cursor_proxy_route_custom_api_base(self):
"""should use CURSOR_API_BASE env var when set"""
mock_request = MagicMock(spec=Request)
mock_request.method = "GET"
mock_request.query_params = {}
mock_request.headers = {}
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch.dict(
os.environ, {"CURSOR_API_BASE": "https://custom-cursor.example.com"}
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
return_value="test-key",
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route:
mock_endpoint_func = AsyncMock(return_value={})
mock_create_route.return_value = mock_endpoint_func
await cursor_proxy_route(
endpoint="v0/me",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
call_args = mock_create_route.call_args[1]
assert call_args["target"] == "https://custom-cursor.example.com/v0/me"
@pytest.mark.asyncio
async def test_cursor_proxy_route_launch_agent(self):
"""should handle POST to launch an agent through the pass-through"""
mock_request = MagicMock(spec=Request)
mock_request.method = "POST"
mock_request.query_params = {}
mock_request.headers = {"content-type": "application/json"}
mock_response = MagicMock(spec=Response)
mock_user_api_key_dict = MagicMock()
with patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials",
return_value="test-key",
), patch(
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route"
) as mock_create_route:
mock_endpoint_func = AsyncMock(
return_value={
"id": "bc_abc123",
"name": "Test Agent",
"status": "CREATING",
}
)
mock_create_route.return_value = mock_endpoint_func
result = await cursor_proxy_route(
endpoint="v0/agents",
request=mock_request,
fastapi_response=mock_response,
user_api_key_dict=mock_user_api_key_dict,
)
call_args = mock_create_route.call_args[1]
assert call_args["target"] == "https://api.cursor.com/v0/agents"
assert result["id"] == "bc_abc123"
assert result["status"] == "CREATING"
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>

After

Width:  |  Height:  |  Size: 548 B

@@ -9,6 +9,7 @@ export enum Providers {
Azure_AI_Studio = "Azure AI Foundry (Studio)",
Cerebras = "Cerebras",
Cohere = "Cohere",
Cursor = "Cursor",
Dashscope = "Dashscope",
Databricks = "Databricks (Qwen API)",
DeepInfra = "DeepInfra",
@@ -60,6 +61,7 @@ export const provider_map: Record<string, string> = {
MiniMax: "minimax",
MistralAI: "mistral",
Cohere: "cohere",
Cursor: "cursor",
OpenAI_Compatible: "openai",
OpenAI_Text_Compatible: "text-completion-openai",
Vertex_AI: "vertex_ai",
@@ -107,6 +109,7 @@ export const providerLogoMap: Record<string, string> = {
[Providers.SageMaker]: `${asset_logos_folder}bedrock.svg`,
[Providers.Cerebras]: `${asset_logos_folder}cerebras.svg`,
[Providers.Cohere]: `${asset_logos_folder}cohere.svg`,
[Providers.Cursor]: `${asset_logos_folder}cursor.svg`,
[Providers.Databricks]: `${asset_logos_folder}databricks.svg`,
[Providers.Dashscope]: `${asset_logos_folder}dashscope.svg`,
[Providers.Deepseek]: `${asset_logos_folder}deepseek.svg`,
@@ -206,6 +209,8 @@ export const getPlaceholder = (selectedProvider: string): string => {
return "runwayml/gen4_turbo";
} else if (selectedProvider === Providers.Watsonx) {
return "watsonx/ibm/granite-3-3-8b-instruct";
} else if (selectedProvider === Providers.Cursor) {
return "cursor/claude-4-sonnet";
} else {
return "gpt-3.5-turbo";
}