change
This commit is contained in:
@@ -46,13 +46,67 @@ class StructuredModelGateway:
|
||||
|
||||
async def call_tool(self, *, messages: list[dict[str, Any]], tool: dict[str, Any], provider_id: str, model_id: str, required_tool_name: str) -> dict[str, Any]:
|
||||
provider, model = self._provider_model(provider_id, model_id)
|
||||
payload = self._payload(provider, model.id, messages, [tool], required_tool_name)
|
||||
response = await self._request(provider, payload)
|
||||
response, compatibility_mode = await self._request_tool_call(
|
||||
provider, model.id, messages, [tool], required_tool_name,
|
||||
)
|
||||
try:
|
||||
return self._normalized_response(provider, response)
|
||||
normalized = self._normalized_response(provider, response)
|
||||
normalized["usage"]["structured_compatibility_mode"] = compatibility_mode
|
||||
return normalized
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
raise StructuredModelError("Provider response cannot be normalized as a structured tool-call result") from error
|
||||
|
||||
async def _request_tool_call(
|
||||
self,
|
||||
provider: ProviderConfig,
|
||||
model_id: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]],
|
||||
required_tool_name: str,
|
||||
) -> tuple[dict[str, Any], str]:
|
||||
"""Keep forced tool choice unless a provider explicitly rejects thinking mode.
|
||||
|
||||
Some OpenAI-compatible gateways reject an otherwise valid forced
|
||||
``tool_choice`` whenever reasoning is enabled or implicit. This is a
|
||||
request compatibility issue rather than an author error. First retry
|
||||
without the client-requested reasoning option; only if that exact
|
||||
rejection remains do we allow automatic choice among the single tool
|
||||
already exposed to the model. The workflow's one-call/schema checks
|
||||
still reject free-form or wrong-tool responses.
|
||||
"""
|
||||
try:
|
||||
return await self._request(
|
||||
provider,
|
||||
self._payload(provider, model_id, messages, tools, required_tool_name),
|
||||
), "forced"
|
||||
except StructuredModelError as first_error:
|
||||
if not self._thinking_tool_choice_rejection(first_error):
|
||||
raise
|
||||
try:
|
||||
return await self._request(
|
||||
provider,
|
||||
self._payload(
|
||||
provider, model_id, messages, tools, required_tool_name,
|
||||
include_reasoning=False,
|
||||
),
|
||||
), "reasoning_disabled"
|
||||
except StructuredModelError as second_error:
|
||||
if not self._thinking_tool_choice_rejection(second_error):
|
||||
raise
|
||||
return await self._request(
|
||||
provider,
|
||||
self._payload(
|
||||
provider, model_id, messages, tools, required_tool_name,
|
||||
include_reasoning=False,
|
||||
force_tool_name=False,
|
||||
),
|
||||
), "single_tool_auto"
|
||||
|
||||
@staticmethod
|
||||
def _thinking_tool_choice_rejection(error: StructuredModelError) -> bool:
|
||||
message = str(error).casefold()
|
||||
return "thinking" in message and ("tool_choice" in message or "tool choice" in message)
|
||||
|
||||
async def conformance(self, *, provider_id: str, model_id: str, tools: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
failures: list[dict[str, str]] = []
|
||||
usage = {"prompt_tokens": 0, "completion_tokens": 0}
|
||||
@@ -190,20 +244,31 @@ class StructuredModelGateway:
|
||||
})
|
||||
return values
|
||||
|
||||
def _payload(self, provider: ProviderConfig, model_id: str, messages: list[dict[str, Any]], tools: list[dict[str, Any]], required_tool_name: str) -> dict[str, Any]:
|
||||
def _payload(
|
||||
self,
|
||||
provider: ProviderConfig,
|
||||
model_id: str,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]],
|
||||
required_tool_name: str,
|
||||
*,
|
||||
include_reasoning: bool = True,
|
||||
force_tool_name: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if len(tools) != 1 or not required_tool_name:
|
||||
raise StructuredModelError("The v3 protocol requires exactly one named tool per provider call.")
|
||||
request_options = provider.request_options if include_reasoning else {}
|
||||
if provider.api_style == "responses":
|
||||
payload: dict[str, Any] = {
|
||||
"model": model_id, "input": self._responses_input(messages), "tools": self._responses_tools(tools),
|
||||
"tool_choice": {"type": "function", "name": required_tool_name},
|
||||
**provider.request_options,
|
||||
"tool_choice": {"type": "function", "name": required_tool_name} if force_tool_name else "auto",
|
||||
**request_options,
|
||||
}
|
||||
return payload
|
||||
return {
|
||||
"model": model_id, "messages": messages, "tools": tools,
|
||||
"tool_choice": {"type": "function", "function": {"name": required_tool_name}},
|
||||
"temperature": 0, **provider.request_options,
|
||||
"tool_choice": {"type": "function", "function": {"name": required_tool_name}} if force_tool_name else "auto",
|
||||
"temperature": 0, **request_options,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user