Skip to content

Chat completions can return an unusable assistant turn: empty content, or tool calls the client never declared #30

Description

@serrade

Summary

When an OpenAI-compatible client sends its own tools array, some models cause the local API to return a chat completion with no content and no usable tool call. Clients treat that as a provider failure rather than a turn.

Reproduced with the Cline VS Code extension (4.0.12), which surfaces:

Invalid API Response: The provider returned an empty or unparsable response. This is a provider-side issue where the model failed to generate valid output or returned tool calls that Cline cannot process.

Cline then writes Failure: I did not provide a response. into the task history, which poisons every following turn of that task. composer-2.5 usually works; claude-opus-5 and claude-sonnet-5 fail routinely.

Cause

Two independent things combine.

1. Unmappable tool calls are dropped. The bridge captures the model's first tool call and returns it, and OpenAICompatibility.resolveToolCall tries to express it as one of the client's declared tools. When no mapping is found it returns nil and the call is discarded. Both response paths then emit nothing usable:

  • chatCompletionResponse sends content: "" with finish_reason: "stop".
  • chatCompletionStreamToolCall returns empty Data(), so no chunk is emitted, while the caller has already counted the call. The stream closes with finish_reason: "tool_calls" and zero tool calls in it.

2. The harness out-competes the client tool channel. The SDK agent runs with the client's real workspace as local.cwd, so its built-in read/write/shell succeed against the project directly. The model has no reason to call back through the client, so it does the work internally and returns narration, which the client cannot act on. Composer 2.5 tends to follow the prompt's routing instructions; Opus and Sonnet reach for the built-ins instead.

Reproduction

Any /v1/chat/completions request that declares tools and asks for file work will do. This one fails against claude-opus-5 while succeeding against composer-2.5:

curl -s http://127.0.0.1:8787/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <key>' \
  -d '{
    "model": "claude-opus-5",
    "stream": true,
    "messages": [
      {"role": "system", "content": "You are Cline, working in /path/to/project."},
      {"role": "user", "content": "Read package.json and report the name field."}
    ],
    "tools": [
      {"type": "function", "function": {"name": "read_file",
        "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
      {"type": "function", "function": {"name": "execute_command",
        "parameters": {"type": "object", "properties": {"command": {"type": "string"}, "requires_approval": {"type": "boolean"}}, "required": ["command", "requires_approval"]}}}
    ]
  }'

Failing shape: a stream whose only chunks are the opener and a finish chunk, with finish_reason claiming tool_calls but no tool_calls delta anywhere. Expected shape: a read_file tool call, or failing that, assistant text.

Environment

  • API for Cursor built from v0.1.10 (987bc50), macOS 26.0, Apple silicon
  • Client: Cline 4.0.12 in VSCodium, native tool calling enabled (its default), reached through a LiteLLM gateway and also directly
  • Models: claude-opus-5, claude-sonnet-5 fail; composer-2.5 mostly succeeds

Suggested direction

Two things seem worth separating:

  1. Make an empty assistant turn impossible: report unmappable calls, retry once with the client's actual tool names, and fall back to the model's narration or an explanation rather than empty content.
  2. Stop the harness from doing the work itself when the caller owns tool execution, so client tools are the only route to the workspace.

Happy to open pull requests for both if that direction sounds right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions