package openai import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" ) const defaultTimeout = 30 * time.Second // Client wraps HTTP access to the OpenAI-compatible Chat Completions API. type Client struct { apiKey string baseURL string httpClient *http.Client } // ClientOption customizes client construction. type ClientOption func(*Client) // WithHTTPClient overrides the default HTTP client. func WithHTTPClient(hc *http.Client) ClientOption { return func(c *Client) { c.httpClient = hc } } // WithBaseURL overrides the default base URL. func WithBaseURL(url string) ClientOption { return func(c *Client) { c.baseURL = url } } // NewClient creates a Client with the provided API key and options. func NewClient(apiKey string, opts ...ClientOption) (*Client, error) { apiKey = strings.TrimSpace(apiKey) if apiKey == "" { return nil, errors.New("api key cannot be empty") } client := &Client{ apiKey: apiKey, baseURL: "https://api.openai.com/v1", httpClient: &http.Client{ Timeout: defaultTimeout, }, } for _, opt := range opts { opt(client) } return client, nil } // CreateChatCompletion issues a chat completion request. func (c *Client) CreateChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) { if c == nil { return nil, errors.New("client is nil") } payload, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("encode request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/chat/completions", bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("execute request: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { return decodeSuccess(resp.Body) } return nil, decodeError(resp.Body, resp.StatusCode) } func decodeSuccess(r io.Reader) (*ChatCompletionResponse, error) { var response ChatCompletionResponse if err := json.NewDecoder(r).Decode(&response); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return &response, nil } func decodeError(r io.Reader, status int) error { var apiErr ErrorResponse if err := json.NewDecoder(r).Decode(&apiErr); err != nil { return fmt.Errorf("api error (status %d): failed to decode body: %w", status, err) } return fmt.Errorf("api error (status %d): %s", status, apiErr.Error.Message) }