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:
@@ -142,7 +142,7 @@
|
||||
>描述</Label>
|
||||
<Input
|
||||
id="model-description"
|
||||
:model-value="form.config?.description || ''"
|
||||
:model-value="getConfigInputValue('description')"
|
||||
placeholder="简短描述此模型的特点"
|
||||
@update:model-value="(v) => setConfigField('description', v || undefined)"
|
||||
/>
|
||||
@@ -155,7 +155,7 @@
|
||||
>最大输出 Token</Label>
|
||||
<Input
|
||||
id="model-output-limit"
|
||||
:model-value="form.config?.output_limit ?? ''"
|
||||
:model-value="getConfigInputValue('output_limit')"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="如 8192"
|
||||
@@ -169,7 +169,7 @@
|
||||
>上下文窗口</Label>
|
||||
<Input
|
||||
id="model-context-limit"
|
||||
:model-value="form.config?.context_limit ?? ''"
|
||||
:model-value="getConfigInputValue('context_limit')"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="如 200000"
|
||||
@@ -177,6 +177,33 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<Checkbox
|
||||
:model-value="isEmbeddingEnabled"
|
||||
class="mt-0.5"
|
||||
@update:model-value="setEmbeddingEnabled"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium">
|
||||
Embedding
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
标记为 Embeddings 模型,并使用独立的 embedding API 格式,不按 Chat 模型处理。
|
||||
</p>
|
||||
<div
|
||||
v-if="isEmbeddingEnabled"
|
||||
class="flex flex-wrap gap-1.5"
|
||||
>
|
||||
<span
|
||||
v-for="format in embeddingApiFormats"
|
||||
:key="format"
|
||||
class="rounded-md border border-border/60 bg-background px-2 py-0.5 text-[11px] font-mono text-muted-foreground"
|
||||
>{{ format }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 价格配置 -->
|
||||
@@ -333,7 +360,7 @@ import {
|
||||
Loader2, Layers, SquarePen,
|
||||
Search, ChevronRight, Plus, Trash2
|
||||
} from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input, Label } from '@/components/ui'
|
||||
import { Dialog, Button, Input, Label, Checkbox } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { parseNumberInput, sortResolutionEntries } from '@/utils/form'
|
||||
@@ -349,10 +376,13 @@ import {
|
||||
createGlobalModel,
|
||||
updateGlobalModel,
|
||||
type GlobalModelResponse,
|
||||
type GlobalModelCreate,
|
||||
type GlobalModelUpdate,
|
||||
} from '@/api/global-models'
|
||||
import type { TieredPricingConfig } from '@/api/endpoints/types'
|
||||
import {
|
||||
EMBEDDING_API_FORMATS,
|
||||
buildGlobalModelCreatePayload,
|
||||
buildGlobalModelUpdatePayload,
|
||||
} from './global-model-form-helpers'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -476,6 +506,8 @@ const VIDEO_RESOLUTION_PRICE_PRESETS: Record<
|
||||
],
|
||||
}
|
||||
|
||||
const embeddingApiFormats = [...EMBEDDING_API_FORMATS]
|
||||
|
||||
interface FormData {
|
||||
name: string
|
||||
display_name: string
|
||||
@@ -496,6 +528,12 @@ const defaultForm = (): FormData => ({
|
||||
|
||||
const form = ref<FormData>(defaultForm())
|
||||
|
||||
const isEmbeddingEnabled = computed(() => {
|
||||
return form.value.supported_capabilities?.includes('embedding') === true
|
||||
|| form.value.config?.embedding === true
|
||||
|| form.value.config?.model_type === 'embedding'
|
||||
})
|
||||
|
||||
const KEEP_FALSE_CONFIG_KEYS = new Set(['streaming'])
|
||||
|
||||
// 设置 config 字段
|
||||
@@ -510,6 +548,34 @@ function setConfigField(key: string, value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigInputValue(key: string): string | number {
|
||||
const value = form.value.config?.[key]
|
||||
return typeof value === 'string' || typeof value === 'number' ? value : ''
|
||||
}
|
||||
|
||||
function setEmbeddingEnabled(enabled: boolean) {
|
||||
const caps = new Set(form.value.supported_capabilities || [])
|
||||
if (enabled) {
|
||||
caps.add('embedding')
|
||||
setConfigField('embedding', true)
|
||||
setConfigField('model_type', 'embedding')
|
||||
setConfigField('streaming', false)
|
||||
form.value.config = {
|
||||
...(form.value.config || {}),
|
||||
api_formats: [...embeddingApiFormats],
|
||||
}
|
||||
} else {
|
||||
caps.delete('embedding')
|
||||
setConfigField('embedding', undefined)
|
||||
if (form.value.config?.model_type === 'embedding') setConfigField('model_type', undefined)
|
||||
if (Array.isArray(form.value.config?.api_formats)
|
||||
&& form.value.config.api_formats.every((format) => embeddingApiFormats.includes(String(format)))) {
|
||||
setConfigField('api_formats', undefined)
|
||||
}
|
||||
}
|
||||
form.value.supported_capabilities = [...caps]
|
||||
}
|
||||
|
||||
function getNested(obj: unknown, path: string): unknown {
|
||||
if (!obj || typeof obj !== 'object') return undefined
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
@@ -670,7 +736,7 @@ function selectModel(model: ModelsDevModelItem) {
|
||||
|
||||
// 构建 config
|
||||
const config: Record<string, unknown> = {
|
||||
streaming: true,
|
||||
streaming: model.supportsEmbedding ? false : true,
|
||||
}
|
||||
if (model.supportsVision) config.vision = true
|
||||
if (model.supportsToolCall) config.function_calling = true
|
||||
@@ -687,6 +753,10 @@ function selectModel(model: ModelsDevModelItem) {
|
||||
if (model.inputModalities?.length) config.input_modalities = model.inputModalities
|
||||
if (model.outputModalities?.length) config.output_modalities = model.outputModalities
|
||||
form.value.config = config
|
||||
form.value.supported_capabilities = model.supportsEmbedding ? ['embedding'] : []
|
||||
if (model.supportsEmbedding) {
|
||||
setEmbeddingEnabled(true)
|
||||
}
|
||||
loadVideoPricingFromConfig()
|
||||
|
||||
if (model.inputPrice !== undefined || model.outputPrice !== undefined) {
|
||||
@@ -796,26 +866,11 @@ async function handleSubmit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isEditMode.value && props.model) {
|
||||
const updateData: GlobalModelUpdate = {
|
||||
display_name: form.value.display_name,
|
||||
config: cleanConfig || null,
|
||||
default_price_per_request: form.value.default_price_per_request ?? null,
|
||||
default_tiered_pricing: finalTieredPricing,
|
||||
supported_capabilities: form.value.supported_capabilities?.length ? form.value.supported_capabilities : null,
|
||||
is_active: form.value.is_active,
|
||||
}
|
||||
const updateData = buildGlobalModelUpdatePayload(form.value, finalTieredPricing)
|
||||
await updateGlobalModel(props.model.id, updateData)
|
||||
success('模型更新成功')
|
||||
} else {
|
||||
const createData: GlobalModelCreate = {
|
||||
name: form.value.name ?? '',
|
||||
display_name: form.value.display_name ?? '',
|
||||
config: cleanConfig,
|
||||
default_price_per_request: form.value.default_price_per_request ?? undefined,
|
||||
default_tiered_pricing: finalTieredPricing,
|
||||
supported_capabilities: form.value.supported_capabilities?.length ? form.value.supported_capabilities : undefined,
|
||||
is_active: form.value.is_active,
|
||||
}
|
||||
const createData = buildGlobalModelCreatePayload(form.value, finalTieredPricing)
|
||||
await createGlobalModel(createData)
|
||||
success('模型创建成功')
|
||||
clearSelection()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
EMBEDDING_API_FORMATS,
|
||||
buildGlobalModelCreatePayload,
|
||||
buildGlobalModelUpdatePayload,
|
||||
} from '../global-model-form-helpers'
|
||||
|
||||
const embeddingPricing = {
|
||||
tiers: [{ up_to: null, input_price_per_1m: 0.02, output_price_per_1m: 0 }],
|
||||
}
|
||||
|
||||
describe('global model form embedding payload helpers', () => {
|
||||
it('preserves embedding metadata in create payloads', () => {
|
||||
const payload = buildGlobalModelCreatePayload({
|
||||
name: 'text-embedding-3-small',
|
||||
display_name: 'text-embedding-3-small',
|
||||
supported_capabilities: ['embedding'],
|
||||
config: {
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: [...EMBEDDING_API_FORMATS],
|
||||
},
|
||||
is_active: true,
|
||||
}, embeddingPricing)
|
||||
|
||||
expect(payload).toMatchObject({
|
||||
name: 'text-embedding-3-small',
|
||||
supported_capabilities: ['embedding'],
|
||||
config: {
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['openai:embedding', 'gemini:embedding', 'jina:embedding', 'doubao:embedding'],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves embedding metadata in update payloads', () => {
|
||||
const payload = buildGlobalModelUpdatePayload({
|
||||
name: 'unused-on-update',
|
||||
display_name: 'Jina Embeddings v3',
|
||||
supported_capabilities: ['embedding'],
|
||||
config: {
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['jina:embedding'],
|
||||
},
|
||||
is_active: true,
|
||||
}, embeddingPricing)
|
||||
|
||||
expect(payload.supported_capabilities).toEqual(['embedding'])
|
||||
expect(payload.config).toEqual({
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['jina:embedding'],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { GlobalModelCreate, GlobalModelUpdate } from '@/api/global-models'
|
||||
import type { TieredPricingConfig } from '@/api/endpoints/types'
|
||||
|
||||
export const EMBEDDING_API_FORMATS = [
|
||||
'openai:embedding',
|
||||
'gemini:embedding',
|
||||
'jina:embedding',
|
||||
'doubao:embedding',
|
||||
] as const
|
||||
|
||||
export const RERANK_API_FORMATS = [
|
||||
'openai:rerank',
|
||||
'jina:rerank',
|
||||
] as const
|
||||
|
||||
export interface GlobalModelFormPayloadState {
|
||||
name: string
|
||||
display_name: string
|
||||
default_price_per_request?: number
|
||||
supported_capabilities?: string[]
|
||||
config?: Record<string, unknown>
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
function cleanGlobalModelConfig(form: GlobalModelFormPayloadState): Record<string, unknown> | undefined {
|
||||
return form.config && Object.keys(form.config).length > 0 ? form.config : undefined
|
||||
}
|
||||
|
||||
export function buildGlobalModelCreatePayload(
|
||||
form: GlobalModelFormPayloadState,
|
||||
defaultTieredPricing: TieredPricingConfig,
|
||||
): GlobalModelCreate {
|
||||
return {
|
||||
name: form.name ?? '',
|
||||
display_name: form.display_name ?? '',
|
||||
config: cleanGlobalModelConfig(form),
|
||||
default_price_per_request: form.default_price_per_request ?? undefined,
|
||||
default_tiered_pricing: defaultTieredPricing,
|
||||
supported_capabilities: form.supported_capabilities?.length ? form.supported_capabilities : undefined,
|
||||
is_active: form.is_active,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildGlobalModelUpdatePayload(
|
||||
form: GlobalModelFormPayloadState,
|
||||
defaultTieredPricing: TieredPricingConfig,
|
||||
): GlobalModelUpdate {
|
||||
return {
|
||||
display_name: form.display_name,
|
||||
config: cleanGlobalModelConfig(form) || null,
|
||||
default_price_per_request: form.default_price_per_request ?? null,
|
||||
default_tiered_pricing: defaultTieredPricing,
|
||||
supported_capabilities: form.supported_capabilities?.length ? form.supported_capabilities : null,
|
||||
is_active: form.is_active,
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,17 @@
|
||||
>
|
||||
所有全局模型已添加到此 Provider
|
||||
</p>
|
||||
<div
|
||||
v-if="selectedGlobalModelSupportsEmbedding"
|
||||
class="rounded-lg border border-border/60 bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="text-sm font-medium">
|
||||
Embedding
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
此模型将继承全局模型的 Embeddings 元数据,不按 Chat 能力处理。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑模式:显示模型信息 -->
|
||||
@@ -56,6 +67,13 @@
|
||||
<p class="text-sm text-muted-foreground font-mono">
|
||||
{{ editingModel?.provider_model_name }}
|
||||
</p>
|
||||
<Badge
|
||||
v-if="editingModelSupportsEmbedding"
|
||||
variant="secondary"
|
||||
class="mt-2 text-xs"
|
||||
>
|
||||
Embedding
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,6 +232,7 @@ import {
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Badge,
|
||||
} from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseNumberInput, sortResolutionEntries } from '@/utils/form'
|
||||
@@ -221,6 +240,11 @@ import { createModel, updateModel, getProviderModels } from '@/api/endpoints/mod
|
||||
import { listGlobalModels, type GlobalModelResponse } from '@/api/global-models'
|
||||
import TieredPricingEditor from '@/features/models/components/TieredPricingEditor.vue'
|
||||
import type { Model, TieredPricingConfig } from '@/api/endpoints'
|
||||
import {
|
||||
buildProviderModelCreatePayload,
|
||||
buildProviderModelUpdatePayload,
|
||||
modelSupportsEmbedding,
|
||||
} from './provider-model-form-helpers'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -245,6 +269,16 @@ const tieredPricingEditorRef = ref<InstanceType<typeof TieredPricingEditor> | nu
|
||||
|
||||
const isEditing = computed(() => !!props.editingModel)
|
||||
|
||||
const selectedGlobalModel = computed(() => {
|
||||
return availableGlobalModels.value.find(model => model.id === form.value.global_model_id) || null
|
||||
})
|
||||
|
||||
const selectedGlobalModelSupportsEmbedding = computed(() => modelSupportsEmbedding(selectedGlobalModel.value))
|
||||
const editingModelSupportsEmbedding = computed(() => {
|
||||
return props.editingModel?.effective_supports_embedding === true
|
||||
|| modelSupportsEmbedding(props.editingModel)
|
||||
})
|
||||
|
||||
// 1h 缓存定价始终显示
|
||||
const showCache1h = true
|
||||
|
||||
@@ -571,35 +605,36 @@ async function handleSubmit() {
|
||||
if (isEditing.value && props.editingModel) {
|
||||
// 编辑模式
|
||||
// 注意:使用 null 而不是 undefined 来显式清空字段(undefined 会被 JSON 序列化忽略)
|
||||
await updateModel(props.providerId, props.editingModel.id, {
|
||||
tiered_pricing: finalTieredPricing,
|
||||
price_per_request: form.value.price_per_request ?? null,
|
||||
config: cleanConfig || null,
|
||||
supports_vision: form.value.supports_vision,
|
||||
supports_function_calling: form.value.supports_function_calling,
|
||||
supports_streaming: form.value.supports_streaming,
|
||||
supports_extended_thinking: form.value.supports_extended_thinking,
|
||||
supports_image_generation: form.value.supports_image_generation,
|
||||
is_active: form.value.is_active
|
||||
})
|
||||
await updateModel(props.providerId, props.editingModel.id, buildProviderModelUpdatePayload({
|
||||
finalTieredPricing,
|
||||
pricePerRequest: form.value.price_per_request,
|
||||
cleanConfig,
|
||||
supportsVision: form.value.supports_vision,
|
||||
supportsFunctionCalling: form.value.supports_function_calling,
|
||||
supportsStreaming: form.value.supports_streaming,
|
||||
supportsExtendedThinking: form.value.supports_extended_thinking,
|
||||
supportsImageGeneration: form.value.supports_image_generation,
|
||||
isActive: form.value.is_active
|
||||
}))
|
||||
showSuccess('模型配置已更新')
|
||||
} else {
|
||||
// 添加模式:只有用户修改了配置才提交 tiered_pricing,否则保持继承关系
|
||||
const selectedModel = availableGlobalModels.value.find(m => m.id === form.value.global_model_id)
|
||||
await createModel(props.providerId, {
|
||||
global_model_id: form.value.global_model_id,
|
||||
provider_model_name: selectedModel?.name || '',
|
||||
// 只有修改了才提交,否则传 undefined 让后端继承 GlobalModel 配置
|
||||
tiered_pricing: tieredPricingModified.value ? finalTieredPricing : undefined,
|
||||
price_per_request: form.value.price_per_request,
|
||||
config: configTouched.value ? cleanConfig : undefined,
|
||||
supports_vision: form.value.supports_vision,
|
||||
supports_function_calling: form.value.supports_function_calling,
|
||||
supports_streaming: form.value.supports_streaming,
|
||||
supports_extended_thinking: form.value.supports_extended_thinking,
|
||||
supports_image_generation: form.value.supports_image_generation,
|
||||
is_active: form.value.is_active
|
||||
})
|
||||
await createModel(props.providerId, buildProviderModelCreatePayload({
|
||||
globalModelId: form.value.global_model_id,
|
||||
providerModelName: selectedModel?.name || '',
|
||||
finalTieredPricing,
|
||||
tieredPricingModified: tieredPricingModified.value,
|
||||
pricePerRequest: form.value.price_per_request,
|
||||
cleanConfig,
|
||||
configTouched: configTouched.value,
|
||||
supportsVision: form.value.supports_vision,
|
||||
supportsFunctionCalling: form.value.supports_function_calling,
|
||||
supportsStreaming: form.value.supports_streaming,
|
||||
supportsExtendedThinking: form.value.supports_extended_thinking,
|
||||
supportsImageGeneration: form.value.supports_image_generation,
|
||||
isActive: form.value.is_active
|
||||
}))
|
||||
showSuccess('模型已添加')
|
||||
}
|
||||
emit('update:open', false)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildProviderModelCreatePayload,
|
||||
buildProviderModelUpdatePayload,
|
||||
modelSupportsEmbedding,
|
||||
} from '../provider-model-form-helpers'
|
||||
|
||||
const pricing = {
|
||||
tiers: [{ up_to: null, input_price_per_1m: 0.02, output_price_per_1m: 0 }],
|
||||
}
|
||||
|
||||
describe('provider model form embedding helpers', () => {
|
||||
it.each([
|
||||
{ supported_capabilities: ['embedding'], config: {} },
|
||||
{ supported_capabilities: null, config: { embedding: true } },
|
||||
{ supported_capabilities: null, config: { model_type: 'embedding' } },
|
||||
{ supported_capabilities: null, config: { api_formats: ['doubao:embedding'] } },
|
||||
{ supports_embedding: true, effective_supports_embedding: null, config: {} },
|
||||
{ supports_embedding: null, effective_supports_embedding: true, config: {} },
|
||||
])('detects embedding metadata from %o', (model) => {
|
||||
expect(modelSupportsEmbedding(model)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps provider create payload inherited from the selected embedding global model', () => {
|
||||
const payload = buildProviderModelCreatePayload({
|
||||
globalModelId: 'gm-embedding',
|
||||
providerModelName: 'text-embedding-3-small',
|
||||
finalTieredPricing: pricing,
|
||||
tieredPricingModified: false,
|
||||
pricePerRequest: undefined,
|
||||
cleanConfig: {
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['openai:embedding'],
|
||||
},
|
||||
configTouched: false,
|
||||
supportsStreaming: false,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
expect(payload).toMatchObject({
|
||||
global_model_id: 'gm-embedding',
|
||||
provider_model_name: 'text-embedding-3-small',
|
||||
tiered_pricing: undefined,
|
||||
config: undefined,
|
||||
supports_streaming: false,
|
||||
})
|
||||
expect('supports_embedding' in payload).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves edited provider embedding config without posting unsupported embedding controls', () => {
|
||||
const payload = buildProviderModelUpdatePayload({
|
||||
finalTieredPricing: pricing,
|
||||
pricePerRequest: undefined,
|
||||
cleanConfig: {
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['gemini:embedding'],
|
||||
},
|
||||
supportsStreaming: false,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
expect(payload.config).toEqual({
|
||||
streaming: false,
|
||||
embedding: true,
|
||||
model_type: 'embedding',
|
||||
api_formats: ['gemini:embedding'],
|
||||
})
|
||||
expect(payload.supports_streaming).toBe(false)
|
||||
expect('supports_embedding' in payload).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { ModelCreate, ModelUpdate, TieredPricingConfig } from '@/api/endpoints'
|
||||
|
||||
interface EmbeddingMetadataCarrier {
|
||||
supported_capabilities?: string[] | null
|
||||
supports_embedding?: boolean | null
|
||||
effective_supports_embedding?: boolean | null
|
||||
config?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface ProviderModelCreatePayloadInput {
|
||||
globalModelId: string
|
||||
providerModelName: string
|
||||
finalTieredPricing: TieredPricingConfig | null
|
||||
tieredPricingModified: boolean
|
||||
pricePerRequest?: number
|
||||
cleanConfig?: Record<string, unknown>
|
||||
configTouched: boolean
|
||||
supportsVision?: boolean
|
||||
supportsFunctionCalling?: boolean
|
||||
supportsStreaming?: boolean
|
||||
supportsExtendedThinking?: boolean
|
||||
supportsImageGeneration?: boolean
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export interface ProviderModelUpdatePayloadInput {
|
||||
finalTieredPricing: TieredPricingConfig | null
|
||||
pricePerRequest?: number
|
||||
cleanConfig?: Record<string, unknown>
|
||||
supportsVision?: boolean
|
||||
supportsFunctionCalling?: boolean
|
||||
supportsStreaming?: boolean
|
||||
supportsExtendedThinking?: boolean
|
||||
supportsImageGeneration?: boolean
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export function modelSupportsEmbedding(model: EmbeddingMetadataCarrier | null | undefined): boolean {
|
||||
if (!model) return false
|
||||
if ('effective_supports_embedding' in model && model.effective_supports_embedding === true) return true
|
||||
if ('supports_embedding' in model && model.supports_embedding === true) return true
|
||||
|
||||
const supportedCapabilities = 'supported_capabilities' in model ? model.supported_capabilities : null
|
||||
const config = model.config || {}
|
||||
return supportedCapabilities?.includes('embedding') === true
|
||||
|| config.embedding === true
|
||||
|| config.model_type === 'embedding'
|
||||
|| (Array.isArray(config.api_formats) && config.api_formats.some((format) => String(format).endsWith(':embedding')))
|
||||
}
|
||||
|
||||
export function buildProviderModelCreatePayload(input: ProviderModelCreatePayloadInput): ModelCreate {
|
||||
return {
|
||||
global_model_id: input.globalModelId,
|
||||
provider_model_name: input.providerModelName,
|
||||
tiered_pricing: input.tieredPricingModified && input.finalTieredPricing ? input.finalTieredPricing : undefined,
|
||||
price_per_request: input.pricePerRequest,
|
||||
config: input.configTouched ? input.cleanConfig : undefined,
|
||||
supports_vision: input.supportsVision,
|
||||
supports_function_calling: input.supportsFunctionCalling,
|
||||
supports_streaming: input.supportsStreaming,
|
||||
supports_extended_thinking: input.supportsExtendedThinking,
|
||||
supports_image_generation: input.supportsImageGeneration,
|
||||
is_active: input.isActive,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProviderModelUpdatePayload(input: ProviderModelUpdatePayloadInput): ModelUpdate {
|
||||
return {
|
||||
tiered_pricing: input.finalTieredPricing,
|
||||
price_per_request: input.pricePerRequest ?? null,
|
||||
config: input.cleanConfig || null,
|
||||
supports_vision: input.supportsVision,
|
||||
supports_function_calling: input.supportsFunctionCalling,
|
||||
supports_streaming: input.supportsStreaming,
|
||||
supports_extended_thinking: input.supportsExtendedThinking,
|
||||
supports_image_generation: input.supportsImageGeneration,
|
||||
is_active: input.isActive,
|
||||
}
|
||||
}
|
||||
@@ -700,7 +700,7 @@ function runMappingTest(testingKey: string, modelName: string) {
|
||||
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
|
||||
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
|
||||
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(modelName)
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(modelName, selectedTestEndpoint.value?.api_format)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
}
|
||||
|
||||
|
||||
@@ -543,6 +543,7 @@ async function testModelConnection(model: Model) {
|
||||
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
|
||||
model.global_model_name || model.provider_model_name,
|
||||
selectedTestEndpoint.value?.api_format,
|
||||
)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
modelTest.testResult.value = null
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildDefaultModelTestRequestBody } from '../model-test-request'
|
||||
|
||||
describe('buildDefaultModelTestRequestBody', () => {
|
||||
it.each([
|
||||
'openai:embedding',
|
||||
'gemini:embedding',
|
||||
'jina:embedding',
|
||||
'doubao:embedding',
|
||||
' OPENAI:EMBEDDING ',
|
||||
])('uses embedding input payloads for %s api formats', (apiFormat) => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody('text-embedding-3-small', apiFormat))
|
||||
|
||||
expect(body).toEqual({
|
||||
model: 'text-embedding-3-small',
|
||||
input: 'This is a test embedding input.',
|
||||
})
|
||||
expect(body.messages).toBeUndefined()
|
||||
expect(body.stream).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'openai:rerank',
|
||||
'jina:rerank',
|
||||
' JINA:RERANK ',
|
||||
])('uses rerank query/documents payloads for %s api formats', (apiFormat) => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody('bge-reranker-base', apiFormat))
|
||||
|
||||
expect(body.model).toBe('bge-reranker-base')
|
||||
expect(body.query).toBe('This is a test rerank query.')
|
||||
expect(body.documents).toHaveLength(2)
|
||||
expect(body.top_n).toBe(1)
|
||||
expect(body.return_documents).toBe(true)
|
||||
expect(body.messages).toBeUndefined()
|
||||
expect(body.stream).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps chat payloads for chat api formats', () => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody('gpt-5.1', 'openai:chat'))
|
||||
|
||||
expect(body.messages).toEqual([{ role: 'user', content: 'Hello! This is a test message.' }])
|
||||
expect(body.stream).toBe(true)
|
||||
expect(body.input).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,27 @@ const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
|
||||
export const POOL_TEST_CONCURRENCY = 5
|
||||
export const SINGLE_TEST_CONCURRENCY = 1
|
||||
|
||||
export function buildDefaultModelTestRequestBody(modelName: string): string {
|
||||
export function buildDefaultModelTestRequestBody(modelName: string, apiFormat?: string | null): string {
|
||||
if (apiFormat?.trim().toLowerCase().endsWith(':embedding')) {
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
input: 'This is a test embedding input.',
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
if (apiFormat?.trim().toLowerCase().endsWith(':rerank')) {
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
query: 'This is a test rerank query.',
|
||||
documents: [
|
||||
'This document is relevant to the test query.',
|
||||
'This document is unrelated.',
|
||||
],
|
||||
top_n: 1,
|
||||
return_documents: true,
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
messages: [
|
||||
|
||||
@@ -8,10 +8,16 @@ const ENDPOINT_SORT_ORDER = [
|
||||
'openai:chat',
|
||||
'openai:responses',
|
||||
'openai:responses:compact',
|
||||
'openai:embedding',
|
||||
'openai:rerank',
|
||||
'gemini:generate_content',
|
||||
'gemini:embedding',
|
||||
'openai:video',
|
||||
'gemini:video',
|
||||
'gemini:files',
|
||||
'jina:embedding',
|
||||
'jina:rerank',
|
||||
'doubao:embedding',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,13 @@ export function useProviderFilters(
|
||||
{ value: 'openai:chat', label: 'OpenAI Chat' },
|
||||
{ value: 'openai:responses', label: 'OpenAI Responses' },
|
||||
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact' },
|
||||
{ value: 'openai:embedding', label: 'OpenAI Embedding' },
|
||||
{ value: 'openai:rerank', label: 'OpenAI Rerank' },
|
||||
{ value: 'gemini:generate_content', label: 'Gemini Generate Content' },
|
||||
{ value: 'gemini:embedding', label: 'Gemini Embedding' },
|
||||
{ value: 'jina:embedding', label: 'Jina Embedding' },
|
||||
{ value: 'jina:rerank', label: 'Jina Rerank' },
|
||||
{ value: 'doubao:embedding', label: 'Doubao Embedding' },
|
||||
]
|
||||
|
||||
const modelFilters = computed<FilterOption[]>(() => {
|
||||
|
||||
Reference in New Issue
Block a user