client.go 7.99 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"
Michael Yang's avatar
Michael Yang committed
8
	"errors"
9
	"fmt"
Patrick Devine's avatar
Patrick Devine committed
10
	"io"
Michael Yang's avatar
Michael Yang committed
11
	"net"
Jeffrey Morgan's avatar
Jeffrey Morgan committed
12
	"net/http"
Michael Yang's avatar
Michael Yang committed
13
	"net/url"
14
	"os"
Michael Yang's avatar
Michael Yang committed
15
	"runtime"
16
	"strings"
Michael Yang's avatar
Michael Yang committed
17

Michael Yang's avatar
Michael Yang committed
18
	"github.com/jmorganca/ollama/format"
Michael Yang's avatar
Michael Yang committed
19
	"github.com/jmorganca/ollama/version"
20
21
)

Patrick Devine's avatar
Patrick Devine committed
22
type Client struct {
Michael Yang's avatar
Michael Yang committed
23
24
	base *url.URL
	http http.Client
Michael Yang's avatar
Michael Yang committed
25
26
}

Patrick Devine's avatar
Patrick Devine committed
27
func checkError(resp *http.Response, body []byte) error {
Michael Yang's avatar
Michael Yang committed
28
	if resp.StatusCode < http.StatusBadRequest {
Patrick Devine's avatar
Patrick Devine committed
29
		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
}

Michael Yang's avatar
Michael Yang committed
43
func ClientFromEnvironment() (*Client, error) {
Michael Yang's avatar
Michael Yang committed
44
45
	defaultPort := "11434"

Michael Yang's avatar
Michael Yang committed
46
	scheme, hostport, ok := strings.Cut(os.Getenv("OLLAMA_HOST"), "://")
Michael Yang's avatar
Michael Yang committed
47
48
	switch {
	case !ok:
Michael Yang's avatar
Michael Yang committed
49
		scheme, hostport = "http", os.Getenv("OLLAMA_HOST")
Michael Yang's avatar
Michael Yang committed
50
51
52
53
	case scheme == "http":
		defaultPort = "80"
	case scheme == "https":
		defaultPort = "443"
Michael Yang's avatar
Michael Yang committed
54
55
	}

Michael Yang's avatar
Michael Yang committed
56
57
58
	// trim trailing slashes
	hostport = strings.TrimRight(hostport, "/")

Michael Yang's avatar
Michael Yang committed
59
60
	host, port, err := net.SplitHostPort(hostport)
	if err != nil {
Michael Yang's avatar
Michael Yang committed
61
		host, port = "127.0.0.1", defaultPort
Michael Yang's avatar
Michael Yang committed
62
		if ip := net.ParseIP(strings.Trim(hostport, "[]")); ip != nil {
Michael Yang's avatar
Michael Yang committed
63
			host = ip.String()
Michael Yang's avatar
Michael Yang committed
64
65
		} else if hostport != "" {
			host = hostport
Michael Yang's avatar
Michael Yang committed
66
67
68
69
70
71
72
73
		}
	}

	client := Client{
		base: &url.URL{
			Scheme: scheme,
			Host:   net.JoinHostPort(host, port),
		},
74
75
	}

Michael Yang's avatar
Michael Yang committed
76
	mockRequest, err := http.NewRequest(http.MethodHead, client.base.String(), nil)
Michael Yang's avatar
Michael Yang committed
77
78
	if err != nil {
		return nil, err
79
80
	}

Michael Yang's avatar
Michael Yang committed
81
	proxyURL, err := http.ProxyFromEnvironment(mockRequest)
82
	if err != nil {
Michael Yang's avatar
Michael Yang committed
83
		return nil, err
Michael Yang's avatar
Michael Yang committed
84
85
	}

Michael Yang's avatar
Michael Yang committed
86
87
88
89
	client.http = http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
		},
Patrick Devine's avatar
Patrick Devine committed
90
	}
91

Michael Yang's avatar
Michael Yang committed
92
	return &client, nil
Patrick Devine's avatar
Patrick Devine committed
93
94
95
96
97
98
}

func (c *Client) do(ctx context.Context, method, path string, reqData, respData any) error {
	var reqBody io.Reader
	var data []byte
	var err error
Michael Yang's avatar
Michael Yang committed
99
100
101
102
103
104
105
106

	switch reqData := reqData.(type) {
	case io.Reader:
		// reqData is already an io.Reader
		reqBody = reqData
	case nil:
		// noop
	default:
Patrick Devine's avatar
Patrick Devine committed
107
108
109
110
		data, err = json.Marshal(reqData)
		if err != nil {
			return err
		}
Michael Yang's avatar
Michael Yang committed
111

Patrick Devine's avatar
Patrick Devine committed
112
113
114
		reqBody = bytes.NewReader(data)
	}

Michael Yang's avatar
Michael Yang committed
115
	requestURL := c.base.JoinPath(path)
Michael Yang's avatar
Michael Yang committed
116
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody)
Patrick Devine's avatar
Patrick Devine committed
117
118
119
120
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
121
122
123
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json")
	request.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
Patrick Devine's avatar
Patrick Devine committed
124

Michael Yang's avatar
Michael Yang committed
125
	respObj, err := c.http.Do(request)
Patrick Devine's avatar
Patrick Devine committed
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
	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
146
147
}

Michael Yang's avatar
Michael Yang committed
148
const maxBufferSize = 512 * format.KiloByte
149

