mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-11 14:58:39 +03:00
fix(providers): preserve nanobot tools with OpenRouter server tools (#5335)
* fix(providers): preserve functions with extra body tools * docs(providers): clarify extra body tool merging
This commit is contained in:
@@ -330,7 +330,11 @@ By default, OpenAI uses `apiType: "auto"`: nanobot calls Chat Completions normal
|
|||||||
|
|
||||||
Valid `apiType` values are exactly `auto`, `chat_completions`, and `responses`.
|
Valid `apiType` values are exactly `auto`, `chat_completions`, and `responses`.
|
||||||
|
|
||||||
`extraBody` follows the selected OpenAI API surface. With Chat Completions, nanobot passes it through as the SDK `extra_body` value. With Responses, configure it in Responses API body shape; nanobot merges ordinary top-level fields into the Responses request body, appends `extraBody.tools` after generated function tools, and merges `extraBody.include` without duplicates:
|
`extraBody` follows the selected OpenAI API surface. With Chat Completions, nanobot passes
|
||||||
|
ordinary fields through as the SDK `extra_body` value; list-valued `extraBody.tools` is handled
|
||||||
|
specially and appended after generated function tools. With Responses, configure it in Responses
|
||||||
|
API body shape; nanobot merges ordinary top-level fields into the Responses request body, appends
|
||||||
|
`extraBody.tools` after generated function tools, and merges `extraBody.include` without duplicates:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,6 +100,29 @@ Gateway-style setup for model IDs served through OpenRouter.
|
|||||||
|
|
||||||
Use the model ID exactly as OpenRouter lists it.
|
Use the model ID exactly as OpenRouter lists it.
|
||||||
|
|
||||||
|
To opt into OpenRouter server-managed search and fetch, add:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"extraBody": {
|
||||||
|
"tools": [
|
||||||
|
{ "type": "openrouter:web_search" },
|
||||||
|
{ "type": "openrouter:web_fetch" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chat Completions-compatible OpenRouter
|
||||||
|
[server tools](https://openrouter.ai/docs/guides/features/server-tools), such as those above, are
|
||||||
|
appended to nanobot's generated functions. This keeps unrelated local tools such as `write_file`
|
||||||
|
available in the same request. Responses-only server tools require an API surface that the
|
||||||
|
OpenRouter provider does not currently enable.
|
||||||
|
|
||||||
### Eden AI Gateway
|
### Eden AI Gateway
|
||||||
|
|
||||||
Eden AI exposes an OpenAI-compatible chat-completions endpoint at
|
Eden AI exposes an OpenAI-compatible chat-completions endpoint at
|
||||||
|
|||||||
@@ -447,6 +447,28 @@ def _merge_unique_list(base: object, override: object) -> object:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_chat_extra_body(
|
||||||
|
kwargs: dict[str, Any],
|
||||||
|
extra_body: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Merge configured Chat Completions fields without clobbering tools."""
|
||||||
|
regular_extra = {key: value for key, value in extra_body.items() if key != "tools"}
|
||||||
|
merged = dict(kwargs)
|
||||||
|
if regular_extra:
|
||||||
|
existing = kwargs.get("extra_body", {})
|
||||||
|
merged["extra_body"] = _deep_merge(existing, regular_extra)
|
||||||
|
|
||||||
|
if "tools" in extra_body:
|
||||||
|
current_tools = kwargs.get("tools")
|
||||||
|
configured_tools = extra_body["tools"]
|
||||||
|
if isinstance(current_tools, list) and isinstance(configured_tools, list):
|
||||||
|
merged["tools"] = [*current_tools, *configured_tools]
|
||||||
|
else:
|
||||||
|
merged["tools"] = configured_tools
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
def _merge_responses_extra_body(
|
def _merge_responses_extra_body(
|
||||||
body: dict[str, Any],
|
body: dict[str, Any],
|
||||||
extra_body: dict[str, Any],
|
extra_body: dict[str, Any],
|
||||||
@@ -968,14 +990,11 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
if msg.get("role") == "assistant" and "reasoning_content" not in msg:
|
if msg.get("role") == "assistant" and "reasoning_content" not in msg:
|
||||||
msg["reasoning_content"] = ""
|
msg["reasoning_content"] = ""
|
||||||
|
|
||||||
# Merge user-configured extra_body last so it can override or
|
# Merge user-configured extra_body last so ordinary fields can override
|
||||||
# extend provider-specific defaults (e.g. chat_template_kwargs,
|
# provider defaults. Keep configured tools at the top level: the SDK
|
||||||
# guided_json, repetition_penalty). Uses recursive merge so
|
# otherwise lets extra_body.tools replace nanobot's generated functions.
|
||||||
# nested dicts like {"chat_template_kwargs": {"enable_thinking": false}}
|
|
||||||
# do not clobber sibling keys already set by thinking-style logic.
|
|
||||||
if self._extra_body:
|
if self._extra_body:
|
||||||
existing = kwargs.get("extra_body", {})
|
kwargs = _merge_chat_extra_body(kwargs, self._extra_body)
|
||||||
kwargs["extra_body"] = _deep_merge(existing, self._extra_body)
|
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,30 @@ class TestBuildKwargsExtraBody:
|
|||||||
"chat_template_kwargs": {"enable_thinking": False},
|
"chat_template_kwargs": {"enable_thinking": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_extra_body_appends_tools_without_clobbering_functions(self) -> None:
|
||||||
|
function_tool = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "write_file",
|
||||||
|
"description": "Write a local file",
|
||||||
|
"parameters": {"type": "object"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
server_tool = {"type": "openrouter:web_search"}
|
||||||
|
provider = _make_provider({
|
||||||
|
"tools": [server_tool],
|
||||||
|
"custom_param": "value",
|
||||||
|
})
|
||||||
|
|
||||||
|
kwargs = provider._build_kwargs(
|
||||||
|
messages=_simple_messages(),
|
||||||
|
tools=[function_tool], model=None, max_tokens=100,
|
||||||
|
temperature=0.1, reasoning_effort=None, tool_choice=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert kwargs["tools"] == [function_tool, server_tool]
|
||||||
|
assert kwargs["extra_body"] == {"custom_param": "value"}
|
||||||
|
|
||||||
def test_extra_body_merges_with_thinking(self) -> None:
|
def test_extra_body_merges_with_thinking(self) -> None:
|
||||||
"""Config extra_body should merge with (and override) thinking params."""
|
"""Config extra_body should merge with (and override) thinking params."""
|
||||||
from nanobot.providers.registry import ProviderSpec
|
from nanobot.providers.registry import ProviderSpec
|
||||||
|
|||||||
Reference in New Issue
Block a user