| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- """OpenRouter LLM 调用工具。"""
- from __future__ import annotations
- from typing import Any
- from openrouter import OpenRouter
- from openrouter.errors.openroutererror import OpenRouterError as SdkOpenRouterError
- from app.core.config import settings
- _ALLOWED_ROLES = frozenset({"system", "user", "assistant"})
- class OpenRouterCallError(Exception):
- """项目内 OpenRouter 调用失败。"""
- def __init__(self, message: str, *, cause: Exception | None = None):
- super().__init__(message)
- self.cause = cause
- def _build_client() -> OpenRouter:
- kwargs: dict[str, Any] = {
- "api_key": settings.open_router_api_key.strip(),
- "timeout_ms": max(settings.open_router_timeout_seconds, 1) * 1000,
- }
- if settings.open_router_http_referer.strip():
- kwargs["http_referer"] = settings.open_router_http_referer.strip()
- if settings.open_router_app_title.strip():
- kwargs["x_open_router_title"] = settings.open_router_app_title.strip()
- if settings.open_router_base_url.strip():
- kwargs["server_url"] = settings.open_router_base_url.strip()
- return OpenRouter(**kwargs)
- def _normalize_messages(messages: list[dict[str, str]]) -> list[dict[str, str]]:
- if not messages:
- raise OpenRouterCallError("messages must not be empty")
- normalized: list[dict[str, str]] = []
- for index, item in enumerate(messages):
- role = (item.get("role") or "").strip()
- content = item.get("content")
- if role not in _ALLOWED_ROLES:
- raise OpenRouterCallError(
- f"messages[{index}].role must be one of: system, user, assistant"
- )
- if content is None or not str(content).strip():
- raise OpenRouterCallError(f"messages[{index}].content must not be empty")
- normalized.append({"role": role, "content": str(content)})
- return normalized
- def _normalize_chat_response(response: Any) -> dict[str, Any]:
- choices = getattr(response, "choices", None) or []
- first_choice = choices[0] if choices else None
- message = getattr(first_choice, "message", None) if first_choice else None
- content = getattr(message, "content", None) or ""
- usage = getattr(response, "usage", None)
- if hasattr(usage, "model_dump"):
- usage = usage.model_dump()
- elif usage is not None and not isinstance(usage, dict):
- usage = dict(usage) if hasattr(usage, "__iter__") else usage
- return {
- "id": getattr(response, "id", None),
- "model": getattr(response, "model", None),
- "content": content,
- "usage": usage,
- "finish_reason": getattr(first_choice, "finish_reason", None) if first_choice else None,
- }
- def create_chat_completion(
- messages: list[dict[str, str]],
- *,
- model: str | None = None,
- temperature: float | None = None,
- max_tokens: int | None = None,
- ) -> dict[str, Any]:
- """通过 OpenRouter SDK 调用对话补全,返回结构化结果。"""
- if not settings.open_router_api_key.strip():
- raise OpenRouterCallError("OPEN_ROUTER_API_KEY or OPENROUTER_API_KEY is not configured")
- normalized_messages = _normalize_messages(messages)
- model_name = (model or settings.open_router_default_model).strip()
- if not model_name:
- raise OpenRouterCallError(
- "model is required (set OPEN_ROUTER_DEFAULT_MODEL or pass model=...)"
- )
- request_kwargs: dict[str, Any] = {
- "model": model_name,
- "messages": normalized_messages,
- }
- temperature_value = (
- temperature if temperature is not None else settings.open_router_temperature
- )
- max_tokens_value = max_tokens if max_tokens is not None else settings.open_router_max_tokens
- if temperature_value is not None:
- request_kwargs["temperature"] = temperature_value
- if max_tokens_value is not None:
- request_kwargs["max_tokens"] = max_tokens_value
- try:
- with _build_client() as client:
- response = client.chat.send(**request_kwargs)
- except SdkOpenRouterError as exc:
- raise OpenRouterCallError(str(exc), cause=exc) from exc
- except Exception as exc:
- raise OpenRouterCallError(f"OpenRouter SDK error: {exc}", cause=exc) from exc
- return _normalize_chat_response(response)
- def chat_text(
- user_prompt: str,
- *,
- system_prompt: str | None = None,
- model: str | None = None,
- temperature: float | None = None,
- max_tokens: int | None = None,
- ) -> str:
- """单轮对话便捷方法,直接返回模型回复文本。"""
- messages: list[dict[str, str]] = []
- if system_prompt and system_prompt.strip():
- messages.append({"role": "system", "content": system_prompt.strip()})
- messages.append({"role": "user", "content": user_prompt.strip()})
- result = create_chat_completion(
- messages,
- model=model,
- temperature=temperature,
- max_tokens=max_tokens,
- )
- return str(result.get("content") or "")
|