Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Cursor credential (a crsr_ key from cursor.com/dashboard). Skip it to log in
# with the browser instead: python -m cursor2api login
CURSOR_API_KEY=

# Key(s) local clients must send as x-api-key or Bearer, comma separated.
# Empty means the port needs no key.
API_KEY=

BIND=127.0.0.1
PORT=8787
DEFAULT_MODEL=claude-sonnet-5

# Prepended to every turn's system prompt.
CURSOR2API_SYSTEM_PROMPT=

# Seconds an unfinished tool round trip may keep its upstream stream open.
CURSOR2API_RESUME_TTL=900
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Changelog

## v0.1.2

- Tool calls now continue the Cursor turn that asked for them: the upstream stream
stays open until the caller returns the `tool_result`, instead of replaying the
whole history as a new request. Claude Code agent loops that used to stall (the
model would re-verify earlier steps, or object that the transcript looked forged)
finish in one pass, each round trip costs one response instead of a full
re-read of Cursor's ~25k token harness.
- When a stream is no longer available, the replayed history is attributed to the
relay rather than pasted in as `Human:`/`Assistant:` lines.

## v0.1.1

- OpenAI `response_format`: `json_object` and `json_schema` are emulated with an
instruction, and markdown code fences are stripped from the answer, streaming
included.
- `API_KEY` accepts several comma-separated client keys (`AUTH_TOKEN` also works).
- A `.env` next to the server is read at startup.
- `Dockerfile` and `docker-compose.yml`.
- Plain conversations no longer talk about a workspace or reach for Cursor's
built-in tools.
- A turn now ends on `turn_ended` or when the stream closes instead of on a short
silence, so long file writes in Claude Code are no longer cut off mid-task.
- SSE keepalives while the upstream is quiet, a first-event timeout, and Cursor
error codes mapped onto Anthropic error types (`403` for a model the account
has not enabled).

## v0.1.0

- Anthropic `POST /v1/messages` and OpenAI `POST /v1/chat/completions`, streaming
and buffered, over Cursor's `agent.v1.AgentService/Run` protocol.
- `GET /v1/models` from the account's own catalog.
- Tools, images, PDFs, thinking, usage, Cursor web search, Claude Code support.
- Cursor API key or browser (PKCE) login.
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY cursor2api ./cursor2api
COPY pyproject.toml README.md LICENSE ./

ENV BIND=0.0.0.0 PORT=8787 CURSOR2API_USE_CLI_AUTH=0 CURSOR2API_AUTO_LOGIN=0
EXPOSE 8787
CMD ["python", "-m", "cursor2api", "serve"]
56 changes: 39 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
# cursor2api
# cursor2api — Cursor to OpenAI / Anthropic compatible API proxy

[中文说明](README.zh-CN.md)
[中文说明](README.zh-CN.md) · [Changelog](CHANGELOG.md)

Use the models of your Cursor account through the Anthropic Messages API and the
OpenAI Chat Completions API.
Turns your Cursor account into an OpenAI- and Anthropic-compatible endpoint, so
Claude Code, Cherry Studio, LobeChat, the OpenAI SDK and the Anthropic SDK can use
Cursor's models directly. Pure protocol client: no browser automation, no Cursor IDE
running in the background.

- `POST /v1/messages` and `POST /v1/chat/completions`, streaming or buffered
- `POST /v1/messages` (Anthropic Messages) and `POST /v1/chat/completions` (OpenAI
Chat Completions), streaming (SSE) or buffered
- every model the signed-in account can use, listed by `GET /v1/models`
- tools, images, PDFs, thinking/reasoning, usage
- authorise with a Cursor API key or a browser login
- function calling / tools, images, PDFs, thinking (reasoning), usage,
`response_format` JSON mode
- works with Claude Code out of the box
- authorise with a Cursor API key or a browser (OAuth2 PKCE) login
- one file per concern, standard library only apart from `h2`

## Install

git clone <your fork>
git clone https://github.com/Yuki13929/cursor2api
cd cursor2api
pip install -r requirements.txt

Python 3.9+ and the `h2` package.
Python 3.9+ and the `h2` package. Docker works too:

CURSOR_API_KEY=crsr_... docker compose up -d

## Authorise

Expand Down Expand Up @@ -61,8 +69,11 @@ Routes: `POST /v1/messages`, `POST /v1/messages/count_tokens`,
`POST /v1/chat/completions`, `GET /v1/models`, `GET /v1/models/{id}`, `GET /login`,
`GET /health`.

