Streamed Chat Completions run raises ValidationError when tool_choice names a function tool called web_search
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
调研方向
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
- 平均合并
- 15 小时 42 分钟
- 30 天内合并 PR
- 124
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
openai/openai-agents-python 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 77/100
openai/openai-agents-python#5451 · 1 条评论 ·
维护者通常 1 天内回复
-
fix(typing): explicitly specialize ToolContext[Any] in ToolFunctionWithToolContext可能重新可做 @AbarnaaSree 于 11 天前认领,目前没有进行中的 PR。 未关闭
难度 1/5 1 小时以内 新手友好度 85/100
openai/openai-agents-python#5387 · 4 条评论 ·
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 35/100
openai/openai-agents-python#5453 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 53/100
openai/openai-agents-python#5450 ·
维护者通常 1 天内回复
-
function_tool rejects valid arguments when a parameter uses Pydantic strict mode (Enum, date, UUID)可能已有人在做 @Lahari25 于 1 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 65/100
openai/openai-agents-python#5448 · 2 条评论 ·
维护者通常 1 天内回复
查看 openai/openai-agents-python 的全部 Issue
相似的 Issue
-
enhancement help wanted
难度 2/5 1-3 小时 新手友好度 76/100
Git-Hospital/githealthcheck#61 ·
维护者通常 1 天内回复
-
changelog investigate
难度 2/5 1-3 小时 新手友好度 67/100
ramnes/notion-sdk-py#433 ·
-
Partial method descriptors raise on class access可能已有人在做 @Gambit-Checkmate 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 19 天内回复
-
docs pkg:core
难度 1/5 1 小时以内 新手友好度 84/100
pydantic/pydantic-ai#11929 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
intake/intake-esm#789 ·