Wrongly reading model stream answer resulting in a duplication of tool-calls with same tool id

### Description

After some hours of investigations, I finally get to the bottom of this that drove me crazy.  
I'm thinking this bug could be a "source bug" of many other issues.

# Context

## Provider

I've used Scaleway Generative APIs  
https://www.scaleway.com/en/docs/ai-data/generative-apis/quickstart/
https://www.scaleway.com/en/generative-apis/  

## Models concerned

At least:
- meta/llama-3.1-70b-instruct:fp8
- meta/llama-3.1-8b-instruct:fp8

But other models could be impacted if they answer the same way.

I expect this problem to be non-provider related but model related.  
You should be able to reproduce it with any Llama 3.1 provider. 

# Possible other issues with same underlying bug

- https://github.com/vercel/ai/issues/3872

I think it may have other issues related but I haven't checked deeply.

# How to reproduce the bug?

Use the Weather Demo Chatbot: https://vercel.com/templates/next.js/nextjs-ai-chatbot  
With a provider providing a Llama 3.1 8B or 70B

# Description of the bug

In the UI : 2 weather widgets are displayed but one has crashed. We have a React error indicating that there is a duplicate key for 2 components.

<img width="1029" alt="image" src="https://github.com/user-attachments/assets/13389bd2-a0c5-4d78-8ad8-a94a54962863">

<img width="974" alt="image" src="https://github.com/user-attachments/assets/002302ae-100b-4a32-84d9-d36c7a17d6ae">

On the Provider side: we have an error `'{"object":"error","message":"This model only supports single tool-calls at once!","type":"BadRequestError","param":null,"code":400}'`

These are both resulting errors but **its not the source of the problem**.

To my investigation point, I think the bug concerns only the "stream features" like "streamText"

# Explanation of the bug

I've narrowed it to the first call to the provider.  
Call done:
```
curl -X POST "$PROVIDER/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"llama-3.1-8b-instruct","parallel_tool_calls":false,"temperature":0,"messages":[{"role":"system","content":"You can use tools to anwser user request."},{"role":"user","content":"Give me the latest news on ai"}],"tools":[{"type":"function","function":{"name":"searchGoogle","description":"Search on the web using Google Search","parameters":{"type":"object","properties":{"query":{"type":"string","description":"the query to search"}},"required":["query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}}],"tool_choice":"auto","stream":true,"stream_options":{"include_usage":true}}'
```

The ReadableStream answered was:
```
data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa","type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"query\": \""}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" ai\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}

data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}

data: [DONE]
```

This flow is equivalent to something like:
```
{
  "id": "chat-4a34e46ee37349d2a167b00a6abea435",
  "object": "chat.completion",
  "created": 1733154079,
  "model": "meta/llama-3.1-70b-instruct:fp8",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "chatcmpl-tool-a0d9e21c09c5480f82d7a92da78ce3ca",
            "type": "function",
            "function": {
              "name": "searchGoogle",
              "arguments": "{\"query\": \"latest news on ai\"}"
            }
          }
        ]
      },
      "logprobs": null,
      "finish_reason": "tool_calls",
      "stop_reason": 128008
    }
  ],
  "usage": {
    "prompt_tokens": 226,
    "total_tokens": 247,
    "completion_tokens": 21
  },
  "prompt_logprobs": null
}
```

The bug appears because of this chunk:
```
data: {"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}
```

The bug is in this function: https://github.com/vercel/ai/blob/main/packages/openai/src/openai-chat-language-model.ts#L359

While reading the stream in this function https://github.com/vercel/ai/blob/main/packages/openai/src/openai-chat-language-model.ts#L359, we get the following logs

## Caption of following logs

