diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 876ee51db1..704f3797c2 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2888,17 +2888,18 @@ export async function handleChatCore({ } else { try { responseBody = rawBody ? JSON.parse(rawBody) : {}; - } catch { + } catch (err) { appendRequestLog({ model, provider, connectionId, status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}`, }).catch(() => {}); + const detailedError = `Invalid JSON response from provider (error: ${err instanceof Error ? err.message : String(err)}): ${rawBody.substring(0, 1000)}`; const invalidJsonMessage = "Invalid JSON response from provider"; persistAttemptLogs({ status: HTTP_STATUS.BAD_GATEWAY, - error: invalidJsonMessage, + error: detailedError, providerRequest: finalBody || translatedBody, providerResponse: normalizedProviderPayload, clientResponse: buildErrorBody(HTTP_STATUS.BAD_GATEWAY, invalidJsonMessage), diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 127fcbb6ae..0988c224fe 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -127,7 +127,7 @@ function toTrimmedString(value): string | null { * 1. Body is valid JSON * 2. Has at least one choice with non-empty content or tool_calls */ -async function validateResponseQuality( +export async function validateResponseQuality( response: Response, isStreaming: boolean, log: { warn?: (...args: unknown[]) => void } @@ -161,7 +161,7 @@ async function validateResponseQuality( try { json = JSON.parse(text); } catch { - if (text.startsWith("data:")) return { valid: true }; + if (text.startsWith("data:") || text.startsWith("event:")) return { valid: true }; return { valid: false, reason: "response is not valid JSON" }; } diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts index 4099dec224..fb3665c790 100644 --- a/open-sse/translator/request/claude-to-gemini.ts +++ b/open-sse/translator/request/claude-to-gemini.ts @@ -178,7 +178,9 @@ export function claudeToGeminiRequest(model, body, stream) { // ── Thinking config ──────────────────────────────────────────── // Priority: thinking.budget_tokens (Claude native) > output_config.effort (Claude Code). - if (body.thinking?.type === "enabled" && body.thinking.budget_tokens) { + if (model.startsWith("gemma-4")) { + // gemma-4 models returns - 400: Thinking budget is not supported for this model + } else if (body.thinking?.type === "enabled" && body.thinking.budget_tokens) { result.generationConfig.thinkingConfig = { thinkingBudget: body.thinking.budget_tokens, includeThoughts: true, diff --git a/src/shared/components/RequestLoggerDetail.tsx b/src/shared/components/RequestLoggerDetail.tsx index f599f1d87e..27da5d8b1b 100644 --- a/src/shared/components/RequestLoggerDetail.tsx +++ b/src/shared/components/RequestLoggerDetail.tsx @@ -45,14 +45,23 @@ function PayloadSection({ title, json, onCopy }) { // ─── Detail Modal ─────────────────────────────────────────────────────────── -export default function RequestLoggerDetail({ log, detail, loading, onClose, onCopy }) { +type StreamChunks = Record; + +export default function RequestLoggerDetail({ + log, + detail, + loading, + debugEnabled, + onClose, + onCopy, +}) { // Close on Escape key useEffect(() => { const handler = (e) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); + globalThis.addEventListener("keydown", handler); + return () => globalThis.removeEventListener("keydown", handler); }, [onClose]); const statusStyle = getStatusStyle(log.status); @@ -64,6 +73,9 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC label: (log.provider || "-").toUpperCase(), }; + const providerStatus = detail?.pipelinePayloads?.providerResponse?.status; + const hasStatusDiscrepancy = providerStatus && providerStatus !== log.status; + const formatDate = (iso) => { try { const d = new Date(iso); @@ -104,6 +116,36 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC : []; const requestJson = detail?.requestBody ? toPrettyJson(detail.requestBody) : null; const responseJson = detail?.responseBody ? toPrettyJson(detail.responseBody) : null; + const streamChunksText = (() => { + if (!debugEnabled || !detail?.pipelinePayloads?.streamChunks) return null; + let chunks: StreamChunks = detail.pipelinePayloads.streamChunks; + + // If stored as a JSON string, try to parse it so we can render joined raw chunks + if (typeof chunks === "string") { + try { + const parsed = JSON.parse(chunks); + chunks = parsed; + } catch { + // Keep as string and return raw text (don't JSON-stringify) + return chunks; + } + } + + if (chunks && typeof chunks === "object") { + try { + return Object.entries(chunks) + .map(([stage, arr]) => { + const joined = Array.isArray(arr) ? arr.join("") : String(arr); + return `--- ${stage} ---\n${joined}`; + }) + .join("\n\n"); + } catch { + return toPrettyJson(chunks); + } + } + + return null; + })(); const detailIssue = detail?.detailState === "missing" ? "Detailed payload artifact is no longer available for this log entry." @@ -144,14 +186,28 @@ export default function RequestLoggerDetail({ log, detail, loading, onClose, onC {/* Modal Header */}
- - {log.status} - - {log.method} - {log.path} +
+
+ + {log.status} + + {hasStatusDiscrepancy && ( + + Upstream: {providerStatus} + + )} + {log.method} +
+ {hasStatusDiscrepancy && ( + + OmniRoute returned {log.status} even though provider returned {providerStatus} + + )} +
+ {log.path}