package lem import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // Message is a single chat message. type Message struct { Role string `json:"role"` Content string `json:"content"` } // ChatRequest is the request body for /v1/chat/completions. type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Temperature float64 `json:"temperature"` MaxTokens int `json:"max_tokens,omitempty"` } // Choice is a single completion choice. type Choice struct { Message Message `json:"message"` } // ChatResponse is the response from /v1/chat/completions. type ChatResponse struct { Choices []Choice `json:"choices"` } // retryableError marks errors that should be retried. type retryableError struct { err error } func (e *retryableError) Error() string { return e.err.Error() } func (e *retryableError) Unwrap() error { return e.err } // Client talks to an OpenAI-compatible API. type Client struct { baseURL string model string MaxTokens int httpClient *http.Client } // NewClient creates a Client for the given base URL and model. func NewClient(baseURL, model string) *Client { return &Client{ baseURL: baseURL, model: model, httpClient: &http.Client{ Timeout: 300 * time.Second, }, } } // Chat sends a prompt and returns the assistant's reply. // Uses the default temperature of 0.1. func (c *Client) Chat(prompt string) (string, error) { return c.ChatWithTemp(prompt, 0.1) } // ChatWithTemp sends a prompt with a specific temperature and returns // the assistant's reply. Retries up to 3 times with exponential backoff // on transient failures (HTTP 5xx or network errors). func (c *Client) ChatWithTemp(prompt string, temp float64) (string, error) { req := ChatRequest{ Model: c.model, Messages: []Message{ {Role: "user", Content: prompt}, }, Temperature: temp, MaxTokens: c.MaxTokens, } body, err := json.Marshal(req) if err != nil { return "", fmt.Errorf("marshal request: %w", err) } maxAttempts := 3 var lastErr error for attempt := range maxAttempts { if attempt > 0 { // Exponential backoff: 100ms, 200ms backoff := time.Duration(100<= 500 { return "", &retryableError{fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBody))} } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody)) } var chatResp ChatResponse if err := json.Unmarshal(respBody, &chatResp); err != nil { return "", fmt.Errorf("unmarshal response: %w", err) } if len(chatResp.Choices) == 0 { return "", fmt.Errorf("no choices in response") } return chatResp.Choices[0].Message.Content, nil }