File size: 6,181 Bytes
1766992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# Cursor2API Go 实现说明:OpenAI `chat/completions` 下的 thinking + tools

本文档只描述当前 Go 仓库已经实现或明确约束的行为,不再沿用旧的 Deno `/v1/messages` 迁移口径。

## 1. 当前范围

当前仓库只支持两个公开接口:

- `GET /v1/models`
- `POST /v1/chat/completions`

不实现 `/v1/messages`,也不承诺 Anthropic 原生 block/SSE 兼容。

## 2. 真实能力边界

### 2.1 公开模型目录

配置项 `MODELS` 只填写基础模型,例如:

```env
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.go`
- `services/cursor.go`
- `services/cursor_protocol.go`

### 3.2 处理步骤

1. `handlers.ChatCompletions` 绑定并校验 OpenAI 请求。
2. `CursorService.buildCursorRequest(...)` 解析模型能力:
   - `claude-sonnet-4.6` -> 基础模型
   - `claude-sonnet-4.6-thinking` -> 基础模型 + thinking 开启
3. `tool_choice` 被解析为三类模式:
   - `auto`
   - `none`
   - `required`
   -`{"type":"function","function":{"name":"..."}}`
4. 工具定义被统一校验:
   - 仅支持 `type=function`
   - `function.name` 必填
   - 不允许重名
5. 构造单次请求专用 `TriggerSignal`,格式:

```text
<<CALL_xxxxxxxx>>
```

6. 构造发往 Cursor 的纯文本消息。

## 4. 内部 prompt 协议

上游 Cursor 仍然只收到文本消息,不使用原生 OpenAI tool calling。

### 4.1 tool 协议

当请求含有 `tools``tool_choice != "none"` 时,system message 会注入:

- 工具桥接说明
- tool 调用格式约束
- `<function_list>` 工具清单
- `required` / 指定 function 的额外约束

模型被要求按如下格式输出工具调用:

```text
<<CALL_xxxxxxxx>>
<invoke name="tool_name">{"arg":"value"}</invoke>
```

### 4.2 thinking 协议

当请求命中 `*-thinking` 模型时:

- system prompt 注入 thinking 规则
- 每条 user message 追加固定 thinking hint

hint 为:

```text
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.go`
- `utils/cursor_protocol.go`
- `utils/utils.go`

### 5.2 解析步骤

1. `consumeSSE(...)` 读取 Cursor SSE。
2. 每个 `delta` 文本片段进入 `CursorProtocolParser`3. 解析器增量产出三类内部事件:
   - `text`
   - `thinking`
   - `tool_call`
4. `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.go`
  - `tool_choice` 解析
  - 工具定义校验
  - OpenAI 历史消息到 Cursor 文本协议的转换
- `utils/cursor_protocol.go`
  - Cursor 文本增量到 `text/thinking/tool_call` 事件的解析
- `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 协议