mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
fix: OpenAI tools (#328)
- MCP tool params schema lost causing all MCP not working well - Compatible with occasional llm return tool call parameters that are invalid json
This commit is contained in:
149
packages/core/src/utils/safeJsonParse.test.ts
Normal file
149
packages/core/src/utils/safeJsonParse.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { safeJsonParse } from './safeJsonParse.js';
|
||||
|
||||
describe('safeJsonParse', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('valid JSON parsing', () => {
|
||||
it('should parse valid JSON correctly', () => {
|
||||
const validJson = '{"name": "test", "value": 123}';
|
||||
const result = safeJsonParse(validJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
});
|
||||
|
||||
it('should parse valid JSON arrays', () => {
|
||||
const validArray = '["item1", "item2", "item3"]';
|
||||
const result = safeJsonParse(validArray);
|
||||
|
||||
expect(result).toEqual(['item1', 'item2', 'item3']);
|
||||
});
|
||||
|
||||
it('should parse valid JSON with nested objects', () => {
|
||||
const validNested =
|
||||
'{"config": {"paths": ["testlogs/*.py"], "options": {"recursive": true}}}';
|
||||
const result = safeJsonParse(validNested);
|
||||
|
||||
expect(result).toEqual({
|
||||
config: {
|
||||
paths: ['testlogs/*.py'],
|
||||
options: { recursive: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('malformed JSON with jsonrepair fallback', () => {
|
||||
it('should handle malformed JSON with single quotes', () => {
|
||||
const malformedJson = "{'name': 'test', 'value': 123}";
|
||||
const result = safeJsonParse(malformedJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
});
|
||||
|
||||
it('should handle malformed JSON with unquoted keys', () => {
|
||||
const malformedJson = '{name: "test", value: 123}';
|
||||
const result = safeJsonParse(malformedJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
});
|
||||
|
||||
it('should handle malformed JSON with trailing commas', () => {
|
||||
const malformedJson = '{"name": "test", "value": 123,}';
|
||||
const result = safeJsonParse(malformedJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
});
|
||||
|
||||
it('should handle malformed JSON with comments', () => {
|
||||
const malformedJson = '{"name": "test", // comment\n "value": 123}';
|
||||
const result = safeJsonParse(malformedJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fallback behavior', () => {
|
||||
it('should return fallback value for empty string', () => {
|
||||
const emptyString = '';
|
||||
const fallback = { default: 'value' };
|
||||
|
||||
const result = safeJsonParse(emptyString, fallback);
|
||||
|
||||
expect(result).toEqual(fallback);
|
||||
});
|
||||
|
||||
it('should return fallback value for null input', () => {
|
||||
const nullInput = null as unknown as string;
|
||||
const fallback = { default: 'value' };
|
||||
|
||||
const result = safeJsonParse(nullInput, fallback);
|
||||
|
||||
expect(result).toEqual(fallback);
|
||||
});
|
||||
|
||||
it('should return fallback value for undefined input', () => {
|
||||
const undefinedInput = undefined as unknown as string;
|
||||
const fallback = { default: 'value' };
|
||||
|
||||
const result = safeJsonParse(undefinedInput, fallback);
|
||||
|
||||
expect(result).toEqual(fallback);
|
||||
});
|
||||
|
||||
it('should return empty object as default fallback', () => {
|
||||
const invalidJson = 'invalid json';
|
||||
|
||||
const result = safeJsonParse(invalidJson);
|
||||
|
||||
// jsonrepair returns the original string for completely invalid JSON
|
||||
expect(result).toEqual('invalid json');
|
||||
});
|
||||
|
||||
it('should return custom fallback when parsing fails', () => {
|
||||
const invalidJson = 'invalid json';
|
||||
const customFallback = { error: 'parsing failed', data: null };
|
||||
|
||||
const result = safeJsonParse(invalidJson, customFallback);
|
||||
|
||||
// jsonrepair returns the original string for completely invalid JSON
|
||||
expect(result).toEqual('invalid json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety', () => {
|
||||
it('should preserve generic type when parsing valid JSON', () => {
|
||||
const validJson = '{"name": "test", "value": 123}';
|
||||
const result = safeJsonParse<{ name: string; value: number }>(validJson);
|
||||
|
||||
expect(result).toEqual({ name: 'test', value: 123 });
|
||||
// TypeScript should infer the correct type
|
||||
expect(typeof result.name).toBe('string');
|
||||
expect(typeof result.value).toBe('number');
|
||||
});
|
||||
|
||||
it('should return fallback type when parsing fails', () => {
|
||||
const invalidJson = 'invalid json';
|
||||
const fallback = { error: 'fallback' } as const;
|
||||
|
||||
const result = safeJsonParse(invalidJson, fallback);
|
||||
|
||||
// jsonrepair returns the original string for completely invalid JSON
|
||||
expect(result).toEqual('invalid json');
|
||||
// TypeScript should preserve the fallback type
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
45
packages/core/src/utils/safeJsonParse.ts
Normal file
45
packages/core/src/utils/safeJsonParse.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
|
||||
/**
|
||||
* Safely parse JSON string with jsonrepair fallback for malformed JSON.
|
||||
* This function attempts to parse JSON normally first, and if that fails,
|
||||
* it uses jsonrepair to fix common JSON formatting issues before parsing.
|
||||
*
|
||||
* @param jsonString - The JSON string to parse
|
||||
* @param fallbackValue - The value to return if parsing fails completely
|
||||
* @returns The parsed object or the fallback value
|
||||
*/
|
||||
export function safeJsonParse<T = Record<string, unknown>>(
|
||||
jsonString: string,
|
||||
fallbackValue: T = {} as T,
|
||||
): T {
|
||||
if (!jsonString || typeof jsonString !== 'string') {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
try {
|
||||
// First attempt: try normal JSON.parse
|
||||
return JSON.parse(jsonString) as T;
|
||||
} catch (error) {
|
||||
try {
|
||||
// Second attempt: use jsonrepair to fix common JSON issues
|
||||
const repairedJson = jsonrepair(jsonString);
|
||||
|
||||
// jsonrepair always returns a string, so we need to parse it
|
||||
return JSON.parse(repairedJson) as T;
|
||||
} catch (repairError) {
|
||||
console.error('Failed to parse JSON even with jsonrepair:', {
|
||||
originalError: error,
|
||||
repairError,
|
||||
jsonString,
|
||||
});
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user