feat: add embedding and rerank support

This commit is contained in:
Kayphoon
2026-05-03 17:32:41 +08:00
parent 3e2eca4fd0
commit 5abe664d65
87 changed files with 5520 additions and 184 deletions

View File

@@ -80,6 +80,14 @@
<div>
<div class="flex items-center gap-2">
<span class="font-medium hover:text-primary transition-colors">{{ model.display_name || model.name }}</span>
<Badge
v-for="capability in getModelCapabilityLabels(model)"
:key="capability"
variant="secondary"
class="text-[10px] px-1.5 py-0"
>
{{ capability }}
</Badge>
</div>
<div class="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
<span>{{ model.name }}</span>
@@ -150,6 +158,16 @@
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<span class="font-medium truncate block">{{ model.display_name || model.name }}</span>
<div class="flex flex-wrap gap-1 mt-1">
<Badge
v-for="capability in getModelCapabilityLabels(model)"
:key="capability"
variant="secondary"
class="text-[10px] px-1.5 py-0"
>
{{ capability }}
</Badge>
</div>
<div class="text-xs text-muted-foreground flex items-center gap-1 mt-0.5">
<span class="truncate">{{ model.name }}</span>
<button
@@ -228,6 +246,7 @@ import UserModelDetailDrawer from './components/UserModelDetailDrawer.vue'
import { useRowClick } from '@/composables/useRowClick'
import { log } from '@/utils/logger'
import { parseApiError } from '@/utils/errorParser'
import { getModelCapabilityLabels } from './model-catalog-helpers'
const { error: showError } = useToast()
const { copyToClipboard } = useClipboard()

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import type { PublicGlobalModel } from '@/api/public-models'
import { getModelCapabilityLabels, supportsEmbedding, supportsRerank } from '../model-catalog-helpers'
function model(overrides: Partial<PublicGlobalModel>): PublicGlobalModel {
return {
id: 'gm-test',
name: 'model-test',
display_name: 'Model Test',
is_active: true,
default_tiered_pricing: null,
default_price_per_request: null,
supported_capabilities: null,
config: null,
usage_count: 0,
...overrides,
}
}
describe('model catalog embedding helpers', () => {
it('labels embedding models distinctly from chat models', () => {
expect(getModelCapabilityLabels(model({
supported_capabilities: ['embedding'],
config: { streaming: false, api_formats: ['openai:embedding'] },
}))).toEqual(['Embedding'])
expect(getModelCapabilityLabels(model({
config: { streaming: true },
}))).toEqual(['Chat'])
})
it('detects embedding metadata from explicit and config-derived frontend fields', () => {
expect(supportsEmbedding(model({ supports_embedding: true }))).toBe(true)
expect(supportsEmbedding(model({ config: { embedding: true } }))).toBe(true)
expect(supportsEmbedding(model({ config: { model_type: 'embedding' } }))).toBe(true)
expect(supportsEmbedding(model({ config: { api_formats: ['jina:embedding'] } }))).toBe(true)
expect(supportsEmbedding(model({ config: { api_formats: ['openai:chat'] } }))).toBe(false)
})
it('labels rerank models distinctly from chat and embedding models', () => {
const rerank = model({
supported_capabilities: ['rerank'],
config: { streaming: false, api_formats: ['jina:rerank'] },
})
expect(supportsRerank(rerank)).toBe(true)
expect(getModelCapabilityLabels(rerank)).toEqual(['Rerank'])
expect(supportsRerank(model({ config: { api_formats: ['openai:embedding'] } }))).toBe(false)
})
})

View File

@@ -96,6 +96,23 @@
{{ model.config?.image_generation === true ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Database class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
<p class="text-sm font-medium">
Embedding
</p>
<p class="text-xs text-muted-foreground">
向量嵌入
</p>
</div>
<Badge
:variant="supportsEmbedding(model) ? 'default' : 'secondary'"
class="text-xs"
>
{{ supportsEmbedding(model) ? '支持' : '不支持' }}
</Badge>
</div>
<div class="flex items-center gap-2 p-3 rounded-lg border">
<Eye class="w-5 h-5 text-muted-foreground" />
<div class="flex-1">
@@ -303,6 +320,7 @@ import {
Zap,
Copy,
Layers,
Database,
Image as ImageIcon
} from 'lucide-vue-next'
import { useEscapeKey } from '@/composables/useEscapeKey'
@@ -376,6 +394,14 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
return get1hCachePrice(tieredPricing.tiers[0])
}
function supportsEmbedding(model: PublicGlobalModel): boolean {
return model.supports_embedding === true
|| model.supported_capabilities?.includes('embedding') === true
|| model.config?.embedding === true
|| model.config?.model_type === 'embedding'
|| (Array.isArray(model.config?.api_formats) && model.config.api_formats.some((format) => String(format).endsWith(':embedding')))
}
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.open) {

View File

@@ -0,0 +1,41 @@
import type { PublicGlobalModel } from '@/api/public-models'
export function supportsEmbedding(model: PublicGlobalModel): boolean {
return model.supports_embedding === true
|| model.supported_capabilities?.includes('embedding') === true
|| model.config?.embedding === true
|| model.config?.model_type === 'embedding'
|| (Array.isArray(model.config?.api_formats) && model.config.api_formats.some((format) => String(format).endsWith(':embedding')))
}
export function supportsRerank(model: PublicGlobalModel): boolean {
return model.supported_capabilities?.includes('rerank') === true
|| model.config?.rerank === true
|| model.config?.model_type === 'rerank'
|| (Array.isArray(model.config?.api_formats) && model.config.api_formats.some((format) => String(format).endsWith(':rerank')))
}
export function hasVideoPricing(model: PublicGlobalModel): boolean {
const billing = model.config?.billing
const video = billing && typeof billing === 'object' && !Array.isArray(billing)
? (billing as Record<string, unknown>).video
: null
const priceByResolution = video && typeof video === 'object' && !Array.isArray(video)
? (video as Record<string, unknown>).price_per_second_by_resolution
: null
return !!priceByResolution && typeof priceByResolution === 'object' && Object.keys(priceByResolution).length > 0
}
export function getModelCapabilityLabels(model: PublicGlobalModel): string[] {
const labels: string[] = []
if (supportsRerank(model)) {
labels.push('Rerank')
} else if (supportsEmbedding(model)) {
labels.push('Embedding')
} else {
labels.push('Chat')
}
if (model.config?.image_generation === true) labels.push('Image')
if (hasVideoPricing(model)) labels.push('Video')
return labels
}