diff --git a/docs/my-website/docs/pass_through/cursor.md b/docs/my-website/docs/pass_through/cursor.md
new file mode 100644
index 0000000000..5726c6bae2
--- /dev/null
+++ b/docs/my-website/docs/pass_through/cursor.md
@@ -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).
+
+
+
+### 2. Launch a Cursor Agent
+
+```bash
+curl -X POST http://0.0.0.0:4000/cursor/v0/agents \
+ -H "Authorization: Bearer " \
+ -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.
+
+
+
+Click on any log entry to see full request details including provider, API base, and metadata.
+
+
+
+## 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 "` (LiteLLM Virtual Key) |
+
+### List Available Models
+
+```bash
+curl http://0.0.0.0:4000/cursor/v0/models \
+ -H "Authorization: Bearer "
+```
+
+### Check Agent Status
+
+```bash
+curl http://0.0.0.0:4000/cursor/v0/agents/bc_abc123 \
+ -H "Authorization: Bearer "
+```
+
+### List All Agents
+
+```bash
+curl http://0.0.0.0:4000/cursor/v0/agents \
+ -H "Authorization: Bearer "
+```
+
+### Add Follow-up to Agent
+
+```bash
+curl -X POST http://0.0.0.0:4000/cursor/v0/agents/bc_abc123/followup \
+ -H "Authorization: Bearer " \
+ -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 "
+```
+
+### Delete an Agent
+
+```bash
+curl -X DELETE http://0.0.0.0:4000/cursor/v0/agents/bc_abc123 \
+ -H "Authorization: Bearer "
+```
+
+### Get API Key Info
+
+```bash
+curl http://0.0.0.0:4000/cursor/v0/me \
+ -H "Authorization: Bearer "
+```
+
+## Related
+
+- [Cursor Cloud Agents API Docs](https://docs.cursor.com/account/api)
+- [Pass-through Endpoints Overview](./intro.md)
+- [Virtual Keys](../proxy/virtual_keys.md)
diff --git a/docs/my-website/img/cursor_add_credential.png b/docs/my-website/img/cursor_add_credential.png
new file mode 100644
index 0000000000..5b0eb1ffe5
Binary files /dev/null and b/docs/my-website/img/cursor_add_credential.png differ
diff --git a/docs/my-website/img/cursor_log_detail.png b/docs/my-website/img/cursor_log_detail.png
new file mode 100644
index 0000000000..5fdb1dd4a1
Binary files /dev/null and b/docs/my-website/img/cursor_log_detail.png differ
diff --git a/docs/my-website/img/cursor_logs.png b/docs/my-website/img/cursor_logs.png
new file mode 100644
index 0000000000..ab5aeafc77
Binary files /dev/null and b/docs/my-website/img/cursor_logs.png differ
diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js
index b01bb53cfe..60325d0efc 100644
--- a/docs/my-website/sidebars.js
+++ b/docs/my-website/sidebars.js
@@ -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",
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index afedb6c8e7..e408abb3c1 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -383,6 +383,7 @@ class LiteLLMRoutes(enum.Enum):
"/vertex-ai",
"/vertex_ai",
"/cohere",
+ "/cursor",
"/gemini",
"/anthropic",
"/langfuse",
diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
index 028edea8c3..13f78f30fa 100644
--- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
+++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py
@@ -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,
diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py
new file mode 100644
index 0000000000..2d687928d9
--- /dev/null
+++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py
@@ -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,
+ }
diff --git a/litellm/proxy/pass_through_endpoints/success_handler.py b/litellm/proxy/pass_through_endpoints/success_handler.py
index 41b92c5611..271c2d7a48 100644
--- a/litellm/proxy/pass_through_endpoints/success_handler.py
+++ b/litellm/proxy/pass_through_endpoints/success_handler.py
@@ -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:
diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json
index ec1c461952..dda8e49d4c 100644
--- a/litellm/proxy/public_endpoints/provider_create_fields.json
+++ b/litellm/proxy/public_endpoints/provider_create_fields.json
@@ -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"
}
]
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index dda32d9838..1b7089d7f1 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -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
diff --git a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_cursor_passthrough_logging_handler.py b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_cursor_passthrough_logging_handler.py
new file mode 100644
index 0000000000..2d025a871b
--- /dev/null
+++ b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_cursor_passthrough_logging_handler.py
@@ -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"
diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
index fdc821ef8b..8acfa2231c 100644
--- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
+++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py
@@ -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"
diff --git a/ui/litellm-dashboard/public/assets/logos/cursor.svg b/ui/litellm-dashboard/public/assets/logos/cursor.svg
new file mode 100644
index 0000000000..79b44c5e83
--- /dev/null
+++ b/ui/litellm-dashboard/public/assets/logos/cursor.svg
@@ -0,0 +1 @@
+
diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
index 246b91f864..8568e79b1e 100644
--- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
+++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx
@@ -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 = {
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 = {
[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";
}