diff --git a/.changeset/fix-gemini-tool-calling.md b/.changeset/fix-gemini-tool-calling.md new file mode 100644 index 000000000..f0d7bbe91 --- /dev/null +++ b/.changeset/fix-gemini-tool-calling.md @@ -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. diff --git a/packages/agent-core/src/agent/context/index.ts b/packages/agent-core/src/agent/context/index.ts index 5f6abc2f2..22beabebd 100644 --- a/packages/agent-core/src/agent/context/index.ts +++ b/packages/agent-core/src/agent/context/index.ts @@ -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; diff --git a/packages/agent-core/src/loop/events.ts b/packages/agent-core/src/loop/events.ts index 62d4c65a9..6fbc6c356 100644 --- a/packages/agent-core/src/loop/events.ts +++ b/packages/agent-core/src/loop/events.ts @@ -78,6 +78,7 @@ export interface LoopToolCallEvent { readonly args: unknown; readonly description?: string | undefined; readonly display?: ToolInputDisplay | undefined; + readonly extras?: Record | undefined; } export interface LoopToolResultEvent { diff --git a/packages/agent-core/src/loop/tool-call.ts b/packages/agent-core/src/loop/tool-call.ts index 801609df9..e4f033780 100644 --- a/packages/agent-core/src/loop/tool-call.ts +++ b/packages/agent-core/src/loop/tool-call.ts @@ -713,5 +713,6 @@ async function dispatchToolCall( args, description: displayFields?.description, display: displayFields?.display, + extras: toolCall.extras, }); } diff --git a/packages/agent-core/test/agent/context.test.ts b/packages/agent-core/test/agent/context.test.ts index 983cdfa0b..9d65556f3 100644 --- a/packages/agent-core/test/agent/context.test.ts +++ b/packages/agent-core/test/agent/context.test.ts @@ -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(); diff --git a/packages/kosong/src/providers/google-genai.ts b/packages/kosong/src/providers/google-genai.ts index 8c28cf215..1b04c928c 100644 --- a/packages/kosong/src/providers/google-genai.ts +++ b/packages/kosong/src/providers/google-genai.ts @@ -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; + parametersJsonSchema: Record; } 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 }; - function_response?: { + functionCall?: { name: string; args: Record }; + functionResponse?: { name: string; response: Record; 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 `` 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 = { ...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 { diff --git a/packages/kosong/test/e2e/google-genai-adapter.test.ts b/packages/kosong/test/e2e/google-genai-adapter.test.ts index 21c9d50aa..12210eff2 100644 --- a/packages/kosong/test/e2e/google-genai-adapter.test.ts +++ b/packages/kosong/test/e2e/google-genai-adapter.test.ts @@ -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), + }), + ], }, ]); diff --git a/packages/kosong/test/google-genai.test.ts b/packages/kosong/test/google-genai.test.ts index eecc48b3a..3bcf00030 100644 --- a/packages/kosong/test/google-genai.test.ts +++ b/packages/kosong/test/google-genai.test.ts @@ -128,7 +128,7 @@ describe('GoogleGenAIChatProvider', () => { expect(body['contents']).toEqual([{ parts: [{ text: 'Hello!' }], role: 'user' }]); const config = body['config'] as Record; - 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; - 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; 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)['function_call'] !== undefined, - ) as { function_call: Record; thought_signature?: unknown } | undefined; + (p) => (p as Record)['functionCall'] !== undefined, + ) as { functionCall: Record; 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>; }; 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: 'This is a system reminder5', @@ -607,7 +607,7 @@ describe('GoogleGenAIChatProvider', () => { }, }, { - function_response: { + functionResponse: { name: 'multiply', response: { output: 'This is a system reminder20', @@ -632,15 +632,15 @@ describe('GoogleGenAIChatProvider', () => { expect(body['contents']).toEqual([{ parts: [{ text: 'Hello!' }], role: 'user' }]); const config = body['config'] as Record; - 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; 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; 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; - 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; - 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; - 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; - return config['thinking_config'] as Record | undefined; + return config['thinkingConfig'] as Record | 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', }); }); });