# lmai — the prompt-orchestration language

Drive a language model from a script: declare prompts, force a JSON shape, branch on the answer, and account for what it cost in tokens. Safe mode with no new env flag, and it needs the host's ai...


Drive a language model from a script: declare prompts, force a JSON shape, branch on the answer, and
account for what it cost in tokens. **Safe** mode with no new env flag, and it needs the host's `ai`
bridge — so `POST /api/lmx/run` or Studio's LMX Playground, not the CLI.

```lmai
MODEL auto
TEMPERATURE 0.2
SYSTEM "You are a terse support classifier."

PROMPT classify = "Label this as billing, bug or other:\n{{input}}"
SHAPE verdict = { "label": "billing", "confidence": 0.9 }

ASK classify -> r AS verdict
PRINT r.label

WHEN r.confidence < 0.7 THEN ASK "Why is this ambiguous?\n{{input}}" -> why
PRINT why

USAGE
RETURN r
```

---

## [critical] Read this before you trust a run

`providerRegistry.resolve()` **never fails**. `auto` walks `AUTO_PRIORITY` — ollama, leumas-slm,
openai, claude, gemini, cursor-agent, codex — and when none is available it falls back to a built-in
**echo stub** that hands your own message back.

So on a machine with no API key and no Ollama, a naive prompt script runs green, prints something
plausible, and means nothing at all.

`lmai` closes that hole three ways:

- every result records **which provider actually answered**;
- `USAGE` prints it;
- and because a script author cannot be relied on to write `USAGE`, **the runner prints a warning of
  its own accord** whenever the stub answered:

  ```
 [warning] the ECHO STUB answered — no real model provider is configured, so this run proves nothing.
  ```

If you are automating on top of `lmai`, branch on `usage.stub` before anything else.

**The shaped path makes this worse, not better.** The stub echoes your prompt back — and a shaped
prompt *contains the `SHAPE` example*, so the JSON extractor finds it and the run succeeds:

```
result.value: {"label":"billing","confidence":0.9}
result.usage: {"calls":1,"tokens":0,"providers":["stub"],"stub":true,"retries":0}
```

That is a perfectly plausible classification of the input. It is your own example, handed back.
`stub: true` and `tokens: 0` are the only things that say so.

---

## Keys

`lmai` invents no policy. It calls `resolveKeyForUser`, already the single decision point behind every
chatbot and coding-agent run:

| Caller | Key used |
|---|---|
| active member, or admin | the env-mounted key for that provider |
| free user | their own stored BYO key |
| neither | a `402` — *"Add your own key in Settings, or upgrade your membership"* |

That message is surfaced to the script verbatim, because it is already written for a human.

**Local providers skip this entirely.** `ollama`, `leumas-slm` and `codex` are configured with an
endpoint or a binary path, not a credential, so `lmai` never demands a key for a model running on your
own machine.

---

## Directives

Config and declarations are hoisted, so a `PROMPT` may be declared after the `ASK` that uses it.
Statements run in source order. `#` starts a comment.

### Config

| Directive | Notes |
|---|---|
| `MODEL <provider>[:<model>]` | `MODEL auto`, `MODEL claude`, `MODEL claude:claude-sonnet-5` |
| `TEMPERATURE <n>` | **`0` is a setting, not "unset"** — an omitted temperature is never defaulted |
| `MAXTOKENS <n>` | same |
| `SYSTEM "…"` or `SYSTEM do … end` | the system prompt |

### Declarations

| Directive | Notes |
|---|---|
| `PROMPT <name> = "…"` | a template; `\n` escapes work |
| `PROMPT <name> do … end` | multi-line, taken verbatim |
| `SHAPE <name> = { … }` | an **example** JSON document — a shape, not a schema |
| `SHAPE <name> do … end` | multi-line JSON |
| `SET <name> = <value>` | a variable; JSON-coerced, so `SET n = 3` is the number 3 |

### Statements

| Directive | Notes |
|---|---|
| `ASK <name\|"literal"> -> <var>` | one model turn; the reply text lands in `<var>` |
| `ASK … -> <var> AS <shape>` | forces JSON; `<var>` is the **parsed document** |
| `WHEN <cond> THEN <statement>` | one guarded statement |
| `PRINT <path>` or `PRINT "… {{slot}} …"` | |
| `USAGE` | the accounting line |
| `RETURN <path>` | the script's value; **stops the script** |

`WHEN` conditions are `path op value` (`=` `!=` `>` `>=` `<` `<=`) or a bare path for truthiness.
A missing path is falsy, never an error.

---

## `{{slots}}`

Templates interpolate from three sources, all in one namespace:

1. the host's named `inputs` (`{"input": "…"}` from the API, or `# inputs: input=…` in a bundled demo);
2. `SET` variables;
3. any prior `ASK` result — including into a document: `{{r.label}}`.

**An unresolved slot is an error, not an empty string.** A prompt that silently loses its input still
runs, still costs tokens, and answers a question nobody asked — the single most expensive way for a
scripted model call to fail.

Non-string values are JSON-stringified rather than coerced, so `{{r}}` on an object gives `{"a":1}`
and never `[object Object]`.

---

## The shaped path (`AS`)

`SHAPE` gives the model an *example* of the document you want. `lmai` appends it plus
`Respond with ONLY the JSON document — no commentary, no code fences.`, then parses the reply with the
same fence-stripping, balanced-bracket extractor `@leumas/generation` uses.

If that fails it retries **exactly once** with a stricter nudge, then gives up with the first 200
characters of the reply. It never loops.

Both calls count toward tokens and toward the ASK budget. `usage.retries` tells you it happened.

---

## `USAGE`

```
--- usage --- calls=1 tokens=12 retries=0 provider=claude (token counts only — the provider layer reports no cost)
```

**Tokens, never money.** `cost` is hardcoded `0` throughout the provider layer and there is no price
table anywhere in the tree. `USAGE` says so in as many words rather than implying a figure it cannot
compute.

---

## Limits

| Limit | Value | Why |
|---|---|---|
| ASK budget | 20 per script (retries count) | a loop cannot spend a key |
| Run timeout | 60s default | the global LMX default is 5s, which kills any real model call |

---

## What it deliberately cannot do

- **Loop, or define functions.** A prompt script that needs those wants `lmx` with the `adapter()`
  bridge, not a second general-purpose language.
- **Call tools.** `toolCalls` come back from the provider and are not surfaced; a tool loop is what
  `@leumas/chatbots` and the coding agent are for.
- **Report a cost.** See above.
- **Run from the CLI.** No `ai` bridge there.

---

## Running it

```bash
curl -s -X POST localhost:3000/api/lmx/run \
  -H 'content-type: application/json' \
  -d '{"mode":"lmai","source":"PROMPT p = \"hi {{input}}\"\nASK p -> r\nPRINT r\nUSAGE","inputs":{"input":"there"}}'
```

Bundled examples: `shared/engines/lmx/src/scripts/demo.lmai`, `shared/engines/lmx/src/scripts/triage.lmai`.
Tests: `shared/engines/lmx/src/test/lmai.test.js`. Guard: `pnpm smoke:lmx`.


---
Source: .claude/skills/leumas-lmx/reference/lmai.md
Canonical: https://docs.leumas.tech/p/skills/leumas-lmx/lmai
