package llm import ( "context" "net/http" "net/http/httptest" "testing" ) func sseProviderServer(t *testing.T, sse string) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(sse)) })) } func collectToolCallDeltas(t *testing.T, p Provider) []ToolCall { t.Helper() var got []ToolCall _, err := p.Stream(context.Background(), "s", []Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}, func(d StreamDelta) { if d.ToolCall != nil { got = append(got, *d.ToolCall) } }) if err != nil { t.Fatal(err) } return got } // TestAnthropicEmitsCompletedToolCall:content_block_stop 一到就提前通知, // 不等 message_stop。 func TestAnthropicEmitsCompletedToolCall(t *testing.T) { srv := sseProviderServer(t, ""+ "data: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":10}}}\n\n"+ "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"read\"}}\n\n"+ "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\":\\\"a\\\"}\"}}\n\n"+ "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n"+ "data: {\"type\":\"message_stop\"}\n\n") defer srv.Close() p := &AnthropicProvider{BaseURL: srv.URL, APIKey: "k", Model: "m", MaxTokens: 128, HTTP: srv.Client()} got := collectToolCallDeltas(t, p) if len(got) != 1 { t.Fatalf("tool call deltas = %d, want 1", len(got)) } if got[0].ID != "t1" || got[0].Name != "read" || got[0].Input != `{"path":"a"}` { t.Fatalf("tool call = %+v", got[0]) } } // TestOpenAIEmitsCompletedToolCalls:下一个 index 出现时前一个调用完整; // finish_reason 时最后一个完整。两者都通过 delta 通道提前通知。 func TestOpenAIEmitsCompletedToolCalls(t *testing.T) { srv := sseProviderServer(t, ""+ "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"t1\",\"function\":{\"name\":\"read\",\"arguments\":\"{\\\"path\\\":\\\"\"}}]}}]}\n\n"+ "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"a\\\"}\"}}]}}]}\n\n"+ "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"t2\",\"function\":{\"name\":\"read\",\"arguments\":\"{}\"}}]}}]}\n\n"+ "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\n"+ "data: [DONE]\n\n") defer srv.Close() p := &OpenAIProvider{BaseURL: srv.URL, APIKey: "k", Model: "m", HTTP: srv.Client()} got := collectToolCallDeltas(t, p) if len(got) != 2 { t.Fatalf("tool call deltas = %d, want 2: %+v", len(got), got) } if got[0].ID != "t1" || got[0].Input != `{"path":"a"}` { t.Fatalf("first call = %+v", got[0]) } if got[1].ID != "t2" || got[1].Input != "{}" { t.Fatalf("second call = %+v", got[1]) } } // TestResponsesEmitsCompletedToolCall:output_item.done 的 parameters 已完整。 func TestResponsesEmitsCompletedToolCall(t *testing.T) { srv := sseProviderServer(t, ""+ "data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"function_call\",\"id\":\"item_1\",\"call_id\":\"c1\",\"name\":\"read\",\"arguments\":\"{\\\"path\\\":\\\"a\\\"}\"}}\n\n"+ "data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"type\":\"function_call\",\"call_id\":\"c1\",\"name\":\"read\",\"arguments\":\"{\\\"path\\\":\\\"a\\\"}\"}],\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}\n\n") defer srv.Close() p := &ResponsesProvider{BaseURL: srv.URL, APIKey: "k", Model: "m", HTTP: srv.Client()} got := collectToolCallDeltas(t, p) if len(got) != 1 { t.Fatalf("tool call deltas = %d, want 1 (dedup by ID): %+v", len(got), got) } if got[0].ID != "c1" || got[0].Input != `{"path":"a"}` { t.Fatalf("tool call = %+v", got[0]) } }