refactor: extract tooling policy helpers

Move tool allowlist, tool_config, and tooling-context helpers into app/http/tooling_policy.py while keeping route behavior unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-04-22 11:37:50 +08:00
parent d081743924
commit 8b012310a2
3 changed files with 132 additions and 117 deletions

View File

@@ -35,7 +35,6 @@ from .http.tool_bridge import (
_allowed_stream_tool_event,
_allowed_tool_events,
_anthropic_forced_tool_name,
_anthropic_tool_name as _shared_anthropic_tool_name,
_anthropic_tool_result_block,
_anthropic_tool_use_block,
_forced_tool_event_from_text,
@@ -43,9 +42,14 @@ from .http.tool_bridge import (
_json_string,
_openai_forced_tool_name,
_openai_tool_call,
_openai_tool_name as _shared_openai_tool_name,
_tool_code_single_arg_name,
)
from .http.tooling_policy import (
_anthropic_has_tooling_context,
_anthropic_tool_config,
_openai_has_tooling_context,
_openai_tool_config,
)
from .lingma_pool import LingmaPool, PoolInstance
from .logging_config import configure_logging, get_logger, request_id_var
from .model_map import build_model_name_map, flatten_model_keys, resolve_model
@@ -379,115 +383,6 @@ def _include_usage(stream_options: dict | None) -> bool:
return bool(stream_options.get("include_usage"))
def _tool_allowlist() -> set[str]:
return {name.strip() for name in settings.tool_allowlist if isinstance(name, str) and name.strip()}
def _openai_tool_name(tool: Any) -> str | None:
return _shared_openai_tool_name(tool)
def _anthropic_tool_name(tool: Any) -> str | None:
return _shared_anthropic_tool_name(tool)
def _filter_allowed_tools(tools: list[dict[str, Any]], *, provider: str) -> list[dict[str, Any]]:
allowlist = _tool_allowlist()
if not allowlist:
return tools
name_fn = _openai_tool_name if provider == "openai" else _anthropic_tool_name
return [tool for tool in tools if (name := name_fn(tool)) and name in allowlist]
def _ensure_tool_choice_allowed(tool_choice: Any, *, provider: str) -> None:
allowlist = _tool_allowlist()
if not allowlist:
return
forced_name = (
_openai_forced_tool_name(tool_choice)
if provider == "openai"
else _anthropic_forced_tool_name(tool_choice)
)
if forced_name and forced_name not in allowlist:
raise HTTPException(
status_code=400,
detail={
"error": {
"type": "invalid_request_error",
"message": f"tool '{forced_name}' is not allowed",
}
},
)
def _openai_tool_config(req: ChatCompletionsRequest) -> dict[str, Any] | None:
if not settings.tool_forward_enabled:
return None
has_tools = isinstance(req.tools, list) and len(req.tools) > 0
has_choice = req.tool_choice is not None
if not has_tools and not has_choice:
return None
_ensure_tool_choice_allowed(req.tool_choice, provider="openai")
tools = _filter_allowed_tools(req.tools or [], provider="openai")
return {
"provider": "openai",
"tools": tools,
"tool_choice": req.tool_choice,
}
def _anthropic_tool_config(req: AnthropicMessagesRequest) -> dict[str, Any] | None:
if not settings.tool_forward_enabled:
return None
has_tools = isinstance(req.tools, list) and len(req.tools) > 0
has_choice = req.tool_choice is not None
if not has_tools and not has_choice:
return None
_ensure_tool_choice_allowed(req.tool_choice, provider="anthropic")
tools = _filter_allowed_tools(req.tools or [], provider="anthropic")
return {
"provider": "anthropic",
"tools": tools,
"tool_choice": req.tool_choice,
}
def _openai_has_tooling_context(req: ChatCompletionsRequest, messages: list[dict[str, Any]]) -> bool:
if isinstance(req.tools, list) and len(req.tools) > 0:
return True
if req.tool_choice is not None:
return True
for m in messages:
role = m.get("role")
if role == "tool":
return True
if role == "assistant" and m.get("tool_calls"):
return True
return False
def _anthropic_content_has_tool_blocks(content: Any) -> bool:
if not isinstance(content, list):
return False
for item in content:
if isinstance(item, dict) and item.get("type") in {"tool_use", "tool_result"}:
return True
return False
def _anthropic_has_tooling_context(req: AnthropicMessagesRequest) -> bool:
if isinstance(req.tools, list) and len(req.tools) > 0:
return True
if req.tool_choice is not None:
return True
if _anthropic_content_has_tool_blocks(req.system):
return True
for m in req.messages:
if _anthropic_content_has_tool_blocks(m.content):
return True
return False
def _resolve_ask_mode(model: str, has_tooling_context: bool) -> str:
return _shared_resolve_ask_mode(
model,
@@ -557,7 +452,7 @@ async def v1_chat_completions(req: ChatCompletionsRequest, request: Request):
# 1. Reuse the upstream sessionId so Lingma/Qwen hits its KV cache.
# 2. Send only the new user message instead of the whole history.
# 3. Stick the request to the pool instance that originally served it.
tool_config = _openai_tool_config(req)
tool_config = _openai_tool_config(req, settings=settings)
has_tooling_context = _openai_has_tooling_context(req, messages_dump)
execution = await prepare_execution_context(
protocol="chat",
@@ -1023,7 +918,7 @@ async def v1_messages(req: AnthropicMessagesRequest, request: Request):
# ------------------------------------------------------------- session reuse
try:
tool_config = _anthropic_tool_config(req)
tool_config = _anthropic_tool_config(req, settings=settings)
except HTTPException as exc:
detail = exc.detail if isinstance(exc.detail, dict) else {}
error = detail.get("error") if isinstance(detail.get("error"), dict) else {}