Repository: vercel/ai (2025-07-16T06:08:51Z)

Issue:
Tool Calls Fail with Empty Arguments in OpenAI-Compatible Provider
### Description
Tool calls fail to execute when certain models (particularly Claude models via OpenAI-compatible providers) return empty string `""` for tool arguments instead of valid JSON `"{}"`. This causes the tool call to be detected but never executed, resulting in premature stream termination.

### Expected Behavior
- Tool calls with no parameters should work correctly
- The AI SDK should handle both `"arguments": "{}"` and `"arguments": ""` formats
- Tool execution should proceed normally for parameterless tools

### Actual Behavior
- Tool calls are detected (finishReason shows 'tool-calls')
- No actual tool execution occurs (tool function never called)
- Stream terminates prematurely without calling the tool
- Tool call count remains 0 despite indication of tool usage

### Root Cause
The issue occurs in the stream parsing logic where `isParsableJson("")` returns `false`, causing the tool call to be considered "incomplete" and never triggering the tool execution.

**Location**: `packages/openai-compatible/src/openai-compatible-chat-language-model.ts` around line 600
```typescript
if (isParsableJson(toolCall.function.arguments)) {
  // This condition fails for empty string ""
  controller.enqueue({
    type: 'tool-call',
    // ...
  });
}
```

### Impact
- Affects compatibility with Claude and potentially other models
- Breaks tool calling functionality for parameterless tools
- Silent failure - no error thrown, just premature termination

### Minimal Reproduction Example
```typescript
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { streamText, tool } from 'ai';
import { z } from 'zod';

async function reproduceIssue() {
  const provider = createOpenAICompatible({
    baseURL: 'https://your-provider-url/v1',
    apiKey: 'your-api-key',
    name: 'your-provider',
  });

  const model = provider.chatModel('claude-sonnet-4-0'); // or any model that returns empty arguments

  const result = streamText({
    model,
    prompt: 'List files in my knowledge base',
    toolChoice: 'auto',
    tools: {
      list_files: tool({
        description: 'List all files in the knowledge base',
        parameters: z.object({}), // No parameters required
        execute: async () => {
          console.log('Tool executed!'); // This will never be called
          return { files: ['file1.txt', 'file2.txt'] };
        },
      }),
    },
    maxSteps: 10,
  });

  let toolCallCount = 0;
  for await (const chunk of result.fullStream) {
    if (chunk.type === 'tool-call') {
      toolCallCount++;
      console.log('Tool call detected:', chunk);
    }
  }

  console.log('Tool calls executed:', toolCallCount); // Will be 0
  console.log('Finish reason:', await result.finishReason); // Will be 'tool-calls'
}
```

### Raw Response Analysis 
The problematic response chunk from Claude models:
```json
{
  "choices": [{
    "delta": {
      "tool_calls": [{
        "id": "toolu_xxx",
        "type": "function", 
        "function": {
          "name": "list_files",
          "arguments": ""  // <- Empty string instead of "{}"
        },
        "index": 0
      }]
    }
  }]
}
```

### AI SDK Version

ai: 4.3.1
@ai-sdk/openai-compatible: 0.2.14


Discussion:
fixed in v5 @themez let me know if any issues
@dancer Which version should I use? Currently, I'm using `ai@5.0.0-beta.14` and `@ai-sdk+openai-compatible@1.0.0-beta.3`, but it still doesn't work.I use use anthropic/claude-sonnet-4 with openrouter
```typescript
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { streamText, tool } from "ai";
import { z } from 'zod';
async function test() {
    const openai = createOpenAICompatible({
        baseURL: 'https://openrouter.ai/api/v1',
        apiKey: 'sk-xxx',
        name:'anthropic'
    });
    const result = streamText({
        model: openai('anthropic/claude-sonnet-4'),
        prompt: 'invoke tool getCurrentTime',
        toolChoice: 'auto',
        tools: {
            getCurrentTime: tool({
                description: 'get current time',
                inputSchema: z.object({}),
                execute: async () => {
                    const now = new Date();
                    return now.toLocaleTimeString();
                }
            })
        },
        onChunk: (event) => {
            console.log('onChunk event', event)
        },
        onStepFinish: async (result) => {
            console.log('onStepFinish toolCalls:', result.toolCalls);
        },
    });
    for await (const chunk of result.textStream) {
        console.log(chunk);
    }
}
test();
```
This is my test code.
Hi, @lgrammel, Is there any progress? If there is, please let me know. I'm willing to go for the test.
Hi,@themez Have you found any solutions?
Hi, @lgrammel ，thank you for your repair work. Can this fix be merged into the @ai-sdk/openai-compatible:0.2.x version? Because the API of AI SDK V5 has changed significantly, modifying the original code would require too many changes.
Hey @davidzhangbj, were you able to solve this? we are using `ai@4.0.22`

cc: @lgrammel 
The version of this AI is irrelevant. The return value parsing is in @ai-sdk/openai-compatible or @ai-sdk/openai. I downloaded the source code of @ai-sdk/openai-compatible, made direct modifications, and then integrated it into my project.
Follow the modification method of version V5.I made the following modifications.
Around line 609 of the openai-compatible-chat-language-model.ts file
```
          flush(controller) {
            for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
              controller.enqueue({
                type: 'tool-call',
                toolCallType: 'function',
                toolCallId: toolCall.id ?? generateId(),
                toolName: toolCall.function.name,
                args: toolCall.function.arguments
              });
              toolCall.hasFinished = true;
            }
            const providerMetadata: LanguageModelV1ProviderMetadata = {
              [providerOptionsName]: {},
              ...metadataExtractor?.buildMetadata()
            };
```
Refer to this commit,https://github.com/davidzhangbj/agent/commit/0f71b161e9ad688162423d7b06fc7a1c59803ac9



