fix(shared): restrict cached Anthropic 1M context to GA 4.6 models only

This commit is contained in:
YeonGyu-Kim
2026-03-25 14:29:59 +09:00
parent 719a58270b
commit bf804b0626
3 changed files with 137 additions and 48 deletions
@@ -135,9 +135,96 @@ describe("context-window-monitor modelContextLimitsCache", () => {
})
})
describe("#given Anthropic provider with cached context limit and 1M mode disabled", () => {
describe("#given Anthropic 4.6 provider with cached context limit and 1M mode disabled", () => {
describe("#when cached usage is below threshold of cached limit", () => {
it("#then should respect the cached limit and skip the reminder", async () => {
// given
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-sonnet-4-6", 500000)
const hook = createContextWindowMonitorHook({} as never, {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
const sessionID = "ses_anthropic_cached_limit_respected"
await hook.event({
event: {
type: "message.updated",
properties: {
info: {
role: "assistant",
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
finish: true,
tokens: {
input: 150000,
output: 0,
reasoning: 0,
cache: { read: 10000, write: 0 },
},
},
},
},
})
// when
const output = createOutput()
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
// then — 160K/500K = 32%, well below 70% threshold
expect(output.output).toBe("original")
})
})
describe("#when cached usage exceeds threshold of cached limit", () => {
it("#then should use the cached limit for the reminder", async () => {
// given
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-sonnet-4-6", 500000)
const hook = createContextWindowMonitorHook({} as never, {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
const sessionID = "ses_anthropic_cached_limit_exceeded"
await hook.event({
event: {
type: "message.updated",
properties: {
info: {
role: "assistant",
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
finish: true,
tokens: {
input: 350000,
output: 0,
reasoning: 0,
cache: { read: 10000, write: 0 },
},
},
},
},
})
// when
const output = createOutput()
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
// then — 360K/500K = 72%, above 70% threshold, uses cached 500K limit
expect(output.output).toContain("context remaining")
expect(output.output).toContain("500,000-token context window")
})
})
})
describe("#given older Anthropic provider with cached context limit and 1M mode disabled", () => {
describe("#when cached usage would only exceed the incorrect cached limit", () => {
it("#then should ignore the cached limit and use the 200K default", async () => {
// given
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-sonnet-4-5", 500000)
@@ -146,7 +233,7 @@ describe("context-window-monitor modelContextLimitsCache", () => {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
const sessionID = "ses_anthropic_cached_limit_respected"
const sessionID = "ses_anthropic_older_model_ignores_cached_limit"
await hook.event({
event: {
@@ -173,51 +260,9 @@ describe("context-window-monitor modelContextLimitsCache", () => {
const output = createOutput()
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
// then — 160K/500K = 32%, well below 70% threshold
expect(output.output).toBe("original")
})
})
describe("#when cached usage exceeds threshold of cached limit", () => {
it("#then should use the cached limit for the reminder", async () => {
// given
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-sonnet-4-5", 500000)
const hook = createContextWindowMonitorHook({} as never, {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
const sessionID = "ses_anthropic_cached_limit_exceeded"
await hook.event({
event: {
type: "message.updated",
properties: {
info: {
role: "assistant",
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-5",
finish: true,
tokens: {
input: 350000,
output: 0,
reasoning: 0,
cache: { read: 10000, write: 0 },
},
},
},
},
})
// when
const output = createOutput()
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "call_1" }, output)
// then — 360K/500K = 72%, above 70% threshold, uses cached 500K limit
// then
expect(output.output).toContain("context remaining")
expect(output.output).toContain("500,000-token context window")
expect(output.output).toContain("200,000-token context window")
})
})
})
+35 -1
View File
@@ -28,7 +28,7 @@ describe("resolveActualContextLimit", () => {
resetContextLimitEnv()
})
it("returns cached limit for Anthropic models when 1M mode is disabled (GA support)", () => {
it("returns cached limit for Anthropic 4.6 models when 1M mode is disabled (GA support)", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
delete process.env[VERTEX_CONTEXT_ENV_KEY]
@@ -45,6 +45,23 @@ describe("resolveActualContextLimit", () => {
expect(actualLimit).toBe(1_000_000)
})
it("returns default 200K for older Anthropic models even when cached limit is higher", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
delete process.env[VERTEX_CONTEXT_ENV_KEY]
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-sonnet-4-5", 500_000)
// when
const actualLimit = resolveActualContextLimit("anthropic", "claude-sonnet-4-5", {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
// then
expect(actualLimit).toBe(200_000)
})
it("returns default 200K for Anthropic models without cached limit and 1M mode disabled", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
@@ -92,6 +109,23 @@ describe("resolveActualContextLimit", () => {
expect(actualLimit).toBe(200000)
})
it("supports Anthropic 4.6 dot-version model IDs without explicit 1M mode", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
delete process.env[VERTEX_CONTEXT_ENV_KEY]
const modelContextLimitsCache = new Map<string, number>()
modelContextLimitsCache.set("anthropic/claude-opus-4.6", 1_000_000)
// when
const actualLimit = resolveActualContextLimit("anthropic", "claude-opus-4.6", {
anthropicContext1MEnabled: false,
modelContextLimitsCache,
})
// then
expect(actualLimit).toBe(1_000_000)
})
it("returns null for non-Anthropic providers without a cached limit", () => {
// given
delete process.env[ANTHROPIC_CONTEXT_ENV_KEY]
+11 -1
View File
@@ -1,6 +1,12 @@
import process from "node:process"
const DEFAULT_ANTHROPIC_ACTUAL_LIMIT = 200_000
const ANTHROPIC_NO_HEADER_GA_MODEL_IDS = new Set([
"claude-opus-4-6",
"claude-opus-4.6",
"claude-sonnet-4-6",
"claude-sonnet-4.6",
])
export type ContextLimitModelCacheState = {
anthropicContext1MEnabled: boolean
@@ -20,6 +26,10 @@ function getAnthropicActualLimit(modelCacheState?: ContextLimitModelCacheState):
: DEFAULT_ANTHROPIC_ACTUAL_LIMIT
}
function isAnthropicNoHeaderGaModel(modelID: string): boolean {
return ANTHROPIC_NO_HEADER_GA_MODEL_IDS.has(modelID.toLowerCase())
}
export function resolveActualContextLimit(
providerID: string,
modelID: string,
@@ -30,7 +40,7 @@ export function resolveActualContextLimit(
if (explicit1M === 1_000_000) return explicit1M
const cachedLimit = modelCacheState?.modelContextLimitsCache?.get(`${providerID}/${modelID}`)
if (cachedLimit) return cachedLimit
if (cachedLimit && isAnthropicNoHeaderGaModel(modelID)) return cachedLimit
return DEFAULT_ANTHROPIC_ACTUAL_LIMIT
}