From eccefe219c98c46efdde203c9ea2f5e495a5d6a7 Mon Sep 17 00:00:00 2001 From: NightEmperor Date: Wed, 17 Jun 2026 15:16:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(llm):=20=E4=BF=AE=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89/Anthropic/OpenAI=20=E8=A7=84=E8=8C=83=20LLM?= =?UTF-8?q?=20=E8=BF=9E=E6=8E=A5=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:src/llm/client.py 的 _litellm_kwargs() 把自定义 provider ID 拼成 '/' 前缀(litellm 无法识别),且 type 字段无法路由到 Anthropic 格式端点,导致自定义/OpenAI/Anthropic 规范全部连不上。 后端修复(路由单点收口,Agent 层零改动): - LLMConfig / LLMConfigUpdate 新增 llm_format(openai | anthropic) - config_service.get_llm_runtime_config 读取 LLM_format - 重写 _litellm_kwargs 路由: · 内置 provider -> litellm 原生 provider/model · 自定义 + OpenAI 兼容 -> custom_llm_provider=openai + 裸模型名 + base_url · 自定义 + Anthropic -> custom_llm_provider=anthropic + 裸模型名 + base_url · 自定义未指定格式 -> 按 base_url 启发式(含 /anthropic 视作 anthropic) - /llm/test 返回路由诊断(model/provider/base_url/format + 回复 + 用量) 前端:自定义 Provider 新增「API 格式」下拉,base_url placeholder 随格式联动 已用 DeepSeek key 对内置/自定义+OpenAI/自定义+Anthropic/启发式 5 场景回归验证通过 --- frontend | 2 +- src/api/routers/configs.py | 17 ++++++++++-- src/config.py | 5 ++++ src/llm/client.py | 47 +++++++++++++++++++++++++++------- src/schemas/config.py | 2 ++ src/services/config_service.py | 1 + 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/frontend b/frontend index 1053f9c..5b1e145 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 1053f9c3fbb0857e4cc0a8acefc01d523a9f3c52 +Subproject commit 5b1e145ff77dce497dedeed6453b14f50a99987b diff --git a/src/api/routers/configs.py b/src/api/routers/configs.py index f249793..ac239aa 100644 --- a/src/api/routers/configs.py +++ b/src/api/routers/configs.py @@ -87,9 +87,22 @@ def test_llm_config() -> OkResponse[str]: try: client = LLMClient(runtime_cfg) + # 暴露实际路由参数,便于排查「连不上 / 报错」时定位是 model 前缀还是 base_url 问题 + routed_kwargs = client._litellm_kwargs(temperature=0.7, max_tokens=None) + diagnosis = { + "route": { + "model": routed_kwargs.get("model"), + "custom_llm_provider": routed_kwargs.get("custom_llm_provider"), + "api_base": routed_kwargs.get("api_base"), + "is_custom": (runtime_cfg.type or "").strip().lower() == "custom", + "llm_format": runtime_cfg.llm_format, + }, + } resp = client.call([{"role": "user", "content": "hello"}]) - raw_json = json.dumps(_to_jsonable(resp.raw), ensure_ascii=False) - return OkResponse[str](data=raw_json) + diagnosis["reply"] = resp.content + diagnosis["usage"] = resp.usage + diagnosis["ok"] = True + return OkResponse[str](data=json.dumps(diagnosis, ensure_ascii=False)) except Exception as ex: return OkResponse[str]( success=False, diff --git a/src/config.py b/src/config.py index 2737866..4bd008d 100644 --- a/src/config.py +++ b/src/config.py @@ -138,6 +138,11 @@ class LLMConfig: base_url: Optional[str] = None api_version: Optional[str] = None azure_endpoint: Optional[str] = None + # API 协议格式:仅对「自定义 Provider」生效,用于路由到对应的上游端点。 + # "openai" -> OpenAI 兼容(/v1/chat/completions) + # "anthropic" -> Anthropic 格式(/v1/messages) + # 为空时按 base_url 启发式推断(含 /anthropic 视作 anthropic,否则 openai)。 + llm_format: Optional[str] = None @dataclass diff --git a/src/llm/client.py b/src/llm/client.py index fd43412..d6c5722 100644 --- a/src/llm/client.py +++ b/src/llm/client.py @@ -109,23 +109,52 @@ def __init__(self, config: LLMConfig): self.config = config def _litellm_kwargs(self, temperature: float = 0.7, max_tokens: Optional[int] = None, **kwargs) -> Dict: - """从 config 构建 litellm completion 参数,不依赖环境变量""" - model = self.config.model - if "/" not in model and self.config.provider: - model = f"{self.config.provider}/{model}" + """从 config 构建 litellm completion 参数,不依赖环境变量。 + + 路由策略(单点收口): + - 内置 provider(deepseek / anthropic / openai / azure ...):使用 litellm 原生 + `provider/model` 形式,由 litellm 自行解析 base_url / 鉴权。 + - 自定义 provider:统一通过 ``custom_llm_provider`` 指定上游协议格式, + 传 **裸模型名**(不带 provider 前缀)+ ``api_base``,避免把任意 provider ID + 拼成 ``/`` 这种 litellm 无法识别的前缀。 + 协议格式由 ``llm_format`` 决定(openai / anthropic),缺省时按 base_url 启发式。 + """ + model = (self.config.model or "").strip() + provider = (self.config.provider or "").strip() + is_custom = (self.config.type or "").strip().lower() == "custom" + kw: Dict = { - "model": model, "temperature": temperature, "max_tokens": max_tokens, } if self.config.api_key: kw["api_key"] = self.config.api_key - # base_url:自建或自定义端点;Azure 时也可用 azure_endpoint - api_base = self.config.base_url or getattr(self.config, "azure_endpoint", None) - if api_base and self.config.type != "builtin": - kw["api_base"] = api_base if getattr(self.config, "api_version", None): kw["api_version"] = self.config.api_version + + api_base = self.config.base_url or getattr(self.config, "azure_endpoint", None) + + if is_custom: + # 自定义 Provider:不要拼 provider 前缀,用 custom_llm_provider 路由协议格式 + fmt = (self.config.llm_format or "").strip().lower() + if not fmt: + # 启发式:base_url 含 /anthropic 视作 Anthropic 格式端点,否则 OpenAI 兼容 + fmt = "anthropic" if "/anthropic" in (api_base or "").lower() else "openai" + fmt = fmt if fmt in ("openai", "anthropic") else "openai" + kw["model"] = model + kw["custom_llm_provider"] = fmt + if api_base: + kw["api_base"] = api_base + else: + # 内置 provider:litellm 原生 provider/model 形式 + litellm_model = model + if "/" not in litellm_model and provider: + litellm_model = f"{provider}/{litellm_model}" + kw["model"] = litellm_model + # 自建/兼容端点:builtin 模式下仍允许覆盖 base_url(如自托管的 openai 网关) + if api_base: + kw["api_base"] = api_base + kw.update(kwargs) return {k: v for k, v in kw.items() if v is not None} diff --git a/src/schemas/config.py b/src/schemas/config.py index 543b25c..c57ed22 100644 --- a/src/schemas/config.py +++ b/src/schemas/config.py @@ -29,6 +29,8 @@ class LLMConfigUpdate(BaseModel): LLM_model: Optional[str] = None LLM_baseurl: Optional[str] = None type: Optional[str] = None + # 自定义 Provider 的 API 协议格式:openai | anthropic + LLM_format: Optional[str] = None class CodeAgentConfigUpdate(BaseModel): diff --git a/src/services/config_service.py b/src/services/config_service.py index f10efbe..23b6d22 100644 --- a/src/services/config_service.py +++ b/src/services/config_service.py @@ -113,6 +113,7 @@ def get_llm_runtime_config() -> Optional[LLMConfig]: model=model, type=tp, base_url=cfg.get("LLM_baseurl") or None, + llm_format=(cfg.get("LLM_format") or "").strip().lower() or None, )