fix(gemini): fix Gemini tool calling and thought-signature round-trip (#1389)

- send tool declarations, system prompt, and sampling/thinking settings in the camelCase shape the Google SDK forwards so tool calls reach the model
- thread tool-call extras (thought signatures) through the loop tool-call event into context so Gemini 3 can resume a tool turn
- update and add tests for the corrected request shape and signature round-trip
This commit is contained in:
Haozhe 2026-07-05 14:00:09 +08:00 committed by GitHub
parent be7c9916b0
commit ebdffc7df7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 201 additions and 113 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix tool calling with Google Gemini models, including Gemini 3 thinking-signature round-trips across turns.

View file

@ -620,6 +620,7 @@ export class ContextMemory {
id: event.toolCallId,
name: event.name,
arguments: event.args === undefined ? null : JSON.stringify(event.args),
extras: event.extras,
});
this.pendingToolResultIds.add(event.toolCallId);
return;

View file

@ -78,6 +78,7 @@ export interface LoopToolCallEvent {
readonly args: unknown;
readonly description?: string | undefined;
readonly display?: ToolInputDisplay | undefined;
readonly extras?: Record<string, unknown> | undefined;
}
export interface LoopToolResultEvent {

View file

@ -713,5 +713,6 @@ async function dispatchToolCall(
args,
description: displayFields?.description,
display: displayFields?.display,
extras: toolCall.extras,
});
}

View file

