feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AvailableGroupRef 渠道视图中关联分组的简要信息。
|
2026-04-21 21:44:34 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 用户侧「可用渠道」页面据此展示:专属分组 vs 公开分组(IsExclusive)、
|
|
|
|
|
|
// 订阅 vs 标准(SubscriptionType)、默认倍率(RateMultiplier)。用户专属倍率
|
|
|
|
|
|
// 不在这里暴露,前端自己通过 /groups/rates 拉取,和 API 密钥页面保持一致。
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
type AvailableGroupRef struct {
|
2026-04-21 21:44:34 +08:00
|
|
|
|
ID int64
|
|
|
|
|
|
Name string
|
|
|
|
|
|
Platform string
|
|
|
|
|
|
SubscriptionType string
|
|
|
|
|
|
RateMultiplier float64
|
|
|
|
|
|
IsExclusive bool
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AvailableChannel 可用渠道视图:用于「可用渠道」页面展示渠道基础信息 +
|
|
|
|
|
|
// 关联的分组 + 推导出的支持模型列表(无通配符)。
|
|
|
|
|
|
type AvailableChannel struct {
|
|
|
|
|
|
ID int64
|
|
|
|
|
|
Name string
|
|
|
|
|
|
Description string
|
|
|
|
|
|
Status string
|
|
|
|
|
|
BillingModelSource string
|
|
|
|
|
|
RestrictModels bool
|
|
|
|
|
|
Groups []AvailableGroupRef
|
|
|
|
|
|
SupportedModels []SupportedModel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListAvailable 返回所有渠道的可用视图:每个渠道附带关联分组信息与支持模型列表。
|
|
|
|
|
|
//
|
2026-04-23 00:45:10 +08:00
|
|
|
|
// 支持模型通过 (*Channel).SupportedModels() 计算(mapping ∪ pricing 并联)。
|
|
|
|
|
|
// 对于渠道未配置定价的模型,进一步用 PricingService 的全局 LiteLLM 数据合成
|
|
|
|
|
|
// 一份展示用定价,让用户看到默认价格而非"未配置"。
|
|
|
|
|
|
//
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
// 关联分组信息通过 groupRepo.ListActive 查询后按 ID 映射;渠道 GroupIDs 中未在活跃列表中
|
|
|
|
|
|
// 的分组(已停用或删除)会被忽略。
|
2026-04-21 11:31:54 +08:00
|
|
|
|
//
|
|
|
|
|
|
// 前置条件:s.groupRepo 必须非 nil(由 wire DI 保证)。直接 nil-deref 用于 fail-fast,
|
|
|
|
|
|
// 避免静默掩盖注入缺失。
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel, error) {
|
|
|
|
|
|
channels, err := s.repo.ListAll(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("list channels: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 01:42:18 +08:00
|
|
|
|
groups, err := s.groupRepo.ListActive(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("list active groups: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
groupByID := make(map[int64]AvailableGroupRef, len(groups))
|
|
|
|
|
|
for i := range groups {
|
|
|
|
|
|
g := groups[i]
|
|
|
|
|
|
groupByID[g.ID] = AvailableGroupRef{
|
2026-04-21 21:44:34 +08:00
|
|
|
|
ID: g.ID,
|
|
|
|
|
|
Name: g.Name,
|
|
|
|
|
|
Platform: g.Platform,
|
|
|
|
|
|
SubscriptionType: g.SubscriptionType,
|
|
|
|
|
|
RateMultiplier: g.RateMultiplier,
|
|
|
|
|
|
IsExclusive: g.IsExclusive,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
out := make([]AvailableChannel, 0, len(channels))
|
|
|
|
|
|
for i := range channels {
|
|
|
|
|
|
ch := &channels[i]
|
|
|
|
|
|
groups := make([]AvailableGroupRef, 0, len(ch.GroupIDs))
|
|
|
|
|
|
for _, gid := range ch.GroupIDs {
|
|
|
|
|
|
if ref, ok := groupByID[gid]; ok {
|
|
|
|
|
|
groups = append(groups, ref)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-21 11:31:54 +08:00
|
|
|
|
sort.SliceStable(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
|
2026-04-21 14:10:53 +08:00
|
|
|
|
ch.normalizeBillingModelSource()
|
2026-04-21 01:05:14 +08:00
|
|
|
|
|
2026-04-23 00:45:10 +08:00
|
|
|
|
supported := ch.SupportedModels()
|
|
|
|
|
|
s.fillGlobalPricingFallback(supported)
|
|
|
|
|
|
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
out = append(out, AvailableChannel{
|
|
|
|
|
|
ID: ch.ID,
|
|
|
|
|
|
Name: ch.Name,
|
|
|
|
|
|
Description: ch.Description,
|
|
|
|
|
|
Status: ch.Status,
|
2026-04-21 11:31:54 +08:00
|
|
|
|
BillingModelSource: ch.BillingModelSource,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
RestrictModels: ch.RestrictModels,
|
|
|
|
|
|
Groups: groups,
|
2026-04-23 00:45:10 +08:00
|
|
|
|
SupportedModels: supported,
|
feat(channels): add "Available Channels" aggregate view
Add a read-only aggregate view per channel: its linked groups and a
deterministic wildcard-free supported-model list with pricing details.
Backend
- service.Channel.SupportedModels(): combine ModelMapping keys with
same-platform ModelPricing.Models; trailing "*" keys expand via
pricing prefix match; platforms without a mapping produce no
entries (intentional "no mapping = not shown" rule).
- Extract splitWildcardSuffix() shared with toModelEntry.
- Build a per-call pricing lookup map (platform+lowerName -> *pricing)
to avoid O(N*M) scans in SupportedModels.
- ChannelService.ListAvailable() aggregates channels + active groups;
filters out group IDs no longer active.
- Admin route GET /api/v1/admin/channels/available returns the full
DTO (id, status, billing_model_source, restrict_models, groups,
supported_models).
- User route GET /api/v1/channels/available applies three filters:
Status==active, visible-group intersection, and platform filter
on supported_models (prevents cross-platform leak when a channel
links to both a user-accessible group and an inaccessible one on
another platform). Response is a plain array (matches the
/groups/available sibling shape). Field whitelist omits
billing_model_source, restrict_models, ids, status, sort_order.
Frontend
- New /admin/available-channels and /available-channels views backed
by a shared AvailableChannelsTable component (admin adds status +
billing-source columns via slots).
- PricingRow extracted to its own SFC; SupportedModelChip references
shared billing-mode constants in constants/channel.ts.
- Sidebar: new entry above "渠道管理" for admin; matching entry in
user nav.
- i18n: zh + en coverage for both namespaces.
Tests
- SupportedModels: wildcard-only pricing skipped, prefix-matches-
nothing, cross-platform bleed, case-insensitive dedup, empty
platform mapping.
- ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
case-insensitive name sort.
- User handler: 401 on unauthenticated, visible-group intersection,
platform filter on supported_models, JSON whitelist.
- Admin handler: full DTO including default BillingModelSource
fallback.
Refs: issue #1729
2026-04-21 00:27:10 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
|
|
|
|
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
|
|
|
|
|
})
|
|
|
|
|
|
return out, nil
|
|
|
|
|
|
}
|
2026-04-23 00:45:10 +08:00
|
|
|
|
|
|
|
|
|
|
// fillGlobalPricingFallback 对未命中渠道定价的支持模型,从全局 LiteLLM 数据合成一份
|
|
|
|
|
|
// 展示用定价(按 token 计费)。仅用于「可用渠道」展示,不影响真实计费链路。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 当 s.pricingService 为 nil(测试场景),跳过回落。
|
|
|
|
|
|
func (s *ChannelService) fillGlobalPricingFallback(models []SupportedModel) {
|
|
|
|
|
|
if s.pricingService == nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := range models {
|
|
|
|
|
|
if models[i].Pricing != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
lp := s.pricingService.GetModelPricing(models[i].Name)
|
|
|
|
|
|
if lp == nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
models[i].Pricing = synthesizePricingFromLiteLLM(lp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// synthesizePricingFromLiteLLM 把 LiteLLM 的定价数据转成 ChannelModelPricing 形态,
|
|
|
|
|
|
// 仅用于展示。BillingMode 固定为 token;图片场景的 OutputCostPerImageToken 也归到
|
|
|
|
|
|
// ImageOutputPrice 字段(与渠道侧"图片输出按 token 计价"语义一致)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// LiteLLM 中字段 0 视为未配置,不带入展示。
|
|
|
|
|
|
func synthesizePricingFromLiteLLM(lp *LiteLLMModelPricing) *ChannelModelPricing {
|
|
|
|
|
|
if lp == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return &ChannelModelPricing{
|
|
|
|
|
|
BillingMode: BillingModeToken,
|
|
|
|
|
|
InputPrice: nonZeroPtr(lp.InputCostPerToken),
|
|
|
|
|
|
OutputPrice: nonZeroPtr(lp.OutputCostPerToken),
|
|
|
|
|
|
CacheWritePrice: nonZeroPtr(lp.CacheCreationInputTokenCost),
|
|
|
|
|
|
CacheReadPrice: nonZeroPtr(lp.CacheReadInputTokenCost),
|
|
|
|
|
|
ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func nonZeroPtr(v float64) *float64 {
|
|
|
|
|
|
if v == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return &v
|
|
|
|
|
|
}
|