[PR #551] [MERGED] fix(deepagents): gate cache_control writes on per-call request.model #554

Closed
opened 2026-06-05 17:23:40 -04:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/551
Author: @antonnak
Created: 5/22/2026
Status: Merged
Merged: 6/2/2026
Merged by: @hntrl

Base: mainHead: fix/cache-control-fallback-leak


📝 Commits (2)

  • afcbce9 fix(deepagents): gate cache_control writes on per-call request.model
  • c24e5f4 fix(deepagents): widen isAnthropicModel to accept RunnableInterface

📊 Changes

6 files changed (+204 additions, -5 deletions)

View changed files

.changeset/cache-control-fallback-leak.md (+13 -0)
📝 libs/deepagents/src/middleware/cache.test.ts (+115 -3)
📝 libs/deepagents/src/middleware/cache.ts (+14 -0)
📝 libs/deepagents/src/middleware/memory.test.ts (+41 -0)
📝 libs/deepagents/src/middleware/memory.ts (+11 -1)
📝 libs/deepagents/src/utils.ts (+10 -1)

📄 Description

Fixes #550.

Problem

createCacheBreakpointMiddleware and createMemoryMiddleware write Anthropic-specific cache_control: { type: "ephemeral" } markers into the request payload. Both gate the write at agent-creation time in createDeepAgent (whether the primary model is Anthropic). When modelFallbackMiddleware swaps request.model to a non-Anthropic provider at request time, the markers leak through and the fallback provider rejects the request:

400 Unknown parameter: 'messages[0].content[N].cache_control'

Full root-cause writeup, production trace, and reproduction in #550.

Fix

Move the model classification from boot-time to per-call, using the existing isAnthropicModel helper (src/utils.ts:9).

  • src/middleware/cache.ts — bail at the top of wrapModelCall when request.model isn't Anthropic.
  • src/middleware/memory.ts — AND the existing addCacheControl flag with the same per-call check. addCacheControl keeps its current public meaning (opt-in switch); the per-call gate is an additional safety net.
  • src/agent.ts — left untouched. The boot-time install gate stays (small perf win when primary is non-Anthropic — middleware isn't installed at all). The new per-call check handles the fallback case where the middleware is installed but the model has been swapped.

Tests

Each middleware's .test.ts got a per-call provider gating block. They build a request with a ChatAnthropic-shaped stub vs a ChatOpenAI-shaped stub on request.model, run wrapModelCall, and assert cache_control is/isn't written. Also covers ConfigurableModel with modelProvider: "anthropic" and the string forms ("anthropic:claude-...", "openai:gpt-5").

Existing fixtures that didn't set request.model now pass a ChatAnthropic stub so they keep exercising the write path — they were previously relying on the unconditional behavior. No behavioral assertions changed; only the fixture got more explicit.

31/31 tests pass on libs/deepagents (cache.test.ts + memory.test.ts).

Compatibility

  • No public API change. addCacheControl?: boolean on MemoryMiddlewareOptions keeps its existing semantics.
  • Anthropic-primary happy path unchanged — same writes, same cache hits.
  • Per-call cost: one getName() / string check inside wrapModelCall. Sub-microsecond.
  • Failure mode improvement: users running createDeepAgent with an Anthropic primary + non-Anthropic fallback no longer get a 400 from the fallback provider on swap.

Changeset

patch bump on deepagents.

Out of scope

request.modelSettings.cache_control (written by LangChain core's anthropicPromptCachingMiddleware) can also leak across the swap. Mentioned in #550 for tracking — that's a LangChain-side fix, not deepagents.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/deepagentsjs/pull/551 **Author:** [@antonnak](https://github.com/antonnak) **Created:** 5/22/2026 **Status:** ✅ Merged **Merged:** 6/2/2026 **Merged by:** [@hntrl](https://github.com/hntrl) **Base:** `main` ← **Head:** `fix/cache-control-fallback-leak` --- ### 📝 Commits (2) - [`afcbce9`](https://github.com/langchain-ai/deepagentsjs/commit/afcbce9bce1c3c5caf21b0511b299cdc72788bea) fix(deepagents): gate cache_control writes on per-call request.model - [`c24e5f4`](https://github.com/langchain-ai/deepagentsjs/commit/c24e5f43e5a6b488428810ea1f1b7300df07c128) fix(deepagents): widen isAnthropicModel to accept RunnableInterface ### 📊 Changes **6 files changed** (+204 additions, -5 deletions) <details> <summary>View changed files</summary> ➕ `.changeset/cache-control-fallback-leak.md` (+13 -0) 📝 `libs/deepagents/src/middleware/cache.test.ts` (+115 -3) 📝 `libs/deepagents/src/middleware/cache.ts` (+14 -0) 📝 `libs/deepagents/src/middleware/memory.test.ts` (+41 -0) 📝 `libs/deepagents/src/middleware/memory.ts` (+11 -1) 📝 `libs/deepagents/src/utils.ts` (+10 -1) </details> ### 📄 Description Fixes #550. ## Problem `createCacheBreakpointMiddleware` and `createMemoryMiddleware` write Anthropic-specific `cache_control: { type: "ephemeral" }` markers into the request payload. Both gate the write at **agent-creation time** in `createDeepAgent` (whether the *primary* model is Anthropic). When `modelFallbackMiddleware` swaps `request.model` to a non-Anthropic provider at request time, the markers leak through and the fallback provider rejects the request: ``` 400 Unknown parameter: 'messages[0].content[N].cache_control' ``` Full root-cause writeup, production trace, and reproduction in #550. ## Fix Move the model classification from boot-time to **per-call**, using the existing `isAnthropicModel` helper (`src/utils.ts:9`). - **`src/middleware/cache.ts`** — bail at the top of `wrapModelCall` when `request.model` isn't Anthropic. - **`src/middleware/memory.ts`** — AND the existing `addCacheControl` flag with the same per-call check. `addCacheControl` keeps its current public meaning (opt-in switch); the per-call gate is an additional safety net. - **`src/agent.ts`** — left untouched. The boot-time install gate stays (small perf win when primary is non-Anthropic — middleware isn't installed at all). The new per-call check handles the fallback case where the middleware *is* installed but the model has been swapped. ## Tests Each middleware's `.test.ts` got a `per-call provider gating` block. They build a request with a `ChatAnthropic`-shaped stub vs a `ChatOpenAI`-shaped stub on `request.model`, run `wrapModelCall`, and assert `cache_control` is/isn't written. Also covers `ConfigurableModel` with `modelProvider: "anthropic"` and the string forms (`"anthropic:claude-..."`, `"openai:gpt-5"`). Existing fixtures that didn't set `request.model` now pass a `ChatAnthropic` stub so they keep exercising the write path — they were previously relying on the unconditional behavior. No behavioral assertions changed; only the fixture got more explicit. 31/31 tests pass on `libs/deepagents` (`cache.test.ts` + `memory.test.ts`). ## Compatibility - **No public API change.** `addCacheControl?: boolean` on `MemoryMiddlewareOptions` keeps its existing semantics. - **Anthropic-primary happy path unchanged** — same writes, same cache hits. - **Per-call cost:** one `getName()` / string check inside `wrapModelCall`. Sub-microsecond. - **Failure mode improvement:** users running `createDeepAgent` with an Anthropic primary + non-Anthropic fallback no longer get a 400 from the fallback provider on swap. ## Changeset `patch` bump on `deepagents`. ## Out of scope `request.modelSettings.cache_control` (written by LangChain core's `anthropicPromptCachingMiddleware`) can also leak across the swap. Mentioned in #550 for tracking — that's a LangChain-side fix, not deepagents. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-06-05 17:23:40 -04:00
yindo closed this issue 2026-06-05 17:23:40 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#554