Skip to content

feat(cache): wire prompt caching, the largest available cost reduction - #14

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-541-prompt-caching
Aug 2, 2026
Merged

feat(cache): wire prompt caching, the largest available cost reduction#14
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-541-prompt-caching

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Why now

EnablePromptCache has been a struct field that does nothing since Phase 0, with this comment explaining it:

the current langchaingo/llms/anthropic adapter doesn't expose cache_control yet

That stopped being true. langchaingo v0.1.14 — already in go.mod ships llms.WithCacheControl / llms.CachedContent, and its Anthropic adapter emits cache_control on human content blocks (anthropicllm.go:327-353). The comment outlasted the constraint it described, and with it our biggest available saving. No new dependency, no native HTTP client, so this was never blocked on HAL-539.

What it's worth

Measured against live rates, 120k-token prompt with a 100k stable prefix:

model cold warm
glm-4.6 $0.0738 $0.0248 −66%
claude-sonnet-4-5 $0.3720 $0.1020 −73%
gpt-4o $0.3388 $0.1888 −44%

That's the shape of every vectorless tree navigation: a large document resent each call, then a short question.

API

Message.CacheBreakpoint marks the end of a cacheable prefix. anthropic.Config.EnablePromptCache stays as a shorthand that caches the first user message, for the common "big stable block first" case.

Two constraints enforced, not just documented

The marker only goes on user turns. langchaingo's handleSystemMessage takes a bare TextContent and returns ErrInvalidContentType for anything else — so marking a system message wouldn't cache it, it would fail every request. Assistant and tool turns are excluded for a different reason: a breakpoint belongs at the end of a stable prefix, and those turns are what varies.

JSON mode would have silently broken. It appended its nudge by type-asserting the last part to TextContent; a cached part isn't one, so the nudge would have vanished and JSON mode become a no-op whenever caching was on. It now lands as its own part outside the cached block — which is also correct, since the nudge carries a per-call schema and changing the tail of a cached prefix invalidates it every request.

The shorthand defers to the caller: an explicit breakpoint anywhere leaves the request untouched (a second breakpoint would cache a prefix nobody chose, and providers cap how many a request may carry). The slice is copied rather than marked in place, because Request.Messages belongs to the caller and a retry may hand the same backing array back.

Cost accounting

No change needed — providers meter caching in the usage they return, and HAL-529 already reads CacheWrite/CacheRead and prices them at their own rates. So the saving shows up in Response.Usage.CostUSD automatically.

Tests

Seven new cases in internal/adapter/adapter_cache_test.go covering the marker, the system/assistant/tool guard, shorthand placement, caller deference, the no-mutation guarantee, and the JSON-nudge interaction from both sides (nudge present, and outside the cached block).

Not included

prompt_cache_min_tokens — caching below the provider minimum (1024 tokens on most Anthropic models) silently does nothing. Documented on the field; reading it from the feed is tracked in HAL-565.

Closes HAL-541

Summary by Sourcery

Add configurable prompt caching support to reduce repeated prompt processing costs while preserving JSON-mode behavior and caller control.

New Features:

  • Introduce Message.CacheBreakpoint to mark the end of a cacheable user-prefix in requests.
  • Add adapter-level prompt cache shorthand that can automatically cache the first user message when enabled via provider config.
  • Enable Anthropic provider configuration flag to turn on prompt caching for its requests.

Enhancements:

  • Wire prompt caching through the adapter layer using cache-control wrappers while keeping non-caching providers compatible.
  • Adjust JSON-mode nudge handling so it coexists correctly with cached content without breaking schema enforcement.
  • Ensure adapter does not mutate caller-owned message slices when applying prompt cache shorthands.

Tests:

  • Add adapter_cache tests covering cache breakpoint behavior, role restrictions, shorthand interactions, slice immutability, and JSON-mode integration.

EnablePromptCache has been a struct field that did nothing since Phase 0,
documented as waiting on langchaingo to expose cache_control. That is no
longer true: v0.1.14, already in go.mod, ships llms.WithCacheControl and
its Anthropic adapter emits cache_control on human content blocks. The
comment outlasted the constraint it described, and with it the biggest
saving available to us.

Measured against live rates on a 120k-token prompt whose first 100k are
a stable prefix: glm-4.6 falls 66%, claude-sonnet-4-5 73%, gpt-4o 44%
per warm hop. That is the shape of every vectorless tree navigation —
a large document resent on each call, followed by a short question.

Message.CacheBreakpoint marks the end of a cacheable prefix. Anthropic
bills a cache read at a tenth of the input rate, so the saving is
roughly proportional to how much of the prompt is stable.

