Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

ai-openrouter: malformed Chat Completions tool arguments execute as an empty object

オープン
#1,689 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@tombeckenham がすでに取り組んでいます。

2026年10月9日 から。

  • #1690 @PatrM による — オープン

評価

この issue はまだ評価されていません。

説明

has-pr waiting-on: maintainer
TanStack AI version
  • @tanstack/ai: 0.66.0
  • @tanstack/ai-openrouter: 0.21.2

Also seen on 0.64.1 / 0.20.3. The adapter code is unchanged on main at 377262c0b4e5f59f8fd467a831b9341cc705077e.

Framework/Library version

No UI framework. Node.js 24.18.0, Zod 4.6.5.

Describe the bug and the steps to reproduce it

openRouterText can execute a tool after its streamed arguments fail JSON parsing. On a parse failure the adapter sets TOOL_CALL_END.input to {}, which replaces the original arguments before the core parses them.

#1602 fixed the same fallback in @tanstack/openai-base (#1601). ai-openrouter keeps its own copy of the Chat Completions stream handling, so the fix did not carry over.

OpenRouter produces this input in normal operation. When a model hits its output limit inside a tool call, OpenRouter ends the stream with finish_reason: "tool_calls" (native_finish_reason: "max_output_tokens"), then sends the usage chunk and [DONE]. The arguments are cut off, for example {"sql":"SELECT g.brand, g.model, SUM(s.quantity) AS units.

Steps to reproduce

  1. Define a tool with an optional field: inputSchema: z.object({ sql: z.string().optional() }).
  2. Call chat() with openRouterText('openai/gpt-4.1-mini'), the tool, toolChoice forcing it, and maxCompletionTokens: 12.

A controlled SSE response that sends {"sql": "SELECT 1" with finish_reason: "tool_calls" reproduces it without a provider.

Actual behavior

The handler runs once with {}. The next model request contains a successful tool result. With a required field, the model gets Invalid input: expected string, received undefined instead of a JSON parse error, and the raw arguments are lost.

Expected behavior

Return a tool-result error for malformed JSON without calling the tool, as docs/tools/server-tools.md describes and as openai-base does since #1602.

Cause

Both the finish_reason path and the end-of-body drain set parsedInput = {} after a parse failure. completeToolCall then serializes that object over the original argument string, so the core's malformed-argument check never sees the invalid JSON.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

Self-contained script, no API key. A local SSE server replays the OpenRouter stream described above.

npm init -y && npm pkg set type=module && npm i @tanstack/[email protected] @tanstack/[email protected] [email protected]
node index.mjs

Output:

TOOL_CALL_END input: {}
tool executed with: [{}]
tool result sent to the model: tool ran
index.mjs
// openRouterText runs a tool with {} when its streamed arguments are not valid JSON.
// OpenRouter sends this when a model hits its output limit inside a tool call:
// truncated arguments with finish_reason "tool_calls" (native_finish_reason "max_output_tokens").
// A local SSE server replays that stream, so no API key is needed.
import http from 'node:http'
import { chat, toolDefinition } from '@tanstack/ai'
import { createOpenRouterText } from '@tanstack/ai-openrouter'
import { z } from 'zod'

const chunk = (choice, extra = {}) =>
  `data: ${JSON.stringify({
    id: 'gen-1',
    object: 'chat.completion.chunk',
    created: 1,
    model: 'openai/gpt-4.1-mini',
    choices: [{ index: 0, delta: {}, finish_reason: null, ...choice }],
    ...extra,
  })}\n\n`

const truncatedToolCall = [
  chunk({ delta: { role: 'assistant', content: '' } }),
  chunk({
    delta: {
      tool_calls: [
        {
          index: 0,
          id: 'call_1',
          type: 'function',
          function: { name: 'run_query', arguments: '{"sql":"SELECT brand, model, SUM(units' },
        },
      ],
    },
  }),
  chunk({ finish_reason: 'tool_calls', native_finish_reason: 'max_output_tokens' }),
  chunk(
    { finish_reason: 'tool_calls', native_finish_reason: 'max_output_tokens' },
    { usage: { prompt_tokens: 10, completion_tokens: 12, total_tokens: 22 } },
  ),
  'data: [DONE]\n\n',
]
const followUp = [
  chunk({ delta: { role: 'assistant', content: 'Done.' } }),
  chunk({ finish_reason: 'stop' }, { usage: { prompt_tokens: 20, completion_tokens: 2, total_tokens: 22 } }),
  'data: [DONE]\n\n',
]

const providerRequests = []
const server = http.createServer((req, res) => {
  let body = ''
  req.on('data', (d) => (body += d))
  req.on('end', () => {
    providerRequests.push(JSON.parse(body))
    res.writeHead(200, { 'content-type': 'text/event-stream' })
    for (const c of providerRequests.length === 1 ? truncatedToolCall : followUp) res.write(c)
    res.end()
  })
})
await new Promise((resolve) => server.listen(0, resolve))

const executedInputs = []
const runQuery = toolDefinition({
  name: 'run_query',
  description: 'Run a SQL query',
  inputSchema: z.object({ sql: z.string().optional() }),
}).server((input) => {
  executedInputs.push(input)
  return 'tool ran'
})

const adapter = createOpenRouterText('openai/gpt-4.1-mini', 'sk-dummy', {
  serverURL: `http://127.0.0.1:${server.address().port}/api/v1`,
})

for await (const event of chat({
  adapter,
  tools: [runQuery],
  messages: [{ role: 'user', content: 'Top 20 guitars by revenue in 2025?' }],
})) {
  if (event.type === 'TOOL_CALL_END') console.log('TOOL_CALL_END input:', JSON.stringify(event.input))
}
server.close()

const toolMessage = providerRequests[1]?.messages.find((m) => m.role === 'tool')
console.log('tool executed with:', JSON.stringify(executedInputs))
console.log('tool result sent to the model:', toolMessage?.content)
console.log(
  executedInputs.length > 0
    ? '\nBUG: the tool ran with {} although its arguments were malformed JSON.'
    : '\nOK: the tool did not run; the model got a tool error.',
)
Screenshots or Videos (Optional)

n/a

Do you intend to try to help solve this bug with your own PR?

Yes, I think I know how to fix it and will discuss it in the comments of this issue

Terms & Code of Conduct
  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
主要言語
TypeScript
スター
3.2k
フォーク
361
平均マージ
1日 15時間
マージ済み PR(30日)
210

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

TanStack/ai のほかの issue

TanStack/ai の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。