Aha! Builder applications can make AI and LLM calls from their own server code — summarizing text, classifying input, analyzing images and documents, and streaming generated responses back to the screen. Every request runs server-side, and both the input and the output are automatically checked for unsafe content, so moderation is built in. Elle wires this into your application when you describe an AI-powered feature; you do not write the code yourself.
This reference documents the ai API your application uses: models, reasoning levels, structured output, file attachments, and streaming. Read it to see what your applications can do with AI, to review the code Elle generates, or to ground an AI assistant in the specifics — paste this article into Elle or any LLM so it knows exactly how AI works inside an Aha! Builder application.
Click any of the following links to skip ahead:
Overview
Use ai.singleResponse() inside any server function to make AI/LLM calls. It runs server-side — the client never calls AI directly.
Both inputs and outputs are automatically checked for safety (UnsafeContentError). Moderation is built in — you do not need to add your own content filtering.
Without structuredOutput, ai.singleResponse() returns a plain string. Always wrap the result in an object before returning from a server function — returning a raw string can cause JSON serialization errors when the response contains special characters like quotes or newlines.
import { server, ai } from '@aha-app/builder-core';
server.data('summarize', async ({ text }) => {
const summary = await ai.singleResponse({
systemPrompt: 'Summarize the following text in one sentence.',
userPrompt: text,
});
return { summary };
});
server.data('categorize', async ({ text }) => {
return await ai.singleResponse({
systemPrompt: 'Categorize the input into one of: bug, feature, question.',
userPrompt: text,
reasoning: 'medium',
structuredOutput: [
{ name: 'category', type: 'string', desc: 'The category', required: true },
{
name: 'confidence',
type: 'string',
desc: 'Confidence level: high, medium, low',
required: true,
},
],
});
});Options
type AIMODEL = 'fast' | 'balanced' | 'strong';
// 'minimal' (no reasoning), 'low' (fast/cheap), 'medium' (balanced), 'high' (strongest)
type AIReasoning = 'minimal' | 'low' | 'medium' | 'high';
type AIAttributeType = 'string' | 'integer' | 'float' | 'boolean';
interface SingleResponseOptions {
systemPrompt: string;
userPrompt: string;
model?: AIMODEL; // default: 'fast'
reasoning?: AIReasoning; // default: 'minimal'
temperature?: number; // default: 0.7
structuredOutput?: Array<{
name: string;
type?: AIAttributeType;
desc?: string;
required?: boolean;
}>;
attachments?: string[]; // FileBlob IDs for file inputs (images, documents)
}model
Controls which LLM backs the request. Default: 'fast'.
Value |
Best for |
Trade-off |
|
High-volume, low-latency tasks: summarization, extraction, classification, simple Q&A |
Cheapest and fastest, but less capable on nuanced or multi-step reasoning |
|
Most use cases: content generation, analysis, code review, moderate reasoning |
Good balance of quality, speed, and cost |
|
Tasks where quality matters most: complex reasoning, long-form writing, subtle instructions |
Slowest and most expensive, but highest quality output |
Start with 'fast' and move up only if output quality is insufficient.
reasoning
Controls how much the model "thinks" before responding. Default: 'minimal'.
Value |
Best for |
|
Direct tasks with clear instructions — summarization, extraction, reformatting |
|
Light reasoning — classification, simple comparisons |
|
Multi-step analysis — weighing trade-offs, drawing conclusions from data |
|
Complex reasoning — math, logic puzzles, nuanced judgment, multi-constraint problems |
Higher reasoning increases latency and cost. Most tasks work well with 'minimal' or 'low'.
temperature
Sampling temperature (0–2). Default: 0.7. Lower values (0–0.3) give more deterministic output, useful for classification or extraction. Higher values (0.8–1.5) give more creative output, useful for writing or brainstorming. Most relevant when using the 'fast' model; 'balanced' and 'strong' models rely more on reasoning than sampling.
structuredOutput
Use structuredOutput to get back a typed object instead of free-form text. This is the recommended approach for any RPC-style AI call where you need to parse or act on the response programmatically. Define the fields you want and the response will contain exactly those keys.
Each field takes:
Property |
Required |
Description |
|
yes |
The key name in the returned object |
|
no |
|
|
no |
Description to guide the LLM on what to produce |
|
no |
When |
server.data('analyzeSentiment', async ({ text }) => {
const result = await ai.singleResponse({
systemPrompt: 'Analyze the sentiment of the given text.',
userPrompt: text,
structuredOutput: [
{
name: 'sentiment',
type: 'string',
desc: 'positive, negative, or neutral',
required: true,
},
{ name: 'score', type: 'float', desc: 'Confidence score from 0 to 1' },
{ name: 'summary', type: 'string', desc: 'One sentence explanation' },
],
});
// result is { sentiment: 'positive', score: 0.95, summary: 'The text expresses enthusiasm...' }
return result;
});attachments
Pass an array of FileBlob IDs to include files in the AI request. The model will see the files alongside the text prompts.
Supported file types: images (PNG, JPEG) and documents (PDF, DOCX, XLSX, TXT, CSV, MD).
File size limits: Images must be under 10 MB each. Documents must be under 10 MB each.
Validation errors: If a file has an unsupported type, the AI call throws an error. For non-streaming calls, the error is returned as a thrown exception in the server function. For streaming calls, an error SSE event is emitted with the message.
Upload a file using uploadFile on the client, then pass the blob ID to a server function:
import { uploadFile } from '@aha-app/builder-core';
const blobId = await uploadFile(file);
await analyzeFile({ fileId: blobId });On the server, pass the blob ID in the attachments array:
import { server, ai } from '@aha-app/builder-core';
server.data('analyzeFile', async ({ fileId }) => {
const text = await ai.singleResponse({
systemPrompt: 'Analyze the contents of the attached file.',
userPrompt: 'What do you see?',
model: 'balanced',
attachments: [fileId],
});
return { text };
});You can combine attachments with structuredOutput for typed analysis:
server.data('classifyImage', async ({ imageId }) => {
return await ai.singleResponse({
systemPrompt: 'Classify the image.',
userPrompt: 'What category does this image belong to?',
attachments: [imageId],
structuredOutput: [
{ name: 'category', type: 'string', desc: 'The image category', required: true },
{ name: 'confidence', type: 'float', desc: 'Confidence score from 0 to 1' },
],
});
});Streaming AI
For streaming text to the client (e.g., typing effect), use ai.streamedSingleResponse() to get a stream, then streamToResponse() to return it as an SSE response.
Server side:
import { server, ai, streamToResponse } from '@aha-app/builder-core';
server.data('storyTeller', async ({ topic }) => {
const stream = ai.streamedSingleResponse({
systemPrompt: 'You are a creative storyteller.',
userPrompt: topic,
});
return streamToResponse(stream);
});Client side — use invokeStreamingFunction which handles SSE parsing:
import { invokeStreamingFunction } from '@aha-app/builder-core';
const [text, setText] = useState('');
const handleGenerate = async () => {
setText('');
await invokeStreamingFunction('storyTeller', [{ topic: prompt }], chunk => {
setText(prev => prev + chunk);
});
};streamedSingleResponse accepts the same options as singleResponse except structuredOutput, which is not supported with streaming.
Build it with Elle
Describe the capability you want in plain language, and Elle, the Aha! Builder AI assistant, builds it into your application using the framework above.
New to this? Open Elle, ask what it can do, and try a capability in your application today.