# Google's shim mislabels streamed tool calls finish_reason: "stop"

medium · inference · probed live 2026-07-26 · affects Google (OpenAI-compat) ·
HTML version: [https://inferencecanary.com/google-openai-finish-reason](https://inferencecanary.com/google-openai-finish-reason)

`finish_reason` states why generation stopped; agent loops branch on it — `"tool_calls"` means
execute tools and continue, `"stop"` means the turn is done. It must not depend on transport.

## What was sent

A deterministic tool prompt; the streaming leg sends the identical body plus `"stream": true`
([https://inferencecanary.com/assets/repro/stream-finish.google-openai.json](https://inferencecanary.com/assets/repro/stream-finish.google-openai.json)):

```json
{
  "model": "gemma-4-31b-it",
  "messages": [
    {
      "role": "user",
      "content": "Call `get_weather` for Paris. Do not answer directly."
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Current weather for a city.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string"
            }
          },
          "required": [
            "city"
          ]
        }
      }
    }
  ],
  "reasoning_effort": "minimal",
  "temperature": 0,
  "max_tokens": 200
}
```

## What came back

Non-streaming — the correct label:

```json
{
  "finish_reason": "tool_calls",
  "message": {
    "role": "assistant",
    "tool_calls": [
      { "id": "t6afm4c1", "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }
    ]
  }
}
```

Streaming — identical tool call, wrong label:

```json
{
  "finish_reason": "stop",
  "message": {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      { "id": "xo93xcxj", "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }
    ]
  }
}
```

Every other OpenAI-dialect provider probed returns `"tool_calls"` on both transports. The
streamed leg also drops the `thought_signature` the non-streamed leg returns — the token
Google's own dialect expects replayed on the next turn.

## Why it matters

A harness that dispatches on finish_reason behaves differently depending on transport:
streamed, it is told the turn finished cleanly and terminates with an unexecuted tool call —
HTTP 200, well-formed body, silent.

## Run it yourself

```
curl "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" \
  -H "Authorization: Bearer $GEMINI_API_KEY" -H "Content-Type: application/json" \
  -d @stream-finish.google-openai.json
```

POST the body twice, with and without `"stream": true`; compare the final `finish_reason`.
