Reliability & tracing
Retries, fallback chains, timeouts, concurrency limits, and full run tracing — all implemented as composable provider wrappers, not special-cased flags.
Retries and fallback chains
withRetry() and withFallback([...]) wrap any provider — since they implement the same interface, they compose with guardrails, generateObject(), and streaming without special-casing. createResilientProvider() combines both:
import { createClient, anthropic, openai, createResilientProvider } from "samai-sdk";
// Both retries and fallback: each provider gets its own retries before falling through
const resilient = createResilientProvider(
[anthropic({ apiKey: "..." }), openai({ apiKey: "..." })],
{
retry: { maxRetries: 2, initialDelayMs: 500 },
fallback: {
onFallback: ({ failedProvider, nextProvider }) =>
console.warn(`${failedProvider} failed, falling back to ${nextProvider}`),
},
}
);
const client = createClient({ provider: resilient });
const result = await client.generate({ model: "claude-sonnet-4-6", messages: [/* ... */] });Streaming note
Timeouts
withTimeout() enforces a real deadline using AbortController — not pattern-matching on error messages after the fact. createResilientProvider() applies a 30s default automatically.
import { withTimeout, withRetry, anthropic } from "samai-sdk";
// Put timeout innermost so every retry attempt gets its own fresh window
const provider = withRetry(
withTimeout(anthropic({ apiKey: "..." }), { timeoutMs: 15_000 }),
{ maxRetries: 2 }
);Tool execution gets its own independent timeout too — every execute() call is raced against a deadline (default 30s, overridable per-tool or per-run). A hung tool comes back as an isError result instead of hanging the whole run.
Concurrency and rate limiting
import { withConcurrencyLimit, withRateLimit, anthropic } from "samai-sdk";
// Caps in-flight calls — a QUEUE, not a rejection
const capped = withConcurrencyLimit(anthropic({ apiKey: "..." }), { maxConcurrent: 5 });
// Caps requests per time window — token-bucket, refills continuously
const throttled = withRateLimit(anthropic({ apiKey: "..." }), { maxRequests: 60, intervalMs: 60_000 });Tracing & observability
Every run already produces a RunTrace — every model call, tool call, retry, and handoff recorded with real timing. Turn that into something you can actually look at:
import { writeFileSync } from "node:fs";
import { runAgent, renderTraceHTML, exportRunTraceToOtel } from "samai-sdk";
const result = await runAgent(client, agent, "hi");
// Render as a self-contained, offline-viewable HTML timeline
writeFileSync("trace.json", JSON.stringify(result.trace));
writeFileSync("trace.html", renderTraceHTML(result.trace));
// Or export as real OpenTelemetry spans on your existing tracer
await exportRunTraceToOtel(result.trace); // needs the optional @opentelemetry/api peer dependencyOr skip the intermediate file and serve the same rendered page straight from the CLI:
npx samai-sdk trace ./trace.json --port 4949
# ✅ Trace viewer running at http://localhost:4949