claude code tools error
This commit is contained in:
@@ -49,9 +49,11 @@ func eventLine(name string, obj any) []byte {
|
||||
type messagesToChat struct {
|
||||
sseState
|
||||
id, model string
|
||||
toolIdx map[int]int // messages content block index → chat tool_calls index(顺序编号,避开文本块)
|
||||
nextTool int
|
||||
}
|
||||
|
||||
func newMessagesToChat() *messagesToChat { return &messagesToChat{} }
|
||||
func newMessagesToChat() *messagesToChat { return &messagesToChat{toolIdx: map[int]int{}} }
|
||||
|
||||
func (t *messagesToChat) line(line []byte) []byte {
|
||||
isData, data, done := t.parseLine(line)
|
||||
@@ -72,8 +74,43 @@ func (t *messagesToChat) line(line []byte) []byte {
|
||||
"id": "chatcmpl-" + strings.TrimPrefix(t.id, "msg_"), "object": "chat.completion.chunk", "model": t.model,
|
||||
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"role": "assistant"}, "finish_reason": nil}},
|
||||
})
|
||||
case "content_block_start":
|
||||
cb, _ := m["content_block"].(map[string]any)
|
||||
if cb == nil || cb["type"] != "tool_use" {
|
||||
return nil
|
||||
}
|
||||
blockIdx, _ := m["index"].(float64)
|
||||
tool := t.nextTool
|
||||
t.nextTool++
|
||||
t.toolIdx[int(blockIdx)] = tool
|
||||
toolID, _ := cb["id"].(string)
|
||||
name, _ := cb["name"].(string)
|
||||
return dataLine(map[string]any{
|
||||
"id": "chatcmpl-" + strings.TrimPrefix(t.id, "msg_"), "object": "chat.completion.chunk", "model": t.model,
|
||||
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{
|
||||
"tool_calls": []any{map[string]any{"index": tool, "id": toolID, "type": "function", "function": map[string]any{"name": name, "arguments": ""}}},
|
||||
}, "finish_reason": nil}},
|
||||
})
|
||||
case "content_block_delta":
|
||||
delta, _ := m["delta"].(map[string]any)
|
||||
deltaType, _ := delta["type"].(string)
|
||||
if deltaType == "input_json_delta" {
|
||||
blockIdx, _ := m["index"].(float64)
|
||||
tool, ok := t.toolIdx[int(blockIdx)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
partial, _ := delta["partial_json"].(string)
|
||||
if partial == "" {
|
||||
return nil
|
||||
}
|
||||
return dataLine(map[string]any{
|
||||
"id": "chatcmpl-" + strings.TrimPrefix(t.id, "msg_"), "object": "chat.completion.chunk", "model": t.model,
|
||||
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{
|
||||
"tool_calls": []any{map[string]any{"index": tool, "function": map[string]any{"arguments": partial}}},
|
||||
}, "finish_reason": nil}},
|
||||
})
|
||||
}
|
||||
text, _ := delta["text"].(string)
|
||||
if text == "" {
|
||||
return nil
|
||||
@@ -118,14 +155,19 @@ func joinLines(lines [][]byte) []byte {
|
||||
|
||||
type chatToMessages struct {
|
||||
sseState
|
||||
started bool
|
||||
blockStarted bool
|
||||
model string
|
||||
stopReason string
|
||||
usage any
|
||||
started bool // message_start 已发出
|
||||
nextIndex int // 下一个 content block index(顺序分配)
|
||||
textIndex int // 文本块 index;-1 = 未开始
|
||||
toolIdx map[int]int // chat delta.tool_calls[].index → messages block index
|
||||
openBlocks []int // 已开始未停止的 block index,按开始顺序
|
||||
model string
|
||||
stopReason string
|
||||
usage any
|
||||
}
|
||||
|
||||
func newChatToMessages() *chatToMessages { return &chatToMessages{} }
|
||||
func newChatToMessages() *chatToMessages {
|
||||
return &chatToMessages{textIndex: -1, toolIdx: map[int]int{}}
|
||||
}
|
||||
|
||||
func (t *chatToMessages) line(line []byte) []byte {
|
||||
isData, data, done := t.parseLine(line)
|
||||
@@ -133,18 +175,18 @@ func (t *chatToMessages) line(line []byte) []byte {
|
||||
return nil
|
||||
}
|
||||
if done {
|
||||
// 汇聚最终 message_delta + content_block_stop + message_stop
|
||||
// 汇聚最终:先对每个已开始未停止的块发 content_block_stop,再 message_delta + message_stop
|
||||
var out [][]byte
|
||||
for _, idx := range t.openBlocks {
|
||||
out = append(out, eventLine("content_block_stop", map[string]any{"type": "content_block_stop", "index": idx}))
|
||||
}
|
||||
md := map[string]any{"type": "message_delta", "delta": map[string]any{
|
||||
"stop_reason": stopReasonOrEnd(t.stopReason), "stop_sequence": nil,
|
||||
}}
|
||||
if t.usage != nil {
|
||||
md["usage"] = t.usage
|
||||
}
|
||||
var out [][]byte
|
||||
out = append(out, eventLine("message_delta", md))
|
||||
if t.blockStarted {
|
||||
out = append(out, eventLine("content_block_stop", map[string]any{"type": "content_block_stop", "index": 0}))
|
||||
}
|
||||
out = append(out, eventLine("message_stop", map[string]any{"type": "message_stop"}))
|
||||
return joinLines(out)
|
||||
}
|
||||
@@ -167,47 +209,97 @@ func (t *chatToMessages) line(line []byte) []byte {
|
||||
id, _ := m["id"].(string)
|
||||
|
||||
var out [][]byte
|
||||
// 首个包含内容或角色的块前,先发 message_start + content_block_start
|
||||
if !t.started {
|
||||
role, _ := delta["role"].(string)
|
||||
content, _ := delta["content"].(string)
|
||||
if role == "assistant" || content != "" {
|
||||
t.started = true
|
||||
out = append(out, eventLine("message_start", map[string]any{
|
||||
"type": "message_start",
|
||||
"message": map[string]any{
|
||||
"id": "msg_" + strings.TrimPrefix(id, "chatcmpl-"), "type": "message", "role": "assistant",
|
||||
"model": t.model, "content": []any{}, "usage": map[string]any{"input_tokens": 0, "output_tokens": 0},
|
||||
},
|
||||
}))
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "text", "text": ""},
|
||||
}))
|
||||
t.blockStarted = true
|
||||
// message_start 只在实际有内容(文本或工具)时发出,避免 GLM 等模型的
|
||||
// reasoning_content 块(带 role 无 content)提前开出一个空文本块。
|
||||
ensureStarted := func() {
|
||||
if t.started {
|
||||
return
|
||||
}
|
||||
}
|
||||
if content, _ := delta["content"].(string); content != "" {
|
||||
if !t.started {
|
||||
t.started = true
|
||||
t.blockStarted = true
|
||||
out = append(out, eventLine("message_start", map[string]any{
|
||||
"type": "message_start",
|
||||
"message": map[string]any{"id": "msg_" + strings.TrimPrefix(id, "chatcmpl-"), "type": "message", "role": "assistant", "model": t.model, "content": []any{}},
|
||||
}))
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "text", "text": ""},
|
||||
}))
|
||||
}
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": 0, "delta": map[string]any{"type": "text_delta", "text": content},
|
||||
t.started = true
|
||||
out = append(out, eventLine("message_start", map[string]any{
|
||||
"type": "message_start",
|
||||
"message": map[string]any{
|
||||
"id": "msg_" + strings.TrimPrefix(id, "chatcmpl-"), "type": "message", "role": "assistant",
|
||||
"model": t.model, "content": []any{}, "usage": map[string]any{"input_tokens": 0, "output_tokens": 0},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// 文本:delta.content(string;兼容 {type:text,text} 数组)
|
||||
if content := deltaText(delta); content != "" {
|
||||
if t.textIndex < 0 {
|
||||
t.textIndex = t.nextIndex
|
||||
t.nextIndex++
|
||||
ensureStarted()
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": t.textIndex, "content_block": map[string]any{"type": "text", "text": ""},
|
||||
}))
|
||||
t.openBlocks = append(t.openBlocks, t.textIndex)
|
||||
}
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": t.textIndex, "delta": map[string]any{"type": "text_delta", "text": content},
|
||||
}))
|
||||
}
|
||||
|
||||
// 工具调用:delta.tool_calls(并行调用各 index 独立成块;arguments 支持整段/分段两种流式)
|
||||
if tcs, ok := delta["tool_calls"].([]any); ok {
|
||||
for _, tc := range tcs {
|
||||
call, ok := tc.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
idx, _ := call["index"].(float64)
|
||||
tcIdx := int(idx)
|
||||
fn, _ := call["function"].(map[string]any)
|
||||
name, _ := fn["name"].(string)
|
||||
args, _ := fn["arguments"].(string)
|
||||
blockIdx, seen := t.toolIdx[tcIdx]
|
||||
if !seen {
|
||||
blockIdx = t.nextIndex
|
||||
t.nextIndex++
|
||||
t.toolIdx[tcIdx] = blockIdx
|
||||
toolID, _ := call["id"].(string)
|
||||
ensureStarted()
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": blockIdx, "content_block": map[string]any{
|
||||
"type": "tool_use", "id": toolID, "name": name, "input": map[string]any{},
|
||||
},
|
||||
}))
|
||||
t.openBlocks = append(t.openBlocks, blockIdx)
|
||||
}
|
||||
if args != "" {
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": blockIdx, "delta": map[string]any{"type": "input_json_delta", "partial_json": args},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if u, ok := m["usage"]; ok {
|
||||
t.usage = u
|
||||
}
|
||||
return joinLines(out)
|
||||
}
|
||||
|
||||
// deltaText 取 chat delta.content 文本(string 或 [{type:text,text}] 数组拼接)。
|
||||
func deltaText(delta map[string]any) string {
|
||||
if s, ok := delta["content"].(string); ok {
|
||||
return s
|
||||
}
|
||||
if arr, ok := delta["content"].([]any); ok {
|
||||
var parts []string
|
||||
for _, b := range arr {
|
||||
if bm, ok := b.(map[string]any); ok {
|
||||
if t, _ := bm["text"].(string); t != "" {
|
||||
parts = append(parts, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func stopReasonOrEnd(s string) string {
|
||||
if s == "" {
|
||||
return "end_turn"
|
||||
@@ -220,12 +312,19 @@ func stopReasonOrEnd(s string) string {
|
||||
|
||||
type responsesToMessages struct {
|
||||
sseState
|
||||
started bool
|
||||
model string
|
||||
usage any
|
||||
started bool
|
||||
model string
|
||||
usage any
|
||||
nextIndex int // 下一个 content block index(顺序分配)
|
||||
textIndex int // 文本块 index;-1 = 未开始
|
||||
toolIdx map[string]int // function_call item_id → messages block index
|
||||
openBlocks []int // 已开始未停止的 block index,按开始顺序
|
||||
anyTool bool
|
||||
}
|
||||
|
||||
func newResponsesToMessages() *responsesToMessages { return &responsesToMessages{} }
|
||||
func newResponsesToMessages() *responsesToMessages {
|
||||
return &responsesToMessages{textIndex: -1, toolIdx: map[string]int{}}
|
||||
}
|
||||
|
||||
func (t *responsesToMessages) line(line []byte) []byte {
|
||||
isData, data, done := t.parseLine(line)
|
||||
@@ -243,38 +342,93 @@ func (t *responsesToMessages) line(line []byte) []byte {
|
||||
}
|
||||
}
|
||||
var out [][]byte
|
||||
// message_start 只在 response.created 时发出;文本/工具块在对应事件到达时再开,
|
||||
// 避免纯函数调用响应提前开出一个空文本块。
|
||||
ensureStarted := func() {
|
||||
if t.started {
|
||||
return
|
||||
}
|
||||
t.started = true
|
||||
rid := ""
|
||||
if resp, ok := m["response"].(map[string]any); ok {
|
||||
rid, _ = resp["id"].(string)
|
||||
}
|
||||
out = append(out, eventLine("message_start", map[string]any{
|
||||
"type": "message_start",
|
||||
"message": map[string]any{
|
||||
"id": "msg_" + strings.TrimPrefix(rid, "resp_"), "type": "message", "role": "assistant",
|
||||
"model": t.model, "content": []any{},
|
||||
},
|
||||
}))
|
||||
}
|
||||
switch evt {
|
||||
case "response.created":
|
||||
if !t.started {
|
||||
t.started = true
|
||||
id, _ := m["response"].(map[string]any)
|
||||
rid := ""
|
||||
if id != nil {
|
||||
rid, _ = id["id"].(string)
|
||||
}
|
||||
out = append(out, eventLine("message_start", map[string]any{
|
||||
"type": "message_start",
|
||||
"message": map[string]any{
|
||||
"id": "msg_" + strings.TrimPrefix(rid, "resp_"), "type": "message", "role": "assistant",
|
||||
"model": t.model, "content": []any{},
|
||||
},
|
||||
}))
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": 0, "content_block": map[string]any{"type": "text", "text": ""},
|
||||
}))
|
||||
}
|
||||
ensureStarted()
|
||||
case "response.output_text.delta":
|
||||
delta, _ := m["delta"].(string)
|
||||
if delta != "" {
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": 0, "delta": map[string]any{"type": "text_delta", "text": delta},
|
||||
}))
|
||||
if delta == "" {
|
||||
return nil
|
||||
}
|
||||
case "response.completed":
|
||||
out = append(out, eventLine("message_delta", map[string]any{
|
||||
"type": "message_delta", "delta": map[string]any{"stop_reason": "end_turn", "stop_sequence": nil},
|
||||
if t.textIndex < 0 {
|
||||
t.textIndex = t.nextIndex
|
||||
t.nextIndex++
|
||||
ensureStarted()
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": t.textIndex, "content_block": map[string]any{"type": "text", "text": ""},
|
||||
}))
|
||||
t.openBlocks = append(t.openBlocks, t.textIndex)
|
||||
}
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": t.textIndex, "delta": map[string]any{"type": "text_delta", "text": delta},
|
||||
}))
|
||||
out = append(out, eventLine("content_block_stop", map[string]any{"type": "content_block_stop", "index": 0}))
|
||||
case "response.output_item.added":
|
||||
item, _ := m["item"].(map[string]any)
|
||||
if item == nil || item["type"] != "function_call" {
|
||||
return nil
|
||||
}
|
||||
blockIdx := t.nextIndex
|
||||
t.nextIndex++
|
||||
t.anyTool = true
|
||||
itemID, _ := item["id"].(string)
|
||||
t.toolIdx[itemID] = blockIdx
|
||||
toolUseID, _ := item["call_id"].(string)
|
||||
if toolUseID == "" {
|
||||
toolUseID = itemID
|
||||
}
|
||||
name, _ := item["name"].(string)
|
||||
ensureStarted()
|
||||
out = append(out, eventLine("content_block_start", map[string]any{
|
||||
"type": "content_block_start", "index": blockIdx, "content_block": map[string]any{
|
||||
"type": "tool_use", "id": toolUseID, "name": name, "input": map[string]any{},
|
||||
},
|
||||
}))
|
||||
t.openBlocks = append(t.openBlocks, blockIdx)
|
||||
case "response.function_call_arguments.delta":
|
||||
itemID, _ := m["item_id"].(string)
|
||||
blockIdx, ok := t.toolIdx[itemID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
delta, _ := m["delta"].(string)
|
||||
if delta == "" {
|
||||
return nil
|
||||
}
|
||||
out = append(out, eventLine("content_block_delta", map[string]any{
|
||||
"type": "content_block_delta", "index": blockIdx, "delta": map[string]any{"type": "input_json_delta", "partial_json": delta},
|
||||
}))
|
||||
case "response.completed":
|
||||
for _, idx := range t.openBlocks {
|
||||
out = append(out, eventLine("content_block_stop", map[string]any{"type": "content_block_stop", "index": idx}))
|
||||
}
|
||||
stop := "end_turn"
|
||||
if t.anyTool {
|
||||
stop = "tool_use"
|
||||
}
|
||||
md := map[string]any{"type": "message_delta", "delta": map[string]any{"stop_reason": stop, "stop_sequence": nil}}
|
||||
if t.usage != nil {
|
||||
md["usage"] = t.usage
|
||||
}
|
||||
out = append(out, eventLine("message_delta", md))
|
||||
out = append(out, eventLine("message_stop", map[string]any{"type": "message_stop"}))
|
||||
}
|
||||
return joinLines(out)
|
||||
|
||||
Reference in New Issue
Block a user