Custom providers
How to plug another AI into the cascade: an in-page model (Transformers.js, WebLLM), another API, anything that can chat or embed.
import { BaseProvider } from "@leuria/client"
class MyProvider extends BaseProvider {
constructor() { super("in-page", "Small in-page model", "device", { status: "unknown", capabilities: [] }) }
async detect() { this.setState({ status: "ready", capabilities: ["chat"] }) }
async createSession(options) {
return {
send: async (message, context) => {
// stream with context.text(delta); run page tools with context.runTool({ name, args })
return { text }
},
close() {},
}
}
}
const ai = createLeuria({ providers: [bridge(), new MyProvider(), browserAI()] })
The provider
Implement Provider, or extend BaseProvider, which keeps the state and notifies changes: implement detect() and createSession(), and call setState(patch).
id,labelandlocality(device,visitor-cloudorsite). Ids are unique in a client.getState()returns{ status, capabilities, model?, action?, progress?, detail?, embedModel? };onChange(listener)notifies changes. The core reads the state again after each change.detect()checks availability. Keep it cheap: it runs at creation, on focus and every few seconds while the page is visible.connect()(optional) resolvesneeds-action: pair, start a download… It is called from a click. Setactiontoconnectordownloadso the UI can say what a click does.disconnect()(optional) forgets the visitor's grant in this browser.offers(optional):["chat"]by default;["embed"]for an embedder only,["chat", "embed"]for both.embed({ texts, kind, signal })(forembed): returns{ vectors, model }, one vector per text. Setcapabilitiesto includeembedandembedModelto the model's name when ready.
Capabilities decide what the cascade sends it: chat, tools, structured (native schemas), agent, images, embed (see Providers).
The session
createSession(options) opens a provider-side conversation. It receives:
system;tools: descriptors only (name,description,inputSchema);schema: only when the provider declared nativestructuredsupport (otherwise the core uses the tool route, see Structured output);history: the conversation so far, before the firstsend;maxSteps.
The core owns the history. A session gets each new user message through send(message, context), with the turn context already rendered into the text. It may keep its own context (an agent session) or resend the history (a stateless HTTP API).
- It streams through
context.text(delta)andcontext.reasoning(delta), and may report progress withcontext.status(message). - Tool execution always goes through
context.runTool({ name, args, callId? }), so events, message parts, middleware and the tool budget stay consistent across providers. It never throws: failures come back as{ ok: false, error }, to hand to the model. - When
context.signalaborts (cancel, timeout, a tool'sendTurn), it stops the turn, settles soon after, and keeps itself usable if it can. sendresolves with{ text }when the turn ends.- An optional
warm()prepares it ahead of the first turn (start an agent, load a model). close()ends it.closed(optional): set it when the provider ended the session on its own (e.g. the visitor changed the AI or model): the conversation opens a new one, history included.