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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 为 nil,SHALL 输出 `{"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 协议的错误编码。
|
||||
|
||||
Reference in New Issue
Block a user