feat(channel): 渠道管理全链路集成 — 模型映射、定价、限制、用量统计
- 渠道模型映射:支持精确匹配和通配符映射,按平台隔离 - 渠道模型定价:支持 token/按次/图片三种计费模式,区间分层定价 - 模型限制:渠道可限制仅允许定价列表中的模型 - 计费模型来源:支持 requested/upstream 两种计费模型选择 - 用量统计:usage_logs 新增 channel_id/model_mapping_chain/billing_tier/billing_mode 字段 - Dashboard 支持 model_source 维度(requested/upstream/mapping)查看模型统计 - 全部 gateway handler 统一接入 ResolveChannelMappingAndRestrict - 修复测试:同步 SoraGenerationRepository 接口、SQL INSERT 参数、scan 字段
This commit is contained in:
@@ -73,6 +73,45 @@ export function formIntervalsToAPI(intervals: IntervalFormEntry[]): PricingInter
|
||||
}))
|
||||
}
|
||||
|
||||
// ── 模型模式冲突检测 ──────────────────────────────────────
|
||||
|
||||
interface ModelPattern {
|
||||
pattern: string
|
||||
prefix: string // lowercase, 通配符去掉尾部 *
|
||||
wildcard: boolean
|
||||
}
|
||||
|
||||
function toModelPattern(model: string): ModelPattern {
|
||||
const lower = model.toLowerCase()
|
||||
const wildcard = lower.endsWith('*')
|
||||
return {
|
||||
pattern: model,
|
||||
prefix: wildcard ? lower.slice(0, -1) : lower,
|
||||
wildcard,
|
||||
}
|
||||
}
|
||||
|
||||
function patternsConflict(a: ModelPattern, b: ModelPattern): boolean {
|
||||
if (!a.wildcard && !b.wildcard) return a.prefix === b.prefix
|
||||
if (a.wildcard && !b.wildcard) return b.prefix.startsWith(a.prefix)
|
||||
if (!a.wildcard && b.wildcard) return a.prefix.startsWith(b.prefix)
|
||||
// 双通配符:任一前缀是另一前缀的前缀即冲突
|
||||
return a.prefix.startsWith(b.prefix) || b.prefix.startsWith(a.prefix)
|
||||
}
|
||||
|
||||
/** 检测模型模式列表中的冲突,返回冲突的两个模式名;无冲突返回 null */
|
||||
export function findModelConflict(models: string[]): [string, string] | null {
|
||||
const patterns = models.map(toModelPattern)
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
for (let j = i + 1; j < patterns.length; j++) {
|
||||
if (patternsConflict(patterns[i], patterns[j])) {
|
||||
return [patterns[i].pattern, patterns[j].pattern]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 平台对应的模型 tag 样式(背景+文字) */
|
||||
export function getPlatformTagClass(platform: string): string {
|
||||
switch (platform) {
|
||||
|
||||
@@ -161,6 +161,7 @@ const props = withDefaults(
|
||||
showSourceToggle?: boolean
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
filters?: Record<string, any>
|
||||
}>(),
|
||||
{
|
||||
upstreamEndpointStats: () => [],
|
||||
@@ -193,6 +194,7 @@ const toggleBreakdown = async (endpoint: string) => {
|
||||
breakdownItems.value = []
|
||||
try {
|
||||
const res = await getUserBreakdown({
|
||||
...props.filters,
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
endpoint,
|
||||
|
||||
@@ -125,6 +125,7 @@ const props = withDefaults(defineProps<{
|
||||
showMetricToggle?: boolean
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
filters?: Record<string, any>
|
||||
}>(), {
|
||||
loading: false,
|
||||
metric: 'tokens',
|
||||
@@ -150,6 +151,7 @@ const toggleBreakdown = async (type: string, id: number | string) => {
|
||||
breakdownItems.value = []
|
||||
try {
|
||||
const res = await getUserBreakdown({
|
||||
...props.filters,
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
group_id: Number(id),
|
||||
|
||||
@@ -270,6 +270,7 @@ const props = withDefaults(defineProps<{
|
||||
rankingError?: boolean
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
filters?: Record<string, any>
|
||||
}>(), {
|
||||
upstreamModelStats: () => [],
|
||||
mappingModelStats: () => [],
|
||||
@@ -302,6 +303,7 @@ const toggleBreakdown = async (type: string, id: string) => {
|
||||
breakdownItems.value = []
|
||||
try {
|
||||
const res = await getUserBreakdown({
|
||||
...props.filters,
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
model: id,
|
||||
|
||||
Reference in New Issue
Block a user