Skip to content

Commit 3a721be

Browse files
authored
Merge pull request #11 from mutablelogic/v1
Added streaming outputs to client
2 parents 61f10a1 + 4111a59 commit 3a721be

29 files changed

+800
-112
lines changed

README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ This repository contains a generic HTTP client which can be adapted to provide:
77
* Ability to send files and data of type `multipart/form-data`
88
* Ability to send data of type `application/x-www-form-urlencoded`
99
* Debugging capabilities to see the request and response data
10+
* Streaming JSON responses
1011

1112
Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/client
1213

13-
There are also some example API clients:
14+
There are also some example clients which use this library:
1415

15-
* [Bitwarden Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
16-
* [Elevenlabs Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
17-
* [Home Assistant Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
16+
* [Bitwarden API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden)
17+
* [Elevenlabs API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs)
18+
* [Home Assistant API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant)
1819
* [IPify Client](https://github.com/mutablelogic/go-client/tree/main/pkg/ipify)
1920
* [Mistral API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/mistral)
2021
* [NewsAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/newsapi)
21-
* [OpenAI client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)
22+
* [Ollama API client](https://github.com/mutablelogic/go-client/tree/main/pkg/ollama)
23+
* [OpenAI API client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai)
2224

2325
## Basic Usage
2426

@@ -29,7 +31,7 @@ to a JSON endpoint:
2931
package main
3032

3133
import (
32-
client "github.com/mutablelogic/go-client"
34+
client "github.com/mutablelogic/go-client/pkg/client"
3335
)
3436

3537
func main() {
@@ -69,9 +71,8 @@ Various options can be passed to the client `New` method to control its behaviou
6971
The first argument to the `Do` method is the payload to send to the server, when set. You can create a payload
7072
using the following methods:
7173

72-
* `client.NewRequest(accept string)` returns a new empty payload which defaults to GET. The accept parameter is the
73-
accepted mime-type of the response.
74-
* `client.NewJSONRequest(payload any, accept string)` returns a new request with a JSON payload which defaults to GET.
74+
* `client.NewRequest()` returns a new empty payload which defaults to GET.
75+
* `client.NewJSONRequest(payload any, accept string)` returns a new request with a JSON payload which defaults to POST.
7576
* `client.NewMultipartRequest(payload any, accept string)` returns a new request with a Multipart Form data payload which
7677
defaults to POST.
7778
* `client.NewFormRequest(payload any, accept string)` returns a new request with a Form data payload which defaults to POST.
@@ -82,7 +83,7 @@ For example,
8283
package main
8384

8485
import (
85-
client "github.com/mutablelogic/go-client"
86+
client "github.com/mutablelogic/go-client/pkg/client"
8687
)
8788

8889
func main() {
@@ -97,7 +98,7 @@ func main() {
9798
Reply string `json:"reply"`
9899
}
99100
request.Prompt = "Hello, world!"
100-
payload := client.NewJSONRequest(request, "application/json")
101+
payload := client.NewJSONRequest(request)
101102
if err := c.Do(payload, &response, OptPath("test")); err != nil {
102103
// Handle error
103104
}
@@ -145,7 +146,9 @@ Various options can be passed to modify each individual request when using the `
145146
* `OptToken(value Token)` adds an authorization header (overrides the client OptReqToken option)
146147
* `OptQuery(value url.Values)` sets the query parameters to a request
147148
* `OptHeader(key, value string)` appends a custom header to the request
148-
149+
* `OptResponse(func() error)` allows you to set a callback function to process a streaming response.
150+
See below for more details.
151+
* `OptNoTimeout()` disables the timeout on the request, which is useful for long running requests
149152

150153
## Authentication
151154

@@ -155,7 +158,7 @@ The authentication token can be set as follows:
155158
package main
156159

157160
import (
158-
client "github.com/mutablelogic/go-client"
161+
client "github.com/mutablelogic/go-client/pkg/client"
159162
)
160163

161164
func main() {
@@ -182,3 +185,9 @@ You can create a payload with form data:
182185
* `client.NewMultipartRequest(payload any, accept string)` returns a new request with a Multipart Form data payload which defaults to POST. This is useful for file uploads.
183186

184187
The payload should be a `struct` where the fields are converted to form tuples. File uploads require a field of type `multipart.File`.
188+
189+
## Streaming Responses
190+
191+
If the returned content is a stream of JSON responses, then you can use the `OptResponse(fn func() error)` option, which
192+
will be called by the `Do` method for each response. The function should return an error if the stream should be terminated.
193+
Usually, you would pair this option with `OptNoTimeout` to prevent the request from timing out.

cmd/cli/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
func main() {
1515
name := path.Base(os.Args[0])
16-
flags, err := NewFlags(name, os.Args[1:], OpenAIFlags, MistralFlags, ElevenlabsFlags, HomeAssistantFlags, NewsAPIFlags)
16+
flags, err := NewFlags(name, os.Args[1:], OpenAIFlags, MistralFlags, ElevenlabsFlags, HomeAssistantFlags, NewsAPIFlags, OllamaFlags)
1717
if err != nil {
1818
if err != flag.ErrHelp {
1919
fmt.Fprintln(os.Stderr, err)
@@ -71,6 +71,12 @@ func main() {
7171
os.Exit(1)
7272
}
7373

74+
cmd, err = OllamaRegister(cmd, opts, flags)
75+
if err != nil {
76+
fmt.Fprintln(os.Stderr, err)
77+
os.Exit(1)
78+
}
79+
7480
// Run command
7581
if err := Run(cmd, flags); err != nil {
7682
if errors.Is(err, flag.ErrHelp) {

cmd/cli/ollama.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
// Package imports
5+
"github.com/mutablelogic/go-client/pkg/client"
6+
"github.com/mutablelogic/go-client/pkg/ollama"
7+
)
8+
9+
/////////////////////////////////////////////////////////////////////
10+
// REGISTER FUNCTIONS
11+
12+
func OllamaFlags(flags *Flags) {
13+
flags.String("ollama-endpoint", "${OLLAMA_ENDPOINT}", "Ollama endpoint url")
14+
}
15+
16+
func OllamaRegister(cmd []Client, opts []client.ClientOpt, flags *Flags) ([]Client, error) {
17+
ollama, err := ollama.New(flags.GetString("ollama-endpoint"), opts...)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
// Register commands
23+
cmd = append(cmd, Client{
24+
ns: "ollama",
25+
cmd: []Command{
26+
{Name: "models", Description: "List local models", MinArgs: 2, MaxArgs: 2, Fn: ollamaListModels(ollama, flags)},
27+
},
28+
})
29+
30+
// Return success
31+
return cmd, nil
32+
}
33+
34+
/////////////////////////////////////////////////////////////////////
35+
// API CALL FUNCTIONS
36+
37+
func ollamaListModels(client *ollama.Client, flags *Flags) CommandFn {
38+
return func() error {
39+
if models, err := client.ListModels(); err != nil {
40+
return err
41+
} else {
42+
return flags.Write(models)
43+
}
44+
}
45+
}

etc/test/IMG_20130413_095348.JPG

136 KB
Loading
File renamed without changes.

pkg/client/client.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"encoding/xml"
7+
"errors"
78
"fmt"
89
"io"
910
"mime"
@@ -43,7 +44,6 @@ type Client struct {
4344
}
4445

4546
type ClientOpt func(*Client) error
46-
type RequestOpt func(*http.Request) error
4747

4848
///////////////////////////////////////////////////////////////////////////////
4949
// GLOBALS
@@ -54,6 +54,7 @@ const (
5454
PathSeparator = string(os.PathSeparator)
5555
ContentTypeAny = "*/*"
5656
ContentTypeJson = "application/json"
57+
ContentTypeJsonStream = "application/x-ndjson"
5758
ContentTypeTextXml = "text/xml"
5859
ContentTypeApplicationXml = "application/xml"
5960
ContentTypeTextPlain = "text/plain"
@@ -124,12 +125,12 @@ func (client *Client) DoWithContext(ctx context.Context, in Payload, out any, op
124125
now := time.Now()
125126
if !client.ts.IsZero() && client.rate > 0.0 {
126127
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
127-
if next.After(now) {
128+
if next.After(now) { // TODO allow ctx to cancel the sleep
128129
time.Sleep(next.Sub(now))
129130
}
130131
}
131132

132-
// Set timestamp at return
133+
// Set timestamp at return, for rate limiting
133134
defer func(now time.Time) {
134135
client.ts = now
135136
}(now)
@@ -164,7 +165,7 @@ func (client *Client) Request(req *http.Request, out any, opts ...RequestOpt) er
164165
now := time.Now()
165166
if !client.ts.IsZero() && client.rate > 0.0 {
166167
next := client.ts.Add(time.Duration(float32(time.Second) / client.rate))
167-
if next.After(now) {
168+
if next.After(now) { // TODO allow ctx to cancel the sleep
168169
time.Sleep(next.Sub(now))
169170
}
170171
}
@@ -235,12 +236,23 @@ func (client *Client) request(ctx context.Context, method, accept, mimetype stri
235236
// Do will make a JSON request, populate an object with the response and return any errors
236237
func do(client *http.Client, req *http.Request, accept string, strict bool, out any, opts ...RequestOpt) error {
237238
// Apply request options
239+
reqopts := requestOpts{
240+
Request: req,
241+
}
238242
for _, opt := range opts {
239-
if err := opt(req); err != nil {
243+
if err := opt(&reqopts); err != nil {
240244
return err
241245
}
242246
}
243247

248+
// NoTimeout
249+
if reqopts.noTimeout {
250+
defer func(v time.Duration) {
251+
client.Timeout = v
252+
}(client.Timeout)
253+
client.Timeout = 0
254+
}
255+
244256
// Do the request
245257
response, err := client.Do(req)
246258
if err != nil {
@@ -276,16 +288,31 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
276288
return nil
277289
}
278290

279-
// Decode the body
291+
// Decode the body - and call any callback once the body has been decoded
280292
switch mimetype {
281-
case ContentTypeJson:
282-
if err := json.NewDecoder(response.Body).Decode(out); err != nil {
283-
return err
293+
case ContentTypeJson, ContentTypeJsonStream:
294+
dec := json.NewDecoder(response.Body)
295+
for {
296+
if err := dec.Decode(out); errors.Is(err, io.EOF) {
297+
break
298+
} else if err != nil {
299+
return err
300+
}
301+
if reqopts.callback != nil {
302+
if err := reqopts.callback(); err != nil {
303+
return err
304+
}
305+
}
284306
}
285307
case ContentTypeTextXml, ContentTypeApplicationXml:
286308
if err := xml.NewDecoder(response.Body).Decode(out); err != nil {
287309
return err
288310
}
311+
if reqopts.callback != nil {
312+
if err := reqopts.callback(); err != nil {
313+
return err
314+
}
315+
}
289316
default:
290317
if v, ok := out.(Unmarshaler); ok {
291318
return v.Unmarshal(mimetype, response.Body)
@@ -296,6 +323,11 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
296323
} else {
297324
return ErrInternalAppError.Withf("do: response does not implement Unmarshaler for %q", mimetype)
298325
}
326+
if reqopts.callback != nil {
327+
if err := reqopts.callback(); err != nil {
328+
return err
329+
}
330+
}
299331
}
300332

301333
// Return success

pkg/client/doc.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
/*
2-
client impleemts a generic REST API client which can be used for creating
3-
gateway-specific clients
2+
Implements a generic REST API client which can be used for creating
3+
gateway-specific clients. Basic usage:
4+
5+
package main
6+
7+
import (
8+
client "github.com/mutablelogic/go-client/pkg/client"
9+
)
10+
11+
func main() {
12+
// Create a new client
13+
c := client.New(client.OptEndpoint("https://api.example.com/api/v1"))
14+
15+
// Send a GET request, populating a struct with the response
16+
var response struct {
17+
Message string `json:"message"`
18+
}
19+
if err := c.Do(nil, &response, OptPath("test")); err != nil {
20+
// Handle error
21+
}
22+
23+
// Print the response
24+
fmt.Println(response.Message)
25+
}
426
*/
527
package client

0 commit comments

Comments
 (0)