The Vendor Surface, Quarantined
Everything in this book so far has been provider-neutral on purpose, and this page is the exception — the only one in the first thirteen chapters that names companies, spells out parameters, and describes capabilities that will have moved by the time you read it. Everything here is stated as of 2026.
It is deliberately isolated so the rest of the book does not go stale when a vendor ships. The concepts in the previous five topics — roles, token accounting, sampling, stop reasons, schemas — are common to every major API and have been stable for years. What follows is the surface layer on top of them, and it is the layer that changes.
system field. Tools declared flat, as tools[].input_schema. Stop reason stop_reason, reading tool_use or end_turn. Tool result a block inside a user message.system. Tools wrapped in a function object, as tools[].function.parameters. Stop reason finish_reason, reading tool_calls or stop. Tool result a message with its own role.system_instruction. Tools grouped, as tools[].function_declarations[]. Stop reason finishReason, reading STOP or MAX_TOKENS. Tool result a function_response part.The Same Request Three Ways
Three providers dominate the general-purpose model API market as of 2026: Anthropic, OpenAI and Google, each also resold through a cloud platform, alongside open-weight models from Meta, Mistral, DeepSeek and others that you self-host or reach through a hosting provider. Below is one Sundry turn — the same system instruction, the same tool, the same tool result — expressed against each of the three, trimmed to the parts that differ.
# Anthropic — Messages API: system is its own top-level field {"system": "You are Sundry's support agent...", "tools": [{"name": "get_order", "input_schema": {...}}], "messages": [{"role": "user", "content": [ {"type": "tool_result", "tool_use_id": "c1", "content": "..."}]}]} # OpenAI — Chat Completions: system is a message role, tool results have their own {"messages": [{"role": "system", "content": "You are Sundry's support agent..."}, {"role": "tool", "tool_call_id": "c1", "content": "..."}], "tools": [{"type": "function", "function": {"name": "get_order", "parameters": {...}}}]} # Google — Gemini: instruction is separate, turns are user/model with parts {"system_instruction": {"parts": [{"text": "You are Sundry's support agent..."}]}, "contents": [{"role": "user", "parts": [ {"function_response": {"name": "get_order", "response": {...}}}]}], "tools": [{"function_declarations": [{"name": "get_order", "parameters": {...}}]}]}
Four differences do all the work in those three bodies. The system instruction is a top-level field on two of them and a message role on the third. Tool schemas are declared flat on one, wrapped in a function object on another, and grouped under a declarations list on the third. Tool results come back as a content block inside a user message, as a message with its own role, or as a response part — and each carries a different name for the id that links a result to the request it answers. Everything else in Topic 07 holds unchanged.
| Element | Anthropic | OpenAI | |
|---|---|---|---|
| System instruction | Top-level system | A message with role system (or developer) | Top-level system_instruction |
| Tool declaration | tools[].input_schema | tools[].function.parameters | tools[].function_declarations[] |
| Stop reason field | stop_reason: tool_use, end_turn, max_tokens | finish_reason: tool_calls, stop, length | finishReason: STOP, MAX_TOKENS, SAFETY — a function call arrives as a functionCall part under STOP, so there is no tool-request value to branch on |
| Tool result | tool_result block in a user message | Message with role tool and tool_call_id | function_response part in a user turn |
All three also offer a newer, more stateful surface alongside the request-response one shown here — endpoints that hold conversation state, run server-side tools, or manage an agent loop for you. Those are worth knowing about and worth being careful with: they move the array you learned to read in Topic 07 onto the provider's side, which is convenient until the day you need to see exactly what was sent. Chapter 14 weighs that trade properly.
Model Naming and Versions
Every provider offers two kinds of name for the same model. A pinned snapshot carries a version or a date in its identifier and refers to one fixed set of weights: the same name a year from now is the same model, until it is retired. A moving alias — a family name, or one with a "latest" suffix — resolves to whatever the provider currently considers the best member of that family, and it changes without a deploy on your side.
Point production at a pinned snapshot. An alias means your agent's behaviour can change overnight with no commit to blame, no diff to read, and no way to correlate a drop in resolution rate with a change you made — because you did not make one. Aliases are for local experiments and for the eval run where the new model is the thing being measured. Upgrading is then an ordinary deploy: change the pin, run the 120-ticket eval suite against both versions, compare, ship. Providers publish retirement dates for pinned snapshots, so pinning is a commitment to upgrade on a schedule, not a way to avoid it forever. Chapter 13 versions the pin, the prompt and the sampling settings together as one unit.
The Pricing Shape
Exact prices belong on the provider's pricing page and nowhere else, including here. What is worth learning is the shape, because the shape is what your design responds to and it has been stable across providers for years even as the numbers fell by an order of magnitude.
| Billed unit | What it covers | Relationship to the input rate (as of 2026) |
|---|---|---|
| Input tokens | Everything you send, schemas included | The baseline everything else is quoted against |
| Output tokens | Everything the model generates | Several times the input rate, on all three |
| Cached input reads | A repeated prefix served from cache | Substantially cheaper; the discount ranges from a large fraction to an order of magnitude |
| Cache writes | Placing a prefix into the cache | At or slightly above the input rate where it is charged separately |
| Reasoning tokens | Internal tokens on reasoning modes | Billed as output, and not always returned to you |
| Batch tier | Non-interactive processing with a deadline | Markedly discounted; not usable for a live support queue |
Three of those rows change a design rather than a spreadsheet. Output being the expensive unit is why verbose per-turn narration is a bad default. Cached reads being dramatically cheaper is why the stable prefix in Topic 07 is worth protecting. And reasoning tokens being billed as output while sometimes being withheld from you means a reasoning-mode agent can cost noticeably more per turn than its visible reply suggests — measure the billed counts from the response, never from what you can see.
Capability Differences That Change Design
Context windows as of late 2026 sit at about a million tokens on the mainstream flagship models, with the fast, cheap tier at roughly 200,000 and a few providers advertising several times a million, and some charge a higher rate past a threshold. The spread decides an architectural question rather than a configuration one: on the small tier, Sundry's policy library never fits and retrieval is mandatory (Chapter 6); on the large one you could paste more of it in, and Chapter 5 explains why you still should not, because adherence degrades long before capacity does.
Caching mechanics differ more than anything else on this page. One provider wants explicit breakpoints you place in the request and honours them for five minutes by default, an hour for a higher write price; another caches long prefixes automatically once they pass a length threshold; another offers both an explicit cached-content object and an implicit path. A prompt layout tuned for one of those can cache almost nothing on another — same tokens, same order, no discount — which is the single most expensive portability surprise in this chapter and the reason a provider switch needs a cost re-measurement rather than a rate-card comparison.
Three more differences worth designing around. Parallel tool calls are supported everywhere and can be disabled everywhere, but how eagerly a model batches independent lookups differs, and that is worth measuring because it moves p95 latency directly. Reasoning modes let you spend more output tokens on internal work before the reply, with a budget you control on some providers and a coarse setting on others; they help on genuinely hard judgement and are wasted on a lookup-and-quote ticket. And rate limits are structured differently — per-model, per-tier, per-region, with separate request and token buckets — which means a Monday-morning burst of Sundry's 4,200-ticket week hits a rate limit before it hits a budget, and your concurrency and backoff policy is shaped by the provider you chose.
Portability in Practice
One adapter module, about a hundred lines per provider, is the whole portability story. Its job is to accept your own request shape, emit the provider's, and translate the response back into types your loop already understands — including normalizing the stop reason into the five constants from Topic 10.
class ModelAdapter(Protocol): def send(self, system, messages, tools, max_output, temperature) -> Reply: ... # Reply is ours, not theirs: # .text assembled from the provider's content blocks # .tool_calls list of (id, name, arguments), always a list # .stop_reason COMPLETED | TOOL_REQUEST | OUTPUT_LIMIT | FILTERED | UNKNOWN # .usage input_tokens, output_tokens, cached_input_tokens # .model_version the pinned snapshot that actually served this call
In words: the loop calls send and gets back a Reply it fully understands, so nothing above the adapter contains a vendor spelling. That buys three concrete things — a provider swap becomes a config change rather than a refactor, the same eval suite can be run against two providers to compare them on your own tickets instead of on a leaderboard, and every trace records which pinned version served it. What it does not buy is behavioural equivalence: the prompt that works well on one model needs re-evaluating on the next, and the caching layout almost certainly needs rework.
Cross-provider libraries like LiteLLM and routing layers like OpenRouter do this work for you, and they are a reasonable choice when you want breadth quickly or want to fail over between providers. The costs are real though: they normalize to a common denominator, so the newest capability of any one provider arrives late or not at all, and they add a layer between you and the wire format at exactly the moment you most need to see it. Write the hundred lines yourself when the model call is the core of your product; take the library when it is a component.
Reading This Page in Six Months
This topic and the first topic of Chapter 14 are the two pages the monthly content audit rewrites wholesale. Everything else in this book was written to survive a provider release, and if a page outside these two has gone stale, that is a defect in the writing rather than in the field.
A reviewer checking this page should go to four sources per provider and nowhere else: the API reference for the messages or chat endpoint, which settles the request shape and the stop-reason values; the pricing page, which settles the units and their relationships; the model lifecycle or deprecation page, which settles what a pinned snapshot means and when it retires; and the rate-limit page, which settles the concurrency structure. Blog posts and benchmark tables are not sources for any claim on this page. If those four documents disagree with a sentence above, the documents are right and this page is out of date.
- Building provider specifics into the loop instead of an adapter — a field name in the dispatcher and a stop-reason string in the branch turn a config change into a week-long refactor.
- Pointing production at a moving alias — behaviour changes without a deploy, the regression has no commit to blame, and the resolution-rate drop arrives days before anyone connects it to a model release.
- Choosing a provider on benchmark scores alone — rate-limit structure, caching mechanics and tool-calling behaviour move a real agent's cost and p95 far more than a leaderboard position does.
- Assuming caching works identically across providers — the prefix rules differ, and a layout tuned for one can earn no discount at all on another with the same tokens in the same order.
- Keep one adapter module as the only file that knows vendor spellings, and normalize stop reasons into your own constants inside it.
- Pin model versions in production and upgrade behind the eval suite, comparing old and new on your own tickets (Chapters 9 and 13).
- Record the date on every price and capability claim in your own docs, exactly as this page does, so a stale line is visibly stale.
- Re-read this page and the first topic of Chapter 14 at every audit cycle, and assume the rest of the book aged better than they did.
Knowledge Check
Which difference between provider APIs actually forces a change in the agent loop rather than in a serializer?
- The stop-reason field name and its values, because the loop's control flow branches on them
- Whether the system instruction is a top-level field or a message with the system role
- How deeply the tool schema is nested inside the tools array in the request body
- Whether the response fields are named in snake case or in camel case by the provider
Sundry's resolution rate drops four points overnight with no deploy on Vera's side. Which configuration choice most likely allowed it?
- Production pointed at a moving model alias rather than at a pinned dated snapshot
- Temperature was left at a mid-range value instead of being pinned at the lowest setting
- The prompt cache expired overnight, so the morning's tickets ran without a cached prefix
- The account's rate limits reset on a new billing period and throttled the morning burst
A team moves Sundry to a second provider with a lower published input rate, and the monthly bill goes up. What is the most likely cause?
- The prompt layout no longer matches the new provider's caching rules, so the prefix stopped being discounted
- The new provider's tokenizer splits the same ticket text into several times as many billable tokens per request
- The new provider charges a per-request fee on top of its token rates, which dominates at 4,200 tickets a week
- The new provider bills the nine tool schemas separately from the messages they are attached to on every turn
What belongs inside the provider adapter, and what belongs above it?
- Field names, request shapes and stop-reason normalization go in; the loop's policy and limits stay out
- The $150 refund ceiling goes in, so every provider path enforces the same limit consistently
- The nine tool implementations go in, since their schemas are serialized differently per provider
- The policy retrieval step goes in, because each provider expects retrieved documents in its own format
You got correct