// Package openai is a minimal client for the OpenAI Responses API (function calling) and the Embeddings API. // Shapes follow https://developers.openai.com/api/docs/guides/function-calling and /guides/embeddings (checked 2026-10-08). package openai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) type Client struct { BaseURL string // default https://api.openai.com/v1 KeyFile string // read per call so a rotated key file is picked up without restart HTTP *http.Client keyFn func() (string, error) } func New(baseURL string, keyFn func() (string, error)) *Client { if baseURL == "" { baseURL = "https://api.openai.com/v1" } return &Client{BaseURL: baseURL, keyFn: keyFn, HTTP: &http.Client{Timeout: 120 * time.Second}} } // Tool is a function tool definition (strict mode: additionalProperties false and every property required). type Tool struct { Type string `json:"type"` Name string `json:"name"` Description string `json:"description"` Parameters map[string]any `json:"parameters"` Strict bool `json:"strict"` } // Item is one input/output item of the Responses API. Only the fields this agent uses are modelled. type Item struct { Type string `json:"type"` // message | function_call | function_call_output Role string `json:"role,omitempty"` // message: system | user | assistant Content any `json:"content,omitempty"` // message: string (input) or []ContentPart (output) CallID string `json:"call_id,omitempty"` // function_call / function_call_output Name string `json:"name,omitempty"` // function_call Arguments string `json:"arguments,omitempty"` // function_call (JSON string) Output string `json:"output,omitempty"` // function_call_output ID string `json:"id,omitempty"` Status string `json:"status,omitempty"` } type ContentPart struct { Type string `json:"type"` Text string `json:"text"` } type Request struct { Model string `json:"model"` Input []Item `json:"input"` Tools []Tool `json:"tools,omitempty"` ToolChoice any `json:"tool_choice,omitempty"` // "auto" | "required" | {"type":"function","name":...} PreviousResponseID string `json:"previous_response_id,omitempty"` MaxOutputTokens int `json:"max_output_tokens,omitempty"` ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` Store *bool `json:"store,omitempty"` } type Response struct { ID string `json:"id"` Status string `json:"status"` Model string `json:"model"` Output []Item `json:"output"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` Error *struct { Message string `json:"message"` Type string `json:"type"` } `json:"error"` } // FunctionCalls returns the function_call items of the output. func (r *Response) FunctionCalls() []Item { var out []Item for _, it := range r.Output { if it.Type == "function_call" { out = append(out, it) } } return out } // Text concatenates assistant message text parts. func (r *Response) Text() string { var b bytes.Buffer for _, it := range r.Output { if it.Type != "message" { continue } if parts, ok := it.Content.([]any); ok { for _, p := range parts { if m, ok := p.(map[string]any); ok { if t, ok := m["text"].(string); ok { b.WriteString(t) } } } } } return b.String() } func (c *Client) do(ctx context.Context, path string, body any, out any) error { key, err := c.keyFn() if err != nil { return fmt.Errorf("openai key: %w", err) } if key == "" { return fmt.Errorf("openai key is empty (secret not registered yet)") } b, _ := json.Marshal(body) req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(b)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") resp, err := c.HTTP.Do(req) if err != nil { return err } defer resp.Body.Close() rb, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) if resp.StatusCode/100 != 2 { return fmt.Errorf("openai %s: status %d: %s", path, resp.StatusCode, truncate(string(rb), 600)) } return json.Unmarshal(rb, out) } func (c *Client) CreateResponse(ctx context.Context, req Request) (*Response, error) { var r Response if err := c.do(ctx, "/responses", req, &r); err != nil { return nil, err } if r.Error != nil { return nil, fmt.Errorf("openai response error: %s", r.Error.Message) } return &r, nil } type embedRequest struct { Model string `json:"model"` Input []string `json:"input"` } type embedResponse struct { Data []struct { Index int `json:"index"` Embedding []float32 `json:"embedding"` } `json:"data"` } // Embed returns one vector per input (text-embedding-3-small: 1536 dimensions by default). func (c *Client) Embed(ctx context.Context, model string, inputs []string) ([][]float32, error) { var r embedResponse if err := c.do(ctx, "/embeddings", embedRequest{Model: model, Input: inputs}, &r); err != nil { return nil, err } out := make([][]float32, len(inputs)) for _, d := range r.Data { if d.Index >= 0 && d.Index < len(out) { out[d.Index] = d.Embedding } } return out, nil } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }