mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 16:35:27 +08:00
feat: implement strict-client compatible streaming tool calls with index and add Vercel build troubleshooting guide.
This commit is contained in:
@@ -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
1
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` 开始)
|
||||
|
||||
工具调用响应示例:
|
||||
|
||||
|
||||
19
DEPLOY.en.md
19
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:
|
||||
|
||||
18
DEPLOY.md
18
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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user