@radonsdk/ai
v0.1.0
Published
One unified, provider-agnostic API for 10 AI model providers — OpenAI, Anthropic, Google Gemini, Groq, Mistral, DeepSeek, OpenRouter, Ollama, xAI (Grok), Together AI. Write chat() once and swap providers via config. Normalized streaming, one tool schema t
Readme
@radonsdk/ai
One unified, provider-agnostic API for 10 AI model providers — OpenAI, Anthropic, Google Gemini, Groq, Mistral, DeepSeek, OpenRouter, Ollama, xAI (Grok), and Together AI. Write
ai.chat()once and swap providers with a config change, never a code change.
Radon AI gives you a single, typed interface (chat, stream, embed) that every provider implements. Provider-specific quirks — auth schemes, wildly different tool-calling formats, SSE framing, JSON-mode dialects — are absorbed inside each adapter and never leak into your code. And because AI APIs move faster than any wrapper can keep up, every adapter exposes an escape hatch to the provider's raw or official-SDK client, so you never get stuck.
- 10 providers, one API — one message shape, one result shape.
- One tool schema that works everywhere — define a tool once; Radon translates it into OpenAI's function format, Anthropic's
tool_useblocks, and Gemini'sfunctionDeclarations, and normalizes the calls that come back. This is the hard part, and it's the point. - Normalized streaming —
for awaittext deltas and tool-call deltas identically across providers, no matter how each one frames its SSE. - Escape hatch, first-class —
ai.native()returns the official provider SDK (preconfigured);ai.rawClient()returns a preconfiguredfetchclient. Drop to raw features Radon hasn't wrapped without leaving your config. - Lazy-loaded adapters — an OpenAI + Anthropic app never bundles the other eight. The core is ~43 KB.
- Strict TypeScript, ESM + CJS, Node ≥ 18, zero required dependencies (official SDKs are optional peer deps, used only by
native()).
Install
npm install @radonsdk/ai
# or: pnpm add @radonsdk/aiRadon never stores your secrets. Each adapter reads credentials from RADON_<PROVIDER>_API_KEY environment variables (or from the providers config block, which takes precedence). See .env.example for every provider's variable names.
Quickstart (< 5 minutes)
import { RadonAI } from "@radonsdk/ai";
const ai = new RadonAI({
providers: { openai: {} }, // credentials come from RADON_OPENAI_API_KEY
defaultProvider: "openai",
});
const res = await ai.chat({
messages: [{ role: "user", content: "Say hello in one word." }],
});
console.log(res.content); // "Hello"
console.log(res.usage); // { promptTokens, completionTokens, totalTokens }
console.log(res.finishReason); // "stop" | "length" | "tool_calls" | ...Switch providers without touching your chat code — it's a config/option change:
const ai = new RadonAI({ providers: { openai: {}, anthropic: {} } });
await ai.chat({ messages, provider: "openai" });
await ai.chat({ messages, provider: "anthropic" }); // same messages, same shapeSet model per call (e.g. { model: "gpt-4o-mini" }) or a defaultModel per provider in config. Model catalogs move fast, so model is always yours to set — the built-in defaults are a convenience, not a lock-in.
Free vs. Pro
Three providers are free. The other seven — and a handful of features — require a Radon Pro license key, verified once in await ai.init() and cached for the process lifetime.
| Tier | What you get |
| --- | --- |
| Free | openai, anthropic, groq — full chat + streaming + normalized tool-calling. Tool-calling is core, never an upsell. |
| Pro (license) | The other 7 providers (google, mistral, deepseek, openrouter, xai, together, ollama) plus these features on any provider: embeddings, vision / image input, structured output / JSON mode, and fallback chains. |
const ai = new RadonAI({
providers: { google: {} }, // a Pro provider
licenseKey: process.env.RADON_LICENSE_KEY, // or config.license: { key, verifyUrl, ... }
});
await ai.init(); // verifies the license; unlocks Pro
await ai.chat({ messages, provider: "google" });- Free providers work with no license and no
init(). - Using a Pro provider — or a Pro feature — without a valid license throws
LicenseRequiredError; a bad/unreachable key throwsLicenseInvalidError(fail-closed). - Introspect tiers with the exported
FREE_PROVIDERSset andisProProvider(slug).
Every provider — Free or Pro — is built to the same completeness bar. Free/Pro is a licensing concern, not a quality one.
Tool calling — one schema, every provider
This is the SDK's reason to exist. Define a tool once; it works on all ten providers, which structure function-calling completely differently under the hood.
const tools = [
{
name: "get_weather",
description: "Get the current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string", description: "City name" } },
required: ["city"],
},
},
];
const res = await ai.chat({
messages: [{ role: "user", content: "What's the weather in Lagos?" }],
tools,
toolChoice: "auto", // "auto" | "none" | "required" | { name: "get_weather" }
});
// Normalized across every provider: `arguments` is always a parsed object,
// never a JSON string you have to decode yourself.
for (const call of res.toolCalls) {
console.log(call.name, call.arguments); // "get_weather" { city: "Lagos" }
const result = await runYourTool(call);
// Feed the result back — same message shape everywhere:
messages.push(
{ role: "assistant", content: "", toolCalls: [call] },
{ role: "tool", toolCallId: call.id, name: call.name, content: JSON.stringify(result) },
);
}Under the hood Radon translates your one schema into OpenAI's { type: "function", function: {...} }, Anthropic's { name, input_schema } + tool_use/tool_result blocks, and Gemini's functionDeclarations + functionCall/functionResponse parts — and normalizes the calls that come back. You write the loop once.
Streaming — normalized deltas
const stream = ai.stream({ messages: [{ role: "user", content: "Write a haiku." }] });
// Simple case — just the text:
for await (const text of stream.textStream()) process.stdout.write(text);
// Or the full normalized event stream (text + tool-call deltas + a terminal result):
for await (const chunk of stream) {
if (chunk.type === "text") process.stdout.write(chunk.delta);
else if (chunk.type === "tool_call") { /* incremental tool-call fragment */ }
else if (chunk.type === "finish") console.log(chunk.message); // assembled ChatResult
}
// Or stream AND get the final assembled object (tool-call args reassembled + parsed):
const final = await stream.final();OpenAI streams tool-call arguments as string fragments; Anthropic streams them as input_json_delta; Gemini sends whole functionCall parts. Radon reassembles all of them into complete, parsed toolCalls on the terminal chunk — one implementation, identical behavior.
The escape hatch (this matters more here than anywhere else)
AI providers ship new features weekly. The unified API handles the 80% case; for the other 20%, 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` / `@anthropic-ai/sdk`).
const openai = await ai.native<import("openai").OpenAI>("openai");
const assistants = await openai.beta.assistants.list(); // anything the SDK can do
// 2) A preconfigured fetch client (base URL + auth ready) — zero extra install.
const client = await ai.rawClient("anthropic");
const raw = await client.request("/messages", { method: "POST", json: { /* raw body */ } });
// 3) Per-call escape hatch: merge arbitrary provider fields into the request.
await ai.chat({
messages,
providerOptions: { logit_bias: { "50256": -100 } }, // provider-specific, merged in
});
// 4) The untouched provider response is always on `result.raw`.
const res = await ai.chat({ messages });
res.raw; // the exact JSON the provider returnednative() throws a clear NativeClientUnavailableError (with an npm install hint) if the official SDK isn't installed — rawClient() never needs one.
Embeddings (Pro)
const { embeddings } = await ai.embed({
provider: "openai",
input: ["hello world", "goodbye world"],
});
embeddings[0]; // number[]Not every provider has an embeddings API. Providers without one (anthropic, groq, deepseek, xai, openrouter) throw a typed UnsupportedOperationError — check provider.capabilities.embeddings. See DEFERRED.md.
Vision / multimodal input (Pro)
await ai.chat({
provider: "openai",
messages: [
{
role: "user",
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image", source: { url: "https://example.com/cat.png" } },
// or inline base64: { type: "image", source: { data: "<base64>", mimeType: "image/png" } }
],
},
],
});Vision is model-dependent as well as provider-dependent — pick a multimodal model. Gemini can't fetch arbitrary web URLs (use inline base64 or its File API). See DEFERRED.md.
Structured output / JSON mode (Pro)
// JSON object mode:
await ai.chat({ messages, responseFormat: "json" });
// JSON Schema (upgraded to strict schema enforcement where the provider supports it):
await ai.chat({
messages,
responseFormat: { type: "json_schema", schema: { type: "object", properties: { /* … */ } } },
});This is the least-uniform capability across providers — OpenAI and Gemini enforce a schema natively; Anthropic is best-effort via a system instruction; others offer JSON-object mode only. Radon normalizes the request and documents exactly what each guarantees in DEFERRED.md.
Fallback chains (Pro)
// Try OpenAI; if it fails, fall back to Anthropic, then Groq — same call.
await ai.chat({ messages, provider: "openai", fallback: ["anthropic", "groq"] });Bring your own provider
Implement the ModelProvider interface (or extend BaseProvider) and register it. For an OpenAI-compatible endpoint, extend OpenAICompatibleProvider and you get chat/streaming/tools/embeddings for free.
import { OpenAICompatibleProvider, registerProvider } from "@radonsdk/ai";
class MyLlmProvider extends OpenAICompatibleProvider {
readonly name = "my-llm";
readonly defaultModel = "my-model";
readonly capabilities = { chat: true, streaming: true, tools: true, embeddings: false, vision: false, jsonMode: true };
protected defaultBaseUrl() { return "https://llm.mycompany.internal/v1"; }
}
registerProvider("my-llm", async () => MyLlmProvider);Provider catalog
Import any adapter directly for an explicit dependency: import { OpenAIProvider } from "@radonsdk/ai/providers/openai".
| Slug | Provider | Tier | Wire format | Embeddings | Vision |
| --- | --- | --- | --- | --- | --- |
| openai | OpenAI | Free | OpenAI | ✅ | ✅ |
| anthropic | Anthropic (Claude) | Free | Anthropic Messages | — | ✅ |
| groq | Groq | Free | OpenAI-compatible | — | ✅ |
| google | Google Gemini | Pro | Gemini | ✅ | ✅ |
| mistral | Mistral AI | Pro | OpenAI-compatible | ✅ | ✅ |
| deepseek | DeepSeek | Pro | OpenAI-compatible | — | — |
| openrouter | OpenRouter | Pro | OpenAI-compatible | — | ✅ |
| xai | xAI (Grok) | Pro | OpenAI-compatible | — | ✅ |
| together | Together AI | Pro | OpenAI-compatible | ✅ | ✅ |
| ollama | Ollama (local) | Pro | OpenAI-compatible | ✅ | ✅ |
Vision/embeddings columns are provider-can-ever — the specific model still matters. Full honest caveats in DEFERRED.md.
Errors
Every failure is a typed subclass of AIError with a stable .code, so you branch on kind, not message text:
InvalidConfigError, ProviderNotFoundError, ProviderNotConfiguredError, LicenseRequiredError, LicenseInvalidError, UnsupportedOperationError, AuthenticationError, ProviderApiError (carries status, providerCode, raw), RateLimitError (carries retryAfterSec), ContextLengthExceededError, NetworkError, InvalidToolArgumentsError (carries the raw string the model produced), StreamError, and NativeClientUnavailableError.
Install size & lazy loading
The core (import { RadonAI } from "@radonsdk/ai") contains zero provider code — only the interface, registry, and the streaming/tool-normalization engines (~43 KB). Adapters are loaded on demand via dynamic import() the first time a provider is used, and each is emitted as its own subpath entry. A dev who configures only OpenAI + Anthropic never executes — or, under a code-splitting bundler, bundles — the other eight adapters. Official provider SDKs are optional peer dependencies, pulled in only when you call native().
API surface
RadonAI, createAI, the unified types (ChatMessage, Tool, ToolCall, ChatOptions, ChatResult, StreamChunk, EmbedOptions, …), ChatStream, BaseProvider / OpenAICompatibleProvider, registerProvider, FREE_PROVIDERS / isProProvider / PRO_FEATURES, LicenseClient, the HttpClient + parseSSE + tool-normalization helpers (for BYO adapters), and every typed error. All types are exported.
License
MIT © Radon SDK. The SDK is MIT-licensed; the Pro tier requires a commercial license key at runtime for Pro providers and features. "# radonsdk-ai"
