Cursor2API Go 实现说明:OpenAI chat/completions 下的 thinking + tools
本文档只描述当前 Go 仓库已经实现或明确约束的行为,不再沿用旧的 Deno /v1/messages 迁移口径。
1. 当前范围
当前仓库只支持两个公开接口:
GET /v1/modelsPOST /v1/chat/completions
不实现 /v1/messages,也不承诺 Anthropic 原生 block/SSE 兼容。
2. 真实能力边界
2.1 公开模型目录
配置项 MODELS 只填写基础模型,例如:
MODELS=claude-sonnet-4.6
服务会自动向外暴露两类模型:
- 基础模型:
claude-sonnet-4.6 - thinking 派生模型:
claude-sonnet-4.6-thinking
约定如下:
- 基础模型:保留现有模型名,启用 tool 能力。
-thinking模型:自动映射回基础模型出站,同时启用 thinking 和 tool 能力。- 不允许继续派生
-thinking-thinking。
2.2 外部接口契约
当前实现的是 OpenAI chat/completions 兼容面:
- 请求支持
messages - 请求支持
tools - 请求支持
tool_choice - assistant 历史消息支持
tool_calls - tool 历史消息支持
tool_call_id - 响应支持非流式
message.tool_calls - 响应支持流式
delta.tool_calls
thinking 不作为 OpenAI 独立字段对外暴露。它只作为内部 prompt 协议与解析协议使用。
3. 请求侧链路
3.1 入口文件
handlers/handler.goservices/cursor.goservices/cursor_protocol.go
3.2 处理步骤
handlers.ChatCompletions绑定并校验 OpenAI 请求。CursorService.buildCursorRequest(...)解析模型能力:claude-sonnet-4.6-> 基础模型claude-sonnet-4.6-thinking-> 基础模型 + thinking 开启
tool_choice被解析为三类模式:autononerequired- 或
{"type":"function","function":{"name":"..."}}
- 工具定义被统一校验:
- 仅支持
type=function function.name必填- 不允许重名
- 仅支持
- 构造单次请求专用
TriggerSignal,格式:
<<CALL_xxxxxxxx>>
- 构造发往 Cursor 的纯文本消息。
4. 内部 prompt 协议
上游 Cursor 仍然只收到文本消息,不使用原生 OpenAI tool calling。
4.1 tool 协议
当请求含有 tools 且 tool_choice != "none" 时,system message 会注入:
- 工具桥接说明
- tool 调用格式约束
<function_list>工具清单required/ 指定 function 的额外约束
模型被要求按如下格式输出工具调用:
<<CALL_xxxxxxxx>>
<invoke name="tool_name">{"arg":"value"}</invoke>
4.2 thinking 协议
当请求命中 *-thinking 模型时:
- system prompt 注入 thinking 规则
- 每条 user message 追加固定 thinking hint
hint 为:
Use <thinking>...</thinking> for hidden reasoning when it helps. Keep your final visible answer outside the thinking tags.
4.3 历史消息回放
为了让多轮 tool loop 继续工作,OpenAI 历史消息会被回放成内部文本协议:
- assistant
tool_calls->TriggerSignal + <invoke ...> - tool message ->
<tool_result id="...">...</tool_result>
普通 system/user/assistant 文本则继续按 Cursor parts[].text 发送。
5. 响应解析链路
5.1 入口文件
services/cursor.goutils/cursor_protocol.goutils/utils.go
5.2 解析步骤
consumeSSE(...)读取 Cursor SSE。- 每个
delta文本片段进入CursorProtocolParser。 - 解析器增量产出三类内部事件:
textthinkingtool_call
thinking只保留在内部事件层,不直接回写给客户端。
5.3 解析规则
<thinking>...</thinking>在 thinking 模型下会被消费为内部 thinking 事件。TriggerSignal + <invoke ...>会被消费为工具调用事件。- 不完整标签会在流结束时降级为普通文本,避免丢内容。
- 多个连续
<invoke>会逐个产出,不再只保留第一个。
6. OpenAI 响应写回
6.1 非流式
非流式响应会在 services.CursorService.ChatCompletionNonStream(...) 中聚合内部事件:
- 只有文本时,返回普通
message.content - 有工具调用时,返回 assistant
tool_calls - 若本轮出现工具调用,
finish_reason = "tool_calls" - 否则
finish_reason = "stop"
为提升与部分编排器(例如 Kilo Code)在“必须用工具”场景下的兼容性:
- 当请求含
tools且被判定为“必须至少调用一次工具”(tool_choice=required/指定函数,或启用KILO_TOOL_STRICT)时, 如果第一轮没有产出任何tool_calls,服务会自动重试 1 次(仅非流式)。
6.2 流式
utils.StreamChatCompletion(...) 会输出:
- 首个 role chunk:
assistant - 文本 chunk:
delta.content - 工具调用 chunk:
delta.tool_calls - 收尾 chunk:
- 有工具调用 ->
finish_reason = "tool_calls" - 无工具调用 ->
finish_reason = "stop"
- 有工具调用 ->
thinking 内容不会作为独立 OpenAI 字段透出。
7. 代码落点
本次能力集中在以下模块:
models/- OpenAI request/response/tool 类型
- thinking 模型派生规则
- 基础模型到 Cursor 模型的映射
config/MODELS基础模型配置- 自动扩展
*-thinking公开模型目录
services/cursor_protocol.gotool_choice解析- 工具定义校验
- OpenAI 历史消息到 Cursor 文本协议的转换
utils/cursor_protocol.go- Cursor 文本增量到
text/thinking/tool_call事件的解析
- Cursor 文本增量到
utils/utils.go- OpenAI 流式/非流式响应写回
8. 测试基线
当前测试覆盖以下关键路径:
MODELS自动扩展基础模型和-thinking模型- thinking 模型回落到基础出站模型
- tool prompt 注入与 tool history 回放
<thinking>/<invoke>的增量解析- 流式
delta.tool_calls - 非流式
message.tool_calls - 纯文本聊天回归不破坏
9. 非目标
当前不做以下内容:
- Anthropic
/v1/messages - MCP
- 原生 OpenAI tool execution
- 可见的 reasoning/thinking 对外字段
- 图像、文档、文件等多模态 block 协议