Skip to main content

Spike: what Claude Code sends

Issue: #5 · Date: 2026-10-11, interactive and compaction captures 2026-10-12 · Status: complete

Question​

What request classes does Claude Code produce, how big are they, how do they use the cache, and which signals identify them reliably?

Method​

Three headless sessions (claude -p, Claude Code 2.1.296) run through blush against a scratch copy of the blush source, with ENABLE_TOOL_SEARCH=true:

SessionTaskRequests to /v1/messages
featureAdd a -version flag with test and docs13
debugFind and fix a seeded failing test4
subagentArchitecture review using three parallel subagents19 (8 main, 11 subagent)

36 Messages requests in total. This is a small sample of short tasks on a small repo. It shows the shape of the traffic; it is not a baseline. That is M1's job (#19).

Headless mode identifies itself as cc_entrypoint=sdk-cli and may skip work the interactive UI does, so on 2026-10-12 the first day of real use through Janus was captured as well: interactive sessions of about 30 main-loop turns, and one /compact of a 183-message conversation. What they added is marked with that date below.

Decisions​

  1. Classify on headers first. Subagent requests carry an X-Claude-Code-Agent-Id header that main-loop requests lack. That is a cleaner signal than fingerprinting the system prompt.
  2. Subagents are the first candidate for local routing, ahead of utility calls, which did not appear at all. A subagent is a separate conversation with its own id, starts from a fresh history, and hands back plain text, so routing one locally never mixes origins inside a conversation.
  3. Requests must be normalised before LM Studio will accept them. Real requests use wire features LM Studio rejects. See "Replay".
  4. Charter D1 is answered: subscription auth. Requests carry an OAuth bearer token, so "spend" is rate-limit headroom, and dollar figures are list-price estimates.

Request classes seen​

ClassHow to recognise itToolsSystem promptCache TTL
Main loopNo X-Claude-Code-Agent-Id header113 blocks, about 9k characters1 hour
SubagentX-Claude-Code-Agent-Id present; first system block contains cc_is_subagent83 blocks, about 2.7k characters5 minutes
Connectivity checkGET / and GET /api/hello, once each at session start, empty body–––
Quota checkInteractive sessions only: POST /v1/messages with the main loop's headers, max_tokens: 1, no tools, one user message quota, not streamed. Sent at start-up to read the rate-limit response headers; on a subscription it is often answered 429. Seen 2026-10-12.–––
CompactionThe main loop's request, unchanged, with one text block added to the end of the last user message: an instruction that begins "CRITICAL: Respond with TEXT ONLY" and asks for "a detailed summary of the conversation so far". Seen 2026-10-12; see "Compaction".Same as mainSame as main5 minutes
Token countPOST /v1/messages/count_tokens, two at once just after compaction. One user message of about 10k characters, no tools, main model. Ledger class token_count. Seen 2026-10-12.–––

Common to both Messages classes: model claude-opus-5-5, stream: true, max_tokens: 128000, thinking: {type: "adaptive", display: "omitted"}, output_config: {effort: "medium"}, context_management with a clear_thinking edit.

Not seen​

  • Any model other than the main one. No small/fast-model calls, in headless sessions or, on 2026-10-12, in an interactive one of 30 turns.
  • Title generation, or any summarisation other than compaction.
  • Non-streaming Messages requests, other than the quota check.
  • Auto-compaction. Only /compact run by hand was captured; auto-compaction is assumed to send the same request.

Compaction​

One /compact, run by hand on 2026-10-12 in a session of 183 messages through Janus:

  • It is the previous turn's request plus an instruction. System prompt, tools, and all 182 earlier messages were the same as the turn before it. The last message carried the same tool result, followed by the summary instruction as a second text block. max_tokens, thinking, and context_management were unchanged, and nothing told the model not to use the tools except the instruction's own words.
  • It reads the cache and writes nothing. The breakpoint stayed on the previous turn's tail, and the system blocks asked for a 5-minute TTL where every other main-loop turn asks for an hour. The prefix the main loop had written with a 1-hour TTL was read all the same: 130,041 tokens read, 2,066 uncached, 0 written.
  • Its cost is output. 7,926 tokens of summary in 68 seconds (8.6 s to the first byte), against a few hundred for an ordinary turn of the same session.
  • The turn after it starts small. The first turn of the compacted conversation had 5 messages and 35k tokens of context, and wrote 23,840 tokens to cache with a 1-hour TTL.

