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
2 changes: 2 additions & 0 deletions apps/dsa-web/src/components/settings/LLMChannelEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ const LLM_ERROR_LABELS: Record<string, string> = {
timeout: '请求超时',
quota: '额度或限流',
model_not_found: '模型不可用',
request_blocked: '请求被拦截',
empty_response: '空响应',
format_error: '格式异常',
network_error: '网络异常',
Expand All @@ -735,6 +736,7 @@ const LLM_REASON_HINTS: Record<string, string> = {
rate_limit: '服务商触发 RPM/TPM 或并发限流;请降低请求频率或稍后重试。',
insufficient_balance: '服务商返回余额、账单或额度不足;请检查账户余额和套餐状态。',
quota_exceeded: '服务商返回配额已耗尽;请确认账号套餐、余量和项目额度。',
provider_blocked: '请求被服务商或中转网关拦截;请检查账号风控、地域限制、模型权限、代理商网关策略、内容安全策略或请求来源限制。',
dns_error: '域名解析失败;请检查 Base URL 域名、网络代理和 DNS 配置。',
tls_error: 'TLS/证书握手失败;请检查 HTTPS 证书、中转网关或公司代理策略。',
connection_refused: '目标服务拒绝连接;请确认 Base URL 端口、服务进程和防火墙配置。',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,46 @@ describe('LLMChannelEditor', () => {
}));
});

it('shows provider blocked troubleshooting without network or model-list hints', async () => {
testLLMChannel.mockResolvedValue({
success: false,
message: 'LLM request was blocked by provider or gateway policy',
error: 'litellm.APIError: APIError: OpenAIException - Your request was blocked.',
errorCode: 'request_blocked',
stage: 'chat_completion',
retryable: false,
details: { reason: 'provider_blocked', model: 'openai/gpt-5.5' },
resolvedProtocol: 'openai',
resolvedModel: 'openai/gpt-5.5',
latencyMs: null,
});

render(
<LLMChannelEditor
items={[
{ key: 'LLM_CHANNELS', value: 'proxy' },
{ key: 'LLM_PROXY_PROTOCOL', value: 'openai' },
{ key: 'LLM_PROXY_BASE_URL', value: 'https://gateway.example.com/v1' },
{ key: 'LLM_PROXY_ENABLED', value: 'true' },
{ key: 'LLM_PROXY_API_KEY', value: 'secret-key' },
{ key: 'LLM_PROXY_MODELS', value: 'gpt-5.5,gpt-4o-mini' },
]}
configVersion="v1"
maskToken="******"
onSaved={() => {}}
/>
);

fireEvent.click(screen.getByRole('button', { name: /proxy/i }));
fireEvent.click(screen.getByRole('button', { name: '测试连接' }));

expect(await screen.findByText(/聊天调用 · 请求被拦截/i)).toBeInTheDocument();
expect(screen.getByText(/本次测试模型:openai\/gpt-5\.5/i)).toBeInTheDocument();
expect(screen.getByText(/账号风控、地域限制、模型权限/i)).toBeInTheDocument();
expect(screen.queryByText(/Base URL、代理、TLS/i)).not.toBeInTheDocument();
expect(screen.queryByText(/调整模型顺序或移除不可用模型/i)).not.toBeInTheDocument();
});

