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)