Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Streamed Chat Completions run raises ValidationError when tool_choice names a function tool called web_search

オープン
#5,447 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

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

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

評価

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

調査の方向性

Start with Converter.convert_tool_choice in src/agents/models/chatcmpl_converter.py, then compare how openai_chatcompletions.py, litellm_model.py, and any_llm_model.py build the streaming placeholder Response. Reproduce the failure with the supplied mock Chat Completions example and check existing model and streamed runner tests. Done means named function tools, including web_search and mcp, complete streaming successfully across the affected adapters.

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

説明

Please read this first
  • Have you read the docs? Yes
  • Have you searched for related issues? Yes. #980 and #1846 (LiteLLM) were the same crash for any function name. Their fixes (#1206, #1971) build the tool_choice of the streamed placeholder Response with the Responses converter, which turns web_search and mcp into hosted-tool selectors.
Describe the bug

With a Chat Completions model, Runner.run_streamed raises a pydantic ValidationError when ModelSettings.tool_choice names a function tool called web_search. Runner.run with the same setup works, and a function tool called mcp fails the same way. LitellmModel and AnyLLMModel(api="chat_completions") are affected too. web_search is a natural name for a function tool with Chat Completions models, since the hosted WebSearchTool can't be used with them (chatcmpl_converter.py#L1048-L1051).

