Skip to content

Commit 9075fdb

Browse files
committed
feat(anthropic): support cache diagnostics
1 parent 405116e commit 9075fdb

8 files changed

Lines changed: 256 additions & 0 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ai-sdk/anthropic": minor
3+
"@ai-sdk/google-vertex": patch
4+
---
5+
6+
Add Anthropic cache diagnostics provider option support.

packages/anthropic/src/anthropic-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,7 @@ export const anthropicResponseSchema = lazySchema(() =>
991991
),
992992
})
993993
.nullish(),
994+
diagnostics: z.looseObject({}).nullable().nullish(),
994995
}),
995996
),
996997
);
@@ -1046,6 +1047,7 @@ export const anthropicChunkSchema = lazySchema(() =>
10461047
id: z.string(),
10471048
})
10481049
.nullish(),
1050+
diagnostics: z.looseObject({}).nullable().nullish(),
10491051
}),
10501052
}),
10511053
z.object({

packages/anthropic/src/anthropic-language-model-options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ export const anthropicLanguageModelOptions = z.object({
128128
})
129129
.optional(),
130130

131+
/**
132+
* Cache diagnostics settings for diagnosing prompt cache misses.
133+
* See https://docs.anthropic.com/en/docs/build-with-claude/cache-diagnostics
134+
*/
135+
cacheDiagnostics: z
136+
.object({
137+
/**
138+
* The previous response id to compare against. Use null on the first
139+
* request to opt in before a previous response exists.
140+
*/
141+
previousMessageId: z.string().nullable(),
142+
})
143+
.optional(),
144+
131145
/**
132146
* Metadata to include with the request.
133147
*

packages/anthropic/src/anthropic-language-model.test.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5654,6 +5654,155 @@ describe('AnthropicLanguageModel', () => {
56545654
expect(result.warnings).toStrictEqual([]);
56555655
});
56565656

5657+
it('should pass cache diagnostics to request body and add beta header', async () => {
5658+
prepareJsonFixtureResponse('anthropic-text');
5659+
5660+
const result = await model.doGenerate({
5661+
prompt: TEST_PROMPT,
5662+
providerOptions: {
5663+
anthropic: {
5664+
cacheDiagnostics: { previousMessageId: 'msg_previous' },
5665+
} satisfies AnthropicLanguageModelOptions,
5666+
},
5667+
});
5668+
5669+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
5670+
{
5671+
"diagnostics": {
5672+
"previous_message_id": "msg_previous",
5673+
},
5674+
"max_tokens": 4096,
5675+
"messages": [
5676+
{
5677+
"content": [
5678+
{
5679+
"text": "Hello",
5680+
"type": "text",
5681+
},
5682+
],
5683+
"role": "user",
5684+
},
5685+
],
5686+
"model": "claude-3-haiku-20240307",
5687+
}
5688+
`);
5689+
5690+
expect(server.calls[0].requestHeaders['anthropic-beta']).toBe(
5691+
'cache-diagnosis-2026-04-07',
5692+
);
5693+
expect(result.warnings).toStrictEqual([]);
5694+
});
5695+
5696+
it('should support cache diagnostics first-turn opt-in', async () => {
5697+
prepareJsonFixtureResponse('anthropic-text');
5698+
5699+
const result = await model.doGenerate({
5700+
prompt: TEST_PROMPT,
5701+
providerOptions: {
5702+
anthropic: {
5703+
cacheDiagnostics: { previousMessageId: null },
5704+
} satisfies AnthropicLanguageModelOptions,
5705+
},
5706+
});
5707+
5708+
expect(await server.calls[0].requestBodyJson).toMatchInlineSnapshot(`
5709+
{
5710+
"diagnostics": {
5711+
"previous_message_id": null,
5712+
},
5713+
"max_tokens": 4096,
5714+
"messages": [
5715+
{
5716+
"content": [
5717+
{
5718+
"text": "Hello",
5719+
"type": "text",
5720+
},
5721+
],
5722+
"role": "user",
5723+
},
5724+
],
5725+
"model": "claude-3-haiku-20240307",
5726+
}
5727+
`);
5728+
5729+
expect(server.calls[0].requestHeaders['anthropic-beta']).toBe(
5730+
'cache-diagnosis-2026-04-07',
5731+
);
5732+
expect(result.warnings).toStrictEqual([]);
5733+
});
5734+
5735+
it('should expose cache diagnostics response metadata', async () => {
5736+
server.urls['https://api.anthropic.com/v1/messages'].response = {
5737+
type: 'json-value',
5738+
body: {
5739+
id: 'msg_017TfcQ4AgGxKyBduUpqYPZn',
5740+
type: 'message',
5741+
role: 'assistant',
5742+
content: [{ type: 'text', text: 'Hello, World!' }],
5743+
model: 'claude-3-haiku-20240307',
5744+
stop_reason: 'end_turn',
5745+
stop_sequence: null,
5746+
usage: { input_tokens: 4, output_tokens: 30 },
5747+
diagnostics: {
5748+
cache_miss_reason: {
5749+
type: 'tools_changed',
5750+
cache_missed_input_tokens: 1024,
5751+
},
5752+
},
5753+
},
5754+
};
5755+
5756+
const result = await model.doGenerate({
5757+
prompt: TEST_PROMPT,
5758+
providerOptions: {
5759+
anthropic: {
5760+
cacheDiagnostics: { previousMessageId: 'msg_previous' },
5761+
} satisfies AnthropicLanguageModelOptions,
5762+
},
5763+
});
5764+
5765+
expect(result.providerMetadata?.anthropic?.diagnostics).toEqual({
5766+
cache_miss_reason: {
5767+
type: 'tools_changed',
5768+
cache_missed_input_tokens: 1024,
5769+
},
5770+
});
5771+
});
5772+
5773+
it('should warn and omit cache diagnostics when unsupported', async () => {
5774+
prepareJsonFixtureResponse('anthropic-text');
5775+
5776+
const { AnthropicLanguageModel } =
5777+
await import('./anthropic-language-model');
5778+
const model = new AnthropicLanguageModel('claude-3-haiku-20240307', {
5779+
provider: 'test-provider',
5780+
baseURL: 'https://api.anthropic.com/v1',
5781+
headers: {},
5782+
supportsCacheDiagnostics: false,
5783+
});
5784+
5785+
const result = await model.doGenerate({
5786+
prompt: TEST_PROMPT,
5787+
providerOptions: {
5788+
anthropic: {
5789+
cacheDiagnostics: { previousMessageId: 'msg_previous' },
5790+
} satisfies AnthropicLanguageModelOptions,
5791+
},
5792+
});
5793+
5794+
const requestBody = await server.calls[0].requestBodyJson;
5795+
expect(requestBody.diagnostics).toBeUndefined();
5796+
expect(server.calls[0].requestHeaders['anthropic-beta']).toBeUndefined();
5797+
expect(result.warnings).toEqual([
5798+
{
5799+
type: 'unsupported',
5800+
feature: 'cacheDiagnostics',
5801+
details: 'cache diagnostics is not supported by this provider',
5802+
},
5803+
]);
5804+
});
5805+
56575806
it('should pass metadata with user_id to request body', async () => {
56585807
prepareJsonFixtureResponse('anthropic-text');
56595808

@@ -8167,6 +8316,43 @@ describe('AnthropicLanguageModel', () => {
81678316
);
81688317
});
81698318

8319+
it('should expose streaming cache diagnostics response metadata', async () => {
8320+
server.urls['https://api.anthropic.com/v1/messages'].response = {
8321+
type: 'stream-chunks',
8322+
chunks: [
8323+
`data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[],"model":"claude-3-haiku-20240307","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":17,"output_tokens":1},"diagnostics":{"cache_miss_reason":{"type":"messages_changed","cache_missed_input_tokens":2048}}}}\n\n`,
8324+
`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`,
8325+
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n`,
8326+
`data: {"type":"content_block_stop","index":0}\n\n`,
8327+
`data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5}}\n\n`,
8328+
`data: {"type":"message_stop"}\n\n`,
8329+
],
8330+
};
8331+
8332+
const result = await model.doStream({
8333+
prompt: TEST_PROMPT,
8334+
providerOptions: {
8335+
anthropic: {
8336+
cacheDiagnostics: { previousMessageId: 'msg_previous' },
8337+
} satisfies AnthropicLanguageModelOptions,
8338+
},
8339+
});
8340+
8341+
const chunks = await convertReadableStreamToArray(result.stream);
8342+
const finishPart = chunks.find(part => part.type === 'finish');
8343+
8344+
expect(
8345+
finishPart?.type === 'finish'
8346+
? finishPart.providerMetadata?.anthropic?.diagnostics
8347+
: undefined,
8348+
).toEqual({
8349+
cache_miss_reason: {
8350+
type: 'messages_changed',
8351+
cache_missed_input_tokens: 2048,
8352+
},
8353+
});
8354+
});
8355+
81708356
it('should support cache control', async () => {
81718357
server.urls['https://api.anthropic.com/v1/messages'].response = {
81728358
type: 'stream-chunks',

packages/anthropic/src/anthropic-language-model.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ type AnthropicLanguageModelConfig = {
149149
* Defaults to true.
150150
*/
151151
supportsStrictTools?: boolean;
152+
153+
/**
154+
* When false, cache diagnostics provider options will be ignored and a
155+
* warning emitted. Defaults to true.
156+
*/
157+
supportsCacheDiagnostics?: boolean;
152158
};
153159

154160
export class AnthropicLanguageModel implements LanguageModelV4 {
@@ -338,6 +344,17 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
338344
(this.config.supportsStrictTools ?? true) &&
339345
modelSupportsStructuredOutput;
340346

347+
const supportsCacheDiagnostics =
348+
this.config.supportsCacheDiagnostics ?? true;
349+
if (anthropicOptions?.cacheDiagnostics && !supportsCacheDiagnostics) {
350+
warnings.push({
351+
type: 'unsupported',
352+
feature: 'cacheDiagnostics',
353+
details: 'cache diagnostics is not supported by this provider',
354+
});
355+
anthropicOptions.cacheDiagnostics = undefined;
356+
}
357+
341358
const structureOutputMode =
342359
anthropicOptions?.structuredOutputMode ?? 'auto';
343360
const useStructuredOutput =
@@ -497,6 +514,12 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
497514
...(anthropicOptions?.cacheControl && {
498515
cache_control: anthropicOptions.cacheControl,
499516
}),
517+
...(anthropicOptions?.cacheDiagnostics && {
518+
diagnostics: {
519+
previous_message_id:
520+
anthropicOptions.cacheDiagnostics.previousMessageId,
521+
},
522+
}),
500523
...(anthropicOptions?.metadata?.userId != null && {
501524
metadata: { user_id: anthropicOptions.metadata.userId },
502525
}),
@@ -722,6 +745,10 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
722745
betas.add('task-budgets-2026-03-13');
723746
}
724747

748+
if (anthropicOptions?.cacheDiagnostics) {
749+
betas.add('cache-diagnosis-2026-04-07');
750+
}
751+
725752
if (anthropicOptions?.speed === 'fast') {
726753
betas.add('fast-mode-2026-02-01');
727754
}
@@ -1384,6 +1411,9 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
13841411
usage: response.usage as JSONObject,
13851412
stopSequence: response.stop_sequence ?? null,
13861413
...(stopDetails != null ? { stopDetails } : {}),
1414+
...(response.diagnostics !== undefined
1415+
? { diagnostics: response.diagnostics as JSONObject | null }
1416+
: {}),
13871417

13881418
iterations: response.usage.iterations
13891419
? response.usage.iterations.map(
@@ -1482,6 +1512,7 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
14821512
unified: 'other',
14831513
raw: undefined,
14841514
};
1515+
let diagnostics: JSONObject | null | undefined;
14851516
const usage: AnthropicUsage = {
14861517
input_tokens: 0,
14871518
output_tokens: 0,
@@ -2332,6 +2363,10 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
23322363
};
23332364
}
23342365

