AI Effect: Calling LLMs from AILANG
AILANG v0.5.10 provides a simple, high-level AI effect for calling external AI/ML systems directly from your code. Perfect for game NPCs, agents, CLI tools, and data pipelines.
See the AI effect in action in the browser:
- Claude Chat Demo — SSE streaming with Claude Messages API
- Gemini Live Demo — Bidirectional WebSocket audio with 30 voices
- Safe Agent Demo — Contract-verified AI tool calling
Overview
The AI effect (std/ai) is a general-purpose AI oracle - an opaque string-to-string interface for calling LLMs:
import std/ai (call)
func ask_ai(question: string) -> string ! {AI} =
call(question)
Key Features:
- Simple string → string interface (JSON by convention)
- Multi-provider support (Anthropic, OpenAI, Google)
- Vertex AI ADC support (no API key required with gcloud auth)
- Deterministic stub for testing
- Effect-typed for capability tracking
Quick Start
1. Basic Usage
-- AI Effect Example
-- Demonstrates calling AI from AILANG
--
-- Run with stub handler (testing):
-- ailang run --caps IO,AI --ai-stub --entry main examples/runnable/ai_effect.ail
--
-- Run with Claude Haiku (requires ANTHROPIC_API_KEY):
-- ailang run --caps IO,AI --ai claude-haiku-4-5 --entry main examples/runnable/ai_effect.ail
--
-- Run with GPT-5 Mini (requires OPENAI_API_KEY):
-- ailang run --caps IO,AI --ai gpt5-mini --entry main examples/runnable/ai_effect.ail
--
-- Run with Gemini Flash (requires GOOGLE_API_KEY):
-- ailang run --caps IO,AI --ai gemini-2-5-flash --entry main examples/runnable/ai_effect.ail
--
-- The AI effect is a general-purpose AI oracle:
-- - String→string interface (JSON by convention)
-- - Pluggable handlers (stub, Anthropic, OpenAI, Google)
-- - Model lookup from models.yml (or guessed from prefix)
-- - No silent fallbacks (nil handler = error)
module examples/runnable/ai_effect
import std/ai (call)
import std/io (println)
-- Ask the AI a question and print the response
export func main() -> () ! {IO, AI} {
println("Asking AI: What is the capital of France?")
let response = call("What is the capital of France? Reply in one word.")
println("AI says: ${response}")
}
2. Run with Different Providers
# Claude (Anthropic) - requires ANTHROPIC_API_KEY
ailang run --caps IO,AI --ai claude-haiku-4-5 --entry main my_app.ail
# GPT (OpenAI) - requires OPENAI_API_KEY
ailang run --caps IO,AI --ai gpt5-mini --entry main my_app.ail
# Gemini (Google) - uses Vertex AI ADC if no GOOGLE_API_KEY
ailang run --caps IO,AI --ai gemini-2-5-flash --entry main my_app.ail
# Stub (deterministic testing)
ailang run --caps IO,AI --ai-stub --entry main my_app.ail
Supported Providers
| Provider | Models | Auth Method | Environment Variable |
|---|---|---|---|
| Anthropic | claude-sonnet-4-5, claude-haiku-4-5, etc. | API Key | ANTHROPIC_API_KEY |
| OpenAI | gpt-5, gpt-5-mini, etc. | API Key | OPENAI_API_KEY |
| gemini-2-5-pro, gemini-2-5-flash, etc. | API Key or ADC | GOOGLE_API_KEY (optional) |
Google Vertex AI (ADC)
For Google models, if no GOOGLE_API_KEY is set, AILANG automatically falls back to Application Default Credentials (ADC):
# Configure ADC once
gcloud auth application-default login
# Run without API key - uses ADC automatically
unset GOOGLE_API_KEY
ailang run --caps IO,AI --ai gemini-2-5-flash --entry main my_app.ail
Disabling the ADC fallback, and reading the key from a file
The automatic ADC fallback is convenient on a developer machine, but it is a
billing hazard for an application that should run on its user's key: when
the key is missing, the call silently bills whatever project gcloud points
at. Two switches close that gap:
| Switch | Effect |
|---|---|
--ai-no-adc or AILANG_AI_NO_ADC=1 | The ADC lane is never tried. A missing GOOGLE_API_KEY / GEMINI_API_KEY is a hard AuthFailed error at startup. Combining it with a pinned Vertex project is refused. |
--ai-key-file PATH or AILANG_AI_KEY_FILE=PATH | The API key for the --ai model's provider is read from PATH once, at startup (the flag wins over the variable). The file must hold the key on a single line (surrounding whitespace is trimmed). The key never enters argv or the environment, and AILANG does not echo it in logs, errors or traces; errors name the path only. A key-less lane (local Ollama, a config-driven provider) refuses the flag instead of ignoring it. |
# Run on the player's key only; never fall back to the developer's gcloud project
ailang run --caps IO,AI --ai gemini-2-5-flash --ai-no-adc \
--ai-key-file "$HOME/.config/mygame/gemini.key" --entry main my_app.ail
The Google client sends the key in the x-goog-api-key header, not in the
request URL, so a transport error (which quotes the URL) cannot leak it.
Game Development Example
Here's a complete example of using AI for NPC dialogue generation:
-- npc_dialogue.ail - Game NPC Dialogue using AI Effect
-- Used in: docs/docs/guides/ai-effect.md
module examples/docs/npc_dialogue
import std/ai (call)
import std/io (println)
-- Build dialogue prompt for an NPC
pure func makePrompt(npcName: string, role: string, personality: string, topic: string) -> string =
"You are " ++ npcName ++ ", a " ++ role ++ " in a fantasy RPG. " ++ "Personality: " ++ personality ++ ". " ++ "Generate ONE short line of dialogue (1-2 sentences) about: " ++ topic
-- Generate NPC dialogue using AI
func askNpc(npcName: string, role: string, personality: string, topic: string) -> string ! {AI} = {
let prompt = makePrompt(npcName, role, personality, topic)
call(prompt)
}
-- Main: demonstrate NPC dialogue generation
export func main() -> () ! {IO, AI} = {
println("=== NPC Dialogue Demo ===")
println("")
println("[At the Forge]")
println("Player: Can you forge me an enchanted sword?")
let response = askNpc("Grimnar", "blacksmith", "gruff but kind", "forging an enchanted sword")
println("Grimnar: " ++ response)
}
Run it:
# With Claude
ailang run --caps IO,AI --ai claude-haiku-4-5 --entry main examples/docs/npc_dialogue.ail
Output:
=== NPC Dialogue Demo ===
[At the Forge]
Player: Can you forge me an enchanted sword?
Grimnar: *pounds hammer on anvil*
The metal's got a stubborn spirit—takes patience and a steady hand to coax out the magic.
Testing with Stub Handler
For deterministic testing, use --ai-stub:
ailang run --caps IO,AI --ai-stub --entry main my_app.ail
The stub handler returns {"kind":"Wait"} for all inputs, making tests predictable and fast.
Replaying recorded responses with --ai-stub-fixtures
{"kind":"Wait"} proves the AI capability is wired, but it cannot exercise an
adapter that parses provider-shaped output. Point the stub at a fixture file
and it replays recorded responses instead, with no key, no network and no spend:
ailang run --caps IO,AI --ai-stub --ai-stub-fixtures fixtures.json --entry main my_app.ail
Each request is looked up by the sha256 of its canonical request bytes. A
request with no fixture is an error, never a quiet fall back to the default
stub: callResult/step return Err(AIError{code: "Internal"}) whose message
starts E_AI_STUB_FIXTURE_MISS and names the missing sha256, so you can paste
it straight into the file; the legacy call/callJson fail the run.
--ai-stub-fixtures without --ai-stub is refused, and the whole file is
validated at startup (unknown fields, bad base64 or duplicate keys are errors).
{
"version": 1,
"fixtures": [
{"kind": "text", "prompt": "hello", "response": "GRIMNAR: Well met."},
{"kind": "text", "sha256": "62aef194e227811665a1c0e28b470d18d7d3b6d05145e7cb2767d02455e277c1",
"response": "{\"kind\":\"Move\"}"},
{"kind": "text", "prompt": "look it up", "response": "",
"tool_calls": [{"id": "c1", "name": "lookup", "arguments": "{\"q\":\"forge\"}"}]},
{"kind": "text", "prompt": "flaky", "error": {"code": "RateLimit", "message": "slow down", "retryable": true}},
{"kind": "image", "prompt": "a red cube", "base64": "iVBORw0KGgo...", "mime_type": "image/png"},
{"kind": "speech", "request": {"text": "Well met.", "voice": "Kore", "style": "Say gruffly"},
"pcm_base64": "AAD/fwCA..."}
]
}
(The sha256 above is printf 'what next?' | shasum -a 256.)
| Field | Meaning |
|---|---|
version | Must be 1. |
kind | text, image or speech. Kinds are separate key spaces. |
prompt / request / sha256 | Exactly one. prompt is the literal request for text/image; request ({text, voice, style}) is the literal request for speech; sha256 is the lowercase hex sha256 of the canonical bytes. |
response | text: the reply string. |
tool_calls | text, optional: replayed by step; finish_reason becomes "tool_calls". |
base64, mime_type | image: the image bytes (standard base64) and media type, both required. |
pcm_base64 | speech: s16le mono 24 kHz PCM (standard base64, a whole number of samples). |
error | Any kind, instead of a payload: {code, message, retryable} replayed as a typed AIError. |
Canonical request bytes:
| Kind | Operations | Canonical bytes |
|---|---|---|
text | call, callJson, callJsonSimple and their *Result variants | the prompt (a callJson schema is not part of the key) |
text | step, stepWithCache, stepWithStream | the content of the last message in the conversation |
image | callImage, callImageBase64 | the prompt (options are not part of the key) |
speech | callSpeech | the compact JSON {"style":S,"text":T,"voice":V}: keys sorted, no spaces, UTF-8 not escaped. In Python: json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=False). Options are not part of the key. |
Replay is deterministic: the same file and the same requests always produce the same responses.
Text-to-Speech with callSpeech
import std/ai (callSpeech, speechSampleRate)
import std/audio (wavFromPcm)
-- Result[bytes, AIError] ! {AI}
match callSpeech("Well met, traveller.", "Kore", "Say it gruffly", "") {
Ok(pcm) => writeFileBytes("line.wav", wavFromPcm(pcm, speechSampleRate(), 1, 16)),
Err(e) => println("speech failed (${e.code}): ${e.message}")
}
- Output format is fixed: raw PCM, signed 16-bit little-endian, mono,
24000 Hz (
speechSampleRate()), which is whatstd/audio'swavFromPcmandencodetake. A provider reply in any other format isErr(ProtocolError)rather than bytes that would play as noise. voice: a provider voice name (Gemini prebuilt voices such asKore,Puck);""uses the provider default.style: a natural-language delivery instruction. Gemini receives it as"<style>: <text>";""sends the text alone.options: JSON,""for none.modelpicks the TTS model; the default is the bound--aimodel when its name containstts, elsegemini-2.5-flash-ttson Vertex orgemini-2.5-flash-preview-ttson AI Studio.language_codeis an optional BCP-47 code. Any other key isErr(SchemaValidation).- Providers: Gemini (AI Studio and Vertex, through the same
--aihandler, so--ai-no-adcand--ai-key-fileapply). Every other provider returnsErr(CapabilityNotSupported). - Testing:
--ai-stubreturns a fixed 250 ms 400 Hz tone (12000 bytes, identical on every platform);--ai-stub-fixturesreplaysspeechfixtures.
Runnable: examples/runnable/ai_call_speech.ail.
JSON Input/Output Pattern
By convention, use JSON for structured input/output:
import std/ai (call)
import std/json (encode, decode)
type GameContext = {
player_health: int,
enemy_count: int,
has_weapon: bool
}
type Action = Wait | Attack | Retreat | Heal
func decide_action(ctx: GameContext) -> Action ! {AI} {
let input = encode(ctx) in
let output = call(input) in
match decode[Action](output) {
Ok(action) => action,
Err(_) => Wait -- Safe fallback
}
}
Effect System Integration
The AI effect integrates with AILANG's capability system:
-- Effect declared in signature
func my_ai_func() -> string ! {AI} = ...
-- Requires --caps AI at runtime
ailang run --caps IO,AI --ai <model> --entry main file.ail
Effects are tracked at the type level, ensuring:
- AI calls are explicit in function signatures
- Capability requirements are validated at compile time
- Effect boundaries are clear and auditable
Configuration
CLI Flags
| Flag | Description |
|---|---|
--ai <model> | Set the AI model to use |
--ai-stub | Use deterministic stub handler |
--ai-stub-fixtures <file> | With --ai-stub: replay JSON fixtures keyed by the request's sha256; a miss is an error |
--ai-no-adc | Google: never fall back to Application Default Credentials; a missing key is AuthFailed (also AILANG_AI_NO_ADC=1) |
--ai-key-file PATH | Read the provider API key from a file at startup; never echoed (also AILANG_AI_KEY_FILE) |
--caps AI | Enable AI capability |
Model Lookup
Models are looked up in models.yml or guessed from name prefixes:
claude-*→ Anthropicgpt-*→ OpenAIgemini-*→ Google
Typed Errors with callResult / callJsonResult (v0.17.0+)
The legacy call and callJson crash the host on provider failure, which is
fine for one-shot scripts but wrong for agent loops, retries, or pipelines.
callResult / callJsonResult are drop-in replacements that return
Result[string, AIError] instead — the same wire path, but typed errors flow
back as Err(AIError {code, message, retryable}).
AIError.code is one of a fixed vocabulary that lets agents route on failure
type without parsing message strings:
| Code | Retryable? | Typical cause |
|---|---|---|
AuthFailed | no | 401/403 — bad API key |
RateLimit | yes | 429 — slow down and retry |
Timeout | yes | network / context deadline |
ConnectionFailed | yes | TCP / TLS / DNS issues |
ContextLength | no | prompt exceeds model window |
SchemaValidation | no | response doesn't match callJson schema |
ToolsNotSupported | no | provider can't do tools (Ollama, configdriven) |
ModelNotFound | no | 404 — model name typo |
BudgetExhausted | no | AI cap budget overflow |
Internal | yes | 5xx or unclassified — conservative default |
Same shape as std/ai/streaming.AIError (shipped v0.15.0) — one error
record across the whole std/ai surface, single source of truth for
retry-decision logic.
ailang run --caps AI,IO --ai gemini-3-flash-preview \
examples/runnable/ai_call_result.ail
-- examples/runnable/ai_call_result.ail
-- Demonstrates std/ai.callResult — the Result-returning variant of call,
-- introduced by M-AI-TOOL-LOOP (v0.17.0).
--
-- callResult mirrors call's wire path but returns Result[string, AIError]
-- instead of crashing the host on failure. The typed AIError carries
-- structured retry-vs-surface signal (retryable bool + code string) so
-- agents can route on it without parsing string error messages.
--
-- Use this in code where you want to handle provider failures explicitly
-- (rate limits, auth errors, transient timeouts) — particularly inside
-- agent loops, retries, or pipelines where panic-on-error is unacceptable.
--
-- Setup:
-- ailang run --caps AI,IO --ai gemini-3-flash-preview \
-- examples/runnable/ai_call_result.ail
--
-- For deterministic offline behaviour:
-- ailang run --caps AI,IO --ai-stub examples/runnable/ai_call_result.ail
module examples/runnable/ai_call_result
import std/ai (callResult, AIError)
import std/io (println)
import std/result (Result, Ok, Err)
-- Render a typed AIError with the retry-decision branch explicit.
-- AIError shape: { code: string, message: string, retryable: bool }.
export pure func renderError(e: AIError) -> string {
let retryStr = if e.retryable then "retryable" else "fatal"
"[${e.code}/${retryStr}] ${e.message}"
}
export func main() -> () ! {AI, IO} {
match callResult("Say hi in five words.") {
Ok(response) => println("AI response: ${response}"),
-- Typed error gives us the retry hint directly — no message parsing.
Err(e) => if e.retryable then println("Transient error (would retry): ${renderError(e)}") else println("Fatal error: ${renderError(e)}")
}
}
Multi-Turn Tool Dispatch with step and runTools (v0.17.0+)
For agentic workflows where the model emits tool calls and the host
dispatches them, step (one model turn) and runTools (loop driver) close
the gap. The full agent loop — provider HTTP, tool-call parsing, message
threading, typed errors — fits in a few lines of pure AILANG.
Shapes:
type Message = { role: string, content: string,
tool_calls: [ToolCall], tool_call_id: string }
type ToolSchema = { name: string, description: string, parameters: string }
type ToolCall = { id: string, name: string, arguments: string }
type StepResult = { message: Message, tool_calls: [ToolCall],
finish_reason: string,
input_tokens: int, output_tokens: int }
step is one model turn. Pass conversation + tool catalog, get the
assistant's response back. When finish_reason == "tool_calls", the model
is asking the host to dispatch tools and continue.
runTools is the convenience loop driver. It calls step in a loop,
runs your dispatch callback on each tool call, threads results back as
tool-role messages, and terminates when the model is done OR the step
budget hits zero.
runTools(
model: string,
messages: [Message],
tools: [ToolSchema],
dispatch: (ToolCall) -> string, -- effect-polymorphic; can be ! {FS, Process, ...}
step_budget: int
) -> Result[[Message], AIError] ! {AI}
The dispatch callback's effects propagate via row polymorphism — pass a
dispatch with ! {FS, Process} (e.g. real file I/O + shell exec) and
runTools infers ! {AI, FS, Process} automatically. No signature change
needed.
Provider parity:
| Provider | Tool support | Notes |
|---|---|---|
| Anthropic | yes | Messages API tool_use content blocks |
| Gemini | yes | functionCall parts; adapter generates stable IDs |
| OpenAI | yes | Chat Completions tool_calls |
| OpenRouter | yes | Passthrough (composes with Routing policies) |
| Ollama | no — typed reject | AIError{ToolsNotSupported}; falls through to chat for no-tools |
| Configdriven | no in v1 | Same typed reject pattern |
Worked example: a 2-tool catalog with offline-deterministic dispatch.
ailang run --caps AI,IO --ai gemini-3-flash-preview \
examples/runnable/ai_tool_loop.ail
-- examples/runnable/ai_tool_loop.ail
-- Demonstrates std/ai.runTools — the multi-turn tool dispatch loop driver
-- introduced by M-AI-TOOL-LOOP (v0.17.0).
--
-- The example builds a 2-tool catalog (read_doc, list_docs), starts a
-- conversation with one user turn ("Summarize the contents of nda.docx"),
-- and lets runTools drive the agent loop:
-- 1. step asks the model what to do
-- 2. model emits a tool_call (e.g. read_doc with name=nda.docx)
-- 3. runTools calls dispatch(call) → "<doc body>"
-- 4. step is called again with the tool result appended
-- 5. model emits a final assistant message → loop terminates
--
-- The dispatch callback in this example is pure — it returns hard-coded
-- strings keyed off the tool name. Real consumers (e.g. motoko_agent)
-- run a callback with effects {FS, Process} that actually reads files /
-- runs commands; AILANG's row polymorphism lets such a callback compose
-- with runTools without changing runTools' signature.
--
-- Setup:
-- ailang run --caps AI,IO --ai gemini-3-flash-preview \
-- examples/runnable/ai_tool_loop.ail
--
-- For deterministic behaviour against the stub handler:
-- ailang run --caps AI,IO --ai-stub examples/runnable/ai_tool_loop.ail
module examples/runnable/ai_tool_loop
import std/ai (
runTools, callResult, AIError, Message, ToolCall, ToolSchema, StepResult
)
import std/io (println)
import std/result (Result, Ok, Err)
import std/list (length)
-- Build a tool catalog the model may call.
export func tools() -> [ToolSchema] {
[
{
name: "read_doc",
description: "Read the named document and return its contents",
parameters: "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}"
},
{
name: "list_docs",
description: "List the names of all available documents",
parameters: "{\"type\":\"object\",\"properties\":{}}"
}
]
}
-- Initial conversation: one user message.
export func initialMessages() -> [Message] {
[
{
role: "user",
content: "Summarize the contents of nda.docx",
tool_calls: [],
tool_call_id: "",
images: []
}
]
}
-- Dispatch callback. In a real agent this would do file I/O / shell exec /
-- HTTP requests / etc. with appropriate effects. Here we return canned
-- strings so the example is deterministic and runs offline against any
-- AI handler that emits tool_calls.
export pure func dispatch(call: ToolCall) -> string {
if call.name == "read_doc"
then "[NDA between Acme Corp and Beta LLC, dated 2026-01-15. Confidential information defined as ... 2-year term ... governed by Delaware law.]"
else if call.name == "list_docs"
then "nda.docx, employment_contract.docx, lease.pdf"
else "unknown tool: ${call.name}"
}
-- Render the typed-error path with retry-decision branch.
export pure func renderError(e: AIError) -> string {
let retryStr = if e.retryable then "true" else "false";
"[${e.code}] retryable=${retryStr}: ${e.message}"
}
-- Pull the last message's content for display. If somehow empty (shouldn't
-- happen — runTools always appends at least one message on Ok), return a
-- sentinel so the example output is still informative.
export pure func lastMessage(messages: [Message]) -> string {
match messages {
[] => "<no messages>",
[msg] => msg.content,
[_, ...rest] => lastMessage(rest)
}
}
export func main() -> () ! {AI, IO} {
-- Cap the loop at 8 steps. Reasonable for read_doc → summarize flows;
-- raise for deeper agent traces.
match runTools("", initialMessages(), tools(), dispatch, 8) {
Ok(messages) => {
let n = length(messages);
println("=== runTools succeeded: ${show(n)} messages in transcript ===");
println("Final assistant text:");
println(lastMessage(messages))
},
Err(e) =>
-- Typed-error retry-decision branch — the whole point of returning
-- AIError instead of a plain string.
if e.retryable
then println("Transient error (would retry): ${renderError(e)}")
else println("Fatal error: ${renderError(e)}")
}
}
For the broader integration with motoko_agent (the first AILANG-native
coding agent retiring its hand-rolled tool dispatch), see the
Motoko integration sequence.
Image Generation
std/ai generates images through the same AI effect:
| Function | Effects | Returns |
|---|---|---|
callImage(prompt, output_path, options) | {AI, FS} | the output path |
callImageBase64(prompt, options) | {AI} | {"base64": ..., "mime_type": ...} JSON |
callImageWithRefs(prompt, refs, output_path, options) | {AI, FS} | the output path |
callImageBase64WithRefs(prompt, refs, options) | {AI} | {"base64": ..., "mime_type": ...} JSON |
Providers. Gemini image models (--ai gemini-2.5-flash-image) and
OpenRouter image-output models (for example google/gemini-3-pro-image,
openai/gpt-5-image) through the OpenRouter key. OpenRouter image calls use
the chat endpoint with "modalities": ["image","text"], so the image cost
appears in the same usage.cost figure as text calls. Anthropic, OpenAI-direct,
Ollama and config-driven providers reject image generation with an error.
Options is a JSON string; every key is optional:
aspect_ratio:"1:1","16:9","9:16", ...mime_type:"image/png"(default) or"image/jpeg". OpenRouter also accepts"image/webp"; any other value fails before the request is sent.model: a model for this call only. When it is absent or empty, the--aimodel is used. A friendlymodels.ymlname resolves the same waystep()resolves one. A model that belongs to a different provider than the--aihandler fails withModelNotAllowed. For example, one OpenRouter-bound program can keep a text model bound and name an image model for each portrait.
Reference images (*WithRefs) let the model see one or more images as
well as the prompt. Use them to edit an image, or to keep the same character
across variations. Each ref is an ImagePart record {source, mime}, the
same record step() uses for vision input. source is a base64 payload or a
data-URI. std/fs.readFileBytes returns base64 you can pass straight in, and
mime is required for raw base64. Gemini receives refs as inlineData parts
and OpenRouter as image_url content parts. Every other provider fails with
"not supported" instead of dropping them, and an empty refs list behaves
like the call without refs. Under --ai-stub, all four functions return a
fixed 1x1 PNG without any network call.
-- AILANG AI Image Generation Example
-- Demonstrates generating images via AI models
--
-- Usage with real API (Gemini direct):
-- ailang run --ai gemini-2.5-flash-image --caps AI,FS,IO --entry main examples/runnable/ai_image_generation.ail
--
-- Usage through one OpenRouter key (bind an image-output model; step 3 also
-- names one per call via the options "model" key):
-- ailang run --ai openrouter/google/gemini-3.1-flash-image --caps AI,FS,IO --entry main examples/runnable/ai_image_generation.ail
--
-- Usage with stub (for testing):
-- ailang run --ai-stub --caps AI,FS,IO --entry main examples/runnable/ai_image_generation.ail
--
-- What this demonstrates:
-- - AI.callImage: generate an image and save to file
-- - AI.callImageBase64: generate an image and get base64 data
-- - AI.callImageWithRefs: condition a new image on a reference image
-- - "model" in options: per-call image model override
-- - Effect composition: {AI, FS} for file-writing, {AI} for base64
module examples/runnable/ai_image_generation
import std/ai as AI
import std/fs (readFileBytes)
import std/io (println)
export func main() -> string ! {AI, FS, IO} =
let _ = println("AILANG AI Image Generation Demo") in
let _ = println("===============================") in
let _ = println("") in
-- Generate and save an image
let _ = println("1. Generating image and saving to file...") in
let path = AI.callImage("A simple geometric pattern with blue circles on white background", "/tmp/ailang_demo_image.png", "{}") in
let _ = println(" Saved to: ${path}") in
-- Generate with base64 output
let _ = println("") in
let _ = println("2. Generating image as base64...") in
let b64json = AI.callImageBase64("A small red square on white background", "{\"aspect_ratio\": \"1:1\"}") in
let _ = println(" Got base64 JSON response") in
-- Reference-conditioned generation with a per-call image model
let _ = println("") in
let _ = println("3. Generating a variation of image 1 (reference image + per-call model)...") in
let variation = match readFileBytes(path) {
Ok(ref) => AI.callImageWithRefs("The same pattern, recoloured in green", [{ source: ref, mime: "image/png" }], "/tmp/ailang_demo_variation.png", "{\"model\": \"google/gemini-3-pro-image\"}"),
Err(e) => "reference unreadable: ${e}"
} in
let _ = println(" Saved to: ${variation}") in
let _ = println("") in
let _ = println("Done!") in
path
Builtin Documentation
View full builtin documentation:
ailang builtins show _ai_call
ailang builtins show _ai_call_result # v0.17.0+
ailang builtins show _ai_step # v0.17.0+
ailang builtins show _ai_call_image_with_refs
Comparison: AI Effect vs HTTP API
| Feature | AI Effect (std/ai) | HTTP API (std/net) |
|---|---|---|
| Complexity | Simple string→string | Full HTTP control |
| Provider setup | CLI flag | Manual headers/auth |
| JSON handling | By convention | Required |
| Best for | Quick LLM calls | Custom API integration |
Use the AI effect for simple LLM calls. Use std/net when you need full control over HTTP requests (custom endpoints, streaming, etc.).
OpenRouter for Multi-Vendor Routing
The AI effect can also be backed by OpenRouter,
a single API that fronts ~100 models across many vendors. Pass a
vendor/model model string (e.g., anthropic/claude-sonnet-4.5,
openai/gpt-5-mini, or openrouter/auto) and AILANG dispatches the
call through OpenRouter automatically.
export OPENROUTER_API_KEY=sk-or-...
ailang run --caps IO,AI --ai openrouter/auto --entry main file.ail
For dynamic provider selection (fallback chains, capability requirements,
cost preferences) see the AI Provider Routing guide —
this complements the --ai flag with --routing-fallback,
--routing-require, --routing-prefer, and the --allow-routing safety
gate.
Related Documentation
- AI Provider Routing - OpenRouter routing flags and policy reference
- AI API Integration (HTTP) - Raw HTTP approach
- Effects Reference - Effect system overview
- Module Execution - Running AILANG modules