From 1b136d134b4e7a113a213698449cff246c97c7f8 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 20 Oct 2026 21:04:12 -0700 Subject: [PATCH] test(e2e): Drop OpenRouter's [DONE] frame in the Anthropic test app The `(latest)` variant of `node-anthropic-send-to-sentry` fails on the streaming tests. OpenRouter ends each stream with an OpenAI-style `event: data` / `data: [DONE]` frame, which is not part of Anthropic's protocol. Up to 0.135.0, `@anthropic-ai/sdk` parsed only known event names and ignored this frame. Since 0.136.0 it parses every named event as JSON, so the frame makes it throw. The test app now gives the client a `fetch` wrapper that removes the `[DONE]` frame from event streams. The test keeps the newest SDK, and the pinned variant still passes. Pinning the latest variant to 0.135.0 would also work, but it would stop the test from tracking new SDK releases. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../node-anthropic-send-to-sentry/src/app.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts index 7ad878cdff26..469545162364 100644 --- a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts @@ -12,7 +12,43 @@ if (!apiKey) { // different base URL. It authenticates with a bearer token, so the key goes in `authToken` // (Authorization: Bearer) rather than `apiKey` (x-api-key). The model is incidental: what is under // test is the SDK's own request/response code path, which is what Sentry instruments. -const client = new Anthropic({ authToken: apiKey, baseURL: 'https://openrouter.ai/api' }); +const client = new Anthropic({ authToken: apiKey, baseURL: 'https://openrouter.ai/api', fetch: fetchWithoutDone }); + +/** + * OpenRouter ends each stream with an OpenAI-style `data: [DONE]` frame, which is not part of + * Anthropic's protocol. Since 0.136.0 the SDK JSON-parses every named event, so this frame + * makes it throw. Drop the frame before the SDK sees it. + */ +async function fetchWithoutDone(input: string | URL | Request, init?: RequestInit): Promise { + const response = await fetch(input, init); + if (!response.body || !response.headers.get('content-type')?.includes('text/event-stream')) { + return response; + } + + let buffer = ''; + const keepFrame = (frame: string): boolean => !/^data: \[DONE\]$/m.test(frame); + const body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough( + new TransformStream({ + transform(chunk, controller) { + buffer += chunk; + const frames = buffer.split(/(?<=\r?\n\r?\n)/); + buffer = /\r?\n\r?\n$/.test(frames[frames.length - 1] ?? '') ? '' : (frames.pop() ?? ''); + controller.enqueue(frames.filter(keepFrame).join('')); + }, + flush(controller) { + if (keepFrame(buffer)) { + controller.enqueue(buffer); + } + }, + }), + ); + + return new Response(body.pipeThrough(new TextEncoderStream()), { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); +} const MODEL = 'openai/gpt-4o-mini';