Michael Yang's avatar
Michael Yang committed
150
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
151
152
153
154
155
156
	var buf *bytes.Buffer
	if data != nil {
		bts, err := json.Marshal(data)
		if err != nil {
			return err
		}
Michael Yang's avatar
Michael Yang committed
157

158
		buf = bytes.NewBuffer(bts)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
159
160
	}

Michael Yang's avatar
Michael Yang committed
161
	requestURL := c.base.JoinPath(path)
Michael Yang's avatar
Michael Yang committed
162
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), buf)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
163
164
165
166
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
167
	request.Header.Set("Content-Type", "application/json")
168
	request.Header.Set("Accept", "application/x-ndjson")
Michael Yang's avatar
Michael Yang committed
169
	request.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
Jeffrey Morgan's avatar
Jeffrey Morgan committed
170

Michael Yang's avatar
Michael Yang committed
171
	response, err := c.http.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
172
173
174
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
175
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
176

177
	scanner := bufio.NewScanner(response.Body)
178
179
180
	// increase the buffer size to avoid running out of space
	scanBuf := make([]byte, 0, maxBufferSize)
	scanner.Buffer(scanBuf, maxBufferSize)
181
182
	for scanner.Scan() {
		var errorResponse struct {
Michael Yang's avatar
Michael Yang committed
183
			Error string `json:"error,omitempty"`
184
185
186
187
188
189
190
		}

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

Michael Yang's avatar
Michael Yang committed
191
		if errorResponse.Error != "" {
192
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
193
194
		}

Michael Yang's avatar
Michael Yang committed
195
		if response.StatusCode >= http.StatusBadRequest {
Michael Yang's avatar
Michael Yang committed
196
			return StatusError{
197
198
199
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
200
			}
201
202
		}

Michael Yang's avatar
Michael Yang committed
203
		if err := fn(bts); err != nil {
204
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
205
206
207
		}
	}

Michael Yang's avatar
Michael Yang committed
208
209
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
210

Michael Yang's avatar
Michael Yang committed
211
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
212

Michael Yang's avatar
Michael Yang committed
213
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
214
215
216
217
218
219
220
221
	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
222
}
Bruce MacDonald's avatar
Bruce MacDonald committed
223

Bruce MacDonald's avatar
Bruce MacDonald committed
224
225
226
227
228
229
230
231
232
233
234
235
236
type ChatResponseFunc func(ChatResponse) error

func (c *Client) Chat(ctx context.Context, req *ChatRequest, fn ChatResponseFunc) error {
	return c.stream(ctx, http.MethodPost, "/api/chat", req, func(bts []byte) error {
		var resp ChatResponse
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

237
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
238
239

func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
240
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
241
		var resp ProgressResponse
242
243
244
245
246
247
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
248
}
249

250
type PushProgressFunc func(ProgressResponse) error
251
252
253

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 {
254
		var resp ProgressResponse
255
256
257
258
259
260
261
262
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

263
type CreateProgressFunc func(ProgressResponse) error
264
265
266

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 {
267
		var resp ProgressResponse
268
269
270
271
272
273
274
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
275
276
277
278
279
280
281
282

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
}
283

Patrick Devine's avatar
Patrick Devine committed
284
285
286
287
288
289
290
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
}

291
292
293
294
295
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
296
}
297

Patrick Devine's avatar
Patrick Devine committed
298
299
300
301
302
303
304
305
func (c *Client) Show(ctx context.Context, req *ShowRequest) (*ShowResponse, error) {
	var resp ShowResponse
	if err := c.do(ctx, http.MethodPost, "/api/show", req, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

306
func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
307
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
308
309
310
311
		return err
	}
	return nil
}
Brian Murray's avatar
Brian Murray committed
312
313
314
315
316
317
318
func (c *Client) Embeddings(ctx context.Context, req *EmbeddingRequest) (*EmbeddingResponse, error) {
	var resp EmbeddingResponse
	if err := c.do(ctx, http.MethodPost, "/api/embeddings", req, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}
Michael Yang's avatar
Michael Yang committed
319

Michael Yang's avatar
Michael Yang committed
320
321
func (c *Client) CreateBlob(ctx context.Context, digest string, r io.Reader) error {
	if err := c.do(ctx, http.MethodHead, fmt.Sprintf("/api/blobs/%s", digest), nil, nil); err != nil {
Michael Yang's avatar
Michael Yang committed
322
323
		var statusError StatusError
		if !errors.As(err, &statusError) || statusError.StatusCode != http.StatusNotFound {
Michael Yang's avatar
Michael Yang committed
324
			return err
Michael Yang's avatar
Michael Yang committed
325
326
		}

Michael Yang's avatar
Michael Yang committed
327
328
		if err := c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, nil); err != nil {
			return err
Michael Yang's avatar
Michael Yang committed
329
330
331
		}
	}

Michael Yang's avatar
Michael Yang committed
332
	return nil
Michael Yang's avatar
Michael Yang committed
333
}
Michael Yang's avatar
Michael Yang committed
334
335
336
337
338
339
340
341
342
343
344
345

func (c *Client) Version(ctx context.Context) (string, error) {
	var version struct {
		Version string `json:"version"`
	}

	if err := c.do(ctx, http.MethodGet, "/api/version", nil, &version); err != nil {
		return "", err
	}

	return version.Version, nil
}