mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: add embedding and rerank support
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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) {
|
||||
|
||||
41
frontend/src/views/user/model-catalog-helpers.ts
Normal file
41
frontend/src/views/user/model-catalog-helpers.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user