diff --git a/internal/control/credential_collection_helpers.go b/internal/control/credential_collection_helpers.go index a671e6010..27d1ba0e9 100644 --- a/internal/control/credential_collection_helpers.go +++ b/internal/control/credential_collection_helpers.go @@ -48,7 +48,7 @@ func parseCredentialCollectionQuery(rawQuery string) (CredentialCollectionQuery, } for key, entries := range values { switch key { - case "q", "status", "page", "page_size", "model_cooldown": + case "q", "status", "page", "page_size": default: return CredentialCollectionQuery{}, app_errors.ErrBadRequest } @@ -56,12 +56,6 @@ func parseCredentialCollectionQuery(rawQuery string) (CredentialCollectionQuery, return CredentialCollectionQuery{}, app_errors.ErrBadRequest } } - if entries, exists := values["model_cooldown"]; exists { - if entries[0] != "true" { - return CredentialCollectionQuery{}, app_errors.ErrBadRequest - } - query.ModelCooldown = true - } if entries, exists := values["q"]; exists { query.Query = strings.TrimSpace(entries[0]) } diff --git a/internal/control/credential_mutations.go b/internal/control/credential_mutations.go index fadbf0c38..73b187ee0 100644 --- a/internal/control/credential_mutations.go +++ b/internal/control/credential_mutations.go @@ -785,9 +785,6 @@ func summarizeGroupRuntimeCredentials( continue } summary.Total++ - if hasModelCooldown(view.ModelCooldowns, observedAt) { - summary.ModelCooldown++ - } switch classifyHealthKey(groupView, view, observedAt) { case healthBucketAvailable: summary.Available++ diff --git a/internal/control/credentials.go b/internal/control/credentials.go index 5b99e5859..60a6d96c3 100644 --- a/internal/control/credentials.go +++ b/internal/control/credentials.go @@ -43,11 +43,10 @@ type CredentialRevealResult struct { } type CredentialCollectionQuery struct { - Query string - Status *string - ModelCooldown bool - Page int - PageSize int + Query string + Status *string + Page int + PageSize int } type CredentialCollectionResponse struct { @@ -59,12 +58,11 @@ type CredentialCollectionResponse struct { } type CredentialSummaryResponse struct { - Total int `json:"total"` - Available int `json:"available"` - Cooldown int `json:"cooldown"` - Blacklisted int `json:"blacklisted"` - Disabled int `json:"disabled"` - ModelCooldown int `json:"model_cooldown"` + Total int `json:"total"` + Available int `json:"available"` + Cooldown int `json:"cooldown"` + Blacklisted int `json:"blacklisted"` + Disabled int `json:"disabled"` } type CredentialAccountResponse struct { @@ -468,9 +466,6 @@ func (s *Service) mapCredentialCollection( func summarizeCredentialCollection(records []credentialCollectionRecord) CredentialSummaryResponse { summary := CredentialSummaryResponse{Total: len(records)} for _, record := range records { - if len(record.item.ModelCooldowns) > 0 { - summary.ModelCooldown++ - } switch record.bucket { case healthBucketAvailable: summary.Available++ @@ -489,9 +484,6 @@ func credentialCollectionMatches(record credentialCollectionRecord, query Creden if query.Status != nil && record.item.EffectiveStatus != *query.Status { return false } - if query.ModelCooldown && len(record.item.ModelCooldowns) == 0 { - return false - } if query.Query == "" { return true } diff --git a/internal/control/health.go b/internal/control/health.go index f50a33587..379703326 100644 --- a/internal/control/health.go +++ b/internal/control/health.go @@ -21,18 +21,22 @@ type RequestLogStatsReader interface { } type healthCountsResponse struct { + Credentials int `json:"credentials"` + Available int `json:"available"` + Cooldown int `json:"cooldown"` + Blacklisted int `json:"blacklisted"` +} + +type healthGroupCountsResponse struct { + healthCountsResponse ModelCooldown int `json:"model_cooldown"` - Credentials int `json:"credentials"` - Available int `json:"available"` - Cooldown int `json:"cooldown"` - Blacklisted int `json:"blacklisted"` } type healthGroupResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Enabled bool `json:"enabled"` - Counts healthCountsResponse `json:"counts"` + ID uint `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Counts healthGroupCountsResponse `json:"counts"` } type healthRecoveryResponse struct { @@ -98,20 +102,19 @@ type requestLogHealthResponse struct { } type runtimeHealthResponse struct { - ModelCooldownCredentials []healthModelCooldownCredentialResponse `json:"model_cooldown_credentials"` - ObservedAtMS int64 `json:"observed_at_ms"` - Version string `json:"version"` - UptimeSeconds int64 `json:"uptime_seconds"` - SnapshotRevision uint64 `json:"snapshot_revision"` - StatsWindowSeconds int64 `json:"stats_window_seconds"` - Counts healthCountsResponse `json:"counts"` - Groups []healthGroupResponse `json:"groups"` - CooldownCredentials []healthProblemCredentialResponse `json:"cooldown_credentials"` - BlacklistedCredentials []healthProblemCredentialResponse `json:"blacklisted_credentials"` - LowQuotaCredentials []healthQuotaCredentialResponse `json:"low_quota_credentials"` - ExpiringResetCredits []healthExpiringResetCreditResponse `json:"expiring_reset_credits"` - BlockedAccessKeys []healthAccessKeyCostLimitResponse `json:"blocked_access_keys"` - RequestLog requestLogHealthResponse `json:"request_log"` + ObservedAtMS int64 `json:"observed_at_ms"` + Version string `json:"version"` + UptimeSeconds int64 `json:"uptime_seconds"` + SnapshotRevision uint64 `json:"snapshot_revision"` + StatsWindowSeconds int64 `json:"stats_window_seconds"` + Counts healthCountsResponse `json:"counts"` + Groups []healthGroupResponse `json:"groups"` + CooldownCredentials []healthProblemCredentialResponse `json:"cooldown_credentials"` + BlacklistedCredentials []healthProblemCredentialResponse `json:"blacklisted_credentials"` + LowQuotaCredentials []healthQuotaCredentialResponse `json:"low_quota_credentials"` + ExpiringResetCredits []healthExpiringResetCreditResponse `json:"expiring_reset_credits"` + BlockedAccessKeys []healthAccessKeyCostLimitResponse `json:"blocked_access_keys"` + RequestLog requestLogHealthResponse `json:"request_log"` } type healthAccessKeyCostLimitResponse struct { @@ -270,16 +273,15 @@ func (service *Service) RuntimeHealth() (runtimeHealthResponse, error) { return runtimeHealthResponse{}, fmt.Errorf("map runtime health observed_at_ms: %w", err) } result := runtimeHealthResponse{ - ModelCooldownCredentials: []healthModelCooldownCredentialResponse{}, - ObservedAtMS: observedAtMS, - SnapshotRevision: observation.snapshot.Revision, - StatsWindowSeconds: int64(health.StatsWindow / time.Second), - Groups: []healthGroupResponse{}, - CooldownCredentials: []healthProblemCredentialResponse{}, - BlacklistedCredentials: []healthProblemCredentialResponse{}, - LowQuotaCredentials: []healthQuotaCredentialResponse{}, - ExpiringResetCredits: []healthExpiringResetCreditResponse{}, - BlockedAccessKeys: []healthAccessKeyCostLimitResponse{}, + ObservedAtMS: observedAtMS, + SnapshotRevision: observation.snapshot.Revision, + StatsWindowSeconds: int64(health.StatsWindow / time.Second), + Groups: []healthGroupResponse{}, + CooldownCredentials: []healthProblemCredentialResponse{}, + BlacklistedCredentials: []healthProblemCredentialResponse{}, + LowQuotaCredentials: []healthQuotaCredentialResponse{}, + ExpiringResetCredits: []healthExpiringResetCreditResponse{}, + BlockedAccessKeys: []healthAccessKeyCostLimitResponse{}, } groupIDs := make([]uint, 0, len(observation.snapshot.GroupCatalog)) for groupID := range observation.snapshot.GroupCatalog { @@ -305,23 +307,10 @@ func (service *Service) RuntimeHealth() (runtimeHealthResponse, error) { } } if hasModelCooldown(key.ModelCooldowns, observation.observedAt) { - result.Counts.ModelCooldown++ result.Groups[index].Counts.ModelCooldown++ - if len(result.ModelCooldownCredentials) < healthProblemCredentialDetailLimit { - // 停用分组仍保留模型冷却,身份展示使用完整分组目录。 - identity, err := service.healthProblemCredentialIdentity(observation.problemCiphertexts, key.ID, group.ChannelID, group.ConnectionType) - if err != nil { - return runtimeHealthResponse{}, err - } - limits, err := modelCooldownResponses(key.ModelCooldowns, observation.observedAt) - if err != nil { - return runtimeHealthResponse{}, err - } - result.ModelCooldownCredentials = append(result.ModelCooldownCredentials, healthModelCooldownCredentialResponse{CredentialID: key.ID, GroupID: key.GroupID, GroupName: group.Name, Identity: identity, ModelCooldowns: limits}) - } } addHealthCount(&result.Counts, bucket) - addHealthCount(&result.Groups[index].Counts, bucket) + addHealthCount(&result.Groups[index].Counts.healthCountsResponse, bucket) // 额度只用于管理面展示,不参与健康分桶或调度;低额度凭据在这里单列提示。 if bucket == healthBucketAvailable || bucket == healthBucketCooldown { if remaining := key.ObservedQuotaRemaining(); remaining != nil && diff --git a/internal/control/health_test.go b/internal/control/health_test.go index cc18a38ad..5fb158437 100644 --- a/internal/control/health_test.go +++ b/internal/control/health_test.go @@ -137,7 +137,7 @@ func TestRuntimeHealthReturnsMutuallyExclusiveCurrentState(t *testing.T) { got.Groups[1].ID != 2 || got.Groups[2].ID != 3 || got.Groups[3].ID != 4 { t.Fatalf("group order = %#v", got.Groups) } - if got.Groups[0].Counts != (healthCountsResponse{ + if got.Groups[0].Counts.healthCountsResponse != (healthCountsResponse{ Credentials: 3, Available: 1, Cooldown: 1, Blacklisted: 1, }) { t.Fatalf("active group counts = %#v", got.Groups[0].Counts) diff --git a/internal/control/model_cooldown.go b/internal/control/model_cooldown.go index 286dd1298..4bee6d2c8 100644 --- a/internal/control/model_cooldown.go +++ b/internal/control/model_cooldown.go @@ -10,14 +10,6 @@ type ModelCooldownResponse struct { CooldownUntilMS int64 `json:"cooldown_until_ms"` } -type healthModelCooldownCredentialResponse struct { - CredentialID uint `json:"credential_id"` - GroupID uint `json:"group_id"` - GroupName string `json:"group_name"` - Identity string `json:"identity"` - ModelCooldowns []ModelCooldownResponse `json:"model_cooldowns"` -} - func hasModelCooldown(limits map[string]time.Time, now time.Time) bool { for _, until := range limits { if until.After(now) { diff --git a/internal/control/model_cooldown_test.go b/internal/control/model_cooldown_test.go index 99946045c..09cce1f14 100644 --- a/internal/control/model_cooldown_test.go +++ b/internal/control/model_cooldown_test.go @@ -1,6 +1,7 @@ package control import ( + "encoding/json" "testing" "time" @@ -71,7 +72,7 @@ func TestModelCooldownCollectionAndHealthKeepIndependentAccountStatus(t *testing for _, model := range []string{"b", "a"} { fixture.registry.SetModelCooldown(ref, model, now.Add(time.Hour), now) } - query, parseErr := parseCredentialCollectionQuery("model_cooldown=true") + query, parseErr := parseCredentialCollectionQuery("") if parseErr != nil { t.Fatal(parseErr) } @@ -87,7 +88,7 @@ func TestModelCooldownCollectionAndHealthKeepIndependentAccountStatus(t *testing if err != nil { t.Fatal(err) } - if collection.Summary.ModelCooldown != 1 || collection.Summary.Available != 2 || collection.Pagination.TotalItems != 1 || len(collection.Items) != 1 { + if collection.Summary.Available != 2 || collection.Pagination.TotalItems != 2 || len(collection.Items) != 2 { t.Fatalf("collection = %#v", collection) } item := collection.Items[0] @@ -98,12 +99,38 @@ func TestModelCooldownCollectionAndHealthKeepIndependentAccountStatus(t *testing if err != nil { t.Fatal(err) } - if health.Counts.ModelCooldown != 1 || len(health.ModelCooldownCredentials) != 1 || len(health.CooldownCredentials) != 0 { + if health.Counts.Available != 2 || health.Groups[0].Counts.ModelCooldown != 1 || len(health.CooldownCredentials) != 0 { t.Fatalf("health = %#v", health) } + // 删除的展示不再保留对应汇总字段和明细响应。 + for name, value := range map[string]any{ + "credential_summary": collection.Summary, + "health_counts": health.Counts, + "health": health, + } { + payload, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(payload, &fields); err != nil { + t.Fatal(err) + } + for _, removed := range []string{"model_cooldown", "model_cooldown_credentials"} { + if _, exists := fields[removed]; exists { + t.Errorf("%s still exposes %s", name, removed) + } + } + } +} + +func TestCredentialCollectionRejectsRemovedModelCooldownFilter(t *testing.T) { + if _, err := parseCredentialCollectionQuery("model_cooldown=true"); err == nil { + t.Fatal("removed model cooldown filter is still accepted") + } } -func TestRuntimeHealthKeepsModelCooldownIdentityWhenGroupIsDisabled(t *testing.T) { +func TestRuntimeHealthCountsModelCooldownWhenGroupIsDisabled(t *testing.T) { for _, connectionType := range []string{"api_key", "subscription"} { t.Run(connectionType, func(t *testing.T) { var fixture serviceFixture @@ -122,7 +149,7 @@ func TestRuntimeHealthKeepsModelCooldownIdentityWhenGroupIsDisabled(t *testing.T t.Fatal("model cooldown was rejected") } before, err := fixture.service.RuntimeHealth() - if err != nil || len(before.ModelCooldownCredentials) != 1 { + if err != nil || len(before.Groups) != 1 || before.Groups[0].Counts.ModelCooldown != 1 { t.Fatalf("initial health = %#v, %v", before, err) } for _, enabled := range []bool{false, true} { @@ -135,14 +162,9 @@ func TestRuntimeHealthKeepsModelCooldownIdentityWhenGroupIsDisabled(t *testing.T if err != nil { t.Fatalf("health with group enabled=%t: %v", enabled, err) } - if got.Counts.ModelCooldown != 1 || len(got.ModelCooldownCredentials) != 1 { + if len(got.Groups) != 1 || got.Groups[0].Counts.ModelCooldown != 1 { t.Fatalf("model cooldown disappeared after group enabled=%t: %#v", enabled, got) } - detail := got.ModelCooldownCredentials[0] - if detail.CredentialID != credentialID || detail.GroupID != groupID || - detail.Identity != before.ModelCooldownCredentials[0].Identity || len(detail.ModelCooldowns) != 1 { - t.Fatalf("model cooldown identity changed: %#v", detail) - } if len(got.Groups) != 1 || got.Groups[0].Enabled != enabled || (got.Counts.Credentials > 0) != enabled { t.Fatalf("group health changed: %#v", got) } diff --git a/internal/control/runtime_observation.go b/internal/control/runtime_observation.go index a0938ae44..5214cd3ed 100644 --- a/internal/control/runtime_observation.go +++ b/internal/control/runtime_observation.go @@ -93,7 +93,6 @@ func (service *Service) captureRuntimeHealthObservation() ( problemCiphertexts := make(map[uint]string) cooldownDetails := 0 blacklistedDetails := 0 - modelCooldownDetails := 0 for _, key := range keys { group, exists := snapshot.GroupCatalog[key.GroupID] if !exists { @@ -114,10 +113,6 @@ func (service *Service) captureRuntimeHealthObservation() ( blacklistedDetails++ needsIdentity = true } - if hasModelCooldown(key.ModelCooldowns, observedAt) && modelCooldownDetails < healthProblemCredentialDetailLimit { - modelCooldownDetails++ - needsIdentity = true - } if !needsIdentity { continue } diff --git a/internal/control/wire_v3_contract_test.go b/internal/control/wire_v3_contract_test.go index 6aee02a5a..ab0ff13c4 100644 --- a/internal/control/wire_v3_contract_test.go +++ b/internal/control/wire_v3_contract_test.go @@ -91,7 +91,7 @@ func TestHealthAndRouteInspectionUseCredentialWireNames(t *testing.T) { t.Fatalf("json.Marshal(health) error = %v", err) } for _, token := range []string{ - `"counts":{"model_cooldown":0,"credentials":1,"available":1,"cooldown":0,"blacklisted":0}`, + `"counts":{"credentials":1,"available":1,"cooldown":0,"blacklisted":0}`, `"cooldown_credentials":[{"credential_id":7`, `"blacklisted_credentials":[]`, } { diff --git a/web/src/api/control/types.ts b/web/src/api/control/types.ts index 3c1998ac8..9d82ecd92 100644 --- a/web/src/api/control/types.ts +++ b/web/src/api/control/types.ts @@ -346,7 +346,6 @@ export interface CredentialTestResultDto { } export interface CredentialSummaryDto { - model_cooldown: number total: number available: number cooldown: number @@ -370,7 +369,6 @@ export interface CredentialCollectionDto { } export interface CredentialCollectionFilters { - model_cooldown?: true q?: string status?: CredentialStatus page: number @@ -402,7 +400,6 @@ export interface CredentialCounts { } export interface HealthCredentialCountsDto { - model_cooldown: number credentials: number available: number cooldown: number @@ -413,7 +410,7 @@ export interface HealthGroupDto { id: number name: string enabled: boolean - counts: HealthCredentialCountsDto + counts: HealthCredentialCountsDto & { model_cooldown: number } } export interface HealthRecoveryDto { @@ -459,16 +456,7 @@ export interface RequestLogHealthDto { last_retention_failure_at_ms: number | null } -export interface HealthModelCooldownCredentialDto { - credential_id: number - group_id: number - group_name: string - identity: string - model_cooldowns: ModelCooldownDto[] -} - export interface RuntimeHealthDto { - model_cooldown_credentials: HealthModelCooldownCredentialDto[] observed_at_ms: number version: string uptime_seconds: number diff --git a/web/src/app/query-keys.ts b/web/src/app/query-keys.ts index 8535fc5cc..166536993 100644 --- a/web/src/app/query-keys.ts +++ b/web/src/app/query-keys.ts @@ -35,7 +35,6 @@ export function normalizeCredentialCollectionFilters( const query = filters.q?.trim() if (query) normalized.q = query if (filters.status !== undefined) normalized.status = filters.status - if (filters.model_cooldown) normalized.model_cooldown = true return normalized } diff --git a/web/src/app/resources/credentials.ts b/web/src/app/resources/credentials.ts index dfdbeb4b1..710a07828 100644 --- a/web/src/app/resources/credentials.ts +++ b/web/src/app/resources/credentials.ts @@ -87,7 +87,6 @@ const credentialCollectionFields = [ 'pagination', ] as const const credentialSummaryFields = [ - 'model_cooldown', 'total', 'available', 'cooldown', @@ -502,7 +501,7 @@ function projectObservation(value: unknown): CredentialObservationDto { } } -export function projectModelCooldown(value: unknown) { +function projectModelCooldown(value: unknown) { const record = projectRecord(value) assertNoSecretLikeFields(record, ['model', 'cooldown_until_ms']) return { @@ -516,16 +515,12 @@ export function projectCredentialSummary(value: unknown): CredentialSummaryDto { assertNoSecretLikeFields(record, credentialSummaryFields) const result = { total: projectSafeInteger(record.total, { minimum: 0 }), - model_cooldown: projectSafeInteger(record.model_cooldown, { minimum: 0 }), available: projectSafeInteger(record.available, { minimum: 0 }), cooldown: projectSafeInteger(record.cooldown, { minimum: 0 }), blacklisted: projectSafeInteger(record.blacklisted, { minimum: 0 }), disabled: projectSafeInteger(record.disabled, { minimum: 0 }), } - if ( - result.total !== result.available + result.cooldown + result.blacklisted + result.disabled || - result.model_cooldown > result.total - ) { + if (result.total !== result.available + result.cooldown + result.blacklisted + result.disabled) { invalidResponse() } return result @@ -711,7 +706,6 @@ function credentialCollectionURL( page: String(normalized.page), page_size: String(normalized.page_size), }) - if (normalized.model_cooldown) params.set('model_cooldown', 'true') if (normalized.q !== undefined) params.set('q', normalized.q) if (normalized.status !== undefined) params.set('status', normalized.status) return `/api/groups/${groupId}/credentials?${params.toString()}` @@ -1077,14 +1071,12 @@ function queryFilters(queryKey: QueryKey): CredentialCollectionFilters | undefin if ( !Number.isSafeInteger(record.page) || (record.page_size !== 20 && record.page_size !== 50 && record.page_size !== 100) || - (record.model_cooldown !== undefined && record.model_cooldown !== true) || (record.q !== undefined && typeof record.q !== 'string') || (record.status !== undefined && !effectiveStatuses.includes(record.status as CredentialStatus)) ) { return undefined } return { - ...(record.model_cooldown === true ? { model_cooldown: true as const } : {}), page: record.page as number, page_size: record.page_size as 20 | 50 | 100, ...(record.q === undefined ? {} : { q: record.q }), @@ -1094,7 +1086,6 @@ function queryFilters(queryKey: QueryKey): CredentialCollectionFilters | undefin function matchesFilters(item: CredentialItemDto, filters: CredentialCollectionFilters): boolean { if (filters.status !== undefined && item.effective_status !== filters.status) return false - if (filters.model_cooldown && item.model_cooldowns.length === 0) return false if (filters.q === undefined) return true const query = filters.q.toLowerCase() return ( @@ -1109,8 +1100,6 @@ function withSummaryDelta( next: CredentialItemDto | undefined, ): CredentialSummaryDto { const result = { ...summary } - result.model_cooldown += - Number((next?.model_cooldowns.length ?? 0) > 0) - Number(previous.model_cooldowns.length > 0) result[previous.effective_status]-- if (next !== undefined) result[next.effective_status]++ return result @@ -1152,7 +1141,6 @@ function credentialFilterSetID(filters: CredentialCollectionFilters): string { return JSON.stringify({ q: filters.q ?? null, status: filters.status ?? null, - model_cooldown: filters.model_cooldown ?? false, page_size: filters.page_size, }) } @@ -1162,10 +1150,8 @@ function totalItemsAfterBatchDelete( knownDeletedIDs: Set, summary: CredentialSummaryDto, ): number { - const { q, status, model_cooldown } = pages[0].filters - if (q === undefined && !model_cooldown) - return status === undefined ? summary.total : summary[status] - if (q === undefined && status === undefined && model_cooldown) return summary.model_cooldown + const { q, status } = pages[0].filters + if (q === undefined) return status === undefined ? summary.total : summary[status] return Math.max( 0, Math.max(...pages.map(({ collection }) => collection.pagination.total_items)) - diff --git a/web/src/app/resources/health.ts b/web/src/app/resources/health.ts index c1ee9aa8b..2cb21c75e 100644 --- a/web/src/app/resources/health.ts +++ b/web/src/app/resources/health.ts @@ -16,7 +16,6 @@ import type { import { InvalidResponseError } from '@/api/errors' import { controlQueryKeys } from '@/app/query-keys' import { projectAccessKeyCostLimitRuleStatus } from './access-keys' -import { projectModelCooldown } from './credentials' import { assertNoSecretLikeFields, @@ -41,15 +40,8 @@ export type { RuntimeHealthDto, } from '@/api/control/types' -const countFields = [ - 'model_cooldown', - 'credentials', - 'available', - 'cooldown', - 'blacklisted', -] as const +const countFields = ['credentials', 'available', 'cooldown', 'blacklisted'] as const const healthFields = [ - 'model_cooldown_credentials', 'observed_at_ms', 'version', 'uptime_seconds', @@ -163,7 +155,6 @@ export function projectHealthCounts(value: unknown): HealthCredentialCountsDto { credentials: projectSafeInteger(record.credentials, { minimum: 0 }), available: projectSafeInteger(record.available, { minimum: 0 }), cooldown: projectSafeInteger(record.cooldown, { minimum: 0 }), - model_cooldown: projectSafeInteger(record.model_cooldown, { minimum: 0 }), blacklisted: projectSafeInteger(record.blacklisted, { minimum: 0 }), } if (result.credentials !== result.available + result.cooldown + result.blacklisted) { @@ -175,11 +166,15 @@ export function projectHealthCounts(value: unknown): HealthCredentialCountsDto { function projectHealthGroup(value: unknown): HealthGroupDto { const record = projectRecord(value) assertNoSecretLikeFields(record, ['id', 'name', 'enabled', 'counts']) + const { model_cooldown, ...counts } = projectRecord(record.counts) return { id: projectSafeInteger(record.id, { minimum: 1 }), name: projectNonBlankString(record.name), enabled: projectBoolean(record.enabled), - counts: projectHealthCounts(record.counts), + counts: { + ...projectHealthCounts(counts), + model_cooldown: projectSafeInteger(model_cooldown, { minimum: 0 }), + }, } } @@ -332,23 +327,6 @@ export function projectRuntimeHealth(value: unknown): RuntimeHealthDto { stats_window_seconds: projectSafeInteger(record.stats_window_seconds, { minimum: 1 }), counts: projectHealthCounts(record.counts), groups: projectArray(record.groups, projectHealthGroup), - model_cooldown_credentials: projectArray(record.model_cooldown_credentials, (value) => { - const item = projectRecord(value) - assertNoSecretLikeFields(item, [ - 'credential_id', - 'group_id', - 'group_name', - 'identity', - 'model_cooldowns', - ]) - return { - credential_id: projectSafeInteger(item.credential_id, { minimum: 1 }), - group_id: projectSafeInteger(item.group_id, { minimum: 1 }), - group_name: projectNonBlankString(item.group_name), - identity: projectNonBlankString(item.identity), - model_cooldowns: projectArray(item.model_cooldowns, projectModelCooldown), - } - }), cooldown_credentials: projectArray(record.cooldown_credentials, projectProblemCredential), blacklisted_credentials: projectArray(record.blacklisted_credentials, projectProblemCredential), low_quota_credentials: projectArray(record.low_quota_credentials, projectQuotaCredential), diff --git a/web/src/app/router.ts b/web/src/app/router.ts index 81197478f..232023208 100644 --- a/web/src/app/router.ts +++ b/web/src/app/router.ts @@ -81,7 +81,7 @@ const routes: RouteRecordRaw[] = [ titleKey: 'shell.monitor', requiresAuth: true, primaryNav: 'monitor', - messageNamespaces: ['monitor'], + messageNamespaces: ['monitor', 'group', 'settings'], }, }), pageRoute(pageRouteNames.models, { diff --git a/web/src/components/ui/CredentialHealthBar.vue b/web/src/components/ui/CredentialHealthBar.vue index af4b08813..94cb50384 100644 --- a/web/src/components/ui/CredentialHealthBar.vue +++ b/web/src/components/ui/CredentialHealthBar.vue @@ -13,11 +13,10 @@ const props = defineProps<{ }>() const { n } = useI18n() -const normalizedCounts = computed(() => +const normalizedCounts = computed(() => 'total' in props.counts ? props.counts : { - model_cooldown: props.counts.model_cooldown, total: props.counts.credentials, available: props.counts.available, cooldown: props.counts.cooldown, diff --git a/web/src/components/ui/ModelCooldownDetails.vue b/web/src/components/ui/ModelCooldownDetails.vue index 2dbdb8854..60c57668b 100644 --- a/web/src/components/ui/ModelCooldownDetails.vue +++ b/web/src/components/ui/ModelCooldownDetails.vue @@ -9,50 +9,72 @@ const { locale, t } = useI18n() diff --git a/web/src/features/groups/GroupsView.vue b/web/src/features/groups/GroupsView.vue index 4d5fb867f..49834153d 100644 --- a/web/src/features/groups/GroupsView.vue +++ b/web/src/features/groups/GroupsView.vue @@ -569,6 +569,7 @@ function connectionTypeBadgeClass(type: ConnectionType): string { /> @@ -762,6 +763,10 @@ function connectionTypeBadgeClass(type: ConnectionType): string { gap: var(--space-2); } +.credential-health__model-cooldown { + justify-self: start; +} + .record-actions { display: flex; align-items: center; diff --git a/web/src/features/groups/credentials/GroupCredentialRecord.vue b/web/src/features/groups/credentials/GroupCredentialRecord.vue index c2d8ad35f..cdf88a3bd 100644 --- a/web/src/features/groups/credentials/GroupCredentialRecord.vue +++ b/web/src/features/groups/credentials/GroupCredentialRecord.vue @@ -163,12 +163,14 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { {{ t('group.credentials.columns.status') }} - - {{ t(`group.credentials.effective.${item.effective_status}`) }} - - {{ - t('group.credentials.modelCooldown.count', { count: n(item.model_cooldowns.length) }) - }} +
+ + {{ t(`group.credentials.effective.${item.effective_status}`) }} + + {{ + t('group.credentials.modelCooldown.count', { count: n(item.model_cooldowns.length) }) + }} +
@@ -337,11 +339,12 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { />
+ +
{{ t('group.credentials.diagnostics') }} -
{{ t('group.credentials.detailsFailure') }}
@@ -411,6 +414,13 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { gap: 6px; } +.group-credential-record__status-badges { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 8px; +} + .group-credential-record__weight-none { color: var(--color-text-faint); text-decoration: underline dotted; @@ -473,16 +483,16 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { margin: 0; } -.group-credential-record__details dl div { +.group-credential-record__runtime-details > div { min-width: 0; } -.group-credential-record__details dt { +.group-credential-record__runtime-details dt { color: var(--color-text-faint); font-size: var(--text-label-xs); } -.group-credential-record__details dd { +.group-credential-record__runtime-details dd { margin: 3px 0 0; overflow-wrap: anywhere; font-family: var(--font-mono); @@ -643,7 +653,7 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { } .group-credential-record__mask, - .group-credential-record__recent, + .group-credential-record__status, .group-credential-record__actions { grid-column: 1 / -1; } @@ -657,7 +667,6 @@ function runMenuAction(action: 'test' | 'toggle' | 'restore' | 'remove'): void { gap: 5px; } - .group-credential-record__recent, .group-credential-record__actions { border-top: 1px solid var(--color-border-subtle); padding-top: 11px; diff --git a/web/src/features/groups/credentials/GroupCredentialsTab.vue b/web/src/features/groups/credentials/GroupCredentialsTab.vue index 6131d19dc..e5e5efa58 100644 --- a/web/src/features/groups/credentials/GroupCredentialsTab.vue +++ b/web/src/features/groups/credentials/GroupCredentialsTab.vue @@ -264,10 +264,7 @@ const credentialTestDialogResult = computed(() => { } }) const hasChangedConditions = computed( - () => - filters.value.q !== undefined || - filters.value.status !== undefined || - filters.value.model_cooldown === true, + () => filters.value.q !== undefined || filters.value.status !== undefined, ) const statusSummaryItems = computed(() => { const summary = collection.value?.summary @@ -324,13 +321,7 @@ watch( ) watch( - () => [ - filters.value.status, - filters.value.q, - filters.value.page, - filters.value.page_size, - filters.value.model_cooldown, - ], + () => [filters.value.status, filters.value.q, filters.value.page, filters.value.page_size], () => { selectedIds.value = new Set() }, @@ -379,9 +370,7 @@ function updateRoute( } function setFilter( - patch: Partial< - Pick - >, + patch: Partial>, ): void { updateRoute({ ...filters.value, ...patch, page: 1 }) } @@ -466,7 +455,6 @@ function currentSelectionContext(): string { return JSON.stringify({ groupId: props.groupId, status: filters.value.status ?? null, - modelCooldown: filters.value.model_cooldown ?? false, query: filters.value.q ?? null, page: filters.value.page, pageSize: filters.value.page_size, @@ -1679,23 +1667,8 @@ async function runBatch( -