OpenAIChatCompletionsModel sends tool_choice without tools, which the API rejects, on turns with no tools

オープン 初心者向け
#5,468 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
82/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
python
領域
api

調査の方向性

src/agents/models/openai_chatcompletions.py から始めてください。そこでは tool_choice が変換されて渡されており、parallel_tool_calls はすでに converted_tools で制御されています。同じ制御が src/agents/extensions/models/litellm_model.py の tool_choice にも必要です。変更の前後で tests/models を実行してください。ツールがない場合に tool_choice が省略され、追加したテストがリクエストボディを検証すれば完了です。

索引モデルが issue の本文から書いたものです。

説明

Please read this first
  • Have you read the docs? Yes
  • Have you searched for related issues? Yes. #4326 (fixed by #4359 and #4330) covered the same API rule for parallel_tool_calls. tool_choice was left out.
Describe the bug

OpenAIChatCompletionsModel always sends ModelSettings.tool_choice, even when the converted tool list is empty and tools is left out of the request. The Chat Completions API rejects that combination:

Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.

So an app-wide RunConfig(model_settings=ModelSettings(tool_choice="auto")) breaks every turn of an agent that has no function tools and no handoffs in that turn. Examples are a final summarizer agent after a handoff, or an agent whose tools are all switched off with is_enabled. parallel_tool_calls is already omitted in this case. LitellmModel sends the same request; I checked the body LiteLLM actually posts. AnyLLMModel's chat path has the same line, but I didn't run it.

Debug information
  • Agents SDK version: main (e355d69), also 0.24.0
  • Related library versions: openai 3.29.0, litellm 1.83.0
  • Python version: 3.14.7
  • Operating system: macOS
  • Model and model provider: any Chat Completions model (OpenAIChatCompletionsModel, LitellmModel)
  • Does the issue reproduce with the latest Agents SDK release? Yes
  • Does the issue occur consistently or intermittently? Consistently
Repro steps

A real AsyncOpenAI client on an httpx2.MockTransport, printing what's sent:

import asyncio, json
import httpx2
from openai import AsyncOpenAI
from agents import Agent, ModelSettings, OpenAIChatCompletionsModel, RunConfig, Runner

def handler(request: httpx2.Request) -> httpx2.Response:
    body = json.loads(request.content)
    print("tools in request:", "tools" in body, "| tool_choice:", body.get("tool_choice"),
          "| parallel_tool_calls in request:", "parallel_tool_calls" in body)
    return httpx2.Response(200, json={
        "id": "chatcmpl-1", "object": "chat.completion", "created": 0, "model": "gpt-4o-mini",
        "choices": [{"index": 0, "finish_reason": "stop",
                     "message": {"role": "assistant", "content": "OK"}}],
    })

async def main() -> None:
    client = AsyncOpenAI(api_key="x", http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)))
    agent = Agent(name="Summarizer", instructions="Reply OK",
                  model=OpenAIChatCompletionsModel("gpt-4o-mini", client))
    cfg = RunConfig(tracing_disabled=True,
                    model_settings=ModelSettings(tool_choice="auto", parallel_tool_calls=False))
    await Runner.run(agent, "Say OK.", run_config=cfg)

asyncio.run(main())
tools in request: False | tool_choice: auto | parallel_tool_calls in request: False
Expected behavior

tool_choice is left out when the request has no tools, the same way parallel_tool_calls already is.

Root cause and a possible fix

tool_choice is converted at openai_chatcompletions.py#L642 and sent at #L709, while only parallel_tool_calls is gated on converted_tools (#L652-L656). LiteLLM: litellm_model.py#L646. AnyLLM: any_llm_model.py#L951.

--- a/src/agents/models/openai_chatcompletions.py
+++ b/src/agents/models/openai_chatcompletions.py
@@ -654,6 +654,9 @@ class OpenAIChatCompletionsModel(Model):
         parallel_tool_calls: bool | Omit = (
             self._non_null_or_omit(model_settings.parallel_tool_calls) if converted_tools else omit
         )
+        # The API rejects tool_choice without tools, the same as parallel_tool_calls.
+        if not converted_tools:
+            tool_choice = omit
--- a/src/agents/extensions/models/litellm_model.py
+++ b/src/agents/extensions/models/litellm_model.py
@@ -643,7 +643,7 @@ class LitellmModel(Model):
-            tool_choice=self._remove_not_given(tool_choice),
+            tool_choice=self._remove_not_given(tool_choice) if converted_tools else None,

With that change, both repros send no tool_choice, and tests/models passes (1023 passed, 1 skipped). The placeholder Response.tool_choice built for streaming is separate and unchanged.

主要言語
Python
スター
30.4k
フォーク
4.9k
平均マージ
16時間 21分
マージ済み PR(30日)
86

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

openai/openai-agents-python のほかの issue

openai/openai-agents-python の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。