203 lines
8.3 KiB
Python
203 lines
8.3 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class LLMChatTransportMixin:
|
|
async def _chat_completion(
|
|
self,
|
|
*,
|
|
provider: str,
|
|
prompt: str,
|
|
system_prompt: str,
|
|
api_key: str,
|
|
base_url: str,
|
|
model: str,
|
|
temperature: float,
|
|
max_tokens: int,
|
|
function_name: str = "",
|
|
response_mode: str = "complete",
|
|
routing_meta: dict[str, Any] | None = None,
|
|
) -> str:
|
|
result = await self._chat_completion_raw(
|
|
provider=provider,
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
model=model,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
function_name=function_name,
|
|
response_mode=response_mode,
|
|
routing_meta=routing_meta,
|
|
)
|
|
return result.text
|
|
|
|
async def _chat_completion_raw(
|
|
self,
|
|
*,
|
|
provider: str,
|
|
prompt: str,
|
|
system_prompt: str,
|
|
api_key: str,
|
|
base_url: str,
|
|
model: str,
|
|
temperature: float,
|
|
max_tokens: int,
|
|
function_name: str = "",
|
|
response_mode: str = "complete",
|
|
routing_meta: dict[str, Any] | None = None,
|
|
) -> ChatCompletionResult:
|
|
provider_name = _normalize_provider(provider)
|
|
normalized_routing_meta = _normalize_routing_meta(routing_meta)
|
|
structured_output = self._structured_output_required(function_name, routing_meta=normalized_routing_meta)
|
|
request_timeout = self._request_timeout_for_function(
|
|
function_name,
|
|
response_mode=response_mode,
|
|
routing_meta=normalized_routing_meta,
|
|
)
|
|
is_control_plane_gateway_request = self._is_control_plane_gateway_request(
|
|
provider_name=provider_name,
|
|
base_url=base_url,
|
|
)
|
|
control_plane_agents_timeout_seconds = self._control_plane_agents_timeout_seconds()
|
|
if is_control_plane_gateway_request:
|
|
# 网关内部 agent 超时需小于客户端超时,避免本地请求先被中止。
|
|
request_timeout = max(float(request_timeout), float(control_plane_agents_timeout_seconds) + 5.0)
|
|
use_provider_circuit_breaker = self._use_provider_circuit_breaker(
|
|
function_name,
|
|
routing_meta=normalized_routing_meta,
|
|
)
|
|
circuit_key = self._provider_circuit_key(
|
|
provider=provider_name,
|
|
base_url=base_url,
|
|
model=model,
|
|
)
|
|
if use_provider_circuit_breaker and self._is_provider_circuit_open(circuit_key):
|
|
raise ValueError(f"provider_circuit_open:{provider_name}")
|
|
key = api_key.strip()
|
|
messages: list[dict[str, str]] = []
|
|
cleaned_system_prompt = self._inject_function_guardrails(
|
|
system_prompt=system_prompt,
|
|
function_name=function_name,
|
|
)
|
|
if cleaned_system_prompt:
|
|
messages.append({"role": "system", "content": cleaned_system_prompt})
|
|
messages.append({"role": "user", "content": prompt})
|
|
request_trace_id = uuid4().hex[:12]
|
|
request_started_at_perf = time.perf_counter()
|
|
function_label = str(function_name or "llm_generate")
|
|
control_plane_agent_timeout = int(control_plane_agents_timeout_seconds) if is_control_plane_gateway_request else None
|
|
|
|
if is_control_plane_gateway_request:
|
|
_runtime_log(
|
|
"PMFlow 本地路由层调用",
|
|
(
|
|
f"start ts={_cn_now_text_ms()} trace_id={request_trace_id} "
|
|
f"function={function_label} "
|
|
f"request_timeout={float(request_timeout):.2f}s "
|
|
f"agent_timeout={control_plane_agent_timeout if control_plane_agent_timeout is not None else 'N/A'}"
|
|
),
|
|
)
|
|
|
|
try:
|
|
if provider_name == "ollama":
|
|
result = await self._chat_completion_via_ollama(
|
|
base_url=base_url,
|
|
model=model,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
messages=messages,
|
|
request_timeout=request_timeout,
|
|
)
|
|
else:
|
|
if not key:
|
|
raise ValueError("未配置 LLM API Key")
|
|
result = await self._chat_completion_via_openai_compatible(
|
|
key=key,
|
|
base_url=base_url,
|
|
model=model,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
messages=messages,
|
|
request_timeout=request_timeout,
|
|
structured_output=structured_output,
|
|
function_label=function_label,
|
|
provider_name=provider_name,
|
|
is_control_plane_gateway_request=is_control_plane_gateway_request,
|
|
control_plane_agents_timeout_seconds=control_plane_agents_timeout_seconds,
|
|
request_started_at_perf=request_started_at_perf,
|
|
request_trace_id=request_trace_id,
|
|
control_plane_agent_timeout=control_plane_agent_timeout,
|
|
routing_meta=normalized_routing_meta,
|
|
)
|
|
if use_provider_circuit_breaker:
|
|
self._record_provider_success(circuit_key)
|
|
return result
|
|
except Exception as exc:
|
|
if _is_control_plane_timeout_message(str(exc)):
|
|
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
|
|
_runtime_log(
|
|
"LLM超时异常",
|
|
(
|
|
f"detected ts={_cn_now_text_ms()} trace_id={request_trace_id} "
|
|
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
|
|
f"error={type(exc).__name__}:{str(exc)[:260]}"
|
|
),
|
|
)
|
|
logger.warning(
|
|
"llm_timeout_exception_detected",
|
|
trace_id=request_trace_id,
|
|
function_name=function_label,
|
|
request_timeout_seconds=float(request_timeout),
|
|
agent_timeout_seconds=control_plane_agent_timeout,
|
|
error_type=type(exc).__name__,
|
|
error=str(exc)[:260],
|
|
)
|
|
if is_control_plane_gateway_request:
|
|
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
|
|
_runtime_log(
|
|
"PMFlow 本地路由层调用",
|
|
(
|
|
f"exception ts={_cn_now_text_ms()} trace_id={request_trace_id} "
|
|
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
|
|
f"error={type(exc).__name__}:{str(exc)[:260]}"
|
|
),
|
|
)
|
|
if use_provider_circuit_breaker:
|
|
self._record_provider_failure(circuit_key, exc)
|
|
raise
|
|
|
|
async def _chat_completion_via_ollama(
|
|
self,
|
|
*,
|
|
base_url: str,
|
|
model: str,
|
|
temperature: float,
|
|
max_tokens: int,
|
|
messages: list[dict[str, str]],
|
|
request_timeout: float,
|
|
) -> ChatCompletionResult:
|
|
endpoint = f"{base_url.rstrip('/')}/api/chat"
|
|
payload = {
|
|
"model": model,
|
|
"messages": messages,
|
|
"stream": False,
|
|
"options": {
|
|
"temperature": temperature,
|
|
"num_predict": max_tokens,
|
|
},
|
|
}
|
|
async with httpx.AsyncClient(timeout=request_timeout, trust_env=False) as client:
|
|
resp = await client.post(endpoint, json=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
message = data.get("message") if isinstance(data, dict) else {}
|
|
content = message.get("content") if isinstance(message, dict) else None
|
|
text = self._sanitize_visible_answer(self._extract_text_from_value(content))
|
|
if not text:
|
|
text = self._sanitize_visible_answer(self._extract_text_from_value(data.get("response")))
|
|
if not text and not isinstance(data, dict):
|
|
raise ValueError("Ollama 返回空内容")
|
|
return ChatCompletionResult(text=text, raw_response=data)
|