diff --git a/llm-proxy/backend/src/controllers/ProxyController.js b/llm-proxy/backend/src/controllers/ProxyController.js index c5850e1..38aeed9 100644 --- a/llm-proxy/backend/src/controllers/ProxyController.js +++ b/llm-proxy/backend/src/controllers/ProxyController.js @@ -146,6 +146,47 @@ async function proxyToOllama(req, res) { } catch (e) { logger.error(`❌ Internal Server Error [${shortHash}]: ${e.message || e}`); + + // If the payload expects JSON, return a valid fallback JSON response instead of a raw 500/502 error object + // to prevent the App Script pipeline from crashing when trying to parse the error text. + let expectsJson = false; + if (payload.response_format && payload.response_format.type === "json_object") { + expectsJson = true; + } else if (messages) { + for (const msg of messages) { + if (msg.content && msg.content.toLowerCase().includes("json")) { + expectsJson = true; + break; + } + } + } + + if (expectsJson) { + const fallbackJson = JSON.stringify({ + final_evaluation: { + decision: "Error", + exclusion_code: "N/A", + reasoning: `Server Error: ${e.message || String(e)}` + }, + logic_trace: {} + }); + + return res.status(500).json({ + id: `error-${Date.now()}`, + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: payload.model || "unknown", + choices: [{ + index: 0, + message: { + role: "assistant", + content: fallbackJson + }, + finish_reason: "stop" + }] + }); + } + if (e.message && e.message.includes('fetch')) { return res.status(502).json({ error: `Ollama connection error: ${e.message}` }); } diff --git a/llm-proxy/backend/src/services/RoutingService.js b/llm-proxy/backend/src/services/RoutingService.js index 5743348..58c76a6 100644 --- a/llm-proxy/backend/src/services/RoutingService.js +++ b/llm-proxy/backend/src/services/RoutingService.js @@ -68,7 +68,23 @@ class RoutingService { } else { logger.warn(`⚠️ Invalid JSON detected for [${reqHash.substring(0, 8)}] on attempt ${attempt + 1}/${maxRetries}. Retrying...`); if (attempt === maxRetries - 1) { - throw new Error("LLM failed to produce valid JSON after retries."); + logger.warn(`⚠️ Exhausted retries for [${reqHash.substring(0, 8)}]. Returning fallback error JSON.`); + const fallbackJson = JSON.stringify({ + final_evaluation: { + decision: "Error", + exclusion_code: "N/A", + reasoning: "LLM failed to produce valid JSON after retries." + }, + logic_trace: {} + }); + + // Overwrite the content with our fallback + if (responseData && responseData.choices && responseData.choices.length > 0) { + if (!responseData.choices[0].message) { + responseData.choices[0].message = { role: "assistant" }; + } + responseData.choices[0].message.content = fallbackJson; + } } } } else { diff --git a/llm-proxy/backend/src/utils/parsers.js b/llm-proxy/backend/src/utils/parsers.js index 4182df8..be43f29 100644 --- a/llm-proxy/backend/src/utils/parsers.js +++ b/llm-proxy/backend/src/utils/parsers.js @@ -1,14 +1,59 @@ function optimisticRepairJson(text) { - // A robust regex to find JSON string values and escape unescaped double quotes and newlines - const pattern = /("\w+"\s*:\s*")([\s\S]*?)("\s*(?:,|}|]))/g; + // 1. Repair quotes in flat string values + const pattern = /("\w+"\s*:\s*")([\s\S]*?)("\s*(?:,|}|\]))/g; + + let repaired = text.replace(pattern, (match, start, content, end) => { + // If content contains standard object/array syntax or looks like nested JSON mapping, skip quote repair + if (/\{\s*\\?"\w+\\?"\s*:/.test(content)) { + return match; + } - return text.replace(pattern, (match, start, content, end) => { - // Escape double quotes by unescaping first to avoid double-escaping, then escape all let repairedContent = content.replace(/\\"/g, '"').replace(/"/g, '\\"'); - // Escape newlines - repairedContent = repairedContent.replace(/\n/g, '\\n').replace(/\r/g, ''); return start + repairedContent + end; }); + + // 2. Safely encode newlines and control characters everywhere inside strings + let inString = false; + let escapeNext = false; + let result = ''; + + for (let i = 0; i < repaired.length; i++) { + const char = repaired[i]; + + if (escapeNext) { + result += char; + escapeNext = false; + continue; + } + + if (char === '\\') { + result += char; + escapeNext = true; + continue; + } + + if (char === '"') { + inString = !inString; + result += char; + continue; + } + + if (inString) { + if (char === '\n') { + result += '\\n'; + } else if (char === '\r') { + // skip + } else if (char === '\t') { + result += '\\t'; + } else { + result += char; + } + } else { + result += char; + } + } + + return result; } function extractJsonFromMixedText(text) {