diff --git a/open-sse/services/combo.js b/open-sse/services/combo.js index 85a874b9..011b484c 100644 --- a/open-sse/services/combo.js +++ b/open-sse/services/combo.js @@ -52,11 +52,20 @@ export async function handleComboChat({ body, models, handleSingleModel, log }) let errorText = result.statusText || ""; try { const errorBody = await result.clone().json(); - errorText = errorBody.error || errorBody.message || errorText; + errorText = errorBody?.error ?? errorBody?.message ?? errorText; } catch { // Ignore JSON parse errors } + // Normalize error text to string (Worker-safe) + if (typeof errorText !== "string") { + try { + errorText = JSON.stringify(errorText); + } catch { + errorText = String(errorText); + } + } + // Check if should fallback to next model const { shouldFallback } = checkFallbackError(result.status, errorText); diff --git a/open-sse/translator/request/claude-to-openai.js b/open-sse/translator/request/claude-to-openai.js index 98d6006b..d9545933 100644 --- a/open-sse/translator/request/claude-to-openai.js +++ b/open-sse/translator/request/claude-to-openai.js @@ -3,7 +3,7 @@ import { FORMATS } from "../formats.js"; import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; // Convert Claude request to OpenAI format -function claudeToOpenAIRequest(model, body, stream) { +export function claudeToOpenAIRequest(model, body, stream) { const result = { model: model, messages: [], diff --git a/open-sse/translator/request/gemini-to-openai.js b/open-sse/translator/request/gemini-to-openai.js index 87f88839..5b3d0f01 100644 --- a/open-sse/translator/request/gemini-to-openai.js +++ b/open-sse/translator/request/gemini-to-openai.js @@ -3,7 +3,7 @@ import { FORMATS } from "../formats.js"; import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; // Convert Gemini request to OpenAI format -function geminiToOpenAIRequest(model, body, stream) { +export function geminiToOpenAIRequest(model, body, stream) { const result = { model: model, messages: [], diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index d2ffa54c..4467c8e7 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -10,7 +10,7 @@ import { FORMATS } from "../formats.js"; /** * Convert OpenAI Responses API request to OpenAI Chat Completions format */ -function openaiResponsesToOpenAIRequest(model, body, stream, credentials) { +export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) { if (!body.input) return body; const result = { ...body }; @@ -134,7 +134,7 @@ function openaiResponsesToOpenAIRequest(model, body, stream, credentials) { /** * Convert OpenAI Chat Completions to OpenAI Responses API format */ -function openaiToOpenAIResponsesRequest(model, body, stream, credentials) { +export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) { const result = { model, input: [], diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index 3dd98ecc..8ab59187 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -7,7 +7,7 @@ import { adjustMaxTokens } from "../helpers/maxTokensHelper.js"; const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; // Convert OpenAI request to Claude format -function openaiToClaudeRequest(model, body, stream) { +export function openaiToClaudeRequest(model, body, stream) { // Tool name mapping for Claude OAuth (capitalizedName → originalName) const toolNameMap = new Map(); const result = { diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index fa6bde4e..e09b436a 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -191,12 +191,12 @@ function openaiToGeminiBase(model, body, stream) { } // OpenAI -> Gemini (standard API) -function openaiToGeminiRequest(model, body, stream) { +export function openaiToGeminiRequest(model, body, stream) { return openaiToGeminiBase(model, body, stream); } // OpenAI -> Gemini CLI (Cloud Code Assist) -function openaiToGeminiCLIRequest(model, body, stream) { +export function openaiToGeminiCLIRequest(model, body, stream) { const gemini = openaiToGeminiBase(model, body, stream); const isClaude = model.toLowerCase().includes("claude"); @@ -386,7 +386,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu } // OpenAI -> Antigravity (Sandbox Cloud Code with wrapper) -function openaiToAntigravityRequest(model, body, stream, credentials = null) { +export function openaiToAntigravityRequest(model, body, stream, credentials = null) { const isClaude = model.toLowerCase().includes("claude"); if (isClaude) { diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index eb91d470..75ac04f6 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -230,7 +230,7 @@ function convertMessages(messages, tools, model) { /** * Build Kiro payload from OpenAI format */ -function buildKiroPayload(model, body, stream, credentials) { +export function buildKiroPayload(model, body, stream, credentials) { const messages = body.messages || []; const tools = body.tools || []; const maxTokens = 32000; @@ -278,5 +278,3 @@ function buildKiroPayload(model, body, stream, credentials) { } register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null); - -export { buildKiroPayload }; diff --git a/open-sse/translator/response/claude-to-openai.js b/open-sse/translator/response/claude-to-openai.js index 3c2ce2d4..09ead173 100644 --- a/open-sse/translator/response/claude-to-openai.js +++ b/open-sse/translator/response/claude-to-openai.js @@ -17,7 +17,7 @@ function createChunk(state, delta, finishReason = null) { } // Convert Claude stream chunk to OpenAI format -function claudeToOpenAIResponse(chunk, state) { +export function claudeToOpenAIResponse(chunk, state) { if (!chunk) return null; const results = []; diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 1a73b7ad..6abecaa6 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -2,7 +2,7 @@ import { register } from "../index.js"; import { FORMATS } from "../formats.js"; // Convert Gemini response chunk to OpenAI format -function geminiToOpenAIResponse(chunk, state) { +export function geminiToOpenAIResponse(chunk, state) { if (!chunk) return null; // Handle Antigravity wrapper diff --git a/open-sse/translator/response/kiro-to-openai.js b/open-sse/translator/response/kiro-to-openai.js index 5f3d136a..4fe0b624 100644 --- a/open-sse/translator/response/kiro-to-openai.js +++ b/open-sse/translator/response/kiro-to-openai.js @@ -9,7 +9,7 @@ import { FORMATS } from "../formats.js"; * Parse Kiro SSE event and convert to OpenAI format * Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc. */ -function convertKiroToOpenAI(chunk, state) { +export function convertKiroToOpenAI(chunk, state) { if (!chunk) return null; @@ -181,5 +181,3 @@ function convertKiroToOpenAI(chunk, state) { // Register translator register(FORMATS.KIRO, FORMATS.OPENAI, null, convertKiroToOpenAI); - -export { convertKiroToOpenAI }; diff --git a/open-sse/translator/response/openai-responses.js b/open-sse/translator/response/openai-responses.js index 3596b5fc..9789bbbc 100644 --- a/open-sse/translator/response/openai-responses.js +++ b/open-sse/translator/response/openai-responses.js @@ -9,7 +9,7 @@ import { FORMATS } from "../formats.js"; * Translate OpenAI chunk to Responses API events * @returns {Array} Array of events with { event, data } structure */ -function openaiToOpenAIResponsesResponse(chunk, state) { +export function openaiToOpenAIResponsesResponse(chunk, state) { if (!chunk) { return flushEvents(state); } @@ -359,7 +359,7 @@ function flushEvents(state) { * Translate OpenAI Responses API chunk to OpenAI Chat Completions format * This is for when Codex returns data and we need to send it to an OpenAI-compatible client */ -function openaiResponsesToOpenAIResponse(chunk, state) { +export function openaiResponsesToOpenAIResponse(chunk, state) { if (!chunk) { // Flush: send final chunk with finish_reason if (!state.finishReasonSent && state.started) { diff --git a/open-sse/translator/response/openai-to-claude.js b/open-sse/translator/response/openai-to-claude.js index ca4270e7..ad625ed2 100644 --- a/open-sse/translator/response/openai-to-claude.js +++ b/open-sse/translator/response/openai-to-claude.js @@ -26,7 +26,7 @@ function stopTextBlock(state, results) { } // Convert OpenAI stream chunk to Claude format -function openaiToClaudeResponse(chunk, state) { +export function openaiToClaudeResponse(chunk, state) { if (!chunk || !chunk.choices?.[0]) return null; const results = []; diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index d3bd4289..76ab5521 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -2,6 +2,10 @@ import { translateResponse, initState } from "../translator/index.js"; import { FORMATS } from "../translator/formats.js"; import { saveRequestUsage, trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js"; +// Singleton TextEncoder/Decoder for performance (reuse across all streams) +const sharedDecoder = new TextDecoder(); +const sharedEncoder = new TextEncoder(); + // Get HH:MM:SS timestamp function getTimeString() { return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }); @@ -91,9 +95,9 @@ function logUsage(provider, usage, model = null, connectionId = null) { }); } -// Parse SSE data line +// Parse SSE data line (optimized - reduce string operations) function parseSSELine(line) { - if (!line || !line.startsWith("data:")) return null; + if (!line || line.charCodeAt(0) !== 100) return null; // 'd' = 100 const data = line.slice(5).trim(); if (data === "[DONE]") return { done: true }; @@ -178,8 +182,6 @@ export function createSSEStream(options = {}) { connectionId = null } = options; - const decoder = new TextDecoder(); - const encoder = new TextEncoder(); let buffer = ""; let usage = null; @@ -188,7 +190,7 @@ export function createSSEStream(options = {}) { return new TransformStream({ transform(chunk, controller) { - const text = decoder.decode(chunk, { stream: true }); + const text = sharedDecoder.decode(chunk, { stream: true }); buffer += text; reqLogger?.appendProviderChunk?.(text); @@ -215,7 +217,7 @@ export function createSSEStream(options = {}) { output = line + "\n"; } reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); continue; } @@ -228,7 +230,7 @@ export function createSSEStream(options = {}) { if (parsed && parsed.done) { const output = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); continue; } @@ -251,7 +253,7 @@ export function createSSEStream(options = {}) { for (const item of translated) { const output = formatSSE(item, sourceFormat); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); } } } @@ -260,7 +262,7 @@ export function createSSEStream(options = {}) { flush(controller) { trackPendingRequest(model, provider, connectionId, false); try { - const remaining = decoder.decode(); + const remaining = sharedDecoder.decode(); if (remaining) buffer += remaining; if (mode === STREAM_MODE.PASSTHROUGH) { @@ -270,7 +272,7 @@ export function createSSEStream(options = {}) { output = "data: " + buffer.slice(5); } reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); } if (usage) { logUsage(provider, usage, model, connectionId); @@ -299,7 +301,7 @@ export function createSSEStream(options = {}) { for (const item of translated) { const output = formatSSE(item, sourceFormat); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); } } } @@ -320,14 +322,14 @@ export function createSSEStream(options = {}) { for (const item of flushed) { const output = formatSSE(item, sourceFormat); reqLogger?.appendConvertedChunk?.(output); - controller.enqueue(encoder.encode(output)); + controller.enqueue(sharedEncoder.encode(output)); } } // Send [DONE] and log usage const doneOutput = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(doneOutput); - controller.enqueue(encoder.encode(doneOutput)); + controller.enqueue(sharedEncoder.encode(doneOutput)); if (state?.usage) { logUsage(state.provider || targetFormat, state.usage, model, connectionId);