stackbone.ai

An OpenAI-compatible client pointed at OpenRouter. Same surface as the official openai SDK (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 model you pass to defineDeepAgent(...) in its index.ts. The runtime resolves that model itself; you do not call stackbone.ai for 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 call stackbone.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.ai from its tools.
  • Workflows — call stackbone.ai from a durable 'use step' so completions are persisted and retried. See also Building durable AI agents.
  • stackbone.rag — uses stackbone.ai.embeddings under the hood for ingest and retrieval.
  • stackbone.approval — pair with stackbone.ai to build LLM tools that pause for a human before executing.
  • OpenRouter docs — the upstream provider.

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN