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, )