microsoft/autogen · error · ValueError

tool_choice specified but no tools provided

Error message

tool_choice specified but no tools provided

What it means

create_stream() requires that any tool_choice other than 'auto'/'none' be accompanied by a non-empty tools sequence. Passing tool_choice='required' or a Tool instance with len(tools)==0 raises this ValueError immediately. It mirrors the non-streaming contract: forcing tool use is meaningless with no tools to force.

Source

Thrown at python/packages/autogen-ext/src/autogen_ext/models/llama_cpp/_llama_cpp_completion_client.py:423

    async def create_stream(
        self,
        messages: Sequence[LLMMessage],
        *,
        tools: Sequence[Tool | ToolSchema] = [],
        tool_choice: Tool | Literal["auto", "required", "none"] = "auto",
        # None means do not override the default
        # A value means to override the client default - often specified in the constructor
        json_output: Optional[bool | type[BaseModel]] = None,
        extra_create_args: Mapping[str, Any] = {},
        cancellation_token: Optional[CancellationToken] = None,
    ) -> AsyncGenerator[Union[str, CreateResult], None]:
        # Validate tool_choice parameter even though streaming is not implemented
        if tool_choice != "auto" and tool_choice != "none":
            if not self.model_info["function_calling"]:
                raise ValueError("tool_choice specified but model does not support function calling")
            if len(tools) == 0:
                raise ValueError("tool_choice specified but no tools provided")
            logger.warning("tool_choice parameter specified but may not be supported by llama-cpp-python")

        raise NotImplementedError("Stream not yet implemented for LlamaCppChatCompletionClient")
        yield ""

    # Implement abstract methods
    def actual_usage(self) -> RequestUsage:
        return RequestUsage(
            prompt_tokens=self._total_usage.get("prompt_tokens", 0),
            completion_tokens=self._total_usage.get("completion_tokens", 0),
        )

    @property
    def capabilities(self) -> ModelInfo:
        return self.model_info

    def count_tokens(
        self,

View on GitHub (pinned to 027ecf0a37)

Solutions

  1. Supply the tools: create_stream(messages, tools=[tool1], tool_choice='required')
  2. Or reset tool_choice='auto' on code paths where no tools are registered
  3. Guard at the call site: tool_choice = 'required' if tools else 'auto'

Example fix

# before
async for c in client.create_stream(msgs, tool_choice="required"): ...

# after
tool_choice = "required" if tools else "auto"
async for c in client.create_stream(msgs, tools=tools, tool_choice=tool_choice): ...
Defensive patterns

Strategy: validation

Validate before calling

if tool_choice not in ("auto", "none") and len(tools) == 0:
    tool_choice = "auto"  # or raise your own config error before the call

Try / catch

try:
    ...  # call with tool_choice
except ValueError as e:
    if "no tools provided" in str(e):
        result = await client.create(messages, tools=default_tools, tool_choice=tool_choice)
    else:
        raise

Prevention

When it happens

Trigger: create_stream(messages, tool_choice='required') with no tools argument (default empty list); tools=[] passed explicitly; a pipeline that conditionally clears tools but leaves tool_choice set.

Common situations: Template code that always sets tool_choice='required' but only sometimes attaches tools; refactoring that moved tool registration behind a flag without defaulting tool_choice back to 'auto'.

Related errors


AI-assisted analysis of microsoft/autogen@027ecf0a37 (2026-08-15). Data as JSON: /api/errors/abc03d001f58b972. Report an issue: GitHub.