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)