From 476ec3facf60644a7cbe582d090dc01baaadb3cd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 13:28:04 +0100 Subject: [PATCH] refactor(anthropic): use SDK SSE parser --- packages/ai/src/providers/anthropic.ts | 152 +------------------------ 1 file changed, 3 insertions(+), 149 deletions(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index b64de20f145b..50839278545c 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -1,5 +1,6 @@ // Anthropic provider adapts Anthropic streams and tool calls for the runtime. import Anthropic from "@anthropic-ai/sdk"; +import { Stream } from "@anthropic-ai/sdk/core/streaming.js"; import type { CacheControlEphemeral, ContentBlockParam, @@ -303,18 +304,6 @@ function mergeHeaders( return merged; } -interface ServerSentEvent { - event: string | null; - data: string; - raw: string[]; -} - -interface SseDecoderState { - event: string | null; - data: string[]; - raw: string[]; -} - const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet = new Set([ "message_start", "message_delta", @@ -324,139 +313,8 @@ const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet = new Set([ "content_block_stop", ]); -function flushSseEvent(state: SseDecoderState): ServerSentEvent | null { - if (!state.event && state.data.length === 0) { - return null; - } - - const event: ServerSentEvent = { - event: state.event, - data: state.data.join("\n"), - raw: [...state.raw], - }; - state.event = null; - state.data = []; - state.raw = []; - return event; -} - -function decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null { - if (line === "") { - return flushSseEvent(state); - } - - state.raw.push(line); - if (line.startsWith(":")) { - return null; - } - - const delimiterIndex = line.indexOf(":"); - const fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex); - let value = delimiterIndex === -1 ? "" : line.slice(delimiterIndex + 1); - if (value.startsWith(" ")) { - value = value.slice(1); - } - - if (fieldName === "event") { - state.event = value; - } else if (fieldName === "data") { - state.data.push(value); - } - - return null; -} - -function nextLineBreakIndex(text: string): number { - const carriageReturnIndex = text.indexOf("\r"); - const newlineIndex = text.indexOf("\n"); - if (carriageReturnIndex === -1) { - return newlineIndex; - } - if (newlineIndex === -1) { - return carriageReturnIndex; - } - return Math.min(carriageReturnIndex, newlineIndex); -} - -function consumeLine(text: string): { line: string; rest: string } | null { - const lineBreakIndex = nextLineBreakIndex(text); - if (lineBreakIndex === -1) { - return null; - } - - let nextIndex = lineBreakIndex + 1; - if (text[lineBreakIndex] === "\r" && text[nextIndex] === "\n") { - nextIndex += 1; - } - - return { - line: text.slice(0, lineBreakIndex), - rest: text.slice(nextIndex), - }; -} - -async function* iterateSseMessages( - body: ReadableStream, - signal?: AbortSignal, -): AsyncGenerator { - const reader = body.getReader(); - const decoder = new TextDecoder(); - const state: SseDecoderState = { event: null, data: [], raw: [] }; - let buffer = ""; - - try { - while (true) { - if (signal?.aborted) { - throw new Error("Request was aborted"); - } - - const { value, done } = await reader.read(); - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - let consumed = consumeLine(buffer); - while (consumed) { - buffer = consumed.rest; - const event = decodeSseLine(consumed.line, state); - if (event) { - yield event; - } - consumed = consumeLine(buffer); - } - } - - buffer += decoder.decode(); - let consumed = consumeLine(buffer); - while (consumed) { - buffer = consumed.rest; - const event = decodeSseLine(consumed.line, state); - if (event) { - yield event; - } - consumed = consumeLine(buffer); - } - - if (buffer.length > 0) { - const event = decodeSseLine(buffer, state); - if (event) { - yield event; - } - } - - const trailingEvent = flushSseEvent(state); - if (trailingEvent) { - yield trailingEvent; - } - } finally { - reader.releaseLock(); - } -} - async function* iterateAnthropicEvents( response: Response, - signal?: AbortSignal, requireMessageStop = false, ): AsyncGenerator { if (!response.body) { @@ -466,7 +324,7 @@ async function* iterateAnthropicEvents( let sawMessageStart = false; let sawMessageEnd = false; - for await (const sse of iterateSseMessages(response.body, signal)) { + for await (const sse of Stream.rawEvents(response)) { if (sse.event === "error") { throw new Error(sse.data); } @@ -612,11 +470,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti const blocks = output.content as Block[]; const blockIndexes = new Map(); - for await (const event of iterateAnthropicEvents( - response, - requestOptions?.signal, - refusalBuffer !== undefined, - )) { + for await (const event of iterateAnthropicEvents(response, refusalBuffer !== undefined)) { if (event.type === "message_start") { output.responseId = event.message.id; output.responseModel = event.message.model;