Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions backend/app/services/llm/model_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,22 @@ def resolve_litellm_model(model: ModelLike) -> str:
return explicit_model

model_id = _clean(model.model_id) or ""
if "/" in model_id:
return model_id

provider = _clean(params.get("litellm_provider") or litellm_params.get("custom_llm_provider") or model.provider)

if provider == "custom":
# litellm 没有 "custom" provider,"custom/<model>" 会被解析成未知路由
# (对可达端点也会打出错误路径)。选了"完全自定义"预设但只填裸模型名
# 时,唯一可行的路由是 OpenAI 兼容网关;模型名自带 "/" 的已在上面原样返回。
# litellm 没有 "custom" provider,"custom/<model>" 会被解析成未知路由。
# 选了「完全自定义」预设时唯一可行的路由是 OpenAI 兼容网关——模型名
# 含 "/"(如组织/模型命名 XingChenAGI/Xing4.0-29B)也必须走该路由:
# 原样透传会被 litellm 把首段当 provider 解析而报 BadRequestError(#83)。
return f"openai/{model_id}"

if provider:
return f"{provider}/{model_id}"
# 显式 provider 优先;模型名已带同前缀时去重,避免 openai/openai/...。
prefix = f"{provider}/"
return model_id if model_id.startswith(prefix) else f"{provider}/{model_id}"

# 无任何 provider 信息时,含 "/" 的模型名视为用户显式给出的 litellm 路由串
# (如 deepseek/deepseek-chat)原样返回;裸模型名同样原样返回。
return model_id


Expand Down
37 changes: 37 additions & 0 deletions backend/tests/test_llm_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,43 @@ def test_shared_model_resolver_prefers_explicit_litellm_model():
assert resolve_litellm_model(model) == "openai/deepseek-v4-flash-free"


def test_shared_model_resolver_routes_slash_namespaced_custom_model_as_openai():
"""#83:组织/模型命名(XingChenAGI/Xing4.0-29B)在 custom 预设下必须走
OpenAI 兼容路由,原样透传会被 litellm 当未知 provider 拒绝。"""
model = SimpleNamespace(
provider="custom",
model_id="XingChenAGI/Xing4.0-29B",
api_base="https://api.xingchen.example/v1",
)

assert resolve_litellm_model(model) == "openai/XingChenAGI/Xing4.0-29B"


def test_shared_model_resolver_routes_slash_namespaced_model_with_explicit_provider():
"""显式 provider(如 openai 预设 + 自定义网关)时,组织/模型命名按该 provider 路由。"""
model = SimpleNamespace(
provider="openai",
model_id="XingChenAGI/Xing4.0-29B",
api_base="https://api.xingchen.example/v1",
)

assert resolve_litellm_model(model) == "openai/XingChenAGI/Xing4.0-29B"


def test_shared_model_resolver_dedupes_provider_prefix():
"""模型名已带同 provider 前缀时不重复拼接。"""
model = SimpleNamespace(provider="openai", model_id="openai/gpt-4.1-mini", api_base=None)

assert resolve_litellm_model(model) == "openai/gpt-4.1-mini"


def test_shared_model_resolver_keeps_slash_model_id_without_any_provider():
"""无任何 provider 信息时,含 "/" 的模型名保持原样(用户显式路由串)。"""
model = SimpleNamespace(provider="", model_id="deepseek/deepseek-chat", api_base=None)

assert resolve_litellm_model(model) == "deepseek/deepseek-chat"


def test_shared_model_resolver_routes_bare_custom_provider_as_openai_compatible():
# "完全自定义"预设落库的 provider=custom + 裸模型名:litellm 没有 custom
# provider,唯一可行路由是 OpenAI 兼容网关。
Expand Down
7 changes: 7 additions & 0 deletions docs/quality/regression-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@
- 症状:`_cache_warmup_task` 非 CancelledError 异常会中断后续全部清理步骤;jieba 预热 await 无超时且 `to_thread` 不可取消,可挂死停机;整体停机无 deadline。
- 回归测试:`tests/test_shutdown_prewarm.py`(异常不外抛且留痕 / jieba 超时不挂死 / 正常与已取消路径)。owner:#73(Parent #6,修复 PR #77:`_shutdown_prewarm_tasks`)。

### D-11 含斜杠模型名的网关模型被 litellm 拒绝 — #83 ✅ 已修复关闭(2026-09-27)
- 症状:model_test 对 `XingChenAGI/Xing4.0-29B` 报 `litellm.BadRequestError: LLM Provider NOT provided`。
- 根因:`resolve_litellm_model` 对含 `/` 的 model_id 原样透传、跳过 provider 路由;「组织/模型」命名被 litellm 按首段当未知 provider 解析。影响 model_test 与分析管线全调用面。
- 边界:llm/model_resolver / 模型目录;严重度 P1;复现性 always(该命名格式)。
- 回归测试:`tests/test_llm_models.py` 四场景(custom+org/model → `openai/` 前缀、显式 provider+org/model、同前缀去重、无 provider 保持原样)。
- owner:#83。

## 三、关键流程基线(9 项)

状态标记:✅ = 2026-09-27 在 main @ 7203847 新鲜复跑通过;📋 = 现有套件覆盖、未逐项复跑(跑全量即覆盖)。
Expand Down
Loading