Files
oci-portal/internal/service/aiconvert_test.go
T

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)
}
}