client.go 6.65 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
)

21
22
23
24
const DefaultHost = "127.0.0.1:11434"

var envHost = os.Getenv("OLLAMA_HOST")

Patrick Devine's avatar
Patrick Devine committed
25
type Client struct {
Michael Yang's avatar
Michael Yang committed
26
27
	base *url.URL
	http http.Client
Michael Yang's avatar
Michael Yang committed
28
29
}

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

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

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

	return apiError
Michael Yang's avatar
Michael Yang committed
44
45
}

Michael Yang's avatar
Michael Yang committed
46
func ClientFromEnvironment() (*Client, error) {
Michael Yang's avatar
Michael Yang committed
47
48
	defaultPort := "11434"

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

Michael Yang's avatar
Michael Yang committed
59
60
61
	// trim trailing slashes
	hostport = strings.TrimRight(hostport, "/")

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

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

Michael Yang's avatar
Michael Yang committed
79
80
81
	mockRequest, err := http.NewRequest("HEAD", client.base.String(), nil)
	if err != nil {
		return nil, err
82
83
	}

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

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

Michael Yang's avatar
Michael Yang committed
95
	return &client, nil
Patrick Devine's avatar
Patrick Devine committed
96
97
98
99
100
101
102
103
104
105
106
107
108
109
}

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
110
	requestURL := c.base.JoinPath(path)
Michael Yang's avatar
Michael Yang committed
111
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody)
Patrick Devine's avatar
Patrick Devine committed
112
113
114
115
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
116
117
118
	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
119

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

Michael Yang's avatar
Michael Yang committed
143
const maxBufferSize = 512 * format.KiloByte
144

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

153
		buf = bytes.NewBuffer(bts)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
154
155
	}

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

Michael Yang's avatar
Michael Yang committed
162
	request.Header.Set("Content-Type", "application/json")
163
	request.Header.Set("Accept", "application/x-ndjson")
Michael Yang's avatar
Michael Yang committed
164
	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
165

Michael Yang's avatar
Michael Yang committed
166
	response, err := c.http.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
167
168
169
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
170
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
171

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

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

Michael Yang's avatar
Michael Yang committed
186
		if errorResponse.Error != "" {
187
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
188
189
		}

Michael Yang's avatar
Michael Yang committed
190
		if response.StatusCode >= http.StatusBadRequest {
Michael Yang's avatar
Michael Yang committed
191
			return StatusError{
192
193
194
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
195
			}
196
197
		}

Michael Yang's avatar
Michael Yang committed
198
		if err := fn(bts); err != nil {
199
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
200
201
202
		}
	}

Michael Yang's avatar
Michael Yang committed
203
204
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
205

Michael Yang's avatar
Michael Yang committed
206
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
207

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

219
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
220
221

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

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
230
}
231

232
type PushProgressFunc func(ProgressResponse) error
233
234
235

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

		return fn(resp)
	})
}

245
type CreateProgressFunc func(ProgressResponse) error
246
247
248

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

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
257
258
259
260
261
262
263
264

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

Patrick Devine's avatar
Patrick Devine committed
266
267
268
269
270
271
272
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
}

273
274
275
276
277
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
278
}
279

Patrick Devine's avatar
Patrick Devine committed
280
281
282
283
284
285
286
287
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
}

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