@ -60,6 +60,55 @@ describe('Agent context', () => {
expect(ctx.agent.context.messages.some((message) => 'origin' in message)).toBe(false);
});
it('preserves tool call extras (Gemini thought_signature) through to projection', () => {
// Regression: Gemini 3 requires the thought_signature returned on a
// functionCall to be echoed back when the call is re-sent in the next turn.
// The signature travels as ToolCall.extras.thought_signature_b64; it must
// survive the loop tool.call event -> context recording -> projection so
// the provider can put it back on the outbound functionCall part.
const ctx = testAgent();
ctx.configure();
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'run' }]);
ctx.dispatch({
type: 'context.append_loop_event',
event: { type: 'step.begin', uuid: 'sig-step', turnId: '', step: 1 },
});
ctx.dispatch({
type: 'context.append_loop_event',
event: {
type: 'tool.call',
uuid: 'sig-tool',
turnId: '',
step: 1,
stepUuid: 'sig-step',
toolCallId: 'call_sig',
name: 'Bash',
args: { command: 'echo hi' },
extras: { thought_signature_b64: 'c2lnbmF0dXJl' },
},
});
ctx.dispatch({
type: 'context.append_loop_event',
event: { type: 'step.end', uuid: 'sig-step', turnId: '', step: 1 },
});
ctx.dispatch({
type: 'context.append_loop_event',
event: {
type: 'tool.result',
parentUuid: 'sig-tool',
toolCallId: 'call_sig',
result: { output: 'hi' },
},
});
const expectedExtras = { thought_signature_b64: 'c2lnbmF0dXJl' };
const recorded = ctx.agent.context.history.find((m) => m.role === 'assistant');
expect(recorded?.toolCalls[0]?.extras).toEqual(expectedExtras);
const projected = ctx.agent.context.messages.find((m) => m.role === 'assistant');
expect(projected?.toolCalls[0]?.extras).toEqual(expectedExtras);
});
it('reroutes an inline image-compression caption into a hidden system reminder', () => {
const ctx = testAgent();
ctx.configure();

View file

@ -92,36 +92,36 @@ export interface GoogleGenAIOptions {
}
export interface GoogleGenAIGenerationKwargs {
max_output_tokens?: number | undefined;
maxOutputTokens?: number | undefined;
temperature?: number | undefined;
top_k?: number | undefined;
top_p?: number | undefined;
thinking_config?: ThinkingConfig | undefined;
topK?: number | undefined;
topP?: number | undefined;
thinkingConfig?: ThinkingConfig | undefined;
[key: string]: unknown;
}
interface ThinkingConfig {
include_thoughts?: boolean;
thinking_budget?: number;
thinking_level?: string;
includeThoughts?: boolean;
thinkingBudget?: number;
thinkingLevel?: string;
}
interface GoogleFunctionDeclaration {
name: string;
description: string;
parameters_json_schema: Record<string, unknown>;
parametersJsonSchema: Record<string, unknown>;
}
interface GoogleTool {
function_declarations: GoogleFunctionDeclaration[];
functionDeclarations: GoogleFunctionDeclaration[];
}
function toolToGoogleGenAI(tool: Tool): GoogleTool {
return {
function_declarations: [
functionDeclarations: [
{
name: tool.name,
description: tool.description,
parameters_json_schema: tool.parameters,
parametersJsonSchema: tool.parameters,
},
],
};
@ -133,13 +133,13 @@ interface GoogleContent {
interface GooglePart {
text?: string;
function_call?: { name: string; args: Record<string, unknown> };
function_response?: {
functionCall?: { name: string; args: Record<string, unknown> };
functionResponse?: {
name: string;
response: Record<string, string>;
parts: unknown[];
};
thought_signature?: string;
thoughtSignature?: string;
[key: string]: unknown;
}
@ -274,15 +274,15 @@ function messageToGoogleGenAI(message: Message): GoogleContent {
}
const functionCallPart: GooglePart = {
function_call: {
functionCall: {
name: toolCall.name,
args,
},
};
// Restore thought_signature if available
// Restore thoughtSignature if available
if (toolCall.extras && 'thought_signature_b64' in toolCall.extras) {
functionCallPart['thought_signature'] = toolCall.extras['thought_signature_b64'] as string;
functionCallPart['thoughtSignature'] = toolCall.extras['thought_signature_b64'] as string;
}
parts.push(functionCallPart);
@ -335,7 +335,7 @@ function toolMessageToFunctionResponseParts(
}
const functionResponsePart: GooglePart = {
function_response: {
functionResponse: {
name: toolCallIdToName(message.toolCallId, toolNameById),
response: { output: textOutput },
parts: [],
@ -361,7 +361,7 @@ export function messagesToGoogleGenAIContents(messages: Message[]): GoogleConten
// the content by wrapping it in a `<system>` tag and attaching it as
// a user turn — mirrors the Anthropic provider's behavior. The
// dedicated top-level `systemPrompt` still flows into
// `system_instruction` separately; only historical system messages
// `systemInstruction` separately; only historical system messages
// come through here.
const text = message.content
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
@ -463,7 +463,7 @@ export function messagesToGoogleGenAIContents(messages: Message[]): GoogleConten
isUser: (content) => content.role === 'user',
isToolResultOnly: (content) =>
content.parts.length > 0 &&
content.parts.every((part) => part.function_response !== undefined),
content.parts.every((part) => part.functionResponse !== undefined),
merge: (last, next) => ({ ...last, parts: [...last.parts, ...next.parts] }),
});
}
@ -746,16 +746,16 @@ export class GoogleGenAIChatProvider implements ChatProvider {
}
get thinkingEffort(): ThinkingEffort | null {
const thinkingConfig = this._generationKwargs.thinking_config;
const thinkingConfig = this._generationKwargs.thinkingConfig;
if (thinkingConfig === undefined) return null;
// For gemini-3 models that use thinking_level
if (thinkingConfig.thinking_level !== undefined) {
switch (thinkingConfig.thinking_level) {
// For gemini-3 models that use thinkingLevel
if (thinkingConfig.thinkingLevel !== undefined) {
switch (thinkingConfig.thinkingLevel) {
case 'MINIMAL':
// MINIMAL + suppressed thoughts is how 'off' is encoded for Gemini 3,
// which has no true "disabled" level.
return thinkingConfig.include_thoughts === false ? 'off' : 'low';
return thinkingConfig.includeThoughts === false ? 'off' : 'low';
case 'LOW':
return 'low';
case 'MEDIUM':
@ -767,11 +767,11 @@ export class GoogleGenAIChatProvider implements ChatProvider {
}
}
// For other models that use thinking_budget
if (thinkingConfig.thinking_budget !== undefined) {
if (thinkingConfig.thinking_budget === 0) return 'off';
if (thinkingConfig.thinking_budget <= 1024) return 'low';
if (thinkingConfig.thinking_budget <= 4096) return 'medium';
// For other models that use thinkingBudget
if (thinkingConfig.thinkingBudget !== undefined) {
if (thinkingConfig.thinkingBudget === 0) return 'off';
if (thinkingConfig.thinkingBudget <= 1024) return 'low';
if (thinkingConfig.thinkingBudget <= 4096) return 'medium';
return 'high';
}
@ -801,7 +801,7 @@ export class GoogleGenAIChatProvider implements ChatProvider {
const config: Record<string, unknown> = {
...this._generationKwargs,
system_instruction: systemPrompt,
systemInstruction: systemPrompt,
...(tools.length > 0 ? { tools: tools.map((t) => toolToGoogleGenAI(t)) } : {}),
};
@ -866,53 +866,53 @@ export class GoogleGenAIChatProvider implements ChatProvider {
}
withThinking(effort: ThinkingEffort): GoogleGenAIChatProvider {
const thinkingConfig: ThinkingConfig = { include_thoughts: true };
const thinkingConfig: ThinkingConfig = { includeThoughts: true };
if (this._model.includes('gemini-3')) {
// Gemini 3 models use thinking_level (MINIMAL/LOW/MEDIUM/HIGH). The SDK
// Gemini 3 models use thinkingLevel (MINIMAL/LOW/MEDIUM/HIGH). The SDK
// does not expose a "disabled" level, so 'off' maps to MINIMAL with
// thought output suppressed — the lowest thinking intensity available.
switch (effort) {
case 'off':
thinkingConfig.thinking_level = 'MINIMAL';
thinkingConfig.include_thoughts = false;
thinkingConfig.thinkingLevel = 'MINIMAL';
thinkingConfig.includeThoughts = false;
break;
case 'low':
thinkingConfig.thinking_level = 'LOW';
thinkingConfig.thinkingLevel = 'LOW';
break;
case 'medium':
thinkingConfig.thinking_level = 'MEDIUM';
thinkingConfig.thinkingLevel = 'MEDIUM';
break;
case 'high':
case 'xhigh':
case 'max':
thinkingConfig.thinking_level = 'HIGH';
thinkingConfig.thinkingLevel = 'HIGH';
break;
}
} else {
switch (effort) {
case 'off':
thinkingConfig.thinking_budget = 0;
thinkingConfig.include_thoughts = false;
thinkingConfig.thinkingBudget = 0;
thinkingConfig.includeThoughts = false;
break;
case 'low':
thinkingConfig.thinking_budget = 1024;
thinkingConfig.include_thoughts = true;
thinkingConfig.thinkingBudget = 1024;
thinkingConfig.includeThoughts = true;
break;
case 'medium':
thinkingConfig.thinking_budget = 4096;
thinkingConfig.include_thoughts = true;
thinkingConfig.thinkingBudget = 4096;
thinkingConfig.includeThoughts = true;
break;
case 'high':
case 'xhigh':
case 'max':
thinkingConfig.thinking_budget = 32_000;
thinkingConfig.include_thoughts = true;
thinkingConfig.thinkingBudget = 32_000;
thinkingConfig.includeThoughts = true;
break;
}
}
return this.withGenerationKwargs({ thinking_config: thinkingConfig });
return this.withGenerationKwargs({ thinkingConfig });
}
withGenerationKwargs(kwargs: GoogleGenAIGenerationKwargs): GoogleGenAIChatProvider {
@ -922,7 +922,7 @@ export class GoogleGenAIChatProvider implements ChatProvider {
}
withMaxCompletionTokens(maxCompletionTokens: number): GoogleGenAIChatProvider {
return this.withGenerationKwargs({ max_output_tokens: maxCompletionTokens });
return this.withGenerationKwargs({ maxOutputTokens: maxCompletionTokens });
}
private _clone(): GoogleGenAIChatProvider {

View file

@ -62,11 +62,42 @@ describe('e2e: Google GenAI adapter bridge', () => {
{ role: 'user', parts: [{ text: 'Add and multiply these numbers.' }] },
{
role: 'model',
parts: [{ text: 'I will calculate both.' }, {}, {}],
parts: [
{ text: 'I will calculate both.' },
{ functionCall: { name: 'add', args: { a: 2, b: 3 } } },
{ functionCall: { name: 'multiply', args: { a: 4, b: 5 } } },
],
},
{
role: 'user',
parts: [{}, {}],
parts: [
{ functionResponse: { name: 'add', response: { output: '5' }, parts: [] } },
{
functionResponse: { name: 'multiply', response: { output: '20' }, parts: [] },
},
],
},
]);
// Regression: the snake_case `system_instruction` / tool declarations used
// to be silently dropped by the @google/genai SDK, so the model saw neither
// a system prompt nor any tools. Both must now reach the wire as camelCase.
expect(body['systemInstruction']).toEqual({
parts: [{ text: 'You are a calculator.' }],
role: 'user',
});
expect(body['tools']).toEqual([
{
functionDeclarations: [
expect.objectContaining({ name: 'add', parametersJsonSchema: expect.any(Object) }),
],
},
{
functionDeclarations: [
expect.objectContaining({
name: 'multiply',
parametersJsonSchema: expect.any(Object),
}),
],
},
]);

View file

@ -128,7 +128,7 @@ describe('GoogleGenAIChatProvider', () => {
expect(body['contents']).toEqual([{ parts: [{ text: 'Hello!' }], role: 'user' }]);
const config = body['config'] as Record<string, unknown>;
expect(config['system_instruction']).toBe('You are helpful.');
expect(config['systemInstruction']).toBe('You are helpful.');
});
it('system messages in history are wrapped and emitted as user content', () => {
@ -253,11 +253,11 @@ describe('GoogleGenAIChatProvider', () => {
expect(contents.map((c) => c.role)).toEqual(['user', 'model', 'user']);
const last = contents.at(-1)!;
expect(last.parts.some((p) => p.function_response !== undefined)).toBe(true);
expect(last.parts.some((p) => p.functionResponse !== undefined)).toBe(true);
expect(last.parts.some((p) => p.text === 'Now multiply')).toBe(true);
});
it('multi-turn conversation with system prompt sets system_instruction', async () => {
it('multi-turn conversation with system prompt sets systemInstruction', async () => {
const provider = createProvider();
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'What is 2+2?' }], toolCalls: [] },
@ -273,10 +273,10 @@ describe('GoogleGenAIChatProvider', () => {
]);
const config = body['config'] as Record<string, unknown>;
expect(config['system_instruction']).toBe('You are a math tutor.');
expect(config['systemInstruction']).toBe('You are a math tutor.');
});
it('tool definitions use parameters_json_schema', async () => {
it('tool definitions use parametersJsonSchema', async () => {
const provider = createProvider();
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] },
@ -286,11 +286,11 @@ describe('GoogleGenAIChatProvider', () => {
const config = body['config'] as Record<string, unknown>;
expect(config['tools']).toEqual([
{
function_declarations: [
functionDeclarations: [
{
name: 'add',
description: 'Add two integers.',
parameters_json_schema: {
parametersJsonSchema: {
type: 'object',
properties: {
a: { type: 'integer', description: 'First number' },
@ -302,11 +302,11 @@ describe('GoogleGenAIChatProvider', () => {
],
},
{
function_declarations: [
functionDeclarations: [
{
name: 'multiply',
description: 'Multiply two integers.',
parameters_json_schema: {
parametersJsonSchema: {
type: 'object',
properties: {
a: { type: 'integer', description: 'First number' },
@ -348,14 +348,14 @@ describe('GoogleGenAIChatProvider', () => {
{
parts: [
{ text: "I'll add those numbers for you." },
{ function_call: { name: 'add', args: { a: 2, b: 3 } } },
{ functionCall: { name: 'add', args: { a: 2, b: 3 } } },
],
role: 'model',
},
{
parts: [
{
function_response: {
functionResponse: {
name: 'add',
response: { output: '5' },
parts: [],
@ -367,11 +367,11 @@ describe('GoogleGenAIChatProvider', () => {
]);
});
it('tool call with thought_signature_b64 emits thoughtSignature on outbound function_call', async () => {
it('tool call with thought_signature_b64 emits thoughtSignature on outbound functionCall', async () => {
// Round-trip: a previous turn returned a tool call with thoughtSignature
// (decoded into ToolCall.extras.thought_signature_b64). When we send
// the assistant message back, the converter must put the original
// signature back into the function_call part so Gemini can resume the
// signature back into the functionCall part so Gemini can resume the
// reasoning chain.
const provider = createProvider();
const history: Message[] = [
@ -394,15 +394,15 @@ describe('GoogleGenAIChatProvider', () => {
const contents = body['contents'] as Array<{ parts: unknown[]; role: string }>;
const assistantParts = contents.find((c) => c.role === 'model')!.parts;
const fnCallPart = assistantParts.find(
(p) => (p as Record<string, unknown>)['function_call'] !== undefined,
) as { function_call: Record<string, unknown>; thought_signature?: unknown } | undefined;
(p) => (p as Record<string, unknown>)['functionCall'] !== undefined,
) as { functionCall: Record<string, unknown>; thoughtSignature?: unknown } | undefined;
expect(fnCallPart).toMatchObject({
function_call: { name: 'add', args: { a: 2, b: 3 } },
thought_signature: 'dGhvdWdodF9zaWduYXR1cmVfZGF0YQ==',
functionCall: { name: 'add', args: { a: 2, b: 3 } },
thoughtSignature: 'dGhvdWdodF9zaWduYXR1cmVfZGF0YQ==',
});
});
it('tool message with image_url result yields function_response + inline data part', () => {
it('tool message with image_url result yields functionResponse + inline data part', () => {
const messages: Message[] = [
{
role: 'assistant',
@ -437,11 +437,11 @@ describe('GoogleGenAIChatProvider', () => {
expect(userContent.role).toBe('user');
expect(userContent.parts.length).toBeGreaterThanOrEqual(2);
const fnResp = userContent.parts.find((p) => 'function_response' in p) as
| { function_response: { name: string; response: { output: string } } }
const fnResp = userContent.parts.find((p) => 'functionResponse' in p) as
| { functionResponse: { name: string; response: { output: string } } }
| undefined;
expect(fnResp).toMatchObject({
function_response: {
functionResponse: {
name: 'fetch_image',
response: { output: 'Found image:' },
},
@ -488,14 +488,14 @@ describe('GoogleGenAIChatProvider', () => {
parts: Array<Record<string, unknown>>;
};
expect(userContent.role).toBe('user');
// function_response + audio + video
// functionResponse + audio + video
expect(userContent.parts).toHaveLength(3);
const fnResp = userContent.parts.find((p) => 'function_response' in p) as
| { function_response: { response: { output: string } } }
const fnResp = userContent.parts.find((p) => 'functionResponse' in p) as
| { functionResponse: { response: { output: string } } }
| undefined;
expect(fnResp).toMatchObject({
function_response: { response: { output: 'Got audio and video:' } },
functionResponse: { response: { output: 'Got audio and video:' } },
});
const fileDataParts = userContent.parts.filter((p) => 'fileData' in p) as Array<{
@ -581,7 +581,7 @@ describe('GoogleGenAIChatProvider', () => {
const body = await captureRequestBody(provider, '', [ADD_TOOL, MUL_TOOL], history);
// Snapshot of the expected wire format:
// - exactly 3 contents in order (user, model with 2 function_calls, user with 2 function_responses bundled)
// - exactly 3 contents in order (user, model with 2 functionCalls, user with 2 functionResponses bundled)
// - both tool results are N:1 packed into ONE user Content
// - text parts are concatenated into `response.output` (system-reminder + result)
// - functionCall / functionResponse never include an `id` field
@ -590,15 +590,15 @@ describe('GoogleGenAIChatProvider', () => {
{
parts: [
{ text: "I'll calculate both." },
{ function_call: { name: 'add', args: { a: 2, b: 3 } } },
{ function_call: { name: 'multiply', args: { a: 4, b: 5 } } },
{ functionCall: { name: 'add', args: { a: 2, b: 3 } } },
{ functionCall: { name: 'multiply', args: { a: 4, b: 5 } } },
],
role: 'model',
},
{
parts: [
{
function_response: {
functionResponse: {
name: 'add',
response: {
output: '<system-reminder>This is a system reminder</system-reminder>5',
@ -607,7 +607,7 @@ describe('GoogleGenAIChatProvider', () => {
},
},
{
function_response: {
functionResponse: {
name: 'multiply',
response: {
output: '<system-reminder>This is a system reminder</system-reminder>20',
@ -632,15 +632,15 @@ describe('GoogleGenAIChatProvider', () => {
expect(body['contents']).toEqual([{ parts: [{ text: 'Hello!' }], role: 'user' }]);
const config = body['config'] as Record<string, unknown>;
expect(config['system_instruction']).toBe('You are helpful.');
expect(config['systemInstruction']).toBe('You are helpful.');
});
});
describe('generation kwargs', () => {
it('applies temperature and max_output_tokens', async () => {
it('applies temperature and maxOutputTokens', async () => {
const provider = createProvider().withGenerationKwargs({
temperature: 0.7,
max_output_tokens: 2048,
maxOutputTokens: 2048,
});
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] },
@ -649,10 +649,10 @@ describe('GoogleGenAIChatProvider', () => {
const config = body['config'] as Record<string, unknown>;
expect(config['temperature']).toBe(0.7);
expect(config['max_output_tokens']).toBe(2048);
expect(config['maxOutputTokens']).toBe(2048);
});
it('withMaxCompletionTokens sets max_output_tokens on the cloned provider', async () => {
it('withMaxCompletionTokens sets maxOutputTokens on the cloned provider', async () => {
const original = createProvider();
const provider = original.withMaxCompletionTokens(1024);
const history: Message[] = [
@ -662,7 +662,7 @@ describe('GoogleGenAIChatProvider', () => {
const config = body['config'] as Record<string, unknown>;
expect(provider).not.toBe(original);
expect(config['max_output_tokens']).toBe(1024);
expect(config['maxOutputTokens']).toBe(1024);
});
});
@ -678,7 +678,7 @@ describe('GoogleGenAIChatProvider', () => {
const contents = messagesToGoogleGenAIContents(history);
for (const content of contents) {
for (const part of content.parts) {
if (part.function_response) return part.function_response.name;
if (part.functionResponse) return part.functionResponse.name;
}
}
return undefined;
@ -733,8 +733,8 @@ describe('GoogleGenAIChatProvider', () => {
});
});
describe('no id in function_call or function_response', () => {
it('does not include id in function_call or function_response parts', () => {
describe('no id in functionCall or functionResponse', () => {
it('does not include id in functionCall or functionResponse parts', () => {
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'Add 2 and 3' }], toolCalls: [] },
{
@ -760,11 +760,11 @@ describe('GoogleGenAIChatProvider', () => {
for (const content of contents) {
for (const part of content.parts) {
if (part.function_call) {
expect(part.function_call).not.toHaveProperty('id');
if (part.functionCall) {
expect(part.functionCall).not.toHaveProperty('id');
}
if (part.function_response) {
expect(part.function_response).not.toHaveProperty('id');
if (part.functionResponse) {
expect(part.functionResponse).not.toHaveProperty('id');
}
}
}
@ -772,7 +772,7 @@ describe('GoogleGenAIChatProvider', () => {
});
describe('with thinking', () => {
it('non-gemini-3 model uses thinking_budget', async () => {
it('non-gemini-3 model uses thinkingBudget', async () => {
const provider = createProvider({ model: 'gemini-2.5-flash' }).withThinking('high');
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'Think' }], toolCalls: [] },
@ -780,13 +780,13 @@ describe('GoogleGenAIChatProvider', () => {
const body = await captureRequestBody(provider, '', [], history);
const config = body['config'] as Record<string, unknown>;
expect(config['thinking_config']).toEqual({
include_thoughts: true,
thinking_budget: 32_000,
expect(config['thinkingConfig']).toEqual({
includeThoughts: true,
thinkingBudget: 32_000,
});
});
it('gemini-3 model uses thinking_level', async () => {
it('gemini-3 model uses thinkingLevel', async () => {
const provider = createProvider({ model: 'gemini-3-pro-preview' }).withThinking('high');
const history: Message[] = [
{ role: 'user', content: [{ type: 'text', text: 'Think' }], toolCalls: [] },
@ -794,9 +794,9 @@ describe('GoogleGenAIChatProvider', () => {
const body = await captureRequestBody(provider, '', [], history);
const config = body['config'] as Record<string, unknown>;
expect(config['thinking_config']).toEqual({
include_thoughts: true,
thinking_level: 'HIGH',
expect(config['thinkingConfig']).toEqual({
includeThoughts: true,
thinkingLevel: 'HIGH',
});
});
@ -808,9 +808,9 @@ describe('GoogleGenAIChatProvider', () => {
const body = await captureRequestBody(provider, '', [], history);
const config = body['config'] as Record<string, unknown>;
expect(config['thinking_config']).toEqual({
include_thoughts: false,
thinking_budget: 0,
expect(config['thinkingConfig']).toEqual({
includeThoughts: false,
thinkingBudget: 0,
});
});
@ -824,7 +824,7 @@ describe('GoogleGenAIChatProvider', () => {
];
const body = await captureRequestBody(provider, '', [], history);
const config = body['config'] as Record<string, unknown>;
return config['thinking_config'] as Record<string, unknown> | undefined;
return config['thinkingConfig'] as Record<string, unknown> | undefined;
}
it('off minimizes thinking and hides thoughts (not just default config)', async () => {
@ -832,32 +832,32 @@ describe('GoogleGenAIChatProvider', () => {
// Gemini 3 cannot be fully disabled, but we should request the lowest
// available level (MINIMAL) and suppress thought output.
expect(thinkingConfig).toEqual({
include_thoughts: false,
thinking_level: 'MINIMAL',
includeThoughts: false,
thinkingLevel: 'MINIMAL',
});
});
it('low maps to LOW', async () => {
const thinkingConfig = await captureThinkingConfig('low');
expect(thinkingConfig).toEqual({
include_thoughts: true,
thinking_level: 'LOW',
includeThoughts: true,
thinkingLevel: 'LOW',
});
});
it('medium maps to MEDIUM (not HIGH)', async () => {
const thinkingConfig = await captureThinkingConfig('medium');
expect(thinkingConfig).toEqual({
include_thoughts: true,
thinking_level: 'MEDIUM',
includeThoughts: true,
thinkingLevel: 'MEDIUM',
});
});
it('high maps to HIGH', async () => {
const thinkingConfig = await captureThinkingConfig('high');
expect(thinkingConfig).toEqual({
include_thoughts: true,
thinking_level: 'HIGH',
includeThoughts: true,
thinkingLevel: 'HIGH',
});
});
});