Skip to content

Commit cc80c4f

Browse files
committed
tag(v2.0.7): 修复Cursor流式响应token统计和添加调试日志
主要改进: 1. 修复Cursor流式响应中tokens=0的问题 - 在passthrough模式下添加stream_options参数 - 请求后端返回usage信息 2. 添加详细的调试日志 - streamDirect函数添加字节数统计 - 记录响应内容预览(前500字符) - 明确显示token提取结果 3. 优化日志输出 - 添加错误日志记录 - 提供响应缓冲区大小信息 4. 版本更新 - 更新到v2.0.7 - 同步前端和构建配置版本号 5. 文档 - 创建CURSOR_STREAM_DEBUG.md调试指南 - 创建WAILS_LOG_CONTROL.md日志控制说明 - 创建run-quiet脚本用于安静模式运行 修复了OpenAI->Claude流式转换的content_block_start格式问题
1 parent 2c882f2 commit cc80c4f

10 files changed

Lines changed: 1469 additions & 6 deletions

File tree

CURSOR_STREAM_DEBUG.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Cursor 流式响应调试指南
2+
3+
## 🐛 当前问题
4+
5+
**症状**
6+
- Cursor 请求显示 `tokens=0`
7+
- 后端(Antigravity)显示请求成功并返回数据
8+
- 流式响应可能没有正确传递到 Cursor
9+
10+
## ✅ 已添加的调试日志
11+
12+
`internal/service/proxy_service.go``streamDirect` 函数中添加了详细的调试日志:
13+
14+
1. **流量统计**:记录传输的总字节数
15+
2. **响应缓冲**:显示响应缓冲区的大小
16+
3. **响应预览**:显示前500字符的响应内容(debug级别)
17+
4. **Token提取**:记录提取到的 token 数量
18+
19+
## 🧪 测试步骤
20+
21+
### 1. 启用 Debug 日志级别
22+
23+
`main.go` 中修改日志级别(如果需要看到 Debug 日志):
24+
25+
```go
26+
// 第94行,将 InfoLevel 改为 DebugLevel
27+
log.SetLevel(log.DebugLevel) // 原来是 log.InfoLevel
28+
```
29+
30+
### 2. 重新运行程序
31+
32+
```powershell
33+
# 重新编译并运行
34+
go run .
35+
```
36+
37+
### 3. 发送测试请求
38+
39+
从 Cursor 发送一个请求,然后查看日志输出。
40+
41+
### 4. 查看关键日志
42+
43+
应该会看到类似这样的日志:
44+
45+
```
46+
time="..." level=info msg="[Cursor Stream] Received request for model: proxy_auto"
47+
time="..." level=info msg="[Cursor Stream] Routing to: http://127.0.0.1:8545/v1/chat/completions ..."
48+
time="..." level=debug msg="[Stream Direct] Stream completed. Total bytes: 12345"
49+
time="..." level=debug msg="[Stream Direct] Response buffer length: 12345 bytes"
50+
time="..." level=debug msg="[Stream Direct] Response preview: data: {\"id\":\"...\", ..."
51+
time="..." level=info msg="[Stream Direct] Extracted tokens: prompt=100, completion=50, total=150"
52+
time="..." level=info msg="LogRequest: model=claude-opus-4-5-20251101, tokens=150, success=true"
53+
```
54+
55+
## 🔍 诊断要点
56+
57+
### 如果看到 `tokens=0`
58+
59+
检查以下内容:
60+
61+
1. **响应预览中是否包含 `usage` 字段**
62+
- OpenAI 格式:`"usage": {"prompt_tokens": N, "completion_tokens": M}`
63+
- Claude 格式:在 `message_start``message_delta` 事件中
64+
65+
2. **响应是否完整**
66+
- 检查 `Total bytes``Response buffer length` 是否一致
67+
- 确认没有 "Stream error" 日志
68+
69+
3. **SSE 格式是否正确**
70+
- 应该是 `data: {...}\n\n` 格式
71+
- 检查是否有 `[DONE]` 标记
72+
73+
### 如果响应中没有 usage 信息
74+
75+
可能的原因:
76+
77+
1. **后端没有发送 usage**
78+
- 某些 OpenAI 兼容 API 可能不会在流式模式下发送 usage
79+
- 需要在请求中添加 `stream_options: {include_usage: true}`
80+
81+
2. **格式不匹配**
82+
- 后端可能使用了非标准的格式
83+
- 需要调整 `extractTokensFromStreamResponse` 函数
84+
85+
## 🛠️ 可能的修复方案
86+
87+
### 方案 1: 确保请求 usage 信息
88+
89+
在 Cursor 流式请求中添加 `stream_options`(第3912行附近):
90+
91+
```go
92+
else {
93+
reqData["stream"] = true
94+
reqData["stream_options"] = map[string]interface{}{
95+
"include_usage": true,
96+
}
97+
transformedBody, _ = json.Marshal(reqData)
98+
targetURL = buildOpenAIChatURL(route.APIUrl)
99+
}
100+
```
101+
102+
### 方案 2: 从非流式字段提取 tokens
103+
104+
如果后端在每个 chunk 中都包含临时的 token 计数,修改 `extractTokensFromStreamResponse` 来累积这些值。
105+
106+
### 方案 3: Post-hoc Token 计数
107+
108+
如果实在无法从流式响应中获取,可以考虑:
109+
- 使用 tiktoken 库估算 token 数(需要添加依赖)
110+
- 或者在非流式模式下获取准确的 token 数
111+
112+
## 📊 预期结果
113+
114+
修复后应该看到:
115+
116+
```
117+
time="..." level=info msg="[Stream Direct] Extracted tokens: prompt=XXX, completion=YYY, total=ZZZ"
118+
time="..." level=info msg="LogRequest: model=..., tokens=ZZZ, success=true"
119+
time="..." level=info msg="POST /api/cursor/v1/chat/completions 200"
120+
```
121+
122+
其中 XXX、YYY、ZZZ 都是大于 0 的数字。
123+
124+
## 📝 下一步
125+
126+
1. 运行测试,收集日志
127+
2. 根据日志输出确定根本原因
128+
3. 应用相应的修复方案
129+
4. 验证修复效果
130+
131+
---
132+
133+
**调试完成后记得**:将日志级别改回 `log.InfoLevel` 以减少日志输出。

