转换器: 修复 Responses 流式 response.completed 缺 usage

- 上游 chat 流 finish_reason 块先于 usage 块, 原实现在 finish 时即发
  response.completed 导致 usage 为空; 改为等 usage 块到达(或 [DONE] 兜底)再发,
  completed 携带 input/output tokens

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-16 05:05:46 +08:00
co-authored by Claude
parent db83972b6f
commit e96f194a59
+10 -4
View File
@@ -412,9 +412,10 @@ func (t *responsesToChat) line(line []byte) []byte {
type chatToResponses struct { type chatToResponses struct {
sseState sseState
model string model string
usage any usage any
done bool finishSeen bool
done bool
} }
func newChatToResponses() *chatToResponses { return &chatToResponses{} } func newChatToResponses() *chatToResponses { return &chatToResponses{} }
@@ -425,6 +426,7 @@ func (t *chatToResponses) line(line []byte) []byte {
return nil return nil
} }
if done { if done {
// 流结束兜底:finish 后 usage 未随块到达时在此补发 completed
if !t.done { if !t.done {
t.done = true t.done = true
return eventLine("response.completed", map[string]any{ return eventLine("response.completed", map[string]any{
@@ -453,6 +455,9 @@ func (t *chatToResponses) line(line []byte) []byte {
finish, _ = c0["finish_reason"].(string) finish, _ = c0["finish_reason"].(string)
} }
} }
if finish != "" {
t.finishSeen = true
}
var out [][]byte var out [][]byte
if role, _ := delta["role"].(string); role == "assistant" { if role, _ := delta["role"].(string); role == "assistant" {
out = append(out, eventLine("response.created", map[string]any{ out = append(out, eventLine("response.created", map[string]any{
@@ -465,7 +470,8 @@ func (t *chatToResponses) line(line []byte) []byte {
"type": "response.output_text.delta", "delta": content, "item_id": "msg_1", "output_index": 0, "content_index": 0, "type": "response.output_text.delta", "delta": content, "item_id": "msg_1", "output_index": 0, "content_index": 0,
})) }))
} }
if finish != "" && !t.done { // 上游 usage 块(choices 为空)通常晚于 finish_reason:此时再发 completed,携带 usage
if _, hasUsage := m["usage"]; hasUsage && t.finishSeen && !t.done {
t.done = true t.done = true
out = append(out, eventLine("response.completed", map[string]any{ out = append(out, eventLine("response.completed", map[string]any{
"type": "response.completed", "type": "response.completed",