client.go 7.23 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

224
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
225
226

func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
227
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
228
		var resp ProgressResponse
229
230
231
232
233
234
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
235
}
236

237
type PushProgressFunc func(ProgressResponse) error
238
239
240

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 {
241
		var resp ProgressResponse
242
243
244
245
246
247
248
249
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

250
type CreateProgressFunc func(ProgressResponse) error
251
252
253

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

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
262
263
264
265
266
267
268
269

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

Patrick Devine's avatar
Patrick Devine committed
271
272
273
274
275
276
277
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
}

278
279
280
281
282
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
283
}
284

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

293
func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
294
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
295
296
297
298
		return err
	}
	return nil
}
Michael Yang's avatar
Michael Yang committed
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314

func (c *Client) CreateBlob(ctx context.Context, digest string, r io.Reader) (string, error) {
	var response CreateBlobResponse
	if err := c.do(ctx, http.MethodGet, fmt.Sprintf("/api/blobs/%s/path", digest), nil, &response); err != nil {
		var statusError StatusError
		if !errors.As(err, &statusError) || statusError.StatusCode != http.StatusNotFound {
			return "", err
		}

		if err := c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, &response); err != nil {
			return "", err
		}
	}

	return response.Path, nil
}