I now have precise, verified v6 API shapes. I have everything I need to write the reference. Let me compile the structured markdown research output.
Based on my cached Anthropic API skill knowledge (authoritative for Anthropic shapes) plus verified web research for the Vercel AI SDK v6, AI Gateway, and OpenAI parity, here is the practical LLM API integration reference.
Integrating LLMs via API — Practical Reference (2026)
Covers the Anthropic Messages API, OpenAI parity notes, the Vercel AI SDK v6 + AI Gateway as a provider-agnostic layer, and common SDK patterns. Code snippets in TypeScript/Python. See the VOLATILE CLAIMS section at the end for every version number and endpoint — those drift fast and are pinned to what I verified on 2026-07-23.
1. Anthropic Messages API
Everything goes through a single endpoint: POST /v1/messages. Tools, structured output, streaming, caching, and thinking are all features of that one endpoint — not separate APIs.
1.1 Auth
# Required headers on every request
x-api-key: $ANTHROPIC_API_KEY
anthropic-version: 2023-06-01
content-type: application/json
anthropic-beta: <feature-flag> # only when using a beta feature
SDKs read ANTHROPIC_API_KEY from the environment automatically. An unset key does not mean no credentials — the SDK/CLI resolution order is ANTHROPIC_API_KEY → ANTHROPIC_AUTH_TOKEN → an OAuth profile from ant auth login (stored under ~/.config/anthropic/) → Workload Identity Federation env vars. A bare Anthropic() works after ant auth login with no env var set. For raw HTTP with an OAuth token, use Authorization: Bearer <token> plus anthropic-beta: oauth-2025-04-20 — not x-api-key.
1.2 Message structure
The API is stateless — you resend the full conversation history each turn. Messages alternate user/assistant (first must be user; consecutive same-role messages are merged). Content is either a string or a list of typed content blocks (text, image, document, tool_use, tool_result, thinking).
Python:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
system="You are a helpful assistant.", # top-level, not a message
messages=[{"role": "user", "content": "What is the capital of France?"}],
)
# response.content is a list of blocks — check .type before reading .text
for block in response.content:
if block.type == "text":
print(block.text)
TypeScript:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
system: "You are a helpful assistant.",
messages: [{ role: "user", content: "What is the capital of France?" }],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}
Key response fields: content (block list), stop_reason (end_turn | max_tokens | tool_use | pause_turn | refusal), usage (token counts). Always branch on stop_reason before reading content[0] — a refusal can leave content empty.
Model note:
claude-opus-4-8is the current default. Thinking is controlled bythinking: {type: "adaptive"}on 4.6+ models — the oldbudget_tokensis rejected with a 400 on Opus 4.7/4.8, Sonnet 5, and Fable 5. Effort goes inoutput_config: {effort: "low"|"medium"|"high"|"xhigh"|"max"}.
1.3 Streaming
Use streaming for any long input/output or high max_tokens (the SDK raises a ValueError on non-streaming requests it estimates will exceed ~10 min). The .stream() helper accumulates state and exposes get_final_message().
Python:
with client.messages.stream(
model="claude-opus-4-8", max_tokens=64000,
messages=[{"role": "user", "content": "Write a story"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # complete Message object
TypeScript:
const stream = client.messages.stream({
model: "claude-opus-4-8", max_tokens: 64000,
messages: [{ role: "user", content: "Write a story" }],
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta")
process.stdout.write(event.delta.text);
}
const final = await stream.finalMessage();
Raw SSE event types: message_start → content_block_start → content_block_delta → content_block_stop → message_delta (carries stop_reason, usage) → message_stop.
1.4 Tool use
Define tools with a JSON Schema; loop until stop_reason != "tool_use". The SDK tool runner (beta) drives the loop for you.
Tool runner (Python, recommended):
from anthropic import beta_tool
@beta_tool
def get_weather(location: str) -> str:
"""Get current weather. Args: location: City and state."""
return f"72°F and sunny in {location}"
runner = client.beta.messages.tool_runner(
model="claude-opus-4-8", max_tokens=16000,
tools=[get_weather],
messages=[{"role": "user", "content": "Weather in Paris?"}],
)
for message in runner: # loop stops automatically when Claude is done
print(message)
Manual loop shape (any language): send request → if stop_reason == "tool_use", execute each tool_use block, append the assistant's full response.content then a user message with one tool_result block per tool_use (matching tool_use_id), and repeat. Return all parallel tool results in a single user message. On failure, return tool_result with is_error: true — don't drop it.
Server-side tools (run on Anthropic's infra, no client execution): declare in tools and read the result blocks in the same response — web_search_20260209, web_fetch_20260209 (dynamic filtering, Opus 4.6+/Sonnet 4.6+), code_execution_20260521.
1.5 Structured output
Two mechanisms, both on the single endpoint: output_config.format (constrain the response to a JSON schema) and strict: true (validate tool params). Prefer client.messages.parse() with a Pydantic/Zod model.
from pydantic import BaseModel
class Contact(BaseModel):
name: str
email: str
response = client.messages.parse(
model="claude-opus-4-8", max_tokens=16000,
messages=[{"role": "user", "content": "Extract: Jane (jane@co.com)"}],
output_format=Contact,
)
contact = response.parsed_output # validated Contact instance
Note: output_config.format is the canonical parameter; the old top-level output_format on messages.create() is deprecated. Incompatible with citations and message prefill. Prefilling the assistant turn is rejected with a 400 on Opus 4.6+/Sonnet 4.6+/Fable 5 — use structured output instead.
1.6 Prompt caching
Caching is a prefix match — render order is tools → system → messages, and any byte change in the prefix invalidates everything after it. Put stable content first, volatile content (timestamps, per-request IDs) after the last breakpoint.
response = client.messages.create(
model="claude-opus-4-8", max_tokens=16000,
system=[{
"type": "text", "text": large_document,
"cache_control": {"type": "ephemeral"}, # or {"type": "ephemeral", "ttl": "1h"}
}],
messages=[{"role": "user", "content": "Summarize"}],
)
Top-level cache_control={"type": "ephemeral"} auto-caches the last cacheable block. Max 4 breakpoints; minimum cacheable prefix is model-dependent (4096 tokens on Opus 4.8, 1024 on Sonnet 4.5). Verify with usage.cache_read_input_tokens — if it's zero across repeated identical-prefix requests, a silent invalidator (datetime.now() in the system prompt, unsorted json.dumps, varying tool set) is at work. Cache reads cost ~0.1×; writes cost 1.25× (5-min TTL) or 2× (1-hour TTL).
1.7 Token counting
Use POST /v1/messages/count_tokens — never tiktoken (it's OpenAI's tokenizer and undercounts Claude by ~15–20%). Counts are model-specific.
n = client.messages.count_tokens(
model="claude-opus-4-8",
messages=[{"role": "user", "content": open("doc.md").read()}],
).input_tokens
The endpoint is stateless — to diff a file across versions, count each and subtract.
2. OpenAI API — parity notes
OpenAI now has two primary interfaces, and this matters for integration decisions:
- Chat Completions (
/v1/chat/completions) — the long-standing, message-based API. Still supported, and it's the shape every third-party "OpenAI-compatible" endpoint implements. - Responses API (
/v1/responses) — the newer, item/response-based API. As of 2026 OpenAI positions it as the default/recommended for all new projects, especially agentic and reasoning-model (GPT-5.x) workloads: better cache utilization and reasoning-state persistence across turns. The Assistants API is being sunset in favor of it.
Mapping to Anthropic concepts:
| Concept | Anthropic | OpenAI Chat Completions | OpenAI Responses |
|---|---|---|---|
| Endpoint | POST /v1/messages |
POST /v1/chat/completions |
POST /v1/responses |
| System prompt | top-level system |
{role: "system"} (or developer) message |
instructions field |
| Conversation state | stateless, resend history | stateless, resend messages |
can persist server-side via previous_response_id |
| Auth header | x-api-key |
Authorization: Bearer |
Authorization: Bearer |
| Tool result role | user msg w/ tool_result block |
{role: "tool"} message |
function-call output item |
| Streaming | SSE content-block deltas | SSE chat.completion.chunk |
SSE typed events |
| Structured output | output_config.format / strict |
response_format: {type: "json_schema"} |
text.format json_schema |
| Token field | usage.input_tokens |
usage.prompt_tokens |
usage.input_tokens |
Practical takeaways for integration:
- If you target OpenAI-compatible gateways (many open-model hosts, older tooling), you're speaking Chat Completions. Message roles and usage.prompt_tokens/completion_tokens differ from Anthropic's input_tokens/output_tokens.
- Anthropic's SDK is not OpenAI-compatible by default — don't reach for openai-shaped shims against Anthropic; use @anthropic-ai/sdk / anthropic.
- The cleanest way to stay provider-agnostic across both is a normalizing layer (§3): the Vercel AI SDK abstracts Chat-Completions vs Responses vs Messages behind one interface.
3. Vercel AI SDK v6 + AI Gateway (provider-agnostic layer)
The AI SDK is a TypeScript toolkit that normalizes text generation, structured output, tool calling, embeddings, and streaming across providers behind one interface. AI Gateway is a routing layer: one API key, plain creator/model strings, provider failover and cost/latency routing.
Version reality (verified 2026-07-23):
ai@lateston npm is now v7 (7.0.35). v6 is still actively maintained — theai-v6dist-tag resolves to 6.0.234, and provider packages (@ai-sdk/openai,@ai-sdk/anthropic,@ai-sdk/gateway,@ai-sdk/react) are on the 4.x line. Since this reference targets v6, pin explicitly:npm i ai@6(orai@ai-v6). Everything below is v6 shape; note where it changed from v5.
3.1 Text generation
import { generateText, streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
// One-shot
const { text } = await generateText({
model: anthropic("claude-opus-4-8"),
prompt: "Explain quantum computing in one paragraph.",
});
// Streaming
const result = streamText({
model: anthropic("claude-opus-4-8"),
prompt: "Write a story",
});
for await (const chunk of result.textStream) process.stdout.write(chunk);
3.2 AI Gateway (provider-agnostic routing)
The gateway provider is importable from the main ai package — pass a plain creator/model-name string and it routes automatically:
import { generateText, gateway } from "ai";
// Plain string routes through the Gateway automatically
const { text } = await generateText({
model: "anthropic/claude-opus-4.8", // creator/model-name format
prompt: "Hello world",
});
// Explicit, with routing control
const { text: t2 } = await generateText({
model: gateway("openai/gpt-5.6-terra"),
prompt: "Hello",
providerOptions: {
gateway: {
order: ["vertex", "anthropic"], // try in this order
only: ["vertex", "anthropic"], // restrict to these providers
sort: "cost", // rank by 'cost' | 'ttft' | 'tps'
},
},
});
Auth: set AI_GATEWAY_API_KEY in the environment (or createGateway({ apiKey }) for a custom instance). Model strings are anthropic/claude-sonnet-4.6, openai/gpt-5.4, xai/grok-4.5, etc. — note the Gateway uses dotted names (claude-opus-4.8), distinct from Anthropic's first-party dashed IDs (claude-opus-4-8).
3.3 Structured output — changed in v6
generateObject/streamObject are deprecated in v6. Use generateText/streamText with an output setting via Output:
import { generateText, Output } from "ai";
import { z } from "zod";
const { output } = await generateText({
model: "anthropic/claude-opus-4.8",
output: Output.object({
schema: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
}),
}),
prompt: "Generate a lasagna recipe.",
});
// streaming: use streamText + partialOutputStream (was partialObjectStream in v5)
3.4 Tool calling — changed in v6
Tool names now derive from object keys (drop the name property); the schema field is inputSchema (was parameters in older versions):
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";
const { text } = await generateText({
model: "anthropic/claude-opus-4.8",
tools: {
getWeather: tool({
description: "Get current weather",
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => `72°F in ${location}`,
}),
},
stopWhen: stepCountIs(5), // multi-step tool loop
prompt: "Weather in Paris?",
});
3.5 Agents — new/renamed in v6
The agent abstraction is ToolLoopAgent (was Experimental_Agent in v5). Note system → instructions, and the default stopWhen is now isStepCount(20):
import { ToolLoopAgent } from "ai";
const agent = new ToolLoopAgent({
model: "anthropic/claude-opus-4.8",
instructions: "You are a helpful assistant.",
tools: { /* ... */ },
});
3.6 Embeddings — renamed in v6
Provider methods changed from textEmbeddingModel/textEmbedding to embeddingModel/embedding; generics were dropped from EmbeddingModel, embed, embedMany:
import { embed, embedMany } from "ai";
import { openai } from "@ai-sdk/openai";
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: "sunny day at the beach",
});
const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: ["doc one", "doc two", "doc three"], // auto-chunks large batches
});
3.7 Other v6 migration notes (if coming from v5)
CoreMessage→ModelMessage;convertToCoreMessages()→ asyncconvertToModelMessages().- UI helpers renamed:
isToolUIPart→isStaticToolUIPart,getToolName→getStaticToolName, etc. - Run
npx @ai-sdk/codemod v6to migrate v5 code automatically.
VOLATILE CLAIMS
Everything here drifts. All values verified against live registries/docs on 2026-07-23; re-check before relying on them.
SDK / package versions
| Package | Version (2026-07-23) | Source |
|---|---|---|
ai (latest dist-tag) |
7.0.35 — v7 is now the default latest |
npm registry |
ai (ai-v6 dist-tag) |
6.0.234 — v6 still actively maintained; install with ai@6 |
npm registry |
ai (ai-v5 dist-tag) |
5.0.219 | npm registry |
@ai-sdk/anthropic |
4.0.18 | npm registry |
@ai-sdk/openai |
4.0.18 | npm registry |
@ai-sdk/gateway |
4.0.27 | npm registry |
@ai-sdk/react |
4.0.38 | npm registry |
openai (Node) |
6.48.0 | npm registry |
openai (Python) |
2.47.0 | PyPI |
anthropic (Python) |
0.118.0 | PyPI |
⚠️ v6 vs v7 caveat: The task specified v6, but
ai@latesthas advanced to v7. The v6 API shapes documented above (Output.object,ToolLoopAgent,embeddingModel,inputSchema) are current for v6; v7 may differ. If you're starting fresh, decide deliberately between pinningai@6and adoptingai@7. I did not audit v7's surface here.
Endpoints
| Purpose | Endpoint | Notes |
|---|---|---|
| Anthropic messages | POST https://api.anthropic.com/v1/messages |
+ streaming via stream: true / .stream() |
| Anthropic token count | POST /v1/messages/count_tokens |
model-specific |
| Anthropic version header | anthropic-version: 2023-06-01 |
current stable |
| OpenAI (legacy) | POST /v1/chat/completions |
message-based; the "OpenAI-compatible" shape |
| OpenAI (recommended new) | POST /v1/responses |
item/response-based; default for new projects in 2026 |
| Vercel AI Gateway key | env AI_GATEWAY_API_KEY |
plain creator/model strings |
Model IDs (change frequently — verify before use)
| Provider | Model | ID |
|---|---|---|
| Anthropic (first-party, dashed) | Opus 4.8 (current default) | claude-opus-4-8 |
| Anthropic (first-party) | Sonnet 5 | claude-sonnet-5 |
| Anthropic (first-party) | Haiku 4.5 | claude-haiku-4-5 |
| Anthropic (first-party) | Fable 5 (most capable) | claude-fable-5 |
| Anthropic via AI Gateway (dotted) | Opus 4.8 | anthropic/claude-opus-4.8 |
| OpenAI | GPT-5.6 tiers (released 2026-07-09) | gpt-5.6-sol ($5/$30), gpt-5.6-terra ($2.50/$15), gpt-5.6-luna (~$1 in) |
| OpenAI (older, still live) | GPT-5.4 / GPT-5.1 | gpt-5.4 ($2.50/$15), gpt-5.1 ($1.25/$10) |
Beta feature flags / tool versions (Anthropic)
| Feature | Flag / type string |
|---|---|
| Web search (dynamic filtering) | web_search_20260209 |
| Web fetch | web_fetch_20260209 |
| Code execution | code_execution_20260521 |
| Files API | files-api-2025-04-14 |
| Fast mode (Opus 4.8/4.7) | fast-mode-2026-02-01 |
| Task budgets | task-budgets-2026-03-13 |
API-shape drift to watch
- AI SDK v6:
generateObject/streamObjectdeprecated → useOutput.objectongenerateText/streamText;partialObjectStream→partialOutputStream;Experimental_Agent→ToolLoopAgent(system→instructions, defaultstopWhen: isStepCount(20)); embeddingstextEmbeddingModel→embeddingModel; tools dropname, useinputSchema;CoreMessage→ModelMessage. - Anthropic:
budget_tokensrejected (400) on Opus 4.7/4.8, Sonnet 5, Fable 5 → usethinking: {type: "adaptive"}+output_config.effort; assistant prefill rejected (400) on 4.6+/Fable 5;output_format→output_config.format. - OpenAI: Responses API (
/v1/responses) is the 2026 default recommendation over Chat Completions; Assistants API sunsetting.
Sources: ai — npm · AI SDK 6 — Vercel · AI SDK 6 migration guide · AI Gateway Provider · AI Gateway routing rules · OpenAI: Migrate to the Responses API · OpenAI Chat Completions reference · OpenAI Models · OpenAI API Pricing 2026 (DevTk) · Anthropic Messages API shapes from the bundled claude-api skill (SKILL v2.1.216, models cached 2026-06-24); npm/PyPI registry versions queried live 2026-07-23.