refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
- 新增 ConversionEngine 核心引擎,支持 OpenAI 和 Anthropic 协议转换 - 添加 stream decoder/encoder 实现 - 更新 provider client 支持新引擎 - 补充单元测试和集成测试 - 更新 specs 文档
This commit is contained in:
@@ -353,3 +353,120 @@ func TestStreamDecoder_MultipleChunks_Text(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, []string{"你好", "世界"}, deltas)
|
||||
}
|
||||
|
||||
func TestStreamDecoder_UTF8Truncation(t *testing.T) {
|
||||
d := NewStreamDecoder()
|
||||
|
||||
chunk := map[string]any{
|
||||
"id": "chatcmpl-utf8",
|
||||
"model": "gpt-4",
|
||||
"choices": []any{
|
||||
map[string]any{
|
||||
"index": 0,
|
||||
"delta": map[string]any{"content": "你"},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(chunk)
|
||||
sseData := []byte("data: " + string(data) + "\n\n")
|
||||
|
||||
mid := len(sseData) - 5
|
||||
part1 := sseData[:mid]
|
||||
part2 := sseData[mid:]
|
||||
|
||||
events1 := d.ProcessChunk(part1)
|
||||
for _, e := range events1 {
|
||||
if e.Type == canonical.EventContentBlockDelta && e.Delta != nil {
|
||||
assert.Equal(t, "你", e.Delta.Text)
|
||||
}
|
||||
}
|
||||
|
||||
events2 := d.ProcessChunk(part2)
|
||||
_ = events2
|
||||
}
|
||||
|
||||
func TestStreamDecoder_ToolCallSubsequentDelta(t *testing.T) {
|
||||
d := NewStreamDecoder()
|
||||
|
||||
idx := 0
|
||||
chunk1 := map[string]any{
|
||||
"id": "chatcmpl-tc",
|
||||
"model": "gpt-4",
|
||||
"choices": []any{
|
||||
map[string]any{
|
||||
"index": 0,
|
||||
"delta": map[string]any{
|
||||
"tool_calls": []any{
|
||||
map[string]any{
|
||||
"index": &idx,
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "get_weather",
|
||||
"arguments": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
chunk2 := map[string]any{
|
||||
"id": "chatcmpl-tc",
|
||||
"model": "gpt-4",
|
||||
"choices": []any{
|
||||
map[string]any{
|
||||
"index": 0,
|
||||
"delta": map[string]any{
|
||||
"tool_calls": []any{
|
||||
map[string]any{
|
||||
"index": &idx,
|
||||
"function": map[string]any{
|
||||
"arguments": "{\"city\":\"Beijing\"}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
events1 := d.ProcessChunk(makeChunkSSE(chunk1))
|
||||
require.NotEmpty(t, events1)
|
||||
|
||||
events2 := d.ProcessChunk(makeChunkSSE(chunk2))
|
||||
require.NotEmpty(t, events2)
|
||||
|
||||
foundInputJSON := false
|
||||
for _, e := range events2 {
|
||||
if e.Type == canonical.EventContentBlockDelta && e.Delta != nil && e.Delta.Type == "input_json_delta" {
|
||||
foundInputJSON = true
|
||||
assert.Equal(t, "{\"city\":\"Beijing\"}", e.Delta.PartialJSON)
|
||||
}
|
||||
}
|
||||
assert.True(t, foundInputJSON, "subsequent tool call delta should emit input_json_delta")
|
||||
}
|
||||
|
||||
func TestStreamDecoder_InvalidJSON(t *testing.T) {
|
||||
d := NewStreamDecoder()
|
||||
|
||||
raw := []byte("data: {invalid json}\n\n")
|
||||
events := d.ProcessChunk(raw)
|
||||
assert.Nil(t, events)
|
||||
}
|
||||
|
||||
func TestStreamDecoder_NonDataLines(t *testing.T) {
|
||||
d := NewStreamDecoder()
|
||||
|
||||
raw := []byte(": comment line\ndata: {\"id\":\"1\",\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\n")
|
||||
events := d.ProcessChunk(raw)
|
||||
require.NotEmpty(t, events)
|
||||
found := false
|
||||
for _, e := range events {
|
||||
if e.Type == canonical.EventContentBlockDelta && e.Delta != nil {
|
||||
found = true
|
||||
assert.Equal(t, "hi", e.Delta.Text)
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user