diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 1d39fa1e31a..4a187d0538a 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -225,7 +225,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig) userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient) userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig) - gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, userMessageQueueService, configConfig, settingService) + gatewayHandler := handler.NewGatewayHandler(gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, userMessageQueueService, configConfig, settingService) openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, configConfig) handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo) totpHandler := handler.NewTotpHandler(totpService) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index f5eff8c9a51..3542fc2c0cc 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -37,6 +37,7 @@ var gatewayCompatibilityMetricsLogCounter atomic.Uint64 // GatewayHandler handles API gateway requests type GatewayHandler struct { gatewayService *service.GatewayService + openAIGatewayService *service.OpenAIGatewayService geminiCompatService *service.GeminiMessagesCompatService antigravityGatewayService *service.AntigravityGatewayService userService *service.UserService @@ -56,6 +57,7 @@ type GatewayHandler struct { // NewGatewayHandler creates a new GatewayHandler func NewGatewayHandler( gatewayService *service.GatewayService, + openAIGatewayService *service.OpenAIGatewayService, geminiCompatService *service.GeminiMessagesCompatService, antigravityGatewayService *service.AntigravityGatewayService, userService *service.UserService, @@ -90,6 +92,7 @@ func NewGatewayHandler( return &GatewayHandler{ gatewayService: gatewayService, + openAIGatewayService: openAIGatewayService, geminiCompatService: geminiCompatService, antigravityGatewayService: antigravityGatewayService, userService: userService, @@ -107,6 +110,52 @@ func NewGatewayHandler( } } +type messagesForwardRoute int + +const ( + messagesForwardRouteAnthropicNative messagesForwardRoute = iota + messagesForwardRouteOpenAICompat + messagesForwardRouteAntigravity +) + +func messagesForwardRouteKind(group *service.Group, account *service.Account) messagesForwardRoute { + if group != nil && group.Platform == service.PlatformAnthropic && account != nil && account.IsOpenAIOAuth() { + return messagesForwardRouteOpenAICompat + } + if account != nil && account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey { + return messagesForwardRouteAntigravity + } + return messagesForwardRouteAnthropicNative +} + +func openAICompatForwardResult(result *service.OpenAIForwardResult) *service.ForwardResult { + if result == nil { + return nil + } + inputTokens := result.Usage.InputTokens - result.Usage.CacheReadInputTokens + if inputTokens < 0 { + inputTokens = 0 + } + return &service.ForwardResult{ + RequestID: result.RequestID, + Usage: service.ClaudeUsage{ + InputTokens: inputTokens, + OutputTokens: result.Usage.OutputTokens, + CacheCreationInputTokens: result.Usage.CacheCreationInputTokens, + CacheReadInputTokens: result.Usage.CacheReadInputTokens, + ImageOutputTokens: result.Usage.ImageOutputTokens, + }, + Model: result.Model, + BillingModel: result.BillingModel, + UpstreamModel: result.UpstreamModel, + ServiceTier: result.ServiceTier, + Stream: result.Stream, + Duration: result.Duration, + FirstTokenMs: result.FirstTokenMs, + ReasoningEffort: result.ReasoningEffort, + } +} + // Messages handles Claude API compatible messages endpoint // POST /v1/messages func (h *GatewayHandler) Messages(c *gin.Context) { @@ -684,9 +733,32 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } // 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover writerSizeBeforeForward := c.Writer.Size() - if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey { + switch messagesForwardRouteKind(currentAPIKey.Group, account) { + case messagesForwardRouteAntigravity: result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, body, hasBoundSession) - } else { + case messagesForwardRouteOpenAICompat: + if h.openAIGatewayService == nil { + err = errors.New("openai gateway service unavailable") + break + } + defaultMappedModel := "" + if currentAPIKey.Group != nil { + defaultMappedModel = strings.TrimSpace(currentAPIKey.Group.ResolveMessagesDispatchModel(reqModel)) + if defaultMappedModel == "" { + defaultMappedModel = strings.TrimSpace(currentAPIKey.Group.ResolveMessagesDispatchModel(parsedReq.Model)) + } + } + openAIResult, forwardErr := h.openAIGatewayService.ForwardAsAnthropic( + requestCtx, + c, + account, + body, + sessionKey, + defaultMappedModel, + ) + err = forwardErr + result = openAICompatForwardResult(openAIResult) + default: result, err = h.gatewayService.Forward(requestCtx, c, account, parsedReq) } diff --git a/backend/internal/handler/gateway_handler_codex_routing_test.go b/backend/internal/handler/gateway_handler_codex_routing_test.go new file mode 100644 index 00000000000..ccb7e628601 --- /dev/null +++ b/backend/internal/handler/gateway_handler_codex_routing_test.go @@ -0,0 +1,26 @@ +package handler + +import ( + "testing" + + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/require" +) + +func TestMessagesForwardRouteKind(t *testing.T) { + t.Run("anthropic group routed to openai oauth uses openai compat forwarder", func(t *testing.T) { + kind := messagesForwardRouteKind( + &service.Group{Platform: service.PlatformAnthropic}, + &service.Account{Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth}, + ) + require.Equal(t, messagesForwardRouteOpenAICompat, kind) + }) + + t.Run("anthropic native accounts keep anthropic forwarder", func(t *testing.T) { + kind := messagesForwardRouteKind( + &service.Group{Platform: service.PlatformAnthropic}, + &service.Account{Platform: service.PlatformAnthropic, Type: service.AccountTypeOAuth}, + ) + require.Equal(t, messagesForwardRouteAnthropicNative, kind) + }) +} diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index 728328373c6..1e6279772bb 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -2775,6 +2775,118 @@ func TestGatewayService_SelectAccountWithLoadAwareness(t *testing.T) { require.Equal(t, int64(3), cache.sessionBindings["fallback"]) }) + t.Run("模型路由-可显式路由到OpenAI OAuth账号", func(t *testing.T) { + groupID := int64(24) + + repo := &mockAccountRepoForPlatform{ + accounts: []Account{ + {ID: 1, Platform: PlatformAnthropic, Priority: 1, Status: StatusActive, Schedulable: true, Concurrency: 5}, + {ID: 99, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Priority: 0, Status: StatusActive, Schedulable: true, Concurrency: 5, AccountGroups: []AccountGroup{{GroupID: groupID}}}, + }, + accountsByID: map[int64]*Account{}, + } + for i := range repo.accounts { + repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i] + } + + cache := &mockGatewayCacheForPlatform{} + + groupRepo := &mockGroupRepoForGateway{ + groups: map[int64]*Group{ + groupID: { + ID: groupID, + Platform: PlatformAnthropic, + Status: StatusActive, + Hydrated: true, + ModelRoutingEnabled: true, + ModelRouting: map[string][]int64{ + "claude-sonnet-4-5-20250929": {99}, + }, + }, + }, + } + + cfg := testConfig() + cfg.Gateway.Scheduling.LoadBatchEnabled = true + + concurrencyCache := &mockConcurrencyCache{ + loadMap: map[int64]*AccountLoadInfo{ + 1: {AccountID: 1, LoadRate: 10}, + 99: {AccountID: 99, LoadRate: 0}, + }, + } + + svc := &GatewayService{ + accountRepo: repo, + groupRepo: groupRepo, + cache: cache, + cfg: cfg, + concurrencyService: NewConcurrencyService(concurrencyCache), + } + + result, err := svc.SelectAccountWithLoadAwareness(ctx, &groupID, "codex-route", "claude-sonnet-4-5-20250929", nil, "", int64(0)) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, result.Account) + require.Equal(t, int64(99), result.Account.ID) + }) + + t.Run("模型路由-未绑定分组的OpenAI OAuth账号会被跳过", func(t *testing.T) { + groupID := int64(25) + + repo := &mockAccountRepoForPlatform{ + accounts: []Account{ + {ID: 1, Platform: PlatformAnthropic, Priority: 1, Status: StatusActive, Schedulable: true, Concurrency: 5, AccountGroups: []AccountGroup{{GroupID: groupID}}}, + {ID: 99, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Priority: 0, Status: StatusActive, Schedulable: true, Concurrency: 5}, + }, + accountsByID: map[int64]*Account{}, + } + for i := range repo.accounts { + repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i] + } + + cache := &mockGatewayCacheForPlatform{} + + groupRepo := &mockGroupRepoForGateway{ + groups: map[int64]*Group{ + groupID: { + ID: groupID, + Platform: PlatformAnthropic, + Status: StatusActive, + Hydrated: true, + ModelRoutingEnabled: true, + ModelRouting: map[string][]int64{ + "claude-sonnet-4-5-20250929": {99}, + }, + }, + }, + } + + cfg := testConfig() + cfg.Gateway.Scheduling.LoadBatchEnabled = true + + concurrencyCache := &mockConcurrencyCache{ + loadMap: map[int64]*AccountLoadInfo{ + 1: {AccountID: 1, LoadRate: 0}, + 99: {AccountID: 99, LoadRate: 0}, + }, + } + + svc := &GatewayService{ + accountRepo: repo, + groupRepo: groupRepo, + cache: cache, + cfg: cfg, + concurrencyService: NewConcurrencyService(concurrencyCache), + } + + result, err := svc.SelectAccountWithLoadAwareness(ctx, &groupID, "codex-unbound", "claude-sonnet-4-5-20250929", nil, "", int64(0)) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, result.Account) + require.Equal(t, int64(1), result.Account.ID) + }) + t.Run("负载批量失败且无法获取-兜底等待", func(t *testing.T) { repo := &mockAccountRepoForPlatform{ accounts: []Account{ diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index c65e828a0dc..adabbf7072c 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -491,9 +491,13 @@ type ForwardResult struct { RequestID string Usage ClaudeUsage Model string + // BillingModel is the model used for cost calculation when it differs from + // the client-facing model, such as OpenAI compatibility forwarding. + BillingModel string // UpstreamModel is the actual upstream model after mapping. // Prefer empty when it is identical to Model; persistence normalizes equal values away as no-op mappings. UpstreamModel string + ServiceTier *string Stream bool Duration time.Duration FirstTokenMs *int // 首字时间(流式请求) @@ -1377,7 +1381,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro filteredExcluded++ continue } - account, ok := accountByID[routingAccountID] + account, ok := s.resolveExplicitModelRoutingAccount(ctx, group, groupID, routingAccountID, accountByID) if !ok || !s.isAccountSchedulableForSelection(account) { if !ok { filteredMissing++ @@ -1386,7 +1390,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro } continue } - if !s.isAccountAllowedForPlatform(account, platform, useMixed) { + if !s.isExplicitModelRoutingAccountAllowed(group, groupID, account, platform, useMixed) { filteredPlatform++ continue } @@ -1430,11 +1434,11 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro if sessionHash != "" && stickyAccountID > 0 { if containsInt64(routingAccountIDs, stickyAccountID) && !isExcluded(stickyAccountID) { // 粘性账号在路由列表中,优先使用 - if stickyAccount, ok := accountByID[stickyAccountID]; ok { + if stickyAccount, ok := s.resolveExplicitModelRoutingAccount(ctx, group, groupID, stickyAccountID, accountByID); ok { var stickyCacheMissReason string gatePass := s.isAccountSchedulableForSelection(stickyAccount) && - s.isAccountAllowedForPlatform(stickyAccount, platform, useMixed) && + s.isExplicitModelRoutingAccountAllowed(group, groupID, stickyAccount, platform, useMixed) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, stickyAccount, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, stickyAccount, requestedModel) && s.isAccountSchedulableForQuota(stickyAccount) && @@ -2056,6 +2060,48 @@ func (s *GatewayService) isAccountAllowedForPlatform(account *Account, platform return account.Platform == platform } +func (s *GatewayService) isExplicitModelRoutingOpenAIOAuthAccount(group *Group, account *Account) bool { + return group != nil && group.Platform == PlatformAnthropic && account != nil && account.IsOpenAIOAuth() +} + +func (s *GatewayService) isExplicitModelRoutingAccountAllowed(group *Group, groupID *int64, account *Account, platform string, useMixed bool) bool { + if account == nil { + return false + } + if s.isExplicitModelRoutingOpenAIOAuthAccount(group, account) { + return s.isAccountInGroup(account, groupID) + } + return s.isAccountAllowedForPlatform(account, platform, useMixed) +} + +func (s *GatewayService) resolveExplicitModelRoutingAccount( + ctx context.Context, + group *Group, + groupID *int64, + accountID int64, + accountByID map[int64]*Account, +) (*Account, bool) { + if accountByID != nil { + if account, ok := accountByID[accountID]; ok && account != nil { + return account, true + } + } + if group == nil || group.Platform != PlatformAnthropic || accountID <= 0 { + return nil, false + } + account, err := s.getSchedulableAccount(ctx, accountID) + if err != nil || account == nil { + return nil, false + } + if !account.IsOpenAIOAuth() || !s.isAccountInGroup(account, groupID) { + return nil, false + } + if accountByID != nil { + accountByID[accountID] = account + } + return account, true +} + func (s *GatewayService) isAccountSchedulableForSelection(account *Account) bool { if account == nil { return false @@ -2713,6 +2759,10 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, if groupID != nil && s.groupRepo != nil { schedGroup, _ = s.groupRepo.GetByID(ctx, *groupID) } + routingGroup := schedGroup + if routingGroup == nil && groupID != nil { + routingGroup, _ = s.resolveGroupByID(ctx, *groupID) + } var accounts []Account accountsLoaded := false @@ -2737,7 +2787,7 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, if clearSticky { _ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash) } - if !clearSticky && s.isAccountInGroup(account, groupID) && account.Platform == platform && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) && !s.isStickyAccountUpstreamRestricted(ctx, groupID, account, requestedModel) { + if !clearSticky && s.isAccountInGroup(account, groupID) && s.isExplicitModelRoutingAccountAllowed(routingGroup, groupID, account, platform, false) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) && !s.isStickyAccountUpstreamRestricted(ctx, groupID, account, requestedModel) { if s.debugModelRoutingEnabled() { logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID) } @@ -2764,17 +2814,15 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, ctx = s.withWindowCostPrefetch(ctx, accounts) ctx = s.withRPMPrefetch(ctx, accounts) - routingSet := make(map[int64]struct{}, len(routingAccountIDs)) - for _, id := range routingAccountIDs { - if id > 0 { - routingSet[id] = struct{}{} - } + accountByID := make(map[int64]*Account, len(accounts)) + for i := range accounts { + accountByID[accounts[i].ID] = &accounts[i] } var selected *Account - for i := range accounts { - acc := &accounts[i] - if _, ok := routingSet[acc.ID]; !ok { + for _, routingAccountID := range routingAccountIDs { + acc, ok := s.resolveExplicitModelRoutingAccount(ctx, routingGroup, groupID, routingAccountID, accountByID) + if !ok { continue } if _, excluded := excludedIDs[acc.ID]; excluded { @@ -2791,6 +2839,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) continue } + if !s.isExplicitModelRoutingAccountAllowed(routingGroup, groupID, acc, platform, false) { + continue + } if requestedModel != "" && !s.isModelSupportedByAccountWithContext(ctx, acc, requestedModel) { continue } @@ -2973,6 +3024,10 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g if groupID != nil && s.groupRepo != nil { schedGroup, _ = s.groupRepo.GetByID(ctx, *groupID) } + routingGroup := schedGroup + if routingGroup == nil && groupID != nil { + routingGroup, _ = s.resolveGroupByID(ctx, *groupID) + } var accounts []Account accountsLoaded := false @@ -2996,7 +3051,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g _ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash) } if !clearSticky && s.isAccountInGroup(account, groupID) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) { - if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) { + if s.isExplicitModelRoutingAccountAllowed(routingGroup, groupID, account, nativePlatform, true) { if s.debugModelRoutingEnabled() { logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy mixed routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID) } @@ -3020,17 +3075,15 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g ctx = s.withWindowCostPrefetch(ctx, accounts) ctx = s.withRPMPrefetch(ctx, accounts) - routingSet := make(map[int64]struct{}, len(routingAccountIDs)) - for _, id := range routingAccountIDs { - if id > 0 { - routingSet[id] = struct{}{} - } + accountByID := make(map[int64]*Account, len(accounts)) + for i := range accounts { + accountByID[accounts[i].ID] = &accounts[i] } var selected *Account - for i := range accounts { - acc := &accounts[i] - if _, ok := routingSet[acc.ID]; !ok { + for _, routingAccountID := range routingAccountIDs { + acc, ok := s.resolveExplicitModelRoutingAccount(ctx, routingGroup, groupID, routingAccountID, accountByID) + if !ok { continue } if _, excluded := excludedIDs[acc.ID]; excluded { @@ -3047,8 +3100,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) continue } - // 过滤:原生平台直接通过,antigravity 需要启用混合调度 - if acc.Platform == PlatformAntigravity && !acc.IsMixedSchedulingEnabled() { + if !s.isExplicitModelRoutingAccountAllowed(routingGroup, groupID, acc, nativePlatform, true) { continue } if requestedModel != "" && !s.isModelSupportedByAccountWithContext(ctx, acc, requestedModel) { @@ -7799,6 +7851,9 @@ func (s *GatewayService) recordUsageCore(ctx context.Context, input *recordUsage // 确定计费模型 billingModel := forwardResultBillingModel(result.Model, result.UpstreamModel) + if result.BillingModel != "" { + billingModel = strings.TrimSpace(result.BillingModel) + } if input.BillingModelSource == BillingModelSourceChannelMapped && input.ChannelMappedModel != "" { billingModel = input.ChannelMappedModel } @@ -7968,6 +8023,10 @@ func (s *GatewayService) calculateTokenCost( var cost *CostBreakdown var err error + serviceTier := "" + if result.ServiceTier != nil { + serviceTier = strings.TrimSpace(*result.ServiceTier) + } // 优先尝试渠道定价 → CalculateCostUnified if resolved := s.resolveChannelPricing(ctx, billingModel, apiKey); resolved != nil { @@ -7979,6 +8038,7 @@ func (s *GatewayService) calculateTokenCost( Tokens: tokens, RequestCount: 1, RateMultiplier: multiplier, + ServiceTier: serviceTier, Resolver: s.resolver, Resolved: resolved, }) @@ -7988,6 +8048,8 @@ func (s *GatewayService) calculateTokenCost( billingModel, tokens, multiplier, opts.LongContextThreshold, opts.LongContextMultiplier, ) + } else if serviceTier != "" { + cost, err = s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, serviceTier) } else { cost, err = s.billingService.CalculateCost(billingModel, tokens, multiplier) } @@ -8025,6 +8087,7 @@ func (s *GatewayService) buildRecordUsageLog( Model: result.Model, RequestedModel: requestedModel, UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model), + ServiceTier: result.ServiceTier, ReasoningEffort: result.ReasoningEffort, InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint), UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint), diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 8ab08b733ea..cf6f6979efe 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -1331,7 +1331,7 @@ :key="account.id" class="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300" > - {{ account.name }} + {{ formatModelRoutingAccountLabel(account) }}