Merge remote-tracking branch 'origin/pr/593'

This commit is contained in:
elky
2026-06-02 21:40:59 +08:00
56 changed files with 1605 additions and 42 deletions
@@ -21,6 +21,8 @@ describe('api format display helpers', () => {
expect(normalizeApiFormatAlias('JINA_EMBEDDING')).toBe(API_FORMATS.JINA_EMBEDDING)
expect(normalizeApiFormatAlias('JINA_RERANK')).toBe(API_FORMATS.JINA_RERANK)
expect(normalizeApiFormatAlias('DOUBAO_EMBEDDING')).toBe(API_FORMATS.DOUBAO_EMBEDDING)
expect(normalizeApiFormatAlias('ALIYUN_MULTIMODAL_EMBEDDING')).toBe(API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING)
expect(normalizeApiFormatAlias('dashscope:multimodal_embedding')).toBe(API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING)
})
it('formats rerank api format ids distinctly from chat formats', () => {
@@ -35,10 +37,12 @@ describe('api format display helpers', () => {
expect(formatApiFormat(API_FORMATS.GEMINI_EMBEDDING)).toBe('Gemini Embedding')
expect(formatApiFormat(API_FORMATS.JINA_EMBEDDING)).toBe('Jina Embedding')
expect(formatApiFormat(API_FORMATS.DOUBAO_EMBEDDING)).toBe('Doubao Embedding')
expect(formatApiFormat(API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING)).toBe('Aliyun Multimodal Embedding')
expect(formatApiFormatShort(API_FORMATS.OPENAI_EMBEDDING)).toBe('OE')
expect(formatApiFormatShort(API_FORMATS.GEMINI_EMBEDDING)).toBe('GE')
expect(formatApiFormatShort(API_FORMATS.JINA_EMBEDDING)).toBe('JE')
expect(formatApiFormatShort(API_FORMATS.DOUBAO_EMBEDDING)).toBe('DE')
expect(formatApiFormatShort(API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING)).toBe('AE')
})
it('does not remap retired api format ids', () => {
@@ -72,6 +76,7 @@ describe('api format display helpers', () => {
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING,
])).toEqual([
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_RESPONSES,
@@ -81,6 +86,7 @@ describe('api format display helpers', () => {
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.DOUBAO_EMBEDDING,
API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING,
'openai:compact',
])
})
@@ -104,6 +110,7 @@ describe('api format display helpers', () => {
it('groups embedding api formats by provider family', () => {
expect(groupApiFormats([
API_FORMATS.DOUBAO_EMBEDDING,
API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.GEMINI_EMBEDDING,
@@ -114,6 +121,7 @@ describe('api format display helpers', () => {
{ family: 'gemini', label: 'Gemini', formats: [API_FORMATS.GEMINI_EMBEDDING] },
{ family: 'jina', label: 'Jina', formats: [API_FORMATS.JINA_EMBEDDING, API_FORMATS.JINA_RERANK] },
{ family: 'doubao', label: 'Doubao', formats: [API_FORMATS.DOUBAO_EMBEDDING] },
{ family: 'aliyun', label: 'Aliyun', formats: [API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING] },
])
})
+21 -2
View File
@@ -18,6 +18,7 @@ export const API_FORMATS = {
JINA_EMBEDDING: 'jina:embedding',
JINA_RERANK: 'jina:rerank',
DOUBAO_EMBEDDING: 'doubao:embedding',
ALIYUN_MULTIMODAL_EMBEDDING: 'aliyun:multimodal_embedding',
} as const
export type APIFormat = typeof API_FORMATS[keyof typeof API_FORMATS]
@@ -39,6 +40,7 @@ export const API_FORMAT_LABELS: Record<string, string> = {
[API_FORMATS.JINA_EMBEDDING]: 'Jina Embedding',
[API_FORMATS.JINA_RERANK]: 'Jina Rerank',
[API_FORMATS.DOUBAO_EMBEDDING]: 'Doubao Embedding',
[API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING]: 'Aliyun Multimodal Embedding',
CLAUDE: 'Claude Messages',
CLAUDE_MESSAGES: 'Claude Messages',
OPENAI: 'OpenAI Chat',
@@ -56,6 +58,7 @@ export const API_FORMAT_LABELS: Record<string, string> = {
JINA_EMBEDDING: 'Jina Embedding',
JINA_RERANK: 'Jina Rerank',
DOUBAO_EMBEDDING: 'Doubao Embedding',
ALIYUN_MULTIMODAL_EMBEDDING: 'Aliyun Multimodal Embedding',
}
// API 格式缩写映射(用于空间紧凑的显示场景)
@@ -75,6 +78,7 @@ export const API_FORMAT_SHORT: Record<string, string> = {
[API_FORMATS.JINA_EMBEDDING]: 'JE',
[API_FORMATS.JINA_RERANK]: 'JR',
[API_FORMATS.DOUBAO_EMBEDDING]: 'DE',
[API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING]: 'AE',
OPENAI: 'O',
OPENAI_RESPONSES: 'OR',
OPENAI_RESPONSES_COMPACT: 'ORC',
@@ -92,6 +96,7 @@ export const API_FORMAT_SHORT: Record<string, string> = {
JINA_EMBEDDING: 'JE',
JINA_RERANK: 'JR',
DOUBAO_EMBEDDING: 'DE',
ALIYUN_MULTIMODAL_EMBEDDING: 'AE',
}
// API 格式排序顺序(统一的显示顺序)
@@ -111,6 +116,7 @@ export const API_FORMAT_ORDER: string[] = [
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.DOUBAO_EMBEDDING,
API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING,
]
// Family 显示名称映射
@@ -120,6 +126,7 @@ export const API_FORMAT_FAMILY_LABELS: Record<string, string> = {
gemini: 'Gemini',
jina: 'Jina',
doubao: 'Doubao',
aliyun: 'Aliyun',
}
// Kind 显示名称映射
@@ -137,7 +144,7 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
}
// Family 排序顺序
const FAMILY_ORDER = ['openai', 'claude', 'gemini', 'jina', 'doubao']
const FAMILY_ORDER = ['openai', 'claude', 'gemini', 'jina', 'doubao', 'aliyun']
// 工具函数:从 API 格式中提取 family 和 kind
export function parseApiFormat(format: string): { family: string; kind: string } {
@@ -183,8 +190,20 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
return API_FORMATS.JINA_RERANK
case 'DOUBAO_EMBEDDING':
return API_FORMATS.DOUBAO_EMBEDDING
case 'ALIYUN_MULTIMODAL_EMBEDDING':
case 'ALIYUN_EMBEDDING':
case 'DASHSCOPE_MULTIMODAL_EMBEDDING':
case 'DASHSCOPE_EMBEDDING':
return API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING
default:
return raw.toLowerCase()
switch (raw.toLowerCase()) {
case 'dashscope:multimodal_embedding':
case 'aliyun_multimodal_embedding':
case 'dashscope_multimodal_embedding':
return API_FORMATS.ALIYUN_MULTIMODAL_EMBEDDING
default:
return raw.toLowerCase()
}
}
}
@@ -32,7 +32,13 @@ describe('global model form embedding payload helpers', () => {
streaming: false,
embedding: true,
model_type: 'embedding',
api_formats: ['openai:embedding', 'gemini:embedding', 'jina:embedding', 'doubao:embedding'],
api_formats: [
'openai:embedding',
'gemini:embedding',
'jina:embedding',
'doubao:embedding',
'aliyun:multimodal_embedding',
],
},
})
})
@@ -6,6 +6,7 @@ export const EMBEDDING_API_FORMATS = [
'gemini:embedding',
'jina:embedding',
'doubao:embedding',
'aliyun:multimodal_embedding',
] as const
export const RERANK_API_FORMATS = [
@@ -16,6 +16,7 @@ describe('provider model form embedding helpers', () => {
{ supported_capabilities: null, config: { embedding: true } },
{ supported_capabilities: null, config: { model_type: 'embedding' } },
{ supported_capabilities: null, config: { api_formats: ['doubao:embedding'] } },
{ supported_capabilities: null, config: { api_formats: ['aliyun:multimodal_embedding'] } },
{ supports_embedding: true, effective_supports_embedding: null, config: {} },
{ supports_embedding: null, effective_supports_embedding: true, config: {} },
])('detects embedding metadata from %o', (model) => {
@@ -7,6 +7,11 @@ interface EmbeddingMetadataCarrier {
config?: Record<string, unknown> | null
}
function isEmbeddingApiFormat(format: unknown): boolean {
const value = String(format).trim().toLowerCase()
return value.endsWith(':embedding') || value === 'aliyun:multimodal_embedding'
}
export interface ProviderModelCreatePayloadInput {
globalModelId: string
providerModelName: string
@@ -45,7 +50,7 @@ export function modelSupportsEmbedding(model: EmbeddingMetadataCarrier | null |
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')))
|| (Array.isArray(config.api_formats) && config.api_formats.some(isEmbeddingApiFormat))
}
export function buildProviderModelCreatePayload(input: ProviderModelCreatePayloadInput): ModelCreate {
@@ -34,6 +34,21 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(body.stream).toBeUndefined()
})
it('uses multimodal input and qwen3-vl defaults for Aliyun embedding tests', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody('qwen3-vl-embedding', 'aliyun:multimodal_embedding'))
expect(body).toEqual({
model: 'qwen3-vl-embedding',
input: [
{ text: 'White running shoes, lightweight and breathable.' },
{ image: 'https://dashscope.oss-cn-beijing.aliyuncs.com/images/256_1.png' },
],
parameters: {
enable_fusion: true,
},
})
})
it.each([
'openai:rerank',
'jina:rerank',
@@ -151,6 +151,19 @@ export function buildDefaultModelTestRequestBody(
): string {
const normalizedApiFormat = normalizeApiFormatAlias(apiFormat ?? '')
if (normalizedApiFormat === 'aliyun:multimodal_embedding') {
return JSON.stringify({
model: modelName,
input: [
{ text: 'White running shoes, lightweight and breathable.' },
{ image: 'https://dashscope.oss-cn-beijing.aliyuncs.com/images/256_1.png' },
],
parameters: {
enable_fusion: true,
},
}, null, 2)
}
if (normalizedApiFormat.endsWith(':embedding')) {
return JSON.stringify({
model: modelName,
@@ -273,4 +286,4 @@ export function parseModelTestRequestHeadersDraft(
emptyError: null,
invalidTypeError: '测试请求头必须是 JSON 对象',
})
}
}
@@ -18,6 +18,7 @@ const ENDPOINT_SORT_ORDER = [
'jina:embedding',
'jina:rerank',
'doubao:embedding',
'aliyun:multimodal_embedding',
]
/**
@@ -34,6 +34,7 @@ export function useProviderFilters(
{ value: 'jina:embedding', label: 'Jina Embedding' },
{ value: 'jina:rerank', label: 'Jina Rerank' },
{ value: 'doubao:embedding', label: 'Doubao Embedding' },
{ value: 'aliyun:multimodal_embedding', label: 'Aliyun Multimodal Embedding' },
]
const modelFilters = computed<FilterOption[]>(() => {
@@ -20,7 +20,7 @@ describe('embedding mock metadata', () => {
it('includes all embedding API formats as distinct catalog formats', () => {
const embeddingFormats = MOCK_API_FORMATS.formats
.filter(format => format.value.endsWith(':embedding'))
.filter(format => format.value.endsWith(':embedding') || format.value.endsWith('_embedding'))
.map(format => [format.value, format.label])
expect(embeddingFormats).toEqual([
@@ -28,6 +28,7 @@ describe('embedding mock metadata', () => {
['gemini:embedding', 'Gemini Embedding'],
['jina:embedding', 'Jina Embedding'],
['doubao:embedding', 'Doubao Embedding'],
['aliyun:multimodal_embedding', 'Aliyun Multimodal Embedding'],
])
})
+7 -1
View File
@@ -1196,6 +1196,12 @@ export const MOCK_API_FORMATS = {
{ value: 'gemini:video', label: 'Gemini Video', default_path: '/v1beta/models/{model}:predictLongRunning', aliases: [] },
{ value: 'jina:embedding', label: 'Jina Embedding', default_path: '/v1/embeddings', aliases: [] },
{ value: 'jina:rerank', label: 'Jina Rerank', default_path: '/v1/rerank', aliases: [] },
{ value: 'doubao:embedding', label: 'Doubao Embedding', default_path: '/embeddings/multimodal', aliases: [] }
{ value: 'doubao:embedding', label: 'Doubao Embedding', default_path: '/embeddings/multimodal', aliases: [] },
{
value: 'aliyun:multimodal_embedding',
label: 'Aliyun Multimodal Embedding',
default_path: '/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding',
aliases: ['dashscope:multimodal_embedding'],
},
]
}
+1 -1
View File
@@ -1827,7 +1827,7 @@ function generateMockModelsForProvider(providerId: string) {
const hasClaude = provider.api_formats.some(f => f.includes('claude'))
const hasOpenAI = provider.api_formats.some(f => f.includes('openai'))
const hasGemini = provider.api_formats.some(f => f.includes('gemini'))
const hasEmbedding = provider.api_formats.some(f => f.endsWith(':embedding'))
const hasEmbedding = provider.api_formats.some(f => f.endsWith(':embedding') || f === 'aliyun:multimodal_embedding')
const hasRerank = provider.api_formats.some(f => f.endsWith(':rerank'))
const models: Record<string, unknown>[] = []
@@ -35,6 +35,7 @@ describe('model catalog embedding helpers', () => {
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: ['aliyun:multimodal_embedding'] } }))).toBe(true)
expect(supportsEmbedding(model({ config: { api_formats: ['openai:chat'] } }))).toBe(false)
})
@@ -395,7 +395,10 @@ function supportsEmbedding(model: PublicGlobalModel): boolean {
|| 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')))
|| (Array.isArray(model.config?.api_formats) && model.config.api_formats.some((format) => {
const value = String(format).trim().toLowerCase()
return value.endsWith(':embedding') || value === 'aliyun:multimodal_embedding'
}))
}
// 添加 ESC 键监听
@@ -1,11 +1,16 @@
import type { PublicGlobalModel } from '@/api/public-models'
function isEmbeddingApiFormat(format: unknown): boolean {
const value = String(format).trim().toLowerCase()
return value.endsWith(':embedding') || value === 'aliyun:multimodal_embedding'
}
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')))
|| (Array.isArray(model.config?.api_formats) && model.config.api_formats.some(isEmbeddingApiFormat))
}
export function supportsRerank(model: PublicGlobalModel): boolean {