From bf4a4e6d4cab719181f0ec1d116f55a94f2638ff Mon Sep 17 00:00:00 2001 From: zerone0x <39543393+zerone0x@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:06:52 +0000 Subject: [PATCH] fix(llm): send whip user agent --- internal/llm/openai.go | 18 ++++++--- internal/llm/user_agent_test.go | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 internal/llm/user_agent_test.go diff --git a/internal/llm/openai.go b/internal/llm/openai.go index 9776ecce..eff43ba2 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -349,6 +349,16 @@ func New(baseURL, apiKey string) *Client { } } +const userAgent = "whip" + +func (c *Client) setProviderHeaders(req *http.Request, contentType bool) { + if contentType { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("Authorization", "Bearer "+c.APIKey) + req.Header.Set("User-Agent", userAgent) +} + // Request is a chat completions request. type Request struct { Model string `json:"model"` @@ -600,7 +610,7 @@ func (c *Client) Models(ctx context.Context) ([]ModelInfo, error) { if err != nil { return nil, err } - hr.Header.Set("Authorization", "Bearer "+c.APIKey) + c.setProviderHeaders(hr, false) resp, err := c.HTTP.Do(hr) if err != nil { return nil, err @@ -683,8 +693,7 @@ func (c *Client) streamOnce(ctx context.Context, body []byte, onText, onThink fu if err != nil { return Message{}, Usage{}, err } - hr.Header.Set("Content-Type", "application/json") - hr.Header.Set("Authorization", "Bearer "+c.APIKey) + c.setProviderHeaders(hr, true) resp, err := c.HTTP.Do(hr) if err != nil { return Message{}, Usage{}, err @@ -814,8 +823,7 @@ func (c *Client) completeOnce(ctx context.Context, body []byte) (string, Usage, if err != nil { return "", Usage{}, err } - hr.Header.Set("Content-Type", "application/json") - hr.Header.Set("Authorization", "Bearer "+c.APIKey) + c.setProviderHeaders(hr, true) resp, err := c.HTTP.Do(hr) if err != nil { return "", Usage{}, err diff --git a/internal/llm/user_agent_test.go b/internal/llm/user_agent_test.go new file mode 100644 index 00000000..7354b1a9 --- /dev/null +++ b/internal/llm/user_agent_test.go @@ -0,0 +1,72 @@ +package llm + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +func TestClientSendsWhipUserAgent(t *testing.T) { + tests := []struct { + name string + run func(*Client) error + }{ + { + name: "models", + run: func(c *Client) error { + _, err := c.Models(context.Background()) + return err + }, + }, + { + name: "stream", + run: func(c *Client) error { + _, _, err := c.Stream(context.Background(), Request{Model: "m", Messages: []Message{{Role: "user", Content: "hi"}}}, nil, nil, nil) + return err + }, + }, + { + name: "complete", + run: func(c *Client) error { + _, _, err := c.Complete(context.Background(), Request{Model: "m", Messages: []Message{{Role: "user", Content: "hi"}}}) + return err + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotUserAgent string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotUserAgent = r.Header.Get("User-Agent") + switch r.URL.Path { + case "/models": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[]}`)) + case "/chat/completions": + if r.Header.Get("Content-Type") != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", r.Header.Get("Content-Type")) + } + w.Header().Set("Content-Type", "text/event-stream") + if tt.name == "complete" { + _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) + return + } + _, _ = w.Write([]byte("data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n")) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer srv.Close() + + c := New(srv.URL, "k") + if err := tt.run(c); err != nil { + t.Fatal(err) + } + if gotUserAgent != "whip" { + t.Fatalf("User-Agent = %q, want whip", gotUserAgent) + } + }) + } +}