code.oscarkilo.com/okg

Hash:
ac9ffc816eff8c1b9dba099b0b8b5d10fcdc439d
Author:
Igor Naverniouk <[email protected]>
Date:
Thu Jun 4 19:27:31 2026 -0400
Message:
okg: add `okg one` — single LLM inference Port the `klex-git/one` binary to an okg subcommand. Reads stdin as an api.MessagesRequest JSON ({} for empty), then overrides individual fields from flags: --model NAME override .Model --system-file FILE override .System from file --prompt-file FILE append as a user prompt --attach FILE attach an image/PDF to that prompt --format FORMAT text | json | jsonindent --fast-fail preflight attachment MIME (default on) Flag names are dash-form to match okg's convention; the old underscore forms (--system_file, --prompt_file) are not carried over since klex-git/one is being deprecated anyway. Bumps klex-git to pick up the api.NewDocumentBlock helper added in 7fda718. No unit test — the function calls klex.Messages over HTTP; will backfill with a mock-API test in a follow-up alongside one for embed.
diff --git a/README.md b/README.md
index 6fa9ead..bdf96e5 100644
--- a/README.md
+++ b/README.md
@@ -58,16 +58,18 @@ okg auth login [--key KEY] [--host HOST]

okg embed [--model NAME] [--dims N] [--full-path]
(reads stdin → vectors on stdout)
+okg one [--model NAME] [--system-file FILE] \
+ [--prompt-file FILE] [--attach FILE] \
+ [--format text|json|jsonindent] [--fast-fail]
+ (reads stdin as a JSON MessagesRequest)
```

### Coming next

-- `okg one` — one LLM inference on one input (from
- `klex-git/one`).
- `okg exemplary` — few-shot batch runner (from
`klex-git/exemplary`).

-Once those land, the standalone `klex-git` binaries are
+Once that lands, the standalone `klex-git` binaries are
deprecated.

### Flags
diff --git a/go.mod b/go.mod
index d50f747..99c5a87 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module oscarkilo.com/okg

go 1.23

-require oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9 // indirect
+require oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d // indirect
diff --git a/go.sum b/go.sum
index ebbe14d..c3f47d7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9 h1:QvENdX6T54dU1GdjYawBQQWAtVGQyvTOR0yCUHwDb/k=
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9/go.mod h1:147EoC+UxFadHm6fgxyR2lmvex3FE9Z5wkfiKyi40kg=
+oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d h1:fH3ARdJWTz+e+I7F6g0vx8puPYcuwuDhKAqp/DFjsZM=
+oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d/go.mod h1:147EoC+UxFadHm6fgxyR2lmvex3FE9Z5wkfiKyi40kg=
diff --git a/main.go b/main.go
index 61a4605..116bb98 100644
--- a/main.go
+++ b/main.go
@@ -20,6 +20,8 @@ func main() {
err = runAuth(args[1:])
case "embed":
err = runEmbed(args[1:])
+ case "one":
+ err = runOne(args[1:])
case "help", "--help", "-h":
printUsage()
return
@@ -90,6 +92,16 @@ ARTIFICIAL INTELLIGENCE
--full-path one vector per prefix of input
(reads stdin; writes vectors to stdout, one per line)

+ okg one
+ --model NAME override .Model in the request
+ --system-file FILE override .System with contents of FILE
+ --prompt-file FILE append FILE as a user prompt
+ --attach FILE attach an image or PDF to the prompt
+ --format FORMAT text | json | jsonindent (default: text)
+ --fast-fail preflight attachment MIME (default: on)
+ (reads stdin as a JSON MessagesRequest; flags override
+ its fields)
+
GLOBAL FLAGS
--repo REPO override auto-detected repo name
--json output raw JSON (where applicable)
@@ -110,5 +122,7 @@ EXAMPLES

Artificial intelligence
echo 'hello world' | okg embed --dims 384
+ echo Hello? > /tmp/q.txt && \
+ okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
`)
}
diff --git a/one.go b/one.go
new file mode 100644
index 0000000..c19a421
--- /dev/null
+++ b/one.go
@@ -0,0 +1,196 @@
+package main
+
+import "encoding/json"
+import "flag"
+import "fmt"
+import "io"
+import "os"
+import "strings"
+
+import "oscarkilo.com/klex-git/api"
+
+// runOne runs one LLM inference on one input. Mirrors the (now-
+// deprecated) `klex-git/one` binary.
+//
+// Reads stdin as an api.MessagesRequest JSON; empty stdin is
+// allowed (treated as {}). Flags override individual fields.
+func runOne(args []string) error {
+ fs := flag.NewFlagSet("one", flag.ContinueOnError)
+ model := fs.String("model", "",
+ "override .Model, if non-empty")
+ systemFile := fs.String("system-file", "",
+ "override .System with the contents of this file")
+ promptFile := fs.String("prompt-file", "",
+ "append this file to .Messages as a user prompt")
+ attach := fs.String("attach", "",
+ "path to a file (image or PDF) to attach to the prompt")
+ format := fs.String("format", "text",
+ "text | json | jsonindent")
+ fastFail := fs.Bool("fast-fail", true,
+ "preflight-check the attachment MIME against the model's "+
+ "llm2 capabilities; fail before paying for the call. "+
+ "Set false in tight loops to skip the extra HTTP "+
+ "round-trip per call.")
+ if err := fs.Parse(args); err != nil {
+ return err
+ }
+
+ cfg, err := loadConfig()
+ if err != nil {
+ return err
+ }
+ if cfg.ApiKey == "" {
+ return fmt.Errorf(
+ "no API key — run `okg auth login --key sk-...`")
+ }
+ client := newKlexClient(cfg)
+
+ // Parse stdin as a MessagesRequest; empty → {}.
+ sin, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fmt.Errorf("read stdin: %v", err)
+ }
+ if len(sin) == 0 {
+ sin = []byte("{}")
+ }
+ var req api.MessagesRequest
+ if err := json.Unmarshal(sin, &req); err != nil {
+ return fmt.Errorf("parse MessagesRequest: %v", err)
+ }
+
+ // Flag overrides.
+ if *model != "" {
+ req.Model = *model
+ }
+ if *systemFile != "" {
+ s, err := os.ReadFile(*systemFile)
+ if err != nil {
+ return fmt.Errorf(
+ "read --system-file %s: %v", *systemFile, err)
+ }
+ req.System = string(s)
+ }
+ if *attach != "" && *promptFile == "" {
+ return fmt.Errorf(
+ "--attach requires a non-empty --prompt-file")
+ }
+ var attachMime string
+ if *promptFile != "" {
+ msg := api.ChatMessage{Role: "user"}
+ if *attach != "" {
+ data, err := os.ReadFile(*attach)
+ if err != nil {
+ return fmt.Errorf(
+ "read --attach %s: %v", *attach, err)
+ }
+ blk := api.NewDocumentBlock(data)
+ attachMime = blk.Source.MediaType
+ msg.Content = append(msg.Content, blk)
+ }
+ p, err := os.ReadFile(*promptFile)
+ if err != nil {
+ return fmt.Errorf(
+ "read --prompt-file %s: %v", *promptFile, err)
+ }
+ msg.Content = append(msg.Content, api.ContentBlock{
+ Type: "text",
+ Text: string(p),
+ })
+ req.Messages = append(req.Messages, msg)
+ }
+
+ // Preflight: catch unsupported attachment types before the
+ // call.
+ if *fastFail && attachMime != "" {
+ if err := preflightAttachment(
+ client, req.Model, attachMime,
+ ); err != nil {
+ return err
+ }
+ }
+
+ res, err := client.Messages(req)
+ if err != nil {
+ return fmt.Errorf("klex f() failed: %v", err)
+ }
+
+ out, err := formatMessagesResponse(res, *format)
+ if err != nil {
+ return err
+ }
+ fmt.Print(out)
+ return nil
+}
+
+func formatMessagesResponse(
+ res *api.MessagesResponse, format string,
+) (string, error) {
+ switch format {
+ case "text":
+ var parts []string
+ for _, c := range res.Content {
+ if c.Type == "text" {
+ parts = append(parts, c.Text+"\n")
+ }
+ }
+ return strings.Join(parts, "\n"), nil
+ case "json":
+ buf, err := json.Marshal(res)
+ return string(buf), err
+ case "jsonindent":
+ buf, err := json.MarshalIndent(res, "", " ")
+ return string(buf), err
+ default:
+ return "", fmt.Errorf(
+ "unsupported --format=%s", format)
+ }
+}
+
+// preflightAttachment fetches the model's llm2 capabilities and
+// returns an error if it can't accept the given attachment MIME
+// type. Returns nil silently for MIME families Klex has no
+// capability flag for (anything that isn't image/* or
+// application/pdf).
+func preflightAttachment(
+ client *api.Client, modelName, mimeType string,
+) error {
+ resp, err := client.ListFuncs("latest")
+ if err != nil {
+ return fmt.Errorf(
+ "preflight ListFuncs failed "+
+ "(--fast-fail=false to bypass): %v", err)
+ }
+ var fn *api.Func
+ for i := range resp.Funcs {
+ if resp.Funcs[i].Name == modelName {
+ fn = &resp.Funcs[i]
+ break
+ }
+ }
+ if fn == nil {
+ return fmt.Errorf(
+ "unknown model %q (--fast-fail=false to bypass)",
+ modelName)
+ }
+ if len(fn.Versions) == 0 ||
+ fn.Versions[len(fn.Versions)-1].LLM2 == nil {
+ return fmt.Errorf(
+ "model %q has no llm2 config", modelName)
+ }
+ llm := fn.Versions[len(fn.Versions)-1].LLM2
+ switch {
+ case strings.HasPrefix(mimeType, "image/"):
+ if !llm.CanSeeImages {
+ return fmt.Errorf(
+ "model %q does not accept images "+
+ "(can_see_images=false)", modelName)
+ }
+ case mimeType == "application/pdf":
+ if !llm.CanSeePDFs {
+ return fmt.Errorf(
+ "model %q does not accept PDFs "+
+ "(can_see_pdfs=false)", modelName)
+ }
+ }
+ return nil
+}
a/README.md
b/README.md
1
# okg — Oscar Kilo Goodness
1
# okg — Oscar Kilo Goodness
2
2
3
A command-line tool that bundles Oscar Kilo's externally usable
3
A command-line tool that bundles Oscar Kilo's externally usable
4
services into one binary. Today: git ops against
4
services into one binary. Today: git ops against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
8
8
9
Designed for both humans and AI agents (Claude, OpenClaw) to
9
Designed for both humans and AI agents (Claude, OpenClaw) to
10
use directly from the command line.
10
use directly from the command line.
11
11
12
## Install
12
## Install
13
13
14
```bash
14
```bash
15
go install oscarkilo.com/okg@latest
15
go install oscarkilo.com/okg@latest
16
```
16
```
17
17
18
Or build from source:
18
Or build from source:
19
19
20
```bash
20
```bash
21
git clone https://code.oscarkilo.com/okg
21
git clone https://code.oscarkilo.com/okg
22
cd okg && go build .
22
cd okg && go build .
23
```
23
```
24
24
25
## Setup
25
## Setup
26
26
27
By default okg talks to the production klee host
27
By default okg talks to the production klee host
28
(`https://code.oscarkilo.com`); the only setup you need is your
28
(`https://code.oscarkilo.com`); the only setup you need is your
29
API key. `okg auth login` saves it to
29
API key. `okg auth login` saves it to
30
`~/.config/okg/config.json`.
30
`~/.config/okg/config.json`.
31
31
32
```bash
32
```bash
33
# Non-interactive (for agents and scripts).
33
# Non-interactive (for agents and scripts).
34
okg auth login --key sk-...
34
okg auth login --key sk-...
35
cat ~/.klex.key | okg auth login # equivalent, ps-safe
35
cat ~/.klex.key | okg auth login # equivalent, ps-safe
36
36
37
# Interactive (prompts for host then key).
37
# Interactive (prompts for host then key).
38
okg auth login
38
okg auth login
39
```
39
```
40
40
41
Override the host for dev/local work with `--host` on `auth login`.
41
Override the host for dev/local work with `--host` on `auth login`.
42
42
43
## Commands
43
## Commands
44
44
45
```
45
```
46
okg repo list
46
okg repo list
47
47
48
okg pr list [--state open|closed]
48
okg pr list [--state open|closed]
49
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
49
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
50
okg pr view NUMBER
50
okg pr view NUMBER
51
okg pr diff NUMBER
51
okg pr diff NUMBER
52
okg pr comment NUMBER --body BODY [--approve | --request-changes]
52
okg pr comment NUMBER --body BODY [--approve | --request-changes]
53
okg pr merge NUMBER
53
okg pr merge NUMBER
54
okg pr close NUMBER
54
okg pr close NUMBER
55
okg pr reopen NUMBER
55
okg pr reopen NUMBER
56
56
57
okg auth login [--key KEY] [--host HOST]
57
okg auth login [--key KEY] [--host HOST]
58
58
59
okg embed [--model NAME] [--dims N] [--full-path]
59
okg embed [--model NAME] [--dims N] [--full-path]
60
(reads stdin → vectors on stdout)
60
(reads stdin → vectors on stdout)
61
okg one [--model NAME] [--system-file FILE] \
62
[--prompt-file FILE] [--attach FILE] \
63
[--format text|json|jsonindent] [--fast-fail]
64
(reads stdin as a JSON MessagesRequest)
61
```
65
```
62
66
63
### Coming next
67
### Coming next
64
68
65
- `okg one` — one LLM inference on one input (from
66
`klex-git/one`).
67
- `okg exemplary` — few-shot batch runner (from
69
- `okg exemplary` — few-shot batch runner (from
68
`klex-git/exemplary`).
70
`klex-git/exemplary`).
69
71
70
Once those land, the standalone `klex-git` binaries are
72
Once that lands, the standalone `klex-git` binaries are
71
deprecated.
73
deprecated.
72
74
73
### Flags
75
### Flags
74
76
75
- `--repo REPO` overrides auto-detected repo name
77
- `--repo REPO` overrides auto-detected repo name
76
(normally parsed from `git remote get-url origin`)
78
(normally parsed from `git remote get-url origin`)
77
- `--json` outputs raw JSON for any command
79
- `--json` outputs raw JSON for any command
78
- `OKG_REPO` env var also overrides repo detection
80
- `OKG_REPO` env var also overrides repo detection
79
81
80
## Repo Detection
82
## Repo Detection
81
83
82
Like `gh`, okg detects the repo from the current directory's
84
Like `gh`, okg detects the repo from the current directory's
83
git remote:
85
git remote:
84
86
85
```
87
```
86
git remote get-url origin
88
git remote get-url origin
87
→ https://code.oscarkilo.com/widget.git
89
→ https://code.oscarkilo.com/widget.git
88
→ repo = "widget"
90
→ repo = "widget"
89
```
91
```
90
92
91
## Dependencies
93
## Dependencies
92
94
93
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
95
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
94
to be folded in once all `klex-git` binaries have moved here).
96
to be folded in once all `klex-git` binaries have moved here).
95
- Otherwise Go standard library only.
97
- Otherwise Go standard library only.
a/go.mod
b/go.mod
1
module oscarkilo.com/okg
1
module oscarkilo.com/okg
2
2
3
go 1.23
3
go 1.23
4
4
5
require oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9 // indirect
5
require oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d // indirect
a/go.sum
b/go.sum
1
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9 h1:QvENdX6T54dU1GdjYawBQQWAtVGQyvTOR0yCUHwDb/k=
1
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9 h1:QvENdX6T54dU1GdjYawBQQWAtVGQyvTOR0yCUHwDb/k=
2
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9/go.mod h1:147EoC+UxFadHm6fgxyR2lmvex3FE9Z5wkfiKyi40kg=
2
oscarkilo.com/klex-git v0.0.0-20260602231643-098e5bd263e9/go.mod h1:147EoC+UxFadHm6fgxyR2lmvex3FE9Z5wkfiKyi40kg=
3
oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d h1:fH3ARdJWTz+e+I7F6g0vx8puPYcuwuDhKAqp/DFjsZM=
4
oscarkilo.com/klex-git v0.0.0-20260604231856-880ee3e3285d/go.mod h1:147EoC+UxFadHm6fgxyR2lmvex3FE9Z5wkfiKyi40kg=
a/main.go
b/main.go
1
package main
1
package main
2
2
3
import "fmt"
3
import "fmt"
4
import "os"
4
import "os"
5
5
6
func main() {
6
func main() {
7
args := os.Args[1:]
7
args := os.Args[1:]
8
if len(args) == 0 {
8
if len(args) == 0 {
9
printUsage()
9
printUsage()
10
os.Exit(1)
10
os.Exit(1)
11
}
11
}
12
12
13
var err error
13
var err error
14
switch args[0] {
14
switch args[0] {
15
case "pr":
15
case "pr":
16
err = runPR(args[1:])
16
err = runPR(args[1:])
17
case "repo":
17
case "repo":
18
err = runRepo(args[1:])
18
err = runRepo(args[1:])
19
case "auth":
19
case "auth":
20
err = runAuth(args[1:])
20
err = runAuth(args[1:])
21
case "embed":
21
case "embed":
22
err = runEmbed(args[1:])
22
err = runEmbed(args[1:])
23
case "one":
24
err = runOne(args[1:])
23
case "help", "--help", "-h":
25
case "help", "--help", "-h":
24
printUsage()
26
printUsage()
25
return
27
return
26
default:
28
default:
27
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
29
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
28
printUsage()
30
printUsage()
29
os.Exit(1)
31
os.Exit(1)
30
}
32
}
31
33
32
if err != nil {
34
if err != nil {
33
fmt.Fprintf(os.Stderr, "error: %v\n", err)
35
fmt.Fprintf(os.Stderr, "error: %v\n", err)
34
os.Exit(1)
36
os.Exit(1)
35
}
37
}
36
}
38
}
37
39
38
func printUsage() {
40
func printUsage() {
39
fmt.Fprintf(os.Stderr, `NAME
41
fmt.Fprintf(os.Stderr, `NAME
40
okg — Oscar Kilo Goodness
42
okg — Oscar Kilo Goodness
41
43
42
SETUP
44
SETUP
43
okg auth login
45
okg auth login
44
--key KEY API key (also accepted via stdin)
46
--key KEY API key (also accepted via stdin)
45
--host HOST klee host (default: production)
47
--host HOST klee host (default: production)
46
48
47
GIT REPOS
49
GIT REPOS
48
okg repo list
50
okg repo list
49
--json output raw JSON
51
--json output raw JSON
50
52
51
okg repo create NAME
53
okg repo create NAME
52
--reader USER grant read to USER (default: anyone)
54
--reader USER grant read to USER (default: anyone)
53
55
54
PULL REQUESTS
56
PULL REQUESTS
55
okg pr list
57
okg pr list
56
--state STATE open or closed (default: open)
58
--state STATE open or closed (default: open)
57
--json output raw JSON
59
--json output raw JSON
58
60
59
okg pr create
61
okg pr create
60
--head BRANCH source branch
62
--head BRANCH source branch
61
--base BRANCH target branch (default: master)
63
--base BRANCH target branch (default: master)
62
--title TITLE PR title
64
--title TITLE PR title
63
--body BODY PR body (optional)
65
--body BODY PR body (optional)
64
--json output raw JSON
66
--json output raw JSON
65
67
66
okg pr view NUMBER
68
okg pr view NUMBER
67
--json output raw JSON
69
--json output raw JSON
68
70
69
okg pr diff NUMBER
71
okg pr diff NUMBER
70
72
71
okg pr comment NUMBER
73
okg pr comment NUMBER
72
--body BODY comment body
74
--body BODY comment body
73
--approve also approve the PR
75
--approve also approve the PR
74
--request-changes also request changes
76
--request-changes also request changes
75
77
76
okg pr merge NUMBER
78
okg pr merge NUMBER
77
--json output raw JSON
79
--json output raw JSON
78
80
79
okg pr close NUMBER
81
okg pr close NUMBER
80
--json output raw JSON
82
--json output raw JSON
81
83
82
okg pr reopen NUMBER
84
okg pr reopen NUMBER
83
--json output raw JSON
85
--json output raw JSON
84
86
85
ARTIFICIAL INTELLIGENCE
87
ARTIFICIAL INTELLIGENCE
86
okg embed
88
okg embed
87
--model NAME embedding model
89
--model NAME embedding model
88
(default: openai:text-embedding-3-small)
90
(default: openai:text-embedding-3-small)
89
--dims N number of dimensions (default: 1536)
91
--dims N number of dimensions (default: 1536)
90
--full-path one vector per prefix of input
92
--full-path one vector per prefix of input
91
(reads stdin; writes vectors to stdout, one per line)
93
(reads stdin; writes vectors to stdout, one per line)
92
94
95
okg one
96
--model NAME override .Model in the request
97
--system-file FILE override .System with contents of FILE
98
--prompt-file FILE append FILE as a user prompt
99
--attach FILE attach an image or PDF to the prompt
100
--format FORMAT text | json | jsonindent (default: text)
101
--fast-fail preflight attachment MIME (default: on)
102
(reads stdin as a JSON MessagesRequest; flags override
103
its fields)
104
93
GLOBAL FLAGS
105
GLOBAL FLAGS
94
--repo REPO override auto-detected repo name
106
--repo REPO override auto-detected repo name
95
--json output raw JSON (where applicable)
107
--json output raw JSON (where applicable)
96
108
97
EXAMPLES
109
EXAMPLES
98
Setup
110
Setup
99
okg auth login --key sk-...
111
okg auth login --key sk-...
100
cat ~/.klex.key | okg auth login
112
cat ~/.klex.key | okg auth login
101
113
102
Git repos
114
Git repos
103
okg repo list
115
okg repo list
104
okg repo create my-new-repo
116
okg repo create my-new-repo
105
117
106
Pull requests
118
Pull requests
107
okg pr list --state open
119
okg pr list --state open
108
okg pr view 42
120
okg pr view 42
109
okg pr comment 42 --body 'LGTM' --approve
121
okg pr comment 42 --body 'LGTM' --approve
110
122
111
Artificial intelligence
123
Artificial intelligence
112
echo 'hello world' | okg embed --dims 384
124
echo 'hello world' | okg embed --dims 384
125
echo Hello? > /tmp/q.txt && \
126
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
113
`)
127
`)
114
}
128
}
/dev/null
b/one.go
1
package main
2
3
import "encoding/json"
4
import "flag"
5
import "fmt"
6
import "io"
7
import "os"
8
import "strings"
9
10
import "oscarkilo.com/klex-git/api"
11
12
// runOne runs one LLM inference on one input. Mirrors the (now-
13
// deprecated) `klex-git/one` binary.
14
//
15
// Reads stdin as an api.MessagesRequest JSON; empty stdin is
16
// allowed (treated as {}). Flags override individual fields.
17
func runOne(args []string) error {
18
fs := flag.NewFlagSet("one", flag.ContinueOnError)
19
model := fs.String("model", "",
20
"override .Model, if non-empty")
21
systemFile := fs.String("system-file", "",
22
"override .System with the contents of this file")
23
promptFile := fs.String("prompt-file", "",
24
"append this file to .Messages as a user prompt")
25
attach := fs.String("attach", "",
26
"path to a file (image or PDF) to attach to the prompt")
27
format := fs.String("format", "text",
28
"text | json | jsonindent")
29
fastFail := fs.Bool("fast-fail", true,
30
"preflight-check the attachment MIME against the model's "+
31
"llm2 capabilities; fail before paying for the call. "+
32
"Set false in tight loops to skip the extra HTTP "+
33
"round-trip per call.")
34
if err := fs.Parse(args); err != nil {
35
return err
36
}
37
38
cfg, err := loadConfig()
39
if err != nil {
40
return err
41
}
42
if cfg.ApiKey == "" {
43
return fmt.Errorf(
44
"no API key — run `okg auth login --key sk-...`")
45
}
46
client := newKlexClient(cfg)
47
48
// Parse stdin as a MessagesRequest; empty → {}.
49
sin, err := io.ReadAll(os.Stdin)
50
if err != nil {
51
return fmt.Errorf("read stdin: %v", err)
52
}
53
if len(sin) == 0 {
54
sin = []byte("{}")
55
}
56
var req api.MessagesRequest
57
if err := json.Unmarshal(sin, &req); err != nil {
58
return fmt.Errorf("parse MessagesRequest: %v", err)
59
}
60
61
// Flag overrides.
62
if *model != "" {
63
req.Model = *model
64
}
65
if *systemFile != "" {
66
s, err := os.ReadFile(*systemFile)
67
if err != nil {
68
return fmt.Errorf(
69
"read --system-file %s: %v", *systemFile, err)
70
}
71
req.System = string(s)
72
}
73
if *attach != "" && *promptFile == "" {
74
return fmt.Errorf(
75
"--attach requires a non-empty --prompt-file")
76
}
77
var attachMime string
78
if *promptFile != "" {
79
msg := api.ChatMessage{Role: "user"}
80
if *attach != "" {
81
data, err := os.ReadFile(*attach)
82
if err != nil {
83
return fmt.Errorf(
84
"read --attach %s: %v", *attach, err)
85
}
86
blk := api.NewDocumentBlock(data)
87
attachMime = blk.Source.MediaType
88
msg.Content = append(msg.Content, blk)
89
}
90
p, err := os.ReadFile(*promptFile)
91
if err != nil {
92
return fmt.Errorf(
93
"read --prompt-file %s: %v", *promptFile, err)
94
}
95
msg.Content = append(msg.Content, api.ContentBlock{
96
Type: "text",
97
Text: string(p),
98
})
99
req.Messages = append(req.Messages, msg)
100
}
101
102
// Preflight: catch unsupported attachment types before the
103
// call.
104
if *fastFail && attachMime != "" {
105
if err := preflightAttachment(
106
client, req.Model, attachMime,
107
); err != nil {
108
return err
109
}
110
}
111
112
res, err := client.Messages(req)
113
if err != nil {
114
return fmt.Errorf("klex f() failed: %v", err)
115
}
116
117
out, err := formatMessagesResponse(res, *format)
118
if err != nil {
119
return err
120
}
121
fmt.Print(out)
122
return nil
123
}
124
125
func formatMessagesResponse(
126
res *api.MessagesResponse, format string,
127
) (string, error) {
128
switch format {
129
case "text":
130
var parts []string
131
for _, c := range res.Content {
132
if c.Type == "text" {
133
parts = append(parts, c.Text+"\n")
134
}
135
}
136
return strings.Join(parts, "\n"), nil
137
case "json":
138
buf, err := json.Marshal(res)
139
return string(buf), err
140
case "jsonindent":
141
buf, err := json.MarshalIndent(res, "", " ")
142
return string(buf), err
143
default:
144
return "", fmt.Errorf(
145
"unsupported --format=%s", format)
146
}
147
}
148
149
// preflightAttachment fetches the model's llm2 capabilities and
150
// returns an error if it can't accept the given attachment MIME
151
// type. Returns nil silently for MIME families Klex has no
152
// capability flag for (anything that isn't image/* or
153
// application/pdf).
154
func preflightAttachment(
155
client *api.Client, modelName, mimeType string,
156
) error {
157
resp, err := client.ListFuncs("latest")
158
if err != nil {
159
return fmt.Errorf(
160
"preflight ListFuncs failed "+
161
"(--fast-fail=false to bypass): %v", err)
162
}
163
var fn *api.Func
164
for i := range resp.Funcs {
165
if resp.Funcs[i].Name == modelName {
166
fn = &resp.Funcs[i]
167
break
168
}
169
}
170
if fn == nil {
171
return fmt.Errorf(
172
"unknown model %q (--fast-fail=false to bypass)",
173
modelName)
174
}
175
if len(fn.Versions) == 0 ||
176
fn.Versions[len(fn.Versions)-1].LLM2 == nil {
177
return fmt.Errorf(
178
"model %q has no llm2 config", modelName)
179
}
180
llm := fn.Versions[len(fn.Versions)-1].LLM2
181
switch {
182
case strings.HasPrefix(mimeType, "image/"):
183
if !llm.CanSeeImages {
184
return fmt.Errorf(
185
"model %q does not accept images "+
186
"(can_see_images=false)", modelName)
187
}
188
case mimeType == "application/pdf":
189
if !llm.CanSeePDFs {
190
return fmt.Errorf(
191
"model %q does not accept PDFs "+
192
"(can_see_pdfs=false)", modelName)
193
}
194
}
195
return nil
196
}