Common settings: `PORT`, `BIND`, `API_KEY` (require an `x-api-key` from local clients),
`DEFAULT_MODEL`.
Common settings, from the environment or a `.env` next to the server
(see [.env.example](.env.example)): `PORT`, `BIND`, `API_KEY` (one or more
comma-separated keys local clients must send as `x-api-key` or `Bearer`),
`DEFAULT_MODEL`, `CURSOR2API_SYSTEM_PROMPT` (prepended to every turn),
`CURSOR2API_RESUME_TTL` (how long an unfinished tool round trip may stay open).

## Models

Expand All @@ -81,10 +92,15 @@ Unknown ids fall back to `DEFAULT_MODEL`.

## Notes

- `temperature`, `top_p`, `top_k`, `cache_control`, `n`, `seed` and `response_format`
have no equivalent upstream and are ignored; `stop_sequences`, `max_tokens` and
`tool_choice` are approximated locally.
- `temperature`, `top_p`, `top_k`, `cache_control`, `n` and `seed` have no equivalent
upstream and are ignored; `stop_sequences`, `max_tokens`, `tool_choice` and
`response_format` are approximated locally. JSON mode is an instruction plus fence
stripping, not a grammar, so a schema is requested rather than enforced.
- Thinking text is Cursor's summary and its Anthropic `signature` is always `""`.
- A tool round trip continues the Cursor turn it came from: the upstream stream is
kept open until the caller sends the `tool_result` back, which keeps agent loops
fast and coherent. Reconnecting instead replays the history as an attributed
transcript, which costs a full re-read of Cursor's harness.
- Web search is Cursor's own server-side tool, reported as `server_tool_use` +
`web_search_tool_result` with titles and urls only.
- Cursor's agent system prompt is always present and inflates `input_tokens`.
Expand Down Expand Up @@ -118,6 +134,12 @@ MIT.

## Repository topics

cursor cursor-ai cursor-api anthropic-api claude openai-api openai-compatible
anthropic-compatible llm-proxy api-proxy reverse-engineering protobuf connect-rpc
python sse streaming function-calling claude-code
cursor cursor-ai cursor-api cursor2api anthropic-api claude claude-code
openai-api openai-compatible anthropic-compatible llm-proxy api-proxy ai-gateway
reverse-engineering protobuf connect-rpc python sse streaming function-calling
gpt free-api

Suggested repository description:

Cursor to OpenAI/Anthropic compatible API proxy: use your Cursor account's models
from Claude Code and any OpenAI or Anthropic client. Tools, images, PDFs, streaming.
34 changes: 23 additions & 11 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# cursor2api
# cursor2api — Cursor 转 OpenAI / Anthropic 兼容 API 代理

[English](README.md)
[English](README.md) · [更新日志](CHANGELOG.md)

用 Anthropic Messages API 和 OpenAI Chat Completions API 调用你 Cursor 账号里的模型。
把你的 Cursor 账号变成 OpenAI 和 Anthropic 兼容的接口,Claude Code、Cherry Studio、
LobeChat、OpenAI SDK、Anthropic SDK 都能直接用 Cursor 的模型。纯协议实现,不需要浏览器
自动化,也不需要后台开着 Cursor IDE。

- `POST /v1/messages`、`POST /v1/chat/completions`,支持流式和非流式
- `POST /v1/messages`、`POST /v1/chat/completions`,支持流式(SSE)和非流式
- 账号能用的所有模型,`GET /v1/models` 列出
- 工具调用、图片、PDF、thinking/reasoning、用量统计
- 用 Cursor API key 或浏览器授权登录
- 工具调用、图片、PDF、thinking/reasoning、用量统计、`response_format` JSON 模式
- 开箱支持 Claude Code
- 用 Cursor API key 或浏览器授权(OAuth2 PKCE)登录

## 安装

git clone <你的仓库>
git clone https://github.com/Yuki13929/cursor2api
cd cursor2api
pip install -r requirements.txt

需要 Python 3.9+ 和 `h2`。
需要 Python 3.9+ 和 `h2`。也可以用 Docker:

CURSOR_API_KEY=crsr_... docker compose up -d

## 授权

Expand Down Expand Up @@ -59,7 +64,10 @@ OpenAI 客户端,同一个端口:
`POST /v1/chat/completions`、`GET /v1/models`、`GET /v1/models/{id}`、`GET /login`、
`GET /health`。

