Skip to content

Commit e49c34d

Browse files
feat(anthropic, amazon-bedrock): expose anthropicBeta to downstream providers (#13066)
<!-- Welcome to contributing to AI SDK! We're excited to see your changes. We suggest you read the following contributing guide we've created before submitting: https://github.com/vercel/ai/blob/main/CONTRIBUTING.md --> ## Background <!-- Why was this change necessary? --> `amazon-bedrock/anthropic` requires `anthropic_beta` in its request body to enable extended capabilities for anthropic models, like 1M context (context-1m-2025-08-07) and fine-grained tool streaming (fine-grained-tool-streaming-2025-05-14). ## Summary <!-- What did you change? --> - `anthropic`: added `anthropicBeta` and forwarded to downstream providers via transformRequestBody - `amazon-bedrock`: receive betas passed from upstream - `v5` changes here: #13059 ## Manual Verification <!-- For features & bugfixes. Please explain how you *manually* verified that the change works end-to-end as expected (excluding automated tests). Remove the section if it's not needed (e.g. for docs). --> - added example scripts with verification checks ## Checklist <!-- Do not edit this list. Leave items unchecked that don't apply. If you need to track subtasks, create a new "## Tasks" section Please check if the PR fulfills the following requirements: --> - [x] Tests have been added / updated (for bug fixes / features) - [x] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] I have reviewed this pull request (self-review)
1 parent 2291047 commit e49c34d

File tree

9 files changed

+403
-64
lines changed

9 files changed

+403
-64
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/anthropic': patch
3+
'@ai-sdk/amazon-bedrock': patch
4+
---
5+
6+
feat(anthropic): expose anthropic.anthropicBeta to downstream providers
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/amazon-bedrock': patch
3+
'@ai-sdk/anthropic': patch
4+
---
5+
6+
feat(anthropic): expose anthropic.anthropicBeta to downstream provider
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createBedrockAnthropic } from '@ai-sdk/amazon-bedrock/anthropic';
2+
import { streamText } from 'ai';
3+
import { run } from '../../lib/run';
4+
5+
const bedrockAnthropic = createBedrockAnthropic({
6+
headers: {
7+
'anthropic-beta': 'context-1m-2025-08-07',
8+
},
9+
});
10+
11+
// Build a large text block intended to exceed the standard context window
12+
const APPROX_TOKENS = 110_000;
13+
const CHARS_PER_TOKEN = 4;
14+
const TARGET_CHARS = APPROX_TOKENS * CHARS_PER_TOKEN;
15+
16+
function makeBaseChunk(): string {
17+
let s = '';
18+
for (let j = 0; j < 500; j++) {
19+
s += `IDX:${j.toString(36)} | alpha:${'abcdefghijklmnopqrstuvwxyz'.slice(0, (j % 26) + 1)} | nums:${'0123456789'.repeat(3)} | sym:${'-=_+'.repeat(5)}\n`;
20+
}
21+
return s;
22+
}
23+
24+
const baseChunk = makeBaseChunk();
25+
const repeats = Math.ceil(TARGET_CHARS / baseChunk.length);
26+
const largeContextText = baseChunk
27+
.repeat(repeats)
28+
.slice(0, TARGET_CHARS + 50_000);
29+
30+
run(async () => {
31+
const estimatedTokens = Math.ceil(largeContextText.length / CHARS_PER_TOKEN);
32+
console.log('Prepared large context chars:', largeContextText.length);
33+
console.log('Estimated tokens (chars/4):', estimatedTokens);
34+
35+
const startstamp = performance.now();
36+
37+
const result = streamText({
38+
model: bedrockAnthropic('us.anthropic.claude-sonnet-4-20250514-v1:0'),
39+
messages: [
40+
{
41+
role: 'user',
42+
content: [
43+
{
44+
type: 'text',
45+
text: `The following is a large context payload.\n\n${largeContextText}`,
46+
},
47+
{
48+
type: 'text',
49+
text: 'Summarize the structure of the data above in one sentence.',
50+
},
51+
],
52+
},
53+
],
54+
providerOptions: {
55+
// alternatively this feature can be enabled with:
56+
// anthropic: {
57+
// anthropicBeta: ['context-1m-2025-08-07'],
58+
// },
59+
},
60+
});
61+
62+
for await (const textPart of result.textStream) {
63+
process.stdout.write(textPart);
64+
}
65+
66+
const endstamp = performance.now();
67+
console.log();
68+
console.log(
69+
'Time taken:',
70+
((endstamp - startstamp) / 1000).toFixed(2),
71+
'seconds',
72+
);
73+
console.log('Token usage:', await result.usage);
74+
console.log('Finish reason:', await result.finishReason);
75+
});
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { createBedrockAnthropic } from '@ai-sdk/amazon-bedrock/anthropic';
2+
import { streamText } from 'ai';
3+
import { run } from '../../lib/run';
4+
import { z } from 'zod';
5+
6+
const bedrockAnthropic = createBedrockAnthropic({
7+
headers: {
8+
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
9+
},
10+
});
11+
12+
const tools = {
13+
write_to_file: {
14+
description:
15+
'Request to write content to a file. ALWAYS provide the COMPLETE file content, without any truncation.',
16+
inputSchema: z.object({
17+
path: z
18+
.string()
19+
.describe(
20+
'The path of the file to write to (relative to the current workspace directory)',
21+
),
22+
content: z.string().describe('The content to write to the file.'),
23+
}),
24+
},
25+
} as const;
26+
27+
run(async () => {
28+
console.log('=== Fine-grained tool streaming test (Bedrock Anthropic) ===\n');
29+
30+
const result = streamText({
31+
model: bedrockAnthropic('global.anthropic.claude-sonnet-4-6'),
32+
33+
messages: [
34+
{
35+
role: 'user',
36+
content: 'Write a bubble sort implementation in JavaScript to sort.js',
37+
},
38+
],
39+
40+
tools,
41+
toolChoice: 'required',
42+
providerOptions: {
43+
// alternatively this feature can be enabled with:
44+
// anthropic: {
45+
// anthropicBeta: ['fine-grained-tool-streaming-2025-05-14'],
46+
// },
47+
},
48+
});
49+
50+
let sawToolInputStart = false;
51+
let toolInputDeltaCount = 0;
52+
let sawToolInputEnd = false;
53+
const toolInputDeltaTimestamps: number[] = [];
54+
55+
// ts() prints a compact timestamp to stderr so we can see real-time ordering
56+
const T0 = Date.now();
57+
const ts = (label: string) =>
58+
process.stderr.write(
59+
`+${((Date.now() - T0) / 1000).toFixed(2)}s ${label}\n`,
60+
);
61+
62+
ts('stream started');
63+
64+
for await (const part of result.fullStream) {
65+
switch (part.type) {
66+
case 'text-start':
67+
ts('[text-start]');
68+
process.stdout.write('\n[text-start]\n');
69+
break;
70+
case 'text-delta':
71+
process.stdout.write(part.text);
72+
break;
73+
case 'text-end':
74+
ts('[text-end]');
75+
process.stdout.write('\n[text-end]\n');
76+
break;
77+
case 'tool-input-start':
78+
sawToolInputStart = true;
79+
ts(`[tool-input-start] tool=${part.toolName}`);
80+
process.stdout.write(
81+
`\n[tool-input-start] id=${part.id} tool=${part.toolName}\n`,
82+
);
83+
break;
84+
case 'tool-input-delta':
85+
toolInputDeltaCount++;
86+
toolInputDeltaTimestamps.push(Date.now());
87+
if (toolInputDeltaCount === 1 || toolInputDeltaCount % 100 === 0) {
88+
ts(`[tool-input-delta #${toolInputDeltaCount}]`);
89+
}
90+
process.stdout.write(part.delta);
91+
break;
92+
case 'tool-input-end':
93+
sawToolInputEnd = true;
94+
ts(`[tool-input-end]`);
95+
process.stdout.write(`\n[tool-input-end] id=${part.id}\n`);
96+
break;
97+
case 'tool-call': {
98+
const preview = JSON.stringify(part.input).slice(0, 80);
99+
ts(`[tool-call] tool=${part.toolName}`);
100+
console.log(
101+
`\n[tool-call] tool=${part.toolName} input(preview)=${preview}…`,
102+
);
103+
break;
104+
}
105+
case 'start-step':
106+
ts('[start-step]');
107+
console.log('\n[start-step]');
108+
break;
109+
case 'finish-step':
110+
ts(`[finish-step] reason=${part.finishReason}`);
111+
console.log(
112+
`[finish-step] finishReason=${part.finishReason} usage=${JSON.stringify(part.usage)}`,
113+
);
114+
break;
115+
case 'finish':
116+
ts(`[finish] reason=${part.finishReason}`);
117+
console.log(
118+
`\n[finish] finishReason=${part.finishReason} usage=${JSON.stringify(part.totalUsage)}`,
119+
);
120+
break;
121+
case 'error':
122+
ts('[error]');
123+
console.error('\n[error]', part.error);
124+
break;
125+
}
126+
}
127+
128+
// ── diagnosis ─────────────────────────────────────────────────────────────
129+
// Count consecutive delta intervals >= 5ms (genuine per-token generation time).
130+
// A buffered dump replays all deltas synchronously (<1ms each); real streaming
131+
// has most intervals >= 5ms as each token takes time to generate.
132+
// Threshold is heuristic — the desired behavior is smooth continuous streaming
133+
// instead of a long hang followed by a dump of all chunks at once.
134+
let liveIntervals = 0;
135+
for (let i = 1; i < toolInputDeltaTimestamps.length; i++) {
136+
if (toolInputDeltaTimestamps[i] - toolInputDeltaTimestamps[i - 1] >= 5)
137+
liveIntervals++;
138+
}
139+
const liveRatio =
140+
toolInputDeltaTimestamps.length > 1
141+
? liveIntervals / (toolInputDeltaTimestamps.length - 1)
142+
: 0;
143+
const isStreaming = toolInputDeltaCount >= 5 && liveRatio > 0.5;
144+
145+
console.log('\n=== Summary ===');
146+
console.log('tool-input-start received :', sawToolInputStart);
147+
console.log('tool-input-delta count :', toolInputDeltaCount);
148+
console.log('tool-input-end received :', sawToolInputEnd);
149+
console.log(
150+
`Fine-grained tool streaming: ${isStreaming ? 'YES' : 'NO'} (${toolInputDeltaCount} deltas, ${(liveRatio * 100).toFixed(0)}% live intervals)`,
151+
);
152+
});

0 commit comments

Comments
 (0)