"vscode:/vscode.git/clone" did not exist on "bf78ed6ee94e593a7edae2e277a736379cbc2413"
client.go 5.53 KB
Newer Older
Jeffrey Morgan's avatar
Jeffrey Morgan committed
1
2
3
package api

import (
Jeffrey Morgan's avatar
Jeffrey Morgan committed
4
	"bufio"
Jeffrey Morgan's avatar
Jeffrey Morgan committed
5
6
7
	"bytes"
	"context"
	"encoding/json"
8
	"fmt"
Patrick Devine's avatar
Patrick Devine committed
9
	"io"
Jeffrey Morgan's avatar
Jeffrey Morgan committed
10
	"net/http"
Michael Yang's avatar
Michael Yang committed
11
	"net/url"
12
13
14
15
16
17
18
	"os"
)

const DefaultHost = "localhost:11434"

var (
	envHost = os.Getenv("OLLAMA_HOST")
Jeffrey Morgan's avatar
Jeffrey Morgan committed
19
20
)

Patrick Devine's avatar
Patrick Devine committed
21
type Client struct {
22
	Base    url.URL
Patrick Devine's avatar
Patrick Devine committed
23
24
	HTTP    http.Client
	Headers http.Header
Michael Yang's avatar
Michael Yang committed
25
26
}

Patrick Devine's avatar
Patrick Devine committed
27
28
29
func checkError(resp *http.Response, body []byte) error {
	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
		return nil
Michael Yang's avatar
Michael Yang committed
30
31
	}

Patrick Devine's avatar
Patrick Devine committed
32
	apiError := StatusError{StatusCode: resp.StatusCode}
Michael Yang's avatar
Michael Yang committed
33

Patrick Devine's avatar
Patrick Devine committed
34
35
36
	err := json.Unmarshal(body, &apiError)
	if err != nil {
		// Use the full body as the message if we fail to decode a response.
37
		apiError.ErrorMessage = string(body)
Patrick Devine's avatar
Patrick Devine committed
38
39
40
	}

	return apiError
Michael Yang's avatar
Michael Yang committed
41
42
}

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Host returns the default host to use for the client. It is determined in the following order:
// 1. The OLLAMA_HOST environment variable
// 2. The default host (localhost:11434)
func Host() string {
	if envHost != "" {
		return envHost
	}
	return DefaultHost
}

// FromEnv creates a new client using Host() as the host. An error is returns
// if the host is invalid.
func FromEnv() (*Client, error) {
	u, err := url.Parse(Host())
	if err != nil {
		return nil, err
	}
	return &Client{Base: *u}, nil
}

Michael Yang's avatar
Michael Yang committed
63
func NewClient(hosts ...string) *Client {
64
	host := DefaultHost
Michael Yang's avatar
Michael Yang committed
65
66
67
68
69
	if len(hosts) > 0 {
		host = hosts[0]
	}

	return &Client{
70
		Base: url.URL{Scheme: "http", Host: host},
Patrick Devine's avatar
Patrick Devine committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
		HTTP: http.Client{},
	}
}

func (c *Client) do(ctx context.Context, method, path string, reqData, respData any) error {
	var reqBody io.Reader
	var data []byte
	var err error
	if reqData != nil {
		data, err = json.Marshal(reqData)
		if err != nil {
			return err
		}
		reqBody = bytes.NewReader(data)
	}

87
	url := c.Base.JoinPath(path).String()
Patrick Devine's avatar
Patrick Devine committed
88
89
90
91
92
93
94
95
96
97
98

	req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	for k, v := range c.Headers {
		req.Header[k] = v
Michael Yang's avatar
Michael Yang committed
99
	}
Patrick Devine's avatar
Patrick Devine committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

	respObj, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer respObj.Body.Close()

	respBody, err := io.ReadAll(respObj.Body)
	if err != nil {
		return err
	}

	if err := checkError(respObj, respBody); err != nil {
		return err
	}

	if len(respBody) > 0 && respData != nil {
		if err := json.Unmarshal(respBody, respData); err != nil {
			return err
		}
	}
	return nil
Jeffrey Morgan's avatar
Jeffrey Morgan committed
122
123
}

Michael Yang's avatar
Michael Yang committed
124
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
125
126
127
128
129
130
	var buf *bytes.Buffer
	if data != nil {
		bts, err := json.Marshal(data)
		if err != nil {
			return err
		}
Michael Yang's avatar
Michael Yang committed
131

132
		buf = bytes.NewBuffer(bts)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
133
134
	}

135
	request, err := http.NewRequestWithContext(ctx, method, c.Base.JoinPath(path).String(), buf)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
136
137
138
139
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
140
141
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json")
Jeffrey Morgan's avatar
Jeffrey Morgan committed
142

Michael Yang's avatar
Michael Yang committed
143
	response, err := http.DefaultClient.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
144
145
146
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
147
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
148

149
150
151
	scanner := bufio.NewScanner(response.Body)
	for scanner.Scan() {
		var errorResponse struct {
Michael Yang's avatar
Michael Yang committed
152
			Error string `json:"error,omitempty"`
153
154
155
156
157
158
159
		}

		bts := scanner.Bytes()
		if err := json.Unmarshal(bts, &errorResponse); err != nil {
			return fmt.Errorf("unmarshal: %w", err)
		}

Michael Yang's avatar
Michael Yang committed
160
		if errorResponse.Error != "" {
161
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
162
163
		}

Michael Yang's avatar
Michael Yang committed
164
165
		if response.StatusCode >= 400 {
			return StatusError{
166
167
168
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
169
			}
170
171
		}

Michael Yang's avatar
Michael Yang committed
172
		if err := fn(bts); err != nil {
173
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
174
175
176
		}
	}

Michael Yang's avatar
Michael Yang committed
177
178
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
179

Michael Yang's avatar
Michael Yang committed
180
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
181

Michael Yang's avatar
Michael Yang committed
182
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
183
184
185
186
187
188
189
190
	return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
		var resp GenerateResponse
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
Jeffrey Morgan's avatar
Jeffrey Morgan committed
191
}
Bruce MacDonald's avatar
Bruce MacDonald committed
192

193
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
194
195

func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
196
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
197
		var resp ProgressResponse
198
199
200
201
202
203
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
204
}
205

206
type PushProgressFunc func(ProgressResponse) error
207
208
209

func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc) error {
	return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error {
210
		var resp ProgressResponse
211
212
213
214
215
216
217
218
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

219
type CreateProgressFunc func(ProgressResponse) error
220
221
222

func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error {
	return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error {
223
		var resp ProgressResponse
224
225
226
227
228
229
230
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
231
232
233
234
235
236
237
238

func (c *Client) List(ctx context.Context) (*ListResponse, error) {
	var lr ListResponse
	if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil {
		return nil, err
	}
	return &lr, nil
}
239

Patrick Devine's avatar
Patrick Devine committed
240
241
242
243
244
245
246
func (c *Client) Copy(ctx context.Context, req *CopyRequest) error {
	if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil {
		return err
	}
	return nil
}

247
248
249
250
251
func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
	if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil {
		return err
	}
	return nil
252
}
253
254

func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
255
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
256
257
258
259
		return err
	}
	return nil
}