M41 的「LLM / embedding / 视觉 上游错误体」一族(M38 #5 #6 #7 #8 #9 #14): 非 2xx 时把响应体截断后拼进 error,而 error 会被 chat run / loop step / retrieve_fallback 打日志,等于把她的提问、记忆正文、要念的台词写进日志。 - model:openai/anthropic 两处 upstreamError 收口到 upstream.go 的 providerUpstreamError;anthropic 流内 error 事件的 evt.Error.Message (原本无上限)整段丢弃,只保留令牌形状的 type - embedding:readErrorBody 删除,改走 helper(input 就是她的记忆正文) - vision:错误路径不再沿用取图的 50 MiB 上限,改为 4 KiB 有界读且不拼原文 - voice:classifyDashscopeStatus 只解 code/request_id,正文只留在内存里做 分类判据;错误体读取同样改为 4 KiB 有界 - 测试:五个上游各一条「日志/error 里搜不到哨兵正文」用例,且能变异打红
466 lines
16 KiB
Go
466 lines
16 KiB
Go
package model
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||
)
|
||
|
||
const okCompletion = `{
|
||
"id": "cmpl-1",
|
||
"model": "gpt-test",
|
||
"choices": [
|
||
{"index": 0, "message": {"role": "assistant", "content": "Hello!"}, "finish_reason": "stop"}
|
||
],
|
||
"usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7}
|
||
}`
|
||
|
||
// capturedRequest 记录 OpenAIChatAdapter 发往 stub 服务器的内容。
|
||
type capturedRequest struct {
|
||
method string
|
||
path string
|
||
contentType string
|
||
body map[string]any
|
||
}
|
||
|
||
func newCompletionServer(t *testing.T, status int, contentType, body string, ch chan<- capturedRequest) *httptest.Server {
|
||
t.Helper()
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var reqBody map[string]any
|
||
if ch != nil {
|
||
_ = json.NewDecoder(r.Body).Decode(&reqBody)
|
||
}
|
||
if contentType != "" {
|
||
w.Header().Set("Content-Type", contentType)
|
||
}
|
||
w.WriteHeader(status)
|
||
if body != "" {
|
||
_, _ = io.WriteString(w, body)
|
||
}
|
||
if ch != nil {
|
||
ch <- capturedRequest{
|
||
method: r.Method,
|
||
path: r.URL.Path,
|
||
contentType: r.Header.Get("Content-Type"),
|
||
body: reqBody,
|
||
}
|
||
}
|
||
}))
|
||
t.Cleanup(srv.Close)
|
||
return srv
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_NonStreaming(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, ch)
|
||
|
||
recs := make([]CallRecord, 0, 1)
|
||
temp := 1.0
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, APIKey: "sk-test", Model: "gpt-test"},
|
||
WithRecorder(func(cr CallRecord) { recs = append(recs, cr) }))
|
||
|
||
ctx := context.Background()
|
||
resp, err := ad.Chat(ctx, Request{
|
||
Messages: []Message{TextMessage(RoleUser, "hi")},
|
||
Model: "gpt-test",
|
||
Temperature: &temp,
|
||
MaxTokens: 64,
|
||
Purpose: "chat",
|
||
}, Options{})
|
||
if err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
|
||
req := <-ch
|
||
if req.method != http.MethodPost {
|
||
t.Errorf("method = %s, want POST", req.method)
|
||
}
|
||
if req.path != "/chat/completions" {
|
||
t.Errorf("path = %s, want /chat/completions", req.path)
|
||
}
|
||
if req.contentType != "application/json" {
|
||
t.Errorf("content-type = %s, want application/json", req.contentType)
|
||
}
|
||
if req.body["model"] != "gpt-test" {
|
||
t.Errorf("body.model = %v, want gpt-test", req.body["model"])
|
||
}
|
||
if req.body["stream"] != nil && req.body["stream"] != false {
|
||
t.Errorf("body.stream = %v, want omitted(false)", req.body["stream"])
|
||
}
|
||
if req.body["temperature"] != 1.0 {
|
||
t.Errorf("body.temperature = %v, want 1.0", req.body["temperature"])
|
||
}
|
||
if req.body["max_tokens"] != float64(64) {
|
||
t.Errorf("body.max_tokens = %v, want 64", req.body["max_tokens"])
|
||
}
|
||
msgs, _ := req.body["messages"].([]any)
|
||
if len(msgs) != 1 {
|
||
t.Fatalf("body.messages = %v, want 1 message", req.body["messages"])
|
||
}
|
||
|
||
if resp.Model != "gpt-test" {
|
||
t.Errorf("resp.Model = %q, want gpt-test", resp.Model)
|
||
}
|
||
if resp.Content != "Hello!" {
|
||
t.Errorf("resp.Content = %q, want Hello!", resp.Content)
|
||
}
|
||
if resp.TotalTokens != 7 || resp.PromptTokens != 5 || resp.CompletionTokens != 2 {
|
||
t.Errorf("resp usage = (%d,%d,%d), want (5,2,7)", resp.PromptTokens, resp.CompletionTokens, resp.TotalTokens)
|
||
}
|
||
if resp.Message.Role != RoleAssistant || len(resp.Message.Content) != 1 || resp.Message.Content[0].Text != "Hello!" {
|
||
t.Errorf("resp.Message = %+v", resp.Message)
|
||
}
|
||
|
||
if len(recs) != 1 {
|
||
t.Fatalf("records = %d, want 1", len(recs))
|
||
}
|
||
if recs[0].Model != "gpt-test" || recs[0].Purpose != "chat" || recs[0].Tokens != 7 || recs[0].TookMs < 0 {
|
||
t.Errorf("record = %+v, want model=gpt-test purpose=chat tokens=7 took_ms>=0", recs[0])
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_ImageContent(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, ch)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"})
|
||
|
||
req := Request{Messages: []Message{ImageMessage(RoleUser, "what is this", "data:image/png;base64,abc")}}
|
||
if _, err := ad.Chat(context.Background(), req, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
|
||
got := <-ch
|
||
msgs, _ := got.body["messages"].([]any)
|
||
part0, _ := msgs[0].(map[string]any)
|
||
content, _ := part0["content"].([]any)
|
||
if len(content) != 2 {
|
||
t.Fatalf("content parts = %d, want 2 (text+image)", len(content))
|
||
}
|
||
found := false
|
||
for _, c := range content {
|
||
cp, _ := c.(map[string]any)
|
||
if cp["type"] != "image_url" {
|
||
continue
|
||
}
|
||
found = true
|
||
iu, _ := cp["image_url"].(map[string]any)
|
||
if iu["url"] != "data:image/png;base64,abc" {
|
||
t.Fatalf("image url = %v, want data:image/png;base64,abc", iu["url"])
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("no image_url part found in %v", content)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_ToolsPlaceholder(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, ch)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"})
|
||
|
||
req := Request{Messages: []Message{TextMessage(RoleUser, "hi")},
|
||
Tools: []ToolSpec{{Name: "lookup", Description: "look it up", Parameters: map[string]any{"type": "object"}}}}
|
||
if _, err := ad.Chat(context.Background(), req, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
|
||
got := <-ch
|
||
tools, ok := got.body["tools"].([]any)
|
||
if !ok || len(tools) != 1 {
|
||
t.Fatalf("body.tools = %v, want 1 entry", got.body["tools"])
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_ChatStream_SSE(t *testing.T) {
|
||
t.Parallel()
|
||
sse := strings.Join([]string{
|
||
`data: {"model":"gpt-test","choices":[{"index":0,"delta":{"role":"assistant","content":"Hel"}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{"content":"lo"}}]}`,
|
||
``,
|
||
`data: {"model":"gpt-test","choices":[],"usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}`,
|
||
``,
|
||
`data: [DONE]`,
|
||
``,
|
||
}, "\n")
|
||
srv := newCompletionServer(t, http.StatusOK, "text/event-stream", sse, nil)
|
||
|
||
recs := make([]CallRecord, 0, 1)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"},
|
||
WithRecorder(func(cr CallRecord) { recs = append(recs, cr) }))
|
||
|
||
var events []StreamEvent
|
||
resp, err := ad.ChatStream(context.Background(),
|
||
Request{Messages: []Message{TextMessage(RoleUser, "hi")}, Model: "gpt-test", Purpose: "chat"},
|
||
func(ev StreamEvent) { events = append(events, ev) })
|
||
if err != nil {
|
||
t.Fatalf("ChatStream error: %v", err)
|
||
}
|
||
if resp.Content != "Hello" {
|
||
t.Errorf("resp.Content = %q, want Hello", resp.Content)
|
||
}
|
||
if resp.Model != "gpt-test" {
|
||
t.Errorf("resp.Model = %q, want gpt-test", resp.Model)
|
||
}
|
||
if resp.TotalTokens != 7 {
|
||
t.Errorf("resp.TotalTokens = %d, want 7", resp.TotalTokens)
|
||
}
|
||
wantEvents := []StreamEvent{
|
||
{Kind: StreamKindTextStart},
|
||
{Kind: StreamKindTextDelta, Delta: "Hel"},
|
||
{Kind: StreamKindTextDelta, Delta: "lo"},
|
||
{Kind: StreamKindTextEnd},
|
||
}
|
||
if !reflect.DeepEqual(events, wantEvents) {
|
||
t.Errorf("events = %+v, want %+v", events, wantEvents)
|
||
}
|
||
if len(recs) != 1 || recs[0].Tokens != 7 || recs[0].Purpose != "chat" {
|
||
t.Errorf("records = %+v, want 1 record tokens=7 purpose=chat", recs)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_UpstreamError(t *testing.T) {
|
||
t.Parallel()
|
||
srv := newCompletionServer(t, http.StatusInternalServerError, "application/json", `{"error":"boom"}`, nil)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "m"})
|
||
|
||
_, err := ad.Chat(context.Background(), Request{Messages: []Message{TextMessage(RoleUser, "hi")}}, Options{})
|
||
if err == nil {
|
||
t.Fatal("want error, got nil")
|
||
}
|
||
if !errors.Is(err, ErrUpstream) {
|
||
t.Fatalf("err = %v, want ErrUpstream", err)
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_Chat_UpstreamErrorDropsPromptEcho 覆盖 M41 的一家 provider:
|
||
// 4xx 的错误体里回显了她的提问,**提问不能出现在 error 文本里**。
|
||
//
|
||
// 为什么断言的是「文本里搜不到」而不是「error 非空」:旧实现把响应体截断
|
||
// 512 字节后拼进 error,error 又被 chat run / loop step 打日志 —— 截断只是把
|
||
// 「整段泄漏」变成「前 512 字节泄漏」。只有「搜不到原文」这个断言能把实现
|
||
// 变异回旧写法时打红。
|
||
func TestOpenAIChat_Chat_UpstreamErrorDropsPromptEcho(t *testing.T) {
|
||
t.Parallel()
|
||
const echoed = "SENTINEL-OPENAI-PROMPT-9f3a"
|
||
body := `{"error":{"code":"invalid_request_error","type":"invalid_request_error",` +
|
||
`"param":"messages","message":"Invalid value for 'messages[0].content': '` + echoed + `'"}}`
|
||
srv := newCompletionServer(t, http.StatusUnprocessableEntity, "application/json", body, nil)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "m"})
|
||
|
||
_, err := ad.Chat(context.Background(), Request{Messages: []Message{TextMessage(RoleUser, echoed)}}, Options{})
|
||
if err == nil {
|
||
t.Fatal("want error, got nil")
|
||
}
|
||
if !errors.Is(err, ErrUpstream) || !errors.Is(err, trace.ErrUpstream) {
|
||
t.Fatalf("err = %v, want both ErrUpstream and trace.ErrUpstream", err)
|
||
}
|
||
if strings.Contains(err.Error(), echoed) {
|
||
t.Errorf("error text echoes the prompt: %v", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "422") {
|
||
t.Errorf("err = %v, want the status kept for triage", err)
|
||
}
|
||
if !strings.Contains(err.Error(), "invalid_request_error") {
|
||
t.Errorf("err = %v, want the error.code kept for triage", err)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_NoBaseURL(t *testing.T) {
|
||
t.Parallel()
|
||
ad := NewOpenAIChat(config.LLMConfig{})
|
||
if _, err := ad.Chat(context.Background(), Request{Messages: []Message{TextMessage(RoleUser, "hi")}}, Options{}); !errors.Is(err, ErrUpstream) {
|
||
t.Fatalf("err = %v, want ErrUpstream", err)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_NoModel(t *testing.T) {
|
||
t.Parallel()
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, nil)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL})
|
||
if _, err := ad.Chat(context.Background(), Request{Messages: []Message{TextMessage(RoleUser, "hi")}}, Options{}); !errors.Is(err, ErrNoModel) {
|
||
t.Fatalf("err = %v, want ErrNoModel", err)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_ChatStream_ReasoningAndText(t *testing.T) {
|
||
t.Parallel()
|
||
sse := strings.Join([]string{
|
||
`data: {"model":"gpt-test","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Let me th"}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{"reasoning_content":"ink about it"}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{"content":"Hel"}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{"content":"lo"}}]}`,
|
||
``,
|
||
`data: {"choices":[],"usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}`,
|
||
``,
|
||
`data: [DONE]`,
|
||
``,
|
||
}, "\n")
|
||
srv := newCompletionServer(t, http.StatusOK, "text/event-stream", sse, nil)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"})
|
||
|
||
var events []StreamEvent
|
||
resp, err := ad.ChatStream(context.Background(),
|
||
Request{Messages: []Message{TextMessage(RoleUser, "hi")}, Model: "gpt-test"},
|
||
func(ev StreamEvent) { events = append(events, ev) })
|
||
if err != nil {
|
||
t.Fatalf("ChatStream error: %v", err)
|
||
}
|
||
if resp.Content != "Hello" {
|
||
t.Errorf("resp.Content = %q, want Hello (reasoning must not leak into body content)", resp.Content)
|
||
}
|
||
wantEvents := []StreamEvent{
|
||
{Kind: StreamKindThinkingStart},
|
||
{Kind: StreamKindThinkingDelta, Delta: "Let me th"},
|
||
{Kind: StreamKindThinkingDelta, Delta: "ink about it"},
|
||
{Kind: StreamKindThinkingEnd},
|
||
{Kind: StreamKindTextStart},
|
||
{Kind: StreamKindTextDelta, Delta: "Hel"},
|
||
{Kind: StreamKindTextDelta, Delta: "lo"},
|
||
{Kind: StreamKindTextEnd},
|
||
}
|
||
if !reflect.DeepEqual(events, wantEvents) {
|
||
t.Errorf("events = %+v, want %+v", events, wantEvents)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_ChatStream_ToolCall(t *testing.T) {
|
||
t.Parallel()
|
||
sse := strings.Join([]string{
|
||
`data: {"model":"gpt-test","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_1","function":{"name":"lookup","arguments":"{\"loc"}}]}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ation\":\"Paris\"}"}}]}}]}`,
|
||
``,
|
||
`data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}`,
|
||
``,
|
||
`data: {"choices":[],"usage":{"prompt_tokens":5,"completion_tokens":6,"total_tokens":11}}`,
|
||
``,
|
||
`data: [DONE]`,
|
||
``,
|
||
}, "\n")
|
||
srv := newCompletionServer(t, http.StatusOK, "text/event-stream", sse, nil)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"})
|
||
|
||
var events []StreamEvent
|
||
resp, err := ad.ChatStream(context.Background(),
|
||
Request{Messages: []Message{TextMessage(RoleUser, "hi")}, Model: "gpt-test"},
|
||
func(ev StreamEvent) { events = append(events, ev) })
|
||
if err != nil {
|
||
t.Fatalf("ChatStream error: %v", err)
|
||
}
|
||
if len(resp.ToolCalls) != 1 {
|
||
t.Fatalf("resp.ToolCalls = %+v, want 1", resp.ToolCalls)
|
||
}
|
||
tc := resp.ToolCalls[0]
|
||
if tc.Name != "lookup" {
|
||
t.Errorf("tool call name = %q, want lookup", tc.Name)
|
||
}
|
||
if tc.Args["location"] != "Paris" {
|
||
t.Errorf("tool call args.location = %v, want Paris (reassembled from argument fragments)", tc.Args["location"])
|
||
}
|
||
if resp.Content != "" {
|
||
t.Errorf("resp.Content = %q, want empty (tool-only response)", resp.Content)
|
||
}
|
||
wantEvents := []StreamEvent{
|
||
{Kind: StreamKindToolCallStart, Index: 0, ID: "call_1"},
|
||
{Kind: StreamKindToolCallDelta, Index: 0, ID: "call_1", Delta: `{"loc`},
|
||
{Kind: StreamKindToolCallDelta, Index: 0, ID: "call_1", Delta: `ation":"Paris"}`},
|
||
{Kind: StreamKindToolCallEnd, Index: 0, ID: "call_1"},
|
||
}
|
||
if !reflect.DeepEqual(events, wantEvents) {
|
||
t.Errorf("events = %+v, want %+v", events, wantEvents)
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Chat_ToolRoundTrip(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, ch)
|
||
ad := NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: "gpt-test"})
|
||
|
||
req := Request{Messages: []Message{
|
||
TextMessage(RoleUser, "what's the weather in Paris?"),
|
||
AssistantToolCallMessage([]ToolCall{{ID: "call_1", Name: "weather", Args: map[string]any{"city": "Paris"}}}),
|
||
ToolResultMessage("call_1", `{"temp":22}`, true),
|
||
}, Model: "gpt-test"}
|
||
if _, err := ad.Chat(context.Background(), req, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
|
||
got := <-ch
|
||
msgs, _ := got.body["messages"].([]any)
|
||
if len(msgs) != 3 {
|
||
t.Fatalf("body.messages = %v, want 3 messages", got.body["messages"])
|
||
}
|
||
|
||
// 助手回合必须携带 tool_calls(好让 tool 消息能被
|
||
// 提供商关联)——这一点此前被完全丢弃。
|
||
assistant, _ := msgs[1].(map[string]any)
|
||
if assistant["role"] != "assistant" {
|
||
t.Errorf("assistant role = %v, want assistant", assistant["role"])
|
||
}
|
||
tcs, _ := assistant["tool_calls"].([]any)
|
||
if len(tcs) != 1 {
|
||
t.Fatalf("assistant tool_calls = %v, want 1 entry", assistant["tool_calls"])
|
||
}
|
||
tc := tcs[0].(map[string]any)
|
||
if tc["id"] != "call_1" || tc["type"] != "function" {
|
||
t.Errorf("tool_call = %v, want id=call_1 type=function", tc)
|
||
}
|
||
fn, _ := tc["function"].(map[string]any)
|
||
if fn["name"] != "weather" || fn["arguments"] != `{"city":"Paris"}` {
|
||
t.Errorf("function = %#v, want name=weather arguments={\"city\":\"Paris\"}", fn)
|
||
}
|
||
|
||
// tool 消息必须携带 tool_call_id(审计指出的缺口)与结果。
|
||
tool, _ := msgs[2].(map[string]any)
|
||
if tool["role"] != "tool" {
|
||
t.Errorf("tool role = %v, want tool", tool["role"])
|
||
}
|
||
if tool["tool_call_id"] != "call_1" {
|
||
t.Errorf("tool_call_id = %v, want call_1", tool["tool_call_id"])
|
||
}
|
||
if tool["content"] != `{"temp":22}` {
|
||
t.Errorf("tool content = %v, want the raw result", tool["content"])
|
||
}
|
||
}
|
||
|
||
func TestOpenAIChat_Build_ToolMessageLegacy(t *testing.T) {
|
||
t.Parallel()
|
||
// loop 当前的形态:一个携带纯文本片段与
|
||
// Message.ToolCallID 的 RoleTool 消息。buildMessages 必须发出带 tool_call_id 的 tool 消息。
|
||
m := Message{Role: RoleTool, ToolCallID: "call_9",
|
||
Content: []Content{{Type: ContentTypeText, Text: "tool weather result: 22"}}}
|
||
msgs, err := buildMessages([]Message{m})
|
||
if err != nil {
|
||
t.Fatalf("buildMessages error: %v", err)
|
||
}
|
||
if len(msgs) != 1 {
|
||
t.Fatalf("messages = %d, want 1", len(msgs))
|
||
}
|
||
if msgs[0].Role != "tool" {
|
||
t.Errorf("role = %q, want tool", msgs[0].Role)
|
||
}
|
||
if msgs[0].ToolCallID != "call_9" {
|
||
t.Errorf("tool_call_id = %q, want call_9", msgs[0].ToolCallID)
|
||
}
|
||
if msgs[0].Content != "tool weather result: 22" {
|
||
t.Errorf("content = %v, want the text", msgs[0].Content)
|
||
}
|
||
}
|