x

Programs, configuration and documentation that don't fit anywhere else
Log | Files | Refs | README | LICENSE

openai.go (3942B)


      1 package openai
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/json"
      6 	"fmt"
      7 	"net/http"
      8 	"strings"
      9 	"time"
     10 )
     11 
     12 type Role string
     13 
     14 const (
     15 	RoleSystem    = "system"
     16 	RoleUser      = "user"
     17 	RoleAssistant = "assistant"
     18 )
     19 
     20 type Message struct {
     21 	Role       string     `json:"role"`
     22 	Content    string     `json:"content"`
     23 	Reasoning  string     `json:"reasoning,omitempty"`
     24 	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
     25 	ToolCallID string     `json:"tool_call_id,omitempty"`
     26 }
     27 
     28 type Function struct {
     29 	Name        string          `json:"name"`
     30 	Description string          `json:"description"`
     31 	Parameters  json.RawMessage `json:"parameters"` // JSON Schema for tool arguments
     32 }
     33 
     34 type Tool struct {
     35 	Type     string   `json:"type"`
     36 	Function Function `json:"function"`
     37 }
     38 
     39 type ToolCall struct {
     40 	ID       string `json:"id"`
     41 	Type     string `json:"type"` // "function"
     42 	Function struct {
     43 		Name      string `json:"name"`
     44 		Arguments string `json:"arguments"` // JSON string; parse per tool
     45 	} `json:"function"`
     46 }
     47 
     48 type Chat struct {
     49 	Messages  []Message `json:"messages"`
     50 	Model     string    `json:"model"`
     51 	Reasoning string    `json:"reasoning_effort,omitempty"`
     52 }
     53 
     54 type Model struct {
     55 	ID               string
     56 	Name             string
     57 	Description      string
     58 	Aliases          []string
     59 	MaxContextLength int
     60 	Deprecation      time.Time
     61 }
     62 
     63 type Client struct {
     64 	*http.Client
     65 	Token   string
     66 	BaseURL string
     67 }
     68 
     69 type apiError struct {
     70 	Message struct {
     71 		Detail []struct {
     72 			Msg string
     73 		}
     74 	}
     75 	Type string
     76 }
     77 
     78 func (e apiError) Error() string {
     79 	messages := make([]string, len(e.Message.Detail))
     80 	for i := range e.Message.Detail {
     81 		messages[i] = e.Message.Detail[i].Msg
     82 	}
     83 	return fmt.Sprintf("%s: %s", e.Type, strings.Join(messages, ", "))
     84 }
     85 
     86 func (c *Client) do(req *http.Request) (*http.Response, error) {
     87 	if c.Client == nil {
     88 		c.Client = http.DefaultClient
     89 	}
     90 	if c.Token != "" {
     91 		req.Header.Set("Authorization", "Bearer "+c.Token)
     92 	}
     93 	// req.Header.Set("Accept", "application/json")
     94 	// assume we're sending JSON if not already set since most requests mandate it
     95 	if req.Body != nil && req.Header.Get("Content-Type") == "" {
     96 		req.Header.Set("Content-Type", "application/json")
     97 	}
     98 	return c.Do(req)
     99 }
    100 
    101 type completeResponse struct {
    102 	Choices []struct {
    103 		Message Message
    104 	}
    105 }
    106 
    107 func (c *Client) Complete(chat *Chat) (*Message, error) {
    108 	b, err := json.Marshal(chat)
    109 	if err != nil {
    110 		return nil, fmt.Errorf("encode messages: %w", err)
    111 	}
    112 	u := c.BaseURL + "/chat/completions"
    113 	req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
    114 	if err != nil {
    115 		return nil, err
    116 	}
    117 	resp, err := c.do(req)
    118 	if err != nil {
    119 		return nil, err
    120 	}
    121 	defer resp.Body.Close()
    122 	if resp.StatusCode == http.StatusUnauthorized {
    123 		return nil, fmt.Errorf("unauthorised")
    124 	} else if resp.StatusCode >= 400 && resp.StatusCode <= 499 {
    125 		var aerr apiError
    126 		if err := json.NewDecoder(resp.Body).Decode(&aerr); err != nil {
    127 			return nil, fmt.Errorf(resp.Status)
    128 		}
    129 		return nil, aerr
    130 	} else if resp.StatusCode >= 500 {
    131 		return nil, fmt.Errorf(resp.Status)
    132 	}
    133 
    134 	var cresp completeResponse
    135 	if err := json.NewDecoder(resp.Body).Decode(&cresp); err != nil {
    136 		return nil, fmt.Errorf("decode response: %w", err)
    137 	}
    138 	if len(cresp.Choices) == 0 {
    139 		return nil, fmt.Errorf("no completions in response")
    140 	}
    141 	return &cresp.Choices[0].Message, nil
    142 }
    143 
    144 func (c *Client) Models() ([]Model, error) {
    145 	u := c.BaseURL + "/models"
    146 	req, err := http.NewRequest(http.MethodGet, u, nil)
    147 	if err != nil {
    148 		return nil, err
    149 	}
    150 	resp, err := c.do(req)
    151 	if err != nil {
    152 		return nil, err
    153 	}
    154 	defer resp.Body.Close()
    155 	if resp.StatusCode != http.StatusOK {
    156 		var aerr apiError
    157 		if err := json.NewDecoder(resp.Body).Decode(&aerr); err != nil {
    158 			return nil, fmt.Errorf(resp.Status)
    159 		}
    160 		return nil, aerr
    161 	}
    162 	v := struct {
    163 		Data []Model
    164 	}{}
    165 	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
    166 		return nil, fmt.Errorf("decode response: %w", err)
    167 	}
    168 	return v.Data, nil
    169 }