Skip to content

fix: auto 分组跨分组重试用优先级数量替代 RetryTimes 判断跳组 - #4226

Closed
zuiho-kai wants to merge 4 commits into
QuantumNous:mainfrom
zuiho-kai:fix/cross-group-retry
Closed

fix: auto 分组跨分组重试用优先级数量替代 RetryTimes 判断跳组#4226
zuiho-kai wants to merge 4 commits into
QuantumNous:mainfrom
zuiho-kai:fix/cross-group-retry

Conversation

@zuiho-kai

@zuiho-kai zuiho-kai commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

修复 auto 分组「跨分组重试」不生效的问题。

原逻辑用全局 RetryTimes 判断是否该跳组:

if crossGroupRetry && priorityRetry >= common.RetryTimes {

GetRandomSatisfiedChannelretry >= len(uniquePriorities) 时会 clamp 到最后一个优先级,仍然返回 channel。当分组内该模型的优先级数量远小于 RetryTimes 时,所有重试都耗在同一个分组反复选同一个渠道,永远不会触发跳组。

修复方案:

  1. model/channel_cache.go — 新增 GetGroupModelPriorityCount 函数,查询分组内某模型的实际优先级数量
  2. service/channel_select.go — 用实际优先级数量替代 RetryTimes 判断跳组时机:每个优先级用完后立刻跳到下一个分组

修复后行为:如果分组内只有 1 个优先级(如只有 1 个渠道有该模型),失败 1 次就跳到下一个分组;如果有 3 个优先级,用完 3 个优先级后跳组。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 Issues 与 PRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

场景验证(分组内只有 1 个渠道有该模型,priorityCount=1):

修复前:3->3->3->3->3->3->3->3(7 次重试全打在同一个渠道)
修复后:3->下一分组渠道->...(失败 1 次即跳组)

Summary by CodeRabbit

  • New Features

    • Retries now can exclude previously used channels so subsequent attempts avoid the same channels.
    • Added a way to force-update channel affinity after a successful relay to improve future routing.
  • Refactor

    • Improved cross-group retry behavior: group switching uses per-group priority availability rather than a fixed retry limit.
    • Pre-checks skip groups whose priorities are exhausted, preserving global retry state.
  • Bug Fixes

    • Affinity recording now respects a final channel selection when present, reducing incorrect affinity assignments.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Added channel-exclusion support and a group-model priority-count API to channel cache; reworked auto cross-group retry logic to use per-group priority counts and preserve global retry state; propagate excluded channel IDs through selection and use_channel tracking; added forced affinity update API and context-aware affinity recording.

Changes

Cohort / File(s) Summary
Channel cache & priority API
model/channel_cache.go
Added GetGroupModelPriorityCount(group, modelName) int. GetRandomSatisfiedChannel now accepts excludeIDs ...int, builds an exclude set, skips excluded channels in fast path, unique-priority computation, and weighted selection; returns (nil, nil) if all candidates excluded.
Channel selection & retry params
service/channel_select.go
Added ExcludeChannelIDs []int to RetryParam; compute priorityRetry = param.GetRetry() - startRetryIndex using ContextKeyAutoGroupRetryIndex; pre-skip exhausted groups by calling GetGroupModelPriorityCount; update auto-group index/retry-context based on per-group priorityCount rather than fixed RetryTimes; preserve global retry state when no channel found.
Relay controller retry exclusion
controller/relay.go
Added getUsedChannelIDs(c *gin.Context) to parse use_channel into ints; set retryParam.ExcludeChannelIDs before selection so retries exclude already-used channels; call service.ForceUpdateChannelAffinity on success when multiple used channels present.
Channel affinity APIs & recording
service/channel_affinity.go, middleware/distributor.go
Added ForceUpdateChannelAffinity(c, successChannelID) to overwrite affinity cache entry with TTL and logging; Distribute() now reads finalChannelID from context (ContextKeyChannelId) and falls back to channel.Id before calling service.RecordChannelAffinity.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Relay
    participant Selector
    participant Cache
    participant Channels

    Client->>Relay: Request(model, params, context)
    Relay->>Selector: Build RetryParam (Include ExcludeChannelIDs from use_channel)
    Selector->>Cache: GetGroupModelPriorityCount(group, model)
    Cache-->>Selector: priorityCount
    Selector->>Channels: GetRandomSatisfiedChannel(group, model, priorityRetry, excludeIDs...)
    Channels-->>Selector: channel / nil
    alt channel returned
        Selector->>Relay: channel
        Relay->>service: (on success) ForceUpdateChannelAffinity if needed
        Relay->>Client: response
    else nil returned
        Selector->>Selector: update ContextKeyAutoGroupIndex / ContextKeyAutoGroupRetryIndex (per-group logic)
        Selector->>Channels: try next group...
        Selector->>Client: channel / error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through groups and count priority clay,
Excluding old burrows as I bound on my way.
Retries now leap when a group runs dry,
Affinity updated beneath moonlit sky,
Hooray — more hops, less stuck, happy day! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: replacing RetryTimes with priority count for auto-group cross-group retry switching logic.
Linked Issues check ✅ Passed All code changes directly address issue #4225 requirements: GetGroupModelPriorityCount queries actual priorities, and channel_select.go replaces RetryTimes threshold with actual priority count for cross-group switching.
Out of Scope Changes check ✅ Passed All changes support the core fix or enhance retry behavior consistently: excludeIDs prevents re-attempting failed channels, ForceUpdateChannelAffinity records successful attempts, and distributor reads final channel ID from context.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
model/channel_cache.go (1)

201-215: Consider sharing this priority-resolution logic with GetRandomSatisfiedChannel.

This helper now duplicates the same model fallback and unique-priority scan from GetRandomSatisfiedChannel (Lines 105-142). Since the new cross-group switch threshold has to stay identical to the actual selection path, extracting a private shared helper would reduce the risk of those two behaviors drifting apart later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/channel_cache.go` around lines 201 - 215, The priority-resolution code
in this block duplicates the model-name fallback and unique-priority counting
logic used by GetRandomSatisfiedChannel; extract that logic into a single
private helper (e.g., computeUniquePriorityCount or getUniquePrioritiesForModel)
that takes the group, modelName, group2model2channels, and channelsIDM, performs
the normalized model fallback (using ratio_setting.FormatMatchingModelName),
iterates channels to build the uniquePriorities map from channel.GetPriority(),
and returns the count; replace the duplicated code in both this function and
GetRandomSatisfiedChannel with calls to the new helper so both paths share
identical behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@service/channel_select.go`:
- Around line 117-126: The code currently calls param.SetRetry(0) when switching
auto-groups which mutates the global Retry counter (RetryParam.Retry) used by
controller/relay.go and can exceed the global retry budget; instead, stop
resetting Retry, store the group's starting retry index in context via
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupRetryIndex,
param.GetRetry()) and keep setting ContextKeyAutoGroupIndex as now; then compute
priorityRetry as param.GetRetry() - startRetryIndex when checking against
model.GetGroupModelPriorityCount(autoGroup, param.ModelName). Apply the same
change to the other group-transition branch (the channel == nil branch) so no
code resets the global retry counter and all group transitions preserve the
outer retry budget.

