diff --git a/API.en.md b/API.en.md index 678e6b0..122d9c9 100644 --- a/API.en.md +++ b/API.en.md @@ -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: diff --git a/API.md b/API.md index 55f34e5..e66ba30 100644 --- a/API.md +++ b/API.md @@ -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` 开始) 工具调用响应示例: diff --git a/DEPLOY.en.md b/DEPLOY.en.md index 503efa7..bf09b31 100644 --- a/DEPLOY.en.md +++ b/DEPLOY.en.md @@ -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: diff --git a/DEPLOY.md b/DEPLOY.md index 1ccc080..fa75a36 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -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: diff --git a/internal/adapter/openai/handler.go b/internal/adapter/openai/handler.go index d507849..d497659 100644 --- a/internal/adapter/openai/handler.go +++ b/internal/adapter/openai/handler.go @@ -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" diff --git a/internal/adapter/openai/handler_toolcall_test.go b/internal/adapter/openai/handler_toolcall_test.go index df39d51..6bc8d30 100644 --- a/internal/adapter/openai/handler_toolcall_test.go +++ b/internal/adapter/openai/handler_toolcall_test.go @@ -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()) } diff --git a/internal/util/toolcalls.go b/internal/util/toolcalls.go index a594a6a..9b9d4e6 100644 --- a/internal/util/toolcalls.go +++ b/internal/util/toolcalls.go @@ -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 +}