PMFlow/backend/app/services/llm_service/mixin_chat_openai.py

194 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
class LLMChatOpenAIMixin:
async def _chat_completion_via_openai_compatible(
self,
*,
key: str,
base_url: str,
model: str,
temperature: float,
max_tokens: int,
messages: list[dict[str, str]],
request_timeout: float,
structured_output: bool,
function_label: str,
provider_name: str,
is_openclaw_gateway_request: bool,
openclaw_agents_timeout_seconds: int,
request_started_at_perf: float,
request_trace_id: str,
openclaw_agent_timeout: Optional[int],
) -> ChatCompletionResult:
endpoint = f"{base_url.rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
}
current_max_tokens = max(int(max_tokens or 0), 1)
empty_content_retry_budget = 1
http_status_retry_budget = 2
http_status_retry_attempt = 0
while True:
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": current_max_tokens,
}
if is_openclaw_gateway_request:
payload["agents"] = {
"defaults": {
"timeoutSeconds": int(openclaw_agents_timeout_seconds),
}
}
async with httpx.AsyncClient(timeout=request_timeout, trust_env=False) as client:
resp = await client.post(endpoint, headers=headers, json=payload)
if is_openclaw_gateway_request:
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw调用",
(
f"http ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} status={resp.status_code} elapsed_ms={elapsed_ms:.0f}"
),
)
if resp.status_code >= 400:
body_preview = str(resp.text or "").replace("\n", " ").strip()[:260]
if is_openclaw_gateway_request:
_runtime_log(
"OpenClaw调用",
(
f"http_error ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} status={resp.status_code} body={body_preview}"
),
)
status_code = int(resp.status_code)
can_retry_http_status = (
http_status_retry_budget > 0
and self._is_retryable_http_status(status_code)
)
if can_retry_http_status:
retry_delay = self._http_retry_delay_seconds(
status_code=status_code,
attempt=http_status_retry_attempt,
retry_after_header=resp.headers.get("Retry-After"),
)
_runtime_log(
"LLM",
(
"HTTP状态重试:"
f"function={function_label}, provider={provider_name}, model={model}, "
f"status={status_code}, retry_in={retry_delay:.2f}s, "
f"left={http_status_retry_budget - 1}"
),
)
http_status_retry_budget -= 1
http_status_retry_attempt += 1
await asyncio.sleep(retry_delay)
continue
resp.raise_for_status()
data = resp.json()
choices = data.get("choices") or []
if not choices:
raise ValueError("LLM 响应无 choices")
first_choice = choices[0] if isinstance(choices[0], dict) else {}
message = first_choice.get("message") if isinstance(first_choice, dict) else {}
text = self._extract_text_from_chat_response(
message=message,
choice=first_choice,
response=data,
structured_output=structured_output,
)
if (
not text
and not structured_output
and self._reasoning_content_fallback_enabled(function_name=function_label)
):
fallback_text = self._extract_reasoning_fallback_text(
message=message,
choice=first_choice,
response=data,
)
if fallback_text:
text = fallback_text
_runtime_log(
"LLM",
(
"空content回退reasoning_content:"
f"function={function_label}, provider={provider_name}, model={model}, "
f"text_len={len(text)}"
),
)
if text or structured_output:
break
msg_keys = list(message.keys()) if isinstance(message, dict) else []
choice_keys = list(first_choice.keys()) if isinstance(first_choice, dict) else []
choice_preview = ""
try:
choice_preview = json.dumps(first_choice, ensure_ascii=False)[:300]
except Exception:
choice_preview = str(first_choice)[:300]
empty_error = ValueError(
"LLM 返回空内容: "
f"message_keys={msg_keys}, choice_keys={choice_keys}, choice_preview={choice_preview}"
)
can_retry_empty = (
empty_content_retry_budget > 0
and self._should_retry_empty_chat_response(
structured_output=structured_output,
message=message,
choice=first_choice,
current_max_tokens=current_max_tokens,
)
)
if not can_retry_empty:
raise empty_error
empty_content_retry_budget -= 1
next_max_tokens = self._next_retry_max_tokens_for_empty_chat(current_max_tokens)
_runtime_log(
"LLM",
(
"空内容提额重试:"
f"function={function_label}, provider={provider_name}, model={model}, "
f"max_tokens={current_max_tokens}->{next_max_tokens}"
),
)
current_max_tokens = next_max_tokens
if _looks_like_openclaw_timeout_message(text):
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw超时" if is_openclaw_gateway_request else "LLM超时文本",
(
f"detected ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} "
f"text={text.replace(chr(10), ' ')[:260]}"
),
)
logger.warning(
"llm_timeout_payload_detected",
trace_id=request_trace_id,
function_name=function_label,
provider=provider_name,
model=model,
base_url=str(base_url or "")[:120],
request_timeout_seconds=float(request_timeout),
agent_timeout_seconds=openclaw_agent_timeout,
text_preview=text.replace("\n", " ")[:260],
)
if is_openclaw_gateway_request:
elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0
_runtime_log(
"OpenClaw调用",
(
f"end ts={_cn_now_text_ms()} trace_id={request_trace_id} "
f"function={function_label} elapsed_ms={elapsed_ms:.0f} text_len={len(text)}"
),
)
return ChatCompletionResult(text=text, raw_response=data)