Streamed Chat Completions run raises ValidationError when tool_choice names a function tool called web_search
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 65/100
調査の方向性
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_choiceof the streamed placeholderResponsewith the Responses converter, which turnsweb_searchandmcpinto 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
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
openai/openai-agents-python のほかの issue
-
Docs: `tool_choice="required"` can select a state-changing tool when the needed one is not shownオープン
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
openai/openai-agents-python#5461 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
openai/openai-agents-python#5460 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
openai/openai-agents-python#5459 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
openai/openai-agents-python#5458 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
openai/openai-agents-python#5455 ·
メンテナーはふだん 1 日以内に返信
openai/openai-agents-python の issue をすべて見る
似ている issue
-
_chunk_scan_fwd launches its Triton kernel without a device guard: NaN when a model is split across GPUs対応中かも @toddwbucy が今日担当しました。 オープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
state-spaces/mamba#1072 ·
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
loopj/aiovantage#414 ·
メンテナーはふだん 1 日以内に返信
-
power_mismatch_deline docstring: the rear-to-total RMAD conversion has an extra bifaciality factorオープン
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
pvlib/pvlib-python#2929 ·
メンテナーはふだん 1 日以内に返信
-
data good first issue launch-day unverified
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
BradGroux/wow-forever-mission-control#8 ·
メンテナーはふだん 1 日以内に返信
-
check bilibili falsely reports read/subtitle as broken: live_test fixture BV1GJ411x7h7 is gone (404)オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
Panniantong/Agent-Reach#889 ·
メンテナーはふだん 2 日以内に返信