Skip to content

fix(injection): keep injected context out of the prompt cache prefix - #3642

Closed
opieter-aws wants to merge 2 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/context-injector-caching
Closed

opieter-aws wants to merge 2 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/context-injector-caching

Conversation

@opieter-aws

@opieter-aws opieter-aws commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

ContextInjector prepended its per-call text into the latest user message, ahead of the stable conversation. Because that text is rebuilt on every call and the provider's cache point landed after it, the cached prefix diverged exactly at the injection site — so every turn re-ingested the whole conversation at full price. Measured against Bedrock, turn 2 of a two-turn conversation reused zero cached tokens.

Injected text is now appended behind a cachePoint boundary that marks where the reusable prefix ends, and Bedrock's auto mode honors a boundary already present instead of relocating it to the end. Placement belongs to whoever appends the volatile content, so the agentic token-usage middleware shares the same primitive — its live <context-status> count previously stranded itself inside the cached prefix even with the injector fixed.

OpenAIModel now warns and skips a cachePoint inside a message rather than raising TypeError, matching its own system-prompt path and the Gemini/Vercel providers. Without it, a provider-agnostic caller cannot mark ephemeral content without special-casing OpenAI.

Related Issues

N/A

Documentation PR

Included

Type of Change

Bug fix

Testing

Verified end to end against real Bedrock (claude-sonnet-4-5), A/B against the pre-fix placement with a unique nonce per arm so no stale cache entry could flatter the result:

turn 2 cacheRead cacheWrite
before 0 3702
after 3645 19
  • I ran hatch run prepare

Risks / Callouts

This changes billing shape for users who never enabled caching. The boundary is honored by Bedrock and Anthropic even with no cache_config set, so ContextInjector now implies prompt caching on those providers. Bounds, measured:

The only loss case is a single-turn conversation, multi-turn always wins. I considered gating on a Model capability so the boundary is only emitted where the user opted in, but that forfeits the win for everyone who hasn't set cache_config — most users. Flagging it as the reviewable trade-off: if "no silent cost change" is a hard rule regardless of magnitude, the gate is the alternative.

Bedrock's _inject_cache_point behavior change is broader than the injector needs. It now honors a caller-placed cache point in the last user message (still stripping stale ones elsewhere, so the 4-cache-point cap can't be exhausted). That affects anyone hand-placing cache points, and an existing test asserting the old strip-everything behavior was updated.

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@opieter-aws
opieter-aws marked this pull request as ready for review August 4, 2026 21:01
@opieter-aws
opieter-aws requested a review from a team as a code owner August 4, 2026 21:01
@opieter-aws
opieter-aws requested a review from lizradway August 4, 2026 21:01
@github-actions github-actions Bot added python Pull requests that update python code area-context Session or context related area-model Related to models or model providers labels Aug 4, 2026
@github-actions github-actions Bot added bug Something isn't working size/l labels Aug 4, 2026
@codecov

codecov Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread strands-py/src/strands/types/content.py Resolved
Comment thread strands-ts/src/models/bedrock.ts Resolved
Comment thread strands-ts/src/types/messages.ts Resolved
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Assessment: Comment

Solid, well-reasoned fix — the root cause (per-call text landing inside the cached prefix) is correctly addressed by moving volatile content behind a shared cachePoint boundary, and the cross-SDK parity plus real-Bedrock cache-counter integ tests are excellent. My comments are minor except for one theme that needs a decision, not a code change.

Review themes
  • Behavior/billing contract (needs a call, not necessarily a change): As you flagged, ContextInjector now implies prompt caching on Bedrock/Anthropic even when the user never set cache_config. This is a change to a public behavior contract, and the "no silent cost change" question you raised is exactly the kind of trade-off the API bar-raising process exists to resolve. Recommend adding the api/needs-review label and getting an explicit reviewer/team sign-off recorded on this PR before merge — the analysis in your description is a strong starting point. I lean toward your "multi-turn always wins, only single-turn loses" reasoning, but the decision should be owned by an API reviewer.
  • Provider interaction: One correctness detail on the Bedrock honor branch dropping a configured TTL (inline).
  • Consistency / coverage / docs: Cross-SDK parity on tracking_id, a direct unit test for the new shared primitive, and two doc typos (inline).