it('shows focused quota exceeded troubleshooting hints', async () => {
testLLMChannel.mockResolvedValue({
success: false,
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [改进] 放宽 LiteLLM 依赖约束,保留 `>=1.80.10` 最低版本并显式排除 PyPI 事故版本 `1.82.7` / `1.82.8`,允许安装后续 1.x 修复版本。
- [改进] 补齐通知渠道 P0 基线、Actions 映射与 `--check-notify` 只读诊断,完善 AstrBot 配置入口和通知回归快照。
- [修复] 修正 LLM 渠道测试中 `Model disabled` 被误报为网络异常的问题,并在失败提示中展示本次实际测试模型。
- [修复] 修正 LLM 渠道测试中 `Your request was blocked` 等服务商或网关拦截错误被误报为网络异常的问题。
- [chore] 清理仓库根目录:移除误入库的 `.codex`、`review.md` 跟踪记录,将 smoke 测试入口迁移到 `scripts/`、环境检查脚本迁移为 `scripts/check_env.py`,并将 LiteLLM YAML 示例迁移到 `docs/examples/`。
- [新功能] Web 设置页新增通知渠道一键测试,支持临时配置、耗时与脱敏 attempts 展示。

Expand Down
1 change: 1 addition & 0 deletions docs/LLM_CONFIG_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ LITELLM_MODEL=ollama/qwen3:8b

- 预设里的 provider / Base URL / 示例模型只用于**初始化表单**;真正落盘时仍是你当前输入的 `LLM_{CHANNEL}_PROTOCOL`、`LLM_{CHANNEL}_BASE_URL`、`LLM_{CHANNEL}_MODELS`、`LLM_{CHANNEL}_API_KEY(S)`,不会在后台偷偷改成别的 provider 名或 URL。
- 设置页的“获取模型”只对 `OpenAI Compatible` / `DeepSeek` 渠道调用 `{base_url}/models`;“测试连接”默认只对模型列表首项发起一次最小聊天请求,并在结果中展示后端规范化后的 `resolved_model`。若返回 `details.reason=model_access_denied`(例如 Issue #1208 中已观测到的 SiliconFlow / OpenAI Compatible 经 LiteLLM 返回 `Model disabled`),请把它视为基于 provider 文案的 best-effort 模型可用性诊断,优先确认该模型是否已在当前账号/key 下开通,必要时调整模型顺序或移除不可用模型后重试;未覆盖或语义不同的 provider 文案会继续走兜底诊断。可选的“运行时能力检测”必须由用户显式选择后触发,会额外发起 JSON / tools / stream / vision smoke 请求,结果仅代表当前账号、模型和 endpoint 的一次 best-effort 检测。上述检测返回的 `stage / error_code / details / latency_ms / capability_results` 仅用于结构化诊断提示,**不会写回** `.env`,也不会阻止保存。
- 若返回 `details.reason=provider_blocked`,表示服务商或中转网关明确拦截了本次请求;它区别于本地网络 / TLS 异常和 `model_access_denied`,应优先检查账号风控、地域或请求来源限制、模型权限、代理商网关策略和内容安全策略。
- 运行时能力检测会产生真实 LLM 请求,可能带来 token / 图像输入费用、RPM/TPM 限流、余额不足或超时。检测失败可能来自账号权限、模型未开通、endpoint 区域、余额、服务商兼容层或 LiteLLM 转换路径,不等于该 provider 全局不支持对应能力。P3 未对所有真实 provider 做在线 smoke;兼容依据来自当前依赖约束 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0` 下的 LiteLLM `completion()` / OpenAI I/O format / streaming / exception mapping,以及 OpenAI Chat Completions 的 JSON mode、tool calling、streaming 和 vision input 形状。
- 相关外部来源:LiteLLM Python SDK / OpenAI I/O format / streaming / exception mapping:<https://docs.litellm.ai/>;LiteLLM OpenAI-compatible 路由:<https://docs.litellm.ai/docs/providers/openai_compatible>;OpenAI Chat Completions:<https://platform.openai.com/docs/api-reference/chat/create>;JSON mode:<https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat>;tool calling:<https://platform.openai.com/docs/guides/function-calling?api-mode=chat>;streaming:<https://platform.openai.com/docs/guides/streaming-responses?api-mode=chat>;vision input:<https://platform.openai.com/docs/guides/images-vision?api-mode=chat>。
- 保存渠道时,只会更新这次提交的 key;不会因为切换渠道模式而静默迁移整个旧配置。唯一会被**同步清理**的是运行时模型引用:如果 `LITELLM_MODEL`、`AGENT_LITELLM_MODEL`、`VISION_MODEL` 或 `LITELLM_FALLBACK_MODELS` 指向了当前已启用渠道里已经不存在的模型,设置页会在保存前把这些失效引用清空/移除,避免运行时继续指向无效模型;即使当前启用渠道没有任何可选模型,也会清理缺少 legacy Key 支撑的托管 provider 旧值。`cohere/*`、`google/*`、`xai/*` 这类直连模型仅用于说明历史 `direct-env` 兼容保留语义,不等于可用性承诺,是否可用请按各厂商官方模型/API 文档再做实际验证。
Expand Down
1 change: 1 addition & 0 deletions docs/LLM_CONFIG_GUIDE_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ The backend exposes a read-only status endpoint at `GET /api/v1/system/config/se

- The preset provider / Base URL / sample models are **form defaults only**. What gets persisted is still exactly what you submit in `LLM_{CHANNEL}_PROTOCOL`, `LLM_{CHANNEL}_BASE_URL`, `LLM_{CHANNEL}_MODELS`, and `LLM_{CHANNEL}_API_KEY(S)`; the editor does not silently rewrite them to a different provider name or URL.
- "Discover models" only calls `{base_url}/models` for `OpenAI Compatible` / `DeepSeek` channels, and the default "Test connection" action sends one minimal chat completion request against the first model in the list and shows the backend-normalized `resolved_model` in the result. If the response includes `details.reason=model_access_denied` (for example, the observed Issue #1208 SiliconFlow / OpenAI Compatible sample returned `Model disabled` through LiteLLM), treat it as a best-effort model availability diagnostic based on provider wording: first confirm that the tested model is enabled for the current account/key, then adjust the model order or remove unavailable models before retrying. Provider messages not covered by this conservative rule, or provider messages with different semantics, continue to use the fallback diagnostic path. Optional runtime capability checks must be explicitly selected by the user and send additional JSON / tools / stream / vision smoke requests; the result only represents a best-effort check for the current account, model, and endpoint at that moment. The returned `stage / error_code / details / latency_ms / capability_results` fields are for structured diagnostics only, are **never persisted** back into `.env`, and do not block saving.
- If the response includes `details.reason=provider_blocked`, the provider or relay gateway explicitly blocked this request. This is distinct from local network / TLS failures and `model_access_denied`; first check account risk controls, region or request-source restrictions, model entitlement, relay gateway policy, and content-safety policy.
- Runtime capability checks send real LLM requests and may incur token / image-input cost, RPM/TPM rate limiting, insufficient balance errors, or timeouts. A failed check may come from account permissions, model entitlement, endpoint region, balance, provider compatibility layers, or LiteLLM translation behavior; it does not prove that the provider globally lacks that capability. P3 does not include online smoke coverage for every real provider. Its compatibility basis is the repository dependency constraint `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0`, LiteLLM `completion()` / OpenAI I/O format / streaming / exception mapping, and the OpenAI Chat Completions shapes for JSON mode, tool calling, streaming, and vision input.
- External references: LiteLLM Python SDK / OpenAI I/O format / streaming / exception mapping: <https://docs.litellm.ai/>; LiteLLM OpenAI-compatible routing: <https://docs.litellm.ai/docs/providers/openai_compatible>; OpenAI Chat Completions: <https://platform.openai.com/docs/api-reference/chat/create>; JSON mode: <https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat>; tool calling: <https://platform.openai.com/docs/guides/function-calling?api-mode=chat>; streaming: <https://platform.openai.com/docs/guides/streaming-responses?api-mode=chat>; vision input: <https://platform.openai.com/docs/guides/images-vision?api-mode=chat>.
- Saving channels only updates the keys submitted in that save operation; there is no whole-config silent migration when you switch channel settings. The one deliberate cleanup is runtime model references: if `LITELLM_MODEL`, `AGENT_LITELLM_MODEL`, `VISION_MODEL`, or `LITELLM_FALLBACK_MODELS` point to models that no longer exist in the currently enabled channels, the editor clears/removes those stale references before saving so runtime calls do not keep targeting invalid models. Even when enabled channels expose no selectable models, stale managed-provider values without a matching legacy key are cleaned. `cohere/*`, `google/*`, and `xai/*` are kept as explicit direct-env compatibility examples for legacy retention behavior only, and are not a runtime availability guarantee.
Expand Down
3 changes: 2 additions & 1 deletion docs/llm-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Ollama 默认 Base URL `http://127.0.0.1:11434` 主要面向本地、Docker 或
| `connection_refused` | 目标端口无服务,或本地服务未启动。 | 检查 Base URL、端口、防火墙;Ollama 确认本机或 runner 能访问服务。 |
| `endpoint_not_found` | `/models` 或 chat endpoint 路径不存在。 | 确认 Base URL 是否填到兼容入口,不要多拼或少拼厂商要求的路径。 |
| `model_access_denied` | 基于已观测 provider 文案的 best-effort 模型可用性归类:模型可能被禁用、未开通、账号不可见或当前 key 无权限访问。 | 先查看测试结果里的“本次测试模型”,在服务商控制台确认该模型已开通;必要时调整模型顺序、移除不可用模型,或点击「获取模型」核对账号可见模型。 |
| `provider_blocked` | 服务商或中转网关明确拦截了本次请求,可能来自账号风控、地域、请求来源、模型权限、代理商策略或内容安全策略。 | 先查看测试结果里的“本次测试模型”和服务商控制台日志;检查账号/项目状态、地域或来源限制、网关策略和内容安全规则,而不是优先排查 Base URL、TLS 或本地网络。 |
| `provider_prefix_mismatch` | LiteLLM provider prefix 与渠道协议不匹配。 | OpenAI-compatible 渠道通常使用 `openai/<model>`;不要把 `Qwen/...`、`deepseek-ai/...` 误当 provider prefix。 |
| `non_json` | 服务商返回非 JSON 或代理返回 HTML / 文本错误页。 | 检查 Base URL、网关路径、代理错误页和 Chat Completions 兼容入口。 |
| `null_response` | LiteLLM 没有返回可解析响应对象。 | 检查 provider 是否兼容 Chat Completions,必要时换模型或 endpoint 重试。 |
Expand All @@ -148,7 +149,7 @@ Ollama 默认 Base URL `http://127.0.0.1:11434` 主要面向本地、Docker 或
- Issue #1208 中真实脱敏样例来自 SiliconFlow / OpenAI Compatible 渠道测试,经 LiteLLM 返回 `litellm.APIError: APIError: OpenAIException - Model disabled.`。
- 线上复核记录(2026-05-06T16:21:21Z):在 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0` 约束下,本地验证环境为 Python `3.13.12`、LiteLLM `1.82.3`、Base URL `https://api.siliconflow.cn/v1`、模型 `Qwen/Qwen3-235B-A22B-Thinking-2507`。直连 SiliconFlow Chat Completions 返回 HTTP `403`,响应体为 `{"code":30003,"message":"Model disabled.","data":null}`;同一模型通过 LiteLLM `completion(model="openai/Qwen/Qwen3-235B-A22B-Thinking-2507")` 返回 `APIError: OpenAIException - Model disabled.`。

因此当前运行时把该已观测 provider `message` 作为 best-effort 模型可用性诊断,而不是把它声明为官方跨 provider 错误码。实现仅在错误文本同时包含 `model` 和明确权限、禁用或不可用信号时进入该诊断;未覆盖或语义不同的 provider 文案会继续走既有兜底诊断。
因此当前运行时把该已观测 provider `message` 作为 best-effort 模型可用性诊断,而不是把它声明为官方跨 provider 错误码。实现仅在错误文本同时包含 `model` 和明确权限、禁用或不可用信号时进入该诊断;未覆盖或语义不同的 provider 文案会继续走既有兜底诊断。`provider_blocked` 同样是基于明确拦截文案的 best-effort 诊断,用于区分服务商/网关策略拦截与本地网络、TLS 或模型不可用问题。

## 运行时能力检测边界

Expand Down
Loading
Loading