Files
PiAgentUI/src/utils/modelUtils.ts
T
Evandro b3489e1ede
Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
A professional workspace for AI coding agents with:
- Real-time streaming chat with SSE
- Session management (fork, clone, tree navigation)
- MCP tools browser & metadata panel
- Skills browser (global, project, package sources)
- Voice input & STT provider configuration
- Integrated terminal & file explorer
- Model selector with capability metadata
- Keyboard-first UI with command palette
- 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi)
- Tauri 2 desktop app
- React 19 + TypeScript + Vite 8 + Tailwind CSS 4
- Pi Agent extension runtime

Originally forked from OpenCodeUI, now an independent project.
License: GPL-3.0-only
2026-06-01 00:50:06 -03:00

402 lines
10 KiB
TypeScript

/**
* 模型工具函数
* - 唯一标识管理
* - 使用频率记录和排序
*/
import type { ModelInfo } from '../api'
import { serverStorage } from './perServerStorage'
// ============================================
// 模型唯一标识
// ============================================
/**
* 生成模型的唯一标识符
* 格式: providerId:modelId
*/
export function getModelKey(model: ModelInfo): string {
return `${model.providerId}:${model.id}`
}
/**
* 从唯一标识符解析 providerId 和 modelId
*/
export function parseModelKey(key: string): { providerId: string; modelId: string } | null {
const idx = key.indexOf(':')
if (idx === -1) return null
return {
providerId: key.slice(0, idx),
modelId: key.slice(idx + 1),
}
}
/**
* 根据 key 找到对应的模型
*/
export function findModelByKey(models: ModelInfo[], key: string): ModelInfo | undefined {
const parsed = parseModelKey(key)
if (!parsed) return undefined
return models.find(m => m.providerId === parsed.providerId && m.id === parsed.modelId)
}
// ============================================
// 使用频率存储
// ============================================
const STORAGE_KEY = 'model-usage-stats'
const VARIANT_STORAGE_KEY = 'model-variant-prefs'
const PINNED_STORAGE_KEY = 'model-pinned'
const SESSION_SELECTION_STORAGE_KEY = 'session-model-selection'
interface ModelUsageStats {
[modelKey: string]: {
count: number
lastUsed: number
}
}
interface ModelVariantPrefs {
[modelKey: string]: string // modelKey -> variant
}
interface SessionModelSelections {
[sessionId: string]: {
modelKey: string
variant?: string
}
}
/**
* 获取所有模型的使用统计
*/
export function getModelUsageStats(): ModelUsageStats {
try {
const stored = serverStorage.get(STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
} catch {
return {}
}
}
/**
* 记录模型使用
*/
export function recordModelUsage(model: ModelInfo): void {
const key = getModelKey(model)
const stats = getModelUsageStats()
if (!stats[key]) {
stats[key] = { count: 0, lastUsed: 0 }
}
stats[key].count += 1
stats[key].lastUsed = Date.now()
try {
serverStorage.set(STORAGE_KEY, JSON.stringify(stats))
} catch (e) {
console.warn('Failed to save model usage stats:', e)
}
}
/**
* 获取模型的使用次数
*/
export function getModelUsageCount(model: ModelInfo): number {
const key = getModelKey(model)
const stats = getModelUsageStats()
return stats[key]?.count ?? 0
}
// ============================================
// Variant 偏好存储
// ============================================
/**
* 获取所有模型的 variant 偏好
*/
export function getModelVariantPrefs(): ModelVariantPrefs {
try {
const stored = serverStorage.get(VARIANT_STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
} catch {
return {}
}
}
/**
* 保存模型的 variant 偏好
*/
export function saveModelVariantPref(modelKey: string, variant: string | undefined): void {
const prefs = getModelVariantPrefs()
if (variant) {
prefs[modelKey] = variant
} else {
delete prefs[modelKey]
}
try {
serverStorage.set(VARIANT_STORAGE_KEY, JSON.stringify(prefs))
} catch (e) {
console.warn('Failed to save model variant pref:', e)
}
}
/**
* 获取模型的 variant 偏好
*/
export function getModelVariantPref(modelKey: string): string | undefined {
const prefs = getModelVariantPrefs()
return prefs[modelKey]
}
// ============================================
// Session 模型选择存储
// ============================================
export function getSessionModelSelections(): SessionModelSelections {
try {
const stored = serverStorage.get(SESSION_SELECTION_STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
} catch {
return {}
}
}
export function getSessionModelSelection(sessionId: string): { modelKey: string; variant?: string } | undefined {
const selections = getSessionModelSelections()
return selections[sessionId]
}
export function saveSessionModelSelection(sessionId: string, modelKey: string, variant: string | undefined): void {
const selections = getSessionModelSelections()
selections[sessionId] = variant ? { modelKey, variant } : { modelKey }
try {
serverStorage.set(SESSION_SELECTION_STORAGE_KEY, JSON.stringify(selections))
} catch (e) {
console.warn('Failed to save session model selection:', e)
}
}
// ============================================
// 模型排序
// ============================================
export type ModelSortMode = 'frequency' | 'alphabetical' | 'provider'
/**
* 根据使用频率排序模型
* - 最近使用的优先
* - 使用次数多的优先
* - 未使用过的按原始顺序
*/
export function sortModelsByFrequency(models: ModelInfo[]): ModelInfo[] {
const stats = getModelUsageStats()
return [...models].sort((a, b) => {
const keyA = getModelKey(a)
const keyB = getModelKey(b)
const statsA = stats[keyA]
const statsB = stats[keyB]
// 都没使用过,保持原始顺序
if (!statsA && !statsB) return 0
// 只有一个使用过,使用过的排前面
if (!statsA) return 1
if (!statsB) return -1
// 都使用过,按使用次数排序,次数相同按最近使用时间
if (statsA.count !== statsB.count) {
return statsB.count - statsA.count
}
return statsB.lastUsed - statsA.lastUsed
})
}
/**
* 按字母顺序排序模型
*/
export function sortModelsAlphabetically(models: ModelInfo[]): ModelInfo[] {
return [...models].sort((a, b) => a.name.localeCompare(b.name))
}
/**
* 按 provider 分组(返回分组后的扁平数组,常用的 provider 在前)
*/
export function sortModelsByProvider(models: ModelInfo[]): ModelInfo[] {
const stats = getModelUsageStats()
// 计算每个 provider 的总使用次数
const providerUsage: Record<string, number> = {}
for (const model of models) {
const key = getModelKey(model)
const count = stats[key]?.count ?? 0
providerUsage[model.providerId] = (providerUsage[model.providerId] ?? 0) + count
}
// 按 provider 使用频率分组
const groups = models.reduce(
(acc, model) => {
if (!acc[model.providerId]) {
acc[model.providerId] = []
}
acc[model.providerId].push(model)
return acc
},
{} as Record<string, ModelInfo[]>,
)
// 按 provider 使用频率排序
const sortedProviders = Object.keys(groups).sort((a, b) => {
return (providerUsage[b] ?? 0) - (providerUsage[a] ?? 0)
})
// 扁平化,每个 provider 内部按使用频率排序
return sortedProviders.flatMap(providerId => sortModelsByFrequency(groups[providerId]))
}
// ============================================
// 模型分组(用于 UI 展示)
// ============================================
export interface ModelGroup {
providerId: string
providerName: string
models: ModelInfo[]
}
/**
* 将模型按 provider 分组,常用的 provider 在前
*/
export function groupModelsByProvider(models: ModelInfo[]): ModelGroup[] {
const stats = getModelUsageStats()
// 按 providerName 分组 (合并同名 Provider)
const groupMap = models.reduce(
(acc, model) => {
const key = model.providerName // 使用 Name 作为分组键
if (!acc[key]) {
acc[key] = {
providerId: model.providerId, // 这里的 ID 仅作参考,可能不唯一
providerName: model.providerName,
models: [],
}
}
acc[key].models.push(model)
return acc
},
{} as Record<string, ModelGroup>,
)
// 按 provider 使用频率排序 (累加该 Name 下所有模型的使用次数)
const groups = Object.values(groupMap).sort((a, b) => {
// 计算 Group A 的总权重
const weightA = a.models.reduce((sum, m) => sum + (stats[getModelKey(m)]?.count ?? 0), 0)
// 计算 Group B 的总权重
const weightB = b.models.reduce((sum, m) => sum + (stats[getModelKey(m)]?.count ?? 0), 0)
return weightB - weightA
})
// 每个 provider 内部按使用频率排序
for (const group of groups) {
group.models = sortModelsByFrequency(group.models)
}
return groups
}
/**
* 获取最近使用的模型列表(用于快速访问)
*/
export function getRecentModels(models: ModelInfo[], limit = 5): ModelInfo[] {
const stats = getModelUsageStats()
// 过滤出使用过的模型并按最近使用时间排序
// 只要有记录且 count > 0 就视为使用过
const usedModels = models
.filter(m => {
const s = stats[getModelKey(m)]
return s && (s.count > 0 || s.lastUsed > 0)
})
.sort((a, b) => {
const statsA = stats[getModelKey(a)]
const statsB = stats[getModelKey(b)]
// 优先按时间倒序
return (statsB?.lastUsed ?? 0) - (statsA?.lastUsed ?? 0)
})
return usedModels.slice(0, limit)
}
// ============================================
// 模型置顶
// ============================================
/**
* 获取所有置顶的模型 key 列表(有序)
*/
export function getPinnedModelKeys(): string[] {
try {
const stored = serverStorage.get(PINNED_STORAGE_KEY)
return stored ? JSON.parse(stored) : []
} catch {
return []
}
}
/**
* 判断模型是否已置顶
*/
export function isModelPinned(model: ModelInfo): boolean {
const key = getModelKey(model)
return getPinnedModelKeys().includes(key)
}
/**
* 切换模型置顶状态
*/
export function toggleModelPin(model: ModelInfo): boolean {
const key = getModelKey(model)
const pinned = getPinnedModelKeys()
const index = pinned.indexOf(key)
let nowPinned: boolean
if (index !== -1) {
pinned.splice(index, 1)
nowPinned = false
} else {
pinned.push(key)
nowPinned = true
}
try {
serverStorage.set(PINNED_STORAGE_KEY, JSON.stringify(pinned))
} catch (e) {
console.warn('Failed to save pinned models:', e)
}
return nowPinned
}
/**
* 获取置顶模型列表(保持置顶顺序)
*/
export function getPinnedModels(models: ModelInfo[]): ModelInfo[] {
const pinnedKeys = getPinnedModelKeys()
if (pinnedKeys.length === 0) return []
const keySet = new Set(pinnedKeys)
const modelMap = new Map<string, ModelInfo>()
for (const m of models) {
const k = getModelKey(m)
if (keySet.has(k)) modelMap.set(k, m)
}
// 保持置顶顺序
return pinnedKeys.map(k => modelMap.get(k)).filter((m): m is ModelInfo => !!m)
}