Skip to content

Conversation

@jaideepr97
Copy link
Contributor

@jaideepr97 jaideepr97 commented Nov 8, 2025

What does this PR do?

Adds support for enforcing tool usage via responses api. See https://platform.openai.com/docs/api-reference/responses/create#responses_create-tool_choice for details from official documentation.
Note: at present this PR only supports file_search and web_search_preview as options to enforce builtin tool usage

Closes #3548

Test Plan

uv run --no-sync ./scripts/integration-tests.sh --stack-config server:ci-tests --inference-mode replay --setup ollama --suite base

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 8, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 8, 2025

✱ Stainless preview builds

This PR will update the llama-stack-client SDKs with the following commit message.

feat: add support for tool_choice to repsponses api

Edit this comment to update it. It will appear in the SDK's changelogs.

llama-stack-client-node studio · code · diff

Your SDK built successfully.
generate ❗build ✅lint ✅test ❗

npm install https://pkg.stainless.com/s/llama-stack-client-node/c11a3f6e53f8d6e4ba23d063a31b81aa990e3223/dist.tar.gz
New diagnostics (7 note)
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceAllowedTools` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFileSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceWebSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFunctionTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceMCPTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceCustomTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Go/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
llama-stack-client-kotlin studio · code · diff

Your SDK built successfully.
generate ❗lint ✅test ❗

New diagnostics (10 note)
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceAllowedTools` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFileSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceWebSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFunctionTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceMCPTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceCustomTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Go/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
💡 Java/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
💡 Java/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
💡 Java/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
llama-stack-client-python studio · code · diff

Your SDK built successfully.
generate ❗build ⏳lint ⏳test ⏳

New diagnostics (7 note)
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceAllowedTools` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFileSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceWebSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFunctionTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceMCPTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceCustomTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Go/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.
llama-stack-client-go studio · code · diff

Your SDK built successfully.
generate ❗lint ❗test ❗

go get github.com/stainless-sdks/llama-stack-client-go@d8a8d4b4525afb32e7cf9a5ae1007f20f677d18f
New diagnostics (7 note)
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceAllowedTools` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFileSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceWebSearch` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceFunctionTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceMCPTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Model/Recommended: `#/components/schemas/OpenAIResponseInputToolChoiceCustomTool` could potentially be defined as a [model](https://www.stainless.com/docs/guides/configure#models) within `#/resources/responses`.
💡 Go/SchemaUnionDiscriminatorMissing: This union schema has more than one object variant, but no [`discriminator`](https://www.stainless.com/docs/reference/openapi-support#discriminator) property, so deserializing the union may be inefficient or ambiguous.

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
Last updated: 2025-11-13 18:07:16 UTC

@jaideepr97 jaideepr97 force-pushed the tool-choice branch 4 times, most recently from 9bab29b to 55bd671 Compare November 8, 2025 09:10
@jaideepr97 jaideepr97 changed the title feat: add support for tool_choice to repsponses api feat: add support for tool_choice to responses api Nov 8, 2025
@jaideepr97 jaideepr97 force-pushed the tool-choice branch 2 times, most recently from 3fd6509 to a7e1132 Compare November 10, 2025 16:21
@jaideepr97 jaideepr97 marked this pull request as ready for review November 10, 2025 19:47
def convert_web_search_tool_choice(chat_tool_names: list[str]) -> dict[str, Any]:
"""Convert a responses tool choice of type web_search to a chat completions compatible function tool choice."""
tool_name = "web_search_preview"
if tool_name not in chat_tool_names:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks for web_search_preview, but _process_new_tools only registers the builtin search tool as web_search, so every tool_choice={"type":"web_search_preview"} hits the ValueError and we fall back to auto. Can we check the internal name (or share the alias list) so web-search tool_choice actually works?

)
continue
elif tool["type"] == "web_search":
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowed-tools only checks for tool["type"] == "web_search", but the public API sends web_search_preview, web_search_2025_08_26, etc., so those entries drop into the unsupported branch and never constrain the model. Could we treat the WebSearchToolTypes the same way we do when registering the tool?

top_p: float | None = None
tools: Sequence[OpenAIResponseTool] | None = None
tool_choice: OpenAIResponseInputToolChoice | None = None
truncation: str | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We add tool_choice to the schema here, but the orchestrator never sets it on OpenAIResponseObject, so stored responses still show null even when the caller specified a tool choice. Can we plumb the requested/processed tool_choice through when building the response?

Copy link
Contributor

@ashwinb ashwinb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of inline comments. Thanks for this PR!


async def _process_tool_choice(self) -> str | dict[str, Any] | None:
"""Process and validate the OpenAI Response tool choice and return the appropriate chat completion tool choice string or object."""
if self.ctx.responses_tool_choice:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

early return here please

tool["function"]["name"] for tool in self.ctx.chat_tools if tool.get("type") == "function"
]

if isinstance(self.ctx.responses_tool_choice, OpenAIResponseInputToolChoiceMode):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should make _process_tool_choice a free floating function -- there is no reason why it should need "state" from self, whatever 1-2 things it needs can be passed as parameters.

if isinstance(self.ctx.responses_tool_choice, OpenAIResponseInputToolChoiceMode):
if self.ctx.responses_tool_choice.value == "required":
if len(chat_tool_names) == 0:
logger.warning("No tools available to enforce tool choice, skipping tool choice enforcement")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this warning is necessary

return None

# add all function tools to the allowed tools list and set mode to required
return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we returning untyped dicts? does this correspond to a type somewhere -- feels like it must?

},
}

elif isinstance(self.ctx.responses_tool_choice, OpenAIResponseInputToolChoiceCustomTool):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can improve the way this whole function is written perhaps -- these cases feel a bit duplicative? sorry about this being a vague, slightly unactionable comment. but there just feels a bit too much code here for what we want to do.

return {"type": "function", "function": {"name": "knowledge_search"}}


def convert_web_search_tool_choice(chat_tool_names: list[str]) -> dict[str, Any]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other than the mcp tool choice function, these other things are useless wrappers in my opinion and make the calling code harder to follow actually, what with the try: except: etc. all of that is unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Responses Tool Choice

2 participants