1
package api
1
package api
2
2
3
type NewFuncRequest struct {
3
type NewFuncRequest struct {
4
Name string `json:"name"` // no need to be unique
4
Name string `json:"name"` // no need to be unique
5
JsCode string `json:"js_code,omitempty"` // LLM2 must be nil
5
JsCode string `json:"js_code,omitempty"` // LLM2 must be nil
6
LLM2 *LLMFunc `json:"llm2,omitempty"` // JSCode must be ""
6
LLM2 *LLMFunc `json:"llm2,omitempty"` // JSCode must be ""
7
}
7
}
8
8
9
type NewFuncResponse struct {
9
type NewFuncResponse struct {
10
Name string `json:"name"`
10
Name string `json:"name"`
11
DateCreated string `json:"date_created"` // RFC8601 with millis
11
DateCreated string `json:"date_created"` // RFC8601 with millis
12
}
12
}
13
13
14
type DeleteFuncRequest struct {
14
type DeleteFuncRequest struct {
15
Name string `json:"name"`
15
Name string `json:"name"`
16
}
16
}
17
17
18
type DeleteFuncResponse struct {
18
type DeleteFuncResponse struct {
19
}
19
}
20
20
21
type FuncVersion struct {
21
type FuncVersion struct {
22
// Hash is the globally unique ID of this immutable object.
22
// Hash is the globally unique ID of this immutable object.
23
Hash string `json:"hash"`
23
Hash string `json:"hash"`
24
24
25
// Date is the time of creation, according to the server's clock.
25
// Date is the time of creation, according to the server's clock.
26
Date string `json:"date"`
26
Date string `json:"date"`
27
27
28
// JS is JavaScript code that implements this function.
28
// JS is JavaScript code that implements this function.
29
JS string `json:"js,omitempty"`
29
JS string `json:"js,omitempty"`
30
30
31
// LLM is the old "provider:model" string. Deprecated.
31
// LLM is the old "provider:model" string. Deprecated.
32
LLM string `json:"llm,omitempty"`
32
LLM string `json:"llm,omitempty"`
33
33
34
// LLM2 confugures where an how the LLM is run.
34
// LLM2 confugures where an how the LLM is run.
35
LLM2 *LLMFunc `json:"llm2,omitempty"`
35
LLM2 *LLMFunc `json:"llm2,omitempty"`
36
}
36
}
37
37
38
type LLMFunc struct {
38
type LLMFunc struct {
39
// Provider is "openai", "anthropic", "fireworks", etc.
39
// Provider is "openai", "anthropic", "fireworks", etc.
40
Provider string `json:"provider"`
40
Provider string `json:"provider"`
41
41
42
// Model is the provider-assigned name of the LLM.
42
// Model is the provider-assigned name of the LLM.
43
Model string `json:"model"`
43
Model string `json:"model"`
44
44
45
// CanSeeImages is true iff the model accepts image attachments
46
// (PNG/JPEG/GIF/WEBP). Production func versions already use
47
// this flag — do not rename.
45
CanSeeImages bool `json:"can_see_images"`
48
CanSeeImages bool `json:"can_see_images"`
49
50
// CanSeePDFs is true iff the model accepts PDF attachments.
51
// Independent from CanSeeImages because some providers (e.g.
52
// Fireworks, Ollama) support images but not PDFs. Defaults to
53
// false; stored func versions without the key get false on
54
// unmarshal.
55
CanSeePDFs bool `json:"can_see_pdfs"`
56
46
CanHaveSystemPromps bool `json:"can_have_system_prompts"`
57
CanHaveSystemPromps bool `json:"can_have_system_prompts"`
47
CanUseTools bool `json:"can_use_tools"`
58
CanUseTools bool `json:"can_use_tools"`
48
CanStream bool `json:"can_stream"`
59
CanStream bool `json:"can_stream"`
49
}
60
}
50
61
51
type Func struct {
62
type Func struct {
52
Name string `json:"name"`
63
Name string `json:"name"`
53
Versions []FuncVersion `json:"versions"`
64
Versions []FuncVersion `json:"versions"`
54
}
65
}
55
66
56
type ListFuncsResponse struct {
67
type ListFuncsResponse struct {
57
Funcs []Func `json:"funcs"`
68
Funcs []Func `json:"funcs"`
58
}
69
}
1
package api
2
3
// These tests pin the wire format of LLMFunc. CanSeeImages is the
4
// pre-existing capability flag (do not rename — production func
5
// definitions at oscarkilo.com/klex/funcs already use it).
6
// CanSeePDFs is the new capability flag for PDF attachments. The
7
// two are independent because some providers support images but
8
// not PDFs (Fireworks, Ollama, xAI-via-v1-of-our-adapter).
9
10
import "encoding/json"
11
import "strings"
12
import "testing"
13
14
func TestLLMFuncMarshalImagesPDFs(t *testing.T) {
15
f := LLMFunc{
16
Provider: "anthropic",
17
Model: "claude-sonnet-4-6",
18
CanSeeImages: true,
19
CanSeePDFs: true,
20
CanStream: true,
21
}
22
buf, err := json.Marshal(f)
23
if err != nil {
24
t.Fatalf("marshal: %v", err)
25
}
26
s := string(buf)
27
if !strings.Contains(s, `"can_see_images":true`) {
28
t.Errorf("missing can_see_images in %s", s)
29
}
30
if !strings.Contains(s, `"can_see_pdfs":true`) {
31
t.Errorf("missing can_see_pdfs in %s", s)
32
}
33
}
34
35
func TestLLMFuncUnmarshalLegacyData(t *testing.T) {
36
// Existing production func versions are stored without a
37
// can_see_pdfs key. They must unmarshal to CanSeePDFs=false
38
// without error.
39
in := []byte(
40
`{"provider":"anthropic","model":"x","can_see_images":true}`)
41
var f LLMFunc
42
if err := json.Unmarshal(in, &f); err != nil {
43
t.Fatalf("unmarshal: %v", err)
44
}
45
if !f.CanSeeImages {
46
t.Errorf("CanSeeImages = false, want true")
47
}
48
if f.CanSeePDFs {
49
t.Errorf("CanSeePDFs = true, want false (key absent)")
50
}
51
}
52
53
func TestLLMFuncUnmarshalPDFsOnly(t *testing.T) {
54
// A model that accepts PDFs but not images (rare but possible
55
// if a future provider goes that way).
56
in := []byte(
57
`{"provider":"x","model":"y","can_see_pdfs":true}`)
58
var f LLMFunc
59
if err := json.Unmarshal(in, &f); err != nil {
60
t.Fatalf("unmarshal: %v", err)
61
}
62
if f.CanSeeImages {
63
t.Errorf("CanSeeImages = true, want false")
64
}
65
if !f.CanSeePDFs {
66
t.Errorf("CanSeePDFs = false, want true")
67
}
68
}
69
70
func TestLLMFuncUnmarshalBoth(t *testing.T) {
71
in := []byte(
72
`{"provider":"openai","model":"gpt-4o",` +
73
`"can_see_images":true,"can_see_pdfs":true}`)
74
var f LLMFunc
75
if err := json.Unmarshal(in, &f); err != nil {
76
t.Fatalf("unmarshal: %v", err)
77
}
78
if !f.CanSeeImages || !f.CanSeePDFs {
79
t.Errorf("expected both flags true; got %+v", f)
80
}
81
}
82
83
func TestLLMFuncUnmarshalNeither(t *testing.T) {
84
in := []byte(`{"provider":"x","model":"y"}`)
85
var f LLMFunc
86
if err := json.Unmarshal(in, &f); err != nil {
87
t.Fatalf("unmarshal: %v", err)
88
}
89
if f.CanSeeImages || f.CanSeePDFs {
90
t.Errorf("expected both flags false; got %+v", f)
91
}
92
}
93
94
func TestLLMFuncRoundTrip(t *testing.T) {
95
orig := LLMFunc{
96
Provider: "openai",
97
Model: "gpt-4o",
98
CanSeeImages: true,
99
CanSeePDFs: true,
100
CanUseTools: true,
101
CanStream: true,
102
}
103
buf, err := json.Marshal(orig)
104
if err != nil {
105
t.Fatalf("marshal: %v", err)
106
}
107
var got LLMFunc
108
if err := json.Unmarshal(buf, &got); err != nil {
109
t.Fatalf("unmarshal: %v", err)
110
}
111
if got != orig {
112
t.Errorf("round-trip diff\nwant: %+v\n got: %+v", orig, got)
113
}
114
}
1
package api
1
package api
2
2
3
type ChatMessage struct {
3
type ChatMessage struct {
4
Role string `json:"role"`
4
Role string `json:"role"`
5
Content []ContentBlock `json:"content"`
5
Content []ContentBlock `json:"content"`
6
}
6
}
7
7
8
// MessageRequest is generalizes across OpenAI, Anthropic, etc.
8
// MessageRequest is generalizes across OpenAI, Anthropic, etc.
9
type MessagesRequest struct {
9
type MessagesRequest struct {
10
Model string `json:"model"`
10
Model string `json:"model"`
11
Messages []ChatMessage `json:"messages"`
11
Messages []ChatMessage `json:"messages"`
12
MaxTokens int `json:"max_tokens"`
12
MaxTokens int `json:"max_tokens"`
13
System string `json:"system,omitempty"`
13
System string `json:"system,omitempty"`
14
Temperature float64 `json:"temperature"`
14
Temperature float64 `json:"temperature"`
15
Tools []Tool `json:"tools,omitempty"`
15
Tools []Tool `json:"tools,omitempty"`
16
}
16
}
17
17
18
type ContentBlock struct {
18
type ContentBlock struct {
19
// Type is "text", "e", "tool_use", or "tool_result".
19
// Type is "text", "e", "tool_use", or "tool_result".
20
Type string `json:"type"`
20
Type string `json:"type"`
21
21
22
// Text is for Type="text"
22
// Text is for Type="text"
23
Text string `json:"text,omitempty"`
23
Text string `json:"text,omitempty"`
24
24
25
// Source is for Type="mage
25
// Source is for Type="mage
26
// between image attachments and PDF attachments.
26
Source *ContentSource `json:"source,omitempty"`
27
Source *ContentSource `json:"source,omitempty"`
27
28
28
// ID, Name, and Input are for Type="tool_use".
29
// ID, Name, and Input are for Type="tool_use".
29
ID string `json:"id,omitempty"`
30
ID string `json:"id,omitempty"`
30
Name string `json:"name,omitempty"`
31
Name string `json:"name,omitempty"`
31
Input interface{} `json:"input,omitempty"`
32
Input interface{} `json:"input,omitempty"`
32
33
33
// ToolUseID, Content, and Output are for
34
// ToolUseID, Content, and Output are for
34
// Type="tool_result".
35
// Type="tool_result".
35
ToolUseID string `json:"tool_use_id,omitempty"`
36
ToolUseID string `json:"tool_use_id,omitempty"`
36
Content string `json:"content,omitempty"`
37
Content string `json:"content,omitempty"`
37
Output string `json:"output,omitempty"`
38
Output string `json:"output,omitempty"`
38
}
39
}
39
40
40
type ContentSource struct {
41
type ContentSource struct {
41
// Type can only be "base64".
42
// Type can only be "base64".
42
Type string `json:"type"`
43
Type string `json:"type"`
43
44
44
// MediaType can be one of:
45
// MediaType can be one of:
45
// - "image/jpeg",
46
// - "image/jpeg",
46
// - "image/png",
47
// - "image/png",
47
// - "image/gif",
48
// - "image/gif",
48
// - "image/webp"
49
// - "image/webp"
50
// - "application/pdf".
51
// Whether a particular MIME is accepted depends on the
52
// backing model; the func registry's CanSeeDocuments
53
// capability gates attachments at all but does not split
54
// by MIME.
49
MediaType string `json:"media_type,omitempty"`
55
MediaType string `json:"media_type,omitempty"`
50
56
51
Data string `json:"data,omitempty"`
57
Data string `json:"data,omitempty"`
52
}
58
}
53
59
54
type Usage struct {
60
type Usage struct {
55
InputTokens int `json:"input_tokens,omitempty"`
61
InputTokens int `json:"input_tokens,omitempty"`
56
CacheCreationInputTokens *int `json:"cache_creation_input_tokens,omitempty"`
62
CacheCreationInputTokens *int `json:"cache_creation_input_tokens,omitempty"`
57
CacheReadInputTokens *int `json:"cache_read_input_tokens,omitempty"`
63
CacheReadInputTokens *int `json:"cache_read_input_tokens,omitempty"`
58
OutputTokens int `json:"output_tokens,omitempty"`
64
OutputTokens int `json:"output_tokens,omitempty"`
59
}
65
}
60
66
61
type ErrorResponse struct {
67
type ErrorResponse struct {
62
Type string `json:"type"`
68
Type string `json:"type"`
63
Message string `json:"message"`
69
Message string `json:"message"`
64
}
70
}
65
71
66
type MessagesResponse struct {
72
type MessagesResponse struct {
67
Id string `json:"id"`
73
Id string `json:"id"`
68
Type string `json:"type"`
74
Type string `json:"type"`
69
Role string `json:"role"`
75
Role string `json:"role"`
70
Content []ContentBlock `json:"content"`
76
Content []ContentBlock `json:"content"`
71
Model string `json:"model"`
77
Model string `json:"model"`
72
StopReason *string `json:"stop_reason,omitempty"`
78
StopReason *string `json:"stop_reason,omitempty"`
73
StopSequence *string `json:"stop_sequence,omitempty"`
79
StopSequence *string `json:"stop_sequence,omitempty"`
74
Usage Usage `json:"usage"`
80
Usage Usage `json:"usage"`
75
Error *ErrorResponse `json:"error,omitempty"`
81
Error *ErrorResponse `json:"error,omitempty"`
76
}
82
}
77
83
78
type Tool struct {
84
type Tool struct {
79
Type string `json:"type"`
85
Type string `json:"type"`
80
Function *ToolFunction `json:"function"`
86
Function *ToolFunction `json:"function"`
81
}
87
}
82
88
83
type ToolFunction struct {
89
type ToolFunction struct {
84
Name string `json:"name"`
90
Name string `json:"name"`
85
Description string `json:"description"`
91
Description string `json:"description"`
86
InputSchema interface{} `json:"input_schema"`
92
InputSchema interface{} `json:"input_schema"`
87
}
93
}
1
package api
2
3
// These tests pin the wire format of ContentBlock so callers across
4
// //funky, //ithfm, and other consumers can rely on stable JSON
5
// shapes. They fail loudly if the JSON tags or field names ever
6
// drift.
7
8
import "encoding/json"
9
import "strings"
10
import "testing"
11
12
func TestDocumentBlockMarshal(t *testing.T) {
13
block := ContentBlock{
14
Type: "document",
15
Source: &ContentSource{
16
Type: "base64",
17
MediaType: "application/pdf",
18
Data: "JVBERi0xLjQK",
19
},
20
}
21
buf, err := json.Marshal(block)
22
if err != nil {
23
t.Fatalf("marshal: %v", err)
24
}
25
want := `{"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"JVBERi0xLjQK"}}`
26
if string(buf) != want {
27
t.Errorf(
28
"marshal mismatch\nwant: %s\n got: %s", want, buf)
29
}
30
}
31
32
func TestDocumentBlockUnmarshal(t *testing.T) {
33
in := []byte(
34
`{"type":"document","source":{"type":"base64",` +
35
`"media_type":"application/pdf","data":"JVBERi0xLjQK"}}`)
36
var block ContentBlock
37
if err := json.Unmarshal(in, &block); err != nil {
38
t.Fatalf("unmarshal: %v", err)
39
}
40
if block.Type != "document" {
41
t.Errorf("Type = %q, want %q", block.Type, "document")
42
}
43
if block.Source == nil {
44
t.Fatalf("Source is nil")
45
}
46
if block.Source.MediaType != "application/pdf" {
47
t.Errorf(
48
"MediaType = %q, want application/pdf",
49
block.Source.MediaType)
50
}
51
if block.Source.Data != "JVBERi0xLjQK" {
52
t.Errorf("Data = %q", block.Source.Data)
53
}
54
}
55
56
func TestDocumentBlockImageMimeRoundTrip(t *testing.T) {
57
// A "document" with an image/* MIME is how the wire format
58
// carries what used to live under Type:"image".
59
block := ContentBlock{
60
Type: "document",
61
Source: &ContentSource{
62
Type: "base64",
63
MediaType: "image/png",
64
Data: "iVBORw0KGgo=",
65
},
66
}
67
buf, err := json.Marshal(block)
68
if err != nil {
69
t.Fatalf("marshal: %v", err)
70
}
71
if !strings.Contains(string(buf), `"type":"document"`) {
72
t.Errorf("expected type:document, got %s", buf)
73
}
74
if !strings.Contains(string(buf), `"media_type":"image/png"`) {
75
t.Errorf("expected media_type image/png, got %s", buf)
76
}
77
var got ContentBlock
78
if err := json.Unmarshal(buf, &got); err != nil {
79
t.Fatalf("unmarshal: %v", err)
80
}
81
if got.Type != "document" {
82
t.Errorf("Type = %q", got.Type)
83
}
84
if got.Source == nil ||
85
got.Source.MediaType != "image/png" {
86
t.Errorf("Source mismatch: %+v", got.Source)
87
}
88
}
89
90
func TestTextBlockUnchanged(t *testing.T) {
91
// Documents the unchanged text-block wire format.
92
block := ContentBlock{Type: "text", Text: "hello"}
93
buf, err := json.Marshal(block)
94
if err != nil {
95
t.Fatalf("marshal: %v", err)
96
}
97
if string(buf) != `{"type":"text","text":"hello"}` {
98
t.Errorf("unexpected text-block JSON: %s", buf)
99
}
100
}
1
package main
1
package main
2
2
3
import "encoding/base64"
3
import "encoding/base64"
4
import "encoding/json"
4
import "encoding/json"
5
import "flag"
5
import "flag"
6
import "io/ioutil"
6
import "io/ioutil"
7
import "log"
7
import "log"
8
import "net/http"
8
import "net/http"
9
import "os"
9
import "os"
10
import "path"
10
import "path"
11
import "sort"
11
import "sort"
12
import "strings"
12
import "strings"
13
13
14
import "oscarkilo.com/klex-git/api"
14
import "oscarkilo.com/klex-git/api"
15
import "oscarkilo.com/klex-git/config"
15
import "oscarkilo.com/klex-git/config"
16
16
17
var dir = flag.String("dir", ".", "Directory to scan and write to.")
17
var dir = flag.String("dir", ".", "Directory to scan and write to.")
18
var model = flag.String("model", "Gemini 3 Pro", "")
18
var model = flag.String("model", "Gemini 3 Pro", "")
19
var format = flag.String("format", "text", "text|json|jsonindent")
19
var format = flag.String("format", "text", "text|json|jsonindent")
20
var dry_run = flag.Bool("dry_run", false, "")
20
var dry_run = flag.Bool("dry_run", false, "")
21
var debug = flag.Bool("debug", false, "")
21
var debug = flag.Bool("debug", false, "")
22
22
23
type Case struct {
23
type Case struct {
24
Name string
24
Name string
25
Before []string
25
Before []string
26
After string
26
After string
27
Request *api.MessagesRequest
27
Request *api.MessagesRequest
28
}
28
}
29
29
30
func scanForCases() []Case {
30
func scanForCases() []Case {
31
entries, err := os.ReadDir(*dir)
31
entries, err := os.ReadDir(*dir)
32
if err != nil {
32
if err != nil {
33
log.Fatalf("Failed to read dir %s: %v", *dir, err)
33
log.Fatalf("Failed to read dir %s: %v", *dir, err)
34
}
34
}
35
35
36
before := make(map[string][]string)
36
before := make(map[string][]string)
37
after := make(map[string]string)
37
after := make(map[string]string)
38
for _, entry := range entries {
38
for _, entry := range entries {
39
if entry.IsDir() {
39
if entry.IsDir() {
40
continue
40
continue
41
}
41
}
42
if entry.Name() == "system_prompt.txt" {
42
if entry.Name() == "system_prompt.txt" {
43
continue
43
continue
44
}
44
}
45
chunks := strings.Split(entry.Name(), ".")
45
chunks := strings.Split(entry.Name(), ".")
46
if len(chunks) < 2 {
46
if len(chunks) < 2 {
47
continue
47
continue
48
}
48
}
49
name := strings.Join(chunks[0:len(chunks)-1], ".")
49
name := strings.Join(chunks[0:len(chunks)-1], ".")
50
suffix := chunks[len(chunks)-1]
50
suffix := chunks[len(chunks)-1]
51
switch suffix {
51
switch suffix {
52
case "txt", "json", "jpg", "jpeg", "png", "webp":
52
case "txt", "json", "jpg", "jpeg", "png", "webp":
53
before[name] = append(before[name], suffix)
53
before[name] = append(before[name], suffix)
54
case "out":
54
case "out":
55
after[name] = suffix
55
after[name] = suffix
56
}
56
}
57
}
57
}
58
58
59
var cases []Case
59
var cases []Case
60
for name, b := range before {
60
for name, b := range before {
61
sort.Slice(b, func(i, j int) bool {
61
sort.Slice(b, func(i, j int) bool {
62
if b[i] == "txt" && b[j] != "txt" {
62
if b[i] == "txt" && b[j] != "txt" {
63
return true
63
return true
64
}
64
}
65
if b[i] != "txt" && b[j] == "txt" {
65
if b[i] != "txt" && b[j] == "txt" {
66
return false
66
return false
67
}
67
}
68
return b[i] < b[j]
68
return b[i] < b[j]
69
})
69
})
70
cases = append(cases, Case{
70
cases = append(cases, Case{
71
Name: name,
71
Name: name,
72
Before: b,
72
Before: b,
73
After: after[name],
73
After: after[name],
74
})
74
})
75
}
75
}
76
76
77
sort.Slice(cases, func(i, j int) bool {
77
sort.Slice(cases, func(i, j int) bool {
78
a, b := cases[i], cases[j]
78
a, b := cases[i], cases[j]
79
if a.After != "" && b.After == "" {
79
if a.After != "" && b.After == "" {
80
return true
80
return true
81
}
81
}
82
if a.After == "" && b.After != "" {
82
if a.After == "" && b.After != "" {
83
return false
83
return false
84
}
84
}
85
return a.Name < b.Name
85
return a.Name < b.Name
86
})
86
})
87
87
88
num_examples := 0
88
num_examples := 0
89
for ; num_examples < len(cases); num_examples++ {
89
for ; num_examples < len(cases); num_examples++ {
90
if cases[num_examples].After == "" {
90
if cases[num_examples].After == "" {
91
break
91
break
92
}
92
}
93
}
93
}
94
log.Printf(
94
log.Printf(
95
"%s:\n num_examples = %d\n num_inputs = %d",
95
"%s:\n num_examples = %d\n num_inputs = %d",
96
*dir,
96
*dir,
97
num_examples,
97
num_examples,
98
len(cases) - num_examples,
98
len(cases) - num_examples,
99
)
99
)
100
100
101
return cases
101
return cases
102
}
102
}
103
103
104
func readFile(fname string) []byte {
104
func readFile(fname string) []byte {
105
data, err := ioutil.ReadFile(path.Join(*dir, fname))
105
data, err := ioutil.ReadFile(path.Join(*dir, fname))
106
if err != nil {
106
if err != nil {
107
log.Fatalf("Failed to read file %s: %v", fname, err)
107
log.Fatalf("Failed to read file %s: %v", fname, err)
108
}
108
}
109
return data
109
return data
110
}
110
}
111
111
112
func writeFile(fname string, data []byte) error {
112
func writeFile(fname string, data []byte) error {
113
return ioutil.WriteFile(path.Join(*dir, fname), data, 0644)
113
return ioutil.WriteFile(path.Join(*dir, fname), data, 0644)
114
}
114
}
115
115
116
func copyRequest(req api.MessagesRequest) *api.MessagesRequest {
116
func copyRequest(req api.MessagesRequest) *api.MessagesRequest {
117
bytes, err := json.Marshal(req)
117
bytes, err := json.Marshal(req)
118
if err != nil {
118
if err != nil {
119
log.Fatalf("Failed to copy request: %+v", err)
119
log.Fatalf("Failed to copy request: %+v", err)
120
}
120
}
121
req2 := &api.MessagesRequest{}
121
req2 := &api.MessagesRequest{}
122
err = json.Unmarshal(bytes, req2)
122
err = json.Unmarshal(bytes, req2)
123
if err != nil {
123
if err != nil {
124
log.Fatalf("Failed to copy request: %+v", err)
124
log.Fatalf("Failed to copy request: %+v", err)
125
}
125
}
126
return req2
126
return req2
127
}
127
}
128
128
129
func main() {
129
func main() {
130
flag.Parse()
130
flag.Parse()
131
131
132
// Find the API keys and configure a Klex client.
132
// Find the API keys and configure a Klex client.
133
config, err := config.ReadConfig()
133
config, err := config.ReadConfig()
134
if err != nil {
134
if err != nil {
135
log.Fatalf("Failed to read config: %v", err)
135
log.Fatalf("Failed to read config: %v", err)
136
}
136
}
137
client := api.NewClient(config.KlexUrl, config.ApiKey)
137
client := api.NewClient(config.KlexUrl, config.ApiKey)
138
if client == nil {
138
if client == nil {
139
log.Fatalf("Failed to create Klex client")
139
log.Fatalf("Failed to create Klex client")
140
}
140
}
141
141
142
// Create MessagesRequest objects.
142
// Create MessagesRequest objects.
143
req := api.MessagesRequest{
143
req := api.MessagesRequest{
144
Model: *model,
144
Model: *model,
145
System: string(readFile("system_prompt.txt")),
145
System: string(readFile("system_prompt.txt")),
146
}
146
}
147
cases := scanForCases()
147
cases := scanForCases()
148
for i, c := range cases {
148
for i, c := range cases {
149
user := api.ChatMessage{Role: "user"}
149
user := api.ChatMessage{Role: "user"}
150
text := "Case name: " + c.Name + "\n\n"
150
text := "Case name: " + c.Name + "\n\n"
151
for _, suffix := range c.Before {
151
for _, suffix := range c.Before {
152
switch suffix {
152
switch suffix {
153
case "txt":
153
case "txt":
154
text += string(readFile(c.Name + ".txt"))
154
text += string(readFile(c.Name + ".txt"))
155
case "json":
155
case "json":
156
text += "\n\n```json\n"
156
text += "\n\n```json\n"
157
text += string(readFile(c.Name + ".json"))
157
text += string(readFile(c.Name + ".json"))
158
text += "\n```\n"
158
text += "\n```\n"
159
case "jpg", "jpeg", "png", "webp":
159
case "jpg", "jpeg", "png", "webp":
160
bytes := readFile(c.Name + "." + suffix)
160
bytes := readFile(c.Name + "." + suffix)
161
user.Content = append(user.Content, api.ContentBlock{
161
user.Content = append(user.Content, api.ContentBlock{
162
Type: "e",
162
Type: "e",
163
Source: &api.ContentSource{
163
Source: &api.ContentSource{
164
Type: "base64",
164
Type: "base64",
165
MediaType: http.DetectContentType(bytes),
165
MediaType: http.DetectContentType(bytes),
166
Data: base64.StdEncoding.EncodeToString(bytes),
166
Data: base64.StdEncoding.EncodeToString(bytes),
167
},
167
},
168
})
168
})
169
default:
169
default:
170
log.Fatalf("Unsupported suffix %s in case %s", suffix, c.Name)
170
log.Fatalf("Unsupported suffix %s in case %s", suffix, c.Name)
171
}
171
}
172
}
172
}
173
user.Content = append(user.Content, api.ContentBlock{
173
user.Content = append(user.Content, api.ContentBlock{
174
Type: "text",
174
Type: "text",
175
Text: text,
175
Text: text,
176
})
176
})
177
req.Messages = append(req.Messages, user)
177
req.Messages = append(req.Messages, user)
178
if c.After != "" {
178
if c.After != "" {
179
asst := api.ChatMessage{Role: "assistant"}
179
asst := api.ChatMessage{Role: "assistant"}
180
asst.Content = append(asst.Content, api.ContentBlock{
180
asst.Content = append(asst.Content, api.ContentBlock{
181
Type: "text",
181
Type: "text",
182
Text: string(readFile(c.Name + ".out")),
182
Text: string(readFile(c.Name + ".out")),
183
})
183
})
184
req.Messages = append(req.Messages, asst)
184
req.Messages = append(req.Messages, asst)
185
} else {
185
} else {
186
cases[i].Request = copyRequest(req)
186
cases[i].Request = copyRequest(req)
187
req.Messages = req.Messages[:len(req.Messages)-1]
187
req.Messages = req.Messages[:len(req.Messages)-1]
188
}
188
}
189
}
189
}
190
190
191
if *dry_run {
191
if *dry_run {
192
log.Printf("Dry run; not sending requests.")
192
log.Printf("Dry run; not sending requests.")
193
return
193
return
194
}
194
}
195
195
196
// Send 'em.
196
// Send 'em.
197
// TODO: parallelize
197
// TODO: parallelize
198
for _, c := range cases {
198
for _, c := range cases {
199
if c.Request == nil {
199
if c.Request == nil {
200
continue
200
continue
201
}
201
}
202
if *debug {
202
if *debug {
203
log.Printf("Case %s: sending request:", c.Name)
203
log.Printf("Case %s: sending request:", c.Name)
204
enc := json.NewEncoder(os.Stderr)
204
enc := json.NewEncoder(os.Stderr)
205
enc.SetIndent("", " ")
205
enc.SetIndent("", " ")
206
enc.Encode(c.Request)
206
enc.Encode(c.Request)
207
}
207
}
208
res, err := client.Messages(*c.Request)
208
res, err := client.Messages(*c.Request)
209
if err != nil {
209
if err != nil {
210
log.Printf("Case %s: request failed: %+v", c.Name, err)
210
log.Printf("Case %s: request failed: %+v", c.Name, err)
211
continue
211
continue
212
}
212
}
213
if len(res.Content) != 1 {
213
if len(res.Content) != 1 {
214
log.Printf("Case %s: empty response", c.Name)
214
log.Printf("Case %s: empty response", c.Name)
215
continue
215
continue
216
}
216
}
217
c0 := res.Content[0]
217
c0 := res.Content[0]
218
if c0.Type != "text" {
218
if c0.Type != "text" {
219
log.Printf("Case %s: Content[0].Type = %s", c.Name, c0.Type)
219
log.Printf("Case %s: Content[0].Type = %s", c.Name, c0.Type)
220
continue
220
continue
221
}
221
}
222
err = writeFile(c.Name + ".out", append([]byte(c0.Text), '\n'))
222
err = writeFile(c.Name + ".out", append([]byte(c0.Text), '\n'))
223
if err != nil {
223
if err != nil {
224
log.Printf("Case %s: failed to write %s.out: %+v", c.Name, c.Name, err)
224
log.Printf("Case %s: failed to write %s.out: %+v", c.Name, c.Name, err)
225
continue
225
continue
226
}
226
}
227
log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
227
log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
228
}
228
}
229
}
229
}
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 image = flag.String("image_file", "", "attaches an image to the prompt")
21
var image = flag.String("image_file", "", "attaches an image to the prompt")
22
var format = flag.String("format", "text", "text|json|jsonindent")
22
var format = flag.String("format", "text", "text|json|jsonindent")
23
23
24
// guessMimeType returns the MIME type inferred from file contents.
24
// guessMimeType returns the MIME type inferred from file contents.
25
func guessMimeType(b []byte) string {
25
func guessMimeType(b []byte) string {
26
if len(b) > 512 {
26
if len(b) > 512 {
27
b = b[:512]
27
b = b[:512]
28
}
28
}
29
return http.DetectContentType(b)
29
return http.DetectContentType(b)
30
}
30
}
31
31
32
func main() {
32
func main() {
33
flag.Parse()
33
flag.Parse()
34
34
35
// Find the API keys and configure a Klex client.
35
// Find the API keys and configure a Klex client.
36
config, err := config.ReadConfig()
36
config, err := config.ReadConfig()
37
if err != nil {
37
if err != nil {
38
log.Fatalf("Failed to read config: %v", err)
38
log.Fatalf("Failed to read config: %v", err)
39
}
39
}
40
client := api.NewClient(config.KlexUrl, config.ApiKey)
40
client := api.NewClient(config.KlexUrl, config.ApiKey)
41
if client == nil {
41
if client == nil {
42
log.Fatalf("Failed to create Klex client")
42
log.Fatalf("Failed to create Klex client")
43
}
43
}
44
44
45
// Parse stdin as a MessagesRequest object, allowing empty input.
45
// Parse stdin as a MessagesRequest object, allowing empty input.
46
sin, err := ioutil.ReadAll(os.Stdin)
46
sin, err := ioutil.ReadAll(os.Stdin)
47
if err != nil {
47
if err != nil {
48
log.Fatalf("Failed to read stdin: %v", err)
48
log.Fatalf("Failed to read stdin: %v", err)
49
}
49
}
50
if len(sin) == 0 {
50
if len(sin) == 0 {
51
sin = []byte("{}")
51
sin = []byte("{}")
52
}
52
}
53
var req api.MessagesRequest
53
var req api.MessagesRequest
54
err = json.Unmarshal(sin, &req)
54
err = json.Unmarshal(sin, &req)
55
if err != nil {
55
if err != nil {
56
log.Fatalf("Failed to parse a MessagesRequest from stdin: %v", err)
56
log.Fatalf("Failed to parse a MessagesRequest from stdin: %v", err)
57
}
57
}
58
58
59
// Use flags to override parts of the request.
59
// Use flags to override parts of the request.
60
if *model != "" {
60
if *model != "" {
61
req.Model = *model
61
req.Model = *model
62
}
62
}
63
if *system != "" {
63
if *system != "" {
64
s, err := ioutil.ReadFile(*system)
64
s, err := ioutil.ReadFile(*system)
65
if err != nil {
65
if err != nil {
66
log.Fatalf("Failed to read --system_file %s: %v", *system, err)
66
log.Fatalf("Failed to read --system_file %s: %v", *system, err)
67
}
67
}
68
req.System = string(s)
68
req.System = string(s)
69
}
69
}
70
if *image != "" && *prompt == "" {
70
if *image != "" && *prompt == "" {
71
log.Fatalf("--image_file requires a non-empty --prompt_file, too")
71
log.Fatalf("--image_file requires a non-empty --prompt_file, too")
72
}
72
}
73
if *prompt != "" {
73
if *prompt != "" {
74
msg := api.ChatMessage{Role: "user"}
74
msg := api.ChatMessage{Role: "user"}
75
if *image != "" {
75
if *image != "" {
76
i, err := ioutil.ReadFile(*image)
76
i, err := ioutil.ReadFile(*image)
77
if err != nil {
77
if err != nil {
78
log.Fatalf("Failed to read --image_file %s: %v", *image, err)
78
log.Fatalf("Failed to read --image_file %s: %v", *image, err)
79
}
79
}
80
mime_type := guessMimeType(i)
80
mime_type := guessMimeType(i)
81
switch mime_type {
81
switch mime_type {
82
case "image/jpeg", "image/png", "image/gif", "image/webp":
82
case "image/jpeg", "image/png", "image/gif", "image/webp":
83
default:
83
default:
84
log.Fatalf("Unsupported image type: %s", mime_type)
84
log.Fatalf("Unsupported image type: %s", mime_type)
85
}
85
}
86
msg.Content = append(msg.Content, api.ContentBlock{
86
msg.Content = append(msg.Content, api.ContentBlock{
87
Type: "e",
87
Type: "e",
88
Source: &api.ContentSource{
88
Source: &api.ContentSource{
89
Type: "base64",
89
Type: "base64",
90
MediaType: mime_type,
90
MediaType: mime_type,
91
Data: base64.StdEncoding.EncodeToString(i),
91
Data: base64.StdEncoding.EncodeToString(i),
92
},
92
},
93
})
93
})
94
}
94
}
95
p, err := ioutil.ReadFile(*prompt)
95
p, err := ioutil.ReadFile(*prompt)
96
if err != nil {
96
if err != nil {
97
log.Fatalf("Failed to read --prompt_file %s: %v", *prompt, err)
97
log.Fatalf("Failed to read --prompt_file %s: %v", *prompt, err)
98
}
98
}
99
msg.Content = append(msg.Content, api.ContentBlock{
99
msg.Content = append(msg.Content, api.ContentBlock{
100
Type: "text",
100
Type: "text",
101
Text: string(p),
101
Text: string(p),
102
})
102
})
103
req.Messages = append(req.Messages, msg)
103
req.Messages = append(req.Messages, msg)
104
}
104
}
105
105
106
// Get LLM output from Klex.
106
// Get LLM output from Klex.
107
res, err := client.Messages(req)
107
res, err := client.Messages(req)
108
if err != nil {
108
if err != nil {
109
log.Fatalf("Klex f() failure: %v", err)
109
log.Fatalf("Klex f() failure: %v", err)
110
}
110
}
111
111
112
// Print according to the --format flag.
112
// Print according to the --format flag.
113
out, err := formatResponse(res)
113
out, err := formatResponse(res)
114
if err != nil {
114
if err != nil {
115
log.Fatalf("Failed to format response: %v", err)
115
log.Fatalf("Failed to format response: %v", err)
116
}
116
}
117
fmt.Print(out)
117
fmt.Print(out)
118
}
118
}
119
119
120
func formatResponse(res *api.MessagesResponse) (string, error) {
120
func formatResponse(res *api.MessagesResponse) (string, error) {
121
switch *format {
121
switch *format {
122
case "text":
122
case "text":
123
var content []string
123
var content []string
124
for _, c := range res.Content {
124
for _, c := range res.Content {
125
if c.Type == "text" {
125
if c.Type == "text" {
126
content = append(content, c.Text + "\n")
126
content = append(content, c.Text + "\n")
127
}
127
}
128
}
128
}
129
return strings.Join(content, "\n"), nil
129
return strings.Join(content, "\n"), nil
130
case "json":
130
case "json":
131
buf, err := json.Marshal(res)
131
buf, err := json.Marshal(res)
132
return string(buf), err
132
return string(buf), err
133
case "jsonindent":
133
case "jsonindent":
134
buf, err := json.MarshalIndent(res, "", " ")
134
buf, err := json.MarshalIndent(res, "", " ")
135
return string(buf), err
135
return string(buf), err
136
default:
136
default:
137
return "", fmt.Errorf("Unsupported --format=%s", *format)
137
return "", fmt.Errorf("Unsupported --format=%s", *format)
138
}
138
}
139
}
139
}