---

Nitpick comments:
In `@model/channel_cache.go`:
- Around line 201-215: The priority-resolution code in this block duplicates the
model-name fallback and unique-priority counting logic used by
GetRandomSatisfiedChannel; extract that logic into a single private helper
(e.g., computeUniquePriorityCount or getUniquePrioritiesForModel) that takes the
group, modelName, group2model2channels, and channelsIDM, performs the normalized
model fallback (using ratio_setting.FormatMatchingModelName), iterates channels
to build the uniquePriorities map from channel.GetPriority(), and returns the
count; replace the duplicated code in both this function and
GetRandomSatisfiedChannel with calls to the new helper so both paths share
identical behavior.
🪄 Autofix (Beta)

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

Run ID: 168b33ad-7fae-4062-8213-88d369053937

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8661d and 0a5a556.

📒 Files selected for processing (2)
  • model/channel_cache.go
  • service/channel_select.go

Comment thread service/channel_select.go
按 coderabbitai review 建议修改:
- 移除所有 param.SetRetry(0),全局 Retry 始终递增
- 跳组时将当前 Retry 存入 ContextKeyAutoGroupRetryIndex
- priorityRetry = param.GetRetry() - startRetryIndex
- 移除不再需要的 ResetRetryNextTry() 调用
- GetRandomSatisfiedChannel 加 excludeIDs variadic 参数,retry 时跳过已失败渠道
- RetryParam 新增 ExcludeChannelIDs,relay retry loop 每次迭代传入已用渠道列表
- 新增 ForceUpdateChannelAffinity,retry 成功时主动把亲和缓存更新到成功渠道
- distributor c.Next() 后从 context 读最终 channel ID,避免用初始渠道覆盖亲和缓存

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/relay.go`:
- Line 192: Relay() correctly sets retryParam.ExcludeChannelIDs =
getUsedChannelIDs(c) but RelayTask() still uses an empty ExcludeChannelIDs when
retrying, so update RelayTask (or the retry loop that calls it) to populate
RetryParam.ExcludeChannelIDs with the same getUsedChannelIDs(c) value before
retrying; specifically locate the RelayTask function and the retry loop that
appends "use_channel" and ensure the RetryParam passed into or constructed
inside RelayTask carries the getUsedChannelIDs(c) exclusion list so retries
honor the same excluded channels.

In `@service/channel_select.go`:
- Around line 122-126: The retry/exhaustion logic uses the original priority
count and priorityRetry which can be wrong after ExcludeChannelIDs filtering;
update the code so that the exhaustion check and the index math use the
post-exclusion priority set produced by model.GetRandomSatisfiedChannel (the
uniquePriorities rebuilt after ExcludeChannelIDs) — i.e., obtain the filtered
priority list/count (or determine the target priority before applying
exclusions) and then compare priorityRetry against that filtered count and
compute the selected priority from that filtered list; apply the same change for
the other occurrences that use priorityRetry/GetGroupModelPriorityCount (the
blocks around the other matches you noted) so all checks use the post-filtered
uniquePriorities instead of the unfiltered GetGroupModelPriorityCount.
🪄 Autofix (Beta)

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

Run ID: abdfb03e-344c-4fef-ad36-83be04ef2b9a

📥 Commits

Reviewing files that changed from the base of the PR and between 37e0c52 and 2d1c3d0.

📒 Files selected for processing (5)
  • controller/relay.go
  • middleware/distributor.go
  • model/channel_cache.go
  • service/channel_affinity.go
  • service/channel_select.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • model/channel_cache.go

Comment thread controller/relay.go

for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() {
relayInfo.RetryIndex = retryParam.GetRetry()
retryParam.ExcludeChannelIDs = getUsedChannelIDs(c)

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.

⚠️ Potential issue | 🟠 Major

The exclusion list is only applied to one of the two retry loops.

Relay() now forwards getUsedChannelIDs(c), but RelayTask() below still retries with an empty RetryParam.ExcludeChannelIDs even though it also appends to "use_channel". That means task submissions can keep bouncing back to the same failed channel, so this retry fix is only partially wired in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/relay.go` at line 192, Relay() correctly sets
retryParam.ExcludeChannelIDs = getUsedChannelIDs(c) but RelayTask() still uses
an empty ExcludeChannelIDs when retrying, so update RelayTask (or the retry loop
that calls it) to populate RetryParam.ExcludeChannelIDs with the same
getUsedChannelIDs(c) value before retrying; specifically locate the RelayTask
function and the retry loop that appends "use_channel" and ensure the RetryParam
passed into or constructed inside RelayTask carries the getUsedChannelIDs(c)
exclusion list so retries honor the same excluded channels.

