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", ) -> 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, ) 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", ) -> ChatCompletionResult: provider_name = _normalize_provider(provider) structured_output = self._structured_output_required(function_name) request_timeout = self._request_timeout_for_function(function_name, response_mode=response_mode) is_openclaw_gateway_request = self._is_openclaw_gateway_request( provider_name=provider_name, base_url=base_url, ) openclaw_agents_timeout_seconds = self._openclaw_agents_timeout_seconds() if is_openclaw_gateway_request: # 网关内部 agent 超时需小于客户端超时,避免本地请求先被中止。 request_timeout = max(float(request_timeout), float(openclaw_agents_timeout_seconds) + 5.0) use_provider_circuit_breaker = self._use_provider_circuit_breaker(function_name) 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") openclaw_agent_timeout = int(openclaw_agents_timeout_seconds) if is_openclaw_gateway_request else None if is_openclaw_gateway_request: _runtime_log( "OpenClaw调用", ( 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={openclaw_agent_timeout if openclaw_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_openclaw_gateway_request=is_openclaw_gateway_request, openclaw_agents_timeout_seconds=openclaw_agents_timeout_seconds, request_started_at_perf=request_started_at_perf, request_trace_id=request_trace_id, openclaw_agent_timeout=openclaw_agent_timeout, ) if use_provider_circuit_breaker: self._record_provider_success(circuit_key) return result except Exception as exc: if _looks_like_openclaw_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=openclaw_agent_timeout, error_type=type(exc).__name__, error=str(exc)[:260], ) if is_openclaw_gateway_request: elapsed_ms = (time.perf_counter() - request_started_at_perf) * 1000.0 _runtime_log( "OpenClaw调用", ( 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)