Skip to content

generateText(): generated text is empty when used with custom tool #6414

Description

@eyaldar

Description

Hi, I have a custom tool I wrote and when I'm trying to use it with generateText it return an empty text result.
my tool definition (assume I see the code is called and return result)

tool({
                description: ' "Use this tool to fetch real-time web data for queries that require the latest information, niche topics, local context, or when the answer may have changed since the model’s last update. This includes news, recent events, evolving technologies, up-to-date documentation, or anything likely not captured in training data. The tool supports issuing queries to a search engine and browsing the returned links to extract relevant content. ALWAYS PASS engine parameter with \'google\'',
                parameters: z.object({
                    query: z.string()
                }),
                // Function to execute the search
                execute: async ({ query }) => {
                    try {
                        console.log(`[SERPAPI] Searching for: ${query}`);
                        // Format our query for SerpAPI directly
                        const params = new URLSearchParams({
                            engine: 'google',
                            api_key: apiKey,
                            q: query,
                            output: 'json'
                        });
                        // Make the request to SERP API
                        const response = await fetch(`https://serpapi.com/search?${params.toString()}`);
                        if (!response.ok) {
                            throw new Error(`SERP API error: ${response.status} ${response.statusText}`);
                        }
                        const data = await response.json();
                        console.log(`[SERPAPI] Received response with ${data.organic_results?.length || 0} results`);
                        // Format the results to match our expected schema defined in constants.js
                        const results = data.organic_results?.map(result => ({
                            url: result.link,
                            snippet: result.snippet || result.title // Fallback to title if snippet isn't available
                        })) || [];
                        // Return in the format our app expects
                        return JSON.stringify({ results });
                    } catch (error) {
                        // Enhanced error logging with full stack trace
                        console.error('[SERPAPI] Search error:');
                        console.error(error);
                        console.error(error.stack);
                        return JSON.stringify({ results: [] }); // Return empty results on error to match schema
                    }
                }
            })

And this is my call:

import { openai } from '@ai-sdk/openai';
import { generateText, Output } from 'ai';
import { z } from 'zod';
const searchResultsSchema = z.object({
  results: z.array(z.object({
    url: z.string(),
    snippet: z.string()
  }))
});
const response = await generateText({
    model: openai.responses('gpt-4o-mini'),
    prompt: `${prompt}
    IMPORTANT: You must ALWAYS return this exact structure no matter what the tool returns.
    NEVER return raw markdown or the original tool response or empty strings.`,
    tools: {
      serpapi_search: /* the tool definition above  */
    },
    tool_choice: { type: 'tool', toolName: 'serpapi_search' },
    experimental_output: Output.object({ schema: searchResultsSchema }),
    debug: true,
    maxToolRoundTrip: 2,
    onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) {
      // Log the tool results
    }
});
        // Safely access results, handling null/undefined
        let results = response.experimental_output?.results || [];

Adding the following parts work:

    maxToolRoundTrip: 1,
    maxSteps: 2,
...
    experimental_prepareStep: async ({ model, stepNumber, maxSteps, steps }) => {
      if (stepNumber > 0) {
        return {
          experimental_activeTools: [],
        };
      }
    },

Is there a better way to control how many times a tool is called? what am I doing wrong?

AI SDK Version

  • ai: 4.3.16
  • @ai-sdk/openai: 1.3.22