WAILS_LOG_CONTROL.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# OpenAI Router - Wails 日志控制说明
2+
3+
## 问题说明
4+
5+
Wails v3 框架会输出详细的绑定调用日志:
6+
```
7+
Jan 12 14:47:58.238 INF Binding call started: id=...
8+
Jan 12 14:47:58.241 INF Binding call complete: id=...
9+
Jan 12 14:47:58.238 INF Asset Request: windowName=...
10+
```
11+
12+
这些日志在开发阶段有用,但在生产环境会产生大量输出。
13+
14+
## 解决方案
15+
16+
### 方法 1: 使用生产构建(推荐)
17+
18+
构建生产版本时,Wails 会自动减少日志输出:
19+
20+
```powershell
21+
# 构建生产版本
22+
wails build
23+
24+
# 或使用 Go 构建
25+
go build -ldflags "-s -w" -o anyproxyai.exe .
26+
```
27+
28+
### 方法 2: 设置环境变量
29+
30+
在运行时设置环境变量来控制日志级别:
31+
32+
```powershell
33+
# Windows PowerShell
34+
$env:WAILS_LOGLEVEL="error"
35+
go run .
36+
37+
# 或运行已编译的程序
38+
$env:WAILS_LOGLEVEL="error"
39+
.\anyproxyai.exe
40+
```
41+
42+
```bash
43+
# Linux/macOS
44+
WAILS_LOGLEVEL=error go run .
45+
```
46+
47+
可用的日志级别:
48+
- `trace` - 所有日志(最详细)
49+
- `debug` - 调试信息
50+
- `info` - 信息日志(默认)
51+
- `warning` - 警告信息
52+
- `error` - 仅错误信息
53+
- `fatal` - 仅致命错误
54+
55+
### 方法 3: 创建启动脚本
56+
57+
创建 `run-quiet.ps1`:
58+
59+
```powershell
60+
# run-quiet.ps1
61+
$env:WAILS_LOGLEVEL="error"
62+
go run .
63+
```
64+
65+
`run-quiet.bat`:
66+
67+
```batch
68+
@echo off
69+
set WAILS_LOGLEVEL=error
70+
go run .
71+
```
72+
73+
### 方法 4: 在配置文件中添加日志控制(待实现)
74+
75+
未来可以在 `config.json` 中添加:
76+
77+
```json
78+
{
79+
"debug": false,
80+
"wailsLogLevel": "error"
81+
}
82+
```
83+
84+
然后在代码中根据配置设置环境变量。
85+
86+
## 当前状态
87+
88+
目前最简单的方式是:
89+
90+
1. **开发时**: 直接使用 `go run .`(查看所有日志)
91+
2. **生产时**: 使用 `wails build` 构建(自动优化日志)
92+
3. **临时需要**: 使用环境变量 `$env:WAILS_LOGLEVEL="error"`
93+
94+
## 注意事项
95+
96+
- Wails 框架日志(`INF Binding call`)无法通过应用代码完全禁用
97+
- 这是 Wails v3 的预期行为,用于调试前后端通信
98+
- 生产构建会自动优化这些日志输出
99+
- 应用自己的日志(`time="..." level=info msg="..."`)可以通过 logrus 配置控制

