"frontend/src/components/vscode:/vscode.git/clone" did not exist on "b7f69844e1f8eada74167848dfa8d2456792d639"
Unverified Commit 06093d4f authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #311 from longgexx/main

添加分组级别模型路由配置功能(Anthropic平台)
parents 452fa53c 577ee161
This diff is collapsed.
package service
import "time"
import (
"strings"
"time"
)
type Group struct {
ID int64
......@@ -27,6 +30,12 @@ type Group struct {
ClaudeCodeOnly bool
FallbackGroupID *int64
// 模型路由配置
// key: 模型匹配模式(支持 * 通配符,如 "claude-opus-*")
// value: 优先账号 ID 列表
ModelRouting map[string][]int64
ModelRoutingEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
......@@ -90,3 +99,41 @@ func IsGroupContextValid(group *Group) bool {
}
return true
}
// GetRoutingAccountIDs 根据请求模型获取路由账号 ID 列表
// 返回匹配的优先账号 ID 列表,如果没有匹配规则则返回 nil
func (g *Group) GetRoutingAccountIDs(requestedModel string) []int64 {
if !g.ModelRoutingEnabled || len(g.ModelRouting) == 0 || requestedModel == "" {
return nil
}
// 1. 精确匹配优先
if accountIDs, ok := g.ModelRouting[requestedModel]; ok && len(accountIDs) > 0 {
return accountIDs
}
// 2. 通配符匹配(前缀匹配)
for pattern, accountIDs := range g.ModelRouting {
if matchModelPattern(pattern, requestedModel) && len(accountIDs) > 0 {
return accountIDs
}
}
return nil
}
// matchModelPattern 检查模型是否匹配模式
// 支持 * 通配符,如 "claude-opus-*" 匹配 "claude-opus-4-20250514"
func matchModelPattern(pattern, model string) bool {
if pattern == model {
return true
}
// 处理 * 通配符(仅支持末尾通配符)
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
return strings.HasPrefix(model, prefix)
}
return false
}
......@@ -545,20 +545,20 @@ func TestOpenAITokenProvider_OAuthServiceNotConfigured(t *testing.T) {
func TestOpenAITokenProvider_TTLCalculation(t *testing.T) {
tests := []struct {
name string
expiresIn time.Duration
name string
expiresIn time.Duration
}{
{
name: "far_future_expiry",
expiresIn: 1 * time.Hour,
name: "far_future_expiry",
expiresIn: 1 * time.Hour,
},
{
name: "medium_expiry",
expiresIn: 10 * time.Minute,
name: "medium_expiry",
expiresIn: 10 * time.Minute,
},
{
name: "near_expiry",
expiresIn: 6 * time.Minute,
name: "near_expiry",
expiresIn: 6 * time.Minute,
},
}
......
......@@ -110,8 +110,8 @@ func TestCompositeTokenCacheInvalidator_SkipNonOAuth(t *testing.T) {
invalidator := NewCompositeTokenCacheInvalidator(cache)
tests := []struct {
name string
account *Account
name string
account *Account
}{
{
name: "gemini_api_key",
......@@ -210,8 +210,8 @@ func TestCompositeTokenCacheInvalidator_DeleteError(t *testing.T) {
invalidator := NewCompositeTokenCacheInvalidator(cache)
tests := []struct {
name string
account *Account
name string
account *Account
}{
{
name: "openai_delete_error",
......
......@@ -276,9 +276,9 @@ func TestTokenRefreshService_RefreshWithRetry_RefreshFailed(t *testing.T) {
err := service.refreshWithRetry(context.Background(), account, refresher)
require.Error(t, err)
require.Equal(t, 0, repo.updateCalls) // 刷新失败不应更新
require.Equal(t, 0, invalidator.calls) // 刷新失败不应触发缓存失效
require.Equal(t, 1, repo.setErrorCalls) // 应设置错误状态
require.Equal(t, 0, repo.updateCalls) // 刷新失败不应更新
require.Equal(t, 0, invalidator.calls) // 刷新失败不应触发缓存失效
require.Equal(t, 1, repo.setErrorCalls) // 应设置错误状态
}
// TestTokenRefreshService_RefreshWithRetry_AntigravityRefreshFailed 测试 Antigravity 刷新失败不设置错误状态
......
-- 040_add_group_model_routing.sql
-- 添加分组级别的模型路由配置功能
-- 添加 model_routing 字段:模型路由配置(JSONB 格式)
-- 格式: {"model_pattern": [account_id1, account_id2], ...}
-- 例如: {"claude-opus-*": [1, 2], "claude-sonnet-*": [3, 4, 5]}
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS model_routing JSONB DEFAULT '{}';
-- 添加字段注释
COMMENT ON COLUMN groups.model_routing IS '模型路由配置:{"model_pattern": [account_id1, account_id2], ...},支持通配符匹配';
-- Add model_routing_enabled field to groups table
ALTER TABLE groups ADD COLUMN model_routing_enabled BOOLEAN NOT NULL DEFAULT false;
......@@ -916,6 +916,26 @@ export default {
fallbackGroup: 'Fallback Group',
fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.',
noFallback: 'No Fallback (Reject)'
},
modelRouting: {
title: 'Model Routing',
tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.',
enabled: 'Enabled',
disabled: 'Disabled',
disabledHint: 'Routing rules will only take effect when enabled',
addRule: 'Add Routing Rule',
modelPattern: 'Model Pattern',
modelPatternPlaceholder: 'claude-opus-*',
modelPatternHint: 'Supports * wildcard, e.g., claude-opus-* matches all opus models',
accounts: 'Priority Accounts',
selectAccounts: 'Select accounts',
noAccounts: 'No accounts in this group',
loadingAccounts: 'Loading accounts...',
removeRule: 'Remove Rule',
noRules: 'No routing rules',
noRulesHint: 'Add routing rules to route specific model requests to designated accounts',
searchAccountPlaceholder: 'Search accounts...',
accountsHint: 'Select accounts to prioritize for this model pattern'
}
},
......
......@@ -992,6 +992,26 @@ export default {
fallbackGroup: '降级分组',
fallbackHint: '非 Claude Code 请求将使用此分组,留空则直接拒绝',
noFallback: '不降级(直接拒绝)'
},
modelRouting: {
title: '模型路由配置',
tooltip: '配置特定模型请求优先路由到指定账号。支持通配符匹配,如 claude-opus-* 匹配所有 opus 模型。',
enabled: '已启用',
disabled: '已禁用',
disabledHint: '启用后,配置的路由规则才会生效',
addRule: '添加路由规则',
modelPattern: '模型模式',
modelPatternPlaceholder: 'claude-opus-*',
modelPatternHint: '支持 * 通配符,如 claude-opus-* 匹配所有 opus 模型',
accounts: '优先账号',
selectAccounts: '选择账号',
noAccounts: '此分组暂无账号',
loadingAccounts: '加载账号中...',
removeRule: '删除规则',
noRules: '暂无路由规则',
noRulesHint: '添加路由规则以将特定模型请求优先路由到指定账号',
searchAccountPlaceholder: '搜索账号...',
accountsHint: '选择此模型模式优先使用的账号'
}
},
......
......@@ -269,6 +269,9 @@ export interface Group {
// Claude Code 客户端限制
claude_code_only: boolean
fallback_group_id: number | null
// 模型路由配置(仅 anthropic 平台使用)
model_routing: Record<string, number[]> | null
model_routing_enabled: boolean
account_count?: number
created_at: string
updated_at: string
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment