Skip to content

feat: add group-based refusal fallback routing - #6171

Draft
Faye-labs wants to merge 2 commits into
QuantumNous:mainfrom
Faye-labs:feature/refusal-fallback-routing
Draft

feat: add group-based refusal fallback routing#6171
Faye-labs wants to merge 2 commits into
QuantumNous:mainfrom
Faye-labs:feature/refusal-fallback-routing

Conversation

@Faye-labs

@Faye-labs Faye-labs commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 本 PR 使用 Codex 辅助完成代码实现、测试补充与描述整理。以下说明已按实际代码路径和验证结果重新核对;没有将未经核对的生成内容作为结论。

📝 变更描述 / Description

新增可配置的 refusal fallback 路由,用于上游完整返回 stop_reason=refusal 后,将后续重试临时切换到一个备用分组:

  • 按令牌、模型、原令牌分组和规则属性生成稳定缓存键,不依赖请求 metadata 中可能变化的亲和身份。
  • 备用分组继续使用 New API 原生的渠道优先级、权重、可用性检查和重试逻辑,不绑定具体渠道 ID。
  • 冷却期使用固定 TTL;并发 refusal 通过 set-if-absent 激活,不会从最后一个并发响应重新计时。到期后下一次请求自动回探原分组,再次 refusal 时重新激活。
  • 备用分组只覆盖内部选路,ContextKeyUsingGroup 保持原值,因此普通明确分组的计费语义不变,备用渠道也不会覆盖原有渠道亲和记录。
  • auto 来源分组当前明确跳过 fallback,避免在没有具体 auto_group 的情况下错误回落到 1 倍计费;fallback 分组本身也不能配置为 auto
  • 支持按模型正则、请求路径和来源分组配置有序规则;Default 与 Classic Admin 均提供配置界面。启用时先保存并校验规则,成功后再打开开关。
  • refusal 检测通过 provider-neutral context signal 与路由策略解耦,目前由 Claude relay 写入,后续 provider 可复用该信号接入。

当前发生 refusal 的 HTTP 请求不会在 New API 内部自动重放;本功能作用于其后的客户端或上层服务重试请求。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • N/A

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 已搜索现有 Issues 与 PRs,未发现同类 refusal fallback 提交。
  • Bug fix 说明: 本 PR 为新功能,不标记为 Bug fix。
  • 变更理解: 已核对路由、计费、亲和、缓存和 Admin 保存路径及其可能影响。
  • 范围聚焦: 本 PR 未包含与当前功能无关的代码改动。
  • 本地验证: 已运行下列定向测试、全仓编译检查和前端检查。
  • 安全合规: 代码中无敏感凭据、真实用户数据或生产派生 fixture。

📸 运行证明 / Proof of Work

Go:

go test ./pkg/cachex -run TestSetIfAbsentWithTTL -count=10
go test ./setting/operation_setting -run RefusalFallback -count=10
go test ./service -run 'RefusalFallback|ResolveRoutingGroup' -count=10
go test ./middleware ./relay/channel/claude ./controller -count=1
go test ./... -run '^$'

Default Admin:

oxfmt --check(本次变更文件)
oxlint(本次变更文件)
tsgo -b
production build

Classic Admin:

Prettier check(本次变更文件)
ESLint(本次变更文件)
production build

其他:

git diff --check
locale JSON parse / key sync check
official Dockerfile build
isolated container startup and /api/status smoke check

Summary by CodeRabbit

  • New Features
    • Added configurable refusal fallback routing after upstream refusals, with rule matching, cooldowns, fallback groups, and primary-route recovery.
    • Added settings interfaces to create, edit, delete, enable, and validate fallback rules.
    • Added multilingual labels, guidance, and validation messages.
  • Bug Fixes
    • Improved channel selection to respect fallback and affinity availability.
    • Preserved original billing-group context while applying routing overrides.
    • Prevented cooldowns from being overwritten or unintentionally extended during concurrent requests.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds configurable refusal-fallback rules, validates and stores cooldown state, detects upstream refusals, routes requests through fallback groups, and exposes configuration in both settings interfaces with localized text.

Changes

Refusal fallback routing

