Summary
When using the Copilot SDK with OpenAI-compatible backends (vLLM, Ollama, LM Studio), the model may return tool_calls: [] (empty array) instead of null/omitting the field. The SDK interprets the presence of the tool_calls key as an indication that tool execution is pending, enters its tool-call handling path, finds no tools to execute, and loops indefinitely — burning tokens and never producing a response.
This is the same class of bug that was fixed in the Codex CLI (openai/codex#1234) and in the OpenAI Python SDK itself (openai/openai-python#2061, fixed in v1.62.0 via PR #2106).
Root cause
Per the OpenAI Chat Completions API spec, tool_calls should be null (or absent) when the model does not invoke any tools. However, several widely-used OpenAI-compatible inference engines return tool_calls: [] instead:
While this is technically an upstream spec-compliance issue, the impact on the SDK is severe (infinite loop, unbounded token burn), and the fix is trivial. The Codex CLI already ships this hardening.
Reproduction steps
- Start a vLLM or Ollama server serving any model (e.g., Qwen3-235B-A22B, DeepSeek-R1, Gemma-3):
# vLLM example
vllm serve Qwen/Qwen3-235B-A22B-FP8 --enable-auto-tool-choice --tool-call-parser hermes
- Configure the Copilot SDK to use the local endpoint as the provider.
- Register at least one tool with the SDK.
- Send a chat message where the model responds with text only (no tool invocation needed).
- Observed: The model returns a response with
tool_calls: []. The SDK enters tool-call handling, finds nothing to dispatch, but does not exit cleanly — it loops or hangs indefinitely, never surfacing the text response.
- Expected: The SDK should treat
tool_calls: [] the same as tool_calls: null — skip tool handling and emit the text response.
Suggested fix
The same one-line fix applied in the Codex CLI (commit):
# Pseudocode — wherever the SDK checks for tool_calls presence:
- if hasattr(message, 'tool_calls') and message.tool_calls:
+ if hasattr(message, 'tool_calls') and message.tool_calls and len(message.tool_calls) > 0:
Or equivalently, normalize tool_calls: [] → None early in the response processing pipeline.
This is a defensive hardening measure — it costs nothing in the happy path and prevents a severe failure mode (infinite loop + token burn) when the SDK is used with non-OpenAI backends, which is a common and growing use case.
References
Summary
When using the Copilot SDK with OpenAI-compatible backends (vLLM, Ollama, LM Studio), the model may return
tool_calls: [](empty array) instead ofnull/omitting the field. The SDK interprets the presence of thetool_callskey as an indication that tool execution is pending, enters its tool-call handling path, finds no tools to execute, and loops indefinitely — burning tokens and never producing a response.This is the same class of bug that was fixed in the Codex CLI (openai/codex#1234) and in the OpenAI Python SDK itself (openai/openai-python#2061, fixed in v1.62.0 via PR #2106).
Root cause
Per the OpenAI Chat Completions API spec,
tool_callsshould benull(or absent) when the model does not invoke any tools. However, several widely-used OpenAI-compatible inference engines returntool_calls: []instead:tool_calls: []in all responsesWhile this is technically an upstream spec-compliance issue, the impact on the SDK is severe (infinite loop, unbounded token burn), and the fix is trivial. The Codex CLI already ships this hardening.
Reproduction steps
# vLLM example vllm serve Qwen/Qwen3-235B-A22B-FP8 --enable-auto-tool-choice --tool-call-parser hermestool_calls: []. The SDK enters tool-call handling, finds nothing to dispatch, but does not exit cleanly — it loops or hangs indefinitely, never surfacing the text response.tool_calls: []the same astool_calls: null— skip tool handling and emit the text response.Suggested fix
The same one-line fix applied in the Codex CLI (commit):
Or equivalently, normalize
tool_calls: []→Noneearly in the response processing pipeline.This is a defensive hardening measure — it costs nothing in the happy path and prevents a severe failure mode (infinite loop + token burn) when the SDK is used with non-OpenAI backends, which is a common and growing use case.
References
tool_callstool_calls: []bugtool_calls=[]tool_callstool_calls: []