aboutsummaryrefslogtreecommitdiff
path: root/web/openai/chatgpt/chatgpt.go
blob: 2fb044f5834a2a5c5cba29c1c91b90b14c331f6b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// Package chatgpt is a simple binding to the ChatGPT API.
package chatgpt

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"within.website/x/web"
)

type Request struct {
	Model       string     `json:"model"`
	Messages    []Message  `json:"messages"`
	Functions   []Function `json:"functions,omitempty"`
	Seed        *int       `json:"seed,omitempty"`
	Temperature *float64   `json:"temperature,omitempty"`
}

type Function struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Parameters  Param  `json:"parameters"`
}

type Param struct {
	Type        string     `json:"type"`
	Description string     `json:"description,omitempty"`
	Enum        []string   `json:"enum,omitempty"`
	Properties  Properties `json:"properties"`
	Required    []string   `json:"required,omitempty"`
}

type Properties map[string]Param

func (p Properties) MarshalJSON() ([]byte, error) {
	if len(p) == 0 {
		return []byte("{}"), nil
	}

	return json.Marshal(map[string]Param(p))
}

type Message struct {
	Role         string   `json:"role"`
	Content      string   `json:"content"`
	FunctionCall *Funcall `json:"function_call,omitempty"`
}

type Funcall struct {
	Name      string `json:"name"`
	Arguments string `json:"arguments"`
}

func (m Message) ProxyFormat() string {
	return fmt.Sprintf("%s\\ %s", strings.Title(m.Role), m.Content)
}

type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

type Choice struct {
	Message      Message `json:"message"`
	FinishReason string  `json:"finish_reason"`
	Index        int     `json:"index"`
}

type Response struct {
	ID      string   `json:"id"`
	Object  string   `json:"object"`
	Created int      `json:"created"`
	Model   string   `json:"model"`
	Usage   Usage    `json:"usage"`
	Choices []Choice `json:"choices"`
}

type Client struct {
	httpCli *http.Client
	apiKey  string
	baseURL string
}

func (c Client) WithBaseURL(baseURL string) Client {
	c.baseURL = baseURL
	return c
}

func NewClient(apiKey string) Client {
	return Client{
		httpCli: &http.Client{},
		apiKey:  apiKey,
		baseURL: "https://api.openai.com",
	}
}

func (c Client) Complete(ctx context.Context, r Request) (*Response, error) {
	if r.Model == "" {
		r.Model = "gpt-3.5-turbo"
	}

	data, err := json.Marshal(r)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/chat/completions", bytes.NewBuffer(data))
	if err != nil {
		return nil, fmt.Errorf("chatgpt: [unexpected] can't make request???: %w", err)
	}

	req.Header.Add("Authorization", "Bearer "+c.apiKey)
	req.Header.Add("Content-Type", "application/json")

	resp, err := c.httpCli.Do(req)
	if err != nil {
		return nil, fmt.Errorf("chatgpt: can't reach API: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, web.NewError(http.StatusOK, resp)
	}
	defer resp.Body.Close()

	var result Response
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("chatgpt: can't decode result: %w", err)
	}

	return &result, nil
}