It's a big difference from your usual terse style! But interesting use case.
John -- Sent from my Mac 512Ke On May 19, 2026, Alexander Burger <[email protected]> wrote: Hi all, With PicoLisp 26.5.19 there are two new files in the distribution: 1. `lib/ai.l` – a small OpenAI client 2. `lib/vip/ai.rc.l` – a Vip plugin implementing a chat interface Below is a short overview and some comments on the more interesting parts of the code. ## 1. Library `lib/ai.l` This is a minimal JSON/OpenAI client. At the moment it only supports the `chat/completions` endpoint, but is structured so more endpoints can be added easily. # 13may26 Software Lab. Alexander Burger (symbols 'ai 'pico) # https://developers.openai.com/api/reference/overview # https://platform.deutschlandgpt.de/docs (local) (*ApiUrl *ApiKey *Max *Result curl result res api models completions) Everything runs in the `ai` namespace, with a few globals: - `*ApiUrl` – base host, e.g. `api.openai.com/v1/` - `*ApiKey` – bearer token - `*Max` – optional `max_tokens` limit (used by the Vip plugin) - `*Result` – last parsed JSON response as a tree of PicoLisp pairs ### The `curl` wrapper (de curl (Res . @) (pass list "curl" "-s" (pack "https://" *ApiUrl Res) "-H" (pack "Authorization: Bearer " *ApiKey) ) ) — `curl` composes an argument list for an external `curl` call. — `Res` is appended to the base URL (`*ApiUrl`), so the same helper can be used for different endpoints (`"models"`, `"chat/completions"`, etc.). — Extra arguments (like `"--json" "@-"`) are passed via `. @`. ### Reading and post-processing the JSON (de result @ (when (setq *Result (readJson)) (pass res) ) ) (de res @ (let (Rest *Result Lst (rest)) (recur (Lst Rest) (loop (NIL Lst Rest) (setq Rest (get Rest (++ Lst))) (T (and (pair Rest) (pair (caar @))) (mapcar '((Rest) (recurse Lst Rest)) Rest ) ) ) ) ) ) Here are two interesting aspects: 1. `result`: — Reads the entire JSON reply from stdin via `readJson`. — Stores it globally in `*Result`. — Then calls `res` with any symbol path given (e.g. `'choices 'message 'content`). 2. `res`: — `*Result` is a tree of JSON data (lists and conses). — `res` interprets its arguments as a *path* into this tree: — `(res 'choices 'message 'content)` walks recursively and collects all matching fields. — It uses `(get Rest (++ Lst))` and a `recur` to traverse arbitrarily nested objects/arrays, and returns a flattened list of all matches. — This gives a very compact “query language” over the JSON tree. ### API configuration # (api 'file) (de api (File) (in File (setq *ApiUrl (line T) *ApiKey (line T)) (line T) ) ) — Reads the first two lines of `File`: the API URL and key. — A third line is read and returned. It may hold a default model, I use it in some scripts. — The Vip plugin expects this to live in `~/.ai`. ### Models listing # (models) (de models () (in (curl "models") (result 'data 'id) ) ) — Calls `GET /models`. — Extracts all `id` values below `data` using the `result/res` mechanism. — Return value is a list of strings (model IDs). ### Chat completions # (completions 'model 'role 'text 'temp ['role 'text 'temp ..] (de completions (Model . @) (pipe (out (curl "chat/completions" "--json" "@-") (printJson (list (cons 'model Model) (make (link 'messages T) (while (args) (link (make (link (cons 'role (next)) (cons 'content (next)) ) (and (next) (link (cons 'temperature @))) (and *Max (link (cons 'max_tokens @))) ) ) ) ) ) ) ) (result 'choices 'message 'content) ) ) Key points: — The function interface is compact: `(completions "gpt-4.1" "user" "Hello" 2 "system" "You are ..." NIL ...)` — Arguments are consumed in triples: role, content, temperature. — `printJson` builds a JSON object like: { "model": "gpt-4.1", "messages": [ { "role": "user", "content": "Hello", "temperature": 2, "max_tokens": 200 }, ... ] } (`max_tokens` is injected from the global `*Max` when present.) — `pipe`: — Sends the JSON to `curl` on stdin (`"--json" "@-"`). — Then reads the JSON response from `curl`’s stdout and returns all `choices.message.content` strings. So you get a pure PicoLisp function to call the OpenAI chat completions endpoint, with usable defaults and a simple path mechanism into the reply. ## 2. Vip plugin `lib/vip/ai.rc.l` The second file integrates the above into the Vip editor as a mini chat client bound to a buffer. # 16may26 Software Lab. Alexander Burger (symbols '(pico) (load "@lib/json.l" "@lib/ai.l") ) (symbols '(ai vip pico)) — Loads the JSON and AI libraries. — Switches to a combined namespace so `ai`, `vip` and core `pico` symbols are all visible. The plugin defines three commands: `:models`, `:ai`, and `:ai?`. ### `:models` – list model IDs (cmd "models" (L Lst Cnt) (symbols '(ai vip pico) (api "~/.ai") (prCmd (sort (extract '((S) (chop (pre? L S))) (models) ) ) ) ) ) — Calls `(api "~/.ai")` to set URL and key. — `(models)` returns all available models. — `L` is the optional prefix typed after `:models`. — `pre?` filters only those model IDs starting with that prefix. — They are sorted and printed with `prCmd` in the Vip command area. ### Conversation file parsing in `:ai` (cmd "ai" (L Lst Cnt) (symbols '(ai vip pico) (api "~/.ai") (let (Lst (split (make (for L (: buffer text) (when (or (= '("+" "+" "+") L) (and (head '("+" "+" "+" " ") L) (format (cadr (split L " "))) ) (= '("=" "=" "=") L) ) (link 0) ) (link L) ) 0 ) L (split (caar Lst) " ") ) (setq *Model (pack (car L)) *Max (format (cadr L)) ) ... Interesting details: — `(: buffer text)` holds all lines of the current buffer. — The code inserts the marker `0` into the stream whenever it finds: — a line `+++` (start of user prompt), — a line `+++ <temp>` (prompt with explicit temperature, e.g. `+++ 1`), — or a line `===` (start of assistant response). — Then `(split ... 0)` splits the whole buffer into chunks at these zero markers, giving a list of sections, each corresponding to one “block” (system messages, prompts, responses). — `caar Lst` is the first block, which contains the first line: `model [max_tokens]`. — `L` is that line split by spaces. — `*Model` is set from the model name. — `*Max` is optionally set from the second item (token limit). So the *file format* is: <model> [max_tokens] [optional system text...] +++ <prompt 1> === <response 1> +++ <prompt 2> Temperature is given on the `+++` line, optionally, like: +++ 2 This prompt uses temperature 2 #### Building messages for the API call Continuing inside `:ai`: (let? R (or (mapcan '((S) (split (chop S) "\n")) (apply completions (mapcan '((L) (let? S (glue "\n" (cdr L)) (cond ((head '("+" "+" "+") (setq L (car L))) (list "user" S (format (cadr (split L " ")))) ) ((head '("=" "=" "=") L) (list "assistant" S NIL) ) (T (list "system" S NIL)) ) ) ) Lst ) *Model ) ) (mapcar '((X) (chop (cdr X))) (fish '((X) (and (pair X) (memq (car X) '(error message)) (str? (cdr X)) ) ) *Result ) ) ) — Each section in `Lst` is a list where: — `car L` is the marker line (`+++`, `+++ 1`, or `===`). — `cdr L` is the actual multi-line text in that section. — For each such block: — `S` is glued together with `"\n"` to preserve multi-line prompts/responses. — The role is chosen from the marker: — `+++`* → `"user"` and temperature parsed from that line, if present. — `===` → `"assistant"`. — otherwise → `"system"` (the initial text after the first line). — The resulting triples `(role content temp)` are flattened with `mapcan` and passed to `completions` along with `*Model`. If the API call returns successfully, `R` becomes a list of reply strings. These are split into lines again so we can paste them nicely into the buffer. If there is an error, the second branch: (mapcar '((X) (chop (cdr X))) (fish '((X) (and (pair X) (memq (car X) '(error message)) (str? (cdr X)) ) ) *Result ) ) — Searches `*Result` for any `(error . "...")` or `(message . "...")` string pairs. — Returns them as lines instead, so the error text is visible in the buffer. #### Pasting the response into the buffer (move 'goAbs 1 T) (paste (make (link T (chop (if (res 'error) "???" "==="))) (chain R) (unless (res 'error) (and (last R) (link NIL)) (link (chop "+++") NIL) (evCmd (cons (res 'usage 'total_tokens) (res 'choices 'finish_reason) ) ) ) ) 1 ) (=: buffer fmt *Columns) ) ) ) ) — The cursor is moved to the end (`goAbs 1 T`). — `paste` inserts: — A marker line: — `"???"` if there was an error, — or `"==="` otherwise (start of assistant’s response). — All response lines (`R`). — When there is no error, appends: — An empty line (if needed), — A `+++` marker for the next user prompt. — Additionally, it calls `evCmd` with `(total_tokens . finish_reason)` extracted from the response to show token usage info in the Vip REPL. — Finally, buffer formatting is recalculated (`=: buffer fmt *Columns`). Thus the buffer remains a self-contained, incremental chat log that can be re-sent in full at any time. ### `:ai?` – show raw JSON (cmd "ai?" (L Lst Cnt) (symbols '(ai vip pico) (evCmd (pretty *Result)) ) ) — Dumps `*Result` (the last JSON response) as a nicely formatted s-expression using `pretty`. — Useful for debugging, to see all returned fields from OpenAI. ## 3. Conversation file structure (recap) Per-buffer chat format: 1. First line: model, optionally followed by max token limit gpt-4.1-nano 1024 2. Optional system description lines (the “role” / behavior of the model). 3. `+++` starts a user prompt. — Optionally `+++ N` with integer `N` as temperature. 4. `===` starts an assistant response. 5. `:ai`: — Sends the entire conversation so far (system, all prompts and responses). — Appends the new assistant response plus another `+++` for the next prompt. 6. `:models [prefix]`: — Shows available models, optionally filtered by prefix. 7. `:ai?`: — Shows raw JSON response. Example session (shortened): gpt-4.1-nano +++ What is the capital of Bavaria? === ; after :ai The capital of Bavaria is Munich. +++ How many inhabitants does it have? === ; after another :ai As of 2023, Munich has a population of approximately 1.5 million inhabitants. +++ Have fun! Next endpoint will probably be "responses". ☺/ A!ex P.S. I produced this mail by editing the attached file "Chat" with Vip in the way described above. I only changed formatting a little and fixed two or three minor errors.
