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
This commit is contained in:
Han Xiao 2025-12-13 12:07:37 +01:00
parent 579fd95fff
commit d44cec6524
6 changed files with 91 additions and 25 deletions

View File

@ -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
},

View File

@ -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": {

10
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<T> {
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,29 +244,43 @@ export class ObjectGeneratorSafe {
private async handleGenerateObjectError<T>(error: unknown): Promise<GenerateObjectResult<T>> {
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);
const jsonrepair = await getJsonRepair();
const repairedJson = jsonrepair(rawText);
const repairedResponse = JSON.parse(repairedJson);
logDebug('jsonrepair parse success!');
return {
object: repairedResponse as T,
usage: (error as any).usage
};
} 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('Both JSON and Hjson parsing failed:', { error: hjsonError });
logError('All JSON parsing attempts failed (JSON, jsonrepair, Hjson):', { error: hjsonError });
throw error;
}
}
}
}
throw error;
}
}

View File

@ -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) {