fix(extra-params): preserve resolved cacheRetention against undefined own-property clobber (#106069)

* fix(extra-params): preserve resolved cacheRetention when options carries undefined own-property

When the proxy transport emits cacheRetention as an own property set to
undefined, JS spread semantics clobber the resolved cacheRetention value
from per-model params. Re-assert the resolved value after the spread so
it takes precedence over an undefined own-property in the caller's options.

Fixes #106014

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(agents): strengthen cache retention precedence proof

* refactor(agents): keep cache retention merge compact

* style(agents): format cache retention matrix

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
krissding
2026-07-14 14:54:56 +08:00
committed by GitHub
parent e0b52a9618
commit e588f2c0ce
2 changed files with 52 additions and 4 deletions
@@ -511,4 +511,52 @@ describe("createStreamFnWithExtraParams sampling overrides", () => {
expect(first.fastMode).toBe(firstFastMode);
expect(second.fastMode).toBe(secondFastMode);
});
it.each([
{ requestRetention: undefined, expected: "long", name: "own undefined" },
{ requestRetention: "none" as const, expected: "none", name: "explicit none" },
{ requestRetention: "short" as const, expected: "short", name: "explicit short" },
{ requestRetention: "long" as const, expected: "long", name: "explicit long" },
])(
"merges configured cache retention with $name request options",
({ requestRetention, expected }) => {
const underlying = vi.fn(() => ({
push: vi.fn(),
result: vi.fn(async () => undefined),
[Symbol.asyncIterator]: vi.fn(async function* () {
// empty stream
}),
})) as unknown as StreamFn;
const agent: { streamFn?: StreamFn } = { streamFn: underlying };
applyExtraParamsToAgent(
agent,
undefined,
"anthropic",
"claude-sonnet-5",
{ cacheRetention: "long" },
undefined,
undefined,
undefined,
{ supportsPromptCacheKey: true } as never,
);
if (!agent.streamFn) {
throw new Error("expected extra params to wrap streamFn");
}
const requestOptions = { cacheRetention: requestRetention };
expect(requestOptions).toHaveProperty("cacheRetention");
void agent.streamFn(
{ id: "claude-sonnet-5", api: "anthropic-messages", provider: "anthropic" } as never,
{ messages: [], tools: [] } as never,
requestOptions,
);
expect(underlying).toHaveBeenCalledTimes(1);
const callOptions = (underlying as unknown as { mock: { calls: unknown[][] } }).mock
.calls[0]?.[2] as { cacheRetention?: string } | undefined;
expect(callOptions?.cacheRetention).toBe(expected);
},
);
});
@@ -553,15 +553,15 @@ function createStreamFnWithExtraParams(
typeof callModel.id === "string" ? callModel.id : undefined,
readSupportsPromptCacheKey(callModel),
);
const hasStreamParams = Object.keys(streamParams).length > 0 || cacheRetention;
if (!hasStreamParams) {
if (Object.keys(streamParams).length === 0 && !cacheRetention) {
return underlying(callModel, context, options);
}
const effectiveCacheRetention = options?.cacheRetention ?? cacheRetention;
return underlying(callModel, context, {
...streamParams,
...(cacheRetention ? { cacheRetention } : {}),
...options,
// Own undefined means no request override; explicit none/short/long still wins.
...(effectiveCacheRetention ? { cacheRetention: effectiveCacheRetention } : {}),
});
};