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