samai-sdk
Core concepts

Structured output

generateObject() and streamObject() guarantee a typed, schema-validated object — with an automatic repair prompt when the model's output doesn't match, not just hopeful JSON.parse().

generateObject()

If the model's output fails validation, it's automatically retried with a repair prompt describing exactly what was wrong, up to maxRepairAttempts times (default 2). If it never succeeds, it throws a GenerateObjectError with .attempts and .lastError so you can log or fall back gracefully.

generate-object.ts
import { z } from "zod";
import { createClient, anthropic, generateObject } from "samai-sdk";

const client = createClient({ provider: anthropic({ apiKey: "..." }) });

const ReviewSchema = z.object({
  summary: z.string(),
  sentiment: z.enum(["positive", "negative", "mixed"]),
  score: z.number().min(1).max(5),
});

const result = await generateObject(client, {
  model: "claude-sonnet-4-6",
  schema: ReviewSchema,
  messages: [{ role: "user", content: "Extract structured data from this review: ..." }],
});

result.object.sentiment; // fully typed: "positive" | "negative" | "mixed"
result.attempts;         // how many tries it took (1 = first try succeeded)
result.usage;             // total tokens summed across all attempts, including failed ones

Works identically across every provider — validation happens on your side via the schema, not provider-specific JSON modes, so there's nothing extra to configure per provider.

Batch extraction with generateObjectBatch()

Runs generateObject() across many inputs with bounded concurrency — the shape of a data-extraction pipeline like "classify these 500 support tickets". One bad input never aborts the rest of the batch, and results come back in the same order as items regardless of completion order.

batch.ts
const tickets = ["My card was charged twice", "App crashes on launch", "Please add dark mode"];

const batch = await generateObjectBatch(client, {
  items: tickets,
  buildOptions: (ticketText) => ({
    model: "claude-sonnet-4-6",
    schema: TicketSchema,
    messages: [{ role: "user", content: `Classify this support ticket: "${ticketText}"` }],
  }),
  concurrency: 5, // max calls in flight at once, default 5
  onItemSettled: (item) => console.log(`Ticket ${item.index}: ${item.status}`),
});

console.log(`${batch.succeeded}/${batch.results.length} succeeded`);

for (const r of batch.results) {
  if (r.status === "fulfilled") console.log(r.item, "->", r.result.object);
  else console.warn(r.item, "failed:", r.error.message);
}

Pass throwOnAnyFailure: true to instead throw a GenerateObjectBatchError once every item has settled if any failed — it carries the full batchResult on .batchResult, so you don't lose completed work just because one item failed.

Using valibot instead of zod

schema accepts any Standard Schema V1 validator — zod behavior is completely unchanged, this is purely additive:

valibot.ts
import * as v from "valibot";
import { createClient, anthropic, generateObject } from "samai-sdk";

const ReviewSchema = v.object({
  summary: v.string(),
  score: v.pipe(v.number(), v.minValue(1), v.maxValue(10)),
});

const result = await generateObject(client, {
  model: "claude-sonnet-4-6",
  schema: ReviewSchema, // a valibot schema, not zod — works exactly the same way
  messages: [{ role: "user", content: "Extract structured data from this review: ..." }],
});

result.object.score; // fully typed via valibot's own type inference

Streaming version

streamObject() gives you the same guarantee with incremental partial-object updates as the model streams — useful for driving a UI that fills in fields as they arrive.