fix: canonicalize JSON values for stable vLLM prefix caching#30
fix: canonicalize JSON values for stable vLLM prefix caching#30myshytf wants to merge 6 commits into
Conversation
… instructions System reminders, local command caveats, skill listings, and system-role history turns were lifted into the Responses `instructions` field, which renders at the very top of the upstream prompt. Any mid-session reminder therefore rewrote the prompt head and invalidated the entire prefix cache (observed via analyze-log on real Claude Code traffic: system message rewritten at ~16-32KB offsets, 0% of a 100k+-token history reused). Now private context is emitted at its original position as a guarded user turn (same wrap_private_context framing), keeping request histories append-only. /clear command echoes are still dropped entirely. Measured through the full gateway against GLM-5.2 + LMCache: a turn that introduces a <system-reminder> now reuses 2304/2435 prompt tokens (94.6%, GPU prefix + LMCache) instead of 0%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t_tokens vLLM reports prompt_tokens_details.cached_tokens (with --enable-prompt-tokens-details); the Anthropic adapter now maps it into usage.cache_read_input_tokens on message_start/message_delta and the non-stream response, with input_tokens excluding cache reads per Anthropic semantics. Claude Code's cache indicator previously always showed a miss even when the backend hit 98% (verified: turn-2 now reports cache_read_input_tokens=2048, input_tokens=200, 27.7s -> 1.6s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cherry-pick 0a5ba94 (G4: strip images to [Image #N] placeholders, cache originals per-session, inject analyzeImage server tool, run it against an OpenAI-compatible vision backend) adapted to this fork lineage: - responses_to_chat: merge lower_request_with_default_reasoning_effort and lower_request_with_image_agent into lower_request_with_options (this lineage threads the config default reasoning effort through lowering). - engine: request_model_genuinely_resolves rewritten against this lineage's UpstreamModelCatalog (new exact_id/canonical_unique/default_id helpers mirroring normalize()); no G7 model_routes here. Dropped the G2 chat_reasoning_suppressed hunk that rode along in the cherry-pick (its callers do not exist in this lineage). - upstream: took candidate_backend_models trait/impl additions + image-URI log/error redaction; kept this lineage's simple ensure_success (no G1 shrink-and-retry, no G6 bounded SSE frames). - config: image_agent_enabled / vision_url / vision_model / image_cache_max_size / image_cache_ttl_secs + env overrides + per-profile native_vision override, grafted onto the capabilities-era config shapes. - Dropped G4's tests/common + port_* test additions (harness not present in this lineage); gateway tests updated for the new Gateway::new arity. cargo test: 280 lib + 3 main + 89 gateway green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ix caching Stock G4 gating injects the analyzeImage tool + IMAGE HANDLING system prefix only on turns whose LATEST user message carries an image. On a long session that means the prompt head (tools block + system prefix, at the very top of the GLM template) flips when the first image arrives and flips BACK on the next text-only turn — each flip invalidates the entire prefix cache and re-prefills the whole history. It also leaves images in OLDER history turns un-stripped on those follow-up turns (clients replay full history), sending raw image bytes to a text-only backend. image_agent_always_active: true (config / LLMCONDUIT_IMAGE_AGENT_ALWAYS_ACTIVE) activates the agent on EVERY turn of an eligible backend: constant tool + system prefix (byte-identical head, placeholder numbering deterministic in scan order so history placeholders re-materialize identically), history images always stripped+cached. Costs a constant few hundred prompt tokens. tool_choice:"none" turns keep the identical head too (the model simply cannot call the tool there). Default false (stock G4 behavior). Two gateway tests pin the always mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serde_json without preserve_order serializes #[serde(flatten)] fields via BTreeMap (alphabetical), causing chat_template_kwargs to appear before model in upstream requests. vLLM prefix cache is byte-sensitive, so inconsistent key ordering between requests degrades cache hit rates. Enable preserve_order feature so serde_json uses IndexMap (insertion order = struct field order), ensuring model is always first in the serialized JSON sent to vLLM.
Commit 27a9fdf enabled serde_json preserve_order to fix struct field serialization order (model before chat_template_kwargs). However, preserve_order switches Value::Object from BTreeMap (alphabetical) to IndexMap (insertion order), removing the free canonicalization BTreeMap provided. This caused severe prefix cache misses: different clients or code paths constructing the same logical JSON with different key insertion orders now produce different serialized bytes → different vLLM token sequences → cache miss. Fix: add canonicalize_chat_request_json_values() which recursively sorts all Value::Object keys before sending to upstream vLLM. This restores the alphabetical key ordering guarantee while keeping preserve_order for struct-level field order (model first). Canonicalization targets: - message content - assistant tool_call arguments - tool/function parameters - tool_choice - response_format - extra_body / chat_template_kwargs Also canonicalize in stringify_json_value() before string conversion. Regression test: sanitize_chat_request_canonicalizes_preserve_order_sensitive_json_values — same logical request with reversed key order → asserts identical upstream JSON bytes after sanitize. Supersedes local-inference-lab#28.
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (18)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Supersedes #28 (closed, not merged).
Problem
#28 enabled
serde_jsonpreserve_orderto fix struct field serialization order (modelbeforechat_template_kwargsin#[serde(flatten)]output). The fix was correct for the struct-level problem but incomplete:preserve_orderswitchesValue::ObjectfromBTreeMap(alphabetical) toIndexMap(insertion order), removing the free key canonicalizationBTreeMapprovided.Without
preserve_order,BTreeMapautomatically sorted all JSON object keys alphabetically — regardless of how clients constructed the JSON. This gave consistent serialized bytes for free, which is critical for vLLM prefix cache (byte-sensitive token matching).With
preserve_orderON, dynamic JSON values (tool parameters, tool_call arguments, content objects,extra_body) now preserve insertion order. Different clients or code paths constructing the same logical JSON with different key orders produce different bytes → different token sequences → cache miss.Fix
Add
canonicalize_chat_request_json_values()insrc/upstream.rswhich recursively sorts allValue::Objectkeys before sending to upstream vLLM. This restores the alphabetical key ordering guarantee thatBTreeMapprovided, while keepingpreserve_orderfor struct-level field order (modelfirst).Canonicalization targets
contenttool_callargumentsparameterstool_choiceresponse_formatextra_body/chat_template_kwargsAlso canonicalize in
stringify_json_value()before string conversion.Regression test
sanitize_chat_request_canonicalizes_preserve_order_sensitive_json_values— constructs the same logical request with reversed key order and asserts identical upstream JSON bytes aftersanitize_chat_request().Files changed
Cargo.toml/Cargo.lockserde_jsonpreserve_orderfeature (from #28)src/upstream.rscanonicalize_chat_request_json_values()+canonicalize_json_value()+stringify_json_valuecanonicalization + regression testTest results