Patch:
diff --git a/.changeset/young-sheep-roll.md b/.changeset/young-sheep-roll.md
new file mode 100644
index 000000000000..7717b14fd2c7
--- /dev/null
+++ b/.changeset/young-sheep-roll.md
@@ -0,0 +1,5 @@
+---
+'@ai-sdk/openai-compatible': patch
+---
+
+fix (provider/openai-incompatible): support empty tool calls
diff --git a/packages/openai-compatible/src/openai-compatible-chat-language-model.ts b/packages/openai-compatible/src/openai-compatible-chat-language-model.ts
index fd38b5ca21e9..bb587d162473 100644
--- a/packages/openai-compatible/src/openai-compatible-chat-language-model.ts
+++ b/packages/openai-compatible/src/openai-compatible-chat-language-model.ts
@@ -614,6 +614,23 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
               controller.enqueue({ type: 'text-end', id: 'txt-0' });
             }
 
+            // go through all tool calls and send the ones that are not finished
+            for (const toolCall of toolCalls.filter(
+              toolCall => !toolCall.hasFinished,
+            )) {
+              controller.enqueue({
+                type: 'tool-input-end',
+                id: toolCall.id,
+              });
+
+              controller.enqueue({
+                type: 'tool-call',
+                toolCallId: toolCall.id ?? generateId(),
+                toolName: toolCall.function.name,
+                input: toolCall.function.arguments,
+              });
+            }
+
             const providerMetadata: SharedV2ProviderMetadata = {
               [providerOptionsName]: {},
               ...metadataExtractor?.buildMetadata(),


Test patch:
diff --git a/packages/ai/src/generate-text/parse-tool-call.test.ts b/packages/ai/src/generate-text/parse-tool-call.test.ts
index 6303d3c9d231..6775d27a8829 100644
--- a/packages/ai/src/generate-text/parse-tool-call.test.ts
+++ b/packages/ai/src/generate-text/parse-tool-call.test.ts
@@ -80,7 +80,7 @@ describe('parseToolCall', () => {
     `);
   });
 
-  it('should successfully process empty calls for tools that have no inputSchema', async () => {
+  it('should successfully process empty tool calls for tools that have no inputSchema', async () => {
     const result = await parseToolCall({
       toolCall: {
         type: 'tool-call',
@@ -106,6 +106,32 @@ describe('parseToolCall', () => {
     });
   });
 
+  it('should successfully process empty object tool calls for tools that have no inputSchema', async () => {
+    const result = await parseToolCall({
+      toolCall: {
+        type: 'tool-call',
+        toolName: 'testTool',
+        toolCallId: '123',
+        input: '{}',
+      },
+      tools: {
+        testTool: tool({
+          inputSchema: z.object({}),
+        }),
+      } as const,
+      repairToolCall: undefined,
+      messages: [],
+      system: undefined,
+    });
+
+    expect(result).toEqual({
+      type: 'tool-call',
+      toolCallId: '123',
+      toolName: 'testTool',
+      input: {},
+    });
+  });
+
   it('should throw NoSuchToolError when tools is null', async () => {
     await expect(
       parseToolCall({
diff --git a/packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts b/packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts
index ed81e093399c..eb5e689d62cf 100644
--- a/packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts
+++ b/packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts
@@ -1608,6 +1608,84 @@ describe('doStream', () => {
     `);
   });
 
+  it('should stream empty tool call that is sent in one chunk', async () => {
+    server.urls['https://my.api.com/v1/chat/completions'].response = {
+      type: 'stream-chunks',
+      chunks: [
+        `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-beta",` +
+          `"system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,` +
+          `"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},` +
+          `"finish_reason":null}]}\n\n`,
+        `data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-beta",` +
+          `"system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],` +
+          `"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,` +
+          `"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}\n\n`,
+        'data: [DONE]\n\n',
+      ],
+    };
+
+    const { stream } = await model.doStream({
+      tools: [
+        {
+          type: 'function',
+          name: 'test-tool',
+          inputSchema: {
+            type: 'object',
+            properties: {},
+            additionalProperties: false,
+            $schema: 'http://json-schema.org/draft-07/schema#',
+          },
+        },
+      ],
+      prompt: TEST_PROMPT,
+      includeRawChunks: false,
+    });
+
+    expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
+      [
+        {
+          "type": "stream-start",
+          "warnings": [],
+        },
+        {
+          "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798",
+          "modelId": "grok-beta",
+          "timestamp": 2024-03-25T09:06:38.000Z,
+          "type": "response-metadata",
+        },
+        {
+          "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
+          "toolName": "test-tool",
+          "type": "tool-input-start",
+        },
+        {
+          "id": "call_O17Uplv4lJvD6DVdIvFFeRMw",
+          "type": "tool-input-end",
+        },
+        {
+          "input": "",
+          "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw",
+          "toolName": "test-tool",
+          "type": "tool-call",
+        },
+        {
+          "finishReason": "tool-calls",
+          "providerMetadata": {
+            "test-provider": {},
+          },
+          "type": "finish",
+          "usage": {
+            "cachedInputTokens": undefined,
+            "inputTokens": 18,
+            "outputTokens": 439,
+            "reasoningTokens": undefined,
+            "totalTokens": 457,
+          },
+        },
+      ]
+    `);
+  });
+
   it('should handle error stream parts', async () => {
     server.urls['https://my.api.com/v1/chat/completions'].response = {
       type: 'stream-chunks',