Skip to content

Gateway-native agentic loop: tool inputSchema not wrapped in jsonSchema() -> 'schema is not a function' on openai-compatible providers #1764

@andrea-kingautomation

Description

@andrea-kingautomation

Summary

With agent.use_gateway_loop=true, running the agentic loop on a non-Anthropic, openai-compatible provider fails on the very first turn with:

[chat(<model>)] schema is not a function. (In 'schema()', 'schema' is an instance of Object)

This kills every agentic subagent run on a non-Anthropic backend — including the dream synthesize writer, so wiki distillation produces 0 pages when the synth model is openai-compatible. The significance judge and all non-agentic chat (gbrain think, query expansion, facts, takes) work fine on the same provider; only the tool-using agentic loop breaks.

Version: 0.41.28.0, AI SDK ai@6.0.174.

Repro

gbrain config set agent.use_gateway_loop true --force
# any openai-compatible recipe model (deepseek/groq/openrouter/etc.)
gbrain agent run --model groq:gpt-5.5 --max-turns 8 "Use the put_page tool once to write a short page, then stop."
gbrain agent logs <job_id>
# -> turn 0: llm_call_failed error="[chat(groq:gpt-5.5)] schema is not a function ..."

Root cause

core/ai/gateway.ts (~line 2360, the chat() tool-construction reduce) builds each tool's inputSchema as a hand-rolled object literal cast to any:

const tools = (opts.tools ?? []).reduce((acc, t) => {
  acc[t.name] = {
    description: t.description,
    inputSchema: { jsonSchema: t.inputSchema } as any,   // <-- not a Schema
  };
  return acc;
}, {} as Record<string, any>);

AI SDK v6 expects inputSchema to be a FlexibleSchema (a Zod schema or the result of jsonSchema(...)), which carries the schema symbol + a validate function. A plain { jsonSchema } object lacks those, so when the SDK normalizes the tool for an openai-compatible provider it invokes the schema as a function and throws. (The Anthropic path happens to tolerate the same object in some configs, which is likely why it went unnoticed.)

Fix

Wrap with the SDK's jsonSchema() helper:

import { /* ... */ jsonSchema } from 'ai';

acc[t.name] = {
  description: t.description,
  inputSchema: jsonSchema(t.inputSchema),
};

Verified locally: with this one change the full agentic loop runs on an openai-compatible provider (groq/gpt-5.5) — llm_call_completed → tool_called → tool_result → page persisted, embedded, and searchable.

Secondary issue (same loop, parallel tool calls)

After the schema fix, when the model emits more than one tool call in a single turn, only one tool_result is paired back, and the next turn fails with:

[chat(<model>)] Tool result is missing for tool call <id>.
[chat(<model>)] Invalid prompt: The messages do not match the ModelMessage[] schema.

The writes themselves succeed, but the subagent job is then marked dead. The gateway loop should emit a tool_result block for every tool call in the assistant turn (including errors/“not executed” stubs) before the next model call, so the ModelMessage[] history stays valid. This matters because openai-compatible models emit parallel tool calls more readily than Claude.

Impact / who hits this

Anyone pointing gbrain's agentic features at a non-Anthropic provider (cost reasons, local models, gateways). Everything except the agentic loop already works on these providers; this is the last gap for a fully non-Anthropic brain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions