Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đang mở
#1,689 0 bình luận 0 reaction 1 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@tombeckenham đang làm issue này rồi.

Từ ngày 9/10/2026.

  • #1690 của @PatrM — đang mở

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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.
Ngôn ngữ chính
TypeScript
Star
3.2k
Fork
361
Merge trung bình
1 ngày 15 giờ
Pull request đã merge (30 ngày)
221

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của TanStack/ai

Tất cả issue của TanStack/ai

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.