Nice work on the measurement rigor (nonce-per-arm A/B) and the thorough write-up of the trade-off.

Comment thread strands-py/src/strands/types/content.py Resolved
@opieter-aws
opieter-aws force-pushed the opieter-aws/context-injector-caching branch from a8dc17f to 767b13f Compare August 5, 2026 13:23
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Re-review of the latest push (767b13f) — thanks for the fast turnaround. All five points from the previous round are addressed, and I verified the Python changes locally (test_append_ephemeral_content.py 6/6 pass; Bedrock cache-point suite 21/21 pass).

Follow-up items resolved
  • ✅ Honored-boundary TTL — both bedrock.py and bedrock.ts now apply the configured cache_config/messagesTTL to a caller-placed boundary, and correctly leave a hand-placed TTL untouched. Nice touch adding the keeps a hand-placed ttl edge-case test in both SDKs.
  • ✅ tracking_id parity — Python now carries tracking_id through, matching TS; covered by preserves identity fields.
  • ✅ Direct primitive tests — _append_ephemeral_content / appendEphemeralContent now pinned directly in both SDKs (empty-blocks, empty-content, boundary-reuse, no-mutation, identity), independent of the callers.
  • ✅ Docstring typos — fixed.

The only thing still open is not a code issue: the behavior/billing contract call (ContextInjector implying prompt caching without cache_config). That still needs an API-reviewer/maintainer sign-off — recommend adding the api/needs-review label so the decision is recorded before merge. Code-wise this LGTM.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview Ready

Your documentation preview has been successfully deployed!

Changed pages:

Updated at: 2026-08-05T13:29:31.419Z

@opieter-aws
opieter-aws requested review from arronbailiss and mkmeral and removed request for lizradway August 5, 2026 13:47
@opieter-aws

Copy link
Copy Markdown
Contributor Author

Putting back into draft - integration test is pointing at some token counting issues with caching, need to take a look there

@opieter-aws
opieter-aws marked this pull request as draft August 5, 2026 14:56
@github-actions github-actions Bot added size/xl and removed size/l labels Aug 5, 2026
@opieter-aws
opieter-aws marked this pull request as ready for review August 5, 2026 19:10
last_assistant_idx = i
for index, message in reversed(list(enumerate(messages))):
usage = message.get("metadata", {}).get("usage") if message.get("role") == "assistant" else None
if usage and not (usage.get("cacheReadInputTokens") or usage.get("cacheWriteInputTokens")):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue (Suggestion): The new cache-skip branch is the crux of this fix, but it's only exercised by the Bedrock-gated integration tests (skipped by default in CI). The fast TestEstimateInputTokens suite (test_event_loop.py) has sibling cases for cold-start, baseline-only, and baseline+delta, but none where an assistant usage carries cacheReadInputTokens/cacheWriteInputTokens. So the not (...) skip path can regress without any always-on test failing.

Suggestion: Add a unit case here (and the mirror in TS agent.test.ts _estimateInputTokens): a recent assistant message with cacheReadInputTokens set should be skipped, so the estimator either anchors on an earlier uncached assistant or falls through to the cold-start count path. That pins the branch independently of live Bedrock. The integ tests are great for the end-to-end contract; this just guards the logic cheaply.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Re-review of 8a08eea ("Fix token counting with cache points") — reviewed the follow-up you pushed after moving this back to draft. The diagnosis and fix look right.

