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

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest'
import { MOCK_API_FORMATS, MOCK_GLOBAL_MODELS } from '../data'
describe('embedding mock metadata', () => {
it('exposes embedding model metadata to frontend code without chat treatment', () => {
const model = MOCK_GLOBAL_MODELS.find(item => item.name === 'text-embedding-3-small')
expect(model).toMatchObject({
supported_capabilities: ['embedding'],
supports_embedding: true,
config: {
streaming: false,
embedding: true,
model_type: 'embedding',
api_formats: ['openai:embedding'],
},
})
})
it('includes all embedding API formats as distinct catalog formats', () => {
const embeddingFormats = MOCK_API_FORMATS.formats
.filter(format => format.value.endsWith(':embedding'))
.map(format => [format.value, format.label])
expect(embeddingFormats).toEqual([
['openai:embedding', 'OpenAI Embedding'],
['gemini:embedding', 'Gemini Embedding'],
['jina:embedding', 'Jina Embedding'],
['doubao:embedding', 'Doubao Embedding'],
])
})
it('includes rerank API formats as distinct catalog formats', () => {
const rerankFormats = MOCK_API_FORMATS.formats
.filter(format => format.value.endsWith(':rerank'))
.map(format => [format.value, format.label])
expect(rerankFormats).toEqual([
['openai:rerank', 'OpenAI Rerank'],
['jina:rerank', 'Jina Rerank'],
])
})
})

View File

@@ -546,20 +546,21 @@ export const MOCK_PROVIDERS: ProviderWithEndpointsSummary[] = [
billing_type: 'pay_as_you_go',
monthly_used_usd: 5.29,
is_active: true,
total_endpoints: 4,
active_endpoints: 4,
total_endpoints: 5,
active_endpoints: 5,
total_keys: 11,
active_keys: 11,
total_models: 8,
active_models: 8,
total_models: 9,
active_models: 9,
avg_health_score: 0.863,
unhealthy_endpoints: 1,
api_formats: ['claude:messages', 'gemini:generate_content', 'openai:chat', 'openai:responses'],
api_formats: ['claude:messages', 'gemini:generate_content', 'openai:chat', 'openai:responses', 'openai:embedding'],
endpoint_health_details: [
{ api_format: 'claude:messages', health_score: 1.0, is_active: true, active_keys: 2 },
{ api_format: 'gemini:generate_content', health_score: 1.0, is_active: true, active_keys: 2 },
{ api_format: 'openai:chat', health_score: 0.85, is_active: true, active_keys: 2 },
{ api_format: 'openai:responses', health_score: 1.0, is_active: true, active_keys: 1 }
{ api_format: 'openai:responses', health_score: 1.0, is_active: true, active_keys: 1 },
{ api_format: 'openai:embedding', health_score: 0.98, is_active: true, active_keys: 1 }
],
created_at: '2024-12-07T22:56:09.712806+08:00',
updated_at: new Date().toISOString()
@@ -805,6 +806,46 @@ export const MOCK_GLOBAL_MODELS: GlobalModelResponse[] = [
},
provider_count: 2,
created_at: '2024-01-01T00:00:00Z'
},
{
id: 'gm-010',
name: 'text-embedding-3-small',
display_name: 'text-embedding-3-small',
is_active: true,
default_tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.02, output_price_per_1m: 0 }]
},
supported_capabilities: ['embedding'],
supports_embedding: true,
config: {
streaming: false,
embedding: true,
model_type: 'embedding',
api_formats: ['openai:embedding'],
dimensions: 1536,
description: 'OpenAI 文本向量嵌入模型'
},
provider_count: 1,
created_at: '2024-01-01T00:00:00Z'
},
{
id: 'gm-rerank-001',
name: 'bge-reranker-base',
display_name: 'bge-reranker-base',
is_active: true,
default_tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.05, output_price_per_1m: 0 }]
},
supported_capabilities: ['rerank'],
config: {
streaming: false,
rerank: true,
model_type: 'rerank',
api_formats: ['openai:rerank'],
description: '文本重排序模型'
},
provider_count: 1,
created_at: '2024-01-01T00:00:00Z'
}
]
@@ -878,9 +919,15 @@ export const MOCK_API_FORMATS = {
{ value: 'openai:chat', label: 'OpenAI Chat', default_path: '/v1/chat/completions', aliases: [] },
{ value: 'openai:responses', label: 'OpenAI Responses', default_path: '/v1/responses', aliases: [] },
{ value: 'openai:responses:compact', label: 'OpenAI Responses Compact', default_path: '/v1/responses/compact', aliases: [] },
{ value: 'openai:embedding', label: 'OpenAI Embedding', default_path: '/v1/embeddings', aliases: [] },
{ value: 'openai:rerank', label: 'OpenAI Rerank', default_path: '/v1/rerank', aliases: [] },
{ value: 'openai:image', label: 'OpenAI Image', default_path: '/v1/images/generations', aliases: [] },
{ value: 'openai:video', label: 'OpenAI Video', default_path: '/v1/videos', aliases: [] },
{ value: 'gemini:generate_content', label: 'Gemini Generate Content', default_path: '/v1beta/models/{model}:{action}', aliases: [] },
{ value: 'gemini:video', label: 'Gemini Video', default_path: '/v1beta/models/{model}:predictLongRunning', aliases: [] }
{ value: 'gemini:embedding', label: 'Gemini Embedding', default_path: '/v1beta/models/{model}:embedContent', aliases: [] },
{ 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: [] }
]
}

