feat: add context.Context + unexport client fields
REVIEW.md findings 1-4, 14: - All Gitea client methods now accept context.Context as first param - All LLM client methods now accept context.Context as first param - Use http.NewRequestWithContext for cancellation/timeout support - Main uses 3-minute timeout context for all operations - Unexport Client struct fields (baseURL, token, apiKey, etc.) - Use bytes.NewReader instead of strings.NewReader(string(...))
This commit is contained in:
+18
-18
@@ -2,6 +2,7 @@ package llm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,26 +12,26 @@ import (
|
||||
|
||||
// Client calls an OpenAI-compatible chat completion API.
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
Temperature float64
|
||||
HTTP *http.Client
|
||||
baseURL string
|
||||
apiKey string
|
||||
model string
|
||||
temperature float64
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new LLM client.
|
||||
func NewClient(baseURL, apiKey, model string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
Model: model,
|
||||
HTTP: &http.Client{},
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
apiKey: apiKey,
|
||||
model: model,
|
||||
http: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithTemperature sets the temperature for LLM requests (0 = omit, uses server default).
|
||||
func (c *Client) WithTemperature(t float64) *Client {
|
||||
c.Temperature = t
|
||||
c.temperature = t
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -57,12 +58,11 @@ type ChatResponse struct {
|
||||
}
|
||||
|
||||
// Complete sends a chat completion request and returns the assistant's response content.
|
||||
func (c *Client) Complete(messages []Message) (string, error) {
|
||||
func (c *Client) Complete(ctx context.Context, messages []Message) (string, error) {
|
||||
reqBody := ChatRequest{
|
||||
Model: c.Model,
|
||||
Temperature: c.Temperature,
|
||||
Model: c.model,
|
||||
Temperature: c.temperature,
|
||||
Messages: messages,
|
||||
|
||||
}
|
||||
|
||||
data, err := json.Marshal(reqBody)
|
||||
@@ -70,15 +70,15 @@ func (c *Client) Complete(messages []Message) (string, error) {
|
||||
return "", fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
url := c.BaseURL + "/chat/completions"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||
url := c.baseURL + "/chat/completions"
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LLM request: %w", err)
|
||||
}
|
||||
|
||||
+12
-11
@@ -1,6 +1,7 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -51,7 +52,7 @@ func TestComplete_Success(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-key", "gpt-4")
|
||||
got, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
got, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -68,7 +69,7 @@ func TestComplete_APIError(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-key", "gpt-4")
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 429, got nil")
|
||||
}
|
||||
@@ -82,7 +83,7 @@ func TestComplete_NoChoices(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-key", "gpt-4")
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for no choices, got nil")
|
||||
}
|
||||
@@ -95,7 +96,7 @@ func TestComplete_BadJSON(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-key", "gpt-4")
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for bad JSON, got nil")
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func TestComplete_BadJSON(t *testing.T) {
|
||||
|
||||
func TestComplete_ServerDown(t *testing.T) {
|
||||
client := NewClient("http://127.0.0.1:1", "test-key", "gpt-4")
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for connection refused, got nil")
|
||||
}
|
||||
@@ -111,16 +112,16 @@ func TestComplete_ServerDown(t *testing.T) {
|
||||
|
||||
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)
|
||||
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)
|
||||
if client.temperature != 0.7 {
|
||||
t.Errorf("expected temperature 0.7, got %f", client.temperature)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +148,7 @@ func TestComplete_TemperatureOmittedWhenZero(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "key", "model")
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -180,7 +181,7 @@ func TestComplete_TemperatureIncludedWhenSet(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "key", "model").WithTemperature(0.7)
|
||||
_, err := client.Complete([]Message{{Role: "user", Content: "Hi"}})
|
||||
_, err := client.Complete(context.Background(), []Message{{Role: "user", Content: "Hi"}})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user