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

@@ -200,6 +200,7 @@ export interface ModelExport {
supports_streaming?: boolean | null
supports_extended_thinking?: boolean | null
supports_image_generation?: boolean | null
supports_embedding?: boolean | null
is_active: boolean
config?: Record<string, unknown>
}

View File

@@ -15,6 +15,30 @@ describe('api format display helpers', () => {
expect(normalizeApiFormatAlias('OPENAI_RESPONSES')).toBe(API_FORMATS.OPENAI_RESPONSES)
expect(normalizeApiFormatAlias('OPENAI_RESPONSES_COMPACT')).toBe(API_FORMATS.OPENAI_RESPONSES_COMPACT)
expect(normalizeApiFormatAlias('GEMINI_GENERATE_CONTENT')).toBe(API_FORMATS.GEMINI_GENERATE_CONTENT)
expect(normalizeApiFormatAlias('OPENAI_EMBEDDING')).toBe(API_FORMATS.OPENAI_EMBEDDING)
expect(normalizeApiFormatAlias('OPENAI_RERANK')).toBe(API_FORMATS.OPENAI_RERANK)
expect(normalizeApiFormatAlias('GEMINI_EMBEDDING')).toBe(API_FORMATS.GEMINI_EMBEDDING)
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)
})
it('formats rerank api format ids distinctly from chat formats', () => {
expect(formatApiFormat(API_FORMATS.OPENAI_RERANK)).toBe('OpenAI Rerank')
expect(formatApiFormat(API_FORMATS.JINA_RERANK)).toBe('Jina Rerank')
expect(formatApiFormatShort(API_FORMATS.OPENAI_RERANK)).toBe('ORR')
expect(formatApiFormatShort(API_FORMATS.JINA_RERANK)).toBe('JR')
})
it('formats embedding api format ids distinctly from chat formats', () => {
expect(formatApiFormat(API_FORMATS.OPENAI_EMBEDDING)).toBe('OpenAI Embedding')
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(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')
})
it('does not remap retired api format ids', () => {
@@ -40,15 +64,59 @@ describe('api format display helpers', () => {
it('sorts only current canonical formats into known slots', () => {
expect(sortApiFormats([
'openai:compact',
API_FORMATS.DOUBAO_EMBEDDING,
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_RESPONSES,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
])).toEqual([
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_RESPONSES,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.DOUBAO_EMBEDDING,
'openai:compact',
])
})
it('keeps embedding formats after chat/generation formats within each family', () => {
expect(sortApiFormats([
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.OPENAI,
])).toEqual([
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.GEMINI_EMBEDDING,
])
})
it('groups embedding api formats by provider family', () => {
expect(groupApiFormats([
API_FORMATS.DOUBAO_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
])).toEqual([
{ family: 'openai', label: 'OpenAI', formats: [API_FORMATS.OPENAI_EMBEDDING, API_FORMATS.OPENAI_RERANK] },
{ 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] },
])
})
it('groups retired enum-style aliases as unknown raw families', () => {
expect(groupApiFormats(['OPENAI_CLI'])).toEqual([{
family: 'openai_cli',

View File

@@ -8,10 +8,16 @@ export const API_FORMATS = {
OPENAI_RESPONSES_COMPACT: 'openai:responses:compact',
OPENAI_IMAGE: 'openai:image',
OPENAI_VIDEO: 'openai:video',
OPENAI_EMBEDDING: 'openai:embedding',
OPENAI_RERANK: 'openai:rerank',
GEMINI: 'gemini:generate_content',
GEMINI_GENERATE_CONTENT: 'gemini:generate_content',
GEMINI_VIDEO: 'gemini:video',
GEMINI_FILES: 'gemini:files',
GEMINI_EMBEDDING: 'gemini:embedding',
JINA_EMBEDDING: 'jina:embedding',
JINA_RERANK: 'jina:rerank',
DOUBAO_EMBEDDING: 'doubao:embedding',
} as const
export type APIFormat = typeof API_FORMATS[keyof typeof API_FORMATS]
@@ -24,9 +30,15 @@ export const API_FORMAT_LABELS: Record<string, string> = {
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'OpenAI Responses Compact',
[API_FORMATS.OPENAI_IMAGE]: 'OpenAI Image',
[API_FORMATS.OPENAI_VIDEO]: 'OpenAI Video',
[API_FORMATS.OPENAI_EMBEDDING]: 'OpenAI Embedding',
[API_FORMATS.OPENAI_RERANK]: 'OpenAI Rerank',
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'Gemini Generate Content',
[API_FORMATS.GEMINI_VIDEO]: 'Gemini Video',
[API_FORMATS.GEMINI_FILES]: 'Gemini Files',
[API_FORMATS.GEMINI_EMBEDDING]: 'Gemini Embedding',
[API_FORMATS.JINA_EMBEDDING]: 'Jina Embedding',
[API_FORMATS.JINA_RERANK]: 'Jina Rerank',
[API_FORMATS.DOUBAO_EMBEDDING]: 'Doubao Embedding',
CLAUDE: 'Claude Messages',
CLAUDE_MESSAGES: 'Claude Messages',
OPENAI: 'OpenAI Chat',
@@ -34,10 +46,16 @@ export const API_FORMAT_LABELS: Record<string, string> = {
OPENAI_RESPONSES_COMPACT: 'OpenAI Responses Compact',
OPENAI_IMAGE: 'OpenAI Image',
OPENAI_VIDEO: 'OpenAI Video',
OPENAI_EMBEDDING: 'OpenAI Embedding',
OPENAI_RERANK: 'OpenAI Rerank',
GEMINI: 'Gemini Generate Content',
GEMINI_GENERATE_CONTENT: 'Gemini Generate Content',
GEMINI_VIDEO: 'Gemini Video',
GEMINI_FILES: 'Gemini Files',
GEMINI_EMBEDDING: 'Gemini Embedding',
JINA_EMBEDDING: 'Jina Embedding',
JINA_RERANK: 'Jina Rerank',
DOUBAO_EMBEDDING: 'Doubao Embedding',
}
// API 格式缩写映射(用于空间紧凑的显示场景)
@@ -47,21 +65,33 @@ export const API_FORMAT_SHORT: Record<string, string> = {
[API_FORMATS.OPENAI_RESPONSES_COMPACT]: 'ORC',
[API_FORMATS.OPENAI_IMAGE]: 'OI',
[API_FORMATS.OPENAI_VIDEO]: 'OV',
[API_FORMATS.OPENAI_EMBEDDING]: 'OE',
[API_FORMATS.OPENAI_RERANK]: 'ORR',
[API_FORMATS.CLAUDE_MESSAGES]: 'CM',
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'G',
[API_FORMATS.GEMINI_VIDEO]: 'GV',
[API_FORMATS.GEMINI_FILES]: 'GF',
[API_FORMATS.GEMINI_EMBEDDING]: 'GE',
[API_FORMATS.JINA_EMBEDDING]: 'JE',
[API_FORMATS.JINA_RERANK]: 'JR',
[API_FORMATS.DOUBAO_EMBEDDING]: 'DE',
OPENAI: 'O',
OPENAI_RESPONSES: 'OR',
OPENAI_RESPONSES_COMPACT: 'ORC',
OPENAI_IMAGE: 'OI',
OPENAI_VIDEO: 'OV',
OPENAI_EMBEDDING: 'OE',
OPENAI_RERANK: 'ORR',
CLAUDE: 'CM',
CLAUDE_MESSAGES: 'CM',
GEMINI: 'G',
GEMINI_GENERATE_CONTENT: 'G',
GEMINI_VIDEO: 'GV',
GEMINI_FILES: 'GF',
GEMINI_EMBEDDING: 'GE',
JINA_EMBEDDING: 'JE',
JINA_RERANK: 'JR',
DOUBAO_EMBEDDING: 'DE',
}
// API 格式排序顺序(统一的显示顺序)
@@ -69,12 +99,18 @@ export const API_FORMAT_ORDER: string[] = [
API_FORMATS.OPENAI,
API_FORMATS.OPENAI_RESPONSES,
API_FORMATS.OPENAI_RESPONSES_COMPACT,
API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK,
API_FORMATS.OPENAI_IMAGE,
API_FORMATS.OPENAI_VIDEO,
API_FORMATS.CLAUDE_MESSAGES,
API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.GEMINI_VIDEO,
API_FORMATS.GEMINI_FILES,
API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK,
API_FORMATS.DOUBAO_EMBEDDING,
]
// Family 显示名称映射
@@ -82,6 +118,8 @@ export const API_FORMAT_FAMILY_LABELS: Record<string, string> = {
openai: 'OpenAI',
claude: 'Claude',
gemini: 'Gemini',
jina: 'Jina',
doubao: 'Doubao',
}
// Kind 显示名称映射
@@ -94,10 +132,12 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
image: 'Image',
video: 'Video',
files: 'Files',
embedding: 'Embedding',
rerank: 'Rerank',
}
// Family 排序顺序
const FAMILY_ORDER = ['openai', 'claude', 'gemini']
const FAMILY_ORDER = ['openai', 'claude', 'gemini', 'jina', 'doubao']
// 工具函数:从 API 格式中提取 family 和 kind
export function parseApiFormat(format: string): { family: string; kind: string } {
@@ -124,6 +164,10 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
return API_FORMATS.OPENAI_IMAGE
case 'OPENAI_VIDEO':
return API_FORMATS.OPENAI_VIDEO
case 'OPENAI_EMBEDDING':
return API_FORMATS.OPENAI_EMBEDDING
case 'OPENAI_RERANK':
return API_FORMATS.OPENAI_RERANK
case 'GEMINI':
case 'GEMINI_GENERATE_CONTENT':
return API_FORMATS.GEMINI_GENERATE_CONTENT
@@ -131,6 +175,14 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
return API_FORMATS.GEMINI_VIDEO
case 'GEMINI_FILES':
return API_FORMATS.GEMINI_FILES
case 'GEMINI_EMBEDDING':
return API_FORMATS.GEMINI_EMBEDDING
case 'JINA_EMBEDDING':
return API_FORMATS.JINA_EMBEDDING
case 'JINA_RERANK':
return API_FORMATS.JINA_RERANK
case 'DOUBAO_EMBEDDING':
return API_FORMATS.DOUBAO_EMBEDDING
default:
return raw.toLowerCase()
}

View File

@@ -38,6 +38,7 @@ export interface Model {
supports_streaming?: boolean | null
supports_extended_thinking?: boolean | null
supports_image_generation?: boolean | null
supports_embedding?: boolean | null
// 有效值(合并 Model 和 GlobalModel 默认值后的结果)
effective_tiered_pricing?: TieredPricingConfig | null // 有效阶梯计费配置
effective_input_price?: number | null
@@ -48,6 +49,7 @@ export interface Model {
effective_supports_streaming?: boolean | null
effective_supports_extended_thinking?: boolean | null
effective_supports_image_generation?: boolean | null
effective_supports_embedding?: boolean | null
is_active: boolean
is_available: boolean
created_at: string
@@ -96,6 +98,7 @@ export interface ModelCapabilities {
supports_vision: boolean
supports_function_calling: boolean
supports_streaming: boolean
supports_embedding: boolean
[key: string]: boolean
}
@@ -130,6 +133,7 @@ export interface ModelCatalogProviderDetail {
supports_vision?: boolean | null
supports_function_calling?: boolean | null
supports_streaming?: boolean | null
supports_embedding?: boolean | null
is_active: boolean
mapping_id?: string | null
}
@@ -211,6 +215,7 @@ export interface GlobalModelResponse {
default_tiered_pricing: TieredPricingConfig
// Key 能力配置 - 模型支持的能力列表
supported_capabilities?: string[] | null
supports_embedding?: boolean | null
// 模型配置JSON格式
config?: Record<string, unknown> | null
// 统计数据

View File

@@ -340,6 +340,7 @@ export const meApi = {
default_price_per_request: number | null
default_tiered_pricing: TieredPricingConfig | null
supported_capabilities: string[] | null
supports_embedding?: boolean | null
config: Record<string, unknown> | null
usage_count: number
}>

View File

@@ -72,6 +72,7 @@ export interface ModelsDevModelItem {
supportsStructuredOutput?: boolean
supportsTemperature?: boolean
supportsAttachment?: boolean
supportsEmbedding?: boolean
openWeights?: boolean
deprecated?: boolean
official?: boolean // 是否来自官方提供商
@@ -180,6 +181,9 @@ export async function getModelsDevList(officialOnly: boolean = true): Promise<Mo
supportsStructuredOutput: model.structured_output,
supportsTemperature: model.temperature,
supportsAttachment: model.attachment,
supportsEmbedding: model.id.toLowerCase().includes('embedding')
|| model.name.toLowerCase().includes('embedding')
|| model.family?.toLowerCase().includes('embedding') === true,
openWeights: model.open_weights,
deprecated: model.deprecated,
official: provider.official,

View File

@@ -15,6 +15,7 @@ export interface PublicGlobalModel {
default_price_per_request: number | null // 按次计费价格
// Key 能力支持
supported_capabilities: string[] | null
supports_embedding?: boolean | null
// 模型配置JSON
config: Record<string, unknown> | null
// 调用次数