stackbone.ai
An OpenAI-compatible client pointed at OpenRouter. Same surface as the official
openaiSDK (chat.completions,embeddings,images,models), so any code or tool that speaks OpenAI works against any of the 300+ models OpenRouter resolves.
Two ways to use models
A workspace reaches models in two distinct ways — don't confuse them:
- The agent's own reasoning model. A deep agent's
main loop is driven by the
modelyou pass todefineDeepAgent(...)in itsindex.ts. The runtime resolves that model itself; you do not callstackbone.aifor it. - Ad-hoc model calls —
stackbone.ai. When you want a one-off completion, an embedding, an image, or the model catalogue from inside a tool or a workflow step, you callstackbone.ai. This page is about that surface.
Mental model
stackbone.ai wraps the official openai package with baseURL
overridden to OpenRouter. The underlying OpenAI instance is built
lazily on the first method call, so env var rotation between the
moment the client was resolved and the first request is honoured. A
single instance is reused across all four namespaces.
You reach the surface through the ambient stackbone handle, backed by a
single OpenRouter client per process. stackbone.ai is the entrypoint you
use inside an agent tool and inside a
durable workflow step. Import once:
import { stackbone } from '@stackbone/sdk';. You get the same method
surface, the same Result envelope, and the same ai_* error catalogue
wherever you call it.
Required capability: ai.openrouter. Every method awaits the
contract handshake before
issuing the request — this gate runs on every stackbone.ai.* call no
matter where it originates (an agent tool or a workflow step).
Configuration
| Source | Falls back to |
|---|---|
createClient({ openrouterKey }) |
OPENROUTER_API_KEY |
createClient({ openrouterBaseUrl }) |
OPENROUTER_BASE_URL, then https://openrouter.ai/api/v1 |
In a real workspace you rarely call createClient — the ambient
stackbone handle resolves the key from the injected OPENROUTER_API_KEY
on its own. When you run stackbone dev, the CLI seeds
OPENROUTER_API_KEY for you: a key exported in your shell always wins,
otherwise the CLI uses your organization's OpenRouter key. With no key
available, your agents and workflows boot fine and stackbone.ai calls
simply return openrouter_key_missing until one is set.
Both HTTP-Referer and X-Title are sent on every request so
OpenRouter's leaderboard credits the platform.
Chat completions
Call stackbone.ai from inside a tool. The example below is an agent tool
in deep-agents/<name>/index.ts:
import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';
const summarise = tool(
async ({ text }: { text: string }) => {
const result = await stackbone.ai.chat.completions.create({
model: 'openai/gpt-4o-mini',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: `Summarise:\n\n${text}` },
],
});
if (result.error) throw new Error(result.error.code);
return result.data.choices[0]?.message.content ?? '';
},
{
name: 'summarise',
description: 'Summarise a block of text.',
schema: z.object({ text: z.string() }),
},
);The same call works verbatim from a durable
workflow step — put it inside a function
marked 'use step' so the completion is persisted and the step is
retried on failure.
The model string is a creator/model-name id — see the
Vercel AI SDK model addressing reference
for the convention, and the model catalogue below to
list everything OpenRouter resolves.
Streaming
Pass stream: true and consume the resulting iterator. The Result
envelope only covers connection establishment; once the stream is
open, mid-flight errors propagate through the iterator — wrap your
for await loop in try/catch.
const stream = await stackbone.ai.chat.completions.create({
model: 'openai/gpt-4o-mini',
messages: [{ role: 'user', content: 'Stream a haiku.' }],
stream: true,
});
if (stream.error) throw new Error(stream.error.code);
let text = '';
try {
for await (const chunk of stream.data) {
text += chunk.choices[0]?.delta?.content ?? '';
}
} catch (cause) {
// Mid-flight provider error — same `mapApiError` shape as non-streaming.
}Aborting
All four namespaces accept an optional signal: AbortSignal in the
second argument. Aborts surface as ai_aborted in non-streaming
calls; in streaming, the iterator simply terminates.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
await stackbone.ai.chat.completions.create(
{ model: 'openai/gpt-4o-mini', messages },
{ signal: controller.signal },
);Embeddings
const result = await stackbone.ai.embeddings.create({
model: 'openai/text-embedding-3-small',
input: 'Hello, world.',
});
if (result.error) throw new Error(result.error.code);
const [embedding] = result.data.data;For RAG ingest/retrieve you almost never need to call this directly —
stackbone.rag embeds for you using the same OpenRouter pool.
Image generation
OpenRouter does not implement OpenAI's /v1/images/generations
endpoint; image models are reached via /v1/chat/completions with
modalities: ['image'] and the resulting image comes back as a
non-standard message.images[] array of base64 data URLs. The SDK
encapsulates that quirk and surfaces an OpenAI-shaped response:
const result = await stackbone.ai.images.generate({
model: 'google/gemini-2.5-flash-image-preview',
prompt: 'A retro pixel-art skyline at sunset.',
imageConfig: { aspect_ratio: '16:9' }, // forwarded as `image_config`
});
if (result.error) throw new Error(result.error.code);
const [image] = result.data.data;
console.log(image.mimeType, image.b64Json?.slice(0, 32), '…');If the model returns no images (wrong model, content policy, …) the
result is ai_no_image_generated rather than an empty success — the
failure is never silently swallowed.
Model catalogue
const result = await stackbone.ai.models.list();
if (result.error) throw new Error(result.error.code);
for (const model of result.data.data) {
console.log(model.id, model.context_length, model.pricing);
}models.list() calls GET ${baseURL}/models directly (instead of
going through the upstream openai.models.list() parser) so
OpenRouter-specific fields like pricing, context_length,
supported_parameters and architecture make it through verbatim.
Errors
Upstream OpenAI.APIErrors are mapped to stable ai_* codes:
| Code | When |
|---|---|
ai_unauthorized |
401 — bad or revoked API key. |
ai_credits_exhausted |
402 — organization ran out of OpenRouter credits. |
ai_forbidden |
403. |
ai_validation_error |
400 / 422 — malformed request. |
ai_rate_limited |
429. |
ai_moderation_blocked |
451. |
ai_timeout |
408 or APIConnectionTimeoutError. |
ai_aborted |
The caller's AbortSignal fired. |
ai_network_error |
Anything that did not reach the provider (connection reset, DNS, …). |
ai_provider_error |
Anything else from OpenRouter (5xx, unmapped 4xx). |
ai_no_image_generated |
images.generate() succeeded but the model returned no images. |
openrouter_key_missing |
OPENROUTER_API_KEY (and openrouterKey override) absent. |
error.meta includes status, model, and OpenRouter's own
code / type when present. error.cause is the original
OpenAI.APIError instance for callers that want to introspect it.
The contract gate adds contract_version_unsupported,
capability_unavailable, contract_unreachable and
contract_malformed — see
overview.
Where to go next
- Agents — set your
agent's reasoning model and reach
stackbone.aifrom its tools. - Workflows — call
stackbone.aifrom a durable'use step'so completions are persisted and retried. See also Building durable AI agents. stackbone.rag— usesstackbone.ai.embeddingsunder the hood for ingest and retrieval.stackbone.approval— pair withstackbone.aito build LLM tools that pause for a human before executing.- OpenRouter docs — the upstream provider.