Comment thread service/channel_select.go
Comment on lines +122 to +126
// 跨分组重试时,检查当前分组的优先级是否已耗尽
// 如果 priorityRetry >= 该分组的优先级数量,说明已经没有新的优先级可用,应跳到下一个分组
if crossGroupRetry && priorityRetry > 0 {
priorityCount := model.GetGroupModelPriorityCount(autoGroup, param.ModelName)
if priorityCount > 0 && priorityRetry >= priorityCount {

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.

⚠️ Potential issue | 🟠 Major

Excluded channels can shift retries onto the wrong priority.

model.GetRandomSatisfiedChannel rebuilds uniquePriorities after applying ExcludeChannelIDs (see model/channel_cache.go:118-151), but this code still uses the unadjusted priorityRetry and the unfiltered GetGroupModelPriorityCount(...). If the failed channel was the only member of a higher priority, the next retry can skip the next remaining priority entirely and the group-switch threshold no longer matches the actually selectable priorities. Please base both the retry index and the exhaustion check on the post-exclusion priority set, or determine the target priority before filtering excluded channel IDs.

Also applies to: 136-136, 149-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/channel_select.go` around lines 122 - 126, The retry/exhaustion logic
uses the original priority count and priorityRetry which can be wrong after
ExcludeChannelIDs filtering; update the code so that the exhaustion check and
the index math use the post-exclusion priority set produced by
model.GetRandomSatisfiedChannel (the uniquePriorities rebuilt after
ExcludeChannelIDs) — i.e., obtain the filtered priority list/count (or determine
the target priority before applying exclusions) and then compare priorityRetry
against that filtered count and compute the selected priority from that filtered
list; apply the same change for the other occurrences that use
priorityRetry/GetGroupModelPriorityCount (the blocks around the other matches
you noted) so all checks use the post-filtered uniquePriorities instead of the
unfiltered GetGroupModelPriorityCount.

@zuiho-kai zuiho-kai closed this Jul 8, 2026
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.

auto 分组跨分组重试不生效,所有重试都打在同一个渠道上

1 participant