Providers and frameworks

How chat completions and the Messages API differ

The same agent request, sent as OpenAI-style chat completions through OpenRouter and as Anthropic’s Messages API, differs in twelve places, from where the system prompt goes to how a stream ends. A table of each difference, with the chapter and the code that handles it.

Draws on chapters 2, 4, 6, 8, and 11 of the bookUpdated

This page works on one stage of the loop

  1. 1Build the request
  2. 2Call the model (this page)
  3. 3Run the tools
  4. 4Decide

The book’s agent talks to models through one interface, Model, with one method. Behind it sit two clients: one for the chat completions format, which OpenRouter, OpenAI, and many local servers accept, and one for Anthropic’s Messages API. Chapter 11 writes the second as an adapter and lists what an adapter for any API has to decide. This page collects the answers for the two APIs the book covers into one table, so you can see the size of the job before you start a third.

Facts below are from chapters 2, 4, 6, 8, and 11, checked against the companion code, as of October 2026. Provider APIs change; each row names the place in the code to check when one does.

The table

What differs Chat completions (OpenRouter) Messages API (Anthropic) Where the book handles it
System prompt A message with role system A top-level system field ch11/anthropic/wire.go
Reply length limit Optional; the provider’s default if unset max_tokens is required; the client sends 16,000 when the agent sets none, since thinking counts against it Chapter 11
Roles system, user, assistant, and tool user and assistant only, alternating; tool results and harness notes after them are joined into one user turn Chapter 11
Tool-call arguments A string that holds JSON A JSON object; arguments that aren’t valid JSON go as an empty object, and the tool’s error explains Chapters 4 and 11
Tool results A message with role tool and the call’s ID; no failure flag, so the client writes “Error:” into the text A tool_result block in a user turn, with an is_error flag Chapters 4 and 11
Reasoning sent back OpenRouter’s reasoning_details, returned as received Thinking blocks returned exactly as received, in order, only to the provider and host that wrote them Chapter 11 (ReasoningFrom)
Why the reply stopped finish_reason: stop, tool_calls, length, content_filter, error stop_reason: end_turn, tool_use, max_tokens, refusal, and others, mapped onto the left column ch11/anthropic/wire.go
Prompt token count prompt_tokens includes tokens read from the cache input_tokens leaves out cache reads and writes, so the prompt is the sum of three counts Chapter 11
Cost OpenRouter reports what it billed in each reply Not reported by Anthropic’s API; the client prices each reply from a table kept in configuration (OpenRouter’s Messages endpoint does report it) Chapter 11 (prices.json)
Prompt caching Automatic on some models; Anthropic’s models need a cache_control marker, which OpenRouter passes on cache_control markers, up to four per request Chapter 8
Errors An HTTP status, or an error object inside a 200 reply An HTTP status, including 529 when overloaded; an error body in a 200 or an error event mid-stream becomes the same error type Chapters 5, 11, and 12
Streaming data: lines of chunks; the usage arrives after the finish reason; ends with data: [DONE] Named events per content block; the stop reason and final usage arrive together in message_delta; ends with message_stop Chapters 6 and 11

A few rows need more than a cell.

Refusals. A Claude model that declines a request stops with refusal, in a normal reply that can hold some text. The conversation can’t usefully continue from there, so the book’s loop counts it among the replies it can’t continue from and stops with ErrRefused, a new member of the bad-reply family in chapter 11. Here is the whole mapping, from the companion code:

ch11/anthropic/wire.go
// finishReason maps a stop reason onto the finish reasons the loop
// checks (chapter 5). A refusal is its own reason, which the loop turns
// into ErrRefused. pause_turn comes only from server-side tools, which
// this agent doesn't use, so it can't be continued here.
func finishReason(stop string) string {
	switch stop {
	case "end_turn", "stop_sequence":
		return "stop"
	case "tool_use":
		return "tool_calls"
	case "max_tokens", "model_context_window_exceeded":
		return "length"
	case "refusal":
		return "refusal"
	}
	return "error" // pause_turn, or a reason this client doesn't know
}

Reasoning and hosts. OpenRouter routes one Claude model to several hosts, each under its own account. In chapter 11’s experiment, a thinking block sent back to the host that wrote it was accepted in both runs, and sent to any of three other hosts it was refused with a 400. That is why the adapter records where reasoning came from, and why chapter 13 later pins one host.

Stream size. Over the 13 replies of each recorded run of the same task, the Messages API’s stream had a median of 64 events per reply, and chat completions’ 31; per output token, about 1.2 times as many. The adapter turns each event into at most one of the loop’s Delta values, so the loop never learns which format it is reading.

Bytes on the wire

Each format writes the same conversation its own way, and the sizes come out close. Chapter 11 encoded one request, the last of a recorded run, with each client:

Client Total bytes Of it, reasoning
Chapter 2’s, chat completions 55,162 11,156
OpenRouter’s Go SDK, chat completions 55,227 11,156
The Messages API’s 43,219 0

Without its reasoning, the chat completions conversation was within about 1 percent of the Messages API’s. The Messages API request carried no reasoning because that reasoning came from another client, and sending it would break the rule in the reasoning row above.

Other servers that speak chat completions

The chat completions format is the one many servers accept, and the book’s client reads its address from OPENROUTER_BASE_URL. Chapter 2 points it at a local Ollama server, http://localhost:11434/v1, with no other change. Two things differ there: the program needs any non-empty key to stop replaying, and fields only OpenRouter sends, such as each call’s cost, read as zero, so a dollar budget does nothing. More providers lists the native APIs of Gemini, Ollama, Bedrock, and Cohere, with their official documentation.

Writing a third adapter

For any new API, the rows of the table are the questions. Answer each one in code, keep the API’s format inside its adapter, and test it two ways: httptest replies for each error and edge case, and a recorded run replayed through the client. For scale, chapter 11’s Messages API client, ch11/anthropic, is 1,559 lines of Go in 6 files, its tests included.