Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Aperta
#5,447 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

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
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
api, backend, testing

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_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.

Lingua principale
Python
Stelle
30.4k
Fork
4.9k
Merge medio
15h 42m
PR unite (30g)
124

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di openai/openai-agents-python

Tutte le issue di openai/openai-agents-python

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.