mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
324 lines
8.7 KiB
TypeScript
324 lines
8.7 KiB
TypeScript
import {
|
|
apiFormatPermissionCovers,
|
|
normalizeApiFormatAlias,
|
|
} from '@/api/endpoints/types/api-format'
|
|
import type { ProviderModelMapping } from '@/api/endpoints/types'
|
|
import type { TestModelRequest } from '@/api/endpoints/providers'
|
|
import {
|
|
modelSupportsImageGeneration,
|
|
normalizeModelTestStringList,
|
|
type ModelTestImageSource,
|
|
} from './model-test-capabilities'
|
|
|
|
export {
|
|
formatModelTestDiagnostic,
|
|
getOpenAiImageModelTestCapability,
|
|
getOpenAiImageModelTestMaxGenerationCount,
|
|
isModelTestableApiFormat,
|
|
isModelTestableEndpoint,
|
|
modelTestKeySupportsEndpoint,
|
|
selectPreferredModelTestEndpoint,
|
|
} from './model-test-capabilities'
|
|
export type {
|
|
ModelTestEndpointSource,
|
|
ModelTestImageSource,
|
|
ModelTestKeySource,
|
|
} from './model-test-capabilities'
|
|
export {
|
|
extractModelTestImagePreviews,
|
|
extractModelTestResponsePreview,
|
|
} from './model-test-preview'
|
|
export type { ModelTestImagePreview } from './model-test-preview'
|
|
|
|
const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
|
|
|
|
function buildModelTestSearchSessionId(): string {
|
|
const id = globalThis.crypto?.randomUUID?.()
|
|
?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
|
|
return `aether-model-test-${id}`
|
|
}
|
|
|
|
type ModelTestMappingSource = {
|
|
provider_model_name: string
|
|
provider_model_mappings?: ProviderModelMapping[] | null
|
|
}
|
|
|
|
export type ModelTestMappingEndpoint = {
|
|
id: string
|
|
api_format: string
|
|
}
|
|
|
|
export type ModelTestMappedModelOption = {
|
|
name: string
|
|
priority: number
|
|
}
|
|
|
|
export function modelTestMappingScopeMatchesEndpoint(
|
|
apiFormats: string[] | null | undefined,
|
|
endpointIds: string[] | null | undefined,
|
|
endpoint: ModelTestMappingEndpoint,
|
|
): boolean {
|
|
const normalizedApiFormats = normalizeModelTestStringList(apiFormats)
|
|
const endpointFormat = normalizeApiFormatAlias(endpoint.api_format)
|
|
const apiFormatMatches = normalizedApiFormats.length === 0
|
|
|| normalizedApiFormats.some(format => apiFormatPermissionCovers(format, endpointFormat))
|
|
const normalizedEndpointIds = normalizeModelTestStringList(endpointIds)
|
|
const endpointMatches = normalizedEndpointIds.length === 0
|
|
|| normalizedEndpointIds.includes(endpoint.id)
|
|
return apiFormatMatches && endpointMatches
|
|
}
|
|
|
|
export function listModelTestMappedModelOptions(
|
|
model: ModelTestMappingSource | null | undefined,
|
|
endpoint: ModelTestMappingEndpoint | null | undefined,
|
|
): ModelTestMappedModelOption[] {
|
|
if (!model || !endpoint || !Array.isArray(model.provider_model_mappings)) return []
|
|
|
|
const matchedMappings = model.provider_model_mappings
|
|
.filter(mapping => mapping.name.trim())
|
|
.filter(mapping => modelTestMappingScopeMatchesEndpoint(
|
|
mapping.api_formats,
|
|
mapping.endpoint_ids,
|
|
endpoint,
|
|
))
|
|
.sort((left, right) => {
|
|
const leftPriority = Number.isFinite(left.priority) ? left.priority : 1
|
|
const rightPriority = Number.isFinite(right.priority) ? right.priority : 1
|
|
return leftPriority - rightPriority || left.name.localeCompare(right.name)
|
|
})
|
|
const seen = new Set<string>()
|
|
return matchedMappings.flatMap((mapping) => {
|
|
const name = mapping.name.trim()
|
|
const dedupeKey = name.toLowerCase()
|
|
if (seen.has(dedupeKey)) return []
|
|
seen.add(dedupeKey)
|
|
return [{
|
|
name,
|
|
priority: Number.isFinite(mapping.priority) ? mapping.priority : 1,
|
|
}]
|
|
})
|
|
}
|
|
|
|
export function normalizeModelTestMappedModelSelection(
|
|
options: ModelTestMappedModelOption[],
|
|
preferredName: string | null | undefined,
|
|
): string | null {
|
|
const preferred = preferredName?.trim()
|
|
if (!preferred) return null
|
|
return options.find(option => option.name === preferred)?.name ?? null
|
|
}
|
|
|
|
export function setModelTestRequestBodyModel(draft: string, modelName: string): string {
|
|
const parsed = parseModelTestRequestBodyDraft(draft)
|
|
if (!parsed.value || parsed.error) return draft
|
|
|
|
return JSON.stringify({
|
|
...parsed.value,
|
|
model: modelName,
|
|
}, null, 2)
|
|
}
|
|
|
|
export function syncModelTestRequestBodyDraft(
|
|
draft: string,
|
|
resetValue: string,
|
|
nextResetValue: string,
|
|
modelName?: string | null,
|
|
): { draft: string; resetValue: string } {
|
|
const nextReset = modelName?.trim()
|
|
? setModelTestRequestBodyModel(nextResetValue, modelName.trim())
|
|
: nextResetValue
|
|
const draftIsUntouched = !draft || draft === resetValue
|
|
|
|
if (draftIsUntouched) {
|
|
return {
|
|
draft: nextReset,
|
|
resetValue: nextReset,
|
|
}
|
|
}
|
|
|
|
return {
|
|
draft: modelName?.trim()
|
|
? setModelTestRequestBodyModel(draft, modelName.trim())
|
|
: draft,
|
|
resetValue: nextReset,
|
|
}
|
|
}
|
|
|
|
export function buildExactModelMappingTestRequest(
|
|
providerId: string,
|
|
modelName: string,
|
|
apiFormat: string | null | undefined,
|
|
): TestModelRequest {
|
|
return {
|
|
provider_id: providerId,
|
|
model_name: modelName,
|
|
mode: 'direct',
|
|
apply_model_mapping: false,
|
|
api_format: apiFormat || undefined,
|
|
}
|
|
}
|
|
|
|
export function buildDefaultModelTestRequestBody(
|
|
modelName: string,
|
|
apiFormat?: string | null,
|
|
model?: ModelTestImageSource | null,
|
|
): 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,
|
|
input: 'This is a test embedding input.',
|
|
}, null, 2)
|
|
}
|
|
|
|
if (normalizedApiFormat.endsWith(':rerank')) {
|
|
return JSON.stringify({
|
|
model: modelName,
|
|
query: 'Apple',
|
|
documents: [
|
|
'apple',
|
|
'banana',
|
|
'fruit',
|
|
'vegetable',
|
|
],
|
|
return_documents: true,
|
|
top_n: 4,
|
|
}, null, 2)
|
|
}
|
|
|
|
if (normalizedApiFormat === 'openai:image') {
|
|
return JSON.stringify({
|
|
model: modelName,
|
|
prompt: DEFAULT_MODEL_TEST_MESSAGE,
|
|
n: 1,
|
|
size: '1024x1024',
|
|
stream: true,
|
|
}, null, 2)
|
|
}
|
|
|
|
if (normalizedApiFormat === 'gemini:interactions') {
|
|
return JSON.stringify({
|
|
model: modelName,
|
|
input: DEFAULT_MODEL_TEST_MESSAGE,
|
|
stream: true,
|
|
}, null, 2)
|
|
}
|
|
|
|
if (normalizedApiFormat === 'openai:search') {
|
|
return JSON.stringify({
|
|
id: buildModelTestSearchSessionId(),
|
|
model: modelName,
|
|
input: DEFAULT_MODEL_TEST_MESSAGE,
|
|
commands: {
|
|
search_query: [{ q: DEFAULT_MODEL_TEST_MESSAGE }],
|
|
},
|
|
max_output_tokens: 256,
|
|
}, null, 2)
|
|
}
|
|
|
|
if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) {
|
|
return JSON.stringify({
|
|
model: modelName,
|
|
input: DEFAULT_MODEL_TEST_MESSAGE,
|
|
tools: [
|
|
{
|
|
type: 'image_generation',
|
|
size: '1024x1024',
|
|
output_format: 'png',
|
|
},
|
|
],
|
|
tool_choice: {
|
|
type: 'image_generation',
|
|
},
|
|
stream: true,
|
|
}, null, 2)
|
|
}
|
|
|
|
return JSON.stringify({
|
|
model: modelName,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: DEFAULT_MODEL_TEST_MESSAGE,
|
|
},
|
|
],
|
|
max_tokens: 30,
|
|
temperature: 0.7,
|
|
stream: true,
|
|
}, null, 2)
|
|
}
|
|
|
|
export function buildDefaultModelTestRequestHeaders(): string {
|
|
return JSON.stringify({}, null, 2)
|
|
}
|
|
|
|
function parseModelTestJsonObjectDraft(
|
|
draft: string,
|
|
options: {
|
|
emptyValue: Record<string, unknown> | null
|
|
emptyError: string | null
|
|
invalidTypeError: string
|
|
},
|
|
): { value: Record<string, unknown> | null; error: string | null } {
|
|
const normalized = draft.trim()
|
|
if (!normalized) {
|
|
return {
|
|
value: options.emptyValue,
|
|
error: options.emptyError,
|
|
}
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(normalized)
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
return {
|
|
value: null,
|
|
error: options.invalidTypeError,
|
|
}
|
|
}
|
|
return {
|
|
value: parsed as Record<string, unknown>,
|
|
error: null,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
value: null,
|
|
error: error instanceof Error ? error.message : '无效的 JSON',
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parseModelTestRequestBodyDraft(
|
|
draft: string,
|
|
): { value: Record<string, unknown> | null; error: string | null } {
|
|
return parseModelTestJsonObjectDraft(draft, {
|
|
emptyValue: null,
|
|
emptyError: '测试请求体不能为空',
|
|
invalidTypeError: '测试请求体必须是 JSON 对象',
|
|
})
|
|
}
|
|
|
|
export function parseModelTestRequestHeadersDraft(
|
|
draft: string,
|
|
): { value: Record<string, unknown> | null; error: string | null } {
|
|
return parseModelTestJsonObjectDraft(draft, {
|
|
emptyValue: {},
|
|
emptyError: null,
|
|
invalidTypeError: '测试请求头必须是 JSON 对象',
|
|
})
|
|
}
|