feat: implement strict-client compatible streaming tool calls with index and add Vercel build troubleshooting guide.

This commit is contained in:
CJACK
2026-02-16 17:58:40 +08:00
parent c7ed01bfe7
commit 63ee2e41c2
7 changed files with 111 additions and 1 deletions

View File

@@ -167,6 +167,7 @@ When `tools` is present, DS2API injects a tool prompt and parses tool-call paylo
- Non-stream: if detected, returns `message.tool_calls`, `finish_reason=tool_calls`, and `message.content=null`
- Stream: to avoid leaking raw tool-call JSON, DS2API buffers text first; if tool call is detected, only structured `delta.tool_calls` is emitted
- Stream `delta.tool_calls` is strict-client compatible: each tool call object includes `index` (starting from `0`)
Tool-call response example:

1
API.md
View File

@@ -171,6 +171,7 @@ data: [DONE]
- 非流式:若识别到工具调用,返回 `message.tool_calls`,并设置 `finish_reason=tool_calls``message.content=null`
- 流式:为防止原始 toolcall JSON 泄漏,正文会先缓冲;若识别到工具调用,仅输出结构化 `delta.tool_calls`
- 流式 `delta.tool_calls` 兼容严格客户端:每个 tool call 对象都带 `index`(从 `0` 开始)
工具调用响应示例:

View File

@@ -94,6 +94,25 @@ This repo includes `.github/workflows/release-artifacts.yml`:
- Builds Linux/macOS/Windows archives and uploads them to Release Assets
- Generates `sha256sums.txt` for integrity checks
## 3.2 Vercel Build Troubleshooting
If you see an error like:
```text
Error: Command failed: go build -ldflags -s -w -o .../bootstrap .../main__vc__go__.go
```
it is usually caused by invalid Go build flag settings in Vercel
(`-ldflags` not passed as a single argument).
How to fix:
1. Open Vercel Project Settings -> Build and Development Settings
2. Clear custom Go Build Flags / Build Command (recommended)
3. If ldflags must be used, set `-ldflags=\"-s -w\"` so it is passed as one argument
4. Ensure `go.mod` uses a supported version (this repo uses `go 1.24`)
5. Redeploy (preferably with cache cleared)
## 4. Reverse Proxy (Nginx)
Disable buffering for SSE:

View File

@@ -94,6 +94,24 @@ docker-compose up -d --build
- 自动构建 Linux/macOS/Windows 二进制包并上传到 Release Assets
- 生成 `sha256sums.txt` 供校验
## 3.2 Vercel 常见报错排查
若看到类似报错:
```text
Error: Command failed: go build -ldflags -s -w -o .../bootstrap .../main__vc__go__.go
```
通常是 Vercel 项目里的 Go 构建参数配置不正确(`-ldflags` 没有作为一个整体字符串传递)。
处理方式:
1. 进入 Vercel Project Settings -> Build and Development Settings
2. 清空自定义 Go Build Flags / Build Command推荐
3. 若必须设置 ldflags使用 `-ldflags=\"-s -w\"`(保证它是一个参数)
4. 确认仓库 `go.mod` 为受支持版本(当前为 `go 1.24`
5. 重新部署(建议 `Redeploy` 并清缓存)
## 4. 反向代理Nginx
如果在 Nginx 后挂载,建议关闭缓冲以保证 SSE

View File

@@ -246,7 +246,7 @@ func (h *Handler) handleStream(w http.ResponseWriter, r *http.Request, resp *htt
if len(detected) > 0 {
finishReason = "tool_calls"
delta := map[string]any{
"tool_calls": util.FormatOpenAIToolCalls(detected),
"tool_calls": util.FormatOpenAIStreamToolCalls(detected),
}
if !firstChunkSent {
delta["role"] = "assistant"

View File

@@ -209,6 +209,24 @@ func TestHandleStreamToolCallInterceptsWithoutRawContentLeak(t *testing.T) {
if !streamHasToolCallsDelta(frames) {
t.Fatalf("expected tool_calls delta, body=%s", rec.Body.String())
}
foundToolIndex := false
for _, frame := range frames {
choices, _ := frame["choices"].([]any)
for _, item := range choices {
choice, _ := item.(map[string]any)
delta, _ := choice["delta"].(map[string]any)
toolCalls, _ := delta["tool_calls"].([]any)
for _, tc := range toolCalls {
tcm, _ := tc.(map[string]any)
if _, ok := tcm["index"].(float64); ok {
foundToolIndex = true
}
}
}
}
if !foundToolIndex {
t.Fatalf("expected stream tool_calls item with index, body=%s", rec.Body.String())
}
if streamHasRawToolJSONContent(frames) {
t.Fatalf("raw tool_calls JSON leaked in content delta: %s", rec.Body.String())
}
@@ -236,6 +254,24 @@ func TestHandleStreamReasonerToolCallInterceptsWithoutRawContentLeak(t *testing.
if !streamHasToolCallsDelta(frames) {
t.Fatalf("expected tool_calls delta, body=%s", rec.Body.String())
}
foundToolIndex := false
for _, frame := range frames {
choices, _ := frame["choices"].([]any)
for _, item := range choices {
choice, _ := item.(map[string]any)
delta, _ := choice["delta"].(map[string]any)
toolCalls, _ := delta["tool_calls"].([]any)
for _, tc := range toolCalls {
tcm, _ := tc.(map[string]any)
if _, ok := tcm["index"].(float64); ok {
foundToolIndex = true
}
}
}
}
if !foundToolIndex {
t.Fatalf("expected stream tool_calls item with index, body=%s", rec.Body.String())
}
if streamHasRawToolJSONContent(frames) {
t.Fatalf("raw tool_calls JSON leaked in content delta: %s", rec.Body.String())
}
@@ -277,6 +313,24 @@ func TestHandleStreamUnknownToolStillIntercepted(t *testing.T) {
if !streamHasToolCallsDelta(frames) {
t.Fatalf("expected tool_calls delta, body=%s", rec.Body.String())
}
foundToolIndex := false
for _, frame := range frames {
choices, _ := frame["choices"].([]any)
for _, item := range choices {
choice, _ := item.(map[string]any)
delta, _ := choice["delta"].(map[string]any)
toolCalls, _ := delta["tool_calls"].([]any)
for _, tc := range toolCalls {
tcm, _ := tc.(map[string]any)
if _, ok := tcm["index"].(float64); ok {
foundToolIndex = true
}
}
}
}
if !foundToolIndex {
t.Fatalf("expected stream tool_calls item with index, body=%s", rec.Body.String())
}
if streamHasRawToolJSONContent(frames) {
t.Fatalf("raw tool_calls JSON leaked in content delta: %s", rec.Body.String())
}

View File

@@ -298,3 +298,20 @@ func FormatOpenAIToolCalls(calls []ParsedToolCall) []map[string]any {
}
return out
}
func FormatOpenAIStreamToolCalls(calls []ParsedToolCall) []map[string]any {
out := make([]map[string]any, 0, len(calls))
for i, c := range calls {
args, _ := json.Marshal(c.Input)
out = append(out, map[string]any{
"index": i,
"id": "call_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
"type": "function",
"function": map[string]any{
"name": c.Name,
"arguments": string(args),
},
})
}
return out
}