Assessment of the token-counting fix
  • Correctness ✅ — Skipping assistant usage that carries cacheRead/cacheWriteInputTokens is the right move: once the cache warms, inputTokens no longer reflects the true prompt size, so anchoring the estimate on it would collapse the reported context and stop compression from firing. Anchoring on an earlier uncached turn keeps baseline+delta correct, and falling through to a full count_tokens when every turn is cached is accurate (just slightly costlier). Comment explains the "providers disagree" rationale well, and Python/TS stay in parity.
  • Regression tests ✅ — The warm-cache integ tests are exactly the right end-to-end guard, and the cacheReadInputTokens > 0 anti-vacuous assertion is a nice touch. Good call also fixing the existing "context grows" test to compare total context (inputTokens + cacheRead + cacheWrite) rather than raw inputTokens.
  • One gap (inline) — the cache-skip branch itself has no fast unit test; only the Bedrock-gated integ tests cover it. Suggested adding a unit case to the existing estimator suites in both SDKs.

No blockers from me on the code. Still in draft, so flagging lightly — and the earlier open item stands: the ContextInjector-implies-caching billing/behavior contract still warrants an api/needs-review sign-off before this leaves draft.

@mkmeral

mkmeral commented Aug 5, 2026

Copy link
Copy Markdown
Member

@strandly-the-agent review please

@mkmeral mkmeral left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall for caching, i'd like to have higher integ test count to catch issues (or maybe merge the logic to some other test). aside from that looks good to me


content = list(message["content"])
if content and not any("cachePoint" in block for block in content):
content.append({"cachePoint": {"type": "default"}})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rest of the model providers translate based on allowlist right? so new message should be ignored, if the model provider doesn't support?

if msg.get("role") == "user":
last_user_idx = msg_idx

# Strip cache points everywhere except the last user message, where auto mode would

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have an integ test for this?

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR — recommend changes before merge. The cache-prefix diagnosis is right and the nonce-per-arm A/B is the kind of evidence I wish every perf PR had. But the boundary is emitted for every provider with no gate, and I reproduced two regressions from that. One gate fixes both.

What I actually ran (branch pr3642 @ 8a08eea4, merge-base 0ab31d7c1):

  • 🔴 Agent(model=OllamaModel(...), context_manager="agentic") → TypeError: content_type=<cachePoint> at ollama.py:158, turn 1, no plugin, no special input. Same for gemini / writer / llamaapi / llamacpp / openai-responses; mistral silently drops the block. The identical script on main reaches the network instead.
  • 🔴 A last user turn of [text, document(csv)] + any injected content ships [text, document, cachePoint, text] — the adjacency bedrock.py:456-472 exists to prevent. It also happens with no cache_config at all, where _inject_cache_point never runs to relocate anything.
  • ✅ Suites green, pre-existing failures only: py 4408 passed / 2 failed (otel) / 28 errors (mantle-config); ts 3928 passed / 2 failed (otel env). test_bedrock.py 185/185.
  • ✅ I applied the honor-branch patch I suggest below and re-ran: invalid ordering gone, 185/185 still green — but the no-cache_config path stays broken, which is why I think the gate matters more than the patch.
  • ⚪ Not verified: the live Bedrock ValidationException itself (no creds here). That rests on this repo's own assertions at test_bedrock.py:3319+.

3 inline comments. mkmeral's question on content.py:312 is answered in the first one — short version: no, most providers don't ignore it, they raise.

