Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e94149b
feat(cursor): add connect-json transport for sdk-bridge
weselben Aug 20, 2026
0e1a99e
feat(cursor): add sdk-bridge subprocess manager
weselben Aug 20, 2026
88ed09e
feat(cursor): add provider core over sdk-bridge
weselben Aug 20, 2026
49f5886
feat(cursor): stream chat completions as OpenAI SSE
weselben Aug 20, 2026
a26d627
feat(cursor): register provider and wire shutdown lifecycle
weselben Aug 20, 2026
79272d0
docs(cursor): add cursor subscription provider docs and env template
weselben Aug 20, 2026
357bbfd
test(cursor): add contract replay tests and goldens
weselben Aug 20, 2026
4437c03
fix(cursor): align base-url docs, correct delta comment, add run-erro…
weselben Aug 20, 2026
8844019
test(cursor): make bridge binary resolution test hermetic
weselben Aug 20, 2026
5bbfb5e
fix(cursor): address pr-review findings (managed base-url, log closeA…
weselben Aug 20, 2026
8e2db18
fix(cursor): close ChatCompletion agent with background context
weselben Aug 20, 2026
4857b54
fix(cursor): distinguish 502/503 in startFailure
weselben Aug 20, 2026
8dd312f
fix(cursor): trim bridge token, raise stderr buffer, forward proxy env
weselben Aug 20, 2026
d97f290
fix(cursor): split frame size cap, propagate read ctx, log malformed …
weselben Aug 20, 2026
cf0dc6e
docs+chore(cursor): drop undocumented CURSOR_MODELS env var
weselben Aug 20, 2026
e15f9d5
test(cursor): expand test surface to 91.1% coverage
weselben Aug 20, 2026
442323a
test(cursor): add branch coverage for inner-loop, EOF, malformed payload
weselben Aug 20, 2026
90f698c
test(cursor): cover resolveBridgeBinary missing-path branches
weselben Aug 20, 2026
e4943aa
fix(cursor): address round-3 review findings
weselben Aug 20, 2026
7c3e963
test(cursor): cover chat_stream inner-loop branches and transport err…
weselben Aug 20, 2026
33e744a
test(cursor): cover SIGKILL escalation, startup timeout edge cases
weselben Aug 20, 2026
de486ae
test(cursor): add concurrent-close race test for inner-loop c.closed …
weselben Aug 20, 2026
90e6f75
test(cursor): cover more connect_transport, parseReadyLine, runSend p…
weselben Aug 21, 2026
191d470
test(cursor): cover chat_stream inner-loop non-EOF error path
weselben Aug 21, 2026
733d0d3
test(cursor): fix Result-frame JSON shape to actually exercise inner-…
weselben Aug 21, 2026
6d73722
test(cursor): cover transport() Start-failure surface path
weselben Aug 21, 2026
00ceb78
test(cursor): cover runSend malformed-frame and explicit-no-bridge paths
weselben Aug 21, 2026
ada64bf
test(cursor): cover StreamChatCompletion + ListModels transport-failu…
weselben Aug 21, 2026
34fb2f7
test(cursor): cover NewTransport nil fallback, transport hc==nil bran…
weselben Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@
# XAI_API_KEY=...
# XAI_BASE_URL=https://api.x.ai/v1

# Cursor (subscription-billed via cursor-sdk-bridge: Composer, Grok 4.5/4.6 pool)
# Generate at Cursor Dashboard → API Keys. Draws from the same plan pools as the CLI login.
# Requires the cursor-sdk-bridge binary: CURSOR_SDK_BRIDGE_BIN, PATH, or ~/.local/share/gomodel/bin/.
# CURSOR_API_KEY=crsr_...

# Groq
# GROQ_API_KEY=gsk_...
# GROQ_BASE_URL=https://api.groq.com/openai/v1
Expand Down
12 changes: 12 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,18 @@ providers:
api_key: "${CHATGPT_API_KEY}"
# models: [gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5]

# Cursor subscription routed through the official cursor-sdk-bridge subprocess
# (loopback HTTP). Requires the sdk-bridge binary on PATH (or
# CURSOR_SDK_BRIDGE_BIN). The key is a user-level access token from the
# Cursor desktop app, surfaced by the bridge.
cursor:
type: cursor
api_key: "${CURSOR_API_KEY}"
# Note: the cursor provider always discovers available slugs via
# ListModels at runtime; the optional `models:` field is parsed by
# the config layer but is currently cosmetic / reserved for a future
# allow-list filter. Leave unset unless that lands.

cohere:
type: cohere
api_key: "${COHERE_API_KEY}"
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"providers/key-rotation",
"providers/anthropic",
"providers/chatgpt",
"providers/cursor",
"providers/cohere",
"providers/gemini",
"providers/deepseek",
Expand Down
111 changes: 111 additions & 0 deletions docs/providers/cursor.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: "Cursor subscription"
description: "Route chat traffic through a Cursor subscription via the official cursor-sdk-bridge, exposing Cursor's bundled models (including Grok) over GoModel's OpenAI-compatible API."
icon: "message-circle"
keywords: ["Cursor", "cursor-agent", "subscription", "Grok", "Composer", "sdk-bridge", "provider setup"]
---

The `cursor` provider routes chat traffic through a Cursor subscription by
spawning the official [`cursor-sdk-bridge`](https://github.com/cursor/sdk-bridge)
subprocess (MIT) and speaking its stable `sdk.v1` Connect contract over
loopback HTTP. Usage is billed against the Cursor plan's pools — the same
pools the `cursor-agent` CLI draws from — not pay-per-token API credit.

## Configure

The credential is a Cursor **User API key** (`crsr_...`), generated at
Cursor Dashboard → API Keys:

```bash
CURSOR_API_KEY=crsr_...
```

Or in `config.yaml`:

```yaml
providers:
cursor:
type: cursor
api_key: "${CURSOR_API_KEY}"
```

GoModel exchanges the key for a short-lived JWT behind the scenes; the key
itself is long-lived and re-exchanges on demand. A User API key draws from
the same plan pools as the CLI browser login — there is no separate metered
billing for it.

The provider needs the bridge binary on the host. Install one of:

```bash
# 1. Explicit override
CURSOR_SDK_BRIDGE_BIN=/path/to/cursor-sdk-bridge

# 2. PATH lookup
cursor-sdk-bridge # any directory on PATH

# 3. Conventional location
~/.local/share/gomodel/bin/cursor-sdk-bridge
```

Download the matching archive from
[cursor/sdk-bridge releases](https://github.com/cursor/sdk-bridge/releases/latest)
and unpack so the binary lands at one of those paths. The provider spawns it
lazily on first request with a scrubbed environment (only `CURSOR_API_KEY`
plus `PATH`/`HOME`/`TMPDIR`/`USER`/`LANG` are inherited) and shuts it down
cleanly when GoModel stops.

<Warning>
The bridge binary is a hard runtime requirement. Without it the provider
returns a clear install-hint error; no traffic is attempted.
</Warning>

## Models

`ListModels` is served from the bridge's `SdkCursorService.ListModels`, so

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: CURSOR_MODELS=composer,auto is documented as the way to "Pin a static list ... when you want a fixed surface". The provider never reads this env var (verified — internal/providers/cursor/*.go has zero CURSOR_MODELS references; grep across the whole repo shows only .env.template and this doc mention it). Fix: either remove the documentation and the .env.template line, or implement the static-list override.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

`GET /v1/models` returns whatever the account's plan exposes. The provider
always discovers the available slugs at runtime — there is no static-list
override on the cursor provider. Which slugs exist depends on the
subscription tier. The **Cursor Models pool** (generous included usage)
currently includes Composer 2.5 and Grok 4.5 / 4.6 on Pro and above; a
Hobby or trial account may only expose `composer` / `auto`. Confirm with
a live `GET /v1/models` against your key.

## Dialect and limits

- Only `/v1/chat/completions` is served. `/v1/responses`, `/v1/embeddings`,
`/v1/files`, and `/v1/batches` answer `501` with
`unsupported_provider_operation`.
- Requests are **stateless**: each chat completion creates a fresh bridge
agent, flattens the full message history into one user message, and
closes the agent when the run ends. Multi-turn quality and billing
semantics match a fresh `cursor-agent` session per request.
- Streaming emits OpenAI-conservative SSE. When the bridge run result
carries token usage, the final chunk includes a top-level `usage` object;
otherwise usage is omitted and the request is recorded without token
counts.
- Model IDs pass through unchanged — GoModel does not translate between
Cursor's internal slugs and OpenAI names.

## Reported cost is not real spend

Cursor subscriptions are flat-rate, but model IDs that also exist on public
provider catalogs pick up their per-token prices in GoModel's catalog. Usage
records and dashboard totals for `cursor` therefore show a figure that
corresponds to no actual charge.

<Warning>
A **budget** can reject `cursor` traffic for "spending" money the
subscription never charges. Scope budgets to a
[user path](/features/user-path) that excludes subscription traffic, or
leave budgets off for it.
</Warning>

## Terms of service

Routing subscription traffic through a gateway sits in the same gray zone as
the community `cursor-agent` proxies. Cursor's ToS prohibits reverse
engineering its private protocols; this provider uses only the official
MIT-licensed bridge and the documented User API key, which is the most
conservative integration available — but it is not a supported Cursor
product surface. Treat it as best-effort and keep a direct `xai` or other
pay-per-token provider as fallback if you rely on Grok specifically.
1 change: 1 addition & 0 deletions docs/providers/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ support, not every individual model capability exposed by an upstream provider.
| -------- | ---------- | ------------- | :--: | :----------: | :---: | :---: | :-----: | :------: | ----- |
| OpenAI | `OPENAI_API_KEY` | `gpt-5.5` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| ChatGPT subscription | `CHATGPT_API_KEY` (Codex sign-in token) | `gpt-5.6-sol` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | [ChatGPT subscription](/providers/chatgpt) |
| Cursor subscription | `CURSOR_API_KEY` (User API key) | `composer` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | [Cursor subscription](/providers/cursor) |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | [Anthropic](/providers/anthropic) |
| Cohere | `COHERE_API_KEY` | `command-a-plus-05-2026` | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | [Cohere](/providers/cohere) |
| Google Gemini | `GEMINI_API_KEY` | `gemini-3.7-flash` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | [Google Gemini](/providers/gemini) |
Expand Down
3 changes: 3 additions & 0 deletions internal/providers/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ var testDiscoveryConfigs = map[string]DiscoveryConfig{
"kimicode": {
DefaultBaseURL: "https://api.kimi.com/coding/v1",
},
"cursor": {
DefaultBaseURL: "http://127.0.0.1:32123",
},
"hetzner": {
DefaultBaseURL: "https://inference.hetzner.com/api/v1",
},
Expand Down
Loading