Skip to content

@ai-sdk/openai: support ordered additional_tools after a normal tool result #17155

Description

@ruiconti

Description

Request

Please let @ai-sdk/openai serialize an OpenAI Responses additional_tools item at an exact point in ModelMessage history, including immediately after a normal tool-result.

This is the concrete application case behind draft PR #17097. The proposed providerOptions.openai.additionalTools direction would work for eve if it can be attached to a tool-result part and produces a sibling additional_tools input item after the normal function_call_output.

eve does not need a provider-neutral abstraction for this. An OpenAI-specific option on the existing tool-result part is sufficient.

This request is narrower than adding tool search. AI SDK already supports OpenAI's hosted and client-executed tool_search. The missing operation is preserving a normal tool result while loading the tools discovered by that result:

R = the ordinary connection_search result
T = the function definitions discovered by that search

input = [
  ...history,
  function_call("connection_search"),
  function_call_output(R),
  additional_tools(T),
]

OpenAI makes tools available only after the additional_tools item's position and requires that position to be preserved when history is replayed. OpenAI's tool-search documentation also states that injecting newly discovered tools at the end of the context preserves the model's cache.

Why eve needs both R and T

eve exposes connection_search as a normal function tool. Its available connection tools depend on runtime state such as the current tenant, installed connections, authorization, and the external tool registry.

The result is richer than a list of OpenAI function definitions:

type ConnectionSearchResult = Array<{
  connection: string;
  description: string;
  tool?: string;
  qualifiedName?: string;
  inputSchema?: object;
  outputSchema?: object;
  needsAuthorization?: boolean;
  error?: string;
}>;

That result tells the model which tools matched, but it also preserves connection summaries, partial failures, and authorization state. eve must send it back as the ordinary connection_search result.

For each successful match, eve also creates a normal AI SDK tool with its input schema and a local execute callback. For example, a result for Linear may create linear__list_issues. On the next model step, that function must be available to OpenAI as a structured tool, validated by AI SDK, and executed by eve when called.

The desired runtime path is:

  1. The request starts with a stable tool list that includes connection_search, but not every possible connection tool schema.
  2. The model calls connection_search.
  3. eve performs the external lookup and returns R.
  4. eve builds the discovered AI SDK tools and their local executors.
  5. The next request replays function_call_output(R) and immediately appends additional_tools(T).
  6. The model calls a discovered function such as linear__list_issues.
  7. AI SDK validates the call against the full tools map, then eve's local callback executes it.
  8. On later requests, the same additional_tools item remains at the same history position.

Why client-executed tool_search is not equivalent

openai.tools.toolSearch({ execution: "client" }) is a good fit when the discovery result is the tool list itself. Its current output contract is only { tools: Array<JSONObject> }, and the OpenAI adapter serializes that result as tool_search_output:

Replacing eve's normal connection_search result with that output would discard the summaries, errors, and authorization state in R. Returning R in a separate text message would change the protocol and would no longer preserve the original tool result as one ordered history item.

Adding newly discovered tools to the request-level tools array on every later step is also not equivalent. It changes the early request prefix as the tool set grows. OpenAI's additional_tools guidance exists for applications that load tools outside the normal tool-search flow and need to preserve insertion order. OpenAI's prompt-caching guidance recommends appending runtime changes instead of modifying the existing prefix.

How eve would consume this

An illustrative AI SDK call would look like this. The exact option name is not important, but the placement and serialization are:

const tools = {
  connection_search: connectionSearchTool,
  linear__list_issues: tool({
    description: "List Linear issues",
    inputSchema: linearListIssuesSchema,
    execute: executeLinearListIssues,
  }),
};

await streamText({
  model: openai.responses("gpt-5.4"),

  // Keep discovered tools in the full ToolSet so AI SDK can validate and
  // execute their calls, but do not serialize them in the top-level tools list.
  tools,
  activeTools: ["connection_search"],

  messages: [
    ...history,
    {
      role: "tool",
      content: [
        {
          type: "tool-result",
          toolCallId: searchCallId,
          toolName: "connection_search",
          output: { type: "json", value: connectionSearchResult },
          providerOptions: {
            openai: {
              additionalTools: [linearListIssuesOpenAIDefinition],
            },
          },
        },
      ],
    },
  ],
});

AI SDK already separates these two concerns: activeTools determines the tools prepared for the provider call, while tool-call parsing and local execution use the full tools map. See the current generateText provider preparation, parseToolCall invocation, and executeTools invocation. That gives eve a way to keep validation and execution without duplicating the discovered definitions in the request-level tools list.

The OpenAI adapter would serialize the tool-result part above as:

[
  {
    "type": "function_call_output",
    "call_id": "call_connection_search",
    "output": "[...the complete connection_search result...]"
  },
  {
    "type": "additional_tools",
    "role": "developer",
    "tools": [
      {
        "type": "function",
        "name": "linear__list_issues",
        "description": "List Linear issues",
        "parameters": { "type": "object" }
      }
    ]
  }
]

Acceptance criteria

  • A tool-result part can request an OpenAI additional_tools item without replacing or changing its normal function_call_output.
  • The adapter emits additional_tools immediately after that result and preserves the same order when messages are replayed.
  • The loaded functions may remain in the full AI SDK tools map with local execute callbacks while activeTools omits them from the request-level tools list.
  • A subsequent function call to one of those tools passes AI SDK validation and invokes its local executor.
  • The behavior works in both generateText and streamText.
  • Direct @ai-sdk/openai support is the immediate need. The same message representation should eventually work through AI Gateway when it routes to OpenAI, or the Gateway limitation should be documented separately.

Related eve work:

AI SDK Version

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

Fields

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions