Repository navigation
fix(proxy): keep the system prompt out of the cacheable prefix - #74
insterbridge wants to merge 1 commit into
Conversation
The three canonical system blocks are the cacheable prefix. Folding the client's own system prompt into block 2 (issue TriDefender#71's fix) makes the whole prefix depend on text a harness is free to vary per turn — omp injects live context (date, cwd, counters) — so every turn changed block 2 and invalidated all message segments behind it. Measured against the live gateway on a 1.82 MB conversation: 26,433 bytes of reusable prefix (98.5% loss), cache_read 8.8k on a 400k-token context. Attach the client text as a user turn instead, inserted BEFORE the final turn. Placement matters: applyAnthropicCacheControl marks the last non-system message, so appending the volatile text after that point makes the breakpoint itself vary — every request then writes a fresh cache entry and reads nothing (measured cache_read=0 despite a stable history). Inserting it ahead of the marked turn keeps the breakpoint anchored on append-only history. Result on glm-5.3-flash, identical 20-turn history, client system prompt changed between the two calls: input_tokens 60, cache_read 15,552 (full prefix reused). The instructions still reach the model. Tests pin both properties: differing client prompts yield byte-identical system blocks, and the breakpoint never lands on the volatile instructions.
|
I'm not sure what will changes will happen to agentic turns if you pass the harness instructions in as user context, did you try that before hand? also the current system-prompt injected carried filepath locations of the proxy program file (which is how zcode structured their system prompt) this might add more confusion regarding r/w operations |
|
Thanks for investigating this and sharing the gateway measurements. The cache loss is plausible: if a harness changes its system prompt on every turn, a prefix cache can lose reuse for the conversation that follows, including when the harness talks directly to an upstream service. Our proxy also changes the prompt structure, so its handling of block boundaries and cache markers can affect how much is reusable. I’m concerned about adopting this particular fix without further validation:
Could we also compare representative harness behavior before and after the change, especially conflicting tool/permission instructions, and test the relevant gateway paths? The cache problem deserves attention, but these compatibility tradeoffs need to be understood before merging. |
|
If your harness is changing system_prompt every turn then you will get cache misses no matter you use this proxy or not. |
|
You are right on all three points, and I verified the last two in code rather than arguing from the earlier measurement. Two of them were real defects in the patch as submitted. Point 3 — tool-call pairing (confirmed defect). Inserting before the last message put the extra user turn between an assistant That breaks the required adjacency. Fixed. Point 2 — growing conversations (confirmed defect, and the more serious one). You were exactly right that splicing before the current last message makes the block move. Real append-only turns produced: The prefix diverges right after Current approach. Append the client text after the history instead, and move the cache breakpoint off it: That is append-only, keeps tool adjacency, and the breakpoint sits on history rather than on volatile text. Point 1 — instruction semantics. I do not have evidence that moving harness instructions from system to user preserves tool, permission, and output behavior, and I should not have shipped it as though the cache result settled that question. Your framing is right: reaching the model as text is not the same as the harness still behaving as intended. The filepath-location concern is also fair — the injected system prompt carries proxy paths, and instructions arriving as user context alongside that invites exactly the confusion you describe. I cannot produce the measurements you asked for right now: both models on this account have hit Two options, your call:
Your third comment is the part I keep coming back to: if the harness rewrites its system prompt every turn, the miss happens upstream too. That suggests the more useful fix might be on the harness side, or scoped to only the volatile part of the prompt, rather than relocating all client instructions. I would like your read on which of those is worth pursuing. |
|
Closing this. Your review was right on the points that mattered, and the more I measured the less the proposed fix looked like the right shape. Summary of what the investigation actually established: The proxy-side bug is real. Upstream's issue #71 fix folds the client system prompt into block 2, which carries the cache breakpoint. On a live 1.82 MB session that left 26,433 bytes reusable — 1.44%. Keeping the client text out of those blocks raised it to 1,810,729 bytes, 98.76%. Those are measurements from real traffic, not a synthetic harness. But the fix I proposed is not the right one. Two defects you identified were real, and I confirmed both in code: inserting before the last message wedged a user turn between And your third comment is the one that settles it. With a stable system prompt, growing turns reuse 12,416 tokens every turn (input_tokens 14/24/34). With a system prompt that changes each turn, the same conversation re-bills ~10.5k on turns 1 and 2 before recovering to 99.2% by turn 3. The miss originates in what the harness sends, not in the proxy. Moving the text around inside the proxy does not change that. Instruction semantics I never validated. I have no evidence that relocating harness instructions from system to user preserves tool, permission, or output behavior, and the filepath concern you raised is a fair reason to be cautious about it independently. I am keeping the cache-stable-system-block change locally, since it measurably helps my setup, but I agree it should not land upstream on this evidence. Thanks for the careful review — points 2 and 3 in particular saved a bad merge. |
Problem
Prompt caching collapses on large conversations. With a ~400k-token context the
gateway reported
cache_readof only ~8.8k — about 1.5% of the body.Cause: the client's own
systemprompt is folded into block 2 of thecanonical 3-block system array, and that block carries the
cache_controlbreakpoint. Harnesses vary their system prompt per turn — omp injects live
context (date, cwd, counters) — so block 2 changed on every request and
invalidated every message segment behind it.
Measured on a live session, 1.82 MB conversation:
Fix
Two changes, both small:
buildStartPlanSystemno longer folds the client text into the systemblocks, so they are byte-stable across turns.
applyStartPlanSystemattaches that text as a user turn, inserted beforethe final message.
Placement is the subtle part.
applyAnthropicCacheControlmarks the lastnon-system message's last block. Appending the volatile text after that point
makes the breakpoint itself vary, so every request writes a fresh cache entry
and reads nothing — I measured
cache_read=0with a perfectly stable historybefore moving it. Inserting it ahead of the marked turn anchors the breakpoint
on append-only history.
Verification
Against the live gateway on
glm-5.3-flash, identical 20-turn history, clientsystem prompt deliberately changed between the two calls:
Full prefix reused. The instructions still reach the model.
Tests pin both properties:
bun test: 1121 pass, 0 fail.tsc --noEmit: clean.Tradeoff
Client system instructions now arrive as a user turn rather than a system
block, so they may carry slightly less authority. The alternative is keeping
them in the system block and losing the cache for the whole conversation.