Summary
@tanstack/ai-openai sends every function tool with strict: true, but its schema converter does not remove JSON-Schema keywords that OpenAI's strict function-calling validator forbids, nor does it handle free-form (open-map) objects that are unrepresentable in strict mode. When any tool's inputSchema contains such a construct, OpenAI rejects the entire request with a 400 before the model runs, so the whole chat turn dies — not just that one tool.
This is easy to hit with MCP tools, whose schemas are authored by third-party servers and routinely use keywords TanStack's converter doesn't touch.
Versions
@tanstack/ai@0.32.0
@tanstack/ai-openai@0.15.2
@tanstack/ai-mcp@0.1.3
Real-world repro
Connect to Linear's hosted MCP and let the model see its tools:
const mcp = await createMCPClient({
transport: { type: "http", url: "https://mcp.linear.app/mcp",
headers: { Authorization: `Bearer ${process.env.LINEAR_API_KEY}` } },
});
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
mcp: { clients: [mcp] }, // or: tools: [...await mcp.tools()]
});
Linear's save_diff_comment tool declares:
Actual result
400 Invalid schema for function 'save_diff_comment':
In context=('properties','anchor'), 'propertyNames' is not permitted.
Every turn fails, because OpenAI validates the whole tool array up front.
Expected result
The request succeeds. Either the tool's schema is made strict-safe automatically, or the tool falls back to strict: false, so one third-party tool with an over-rich schema can't take down the entire chat.
Root cause
Two behaviors in @tanstack/ai-openai combine:
convertFunctionToolToAdapterFormat hardcodes strict: true on every tool (tools/function-tool.js), so OpenAI's strictest validation always applies.
makeOpenAIStructuredOutputCompatible (utils/schema-converter.js) normalizes shape but not keywords. It wraps optionals as nullable, sets additionalProperties: false and marks all keys required — but it passes through non-whitelisted keywords verbatim (propertyNames, patternProperties, unevaluatedProperties, $schema, $id, …), and it only sets additionalProperties: false on objects that already have properties, so a property-less / open-map object keeps its open additionalProperties and gets no properties. Both are invalid in strict mode.
(Line references are from 0.8.2 source, but the observed 400 on 0.15.2 confirms the behavior is unchanged.)
Suggested fix
In the strict-mode converter:
- Strip keywords OpenAI's strict validator rejects (
propertyNames, patternProperties, unevaluatedProperties, $schema, $id) at every level.
- Coerce free-form objects (type
object with no properties) into a closed empty object (properties: {}, additionalProperties: false, required: []) so they validate.
- Alternatively, detect strict-incompatible schemas and emit that tool with
strict: false instead of forcing strict: true universally, preserving the tool's full expressiveness.
Workaround (for others hitting this)
Discover MCP tools manually and scrub their schemas before passing to chat():
const UNSUPPORTED = new Set(["propertyNames","patternProperties","unevaluatedProperties","$schema","$id"]);
function toStrictSafe(s: any): any {
if (Array.isArray(s)) return s.map(toStrictSafe);
if (!s || typeof s !== "object") return s;
const out: any = {};
for (const [k, v] of Object.entries(s)) if (!UNSUPPORTED.has(k)) out[k] = toStrictSafe(v);
const isObj = out.type === "object" || (Array.isArray(out.type) && out.type.includes("object"));
if (isObj && !out.properties) { out.properties = {}; out.additionalProperties = false; out.required = []; }
return out;
}
const tools = (await mcp.tools()).map(t =>
t.inputSchema ? { ...t, inputSchema: toStrictSafe(t.inputSchema) } : t
);
chat({ adapter: openaiText("gpt-5.5"), messages, tools /* + your close middleware */ });
Summary
@tanstack/ai-openaisends every function tool withstrict: true, but its schema converter does not remove JSON-Schema keywords that OpenAI's strict function-calling validator forbids, nor does it handle free-form (open-map) objects that are unrepresentable in strict mode. When any tool'sinputSchemacontains such a construct, OpenAI rejects the entire request with a 400 before the model runs, so the whole chat turn dies — not just that one tool.This is easy to hit with MCP tools, whose schemas are authored by third-party servers and routinely use keywords TanStack's converter doesn't touch.
Versions
@tanstack/ai@0.32.0@tanstack/ai-openai@0.15.2@tanstack/ai-mcp@0.1.3Real-world repro
Connect to Linear's hosted MCP and let the model see its tools:
Linear's
save_diff_commenttool declares:{ "type": "object", "properties": { "anchor": { "type": "object", "additionalProperties": {}, "propertyNames": { "type": "string" }, // <-- open map "description": "Optional comment anchor..." }, "body": { "type": "string", "minLength": 1 } }, "required": ["body"] }Actual result
Every turn fails, because OpenAI validates the whole tool array up front.
Expected result
The request succeeds. Either the tool's schema is made strict-safe automatically, or the tool falls back to
strict: false, so one third-party tool with an over-rich schema can't take down the entire chat.Root cause
Two behaviors in
@tanstack/ai-openaicombine:convertFunctionToolToAdapterFormathardcodesstrict: trueon every tool (tools/function-tool.js), so OpenAI's strictest validation always applies.makeOpenAIStructuredOutputCompatible(utils/schema-converter.js) normalizes shape but not keywords. It wraps optionals as nullable, setsadditionalProperties: falseand marks all keys required — but it passes through non-whitelisted keywords verbatim (propertyNames,patternProperties,unevaluatedProperties,$schema,$id, …), and it only setsadditionalProperties: falseon objects that already haveproperties, so a property-less / open-map object keeps its openadditionalPropertiesand gets noproperties. Both are invalid in strict mode.(Line references are from
0.8.2source, but the observed 400 on0.15.2confirms the behavior is unchanged.)Suggested fix
In the strict-mode converter:
propertyNames,patternProperties,unevaluatedProperties,$schema,$id) at every level.objectwith noproperties) into a closed empty object (properties: {},additionalProperties: false,required: []) so they validate.strict: falseinstead of forcingstrict: trueuniversally, preserving the tool's full expressiveness.Workaround (for others hitting this)
Discover MCP tools manually and scrub their schemas before passing to
chat():