Claude and LLMs
bash
npm install @flattype/aits
import Anthropic from "@anthropic-ai/sdk";
import { countTokens, fromModelOutput, outputInstructions, toPrompt } from "@flattype/ai";
const client = new Anthropic();
// 1. Data in: Flat Notation inside <data> tags, with a one-line legend the model reads once.
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
betas: ["server-side-fallback-2026-07-01"],
fallbacks: "default", // if a safety classifier declines, retry on the recommended fallback model
messages: [{
role: "user",
content: `${toPrompt(orders, { name: "orders" })}\n\nTotal spent per customer?\n\n${outputInstructions([{ customer: "Alice", spent: 120 }])}`,
}],
});
// 2. Data out: tolerant of tags, fences, prose and JSON, then validated into typed models.
const reply = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
const totals = fromModelOutput(reply, { model: Total, list: true }); // Total[]
// 3. Measure it with Claude's own tokenizer (count_tokens is free).
const { json, flat, saved } = await countTokens(client, orders);Tool results shrink too, which matters most for agents that call tools over and over:
ts
import { claudeToolResult, toolResult } from "@flattype/ai";
// MCP server (@modelcontextprotocol/sdk): returns a CallToolResult with Flat Notation text
server.registerTool("list_orders", { description: "…" }, async () => toolResult(await db.orders()));
// Claude API tool loop: a tool_result block for the next user message
messages.push({ role: "user", content: [claudeToolResult(toolUse.id, await runTool(toolUse))] });| API | What it does |
|---|---|
toPrompt(data, { name, legend }) | <data format="flat-notation"> plus a one-line legend. The block is still valid Flat Notation; put the legend in your system prompt once with legend: false |
outputInstructions(example) | Asks the model to answer in Flat Notation and shows the exact shape you expect |
fromModelOutput(reply, { model, list, onWarning }) | Finds the data (last <data> block, code fence, or the text itself), skips prose lines, and accepts JSON too. Repairs go to onWarning, with their line. JSON that doesn't parse throws ModelOutputError, saying where it breaks: The data is JSON, but it is broken at line 1, column 22261: …"language" ⟶ "Galician"… |
countTokens(client, data, { model }) | { json, flat, saved } in real Claude tokens, minus per-message overhead. Takes any client with messages.countTokens |
toolResult(data, { structured }) / claudeToolResult(id, data) | MCP CallToolResult and Claude API tool_result blocks |
See examples/ai-claude for a token report, a full Claude round trip, and a runnable MCP server (claude mcp add flattype-orders -- node mcp-server.mjs).