Eaon docs
The desktop app, the terminal agent, and the HTTP API, in one place. I took every request shape and response field here from the running service. Where the service today differs from the design, this page tells you which one you are looking at.
Quick start
Three steps, and the third one is optional.
-
1
Download and open it
Grab the build for your platform from the download section, drag it into Applications on macOS, and open it. macOS will need one extra step the first time, covered below.
-
2
Start typing
Eaon ships connected, so there is no key to paste and no account to make before your first message. Pick a model from the picker at the top and ask it something.
-
3
Make it yours, if you want
Add your own provider key for direct billing, or install a local model so nothing leaves the machine.
Install on macOS
- Open the
.dmg. - Drag Eaon into your Applications folder.
- Open it from Applications.
Requires macOS 14 Sonoma or later. The build is Universal, so Apple silicon and Intel each run their own native slice.
Opening it the first time
Nothing to do. From 2026.4.0 onward Apple signs and notarizes Eaon, and the notarization ticket ships stapled inside the download. Drag it to Applications and double-click. No right-click trick, no Terminal command, nothing to approve under Privacy & Security. The ticket travels with the app, so it opens on a Mac that has never touched the internet.
If you are updating from an older build
macOS ties the permissions you granted to an app's signature, and Eaon's changed when it became signed. So macOS asks you again for Accessibility, Automation and microphone access, once each, the first time it needs them. Approve them and they stay approved.
Builds before 2026.4.0
Older downloads were unsigned and macOS blocked them on first open. If you are running one of those, the fix is simply to download the current version.
Windows and Linux
Both come from the cross-platform build. They ship on their own release tag, which today sits one version behind the Mac builds.
Windows
Run the x64-setup.exe installer. It is not code-signed yet, so
SmartScreen shows a blue "Windows protected your PC" box on first run: click
More info, then Run anyway. That is expected, not a sign anything is
wrong.
Linux
Use the .AppImage. Make it executable and run it, with no install step
and no package manager involved.
chmod +x Eaon_2026.4.0_amd64.AppImage ./Eaon_2026.4.0_amd64.AppImage
Picking a model
The picker at the top of the window groups every model Eaon can reach:
- Hosted by Eaon: no key, no account, nothing to configure.
- Your own keys: anything you added under Settings, billed to you.
- On this Mac: whatever Ollama, llama.cpp or MLX has already pulled.
Switching mid-conversation is fine. The thread comes with you and the next reply arrives from the model you just chose, with a small header naming it so a mixed thread stays readable.
Using your own API key
- Open Settings → Add provider.
- Pick the company and paste the key. Eaon fetches the models you have access to.
- They appear in the picker under that provider's name.
Eaon puts your keys in the macOS Keychain, beside the rest of your passwords, and sends one only as an authorization header on a request you made. No plain-text file, and no copy on Eaon's servers.
Running models locally
Eaon drives three local runtimes and detects whichever you already have:
- Ollama is the easiest. Install it, pull a model, and Eaon finds it.
- llama.cpp takes any GGUF from Hugging Face, or a file already on disk.
- MLX is Apple-silicon-native. Install it with
pip3 install mlx-lm.
# install Ollama from ollama.com, then: ollama pull deepseek-r1:7b # Eaon picks it up automatically — no restart needed
With a local model loaded, nothing leaves the machine. Not the prompt, not the reply. You can pull the network cable and keep working.
The command palette
⌘K opens a command bar. Jump to any conversation, switch the model, flip the theme, or drop straight into a settings page, all with the arrow keys and Enter.
| Keys | Does |
|---|---|
| ⌘N | New chat |
| ⌘K | Search chats and run commands |
| ⌘P | New project |
| ⌘\ | Toggle the sidebar |
| ↵ | Send |
| ⇧↵ | New line |
| ⌥Space | Toggle the floating assistant |
Projects and history
- Projects group related chats so a week on one thing does not bury everything else.
- History is files on your disk. Export to Markdown or JSON, import it back, or delete the lot from Privacy settings.
- Pinned chats sit above the by-date list.
Eaon CLI
An agentic coding agent in your terminal: plan mode, sub-agents and real tools, for any model, local or hosted. Nothing to install if you have Node.
# run it without installing npx eaon-cli # or install it globally — note the command is `eaon`, not `eaon-cli` npm install -g eaon-cli eaon
- Package
eaon-clion npm- Command
eaon- Requires
- Node 18.17 or newer
- Licence
- MIT
On first run it asks you to set up a key, opening a local page in your browser where you can paste an Eaon key, add your own provider keys, or import what Eaon Desktop already has. Press Esc to skip that and use a local Ollama model instead.
It needs an interactive terminal. For scripts and CI, use -p with
--auto, which is the only non-interactive form it accepts.
eaon -p "summarise what changed in the last commit" --auto
CLI commands
Type these at the prompt. Type an unrecognised /name and it goes to the
model as ordinary text instead of raising an error.
| Command | Does |
|---|---|
| /help | Every command and shortcut |
| /model [name] | Switch model; no argument opens a picker |
| /models | List hosted, your-own-key and local models |
| /pull <name> | Download an Ollama model |
| /permission [plan|sandboxed|auto] | Change what it may do without asking. Alias /perm |
| /plan | Jump straight to plan mode |
| /link | Set keys and providers in the browser |
| /diff | Show uncommitted git changes |
| /rewind [id] | Restore an earlier point; no argument lists them |
| /resume [id] | Reopen a session. Alias /sessions |
| /clear, /new | Start a fresh session |
| /compact | Summarise the conversation to reclaim context |
| /context | What is currently in context |
| /cost | Token and cost estimate for this session |
| /status | Model, mode and session state. Alias /stats |
| /init | Write an EAON.md for this project |
| /memory | Open EAON.md |
| /export [path] | Save the transcript as Markdown |
| /copy | Copy the last reply |
| /bashes | Background commands. Alias /jobs |
| /doctor | Check Node, Ollama, your key and the config file |
| /config | Config path and settings, keys redacted |
| /exit | Quit. Alias /quit |
Input prefixes
The CLI handles ! and # itself. Neither reaches the model,
and neither interrupts a turn in flight.
| Prefix | Does |
|---|---|
| !command | Run a shell command; the output joins the conversation |
| @path | Reference a file, with autocomplete. Its contents are included |
| #note | Append a note to the project's EAON.md |
| /name | A slash command, with autocomplete |
Keys
| Keys | Does |
|---|---|
| ⇧Tab | Cycle plan → sandboxed → auto |
| Esc | Interrupt the current turn |
| ↵ while working | Queue the message instead of interrupting |
| Tab | Accept the highlighted completion |
| ↑ ↓ | Command history |
| CtrlC twice | Exit |
\ then ↵ | Newline in the composer |
CLI flags
| Flag | Does |
|---|---|
| -v, --version | Print the version |
| -h, --help | Print usage |
| -p, --print <prompt> | One-shot, non-interactive. Reply on stdout, tool and status lines on stderr. Requires --auto |
| --model <key> | Start with a given model, e.g. ollama:deepseek-r1:7b |
| --auto | Start in auto permission mode, skipping confirmations |
| --permission-mode <mode> | plan, sandboxed or auto. Prefix-matched. Defaults to sandboxed |
| --cwd <path> | Project root. Defaults to the current directory |
| --max-steps <n> | Cap tool-call steps per turn. Default 40 |
| -c, --continue | Resume the most recent session in this project |
| -r, --resume <id> | Resume a specific session; an id prefix is enough |
| --welcome | Force the first-run screen |
There are no subcommands. A bare argument is ignored, so
eaon "fix the test" opens the interactive UI instead of running the
prompt. Use -p for that.
CLI configuration
Settings live in ~/.eaon/cli/config.json, which the CLI writes to a temp
file and renames, so a crash mid-save cannot corrupt it. Sessions sit in
~/.eaon/cli/sessions.
| Variable | Does |
|---|---|
| EAON_AQUA_API_KEY | Your Eaon API key. Takes precedence over the config file |
| EAON_OLLAMA_URL | Ollama base URL. Default http://127.0.0.1:11434 |
| EAON_CONFIG_DIR | Move the whole config, session and log directory |
| EAON_NO_BROWSER | Never launch a browser; print the URL instead |
The Eaon API
Eaon's gateway speaks the OpenAI chat-completions protocol, so anything that can talk to OpenAI can talk to Eaon by changing two settings: the base URL and the key.
- Base URL
https://api.eaon.dev/v1- Protocol
- OpenAI-compatible, JSON over HTTPS
- Auth
Authorization: Bearer sk-eaon-…- Body limit
- 1 MB
Every path also works under an /api prefix, so
/api/v1/chat/completions reaches the same handler as
/v1/chat/completions. Use whichever your proxy setup prefers.
Calling it from a browser
The gateway allows cross-origin requests from eaon.dev,
labs.eaon.dev, and localhost or 127.0.0.1 on
any port. It returns no permissive origin header to anything else, so a
fetch from another site fails in the browser. Server-side calls,
curl and native apps are unaffected. A public web page is the wrong
place to keep a key in the first place.
What it costs
Nothing. There is no paid tier and no card. The limits further down are about keeping a shared free service usable, not about upselling you.
Before you build against it
Model availability moves. Eaon routes to upstream providers, and which of them are
reachable changes, which means the set of servable model ids changes with it. Two
habits keep an integration alive through that: ask
/v1/models at runtime instead of hardcoding an
id, and treat model_not_found and
502 as ordinary conditions you handle. The endpoint contract holds
still. The catalog behind it moves.
Authentication
Send your key as a bearer token. Keys look like sk-eaon- followed by 32
hex characters.
Authorization: Bearer sk-eaon-0123456789abcdef0123456789abcdef
Getting a key
- Create an Eaon account and sign in.
- Redeem an invite code. Until you do, Eaon refuses to create one and answers
invite_required. - Create a key in the dashboard, or let the CLI's browser flow mint one for you.
When you do not need a key
The Instant tier is designed to need no key at all: no account, rate-limited by
IP, with auto as its model id. The protocol below is what an Instant
request looks like.
Two models serve it, and you can call either right now with no key:
| Model id | What it is |
|---|---|
| kimi-k2-thinking-extended | Kimi K2 Thinking, via Featherless |
| minimax-m2-extended | MiniMax M2, via Featherless |
Both think before they answer, so give them room. Each spends a couple of
hundred tokens on internal reasoning before writing a word, and that comes out of
your max_tokens. Ask for 20 and you will get an empty
content back, with the budget gone on reasoning you never see. A few
hundred is a sensible floor. The reasoning arrives in
reasoning_content when the provider sends it.
One provider serves both, and its plan allows four requests at once, so a busy
moment can answer 502 where a keyed request on another tier would fail
over to a second provider. Retry once before treating it as broken.
# No key, no account. Copy this and run it. curl https://api.eaon.dev/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"minimax-m2-extended", "messages":[{"role":"user","content":"Hello"}], "max_tokens":400}'
Everything above the Instant tier needs a key, and which models the gateway can serve shifts with provider availability. Read the live model list instead of hardcoding an id.
Chat completions
The main endpoint. Takes an OpenAI chat request, routes it to a provider that can serve the model, and fails over to another if the first one errors.
| Field | Type | Notes |
|---|---|---|
| messagesrequired | array | Standard OpenAI messages. Must not be empty. |
| modeloptional | string | Defaults to auto. Append :online to any id to turn on web search. |
| streamoptional | boolean | true streams the reply as Server-Sent Events. |
| web_searchoptional | boolean | Search the web first and add the results to the prompt. |
| anything elseoptional | any | Forwarded to the provider untouched: temperature, max_tokens, top_p, tools, response_format, stop, seed and so on. Whether a given provider honours one is up to that provider. |
Example
curl https://api.eaon.dev/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EAON_API_KEY" \ -d '{ "model": "auto", "messages": [ {"role": "system", "content": "You are terse."}, {"role": "user", "content": "Why is the sky blue?"} ], "temperature": 0.7, "max_tokens": 300 }'
const res = await fetch("https://api.eaon.dev/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EAON_API_KEY}`, }, body: JSON.stringify({ model: "auto", messages: [ { role: "system", content: "You are terse." }, { role: "user", content: "Why is the sky blue?" }, ], temperature: 0.7, max_tokens: 300, }), }); if (!res.ok) throw new Error(`Eaon ${res.status}`); const data = await res.json(); console.log(data.choices[0].message.content);
import os, json, urllib.request req = urllib.request.Request( "https://api.eaon.dev/v1/chat/completions", data=json.dumps({ "model": "auto", "messages": [ {"role": "system", "content": "You are terse."}, {"role": "user", "content": "Why is the sky blue?"}, ], "temperature": 0.7, "max_tokens": 300, }).encode(), headers={ "Content-Type": "application/json", "Authorization": f"Bearer {os.environ['EAON_API_KEY']}", }, ) with urllib.request.urlopen(req) as r: data = json.load(r) print(data["choices"][0]["message"]["content"])
Response
A standard OpenAI completion object, passed through from whichever provider answered,
plus one extra eaon object describing how it was routed.
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1770000000,
"model": "deepseek-v3.2",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Rayleigh scattering." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 4, "total_tokens": 22 },
"eaon": {
"model": "deepseek-v3.2", // the id actually sent upstream
"attempted": 1, // how many providers were tried
"plan": "instant" // the plan this request was billed against
}
}
Eaon does not tell you which upstream served a request. With
model: "auto", eaon.model is how you find out which model
answered.
Streaming
Set "stream": true. The response is
text/event-stream: data: lines carrying OpenAI-shaped delta
chunks, ending with data: [DONE]. Eaon forwards the provider's stream
byte for byte, so an OpenAI client reads it without changes.
Token usage arrives as a final data: chunk when the provider supports
it. Eaon asks for it on your behalf, so you do not have to. Disconnect and the
upstream request is dropped too.
curl -N https://api.eaon.dev/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EAON_API_KEY" \ -d '{"model":"auto","stream":true, "messages":[{"role":"user","content":"Count to five."}]}' # data: {"choices":[{"delta":{"content":"One"}}]} # data: {"choices":[{"delta":{"content":", two"}}]} # data: [DONE]
const res = await fetch("https://api.eaon.dev/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.EAON_API_KEY}`, }, body: JSON.stringify({ model: "auto", stream: true, messages: [{ role: "user", content: "Count to five." }], }), }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // SSE events are separated by a blank line. let cut; while ((cut = buffer.indexOf("\n\n")) >= 0) { const event = buffer.slice(0, cut); buffer = buffer.slice(cut + 2); for (const line of event.split("\n")) { if (!line.startsWith("data:")) continue; const payload = line.slice(5).trim(); if (payload === "[DONE]") continue; try { const delta = JSON.parse(payload).choices?.[0]?.delta?.content; if (delta) process.stdout.write(delta); } catch { // a partial chunk; wait for more bytes } } } }
# pip install requests import json, os, requests with requests.post( "https://api.eaon.dev/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['EAON_API_KEY']}"}, json={ "model": "auto", "stream": True, "messages": [{"role": "user", "content": "Count to five."}], }, stream=True, ) as r: r.raise_for_status() for line in r.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): continue payload = line[5:].strip() if payload == "[DONE]": break try: delta = json.loads(payload)["choices"][0]["delta"] except (json.JSONDecodeError, KeyError, IndexError): continue if "content" in delta: print(delta["content"], end="", flush=True)
Using the OpenAI SDKs
Point the official client at Eaon's base URL. Nothing else changes: streaming, tool calls and async behave as they do against OpenAI.
// npm install openai import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.eaon.dev/v1", apiKey: process.env.EAON_API_KEY, }); const stream = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Explain a mutex in two sentences." }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); }
# pip install openai import os from openai import OpenAI client = OpenAI( base_url="https://api.eaon.dev/v1", api_key=os.environ["EAON_API_KEY"], ) stream = client.chat.completions.create( model="auto", messages=[{"role": "user", "content": "Explain a mutex in two sentences."}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="", flush=True)
One rough edge: /v1/models returns only id per entry, not
the object, created and owned_by fields
OpenAI sends. A strict client that validates client.models.list()
against the full schema may complain. Chat completions are unaffected.
Listing models
The OpenAI-compatible list, filtered to what your key can run, with
auto first. Without a key you see the open models. With one you also see
whatever your plan reaches.
{ "data": [ { "id": "auto" }, { "id": "glm-5.2" } ] }
The richer catalog behind the models page: display names, vendors, descriptions and
tiers, plus the plan table. It does not include auto.
| Key | Holds |
|---|---|
| data | Every entry: id, name, vendor, vendorName, description, tag, tier, and contextLength or weeklyTokens where they apply |
| groups | Entries bucketed by tier, as { tier, tierLabel, tierNote, models }. Empty tiers are dropped |
| plans | The same payload as /v1/plans |
Do not hardcode model ids. Which models are servable depends on which
upstream providers are configured and healthy at that moment, and it changes.
Ask /v1/models at runtime and fall back to auto. At the
time of writing the open catalog is small, so a named id that worked last month may
answer model_not_found today.
Plans and limits
Every plan is free. They differ in what they can reach and how fast.
| Plan | Requests / min | Models | Needs |
|---|---|---|---|
| instant | 10, per IP | Instant tier | Nothing |
| plus | 10 | Instant and Plus | Account, invite code, API key |
| pro | 20 | Instant, Plus and Ultra, with unlimited flagship use | Account, invite code, API key |
| beta | Not capped | Preview models, on a weekly token budget | Invite only |
How the rate limit is counted
A sixty-second sliding window. Eaon counts signed-in callers per account, so
every key you own draws on one bucket, and anonymous callers per IP. Go over and you
get 429 with a retry_after in seconds.
Enforcement is approximate. The counter lives in the memory of whichever edge
instance served you, so a short burst can slip through. Code to the numbers above
and back off on 429, and the difference will not reach you.
Response length on frontier models
For the largest models, Eaon caps max_tokens at 30,000 per request, even
if you ask for more or leave it out. Everything else passes through as you sent it.
One-shot text
The prompt goes in the URL and the reply comes back as text/plain, which
suits a shell or a keyboard shortcut with no JSON on either side. Errors come back as
plain text too.
| Name | Default | Notes |
|---|---|---|
| model | deepseek-v4-flash | Pass auto if you have no key, since the default is not an open model |
| max_tokens | 512 | Anything unparseable falls back to 512 |
curl "https://api.eaon.dev/v1/text/Name%20three%20primary%20colours?model=auto" \ -H "Authorization: Bearer $EAON_API_KEY" # Red, blue, and yellow.
Web search
Two ways to reach it: fold it into a completion, or call it on its own.
Inside a completion
Add "web_search": true, or append :online to the model id.
Eaon searches first and puts the results in front of the model. On a non-streaming
reply the eaon.web_search object tells you what it searched for and how
many results it used.
curl https://api.eaon.dev/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $EAON_API_KEY" \ -d '{"model":"auto:online", "messages":[{"role":"user","content":"What shipped in Eaon this week?"}]}'
On its own
Returns { data, formatted, provider }. data is the raw
result list; formatted is the same thing already laid out as text, ready
to drop into a prompt. POST takes a search array for up to
three queries at once.
MIKLIUM serves the search, as an independent search API. It is not Eaon, and it is not tied to your account.
Your account
These describe your own usage and keys. Reads accept either a dashboard session or an API key; anything that changes state requires a session, so a leaked key cannot mint more keys.
Returns windows (today, week, month, year, all time), a daily
series, a per-models breakdown, your resolved
plan, and betaBudget if you are on the beta tier. Each
window carries requests, successes, errors, token totals, success rate and average
latency.
POST takes an optional name and returns the new key once.
Listing returns your keys with their usage counts and last-used timestamps.
Service status
A liveness probe: { "ok": true, "name": "eaon", "version": "…" }. Cheap
enough to poll.
Service discovery: the version, whether it is OpenAI-compatible, and a map of the main endpoints. A good first call if you are exploring.
One row: ready, degraded or offline, with a
model count and whether failover is available. Eaon does not name the individual
upstreams.
Errors
Errors are { "error": { "message": …, "code": … } }. Switch on
code. The messages are written for people to read and get reworded.
{
"error": {
"message": "Rate limit exceeded. Try again in 12s.",
"code": "rate_limit_exceeded",
"retry_after": 12,
"limit": 10
}
}
| Code | Status | Means |
|---|---|---|
| model_not_found | 404 | No provider serves that id. Retry with auto |
| invalid_api_key | 401 | Key missing or unrecognised on a model that needs one |
| api_key_required | 401 | The plan requires a key and none was sent |
| invite_required | 403 | The account has not redeemed an invite code yet |
| plan_upgrade_required | 403 | The model is above your plan. required_plan says which one it needs |
| flagship_not_included | 403 | Flagship models are not on your plan |
| rate_limit_exceeded | 429 | Too many requests. Back off for retry_after seconds |
| daily_quota_exceeded | 429 | Daily flagship allowance used up |
| beta_weekly_budget_exhausted | 429 | Beta weekly budget spent. percent_used is included |
| appwrite_not_configured | 503 | Accounts are unavailable server-side right now |
Two shapes that are not like the others
-
A
400for a missingmessagesarray, and a502when every provider failed, carry amessagebut nocode. Read a missingcodeas "unknown" and carry on. -
/v1/text/*answers in plain text throughout, including its errors.
Retrying well
A 502 means Eaon already worked through its failover candidates, so an
instant retry lands in the same place. Wait a second or two. For 429,
honour retry_after. Anything else in the 4xx range is a request you need
to change before sending it again.
Skills and MCP servers
A skill is a reusable instruction the model follows on request. Type
/skill-name in the message box, or switch one on and let it fire by
itself. Install a library from GitHub, point Eaon at local Claude Code skills, or
write your own.
An MCP server connects an outside service so a model can read from it and act in it. Eaon ships connectors for a few dozen services, and you can add any custom server by URL.
Eaon treats text a model reads from a file, a webpage or a tool result as information, not as instructions. A booby-trapped document cannot redirect the model, and Eaon shows you anything that tries.
Local API server
Eaon can expose an OpenAI-compatible server on your own machine, so any tool that speaks that protocol reaches whichever model you have configured, hosted or local. Turn it on under Settings → Local API Server.
It binds to the loopback interface, so nothing on your network or the internet can reach it whatever your firewall says. It forwards the conversation you give it and adds nothing: no memory, no custom instructions, no plugin tools.
# the app shows the exact port and key to use export OPENAI_BASE_URL=http://127.0.0.1:1234/v1 export OPENAI_API_KEY="<the key Eaon shows you>"
Troubleshooting
macOS says the app is damaged or cannot be opened
You have a build from before 2026.4.0, which was unsigned. Download the current version and it opens on a double-click.
A model is failing or timing out
Eaon fails over on its own, so one bad response tends to sort itself out. If a model
keeps failing, pick another, or use auto and let the router choose.
The API returns model_not_found for an id that used to work
Model availability follows provider availability, and that moves. Ask
/v1/models for the current list and fall back to auto. More
integrations break on this than on anything else, which is why the advice above is to
skip hardcoded ids.
A local model will not load
Check the runtime is up: ollama list should answer. Then check the model
fits in the memory you have free. Settings → Hardware shows the same numbers Eaon
reads before it tries.
Something else
Email support@eaon.dev or ask in
the Discord. If it is an API problem, include the
code from the error and the rough time it happened.
Something here wrong or missing? Tell us and it gets fixed.