常用配置:`PORT`、`BIND`、`API_KEY`(要求本地客户端带 `x-api-key`)、`DEFAULT_MODEL`。
常用配置可以写在环境变量或服务同目录的 `.env` 里(见 [.env.example](.env.example)):
`PORT`、`BIND`、`API_KEY`(可逗号分隔多个,本地客户端用 `x-api-key` 或 `Bearer` 带上)、
`DEFAULT_MODEL`、`CURSOR2API_SYSTEM_PROMPT`(附加到每一轮的系统提示)、
`CURSOR2API_RESUME_TTL`(未完成的工具回合最多保留多久)。

## 模型

Expand All @@ -77,9 +85,13 @@ OpenAI 客户端,同一个端口:

## 说明

- `temperature`、`top_p`、`top_k`、`cache_control`、`n`、`seed`、`response_format`
在上游没有对应项,直接忽略;`stop_sequences`、`max_tokens`、`tool_choice` 是本地近似实现。
- `temperature`、`top_p`、`top_k`、`cache_control`、`n`、`seed` 在上游没有对应项,直接
忽略;`stop_sequences`、`max_tokens`、`tool_choice`、`response_format` 是本地近似实现,
JSON 模式靠指令加去掉代码围栏,不是语法约束,schema 只是要求而非强制。
- thinking 内容是 Cursor 给的摘要,Anthropic 的 `signature` 恒为 `""`。
- 工具回合会接着原来的 Cursor 回合继续:上游那条流一直保持到调用方把 `tool_result`
发回来,所以 agent 循环又快又连贯。只有在这条流已经不在时才会退回重放历史(以明确
署名的对话记录形式),代价是 Cursor 那套系统提示要重新读一遍。
- 联网搜索用的是 Cursor 自己的服务端工具,转成 `server_tool_use` +
`web_search_tool_result`,只有标题和链接。
- Cursor 的 agent 系统提示始终存在,会抬高 `input_tokens`。
Expand Down
61 changes: 61 additions & 0 deletions cursor2api/openai_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def to_anthropic(body):
out["stop_sequences"] = [stop] if isinstance(stop, str) else list(stop)
if body.get("reasoning_effort") not in (None, "none") or body.get("thinking"):
out["thinking"] = {"type": "enabled"}
instruction = json_instruction(body.get("response_format"))
if instruction:
out["system"] = ((out.get("system", "") + "\n\n") if out.get("system") else "") \
+ instruction

tools = []
for t in body.get("tools") or []:
Expand All @@ -129,6 +133,63 @@ def to_anthropic(body):
return out


def json_instruction(response_format):
"""System text that emulates response_format, which Cursor has no knob for."""
kind = (response_format or {}).get("type")
if kind == "json_object":
return ("Reply with a single JSON object and nothing else: no prose, no "
"explanation and no markdown code fence.")
if kind == "json_schema":
schema = (response_format.get("json_schema") or {}).get("schema") or {}
return ("Reply with a single JSON value matching this JSON Schema and nothing "
"else: no prose, no explanation and no markdown code fence.\n"
+ json.dumps(schema, ensure_ascii=False))
return ""


class Unfence:
"""Strips a markdown code fence from a stream of text.

Models wrap JSON in ```json ... ``` even when told not to, which breaks callers
that pass the content straight to a JSON parser. Text is held back only while a
fence marker could still be forming, so streaming stays incremental.
"""

def __init__(self, enabled):
self.enabled = enabled
self.buf = ""
self.started = False

def feed(self, text):
if not self.enabled:
return text
self.buf += text
if not self.started:
stripped = self.buf.lstrip()
if stripped.startswith("```"):
nl = stripped.find("\n")
if nl < 0: # opening fence still incomplete
return ""
self.buf = stripped[nl + 1:]
elif "```".startswith(stripped[:3]) and len(stripped) < 3:
return ""
self.started = True
out, self.buf = self.buf, ""
tail = len(out) - len(out.rstrip("\n`")) # a closing fence may still be forming
if tail:
out, self.buf = out[:len(out) - tail], out[len(out) - tail:]
return out

def close(self):
out, self.buf = self.buf, ""
return out.rstrip().rstrip("`").rstrip() if self.enabled else out


def unfence(text, enabled):
f = Unfence(enabled)
return f.feed(text) + f.close()


FINISH = {"end_turn": "stop", "stop_sequence": "stop", "max_tokens": "length",
"tool_use": "tool_calls"}

Expand Down
Loading