In streaming mode OpenAIChatCompletionsModel sends the request first and then builds a placeholder Response for the stream events (openai_chatcompletions.py#L751-L781). The request takes its tool_choice from the Chat Completions converter (chatcmpl_converter.py#L81-L104), which returns {"type": "function", "function": {"name": "web_search"}}. The placeholder takes it from OpenAIResponsesConverter.convert_tool_choice, which maps "web_search" to {"type": "web_search"} and "mcp" to {"type": "mcp"} (openai_responses.py#L1865-L1869, L1903-L1906). Neither validates as Response.tool_choice: openai-python's ToolChoiceTypes has no "web_search", and ToolChoiceMcp requires server_label. The LiteLLM and any-llm adapters have the same code (litellm_model.py#L662-L680, any_llm_model.py#L975-L993).

The error is raised after create() returned the stream and before stream_response enters the try/finally that closes it (openai_chatcompletions.py#L490-L524), so that stream is not closed.

Debug information
  • Agents SDK version: 0.24.0, and current main (cd26bbbc)
  • Related library versions: openai 3.29.0, pydantic 2.12.3, httpx2 2.14.0 (litellm 1.83.0 and any-llm-sdk 1.11.0 for the other adapters)
  • Python version: 3.12.7
  • Operating system: macOS 26.6.2
  • Model and model provider: none, offline repro with a mocked Chat Completions endpoint
  • Does the issue reproduce with the latest Agents SDK release? Yes
  • Does the issue occur consistently or intermittently? Consistently

Repro output (paths shortened, traceback and error list trimmed):

request tool_choice: {'type': 'function', 'function': {'name': 'web_search'}}
Runner.run: done
request tool_choice: {'type': 'function', 'function': {'name': 'web_search'}}
Traceback (most recent call last):
  ...
  File "repro.py", line 59, in main
    async for _ in streamed.stream_events():
  File "src/agents/result.py", line 1149, in stream_events
    raise stored_exception
  ...
  File "src/agents/models/openai_chatcompletions.py", line 463, in stream_response
    response, stream = await self._fetch_response(
  File "src/agents/models/openai_chatcompletions.py", line 769, in _fetch_response
    response = Response(
  ...
pydantic_core._pydantic_core.ValidationError: 14 validation errors for Response
tool_choice.literal['none','auto','required']
  Input should be 'none', 'auto' or 'required' [type=literal_error, input_value={'type': 'web_search'}, input_type=dict]
...
tool_choice.ToolChoiceTypes.type
  Input should be 'file_search', 'web_search_preview', 'computer', 'computer_use_preview', 'computer_use', 'web_search_preview_2025_03_11', 'image_generation' or 'code_interpreter' [type=literal_error, input_value='web_search', input_type=str]
...
Repro steps

The fake server's reply doesn't matter for the failure, which happens before the stream is read.

import asyncio
import json

import httpx2
from openai import AsyncOpenAI

from agents import (
    Agent,
    ModelSettings,
    OpenAIChatCompletionsModel,
    Runner,
    function_tool,
    set_tracing_disabled,
)

set_tracing_disabled(True)


@function_tool
def web_search(query: str) -> str:
    """Search the web with the app's own search backend."""
    return f"results for {query}"


def fake_chat_completions(request: httpx2.Request) -> httpx2.Response:
    """Stands in for any OpenAI-compatible server. It always answers "done"."""
    body = json.loads(request.content)
    print("request tool_choice:", body["tool_choice"])
    if not body.get("stream"):
        return httpx2.Response(200, json={
            "id": "c", "object": "chat.completion", "created": 0, "model": "m",
            "choices": [{"index": 0, "finish_reason": "stop",
                         "message": {"role": "assistant", "content": "done"}}]})
    chunk = {"id": "c", "object": "chat.completion.chunk", "created": 0, "model": "m",
             "choices": [{"index": 0, "finish_reason": "stop",
                          "delta": {"role": "assistant", "content": "done"}}]}
    return httpx2.Response(200, content=f"data: {json.dumps(chunk)}\n\ndata: [DONE]\n\n",
                           headers={"content-type": "text/event-stream"})


client = AsyncOpenAI(
    api_key="placeholder",
    base_url="http://localhost:8000/v1",
    http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(fake_chat_completions)),
)
agent = Agent(
    name="assistant",
    model=OpenAIChatCompletionsModel(model="some-model", openai_client=client),
    tools=[web_search],
    model_settings=ModelSettings(tool_choice="web_search"),
)


async def main() -> None:
    result = await Runner.run(agent, "search for x")
    print("Runner.run:", result.final_output)

    streamed = Runner.run_streamed(agent, "search for x")
    async for _ in streamed.stream_events():
        pass
    print("Runner.run_streamed:", streamed.final_output)


asyncio.run(main())
Expected behavior

Runner.run_streamed finishes with done, the same as Runner.run.

Suggested fix

Build the placeholder's tool_choice with the Chat Completions meaning instead of reusing the Responses converter. For example, a helper next to Converter.convert_tool_choice in chatcmpl_converter.py (ToolChoiceFunction is from openai.types.responses):

@classmethod
def convert_tool_choice_for_response(
    cls, tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None
) -> Literal["auto", "required", "none"] | ToolChoiceFunction:
    """tool_choice for the Response built for streaming. Chat Completions has no hosted
    tools, so any other string is a function name, as convert_tool_choice() sends it."""
    if tool_choice in ("auto", "required", "none"):
        return cast(Literal["auto", "required", "none"], tool_choice)
    if isinstance(tool_choice, str):
        return ToolChoiceFunction(type="function", name=tool_choice)
    return "auto"

In the three adapters it replaces the OpenAIResponsesConverter.convert_tool_choice(...) call and its None/omit fallback. MCPToolChoice never reaches the helper, because convert_tool_choice rejects it before the request. The three # type: ignore[arg-type] comments on tool_choice= can then go, and openai_chatcompletions.py and litellm_model.py no longer need OpenAIResponsesConverter. The helper also changes response.created for names like file_search and code_interpreter, which today report a hosted selector although the request sent a function selector.

I tried this on a local copy of main. The repro and its mcp version finish with done, and the stream is closed. Both names also finish through LitellmModel and AnyLLMModel against a local fake server. Ruff and mypy are clean on the four changed files, and tests/models, tests/test_tool_choice_reset.py and tests/test_agent_runner_streamed.py pass.

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

環境構築

はじめの一歩

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

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

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

似ている issue

Python の issue をもっと見る

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

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