Files
OmniRoute/docs/providers/CURSOR-API-KEY-AND-CLI.md
Damian Pozimski 3e0afc8444 feat(providers): Cursor API provider (crsr_ keys) and Cursor CLI passthrough (#10729)
Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
2026-08-20 06:29:11 -03:00

6.2 KiB

title, version, lastUpdated
title version lastUpdated
Cursor API provider and the Cursor CLI passthrough 3.8.50 2026-08-19

Cursor API provider and the Cursor CLI passthrough

Two ways to put Cursor behind OmniRoute without an IDE session:

  1. cursor-api provider (card "Cursor API", alias cua): an API-key provider that holds a Cursor user API key (crsr_…, generated at https://cursor.com/dashboard/api). Any OmniRoute client then reaches Cursor models through /v1/chat/completions as cursor-api/<model> or cua/<model>, with the usual quota, fallback and logging layers. The IDE provider (cursor, OAuth/IDE session) is unchanged.
  2. Cursor CLI passthrough: point the Cursor CLI (agent) at OmniRoute so every RPC the CLI makes is authenticated with an OmniRoute API key, forwarded to Cursor with a cursor-api connection's credential, and recorded in the Logs page.

Why the key is exchanged

api2.cursor.sh rejects a raw crsr_… key as a Bearer token (401). The Cursor CLI first POSTs the key to /auth/exchange_user_api_key and receives a session JWT that expires after one hour; the returned refreshToken carries the same exp, so refreshing means re-exchanging the key. open-sse/services/cursorApiKeyAuth.ts does that exchange, caches one session token per key, re-exchanges five minutes before expiry and drops the cached token when Cursor answers 401. CursorExecutor calls it right before opening the upstream stream for cursor-api connections.

The cursor-api provider

Registry: open-sse/config/providers/registry/cursor/index.ts (cursor_apiProvider, authType: "apikey", same format, baseUrl and models as cursor). Catalog card: src/shared/constants/providers/apikey/specialty-media.ts. Executor map: open-sse/executors/index.ts ("cursor-api" / cuanew CursorExecutor("cursor-api")).

Dashboard: Providers → Cursor API → Add API key.

REST:

curl -sS -X POST http://localhost:20128/api/providers \
  -H "Content-Type: application/json" \
  -d '{"provider":"cursor-api","name":"cursor-api-key","apiKey":"crsr_…","priority":1}'

Then:

curl -sS http://localhost:20128/v1/chat/completions \
  -H "Authorization: Bearer <omniroute-api-key>" \
  -H "Content-Type: application/json" \
  -d '{"model":"cursor-api/auto","messages":[{"role":"user","content":"say PONG"}]}'

Notes:

  • Model listing for cursor-api comes from the static Cursor registry (the same list the IDE provider falls back to); no cursor-agent install is needed on the OmniRoute host.
  • POST /api/providers/{id}/refresh-cursor is for the cursor IDE provider only; cursor-api connections have no IDE session to renew.

Cursor CLI passthrough

Route: src/app/api/cursor-cli/[...path]/route.tsopen-sse/handlers/cursorCliProxy.ts. The prefix /api/cursor-cli/ is registered in src/shared/constants/publicApiRoutes.ts because the handler enforces its own authentication:

Path Auth expected from the CLI What OmniRoute does
POST /auth/exchange_user_api_key Bearer <OmniRoute API key> Validates the key, mints a 1h HS256 JWT (signed with JWT_SECRET) and returns it
every other path (/aiserver.v1.*, /agent.v1.AgentService/RunSSE, /aiserver.v1.BidiService/BidiAppend, /v1/traces, …) Bearer <that JWT> Verifies issuer/audience/expiry, picks an active cursor-api connection, swaps the Authorization header for the exchanged Cursor token and streams the reply back

The CLI decodes exp from whatever token it receives, so handing it an opaque token makes it re-exchange before almost every request; the minted JWT avoids that. A 401 from OmniRoute makes the CLI exchange again.

Setup

  1. Create an OmniRoute API key (Dashboard → API keys) and a cursor-api connection.

  2. Tell the CLI to use HTTP/1.1 for the agent stream. In ~/.cursor/cli-config.json:

    { "network": { "useHttp1ForAgent": true } }
    

    Without this the CLI opens the agent turn over HTTP/2 to a separately configured agent host and only the control-plane RPCs go through the endpoint.

  3. Run the CLI against OmniRoute:

    export CURSOR_API_ENDPOINT=http://localhost:20128/api/cursor-cli
    export CURSOR_API_KEY=<omniroute-api-key>
    agent -p --trust "Reply with exactly OK"
    

Every hop lands in Logs as provider cursor-api, request type cursor-cli, path /api/cursor-cli/<rpc>, attributed to the OmniRoute API key and the connection that served it.

Failure modes

Situation Response to the CLI
Unknown OmniRoute key and REQUIRE_API_KEY=true 401 unauthenticated on exchange
REQUIRE_API_KEY=false anonymous session (mirrors /v1/* behaviour)
Expired / foreign / tampered session JWT 401, the CLI re-exchanges
OmniRoute API key revoked after exchange 401 on the next RPC
No active cursor-api connection 503 unavailable
Cursor rejects the connection's key 401 unauthenticated, cached session dropped
Upstream unreachable 502 unavailable (sanitized message)
JWT_SECRET unset 503 on exchange