mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix generateJson with respond in schema
Co-Authored-By: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -50,7 +50,7 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
// fix source mapping when debugging in sandbox using global installation
|
// fix source mapping when debugging in sandbox using global installation
|
||||||
// note this does not interfere when remoteRoot is also ${workspaceFolder}/packages
|
// note this does not interfere when remoteRoot is also ${workspaceFolder}/packages
|
||||||
"remoteRoot": "/usr/local/share/npm-global/lib/node_modules/@gemini-cli",
|
"remoteRoot": "/usr/local/share/npm-global/lib/node_modules/@qwen-code",
|
||||||
"localRoot": "${workspaceFolder}/packages"
|
"localRoot": "${workspaceFolder}/packages"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export class TestRig {
|
|||||||
mkdirSync(this.testDir, { recursive: true });
|
mkdirSync(this.testDir, { recursive: true });
|
||||||
|
|
||||||
// Create a settings file to point the CLI to the local collector
|
// Create a settings file to point the CLI to the local collector
|
||||||
const geminiDir = join(this.testDir, '.gemini');
|
const geminiDir = join(this.testDir, '.qwen');
|
||||||
mkdirSync(geminiDir, { recursive: true });
|
mkdirSync(geminiDir, { recursive: true });
|
||||||
// In sandbox mode, use an absolute path for telemetry inside the container
|
// In sandbox mode, use an absolute path for telemetry inside the container
|
||||||
// The container mounts the test directory at the same path as the host
|
// The container mounts the test directory at the same path as the host
|
||||||
@@ -567,7 +567,7 @@ export class TestRig {
|
|||||||
// Look for tool call logs
|
// Look for tool call logs
|
||||||
if (
|
if (
|
||||||
logData.attributes &&
|
logData.attributes &&
|
||||||
logData.attributes['event.name'] === 'gemini_cli.tool_call'
|
logData.attributes['event.name'] === 'qwen-code.tool_call'
|
||||||
) {
|
) {
|
||||||
const toolName = logData.attributes.function_name;
|
const toolName = logData.attributes.function_name;
|
||||||
logs.push({
|
logs.push({
|
||||||
|
|||||||
@@ -66,6 +66,17 @@ vi.mock('../utils/generateContentResponseUtilities', () => ({
|
|||||||
getResponseText: (result: GenerateContentResponse) =>
|
getResponseText: (result: GenerateContentResponse) =>
|
||||||
result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
|
result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
|
||||||
undefined,
|
undefined,
|
||||||
|
getFunctionCalls: (result: GenerateContentResponse) => {
|
||||||
|
// Extract function calls from the response
|
||||||
|
const parts = result.candidates?.[0]?.content?.parts;
|
||||||
|
if (!parts) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const functionCallParts = parts
|
||||||
|
.filter((part) => !!part.functionCall)
|
||||||
|
.map((part) => part.functionCall);
|
||||||
|
return functionCallParts.length > 0 ? functionCallParts : undefined;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock('../telemetry/index.js', () => ({
|
vi.mock('../telemetry/index.js', () => ({
|
||||||
logApiRequest: vi.fn(),
|
logApiRequest: vi.fn(),
|
||||||
@@ -158,7 +169,14 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
candidates: [
|
candidates: [
|
||||||
{
|
{
|
||||||
content: {
|
content: {
|
||||||
parts: [{ text: '{"key": "value"}' }],
|
parts: [
|
||||||
|
{
|
||||||
|
functionCall: {
|
||||||
|
name: 'respond_in_schema',
|
||||||
|
args: { key: 'value' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -209,6 +227,7 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
}),
|
}),
|
||||||
getGeminiClient: vi.fn(),
|
getGeminiClient: vi.fn(),
|
||||||
setFallbackMode: vi.fn(),
|
setFallbackMode: vi.fn(),
|
||||||
|
getDebugMode: vi.fn().mockReturnValue(false),
|
||||||
};
|
};
|
||||||
const MockedConfig = vi.mocked(Config, true);
|
const MockedConfig = vi.mocked(Config, true);
|
||||||
MockedConfig.mockImplementation(
|
MockedConfig.mockImplementation(
|
||||||
@@ -387,7 +406,8 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
};
|
};
|
||||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||||
|
|
||||||
await client.generateJson(contents, schema, abortSignal);
|
const result = await client.generateJson(contents, schema, abortSignal);
|
||||||
|
expect(result).toEqual({ key: 'value' });
|
||||||
|
|
||||||
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
@@ -397,8 +417,17 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
systemInstruction: getCoreSystemPrompt(''),
|
systemInstruction: getCoreSystemPrompt(''),
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
topP: 1,
|
topP: 1,
|
||||||
responseSchema: schema,
|
tools: [
|
||||||
responseMimeType: 'application/json',
|
{
|
||||||
|
functionDeclarations: [
|
||||||
|
{
|
||||||
|
name: 'respond_in_schema',
|
||||||
|
description: 'Provide the response in provided schema',
|
||||||
|
parameters: schema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
contents,
|
contents,
|
||||||
},
|
},
|
||||||
@@ -419,13 +448,14 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
};
|
};
|
||||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||||
|
|
||||||
await client.generateJson(
|
const result = await client.generateJson(
|
||||||
contents,
|
contents,
|
||||||
schema,
|
schema,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
customModel,
|
customModel,
|
||||||
customConfig,
|
customConfig,
|
||||||
);
|
);
|
||||||
|
expect(result).toEqual({ key: 'value' });
|
||||||
|
|
||||||
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
@@ -436,8 +466,17 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
topP: 1, // from default
|
topP: 1, // from default
|
||||||
topK: 20,
|
topK: 20,
|
||||||
responseSchema: schema,
|
tools: [
|
||||||
responseMimeType: 'application/json',
|
{
|
||||||
|
functionDeclarations: [
|
||||||
|
{
|
||||||
|
name: 'respond_in_schema',
|
||||||
|
description: 'Provide the response in provided schema',
|
||||||
|
parameters: schema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
contents,
|
contents,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
Content,
|
Content,
|
||||||
Tool,
|
Tool,
|
||||||
GenerateContentResponse,
|
GenerateContentResponse,
|
||||||
|
FunctionDeclaration,
|
||||||
|
Schema,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +27,7 @@ import { Config } from '../config/config.js';
|
|||||||
import { UserTierId } from '../code_assist/types.js';
|
import { UserTierId } from '../code_assist/types.js';
|
||||||
import { getCoreSystemPrompt, getCompressionPrompt } from './prompts.js';
|
import { getCoreSystemPrompt, getCompressionPrompt } from './prompts.js';
|
||||||
import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
||||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
import { getFunctionCalls } from '../utils/generateContentResponseUtilities.js';
|
||||||
import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js';
|
import { checkNextSpeaker } from '../utils/nextSpeakerChecker.js';
|
||||||
import { reportError } from '../utils/errorReporting.js';
|
import { reportError } from '../utils/errorReporting.js';
|
||||||
import { GeminiChat } from './geminiChat.js';
|
import { GeminiChat } from './geminiChat.js';
|
||||||
@@ -44,11 +46,7 @@ import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
|||||||
import { LoopDetectionService } from '../services/loopDetectionService.js';
|
import { LoopDetectionService } from '../services/loopDetectionService.js';
|
||||||
import { ideContext } from '../ide/ideContext.js';
|
import { ideContext } from '../ide/ideContext.js';
|
||||||
import { logNextSpeakerCheck } from '../telemetry/loggers.js';
|
import { logNextSpeakerCheck } from '../telemetry/loggers.js';
|
||||||
import {
|
import { NextSpeakerCheckEvent } from '../telemetry/types.js';
|
||||||
MalformedJsonResponseEvent,
|
|
||||||
NextSpeakerCheckEvent,
|
|
||||||
} from '../telemetry/types.js';
|
|
||||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
|
||||||
|
|
||||||
function isThinkingSupported(model: string) {
|
function isThinkingSupported(model: string) {
|
||||||
if (model.startsWith('gemini-2.5')) return true;
|
if (model.startsWith('gemini-2.5')) return true;
|
||||||
@@ -539,6 +537,19 @@ export class GeminiClient {
|
|||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert schema to function declaration
|
||||||
|
const functionDeclaration: FunctionDeclaration = {
|
||||||
|
name: 'respond_in_schema',
|
||||||
|
description: 'Provide the response in provided schema',
|
||||||
|
parameters: schema as Schema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools: Tool[] = [
|
||||||
|
{
|
||||||
|
functionDeclarations: [functionDeclaration],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const apiCall = () =>
|
const apiCall = () =>
|
||||||
this.getContentGenerator().generateContent(
|
this.getContentGenerator().generateContent(
|
||||||
{
|
{
|
||||||
@@ -546,8 +557,7 @@ export class GeminiClient {
|
|||||||
config: {
|
config: {
|
||||||
...requestConfig,
|
...requestConfig,
|
||||||
systemInstruction,
|
systemInstruction,
|
||||||
responseSchema: schema,
|
tools,
|
||||||
responseMimeType: 'application/json',
|
|
||||||
},
|
},
|
||||||
contents,
|
contents,
|
||||||
},
|
},
|
||||||
@@ -559,73 +569,16 @@ export class GeminiClient {
|
|||||||
await this.handleFlashFallback(authType, error),
|
await this.handleFlashFallback(authType, error),
|
||||||
authType: this.config.getContentGeneratorConfig()?.authType,
|
authType: this.config.getContentGeneratorConfig()?.authType,
|
||||||
});
|
});
|
||||||
|
const functionCalls = getFunctionCalls(result);
|
||||||
let text = getResponseText(result);
|
if (functionCalls && functionCalls.length > 0) {
|
||||||
if (!text) {
|
const functionCall = functionCalls.find(
|
||||||
const error = new Error(
|
(call) => call.name === 'respond_in_schema',
|
||||||
'API returned an empty response for generateJson.',
|
|
||||||
);
|
);
|
||||||
await reportError(
|
if (functionCall && functionCall.args) {
|
||||||
error,
|
return functionCall.args as Record<string, unknown>;
|
||||||
'Error in generateJson: API returned an empty response.',
|
|
||||||
contents,
|
|
||||||
'generateJson-empty-response',
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefix = '```json';
|
|
||||||
const suffix = '```';
|
|
||||||
if (text.startsWith(prefix) && text.endsWith(suffix)) {
|
|
||||||
ClearcutLogger.getInstance(this.config)?.logMalformedJsonResponseEvent(
|
|
||||||
new MalformedJsonResponseEvent(modelToUse),
|
|
||||||
);
|
|
||||||
text = text
|
|
||||||
.substring(prefix.length, text.length - suffix.length)
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try to extract JSON from various formats
|
|
||||||
const extractors = [
|
|
||||||
// Match ```json ... ``` or ``` ... ``` blocks
|
|
||||||
/```(?:json)?\s*\n?([\s\S]*?)\n?```/,
|
|
||||||
// Match inline code blocks `{...}`
|
|
||||||
/`(\{[\s\S]*?\})`/,
|
|
||||||
// Match raw JSON objects or arrays
|
|
||||||
/(\{[\s\S]*\}|\[[\s\S]*\])/,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const regex of extractors) {
|
|
||||||
const match = text.match(regex);
|
|
||||||
if (match && match[1]) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(match[1].trim());
|
|
||||||
} catch {
|
|
||||||
// Continue to next pattern if parsing fails
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no patterns matched, try parsing the entire text
|
|
||||||
return JSON.parse(text.trim());
|
|
||||||
} catch (parseError) {
|
|
||||||
await reportError(
|
|
||||||
parseError,
|
|
||||||
'Failed to parse JSON response from generateJson.',
|
|
||||||
{
|
|
||||||
responseTextFailedToParse: text,
|
|
||||||
originalRequestContents: contents,
|
|
||||||
},
|
|
||||||
'generateJson-parse',
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`Failed to parse API response as JSON: ${getErrorMessage(
|
|
||||||
parseError,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user