"""LLM 调用共享工具(OpenAI 兼容 API / Cursor SDK)。""" from __future__ import annotations import json import re from typing import Any import certifi import httpx from daily.config import env, env_int from daily.cursor_client import cursor_chat _JSON_BLOCK = re.compile(r"```(?:json)?\s*([\s\S]*?)```", re.IGNORECASE) def extract_json_object(text: str) -> dict[str, Any]: text = text.strip() if not text: return {} try: data = json.loads(text) return data if isinstance(data, dict) else {} except json.JSONDecodeError: pass match = _JSON_BLOCK.search(text) if match: try: data = json.loads(match.group(1).strip()) return data if isinstance(data, dict) else {} except json.JSONDecodeError: pass start, end = text.find("{"), text.rfind("}") if start >= 0 and end > start: try: data = json.loads(text[start : end + 1]) return data if isinstance(data, dict) else {} except json.JSONDecodeError: pass return {} def _has_openai_configured() -> bool: return bool(env("DAILY_LLM_API_KEY") or env("OPENAI_API_KEY")) def _has_cursor_configured() -> bool: return bool(env("CURSOR_API_KEY")) def llm_provider() -> str: raw = (env("DAILY_LLM_PROVIDER") or "auto").strip().lower() if raw in {"openai", "cursor"}: return raw return "auto" def resolve_llm_backend() -> str: """返回 openai | cursor | 空字符串。""" provider = llm_provider() has_openai = _has_openai_configured() has_cursor = _has_cursor_configured() if provider == "openai": if has_openai: return "openai" return "cursor" if has_cursor else "" if provider == "cursor": if has_cursor: return "cursor" return "openai" if has_openai else "" agent_mode = (env("DAILY_REPORT_MODE") or "").strip().lower() == "agent" if agent_mode and has_cursor: return "cursor" if has_openai: return "openai" if has_cursor: return "cursor" return "" def _openai_chat(system: str, user: str) -> str: api_key = (env("DAILY_LLM_API_KEY") or env("OPENAI_API_KEY") or "").strip() if not api_key: return "" base = (env("DAILY_LLM_API_BASE") or env("OPENAI_API_BASE") or "https://api.openai.com/v1").rstrip("/") model = env("DAILY_LLM_MODEL") or env("OPENAI_MODEL") or "gpt-4o-mini" timeout = env_int("DAILY_LLM_TIMEOUT", 120) payload = { "model": model, "temperature": 0.2, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], } with httpx.Client(timeout=timeout, verify=certifi.where()) as client: resp = client.post( f"{base}/chat/completions", headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, json=payload, ) resp.raise_for_status() data = resp.json() return str(data["choices"][0]["message"]["content"] or "").strip() def llm_chat(system: str, user: str) -> str: backend = resolve_llm_backend() if backend == "openai": return _openai_chat(system, user) if backend == "cursor": return cursor_chat(system, user) return "" def has_llm_configured() -> bool: return bool(resolve_llm_backend())