build/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ info:
1010
description: "Universal AI API Proxy Router with Multi-Format Support"
1111
copyright: "(c) 2025, AnyProxyAi"
1212
comments: "AI API Proxy Router supporting OpenAI, Claude, and Gemini formats"
13-
version: "2.0.6"
13+
version: "2.0.7"
1414
# Dev mode configuration
1515
dev_mode:
1616
root_path: .

build/windows/anyproxyai.exe.manifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
22
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3-
<assemblyIdentity type="win32" name="com.anyproxyai.app" version="2.0.6.0" processorArchitecture="*"/>
3+
<assemblyIdentity type="win32" name="com.anyproxyai.app" version="2.0.7.0" processorArchitecture="*"/>
44
<dependency>
55
<dependentAssembly>
66
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>

frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openai-router-frontend",
3-
"version": "2.0.6",
3+
"version": "2.0.7",
44
"type": "module",
55
"scripts": {
66
"dev": "vite",
@@ -21,4 +21,4 @@
2121
"@vitejs/plugin-vue": "^5.0.0",
2222
"vite": "^5.0.0"
2323
}
24-
}
24+
}

frontend/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@
585585

586586
<n-space align="center">
587587
<n-icon size="20"><InformationCircleIcon /></n-icon>
588-
<n-text>{{ t('settings.version') }}: v2.0.6</n-text>
588+
<n-text>{{ t('settings.version') }}: v2.0.7</n-text>
589589
</n-space>
590590

591591
<n-space align="center">

internal/service/proxy_service.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1203,27 +1203,46 @@ func (s *ProxyService) streamWithAdapter(reader io.Reader, writer io.Writer, flu
12031203
func (s *ProxyService) streamDirect(reader io.Reader, writer io.Writer, flusher http.Flusher, model string, routeID int64) error {
12041204
buf := make([]byte, 4096)
12051205
var responseBuffer bytes.Buffer
1206+
var bytesWritten int64
12061207

12071208
for {
12081209
n, err := reader.Read(buf)
12091210
if n > 0 {
12101211
// 将数据写入缓冲区以便后续解析token使用信息
12111212
responseBuffer.Write(buf[:n])
1213+
bytesWritten += int64(n)
12121214

12131215
if _, writeErr := writer.Write(buf[:n]); writeErr != nil {
1216+
log.Errorf("[Stream Direct] Failed to write to client: %v", writeErr)
12141217
s.routeService.LogRequest(model, routeID, 0, 0, 0, false, writeErr.Error())
12151218
return writeErr
12161219
}
12171220
flusher.Flush()
12181221
}
12191222
if err != nil {
12201223
if err == io.EOF {
1224+
log.Debugf("[Stream Direct] Stream completed. Total bytes: %d", bytesWritten)
1225+
12211226
// 尝试从响应中提取token使用信息
1222-
promptTokens, completionTokens := s.extractTokensFromStreamResponse(responseBuffer.String())
1227+
responseStr := responseBuffer.String()
1228+
log.Debugf("[Stream Direct] Response buffer length: %d bytes", len(responseStr))
1229+
1230+
// 仅在debug模式下记录响应内容(前500字符)
1231+
if len(responseStr) > 0 {
1232+
previewLen := 500
1233+
if len(responseStr) < previewLen {
1234+
previewLen = len(responseStr)
1235+
}
1236+
log.Debugf("[Stream Direct] Response preview: %s", responseStr[:previewLen])
1237+
}
1238+
1239+
promptTokens, completionTokens := s.extractTokensFromStreamResponse(responseStr)
12231240
totalTokens := promptTokens + completionTokens
1241+
log.Infof("[Stream Direct] Extracted tokens: prompt=%d, completion=%d, total=%d", promptTokens, completionTokens, totalTokens)
12241242
s.routeService.LogRequest(model, routeID, promptTokens, completionTokens, totalTokens, true, "")
12251243
return nil
12261244
}
1245+
log.Errorf("[Stream Direct] Stream error: %v", err)
12271246
s.routeService.LogRequest(model, routeID, 0, 0, 0, false, err.Error())
12281247
return err
12291248
}
@@ -3910,6 +3929,10 @@ func (s *ProxyService) ProxyCursorStreamRequest(requestBody []byte, headers map[
39103929
targetURL = s.buildAdapterStreamURL(cleanAPIUrl, adapterName, model)
39113930
} else {
39123931
reqData["stream"] = true
3932+
// 请求后端在流式响应中包含 usage 信息
3933+
reqData["stream_options"] = map[string]interface{}{
3934+
"include_usage": true,
3935+
}
39133936
transformedBody, _ = json.Marshal(reqData)
39143937
targetURL = buildOpenAIChatURL(route.APIUrl)
39153938
}
34.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)