client.go 6.17 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
	"os"
Michael Yang's avatar
Michael Yang committed
13
	"runtime"
14
	"strings"
Michael Yang's avatar
Michael Yang committed
15
16

	"github.com/jmorganca/ollama/version"
17
18
)

19
const DefaultHost = "127.0.0.1:11434"
20
21
22

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

Patrick Devine's avatar
Patrick Devine committed
25
type Client struct {
26
	Base    url.URL
Patrick Devine's avatar
Patrick Devine committed
27
28
	HTTP    http.Client
	Headers http.Header
Michael Yang's avatar
Michael Yang committed
29
30
}

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

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

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

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

47
48
49
50
51
52
53
54
55
56
57
58
59
// 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) {
60
61
62
	h := Host()
	if !strings.HasPrefix(h, "http://") && !strings.HasPrefix(h, "https://") {
		h = "http://" + h
63
64
	}

65
66
67
	u, err := url.Parse(h)
	if err != nil {
		return nil, fmt.Errorf("could not parse host: %w", err)
Michael Yang's avatar
Michael Yang committed
68
69
	}

70
71
	if u.Port() == "" {
		u.Host += ":11434"
Patrick Devine's avatar
Patrick Devine committed
72
	}
73
74

	return &Client{Base: *u, HTTP: http.Client{}}, nil
Patrick Devine's avatar
Patrick Devine committed
75
76
77
78
79
80
81
82
83
84
85
86
87
88
}

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
89
90
	requestURL := c.Base.JoinPath(path)
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody)
Patrick Devine's avatar
Patrick Devine committed
91
92
93
94
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
95
96
97
	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
98
99

	for k, v := range c.Headers {
Michael Yang's avatar
Michael Yang committed
100
		request.Header[k] = v
Michael Yang's avatar
Michael Yang committed
101
	}
Patrick Devine's avatar
Patrick Devine committed
102

Michael Yang's avatar
Michael Yang committed
103
	respObj, err := c.HTTP.Do(request)
Patrick Devine's avatar
Patrick Devine committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
	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
124
125
}

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

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

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

Michael Yang's avatar
Michael Yang committed
143
144
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json")
Michael Yang's avatar
Michael Yang committed
145
	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
146

Michael Yang's avatar
Michael Yang committed
147
	response, err := http.DefaultClient.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
148
149
150
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
151
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
152

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

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

Michael Yang's avatar
Michael Yang committed
164
		if errorResponse.Error != "" {
165
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
166
167
		}

Michael Yang's avatar
Michael Yang committed
168
		if response.StatusCode >= http.StatusBadRequest {
Michael Yang's avatar
Michael Yang committed
169
			return StatusError{
170
171
172
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
173
			}
174
175
		}

Michael Yang's avatar
Michael Yang committed
176
		if err := fn(bts); err != nil {
177
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
178
179
180
		}
	}

Michael Yang's avatar
Michael Yang committed
181
182
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
183

Michael Yang's avatar
Michael Yang committed
184
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
185

Michael Yang's avatar
Michael Yang committed
186
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
187
188
189
190
191
192
193
194
	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
195
}
Bruce MacDonald's avatar
Bruce MacDonald committed
196

197
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
198
199

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

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
208
}
209

210
type PushProgressFunc func(ProgressResponse) error
211
212
213

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 {
214
		var resp ProgressResponse
215
216
217
218
219
220
221
222
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

223
type CreateProgressFunc func(ProgressResponse) error
224
225
226

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 {
227
		var resp ProgressResponse
228
229
230
231
232
233
234
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
235
236
237
238
239
240
241
242

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

Patrick Devine's avatar
Patrick Devine committed
244
245
246
247
248
249
250
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
}

251
252
253
254
255
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
256
}
257

Patrick Devine's avatar
Patrick Devine committed
258
259
260
261
262
263
264
265
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
}

266
func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
267
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
268
269
270
271
		return err
	}
	return nil
}