mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
fix: reject unsupported web search before LiteLLM translation
This commit is contained in:
@@ -507,6 +507,32 @@ async def dispatch_anthropic_messages(
|
|||||||
|
|
||||||
model_suffix = adapt_request(body) if adapt_request else ""
|
model_suffix = adapt_request(body) if adapt_request else ""
|
||||||
|
|
||||||
|
# LiteLLM turns Anthropic's server-side web_search tool into the OpenAI
|
||||||
|
# `web_search_options` parameter. Generic OpenAI-compatible chat endpoints
|
||||||
|
# (including those serving Claude through a proxy) may reject that field.
|
||||||
|
# Only a provider with an explicit adaptation (e.g. Venice's model suffix)
|
||||||
|
# can preserve search semantics; do not silently remove the tool and return
|
||||||
|
# an answer that never searched. Native /v1/messages providers bypass this
|
||||||
|
# dispatcher and receive the original tool unchanged.
|
||||||
|
tools = body.get("tools")
|
||||||
|
if provider_prefix == "openai/" and isinstance(tools, list) and any(
|
||||||
|
isinstance(tool, dict)
|
||||||
|
and (
|
||||||
|
(
|
||||||
|
isinstance(tool.get("type"), str)
|
||||||
|
and tool["type"].startswith("web_search")
|
||||||
|
)
|
||||||
|
or tool.get("name") == "web_search"
|
||||||
|
)
|
||||||
|
for tool in tools
|
||||||
|
):
|
||||||
|
raise UpstreamError(
|
||||||
|
"This upstream does not support Anthropic web search through "
|
||||||
|
"OpenAI-compatible /v1/messages translation",
|
||||||
|
status_code=400,
|
||||||
|
code="UNSUPPORTED_WEB_SEARCH",
|
||||||
|
)
|
||||||
|
|
||||||
# Convention: `model.id` is the canonical upstream model name;
|
# Convention: `model.id` is the canonical upstream model name;
|
||||||
# `forwarded_model_id` is the public alias the internal API exposes
|
# `forwarded_model_id` is the public alias the internal API exposes
|
||||||
# and echoes back to the client.
|
# and echoes back to the client.
|
||||||
|
|||||||
@@ -123,13 +123,35 @@ async def test_requests_without_web_search_are_untouched() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_other_providers_keep_their_existing_behaviour() -> None:
|
async def test_generic_openai_upstream_rejects_untranslatable_web_search() -> None:
|
||||||
"""The base hook is a no-op, so no non-Venice upstream changes shape."""
|
"""Do not let LiteLLM send unsupported web_search_options to a generic API."""
|
||||||
provider = BaseUpstreamProvider(base_url="http://test", api_key="k")
|
provider = BaseUpstreamProvider(base_url="http://test", api_key="k")
|
||||||
|
|
||||||
|
with pytest.raises(UpstreamError) as excinfo:
|
||||||
|
await _dispatch(provider, _body(tools=[WEB_SEARCH_TOOL, FUNCTION_TOOL]))
|
||||||
|
|
||||||
|
assert excinfo.value.status_code == 400
|
||||||
|
assert excinfo.value.code == "UNSUPPORTED_WEB_SEARCH"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_generic_openai_upstream_still_accepts_function_tools() -> None:
|
||||||
|
provider = BaseUpstreamProvider(base_url="http://test", api_key="k")
|
||||||
|
|
||||||
|
kwargs = await _dispatch(provider, _body(tools=[FUNCTION_TOOL]))
|
||||||
|
|
||||||
|
assert kwargs["model"] == "openai/deepseek-v4-flash-0731"
|
||||||
|
assert kwargs["tools"] == [FUNCTION_TOOL]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_openai_adapter_can_still_handle_search_tool() -> None:
|
||||||
|
provider = BaseUpstreamProvider(
|
||||||
|
base_url="https://openrouter.ai/api/v1", api_key="k"
|
||||||
|
)
|
||||||
|
|
||||||
kwargs = await _dispatch(provider, _body(tools=[WEB_SEARCH_TOOL]))
|
kwargs = await _dispatch(provider, _body(tools=[WEB_SEARCH_TOOL]))
|
||||||
|
|
||||||
assert kwargs["model"] == "openai/deepseek-v4-flash-0731"
|
|
||||||
assert kwargs["tools"] == [WEB_SEARCH_TOOL]
|
assert kwargs["tools"] == [WEB_SEARCH_TOOL]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user