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",
|
||||
// fix source mapping when debugging in sandbox using global installation
|
||||
// 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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ export class TestRig {
|
||||
mkdirSync(this.testDir, { recursive: true });
|
||||
|
||||
// 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 });
|
||||
// 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
|
||||
@@ -567,7 +567,7 @@ export class TestRig {
|
||||
// Look for tool call logs
|
||||
if (
|
||||
logData.attributes &&
|
||||
logData.attributes['event.name'] === 'gemini_cli.tool_call'
|
||||
logData.attributes['event.name'] === 'qwen-code.tool_call'
|
||||
) {
|
||||
const toolName = logData.attributes.function_name;
|
||||
logs.push({
|
||||
|
||||
@@ -66,6 +66,17 @@ vi.mock('../utils/generateContentResponseUtilities', () => ({
|
||||
getResponseText: (result: GenerateContentResponse) =>
|
||||
result.candidates?.[0]?.content?.parts?.map((part) => part.text).join('') ||
|
||||
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', () => ({
|
||||
logApiRequest: vi.fn(),
|
||||
@@ -158,7 +169,14 @@ describe('Gemini Client (client.ts)', () => {
|
||||
candidates: [
|
||||
{
|
||||
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(),
|
||||
setFallbackMode: vi.fn(),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
};
|
||||
const MockedConfig = vi.mocked(Config, true);
|
||||
MockedConfig.mockImplementation(
|
||||
@@ -387,7 +406,8 @@ describe('Gemini Client (client.ts)', () => {
|
||||
};
|
||||
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(
|
||||
{
|
||||
@@ -397,8 +417,17 @@ describe('Gemini Client (client.ts)', () => {
|
||||
systemInstruction: getCoreSystemPrompt(''),
|
||||
temperature: 0,
|
||||
topP: 1,
|
||||
responseSchema: schema,
|
||||
responseMimeType: 'application/json',
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'respond_in_schema',
|
||||
description: 'Provide the response in provided schema',
|
||||
parameters: schema,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
contents,
|
||||
},
|
||||
@@ -419,13 +448,14 @@ describe('Gemini Client (client.ts)', () => {
|
||||
};
|
||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||
|
||||
await client.generateJson(
|
||||
const result = await client.generateJson(
|
||||
contents,
|
||||
schema,
|
||||
abortSignal,
|
||||
customModel,
|
||||
customConfig,
|
||||
);
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
|
||||
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -436,8 +466,17 @@ describe('Gemini Client (client.ts)', () => {
|
||||
temperature: 0.9,
|
||||
topP: 1, // from default
|
||||
topK: 20,
|
||||
responseSchema: schema,
|
||||
responseMimeType: 'application/json',
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: 'respond_in_schema',
|
||||
description: 'Provide the response in provided schema',
|
||||
parameters: schema,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
contents,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
Content,
|
||||
Tool,
|
||||
GenerateContentResponse,
|
||||
FunctionDeclaration,
|
||||
Schema,
|
||||
} from '@google/genai';
|
||||
import { getFolderStructure } from '../utils/getFolderStructure.js';
|
||||
import {
|
||||
@@ -25,7 +27,7 @@ import { Config } from '../config/config.js';
|
||||
import { UserTierId } from '../code_assist/types.js';
|
||||
import { getCoreSystemPrompt, getCompressionPrompt } from './prompts.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 { reportError } from '../utils/errorReporting.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 { ideContext } from '../ide/ideContext.js';
|
||||
import { logNextSpeakerCheck } from '../telemetry/loggers.js';
|
||||
import {
|
||||
MalformedJsonResponseEvent,
|
||||
NextSpeakerCheckEvent,
|
||||
} from '../telemetry/types.js';
|
||||
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';
|
||||
import { NextSpeakerCheckEvent } from '../telemetry/types.js';
|
||||
|
||||
function isThinkingSupported(model: string) {
|
||||
if (model.startsWith('gemini-2.5')) return true;
|
||||
@@ -539,6 +537,19 @@ export class GeminiClient {
|
||||
...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 = () =>
|
||||
this.getContentGenerator().generateContent(
|
||||
{
|
||||
@@ -546,8 +557,7 @@ export class GeminiClient {
|
||||
config: {
|
||||
...requestConfig,
|
||||
systemInstruction,
|
||||
responseSchema: schema,
|
||||
responseMimeType: 'application/json',
|
||||
tools,
|
||||
},
|
||||
contents,
|
||||
},
|
||||
@@ -559,73 +569,16 @@ export class GeminiClient {
|
||||
await this.handleFlashFallback(authType, error),
|
||||
authType: this.config.getContentGeneratorConfig()?.authType,
|
||||
});
|
||||
|
||||
let text = getResponseText(result);
|
||||
if (!text) {
|
||||
const error = new Error(
|
||||
'API returned an empty response for generateJson.',
|
||||
const functionCalls = getFunctionCalls(result);
|
||||
if (functionCalls && functionCalls.length > 0) {
|
||||
const functionCall = functionCalls.find(
|
||||
(call) => call.name === 'respond_in_schema',
|
||||
);
|
||||
await reportError(
|
||||
error,
|
||||
'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 (functionCall && functionCall.args) {
|
||||
return functionCall.args as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (abortSignal.aborted) {
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user