Skip to content

support google genai code execution tool #3205

@altbdoor

Description

@altbdoor

Description

Background

Google Gemini API supports code execution, which runs Python code in Google servers.

I was not able to set a codeExecution tool with the SDK, so I resorted to overriding the fetch.

import { createGoogleGenerativeAI } from "@ai-sdk/google";

const google = createGoogleGenerativeAI({
  apiKey: process.env.GOOGLE_API_KEY,
  fetch: async (url, options) => {
    const fixedOptions = JSON.parse(options!.body! as string);
    if (!('tools' in fixedOptions)) {
      fixedOptions['tools'] = [{ codeExecution: {} }]
    }

    options!.body = JSON.stringify(fixedOptions);
    return await fetch(url, options);
  },
});

Running streamText will then throw AI_TypeValidationError.

Show error
{
  type: 'error',
  error: _TypeValidationError [AI_TypeValidationError]: Type validation failed: Value: {"candidates":[{"content":{"parts":[{"codeExecutionResult":{"outcome":"OUTCOME_OK","output":"sum of first 50 primes = 4227\n"}}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":659,"candidatesTokenCount":74,"totalTokenCount":733}}.
  Error message: [
    {
      "code": "invalid_union",
      "unionErrors": [
        {
          "issues": [
            {
              "code": "invalid_type",
              "expected": "string",
              "received": "undefined",
              "path": [
                "candidates",
                0,
                "content",
                "parts",
                0,
                "text"
              ],
              "message": "Required"
            }
          ],
          "name": "ZodError"
        },
        {
          "issues": [
            {
              "code": "invalid_type",
              "expected": "object",
              "received": "undefined",
              "path": [
                "candidates",
                0,
                "content",
                "parts",
                0,
                "functionCall"
              ],
              "message": "Required"
            }
          ],
          "name": "ZodError"
        }
      ],
      "path": [
        "candidates",
        0,
        "content",
        "parts",
        0
      ],
      "message": "Invalid input"
    }
  ]
      at _TypeValidationError.wrap (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider/dist/index.mjs:483:86)
      at safeValidateTypes (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:257:80)
      at safeParseJSON (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:297:12)
      ... 45 lines matching cause stack trace ...
      at readableByteStreamControllerEnqueue (node:internal/webstreams/readablestream:2837:7)
      at ReadableByteStreamController.enqueue (node:internal/webstreams/readablestream:1184:5) {
    cause: ZodError: [
      {
        "code": "invalid_union",
        "unionErrors": [
          {
            "issues": [
              {
                "code": "invalid_type",
                "expected": "string",
                "received": "undefined",
                "path": [
                  "candidates",
                  0,
                  "content",
                  "parts",
                  0,
                  "text"
                ],
                "message": "Required"
              }
            ],
            "name": "ZodError"
          },
          {
            "issues": [
              {
                "code": "invalid_type",
                "expected": "object",
                "received": "undefined",
                "path": [
                  "candidates",
                  0,
                  "content",
                  "parts",
                  0,
                  "functionCall"
                ],
                "message": "Required"
              }
            ],
            "name": "ZodError"
          }
        ],
        "path": [
          "candidates",
          0,
          "content",
          "parts",
          0
        ],
        "message": "Invalid input"
      }
    ]
        at get error (webpack-internal:///(action-browser)/./node_modules/zod/lib/index.mjs:699:31)
        at Object.eval [as validate] (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:227:101)
        at safeValidateTypes (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:251:31)
        at safeParseJSON (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:297:12)
        at Object.transform (webpack-internal:///(action-browser)/./node_modules/@ai-sdk/provider-utils/dist/index.mjs:503:13)
        at invokePromiseCallback (node:internal/webstreams/util:181:10)
        at Object.transformAlgorithm (node:internal/webstreams/util:186:23)
        at transformStreamDefaultControllerPerformTransform (node:internal/webstreams/transformstream:526:37)
        at transformStreamDefaultSinkWriteAlgorithm (node:internal/webstreams/transformstream:572:10)
        at node:internal/webstreams/transformstream:377:16
        at writableStreamDefaultControllerProcessWrite (node:internal/webstreams/writablestream:1127:5)
        at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1242:5)
        at writableStreamDefaultControllerWrite (node:internal/webstreams/writablestream:1116:3)
        at writableStreamDefaultWriterWrite (node:internal/webstreams/writablestream:1006:3)
        at [kChunk] (node:internal/webstreams/readablestream:1585:31)
        at readableStreamFulfillReadRequest (node:internal/webstreams/readablestream:2118:24)
        at readableStreamDefaultControllerEnqueue (node:internal/webstreams/readablestream:2310:5)
        at transformStreamDefaultControllerEnqueue (node:internal/webstreams/transformstream:507:5)
        at TransformStreamDefaultController.enqueue (node:internal/webstreams/transformstream:323:5)
        at eval (webpack-internal:///(action-browser)/./node_modules/eventsource-parser/dist/stream.js:14:24)
        at parseEventStreamLine (webpack-internal:///(action-browser)/./node_modules/eventsource-parser/dist/index.js:77:9)
        at Object.feed (webpack-internal:///(action-browser)/./node_modules/eventsource-parser/dist/index.js:65:7)
        at Object.transform (webpack-internal:///(action-browser)/./node_modules/eventsource-parser/dist/stream.js:19:16)
        at invokePromiseCallback (node:internal/webstreams/util:181:10)
        at Object.transformAlgorithm (node:internal/webstreams/util:186:23)
        at transformStreamDefaultControllerPerformTransform (node:internal/webstreams/transformstream:526:37)
        at transformStreamDefaultSinkWriteAlgorithm (node:internal/webstreams/transformstream:572:10)
        at node:internal/webstreams/transformstream:377:16
        at writableStreamDefaultControllerProcessWrite (node:internal/webstreams/writablestream:1127:5)
        at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1242:5)
        at writableStreamDefaultControllerWrite (node:internal/webstreams/writablestream:1116:3)
        at writableStreamDefaultWriterWrite (node:internal/webstreams/writablestream:1006:3)
        at [kChunk] (node:internal/webstreams/readablestream:1585:31)
        at readableStreamFulfillReadRequest (node:internal/webstreams/readablestream:2118:24)
        at readableStreamDefaultControllerEnqueue (node:internal/webstreams/readablestream:2310:5)
        at transformStreamDefaultControllerEnqueue (node:internal/webstreams/transformstream:507:5)
        at TransformStreamDefaultController.enqueue (node:internal/webstreams/transformstream:323:5)
        at Object.transform (node:internal/webstreams/encoding:138:22)
        at invokePromiseCallback (node:internal/webstreams/util:181:10)
        at Object.transformAlgorithm (node:internal/webstreams/util:186:23)
        at transformStreamDefaultControllerPerformTransform (node:internal/webstreams/transformstream:526:37)
        at transformStreamDefaultSinkWriteAlgorithm (node:internal/webstreams/transformstream:572:10)
        at node:internal/webstreams/transformstream:377:16
        at writableStreamDefaultControllerProcessWrite (node:internal/webstreams/writablestream:1127:5)
        at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1242:5)
        at writableStreamDefaultControllerWrite (node:internal/webstreams/writablestream:1116:3)
        at writableStreamDefaultWriterWrite (node:internal/webstreams/writablestream:1006:3)
        at [kChunk] (node:internal/webstreams/readablestream:1585:31)
        at readableStreamFulfillReadRequest (node:internal/webstreams/readablestream:2118:24)
        at readableByteStreamControllerEnqueue (node:internal/webstreams/readablestream:2837:7) {
      issues: [Array],
      addIssue: [Function (anonymous)],
      addIssues: [Function (anonymous)],
      errors: [Array]
    },
    value: { candidates: [Array], usageMetadata: [Object] },
    [Symbol(vercel.ai.error)]: true,
    [Symbol(vercel.ai.error.AI_TypeValidationError)]: true
  }
}

I tried logging the chunk response, and it appears that the chunk does not have the text property.

Show chunk response
// first chunk
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "executableCode": {
              "language": "PYTHON",
              "code": "\nimport math\n\ndef is_prime(num):\n    \"\"\"\n    Checks if a number is prime.\n    \"\"\"\n    if num <= 1:\n        return False\n    for i in range(2, int(math.sqrt(num)) + 1):\n        if num % i == 0:\n            return False\n    return True\n\nprimes = []\nnum = 2\ncount = 0\nwhile count < 50:\n    if is_prime(num):\n        primes.append(num)\n        count += 1\n    num += 1\n\nprint(f'The sum of the first 50 prime numbers is: {sum(primes)}')\n"
            }
          }
        ],
        "role": "model"
      },
      "finishReason": "STOP",
      "index": 0,
      "safetyRatings": [
        {
          "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HATE_SPEECH",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HARASSMENT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
          "probability": "NEGLIGIBLE"
        }
      ]
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 659,
    "candidatesTokenCount": 197,
    "totalTokenCount": 856
  }
}
// second chunk
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "codeExecutionResult": {
              "outcome": "OUTCOME_OK",
              "output": "The sum of the first 50 prime numbers is: 5117\n"
            }
          }
        ],
        "role": "model"
      },
      "index": 0
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 659,
    "candidatesTokenCount": 197,
    "totalTokenCount": 856
  }
}

