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';