← All @molecule/* packages · App templates
@molecule/api-aiCore interface · ai · API (Node) · v1.2.0 · Apache-2.0
Model-agnostic AI provider interface with streaming chat and tool use
npm install @molecule/api-ai@molecule/api-ai is the ai core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 10 providers: @molecule/api-ai-alibaba, @molecule/api-ai-anthropic, @molecule/api-ai-deepseek, @molecule/api-ai-google, @molecule/api-ai-local, @molecule/api-ai-minimax, @molecule/api-ai-moonshot, @molecule/api-ai-openai, @molecule/api-ai-xai, @molecule/api-ai-zhipu.
import { requireProvider } from '@molecule/api-ai'
import type { ChatParams } from '@molecule/api-ai'
const ai = requireProvider()
const params: ChatParams = {
messages: [{ role: 'user', content: 'Hello!' }],
stream: true,
}
let reply = ''
for await (const event of ai.chat(params)) {
if (event.type === 'text') reply += event.content // or forward the chunk to the client (SSE)
}
console.log(reply)Providers (10): @molecule/api-ai-alibaba, @molecule/api-ai-anthropic, @molecule/api-ai-deepseek, @molecule/api-ai-google, @molecule/api-ai-local, @molecule/api-ai-minimax, @molecule/api-ai-moonshot, @molecule/api-ai-openai, @molecule/api-ai-xai, @molecule/api-ai-zhipu
Works with: @molecule/api-bond, @molecule/api-i18n
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Model-agnostic AI chat interface for molecule.dev.
Defines the AIProvider interface that bond packages (Anthropic, OpenAI, etc.)
implement, plus types for messages, streaming events, tool use, and token usage.
import { requireProvider } from '@molecule/api-ai'
import type { ChatParams } from '@molecule/api-ai'
const ai = requireProvider()
const params: ChatParams = {
messages: [{ role: 'user', content: 'Hello!' }],
stream: true,
}
let reply = ''
for await (const event of ai.chat(params)) {
if (event.type === 'text') reply += event.content // or forward the chunk to the client (SSE)
}
console.log(reply)
core
npm install @molecule/api-ai @molecule/api-bond @molecule/api-i18n
AIConfigAI provider configuration.
interface AIConfig {
/** Default model to use when not specified in ChatParams. */
defaultModel?: string
/** Maximum tokens for completions. */
maxTokens?: number
/** Default temperature. */
temperature?: number
}
AIProviderAI provider interface.
Each bond package (Anthropic, OpenAI, Gemini, etc.) implements this interface to provide model-specific chat functionality.
interface AIProvider {
readonly name: string
/**
* Send a chat request and stream back events.
*
* Returns an async iterable of ChatEvent objects.
* Always yields a final 'done' event with token usage.
* @returns An async iterable that yields `ChatEvent` objects (text chunks, tool calls, done, or error).
*/
chat(params: ChatParams): AsyncIterable<ChatEvent>
}
AiRateLimitEventDetails of one rate-limited or overloaded upstream response, reported via a
provider config's onRateLimit callback each time the provider receives an
HTTP 429/503 (or a provider-specific overload status such as Anthropic's
529), before any retry sleep. This surfaces the hits the provider's internal
retry loop goes on to recover — the early-warning signal that capacity is
running out, which a terminal error event alone would miss entirely.
interface AiRateLimitEvent {
/** Provider name (e.g. 'anthropic'). */
provider: string
/** Model the rejected request targeted. */
model: string
/** HTTP status the upstream returned (429, 503, or 529). */
status: number
/** 1-based attempt number that was rejected. */
attempt: number
/** Whether the provider will retry this request again after a delay. */
willRetry: boolean
/** Delay before the next retry in milliseconds; 0 when `willRetry` is false. */
retryInMs: number
/** The response's parsed `Retry-After` header in seconds, when present and valid. */
retryAfterSeconds?: number
}
AIToolTool definition that the AI model can invoke.
interface AITool {
name: string
description: string
parameters: JSONSchema
execute: (input: unknown) => Promise<unknown>
}
ChatMessageChat message in a conversation.
interface ChatMessage {
role: 'user' | 'assistant' | 'system'
content: string | ContentBlock[]
/**
* Model-native reasoning text produced alongside this (assistant) message —
* e.g. an OpenAI-compatible provider's `reasoning_content`. Some providers
* (Moonshot's Kimi K3 / k2.7-code and other preserved-thinking models)
* require it to be replayed verbatim on subsequent requests within a
* tool-call loop to keep reasoning continuity; callers that captured
* `thinking` stream events should set it when rebuilding history. Bonds
* whose provider has no such requirement ignore it.
*/
reasoning?: string
}
ChatParamsParameters for a chat call.
interface ChatParams {
messages: ChatMessage[]
tools?: AITool[]
/** Provider-native server tools (e.g. web search) — executed by the provider, not the caller. */
serverTools?: ServerTool[]
system?: string
stream?: boolean
maxTokens?: number
temperature?: number
model?: string
/**
* Enable extended thinking / reasoning on models that support it.
*
* `budgetTokens` is the abstract reasoning budget; bonds without a native
* token-budget param translate it (e.g. via thresholds) into their provider's
* control. `effort` — when present — is the PROVIDER-NATIVE effort value for
* the active model, resolved by the caller from the model catalog (the model's
* own `supportedEffortLevels`), e.g. Anthropic `output_config.effort`
* (`'low' | 'medium' | 'high' | 'xhigh' | 'max'`) or an OpenAI-compatible
* `reasoning_effort`. Bonds MUST prefer `effort` over
* `budgetTokens` when set: on current Anthropic models (Fable 5 / Opus 4.8 /
* Sonnet 5) a raw `budget_tokens` request is rejected with a 400 — adaptive
* thinking + effort is the only control.
*/
thinking?: { type: 'enabled'; budgetTokens: number; effort?: string }
/**
* Request the provider's fast/priority speed tier for this call (e.g.
* Anthropic's `speed: "fast"` — same model, faster output, premium pricing).
*
* Callers should only set `'fast'` for models whose catalog entry declares
* `fastPricing` (that field is the capability flag). Bonds whose provider has
* no speed tier ignore it. Bonds that support it MUST report the speed the
* provider says the request actually ran at in `TokenUsage.speed`, so
* metering prices the served tier, not the requested one. No automatic
* fallback is performed here: a fast-tier rate limit surfaces as a normal
* error/retry, and the caller decides whether to drop back to standard.
*/
speed?: 'standard' | 'fast'
/** Enable prompt caching. Providers that support it will cache system prompts and tools. */
cacheControl?: { type: 'ephemeral' }
/** Abort signal to cancel in-flight API requests when the client disconnects. */
signal?: AbortSignal
/**
* Control whether the model must call a tool. 'auto' (default) lets the model
* decide; 'required' forces at least one tool call (any tool); `{ type: 'tool',
* name }` forces that ONE specific tool — stronger than 'required', which only
* guarantees *some* tool and can let the model drift to a different one when the
* conversation history is biased toward it.
*/
toolChoice?: 'auto' | 'required' | { type: 'tool'; name: string }
/**
* Opaque, stable identifier for the END USER on whose behalf this request is
* made — forwarded to providers that accept one (Anthropic `metadata.user_id`,
* OpenAI-compatible `user`).
*
* This is an ABUSE-ATTRIBUTION control, and it is the difference between a
* provider suspending one account and suspending your organization's key. With
* nothing sent, every request from every tenant is indistinguishable from the
* platform itself, so the only enforcement action available to the provider is
* against the key that serves all of them.
*
* It MUST be opaque — a hash/uuid, never an email, name, phone or anything else
* that identifies a person to the provider — and it MUST be stable per user, so
* a repeat offender is recognizable across sessions. Callers are expected to
* derive it (e.g. an HMAC of their internal user id) and to keep the mapping on
* their side so an abuse report naming this value can be traced back.
*
* Bonds whose provider has no equivalent field ignore it.
*/
endUserId?: string
/**
* Extra provider-native request-body params, shallow-merged into the outgoing
* request body as a BASE — the bond's own structural fields (model, messages,
* tools, stream, and its computed token limit) are applied AFTER and always
* win, so this can add or override tunables the SDK does not model
* (`reasoning_effort`, `enable_thinking`, `top_k`, `top_p`, penalties, …)
* without ever corrupting the wire protocol.
*
* Primarily for "bring your own AI" endpoints whose server accepts params
* this SDK has no typed field for. Bonds merge it at the request-body ROOT;
* for a nested-config wire format (Gemini) a `generationConfig` object here is
* merged INTO the bond's `generationConfig` rather than replacing it. Values
* must be JSON-serializable. Bonds that build a fixed body ignore it.
*/
extraBody?: Record<string, unknown>
}
JSONSchemaJSON Schema subset for tool parameter definitions.
interface JSONSchema {
type: string
properties?: Record<string, JSONSchema>
items?: JSONSchema
required?: string[]
description?: string
enum?: unknown[]
[key: string]: unknown
}
ServerToolProvider-native tool handled server-side (e.g., Anthropic's web_search).
Unlike AITool, server tools are executed by the AI provider itself —
no client-side execute callback is needed. The provider passes them
through to the API alongside custom tools.
interface ServerTool {
/**
* Provider-specific tool type identifier — a VERSIONED string the provider
* publishes (e.g. Anthropic's `"web_search_20260209"` on current models;
* older models use earlier variants like `"web_search_20250305"`). Copy the
* exact value from the provider's docs for the model in use.
*/
type: string
/** Tool name. */
name: string
/** Allow additional provider-specific fields (max_uses, etc.). */
[key: string]: unknown
}
TokenUsageToken usage from a chat completion.
interface TokenUsage {
inputTokens: number
outputTokens: number
/** Number of input tokens written to the prompt cache. */
cacheCreationInputTokens?: number
/** Number of input tokens read from the prompt cache. */
cacheReadInputTokens?: number
/**
* The speed tier the provider REPORTS the request ran at (Anthropic wires it
* as `usage.speed` per the fast-mode docs, and as `usage.service_tier` on
* observed live responses — bonds read both). Only set by bonds whose
* provider has a fast/priority tier — absent means unreported. Metering
* prefers THIS field over the requested `ChatParams.speed`; when a
* fast-requested turn succeeds with no report, callers may conservatively
* bill the requested tier (Anthropic enforces fast-mode entitlement with a
* hard 429, so a successful fast request was served fast).
*/
speed?: 'standard' | 'fast'
}
AiRateLimitCallbackCallback invoked on each rate-limited/overloaded upstream response. Must not throw (providers guard it anyway); keep it fast — it runs on the request path.
type AiRateLimitCallback = (event: AiRateLimitEvent) => void
ChatEventStreaming event from an AI chat call.
type ChatEvent =
| { type: 'text'; content: string }
| { type: 'thinking'; content: string }
// `signature`: provider-opaque replay token for this tool call (see the
// ContentBlock tool_use variant) — consumers must persist it alongside
// id/name/input and echo it on the replayed tool_use block.
| { type: 'tool_use'; id: string; name: string; input: unknown; signature?: string }
// Emitted as soon as the model BEGINS a tool call — id + name are known but the
// input is still streaming. Lets consumers show what's happening immediately
// (e.g. "Writing the plan") instead of staring at a frozen spinner for the
// seconds-to-minutes it takes to generate a large tool input (a file, a plan).
| { type: 'tool_use_start'; id: string; name: string }
// Progress for a tool call's input as the model streams its arguments:
// `chars` is the number of input characters in this chunk. Carries real
// progress (re-arms the stream-progress timeout; ticks the UI's live token
// estimate) where previously these chunks produced only `keep_alive` (no
// content) — the root cause of the dead loading indicator while a big tool
// input was being written. `chars` is a COUNT, not the full content (the UI
// only needs the magnitude, and forwarding the whole input would duplicate the
// final `tool_use`). `text` is the raw partial-JSON CHUNK for this delta —
// consumed SERVER-SIDE only (a coalescing consumer accumulates it to extract a
// few short display fields, e.g. the file `path`, so the UI can label the
// in-flight tool card before the args finish). It is never echoed wholesale to
// the client. Optional so non-streaming/legacy providers can omit it.
| { type: 'tool_input_delta'; id: string; chars: number; text?: string }
// Incremental usage SNAPSHOT — the provider's own token counts as reported so
// far on the wire (e.g. Anthropic's message_start carries the full input +
// cache token counts before any output streams). LATEST WINS; `done.usage`
// remains the authoritative final figure. METERING CONTRACT: consumers MUST
// retain the latest snapshot and book it when a stream ends WITHOUT a `done`
// (client abort, disconnect, progress timeout, provider error) — the upstream
// provider bills those tokens even though the stream was cut, and dropping
// them silently under-meters real spend. Providers whose wire protocol only
// reports usage at stream end (OpenAI-compatible `include_usage`) cannot emit
// mid-stream snapshots; consumers must estimate aborted turns for those.
| { type: 'usage'; usage: TokenUsage }
| { type: 'done'; usage: TokenUsage }
| { type: 'error'; message: string; errorKey?: string }
// Liveness signal. PROVIDER CONTRACT: every streaming provider MUST yield
// `keep_alive` whenever it receives data from the upstream API that produces
// no other ChatEvent — e.g. an SSE ping/keepalive, an empty delta, or buffered
// tool-input/argument chunks streaming in. Consumers use a (long) inter-event
// timeout to detect a dead stream; without keep_alive that timeout false-fires
// while the model is alive but producing only silent chunks (e.g. streaming a
// large tool input). Not forwarded to end clients. Enforced by the provider
// conformance test in this package's __tests__.
| { type: 'keep_alive' }
ContentBlockRich content block within a message.
Includes text, tool interactions, and file attachments (images, documents, audio, video). Provider bonds map these generic blocks to their native API format (e.g., Anthropic base64 source, OpenAI image_url, etc.).
type ContentBlock =
| { type: 'text'; text: string }
| { type: 'image'; mediaType: string; data: string }
| { type: 'document'; mediaType: string; data: string; filename?: string }
| { type: 'audio'; mediaType: string; data: string }
| { type: 'video'; mediaType: string; data: string }
// `signature` is a provider-opaque replay token attached to the tool call
// (Gemini 3.x `thoughtSignature`). Callers that persist tool calls and replay
// them in later requests MUST carry it back on the replayed block verbatim —
// Gemini rejects a replayed functionCall without it (400 "Function call is
// missing a thought_signature"). Providers without such a token omit it, and
// every bond ignores it when it has no native equivalent.
| { type: 'tool_use'; id: string; name: string; input: unknown; signature?: string }
| { type: 'tool_result'; tool_use_id: string; content: string | unknown }
getAllProviders()Retrieves all named AI providers as a Map keyed by provider name.
function getAllProviders(): Map<string, AIProvider>
Returns: Map of provider name → AIProvider.
getProvider()Retrieves the singleton AI provider, or null if none is bonded.
Falls back to a single named provider when no singleton is bonded —
this lets apps that wire bond('ai', 'anthropic', provider) directly
(without going through setProvider's singleton-fallback) still work
with code that uses the simple getProvider() / requireProvider()
accessors. When multiple named providers are bonded, the fallback
declines (returns null) because the choice is ambiguous — those
call sites must use getProviderByName(name) explicitly.
This also applies to an auto-promoted singleton: setProvider('a', p1)
followed by setProvider('b', p2) does NOT leave getProvider() stuck
returning p1 forever — once 'b' is registered the pick is genuinely
ambiguous and getProvider() declines (null), same as if neither had
been auto-promoted. An explicit setProvider(provider) singleton is
unaffected by how many named providers exist.
function getProvider(): AIProvider | null
Returns: The bonded AI provider, or null.
getProviderByName(name)Retrieves a named AI provider, or null if not bonded.
function getProviderByName(name: string): AIProvider | null
name — The provider name (e.g. 'anthropic', 'xai').Returns: The named AI provider, or null.
hasProvider(name)Checks whether an AI provider is currently bonded.
function hasProvider(name?: string): boolean
name — Optional provider name. If omitted, checks the singleton.Returns: true if the provider is bonded.
requireProvider()Retrieves the bonded AI provider, throwing if none is bonded. Use this when AI functionality is required.
Routes through the same resolution as getProvider() so the single-named-
bond fallback applies — apps that wire bond('ai', 'anthropic', provider)
directly still satisfy this call without having to switch to the explicit
getProviderByName() pattern. When resolution declined because MULTIPLE
named providers are bonded with no explicit singleton, the thrown message
says so (distinct from "nothing bonded at all") and points at
getProviderByName().
function requireProvider(): AIProvider
Returns: The bonded AI provider.
setProvider(provider)Registers an AI provider in singleton mode.
setProvider(provider) — bonds a single default provider.function setProvider(provider: AIProvider): void
provider — The default provider implementation for this process.| Provider | Package |
|---|---|
| Alibaba Qwen | @molecule/api-ai-alibaba |
| Anthropic | @molecule/api-ai-anthropic |
| Ai | @molecule/api-ai-deepseek |
| Ai | @molecule/api-ai-google |
| Ai | @molecule/api-ai-local |
| MiniMax | @molecule/api-ai-minimax |
| Moonshot | @molecule/api-ai-moonshot |
| Ai | @molecule/api-ai-openai |
| xAI | @molecule/api-ai-xai |
| Zhipu GLM | @molecule/api-ai-zhipu |
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-bond@molecule/api-i18nThe AI provider is a SERVER-side integration — a weak integration leaks the key, trusts the model, or gets billed:
chat() from YOUR API and stream results
to the browser (SSE); NEVER put the AI key in the frontend or call the provider directly
from the browser — the key would ship to every user.maxTokens — an
open, unauthenticated AI route is an unbounded bill.chat() returns an async iterable of ChatEvent (text chunks, tool calls, a final
done with usage) — iterate it and forward chunks to the client.setProvider(name, provider) ambiguity: the FIRST named provider you register also
auto-promotes to the singleton (so plain getProvider()/requireProvider() work without
the caller knowing the name) — but that promotion is a single-provider convenience, not a
permanent pick. The moment a SECOND, differently-named provider is registered,
getProvider() stops returning the first one and declines (null) instead —
requireProvider() throws pointing at getProviderByName(name). Call the explicit
setProvider(provider) (no name) form if you want one provider to always win regardless of
how many named providers you also register.Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual chat/AI screens, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. The sandbox HAS an AI provider bonded, so the flow runs live end-to-end; AI output is NON-DETERMINISTIC, so assert on STRUCTURE/behavior, not exact text:
chat() yields text chunks then a final done; a single late
blob means the reply was awaited whole and streaming is broken.)messages history is sent, not just the last
line.Translation strings are provided by @molecule/api-locales-ai.