Skip to content

feat(agent): mark Bedrock Converse prompts for caching behind a flag [PC-5190] - #1142

Merged
tudormatei1 merged 11 commits into
mainfrom
feat/bedrock-prompt-caching
Nov 5, 2026
Merged

tudormatei1 merged 11 commits into
mainfrom
feat/bedrock-prompt-caching

Conversation

@tudormatei1

@tudormatei1 tudormatei1 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Low-code and conversational agents never send prompt cache markers, so their Claude calls on Bedrock almost never hit the cache. The LLM Gateway team measured 1.3% for agentsruntimefallback and 0% for agentsplaygroundfallback in September, on roughly $120k of spend, and estimates $25-56k a month in savings. Advanced agents aren't affected: deepagents already adds its own caching middleware.

Callers turn it on with AgentGraphConfig.prompt_caching, which create_agent passes to the LLM node the same way as strict_mode and parallel_tool_calls. uipath-agents sets it from the EnableBedrockPromptCaching flag (UiPath/uipath-agents-python#804); this repo doesn't read the flag itself. When it's on, the node resolves handler.get_prompt_caching_kwargs() once when it's built and _invoke_model merges the result into the bind_tools kwargs, and for Claude and Nova the Bedrock Converse handler returns cache_control, which langchain-aws turns into cachePoint blocks after the tools, the system prompt and the last message (3 of Bedrock's 4). LLMCall.cacheable (default True) is False for the forced-extraction retry and the AutoToolChoice finish call, since their prefix differs from the run's turns (thinking stripped, or a reduced tool list) and a cache write there is never read.

The Bedrock Converse handler only returns cache_control for Claude and Nova, the model families langchain-aws's BedrockPromptCachingMiddleware treats as cache-capable, and never for BYO deployments. The check reads model_id; langchain-aws also looks at base_model_id, but the client only sets that for BYO entries, which are skipped anyway. It looks the model up in discovery with its byo_connection_id (the list is already cached, so no extra calls) and skips caching unless modelSubscriptionType is UiPathOwned. An entry without a subscription type gets no markers. That covers BYO replacements selected by name, which are listed under the UiPath model name, BYO additions and explicit connections. A failed lookup turns caching off. Custom BYO gateways can rewrite the Converse body, and the rollout targets UiPath-billed traffic. For UiPath-owned agents traffic this changes nothing today, since every Bedrock model the agents Model Hub serves is Claude 4.x/5.x (older ids are routed to Sonnet 4.5), but it keeps a 400 off the table for BYO Llama/Mistral or a future non-Claude model. Other handlers return {}. The node holds no provider logic: it knows whether caching is on and which calls reuse their prefix, and the handler decides how to mark it. Adding another provider later (Claude on Vertex through AnthropicPayloadHandler takes the same cache_control kwarg) is one handler override. The BYO check stays private to the Bedrock handler until a second handler needs it.

Things to know:

  • prompt_caching defaults to off, so coded agents and other callers are unchanged. The flag is created for agents on alpha in UiPath/gitops-centralized-cluster#600765. UiPath/uipath-agents-python#804 reads it and sets the field, and has to ship with the agents pin bump to this release, since the field doesn't exist before it.
  • No routing change is needed: the gateway calls Bedrock through cross-region inference profiles, so AWS picks the region that holds the cache.
  • Haiku 4.5 and Opus 4.5-4.7 only cache prefixes of 4,096+ tokens, so small agents on those models won't see a difference.
  • BYO replacements show up as BYO in discovery, so they're excluded. If the gateway ever redirects a model to BYO without listing it that way in discovery, the client couldn't tell; the Pepgenx BYO mapper is the route known to rewrite the markers.

Testing: the handler tests cover the model gate (Claude via a us. profile, Nova, Llama) and the BYO gate, building the model through get_chat_model with only discovery patched (UiPath-owned, an entry without modelSubscriptionType, a BYO replacement, a BYO addition by ARN, an explicit BYO connection next to a UiPath-owned entry of the same name, a failed lookup). The LLMNode tests drive a real ChatBedrockConverse with a mocked boto client and count cachePoint blocks in the request: 3 on each of two turns with prompt_caching on and 0 with it off, 3 on a conversational turn, 0 on the extraction and finish calls, and no cache_control for a non-Bedrock model. A create_agent test checks that AgentGraphConfig.prompt_caching reaches the bound model. Each check was confirmed by breaking the code it guards (setting dropped at each step from the config to every node class, cacheable ignored, extraction or finish call cached, BYO check removed, lookup failure cached, Nova dropped, subscription ignored, connection id left out of the lookup). Full suite: 4136 passed, 3 skipped. mypy and ruff are clean.

Live check on alpha (this branch plus UiPath/uipath-agents-python#804 run locally): a low-code agent on Claude Sonnet 4.5 (Bedrock) with a 3.3k-token prompt, 5 runs per row, starting from a cold cache. Cost is input tokens weighted by Bedrock's cache pricing (reads 0.1x, writes 1.25x), relative to the same runs with the flag off.

Flag Questions Input read from cache Input cost
off different each run 0% 100%
on different each run 87% after the first run 125% first run, then 25%
on the same each run 99% 11%

Every warm run hit the cache, and the cached token counts were identical from run to run. A new question still re-writes about 400 tokens beyond the question itself, which looks like the tool definitions, so a new input settles at 25% rather than ~11%. LLM latency didn't change at this prompt size. Not covered live: caching across several LLM calls in one run, and BYO models through the gateway.

Version bumped to 0.19.2 (main is on 0.19.1).

@tudormatei1
tudormatei1 force-pushed the feat/bedrock-prompt-caching branch 4 times, most recently from 50c8a4d to 8c31d27 Compare October 14, 2026 13:32
@tudormatei1 tudormatei1 changed the title feat(agent): mark Bedrock Converse prompts for caching behind a flag feat(agent): mark Bedrock Converse prompts for caching behind a flag [PC-5190] Oct 15, 2026
@tudormatei1
tudormatei1 marked this pull request as ready for review October 15, 2026 10:56
Copilot AI balanced review requested due to automatic review settings October 15, 2026 10:56

Copilot AI 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.

🟡 Changes recommended

Legacy Bedrock clients bypass the BYO exclusion and receive cache markers when caching is enabled.

1 open finding
What changed in this PR

Adds opt-in Bedrock prompt caching to agent LLM calls, with provider eligibility checks and exclusions for non-reusable calls.

Changes:

  • Threads prompt_caching from agent configuration into LLM nodes.
  • Adds Claude/Nova cache markers and BYO eligibility checks.
  • Adds regression tests and bumps the version to 0.18.40.
File Description
uv.lock Updates package version.
tests/​chat/​test_bedrock_payload_handler.py Tests provider and ownership eligibility.
tests/​agent/​react/​test_llm_node.py Tests cache markers and excluded calls.
tests/​agent/​react/​test_create_agent.py Tests configuration propagation.
src/​uipath_langchain/​chat/​handlers/​bedrock.py Adds Bedrock caching eligibility.
src/​uipath_langchain/​chat/​handlers/​base.py Adds default caching hook.
src/​uipath_langchain/​agent/​react/​types.py Adds opt-in configuration field.
src/​uipath_langchain/​agent/​react/​llm_node.py Applies caching to eligible calls.
src/​uipath_langchain/​agent/​react/​agent.py Forwards caching configuration.
pyproject.toml Bumps version to 0.18.40.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +79 to +81
settings = getattr(model, "client_settings", None)
if settings is None:
return False
}


def _is_byo_model(model: Any) -> bool:

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.

Is checking the subscription not sufficient here? Why are we checking multiple fields in order?

@tudormatei1 tudormatei1 Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is. Simplified in 153edc0: the lookup now passes byo_connection_id, and BYO means modelSubscriptionType != "UiPathOwned". The gateway always sets byomDetails together with the subscription type, so that check never changed the result.

@tudormatei1
tudormatei1 force-pushed the feat/bedrock-prompt-caching branch 2 times, most recently from 153edc0 to e53ed5b Compare November 4, 2026 10:03
tudormatei1 and others added 10 commits November 5, 2026 15:22
Low-code and conversational agents never send prompt cache markers, so their
Claude calls on Bedrock almost never hit the cache (the LLM Gateway reports
1.3% for agentsruntimefallback and 0% for agentsplaygroundfallback).

With EnableBedrockPromptCaching on, each LLM turn passes cache_control to
bind_tools, and langchain-aws places cachePoint blocks after the tools, the
system prompt and the last message. LLMCall carries a required cache_prompt:
the normal turn of every node caches, while the forced-extraction retry and
the AutoToolChoice finish call don't, since their prefix differs from the
run's turns and a write there is never read.

The Bedrock Converse handler only returns cache_control for Claude and Nova,
the models that accept checkpoints (the same check as langchain-aws's own
caching middleware), and never for BYO connections. Other providers return
nothing.
Discovery can resolve a BYO-only model name without a connection id. The
client then sets base_model_id from the BYO deployment's model, and since the
gate read base_model_id as the model to check, a BYO Claude still got cache
markers. UiPath-owned models never set base_model_id, so it now counts as a
BYO signal next to byo_connection_id, and the model check reads model_id.

The handler gate gets its own tests (Claude, us. inference profiles, Nova,
non-caching families, BYO on the real UiPathChatBedrockConverse). The node
tests now count checkpoints across the whole request and check their
content, and pass a mocked bedrock_client so building the model no longer
probes AWS for credentials.
The gate inferred BYO from byo_connection_id or base_model_id, but the
client only sets base_model_id for BYO additions whose customerModel parses
as provider.model. A BYO replacement is listed under the UiPath model name
with no customerModel, and agents select it by name, so it still got cache
markers; so did additions backed by an inference profile ARN.

The handler now reads the model's discovery entry through its client
settings (cached, no extra I/O) and treats it as BYO when byomDetails is set
or modelSubscriptionType isn't UiPathOwned, the same signals the client uses
to prefer UiPath-owned entries. A failed lookup turns caching off. The tests
build models through get_chat_model with only discovery patched.

Version bumped to 0.18.36; main already released 0.18.35.
The LLM node now only says whether a call's prefix is reused
(cache_prompt). The Bedrock Converse handler reads
EnableBedrockPromptCaching and decides whether markers apply, so a
second provider is one handler override with its own flag. The BYO
check moves to the handlers' base module since it only reads discovery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The caching switch now arrives the same way as strict_mode and
parallel_tool_calls: AgentGraphConfig.prompt_caching, create_agent,
create_llm_node, the node. The caller reads the flag; uipath_langchain
no longer touches the registry for it, and the Bedrock handler only
decides whether the model takes cache markers.

The per-call field is renamed to LLMCall.cacheable and defaults to
True, so only the extraction retry and the finish call mention it.
The handler's BYO check goes through get_model_info, which logs an INFO
line under uipath run whenever no resource overwrite matches. The model
never changes for a node, so the kwargs are now resolved when the node is
built instead of on every turn. prompt_caching moves after tool_choice in
ConversationalLLMNode so existing positional calls keep binding the same.

Adds the tests the review found missing: an explicit BYO connection next
to a UiPath-owned entry of the same name, an entry without
modelSubscriptionType, and a non-Bedrock model with caching on.
Its fail-closed rule (a failed discovery lookup counts as BYO) is a
caching decision, not a general BYO test, and Bedrock Converse is the
only caller.
LLM Gateway discovery always sets modelSubscriptionType and byomDetails
together, so byomDetails never changed the answer. The explicit
connection check now comes from the lookup itself: get_model_info with
the model's byo_connection_id returns the BYO entry instead of a
UiPath-owned entry with the same name. An entry without a subscription
type now counts as BYO, so it gets no cache markers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tudormatei1
tudormatei1 force-pushed the feat/bedrock-prompt-caching branch from e53ed5b to 84f1246 Compare November 5, 2026 13:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@tudormatei1
tudormatei1 merged commit 4d820e8 into main Nov 5, 2026
47 checks passed
@tudormatei1
tudormatei1 deleted the feat/bedrock-prompt-caching branch November 5, 2026 13:45
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.

3 participants