x

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

openai.go (3825B)


      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 	if req.Body != nil {
     95 		req.Header.Set("Content-Type", "application/json")
     96 	}
     97 	return c.Do(req)
     98 }
     99 
    100 type completeResponse struct {
    101 	Choices []struct {
    102 		Message Message
    103 	}
    104 }
    105 
    106 func (c *Client) Complete(chat *Chat) (*Message, error) {
    107 	b, err := json.Marshal(chat)
    108 	if err != nil {
    109 		return nil, fmt.Errorf("encode messages: %w", err)
    110 	}
    111 	u := c.BaseURL + "/v1/chat/completions"
    112 	req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
    113 	if err != nil {
    114 		return nil, err
    115 	}
    116 	resp, err := c.do(req)
    117 	if err != nil {
    118 		return nil, err
    119 	}
    120 	defer resp.Body.Close()
    121 	if resp.StatusCode == http.StatusUnauthorized {
    122 		return nil, fmt.Errorf("unauthorised")
    123 	} else if resp.StatusCode >= 400 && resp.StatusCode <= 499 {
    124 		var aerr apiError
    125 		if err := json.NewDecoder(resp.Body).Decode(&aerr); err != nil {
    126 			return nil, fmt.Errorf(resp.Status)
    127 		}
    128 		return nil, aerr
    129 	} else if resp.StatusCode >= 500 {
    130 		return nil, fmt.Errorf(resp.Status)
    131 	}
    132 
    133 	var cresp completeResponse
    134 	if err := json.NewDecoder(resp.Body).Decode(&cresp); err != nil {
    135 		return nil, fmt.Errorf("decode response: %w", err)
    136 	}
    137 	if len(cresp.Choices) == 0 {
    138 		return nil, fmt.Errorf("no completions in response")
    139 	}
    140 	return &cresp.Choices[0].Message, nil
    141 }
    142 
    143 func (c *Client) Models() ([]Model, error) {
    144 	u := c.BaseURL + "/v1/models"
    145 	req, err := http.NewRequest(http.MethodGet, u, nil)
    146 	if err != nil {
    147 		return nil, err
    148 	}
    149 	resp, err := c.do(req)
    150 	if err != nil {
    151 		return nil, err
    152 	}
    153 	defer resp.Body.Close()
    154 	if resp.StatusCode != http.StatusOK {
    155 		var aerr apiError
    156 		if err := json.NewDecoder(resp.Body).Decode(&aerr); err != nil {
    157 			return nil, fmt.Errorf(resp.Status)
    158 		}
    159 		return nil, aerr
    160 	}
    161 	v := struct {
    162 		Data []Model
    163 	}{}
    164 	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
    165 		return nil, fmt.Errorf("decode response: %w", err)
    166 	}
    167 	return v.Data, nil
    168 }