Layer / File(s) Summary
Configuration and validation
constant/context_key.go, setting/operation_setting/*, controller/option.go
Registers refusal-fallback settings, validates rule names, groups, cooldowns, and regexes, and adds routing context keys.
Fallback state and cooldown
pkg/cachex/*, service/refusal_fallback*
Adds atomic cache insertion with TTL, rule matching, deterministic scopes, cooldown activation, metadata, and coverage for isolation and expiration.
Provider and channel routing integration
relay/channel/claude/*, service/channel_select.go, middleware/distributor.go, controller/relay.go, service/log_info_generate.go
Propagates upstream refusal signals, resolves routing-group overrides, selects fallback or usable affinity channels, and records fallback information.
Classic settings interface
web/classic/src/components/settings/*, web/classic/src/pages/Setting/Operation/*, web/classic/src/i18n/locales/*
Adds refusal-fallback rule editing, validation, persistence, display, and localized strings.
Default settings interface
web/default/src/features/system-settings/*, web/default/src/features/models/*, web/default/src/i18n/locales/*
Adds typed defaults, section registration, rule editing and persistence, and localized strings.
Estimated code review effort: 4 (Complex) ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeRelay
  participant Distributor
  participant RefusalFallback
  participant HybridCache
  participant ChannelSelector
  ClaudeRelay->>Distributor: Set upstream refusal context
  Distributor->>RefusalFallback: GetRefusalFallbackGroup
  RefusalFallback->>HybridCache: Check active cooldown
  HybridCache-->>RefusalFallback: Fallback group state
  RefusalFallback-->>Distributor: Return routing group
  Distributor->>ChannelSelector: Select satisfied fallback channel
  ChannelSelector-->>Distributor: Selected channel
  Distributor->>RefusalFallback: ObserveRefusalFallback
  RefusalFallback->>HybridCache: Set cooldown if absent
Loading

Possibly related PRs

Poem

A rabbit hops where refusals grew,
Finds a fallback path bright and new.
Cooldowns guard the route with care,
While settings bloom everywhere.
“Binky!” says the bunny—routing’s true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding group-based refusal fallback routing.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
middleware/distributor.go (1)

107-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silently swallows the fallback-channel-selection error.

When CacheGetRandomSatisfiedChannel fails for the fallback group, the original err is discarded (err = nil) with no log before falling through to affinity/normal selection. If the fallback group is misconfigured or has no available channels, operators get no signal — the request just quietly proceeds through the other paths. Consider logging the discarded error (e.g., via common.SysLog) before clearing it, mirroring the common.SysError logging already used elsewhere in this cohort (e.g., in service/refusal_fallback.go's cache-error paths).

🔍 Suggested diagnostic logging
 				} else {
+					if err != nil {
+						common.SysLog(fmt.Sprintf("refusal fallback channel selection failed: group=%s err=%v", fallbackGroup, err))
+					}
 					service.ClearCurrentRefusalFallback(c)
 					channel = nil
 					err = nil
 				}
🤖 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 `@middleware/distributor.go` around lines 107 - 123, In the fallback-selection
failure branch of the distributor flow, log the non-nil error returned by
CacheGetRandomSatisfiedChannel before clearing it and continuing. Use the
existing common.SysLog-style diagnostic pattern established in nearby
refusal-fallback cache-error paths, while preserving the current cleanup and
fallback behavior.
service/refusal_fallback_test.go (1)

49-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

All three new test files use require exclusively, with no assert for non-fatal value checks. As per path instructions for **/*_test.go, "New or substantially rewritten Go backend tests must use require for setup and fatal assertions and assert for non-fatal value checks." Every assertion here aborts the test on first failure, so a single wrong value hides any other assertion failures in the same test run — reducing CI diagnostic signal uniformly across this cohort's test additions.

  • service/refusal_fallback_test.go#L49-L146: keep require for context/setting setup and pointer/error preconditions; switch the outcome checks (e.g. require.False(t, active), require.Equal(t, "claude-fallback", fallbackGroup), require.True(t, ClearCurrentRefusalFallback(retry))) to assert.
  • pkg/cachex/hybrid_cache_test.go#L11-L36: keep require.NoError for setup/error preconditions; switch require.True(t, created), require.False(t, created), require.True(t, found), require.Equal(t, "first", value), require.False(t, found) to assert.
  • relay/channel/claude/relay_claude_test.go#L21-L35: switch require.True(...), require.Equal(...), require.False(...) value checks to assert, keeping require only if a subsequent assertion in the same test would be meaningless after a failure.
🤖 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 `@service/refusal_fallback_test.go` around lines 49 - 146, Replace non-fatal
outcome checks with assert while retaining require for setup, error, and pointer
preconditions. In service/refusal_fallback_test.go lines 49-146, update value
checks around
TestRefusalFallbackUsesStableTokenScopeAcrossAffinityIdentityChanges,
TestRefusalFallbackDoesNotLeakAcrossScope,
TestRefusalFallbackSkipsAutoSourceGroupEvenWhenRuleMatchesAllGroups,
TestShouldActivateRefusalFallbackKeepsFixedCooldownWindow, and
TestResolveRoutingGroupDoesNotChangeBillingGroup; make the same
require-to-assert changes in pkg/cachex/hybrid_cache_test.go lines 11-36 and
relay/channel/claude/relay_claude_test.go lines 21-35, keeping require only
where later assertions depend on successful setup.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@middleware/distributor.go`:
- Around line 107-123: In the fallback-selection failure branch of the
distributor flow, log the non-nil error returned by
CacheGetRandomSatisfiedChannel before clearing it and continuing. Use the
existing common.SysLog-style diagnostic pattern established in nearby
refusal-fallback cache-error paths, while preserving the current cleanup and
fallback behavior.

In `@service/refusal_fallback_test.go`:
- Around line 49-146: Replace non-fatal outcome checks with assert while
retaining require for setup, error, and pointer preconditions. In
service/refusal_fallback_test.go lines 49-146, update value checks around
TestRefusalFallbackUsesStableTokenScopeAcrossAffinityIdentityChanges,
TestRefusalFallbackDoesNotLeakAcrossScope,
TestRefusalFallbackSkipsAutoSourceGroupEvenWhenRuleMatchesAllGroups,
TestShouldActivateRefusalFallbackKeepsFixedCooldownWindow, and
TestResolveRoutingGroupDoesNotChangeBillingGroup; make the same
require-to-assert changes in pkg/cachex/hybrid_cache_test.go lines 11-36 and
relay/channel/claude/relay_claude_test.go lines 21-35, keeping require only
where later assertions depend on successful setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7697c96-f482-4dc7-a5a3-9453c54faa08

📥 Commits

Reviewing files that changed from the base of the PR and between 7c28993 and e98164d.

📒 Files selected for processing (36)
  • constant/context_key.go
  • controller/option.go
  • controller/relay.go
  • middleware/distributor.go
  • pkg/cachex/hybrid_cache.go
  • pkg/cachex/hybrid_cache_test.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go
  • service/channel_select.go
  • service/log_info_generate.go
  • service/refusal_fallback.go
  • service/refusal_fallback_test.go
  • setting/operation_setting/refusal_fallback_setting.go
  • setting/operation_setting/refusal_fallback_setting_test.go
  • web/classic/src/components/settings/ModelSetting.jsx
  • web/classic/src/i18n/locales/en.json
  • web/classic/src/i18n/locales/fr.json
  • web/classic/src/i18n/locales/ja.json
  • web/classic/src/i18n/locales/ru.json
  • web/classic/src/i18n/locales/vi.json
  • web/classic/src/i18n/locales/zh-CN.json
  • web/classic/src/i18n/locales/zh-TW.json
  • web/classic/src/pages/Setting/Operation/SettingsRefusalFallback.jsx
  • web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/default/src/features/system-settings/models/index.tsx
  • web/default/src/features/system-settings/models/refusal-fallback-section.tsx
  • web/default/src/features/system-settings/models/section-registry.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/i18n/locales/_reports/_sync-report.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

@Calcium-Ion
Calcium-Ion force-pushed the main branch 2 times, most recently from 51fdfc5 to 2b6f1df Compare August 30, 2026 15:03
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.

1 participant