So serving compaction locally (#46) would trade a cheap cache read and about 8k output tokens upstream for a cold local prefill of about 160k tokens, which is well past the context qwen/qwen3.8-27b was loaded with. Whether that is worth doing depends on how often it happens, which the compaction class now lets the baseline (#19) count.

Signals​

SignalWhereReliability
X-Claude-Code-Session-IdHeaderOne value per session, shared by the main loop and its subagents. Use as the session key.
X-Claude-Code-Agent-IdHeaderPresent only on subagent requests; one value per subagent. Use as the conversation key.
cc_is_subagent, cc_entrypoint, cc_versionFirst system block, a one-line billing headerConfirms class and entry point. Text inside a prompt, so more likely to change than a header.
metadata.user_idBodyA JSON string holding device, account, and session ids. Redundant with the headers; contains account identifiers, so keep it out of logs meant for sharing.
Tool count and system prompt textBodyDiffer by class but depend on the user's configuration. Fallback only.
User-Agent, X-AppHeaderIdentify Claude Code and its version; useful for noticing a release that changes the wire shape.

Size​

Main loopSubagent
Request body50–129 kB44–105 kB
Largest context in any request (input + cache read + cache write)44,469 tokens38,178 tokens
Context on the first turnabout 17,400 tokensabout 16,200 tokens

Every request fits inside the 61,696-token context that qwen/qwen3.8-27b was loaded with. These were short tasks; long sessions will grow past it, so the context check in FR-3.4 is still required.

With tool search on, tool schemas are about 22k characters for 11 tools. Further tools arrive inside the conversation rather than in tools.

Cache behaviour​

  • Uncached input is effectively zero: 2 to 4 tokens per request. All other input is a cache write or a cache read.
  • Four breakpoints at most. Two on the system blocks, one at the end of the second message (after the injected tool definitions), and one that moves along the tail of the conversation.
  • The main loop writes with a 1-hour TTL, subagents with 5 minutes.
  • Each turn reads the previous prefix from cache and writes only the new tail, typically a few hundred to a few thousand tokens.

Token totals across all three sessions:

RequestsCache writeCache readOutputof which thinking
Main loop2596,200 (1h)719,30615,208940
Subagent1178,896 (5m)230,96012,5874,346

Subagents were 11 of 36 requests but 45% of cache-write tokens and 45% of output tokens, because each one starts a new conversation and pays to write its whole prefix.

These are not priced. Claude Code reported list-price costs of $0.49, $0.26, and $1.28 for the three sessions. Applying the commonly quoted multipliers (cache read 0.1x, 5-minute write 1.25x, 1-hour write 2x, output 5x input) to these token counts does not reproduce those figures with any single base price; the implied base ranges from $3.42 to $4.09 per million. The pricing model in #15 must be built from the published price list and checked against Claude Code's own reported cost, not from those ratios.

What this means for the charter​

  • Compression (M5) has a narrow target. With no uncached input, a transform can only save by shrinking what gets written to and read from cache, and it loses if it disturbs a prefix that is already cached. This is charter risk R1 seen in real numbers.
  • Routing whole subagent conversations avoids the cache entirely, which is why it looks like the better first move.

Wire features beyond the basic Messages API​

All of these appeared in real requests or responses, and the typed model in #11 has to carry them through untouched:

  • Messages with role: "system" in the middle of a conversation, with string or block content.
  • tool_addition content blocks that carry a tool definition inside a message, some with cache_control.
  • defer_loading on tool definitions, and a placeholder tool for deferred tools.
  • Top-level context_management and output_config.
  • cache_control with a ttl.
  • Thinking blocks that stream with empty text and a signature (display: "omitted"), via thinking_delta and signature_delta.
  • usage with cache_creation split by TTL, output_tokens_details, iterations, service_tier, and inference_geo.
  • message_delta with stop_details and container.
  • ping events in the stream.
  • Fourteen anthropic-beta values on every request.
  • Responses are gzip-compressed. Claude Code offers gzip, deflate, br, zstd and the API answered with gzip, streams included. blush hid this by forcing Accept-Encoding: identity; it was found when Janus, which leaves the header alone, first tried to read a response (2026-10-11). Janus now narrows the header to gzip on Messages requests only, so count_tokens answers arrive brotli-compressed, and are captured as they arrived (2026-10-12).

Replay against the local model​

Two real first-turn requests were replayed to qwen/qwen3.8-27b through LM Studio's /v1/messages, one run each.

Unmodified, both were rejected with HTTP 400: request.messages.1.content.1.type: Invalid literal value, expected "text". LM Studio does not accept tool_addition blocks. Removing context_management, output_config, thinking, and metadata made no difference; those are tolerated.

After normalising, both were accepted. The normalisation: move each tool_addition definition into tools, drop the block from the message, and drop deferred placeholder tools.

RequestPrompt tokens (local count)Time to first tokenTotalLocal resultClaude's result
Subagent, turn 111,01820 s53 sThinking, then four Read calls with correct absolute pathsOne Bash call
Main loop, turn 115,15126 s45 sThinking, then one Bash callOne Bash call

So the local model can take a real Claude Code request and return valid tool calls. It is slow to start: 20 to 26 seconds before the first token on a cold prompt of this size, where Claude finished whole turns in 2 to 6 seconds. Whether LM Studio's prompt cache shortens later turns of the same conversation was not measured, and nothing here says whether a local subagent reaches a correct final answer. Both belong to #8.

Follow-ups​

  • #22: add the tool_addition normalisation to the LM Studio backend.
  • #23: classes are main, subagent, connectivity, quota, compaction, token_count, unknown for now.
  • #46: the compaction numbers above are one sample; the baseline decides.
  • #28: evaluate subagents as the first class to switch.