Skip to content

feat(chat): wire chat role into ProviderResolver.resolve_with_fallback (#55) - #75

Open
lunnynight wants to merge 1 commit into
2233admin:mainfrom
lunnynight:feat/chat-failover-wiring-55
Open

feat(chat): wire chat role into ProviderResolver.resolve_with_fallback (#55)#75
lunnynight wants to merge 1 commit into
2233admin:mainfrom
lunnynight:feat/chat-failover-wiring-55

Conversation

@lunnynight

Copy link
Copy Markdown
Contributor

Closes #55 — 选择「接入路径」:把 chat role 接入 ProviderResolver.resolve_with_fallback()

做了什么

  • chat 端点:当 model_defaults.candidateschat role 配置了候选时,走 resolve_with_fallback() —— 连接级失败(connect error/timeout/5xx,decision feat(model-provider): provider management (GOAL-6) #7)按顺序故障转移到下一候选;业务级失败(4xx)立即重抛不转移。显式 provider_id 与无候选 role 保持原有单 provider 行为(兼容既有安装)。
  • 工具循环(JSON + XML):LLM 调用失败改为抛 LlmAdapterErrorclassify_retryable 区分),由 resolver 决定是否转移;502 转换收敛在端点边界。
  • resolverresolve()/resolve_with_fallback() 跳过 enabled=False 的 provider(治理选择,非活性故障,不进 cooldown);新增公开 has_candidates() 供 legacy 回退分支使用。
  • 测试:resolver 单测(disabled-skip、has_candidates)+ 新增 tests/integration/test_chat_failover_api.py,通过真实 POST /api/v1/chat 链路验证:legacy 路径、可重试故障转移、4xx 不转移、全部不可用 502、disabled 跳过、显式 provider 绕过。
  • 顺带修复 test_resolver.py 并发测试的 patch 泄漏(asyncio.gather 内逐任务 unittest.mock.patch 围绕 await 导致属性还原竞争,把 mock 泄漏到后续测试)。

验证证据

  • 全量后端测试:2692 passed / 0 failed(含新增 11 个测试;本机需 pip install socksio 消除环境性 flaky,见下)。
  • mypy 变更文件通过;ruff 无新增错误(仅存量 E501/UP045)。
  • 真实后端 API 验收(fresh sqlite + 本地 fake OpenAI 服务器):provider 创建/编辑/删除、POST /{id}/test 连接测试 ok:true、模型 sync 发现 2 个模型、PUT/GET /model-defaults 默认顺序持久化、chat 错误反馈(SSRF guard 502、无 provider 400、非法 role/未注册 model 400)。

遗留(需人工)

  • 模型供应商 UI 的真机验收(创建/编辑/删除/同步/连接测试/默认顺序/错误提示的浏览器走查)——需要真实后端 + 浏览器,本轮以 API 级验收替代。
  • 环境性发现:本机 host 有 Clash 代理(127.0.0.1:7897),沙箱偶发注入 ALL_PROXY=socks5://...,httpx 0.28.1 在 AsyncClient() 构造时急切构建 SOCKS transport → 无 socksio 时抛 ImportError,导致一批真实 HTTP 测试偶发失败(与本次改动无关,tests/integration 单独跑不包含本 PR 文件时同样复现 17 个)。建议 dev 依赖补 httpx[socks]

2233admin#55)

The failover resolver (PR-D) had no production caller: chat.py still picked
the first enabled provider directly, so model_defaults.candidates order and
cooldown were dead UI surface.

- chat endpoint: when the chat role has model_defaults.candidates, run
  through resolve_with_fallback() — connection-level failures (connect
  error/timeout/5xx, decision 2233admin#7) fail over to the next candidate, business
  failures (4xx) re-raise immediately. Explicit provider_id and roles with
  no candidates keep the legacy single-provider path unchanged.
- tool loops (JSON + XML) now raise LlmAdapterError with classify_retryable
  instead of a bare 502, so the resolver can distinguish failover-worthy
  failures; 502 conversion stays at the endpoint boundary.
- resolver: skip disabled providers in resolve()/resolve_with_fallback()
  (governance choice, not liveness failure — no cooldown), add public
  has_candidates() for the legacy fallback branch.
- tests: unit coverage for disabled-skip + has_candidates; new
  tests/integration/test_chat_failover_api.py exercises the production
  chain through POST /api/v1/chat (legacy path, retryable failover,
  business-error no-failover, all-unavailable 502, disabled-skip,
  explicit-provider bypass).
- fix latent patch leak in test_resolver concurrency test (per-task
  unittest.mock.patch around an await races under asyncio.gather and leaked
  a get_adapter mock into later tests).
@repowise-bot

repowise-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

✅ Health of changed files: 6.3 → 6.8 (+0.6)
🚨 Change risk: high, riskier than 78% of this repo's commits.

📋 At a glance
3 hotspots touched · 3 new findings introduced · 1 file with recent fix history · 1 dead-code finding.

Files & modules (2)
  • backend (2 files)
    • backend/llm/resolver.py
    • .../v1/chat.py
  • tests (1 file)
    • .../llm/test_resolver.py

✅ Health gate: passed

📌 Before you merge

  • Run tests/integration/test_chat_api.py, tests/skills/test_loop.py, .../llm/test_pr_e_consumers.py: they import the changed files
🔎 More signals (3)

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (2 with dependents)"]
    f_backend_api_v1_chat_py[".../v1/chat.py 🔥"]:::changed
    f_backend_llm_resolver_py["backend/llm/resolver.py 🔥"]:::changed
  end
  f_backend_api_v1___init___py[".../v1/__init__.py"]
  f_backend_api_v1_chat_py --> f_backend_api_v1___init___py
  f_backend_llm___init___py["backend/llm/__init__.py"]
  f_backend_llm_resolver_py --> f_backend_llm___init___py
  t_tests_integration_test_chat_api_py(["✅ tests/integration/test_chat_api.py"]):::guard
  t_tests_integration_test_chat_api_py -.-> f_backend_api_v1_chat_py
  t_tests_unit_llm_test_pr_e_consumers_py(["✅ .../llm/test_pr_e_consumers.py"]):::guard
  t_tests_unit_llm_test_pr_e_consumers_py -.-> f_backend_llm_resolver_py
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (2 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🔥 Hotspots touched (3)

  • .../llm/test_resolver.py: 2 commits/90d, 1 dependents · primary owner: Curry (99%)
  • backend/llm/resolver.py: 2 commits/90d, 3 dependents · primary owner: Curry (99%)
  • .../v1/chat.py: 7 commits/90d, 4 dependents · primary owner: Curry (98%)

💀 Dead code (1 finding)

  • 💀 .../v1/chat.py _optional_request_identity (confidence 0.65)

👀 Suggested reviewers @2233admin


📊 See the full report for this PR
Your repo map with this PR's blast radius lit up, every caller of the contracts it changes, and health before and after. No sign-in. · ⭐ Star Repowise · 📥 Install bot · Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot · Updated 2026-08-08 21:54 UTC

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added automatic provider failover for chat requests when configured.
    • Disabled providers are now skipped during provider selection.
    • Explicit provider selections continue to use only the selected provider.
  • Bug Fixes

    • Improved handling of retryable and non-retryable AI service errors.
    • Preserved clear errors when no usable providers are available.
    • Standardized error handling for tool-assisted chat responses.

Walkthrough

The chat endpoint now supports resolver-based provider failover with classified LLM errors. Disabled providers are skipped without adapter creation or cooldown state. Integration and unit tests cover legacy routing, failover, error handling, and explicit provider selection.

Changes

Chat provider failover

Layer / File(s) Summary
Resolver candidate handling
backend/llm/resolver.py, tests/unit/llm/test_resolver.py
ProviderResolver detects configured candidates, excludes disabled providers, and skips them without adapter creation or cooldown state.
Chat failover orchestration
backend/api/v1/chat.py
The shared tool loop now propagates classified LLM errors. /chat applies ordered failover while preserving explicit-provider and legacy behavior. XML handling uses the same error propagation.
Production chat-path validation
tests/integration/test_chat_failover_api.py
Integration tests cover legacy routing, retryable and business failures, disabled candidates, exhausted candidates, and explicit provider selection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatEndpoint
  participant ProviderResolver
  participant ChatClient
  ChatEndpoint->>ProviderResolver: Resolve chat candidates
  ProviderResolver->>ChatClient: Create adapter for enabled candidate
  ChatEndpoint->>ChatClient: Run shared tool loop
  ChatClient-->>ChatEndpoint: Response or classified LLM error
  ChatEndpoint->>ProviderResolver: Try next candidate after retryable failure
Loading

Possibly related PRs

Suggested reviewers: 2233admin

Poem

I hop through providers, one by one,
Retryable errors meet the sun.
Disabled paths stay out of sight,
Tools keep looping clean and light.
Explicit picks remain my way—
A carrot for every successful day! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: wiring the chat role into ProviderResolver fallback resolution.
Description check ✅ Passed The description explains the chat failover behavior, resolver changes, error handling, tests, and validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from 2233admin August 8, 2026 21:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/api/v1/chat.py`:
- Line 445: Remove the hard-coded "gpt-4o-mini" fallback from the legacy model
selection at backend/api/v1/chat.py lines 445-445 and resolve missing
provider.default_model through the Codex/OMX runtime, or reject missing
configuration. Apply the same runtime resolution to candidate failover at
backend/api/v1/chat.py lines 473-473, ensuring both paths avoid hard-coded model
IDs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 602717ea-6110-4602-accc-776c34c466c7

📥 Commits

Reviewing files that changed from the base of the PR and between 94ab53d and a1307cd.

📒 Files selected for processing (4)
  • backend/api/v1/chat.py
  • backend/llm/resolver.py
  • tests/integration/test_chat_failover_api.py
  • tests/unit/llm/test_resolver.py

Comment thread backend/api/v1/chat.py
"""
provider = await _pick_provider(db, provider_id)
client = await _build_client(provider)
model = provider.default_model or "gpt-4o-mini"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Remove the hard-coded provider model fallback.

Both paths select "gpt-4o-mini" when configuration has no model ID. This bypasses the required Codex/OMX runtime model resolution.

  • backend/api/v1/chat.py#L445-L445: resolve the legacy-path fallback through the runtime, or reject missing model configuration.
  • backend/api/v1/chat.py#L473-L473: use the same runtime resolution for candidate failover.

As per coding guidelines, “Never hardcode configured model IDs in application code, scripts, or committed runtime configuration; resolve them through the Codex/OMX runtime.”

📍 Affects 1 file
  • backend/api/v1/chat.py#L445-L445 (this comment)
  • backend/api/v1/chat.py#L473-L473
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/v1/chat.py` at line 445, Remove the hard-coded "gpt-4o-mini"
fallback from the legacy model selection at backend/api/v1/chat.py lines 445-445
and resolve missing provider.default_model through the Codex/OMX runtime, or
reject missing configuration. Apply the same runtime resolution to candidate
failover at backend/api/v1/chat.py lines 473-473, ensuring both paths avoid
hard-coded model IDs.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model providers: close runtime failover and live UI acceptance gaps

1 participant