Activity

  1. eyaldar commented on Jun 15, 2025

    @eyaldar
    Author

    Hey, trying again, is this really the way to solve this issue?

  2. lgrammel commented on Jun 26, 2025

    @lgrammel
    Collaborator

    hi can you log any potential errors with onError? also what is returning an empty result, your tool or the text generation?

  3. eyaldar commented on Jun 26, 2025

    @eyaldar
    Author

    Hey, I'll check the onError and get back to you.
    The tool returns responses correctly, but the text generation returns empty unless I block the tools from hogging all the allocated requests (like you can see in the shared code)

  4. eyaldar commented on Jun 26, 2025

    @eyaldar
    Author

    I'm not sure where can I put the onError, I don't see it as an option in generateText, what am I doing wrong?

  5. lgrammel commented on Jun 26, 2025

    @lgrammel
    Collaborator

    Nevermind, was thinking this is stream text. So you are seeing no errors?

  6. eyaldar commented on Jun 26, 2025

    @eyaldar
    Author

    No errors, toolsResult return the tool results, text result is empty and experimental_output crashes

  7. added
    bugSomething isn't working as documented
    ai/corecore functions like generateText, streamText, etc. Provider utils, and provider spec.
    and removed on Jun 26, 2025
  8. lili21 commented on Aug 6, 2025

    @lili21
    Contributor

    same issue here after I upgrade to v5. the result.content is tool call info. and result.text is empty

  9. anriuss commented on Sep 5, 2025

    @anriuss

    Same issue here after I upgrade to v5. Still no fix?

  10. gr2m commented on Sep 8, 2025

    @gr2m
    Collaborator

    Sorry that we didn't get to it. A lot changed since the issue was originally created.

    @lili21 @anriuss could you or someone else provide a minimal code snippet that reproduces the problem with latest ai?

  11. eyaldar commented on Sep 8, 2025

    @eyaldar
    Author

    Actually the code stays about the same... not a lot have changed in those part specifically (except for tool -> parameters to inputSchema):

    import { tool, generateText, Output } from 'ai';
    import dotenv from 'dotenv';
    import { openai } from '@ai-sdk/openai';
    import { z } from 'zod';
    dotenv.config();
    
    const exampleTool = tool({
        description: ' "Use this tool to fetch real-time web data for queries that require the latest information, niche topics, local context, or when the answer may have changed since the model’s last update. This includes news, recent events, evolving technologies, up-to-date documentation, or anything likely not captured in training data. The tool supports issuing queries to a search engine and browsing the returned links to extract relevant content. ALWAYS PASS engine parameter with \'google\'',
        inputSchema: z.object({
            query: z.string()
        }),
        // Function to execute the search
        execute: async ({ query }) => {
            const results = [{
                "url": "https://www.google.com",
                "snippet": "Google is a search engine"
            }, {
                "url": "https://www.yahoo.com",
                "snippet": "Yahoo is a search engine"
            }]
            // Return in the format our app expects
            return JSON.stringify({ results });
        }
    })
    
    const searchResultsSchema = z.object({
      results: z.array(z.object({
        url: z.string(),
        snippet: z.string()
      }))
    });
    
    const prompt = `Find me a site that has a blog post about the latest news in the world`;
    
    const response = await generateText({
        model: openai.responses('gpt-4o-mini'),
        prompt: `${prompt}
        IMPORTANT: You must ALWAYS return this exact structure no matter what the tool returns.
        NEVER return raw markdown or the original tool response or empty strings.`,
        tools: {
          exampleTool
        },
        tool_choice: { type: 'tool', toolName: 'exampleTool' },
        experimental_output: Output.object({ schema: searchResultsSchema }),
        debug: true,
        onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) {
          // Log the tool results
        }
    });
            // Safely access results, handling null/undefined
    let results = response.experimental_output?.results || [];
    
    console.log(results);
  12. aurindo10 commented on Sep 8, 2025

    @aurindo10

    use the stepCountIs. I had the same issue and it was fixed adding const result = await generateText({ model: openai("gpt-5"), stopWhen: stepCountIs(2), });
    const { generateText, tool, stepCountIs } = await import("ai");

  13. eyaldar commented on Sep 8, 2025

    @eyaldar
    Author

    This works, but I don't understand why... why do I have to add stopWhen property and why isn't that documented?

  14. FakieHeelflip commented on Sep 23, 2025

    @FakieHeelflip

    Wow, this is really weird. I had the same issue and why the heck is stopWhen: stepCountIs(2) solving this?

    edit: do not forget to import stepCountIs from ai: import { generateText, stepCountIs } from 'ai';

  15. added
    ai/providerrelated to a provider package. Must be assigned together with at least one `provider/*` label
    provider/openaiIssues related to the @ai-sdk/openai provider
    on Oct 15, 2025
  16. aayush-kapoor commented on Nov 13, 2025

    @aayush-kapoor
    Collaborator

    So the reason you need to add the stopWhen parameter is that the generateText and other primitive functions only execute in one step (i.e. one call)

    See docs ref here

    By adding that parameter, you essentially let the api be called twice, now with the toolResult. Which then results in obtaining final text.

    Since this is documented and expected behaviour, marking this as closed

  17. lgrammel commented on Jul 22, 2026

    @lgrammel
    Collaborator

    Identify issue type

    Identified as None
    Confidence: High

    Rationale

    This is primarily a usage/support question resolved by configuring documented multi-step execution, not a confirmed bug or a request to change the product or documentation.

    Evidence

    • The request forces a tool call, while generateText performs only one step by default, leaving no subsequent step to generate final text from the tool result.
    • Adding stopWhen: stepCountIs(2) enables the required second model call and resolves the behavior.
    • The closing comment identifies this as documented, expected multi-step tool-calling behavior rather than an SDK failure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai/corecore functions like generateText, streamText, etc. Provider utils, and provider spec.ai/providerrelated to a provider package. Must be assigned together with at least one `provider/*` labelbugSomething isn't working as documentedprovider/openaiIssues related to the @ai-sdk/openai providerreproduction providedtask-identify-issue-type-done

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions