Streamed Chat Completions run raises ValidationError when tool_choice names a function tool called web_search
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 65/100
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.
- Lingua principale
- Python
- Stelle
- 30.4k
- Fork
- 4.9k
- Merge medio
- 15h 42m
- PR unite (30g)
- 124
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di openai/openai-agents-python
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
openai/openai-agents-python#5451 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
fix(typing): explicitly specialize ToolContext[Any] in ToolFunctionWithToolContextForse di nuovo libera @AbarnaaSree l’ha presa 12 giorni fa e non c’è nessuna pull request aperta. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
openai/openai-agents-python#5387 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
openai/openai-agents-python#5453 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 53/100
openai/openai-agents-python#5450 ·
I maintainer di solito rispondono entro 1 giorno
-
function_tool rejects valid arguments when a parameter uses Pydantic strict mode (Enum, date, UUID)Forse già presa @Lahari25 l’ha presa 1 giorno fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 65/100
openai/openai-agents-python#5448 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di openai/openai-agents-python
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
-
Validate finite output clamping limits in StepPredictor.prepare_clamping_params to prevent silent NaN/Inf propagationForse già presa @muskan-creates352 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
mllam/neural-lam#848 ·
I maintainer di solito rispondono entro 2 giorni
-
infra
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
fab-geocommuns/RNB-coeur#1166 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
deepspeedai/DeepSpeed#8951 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
aws/context-ontology-accelerator#343 ·
I maintainer di solito rispondono entro 1 giorno