From d44cec6524d927d2bd1687e0f4b16847a16685bf Mon Sep 17 00:00:00 2001 From: Han Xiao Date: Sat, 13 Dec 2025 12:07:37 +0100 Subject: [PATCH] fix: improve json parsing resilience and disable gemini thinking - add jsonrepair fallback for truncated LLM output - disable gemini built-in thinking mode (thinkingBudget: 0) - increase token limits for errorAnalyzer, queryRewriter, serpCluster - switch production default to gemini-2.5-flash-lite - fix normalizeHostName to handle wildcard patterns --- config.json | 13 ++++++--- jina-ai/config.json | 24 +++++++++------- package-lock.json | 10 +++++++ package.json | 1 + src/utils/safe-generator.ts | 56 +++++++++++++++++++++++++++++-------- src/utils/url-tools.ts | 12 ++++++++ 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/config.json b/config.json index cc1a4ad..fcce234 100644 --- a/config.json +++ b/config.json @@ -38,14 +38,19 @@ }, "evaluator": { "temperature": 0.6, - "maxTokens": 200 + "maxTokens": 1000 + }, + "errorAnalyzer": { + "maxTokens": 4000 }, - "errorAnalyzer": {}, "queryRewriter": { - "temperature": 0.1 + "temperature": 0.1, + "maxTokens": 4000 }, "researchPlanner": {}, - "serpCluster": {}, + "serpCluster": { + "maxTokens": 4000 + }, "agent": { "temperature": 0.7 }, diff --git a/jina-ai/config.json b/jina-ai/config.json index 09293de..2ae774c 100644 --- a/jina-ai/config.json +++ b/jina-ai/config.json @@ -34,31 +34,35 @@ "models": { "gemini": { "default": { - "model": "gemini-2.5-flash", + "model": "gemini-2.5-flash-lite", "temperature": 0.6, "maxTokens": 8000 }, "tools": { "coder": { - "maxTokens": 2000, - "model": "gemini-2.5-flash-lite" + "maxTokens": 2000 }, "researchPlanner": {}, "evaluator": { "maxTokens": 2000 }, - "serpCluster": {}, + "serpCluster": { + "maxTokens": 4000 + }, "errorAnalyzer": { - "maxTokens": 1000 + "maxTokens": 4000 }, "queryRewriter": { - "maxTokens": 2000 + "maxTokens": 4000 + }, + "agent": { + "model": "gemini-2.5-flash" + }, + "agentBeastMode": { + "model": "gemini-2.5-flash" }, - "agent": {}, - "agentBeastMode": {}, "fallback": { - "maxTokens": 8000, - "model": "gemini-2.5-flash-lite" + "maxTokens": 8000 }, "finalizer": {}, "reducer": { diff --git a/package-lock.json b/package-lock.json index 9dbd6f2..1531467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "express-validator": "^7.2.1", "hjson": "^3.2.2", "jsdom": "^26.0.0", + "jsonrepair": "^3.13.1", "node-fetch": "^3.3.2", "sharp": "^0.34.2", "undici": "^7.3.0", @@ -6230,6 +6231,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jsonrepair": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz", + "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", diff --git a/package.json b/package.json index d5d8c83..ade2f56 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "express-validator": "^7.2.1", "hjson": "^3.2.2", "jsdom": "^26.0.0", + "jsonrepair": "^3.13.1", "node-fetch": "^3.3.2", "sharp": "^0.34.2", "undici": "^7.3.0", diff --git a/src/utils/safe-generator.ts b/src/utils/safe-generator.ts index ed28fa4..912bb59 100644 --- a/src/utils/safe-generator.ts +++ b/src/utils/safe-generator.ts @@ -8,9 +8,15 @@ import { } from "ai"; import { TokenTracker } from "./token-tracker"; import { getModel, ToolName, getToolConfig } from "../config"; -import Hjson from 'hjson'; // Import Hjson library +import Hjson from 'hjson'; import { logError, logDebug, logWarning } from '../logging'; +// Dynamic import for ESM module +const getJsonRepair = async () => { + const { jsonrepair } = await import('jsonrepair'); + return jsonrepair; +}; + interface GenerateObjectResult { object: T; usage: LanguageModelUsage; @@ -154,6 +160,13 @@ export class ObjectGeneratorSafe { messages, maxTokens: getToolConfig(model).maxTokens, temperature: getToolConfig(model).temperature, + providerOptions: { + google: { + thinkingConfig: { + thinkingBudget: 0 // Disable Gemini's built-in thinking to avoid conflict with our schema's think field + } + } + } }); this.tokenTracker.trackUsage(model, result.usage); @@ -200,6 +213,13 @@ export class ObjectGeneratorSafe { schema: distilledSchema, prompt: `Following the given JSON schema, extract the field from below: \n\n ${failedOutput}`, temperature: getToolConfig('fallback').temperature, + providerOptions: { + google: { + thinkingConfig: { + thinkingBudget: 0 + } + } + } }); this.tokenTracker.trackUsage('fallback', fallbackResult.usage); // Track against fallback model @@ -224,26 +244,40 @@ export class ObjectGeneratorSafe { private async handleGenerateObjectError(error: unknown): Promise> { if (NoObjectGeneratedError.isInstance(error)) { logWarning('Object not generated according to schema, fallback to manual parsing', { error }); + const rawText = (error as any).text; + + // 1. First try standard JSON parsing try { - // First try standard JSON parsing - const partialResponse = JSON.parse((error as any).text); + const partialResponse = JSON.parse(rawText); logDebug('JSON parse success!'); return { object: partialResponse as T, usage: (error as any).usage }; - } catch (parseError) { - // Use Hjson to parse the error response for more lenient parsing + } catch (jsonError) { + // 2. Try jsonrepair to fix truncated/malformed JSON try { - const hjsonResponse = Hjson.parse((error as any).text); - logDebug('Hjson parse success!'); + const jsonrepair = await getJsonRepair(); + const repairedJson = jsonrepair(rawText); + const repairedResponse = JSON.parse(repairedJson); + logDebug('jsonrepair parse success!'); return { - object: hjsonResponse as T, + object: repairedResponse as T, usage: (error as any).usage }; - } catch (hjsonError) { - logError('Both JSON and Hjson parsing failed:', { error: hjsonError }); - throw error; + } catch (repairError) { + // 3. Try Hjson for lenient parsing (trailing commas, comments, etc.) + try { + const hjsonResponse = Hjson.parse(rawText); + logDebug('Hjson parse success!'); + return { + object: hjsonResponse as T, + usage: (error as any).usage + }; + } catch (hjsonError) { + logError('All JSON parsing attempts failed (JSON, jsonrepair, Hjson):', { error: hjsonError }); + throw error; + } } } } diff --git a/src/utils/url-tools.ts b/src/utils/url-tools.ts index 70c60ba..e64ed0f 100644 --- a/src/utils/url-tools.ts +++ b/src/utils/url-tools.ts @@ -194,6 +194,18 @@ const extractUrlParts = (urlStr: string) => { }; export const normalizeHostName = (hostStr: string) => { + // Handle wildcard patterns like *.medium.com + if (hostStr.startsWith('*.')) { + hostStr = hostStr.slice(2); + } + + // If it doesn't look like a URL (no protocol), just clean up the hostname directly + if (!hostStr.includes('://')) { + const cleaned = hostStr.startsWith('www.') ? hostStr.slice(4) : hostStr; + return cleaned.toLowerCase(); + } + + // Try to parse as URL const extract = extractUrlParts(hostStr); const host = extract.hostname; if (!host) {