194 lines
8.3 KiB
Python
194 lines
8.3 KiB
Python
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)
|
||
|