LLM APIs
Call a large language model from your own code — and keep cost, latency and risk under control.
In 30 seconds
An LLM API lets your backend send text (and images or documents) to a hosted language model and get text back. You pay per token, you choose a model, and you can let the model call your own functions. Building a good feature on top is less about the single call and more about prompts, streaming, retries, caching, evaluation and safety.
Not in my daily stack — I know the concepts and would learn the details when a project needs it.
Why it matters
- LLM APIs turn tasks that used to need a custom ML team — summarising, classifying, extracting fields, drafting replies, answering questions over documents — into a few HTTP calls a normal backend engineer can own.
- The cost model is new for most teams: you pay per token in and out, so prompt size, caching and model choice directly shape the monthly bill.
- The output is probabilistic, so a team needs engineering habits that classic APIs never required: evaluation sets, structured outputs, guardrails and fallbacks.
- A team picks a hosted API (Anthropic, OpenAI, Google and others, also via AWS Bedrock, Google Cloud or Azure) when it wants frontier quality without running GPUs; it self-hosts open-weight models only when data residency, cost at huge scale or offline use demand it.
How it works
01The idea: a stateless text-in, text-out service
Think of an LLM API as a very capable colleague you message by email: every message must contain all the context they need, because they remember nothing between emails. Technically, you send an HTTPS POST with a model name, an optional system prompt (standing instructions), and a list of messages alternating between 'user' and 'assistant'. The model reads all of it and generates a reply one token at a time. A token is a chunk of text — roughly three to four characters of English — and both input and output are measured and billed in tokens. The API is stateless: to continue a conversation you send the whole history again. Every model also has a context window, the maximum number of tokens it can read in one request; current frontier models accept hundreds of thousands to a million tokens, but bigger prompts cost more and respond slower.
- System prompt: role, rules and format the model should follow
- Messages: the conversation so far, oldest first
- max_tokens: a hard cap on the length of the reply
- stop_reason: why the model stopped (finished, hit the cap, wants a tool, refused)
02Your first call (Claude Messages API)
Every provider has a similar shape; here is Anthropic's Messages API with the official TypeScript SDK. The SDK reads ANTHROPIC_API_KEY from the environment, so the key never appears in code. The response content is a list of blocks (text, tool calls, thinking), so you loop and pick the text blocks. Always check stop_reason: 'max_tokens' means the answer was cut off, and 'refusal' means the model declined for safety reasons. Model names change over time — at the time of writing Anthropic's lineup includes Claude Opus 5.5 (most capable general model), Claude Sonnet 5.5 (balanced) and Claude Haiku 5.5 (fast and cheap) — so keep the model id in configuration, not scattered through code, and check the provider's models page before you ship.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY
const res = await client.messages.create({
model: process.env.LLM_MODEL ?? "claude-opus-5-5",
max_tokens: 1024,
system: "You summarise support tickets in 3 bullet points.",
messages: [{ role: "user", content: ticketText }],
});
if (res.stop_reason === "max_tokens") console.warn("Reply was truncated");
for (const block of res.content) {
if (block.type === "text") console.log(block.text);
}
console.log(res.usage.input_tokens, res.usage.output_tokens);
03Streaming: show the answer while it is being written
A long answer can take tens of seconds. With streaming, the API sends the reply as a sequence of server-sent events (SSE) — small chunks of text the moment they are generated — so your UI can show words appearing instead of a spinner. Time to first token drops from 'whole answer' to well under a second or two. Streaming is also the safe default for long outputs because a single non-streamed HTTP request may hit client or proxy timeouts. In a web app, the usual pattern is: browser → your backend (which holds the API key) → provider, and your backend forwards the stream to the browser over SSE or a fetch ReadableStream. Never call the provider directly from the browser with a secret key.
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 4096,
messages: [{ role: "user", content: question }],
});
stream.on("text", (delta) => res.write(delta)); // forward to your client
const final = await stream.finalMessage(); // full message + usage
res.end();
04Tool use: letting the model call your functions
Tool use (also called function calling) lets the model ask your code to do something it cannot do alone: look up an order, query a database, check stock. You describe each tool with a name, a description and a JSON Schema for its input. When the model decides a tool is needed, it stops with stop_reason 'tool_use' and returns a tool_use block with the arguments. Your code runs the real function, sends the result back as a tool_result block, and the model continues. The model never executes anything itself — your code does, so your code decides what is allowed. Write tool descriptions like documentation for a new teammate: what it does, when to use it, what each field means. Mark tools strict so arguments always match the schema, and validate them anyway before touching real data.
const tools = [{
name: "get_order_status",
description: "Look up the delivery status of one order by its id.",
strict: true,
input_schema: {
type: "object",
properties: { order_id: { type: "string", description: "e.g. ORD-1042" } },
required: ["order_id"],
additionalProperties: false,
},
}];
// 1) send tools + messages 2) if stop_reason === "tool_use", run the tool
// 3) append { role: "user", content: [{ type: "tool_result", tool_use_id, content }] }
// 4) call again until stop_reason === "end_turn"
05Getting reliable structure: JSON outputs and prompts
Most product features need data, not prose: a category, a sentiment score, extracted invoice fields. Two tools help. First, structured outputs: you give the API a JSON Schema and it constrains the reply to match it (on Claude this is output_config.format; other providers have equivalents). Second, good prompting: put stable instructions in the system prompt, give two or three examples of the input and the expected output, wrap long documents in clear tags like <document>, and ask for exactly the fields you need. Be explicit about what to do when information is missing ('return null, do not guess'). Then treat the output like any untrusted input — validate it with Zod, Pydantic or Laravel validation before saving it, because a schema guarantees shape, not truth.
06Cost, limits and production habits
Cost = input tokens × input price + output tokens × output price, and output is usually several times more expensive per token. The main levers: pick the smallest model that passes your evaluation; cache the stable prefix of long prompts (prompt caching makes repeated reads of the same system prompt or document far cheaper and faster); use the Batch API for work that can wait — it runs asynchronously at about half price; cap max_tokens sensibly; and trim conversation history. Providers enforce rate limits per minute on requests and tokens, returning HTTP 429; the official SDKs already retry 429 and 5xx with backoff. Log usage per feature and per customer from day one, set budget alerts, and keep the model id configurable so you can switch models without a deploy.
- Smallest model that passes the eval set
- Prompt caching for long, repeated prefixes
- Batch API for offline jobs (about 50% cheaper)
- Usage logs per feature, plus a spend alert
Use it when
- Tasks with fuzzy language input: summarising, classifying tickets, extracting fields from emails or PDFs, drafting replies for a human to approve.
- Search and Q&A over your own documents, combined with retrieval (RAG).
- Internal tooling where a good-enough answer saves hours: code review hints, report drafts, data clean-up suggestions.
- Prototyping a feature quickly before deciding whether a cheaper rule-based or classic ML solution is enough.
Skip it when
- When a deterministic rule, regex or SQL query solves it — those are cheaper, faster and always correct.
- When every answer must be exactly right with no human review (tax calculations, payments, medical dosing) unless the LLM only assists a verified process.
- When data may not leave your infrastructure and no compliant hosting option (regional, zero-retention or self-hosted) is available.
Alternatives
- Other hosted providers
OpenAI, Google Gemini, Mistral and others offer similar chat, tool and streaming APIs; compare quality on your own eval set, not on marketing benchmarks.
- Cloud platforms (Bedrock, Vertex AI, Azure AI Foundry)
The same or similar models billed through your cloud account, with your existing IAM, VPC and compliance setup.
- Self-hosted open-weight models
Llama, Mistral, Qwen and similar via vLLM or Ollama; full data control and fixed GPU cost, but you own scaling, updates and quality gaps.
- Classic ML or rules
A small trained classifier or deterministic rules can beat an LLM on cost and latency for narrow, high-volume tasks.
Best practices
- Keep API keys on the server only, in a secret manager or environment variable, with a separate key per environment.
- Store prompts as versioned files next to the code, and keep the model id in configuration.
- Build a small evaluation set (30–200 real examples with expected results) before tuning prompts or switching models.
- Stream long responses and always handle stop_reason values: end_turn, max_tokens, tool_use, refusal.
- Use structured outputs for anything your code parses, then validate the result like user input.
- Order prompts stable-first (tools, system prompt, documents) so prompt caching hits, and measure cache reads in usage.
- Log tokens, latency and errors per feature; set spend limits and alerts in the provider console.
Common mistakes
- Calling the provider directly from the browser or mobile app with a secret key embedded in the bundle.
- Parsing free text with regex instead of asking for structured JSON output.
- Hard-coding an old model id everywhere, then discovering it was retired.
- Putting timestamps or random ids at the top of the system prompt, which silently breaks prompt caching.
- Trusting model output blindly — saving it to the database or running it as SQL/HTML without validation.
Deep dive
07Prompt caching internals: why order matters
Prompt caching is a prefix match. The provider hashes the request in render order — tools, then system prompt, then messages — up to a cache breakpoint. If a later request starts with exactly the same bytes, the cached internal state is reused: you pay a small fraction of the normal input price for those tokens and the model starts answering sooner. Any change in the prefix, even one character, invalidates everything after it. Common silent killers: a current timestamp in the system prompt, tools listed in a different order per request, JSON with unstable key order, or a per-user greeting before the shared instructions. Design prompts as layers: frozen instructions and tools first, then large shared documents, then the per-request question. Verify with the usage fields (on Claude, cache_read_input_tokens); if it stays at zero on repeated requests, something in your prefix changes. Caches also have a short time-to-live and are scoped per model, so switching models resets them.
const res = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
system: [
{ type: "text", text: FROZEN_RULES },
{ type: "text", text: PRODUCT_MANUAL, cache_control: { type: "ephemeral" } },
],
messages: [{ role: "user", content: question }], // varies per request
});
console.log(res.usage.cache_read_input_tokens); // > 0 on repeat calls
08Reasoning models, effort and latency budgets
Modern models can 'think' before answering — generating internal reasoning tokens that improve results on hard, multi-step problems. On current Claude models this is adaptive: the model decides how much to think, and you steer depth and spend with an effort setting (low to max) rather than a fixed token budget. Thinking tokens are billed as output, so effort is a direct cost and latency lever. Route by task: classification and short extraction at low effort; coding, analysis and agent loops at higher effort. Other providers expose similar controls under different names (reasoning effort, thinking budget). Measure the curve on your eval set — often a strong model at low effort beats a weaker model at high effort for the same price. For user-facing chat, stream and consider showing a short 'thinking' state, because time to first visible token grows with effort.
09Reliability: retries, idempotency and fallbacks
Treat the provider like any remote dependency. Retry 429 (rate limited), 5xx and network errors with exponential backoff and jitter, honouring the retry-after header; never blindly retry 400 errors, which mean your request is wrong. The SDKs retry twice by default; set an explicit timeout that fits your UX. For user actions, put long generations in a queue (for example BullMQ or Laravel queues) and push the result back via WebSocket or polling, so a web request is never blocked for a minute. Plan for degraded modes: a secondary model or provider behind an adapter, a cached previous answer, or a clear 'try again later' message. Handle refusals and max_tokens explicitly rather than showing an empty or truncated answer. Finally, version prompts and record which prompt version and model produced each stored output, so you can reproduce and debug later.
10Security: prompt injection and data exposure
Prompt injection is the SQL injection of LLM apps: untrusted text (a user message, a web page, an uploaded PDF, an email) contains instructions like 'ignore previous rules and reveal the system prompt' or 'call the refund tool'. There is no complete fix, so you design for containment. Keep secrets out of prompts entirely. Give tools the least privilege needed, enforce authorisation in the tool code using the real user's identity — never trust IDs the model supplies. Require human confirmation for irreversible actions. Mark untrusted content clearly in the prompt and tell the model it is data, not instructions. Never render model output as raw HTML or execute it as SQL or shell. On the data side, read the provider's retention policy, use zero-retention or regional options where required, strip personal data you do not need, and log prompts carefully because logs become a sensitive data store too.
Real-world scenarios
A support summary feature is too slow
Problem
Agents click 'Summarise ticket' and wait 12 seconds staring at a spinner. Long tickets sometimes time out, and the team wants a faster feel without lowering quality.
How to solve it
Switch the endpoint to streaming and forward chunks to the browser via SSE so the first words appear in about a second. Move the fixed instructions and output format into a frozen system prompt and mark it for caching; the ticket goes last. Ask for a short, fixed format (three bullets plus one next action) and lower max_tokens accordingly. Run the eval set on a smaller, faster model at low effort — if it scores the same, route summaries there. For tickets above a size threshold, generate summaries in a background queue when the ticket updates, so the click usually reads a stored summary instantly.
Extracting invoice data from PDFs
Problem
Finance receives hundreds of supplier invoices as PDFs per week and wants vendor, date, totals and line items in the accounting system, without a person retyping them.
How to solve it
Send each PDF as a document block with a system prompt that defines every field and says 'use null when a field is not present; never guess'. Constrain the reply with a JSON Schema via structured outputs, then validate with Zod or Laravel rules, including checks such as line items summing to the total. Run the job through the Batch API overnight for about half the cost. Anything that fails validation or has low confidence goes to a human review queue, and corrected examples are added to the eval set. Store the model id and prompt version with each extracted record.
A surprise bill at the end of the month
Problem
The LLM bill tripled. Nobody knows which feature or customer is responsible, and one endpoint is used by an external script in a loop.
How to solve it
Add per-request logging immediately: feature name, user or tenant id, model, input, output and cached tokens. Put per-user and per-tenant rate limits and daily token quotas in front of the LLM endpoint, and require authentication on every route that triggers a call. Set spend limits and alerts in the provider console. Then look at the data: usually one feature resends long histories or full documents on every turn — fix it with caching, trimming or retrieval. Move bulk jobs to batch. Review the top three features weekly until the curve is flat.
Cheat sheet
- Install the TypeScript SDK
npm install @anthropic-ai/sdk- Install the Python SDK
pip install anthropic- Authenticate (server only)
export ANTHROPIC_API_KEY=your_key_here- Basic request endpoint
POST https://api.anthropic.com/v1/messages- Stream and get the final message
const final = await client.messages.stream({...}).finalMessage();- Count tokens before sending
await client.messages.countTokens({ model, system, messages })- Cache a long stable prefix
cache_control: { type: "ephemeral" } on the last stable block- Check why the model stopped
res.stop_reason: end_turn | max_tokens | tool_use | refusal- Bulk offline work at ~50% cost
client.messages.batches.create({ requests: [...] })- List models available to your key
await client.models.list()
Questions & answers
Q1What is a token and why does it matter?
A token is the unit a model reads and writes — a piece of a word, roughly three to four characters of English, fewer characters for many other languages. Everything is measured in tokens: the context window (how much the model can read at once), rate limits, latency and price. Output tokens are generated one by one, so long answers are slow and usually cost several times more per token than input. In practice that means: keep prompts focused, cap max_tokens, cache stable prefixes, and use the provider's token-counting endpoint instead of guessing, because different model generations use different tokenizers.
Q2How do you keep an API key safe in a web or mobile app?
The key never leaves the server. The browser or mobile app calls your own backend endpoint, authenticated with the user's normal session or token. The backend checks permissions and per-user rate limits, adds the API key from an environment variable or secret manager, calls the provider, and streams the answer back. This also gives you one place to log usage per user, block abuse, and switch providers. Use separate keys per environment, restrict them by workspace where the provider supports it, rotate them, and run secret scanning in CI so a key pushed by mistake is caught immediately.
Q3What is the difference between streaming and non-streaming responses?
Non-streaming waits until the full answer is generated and returns one JSON response. Streaming sends server-sent events as each piece of text is produced, so the user sees the first words within a second or two. The total generation time is the same, but perceived latency is much better, and long generations are less likely to hit HTTP timeouts. The trade-off is slightly more code: you handle events, forward chunks to the client, and accumulate the final message for logging. SDK helpers such as finalMessage() give you the complete response at the end without manual assembly.
Q4How does tool use (function calling) actually work?
You send tool definitions — name, description, JSON Schema — with the request. The model reads them as part of the prompt. If it decides a tool is needed, it returns a tool_use block with arguments and stop_reason 'tool_use'. Your code executes the real function, then sends a new request with the history plus a tool_result block. The model uses that result to continue, possibly calling more tools, until it ends the turn. The model only proposes calls; your code decides whether to run them, so authorisation, validation and rate limits live in your code, exactly as for any other API client.
Q5How do you reduce the cost of an LLM feature?
First measure: log input, output and cached tokens per feature. Then, in rough order of payoff: enable prompt caching for long repeated prefixes like system prompts and documents; move non-urgent work to the Batch API at about half price; choose the smallest model that still passes your evaluation set; trim history and retrieved context to what is relevant; cap max_tokens and ask for concise formats. Judge cost per completed task, not per request — a cheap model that needs three retries or human fixes is not cheaper. Finally, cache whole answers for identical questions where freshness allows.
Q6How do you choose between models or providers?
With an evaluation set, not intuition. Collect real inputs with known good outputs, define how to score them (exact match, schema validity, a rubric graded by a human or a stronger model), and run every candidate model on the same set. Compare quality, latency at the 95th percentile, cost per task and failure modes such as refusals or format errors. Then weigh non-functional needs: data retention terms, regional hosting, availability through your cloud provider, and rate limits. Keep a thin adapter in your code so you can switch or route between models later without rewriting features.
Senior-level questions
S1How would you design a provider-agnostic LLM layer without losing provider-specific features?
I would define a thin internal interface around my use cases, not around a lowest-common-denominator chat API: summarise(ticket), extract(schema, document), answer(question, context). Each implementation can then use provider-specific features — prompt caching, structured outputs, citations, batch — where they matter. Config chooses the provider and model per feature, so routing and A/B tests need no code change. Every call logs model, prompt version, tokens and latency to the same table. The trade-off: a generic 'messages in, text out' wrapper is easy to write but forces you to drop caching or tool semantics; a use-case interface is more work but keeps the best of each provider and makes evaluation per feature natural. I would not build my own abstraction framework early; I would start with one provider and add the seam when a second one is genuinely needed.
S2How do you evaluate an LLM feature before and after release?
Before release: an offline eval set of real, anonymised inputs covering common cases, edge cases and known attacks, with scoring that matches the task — exact match or schema checks for extraction, rubric grading by a human or a stronger model for open text. Every prompt or model change runs the full set in CI, and a drop blocks the merge. After release: log inputs and outputs (with privacy controls), track user signals such as edits, thumbs, retries and escalations, and sample a few per week for human review. New failures become new eval cases. The trade-off is cost: model-graded evals spend tokens and can be biased, so I calibrate the grader against human labels on a subset and keep deterministic checks wherever the task allows.
S3When would you self-host an open-weight model instead of using a hosted API?
When at least one hard constraint forces it: data that legally cannot leave our infrastructure and no compliant hosted option exists; very high, steady volume on a narrow task where a small fine-tuned model on owned GPUs is cheaper than per-token pricing; offline or edge deployments; or the need to control model versions indefinitely. Otherwise hosted APIs win: better quality on hard tasks, no GPU operations, instant access to new models, and elastic capacity. Self-hosting means owning inference servers (vLLM or similar), autoscaling, monitoring, security patches and quality evaluation yourself. A common middle path is a cloud platform such as Bedrock or Vertex AI, which keeps data inside the existing cloud account and compliance boundary while still offering frontier models.
Glossary
- Token
- The unit of text a model reads and writes, roughly part of a word; used for limits and billing.
- Context window
- The maximum number of tokens a model can consider in one request, including the prompt and history.
- System prompt
- Standing instructions that set the model's role, rules and output format for a conversation.
- Streaming (SSE)
- Receiving the answer as a series of small events while it is generated, instead of all at once.
- Tool use
- The model returns a structured request to call one of your functions; your code runs it and returns the result.
- Structured output
- A response constrained to match a JSON Schema you provide, so your code can parse it safely.
- Prompt caching
- Reusing the processed form of an identical prompt prefix to cut cost and latency on repeat requests.
- Prompt injection
- Untrusted text containing instructions that try to override your rules or misuse the model's tools.
- Evaluation set
- A fixed collection of real inputs with expected results used to compare prompts and models objectively.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.