1
package api
1
package api
2
2
3
// This file is for Golang clients of Klex.
3
// This file is for Golang clients of Klex.
4
4
5
import (
5
import (
6
"bytes"
6
"bytes"
7
"encoding/json"
7
"encoding/json"
8
"fmt"
8
"fmt"
9
"io/ioutil"
9
"io/ioutil"
10
"log"
10
"log"
11
"net/http"
11
"net/http"
12
"sort"
12
"sort"
13
)
13
)
14
14
15
type Client struct {
15
type Client struct {
16
KlexURL string
16
KlexURL string
17
APIKey string
17
APIKey string
18
}
18
}
19
19
20
func NewClient(klexURL, apiKey string) *Client {
20
func NewClient(klexURL, apiKey string) *Client {
21
if klexURL == "" || apiKey == "" {
21
if klexURL == "" || apiKey == "" {
22
log.Printf("NewClient: missing klexURL or apiKey")
22
log.Printf("NewClient: missing klexURL or apiKey")
23
return nil
23
return nil
24
}
24
}
25
return &Client{klexURL, apiKey}
25
return &Client{klexURL, apiKey}
26
}
26
}
27
27
28
func (c *Client) call(method, path string, req, res interface{}) error {
28
func (c *Client) call(method, path string, req, res interface{}) error {
29
reqBody, err := json.Marshal(req)
29
reqBody, err := json.Marshal(req)
30
if err != nil {
30
if err != nil {
31
return fmt.Errorf("Cannot marshal request: %v", err)
31
return fmt.Errorf("Cannot marshal request: %v", err)
32
}
32
}
33
reqBytes := bytes.NewBuffer(reqBody)
33
reqBytes := bytes.NewBuffer(reqBody)
34
r, err := http.NewRequest(method, c.KlexURL + path, reqBytes)
34
r, err := http.NewRequest(method, c.KlexURL + path, reqBytes)
35
if err != nil {
35
if err != nil {
36
return fmt.Errorf("In http.NewRequest: %v", err)
36
return fmt.Errorf("In http.NewRequest: %v", err)
37
}
37
}
38
r.Header.Set("Authorization", "Bearer " + c.APIKey)
38
r.Header.Set("Authorization", "Bearer " + c.APIKey)
39
r.Header.Set("Content-Type", "application/json")
39
r.Header.Set("Content-Type", "application/json")
40
resHttp, err := http.DefaultClient.Do(r)
40
resHttp, err := http.DefaultClient.Do(r)
41
if err != nil {
41
if err != nil {
42
return fmt.Errorf("http.DefaultClient.Do: %v", err)
42
return fmt.Errorf("http.DefaultClient.Do: %v", err)
43
}
43
}
44
defer resHttp.Body.Close()
44
defer resHttp.Body.Close()
45
resBody, err := ioutil.ReadAll(resHttp.Body)
45
resBody, err := ioutil.ReadAll(resHttp.Body)
46
if err != nil {
46
if err != nil {
47
return fmt.Errorf("Response error: %v", err)
47
return fmt.Errorf("Response error: %v", err)
48
}
48
}
49
if resHttp.StatusCode != 200 && resHttp.StatusCode != 204 {
49
if resHttp.StatusCode != 200 && resHttp.StatusCode != 204 {
50
return fmt.Errorf("Status %d; response=%s", resHttp.StatusCode, resBody)
50
return fmt.Errorf("Status %d; response=%s", resHttp.StatusCode, resBody)
51
}
51
}
52
if res != nil {
52
if res != nil {
53
if err := json.Unmarshal(resBody, res); err != nil {
53
if err := json.Unmarshal(resBody, res); err != nil {
54
return fmt.Errorf("Bad response %s\nerror=%v", resBody, err)
54
return fmt.Errorf("Bad response %s\nerror=%v", resBody, err)
55
}
55
}
56
}
56
}
57
return nil
57
return nil
58
}
58
}
59
59
60
// ListFuncs fetches the list of registered functions from Klex.
61
// `versions` is the value of the `versions` query parameter
62
// ("latest" is the most common; pass "" to omit the parameter
63
// and get the full history).
64
func (c *Client) ListFuncs(versions string) (*ListFuncsResponse, error) {
65
path := "/funcs/list"
66
if versions != "" {
67
path += "?versions=" + versions
68
}
69
var res ListFuncsResponse
70
if err := c.call("GET", path, nil, &res); err != nil {
71
return nil, err
72
}
73
return &res, nil
74
}
75
60
// F executes a function on one given input.
76
// F executes a function on one given input.
61
func (c *Client) F(f, in string) (string, error) {
77
func (c *Client) F(f, in string) (string, error) {
62
var res FResponse
78
var res FResponse
63
err := c.call("POST", "/f", FRequest{FName: f, In: in}, &res)
79
err := c.call("POST", "/f", FRequest{FName: f, In: in}, &res)
64
if err != nil {
80
if err != nil {
65
return "", err
81
return "", err
66
}
82
}
67
if res.Err != "" {
83
if res.Err != "" {
68
return "", fmt.Errorf(res.Err)
84
return "", fmt.Errorf(res.Err)
69
}
85
}
70
return res.Out, nil
86
return res.Out, nil
71
}
87
}
72
88
73
// Messages executes an LLM function using the Messages API.
89
// Messages executes an LLM function using the Messages API.
74
// Set req.Model to one of the Klex LLM function names.
90
// Set req.Model to one of the Klex LLM function names.
75
func (c *Client) Messages(req MessagesRequest) (*MessagesResponse, error) {
91
func (c *Client) Messages(req MessagesRequest) (*MessagesResponse, error) {
76
f := req.Model
92
f := req.Model
77
req.Model = ""
93
req.Model = ""
78
if f == "" {
94
if f == "" {
79
return nil, fmt.Errorf("MessagesRequest.Model is empty")
95
return nil, fmt.Errorf("MessagesRequest.Model is empty")
80
}
96
}
81
in, err := json.Marshal(req)
97
in, err := json.Marshal(req)
82
if err != nil {
98
if err != nil {
83
return nil, fmt.Errorf("Cannot marshal request: %v", err)
99
return nil, fmt.Errorf("Cannot marshal request: %v", err)
84
}
100
}
85
out, err := c.F(f, string(in))
101
out, err := c.F(f, string(in))
86
if err != nil {
102
if err != nil {
87
return nil, err
103
return nil, err
88
}
104
}
89
var res MessagesResponse
105
var res MessagesResponse
90
err = json.Unmarshal([]byte(out), &res)
106
err = json.Unmarshal([]byte(out), &res)
91
if err != nil {
107
if err != nil {
92
// Instead of failing, treat the whole output as text, and add an error.
108
// Instead of failing, treat the whole output as text, and add an error.
93
// Let the caller figure this out.
109
// Let the caller figure this out.
94
res.Error = &ErrorResponse{
110
res.Error = &ErrorResponse{
95
Type: "response-json",
111
Type: "response-json",
96
Message: err.Error(),
112
Message: err.Error(),
97
}
113
}
98
res.Content = []ContentBlock{{Type: "text", Text: out}}
114
res.Content = []ContentBlock{{Type: "text", Text: out}}
99
}
115
}
100
return &res, nil
116
return &res, nil
101
}
117
}
102
118
103
// Embed returns semantic embedding vectors for the given text.
119
// Embed returns semantic embedding vectors for the given text.
104
func (c *Client) Embed(req EmbedRequest) ([][]float32, error) {
120
func (c *Client) Embed(req EmbedRequest) ([][]float32, error) {
105
var resp [][]float32
121
var resp [][]float32
106
err := c.call("POST", "/embed/do", req, &resp)
122
err := c.call("POST", "/embed/do", req, &resp)
107
return resp, err
123
return resp, err
108
}
124
}
109
125
110
// NewDataset creates a new dataset or updates an existing one.
126
// NewDataset creates a new dataset or updates an existing one.
111
// This is the simplest way, meant for datasets smaller than ~1GB.
127
// This is the simplest way, meant for datasets smaller than ~1GB.
112
func (c *Client) NewDataset(name string, data map[string]string) error {
128
func (c *Client) NewDataset(name string, data map[string]string) error {
113
// TODO: this loses key names; get rid of this API.
129
// TODO: this loses key names; get rid of this API.
114
req := NewDatasetRequest{Name: name, Data: nil}
130
req := NewDatasetRequest{Name: name, Data: nil}
115
keys := make([]string, 0, len(data))
131
keys := make([]string, 0, len(data))
116
for k := range data {
132
for k := range data {
117
keys = append(keys, k)
133
keys = append(keys, k)
118
}
134
}
119
sort.Strings(keys)
135
sort.Strings(keys)
120
for _, k := range keys {
136
for _, k := range keys {
121
req.Data = append(req.Data, data[k])
137
req.Data = append(req.Data, data[k])
122
}
138
}
123
139
124
var res NewDatasetResponse
140
var res NewDatasetResponse
125
err := c.call("POST", "/datasets/new", req, &res)
141
err := c.call("POST", "/datasets/new", req, &res)
126
if err != nil {
142
if err != nil {
127
return fmt.Errorf("Error POSTing to /datasets/new: %v", err)
143
return fmt.Errorf("Error POSTing to /datasets/new: %v", err)
128
}
144
}
129
if res.Name != name || res.Size != len(data) {
145
if res.Name != name || res.Size != len(data) {
130
pretty, _ := json.MarshalIndent(res, "", " ")
146
pretty, _ := json.MarshalIndent(res, "", " ")
131
return fmt.Errorf("Unexpected response from /datasets/new: %s", pretty)
147
return fmt.Errorf("Unexpected response from /datasets/new: %s", pretty)
132
}
148
}
133
return nil
149
return nil
134
}
150
}
135
151
136
// BeginNewDataset starts a new dataset upload using the v2 API.
152
// BeginNewDataset starts a new dataset upload using the v2 API.
137
// Returns the version key to use in UploadKv() and EndNewDataset().
153
// Returns the version key to use in UploadKv() and EndNewDataset().
138
// Keep the key secret until EndNewDataset() returns successfully.
154
// Keep the key secret until EndNewDataset() returns successfully.
139
func (c *Client) BeginNewDataset(name string) (string, error) {
155
func (c *Client) BeginNewDataset(name string) (string, error) {
140
req := BeginNewDatasetRequest{Name: name}
156
req := BeginNewDatasetRequest{Name: name}
141
var res BeginNewDatasetResponse
157
var res BeginNewDatasetResponse
142
err := c.call("POST", "/datasets/begin_new", req, &res)
158
err := c.call("POST", "/datasets/begin_new", req, &res)
143
if err != nil {
159
if err != nil {
144
return "", fmt.Errorf("Error POSTing to /datasets/begin_new: %v", err)
160
return "", fmt.Errorf("Error POSTing to /datasets/begin_new: %v", err)
145
}
161
}
146
return res.VersionKey, nil
162
return res.VersionKey, nil
147
}
163
}
148
164
149
// UploadKv uploads more key-value pairs of the dataset being created.
165
// UploadKv uploads more key-value pairs of the dataset being created.
150
func (c *Client) UploadKV(versionKey string, records []KV) error {
166
func (c *Client) UploadKV(versionKey string, records []KV) error {
151
req := UploadKVRequest{VersionKey: versionKey, Records: records}
167
req := UploadKVRequest{VersionKey: versionKey, Records: records}
152
err := c.call("POST", "/datasets/upload_kv", req, nil)
168
err := c.call("POST", "/datasets/upload_kv", req, nil)
153
if err != nil {
169
if err != nil {
154
return fmt.Errorf("Error POSTing to /datasets/upload_kv: %v", err)
170
return fmt.Errorf("Error POSTing to /datasets/upload_kv: %v", err)
155
}
171
}
156
return nil
172
return nil
157
}
173
}
158
174
159
// EndNewDataset commits the dataset being created.
175
// EndNewDataset commits the dataset being created.
160
func (c *Client) EndNewDataset(name, version_key string, size int) error {
176
func (c *Client) EndNewDataset(name, version_key string, size int) error {
161
req := EndNewDatasetRequest{Name: name, VersionKey: version_key, Size: size}
177
req := EndNewDatasetRequest{Name: name, VersionKey: version_key, Size: size}
162
err := c.call("POST", "/datasets/end_new", req, nil)
178
err := c.call("POST", "/datasets/end_new", req, nil)
163
if err != nil {
179
if err != nil {
164
return fmt.Errorf("Error POSTing to /datasets/end_new: %v", err)
180
return fmt.Errorf("Error POSTing to /datasets/end_new: %v", err)
165
}
181
}
166
return nil
182
return nil
167
}
183
}
1
## one: runs an LLM on one input
1
## one: runs an LLM on one input
2
2
3
Before running this script, create a `klex.json` file in the
3
Before running this script, create a `klex.json` file in the
4
root of your git repo, with these contents:
4
root of your git repo, with these contents:
5
5
6
```json
6
```json
7
{
7
{
8
"project_name": "**name**",
8
"project_name": "**name**",
9
"owner_username": "**your oscarkilo.com username**",
9
"owner_username": "**your oscarkilo.com username**",
10
"reader_username": "**your oscarkilo.com username**",
10
"reader_username": "**your oscarkilo.com username**",
11
"datasets_dir": "**optional**",
11
"datasets_dir": "**optional**",
12
"klex_url": "https://las.oscarkilo.com/klex",
12
"klex_url": "https://las.oscarkilo.com/klex",
13
"api_key_file": "klex.key"
13
"api_key_file": "klex.key"
14
}
14
}
15
```
15
```
16
16
17
Then put [your Klex API key](https://oscarkilo.com/login/profile) into
17
Then put [your Klex API key](https://oscarkilo.com/login/profile) into
18
the `klex.key` file.
18
the `klex.key` file.
19
19
20
Then run this command once:
20
Then run this command once:
21
21
22
```bash
22
```bash
23
go get oscarkilo.com/klex-git
23
go get oscarkilo.com/klex-git
24
```
24
```
25
25
26
Now you're ready for the examples below.
26
Now you're ready for the examples below.
27
27
28
### Hello World example
28
### Hello World example
29
29
30
```bash
30
```bash
31
go run oscarkilo.com/klex-git/one <<EOF
31
go run oscarkilo.com/klex-git/one <<EOF
32
{
32
{
33
"model": "Any big LLM",
33
"model": "Any big LLM",
34
"messages": [{
34
"messages": [{
35
"role": "user",
35
"role": "user",
36
"content": [{
36
"content": [{
37
"type": "text",
37
"type": "text",
38
"text": "Which band played Hotel California in Lebowski?"
38
"text": "Which band played Hotel California in Lebowski?"
39
}]
39
}]
40
}]
40
}]
41
}
41
}
42
42
43
EOF
43
EOF
44
```
44
```
45
45
46
### Reading prompts from files
46
### Reading prompts from files
47
47
48
```bash
48
```bash
49
echo "Tell me it's going to rain tomorrow." > prompt.txt
49
echo "Tell me it's going to rain tomorrow." > prompt.txt
50
echo "You're a folksy pirate; speak like it." > system.txt
50
echo "You're a folksy pirate; speak like it." > system.txt
51
echo "{}" | go run oscarkilo.com/klex-git/one \
51
echo "{}" | go run oscarkilo.com/klex-git/one \
52
-model=llama3 \
52
-model=llama3 \
53
-system_file=system.txt \
53
-system_file=system.txt \
54
-prompt_file=prompt.txt
54
-prompt_file=prompt.txt
55
rm prompt.txt system.txt
55
rm prompt.txt system.txt
56
```
56
```
57
57
58
### Analyzing images
58
### Analyzing images
59
60
Use `-attach` to attach a file. The MIME type is detected
61
from the contents; the function must declare matching
62
capabilities (`can_see_images` or `can_see_pdfs` on its
63
`llm2` blob). When `--fast-fail=true` (the default), `one`
64
fetches the function's capabilities before sending and
65
aborts with a clear message if the attachment isn't
66
accepted; set `--fast-fail=false` in tight loops (e.g.
67
MapReduce) to skip the preflight HTTP round-trip.
59
68
60
```bash
69
```bash
70
# Image input.
61
echo "{}" | go run oscarkilo.com/klex-git/one \
71
echo "{}" | go run oscarkilo.com/klex-git/one \
62
-model=gpt-4o \
72
-model=gpt-4o \
63
-a=<(curl -s https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Tux.svg/1024px-Tux.svg.png) \
73
-a=<(curl -s https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Tux.svg/1024px-Tux.svg.png) \
64
-prompt_file=<(echo "What is this guy's name?")
74
-prompt_file=<(echo "What is this guy's name?")
75
76
# PDF input.
77
echo "{}" | go run oscarkilo.com/klex-git/one \
78
-model="Claude Sonnet 4.6" \
79
-attach=document.pdf \
80
-prompt_file=<(echo "Summarize this PDF in one sentence.")
65
```
81
```
1
package main
1
package main
2
2
3
// This binary runs one LLM inference on one input.
3
// This binary runs one LLM inference on one input.
4
4
5
import "encoding/base64"
5
import "encoding/base64"
6
import "flag"
6
import "flag"
7
import "fmt"
7
import "fmt"
8
import "encoding/json"
8
import "encoding/json"
9
import "io/ioutil"
9
import "io/ioutil"
10
import "log"
10
import "log"
11
import "net/http"
11
import "net/http"
12
import "os"
12
import "os"
13
import "strings"
13
import "strings"
14
14
15
import "oscarkilo.com/klex-git/api"
15
import "oscarkilo.com/klex-git/api"
16
import "oscarkilo.com/klex-git/config"
16
import "oscarkilo.com/klex-git/config"
17
17
18
var model = flag.String("model", "", "overrides .Model, if non-empty")
18
var model = flag.String("model", "", "overrides .Model, if non-empty")
19
var system = flag.String("system_file", "", "overrides .System, if non-empty")
19
var system = flag.String("system_file", "", "overrides .System, if non-empty")
20
var prompt = flag.String("prompt_file", "", "appends to .Messages")
20
var prompt = flag.String("prompt_file", "", "appends to .Messages")
21
var a = flag.String("a", "",
21
var a = flag.String("a", "",
22
"path to a file (image or PDF) to attach to the prompt")
22
var format = flag.String("format", "text", "text|json|jsonindent")
23
var format = flag.String("format", "text", "text|json|jsonindent")
24
var fastFail = flag.Bool("fast-fail", true,
25
"before sending, fetch the function's llm2 capabilities and "+
26
"fail if the attached MIME type isn't supported. Set to "+
27
"false in tight loops (e.g. MapReduce) where the extra "+
28
"preflight HTTP round-trip per call is unwanted.")
23
29
24
// guessMimeType returns the MIME type inferred from file contents.
30
// guessMimeType returns the MIME type inferred from file contents.
25
func guessMimeType(b []byte) string {
31
func guessMimeType(b []byte) string {
26
if len(b) > 512 {
32
if len(b) > 512 {
27
b = b[:512]
33
b = b[:512]
28
}
34
}
29
return http.DetectContentType(b)
35
return http.DetectContentType(b)
30
}
36
}
31
37
32
func main() {
38
func main() {
33
flag.Parse()
39
flag.Parse()
34
40
35
// Find the API keys and configure a Klex client.
41
// Find the API keys and configure a Klex client.
36
config, err := config.ReadConfig()
42
config, err := config.ReadConfig()
37
if err != nil {
43
if err != nil {
38
log.Fatalf("Failed to read config: %v", err)
44
log.Fatalf("Failed to read config: %v", err)
39
}
45
}
40
client := api.NewClient(config.KlexUrl, config.ApiKey)
46
client := api.NewClient(config.KlexUrl, config.ApiKey)
41
if client == nil {
47
if client == nil {
42
log.Fatalf("Failed to create Klex client")
48
log.Fatalf("Failed to create Klex client")
43
}
49
}
44
50
45
// Parse stdin as a MessagesRequest object, allowing empty input.
51
// Parse stdin as a MessagesRequest object, allowing empty input.
46
sin, err := ioutil.ReadAll(os.Stdin)
52
sin, err := ioutil.ReadAll(os.Stdin)
47
if err != nil {
53
if err != nil {
48
log.Fatalf("Failed to read stdin: %v", err)
54
log.Fatalf("Failed to read stdin: %v", err)
49
}
55
}
50
if len(sin) == 0 {
56
if len(sin) == 0 {
51
sin = []byte("{}")
57
sin = []byte("{}")
52
}
58
}
53
var req api.MessagesRequest
59
var req api.MessagesRequest
54
err = json.Unmarshal(sin, &req)
60
err = json.Unmarshal(sin, &req)
55
if err != nil {
61
if err != nil {
56
log.Fatalf("Failed to parse a MessagesRequest from stdin: %v", err)
62
log.Fatalf("Failed to parse a MessagesRequest from stdin: %v", err)
57
}
63
}
58
64
59
// Use flags to override parts of the request.
65
// Use flags to override parts of the request.
60
if *model != "" {
66
if *model != "" {
61
req.Model = *model
67
req.Model = *model
62
}
68
}
63
if *system != "" {
69
if *system != "" {
64
s, err := ioutil.ReadFile(*system)
70
s, err := ioutil.ReadFile(*system)
65
if err != nil {
71
if err != nil {
66
log.Fatalf("Failed to read --system_file %s: %v", *system, err)
72
log.Fatalf("Failed to read --system_file %s: %v", *system, err)
67
}
73
}
68
req.System = string(s)
74
req.System = string(s)
69
}
75
}
70
if *a != "" && *prompt == "" {
76
if *a != "" && *prompt == "" {
71
log.Fatalf("--a requires a non-empty --prompt_file, too")
77
log.Fatalf("--a requires a non-empty --prompt_file, too")
72
}
78
}
79
// Tracks the attachment MIME for the preflight below (empty if
80
// no attachment was given).
81
var attach_mime string
73
if *prompt != "" {
82
if *prompt != "" {
74
msg := api.ChatMessage{Role: "user"}
83
msg := api.ChatMessage{Role: "user"}
75
if *a != "" {
84
if *a != "" {
76
i, err := ioutil.ReadFile(*a)
85
i, err := ioutil.ReadFile(*a)
77
if err != nil {
86
if err != nil {
78
log.Fatalf("Failed to read --a %s: %v", *a, err)
87
log.Fatalf("Failed to read --a %s: %v", *a, err)
79
}
80
mime_type := guessMimeType(i)
81
switch mime_type {
82
case "image/jpeg", "image/png", "image/gif", "image/webp":
83
default:
84
log.Fatalf("Unsupported image type: %s", mime_type)
85
}
88
}
89
attach_mime = guessMimeType(i)
86
msg.Content = append(msg.Content, api.ContentBlock{
90
msg.Content = append(msg.Content, api.ContentBlock{
87
Type: "document",
91
Type: "document",
88
Source: &api.ContentSource{
92
Source: &api.ContentSource{
89
Type: "base64",
93
Type: "base64",
90
MediaType: _e,
94
MediaType: _e,
91
Data: base64.StdEncoding.EncodeToString(i),
95
Data: base64.StdEncoding.EncodeToString(i),
92
},
96
},
93
})
97
})
94
}
98
}
95
p, err := ioutil.ReadFile(*prompt)
99
p, err := ioutil.ReadFile(*prompt)
96
if err != nil {
100
if err != nil {
97
log.Fatalf("Failed to read --prompt_file %s: %v", *prompt, err)
101
log.Fatalf("Failed to read --prompt_file %s: %v", *prompt, err)
98
}
102
}
99
msg.Content = append(msg.Content, api.ContentBlock{
103
msg.Content = append(msg.Content, api.ContentBlock{
100
Type: "text",
104
Type: "text",
101
Text: string(p),
105
Text: string(p),
102
})
106
})
103
req.Messages = append(req.Messages, msg)
107
req.Messages = append(req.Messages, msg)
104
}
108
}
105
109
110
// Pre-flight: catch unsupported attachment types before paying
111
// for the LLM call. Skip when --fast-fail=false (e.g. in
112
// MapReduce loops that don't want one extra HTTP round-trip
113
// per call). No local MIME whitelist — server-side flags are
114
// the source of truth.
115
if *fastFail && attach_mime != "" {
116
preflight(client, req.Model, attach_mime)
117
}
118
106
// Get LLM output from Klex.
119
// Get LLM output from Klex.
107
res, err := client.Messages(req)
120
res, err := client.Messages(req)
108
if err != nil {
121
if err != nil {
109
log.Fatalf("Klex f() failure: %v", err)
122
log.Fatalf("Klex f() failure: %v", err)
110
}
123
}
111
124
112
// Print according to the --format flag.
125
// Print according to the --format flag.
113
out, err := formatResponse(res)
126
out, err := formatResponse(res)
114
if err != nil {
127
if err != nil {
115
log.Fatalf("Failed to format response: %v", err)
128
log.Fatalf("Failed to format response: %v", err)
116
}
129
}
117
fmt.Print(out)
130
fmt.Print(out)
118
}
131
}
119
132
120
func formatResponse(res *api.MessagesResponse) (string, error) {
133
func formatResponse(res *api.MessagesResponse) (string, error) {
121
switch *format {
134
switch *format {
122
case "text":
135
case "text":
123
var content []string
136
var content []string
124
for _, c := range res.Content {
137
for _, c := range res.Content {
125
if c.Type == "text" {
138
if c.Type == "text" {
126
content = append(content, c.Text + "\n")
139
content = append(content, c.Text + "\n")
127
}
140
}
128
}
141
}
129
return strings.Join(content, "\n"), nil
142
return strings.Join(content, "\n"), nil
130
case "json":
143
case "json":
131
buf, err := json.Marshal(res)
144
buf, err := json.Marshal(res)
132
return string(buf), err
145
return string(buf), err
133
case "jsonindent":
146
case "jsonindent":
134
buf, err := json.MarshalIndent(res, "", " ")
147
buf, err := json.MarshalIndent(res, "", " ")
135
return string(buf), err
148
return string(buf), err
136
default:
149
default:
137
return "", fmt.Errorf("Unsupported --format=%s", *format)
150
return "", fmt.Errorf("Unsupported --format=%s", *format)
138
}
151
}
139
}
152
}
153
154
// preflight fetches the function's llm2 capabilities and aborts
155
// with a clear error if it can't accept the given attachment
156
// MIME type. Skips silently for MIME families Klex doesn't have
157
// a capability flag for (i.e. anything that isn't image/* or
158
// application/pdf) — the server-side adapter remains the final
159
// arbiter for those.
160
func preflight(client *api.Client, model_name, mime_type string) {
161
resp, err := client.ListFuncs("latest")
162
if err != nil {
163
log.Fatalf("Preflight ListFuncs failed (set "+
164
"--fast-fail=false to bypass): %v", err)
165
}
166
var fn *api.Func
167
for i := range resp.Funcs {
168
if resp.Funcs[i].Name == model_name {
169
fn = &resp.Funcs[i]
170
break
171
}
172
}
173
if fn == nil {
174
log.Fatalf("Unknown model %q (set --fast-fail=false to "+
175
"bypass)", model_name)
176
}
177
if len(fn.Versions) == 0 || fn.Versions[len(fn.Versions)-1].LLM2 == nil {
178
log.Fatalf("Model %q has no llm2 config", model_name)
179
}
180
llm := fn.Versions[len(fn.Versions)-1].LLM2
181
switch {
182
case strings.HasPrefix(mime_type, "image/"):
183
if !llm.CanSeeImages {
184
log.Fatalf("Model %q does not accept images "+
185
"(can_see_images=false)", model_name)
186
}
187
case mime_type == "application/pdf":
188
if !llm.CanSeePDFs {
189
log.Fatalf("Model %q does not accept PDFs "+
190
"(can_see_pdfs=false)", model_name)
191
}
192
}
193
}