View File

@@ -228,6 +228,19 @@ const MOCK_ENDPOINT_STATUS = {
last_event_at: new Date().toISOString(),
// 94.0% 成功率successRate=0.940, failRate=0.043, skipRate=0.017
events: generateHealthEvents(100, 0.940, 0.043, 0.017, 800, 600)
},
{
api_format: 'openai:embedding',
api_path: '/v1/embeddings',
total_attempts: 620,
success_count: 612,
failed_count: 6,
skipped_count: 2,
success_rate: 0.987,
provider_count: 1,
key_count: 1,
last_event_at: new Date().toISOString(),
events: generateHealthEvents(40, 0.987, 0.01, 0.003, 320, 140)
}
]
}
@@ -447,6 +460,12 @@ function getMockEndpointExtras(apiFormat: string) {
extras.config = { upstream_stream_policy: 'force_stream' }
} else if (normalizedFormat === 'openai:responses') {
extras.config = { upstream_stream_policy: 'force_non_stream' }
} else if (normalizedFormat === 'openai:embedding') {
extras.custom_path = '/v1/embeddings'
extras.config = { route_kind: 'embedding' }
} else if (normalizedFormat === 'openai:rerank' || normalizedFormat === 'jina:rerank') {
extras.custom_path = '/v1/rerank'
extras.config = { route_kind: 'rerank' }
} else if (normalizedFormat === 'gemini:generate_content') {
extras.custom_path = '/v1beta/models/gemini-3-pro-preview:generateContent'
extras.body_rules = [
@@ -720,6 +739,23 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
})))
},
'GET /api/users/me/available-models': async () => {
await delay()
const models = MOCK_GLOBAL_MODELS.filter(model => model.is_active).map(model => ({
id: model.id,
name: model.name,
display_name: model.display_name,
is_active: model.is_active,
default_price_per_request: model.default_price_per_request ?? null,
default_tiered_pricing: model.default_tiered_pricing,
supported_capabilities: model.supported_capabilities ?? null,
supports_embedding: model.supports_embedding ?? null,
config: model.config ?? null,
usage_count: model.usage_count ?? 0,
}))
return createMockResponse({ models, total: models.length })
},
'GET /api/users/me/preferences': async () => {
await delay()
return createMockResponse(getCurrentProfile().preferences || { theme: 'auto', language: 'zh-CN' })
@@ -1146,6 +1182,7 @@ const mockHandlers: Record<string, (config: AxiosRequestConfig) => Promise<Axios
default_tiered_pricing: m.default_tiered_pricing,
default_price_per_request: m.default_price_per_request,
supported_capabilities: m.supported_capabilities,
supports_embedding: m.supports_embedding,
config: m.config
})),
total: MOCK_GLOBAL_MODELS.length
@@ -1419,6 +1456,8 @@ 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 hasRerank = provider.api_formats.some(f => f.endsWith(':rerank'))
const models: Record<string, unknown>[] = []
const now = new Date().toISOString()
@@ -1503,6 +1542,66 @@ function generateMockModelsForProvider(providerId: string) {
}
)
}
if (hasEmbedding) {
models.push({
id: `pm-${providerId}-embedding-1`,
provider_id: providerId,
global_model_id: 'gm-010',
provider_model_name: 'text-embedding-3-small',
global_model_name: 'text-embedding-3-small',
global_model_display_name: 'text-embedding-3-small',
effective_input_price: 0.02,
effective_output_price: 0,
supports_embedding: true,
effective_supports_embedding: true,
supports_streaming: false,
effective_supports_streaming: false,
config: {
embedding: true,
model_type: 'embedding',
api_formats: ['openai:embedding'],
},
effective_config: {
embedding: true,
model_type: 'embedding',
api_formats: ['openai:embedding'],
streaming: false,
},
is_active: true,
is_available: true,
created_at: provider.created_at,
updated_at: now
})
}
if (hasRerank) {
models.push({
id: `pm-${providerId}-rerank-1`,
provider_id: providerId,
global_model_id: 'gm-rerank-001',
provider_model_name: 'bge-reranker-base',
global_model_name: 'bge-reranker-base',
global_model_display_name: 'bge-reranker-base',
effective_input_price: 0.05,
effective_output_price: 0,
supports_streaming: false,
effective_supports_streaming: false,
config: {
rerank: true,
model_type: 'rerank',
api_formats: ['openai:rerank'],
},
effective_config: {
rerank: true,
model_type: 'rerank',
api_formats: ['openai:rerank'],
streaming: false,
},
is_active: true,
is_available: true,
created_at: provider.created_at,
updated_at: now
})
}
if (hasGemini) {
models.push(
{