ai — LLM (first-class primitive)
ai.ask, ai.json with confidence scores, the ai.run agent tool loop, and connecting any OpenAI-compatible provider.
This is the biggest thing that sets Fluxon apart from other languages. The LLM is a
keyword, not an SDK. The provider is detected automatically (OS env or .env)
— you configure nothing:
- if
ANTHROPIC_API_KEYis set → Claude (defaultclaude-opus-4-8) - if
OPENAI_API_KEYis set → GPT (defaultgpt-4o) - if both are set, Anthropic wins. Override:
$AI_PROVIDER(anthropic|openai),$AI_KEY(a shared key),$AI_MODEL(the model name).
This adapts to common standard names like OPENAI_API_KEY/ANTHROPIC_API_KEY —
the same .env works with other tools.
use ai
# Simple question-and-answer → text
answer = ai.ask "Translate this message into English: ${text}"
# Structured extraction (typed extraction) → a map according to the schema
schema = {
intent: ":new_order|:question|:other"
items: [{product:str qty:int}]
}
r = ai.json "Extract the order: ${text}" schema
# r.intent, r.items[0].product ...Audit metadata — automatic
Each ai.* result carries metadata under _:
r = ai.json prompt schema
log r._.conf # confidence score (0..1)
log r._.tokens # tokens used
log r._.cost # cost
log r._.ms # latency (milliseconds)This is the basis for confidence routing:
if r._.conf > 0.85
auto_answer r # high confidence → automatic
elif r._.conf >= 0.6
ask_owner r # medium → ask for confirmation
else
escalate_to_owner r # low → full escalationNote
_.conf is the calibrated confidence returned by the LLM battery. In real life
this should be backed by logprobs or self-eval; the language hides this behind the
battery.
ai.run — agent tool loop (ONE step)
If the AI wants to use a tool, ai.run does not execute it itself — it returns
to you what it wants to do. You run the tool (with logging, cost, confirmation)
and return the result. The loop is manual — this gives you full control:
msgs <- [{role::user content:text}]
each i in 1..10 # maximum 10 steps
r = ai.run msgs tools # tools: a list of [{name desc params}]
if r.kind == :final
ret r.text # AI is done → final answer
# r.kind == :call → the AI wants to call tools. The model may call several
# in parallel → all are in r.calls; return a result for EACH one.
each c in r.calls
out = reg.call c.tool c.args # run the tool by name (see below)
log "tool ${c.tool}" # logging/cost/confirmation goes here
msgs <- msgs.push {role::tool id:c.id content:(json.enc out)}r.tool/r.args/r.id mirror r.calls[0] for back-compat (single-tool code
still works). But on parallel calls you must return a result for every
tool_use_id, otherwise the next request 400s. ai.run is intentionally
single-step. If you let the AI's tool calls run automatically and uncontrolled,
you could not do logging/cost/confirmation. The loop is yours — so you see and
control every tool call.
See reg for the registry that turns a tool name into a
callable function.
Connecting to other providers (advanced)
Everything above works with zero config — a standard key in .env is all you
need, and most programs never go further. But the ai battery also speaks any
OpenAI-compatible API (Z.AI / GLM, OpenRouter, Ollama, vLLM, LM Studio, Azure,
Groq, …). You override the parts of the request that differ, and nothing else
changes — the default behavior is byte-for-byte the same when you pass no
overrides.
There are two ways to override, and they compose (per-call wins over global).
ai.config {…} — set global defaults once, at the top of the program (like
http.cors; call it before http.serve). The keys, all optional:
| key | meaning |
|---|---|
url | full endpoint URL (replaces the provider default) |
style | wire format: :openai or :anthropic (the request/response shape) |
key | API key (same role as $AI_KEY, inline) |
model | model name (same role as $AI_MODEL, inline) |
headers | extra HTTP headers, merged onto the defaults (hyphenated names like HTTP-Referer must be string keys) |
extra | extra request-body fields, merged into the JSON |
A map key is a bare identifier, which cannot contain -. HTTP header names with a
hyphen must therefore be written as string keys: {"HTTP-Referer": "…" "X-Title": "…"}. Header names are matched case-insensitively (and sent
lowercased), so a per-call header replaces a global one regardless of casing.
# GLM (Z.AI): the OpenAI wire format at a different URL — that's the whole change.
ai.config {
style: :openai
url: "https://api.z.ai/api/paas/v4/chat/completions"
key: env.ZAI_KEY
model: "glm-4.6"
}
answer = ai.ask "Salom, dunyo" # now goes to Z.AI
# OpenRouter: extra body params + its recommended headers.
ai.config {
style: :openai
url: "https://openrouter.ai/api/v1/chat/completions"
key: env.OPENROUTER_KEY
model: "anthropic/claude-3.5-sonnet"
headers: {"HTTP-Referer": "https://myapp.dev" "X-Title": "My App"} # hyphenated → string keys
extra: {provider: {sort: "throughput"}} # OpenRouter-specific knob
}A trailing opts map on any ai.ask/ai.json/ai.run overrides the global
config for that one call (same keys). Omit it and the call behaves exactly as
before — it is purely additive:
# Same global config, but this one call uses a cheaper/faster model.
r = ai.ask "quick check" {model: "glm-4.5-air"}
r = ai.json prompt schema {model: "glm-4.6"}
r = ai.run msgs tools {extra: {temperature: 0}}
# To pass opts to ai.run WITHOUT tools, use nil for tools:
r = ai.run msgs nil {model: "glm-4.6"}headers and extra merge key-by-key (a key you give wins; the rest of the
defaults stay), while url/style/key/model replace. style selects only
the wire format — a GLM endpoint speaks OpenAI, so style::openai + a custom url
is enough; you don't need the official OpenAI key. Env equivalents exist for the
scalars: $AI_STYLE, $AI_BASE_URL (alongside $AI_KEY / $AI_MODEL /
$AI_PROVIDER).
A custom url requires an explicit key
When you point at a custom host, you must supply the key inline (key: / opts) or
via $AI_KEY. A standard provider key ($OPENAI_API_KEY / $ANTHROPIC_API_KEY)
is never sent to a custom URL — so an OpenAI key in your environment can't leak
to Z.AI/OpenRouter. Custom URL with no explicit key is a clear error before any
network call.