mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-02 15:52:26 +08:00
feat: add daily model statistics aggregation with stats_daily_model table
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
"""add stats_daily_model table and rename provider_model_aliases
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: f30f9936f6a2
|
||||||
|
Create Date: 2025-12-20 12:00:00.000000+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a1b2c3d4e5f6'
|
||||||
|
down_revision = 'f30f9936f6a2'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def table_exists(table_name: str) -> bool:
|
||||||
|
"""检查表是否存在"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
return table_name in inspector.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def column_exists(table_name: str, column_name: str) -> bool:
|
||||||
|
"""检查列是否存在"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||||
|
return column_name in columns
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""创建 stats_daily_model 表,重命名 provider_model_aliases 为 provider_model_mappings"""
|
||||||
|
# 1. 创建 stats_daily_model 表
|
||||||
|
if not table_exists('stats_daily_model'):
|
||||||
|
op.create_table(
|
||||||
|
'stats_daily_model',
|
||||||
|
sa.Column('id', sa.String(36), primary_key=True),
|
||||||
|
sa.Column('date', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('model', sa.String(100), nullable=False),
|
||||||
|
sa.Column('total_requests', sa.Integer(), nullable=False, default=0),
|
||||||
|
sa.Column('input_tokens', sa.BigInteger(), nullable=False, default=0),
|
||||||
|
sa.Column('output_tokens', sa.BigInteger(), nullable=False, default=0),
|
||||||
|
sa.Column('cache_creation_tokens', sa.BigInteger(), nullable=False, default=0),
|
||||||
|
sa.Column('cache_read_tokens', sa.BigInteger(), nullable=False, default=0),
|
||||||
|
sa.Column('total_cost', sa.Float(), nullable=False, default=0.0),
|
||||||
|
sa.Column('avg_response_time_ms', sa.Float(), nullable=False, default=0.0),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False,
|
||||||
|
server_default=sa.func.now()),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False,
|
||||||
|
server_default=sa.func.now(), onupdate=sa.func.now()),
|
||||||
|
sa.UniqueConstraint('date', 'model', name='uq_stats_daily_model'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建索引
|
||||||
|
op.create_index('idx_stats_daily_model_date', 'stats_daily_model', ['date'])
|
||||||
|
op.create_index('idx_stats_daily_model_date_model', 'stats_daily_model', ['date', 'model'])
|
||||||
|
|
||||||
|
# 2. 重命名 models 表的 provider_model_aliases 为 provider_model_mappings
|
||||||
|
if column_exists('models', 'provider_model_aliases') and not column_exists('models', 'provider_model_mappings'):
|
||||||
|
op.alter_column('models', 'provider_model_aliases', new_column_name='provider_model_mappings')
|
||||||
|
|
||||||
|
|
||||||
|
def index_exists(table_name: str, index_name: str) -> bool:
|
||||||
|
"""检查索引是否存在"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
indexes = [idx['name'] for idx in inspector.get_indexes(table_name)]
|
||||||
|
return index_name in indexes
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""删除 stats_daily_model 表,恢复 provider_model_aliases 列名"""
|
||||||
|
# 恢复列名
|
||||||
|
if column_exists('models', 'provider_model_mappings') and not column_exists('models', 'provider_model_aliases'):
|
||||||
|
op.alter_column('models', 'provider_model_mappings', new_column_name='provider_model_aliases')
|
||||||
|
|
||||||
|
# 删除表
|
||||||
|
if table_exists('stats_daily_model'):
|
||||||
|
if index_exists('stats_daily_model', 'idx_stats_daily_model_date_model'):
|
||||||
|
op.drop_index('idx_stats_daily_model_date_model', table_name='stats_daily_model')
|
||||||
|
if index_exists('stats_daily_model', 'idx_stats_daily_model_date'):
|
||||||
|
op.drop_index('idx_stats_daily_model_date', table_name='stats_daily_model')
|
||||||
|
op.drop_table('stats_daily_model')
|
||||||
@@ -112,7 +112,7 @@ export interface KeyExport {
|
|||||||
export interface ModelExport {
|
export interface ModelExport {
|
||||||
global_model_name: string | null
|
global_model_name: string | null
|
||||||
provider_model_name: string
|
provider_model_name: string
|
||||||
provider_model_aliases?: any
|
provider_model_mappings?: any
|
||||||
price_per_request?: number | null
|
price_per_request?: number | null
|
||||||
tiered_pricing?: any
|
tiered_pricing?: any
|
||||||
supports_vision?: boolean | null
|
supports_vision?: boolean | null
|
||||||
|
|||||||
@@ -244,18 +244,21 @@ export interface ConcurrencyStatus {
|
|||||||
key_max_concurrent?: number
|
key_max_concurrent?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderModelAlias {
|
export interface ProviderModelMapping {
|
||||||
name: string
|
name: string
|
||||||
priority: number // 优先级(数字越小优先级越高)
|
priority: number // 优先级(数字越小优先级越高)
|
||||||
api_formats?: string[] // 作用域(适用的 API 格式),为空表示对所有格式生效
|
api_formats?: string[] // 作用域(适用的 API 格式),为空表示对所有格式生效
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保留别名以保持向后兼容
|
||||||
|
export type ProviderModelAlias = ProviderModelMapping
|
||||||
|
|
||||||
export interface Model {
|
export interface Model {
|
||||||
id: string
|
id: string
|
||||||
provider_id: string
|
provider_id: string
|
||||||
global_model_id?: string // 关联的 GlobalModel ID
|
global_model_id?: string // 关联的 GlobalModel ID
|
||||||
provider_model_name: string // Provider 侧的主模型名称
|
provider_model_name: string // Provider 侧的主模型名称
|
||||||
provider_model_aliases?: ProviderModelAlias[] | null // 模型名称别名列表(带优先级)
|
provider_model_mappings?: ProviderModelMapping[] | null // 模型名称映射列表(带优先级)
|
||||||
// 原始配置值(可能为空,为空时使用 GlobalModel 默认值)
|
// 原始配置值(可能为空,为空时使用 GlobalModel 默认值)
|
||||||
price_per_request?: number | null // 按次计费价格
|
price_per_request?: number | null // 按次计费价格
|
||||||
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
||||||
@@ -285,7 +288,7 @@ export interface Model {
|
|||||||
|
|
||||||
export interface ModelCreate {
|
export interface ModelCreate {
|
||||||
provider_model_name: string // Provider 侧的主模型名称
|
provider_model_name: string // Provider 侧的主模型名称
|
||||||
provider_model_aliases?: ProviderModelAlias[] // 模型名称别名列表(带优先级)
|
provider_model_mappings?: ProviderModelMapping[] // 模型名称映射列表(带优先级)
|
||||||
global_model_id: string // 关联的 GlobalModel ID(必填)
|
global_model_id: string // 关联的 GlobalModel ID(必填)
|
||||||
// 计费配置(可选,为空时使用 GlobalModel 默认值)
|
// 计费配置(可选,为空时使用 GlobalModel 默认值)
|
||||||
price_per_request?: number // 按次计费价格
|
price_per_request?: number // 按次计费价格
|
||||||
@@ -302,7 +305,7 @@ export interface ModelCreate {
|
|||||||
|
|
||||||
export interface ModelUpdate {
|
export interface ModelUpdate {
|
||||||
provider_model_name?: string
|
provider_model_name?: string
|
||||||
provider_model_aliases?: ProviderModelAlias[] | null // 模型名称别名列表(带优先级)
|
provider_model_mappings?: ProviderModelMapping[] | null // 模型名称映射列表(带优先级)
|
||||||
global_model_id?: string
|
global_model_id?: string
|
||||||
price_per_request?: number | null // 按次计费价格(null 表示清空/使用默认值)
|
price_per_request?: number | null // 按次计费价格(null 表示清空/使用默认值)
|
||||||
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
tiered_pricing?: TieredPricingConfig | null // 阶梯计费配置
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 别名列表 -->
|
<!-- 映射列表 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Label class="text-sm font-medium">名称映射</Label>
|
<Label class="text-sm font-medium">名称映射</Label>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 别名输入框 -->
|
<!-- 映射输入框 -->
|
||||||
<Input
|
<Input
|
||||||
v-model="alias.name"
|
v-model="alias.name"
|
||||||
placeholder="映射名称,如 Claude-Sonnet-4.5"
|
placeholder="映射名称,如 Claude-Sonnet-4.5"
|
||||||
@@ -184,9 +184,9 @@ const editingPriorityIndex = ref<number | null>(null)
|
|||||||
// 监听 open 变化
|
// 监听 open 变化
|
||||||
watch(() => props.open, (newOpen) => {
|
watch(() => props.open, (newOpen) => {
|
||||||
if (newOpen && props.model) {
|
if (newOpen && props.model) {
|
||||||
// 加载现有别名配置
|
// 加载现有映射配置
|
||||||
if (props.model.provider_model_aliases && Array.isArray(props.model.provider_model_aliases)) {
|
if (props.model.provider_model_mappings && Array.isArray(props.model.provider_model_mappings)) {
|
||||||
aliases.value = JSON.parse(JSON.stringify(props.model.provider_model_aliases))
|
aliases.value = JSON.parse(JSON.stringify(props.model.provider_model_mappings))
|
||||||
} else {
|
} else {
|
||||||
aliases.value = []
|
aliases.value = []
|
||||||
}
|
}
|
||||||
@@ -197,16 +197,16 @@ watch(() => props.open, (newOpen) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加别名
|
// 添加映射
|
||||||
function addAlias() {
|
function addAlias() {
|
||||||
// 新别名优先级为当前最大优先级 + 1,或者默认为 1
|
// 新映射优先级为当前最大优先级 + 1,或者默认为 1
|
||||||
const maxPriority = aliases.value.length > 0
|
const maxPriority = aliases.value.length > 0
|
||||||
? Math.max(...aliases.value.map(a => a.priority))
|
? Math.max(...aliases.value.map(a => a.priority))
|
||||||
: 0
|
: 0
|
||||||
aliases.value.push({ name: '', priority: maxPriority + 1 })
|
aliases.value.push({ name: '', priority: maxPriority + 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除别名
|
// 移除映射
|
||||||
function removeAlias(index: number) {
|
function removeAlias(index: number) {
|
||||||
aliases.value.splice(index, 1)
|
aliases.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ function handleDrop(targetIndex: number) {
|
|||||||
const items = [...aliases.value]
|
const items = [...aliases.value]
|
||||||
const draggedItem = items[dragIndex]
|
const draggedItem = items[dragIndex]
|
||||||
|
|
||||||
// 记录每个别名的原始优先级(在修改前)
|
// 记录每个映射的原始优先级(在修改前)
|
||||||
const originalPriorityMap = new Map<number, number>()
|
const originalPriorityMap = new Map<number, number>()
|
||||||
items.forEach((alias, idx) => {
|
items.forEach((alias, idx) => {
|
||||||
originalPriorityMap.set(idx, alias.priority)
|
originalPriorityMap.set(idx, alias.priority)
|
||||||
@@ -255,7 +255,7 @@ function handleDrop(targetIndex: number) {
|
|||||||
items.splice(targetIndex, 0, draggedItem)
|
items.splice(targetIndex, 0, draggedItem)
|
||||||
|
|
||||||
// 按新顺序为每个组分配新的优先级
|
// 按新顺序为每个组分配新的优先级
|
||||||
// 同组的别名保持相同的优先级(被拖动的别名单独成组)
|
// 同组的映射保持相同的优先级(被拖动的映射单独成组)
|
||||||
const groupNewPriority = new Map<number, number>() // 原优先级 -> 新优先级
|
const groupNewPriority = new Map<number, number>() // 原优先级 -> 新优先级
|
||||||
let currentPriority = 1
|
let currentPriority = 1
|
||||||
|
|
||||||
@@ -263,12 +263,12 @@ function handleDrop(targetIndex: number) {
|
|||||||
const draggedOriginalPriority = originalPriorityMap.get(dragIndex)!
|
const draggedOriginalPriority = originalPriorityMap.get(dragIndex)!
|
||||||
|
|
||||||
items.forEach((alias, newIdx) => {
|
items.forEach((alias, newIdx) => {
|
||||||
// 找到这个别名在原数组中的索引
|
// 找到这个映射在原数组中的索引
|
||||||
const originalIdx = aliases.value.findIndex(a => a === alias)
|
const originalIdx = aliases.value.findIndex(a => a === alias)
|
||||||
const originalPriority = originalIdx >= 0 ? originalPriorityMap.get(originalIdx)! : alias.priority
|
const originalPriority = originalIdx >= 0 ? originalPriorityMap.get(originalIdx)! : alias.priority
|
||||||
|
|
||||||
if (alias === draggedItem) {
|
if (alias === draggedItem) {
|
||||||
// 被拖动的别名是独立的新组,获得当前优先级
|
// 被拖动的映射是独立的新组,获得当前优先级
|
||||||
alias.priority = currentPriority
|
alias.priority = currentPriority
|
||||||
currentPriority++
|
currentPriority++
|
||||||
} else {
|
} else {
|
||||||
@@ -318,11 +318,11 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
// 过滤掉空的别名
|
// 过滤掉空的映射
|
||||||
const validAliases = aliases.value.filter(a => a.name.trim())
|
const validAliases = aliases.value.filter(a => a.name.trim())
|
||||||
|
|
||||||
await updateModel(props.providerId, props.model.id, {
|
await updateModel(props.providerId, props.model.id, {
|
||||||
provider_model_aliases: validAliases.length > 0 ? validAliases : null
|
provider_model_mappings: validAliases.length > 0 ? validAliases : null
|
||||||
})
|
})
|
||||||
|
|
||||||
showSuccess('映射配置已保存')
|
showSuccess('映射配置已保存')
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ const formData = ref<{
|
|||||||
aliases: []
|
aliases: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查是否有有效的别名
|
// 检查是否有有效的映射
|
||||||
const hasValidAliases = computed(() => {
|
const hasValidAliases = computed(() => {
|
||||||
return formData.value.aliases.some(a => a.name.trim())
|
return formData.value.aliases.some(a => a.name.trim())
|
||||||
})
|
})
|
||||||
@@ -538,7 +538,7 @@ function toggleGroupCollapse(apiFormat: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加别名项
|
// 添加映射项
|
||||||
function addAliasItem() {
|
function addAliasItem() {
|
||||||
const maxPriority = formData.value.aliases.length > 0
|
const maxPriority = formData.value.aliases.length > 0
|
||||||
? Math.max(...formData.value.aliases.map(a => a.priority))
|
? Math.max(...formData.value.aliases.map(a => a.priority))
|
||||||
@@ -546,7 +546,7 @@ function addAliasItem() {
|
|||||||
formData.value.aliases.push({ name: '', priority: maxPriority + 1, isEditing: true })
|
formData.value.aliases.push({ name: '', priority: maxPriority + 1, isEditing: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除别名项
|
// 删除映射项
|
||||||
function removeAliasItem(index: number) {
|
function removeAliasItem(index: number) {
|
||||||
formData.value.aliases.splice(index, 1)
|
formData.value.aliases.splice(index, 1)
|
||||||
}
|
}
|
||||||
@@ -719,7 +719,7 @@ async function handleSubmit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentAliases = targetModel.provider_model_aliases || []
|
const currentAliases = targetModel.provider_model_mappings || []
|
||||||
let newAliases: ProviderModelAlias[]
|
let newAliases: ProviderModelAlias[]
|
||||||
|
|
||||||
const buildAlias = (a: FormAlias): ProviderModelAlias => ({
|
const buildAlias = (a: FormAlias): ProviderModelAlias => ({
|
||||||
@@ -762,7 +762,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await updateModel(props.providerId, targetModel.id, {
|
await updateModel(props.providerId, targetModel.id, {
|
||||||
provider_model_aliases: newAliases
|
provider_model_mappings: newAliases
|
||||||
})
|
})
|
||||||
|
|
||||||
showSuccess(props.editingGroup ? '映射组已更新' : '映射已添加')
|
showSuccess(props.editingGroup ? '映射组已更新' : '映射已添加')
|
||||||
|
|||||||
@@ -101,24 +101,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 展开的别名列表 -->
|
<!-- 展开的映射列表 -->
|
||||||
<div
|
<div
|
||||||
v-show="expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`)"
|
v-show="expandedAliasGroups.has(`${group.model.id}-${group.apiFormatsKey}`)"
|
||||||
class="bg-muted/30 border-t border-border/30"
|
class="bg-muted/30 border-t border-border/30"
|
||||||
>
|
>
|
||||||
<div class="px-4 py-2 space-y-1">
|
<div class="px-4 py-2 space-y-1">
|
||||||
<div
|
<div
|
||||||
v-for="alias in group.aliases"
|
v-for="mapping in group.aliases"
|
||||||
:key="alias.name"
|
:key="mapping.name"
|
||||||
class="flex items-center gap-2 py-1"
|
class="flex items-center gap-2 py-1"
|
||||||
>
|
>
|
||||||
<!-- 优先级标签 -->
|
<!-- 优先级标签 -->
|
||||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-background border text-xs font-medium shrink-0">
|
<span class="inline-flex items-center justify-center w-5 h-5 rounded bg-background border text-xs font-medium shrink-0">
|
||||||
{{ alias.priority }}
|
{{ mapping.priority }}
|
||||||
</span>
|
</span>
|
||||||
<!-- 别名名称 -->
|
<!-- 映射名称 -->
|
||||||
<span class="font-mono text-sm truncate">
|
<span class="font-mono text-sm truncate">
|
||||||
{{ alias.name }}
|
{{ mapping.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,9 +222,9 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
|||||||
const groupMap = new Map<string, AliasGroup>()
|
const groupMap = new Map<string, AliasGroup>()
|
||||||
|
|
||||||
for (const model of models.value) {
|
for (const model of models.value) {
|
||||||
if (!model.provider_model_aliases || !Array.isArray(model.provider_model_aliases)) continue
|
if (!model.provider_model_mappings || !Array.isArray(model.provider_model_mappings)) continue
|
||||||
|
|
||||||
for (const alias of model.provider_model_aliases) {
|
for (const alias of model.provider_model_mappings) {
|
||||||
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
const apiFormatsKey = getApiFormatsKey(alias.api_formats)
|
||||||
const groupKey = `${model.id}|${apiFormatsKey}`
|
const groupKey = `${model.id}|${apiFormatsKey}`
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ async function confirmDelete() {
|
|||||||
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
const { model, aliases, apiFormatsKey } = deletingGroup.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentAliases = model.provider_model_aliases || []
|
const currentAliases = model.provider_model_mappings || []
|
||||||
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
const aliasNamesToRemove = new Set(aliases.map(a => a.name))
|
||||||
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
const newAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||||
const currentKey = getApiFormatsKey(a.api_formats)
|
const currentKey = getApiFormatsKey(a.api_formats)
|
||||||
@@ -318,7 +318,7 @@ async function confirmDelete() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await updateModel(props.provider.id, model.id, {
|
await updateModel(props.provider.id, model.id, {
|
||||||
provider_model_aliases: newAliases.length > 0 ? newAliases : null
|
provider_model_mappings: newAliases.length > 0 ? newAliases : null
|
||||||
})
|
})
|
||||||
|
|
||||||
showSuccess('映射组已删除')
|
showSuccess('映射组已删除')
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ function getUsageRecords() {
|
|||||||
return cachedUsageRecords
|
return cachedUsageRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock 别名数据
|
// Mock 映射数据
|
||||||
const MOCK_ALIASES = [
|
const MOCK_ALIASES = [
|
||||||
{ id: 'alias-001', source_model: 'claude-4-sonnet', target_global_model_id: 'gm-001', target_global_model_name: 'claude-sonnet-4-20250514', target_global_model_display_name: 'Claude Sonnet 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
{ id: 'alias-001', source_model: 'claude-4-sonnet', target_global_model_id: 'gm-001', target_global_model_name: 'claude-sonnet-4-20250514', target_global_model_display_name: 'Claude Sonnet 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||||||
{ id: 'alias-002', source_model: 'claude-4-opus', target_global_model_id: 'gm-002', target_global_model_name: 'claude-opus-4-20250514', target_global_model_display_name: 'Claude Opus 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
{ id: 'alias-002', source_model: 'claude-4-opus', target_global_model_id: 'gm-002', target_global_model_name: 'claude-opus-4-20250514', target_global_model_display_name: 'Claude Opus 4', provider_id: null, provider_name: null, scope: 'global', mapping_type: 'alias', is_active: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' },
|
||||||
@@ -1682,7 +1682,7 @@ registerDynamicRoute('GET', '/api/admin/models/mappings/:mappingId', async (_con
|
|||||||
requireAdmin()
|
requireAdmin()
|
||||||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
throw { response: createMockResponse({ detail: '映射不存在' }, 404) }
|
||||||
}
|
}
|
||||||
return createMockResponse(alias)
|
return createMockResponse(alias)
|
||||||
})
|
})
|
||||||
@@ -1693,7 +1693,7 @@ registerDynamicRoute('PATCH', '/api/admin/models/mappings/:mappingId', async (co
|
|||||||
requireAdmin()
|
requireAdmin()
|
||||||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
throw { response: createMockResponse({ detail: '映射不存在' }, 404) }
|
||||||
}
|
}
|
||||||
const body = JSON.parse(config.data || '{}')
|
const body = JSON.parse(config.data || '{}')
|
||||||
return createMockResponse({ ...alias, ...body, updated_at: new Date().toISOString() })
|
return createMockResponse({ ...alias, ...body, updated_at: new Date().toISOString() })
|
||||||
@@ -1705,7 +1705,7 @@ registerDynamicRoute('DELETE', '/api/admin/models/mappings/:mappingId', async (_
|
|||||||
requireAdmin()
|
requireAdmin()
|
||||||
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
const alias = MOCK_ALIASES.find(a => a.id === params.mappingId)
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
throw { response: createMockResponse({ detail: '别名不存在' }, 404) }
|
throw { response: createMockResponse({ detail: '映射不存在' }, 404) }
|
||||||
}
|
}
|
||||||
return createMockResponse({ message: '删除成功(演示模式)' })
|
return createMockResponse({ message: '删除成功(演示模式)' })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1167,14 +1167,14 @@ class AdminModelMappingCacheStatsAdapter(AdminApiAdapter):
|
|||||||
provider.display_name or provider.name
|
provider.display_name or provider.name
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
# 检查是否在别名列表中
|
# 检查是否在映射列表中
|
||||||
if model.provider_model_aliases:
|
if model.provider_model_mappings:
|
||||||
alias_names = [
|
mapping_list = [
|
||||||
a.get("name")
|
a.get("name")
|
||||||
for a in model.provider_model_aliases
|
for a in model.provider_model_mappings
|
||||||
if isinstance(a, dict)
|
if isinstance(a, dict)
|
||||||
]
|
]
|
||||||
if mapping_name in alias_names:
|
if mapping_name in mapping_list:
|
||||||
provider_names.append(
|
provider_names.append(
|
||||||
provider.display_name or provider.name
|
provider.display_name or provider.name
|
||||||
)
|
)
|
||||||
@@ -1236,19 +1236,19 @@ class AdminModelMappingCacheStatsAdapter(AdminApiAdapter):
|
|||||||
try:
|
try:
|
||||||
cached_data = json.loads(cached_str)
|
cached_data = json.loads(cached_str)
|
||||||
provider_model_name = cached_data.get("provider_model_name")
|
provider_model_name = cached_data.get("provider_model_name")
|
||||||
provider_model_aliases = cached_data.get("provider_model_aliases", [])
|
provider_model_mappings = cached_data.get("provider_model_mappings", [])
|
||||||
|
|
||||||
# 获取 Provider 和 GlobalModel 信息
|
# 获取 Provider 和 GlobalModel 信息
|
||||||
provider = provider_map.get(provider_id)
|
provider = provider_map.get(provider_id)
|
||||||
global_model = global_model_map.get(global_model_id)
|
global_model = global_model_map.get(global_model_id)
|
||||||
|
|
||||||
if provider and global_model:
|
if provider and global_model:
|
||||||
# 提取别名名称
|
# 提取映射名称
|
||||||
alias_names = []
|
mapping_names = []
|
||||||
if provider_model_aliases:
|
if provider_model_mappings:
|
||||||
for alias_entry in provider_model_aliases:
|
for mapping_entry in provider_model_mappings:
|
||||||
if isinstance(alias_entry, dict) and alias_entry.get("name"):
|
if isinstance(mapping_entry, dict) and mapping_entry.get("name"):
|
||||||
alias_names.append(alias_entry["name"])
|
mapping_names.append(mapping_entry["name"])
|
||||||
|
|
||||||
# provider_model_name 为空时跳过
|
# provider_model_name 为空时跳过
|
||||||
if not provider_model_name:
|
if not provider_model_name:
|
||||||
@@ -1256,14 +1256,14 @@ class AdminModelMappingCacheStatsAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# 只显示有实际映射的条目:
|
# 只显示有实际映射的条目:
|
||||||
# 1. 全局模型名 != Provider 模型名(模型名称映射)
|
# 1. 全局模型名 != Provider 模型名(模型名称映射)
|
||||||
# 2. 或者有别名配置
|
# 2. 或者有映射配置
|
||||||
has_name_mapping = global_model.name != provider_model_name
|
has_name_mapping = global_model.name != provider_model_name
|
||||||
has_aliases = len(alias_names) > 0
|
has_mappings = len(mapping_names) > 0
|
||||||
|
|
||||||
if has_name_mapping or has_aliases:
|
if has_name_mapping or has_mappings:
|
||||||
# 构建用于展示的别名列表
|
# 构建用于展示的映射列表
|
||||||
# 如果只有名称映射没有别名,则用 global_model_name 作为"请求名称"
|
# 如果只有名称映射没有额外映射,则用 global_model_name 作为"请求名称"
|
||||||
display_aliases = alias_names if alias_names else [global_model.name]
|
display_mappings = mapping_names if mapping_names else [global_model.name]
|
||||||
|
|
||||||
provider_model_mappings.append({
|
provider_model_mappings.append({
|
||||||
"provider_id": provider_id,
|
"provider_id": provider_id,
|
||||||
@@ -1272,7 +1272,7 @@ class AdminModelMappingCacheStatsAdapter(AdminApiAdapter):
|
|||||||
"global_model_name": global_model.name,
|
"global_model_name": global_model.name,
|
||||||
"global_model_display_name": global_model.display_name,
|
"global_model_display_name": global_model.display_name,
|
||||||
"provider_model_name": provider_model_name,
|
"provider_model_name": provider_model_name,
|
||||||
"aliases": display_aliases,
|
"aliases": display_mappings,
|
||||||
"ttl": ttl if ttl > 0 else None,
|
"ttl": ttl if ttl > 0 else None,
|
||||||
"hit_count": hit_count,
|
"hit_count": hit_count,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
{
|
{
|
||||||
"global_model_name": global_model.name if global_model else None,
|
"global_model_name": global_model.name if global_model else None,
|
||||||
"provider_model_name": model.provider_model_name,
|
"provider_model_name": model.provider_model_name,
|
||||||
"provider_model_aliases": model.provider_model_aliases,
|
"provider_model_mappings": model.provider_model_mappings,
|
||||||
"price_per_request": model.price_per_request,
|
"price_per_request": model.price_per_request,
|
||||||
"tiered_pricing": model.tiered_pricing,
|
"tiered_pricing": model.tiered_pricing,
|
||||||
"supports_vision": model.supports_vision,
|
"supports_vision": model.supports_vision,
|
||||||
@@ -790,8 +790,8 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
)
|
)
|
||||||
elif merge_mode == "overwrite":
|
elif merge_mode == "overwrite":
|
||||||
existing_model.global_model_id = global_model_id
|
existing_model.global_model_id = global_model_id
|
||||||
existing_model.provider_model_aliases = model_data.get(
|
existing_model.provider_model_mappings = model_data.get(
|
||||||
"provider_model_aliases"
|
"provider_model_mappings"
|
||||||
)
|
)
|
||||||
existing_model.price_per_request = model_data.get(
|
existing_model.price_per_request = model_data.get(
|
||||||
"price_per_request"
|
"price_per_request"
|
||||||
@@ -824,8 +824,8 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
global_model_id=global_model_id,
|
global_model_id=global_model_id,
|
||||||
provider_model_name=model_data["provider_model_name"],
|
provider_model_name=model_data["provider_model_name"],
|
||||||
provider_model_aliases=model_data.get(
|
provider_model_mappings=model_data.get(
|
||||||
"provider_model_aliases"
|
"provider_model_mappings"
|
||||||
),
|
),
|
||||||
price_per_request=model_data.get("price_per_request"),
|
price_per_request=model_data.get("price_per_request"),
|
||||||
tiered_pricing=model_data.get("tiered_pricing"),
|
tiered_pricing=model_data.get("tiered_pricing"),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from src.api.base.admin_adapter import AdminApiAdapter
|
|||||||
from src.api.base.pipeline import ApiRequestPipeline
|
from src.api.base.pipeline import ApiRequestPipeline
|
||||||
from src.core.enums import UserRole
|
from src.core.enums import UserRole
|
||||||
from src.database import get_db
|
from src.database import get_db
|
||||||
from src.models.database import ApiKey, Provider, RequestCandidate, StatsDaily, Usage
|
from src.models.database import ApiKey, Provider, RequestCandidate, StatsDaily, StatsDailyModel, Usage
|
||||||
from src.models.database import User as DBUser
|
from src.models.database import User as DBUser
|
||||||
from src.services.system.stats_aggregator import StatsAggregatorService
|
from src.services.system.stats_aggregator import StatsAggregatorService
|
||||||
from src.utils.cache_decorator import cache_result
|
from src.utils.cache_decorator import cache_result
|
||||||
@@ -893,69 +893,172 @@ class DashboardDailyStatsAdapter(DashboardAdapter):
|
|||||||
})
|
})
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
# ==================== 模型统计(仍需实时查询)====================
|
# ==================== 模型统计 ====================
|
||||||
model_query = db.query(Usage)
|
if is_admin:
|
||||||
if not is_admin:
|
# 管理员:使用预聚合数据 + 今日实时数据
|
||||||
model_query = model_query.filter(Usage.user_id == user.id)
|
# 历史数据从 stats_daily_model 获取
|
||||||
model_query = model_query.filter(
|
historical_model_stats = (
|
||||||
and_(Usage.created_at >= start_date, Usage.created_at <= end_date)
|
db.query(StatsDailyModel)
|
||||||
)
|
.filter(and_(StatsDailyModel.date >= start_date, StatsDailyModel.date < today))
|
||||||
|
.all()
|
||||||
model_stats = (
|
|
||||||
model_query.with_entities(
|
|
||||||
Usage.model,
|
|
||||||
func.count(Usage.id).label("requests"),
|
|
||||||
func.sum(Usage.total_tokens).label("tokens"),
|
|
||||||
func.sum(Usage.total_cost_usd).label("cost"),
|
|
||||||
func.avg(Usage.response_time_ms).label("avg_response_time"),
|
|
||||||
)
|
)
|
||||||
.group_by(Usage.model)
|
|
||||||
.order_by(func.sum(Usage.total_cost_usd).desc())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
model_summary = [
|
# 按模型汇总历史数据
|
||||||
{
|
model_agg: dict = {}
|
||||||
"model": stat.model,
|
daily_breakdown: dict = {}
|
||||||
"requests": stat.requests or 0,
|
|
||||||
"tokens": int(stat.tokens or 0),
|
|
||||||
"cost": float(stat.cost or 0),
|
|
||||||
"avg_response_time": (
|
|
||||||
float(stat.avg_response_time or 0) / 1000.0 if stat.avg_response_time else 0
|
|
||||||
),
|
|
||||||
"cost_per_request": float(stat.cost or 0) / max(stat.requests or 1, 1),
|
|
||||||
"tokens_per_request": int(stat.tokens or 0) / max(stat.requests or 1, 1),
|
|
||||||
}
|
|
||||||
for stat in model_stats
|
|
||||||
]
|
|
||||||
|
|
||||||
daily_model_stats = (
|
for stat in historical_model_stats:
|
||||||
model_query.with_entities(
|
model = stat.model
|
||||||
func.date(Usage.created_at).label("date"),
|
if model not in model_agg:
|
||||||
Usage.model,
|
model_agg[model] = {
|
||||||
func.count(Usage.id).label("requests"),
|
"requests": 0, "tokens": 0, "cost": 0.0,
|
||||||
func.sum(Usage.total_tokens).label("tokens"),
|
"total_response_time": 0.0, "response_count": 0
|
||||||
func.sum(Usage.total_cost_usd).label("cost"),
|
}
|
||||||
|
model_agg[model]["requests"] += stat.total_requests
|
||||||
|
tokens = (stat.input_tokens + stat.output_tokens +
|
||||||
|
stat.cache_creation_tokens + stat.cache_read_tokens)
|
||||||
|
model_agg[model]["tokens"] += tokens
|
||||||
|
model_agg[model]["cost"] += stat.total_cost
|
||||||
|
if stat.avg_response_time_ms is not None:
|
||||||
|
model_agg[model]["total_response_time"] += stat.avg_response_time_ms * stat.total_requests
|
||||||
|
model_agg[model]["response_count"] += stat.total_requests
|
||||||
|
|
||||||
|
# 按日期分组
|
||||||
|
if stat.date.tzinfo is None:
|
||||||
|
date_utc = stat.date.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
date_utc = stat.date.astimezone(timezone.utc)
|
||||||
|
date_str = date_utc.astimezone(app_tz).date().isoformat()
|
||||||
|
|
||||||
|
daily_breakdown.setdefault(date_str, []).append({
|
||||||
|
"model": model,
|
||||||
|
"requests": stat.total_requests,
|
||||||
|
"tokens": tokens,
|
||||||
|
"cost": stat.total_cost,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 今日实时模型统计
|
||||||
|
today_model_stats = (
|
||||||
|
db.query(
|
||||||
|
Usage.model,
|
||||||
|
func.count(Usage.id).label("requests"),
|
||||||
|
func.sum(Usage.total_tokens).label("tokens"),
|
||||||
|
func.sum(Usage.total_cost_usd).label("cost"),
|
||||||
|
func.avg(Usage.response_time_ms).label("avg_response_time"),
|
||||||
|
)
|
||||||
|
.filter(Usage.created_at >= today)
|
||||||
|
.group_by(Usage.model)
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
.group_by(func.date(Usage.created_at), Usage.model)
|
|
||||||
.order_by(func.date(Usage.created_at).desc(), func.sum(Usage.total_cost_usd).desc())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
breakdown = {}
|
today_str = today_local.date().isoformat()
|
||||||
for stat in daily_model_stats:
|
for stat in today_model_stats:
|
||||||
date_str = stat.date.isoformat()
|
model = stat.model
|
||||||
breakdown.setdefault(date_str, []).append(
|
if model not in model_agg:
|
||||||
|
model_agg[model] = {
|
||||||
|
"requests": 0, "tokens": 0, "cost": 0.0,
|
||||||
|
"total_response_time": 0.0, "response_count": 0
|
||||||
|
}
|
||||||
|
model_agg[model]["requests"] += stat.requests or 0
|
||||||
|
model_agg[model]["tokens"] += int(stat.tokens or 0)
|
||||||
|
model_agg[model]["cost"] += float(stat.cost or 0)
|
||||||
|
if stat.avg_response_time is not None:
|
||||||
|
model_agg[model]["total_response_time"] += float(stat.avg_response_time) * (stat.requests or 0)
|
||||||
|
model_agg[model]["response_count"] += stat.requests or 0
|
||||||
|
|
||||||
|
# 今日 breakdown
|
||||||
|
daily_breakdown.setdefault(today_str, []).append({
|
||||||
|
"model": model,
|
||||||
|
"requests": stat.requests or 0,
|
||||||
|
"tokens": int(stat.tokens or 0),
|
||||||
|
"cost": float(stat.cost or 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
# 构建 model_summary
|
||||||
|
model_summary = []
|
||||||
|
for model, agg in model_agg.items():
|
||||||
|
avg_rt = (agg["total_response_time"] / agg["response_count"] / 1000.0
|
||||||
|
if agg["response_count"] > 0 else 0)
|
||||||
|
model_summary.append({
|
||||||
|
"model": model,
|
||||||
|
"requests": agg["requests"],
|
||||||
|
"tokens": agg["tokens"],
|
||||||
|
"cost": agg["cost"],
|
||||||
|
"avg_response_time": avg_rt,
|
||||||
|
"cost_per_request": agg["cost"] / max(agg["requests"], 1),
|
||||||
|
"tokens_per_request": agg["tokens"] / max(agg["requests"], 1),
|
||||||
|
})
|
||||||
|
model_summary.sort(key=lambda x: x["cost"], reverse=True)
|
||||||
|
|
||||||
|
# 填充 model_breakdown
|
||||||
|
for item in formatted:
|
||||||
|
item["model_breakdown"] = daily_breakdown.get(item["date"], [])
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 普通用户:实时查询(数据量较小)
|
||||||
|
model_query = db.query(Usage).filter(
|
||||||
|
and_(
|
||||||
|
Usage.user_id == user.id,
|
||||||
|
Usage.created_at >= start_date,
|
||||||
|
Usage.created_at <= end_date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
model_stats = (
|
||||||
|
model_query.with_entities(
|
||||||
|
Usage.model,
|
||||||
|
func.count(Usage.id).label("requests"),
|
||||||
|
func.sum(Usage.total_tokens).label("tokens"),
|
||||||
|
func.sum(Usage.total_cost_usd).label("cost"),
|
||||||
|
func.avg(Usage.response_time_ms).label("avg_response_time"),
|
||||||
|
)
|
||||||
|
.group_by(Usage.model)
|
||||||
|
.order_by(func.sum(Usage.total_cost_usd).desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
model_summary = [
|
||||||
{
|
{
|
||||||
"model": stat.model,
|
"model": stat.model,
|
||||||
"requests": stat.requests or 0,
|
"requests": stat.requests or 0,
|
||||||
"tokens": int(stat.tokens or 0),
|
"tokens": int(stat.tokens or 0),
|
||||||
"cost": float(stat.cost or 0),
|
"cost": float(stat.cost or 0),
|
||||||
|
"avg_response_time": (
|
||||||
|
float(stat.avg_response_time or 0) / 1000.0 if stat.avg_response_time else 0
|
||||||
|
),
|
||||||
|
"cost_per_request": float(stat.cost or 0) / max(stat.requests or 1, 1),
|
||||||
|
"tokens_per_request": int(stat.tokens or 0) / max(stat.requests or 1, 1),
|
||||||
}
|
}
|
||||||
|
for stat in model_stats
|
||||||
|
]
|
||||||
|
|
||||||
|
daily_model_stats = (
|
||||||
|
model_query.with_entities(
|
||||||
|
func.date(Usage.created_at).label("date"),
|
||||||
|
Usage.model,
|
||||||
|
func.count(Usage.id).label("requests"),
|
||||||
|
func.sum(Usage.total_tokens).label("tokens"),
|
||||||
|
func.sum(Usage.total_cost_usd).label("cost"),
|
||||||
|
)
|
||||||
|
.group_by(func.date(Usage.created_at), Usage.model)
|
||||||
|
.order_by(func.date(Usage.created_at).desc(), func.sum(Usage.total_cost_usd).desc())
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in formatted:
|
breakdown = {}
|
||||||
item["model_breakdown"] = breakdown.get(item["date"], [])
|
for stat in daily_model_stats:
|
||||||
|
date_str = stat.date.isoformat()
|
||||||
|
breakdown.setdefault(date_str, []).append(
|
||||||
|
{
|
||||||
|
"model": stat.model,
|
||||||
|
"requests": stat.requests or 0,
|
||||||
|
"tokens": int(stat.tokens or 0),
|
||||||
|
"cost": float(stat.cost or 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in formatted:
|
||||||
|
item["model_breakdown"] = breakdown.get(item["date"], [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"daily_stats": formatted,
|
"daily_stats": formatted,
|
||||||
|
|||||||
@@ -260,9 +260,9 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
mapping = await mapper.get_mapping(source_model, provider_id)
|
mapping = await mapper.get_mapping(source_model, provider_id)
|
||||||
|
|
||||||
if mapping and mapping.model:
|
if mapping and mapping.model:
|
||||||
# 使用 select_provider_model_name 支持别名功能
|
# 使用 select_provider_model_name 支持映射功能
|
||||||
# 传入 api_key.id 作为 affinity_key,实现相同用户稳定选择同一别名
|
# 传入 api_key.id 作为 affinity_key,实现相同用户稳定选择同一映射
|
||||||
# 传入 api_format 用于过滤适用的别名作用域
|
# 传入 api_format 用于过滤适用的映射作用域
|
||||||
affinity_key = self.api_key.id if self.api_key else None
|
affinity_key = self.api_key.id if self.api_key else None
|
||||||
mapped_name = mapping.model.select_provider_model_name(
|
mapped_name = mapping.model.select_provider_model_name(
|
||||||
affinity_key, api_format=self.FORMAT_ID
|
affinity_key, api_format=self.FORMAT_ID
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
查找逻辑:
|
查找逻辑:
|
||||||
1. 直接通过 GlobalModel.name 匹配
|
1. 直接通过 GlobalModel.name 匹配
|
||||||
2. 查找该 Provider 的 Model 实现
|
2. 查找该 Provider 的 Model 实现
|
||||||
3. 使用 provider_model_name / provider_model_aliases 选择最终名称
|
3. 使用 provider_model_name / provider_model_mappings 选择最终名称
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_model: 用户请求的模型名(必须是 GlobalModel.name)
|
source_model: 用户请求的模型名(必须是 GlobalModel.name)
|
||||||
@@ -153,9 +153,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
logger.debug(f"[CLI] _get_mapped_model: source={source_model}, provider={provider_id[:8]}..., mapping={mapping}")
|
logger.debug(f"[CLI] _get_mapped_model: source={source_model}, provider={provider_id[:8]}..., mapping={mapping}")
|
||||||
|
|
||||||
if mapping and mapping.model:
|
if mapping and mapping.model:
|
||||||
# 使用 select_provider_model_name 支持别名功能
|
# 使用 select_provider_model_name 支持模型映射功能
|
||||||
# 传入 api_key.id 作为 affinity_key,实现相同用户稳定选择同一别名
|
# 传入 api_key.id 作为 affinity_key,实现相同用户稳定选择同一映射
|
||||||
# 传入 api_format 用于过滤适用的别名作用域
|
# 传入 api_format 用于过滤适用的映射作用域
|
||||||
affinity_key = self.api_key.id if self.api_key else None
|
affinity_key = self.api_key.id if self.api_key else None
|
||||||
mapped_name = mapping.model.select_provider_model_name(
|
mapped_name = mapping.model.select_provider_model_name(
|
||||||
affinity_key, api_format=self.FORMAT_ID
|
affinity_key, api_format=self.FORMAT_ID
|
||||||
@@ -400,7 +400,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
ctx.provider_api_format = str(endpoint.api_format) if endpoint.api_format else ""
|
ctx.provider_api_format = str(endpoint.api_format) if endpoint.api_format else ""
|
||||||
ctx.client_api_format = ctx.api_format # 已在 process_stream 中设置
|
ctx.client_api_format = ctx.api_format # 已在 process_stream 中设置
|
||||||
|
|
||||||
# 获取模型映射(别名/映射 → 实际模型名)
|
# 获取模型映射(映射名称 → 实际模型名)
|
||||||
mapped_model = await self._get_mapped_model(
|
mapped_model = await self._get_mapped_model(
|
||||||
source_model=ctx.model,
|
source_model=ctx.model,
|
||||||
provider_id=str(provider.id),
|
provider_id=str(provider.id),
|
||||||
@@ -1382,7 +1382,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
provider_name = str(provider.name)
|
provider_name = str(provider.name)
|
||||||
provider_api_format = str(endpoint.api_format) if endpoint.api_format else ""
|
provider_api_format = str(endpoint.api_format) if endpoint.api_format else ""
|
||||||
|
|
||||||
# 获取模型映射(别名/映射 → 实际模型名)
|
# 获取模型映射(映射名称 → 实际模型名)
|
||||||
mapped_model = await self._get_mapped_model(
|
mapped_model = await self._get_mapped_model(
|
||||||
source_model=model,
|
source_model=model,
|
||||||
provider_id=str(provider.id),
|
provider_id=str(provider.id),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ model_mapping_resolution_total = Counter(
|
|||||||
"model_mapping_resolution_total",
|
"model_mapping_resolution_total",
|
||||||
"Total number of model mapping resolutions",
|
"Total number of model mapping resolutions",
|
||||||
["method", "cache_hit"],
|
["method", "cache_hit"],
|
||||||
# method: direct_match, provider_model_name, alias, not_found
|
# method: direct_match, provider_model_name, mapping, not_found
|
||||||
# cache_hit: true, false
|
# cache_hit: true, false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -346,9 +346,9 @@ class ModelCreate(BaseModel):
|
|||||||
provider_model_name: str = Field(
|
provider_model_name: str = Field(
|
||||||
..., min_length=1, max_length=200, description="Provider 侧的主模型名称"
|
..., min_length=1, max_length=200, description="Provider 侧的主模型名称"
|
||||||
)
|
)
|
||||||
provider_model_aliases: Optional[List[dict]] = Field(
|
provider_model_mappings: Optional[List[dict]] = Field(
|
||||||
None,
|
None,
|
||||||
description="模型名称别名列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
||||||
)
|
)
|
||||||
global_model_id: str = Field(..., description="关联的 GlobalModel ID(必填)")
|
global_model_id: str = Field(..., description="关联的 GlobalModel ID(必填)")
|
||||||
# 按次计费配置 - 可选,为空时使用 GlobalModel 默认值
|
# 按次计费配置 - 可选,为空时使用 GlobalModel 默认值
|
||||||
@@ -376,9 +376,9 @@ class ModelUpdate(BaseModel):
|
|||||||
"""更新模型请求"""
|
"""更新模型请求"""
|
||||||
|
|
||||||
provider_model_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
provider_model_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||||
provider_model_aliases: Optional[List[dict]] = Field(
|
provider_model_mappings: Optional[List[dict]] = Field(
|
||||||
None,
|
None,
|
||||||
description="模型名称别名列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
description="模型名称映射列表,格式: [{'name': 'alias1', 'priority': 1}, ...]",
|
||||||
)
|
)
|
||||||
global_model_id: Optional[str] = None
|
global_model_id: Optional[str] = None
|
||||||
# 按次计费配置
|
# 按次计费配置
|
||||||
@@ -404,7 +404,7 @@ class ModelResponse(BaseModel):
|
|||||||
provider_id: str
|
provider_id: str
|
||||||
global_model_id: Optional[str]
|
global_model_id: Optional[str]
|
||||||
provider_model_name: str
|
provider_model_name: str
|
||||||
provider_model_aliases: Optional[List[dict]] = None
|
provider_model_mappings: Optional[List[dict]] = None
|
||||||
|
|
||||||
# 按次计费配置
|
# 按次计费配置
|
||||||
price_per_request: Optional[float] = None
|
price_per_request: Optional[float] = None
|
||||||
|
|||||||
@@ -671,10 +671,10 @@ class Model(Base):
|
|||||||
|
|
||||||
# Provider 映射配置
|
# Provider 映射配置
|
||||||
provider_model_name = Column(String(200), nullable=False) # Provider 侧的主模型名称
|
provider_model_name = Column(String(200), nullable=False) # Provider 侧的主模型名称
|
||||||
# 模型名称别名列表(带优先级),用于同一模型在 Provider 侧有多个名称变体的场景
|
# 模型名称映射列表(带优先级),用于同一模型在 Provider 侧有多个名称变体的场景
|
||||||
# 格式: [{"name": "Claude-Sonnet-4.5", "priority": 1}, {"name": "Claude-Sonnet-4-5", "priority": 2}]
|
# 格式: [{"name": "Claude-Sonnet-4.5", "priority": 1}, {"name": "Claude-Sonnet-4-5", "priority": 2}]
|
||||||
# 为空时只使用 provider_model_name
|
# 为空时只使用 provider_model_name
|
||||||
provider_model_aliases = Column(JSON, nullable=True, default=None)
|
provider_model_mappings = Column(JSON, nullable=True, default=None)
|
||||||
|
|
||||||
# 按次计费配置(每次请求的固定费用,美元)- 可为空,为空时使用 GlobalModel 的默认值
|
# 按次计费配置(每次请求的固定费用,美元)- 可为空,为空时使用 GlobalModel 的默认值
|
||||||
price_per_request = Column(Float, nullable=True) # 每次请求固定费用
|
price_per_request = Column(Float, nullable=True) # 每次请求固定费用
|
||||||
@@ -820,25 +820,25 @@ class Model(Base):
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""按优先级选择要使用的 Provider 模型名称
|
"""按优先级选择要使用的 Provider 模型名称
|
||||||
|
|
||||||
如果配置了 provider_model_aliases,按优先级选择(数字越小越优先);
|
如果配置了 provider_model_mappings,按优先级选择(数字越小越优先);
|
||||||
相同优先级的别名通过哈希分散实现负载均衡(与 Key 调度策略一致);
|
相同优先级的映射通过哈希分散实现负载均衡(与 Key 调度策略一致);
|
||||||
否则返回 provider_model_name。
|
否则返回 provider_model_name。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
affinity_key: 用于哈希分散的亲和键(如用户 API Key 哈希),确保同一用户稳定选择同一别名
|
affinity_key: 用于哈希分散的亲和键(如用户 API Key 哈希),确保同一用户稳定选择同一映射
|
||||||
api_format: 当前请求的 API 格式(如 CLAUDE、OPENAI 等),用于过滤适用的别名
|
api_format: 当前请求的 API 格式(如 CLAUDE、OPENAI 等),用于过滤适用的映射
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
if not self.provider_model_aliases:
|
if not self.provider_model_mappings:
|
||||||
return self.provider_model_name
|
return self.provider_model_name
|
||||||
|
|
||||||
raw_aliases = self.provider_model_aliases
|
raw_mappings = self.provider_model_mappings
|
||||||
if not isinstance(raw_aliases, list) or len(raw_aliases) == 0:
|
if not isinstance(raw_mappings, list) or len(raw_mappings) == 0:
|
||||||
return self.provider_model_name
|
return self.provider_model_name
|
||||||
|
|
||||||
aliases: list[dict] = []
|
mappings: list[dict] = []
|
||||||
for raw in raw_aliases:
|
for raw in raw_mappings:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
continue
|
continue
|
||||||
name = raw.get("name")
|
name = raw.get("name")
|
||||||
@@ -846,10 +846,10 @@ class Model(Base):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查 api_formats 作用域(如果配置了且当前有 api_format)
|
# 检查 api_formats 作用域(如果配置了且当前有 api_format)
|
||||||
alias_api_formats = raw.get("api_formats")
|
mapping_api_formats = raw.get("api_formats")
|
||||||
if api_format and alias_api_formats:
|
if api_format and mapping_api_formats:
|
||||||
# 如果配置了作用域,只有匹配时才生效
|
# 如果配置了作用域,只有匹配时才生效
|
||||||
if isinstance(alias_api_formats, list) and api_format not in alias_api_formats:
|
if isinstance(mapping_api_formats, list) and api_format not in mapping_api_formats:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_priority = raw.get("priority", 1)
|
raw_priority = raw.get("priority", 1)
|
||||||
@@ -860,47 +860,47 @@ class Model(Base):
|
|||||||
if priority < 1:
|
if priority < 1:
|
||||||
priority = 1
|
priority = 1
|
||||||
|
|
||||||
aliases.append({"name": name.strip(), "priority": priority})
|
mappings.append({"name": name.strip(), "priority": priority})
|
||||||
|
|
||||||
if not aliases:
|
if not mappings:
|
||||||
return self.provider_model_name
|
return self.provider_model_name
|
||||||
|
|
||||||
# 按优先级排序(数字越小越优先)
|
# 按优先级排序(数字越小越优先)
|
||||||
sorted_aliases = sorted(aliases, key=lambda x: x["priority"])
|
sorted_mappings = sorted(mappings, key=lambda x: x["priority"])
|
||||||
|
|
||||||
# 获取最高优先级(最小数字)
|
# 获取最高优先级(最小数字)
|
||||||
highest_priority = sorted_aliases[0]["priority"]
|
highest_priority = sorted_mappings[0]["priority"]
|
||||||
|
|
||||||
# 获取所有最高优先级的别名
|
# 获取所有最高优先级的映射
|
||||||
top_priority_aliases = [
|
top_priority_mappings = [
|
||||||
alias for alias in sorted_aliases
|
mapping for mapping in sorted_mappings
|
||||||
if alias["priority"] == highest_priority
|
if mapping["priority"] == highest_priority
|
||||||
]
|
]
|
||||||
|
|
||||||
# 如果有多个相同优先级的别名,通过哈希分散选择
|
# 如果有多个相同优先级的映射,通过哈希分散选择
|
||||||
if len(top_priority_aliases) > 1 and affinity_key:
|
if len(top_priority_mappings) > 1 and affinity_key:
|
||||||
# 为每个别名计算哈希得分,选择得分最小的
|
# 为每个映射计算哈希得分,选择得分最小的
|
||||||
def hash_score(alias: dict) -> int:
|
def hash_score(mapping: dict) -> int:
|
||||||
combined = f"{affinity_key}:{alias['name']}"
|
combined = f"{affinity_key}:{mapping['name']}"
|
||||||
return int(hashlib.md5(combined.encode()).hexdigest(), 16)
|
return int(hashlib.md5(combined.encode()).hexdigest(), 16)
|
||||||
|
|
||||||
selected = min(top_priority_aliases, key=hash_score)
|
selected = min(top_priority_mappings, key=hash_score)
|
||||||
elif len(top_priority_aliases) > 1:
|
elif len(top_priority_mappings) > 1:
|
||||||
# 没有 affinity_key 时,使用确定性选择(按名称排序后取第一个)
|
# 没有 affinity_key 时,使用确定性选择(按名称排序后取第一个)
|
||||||
# 避免随机选择导致同一请求重试时选择不同的模型名称
|
# 避免随机选择导致同一请求重试时选择不同的模型名称
|
||||||
selected = min(top_priority_aliases, key=lambda x: x["name"])
|
selected = min(top_priority_mappings, key=lambda x: x["name"])
|
||||||
else:
|
else:
|
||||||
selected = top_priority_aliases[0]
|
selected = top_priority_mappings[0]
|
||||||
|
|
||||||
return selected["name"]
|
return selected["name"]
|
||||||
|
|
||||||
def get_all_provider_model_names(self) -> list[str]:
|
def get_all_provider_model_names(self) -> list[str]:
|
||||||
"""获取所有可用的 Provider 模型名称(主名称 + 别名)"""
|
"""获取所有可用的 Provider 模型名称(主名称 + 映射名称)"""
|
||||||
names = [self.provider_model_name]
|
names = [self.provider_model_name]
|
||||||
if self.provider_model_aliases:
|
if self.provider_model_mappings:
|
||||||
for alias in self.provider_model_aliases:
|
for mapping in self.provider_model_mappings:
|
||||||
if isinstance(alias, dict) and alias.get("name"):
|
if isinstance(mapping, dict) and mapping.get("name"):
|
||||||
names.append(alias["name"])
|
names.append(mapping["name"])
|
||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
@@ -1308,6 +1308,53 @@ class StatsDaily(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StatsDailyModel(Base):
|
||||||
|
"""每日模型统计快照 - 用于快速查询每日模型维度数据"""
|
||||||
|
|
||||||
|
__tablename__ = "stats_daily_model"
|
||||||
|
|
||||||
|
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||||
|
|
||||||
|
# 统计日期 (UTC)
|
||||||
|
date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||||
|
|
||||||
|
# 模型名称
|
||||||
|
model = Column(String(100), nullable=False)
|
||||||
|
|
||||||
|
# 请求统计
|
||||||
|
total_requests = Column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
|
# Token 统计
|
||||||
|
input_tokens = Column(BigInteger, default=0, nullable=False)
|
||||||
|
output_tokens = Column(BigInteger, default=0, nullable=False)
|
||||||
|
cache_creation_tokens = Column(BigInteger, default=0, nullable=False)
|
||||||
|
cache_read_tokens = Column(BigInteger, default=0, nullable=False)
|
||||||
|
|
||||||
|
# 成本统计 (USD)
|
||||||
|
total_cost = Column(Float, default=0.0, nullable=False)
|
||||||
|
|
||||||
|
# 性能统计
|
||||||
|
avg_response_time_ms = Column(Float, default=0.0, nullable=False)
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
created_at = Column(
|
||||||
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||||
|
)
|
||||||
|
updated_at = Column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
default=lambda: datetime.now(timezone.utc),
|
||||||
|
onupdate=lambda: datetime.now(timezone.utc),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 唯一约束:每个模型每天只有一条记录
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("date", "model", name="uq_stats_daily_model"),
|
||||||
|
Index("idx_stats_daily_model_date", "date"),
|
||||||
|
Index("idx_stats_daily_model_date_model", "date", "model"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StatsSummary(Base):
|
class StatsSummary(Base):
|
||||||
"""全局统计汇总 - 单行记录,存储截止到昨天的累计数据"""
|
"""全局统计汇总 - 单行记录,存储截止到昨天的累计数据"""
|
||||||
|
|
||||||
|
|||||||
12
src/services/cache/aware_scheduler.py
vendored
12
src/services/cache/aware_scheduler.py
vendored
@@ -589,14 +589,14 @@ class CacheAwareScheduler:
|
|||||||
|
|
||||||
target_format = normalize_api_format(api_format)
|
target_format = normalize_api_format(api_format)
|
||||||
|
|
||||||
# 0. 解析 model_name 到 GlobalModel(支持直接匹配和别名匹配,使用 ModelCacheService)
|
# 0. 解析 model_name 到 GlobalModel(支持直接匹配和映射名匹配,使用 ModelCacheService)
|
||||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
|
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
|
||||||
|
|
||||||
if not global_model:
|
if not global_model:
|
||||||
logger.warning(f"GlobalModel not found: {model_name}")
|
logger.warning(f"GlobalModel not found: {model_name}")
|
||||||
raise ModelNotSupportedException(model=model_name)
|
raise ModelNotSupportedException(model=model_name)
|
||||||
|
|
||||||
# 使用 GlobalModel.id 作为缓存亲和性的模型标识,确保别名和规范名都能命中同一个缓存
|
# 使用 GlobalModel.id 作为缓存亲和性的模型标识,确保映射名和规范名都能命中同一个缓存
|
||||||
global_model_id: str = str(global_model.id)
|
global_model_id: str = str(global_model.id)
|
||||||
requested_model_name = model_name
|
requested_model_name = model_name
|
||||||
resolved_model_name = str(global_model.name)
|
resolved_model_name = str(global_model.name)
|
||||||
@@ -751,19 +751,19 @@ class CacheAwareScheduler:
|
|||||||
|
|
||||||
支持两种匹配方式:
|
支持两种匹配方式:
|
||||||
1. 直接匹配 GlobalModel.name
|
1. 直接匹配 GlobalModel.name
|
||||||
2. 通过 ModelCacheService 匹配别名(全局查找)
|
2. 通过 ModelCacheService 匹配映射名(全局查找)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: 数据库会话
|
db: 数据库会话
|
||||||
provider: Provider 对象
|
provider: Provider 对象
|
||||||
model_name: 模型名称(可以是 GlobalModel.name 或别名)
|
model_name: 模型名称(可以是 GlobalModel.name 或映射名)
|
||||||
is_stream: 是否是流式请求,如果为 True 则同时检查流式支持
|
is_stream: 是否是流式请求,如果为 True 则同时检查流式支持
|
||||||
capability_requirements: 能力需求(可选),用于检查模型是否支持所需能力
|
capability_requirements: 能力需求(可选),用于检查模型是否支持所需能力
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(is_supported, skip_reason, supported_capabilities) - 是否支持、跳过原因、模型支持的能力列表
|
(is_supported, skip_reason, supported_capabilities) - 是否支持、跳过原因、模型支持的能力列表
|
||||||
"""
|
"""
|
||||||
# 使用 ModelCacheService 解析模型名称(支持别名)
|
# 使用 ModelCacheService 解析模型名称(支持映射名)
|
||||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
|
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
|
||||||
|
|
||||||
if not global_model:
|
if not global_model:
|
||||||
@@ -914,7 +914,7 @@ class CacheAwareScheduler:
|
|||||||
db: 数据库会话
|
db: 数据库会话
|
||||||
providers: Provider 列表
|
providers: Provider 列表
|
||||||
target_format: 目标 API 格式
|
target_format: 目标 API 格式
|
||||||
model_name: 模型名称(用户请求的名称,可能是别名)
|
model_name: 模型名称(用户请求的名称,可能是映射名)
|
||||||
affinity_key: 亲和性标识符(通常为API Key ID)
|
affinity_key: 亲和性标识符(通常为API Key ID)
|
||||||
resolved_model_name: 解析后的 GlobalModel.name(用于 Key.allowed_models 校验)
|
resolved_model_name: 解析后的 GlobalModel.name(用于 Key.allowed_models 校验)
|
||||||
max_candidates: 最大候选数
|
max_candidates: 最大候选数
|
||||||
|
|||||||
32
src/services/cache/model_cache.py
vendored
32
src/services/cache/model_cache.py
vendored
@@ -198,7 +198,7 @@ class ModelCacheService:
|
|||||||
provider_id: Optional[str] = None,
|
provider_id: Optional[str] = None,
|
||||||
global_model_id: Optional[str] = None,
|
global_model_id: Optional[str] = None,
|
||||||
provider_model_name: Optional[str] = None,
|
provider_model_name: Optional[str] = None,
|
||||||
provider_model_aliases: Optional[list] = None,
|
provider_model_mappings: Optional[list] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""清除 Model 缓存
|
"""清除 Model 缓存
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ class ModelCacheService:
|
|||||||
provider_id: Provider ID(用于清除 provider_global 缓存)
|
provider_id: Provider ID(用于清除 provider_global 缓存)
|
||||||
global_model_id: GlobalModel ID(用于清除 provider_global 缓存)
|
global_model_id: GlobalModel ID(用于清除 provider_global 缓存)
|
||||||
provider_model_name: provider_model_name(用于清除 resolve 缓存)
|
provider_model_name: provider_model_name(用于清除 resolve 缓存)
|
||||||
provider_model_aliases: 映射名称列表(用于清除 resolve 缓存)
|
provider_model_mappings: 映射名称列表(用于清除 resolve 缓存)
|
||||||
"""
|
"""
|
||||||
# 清除 model:id 缓存
|
# 清除 model:id 缓存
|
||||||
await CacheService.delete(f"model:id:{model_id}")
|
await CacheService.delete(f"model:id:{model_id}")
|
||||||
@@ -222,16 +222,16 @@ class ModelCacheService:
|
|||||||
else:
|
else:
|
||||||
logger.debug(f"Model 缓存已清除: {model_id}")
|
logger.debug(f"Model 缓存已清除: {model_id}")
|
||||||
|
|
||||||
# 清除 resolve 缓存(provider_model_name 和 aliases 可能都被用作解析 key)
|
# 清除 resolve 缓存(provider_model_name 和 mappings 可能都被用作解析 key)
|
||||||
resolve_keys_to_clear = []
|
resolve_keys_to_clear = []
|
||||||
if provider_model_name:
|
if provider_model_name:
|
||||||
resolve_keys_to_clear.append(provider_model_name)
|
resolve_keys_to_clear.append(provider_model_name)
|
||||||
if provider_model_aliases:
|
if provider_model_mappings:
|
||||||
for alias_entry in provider_model_aliases:
|
for mapping_entry in provider_model_mappings:
|
||||||
if isinstance(alias_entry, dict):
|
if isinstance(mapping_entry, dict):
|
||||||
alias_name = alias_entry.get("name", "").strip()
|
mapping_name = mapping_entry.get("name", "").strip()
|
||||||
if alias_name:
|
if mapping_name:
|
||||||
resolve_keys_to_clear.append(alias_name)
|
resolve_keys_to_clear.append(mapping_name)
|
||||||
|
|
||||||
for key in resolve_keys_to_clear:
|
for key in resolve_keys_to_clear:
|
||||||
await CacheService.delete(f"global_model:resolve:{key}")
|
await CacheService.delete(f"global_model:resolve:{key}")
|
||||||
@@ -261,8 +261,8 @@ class ModelCacheService:
|
|||||||
2. 通过 provider_model_name 匹配(查询 Model 表)
|
2. 通过 provider_model_name 匹配(查询 Model 表)
|
||||||
3. 直接匹配 GlobalModel.name(兜底)
|
3. 直接匹配 GlobalModel.name(兜底)
|
||||||
|
|
||||||
注意:此方法不使用 provider_model_aliases 进行全局解析。
|
注意:此方法不使用 provider_model_mappings 进行全局解析。
|
||||||
provider_model_aliases 是 Provider 级别的别名配置,只在特定 Provider 上下文中生效,
|
provider_model_mappings 是 Provider 级别的映射配置,只在特定 Provider 上下文中生效,
|
||||||
由 resolve_provider_model() 处理。
|
由 resolve_provider_model() 处理。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -301,9 +301,9 @@ class ModelCacheService:
|
|||||||
logger.debug(f"GlobalModel 缓存命中(映射解析): {normalized_name}")
|
logger.debug(f"GlobalModel 缓存命中(映射解析): {normalized_name}")
|
||||||
return ModelCacheService._dict_to_global_model(cached_data)
|
return ModelCacheService._dict_to_global_model(cached_data)
|
||||||
|
|
||||||
# 2. 通过 provider_model_name 匹配(不考虑 provider_model_aliases)
|
# 2. 通过 provider_model_name 匹配(不考虑 provider_model_mappings)
|
||||||
# 重要:provider_model_aliases 是 Provider 级别的别名配置,只在特定 Provider 上下文中生效
|
# 重要:provider_model_mappings 是 Provider 级别的映射配置,只在特定 Provider 上下文中生效
|
||||||
# 全局解析不应该受到某个 Provider 别名配置的影响
|
# 全局解析不应该受到某个 Provider 映射配置的影响
|
||||||
# 例如:Provider A 把 "haiku" 映射到 "sonnet",不应该影响 Provider B 的 "haiku" 解析
|
# 例如:Provider A 把 "haiku" 映射到 "sonnet",不应该影响 Provider B 的 "haiku" 解析
|
||||||
from src.models.database import Provider
|
from src.models.database import Provider
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ class ModelCacheService:
|
|||||||
"provider_id": model.provider_id,
|
"provider_id": model.provider_id,
|
||||||
"global_model_id": model.global_model_id,
|
"global_model_id": model.global_model_id,
|
||||||
"provider_model_name": model.provider_model_name,
|
"provider_model_name": model.provider_model_name,
|
||||||
"provider_model_aliases": getattr(model, "provider_model_aliases", None),
|
"provider_model_mappings": getattr(model, "provider_model_mappings", None),
|
||||||
"is_active": model.is_active,
|
"is_active": model.is_active,
|
||||||
"is_available": model.is_available if hasattr(model, "is_available") else True,
|
"is_available": model.is_available if hasattr(model, "is_available") else True,
|
||||||
"price_per_request": (
|
"price_per_request": (
|
||||||
@@ -424,7 +424,7 @@ class ModelCacheService:
|
|||||||
provider_id=model_dict["provider_id"],
|
provider_id=model_dict["provider_id"],
|
||||||
global_model_id=model_dict["global_model_id"],
|
global_model_id=model_dict["global_model_id"],
|
||||||
provider_model_name=model_dict["provider_model_name"],
|
provider_model_name=model_dict["provider_model_name"],
|
||||||
provider_model_aliases=model_dict.get("provider_model_aliases"),
|
provider_model_mappings=model_dict.get("provider_model_mappings"),
|
||||||
is_active=model_dict["is_active"],
|
is_active=model_dict["is_active"],
|
||||||
is_available=model_dict.get("is_available", True),
|
is_available=model_dict.get("is_available", True),
|
||||||
price_per_request=model_dict.get("price_per_request"),
|
price_per_request=model_dict.get("price_per_request"),
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ class ModelCostService:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
provider: Provider 对象或提供商名称
|
provider: Provider 对象或提供商名称
|
||||||
model: 用户请求的模型名(可能是 GlobalModel.name 或别名)
|
model: 用户请求的模型名(可能是 GlobalModel.name 或映射名)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
按次计费价格,如果没有配置则返回 None
|
按次计费价格,如果没有配置则返回 None
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ class ModelMapperMiddleware:
|
|||||||
获取模型映射
|
获取模型映射
|
||||||
|
|
||||||
简化后的逻辑:
|
简化后的逻辑:
|
||||||
1. 通过 GlobalModel.name 或别名解析 GlobalModel
|
1. 通过 GlobalModel.name 或映射名解析 GlobalModel
|
||||||
2. 找到 GlobalModel 后,查找该 Provider 的 Model 实现
|
2. 找到 GlobalModel 后,查找该 Provider 的 Model 实现
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_model: 用户请求的模型名(可以是 GlobalModel.name 或别名)
|
source_model: 用户请求的模型名(可以是 GlobalModel.name 或映射名)
|
||||||
provider_id: 提供商ID (UUID)
|
provider_id: 提供商ID (UUID)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -101,7 +101,7 @@ class ModelMapperMiddleware:
|
|||||||
|
|
||||||
mapping = None
|
mapping = None
|
||||||
|
|
||||||
# 步骤 1: 解析 GlobalModel(支持别名)
|
# 步骤 1: 解析 GlobalModel(支持映射名)
|
||||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(
|
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(
|
||||||
self.db, source_model
|
self.db, source_model
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ModelService:
|
|||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
global_model_id=model_data.global_model_id,
|
global_model_id=model_data.global_model_id,
|
||||||
provider_model_name=model_data.provider_model_name,
|
provider_model_name=model_data.provider_model_name,
|
||||||
provider_model_aliases=model_data.provider_model_aliases,
|
provider_model_mappings=model_data.provider_model_mappings,
|
||||||
price_per_request=model_data.price_per_request,
|
price_per_request=model_data.price_per_request,
|
||||||
tiered_pricing=model_data.tiered_pricing,
|
tiered_pricing=model_data.tiered_pricing,
|
||||||
supports_vision=model_data.supports_vision,
|
supports_vision=model_data.supports_vision,
|
||||||
@@ -153,9 +153,9 @@ class ModelService:
|
|||||||
if not model:
|
if not model:
|
||||||
raise NotFoundException(f"模型 {model_id} 不存在")
|
raise NotFoundException(f"模型 {model_id} 不存在")
|
||||||
|
|
||||||
# 保存旧的别名,用于清除缓存
|
# 保存旧的映射,用于清除缓存
|
||||||
old_provider_model_name = model.provider_model_name
|
old_provider_model_name = model.provider_model_name
|
||||||
old_provider_model_aliases = model.provider_model_aliases
|
old_provider_model_mappings = model.provider_model_mappings
|
||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
update_data = model_data.model_dump(exclude_unset=True)
|
update_data = model_data.model_dump(exclude_unset=True)
|
||||||
@@ -174,26 +174,26 @@ class ModelService:
|
|||||||
db.refresh(model)
|
db.refresh(model)
|
||||||
|
|
||||||
# 清除 Redis 缓存(异步执行,不阻塞返回)
|
# 清除 Redis 缓存(异步执行,不阻塞返回)
|
||||||
# 先清除旧的别名缓存
|
# 先清除旧的映射缓存
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
ModelCacheService.invalidate_model_cache(
|
ModelCacheService.invalidate_model_cache(
|
||||||
model_id=model.id,
|
model_id=model.id,
|
||||||
provider_id=model.provider_id,
|
provider_id=model.provider_id,
|
||||||
global_model_id=model.global_model_id,
|
global_model_id=model.global_model_id,
|
||||||
provider_model_name=old_provider_model_name,
|
provider_model_name=old_provider_model_name,
|
||||||
provider_model_aliases=old_provider_model_aliases,
|
provider_model_mappings=old_provider_model_mappings,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# 再清除新的别名缓存(如果有变化)
|
# 再清除新的映射缓存(如果有变化)
|
||||||
if (model.provider_model_name != old_provider_model_name or
|
if (model.provider_model_name != old_provider_model_name or
|
||||||
model.provider_model_aliases != old_provider_model_aliases):
|
model.provider_model_mappings != old_provider_model_mappings):
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
ModelCacheService.invalidate_model_cache(
|
ModelCacheService.invalidate_model_cache(
|
||||||
model_id=model.id,
|
model_id=model.id,
|
||||||
provider_id=model.provider_id,
|
provider_id=model.provider_id,
|
||||||
global_model_id=model.global_model_id,
|
global_model_id=model.global_model_id,
|
||||||
provider_model_name=model.provider_model_name,
|
provider_model_name=model.provider_model_name,
|
||||||
provider_model_aliases=model.provider_model_aliases,
|
provider_model_mappings=model.provider_model_mappings,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ class ModelService:
|
|||||||
"provider_id": model.provider_id,
|
"provider_id": model.provider_id,
|
||||||
"global_model_id": model.global_model_id,
|
"global_model_id": model.global_model_id,
|
||||||
"provider_model_name": model.provider_model_name,
|
"provider_model_name": model.provider_model_name,
|
||||||
"provider_model_aliases": model.provider_model_aliases,
|
"provider_model_mappings": model.provider_model_mappings,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -260,7 +260,7 @@ class ModelService:
|
|||||||
provider_id=cache_info["provider_id"],
|
provider_id=cache_info["provider_id"],
|
||||||
global_model_id=cache_info["global_model_id"],
|
global_model_id=cache_info["global_model_id"],
|
||||||
provider_model_name=cache_info["provider_model_name"],
|
provider_model_name=cache_info["provider_model_name"],
|
||||||
provider_model_aliases=cache_info["provider_model_aliases"],
|
provider_model_mappings=cache_info["provider_model_mappings"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ class ModelService:
|
|||||||
provider_id=model.provider_id,
|
provider_id=model.provider_id,
|
||||||
global_model_id=model.global_model_id,
|
global_model_id=model.global_model_id,
|
||||||
provider_model_name=model.provider_model_name,
|
provider_model_name=model.provider_model_name,
|
||||||
provider_model_aliases=model.provider_model_aliases,
|
provider_model_mappings=model.provider_model_mappings,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ class ModelService:
|
|||||||
provider_id=model.provider_id,
|
provider_id=model.provider_id,
|
||||||
global_model_id=model.global_model_id,
|
global_model_id=model.global_model_id,
|
||||||
provider_model_name=model.provider_model_name,
|
provider_model_name=model.provider_model_name,
|
||||||
provider_model_aliases=model.provider_model_aliases,
|
provider_model_mappings=model.provider_model_mappings,
|
||||||
# 原始配置值(可能为空)
|
# 原始配置值(可能为空)
|
||||||
price_per_request=model.price_per_request,
|
price_per_request=model.price_per_request,
|
||||||
tiered_pricing=model.tiered_pricing,
|
tiered_pricing=model.tiered_pricing,
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ class CleanupScheduler:
|
|||||||
StatsAggregatorService.aggregate_daily_stats(
|
StatsAggregatorService.aggregate_daily_stats(
|
||||||
db, current_date_local
|
db, current_date_local
|
||||||
)
|
)
|
||||||
|
StatsAggregatorService.aggregate_daily_model_stats(
|
||||||
|
db, current_date_local
|
||||||
|
)
|
||||||
for (user_id,) in users:
|
for (user_id,) in users:
|
||||||
try:
|
try:
|
||||||
StatsAggregatorService.aggregate_user_daily_stats(
|
StatsAggregatorService.aggregate_user_daily_stats(
|
||||||
@@ -291,6 +294,7 @@ class CleanupScheduler:
|
|||||||
yesterday_local = today_local - timedelta(days=1)
|
yesterday_local = today_local - timedelta(days=1)
|
||||||
|
|
||||||
StatsAggregatorService.aggregate_daily_stats(db, yesterday_local)
|
StatsAggregatorService.aggregate_daily_stats(db, yesterday_local)
|
||||||
|
StatsAggregatorService.aggregate_daily_model_stats(db, yesterday_local)
|
||||||
|
|
||||||
users = db.query(DBUser.id).filter(DBUser.is_active.is_(True)).all()
|
users = db.query(DBUser.id).filter(DBUser.is_active.is_(True)).all()
|
||||||
for (user_id,) in users:
|
for (user_id,) in users:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from src.models.database import (
|
|||||||
ApiKey,
|
ApiKey,
|
||||||
RequestCandidate,
|
RequestCandidate,
|
||||||
StatsDaily,
|
StatsDaily,
|
||||||
|
StatsDailyModel,
|
||||||
StatsSummary,
|
StatsSummary,
|
||||||
StatsUserDaily,
|
StatsUserDaily,
|
||||||
Usage,
|
Usage,
|
||||||
@@ -219,6 +220,120 @@ class StatsAggregatorService:
|
|||||||
logger.info(f"[StatsAggregator] 聚合日期 {date.date()} 完成: {computed['total_requests']} 请求")
|
logger.info(f"[StatsAggregator] 聚合日期 {date.date()} 完成: {computed['total_requests']} 请求")
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def aggregate_daily_model_stats(db: Session, date: datetime) -> list[StatsDailyModel]:
|
||||||
|
"""聚合指定日期的模型维度统计数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话
|
||||||
|
date: 要聚合的业务日期
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StatsDailyModel 记录列表
|
||||||
|
"""
|
||||||
|
day_start, day_end = _get_business_day_range(date)
|
||||||
|
|
||||||
|
# 按模型分组统计
|
||||||
|
model_stats = (
|
||||||
|
db.query(
|
||||||
|
Usage.model,
|
||||||
|
func.count(Usage.id).label("total_requests"),
|
||||||
|
func.sum(Usage.input_tokens).label("input_tokens"),
|
||||||
|
func.sum(Usage.output_tokens).label("output_tokens"),
|
||||||
|
func.sum(Usage.cache_creation_input_tokens).label("cache_creation_tokens"),
|
||||||
|
func.sum(Usage.cache_read_input_tokens).label("cache_read_tokens"),
|
||||||
|
func.sum(Usage.total_cost_usd).label("total_cost"),
|
||||||
|
func.avg(Usage.response_time_ms).label("avg_response_time"),
|
||||||
|
)
|
||||||
|
.filter(and_(Usage.created_at >= day_start, Usage.created_at < day_end))
|
||||||
|
.group_by(Usage.model)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for stat in model_stats:
|
||||||
|
if not stat.model:
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing = (
|
||||||
|
db.query(StatsDailyModel)
|
||||||
|
.filter(and_(StatsDailyModel.date == day_start, StatsDailyModel.model == stat.model))
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
record = existing
|
||||||
|
else:
|
||||||
|
record = StatsDailyModel(
|
||||||
|
id=str(uuid.uuid4()), date=day_start, model=stat.model
|
||||||
|
)
|
||||||
|
|
||||||
|
record.total_requests = stat.total_requests or 0
|
||||||
|
record.input_tokens = int(stat.input_tokens or 0)
|
||||||
|
record.output_tokens = int(stat.output_tokens or 0)
|
||||||
|
record.cache_creation_tokens = int(stat.cache_creation_tokens or 0)
|
||||||
|
record.cache_read_tokens = int(stat.cache_read_tokens or 0)
|
||||||
|
record.total_cost = float(stat.total_cost or 0)
|
||||||
|
record.avg_response_time_ms = float(stat.avg_response_time or 0)
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
db.add(record)
|
||||||
|
results.append(record)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info(
|
||||||
|
f"[StatsAggregator] 聚合日期 {date.date()} 模型统计完成: {len(results)} 个模型"
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_daily_model_stats(db: Session, start_date: datetime, end_date: datetime) -> list[dict]:
|
||||||
|
"""获取日期范围内的模型统计数据(优先使用预聚合)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话
|
||||||
|
start_date: 开始日期 (UTC)
|
||||||
|
end_date: 结束日期 (UTC)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
模型统计数据列表
|
||||||
|
"""
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
app_tz = ZoneInfo(APP_TIMEZONE)
|
||||||
|
|
||||||
|
# 从预聚合表获取历史数据
|
||||||
|
stats = (
|
||||||
|
db.query(StatsDailyModel)
|
||||||
|
.filter(and_(StatsDailyModel.date >= start_date, StatsDailyModel.date < end_date))
|
||||||
|
.order_by(StatsDailyModel.date.asc(), StatsDailyModel.total_cost.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 转换为字典格式,按日期分组
|
||||||
|
result = []
|
||||||
|
for stat in stats:
|
||||||
|
# 转换日期为业务时区
|
||||||
|
if stat.date.tzinfo is None:
|
||||||
|
date_utc = stat.date.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
date_utc = stat.date.astimezone(timezone.utc)
|
||||||
|
date_str = date_utc.astimezone(app_tz).date().isoformat()
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"date": date_str,
|
||||||
|
"model": stat.model,
|
||||||
|
"requests": stat.total_requests,
|
||||||
|
"tokens": (
|
||||||
|
stat.input_tokens + stat.output_tokens +
|
||||||
|
stat.cache_creation_tokens + stat.cache_read_tokens
|
||||||
|
),
|
||||||
|
"cost": stat.total_cost,
|
||||||
|
"avg_response_time": stat.avg_response_time_ms / 1000.0 if stat.avg_response_time_ms else 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def aggregate_user_daily_stats(
|
def aggregate_user_daily_stats(
|
||||||
db: Session, user_id: str, date: datetime
|
db: Session, user_id: str, date: datetime
|
||||||
@@ -497,6 +612,7 @@ class StatsAggregatorService:
|
|||||||
current_date = start_date
|
current_date = start_date
|
||||||
while current_date < today_local:
|
while current_date < today_local:
|
||||||
StatsAggregatorService.aggregate_daily_stats(db, current_date)
|
StatsAggregatorService.aggregate_daily_stats(db, current_date)
|
||||||
|
StatsAggregatorService.aggregate_daily_model_stats(db, current_date)
|
||||||
count += 1
|
count += 1
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user