137 lines
4.8 KiB
Go
137 lines
4.8 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"oci-portal/internal/aiwire"
|
|
)
|
|
|
|
func mustAnthReq(t *testing.T, body string) aiwire.MessagesRequest {
|
|
t.Helper()
|
|
var req aiwire.MessagesRequest
|
|
if err := json.Unmarshal([]byte(body), &req); err != nil {
|
|
t.Fatalf("unmarshal anthropic request: %v", err)
|
|
}
|
|
return req
|
|
}
|
|
|
|
func TestAnthropicToIR(t *testing.T) {
|
|
req := mustAnthReq(t, `{
|
|
"model": "meta.llama-3.3-70b-instruct", "max_tokens": 128, "system": "你是助手",
|
|
"messages": [
|
|
{"role": "user", "content": "东京天气?"},
|
|
{"role": "assistant", "content": [
|
|
{"type": "text", "text": "查询中"},
|
|
{"type": "tool_use", "id": "t1", "name": "get_weather", "input": {"city": "东京"}}
|
|
]},
|
|
{"role": "user", "content": [
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "晴 25 度"},
|
|
{"type": "text", "text": "继续"}
|
|
]}
|
|
],
|
|
"tools": [{"name": "get_weather", "input_schema": {"type": "object"}}],
|
|
"tool_choice": {"type": "any"}
|
|
}`)
|
|
ir, err := AnthropicToIR(req)
|
|
if err != nil {
|
|
t.Fatalf("AnthropicToIR: %v", err)
|
|
}
|
|
roles := make([]string, 0, len(ir.Messages))
|
|
for _, m := range ir.Messages {
|
|
roles = append(roles, m.Role)
|
|
}
|
|
want := []string{"system", "user", "assistant", "tool", "user"}
|
|
if strings.Join(roles, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("roles = %v, want %v", roles, want)
|
|
}
|
|
if ir.Messages[2].ToolCalls[0].Function.Name != "get_weather" {
|
|
t.Errorf("tool_use 未映射: %+v", ir.Messages[2])
|
|
}
|
|
if ir.Messages[3].ToolCallID != "t1" || ir.Messages[3].Content.JoinText() != "晴 25 度" {
|
|
t.Errorf("tool_result 未拆为 tool 消息: %+v", ir.Messages[3])
|
|
}
|
|
if ir.MaxTokens == nil || *ir.MaxTokens != 128 || len(ir.Tools) != 1 {
|
|
t.Errorf("max_tokens/tools 未直通")
|
|
}
|
|
if string(ir.ToolChoice) != `"required"` {
|
|
t.Errorf("tool_choice any → %s, want required", ir.ToolChoice)
|
|
}
|
|
// image 块拒绝
|
|
bad := mustAnthReq(t, `{"model":"m","max_tokens":1,"messages":[{"role":"user","content":[{"type":"image","source":{}}]}]}`)
|
|
if _, err := AnthropicToIR(bad); err == nil {
|
|
t.Error("image 块应被拒绝")
|
|
}
|
|
}
|
|
|
|
func TestIRRespToAnthropic(t *testing.T) {
|
|
resp := &aiwire.ChatResponse{
|
|
Model: "m1",
|
|
Choices: []aiwire.Choice{{
|
|
Message: aiwire.ChatMessage{
|
|
Role: "assistant",
|
|
Content: aiwire.NewTextContent("查到了"),
|
|
ToolCalls: []aiwire.ToolCall{{ID: "t1", Type: "function",
|
|
Function: aiwire.FunctionCall{Name: "get_weather", Arguments: `{"city":"东京"}`}}},
|
|
},
|
|
FinishReason: "tool_calls",
|
|
}},
|
|
Usage: &aiwire.Usage{PromptTokens: 10, CompletionTokens: 5},
|
|
}
|
|
out := IRRespToAnthropic(resp, "msg_1")
|
|
if len(out.Content) != 2 || out.Content[0].Type != "text" || out.Content[1].Type != "tool_use" {
|
|
t.Fatalf("content 块 = %+v", out.Content)
|
|
}
|
|
if out.StopReason != "tool_use" || out.Usage.InputTokens != 10 || out.Usage.OutputTokens != 5 {
|
|
t.Errorf("stop_reason/usage 未映射: %+v", out)
|
|
}
|
|
if string(out.Content[1].Input) != `{"city":"东京"}` {
|
|
t.Errorf("tool input = %s", out.Content[1].Input)
|
|
}
|
|
}
|
|
|
|
// eventTypes 提取事件类型序列便于断言。
|
|
func eventTypes(events []AnthEvent) string {
|
|
types := make([]string, 0, len(events))
|
|
for _, e := range events {
|
|
types = append(types, e.Event)
|
|
}
|
|
return strings.Join(types, ",")
|
|
}
|
|
|
|
func TestAnthStreamTextAndTool(t *testing.T) {
|
|
st := NewAnthStream("msg_1", "m1")
|
|
fr := "tool_calls"
|
|
var events []AnthEvent
|
|
events = append(events, st.Feed(aiwire.ChatChunk{Choices: []aiwire.ChunkChoice{{Delta: aiwire.Delta{Content: "你"}}}})...)
|
|
events = append(events, st.Feed(aiwire.ChatChunk{Choices: []aiwire.ChunkChoice{{Delta: aiwire.Delta{Content: "好"}}}})...)
|
|
events = append(events, st.Feed(aiwire.ChatChunk{Choices: []aiwire.ChunkChoice{{Delta: aiwire.Delta{
|
|
ToolCalls: []aiwire.ToolCallDelta{{ID: "t1", Function: aiwire.FunctionCallDelta{Name: "get_weather", Arguments: `{"ci`}}},
|
|
}}}})...)
|
|
events = append(events, st.Feed(aiwire.ChatChunk{
|
|
Choices: []aiwire.ChunkChoice{{Delta: aiwire.Delta{ToolCalls: []aiwire.ToolCallDelta{{ID: "t1", Function: aiwire.FunctionCallDelta{Arguments: `ty":"东京"}`}}}}, FinishReason: &fr}},
|
|
Usage: &aiwire.Usage{PromptTokens: 8, CompletionTokens: 4},
|
|
})...)
|
|
events = append(events, st.Finish()...)
|
|
|
|
got := eventTypes(events)
|
|
want := "message_start,content_block_start,content_block_delta,content_block_delta," +
|
|
"content_block_stop,content_block_start,content_block_delta,content_block_delta," +
|
|
"content_block_stop,message_delta,message_stop"
|
|
if got != want {
|
|
t.Fatalf("事件序列:\n got %s\nwant %s", got, want)
|
|
}
|
|
if st.Usage().OutputTokens != 4 {
|
|
t.Errorf("usage 未聚合: %+v", st.Usage())
|
|
}
|
|
}
|
|
|
|
func TestAnthStreamEmpty(t *testing.T) {
|
|
st := NewAnthStream("msg_1", "m1")
|
|
events := st.Finish()
|
|
if got := eventTypes(events); got != "message_start,message_delta,message_stop" {
|
|
t.Errorf("空流事件序列 = %s", got)
|
|
}
|
|
}
|