Questions (the ones I can't settle for you)

1. Should a context-shaping feature turn on prompt caching? (blocking, and it's your call, not mine)

models/model.py:138 documents CacheConfig(strategy="auto") as "automatically detect model support and inject cachePoint" — that's the switch users know about, and types/content.py:311-312 now injects one without it. Verified: Bedrock + ContextInjector with zero cache_config goes from 0 cache points on main to 1 here; AnthropicModel converts unconditionally (anthropic.py:206-207, no cache config exists on that provider at all). There's no opt-out — ContextInjector.__init__ takes render_content, name, trigger.

Two bits of prior art worth weighing, both pointing the same way: team/DECISIONS.md:120 says pay-for-play doesn't cover "changes that affect default behavior", and in #449 Unshure declined auto-injecting a message cachePoint even when cache_tools was set, on the grounds that injected message cache points surprise customers. Your "multi-turn always wins" analysis may well carry the day — but if it does, I'd expect that to land as a documented default on CacheConfig plus a release note, rather than as a side effect of enabling an unrelated feature.

The capability gate you floated in the PR body is the option that closes all three of these at once (billing, the 6 crashing providers, the document-adjacency path with no cache_config), and it's cheaper than it looks: TS already has the predicate (_shouldEnableCaching(), bedrock.ts:476-496), py is an extract-method from bedrock.py:580-591, and both integ tests that assert the cache win already set cache_config=CacheConfig(strategy="auto") (test_context_injector_caching.py:66, :127) — so the measured win survives with no test edits.

2. Re your question on bedrock.py:426 (mkmeral) — yes to an integ test, but I'd sequence it after the fix below, otherwise it encodes the current ordering.

3. (non-blocking) The body describes commit 767b13fe5 only; 8a08eea43 ("Fix token counting with cache points") changes _estimate_input_tokens, which feeds proactive compression — worth a paragraph so a future bisector chasing a compression regression has a reason to look here. Related: that gate fixes a bug that was already reachable on main for anyone using cache_config (the new integ test at test_agentic_context_integration.py:115-155 uses no ContextInjector), so "pre-existing, but this PR makes it common" is probably the framing you want.

4. (non-blocking) Also missing the typescript label for 13 TS files, and the api/needs-review label the earlier reviews asked for — mentioning once, not re-litigating.

Appendix — non-blocking (6)
  • ⚪ 4-cache-point ceiling. The strip loop now skips the last user message and the honor branch returns after the first cachePoint, so extras there survive. Getting past Claude's cap of 4 needs ≥3 hand-placed points plus system + tool caching — plausible, not mainline. Worth knowing the margin is gone: two hand-placed now lands exactly at 4.
  • ⚪ Boundary-reuse degenerate shapes. A message seeded with content: [] gets no boundary, and Bedrock auto mode then appends one after the ephemeral text (cache write every call, never a read); a leading hand-placed cachePoint suppresses the real boundary so nothing is cacheable. Both need hand-seeded messages — contrived, noting for completeness.
  • ⚪ No delimiter between the ask and the appended text. Providers that join text blocks fuse them: mistral yields "What is the capital of France? <context-status>…</context-status> INJECTED". <context-status>/<memory> self-delimit; a RenderContentCallback returning plain text doesn't, and nothing in its docstring (_message_injection.py:29-38) says it now needs to. A \n\n prefix in the primitive would settle it.
  • ⚪ ephemeral is overloaded, inverted. anthropic.py:207 writes cache_control={"type": "ephemeral"} for content that is cached; the new primitive uses "ephemeral" for content that must not be cached, three lines from that code. It's private/@internal, so _append_per_call_content is a free rename if you agree (team/DECISIONS.md:168).
  • ⚪ AnthropicModel IndexError — formatted_contents[-1] (anthropic.py:203-207) blows up if every earlier block in the message was filtered out (location-sourced image + an appended boundary). Reproduces on main with a hand-placed cache point, so it's pre-existing, not yours — I'll file it separately. TS avoids it with a lookahead (anthropic.ts:377-388), which is probably the shape to port.
  • ⚪ TS coverage is thinner than py's here. No ts mirror for the stale-boundary-stripping test, and the new integ test at test/integ/agent.test.ts:159-178 constructs the model with no cacheConfig, so cacheRead/cacheWrite are trivially 0 and it passes whether or not 8a08eea43's fix exists. The python integ test does turn caching on — that asymmetry is worth closing given mkmeral's ask for more caching coverage.
  • ⚪ Trailing whitespace content.py:290, :295; stray word in messages.ts:967 ("injected context count").

Everything above is one agent's pass — worth a human check before you act on it, especially the API question.


content = list(message["content"])
if content and not any("cachePoint" in block for block in content):
content.append({"cachePoint": {"type": "default"}})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The boundary goes to every provider, and 6 of them raise on it.

mkmeral asked on this line whether providers translate on an allowlist and would just ignore an unsupported block. I checked all of them — mostly no:

what happens providers
converts it bedrock bedrock.py:637, anthropic anthropic.py:206
warns + skips openai openai.py:413 (this PR), litellm, sagemaker (inherit it)
raises TypeError gemini gemini.py:226, ollama ollama.py:158, writer writer.py:123, llamaapi llamaapi.py:130, llamacpp llamacpp.py:246, openai-responses openai_responses.py:728
silently drops mistral mistral.py:226-232 (if/elif, no else, no warning)

Reachable on turn 1 with no plugin and no unusual input, because create_token_usage_middleware (agent.py:414-419) has no provider check and _estimate_input_tokens runs on every model call:

Agent(model=OllamaModel(host="http://localhost:11434", model_id="llama3"), context_manager="agentic")
agent("What is the capital of France?")
→ TypeError: content_type=<cachePoint> | unsupported type   (ollama.py:158)

Same via ContextInjector and MemoryManager injection. The identical script on main gets to the network, so it's a regression, not a latent gap. TS is fine — its adapters have a generic default: warn+skip — which I suspect is where the PR body's "matching the Gemini/Vercel providers" came from; py Gemini raises.

Two ways out:

  • Gate the emission on a model capability — Model.wants_cache_boundary(), default False, BedrockModel returning _should_enable_caching(), threaded in as a keyword arg here. TS already has the predicate (bedrock.ts:476-496); py is an extract-method from bedrock.py:580-591. This also fixes the Bedrock document-adjacency finding and the no-cache_config billing question in one move.
  • Or make every provider tolerate it — e.g. a drop_unsupported_cache_points() helper alongside _has_location_source in models/_validation.py, wired into the 6 raisers plus Mistral's missing else. A cachePoint carries no content the model would miss, so warn-and-skip everywhere seems like the right SDK-wide rule; raising punishes the user for a marker the framework inserted.

Whichever you pick, context-injector.mdx:123 ("Providers without cache-point control ignore the marker") needs to become true — it's currently false for those 6. And there's no test anywhere on this: grep -c cachePoint is 0 in test_gemini.py, test_ollama.py, test_writer.py, test_llamaapi.py, test_llamacpp.py, test_mistral.py, test_openai_responses.py.

Comment on lines +445 to +450
placed = next((block for block in content if "cachePoint" in block), None)
if placed is not None:
if cache_config and cache_config.ttl and "ttl" not in placed["cachePoint"]:
placed["cachePoint"]["ttl"] = cache_config.ttl
logger.debug("msg_idx=<%s> | honored caller-placed cache point", last_user_idx)
return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Honoring the boundary skips the non-PDF-document rule this method already enforces below.

The return here lands before the relocation logic at bedrock.py:456-472 — the guard added by beddc3f7d / #2001 precisely because Bedrock rejects a cachePoint directly preceded by a non-PDF document. _append_ephemeral_content always appends at the tail, so a [text, document(csv)] turn plus any injected content produces exactly that shape. Verified on this head:

injector output:                      ['text', 'document', 'cachePoint', 'text']
after _inject_cache_point (auto):     ['text', 'document', 'cachePoint', 'text']   ← cachePoint at idx 2
control, no pre-existing boundary:    ['text', 'cachePoint', 'document']           ← correctly relocated

Reached by agent([{"text": "Analyze this file"}, {"document": {"format": "csv", ...}}]) with ContextInjector / MemoryManager / agentic mode — an ordinary "analyze this file" flow. Same in strands-ts/src/models/bedrock.ts:882-891.

This patch fixes it (I applied it and ran test_bedrock.py: 185/185, invalid ordering gone):

Suggested change
placed = next((block for block in content if "cachePoint" in block), None)
if placed is not None:
if cache_config and cache_config.ttl and "ttl" not in placed["cachePoint"]:
placed["cachePoint"]["ttl"] = cache_config.ttl
logger.debug("msg_idx=<%s> | honored caller-placed cache point", last_user_idx)
return
placed = next((block for block in content if "cachePoint" in block), None)
if placed is not None:
if cache_config and cache_config.ttl and "ttl" not in placed["cachePoint"]:
placed["cachePoint"]["ttl"] = cache_config.ttl
# Bedrock rejects a cache point directly preceded by a non-PDF document, so a
# caller-placed point follows the same relocation rule as one added below.
first_non_pdf_doc_idx = next(
(
block_idx
for block_idx, block in enumerate(content)
if "document" in block and block["document"].get("format", "") != "pdf"
),
None,
)
if first_non_pdf_doc_idx is not None and first_non_pdf_doc_idx < content.index(placed):
content.remove(placed)
if first_non_pdf_doc_idx == 0:
# A leading non-PDF document leaves no prefix to cache
logger.debug("msg_idx=<%s> | dropped cache point for leading document", last_user_idx)
return
content.insert(first_non_pdf_doc_idx, placed)
logger.debug("msg_idx=<%s> | relocated caller-placed cache point", last_user_idx)
return
logger.debug("msg_idx=<%s> | honored caller-placed cache point", last_user_idx)
return

But the patch isn't sufficient, and this is the part I'd weigh when deciding on the capability gate: with no cache_config at all, _inject_cache_point never runs, yet the injector's boundary still reaches the wire — the invalid ['text', 'document', 'cachePoint', 'text'] payload happens anyway, with the patch applied. Verified:

BedrockModel(model_id=...)  # no cache_config
format_request([injected_message]) → ['text', 'document', 'cachePoint', 'text']

So either the boundary is only emitted where the model asked for caching (the gate), or the placement rule has to move somewhere that runs unconditionally. A test that pins it: test_inject_cache_point_honored_boundary_does_not_land_after_a_non_pdf_document, asserting "cachePoint" not in content[doc_idx + 1] — it fails against the current code, which is the evidence. Worth mirroring in TS.

last_assistant_idx = i
for index, message in reversed(list(enumerate(messages))):
usage = message.get("metadata", {}).get("usage") if message.get("role") == "assistant" else None
if usage and not (usage.get("cacheReadInputTokens") or usage.get("cacheWriteInputTokens")):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The gate is provider-blind, and for the OpenAI family it makes the estimate worse.

Different point from the existing thread on this line (that one asks for a unit test — still worth doing; this is about the logic).

The premise — inputTokens excludes the cached portion — holds for Bedrock/Anthropic, and the gate is right there. It's inverted for the OpenAI family: prompt_tokens includes cached tokens and cacheReadInputTokens is a subset of it (openai.py:596-597, openai_responses.py:888-896, litellm.py:287-295, gemini.py:479-487). So for those providers the gate throws away the one accurate number available and falls through to count_tokens, which has no image/document branch (model.py:39-86 — deliberately, per its own comment). On a vision-heavy conversation:

provider-reported prompt size:  26,000
before this commit:             26,408   (1.6% off)
after:                              17   (99.9% off)
proactive compression at 0.85 of a 30k window: fires before → does not fire after

Reachable without any of this PR's features: OpenAI caches server-side automatically above ~1024 prompt tokens, so any multi-turn OpenAIModel conversation reports cacheReadInputTokens and lands in the fallthrough. The consequence is bounded — a wrong number feeding compression and the <context-status> text the model reads, not a hard failure — hence 🟡.

Second-order, worth a sentence in the comment either way: once a boundary is emitted by default, every turn carries cacheWrite or cacheRead, so the baseline+delta optimization never engages again for the life of a cache-enabled conversation — you're doing a full recount per model call. Cheap today (local heuristic), a network call per attempt with use_native_token_count=True.

Suggest keying the skip on whether the provider's inputTokens is net-of-cache rather than on the presence of cache counters — the comment already concedes providers disagree, and that disagreement is knowable per provider.

This branch had an error being deployed

1 failed and 1 inactive (outdated) deployments
manual-approval — 8a08eea4 Deployed Aug 5, 2026 by opieter-aws via Build and Deploy Preview #1034
auto-approve — 767b13fe Deployed Aug 5, 2026 by opieter-aws via Build and Deploy Preview #1019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-context Session or context related area-model Related to models or model providers bug Something isn't working python Pull requests that update python code size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants