Core concepts
The mental model behind Radon AI — providers, the one message and result shape, models, streaming, tool calling, and the escape hatch to raw provider access.
Radon AI has a small number of ideas. Once they click, every method and every provider behaves predictably.
Providers
A provider is a model vendor — OpenAI, Anthropic, Google Gemini, Groq. You configure the ones you use; Radon lazy-loads each adapter only when it's first touched, so an OpenAI-plus-Anthropic app never bundles the other eight adapters' code. The core is roughly 43 KB.
const ai = new RadonAI({
providers: {
openai: {}, // credentials from RADON_OPENAI_API_KEY
anthropic: { apiKey: "sk-ant-..." }, // or inline
},
defaultProvider: "openai",
});Every call uses defaultProvider unless you override it per call:
await ai.chat({ messages, provider: "anthropic" });Why swapping is a config change
Every provider implements the same interface — chat, streamChat, embed,
and the escape hatches. The message shape you pass in and the result shape you get
back are identical across providers, so changing model vendor is a config edit,
not a rewrite.
One message shape
A conversation is an array of ChatMessages. Each has a role and content.
content is usually a plain string; for vision it can be an
array of content parts. An assistant turn that called tools carries toolCalls;
a role: "tool" message carries one tool's result, keyed back by toolCallId.
const messages = [
{ role: "system", content: "You are terse." },
{ role: "user", content: "Capital of France?" },
];role"system" | "user" | "assistant" | "tool"requiredWho authored the message.
contentstring | ContentPart[]requiredPlain text, or an array of text / image parts for multimodal input.
toolCallsToolCall[]Present on an assistant turn that invoked tools.
toolCallIdstringOn a role: "tool" message: which call this is the result of.
namestringThe tool/function name for a role: "tool" message.
One result shape
Every chat() returns the same ChatResult, whatever the provider. Radon
maps each vendor's response into one shape and normalizes the finish reason and
token usage.
contentstringThe concatenated assistant text. Empty string when the turn was only tool calls.
toolCallsToolCall[]Tool calls the model made this turn ([] when none). arguments is always a
parsed object, never a JSON string.
finishReason"stop" | "length" | "tool_calls" | "content_filter" | "error" | "other"Why generation stopped, normalized across providers.
usage{ promptTokens, completionTokens, totalTokens }Normalized token accounting. Fields are 0 when a provider doesn't report them.
providerstringThe provider that produced this result.
modelstringThe model that produced it (the provider's echoed id, or the one you asked for).
rawunknownThe untouched provider response — reach here for fields Radon doesn't normalize (reasoning traces, log-probs, provider-specific metadata).
Models
Set the model per call, or a defaultModel per provider in config. Every
provider ships a sensible built-in default (see
Providers), but model catalogs move fast — model is
always yours to set.
await ai.chat({ messages, model: "gpt-4o-mini" }); // per call
new RadonAI({ providers: { openai: { defaultModel: "gpt-4o-mini" } } }); // per providerResolution order is: options.model → provider config defaultModel → the
adapter's built-in default.
Defaults are a convenience, not a lock-in
The built-in defaults (e.g. gpt-4o, claude-sonnet-4-6) are documented as
"update as needed", not a guaranteed-current list. Always set model explicitly
for production.
Streaming
For a typing-indicator experience, ai.stream() returns a ChatStream you can
for await over. Providers frame their server-sent events completely differently
— OpenAI streams tool arguments as string fragments, Anthropic as
input_json_delta, Gemini as whole functionCall parts — and Radon reassembles
all of them into one normalized event stream.
const stream = ai.stream({ messages: [{ role: "user", content: "Write a haiku." }] });
for await (const text of stream.textStream()) process.stdout.write(text);Streams are single-consumption and stream() does not honor
fallback. Full details in Streaming.
Tool calling
This is the SDK's reason to exist. Define a tool once as a JSON-Schema-shaped
object; Radon translates it into each provider's own function-calling format and
normalizes the calls that come back — arguments is always a parsed object.
const tools = [{
name: "get_weather",
description: "Get the current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}];
const res = await ai.chat({ messages, tools });
for (const call of res.toolCalls) console.log(call.name, call.arguments);Tool calling is core and free on every provider. See Tools for the full request-run-respond loop.
Free vs. Pro
Gating happens at two levels:
- Provider-level. Three providers work with no license:
openai,anthropic,groq. The other seven (google,mistral,deepseek,openrouter,xai,together,ollama) are Pro. - Feature-level. Four capabilities are Pro even on a free provider: embeddings, vision, structured output, and fallback chains.
Unlock Pro by setting a license key and calling await ai.init(), which verifies
it once and caches it for the process lifetime.
const ai = new RadonAI({
providers: { google: {} }, // a Pro provider
licenseKey: process.env.RADON_LICENSE_KEY,
});
await ai.init(); // verifies the license; unlocks Pro
await ai.chat({ messages, provider: "google" });Using a Pro provider or feature without a valid license throws
LicenseRequiredError; a bad or unreachable key throws LicenseInvalidError
(fail-closed). Introspect tiers with the exported FREE_PROVIDERS set and
isProProvider(slug).
Free providers work with no license and no init() — providers load lazily
on first use. Call init() when you want misconfiguration to fail fast at boot,
or whenever any Pro provider or feature is in play.
Escape hatch
AI providers ship new features weekly. The unified API handles the common case; for everything else, drop to raw access without leaving your Radon config.
// 1) The official provider SDK, preconfigured with your Radon credentials.
// Requires the optional peer dep (e.g. `npm i openai`).
const openai = await ai.native<import("openai").OpenAI>("openai");
await openai.beta.assistants.list();
// 2) A preconfigured fetch client (base URL + auth ready) — zero extra install.
const client = await ai.rawClient("anthropic");
await client.request("/messages", { method: "POST", json: { /* raw body */ } });
// 3) Per-call: merge arbitrary provider fields into the request body.
await ai.chat({ messages, providerOptions: { logit_bias: { "50256": -100 } } });
// 4) The untouched provider response is always on `result.raw`.
const res = await ai.chat({ messages });
res.raw;native() throws a clear NativeClientUnavailableError (with an npm install
hint) if the official SDK isn't installed. rawClient() never needs a dependency.