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:
| Session | Task | Requests to /v1/messages |
|---|---|---|
| feature | Add a -version flag with test and docs | 13 |
| debug | Find and fix a seeded failing test | 4 |
| subagent | Architecture review using three parallel subagents | 19 (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
- Classify on headers first. Subagent requests carry an
X-Claude-Code-Agent-Idheader that main-loop requests lack. That is a cleaner signal than fingerprinting the system prompt. - 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.
- Requests must be normalised before LM Studio will accept them. Real requests use wire features LM Studio rejects. See "Replay".
- 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
| Class | How to recognise it | Tools | System prompt | Cache TTL |
|---|---|---|---|---|
| Main loop | No X-Claude-Code-Agent-Id header | 11 | 3 blocks, about 9k characters | 1 hour |
| Subagent | X-Claude-Code-Agent-Id present; first system block contains cc_is_subagent | 8 | 3 blocks, about 2.7k characters | 5 minutes |
| Connectivity check | GET / and GET /api/hello, once each at session start, empty body | – | – | – |
| Quota check | Interactive 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. | – | – | – |
| Compaction | The 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 main | Same as main | 5 minutes |
| Token count | POST /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
/compactrun 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, andcontext_managementwere 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
| Signal | Where | Reliability |
|---|---|---|
X-Claude-Code-Session-Id | Header | One value per session, shared by the main loop and its subagents. Use as the session key. |
X-Claude-Code-Agent-Id | Header | Present only on subagent requests; one value per subagent. Use as the conversation key. |
cc_is_subagent, cc_entrypoint, cc_version | First system block, a one-line billing header | Confirms class and entry point. Text inside a prompt, so more likely to change than a header. |
metadata.user_id | Body | A 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 text | Body | Differ by class but depend on the user's configuration. Fallback only. |
User-Agent, X-App | Header | Identify Claude Code and its version; useful for noticing a release that changes the wire shape. |
Size
| Main loop | Subagent | |
|---|---|---|
| Request body | 50–129 kB | 44–105 kB |
| Largest context in any request (input + cache read + cache write) | 44,469 tokens | 38,178 tokens |
| Context on the first turn | about 17,400 tokens | about 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:
| Requests | Cache write | Cache read | Output | of which thinking | |
|---|---|---|---|---|---|
| Main loop | 25 | 96,200 (1h) | 719,306 | 15,208 | 940 |
| Subagent | 11 | 78,896 (5m) | 230,960 | 12,587 | 4,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_additioncontent blocks that carry a tool definition inside a message, some withcache_control.defer_loadingon tool definitions, and a placeholder tool for deferred tools.- Top-level
context_managementandoutput_config. cache_controlwith attl.- Thinking blocks that stream with empty text and a signature
(
display: "omitted"), viathinking_deltaandsignature_delta. usagewithcache_creationsplit by TTL,output_tokens_details,iterations,service_tier, andinference_geo.message_deltawithstop_detailsandcontainer.pingevents in the stream.- Fourteen
anthropic-betavalues on every request. - Responses are gzip-compressed. Claude Code offers
gzip, deflate, br, zstdand the API answered with gzip, streams included. blush hid this by forcingAccept-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, socount_tokensanswers 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.
| Request | Prompt tokens (local count) | Time to first token | Total | Local result | Claude's result |
|---|---|---|---|---|---|
| Subagent, turn 1 | 11,018 | 20 s | 53 s | Thinking, then four Read calls with correct absolute paths | One Bash call |
| Main loop, turn 1 | 15,151 | 26 s | 45 s | Thinking, then one Bash call | One 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_additionnormalisation to the LM Studio backend. - #23: classes are
main,subagent,connectivity,quota,compaction,token_count,unknownfor now. - #46: the compaction numbers above are one sample; the baseline decides.
- #28: evaluate subagents as the first class to switch.