To ease debugging and explanation, I've created a log flow to describe the problem. Here is the caption to understand the log flow:
- `mappedToolCalls` defines the value of https://github.com/vercel/ai/blob/main/packages/openai/src/openai-chat-language-model.ts#L501
- `toolcall-1` is https://github.com/vercel/ai/blob/main/packages/openai/src/openai-chat-language-model.ts#L549
- `duplicate toolcall` is at https://github.com/vercel/ai/blob/main/packages/openai/src/openai-chat-language-model.ts#L605
- `add chunk in forwardstream` is at https://github.com/vercel/ai/blob/main/packages/ai/core/generate-text/run-tools-transformation.ts#L165

Log flow:
```
2024-12-02T18:16:29: stream step
2024-12-02T18:16:30: mappedToolCalls [
2024-12-02T18:16:30:   {
2024-12-02T18:16:30:     index: 0,
2024-12-02T18:16:30:     id: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:     type: 'function',
2024-12-02T18:16:30:     function: { name: 'searchGoogle' }
2024-12-02T18:16:30:   }
2024-12-02T18:16:30: ]
2024-12-02T18:16:30: toolcall-1 {
2024-12-02T18:16:30:   id: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   type: 'function',
2024-12-02T18:16:30:   function: { name: 'searchGoogle', arguments: '' }
2024-12-02T18:16:30: } {
2024-12-02T18:16:30:   index: 0,
2024-12-02T18:16:30:   id: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   type: 'function',
2024-12-02T18:16:30:   function: { name: 'searchGoogle' }
2024-12-02T18:16:30: }
2024-12-02T18:16:30: // check if tool call is complete ===> No
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: '{"query": "' } } ]
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: 'latest' } } ]
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: ' news' } } ]
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: ' on' } } ]
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: ' ai"}' } } ]
2024-12-02T18:16:30: duplicate toolcall {
2024-12-02T18:16:30:   id: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   type: 'function',
2024-12-02T18:16:30:   function: { name: 'searchGoogle', arguments: '{"query": "latest news on ai"}' }
2024-12-02T18:16:30: }
2024-12-02T18:16:30: add chunk in forwardstream {
2024-12-02T18:16:30:   type: 'tool-call',
2024-12-02T18:16:30:   toolCallType: 'function',
2024-12-02T18:16:30:   toolCallId: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   toolName: 'searchGoogle',
2024-12-02T18:16:30:   args: '{"query": "latest news on ai"}'
2024-12-02T18:16:30: }
2024-12-02T18:16:30: mappedToolCalls [ { index: 0, function: { arguments: '' } } ]
2024-12-02T18:16:30: duplicate toolcall {
2024-12-02T18:16:30:   id: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   type: 'function',
2024-12-02T18:16:30:   function: { name: 'searchGoogle', arguments: '{"query": "latest news on ai"}' }
2024-12-02T18:16:30: }
2024-12-02T18:16:30: add chunk in forwardstream {
2024-12-02T18:16:30:   type: 'tool-call',
2024-12-02T18:16:30:   toolCallType: 'function',
2024-12-02T18:16:30:   toolCallId: 'chatcmpl-tool-3d480d27469f4941808ae5f8db4c50fa',
2024-12-02T18:16:30:   toolName: 'searchGoogle',
2024-12-02T18:16:30:   args: '{"query": "latest news on ai"}'
2024-12-02T18:16:30: }
```

# Explanation of the log flow

The tool-call is streamed from the model but at a given point, the arguments became a valid parsableJSON and then the tool-call is send.
Just after that, another chunk is parsed with an empty argument: `mappedToolCalls [ { index: 0, function: { arguments: '' } } ]`

This results in the same tool-call be sent again. This then trigger a lot of different kind of bugs afterward.

# Ideas of resolution

- stop expecting that the tool-call is finished just because the args are parseable and find something more trustworthy (I think it would be the best solution)
- use a simple security so that a tool-call with the same id can't be send twice down the pipe

Don't hesitate to ask if anything is unclear. I'll be glad to help!

### Code example

_No response_

### AI provider

@ai-sdk/openai v1.0.5

### Additional context

Version of packages:
ai: v4.0.9
@ai-sdk/openai: v1.0.5

Labels: bug
State: closed