Skip to main content

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.

Try AI Effects Live

See the AI effect in action in the browser:

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​

examples/runnable/ai_effect.ail
-- 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​

ProviderModelsAuth MethodEnvironment Variable
Anthropicclaude-sonnet-4-5, claude-haiku-4-5, etc.API KeyANTHROPIC_API_KEY
OpenAIgpt-5, gpt-5-mini, etc.API KeyOPENAI_API_KEY
Googlegemini-2-5-pro, gemini-2-5-flash, etc.API Key or ADCGOOGLE_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:

SwitchEffect
--ai-no-adc or AILANG_AI_NO_ADC=1The 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=PATHThe 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:

examples/docs/npc_dialogue.ail
-- 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.)

FieldMeaning
versionMust be 1.
kindtext, image or speech. Kinds are separate key spaces.
prompt / request / sha256Exactly 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.
responsetext: the reply string.
tool_callstext, optional: replayed by step; finish_reason becomes "tool_calls".
base64, mime_typeimage: the image bytes (standard base64) and media type, both required.
pcm_base64speech: s16le mono 24 kHz PCM (standard base64, a whole number of samples).
errorAny kind, instead of a payload: {code, message, retryable} replayed as a typed AIError.

Canonical request bytes:

KindOperationsCanonical bytes
textcall, callJson, callJsonSimple and their *Result variantsthe prompt (a callJson schema is not part of the key)
textstep, stepWithCache, stepWithStreamthe content of the last message in the conversation
imagecallImage, callImageBase64the prompt (options are not part of the key)
speechcallSpeechthe 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 what std/audio's wavFromPcm and encode take. A provider reply in any other format is Err(ProtocolError) rather than bytes that would play as noise.
  • voice: a provider voice name (Gemini prebuilt voices such as Kore, 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. model picks the TTS model; the default is the bound --ai model when its name contains tts, else gemini-2.5-flash-tts on Vertex or gemini-2.5-flash-preview-tts on AI Studio. language_code is an optional BCP-47 code. Any other key is Err(SchemaValidation).
  • Providers: Gemini (AI Studio and Vertex, through the same --ai handler, so --ai-no-adc and --ai-key-file apply). Every other provider returns Err(CapabilityNotSupported).
  • Testing: --ai-stub returns a fixed 250 ms 400 Hz tone (12000 bytes, identical on every platform); --ai-stub-fixtures replays speech fixtures.

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​

FlagDescription
--ai <model>Set the AI model to use
--ai-stubUse 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-adcGoogle: never fall back to Application Default Credentials; a missing key is AuthFailed (also AILANG_AI_NO_ADC=1)
--ai-key-file PATHRead the provider API key from a file at startup; never echoed (also AILANG_AI_KEY_FILE)
--caps AIEnable AI capability

Model Lookup​

Models are looked up in models.yml or guessed from name prefixes:

  • claude-* → Anthropic
  • gpt-* → OpenAI
  • gemini-* → 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:

CodeRetryable?Typical cause
AuthFailedno401/403 — bad API key
RateLimityes429 — slow down and retry
Timeoutyesnetwork / context deadline
ConnectionFailedyesTCP / TLS / DNS issues
ContextLengthnoprompt exceeds model window
SchemaValidationnoresponse doesn't match callJson schema
ToolsNotSupportednoprovider can't do tools (Ollama, configdriven)
ModelNotFoundno404 — model name typo
BudgetExhaustednoAI cap budget overflow
Internalyes5xx 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:

ProviderTool supportNotes
AnthropicyesMessages API tool_use content blocks
GeminiyesfunctionCall parts; adapter generates stable IDs
OpenAIyesChat Completions tool_calls
OpenRouteryesPassthrough (composes with Routing policies)
Ollamano — typed rejectAIError{ToolsNotSupported}; falls through to chat for no-tools
Configdrivenno in v1Same 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:

FunctionEffectsReturns
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 --ai model is used. A friendly models.yml name resolves the same way step() resolves one. A model that belongs to a different provider than the --ai handler fails with ModelNotAllowed. 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​

FeatureAI Effect (std/ai)HTTP API (std/net)
ComplexitySimple string→stringFull HTTP control
Provider setupCLI flagManual headers/auth
JSON handlingBy conventionRequired
Best forQuick LLM callsCustom 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.