[Frontend] Support OpenAI Responses API namespace tools (#47024)
Signed-off-by: zhongjing123 <jimzhong5193@gmail.com> Co-authored-by: zhongjing123 <jimzhong5193@gmail.com>
Zhong committed
Jul 6, 2026 at 14:21 UTC
98ba9b9583cc98dc49039a1353c6ad158f678d64
8 files changed
+383
-22
tests/entrypoints/openai/responses/test_namespace_tool_separator.py
new
+114
@@ -0,0 +1,114 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import json
5
+
6
+import openai # use the official client for correctness check
7
+import pytest
8
+
9
+MODEL_NAME = "Qwen/Qwen3-1.7B"
10
+NAMESPACE = "mcp__computer_use"
11
+TOOL_NAME = "get_app_state"
12
+FLAT_TOOL_NAME = f"{NAMESPACE}__{TOOL_NAME}"
13
+
14
+tools = [
15
+ {
16
+ "type": "namespace",
17
+ "name": NAMESPACE,
18
+ "description": "Computer control tools.",
19
+ "tools": [
20
+ {
21
+ "type": "function",
22
+ "name": TOOL_NAME,
23
+ "description": "Get the current state of a desktop application.",
24
+ "parameters": {
25
+ "type": "object",
26
+ "properties": {
27
+ "app": {
28
+ "type": "string",
29
+ "description": "Application name, for example Chrome.",
30
+ }
31
+ },
32
+ "required": ["app"],
33
+ "additionalProperties": False,
34
+ },
35
+ }
36
+ ],
37
+ }
38
+]
39
+
40
+prompt = [
41
+ {
42
+ "role": "user",
43
+ "content": "Use the computer app state tool to inspect Google Chrome.",
44
+ },
45
+]
46
+
47
+
48
+def _assert_namespace_tool_call(tool_call) -> None:
49
+ assert tool_call.type == "function_call"
50
+ assert tool_call.name == TOOL_NAME
51
+ assert tool_call.namespace == NAMESPACE
52
+ assert tool_call.name != FLAT_TOOL_NAME
53
+
54
+ args = json.loads(tool_call.arguments)
55
+ assert args["app"]
56
+
57
+
58
+@pytest.mark.asyncio
59
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
60
+async def test_namespace_tool_separator(client: openai.AsyncOpenAI, model_name: str):
61
+ response = await client.responses.create(
62
+ model=model_name,
63
+ input=prompt,
64
+ tools=tools,
65
+ temperature=0.0,
66
+ )
67
+
68
+ assert len(response.output) >= 1
69
+ tool_call = next(
70
+ (out for out in response.output if out.type == "function_call"), None
71
+ )
72
+ assert tool_call is not None
73
+ _assert_namespace_tool_call(tool_call)
74
+
75
+
76
+@pytest.mark.asyncio
77
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
78
+async def test_namespace_tool_separator_streaming(
79
+ client: openai.AsyncOpenAI, model_name: str
80
+):
81
+ stream = await client.responses.create(
82
+ model=model_name,
83
+ input=prompt,
84
+ tools=tools,
85
+ temperature=0.0,
86
+ stream=True,
87
+ )
88
+ events = [event async for event in stream]
89
+
90
+ added_call = next(
91
+ (
92
+ event.item
93
+ for event in events
94
+ if event.type == "response.output_item.added"
95
+ and getattr(event.item, "type", None) == "function_call"
96
+ ),
97
+ None,
98
+ )
99
+ done_call = next(
100
+ (
101
+ event.item
102
+ for event in events
103
+ if event.type == "response.output_item.done"
104
+ and getattr(event.item, "type", None) == "function_call"
105
+ ),
106
+ None,
107
+ )
108
+
109
+ assert added_call is not None
110
+ assert added_call.name == TOOL_NAME
111
+ assert added_call.namespace == NAMESPACE
112
+
113
+ assert done_call is not None
114
+ _assert_namespace_tool_call(done_call)
tests/tool_parsers/test_glm47_moe_tool_parser.py
+66
@@ -7,12 +7,16 @@ import json
7
from unittest.mock import Mock
8
9
import pytest
10
+from openai.types.responses import ResponseFunctionToolCall
11
12
from vllm.entrypoints.openai.chat_completion.protocol import (
13
ChatCompletionRequest,
14
ChatCompletionToolsParam,
15
FunctionDefinition,
16
)
17
+from vllm.entrypoints.openai.engine.protocol import FunctionCall
18
+from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
19
+from vllm.entrypoints.openai.responses.utils import build_response_output_items
20
from vllm.tokenizers import get_tokenizer
21
from vllm.tool_parsers.glm47_moe_tool_parser import Glm47MoeModelToolParser
22
@@ -58,7 +62,69 @@ def mock_request(sample_tools) -> ChatCompletionRequest:
62
return request
63
64
65
+@pytest.fixture
66
+def namespace_tool_request() -> ResponsesRequest:
67
+ return ResponsesRequest.model_validate(
68
+ {
69
+ "input": "hi",
70
+ "tools": [
71
+ {
72
+ "type": "namespace",
73
+ "name": "mcp__computer_use",
74
+ "description": "Computer use tools.",
75
+ "tools": [
76
+ {
77
+ "type": "function",
78
+ "name": "get_app_state",
79
+ "description": "Get app state.",
80
+ "parameters": {
81
+ "type": "object",
82
+ "properties": {
83
+ "app": {"type": "string"},
84
+ },
85
+ },
86
+ }
87
+ ],
88
+ }
89
+ ],
90
+ }
91
+ )
92
+
93
+
94
class TestGlm47ExtractToolCalls:
95
+ def test_namespace_tool_call_round_trip_to_responses_output(
96
+ self, glm47_tokenizer, namespace_tool_request
97
+ ):
98
+ parser = Glm47MoeModelToolParser(
99
+ glm47_tokenizer, tools=namespace_tool_request.tools
100
+ )
101
+ out = (
102
+ "<tool_call>mcp__computer_use__get_app_state"
103
+ "<arg_key>app</arg_key>"
104
+ "<arg_value>Google Chrome</arg_value>"
105
+ "</tool_call>"
106
+ )
107
+
108
+ result = parser.extract_tool_calls(out, request=namespace_tool_request)
109
+
110
+ assert result.tools_called
111
+ tool_call = result.tool_calls[0].function
112
+ assert tool_call == FunctionCall(
113
+ name="mcp__computer_use__get_app_state",
114
+ arguments='{"app": "Google Chrome"}',
115
+ )
116
+
117
+ output_items = build_response_output_items(
118
+ reasoning=None,
119
+ content=None,
120
+ tool_calls=[tool_call],
121
+ tools=namespace_tool_request.tools,
122
+ )
123
+ output_tool_call = output_items[0]
124
+ assert isinstance(output_tool_call, ResponseFunctionToolCall)
125
+ assert output_tool_call.name == "get_app_state"
126
+ assert output_tool_call.namespace == "mcp__computer_use"
127
+
128
def test_no_tool_call(self, glm47_tool_parser, mock_request):
129
out = "This is a plain response."
130
r = glm47_tool_parser.extract_tool_calls(out, request=mock_request)
vllm/entrypoints/openai/responses/context.py
+1
@@ -349,6 +349,7 @@ class ParsableContext(ConversationContext):
349
reasoning=reasoning,
350
content=content,
351
tool_calls=tool_calls,
352
+ tools=self.request.tools,
353
)
354
)
355
elif completion.text:
vllm/entrypoints/openai/responses/protocol.py
+13
-4
@@ -592,10 +592,19 @@ class ResponsesRequest(OpenAIBaseModel):
592
)
593
elif is_named_tool_choice and tools is not None:
594
tool_name = tool_choice.get("name")
595
- tool_names = {
596
- t.get("name") if isinstance(t, dict) else getattr(t, "name", None)
597
- for t in tools
598
- }
595
+ tool_names = set()
596
+ for tool in tools:
597
+ if isinstance(tool, dict):
598
+ if tool.get("type") == "namespace":
599
+ namespace = tool.get("name")
600
+ for namespaced_tool in tool.get("tools", []):
601
+ namespaced_name = namespaced_tool.get("name")
602
+ tool_names.add(namespaced_name)
603
+ tool_names.add(f"{namespace}__{namespaced_name}")
604
+ else:
605
+ tool_names.add(tool.get("name"))
606
+ else:
607
+ tool_names.add(getattr(tool, "name", None))
608
if not tool_name or tool_name not in tool_names:
609
raise VLLMValidationError(
610
"Tool choice 'function' not found in 'tools' parameter.",
vllm/entrypoints/openai/responses/serving.py
+2
-1
@@ -1073,6 +1073,7 @@ class OpenAIServingResponses(GenerateBaseServing):
1073
content=content,
1074
tool_calls=tool_calls,
1075
logprobs=logprobs,
1076
+ tools=request.tools,
1077
)
1078
1079
# Fallback when no parser is configured
@@ -1339,7 +1340,7 @@ class OpenAIServingResponses(GenerateBaseServing):
1340
[StreamingResponsesResponse], StreamingResponsesResponse
1341
],
1342
) -> AsyncGenerator[StreamingResponsesResponse, None]:
1342
- processor = SimpleStreamingEventProcessor()
1343
+ processor = SimpleStreamingEventProcessor(tools=request.tools)
1344
1345
def _get_logprobs(
1346
output: CompletionOutput,
vllm/entrypoints/openai/responses/streaming_events.py
+25
-2
@@ -58,6 +58,7 @@ from openai.types.responses.response_output_item import McpCall
58
from openai.types.responses.response_reasoning_item import (
59
Content as ResponseReasoningTextContent,
60
)
61
+from openai.types.responses.tool import Tool
62
from openai_harmony import Message as HarmonyMessage
63
64
from vllm.entrypoints.mcp.tool_server import ToolServer
@@ -71,6 +72,10 @@ from vllm.entrypoints.openai.responses.protocol import (
72
ResponseReasoningPartDoneEvent,
73
StreamingResponsesResponse,
74
)
75
+from vllm.entrypoints.openai.responses.utils import (
76
+ build_responses_tool_call_name_map,
77
+ resolve_responses_tool_call_name,
78
+)
79
from vllm.outputs import CompletionOutput
80
from vllm.parser.harmony import Segment
81
from vllm.utils import random_uuid
@@ -815,6 +820,7 @@ class SimpleStreamingState:
820
accumulated_text: str = ""
821
tool_call_id: str = ""
822
tool_call_name: str = ""
823
+ tool_call_namespace: str | None = None
824
tool_call_index: int | None = None
825
has_emitted_tool_call_delta: bool = False
826
current_state: _StateType = field(default_factory=lambda: _StateType.NONE)
@@ -1016,11 +1022,13 @@ def emit_simple_tool_call_open(
1022
state: SimpleStreamingState,
1023
name: str,
1024
index: int | None,
1025
+ namespace: str | None = None,
1026
) -> list[StreamingResponsesResponse]:
1027
state.current_state = _StateType.TOOL_CALL
1028
state.current_item_id = random_uuid()
1029
state.tool_call_id = f"call_{random_uuid()}"
1030
state.tool_call_name = name
1031
+ state.tool_call_namespace = namespace
1032
state.tool_call_index = index
1033
state.accumulated_text = ""
1034
state.has_emitted_tool_call_delta = False
@@ -1034,6 +1042,7 @@ def emit_simple_tool_call_open(
1042
id=state.current_item_id,
1043
call_id=state.tool_call_id,
1044
name=name,
1045
+ namespace=namespace,
1046
arguments="",
1047
status="in_progress",
1048
),
@@ -1081,6 +1090,7 @@ def emit_simple_tool_call_done(
1090
item=ResponseFunctionToolCall(
1091
type="function_call",
1092
name=state.tool_call_name,
1093
+ namespace=state.tool_call_namespace,
1094
arguments=state.accumulated_text,
1095
status="completed",
1096
id=state.current_item_id,
@@ -1089,6 +1099,7 @@ def emit_simple_tool_call_done(
1099
),
1100
)
1101
state.output_index += 1
1102
+ state.tool_call_namespace = None
1103
state.current_state = _StateType.NONE
1104
return events
1105
@@ -1166,8 +1177,13 @@ class SimpleStreamingEventProcessor:
1177
),
1178
}
1179
1169
- def __init__(self, state: SimpleStreamingState | None = None) -> None:
1180
+ def __init__(
1181
+ self,
1182
+ state: SimpleStreamingState | None = None,
1183
+ tools: list[Tool] | None = None,
1184
+ ) -> None:
1185
self.state = state or SimpleStreamingState()
1186
+ self.tool_call_name_map = build_responses_tool_call_name_map(tools)
1187
1188
def resolve_target_state(
1189
self, delta_message: DeltaMessage
@@ -1224,8 +1240,15 @@ class SimpleStreamingEventProcessor:
1240
handlers = self._STATE_HANDLERS[target_state]
1241
if target_state == _StateType.TOOL_CALL:
1242
assert tool_call is not None
1243
+ call_name = resolve_responses_tool_call_name(
1244
+ tool_call.function.name,
1245
+ tool_call_name_map=self.tool_call_name_map,
1246
+ )
1247
return handlers.open_fn(
1228
- self.state, tool_call.function.name, tool_call.index
1248
+ self.state,
1249
+ call_name.name,
1250
+ tool_call.index,
1251
+ call_name.namespace,
1252
)
1253
return handlers.open_fn(self.state)
1254
vllm/entrypoints/openai/responses/utils.py
+30
-5
@@ -35,6 +35,12 @@ from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionMessa
35
from vllm.entrypoints.openai.engine.protocol import FunctionCall
36
from vllm.entrypoints.openai.responses.protocol import ResponseInputOutputItem
37
from vllm.logger import init_logger
38
+from vllm.tool_parsers.utils import (
39
+ build_responses_tool_call_name_map,
40
+ flat_namespace_tool_name,
41
+ iter_response_function_tool_dicts,
42
+ resolve_responses_tool_call_name,
43
+)
44
from vllm.utils import random_uuid
45
46
logger = init_logger(__name__)
@@ -45,8 +51,10 @@ def build_response_output_items(
51
content: str | None,
52
tool_calls: list[FunctionCall] | None,
53
logprobs: list[Logprob] | None = None,
54
+ tools: list[Tool] | None = None,
55
) -> list[ResponseOutputItem]:
56
outputs: list[ResponseOutputItem] = []
57
+ tool_call_name_map = build_responses_tool_call_name_map(tools)
58
59
if reasoning:
60
outputs.append(
@@ -81,6 +89,9 @@ def build_response_output_items(
89
90
if tool_calls:
91
for idx, tool_call in enumerate(tool_calls):
92
+ call_name = resolve_responses_tool_call_name(
93
+ tool_call.name, tool_call_name_map=tool_call_name_map
94
+ )
95
outputs.append(
96
ResponseFunctionToolCall(
97
id=f"fc_{random_uuid()}",
@@ -88,7 +99,8 @@ def build_response_output_items(
99
or make_tool_call_id(func_name=tool_call.name, idx=idx),
100
type="function_call",
101
status="completed",
91
- name=tool_call.name,
102
+ name=call_name.name,
103
+ namespace=call_name.namespace,
104
arguments=tool_call.arguments,
105
)
106
)
@@ -219,10 +231,13 @@ def _construct_message_from_response_item(
231
)
232
233
if isinstance(item, ResponseFunctionToolCall):
234
+ tool_name = item.name
235
+ if item.namespace:
236
+ tool_name = flat_namespace_tool_name(item.namespace, item.name)
237
tool_call = ChatCompletionMessageToolCallParam(
238
id=item.call_id,
239
function=FunctionCallTool(
225
- name=item.name,
240
+ name=tool_name,
241
arguments=item.arguments,
242
),
243
type="function",
@@ -318,7 +333,17 @@ def _construct_message_from_response_item(
333
334
335
def extract_function_tool_names(tools: list[Tool]) -> frozenset[str]:
321
- return frozenset(tool.name for tool in tools if tool.type == "function")
336
+ names = []
337
+ for tool in tools:
338
+ if tool.type == "function":
339
+ names.append(tool.name)
340
+ elif tool.type == "namespace":
341
+ names.extend(
342
+ flat_namespace_tool_name(tool.name, namespaced_tool.name)
343
+ for namespaced_tool in tool.tools
344
+ if namespaced_tool.type == "function"
345
+ )
346
+ return frozenset(names)
347
348
349
def extract_tool_types(tools: list[Tool]) -> set[str]:
@@ -358,7 +383,7 @@ def construct_tool_dicts(
383
tool_dicts = None
384
else:
385
tool_dicts = [
361
- convert_tool_responses_to_completions_format(tool.model_dump())
362
- for tool in tools
386
+ convert_tool_responses_to_completions_format(tool)
387
+ for tool in iter_response_function_tool_dicts(tools)
388
]
389
return tool_dicts
vllm/tool_parsers/utils.py
+132
-10
@@ -5,12 +5,14 @@ import ast
5
import json
6
import math
7
import warnings
8
+from dataclasses import dataclass
9
from json import JSONDecodeError, JSONDecoder
10
from typing import Any, TypeAlias
11
12
import partial_json_parser
13
from openai.types.responses import (
14
FunctionTool,
15
+ NamespaceTool,
16
ToolChoiceFunction,
17
)
18
from openai.types.responses.tool import Tool as ResponsesTool
@@ -166,6 +168,91 @@ def consume_space(i: int, s: str) -> int:
168
return i
169
170
171
+_NAMESPACE_TOOL_SEPARATOR = "__"
172
+
173
+
174
+@dataclass(frozen=True)
175
+class ResponsesToolCallName:
176
+ name: str
177
+ namespace: str | None = None
178
+
179
+
180
+def flat_namespace_tool_name(namespace: str, name: str) -> str:
181
+ return f"{namespace}{_NAMESPACE_TOOL_SEPARATOR}{name}"
182
+
183
+
184
+def iter_response_function_tool_info(
185
+ tool: ResponsesTool,
186
+) -> list[tuple[str, dict[str, Any] | None]]:
187
+ if isinstance(tool, FunctionTool):
188
+ return [(tool.name, tool.parameters)]
189
+ if not isinstance(tool, NamespaceTool):
190
+ return []
191
+
192
+ namespace = tool.name
193
+ return [
194
+ (
195
+ flat_namespace_tool_name(namespace, namespaced_tool.name),
196
+ namespaced_tool.parameters,
197
+ )
198
+ for namespaced_tool in tool.tools
199
+ if namespaced_tool.type == "function"
200
+ ]
201
+
202
+
203
+def iter_response_function_tool_dicts(
204
+ tools: list[ResponsesTool],
205
+) -> list[dict[str, Any]]:
206
+ function_tools: list[dict[str, Any]] = []
207
+ for tool in tools:
208
+ if isinstance(tool, NamespaceTool):
209
+ namespace = tool.name
210
+ for namespaced_tool in tool.tools:
211
+ if namespaced_tool.type != "function":
212
+ continue
213
+ tool_dict = namespaced_tool.model_dump()
214
+ tool_dict["name"] = flat_namespace_tool_name(
215
+ namespace, namespaced_tool.name
216
+ )
217
+ function_tools.append(tool_dict)
218
+ else:
219
+ function_tools.append(tool.model_dump())
220
+ return function_tools
221
+
222
+
223
+def build_responses_tool_call_name_map(
224
+ tools: list[ResponsesTool] | None,
225
+) -> dict[str, ResponsesToolCallName]:
226
+ if not tools:
227
+ return {}
228
+
229
+ name_map: dict[str, ResponsesToolCallName] = {}
230
+ for tool in tools:
231
+ if not isinstance(tool, NamespaceTool):
232
+ continue
233
+ namespace = tool.name
234
+ for namespaced_tool in tool.tools:
235
+ if namespaced_tool.type != "function":
236
+ continue
237
+ flat_name = flat_namespace_tool_name(namespace, namespaced_tool.name)
238
+ name_map[flat_name] = ResponsesToolCallName(
239
+ name=namespaced_tool.name,
240
+ namespace=namespace,
241
+ )
242
+ return name_map
243
+
244
+
245
+def resolve_responses_tool_call_name(
246
+ name: str,
247
+ tools: list[ResponsesTool] | None = None,
248
+ tool_call_name_map: dict[str, ResponsesToolCallName] | None = None,
249
+) -> ResponsesToolCallName:
250
+ name_map = tool_call_name_map
251
+ if name_map is None:
252
+ name_map = build_responses_tool_call_name_map(tools)
253
+ return name_map.get(name, ResponsesToolCallName(name=name))
254
+
255
+
256
def _is_function_tool(tool: Tool) -> bool:
257
return isinstance(tool, (FunctionTool, ChatCompletionToolsParam))
258
@@ -189,6 +276,11 @@ def find_tool_properties(
276
if not tools:
277
return {}
278
for tool in tools:
279
+ if isinstance(tool, (FunctionTool, NamespaceTool)):
280
+ for name, params in iter_response_function_tool_info(tool):
281
+ if name == tool_name:
282
+ return (params or {}).get("properties", {})
283
+ continue
284
if not _is_function_tool(tool):
285
continue
286
name, params = _extract_tool_info(tool)
@@ -205,6 +297,11 @@ def find_tool_name(
297
if not tools:
298
return False
299
for tool in tools:
300
+ if isinstance(tool, (FunctionTool, NamespaceTool)):
301
+ for name, _ in iter_response_function_tool_info(tool):
302
+ if name == tool_name:
303
+ return True
304
+ continue
305
if not _is_function_tool(tool):
306
continue
307
name, _ = _extract_tool_info(tool)
@@ -213,8 +310,9 @@ def find_tool_name(
310
return False
311
312
216
-def _get_tool_schema_from_tool(tool: Tool) -> dict:
217
- name, params = _extract_tool_info(tool)
313
+def _get_tool_schema_from_name_and_params(
314
+ name: str, params: dict[str, Any] | None
315
+) -> dict:
316
params = params if params else {"type": "object", "properties": {}}
317
return {
318
"properties": {
@@ -225,6 +323,11 @@ def _get_tool_schema_from_tool(tool: Tool) -> dict:
323
}
324
325
326
+def _get_tool_schema_from_tool(tool: Tool) -> dict:
327
+ name, params = _extract_tool_info(tool)
328
+ return _get_tool_schema_from_name_and_params(name, params)
329
+
330
+
331
def _get_tool_schema_defs(
332
tools: list[Tool],
333
) -> dict:
@@ -247,13 +350,25 @@ def _get_tool_schema_defs(
350
def _get_json_schema_from_tools(
351
tools: list[Tool],
352
) -> dict:
250
- fn_tools = [t for t in tools if _is_function_tool(t)]
353
+ fn_tool_schemas: list[dict[str, Any]] = []
354
+ fn_tools: list[Tool] = []
355
+ for tool in tools:
356
+ if isinstance(tool, (FunctionTool, NamespaceTool)):
357
+ fn_tool_schemas.extend(
358
+ _get_tool_schema_from_name_and_params(name, params)
359
+ for name, params in iter_response_function_tool_info(tool)
360
+ )
361
+ if isinstance(tool, FunctionTool):
362
+ fn_tools.append(tool)
363
+ elif _is_function_tool(tool):
364
+ fn_tool_schemas.append(_get_tool_schema_from_tool(tool))
365
+ fn_tools.append(tool)
366
json_schema = {
367
"type": "array",
368
"minItems": 1,
369
"items": {
370
"type": "object",
256
- "anyOf": [_get_tool_schema_from_tool(tool) for tool in fn_tools],
371
+ "anyOf": fn_tool_schemas,
372
},
373
}
374
json_schema_defs = _get_tool_schema_defs(fn_tools)
@@ -274,23 +389,30 @@ def get_json_schema_from_tools(
389
tool_choice, ToolChoiceFunction
390
):
391
tool_name = tool_choice.name
277
- tool_map = {tool.name: tool for tool in tools if isinstance(tool, FunctionTool)}
278
- if tool_name not in tool_map:
392
+ responses_tool_map: dict[str, dict[str, Any] | None] = {}
393
+ for tool in tools:
394
+ if not isinstance(tool, (FunctionTool, NamespaceTool)):
395
+ continue
396
+ for name, params in iter_response_function_tool_info(tool):
397
+ responses_tool_map[name] = params
398
+ if "__" in name:
399
+ responses_tool_map.setdefault(name.rsplit("__", 1)[1], params)
400
+ if tool_name not in responses_tool_map:
401
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
280
- return tool_map[tool_name].parameters
402
+ return responses_tool_map[tool_name]
403
# tool_choice: Forced Function (ChatCompletion)
404
if (not isinstance(tool_choice, str)) and isinstance(
405
tool_choice, ChatCompletionNamedToolChoiceParam
406
):
407
tool_name = tool_choice.function.name
286
- tool_map = {
408
+ chat_tool_map: dict[str, ChatCompletionToolsParam] = {
409
tool.function.name: tool
410
for tool in tools
411
if isinstance(tool, ChatCompletionToolsParam)
412
}
291
- if tool_name not in tool_map:
413
+ if tool_name not in chat_tool_map:
414
raise ValueError(f"Tool '{tool_name}' has not been passed in `tools`.")
293
- return tool_map[tool_name].function.parameters
415
+ return chat_tool_map[tool_name].function.parameters
416
# tool_choice: "required"
417
if tool_choice == "required":
418
return _get_json_schema_from_tools(tools)