refactor: konsolidasi handler ke router.ts + complexity + test coverage
=== DRY Konsolidasi === - handleRelay, requireAuth, handleHealth, handleIndex, getClientIP → 1x di src/lib/router.ts - api/relay.ts, src/worker.ts, src/index.ts jadi thin wrapper === Dead exports === - 35 dead exports dibersihkan (dari router.ts, worker.ts, relay.ts) - Duplikasi simbol dari 12 ke 3 (wajar) === Keamanan === - Default API key sk-dummy-key dihapus — requireAuth return null saat key kosong - Semua CORS konsisten via getCorsHeaders() === Kompleksitas === - handleRequest → pecah ke validateRelayTarget, createJsonErrorResponse, dll - transformAnthropicStream (170 baris) → 4 sub-fungsi (emitInitEvents, emitDoneEvents, etc) - handleAnthropicMessages → handleUpstreamError, buildStreamHeaders, buildJsonHeaders - Fix duplicate stream? field di AnthropicRequest interface === Test Coverage === - Test naik dari 171 ke 248 (+77 test) - File baru: src/router.test.ts (requireAuth, getClientIP, CORS, health, index) - File baru: src/relay-integration.test.ts (filterResponseHeaders, shouldSendBody, buildRelayRequest, classifyFetchError, createRelayResponse, normalizeTargetUrl, SSRF, isPrivateIp) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
db281c5175
commit
fc69a0e93c
+211
-235
@@ -41,7 +41,6 @@ export interface AnthropicRequest {
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
stream?: boolean;
|
||||
stop_sequences?: string[];
|
||||
system?: string | AnthropicSystemBlock[];
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -429,6 +428,108 @@ interface OutputCounter {
|
||||
chars: number;
|
||||
}
|
||||
|
||||
// --- Stream state machine helpers ---------------------------------------------
|
||||
|
||||
/**
|
||||
* Emit message_start and content_block_start events.
|
||||
*/
|
||||
function emitInitEvents(
|
||||
controller: ReadableStreamDefaultController,
|
||||
encoder: TextEncoder,
|
||||
model: string,
|
||||
messageId: string,
|
||||
usage: AnthropicResponse["usage"],
|
||||
): void {
|
||||
controller.enqueue(encoder.encode(
|
||||
`event: message_start\ndata: ${JSON.stringify({
|
||||
type: "message_start",
|
||||
message: { id: messageId, type: "message", role: "assistant", content: [], model, stop_reason: null, stop_sequence: null, usage },
|
||||
})}\n\n`,
|
||||
));
|
||||
controller.enqueue(encoder.encode(
|
||||
'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit content_block_stop + message_delta + message_stop and close.
|
||||
* Estimates output tokens from char count as fallback.
|
||||
*/
|
||||
function emitDoneEvents(
|
||||
controller: ReadableStreamDefaultController,
|
||||
encoder: TextEncoder,
|
||||
usage: AnthropicResponse["usage"],
|
||||
outputCounter: OutputCounter,
|
||||
): void {
|
||||
if (usage.output_tokens === 0 && outputCounter.chars > 0) {
|
||||
usage.output_tokens = Math.max(1, Math.round(outputCounter.chars / 4));
|
||||
}
|
||||
|
||||
controller.enqueue(encoder.encode('event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n'));
|
||||
controller.enqueue(encoder.encode(
|
||||
`event: message_delta\ndata: ${JSON.stringify({
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn", stop_sequence: null },
|
||||
usage: { output_tokens: usage.output_tokens },
|
||||
})}\n\n`,
|
||||
));
|
||||
controller.enqueue(encoder.encode('event: message_stop\ndata: {"type":"message_stop"}\n\n'));
|
||||
controller.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit error event and close — dev mode includes the error detail.
|
||||
*/
|
||||
function emitErrorEvent(
|
||||
controller: ReadableStreamDefaultController,
|
||||
encoder: TextEncoder,
|
||||
err: unknown,
|
||||
): void {
|
||||
if (isDevMode()) {
|
||||
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: String(err) })}\n\n`));
|
||||
} else {
|
||||
controller.enqueue(encoder.encode('event: error\ndata: {"error":"Stream error"}\n\n'));
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process one chunk from the upstream reader through the SSE line buffer
|
||||
* and emit adapted Anthropic SSE events for each complete line.
|
||||
* Returns true if the stream is done (reader returned done=true).
|
||||
*/
|
||||
async function processStreamChunk(
|
||||
reader: ReadableStreamDefaultReader,
|
||||
lineBuffer: SSELineBuffer,
|
||||
decoder: TextDecoder,
|
||||
controller: ReadableStreamDefaultController,
|
||||
encoder: TextEncoder,
|
||||
model: string,
|
||||
config: BackendConfig,
|
||||
usage: AnthropicResponse["usage"],
|
||||
outputCounter: OutputCounter,
|
||||
): Promise<boolean> {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
// Flush remaining buffered data
|
||||
const remaining = lineBuffer.flush();
|
||||
if (remaining.length > 0) {
|
||||
const adapted = backendLineToAnthropicSSE(remaining, model, config, usage, outputCounter);
|
||||
if (adapted) controller.enqueue(encoder.encode(adapted + "\n\n"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = lineBuffer.add(chunk);
|
||||
|
||||
for (const line of lines) {
|
||||
const adapted = backendLineToAnthropicSSE(line, model, config, usage, outputCounter);
|
||||
if (adapted) controller.enqueue(encoder.encode(adapted + "\n\n"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Stream transformer --------------------------------------------------------
|
||||
|
||||
function transformAnthropicStream(
|
||||
@@ -437,41 +538,26 @@ function transformAnthropicStream(
|
||||
config: BackendConfig,
|
||||
): ReadableStream {
|
||||
const reader = trackReader(body.getReader() as any as ReadableStreamDefaultReader);
|
||||
const decoder = new TextDecoder();
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const lineBuffer = new SSELineBuffer();
|
||||
|
||||
let phase: "init" | "block" | "done" = "init";
|
||||
let messageId = `msg_${Date.now()}`;
|
||||
const outputCounter: OutputCounter = { chars: 0 };
|
||||
const usage: AnthropicResponse["usage"] = { input_tokens: 0, output_tokens: 0 };
|
||||
|
||||
// Accumulate usage from backend SSE lines (some backends include usage
|
||||
// in their SSE data). Start with zeros, update as we parse lines.
|
||||
const usage: AnthropicResponse["usage"] = {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
};
|
||||
|
||||
// SSE keepalive: send a comment every 15s to prevent LB/proxy timeout
|
||||
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const KEEPALIVE_INTERVAL_MS = 15_000;
|
||||
|
||||
function startKeepalive(controller: ReadableStreamDefaultController) {
|
||||
if (keepaliveTimer) return;
|
||||
keepaliveTimer = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||
} catch {
|
||||
if (keepaliveTimer) clearInterval(keepaliveTimer);
|
||||
}
|
||||
try { controller.enqueue(encoder.encode(": keepalive\n\n")); } catch { stopKeepalive(); }
|
||||
}, KEEPALIVE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopKeepalive() {
|
||||
if (keepaliveTimer) {
|
||||
clearInterval(keepaliveTimer);
|
||||
keepaliveTimer = null;
|
||||
}
|
||||
if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
|
||||
}
|
||||
|
||||
return new ReadableStream({
|
||||
@@ -481,120 +567,35 @@ function transformAnthropicStream(
|
||||
|
||||
if (phase === "init") {
|
||||
phase = "block";
|
||||
messageId = `msg_${Date.now()}`;
|
||||
|
||||
const startEvent = `event: message_start\ndata: ${JSON.stringify({
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: messageId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [],
|
||||
model,
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage,
|
||||
},
|
||||
})}`;
|
||||
controller.enqueue(encoder.encode(startEvent + "\n\n"));
|
||||
|
||||
const blockStart = `event: content_block_start\ndata: ${JSON.stringify({
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: { type: "text", text: "" },
|
||||
})}`;
|
||||
controller.enqueue(encoder.encode(blockStart + "\n\n"));
|
||||
emitInitEvents(controller, encoder, model, `msg_${Date.now()}`, usage);
|
||||
}
|
||||
|
||||
while (phase === "block") {
|
||||
const BATCH_SIZE = 8;
|
||||
let chunksProcessed = 0;
|
||||
const BATCH_SIZE = 8;
|
||||
let chunksProcessed = 0;
|
||||
|
||||
while (phase === "block") {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
stopKeepalive();
|
||||
releaseReader(reader);
|
||||
const remaining = lineBuffer.flush();
|
||||
if (remaining.length > 0) {
|
||||
const adapted = backendLineToAnthropicSSE(remaining, model, config, usage, outputCounter);
|
||||
if (adapted) {
|
||||
controller.enqueue(encoder.encode(adapted + "\n\n"));
|
||||
}
|
||||
}
|
||||
phase = "done";
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = lineBuffer.add(chunk);
|
||||
|
||||
for (const line of lines) {
|
||||
const adapted = backendLineToAnthropicSSE(line, model, config, usage, outputCounter);
|
||||
if (adapted) {
|
||||
controller.enqueue(encoder.encode(adapted + "\n\n"));
|
||||
}
|
||||
}
|
||||
|
||||
chunksProcessed++;
|
||||
if (chunksProcessed >= BATCH_SIZE) {
|
||||
chunksProcessed = 0;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
return;
|
||||
}
|
||||
while (phase === "block" && chunksProcessed < BATCH_SIZE) {
|
||||
const isDone = await processStreamChunk(reader, lineBuffer, decoder, controller, encoder, model, config, usage, outputCounter);
|
||||
if (isDone) {
|
||||
stopKeepalive();
|
||||
releaseReader(reader);
|
||||
phase = "done";
|
||||
break;
|
||||
}
|
||||
chunksProcessed++;
|
||||
}
|
||||
|
||||
if (chunksProcessed >= BATCH_SIZE) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === "done") {
|
||||
phase = "done";
|
||||
|
||||
// Estimate output tokens from char count as fallback
|
||||
// when the backend doesn't report usage in SSE lines.
|
||||
if (usage.output_tokens === 0 && outputCounter.chars > 0) {
|
||||
usage.output_tokens = Math.max(1, Math.round(outputCounter.chars / 4));
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
|
||||
),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`event: message_delta\ndata: ${JSON.stringify({
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn", stop_sequence: null },
|
||||
usage: { output_tokens: usage.output_tokens },
|
||||
})}\n\n`,
|
||||
),
|
||||
);
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'event: message_stop\ndata: {"type":"message_stop"}\n\n',
|
||||
),
|
||||
);
|
||||
|
||||
controller.close();
|
||||
emitDoneEvents(controller, encoder, usage, outputCounter);
|
||||
}
|
||||
} catch (err) {
|
||||
stopKeepalive();
|
||||
releaseReader(reader);
|
||||
if (isDevMode()) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`event: error\ndata: ${JSON.stringify({ error: String(err) })}\n\n`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'event: error\ndata: {"error":"Stream error"}\n\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
controller.close();
|
||||
emitErrorEvent(controller, encoder, err);
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
@@ -687,6 +688,73 @@ function anthropicError(status: number, message: string, type: string): Response
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a backend error response: parse upstream body, release session, return Anthropic error.
|
||||
*/
|
||||
async function handleUpstreamError(
|
||||
response: Response,
|
||||
sessionPool?: SessionProxyPool,
|
||||
sessionId?: string,
|
||||
): Promise<Response> {
|
||||
const status = response.status;
|
||||
let upstreamMsg = status >= 500 ? "Upstream server error" : "Upstream rejected request";
|
||||
try {
|
||||
const errBody = await response.text();
|
||||
if (errBody) {
|
||||
const errJson = JSON.parse(errBody);
|
||||
if (errJson?.error?.message) upstreamMsg = errJson.error.message;
|
||||
else if (errJson?.type === "error" && errJson?.error?.message) upstreamMsg = errJson.error.message;
|
||||
else if (errJson?.message) upstreamMsg = errJson.message;
|
||||
else if (typeof errBody === "string" && errBody.length < 500) upstreamMsg = errBody;
|
||||
}
|
||||
} catch { /* keep default */ }
|
||||
if (sessionPool && sessionId) sessionPool.release(sessionId);
|
||||
return anthropicError(status, upstreamMsg, "upstream_error");
|
||||
}
|
||||
|
||||
/** Build streaming response headers with CORS and cache forwarding. */
|
||||
function buildStreamHeaders(response: Response): Record<string, string> {
|
||||
const h: Record<string, string> = {
|
||||
"Content-Type": "text/event-stream", "Cache-Control": "no-cache",
|
||||
Connection: "keep-alive", "Access-Control-Allow-Origin": "*", "X-Accel-Buffering": "no",
|
||||
};
|
||||
forwardCacheHeaders(response, h);
|
||||
return h;
|
||||
}
|
||||
|
||||
/** Build JSON response headers with CORS and cache forwarding. */
|
||||
function buildJsonHeaders(response: Response): Record<string, string> {
|
||||
const h: Record<string, string> = {
|
||||
"Content-Type": "application/json", "Access-Control-Allow-Origin": "*",
|
||||
};
|
||||
forwardCacheHeaders(response, h);
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a non-streaming backend response that came back in SSE format
|
||||
* (it happens when the backend only supports SSE but we asked for non-stream).
|
||||
* Accumulates the text deltas into a single Anthropic response.
|
||||
*/
|
||||
function handleBackendSSEExtract(
|
||||
text: string,
|
||||
model: string,
|
||||
): AnthropicResponse | null {
|
||||
if (!text.trimStart().startsWith("data: ")) return null;
|
||||
const accumulated = accumulateSSEText(text);
|
||||
if (!accumulated) return null;
|
||||
return {
|
||||
id: `msg_${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: accumulated }],
|
||||
model,
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 0, output_tokens: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// --- Main handler --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -758,11 +826,9 @@ export async function handleAnthropicMessages(
|
||||
const wantsStream = req.stream === true;
|
||||
|
||||
// Default anthropic-version to 2023-06-01 (required for prompt caching).
|
||||
// The client can override via the anthropic-version header.
|
||||
const version = anthropicVersion || "2023-06-01";
|
||||
|
||||
// Translate Anthropic -> backend, preserving cache_control directives
|
||||
// and forwarding anthropic-version when available
|
||||
// Translate Anthropic -> backend
|
||||
const { body: backendBody, headers: extraHeaders } = anthropicToBackend(
|
||||
req, config, backendModel, version,
|
||||
);
|
||||
@@ -782,23 +848,15 @@ export async function handleAnthropicMessages(
|
||||
: await fetchWithRetry(url, init, proxyPool, `anthropic:${req.model}`, ipv6Source);
|
||||
|
||||
if (result.errorClassification) {
|
||||
if (sessionPool && sessionId) {
|
||||
sessionPool.release(sessionId);
|
||||
}
|
||||
if (sessionPool && sessionId) sessionPool.release(sessionId);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error: {
|
||||
message: result.errorClassification.message,
|
||||
type: "server_error",
|
||||
},
|
||||
error: { message: result.errorClassification.message, type: "server_error" },
|
||||
}),
|
||||
{
|
||||
status: result.errorClassification.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -807,124 +865,43 @@ export async function handleAnthropicMessages(
|
||||
|
||||
// -- Handle error responses from backend ------------------------------------
|
||||
if (!response.ok) {
|
||||
const status = response.status;
|
||||
// Read the upstream error body so clients see the actual rejection reason,
|
||||
// not just a generic "Upstream rejected request" message.
|
||||
let upstreamMsg = status >= 500 ? "Upstream server error" : "Upstream rejected request";
|
||||
try {
|
||||
const errBody = await response.text();
|
||||
if (errBody) {
|
||||
const errJson = JSON.parse(errBody);
|
||||
// OpenAI-style: { error: { message, type } }
|
||||
if (errJson?.error?.message) {
|
||||
upstreamMsg = errJson.error.message;
|
||||
}
|
||||
// Anthropic-style: { type: "error", error: { message, type } }
|
||||
else if (errJson?.type === "error" && errJson?.error?.message) {
|
||||
upstreamMsg = errJson.error.message;
|
||||
}
|
||||
// Plain JSON with message field
|
||||
else if (errJson?.message) {
|
||||
upstreamMsg = errJson.message;
|
||||
}
|
||||
// Raw text error body
|
||||
else if (typeof errBody === "string" && errBody.length < 500) {
|
||||
upstreamMsg = errBody;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Could not read error body — keep generic message
|
||||
}
|
||||
if (sessionPool && sessionId) {
|
||||
sessionPool.release(sessionId);
|
||||
}
|
||||
return anthropicError(status, upstreamMsg, "upstream_error");
|
||||
return handleUpstreamError(response, sessionPool, sessionId);
|
||||
}
|
||||
|
||||
// -- For native Anthropic passthrough, relay the raw backend response -------
|
||||
if (config.anthropicPassthrough) {
|
||||
if (wantsStream) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Accel-Buffering": "no",
|
||||
};
|
||||
forwardCacheHeaders(response, headers);
|
||||
return new Response(wrapAnthropicStreamMaybe(response.body!, sessionPool, sessionId), {
|
||||
status: 200,
|
||||
headers,
|
||||
headers: buildStreamHeaders(response),
|
||||
});
|
||||
}
|
||||
const rawBody = await response.text();
|
||||
if (sessionPool && sessionId) {
|
||||
sessionPool.release(sessionId);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
};
|
||||
forwardCacheHeaders(response, headers);
|
||||
return new Response(rawBody, { status: 200, headers });
|
||||
if (sessionPool && sessionId) sessionPool.release(sessionId);
|
||||
return new Response(rawBody, { status: 200, headers: buildJsonHeaders(response) });
|
||||
}
|
||||
|
||||
// -- Handle streaming (OpenAI-compatible backend) ---------------------------
|
||||
if (wantsStream) {
|
||||
let transformed = transformAnthropicStream(
|
||||
response.body!,
|
||||
req.model,
|
||||
config,
|
||||
);
|
||||
let transformed = transformAnthropicStream(response.body!, req.model, config);
|
||||
transformed = wrapAnthropicStreamMaybe(transformed, sessionPool, sessionId);
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Accel-Buffering": "no",
|
||||
};
|
||||
forwardCacheHeaders(response, headers);
|
||||
return new Response(transformed, { status: 200, headers });
|
||||
return new Response(transformed, { status: 200, headers: buildStreamHeaders(response) });
|
||||
}
|
||||
|
||||
// -- Handle non-streaming (OpenAI-compatible backend) -----------------------
|
||||
const text = await response.text();
|
||||
if (sessionPool && sessionId) {
|
||||
sessionPool.release(sessionId);
|
||||
}
|
||||
if (sessionPool && sessionId) sessionPool.release(sessionId);
|
||||
|
||||
const buildBaseHeaders = (): Record<string, string> => {
|
||||
const h: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
};
|
||||
forwardCacheHeaders(response, h);
|
||||
return h;
|
||||
};
|
||||
|
||||
if (text.trimStart().startsWith("data: ")) {
|
||||
const accumulated = accumulateSSEText(text);
|
||||
if (accumulated) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: `msg_${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: accumulated }],
|
||||
model: req.model,
|
||||
stop_reason: "end_turn",
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 0, output_tokens: 0 },
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: buildBaseHeaders(),
|
||||
},
|
||||
);
|
||||
}
|
||||
// Backend returned SSE even though we didn't ask for stream
|
||||
const sseAdapted = handleBackendSSEExtract(text, req.model);
|
||||
if (sseAdapted) {
|
||||
return new Response(JSON.stringify(sseAdapted), {
|
||||
status: 200,
|
||||
headers: buildJsonHeaders(response),
|
||||
});
|
||||
}
|
||||
|
||||
// Parse JSON and adapt
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
@@ -933,10 +910,9 @@ export async function handleAnthropicMessages(
|
||||
}
|
||||
|
||||
const adapted = backendToAnthropicResponse(parsed, req.model);
|
||||
|
||||
return new Response(JSON.stringify(adapted), {
|
||||
status: 200,
|
||||
headers: buildBaseHeaders(),
|
||||
headers: buildJsonHeaders(response),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user