1
0

refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包

- 新增 ConversionEngine 核心引擎,支持 OpenAI 和 Anthropic 协议转换
- 添加 stream decoder/encoder 实现
- 更新 provider client 支持新引擎
- 补充单元测试和集成测试
- 更新 specs 文档
This commit is contained in:
2026-04-20 13:01:05 +08:00
parent 1dac347d3b
commit bc1ee612d9
39 changed files with 11177 additions and 995 deletions

View File

@@ -170,3 +170,116 @@ func TestStreamEncoder_MessageDelta_WithUsage(t *testing.T) {
assert.Contains(t, s, "usage")
assert.Contains(t, s, "prompt_tokens")
}
func TestStreamEncoder_InputJSONDelta_SubsequentDelta(t *testing.T) {
e := NewStreamEncoder()
e.EncodeEvent(canonical.NewContentBlockStartEvent(0, canonical.StreamContentBlock{
Type: "tool_use",
ID: "call_1",
Name: "get_weather",
}))
e.EncodeEvent(canonical.NewContentBlockDeltaEvent(0, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: "{\"city\":",
}))
event := canonical.NewContentBlockDeltaEvent(0, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: "\"Beijing\"}",
})
chunks := e.EncodeEvent(event)
require.NotEmpty(t, chunks)
s := string(chunks[0])
assert.Contains(t, s, "tool_calls")
assert.Contains(t, s, "Beijing")
}
func TestStreamEncoder_MessageStart_NilMessage(t *testing.T) {
e := NewStreamEncoder()
event := canonical.CanonicalStreamEvent{Type: canonical.EventMessageStart}
chunks := e.EncodeEvent(event)
require.Len(t, chunks, 1)
s := string(chunks[0])
assert.Contains(t, s, "chat.completion.chunk")
}
func TestStreamEncoder_UnknownEvent_ReturnsNil(t *testing.T) {
e := NewStreamEncoder()
event := canonical.CanonicalStreamEvent{Type: "unknown_type"}
chunks := e.EncodeEvent(event)
assert.Nil(t, chunks)
}
func TestStreamEncoder_ContentBlockDelta_NilDelta(t *testing.T) {
e := NewStreamEncoder()
event := canonical.CanonicalStreamEvent{Type: canonical.EventContentBlockDelta}
chunks := e.EncodeEvent(event)
assert.Nil(t, chunks)
}
func TestStreamEncoder_MultiToolCall_IndexMapping(t *testing.T) {
e := NewStreamEncoder()
e.EncodeEvent(canonical.NewContentBlockStartEvent(0, canonical.StreamContentBlock{
Type: "tool_use",
ID: "call_1",
Name: "get_weather",
}))
firstDelta := canonical.NewContentBlockDeltaEvent(0, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: `{"city":"北京"}`,
})
chunks := e.EncodeEvent(firstDelta)
require.NotEmpty(t, chunks)
s := string(chunks[0])
assert.Contains(t, s, `"index":0`)
assert.Contains(t, s, "get_weather")
assert.Contains(t, s, "北京")
e.EncodeEvent(canonical.NewContentBlockStopEvent(0))
e.EncodeEvent(canonical.NewContentBlockStartEvent(1, canonical.StreamContentBlock{
Type: "tool_use",
ID: "call_2",
Name: "get_time",
}))
secondDelta := canonical.NewContentBlockDeltaEvent(1, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: `{"tz":"Asia/Shanghai"}`,
})
chunks = e.EncodeEvent(secondDelta)
require.NotEmpty(t, chunks)
s = string(chunks[0])
assert.Contains(t, s, `"index":1`)
assert.Contains(t, s, "get_time")
assert.Contains(t, s, "Asia/Shanghai")
subsequentDelta0 := canonical.NewContentBlockDeltaEvent(0, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: `"more_data"`,
})
chunks = e.EncodeEvent(subsequentDelta0)
require.NotEmpty(t, chunks)
s = string(chunks[0])
assert.Contains(t, s, `"index":0`)
assert.NotContains(t, s, "get_weather")
assert.Contains(t, s, "more_data")
subsequentDelta1 := canonical.NewContentBlockDeltaEvent(1, canonical.StreamDelta{
Type: string(canonical.DeltaTypeInputJSON),
PartialJSON: `"more_time"`,
})
chunks = e.EncodeEvent(subsequentDelta1)
require.NotEmpty(t, chunks)
s = string(chunks[0])
assert.Contains(t, s, `"index":1`)
assert.Contains(t, s, "more_time")
}