1
0

fix: Anthropic 流式编码器补全 message_start/message_delta 必填字段

跨协议流式转换时,Anthropic 客户端 Zod 校验因 SSE 事件缺少必填字段报错。
由 Anthropic encoder 层(而非 OpenAI decoder 层)负责补全协议默认值,保持权责分离。

- encodeMessageStart 补全 type/content/stop_reason/stop_sequence,usage nil 时输出零值
- encodeMessageDelta usage nil 时输出零值
- 更新相关测试覆盖新增行为
This commit is contained in:
2026-04-26 23:27:34 +08:00
parent 9622d44aac
commit 38a2555c7b
3 changed files with 90 additions and 7 deletions

View File

@@ -50,16 +50,24 @@ func (e *StreamEncoder) encodeMessageStart(event canonical.CanonicalStreamEvent)
}
if event.Message != nil {
msg := map[string]any{
"id": event.Message.ID,
"model": event.Message.Model,
"role": "assistant",
"id": event.Message.ID,
"type": "message",
"role": "assistant",
"content": []any{},
"model": event.Message.Model,
"stop_reason": nil,
"stop_sequence": nil,
}
if event.Message.Usage != nil {
usage := map[string]any{
msg["usage"] = map[string]any{
"input_tokens": event.Message.Usage.InputTokens,
"output_tokens": event.Message.Usage.OutputTokens,
}
msg["usage"] = usage
} else {
msg["usage"] = map[string]any{
"input_tokens": 0,
"output_tokens": 0,
}
}
payload["message"] = msg
}
@@ -147,6 +155,10 @@ func (e *StreamEncoder) encodeMessageDelta(event canonical.CanonicalStreamEvent)
payload["usage"] = map[string]any{
"output_tokens": event.Usage.OutputTokens,
}
} else {
payload["usage"] = map[string]any{
"output_tokens": 0,
}
}
return e.marshalEvent("message_delta", payload)
}

View File

@@ -21,8 +21,55 @@ func TestStreamEncoder_MessageStart(t *testing.T) {
s := string(chunks[0])
assert.True(t, strings.HasPrefix(s, "event: message_start\n"))
assert.Contains(t, s, "data: ")
assert.Contains(t, s, "msg_1")
assert.Contains(t, s, "claude-3")
var payload map[string]any
lines := strings.Split(s, "\n")
for _, l := range lines {
if strings.HasPrefix(l, "data: ") {
require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(l, "data: ")), &payload))
break
}
}
msg, ok := payload["message"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "msg_1", msg["id"])
assert.Equal(t, "message", msg["type"])
assert.Equal(t, "assistant", msg["role"])
assert.Equal(t, []any{}, msg["content"])
assert.Equal(t, "claude-3", msg["model"])
assert.Nil(t, msg["stop_reason"])
assert.Nil(t, msg["stop_sequence"])
usage, okU := msg["usage"].(map[string]any)
require.True(t, okU)
assert.Equal(t, float64(0), usage["input_tokens"])
assert.Equal(t, float64(0), usage["output_tokens"])
}
func TestStreamEncoder_MessageStart_WithUsage(t *testing.T) {
e := NewStreamEncoder()
event := canonical.NewMessageStartEventWithUsage("msg_2", "gpt-4", &canonical.CanonicalUsage{InputTokens: 100, OutputTokens: 50})
chunks := e.EncodeEvent(event)
require.Len(t, chunks, 1)
s := string(chunks[0])
var payload map[string]any
lines := strings.Split(s, "\n")
for _, l := range lines {
if strings.HasPrefix(l, "data: ") {
require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(l, "data: ")), &payload))
break
}
}
msg, ok := payload["message"].(map[string]any)
require.True(t, ok)
usage, okU := msg["usage"].(map[string]any)
require.True(t, okU)
assert.Equal(t, float64(100), usage["input_tokens"])
assert.Equal(t, float64(50), usage["output_tokens"])
}
func TestStreamEncoder_ContentBlockDelta(t *testing.T) {
@@ -179,6 +226,10 @@ func TestStreamEncoder_MessageDelta_WithStopReason(t *testing.T) {
delta, okd := payload["delta"].(map[string]any)
require.True(t, okd)
assert.Equal(t, "end_turn", delta["stop_reason"])
usage, oku := payload["usage"].(map[string]any)
require.True(t, oku, "message_delta SHALL always include usage")
assert.Equal(t, float64(0), usage["output_tokens"])
}
func TestStreamEncoder_MessageDelta_WithUsage(t *testing.T) {

View File

@@ -239,6 +239,26 @@ Decoder 几乎 1:1 映射,维护最小状态机:
- **WHEN** delta.type == "thinking_delta"
- **THEN** SHALL 编码为 Anthropic thinking_delta
#### Scenario: message_start 事件编码完整 message 字段
- **WHEN** 编码 MessageStartEvent
- **THEN** SHALL 输出 `message` 对象包含以下字段:
- `id`: 来自 event.Message.ID
- `type`: 固定值 `"message"`
- `role`: 固定值 `"assistant"`
- `content`: 固定值 `[]`(空数组)
- `model`: 来自 event.Message.Model
- `stop_reason`: 固定值 `null`
- `stop_sequence`: 固定值 `null`
- `usage`: 来自 event.Message.Usage若 Usage 为 nilSHALL 输出 `{"input_tokens": 0, "output_tokens": 0}`
#### Scenario: message_delta 事件编码包含 usage 字段
- **WHEN** 编码 MessageDeltaEvent
- **THEN** SHALL 输出 `usage` 字段
- **WHEN** event.Usage 为 nil
- **THEN** SHALL 输出 `{"output_tokens": 0}`
### Requirement: Anthropic 错误编码
系统 SHALL 实现 Anthropic 协议的错误编码。