2366+
if (value.message.diagnostics !== undefined) {
2367+
diagnostics = value.message.diagnostics as JSONObject | null;
2368+
}
2369+
23352370
if (value.message.stop_reason != null) {
23362371
finishReason = {
23372372
unified: mapAnthropicStopReason({
@@ -2469,6 +2504,7 @@ export class AnthropicLanguageModel implements LanguageModelV4 {
24692504
usage: (rawUsage as JSONObject) ?? null,
24702505
stopSequence,
24712506
...(stopDetails != null ? { stopDetails } : {}),
2507+
...(diagnostics !== undefined ? { diagnostics } : {}),
24722508
iterations: usage.iterations
24732509
? usage.iterations.map(
24742510
iter =>

packages/anthropic/src/anthropic-message-metadata.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ export interface AnthropicMessageMetadata {
8686
recommendedModel?: string;
8787
};
8888

89+
/**
90+
* Cache diagnostics response, when the request opts into Anthropic cache
91+
* diagnostics. `null` means the request had no prior message to compare or
92+
* no divergence was detected; an object may contain a `cache_miss_reason`.
93+
*/
94+
diagnostics?: JSONObject | null;
95+
8996
/**
9097
* Usage breakdown by iteration when compaction is triggered.
9198
*

packages/google-vertex/src/anthropic/google-vertex-anthropic-provider.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('google-vertex-anthropic-provider', () => {
5757
transformRequestBody: expect.any(Function),
5858
supportsNativeStructuredOutput: false,
5959
supportsStrictTools: false,
60+
supportsCacheDiagnostics: false,
6061
}),
6162
);
6263
});
@@ -214,6 +215,7 @@ describe('google-vertex-anthropic-provider', () => {
214215
"headers": {},
215216
"provider": "googleVertex.anthropic.messages",
216217
"supportedUrls": [Function],
218+
"supportsCacheDiagnostics": false,
217219
"supportsNativeStructuredOutput": false,
218220
"supportsStrictTools": false,
219221
"transformRequestBody": [Function],
@@ -237,6 +239,7 @@ describe('google-vertex-anthropic-provider', () => {
237239
"headers": {},
238240
"provider": "googleVertex.anthropic.messages",
239241
"supportedUrls": [Function],
242+
"supportsCacheDiagnostics": false,
240243
"supportsNativeStructuredOutput": false,
241244
"supportsStrictTools": false,
242245
"transformRequestBody": [Function],

packages/google-vertex/src/anthropic/google-vertex-anthropic-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ export function createGoogleVertexAnthropic(
228228
supportsNativeStructuredOutput: false,
229229
// Vertex Anthropic doesn't support strict mode on tool definitions.
230230
supportsStrictTools: false,
231+
// Anthropic cache diagnostics are only supported on the Claude API.
232+
supportsCacheDiagnostics: false,
231233
});
232234

233235
const provider = function (modelId: GoogleVertexAnthropicModelId) {

0 commit comments

Comments
 (0)