# alpaca
Unix native interface for interacting with LLMs.
`alpaca` is a fork of [`cogni`](https://github.com/leoshimo/cogni) by
[leoshimo](https://github.com/leoshimo).
## Focus
`alpaca` brings language model scripting (prompting) into the familiar Unix
environment. It focuses on:
- ergonomics and accessibility in the Unix shell
- composability and interop with other programs, including `alpaca` itself
- easy language model programming, both ad-hoc and repeatable
`alpaca` reads and writes standard streams, so it works with files, editor
buffers, clipboards, system logs, sockets and many external tools, with no
special integrations needed.
## Features
`alpaca` gives you:
- a Unix-minded design (input and output redirection, composability, interop)
- ad-hoc language model scripting
- flexible input and output formats (text, JSON, transcript)
- a standalone binary, with no Python required
- support for any compatible chat endpoint, hosted or local
- editor-backed prompting and provider quota reporting in the same binary
## Non-features
`alpaca` is not built for interactive use. Invoke it from within interactive
environments instead, such as REPLs and Emacs.
## Installation
Building requires Rust and `scdoc`:
```sh
$ make
$ make test
$ make install
```
`PREFIX` defaults to `/usr/local`. You can override `DESTDIR`, `BINDIR` and
`MANDIR` for packaging.
## Setup
`alpaca` reads an API key from `--apikey` or, more simply, the `ALPACA_API_KEY`
environment variable:
```sh
# in shell configuration
export ALPACA_API_KEY=your-api-key
```
`alpaca` sends requests to `https://api.openai.com/v1` by default. Set
`ALPACA_ENDPOINT` or pass `--base-url` to target another host, such as a local
server:
```sh
export ALPACA_ENDPOINT=http://localhost:11434
```
The base URL sets the dialect of the endpoint. A base URL ending in a
version segment, such as `https://host/v1`, gets `/chat/completions`: this
covers any OpenAI-compatible endpoint, hosted or local, not just OpenAI
itself. Any other base URL gets `/api/chat`, the Ollama dialect. `alpaca`
sends authorization only when it knows a key, so endpoints needing none also
work.
Pick a model with `-m/--model` (default `gpt-4o-mini`). Model identifiers are
whatever your endpoint serves.
Shared settings, honoured by every subcommand:
| variable | meaning | default |
| --- | --- | --- |
| `ALPACA_API_KEY` | API key | none |
| `ALPACA_ENDPOINT` | base URL | `https://api.openai.com/v1` |
| `ALPACA_MODEL` | model | `gpt-4o-mini` |
| `ALPACA_EFFORT` | reasoning effort | `none` |
| `ALPACA_TIMEOUT` | request timeout, in seconds | `60` |
## Configuration
Put shared settings in `$XDG_CONFIG_HOME/alpaca/config.toml`, or
`~/.config/alpaca/config.toml` if `XDG_CONFIG_HOME` is unset.
Settings take priority in this order:
- command-line options
- environment variables
- the configuration file
- built-in defaults
See `config.example.toml` for every supported key, and `alpaca-config(5)` for
how each command uses them. Keep the file private if it holds an `apikey`.
A `[profile-name]` table defines a profile: it inherits top-level settings
and overrides only the keys it sets. `default_profile` activates a profile
automatically. `--profile` overrides that:
```toml
model = "gpt-oss:120b"
base_url = "https://ollama.com"
default_profile = "ollama-cloud"
[ollama-cloud]
apikey = "your-ollama-cloud-key"
base_url = "https://ollama.cloud"
[deepinfra]
model = "llama-3.1-70b"
```
```sh
alpaca -u "Hello"
```
## Commands
```
alpaca [OPTIONS] [FILE] # chat, the default command
alpaca chat [OPTIONS] [FILE] # the same thing, named
alpaca quota [OPTIONS] # provider quota usage
alpaca compose [OPTIONS] # edit prompts in $EDITOR until an empty buffer is saved
```
`alpaca` treats a first argument matching a subcommand name as that
subcommand. Read a file with such a name using `--`, as in `alpaca -- quota`.
---
## Basic usage
See `alpaca --help` for documentation.
```sh
# Via stdin
$ echo "What is 50 + 50?" | alpaca
50 + 50 equals 100.
# Via file
$ echo "What is 50 + 50?" > input.txt
$ alpaca input.txt
50 + 50 equals 100.
# Via flags
# -s, --system <MSG> Sets system prompt (Always first)
# -a, --assistant <MSG> Appends assistant message
# -u, --user <MSG> Appends user message
$ alpaca --system "Solve the following math problem" --user "50 + 50"
50 + 50 equals 100.
# Via repetitions of same flags. Useful for few-shot prompting
$ alpaca --system "Solve the following math problem" \
-u "1 + 1" \
-a "2" \
-u "22 + 20" \
-a "42" \
-u "50 + 50"
100
# Via both flags and stdin. Flag messages come before stdin / file
$ echo "50 + 50" | alpaca --system "Solve the following math problem" \
-u "1 + 1" \
-a "2" \
-u "22 + 20" \
-a "42"
100
```
---
## alpaca compose
`alpaca compose` edits a prompt in `$VISUAL`, `$EDITOR`, or `vim`, sends the
saved text, then opens the editor again with the reply in the buffer, ready
for the next prompt. It keeps the conversation going until you save an empty
buffer. Piped input fills the buffer first. The editor runs on the
controlling terminal, so editor input and output stay out of the pipeline.
If the process has no controlling terminal, the editor inherits `alpaca`'s
streams. Saving an empty buffer sends nothing and ends the session.
```sh
# Chat in the editor until you save an empty buffer
$ alpaca compose
# Pick a model and reasoning effort
$ alpaca compose -m gpt-oss:120b --reasoning-effort high
# Set a system prompt
$ alpaca compose -s "Answer in one sentence"
# Edit piped input in the editor before sending
$ git diff --staged | alpaca compose -s "Write a conventional commit message"
# Sit in the middle of a pipeline: pipe in, edit, pipe out
$ curl -s "wttr.in/?1" | alpaca compose -s "Summarize this weather in 1 sentence" | say
```
Options:
- `-m, --model <MODEL>`: model to use
- `-s, --system <MSG>`: system prompt
- `-t, --temperature <TEMP>`: sampling temperature
- `-T, --timeout <SECS>`: request timeout in seconds
- `--reasoning-effort <low|medium|high|none>`: reasoning effort
- `--apikey <KEY>`, `--base-url <URL>`: endpoint settings
- `--json`, `--jsonp`: print the response as JSON instead of the reply text
`$VISUAL` and `$EDITOR` split on whitespace, with no quote or escape parsing,
so use a wrapper script for complex editor commands. `alpaca` always removes
the temporary file. When the editor exits nonzero, `alpaca` exits with that
same status.
`alpaca` saves each successful request as a JSON transcript in
`$XDG_DATA_HOME/alpaca` (or `~/.local/share/alpaca`), mode 0600 in a mode
0700 directory. Each turn sends the whole conversation as context.
When the editor is `vim` or `neovim`, `alpaca` keeps one editor open for the
whole conversation instead of reopening it each turn. Writing the buffer
sends the prompt, and the reply replaces the buffer in place, so every turn
happens in the same window. Quit the editor to end the session. Writing an
empty buffer sends nothing. The editor reloads the buffer through `autoread`
and `checktime`, so a reply that arrives while you have unsaved changes
waits for your next write. In this mode, `alpaca` writes the reply to
standard output only when standard output is not a terminal, since the
editor owns the screen.
See `man/alpaca-compose.1`.
---
## alpaca quota
`alpaca quota` shows how much quota you have used: a bar per window, with a
countdown to its reset, and for some providers a table of per-model counts.
Pick the provider with `-p`. Without it, alpaca reports all providers in one
output and skips the ones it cannot reach:
```sh
$ alpaca quota # all providers
$ alpaca quota -p synthetic # Synthetic
$ alpaca quota -p anthropic # Claude Code
$ alpaca quota -p openai # OpenAI organization usage
$ alpaca quota -p deepinfra # DeepInfra
$ alpaca quota -p codex # OpenAI Codex
```
A provider with no usable credential, or a failed request or response, is
left off the screen. The rest are still drawn, and alpaca exits successfully.
With `-p`, failures for that provider are reported instead. Without `-p`,
`--apikey` and `--base-url` apply to all providers. `--raw` requires `-p`.
Each provider has its own default credential and base URL:
| provider | default credential | base URL |
| --- | --- | --- |
| `synthetic` | `$SYNTHETIC_API_KEY` | `https://api.synthetic.new` |
| `anthropic` | none | `https://api.anthropic.com` |
| `openai` | none | `https://api.openai.com` |
| `deepinfra` | `$DEEPINFRA_API_KEY` | `https://api.deepinfra.com` |
| `codex` | the OAuth token in `$CODEX_HOME/auth.json` | `https://chatgpt.com` |
Every provider also accepts `--apikey`, an explicit `--profile`, or a table
named after the provider, such as `[anthropic]`. Anthropic and OpenAI have no
default credential, so one of these is required.
The Anthropic endpoint serves the Claude Code CLI rather than a documented
public API, so it may change without notice. It expects the same short-lived
OAuth token the CLI uses: take a fresh one from the CLI and pass it again
once it expires.
The OpenAI endpoint is the documented organization Usage API, reporting spend
and token usage for the whole organization rather than the rate limit for one
account. It needs an Admin API key with the `api.usage.read` scope, from
`platform.openai.com/settings/organization/admin-keys`: a regular project key
(`sk-proj-...`) gets a 403.
DeepInfra bills per token or per second, with no fixed quota, so alpaca
prints no window for it. It instead prints the remaining credit as a note.
A negative balance is funds ready to spend, and a positive one is money
owed.
The Codex provider reports your ChatGPT plan usage, not organization spend.
alpaca reads the OAuth token the Codex CLI stores in `$CODEX_HOME/auth.json`
(default `~/.codex/auth.json`), so sign in once with `codex` and the report
works; `--apikey` replaces that token. The endpoint serves the Codex CLI and
the ChatGPT client, so it is not a documented public API and may change
without notice. alpaca prints the 5 hour and weekly windows with their reset
times, the plan name, and the credits balance when the plan has one.
The top-level `base_url` configures the chat endpoint only, so `alpaca
quota` never uses it: each provider already has the correct endpoint built
in. A table named after a provider is picked up automatically, but only for
its `apikey`. Its `base_url`, if set, is assumed to be for chat, not quota.
To deliberately override the endpoint for a provider too, select a profile
by name with `--profile` instead. `default_profile` does not apply here.
Command-line options and `ALPACA_ENDPOINT` still override any profile.
Synthetic and DeepInfra also fall back to the top-level `apikey` when nothing
more specific names one, since both double as chat endpoints. Anthropic and
OpenAI never serve chat, so they ignore the top-level `apikey` and always
need a key from a table or profile. Without a chosen provider, alpaca
resolves each provider key on its own, exactly as an explicit `-p` would,
never sending the top-level `apikey` to them all.
Options:
- `-p, --provider <PROVIDER>`: one of `synthetic`, `anthropic`, `openai`,
`deepinfra`, `codex`. Without it, all providers are reported and the
unreachable ones are omitted
- `-T, --timeout <SECS>`: request timeout in seconds
- `--apikey <KEY>`: API key or OAuth token, replacing the provider default
- `--base-url <URL>`: base URL, replacing the provider default
- `--raw`: print the provider response body verbatim. Requires `-p`
- `--json`, `--jsonp`: print the normalised view as JSON. Without `-p`, one
object keyed by provider
- `--color <auto|always|never>`: colour policy. `auto` colours only when
standard output is a terminal and `NO_COLOR` is unset
See `man/alpaca-quota.1`.
---
## Tour of alpaca
Examples to get you started.
Whatever you feed `alpaca` is sent to the endpoint you configure. Point
`ALPACA_ENDPOINT` at a local server if the data should not leave your machine.
### In the shell
```sh
# Creating Summary of Meeting Transcripts
$ cat meeting_saved_chat.txt \
| alpaca -s "Extract the links mentioned in this transcript, and provide a high level summary of the discussion points"
# Narrate Weather Summary
$ curl -s "wttr.in/?1" \
| alpaca -s "Summarize today's weather using the output. Respond in 1 short sentence." \
| say
# Create a ffmpeg cheatsheet from man page
$ man ffmpeg \
| alpaca -T 300 -s "Create a cheatsheet given a man page. Output should be in Markdown, and should be a set of example usages under headings." \
> cheatsheet.md
# Create a commit message for staged changes
$ git diff --staged \
| alpaca -s "Create a commit message for the given staged changes. Use conventional commit format. Answer in a single-line raw plaintext. Don't use markdown." \
| git commit -F -
```
### In Emacs
Emacs can pipe buffer regions to `alpaca` with `shell-command-on-region`. The
following command sends the selected region to `alpaca`, optionally
replacing the original text:
```emacs-lisp
(defun leoshimo/alpaca-on-region (start end prompt replace)
"Run alpaca on region. Prefix arg means replace region, instead of separate output buffer"
(interactive "r\nsPrompt: \nP")
(shell-command-on-region start end
(format "alpaca -s \"%s\"" prompt)
nil replace))
(global-set-key (kbd "M-c") #'leoshimo/alpaca-on-region)
```
Use this binding for tasks such as:
- normalising nonuniform text, such as unstructured logs to structured JSON events
- editing or organising text semantically, such as rewording or grouping by category
- generating a summary for an Org Agenda document
### In Vim
Vim runs external shell commands on the whole buffer or a visual selection
too, giving similar workflows to Emacs. See `h :!` in Vim.
For example, to sort a bulleted list of fruits by colour:
1. Select the list of fruits in visual mode.
2. Type `:!alpaca -s "Sort this list by color"`.