This commit is contained in:
Mikan
2026-06-19 17:32:21 +03:00
parent 81bbf8aa69
commit 2167493887
6 changed files with 484 additions and 32 deletions

View File

@@ -211,6 +211,141 @@ async def test_llm_endpoint(
return result
@router.post("/llm/test-tools")
async def test_llm_tools_endpoint(
payload: Dict[str, Any] = Body(default={}),
db: AsyncSession = Depends(get_db_dep),
_: User = Depends(require_admin),
):
"""Probe whether the configured LLM endpoint supports OpenAI-style tool calls.
Sends a minimal chat completion request WITH a `tools` array containing one
simple function (`get_time`). Returns:
- ok: bool — did the model produce ANY well-formed response?
- tool_calls_returned: bool — did the model emit at least one tool_call?
- tool_call_name: str|null — the function name the model called (if any)
- tool_call_args: dict|null — the parsed arguments (if any)
- text: str — the model's text response (if any)
- http_status: int — HTTP status of the chat-completions call
- latency_ms: int
- raw_tool_calls: list — the raw tool_calls array from the response
- error: str|null — error message if the request failed
- error_type: str|null
Use this to verify the model actually supports function-calling before
relying on it for world-builder / orchestrator / step-writer flows.
"""
import time
import httpx
import json as _json
settings_map = await get_all_settings(db)
overrides = (payload or {}).get("overrides") or {}
for k, v in overrides.items():
if k in EDITABLE_SETTING_KEYS:
settings_map[k] = v
base_url = str(settings_map.get("llm.base_url", "")).rstrip("/")
model = str(settings_map.get("llm.model", "local-model"))
api_key = str(settings_map.get("llm.api_key", "dummy"))
timeout_s = float(settings_map.get("llm.request_timeout", 30) or 30)
result: Dict[str, Any] = {
"base_url": base_url,
"model": model,
"ok": False,
"tool_calls_returned": False,
}
headers = {"Content-Type": "application/json"}
if api_key and api_key != "dummy":
headers["Authorization"] = f"Bearer {api_key}"
# Minimal tool definition — the model should call this.
tools = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Returns the current time. Call this when the user asks for the time.",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "Optional timezone, e.g. 'UTC' or 'Europe/Moscow'",
},
},
"required": [],
},
},
}
]
chat_url = f"{base_url}/chat/completions"
chat_payload = {
"model": model,
"messages": [
{"role": "system", "content": "You are a helpful assistant. When the user asks for the time, you MUST call the get_current_time tool."},
{"role": "user", "content": "What time is it now? Use the get_current_time tool to find out."},
],
"tools": tools,
"tool_choice": "auto",
"max_tokens": 200,
"temperature": 0.0,
"stream": False,
}
started = time.monotonic()
try:
async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=timeout_s, write=10.0, pool=5.0)) as client:
resp = await client.post(chat_url, json=chat_payload, headers=headers)
result["http_status"] = resp.status_code
result["latency_ms"] = int((time.monotonic() - started) * 1000)
if resp.status_code >= 400:
result["error"] = f"HTTP {resp.status_code}: {resp.text[:500]}"
result["error_type"] = "HTTPError"
return result
data = resp.json()
except httpx.ConnectError as e:
cause = getattr(e, "__cause__", None) or getattr(e, "__context__", None)
result["error"] = f"ConnectError: {e}"
if cause:
result["error"] += f" (cause: {cause})"
result["error_type"] = "ConnectError"
return result
except Exception as e:
result["error"] = f"{type(e).__name__}: {e}"
result["error_type"] = type(e).__name__
return result
try:
choice = (data.get("choices") or [{}])[0]
msg = choice.get("message", {})
text = msg.get("content") or ""
tool_calls = msg.get("tool_calls") or []
result["text"] = text[:500]
result["raw_tool_calls"] = tool_calls
if tool_calls:
result["tool_calls_returned"] = True
first = tool_calls[0]
fn = first.get("function", {}) if isinstance(first, dict) else {}
result["tool_call_name"] = fn.get("name")
args_str = fn.get("arguments", "{}")
try:
result["tool_call_args"] = _json.loads(args_str) if args_str else {}
except _json.JSONDecodeError:
result["tool_call_args"] = {"_raw": args_str}
result["ok"] = True
result["usage"] = data.get("usage", {})
except Exception as e:
result["error"] = f"response_parse_failed: {type(e).__name__}: {e}"
result["error_type"] = type(e).__name__
return result
return result
@router.get("/llm-logs", response_model=List[LlmLogOut])
async def list_llm_logs(
limit: int = 50,