|
8 | 8 | } from '@tanstack/ai' |
9 | 9 | import { AnthropicTextAdapter } from '../src/adapters/text' |
10 | 10 | import type { AnthropicTextProviderOptions } from '../src/adapters/text' |
| 11 | +import { ANTHROPIC_MAX_NONSTREAMING_TOKENS } from '../src/model-meta' |
11 | 12 | import { z } from 'zod' |
12 | 13 |
|
13 | 14 | const mocks = vi.hoisted(() => { |
@@ -549,6 +550,51 @@ describe('Anthropic adapter option mapping', () => { |
549 | 550 | expect(truncationWarning).toBeUndefined() |
550 | 551 | }) |
551 | 552 |
|
| 553 | + it('clamps the default max_tokens on the non-streaming structured-output path so it never trips the SDK 10-minute guard (#849)', async () => { |
| 554 | + // The structured-output fallback issues a NON-streaming |
| 555 | + // `messages.create({ stream: false })`. The Anthropic SDK throws |
| 556 | + // "Streaming is required for operations that may take longer than 10 |
| 557 | + // minutes" once max_tokens exceeds ~21_333, so the defaulted ceiling must |
| 558 | + // be clamped here even though the streaming chat path keeps the full 64K. |
| 559 | + mocks.betaMessagesCreate.mockResolvedValueOnce({ |
| 560 | + id: 'msg_structured', |
| 561 | + type: 'message', |
| 562 | + role: 'assistant', |
| 563 | + model: 'claude-3-7-sonnet', |
| 564 | + content: [ |
| 565 | + { |
| 566 | + type: 'tool_use', |
| 567 | + id: 'toolu_structured_output', |
| 568 | + name: 'structured_output', |
| 569 | + input: { recommendation: 'Strat', price: 1299 }, |
| 570 | + }, |
| 571 | + ], |
| 572 | + stop_reason: 'tool_use', |
| 573 | + usage: { input_tokens: 10, output_tokens: 20 }, |
| 574 | + }) |
| 575 | + |
| 576 | + const adapter = createAdapter('claude-3-7-sonnet') |
| 577 | + |
| 578 | + for await (const _ of chat({ |
| 579 | + adapter, |
| 580 | + messages: [{ role: 'user', content: 'recommend a guitar as json' }], |
| 581 | + outputSchema: z.object({ |
| 582 | + recommendation: z.string(), |
| 583 | + price: z.number(), |
| 584 | + }), |
| 585 | + stream: true, |
| 586 | + })) { |
| 587 | + // consume stream |
| 588 | + } |
| 589 | + |
| 590 | + const [payload] = mocks.betaMessagesCreate.mock.calls[0]! |
| 591 | + expect(payload.stream).toBe(false) |
| 592 | + // Clamped to the non-streaming limit — NOT claude-3-7-sonnet's full 64K |
| 593 | + // streaming ceiling, which would make the SDK throw before the request. |
| 594 | + expect(payload.max_tokens).toBe(ANTHROPIC_MAX_NONSTREAMING_TOKENS) |
| 595 | + expect(payload.max_tokens).toBeLessThanOrEqual(21_333) |
| 596 | + }) |
| 597 | + |
552 | 598 | it('native combined mode (#605): wires outputSchema into output_format alongside tools on Claude 4.5+', async () => { |
553 | 599 | // Final-turn JSON the model emits when output_format is in play. |
554 | 600 | const finalJson = JSON.stringify({ city: 'Berlin', temp: 18 }) |
|
0 commit comments