mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: add embedding and rerank support
This commit is contained in:
44
frontend/src/mocks/__tests__/embedding-metadata.spec.ts
Normal file
44
frontend/src/mocks/__tests__/embedding-metadata.spec.ts
Normal 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'],
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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: [] }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user