--- title: 'stackbone.ai' description: 'OpenAI-compatible chat, embeddings, image generation and model catalogue via OpenRouter.' position: 2 --- # 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](https://openrouter.ai/docs) > resolves. ## Two ways to use models One drives an agent's main loop, the other is for one-off calls: - The `model` you pass to `defineDeepAgent(...)` in its `index.ts` drives a [deep agent's](/docs/sdk/agents/overview) main loop. The runtime resolves that model itself; you do not call `stackbone.ai` for it. - `stackbone.ai` covers the ad-hoc calls: a one-off completion, an embedding, an image, or the model catalogue, _from inside a tool or a workflow step_. This page covers that surface. ## Mental model `stackbone.ai` wraps the official `openai` package with `baseURL` overridden to OpenRouter. The SDK builds the underlying `OpenAI` instance **on the first method call** and re-reads the credential from the environment on every call. When the operator configures or rotates the model provider while your agent is running, the next call picks up the new key. All four namespaces share one instance, and the SDK rebuilds it only when the credential changes. 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](/docs/sdk/workflows/overview) step. Import once: `import { stackbone } from '@stackbone/sdk';`. You get the same method surface, `Result` envelope, and `ai_*` error catalogue wherever you call it. Required capability: `ai.openrouter`. Every method awaits the [contract handshake](/docs/sdk/reference/overview#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({ modelProviderKey })` | `MODEL_PROVIDER_API_KEY` | | `createClient({ modelProviderBaseUrl })` | `MODEL_PROVIDER_BASE_URL`, then `https://openrouter.ai/api/v1` | Any OpenAI-compatible endpoint works: point the base URL at a gateway (Ollama, LM Studio, LiteLLM, vLLM) and the key becomes optional. In a real workspace you rarely call `createClient`: the ambient `stackbone` handle resolves the credential from the injected `MODEL_PROVIDER_API_KEY` on its own. When you run `stackbone dev`, the CLI seeds it for you: a value exported in your shell always wins, otherwise the CLI uses the provider configured in the Studio **Model provider** screen. With nothing configured, your agents and workflows boot fine and `stackbone.ai` calls return `openrouter_key_missing` until you configure a provider. The SDK sends `HTTP-Referer` and `X-Title` 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//index.ts`: ```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](/docs/sdk/workflows/overview) step: put it inside a function marked `'use step'` so the runtime persists the completion and retries the step on failure. The `model` string is a `creator/model-name` id. See the [OpenRouter model list](https://openrouter.ai/models) for the convention, and the [model catalogue](#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`. ```ts 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. } ``` The SDK asks the provider for a final usage-bearing chunk unless you set `stream_options: { include_usage: false }` yourself. The last chunk of the stream carries `usage` and no content delta. That is what lets a streaming call report its tokens like a non-streaming one, so declining the chunk also drops the call off the run's token totals. See [Token usage on the run](#token-usage-on-the-run). ### 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 terminates. ```ts const controller = new AbortController(); setTimeout(() => controller.abort(), 5_000); await stackbone.ai.chat.completions.create( { model: 'openai/gpt-4o-mini', messages }, { signal: controller.signal }, ); ``` ### Token usage on the run The runtime attributes a chat completion to the run that was executing your code when you made the call. The call lands on that run's timeline as its own model call, with the model id, the latency and the input and output tokens. Its tokens fold into the run's totals. You report nothing and you thread no run id through your code. A streaming call reports once your `for await` loop drains the last chunk, so its recorded duration includes your own iteration pace. Only chat completions report usage. `embeddings.create()`, `images.generate()` and `models.list()` run inside the enclosing step, so the run records the step's timing but no tokens for them. Read the timeline with `stackbone runs get ` or in Studio: see [Logging & observability](/docs/sdk/platform/observability#the-run-timeline-is-automatic). ## Embeddings ```ts 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 rarely call this directly: [`stackbone.rag`](/docs/sdk/data/rag) embeds for you using the same OpenRouter pool. ## Image generation OpenRouter does not implement OpenAI's `/v1/images/generations` endpoint; it serves image models through `/v1/chat/completions` with `modalities: ['image']`, and the 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: ```ts 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. ## Model catalogue ```ts 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 The SDK maps upstream `OpenAI.APIError`s to stable `ai_*` codes: | Code | When | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `ai_unauthorized` | 401: bad or revoked API key. | | `ai_credits_exhausted` | 402: the configured model-provider account ran out of 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` | No model provider resolved (`MODEL_PROVIDER_API_KEY` and the `modelProviderKey` override both absent). The code name is kept for wire compatibility. | `error.meta` includes `status`, `model`, and OpenRouter's own code and type (as `openrouterCode` / `openrouterType`) 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](/docs/sdk/reference/overview#the-contract-handshake). ## Where to go next - **[Agents](/docs/sdk/agents/overview)**: set your agent's reasoning model and reach `stackbone.ai` from its tools. - **[Workflows](/docs/sdk/workflows/overview)**: call `stackbone.ai` from a durable `'use step'` so completions are persisted and retried. See also [Building durable AI agents](https://workflow-sdk.dev/docs/ai). - **[`stackbone.rag`](/docs/sdk/data/rag)**: uses `stackbone.ai.embeddings` internally for ingest and retrieval. - **[`stackbone.approval`](/docs/sdk/humans/approval)**: pair with `stackbone.ai` to build LLM tools that pause for a human before executing. - **[OpenRouter docs](https://openrouter.ai/docs)**: the upstream provider.