Overview
Machine intelligence — bind models, ask versioned prompts, run bounded agents, embed into store.index, and connect MCP.
AI is how your backend calls a model as a declared effect. Ticket triage, a weekly
summary, a planner that tools your own Flows — each ask is a named prompt or agent with
budgets, not an SDK call buried in do.
For developers wiring OpenRouter or any OpenAI-compatible /v1 endpoint — bind a model,
mint a prompt, ask through fx.
The one rule
Declare ai.model → model.prompt (or ai.agent / ai.embed), then call through fx.ask /
fx.run / fx.embed. There is no top-level ai.prompt. Budgets and maxSteps are contracts;
PII to a third-party model needs explicit allowPii.
Four blocks, one element
ai.model · prompt · embed · agentmodelbindLogical binding
ai.model("smart", { provider, model })Name → provider / wire model — prod must declare the driver
smart / fast / local — swap per environment
promptaskVersioned artifact
smart.prompt("ticket-triage", { out, version })fx.ask validates the response against `out`
Typed triage, summaries, structured answers
embedintoEmbedding pipeline
ai.embed("kb", { into: index })Vectors land in store.index — searched via fx.search
Knowledge base, semantic recall
agentstep 1/6Bounded agent
ai.agent("support", { tools, maxSteps })Tools are flows — each step goes through fx.call
Support cases, multi-step resolve with budgets
Smallest Example
Bind a model and mint a prompt
import { ai } from "okengine";
import { z } from "zod";
export const smart = ai.model("smart", {
provider: "openrouter",
model: "openrouter/free",
apiKey: process.env.OPENROUTER_API_KEY,
});
export const triage = smart.prompt("ticket-triage", {
version: 1,
budget: { maxCostPerCall: 0.02 },
out: z.object({
category: z.enum(["billing", "technical", "other"]),
urgency: z.enum(["low", "medium", "high"]),
}),
});Import this module before oke() so registries adopt the decls.
Ask from a Flow
import { on, flow, http } from "okengine";
import { z } from "zod";
import { triage } from "@/core/ai";
export const classify = on(
http.post({
in: z.object({ message: z.string().min(1) }),
}),
flow({
do: async ({ message }, fx) => {
return await fx.ask(triage, { message });
},
}),
);The compiler stamps asks: ["ticket-triage"] on the Flow’s effects.
Call the endpoint
curl -X POST http://localhost:6530/support/triage \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{"message":"Invoice double-charged last week"}'With out set, the model must return JSON matching the schema or the ask throws
AiSchemaValidationError.
Progressive Patterns
From a bare ask to fallback models, tools on ask, and a bounded agent:
Prompt handle + input — model comes from the parent ai.model:
const result = await fx.ask(triage, { message: "Cannot reset password" });Declaration Reference
| Declaration | Signature | Purpose |
|---|---|---|
ai.model | ai.model(name, options?) | Logical binding (provider / wire model / key) |
model.prompt | model.prompt(name, options?) | Versioned prompt on that binding |
ai.agent | ai.agent(name, options?) | Bounded tool-calling agent |
ai.embed | ai.embed(name, options?) | Embedding pipeline into store.index |
ai.mcpServer | ai.mcpServer(name, options) | Outbound MCP client (allowlisted tools) |
There is no ai.prompt(...) — mint prompts only via model.prompt.
fx surface
| Method | Capability | Meaning |
|---|---|---|
fx.ask(prompt, input?, opts?) | asks | Complete a versioned prompt (optional tool loop) |
fx.run(agent, input?) | asks (agent name) | Run a declared agent |
fx.stream(model, opts?) | asks | Token stream from a model binding |
fx.embed(model, text) | embeds | Return a vector (does not write an index) |
fx.search(embed, query, opts?) | reads | Similarity search over an embed / index |
fx.ask options
| Option | Type | Default | Meaning |
|---|---|---|---|
via | refs | prompt via / [model] | Recovery chain for this call |
timeout | "30s" | ms | prompt timeout | Per-call deadline |
tools | Flow refs | — | Offered as tools via fx.call |
maxSteps | number | 6 | Cap on tool rounds |
Undeclared ask → OKE1005 UNDECLARED_ASK. Undeclared embed → OKE1009.
Guardrails
Budgets, version pins, step limits, and PII egress are contracts — not guidelines:
Guardrails — contracts, not guidelines
fx.ask(prompt, input)clean ask- 1
versioned promptsmart.prompt("ticket-triage", { version: 1, out }) - 2
PII build gatethird-party + .pii() field → needs allowPii - 3
steps · budgetmaxSteps: 6 · budget.maxCostPerRun - 4
prod driverdrivers.ai.prod must be named — no default - →
Checking guardrails…
PII egress
Sending a .pii() field to a third-party provider fails the build unless the Flow
declares allowPii: true (or pii: "allow"). mock, local, and openai-compatible are
not third-party egress.
PII — same field, opposite egress
fx.ask · .pii() · emailaskcheckverdictanthropicThird-party — build fails
Cloud provider counts as egress. Without allowPii the build stops before deploy.
AiPiiBuildError · field(s) [email]openai-compatibleOn-premise — ask proceeds
mock, local, and openai-compatible are not third-party egress.
ask proceeds · no AiPiiBuildError
Checking egress…
flow({
allowPii: true,
do: async (input, fx) => fx.ask(triage, { email: input.email }),
});Per-environment drivers
AiDriverId: mock · anthropic · openai-compatible · bedrock · vertex.
Boot implements mock, anthropic, and openai-compatible. bedrock / vertex are
reserved and throw until implemented. Dev/test fall back to mock when unset — prod
must declare drivers.ai.
import { defineConfig } from "okengine/config";
export default defineConfig({
drivers: {
ai: {
dev: "openai-compatible",
test: "mock",
prod: "openai-compatible",
},
},
});| Driver | Runs as | Best for |
|---|---|---|
mock | In-process stub | Tests, CI, no network |
openai-compatible | OpenAI HTTP API | OpenRouter, cloud registries, any /v1 server |
anthropic | Native Messages API | Production Claude + reliable tools |
Env knobs: OKE_AI_DRIVER, OKE_AI_URL / OPENAI_BASE_URL, provider API keys.
Compose does not pin inference — BYO URL + keys, or OpenRouter.
The Capabilities of AI
Models
Verified providers, limited-compatibility caveats, cloud and BYO bindings.
Prompts
Versioned artifacts, via chains, budgets, out-schema validation.
Agents
Tool bags are Flows — maxSteps and maxCostPerRun bound the loop.
Model Context Protocol
Expose Flows as MCP tools; consume external servers via ai.mcpServer.
Troubleshooting
fx.ask named a prompt that was never minted with model.prompt, or the declaring module was not
imported before oke().
Cause: Flow "{flow}" asks "{resource}" without declaring it. Touch the prompt handle
inside do (inference) or list effects: { asks: ["ticket-triage"] }.
Cause: ai: schema validation failed for prompt "…"@N: … The model reply did not match out. Fix
the schema, the input, or the model — there is no automatic “schema correction” retry.
Cause: ai: prompt "…" exceeded maxCostPerCall N or ai: agent "…" exceeded maxCostPerRun N.
Raise the budget or shrink the work.
Cause: build failed: flow "…" sends pii field(s) […] to a third-party model without allowPii.
Set allowPii: true on the Flow, or keep the ask on mock / local / openai-compatible.
Prod must pin a real drivers.ai id. bedrock and vertex throw oke boot: AI driver "…" is reserved but not implemented yet.
Learn more
- Models — provider registry and
baseUrlrules - Prompts —
via, budgets, versions, evals - Agents —
fx.run, tools,maxSteps - MCP — inbound
mcp.tooland outboundai.mcpServer - OpenRouter — zero-Docker cloud path
- fx —
fx.ask/fx.run/fx.embed - Errors — OKE1005 · OKE1009