package llm import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func TestComplete_Success(t *testing.T) { resp := ChatResponse{ Choices: []struct { Message struct { Content string `json:"content"` } `json:"message"` }{ {Message: struct { Content string `json:"content"` }{Content: "Hello, world!"}}, }, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/chat/completions" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.Method != "POST" { t.Errorf("expected POST, got %s", r.Method) } if r.Header.Get("Authorization") != "Bearer test-key" { t.Errorf("unexpected auth: %s", r.Header.Get("Authorization")) } if r.Header.Get("Content-Type") != "application/json" { t.Errorf("unexpected content type: %s", r.Header.Get("Content-Type")) } var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } if req.Model != "gpt-4" { t.Errorf("expected model %q, got %q", "gpt-4", req.Model) } if len(req.Messages) != 1 { t.Errorf("expected 1 message, got %d", len(req.Messages)) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) })) defer server.Close() client := NewClient(server.URL, "test-key", "gpt-4") got, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "Hello, world!" { t.Errorf("expected %q, got %q", "Hello, world!", got) } } func TestComplete_APIError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte(`{"error":"rate limited"}`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "gpt-4") _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for 429, got nil") } } func TestComplete_NoChoices(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"choices":[]}`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "gpt-4") _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for no choices, got nil") } } func TestComplete_BadJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`not json at all`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "gpt-4") _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for bad JSON, got nil") } } func TestComplete_ServerDown(t *testing.T) { client := NewClient("http://127.0.0.1:1", "test-key", "gpt-4") _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for connection refused, got nil") } } func TestWithTemperature(t *testing.T) { client := NewClient("http://example.com", "key", "model") if client.temperature != 0 { t.Errorf("expected initial temperature 0, got %f", client.temperature) } result := client.WithTemperature(0.7) if result != client { t.Error("WithTemperature should return the same client for chaining") } if client.temperature != 0.7 { t.Errorf("expected temperature 0.7, got %f", client.temperature) } } func TestComplete_TemperatureOmittedWhenZero(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} json.NewDecoder(r.Body).Decode(&req) if _, exists := req["temperature"]; exists { t.Error("temperature should be omitted when zero (server default)") } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(ChatResponse{ Choices: []struct { Message struct { Content string `json:"content"` } `json:"message"` }{{Message: struct { Content string `json:"content"` }{Content: "ok"}}}, }) })) defer server.Close() client := NewClient(server.URL, "key", "model") _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestComplete_TemperatureIncludedWhenSet(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} json.NewDecoder(r.Body).Decode(&req) temp, exists := req["temperature"] if !exists { t.Error("temperature should be included when set") } if temp != 0.7 { t.Errorf("expected temperature 0.7, got %v", temp) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(ChatResponse{ Choices: []struct { Message struct { Content string `json:"content"` } `json:"message"` }{{Message: struct { Content string `json:"content"` }{Content: "ok"}}}, }) })) defer server.Close() client := NewClient(server.URL, "key", "model").WithTemperature(0.7) _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestWithTimeout(t *testing.T) { client := NewClient("http://example.com", "key", "model") result := client.WithTimeout(10 * time.Second) if result != client { t.Error("WithTimeout should return the same client for chaining") } // Verify timeout causes failure on slow server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(200 * time.Millisecond) w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) })) defer server.Close() shortClient := NewClient(server.URL, "key", "model").WithTimeout(50 * time.Millisecond) _, err := shortClient.Complete(context.Background(), []Message{{Role: "user", Content: "hi"}}) if err == nil { t.Error("expected timeout error with 50ms timeout and 200ms server delay") } } func TestComplete_Anthropic_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/messages" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.Header.Get("x-api-key") != "test-key" { t.Errorf("expected x-api-key header, got %q", r.Header.Get("x-api-key")) } if r.Header.Get("anthropic-version") != "2023-06-01" { t.Errorf("expected anthropic-version header, got %q", r.Header.Get("anthropic-version")) } var req map[string]interface{} json.NewDecoder(r.Body).Decode(&req) if req["system"] != "You are helpful" { t.Errorf("expected system prompt, got %v", req["system"]) } msgs := req["messages"].([]interface{}) if len(msgs) != 1 { t.Errorf("expected 1 user message, got %d", len(msgs)) } if req["max_tokens"] != float64(8192) { t.Errorf("expected max_tokens 8192, got %v", req["max_tokens"]) } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"content":[{"type":"text","text":"Hello from Claude!"}]}`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "claude-sonnet").WithProvider(ProviderAnthropic) got, err := client.Complete(context.Background(), []Message{ {Role: "system", Content: "You are helpful"}, {Role: "user", Content: "Hi"}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "Hello from Claude!" { t.Errorf("expected %q, got %q", "Hello from Claude!", got) } } func TestComplete_Anthropic_NoContent(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"content":[]}`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "claude-sonnet").WithProvider(ProviderAnthropic) _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for empty content, got nil") } } func TestComplete_Anthropic_APIError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":{"message":"invalid request"}}`)) })) defer server.Close() client := NewClient(server.URL, "test-key", "claude-sonnet").WithProvider(ProviderAnthropic) _, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}}) if err == nil { t.Fatal("expected error for 400, got nil") } } func TestWithProvider(t *testing.T) { client := NewClient("http://example.com", "key", "model") if client.provider != ProviderOpenAI { t.Errorf("expected default provider openai, got %s", client.provider) } result := client.WithProvider(ProviderAnthropic) if result != client { t.Error("WithProvider should return the same client for chaining") } if client.provider != ProviderAnthropic { t.Errorf("expected provider anthropic, got %s", client.provider) } }