Two constraints are enforced rather than documented and hoped for.

The marker only goes on user turns. langchaingo's system handler takes a
bare TextContent and returns ErrInvalidContentType for anything else, so
marking a system message would not cache it — it would fail every
request. Assistant and tool turns are excluded for a different reason: a
breakpoint belongs at the end of a stable prefix, and those turns are
part of what varies.

JSON mode appended its nudge by type-asserting the last part to
TextContent. A cached part is not one, so the nudge would have been
dropped silently and JSON mode would have become a no-op whenever
caching was on. It now lands as its own part, outside the cached block,
which is also where it belongs: the nudge carries a per-call schema, and
changing the tail of a cached prefix invalidates it on every request.

EnablePromptCache becomes a shorthand for the common shape, caching the
first user message. It defers to the caller — an explicit breakpoint
anywhere leaves the request untouched, since adding a second would cache
a prefix nobody chose and providers cap how many a request may carry.
The message slice is copied rather than marked in place, because
Request.Messages belongs to the caller and a retry may hand the same
backing array back.

Cost accounting needed no change: providers meter caching in the usage
they return, and HAL-529 already reads CacheWrite and CacheRead and
prices them at their own rates.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ab890b0-54f2-4cbd-95df-a67dab903bcf

📥 Commits

Reviewing files that changed from the base of the PR and between 0c4550f and f34666a.

📒 Files selected for processing (4)
  • client.go
  • internal/adapter/adapter.go
  • internal/adapter/adapter_cache_test.go
  • provider/anthropic/anthropic.go

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR wires provider-level prompt caching into the Anthropic adapter and core request/adapter flow, adds an explicit cache breakpoint API on messages, and ensures JSON mode and caller-owned message slices interact correctly with caching while being covered by targeted tests.

Sequence diagram for Complete with prompt cache and JSON nudge

sequenceDiagram
    participant Caller
    participant AnthropicConfig
    participant AnthropicProvider as Anthropic.New
    participant Adapter
    participant AdapterHelpers as withPromptCache
    participant Langchain as toLangchainMessages

    Caller->>AnthropicConfig: set EnablePromptCache
    AnthropicConfig->>AnthropicProvider: cfg
    AnthropicProvider->>Adapter: NewAdapter(...)
    AnthropicProvider->>Adapter: SetPromptCache(cfg.EnablePromptCache)

    Caller->>Adapter: Complete(ctx, req)
    Adapter->>AdapterHelpers: withPromptCache(req.Messages, promptCache)
    AdapterHelpers-->>Adapter: msgsWithBreakpoint
    Adapter->>Langchain: toLangchainMessages(msgsWithBreakpoint, req.JSONMode, req.JSONSchema)
    Langchain->>Langchain: cacheable(TextContent, Message)
    alt JSONMode
        Langchain->>Langchain: append JSON nudge
    end
    Langchain-->>Adapter: []llms.MessageContent
    Adapter-->>Caller: *llmgate.Response
Loading

File-Level Changes

Change Details Files
Introduce adapter-level prompt caching control and apply it when translating requests to langchaingo messages.
  • Add a promptCache field to Adapter and a SetPromptCache toggle used by providers
  • Wrap request messages with withPromptCache before converting to langchaingo messages in Complete
  • Implement cacheable helper to wrap user message content parts in llms.WithCacheControl when CacheBreakpoint is set
internal/adapter/adapter.go
Add a cache breakpoint API on Message and document how providers account for caching.
  • Extend Message with a CacheBreakpoint boolean indicating end of cacheable prefix
  • Document usage constraints, provider behavior, and minimum cacheable length in the Message comment
client.go
Wire Anthropic Config.EnablePromptCache into adapter construction as a shorthand for caching the first user message.
  • Update Anthropic Config comment to describe EnablePromptCache semantics and interaction with explicit CacheBreakpoint
  • Call Adapter.SetPromptCache with cfg.EnablePromptCache in provider.New
provider/anthropic/anthropic.go
Ensure JSON-mode nudge is preserved and correctly placed when the last human part is cached, and cover caching behavior with tests.
  • Update toLangchainMessages to use cacheable for user content and to handle llms.CachedContent when appending JSON-mode nudge, placing nudge as a separate uncached part
  • Add adapter_cache_test.go with tests for cache breakpoint role restrictions, shorthand behavior, slice immutability, and JSON nudge interaction with cached tails
internal/adapter/adapter.go
internal/adapter/adapter_cache_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hallelx2
hallelx2 merged commit c84ce26 into main Aug 2, 2026
7 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-541-prompt-caching branch August 2, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant