Generative AI
xb.ai provides bounded Gemini and OpenAI queries plus Gemini multipart input,
image generation, and Live audio/video sessions. AI availability, credentials,
network responses, and media streams are runtime state; expose waiting,
unavailable, empty, disconnected, and failed states in the application.
Provider setup
Gemini is the default provider:
const options = new xb.Options();
options.enableAI();
options.ai.model = 'gemini';
options.ai.gemini.model = 'gemini-3.7-flash';
options.ai.promptForApiKey = true; // optional local prototype prompt
await xb.init(options);
enableAI() enables Gemini. For OpenAI, select and enable it explicitly:
const options = new xb.Options();
options.ai.enabled = true;
options.ai.model = 'openai';
options.ai.openai.enabled = true;
options.ai.openai.model = 'gpt-4.1';
Configure the provider before initialization and check
xb.ai.isAvailable() at use time. Availability confirms local wrapper state;
each request can still fail because of credentials, quota, network, or provider
errors.
Credential safety
Never ship long-lived provider keys in production browser code.
Provider options, ?key=, provider-specific URL parameters, the optional
current-page prompt, and keys.json are local-prototype mechanisms. Production
applications use a server-controlled proxy. Gemini Live clients should use
short-lived credentials such as ephemeral tokens.
The browser prompt stores a key only for the current page session. Encoding a key or putting it in a URL does not protect it.
Bounded queries
Use {prompt} for provider-neutral text:
async function askQuestion() {
if (!xb.ai.isAvailable()) {
showAIState('unavailable');
return;
}
showAIState('waiting');
try {
const response = await xb.ai.query({
prompt: 'Write a short description of this XR scene.',
});
const text =
response && typeof response === 'object' ? response.text : response;
showAnswer(text || 'No answer returned');
} catch (error) {
showAIState('failed');
}
}
Gemini also accepts current typed multipart input:
const response = await xb.ai.query({
type: 'multiPart',
parts: [
{inlineData: {data: base64Png, mimeType: 'image/png'}},
{text: 'Name the objects relevant to this task.'},
],
});
Treat null, missing text, and tool-only responses as expected branches.
Prevent repeated requests while one is active unless the application explicitly
supports concurrency.
Image generation
const dataUrl = await xb.ai.generate(
'a futuristic low-poly treehouse in 3D',
'image'
);
if (dataUrl?.startsWith('data:image/')) image.src = dataUrl;
A successful Gemini result is a data URL. It is not an object with a url
field. Validate the result before loading it as a texture or UI image.
Gemini Live
Register callbacks before connecting and use onopen as the ready transition:
await xb.ai.setLiveCallbacks({
onopen: () => showLiveState('listening'),
onmessage: (message) => handleLiveMessage(message),
onerror: () => stopLive('failed'),
onclose: () => stopLive('disconnected'),
});
if (!xb.ai.isLiveAvailable()) {
showLiveState('unavailable');
return;
}
showLiveState('connecting');
await xb.ai.startLiveSession({
responseModalities: ['AUDIO'],
speechConfig: {
voiceConfig: {prebuiltVoiceConfig: {voiceName: 'Aoede'}},
},
inputAudioTranscription: {},
outputAudioTranscription: {},
});
Use current nested realtime input fields:
xb.ai.sendRealtimeInput({
audio: {data: base64Pcm, mimeType: 'audio/pcm;rate=48000'},
});
xb.ai.sendRealtimeInput({
video: {data: base64Jpeg, mimeType: 'image/jpeg'},
});
The flat {data, mimeType} shape is not the current Live input contract.
startLiveSession() can return a provider session before the connection emits
onopen. Keep the UI in connecting until onopen. Route remote close, local
stop, error, and dispose() through one idempotent cleanup method that stops:
xb.ai.stopLiveSession();- application microphone capture and owned media tracks;
- model-audio playback;
- screenshot or camera frame timers;
- pending UI state and the duplicate-start lock.
The GeminiManager addon at
xrblocks/addons/ai/GeminiManager.js owns the common microphone, audio,
camera/screenshot, tool, timer, and cleanup loop. Use it when that ownership
matches the application. Use the lower-level facade for a materially different
loop.
Tools and grounded actions
An xb.Tool exposes a narrow model-requested action. Model arguments are
untrusted input:
const setColor = new xb.Tool({
name: 'set_object_color',
description: 'Set one allowed scene object to an approved hex color.',
parameters: {
type: 'OBJECT',
properties: {
objectId: {type: 'STRING'},
color: {type: 'STRING'},
},
required: ['objectId', 'color'],
},
onTriggered: async ({objectId, color}) => {
// Resolve objectId through an allowlist and validate color.
return {objectId, color};
},
});
Validate schema, ranges, target identity, and authorization again at execution. Return structured failures for invalid arguments, missing targets, unknown tools, denied actions, and execution errors. Keep consequential actions behind visible confirmation.
Choose the smallest grounding input that answers the question: known app state, semantic scene context, a Set-of-Mark image, a rendered screenshot, or a device camera frame. Context collection and provider transmission are separate decisions. Disclose and send only what the behavior requires.