Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions apps/gateway/src/anthropic/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { HTTPException } from "hono/http-exception";
import { streamSSE } from "hono/streaming";

import { app } from "@/app.js";
import {
NATIVE_ANTHROPIC_PASSTHROUGH_FIELD,
NATIVE_ANTHROPIC_PASSTHROUGH_HEADER,
nativeAnthropicPassthroughNonce,
} from "@/chat/native-passthrough.js";
import {
buildAnthropicErrorBody,
getAnthropicErrorType,
Expand Down Expand Up @@ -656,6 +661,11 @@ anthropic.openapi(messages, async (c) => {
"x-debug": c.req.header("x-debug") ?? "",
"HTTP-Referer": c.req.header("HTTP-Referer") ?? "",
...(sessionId ? { "x-session-id": sessionId } : {}),
// Ask the chat-completions pipeline to return the raw upstream
// Anthropic response verbatim when served by an Anthropic-native
// provider, so native content blocks (web search, citations,
// thinking) survive instead of the lossy OpenAI round-trip.
[NATIVE_ANTHROPIC_PASSTHROUGH_HEADER]: nativeAnthropicPassthroughNonce,
},
body: JSON.stringify(openaiRequest),
});
Expand Down Expand Up @@ -729,6 +739,52 @@ anthropic.openapi(messages, async (c) => {
const decoder = new TextDecoder();

let buffer = "";

// Native passthrough detection: the chat-completions pipeline emits a
// raw Anthropic SSE stream (which begins with `event: message_start`)
// when served by an Anthropic-native provider. A reconstructed stream
// (e.g. fallback to a non-Anthropic provider) is OpenAI chunks. Peek
// the first event(s) to classify; forward verbatim when native,
// otherwise fall through to the OpenAI->Anthropic reconstruction below
// (seeded with the already-read buffer).
let isNativePassthrough = false;
let classified = false;
while (!classified) {
const { done, value } = await reader.read();
if (done) {
classified = true;
break;
}
buffer += decoder.decode(value, { stream: true });
if (
/(^|\n)event: ?message_start(\r?\n|$)/.test(buffer) ||
/"type"\s*:\s*"message_start"/.test(buffer)
) {
isNativePassthrough = true;
classified = true;
} else if (
/"object"\s*:\s*"chat\.completion(\.chunk)?"/.test(buffer) ||
buffer.includes("data: [DONE]") ||
buffer.length > 65536
) {
classified = true;
Comment on lines +766 to +770

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse the peeked OpenAI buffer before reading again

For non-native streams this peek can classify after reading an OpenAI SSE chunk into buffer, but the reconstruction loop below calls reader.read() before it ever splits the existing buffer. If the inner stream ends after that first read (common for small/error streams, or fallback to a non-Anthropic provider), the already-buffered events are skipped and /v1/messages returns an empty/truncated Anthropic stream; process the buffered data before waiting for another chunk.

Useful? React with 👍 / 👎.

}
}

if (isNativePassthrough) {
if (buffer) {
await stream.write(buffer);
}
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await stream.write(decoder.decode(value, { stream: true }));
}
return;
}

let messageId = "";
let model = "";
const contentBlocks: Array<{
Expand Down Expand Up @@ -1119,6 +1175,16 @@ anthropic.openapi(messages, async (c) => {
});
}

// Native Anthropic passthrough: when the pipeline served this request with an
// Anthropic-native provider, it attaches the raw upstream Anthropic body.
// Return it verbatim so web search (server_tool_use / web_search_tool_result),
// citations, and thinking blocks match the Messages API exactly.
const nativePassthrough =
openaiResponse?.[NATIVE_ANTHROPIC_PASSTHROUGH_FIELD];
if (nativePassthrough && typeof nativePassthrough === "object") {
return c.json(nativePassthrough);
}

// Transform OpenAI response to Anthropic format
const content: any[] = [];

Expand Down
Loading
Loading