package llm import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" ) func TestEndpointNormalization(t *testing.T) { cases := map[string]string{ "http://h:4001": "http://h:4001/v1/chat/completions", "http://h:4001/": "http://h:4001/v1/chat/completions", "http://h:4001/v1": "http://h:4001/v1/chat/completions", "http://h:4001/v1/": "http://h:4001/v1/chat/completions", "http://h/prefix": "http://h/prefix/v1/chat/completions", "http://h/prefix/v1/": "http://h/prefix/v1/chat/completions", } for in, want := range cases { c := NewClient(in, "k", time.Second, 0) if c.endpoint != want { t.Errorf("NewClient(%q).endpoint = %q, want %q", in, c.endpoint, want) } } } func TestChatSendsAuthAndModel(t *testing.T) { var gotAuth, gotModel string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Errorf("decode request: %v", err) } gotModel = req.Model w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"hi"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}`)) })) defer srv.Close() c := NewClient(srv.URL, "sk-test", time.Second, 0) resp, err := c.Chat(context.Background(), ChatRequest{Model: "glm-5.3", Messages: []Message{{Role: "user", Content: "x"}}}) if err != nil { t.Fatalf("Chat: %v", err) } if gotAuth != "Bearer sk-test" { t.Errorf("auth = %q", gotAuth) } if gotModel != "glm-5.3" { t.Errorf("model = %q", gotModel) } if resp.Choices[0].Message.Content != "hi" || resp.Usage.TotalTokens != 3 { t.Errorf("resp = %+v", resp) } } func TestChatRetriesOn5xxThenSucceeds(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if calls.Add(1) == 1 { http.Error(w, "upstream lost", http.StatusBadGateway) return } w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}`)) })) defer srv.Close() c := NewClient(srv.URL, "k", time.Second, 2) resp, err := c.Chat(context.Background(), ChatRequest{Model: "m"}) if err != nil { t.Fatalf("Chat: %v", err) } if resp.Choices[0].Message.Content != "ok" || calls.Load() != 2 { t.Errorf("resp=%v calls=%d", resp, calls.Load()) } } func TestChatNoRetryOn4xx(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) http.Error(w, `{"error":"bad model"}`, http.StatusBadRequest) })) defer srv.Close() c := NewClient(srv.URL, "k", time.Second, 3) _, err := c.Chat(context.Background(), ChatRequest{Model: "m"}) if err == nil { t.Fatal("expected error") } if calls.Load() != 1 { t.Errorf("4xx must not retry, calls=%d", calls.Load()) } } func TestChatEmptyChoicesIsError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"choices":[]}`)) })) defer srv.Close() c := NewClient(srv.URL, "k", time.Second, 0) if _, err := c.Chat(context.Background(), ChatRequest{Model: "m"}); err == nil { t.Fatal("expected empty-choices error") } }