client.go 6.57 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"
Michael Yang's avatar
Michael Yang committed
10
	"net"
Jeffrey Morgan's avatar
Jeffrey Morgan committed
11
	"net/http"
Michael Yang's avatar
Michael Yang committed
12
	"net/url"
13
	"os"
Michael Yang's avatar
Michael Yang committed
14
	"runtime"
15
	"strings"
Michael Yang's avatar
Michael Yang committed
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Michael Yang's avatar
Michael Yang committed
91
	return &client, nil
Patrick Devine's avatar
Patrick Devine committed
92
93
94
95
96
97
98
99
100
101
102
103
104
105
}

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

Michael Yang's avatar
Michael Yang committed
106
	requestURL := c.base.JoinPath(path)
Michael Yang's avatar
Michael Yang committed
107
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody)
Patrick Devine's avatar
Patrick Devine committed
108
109
110
111
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
112
113
114
	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
115

Michael Yang's avatar
Michael Yang committed
116
	respObj, err := c.http.Do(request)
Patrick Devine's avatar
Patrick Devine committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
	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
137
138
}

Michael Yang's avatar
Michael Yang committed
139
const maxBufferSize = 512 * format.KiloByte
140

Michael Yang's avatar
Michael Yang committed
141
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
142
143
144
145
146
147
	var buf *bytes.Buffer
	if data != nil {
		bts, err := json.Marshal(data)
		if err != nil {
			return err
		}
Michael Yang's avatar
Michael Yang committed
148

149
		buf = bytes.NewBuffer(bts)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
150
151
	}

Michael Yang's avatar
Michael Yang committed
152
	requestURL := c.base.JoinPath(path)
Michael Yang's avatar
Michael Yang committed
153
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), buf)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
154
155
156
157
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
158
	request.Header.Set("Content-Type", "application/json")
159
	request.Header.Set("Accept", "application/x-ndjson")
Michael Yang's avatar
Michael Yang committed
160
	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
161

Michael Yang's avatar
Michael Yang committed
162
	response, err := c.http.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
163
164
165
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
166
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
167

168
	scanner := bufio.NewScanner(response.Body)
169
170
171
	// increase the buffer size to avoid running out of space
	scanBuf := make([]byte, 0, maxBufferSize)
	scanner.Buffer(scanBuf, maxBufferSize)
172
173
	for scanner.Scan() {
		var errorResponse struct {
Michael Yang's avatar
Michael Yang committed
174
			Error string `json:"error,omitempty"`
175
176
177
178
179
180
181
		}

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

Michael Yang's avatar
Michael Yang committed
182
		if errorResponse.Error != "" {
183
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
184
185
		}

Michael Yang's avatar
Michael Yang committed
186
		if response.StatusCode >= http.StatusBadRequest {
Michael Yang's avatar
Michael Yang committed
187
			return StatusError{
188
189
190
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
191
			}
192
193
		}

Michael Yang's avatar
Michael Yang committed
194
		if err := fn(bts); err != nil {
195
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
196
197
198
		}
	}

Michael Yang's avatar
Michael Yang committed
199
200
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
201

Michael Yang's avatar
Michael Yang committed
202
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
203

Michael Yang's avatar
Michael Yang committed
204
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
205
206
207
208
209
210
211
212
	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
213
}
Bruce MacDonald's avatar
Bruce MacDonald committed
214

215
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
216
217

func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
218
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
219
		var resp ProgressResponse
220
221
222
223
224
225
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
226
}
227

228
type PushProgressFunc func(ProgressResponse) error
229
230
231

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 {
232
		var resp ProgressResponse
233
234
235
236
237
238
239
240
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

241
type CreateProgressFunc func(ProgressResponse) error
242
243
244

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

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
253
254
255
256
257
258
259
260

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

Patrick Devine's avatar
Patrick Devine committed
262
263
264
265
266
267
268
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
}

269
270
271
272
273
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
274
}
275

Patrick Devine's avatar
Patrick Devine committed
276
277
278
279
280
281
282
283
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
}

284
func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
285
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
286
287
288
289
		return err
	}
	return nil
}