This format appears to be enforced in the schema, defined in:

const contentSchema = z.object({
role: z.string(),
parts: z.array(
z.union([
z.object({
text: z.string(),
}),
z.object({
functionCall: z.object({
name: z.string(),
args: z.unknown(),
}),
}),
]),
),
});

Question

So I have a number of questions here:

  1. Should the library allow devs to set the codeExecution tool for Gemini somehow?
  2. Should the library adapt to the two code execution parts (executableCode and codeExecutionResult)?

Workaround

If I access the textStream, the code will throw an error:

const { textStream, fullStream } = await streamText({
  model: google('gemini-1.5-flash-latest'),
  /* other configs */
});

// this throws an error as the zod validation fails
for await (const chunk of textStream) {
  stream.update(chunk);
}

So the work around I had was to access the fullStream, and silently ignore these errors.

const { textStream, fullStream } = await streamText({
  model: google('gemini-1.5-flash-latest'),
  /* other configs */
});

for await (const chunk of fullStream) {
  if (chunk.type === "text-delta" && !!chunk.textDelta) {
    stream.update(chunk.textDelta);
  } else if (
    chunk.type === 'error' &&
    chunk.error instanceof TypeValidationError
  ) {
    // silently ignore
    // maybe better conditions can be set to really identify the zod validation error
  }
}

Code example

No response

Additional context

Packages installed:

$ npm ls
<redacted>@0.1.0 <redacted>
├── @ai-sdk/google@0.0.51
├── @ai-sdk/openai@0.0.63
├── @types/dompurify@3.0.5
├── @types/node@20.16.10
├── @types/react-dom@18.3.0
├── @types/react@18.3.10
├── @vercel/blob@0.24.0
├── ai@3.4.7
├── dompurify@3.1.7
├── eslint-config-next@14.2.14
├── eslint@8.57.1
├── highlight.js@11.10.0
├── marked@14.1.2
├── next-auth@5.0.0-beta.22
├── next@14.2.14
├── prettier@3.3.3
├── react-dom@18.3.1
├── react@18.3.1
├── typescript@5.6.2
└── zod@3.23.8

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions