feat(config): named LLM provider list via env vars, runtime selection
- Config.llm_profiles: LLM_PROVIDERS declares names, each group defined by LLM_<NAME>_BASE_URL/_API_KEY/_MODEL (required) and optional _TIMEOUT_SECONDS/ _MAX_RETRIES/_TEMPERATURE/_EXTRA_BODY; JSON-dict form of LLM_PROVIDERS also supported. LLM_PROVIDER selects the active group at runtime; empty/fake = offline scripted model - removed fixed openai/kimi groups (AGENT_LLM_PROVIDER, KIMI_LLM_*) - validate(): incomplete groups and unknown LLM_PROVIDER fail fast with the configured-name list - OpenAILLMService: http client trust_env from LLM_HTTP_TRUST_ENV (default False - local gateways must bypass the Windows system proxy) - build_llm_service(provider): explicit name or LLM_PROVIDER default - .env/.env.example: deepseek + kimi groups side by side, switch by LLM_PROVIDER=kimi (thinking off via EXTRA_BODY reasoning_effort=none) - eval probe scripts updated to the new config interface Verified: 172 unit tests; live calls succeed on both providers (deepseek 28s, kimi 6.6s, both emitting valid <state> prefixes)
This commit is contained in:
+46
-14
@@ -5,7 +5,8 @@ fake 与真实 provider 实现同一 ``LLMService`` Protocol,节点不感知
|
||||
- ``OpenAILLMService``:真实 provider(TODO 关键代码)——
|
||||
超时、取消、有限重试、模型配置。
|
||||
|
||||
``build_llm_service`` 按 ``Config.AGENT_LLM_PROVIDER`` 选择实现。
|
||||
``build_llm_service`` 按命名提供商列表(Config.LLM_PROVIDERS / LLM_PROVIDER)
|
||||
选择实现,见 core.config。
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -92,6 +93,7 @@ class OpenAILLMService:
|
||||
timeout_seconds: float = 30.0,
|
||||
max_retries: int = 2,
|
||||
temperature: float | None = None,
|
||||
extra_body: dict | None = None,
|
||||
):
|
||||
self._model = model
|
||||
self._api_key = api_key
|
||||
@@ -99,17 +101,24 @@ class OpenAILLMService:
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._max_retries = max_retries
|
||||
self._temperature = temperature
|
||||
# 附加请求体(如思考开关),原样合并进 chat.completions.create 参数。
|
||||
self._extra_body = dict(extra_body or {})
|
||||
self._client = None
|
||||
|
||||
def _get_client(self):
|
||||
# 延迟构造:fake provider 使用时无需安装/初始化真实客户端。
|
||||
# AsyncOpenAI 的创建不绑定事件循环,可在首次调用前安全缓存。
|
||||
# trust_env=False:不走系统代理,本地网关直连(代理曾导致 503/500)。
|
||||
if self._client is None:
|
||||
import httpx
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from ..core.config import Config
|
||||
|
||||
kwargs = {"api_key": self._api_key, "timeout": self._timeout_seconds}
|
||||
if self._base_url:
|
||||
kwargs["base_url"] = self._base_url
|
||||
kwargs["http_client"] = httpx.AsyncClient(trust_env=Config.LLM_HTTP_TRUST_ENV)
|
||||
self._client = AsyncOpenAI(**kwargs)
|
||||
return self._client
|
||||
|
||||
@@ -130,6 +139,7 @@ class OpenAILLMService:
|
||||
kwargs: dict = {"model": self._model, "messages": messages}
|
||||
if self._temperature is not None:
|
||||
kwargs["temperature"] = self._temperature
|
||||
kwargs.update(self._extra_body)
|
||||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
@@ -225,19 +235,41 @@ def workflow_fake_llm() -> "FakeLLMService":
|
||||
|
||||
|
||||
def build_llm_service(provider: str | None = None) -> LLMService:
|
||||
"""按配置构造 LLM 服务;provider 缺省读 Config.AGENT_LLM_PROVIDER。"""
|
||||
"""按命名提供商列表构造 LLM 服务;provider 缺省读 Config.LLM_PROVIDER。
|
||||
|
||||
配置见 Config.llm_profiles(LLM_PROVIDERS 名单 + LLM_<名>_* 变量)。
|
||||
provider 为空或 fake = 离线脚本模型。
|
||||
"""
|
||||
from ..core.config import Config
|
||||
|
||||
provider = provider or Config.AGENT_LLM_PROVIDER
|
||||
if provider == "openai":
|
||||
return OpenAILLMService(
|
||||
model=Config.LLM_MODEL,
|
||||
api_key=Config.LLM_API_KEY,
|
||||
base_url=Config.LLM_BASE_URL,
|
||||
timeout_seconds=Config.LLM_TIMEOUT_SECONDS,
|
||||
max_retries=Config.LLM_MAX_RETRIES,
|
||||
temperature=Config.LLM_TEMPERATURE,
|
||||
)
|
||||
if provider == "fake":
|
||||
provider = (provider if provider is not None else Config.LLM_PROVIDER)
|
||||
provider = (provider or "").strip().lower()
|
||||
if not provider or provider == "fake":
|
||||
return FakeLLMService(_default_fake_responder)
|
||||
raise ValueError(f"Unknown AGENT_LLM_PROVIDER: {provider!r}")
|
||||
profiles = Config.llm_profiles()
|
||||
if provider not in profiles:
|
||||
raise ValueError(
|
||||
f"Unknown LLM provider {provider!r}; configured: {sorted(profiles)}"
|
||||
)
|
||||
profile = profiles[provider]
|
||||
extra_body: dict = {}
|
||||
if profile["extra_body"]:
|
||||
import json as _json
|
||||
|
||||
try:
|
||||
parsed = _json.loads(profile["extra_body"])
|
||||
if isinstance(parsed, dict):
|
||||
extra_body = parsed
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"extra_body of provider {provider!r} is not valid JSON: {exc}"
|
||||
) from exc
|
||||
return OpenAILLMService(
|
||||
model=profile["model"],
|
||||
api_key=profile["api_key"],
|
||||
base_url=profile["base_url"],
|
||||
timeout_seconds=profile["timeout_seconds"],
|
||||
max_retries=profile["max_retries"],
|
||||
temperature=profile["temperature"],
|
||||
extra_body=extra_body,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user