client.go 5.56 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"
13
	"strings"
14
15
)

16
const DefaultHost = "localhost:11434"
17
18
19

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

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

Patrick Devine's avatar
Patrick Devine committed
28
29
30
func checkError(resp *http.Response, body []byte) error {
	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
		return nil
Michael Yang's avatar
Michael Yang committed
31
32
	}

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

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

	return apiError
Michael Yang's avatar
Michael Yang committed
42
43
}

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

62
63
64
	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
65
66
	}

67
68
	if u.Port() == "" {
		u.Host += ":11434"
Patrick Devine's avatar
Patrick Devine committed
69
	}
70
71

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

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

86
	url := c.Base.JoinPath(path).String()
Patrick Devine's avatar
Patrick Devine committed
87
88
89
90
91
92
93
94
95
96
97

	req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	for k, v := range c.Headers {
		req.Header[k] = v
Michael Yang's avatar
Michael Yang committed
98
	}
Patrick Devine's avatar
Patrick Devine committed
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

	respObj, err := c.HTTP.Do(req)
	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
121
122
}

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

131
		buf = bytes.NewBuffer(bts)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
132
133
	}

134
	request, err := http.NewRequestWithContext(ctx, method, c.Base.JoinPath(path).String(), buf)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
135
136
137
138
	if err != nil {
		return err
	}

Michael Yang's avatar
Michael Yang committed
139
140
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json")
Jeffrey Morgan's avatar
Jeffrey Morgan committed
141

Michael Yang's avatar
Michael Yang committed
142
	response, err := http.DefaultClient.Do(request)
Jeffrey Morgan's avatar
Jeffrey Morgan committed
143
144
145
	if err != nil {
		return err
	}
Michael Yang's avatar
Michael Yang committed
146
	defer response.Body.Close()
Jeffrey Morgan's avatar
Jeffrey Morgan committed
147

148
149
150
	scanner := bufio.NewScanner(response.Body)
	for scanner.Scan() {
		var errorResponse struct {
Michael Yang's avatar
Michael Yang committed
151
			Error string `json:"error,omitempty"`
152
153
154
155
156
157
158
		}

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

Michael Yang's avatar
Michael Yang committed
159
		if errorResponse.Error != "" {
160
			return fmt.Errorf(errorResponse.Error)
Michael Yang's avatar
Michael Yang committed
161
162
		}

Michael Yang's avatar
Michael Yang committed
163
164
		if response.StatusCode >= 400 {
			return StatusError{
165
166
167
				StatusCode:   response.StatusCode,
				Status:       response.Status,
				ErrorMessage: errorResponse.Error,
Michael Yang's avatar
Michael Yang committed
168
			}
169
170
		}

Michael Yang's avatar
Michael Yang committed
171
		if err := fn(bts); err != nil {
172
			return err
Jeffrey Morgan's avatar
Jeffrey Morgan committed
173
174
175
		}
	}

Michael Yang's avatar
Michael Yang committed
176
177
	return nil
}
Jeffrey Morgan's avatar
Jeffrey Morgan committed
178

Michael Yang's avatar
Michael Yang committed
179
type GenerateResponseFunc func(GenerateResponse) error
Jeffrey Morgan's avatar
Jeffrey Morgan committed
180

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

192
type PullProgressFunc func(ProgressResponse) error
Michael Yang's avatar
Michael Yang committed
193
194

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

		return fn(resp)
	})
Bruce MacDonald's avatar
Bruce MacDonald committed
203
}
204

205
type PushProgressFunc func(ProgressResponse) error
206
207
208

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 {
209
		var resp ProgressResponse
210
211
212
213
214
215
216
217
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}

218
type CreateProgressFunc func(ProgressResponse) error
219
220
221

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 {
222
		var resp ProgressResponse
223
224
225
226
227
228
229
		if err := json.Unmarshal(bts, &resp); err != nil {
			return err
		}

		return fn(resp)
	})
}
Patrick Devine's avatar
Patrick Devine committed
230
231
232
233
234
235
236
237

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

Patrick Devine's avatar
Patrick Devine committed
239
240
241
242
243
244
245
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
}

246
247
248
249
250
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
251
}
252
253

func (c *Client) Heartbeat(ctx context.Context) error {
Bruce MacDonald's avatar
Bruce MacDonald committed
254
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
255
256
257
258
		return err
	}
	return nil
}