Skip to content

Vertex: convertToGoogleGenerativeAIMessages drops provider-executed code_execution tool-result, breaks multi-turn with mixed tools #15613

Description

@ghempton

Description

When a Gemini/Vertex turn contains both the provider-executed code_execution tool and a client-defined function tool, the next turn fails with:

Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn.

The root cause is that the assistant message generated by convertToModelMessages puts both the code_execution tool-call and its tool-result into the assistant content (because providerExecuted === true), and then convertToGoogleGenerativeAIMessages in @ai-sdk/google round-trips them incorrectly:

  • The inbound parser for executableCode / codeExecutionResult parts (dist/index.mjs:1584-1610 in 3.0.79, equivalent in src/google-generative-ai-language-model.ts) emits tool-call / tool-result content parts with providerExecuted: true and toolName: "code_execution", but does not attach providerMetadata.google.serverToolCallId / serverToolType — unlike the sibling toolCall / toolResponse branches a few lines below.
  • On the outbound side in convert-to-google-generative-ai-messages.ts (3.0.79):
    • The assistant tool-call branch sees no serverToolCallId/serverToolType and falls through to functionCall: { name: 'code_execution', args: ... }.
    • The assistant tool-result branch sees no serverToolCallId/serverToolType and return undefined — the result is silently dropped.

So Vertex receives a model turn with N+1 functionCalls but only N functionResponses in the next user turn (one for the client tool, none for code_execution), and rejects the request.

Reproduction

import { createVertex } from '@ai-sdk/google-vertex';
import { streamText, tool } from 'ai';
import { z } from 'zod';

const vertex = createVertex({ project: 'YOUR_PROJECT', location: 'us-east1' });
const model = vertex('gemini-2.5-flash');

const tools = {
  code_execution: {
    type: 'provider' as const,
    id: 'google.code_execution',
    name: 'codeExecution',
    args: {},
  },
  listItems: tool({
    description: 'List items',
    inputSchema: z.object({}),
    execute: async () => ({ items: ['a', 'b'] }),
  }),
};

// First call — model emits code_execution AND listItems in the same step.
const r1 = await streamText({
  model,
  tools,
  messages: [
    { role: 'user', content: 'Compute 2+2 with Python, then call listItems.' },
  ],
});
const messages = (await r1.response).messages;

// Second call — fails with the Vertex 400 above.
const r2 = await streamText({
  model,
  tools,
  messages: [
    { role: 'user', content: 'Compute 2+2 with Python, then call listItems.' },
    ...messages,
    { role: 'user', content: 'Now repeat the analysis on numbers 3 and 4.' },
  ],
});

Proposed fix

In convertToGoogleGenerativeAIMessages, special-case toolName === 'code_execution' with providerExecuted === true so that:

  • assistant tool-call{ executableCode: { code, language } } (mirroring the inbound parser)
  • assistant tool-result{ codeExecutionResult: { outcome, output } }

Symmetry with the inbound parser at dist/index.mjs:1584-1610 would close the round-trip. Alternatively, attach serverToolCallId / serverToolType on the code-execution parts at inbound time so the existing toolCall / toolResponse outbound branches handle them (though Vertex's API for code_execution actually uses the executable_code / code_execution_result parts, not the newer toolCall/toolResponse shape, so the executable-code branch is the more correct fix).

Versions

  • @ai-sdk/google@3.0.79 (latest stable)
  • @ai-sdk/google-vertex@4.0.137
  • ai@6.0.174

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions