Merge remote-tracking branch 'pr-434/fix-provider-model-test-compat' into aether-rust-pioneer

This commit is contained in:
fawney19
2026-05-14 01:28:33 +08:00
29 changed files with 8478 additions and 2385 deletions

View File

@@ -190,15 +190,21 @@ export interface TestModelRequest {
endpoint_id?: string
message?: string
api_format?: string
mode?: 'global' | 'direct' | 'pool'
apply_model_mapping?: boolean
mapped_model_name?: string
request_headers?: Record<string, unknown>
request_body?: Record<string, unknown>
request_id?: string
concurrency?: number
}
export interface TestModelResponse {
success: boolean
error?: string
attempts?: TestAttemptDetail[]
total_candidates?: number
total_attempts?: number
candidate_summary?: TestCandidateSummary
data?: {
response?: {
status_code?: number
@@ -210,6 +216,7 @@ export interface TestModelResponse {
provider?: {
id: string
name: string
provider_type?: string
}
model?: string
}
@@ -230,16 +237,17 @@ export async function testModel(
*/
export interface TestModelFailoverRequest {
provider_id: string
mode: 'global' | 'direct'
mode: 'global' | 'direct' | 'pool'
model_name: string
failover_models?: string[]
api_format?: string
endpoint_id?: string
message?: string
apply_model_mapping?: boolean
mapped_model_name?: string
request_headers?: Record<string, unknown>
request_body?: Record<string, unknown>
request_id?: string
concurrency?: number
}
export interface TestAttemptDetail {
@@ -263,13 +271,36 @@ export interface TestAttemptDetail {
response_body?: unknown
}
export interface TestCandidateSummary {
total_candidates: number
attempted: number
success: number
failed: number
skipped: number
unused: number
pending?: number
available?: number
completed?: number
stop_reason?: 'first_success' | 'exhausted' | 'all_skipped' | 'no_candidate' | 'pending' | string
winning_candidate_index?: number | null
winning_key_name?: string | null
winning_key_id?: string | null
winning_auth_type?: string | null
winning_effective_model?: string | null
winning_endpoint_api_format?: string | null
winning_endpoint_base_url?: string | null
winning_latency_ms?: number | null
winning_status_code?: number | null
}
export interface TestModelFailoverResponse {
success: boolean
model: string
provider: { id: string; name: string }
provider: { id: string; name: string; provider_type?: string }
attempts: TestAttemptDetail[]
total_candidates: number
total_attempts: number
candidate_summary?: TestCandidateSummary
data?: Record<string, unknown> | null
error?: string | null
}

View File

@@ -87,7 +87,7 @@ const props = withDefaults(defineProps<Props>(), {
sideOffset: 4,
align: undefined,
alignOffset: undefined,
disablePortal: false,
disablePortal: undefined,
searchable: true,
searchThreshold: 8,
searchPlaceholder: '输入关键词搜索...',
@@ -95,7 +95,7 @@ const props = withDefaults(defineProps<Props>(), {
const isInsideDialog = inject(DIALOG_CONTEXT_KEY, false)
const shouldDisablePortal = computed(
() => props.disablePortal || isInsideDialog,
() => props.disablePortal ?? isInsideDialog,
)
const searchQuery = ref('')
const searchInputRef = ref<InstanceType<typeof Input> | null>(null)

View File

@@ -4,6 +4,7 @@ import { useToast } from './useToast'
import {
testModel,
testModelFailover,
type TestCandidateSummary,
type TestAttemptDetail,
type TestModelResponse,
type TestModelFailoverResponse,
@@ -12,16 +13,17 @@ import { requestTraceApi, type RequestTrace } from '@/api/requestTrace'
import { parseApiError } from '@/utils/errorParser'
export interface StartTestParams {
mode: 'global' | 'direct'
mode: 'global' | 'direct' | 'pool'
modelName: string
displayLabel: string
apiFormat?: string
endpointId?: string
endpointBaseUrl?: string
message?: string
applyModelMapping?: boolean
mappedModelName?: string
requestHeaders?: Record<string, unknown>
requestBody?: Record<string, unknown>
concurrency?: number
onSuccess?: (result: TestModelFailoverResponse) => void
/** Return `true` to indicate the failure has been handled; otherwise the composable sets `testResult`. */
onFailure?: (result: TestModelFailoverResponse) => boolean | void
@@ -40,7 +42,7 @@ export function useModelTest(options: UseModelTestOptions) {
const LOCAL_FAILOVER_UNCONFIGURED_MESSAGE = 'Rust local provider-query failover simulation is not configured'
const testing = ref(false)
const testMode = ref<'global' | 'direct'>('global')
const testMode = ref<'global' | 'direct' | 'pool'>('global')
const testResult = ref<TestModelFailoverResponse | null>(null)
const testTrace = ref<RequestTrace | null>(null)
const requestId = ref<string | null>(null)
@@ -79,7 +81,7 @@ export function useModelTest(options: UseModelTestOptions) {
: responsePayload?.error?.message
) || null
const syntheticAttempt: TestAttemptDetail = {
candidate_index: 1,
candidate_index: 0,
endpoint_api_format: params.apiFormat || '-',
endpoint_base_url: params.endpointBaseUrl || '',
key_name: null,
@@ -99,14 +101,45 @@ export function useModelTest(options: UseModelTestOptions) {
?? (result.data as Record<string, unknown> | undefined)
?? null,
}
const attempts = Array.isArray(result.attempts) && result.attempts.length > 0
? result.attempts
: [syntheticAttempt]
const totalCandidates = typeof result.total_candidates === 'number'
? result.total_candidates
: attempts.length
const totalAttempts = typeof result.total_attempts === 'number'
? result.total_attempts
: attempts.filter(attempt => !['skipped', 'available', 'unused'].includes(attempt.status)).length
const syntheticSummary: TestCandidateSummary = result.candidate_summary ?? {
total_candidates: totalCandidates,
attempted: totalAttempts,
success: result.success ? 1 : 0,
failed: result.success ? 0 : 1,
skipped: 0,
unused: result.success ? Math.max(0, totalCandidates - totalAttempts) : 0,
pending: 0,
available: 0,
completed: result.success ? totalCandidates : totalAttempts,
stop_reason: result.success ? 'first_success' : 'exhausted',
winning_candidate_index: result.success ? 0 : null,
winning_key_name: null,
winning_key_id: '',
winning_auth_type: '',
winning_effective_model: result.success ? (result.model || params.modelName) : null,
winning_endpoint_api_format: params.apiFormat || null,
winning_endpoint_base_url: params.endpointBaseUrl || null,
winning_latency_ms: null,
winning_status_code: responsePayload?.status_code ?? null,
}
return {
success: result.success,
model: result.model || params.modelName,
provider: result.provider || { id: providerId(), name: providerId() },
attempts: [syntheticAttempt],
total_candidates: 1,
total_attempts: 1,
attempts,
total_candidates: totalCandidates,
total_attempts: totalAttempts,
candidate_summary: syntheticSummary,
data: (result.data as Record<string, unknown> | undefined) ?? null,
error: failureMessage,
}
@@ -120,13 +153,15 @@ export function useModelTest(options: UseModelTestOptions) {
return normalizeDirectTestResult(params, await testModel({
provider_id: providerId(),
model_name: params.modelName,
mode: params.mode,
api_format: params.apiFormat,
endpoint_id: params.endpointId,
...(normalizedMessage(params.message) ? { message: normalizedMessage(params.message) } : {}),
...(typeof params.applyModelMapping === 'boolean' ? { apply_model_mapping: params.applyModelMapping } : {}),
...(params.mappedModelName ? { mapped_model_name: params.mappedModelName } : {}),
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
...(params.requestBody ? { request_body: params.requestBody } : {}),
request_id: reqId,
concurrency: params.concurrency,
}, {
signal,
}))
@@ -225,10 +260,11 @@ export function useModelTest(options: UseModelTestOptions) {
api_format: params.apiFormat,
endpoint_id: params.endpointId,
...(message ? { message } : {}),
...(typeof params.applyModelMapping === 'boolean' ? { apply_model_mapping: params.applyModelMapping } : {}),
...(params.mappedModelName ? { mapped_model_name: params.mappedModelName } : {}),
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
...(params.requestBody ? { request_body: params.requestBody } : {}),
request_id: reqId,
concurrency: params.concurrency,
}, {
signal: abortController.signal,
})

View File

@@ -658,7 +658,9 @@
v-if="hasCodexSparkQuotaDisplayData(key)"
class="mt-3 border-t border-border/60 pt-2"
>
<div class="mb-1 text-[10px] text-muted-foreground">GPT-5.3 Codex Spark</div>
<div class="mb-1 text-[10px] text-muted-foreground">
GPT-5.3 Codex Spark
</div>
<div class="grid gap-3 grid-cols-2">
<div v-if="getCodexQuotaDisplay(key)?.spark_secondary_used_percent !== undefined">
<div class="flex items-center justify-between text-[10px] mb-0.5">
@@ -1078,7 +1080,8 @@
:provider="provider"
:models="providerModels"
:endpoints="endpoints"
:loading="loadingProviderModels"
:provider-keys="providerKeys"
:loading="loadingProviderModels || loadingProviderKeys"
@edit-model="handleEditModel"
@batch-assign="handleBatchAssign"
@refresh="loadEndpoints"
@@ -1291,7 +1294,6 @@ import {
import type {
UpstreamMetadata,
AntigravityModelQuota,
AntigravityUpstreamMetadata,
CodexUpstreamMetadata,
ChatGPTWebUpstreamMetadata,
KiroUpstreamMetadata,

View File

@@ -208,6 +208,7 @@ import {
import { updateModel } from '@/api/endpoints/models'
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { buildExactModelMappingTestRequest } from './model-test-request'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
const props = defineProps<{
@@ -428,11 +429,9 @@ async function testMapping(group: AliasGroup, mapping: ProviderModelAlias) {
apiFormat = group.model.effective_api_format || group.model.api_format
}
const result = await testModel({
provider_id: props.provider.id,
model_name: mapping.name, // 使用映射名称进行测试
api_format: apiFormat
})
const result = await testModel(
buildExactModelMappingTestRequest(props.provider.id, mapping.name, apiFormat)
)
if (result.success) {
showSuccess(`映射 "${mapping.name}" 测试成功`)

View File

@@ -321,8 +321,9 @@
:open="modelTest.dialogOpen.value"
:result="modelTest.testResult.value"
mode="direct"
:provider-type="provider.provider_type"
:selecting-model-name="testingModelName"
:endpoints="activeEndpoints"
:endpoints="selectableTestEndpoints"
:selected-endpoint="selectedTestEndpoint"
:testing="modelTest.testing.value"
:trace="modelTest.testTrace.value"
@@ -365,13 +366,14 @@ import { type EndpointAPIKey } from '@/api/endpoints/keys'
import { updateModel } from '@/api/endpoints/models'
import { parseApiError } from '@/utils/errorParser'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import { normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
import {
buildDefaultModelTestRequestHeaders,
buildDefaultModelTestRequestBody,
isModelTestableEndpoint,
parseModelTestRequestHeadersDraft,
parseModelTestRequestBodyDraft,
POOL_TEST_CONCURRENCY,
SINGLE_TEST_CONCURRENCY,
syncModelTestRequestBodyDraft,
} from './model-test-request'
interface MappingItem {
@@ -431,8 +433,11 @@ const testRequestHeadersDraft = ref('')
const testRequestHeadersResetValue = ref('')
const testRequestBodyDraft = ref('')
const testRequestBodyResetValue = ref('')
const isPoolManagedProvider = computed(() => Boolean(props.provider.pool_advanced))
const activeEndpoints = computed(() => (props.endpoints ?? []).filter(endpoint => endpoint.is_active))
const mappingTestEndpoints = ref<ProviderEndpoint[] | null>(null)
const providerKeysState = computed(() => props.providerKeys ?? [])
const activeEndpoints = computed(() => (props.endpoints ?? [])
.filter(endpoint => isModelTestableEndpoint(endpoint, providerKeysState.value)))
const selectableTestEndpoints = computed(() => mappingTestEndpoints.value ?? activeEndpoints.value)
const parsedTestRequestHeaders = computed(() => parseModelTestRequestHeadersDraft(testRequestHeadersDraft.value))
const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.error)
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
@@ -442,7 +447,6 @@ const isLoading = computed(() => Boolean(props.loading) || localLoading.value)
// 使用 props 传入的数据
const models = computed(() => props.models ?? [])
const aliasMappingPreview = computed(() => props.mappingPreview ?? null)
const providerKeysState = computed(() => props.providerKeys ?? [])
// 是否有 key 配置了自动获取上游模型
const hasAutoFetchKey = computed(() => {
@@ -705,6 +709,7 @@ function handleTestDialogClose() {
testingModelName.value = null
testingMapping.value = null
selectedTestEndpoint.value = null
mappingTestEndpoints.value = null
testRequestHeadersDraft.value = ''
testRequestHeadersResetValue.value = ''
testRequestBodyDraft.value = ''
@@ -718,14 +723,16 @@ function handleTestDialogBack() {
}
function handleSelectTestEndpoint(endpointId: string) {
const endpoint = activeEndpoints.value.find(item => item.id === endpointId)
const endpoint = selectableTestEndpoints.value.find(item => item.id === endpointId)
if (!endpoint) return
selectedTestEndpoint.value = endpoint
syncMappingTestRequestBody()
}
// 测试映射(直连测试,带故障转移和实时进度)
function runMappingTest(testingKey: string, modelName: string) {
if (activeEndpoints.value.length === 0) {
function runMappingTest(testingKey: string, modelName: string, endpointsOverride?: ProviderEndpoint[]) {
const endpoints = endpointsOverride ?? activeEndpoints.value
if (endpoints.length === 0) {
showError('暂无可用于测试的活跃端点')
return
}
@@ -734,16 +741,43 @@ function runMappingTest(testingKey: string, modelName: string) {
modelTest.dialogOpen.value = true
testingMapping.value = null
testingModelName.value = modelName
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
mappingTestEndpoints.value = endpointsOverride ?? null
selectedTestEndpoint.value = endpoints[0] ?? null
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(modelName, selectedTestEndpoint.value?.api_format)
resetMappingTestRequestBody()
}
function resetMappingTestRequestBody() {
if (!testingModelName.value) return
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
testingModelName.value,
selectedTestEndpoint.value?.api_format,
)
testRequestBodyDraft.value = testRequestBodyResetValue.value
}
function syncMappingTestRequestBody() {
if (!testingModelName.value) return
const nextResetValue = buildDefaultModelTestRequestBody(
testingModelName.value,
selectedTestEndpoint.value?.api_format,
)
const next = syncModelTestRequestBodyDraft(
testRequestBodyDraft.value,
testRequestBodyResetValue.value,
nextResetValue,
testingModelName.value,
)
testRequestBodyResetValue.value = next.resetValue
testRequestBodyDraft.value = next.draft
}
async function handleStartMappingTest() {
if (modelTest.testing.value || !testingModelName.value) return
const endpoint = selectedTestEndpoint.value || activeEndpoints.value[0]
const endpoint = selectedTestEndpoint.value || selectableTestEndpoints.value[0]
if (!endpoint) {
showError('请选择要测试的端点')
return
@@ -772,7 +806,6 @@ async function handleStartMappingTest() {
endpointBaseUrl: endpoint.base_url,
requestHeaders,
requestBody,
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
})
if (pendingMappingKey.value === currentMappingKey) {
pendingMappingKey.value = null
@@ -780,9 +813,25 @@ async function handleStartMappingTest() {
testingMapping.value = null
}
function scopedMappingEndpoints(item: CombinedMapping): ProviderEndpoint[] {
const group = item.group
if (!group) return activeEndpoints.value
const apiFormats = new Set(normalizeStringList(group.apiFormats).map(normalizeApiFormatAlias))
const endpointIds = new Set(normalizeStringList(group.endpointIds))
const matched = activeEndpoints.value.filter((endpoint) => {
const apiFormatMatched = apiFormats.size === 0
|| apiFormats.has(normalizeApiFormatAlias(endpoint.api_format))
const endpointMatched = endpointIds.size === 0 || endpointIds.has(endpoint.id)
return apiFormatMatched && endpointMatched
})
return matched.length > 0 ? matched : activeEndpoints.value
}
// 测试精确映射
function testMapping(item: CombinedMapping, mapping: MappingItem) {
runMappingTest(`${item.key}-${mapping.name}`, mapping.name)
runMappingTest(`${item.key}-${mapping.name}`, mapping.name, scopedMappingEndpoints(item))
}
// 测试正则映射

View File

@@ -209,14 +209,15 @@
请前往"模型目录"页面添加模型
</p>
</div>
</Card>
<ModelTestDialog
:open="modelTest.dialogOpen.value"
:result="modelTest.testResult.value"
:mode="modelTest.testMode.value"
:provider-type="provider.provider_type"
:selecting-model-name="pendingTestModel ? (pendingTestModel.global_model_display_name || pendingTestModel.provider_model_name) : null"
:requested-model-name="pendingRequestedModelName"
:endpoints="activeEndpoints"
:selected-endpoint="selectedTestEndpoint"
:testing="modelTest.testing.value"
@@ -228,18 +229,22 @@
:request-body-draft="testRequestBodyDraft"
:request-body-reset-value="testRequestBodyResetValue"
:request-body-error="testRequestBodyError"
:model-mapping-available="testModelMappingAvailable"
:model-mapping-options="testModelMappingOptions"
:selected-model-mapping="selectedTestMappedModelName"
:start-disabled="!selectedTestEndpoint || !!testRequestHeadersError || !!testRequestBodyError"
@close="handleTestDialogClose"
@back="handleTestDialogBack"
@start="handleStartPendingTest"
@select-endpoint="handleSelectTestEndpoint"
@select-model-mapping="handleSelectModelMapping"
@update:request-headers-draft="testRequestHeadersDraft = $event"
@update:request-body-draft="testRequestBodyDraft = $event"
/>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useSmartPagination } from '@/composables/useSmartPagination'
import { useModelTest } from '@/composables/useModelTest'
import { Box, Edit, Layers, Power, Copy, Loader2, Play } from 'lucide-vue-next'
@@ -252,6 +257,7 @@ import {
type Model,
type ProviderEndpoint,
} from '@/api/endpoints'
import { type EndpointAPIKey } from '@/api/endpoints/keys'
import { updateModel } from '@/api/endpoints/models'
import { parseApiError } from '@/utils/errorParser'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
@@ -260,16 +266,19 @@ import ModelTestDialog from './ModelTestDialog.vue'
import {
buildDefaultModelTestRequestHeaders,
buildDefaultModelTestRequestBody,
isModelTestableEndpoint,
listModelTestMappedModelOptions,
normalizeModelTestMappedModelSelection,
parseModelTestRequestHeadersDraft,
parseModelTestRequestBodyDraft,
POOL_TEST_CONCURRENCY,
SINGLE_TEST_CONCURRENCY,
syncModelTestRequestBodyDraft,
} from './model-test-request'
const props = defineProps<{
provider: ProviderWithEndpointsSummary
models?: Model[]
endpoints?: ProviderEndpoint[]
providerKeys?: EndpointAPIKey[]
loading?: boolean
}>()
@@ -295,12 +304,31 @@ const testRequestHeadersDraft = ref('')
const testRequestHeadersResetValue = ref('')
const testRequestBodyDraft = ref('')
const testRequestBodyResetValue = ref('')
const selectedTestMappedModelName = ref<string | null>(null)
const isPoolManagedProvider = computed(() => Boolean(props.provider.pool_advanced))
const activeEndpoints = computed(() => (props.endpoints ?? []).filter(endpoint => endpoint.is_active))
const activeEndpoints = computed(() => (props.endpoints ?? [])
.filter(endpoint => isModelTestableEndpoint(endpoint, props.providerKeys ?? [])))
const parsedTestRequestHeaders = computed(() => parseModelTestRequestHeadersDraft(testRequestHeadersDraft.value))
const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.error)
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
const testRequestBodyError = computed(() => parsedTestRequestBody.value.error)
const pendingRequestedModelName = computed(() => getModelTestRequestedModelName(pendingTestModel.value))
const testModelMappingOptions = computed(() => {
const requestedModelName = pendingRequestedModelName.value.trim()
return listModelTestMappedModelOptions(pendingTestModel.value, selectedTestEndpoint.value)
.filter(option => option.name !== requestedModelName)
})
const mappedTestModelName = computed(() => {
const selected = selectedTestMappedModelName.value?.trim()
if (!selected) return null
return testModelMappingOptions.value.some(option => option.name === selected)
? selected
: null
})
const testModelMappingAvailable = computed(() => testModelMappingOptions.value.length > 0)
const effectiveTestRequestModelName = computed(() => (
mappedTestModelName.value || pendingRequestedModelName.value
))
const models = computed(() => props.models ?? localModels.value)
const isLoading = computed(() => Boolean(props.loading) || localLoading.value)
// 按名称排序的模型列表
@@ -467,6 +495,7 @@ function handleTestDialogClose() {
modelTest.resetState()
pendingTestModel.value = null
selectedTestEndpoint.value = null
selectedTestMappedModelName.value = null
testRequestHeadersDraft.value = ''
testRequestHeadersResetValue.value = ''
testRequestBodyDraft.value = ''
@@ -483,6 +512,16 @@ function handleSelectTestEndpoint(endpointId: string) {
const endpoint = activeEndpoints.value.find(item => item.id === endpointId)
if (!endpoint) return
selectedTestEndpoint.value = endpoint
syncSelectedTestModelMapping()
resetTestRequestBodyForSelectedEndpoint()
}
function handleSelectModelMapping(modelName: string) {
selectedTestMappedModelName.value = normalizeModelTestMappedModelSelection(
testModelMappingOptions.value,
modelName,
)
syncTestRequestBodyModel()
}
async function handleStartPendingTest() {
@@ -512,15 +551,16 @@ async function handleStartPendingTest() {
const modelName = model.global_model_name || model.provider_model_name
const endpointPrefix = `[${formatApiFormat(endpoint.api_format)}] `
await modelTest.startTest({
mode: 'global',
mode: isPoolManagedProvider.value ? 'pool' : 'global',
modelName,
displayLabel: `${endpointPrefix}${modelName}`,
apiFormat: endpoint.api_format,
endpointId: endpoint.id,
endpointBaseUrl: endpoint.base_url,
applyModelMapping: Boolean(mappedTestModelName.value),
mappedModelName: mappedTestModelName.value ?? undefined,
requestHeaders,
requestBody,
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
onError: () => {
if (activeEndpoints.value.length > 1) {
return true
@@ -539,10 +579,12 @@ async function testModelConnection(model: Model) {
pendingTestModel.value = model
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
const requestedModelName = getModelTestRequestedModelName(model)
selectedTestMappedModelName.value = null
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
model.global_model_name || model.provider_model_name,
requestedModelName,
selectedTestEndpoint.value?.api_format,
)
testRequestBodyDraft.value = testRequestBodyResetValue.value
@@ -550,6 +592,59 @@ async function testModelConnection(model: Model) {
modelTest.dialogOpen.value = true
}
function getModelTestRequestedModelName(model: Model | null): string {
return model?.global_model_name || model?.provider_model_name || ''
}
function syncSelectedTestModelMapping(preferredName?: string | null) {
const options = testModelMappingOptions.value
if (options.length === 0) {
selectedTestMappedModelName.value = null
return
}
const preferred = preferredName ?? selectedTestMappedModelName.value
selectedTestMappedModelName.value = normalizeModelTestMappedModelSelection(options, preferred)
}
function syncTestRequestBodyModel() {
const modelName = effectiveTestRequestModelName.value
if (!modelName) return
const resetDraft = testRequestBodyResetValue.value
|| buildDefaultModelTestRequestBody(modelName, selectedTestEndpoint.value?.api_format)
const next = syncModelTestRequestBodyDraft(
testRequestBodyDraft.value,
testRequestBodyResetValue.value,
resetDraft,
modelName,
)
testRequestBodyResetValue.value = next.resetValue
testRequestBodyDraft.value = next.draft
}
function resetTestRequestBodyForSelectedEndpoint() {
const modelName = effectiveTestRequestModelName.value
if (!modelName) return
const nextResetValue = buildDefaultModelTestRequestBody(
modelName,
selectedTestEndpoint.value?.api_format,
)
const next = syncModelTestRequestBodyDraft(
testRequestBodyDraft.value,
testRequestBodyResetValue.value,
nextResetValue,
modelName,
)
testRequestBodyResetValue.value = next.resetValue
testRequestBodyDraft.value = next.draft
}
watch(
[effectiveTestRequestModelName, () => selectedTestEndpoint.value?.api_format],
() => syncTestRequestBodyModel(),
)
// 暴露给父组件
defineExpose({
reload: refresh

View File

@@ -1,6 +1,17 @@
import { describe, expect, it } from 'vitest'
import { buildDefaultModelTestRequestBody } from '../model-test-request'
import {
buildDefaultModelTestRequestBody,
buildExactModelMappingTestRequest,
extractModelTestResponsePreview,
formatModelTestDiagnostic,
isModelTestableEndpoint,
isModelTestableApiFormat,
listModelTestMappedModelOptions,
normalizeModelTestMappedModelSelection,
setModelTestRequestBodyModel,
syncModelTestRequestBodyDraft,
} from '../model-test-request'
describe('buildDefaultModelTestRequestBody', () => {
it.each([
@@ -28,10 +39,10 @@ describe('buildDefaultModelTestRequestBody', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody('bge-reranker-base', apiFormat))
expect(body.model).toBe('bge-reranker-base')
expect(body.query).toBe('This is a test rerank query.')
expect(body.documents).toHaveLength(2)
expect(body.top_n).toBe(1)
expect(body.query).toBe('Apple')
expect(body.documents).toEqual(['apple', 'banana', 'fruit', 'vegetable'])
expect(body.return_documents).toBe(true)
expect(body.top_n).toBe(4)
expect(body.messages).toBeUndefined()
expect(body.stream).toBeUndefined()
})
@@ -43,4 +54,296 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(body.stream).toBe(true)
expect(body.input).toBeUndefined()
})
it('lists endpoint-scoped provider model mappings in test selection order', () => {
const options = listModelTestMappedModelOptions({
provider_model_name: 'claude-opus-4-6',
provider_model_mappings: [
{
name: 'MiniMax-M2.7-balanced',
priority: 3,
api_formats: ['openai:chat'],
endpoint_ids: ['endpoint-minimax-chat'],
},
{
name: 'MiniMax-M2.7-highspeed',
priority: 2,
api_formats: ['OPENAI'],
endpoint_ids: ['endpoint-minimax-chat'],
},
{
name: 'ignored-anthropic-model',
priority: 1,
api_formats: ['anthropic:messages'],
},
{
name: 'MiniMax-M2.7-highspeed',
priority: 4,
api_formats: ['openai:chat'],
endpoint_ids: ['endpoint-minimax-chat'],
},
],
}, {
id: 'endpoint-minimax-chat',
api_format: 'openai:chat',
})
expect(options).toEqual([
{ name: 'MiniMax-M2.7-highspeed', priority: 2 },
{ name: 'MiniMax-M2.7-balanced', priority: 3 },
])
})
it('does not select a provider model mapping outside endpoint scope', () => {
const options = listModelTestMappedModelOptions({
provider_model_name: 'claude-opus-4-6',
provider_model_mappings: [
{
name: 'MiniMax-M2.7-highspeed',
priority: 1,
api_formats: ['openai:chat'],
endpoint_ids: ['another-endpoint'],
},
],
}, {
id: 'endpoint-minimax-chat',
api_format: 'openai:chat',
})
expect(options).toEqual([])
})
it('keeps the current model selected by default until a mapped model is chosen', () => {
const options = [
{ name: 'MiniMax-M2.7-highspeed', priority: 1 },
{ name: 'MiniMax-M2.7-balanced', priority: 2 },
]
expect(normalizeModelTestMappedModelSelection(options, null)).toBeNull()
expect(normalizeModelTestMappedModelSelection(options, '')).toBeNull()
expect(normalizeModelTestMappedModelSelection(options, 'MiniMax-M2.7-balanced')).toBe('MiniMax-M2.7-balanced')
expect(normalizeModelTestMappedModelSelection(options, 'another-model')).toBeNull()
})
it('updates only the request body model when model mapping is toggled', () => {
const draft = buildDefaultModelTestRequestBody('claude-opus-4-6', 'openai:chat')
const body = JSON.parse(setModelTestRequestBodyModel(draft, 'MiniMax-M2.7-highspeed'))
expect(body.model).toBe('MiniMax-M2.7-highspeed')
expect(body.messages).toEqual([{ role: 'user', content: 'Hello! This is a test message.' }])
expect(body.stream).toBe(true)
})
it('updates the draft to the next endpoint default when the user has not edited it', () => {
const previous = buildDefaultModelTestRequestBody('chat-model', 'openai:chat')
const nextDefault = buildDefaultModelTestRequestBody('chat-model', 'openai:embedding')
const synced = syncModelTestRequestBodyDraft(previous, previous, nextDefault, 'embedding-model')
expect(JSON.parse(synced.draft)).toEqual({
model: 'embedding-model',
input: 'This is a test embedding input.',
})
expect(synced.resetValue).toBe(synced.draft)
})
it('preserves edited request bodies when switching endpoints', () => {
const previous = buildDefaultModelTestRequestBody('chat-model', 'openai:chat')
const edited = JSON.stringify({
model: 'chat-model',
messages: [{ role: 'user', content: 'custom prompt' }],
max_tokens: 128,
temperature: 0.2,
stream: true,
}, null, 2)
const nextDefault = buildDefaultModelTestRequestBody('chat-model', 'openai:embedding')
const synced = syncModelTestRequestBodyDraft(edited, previous, nextDefault, 'embedding-model')
const body = JSON.parse(synced.draft)
expect(body).toEqual({
model: 'embedding-model',
messages: [{ role: 'user', content: 'custom prompt' }],
max_tokens: 128,
temperature: 0.2,
stream: true,
})
expect(JSON.parse(synced.resetValue)).toEqual({
model: 'embedding-model',
input: 'This is a test embedding input.',
})
})
})
describe('buildExactModelMappingTestRequest', () => {
it('tests the clicked mapping name without applying another provider mapping', () => {
expect(buildExactModelMappingTestRequest(
'provider-1',
'MiniMax-M2.7-balanced',
'openai:chat',
)).toEqual({
provider_id: 'provider-1',
model_name: 'MiniMax-M2.7-balanced',
mode: 'direct',
apply_model_mapping: false,
api_format: 'openai:chat',
})
})
})
describe('isModelTestableApiFormat', () => {
it.each([
'openai:video',
'gemini:video',
'gemini:files',
' OPENAI:VIDEO ',
])('excludes task and file endpoint formats from model tests: %s', (apiFormat) => {
expect(isModelTestableApiFormat(apiFormat)).toBe(false)
})
it.each([
'openai:chat',
'openai:responses',
'claude:messages',
'gemini:generate_content',
'openai:embedding',
'jina:rerank',
])('allows synchronous model-test endpoint formats: %s', (apiFormat) => {
expect(isModelTestableApiFormat(apiFormat)).toBe(true)
})
})
describe('isModelTestableEndpoint', () => {
it('requires at least one active key compatible with the endpoint format', () => {
const keys = [
{
api_formats: ['openai:chat'],
is_active: true,
},
{
api_formats: ['claude:messages'],
is_active: false,
},
]
expect(isModelTestableEndpoint({
api_format: 'openai:chat',
is_active: true,
}, keys)).toBe(true)
expect(isModelTestableEndpoint({
api_format: 'claude:messages',
is_active: true,
}, keys)).toBe(false)
})
it('treats an active key without explicit api formats as compatible with all testable endpoints', () => {
const keys = [{ api_formats: [], is_active: true }]
expect(isModelTestableEndpoint({
api_format: 'openai:responses',
is_active: true,
}, keys)).toBe(true)
})
})
describe('formatModelTestDiagnostic', () => {
it('maps pool account blocked scheduler code to an actionable label', () => {
expect(formatModelTestDiagnostic('pool_account_blocked')).toBe('账号已失效,需重新授权')
})
it('keeps unknown diagnostics unchanged', () => {
expect(formatModelTestDiagnostic('provider auth is unavailable')).toBe('provider auth is unavailable')
})
})
describe('extractModelTestResponsePreview', () => {
it('extracts assistant text from Claude Messages response content', () => {
expect(extractModelTestResponsePreview({
id: 'msg_1',
content: [
{
type: 'text',
text: 'Hello! This is a test message.\n\nTest message received.',
},
],
})).toBe('Hello! This is a test message. Test message received.')
})
it('extracts assistant text from OpenAI Responses output content', () => {
expect(extractModelTestResponsePreview({
output: [
{
type: 'message',
content: [
{
type: 'output_text',
text: 'Response API test passed.',
},
],
},
],
})).toBe('Response API test passed.')
})
it('extracts assistant text from Gemini candidates', () => {
expect(extractModelTestResponsePreview({
candidates: [
{
content: {
parts: [
{ text: 'Gemini test passed.' },
],
},
},
],
})).toBe('Gemini test passed.')
})
it('uses OpenAI reasoning content when answer text is empty', () => {
expect(extractModelTestResponsePreview({
model: 'deepseek-v4-pro',
choices: [
{
message: {
role: 'assistant',
content: '',
reasoning_content: 'We are asked: "Hello! This is a test message." This is just a greeting.',
},
finish_reason: 'length',
},
],
})).toBe('推理We are asked: "Hello! This is a test message." This is just a greeting.')
})
it('uses Claude thinking content when no text content is present', () => {
expect(extractModelTestResponsePreview({
model: 'deepseek-v4-pro',
content: [
{
type: 'thinking',
thinking: 'We are given a simple test message: "Hello! This is a test message."',
},
],
stop_reason: 'max_tokens',
})).toBe('推理We are given a simple test message: "Hello! This is a test message."')
})
it('summarizes embedding and rerank responses without assistant text', () => {
expect(extractModelTestResponsePreview({
data: [
{ embedding: [0.1, 0.2, 0.3] },
],
})).toBe('Embedding 维度3')
expect(extractModelTestResponsePreview({
results: [
{ index: 0, relevance_score: 0.9 },
{ index: 1, relevance_score: 0.5 },
],
})).toBe('Rerank 结果2 条')
})
it('falls back to response model when no text payload exists', () => {
expect(extractModelTestResponsePreview({
model: 'glm-4.5-air',
})).toBe('返回模型glm-4.5-air')
})
})

View File

@@ -1,8 +1,412 @@
const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
import { normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
import type { ProviderModelMapping } from '@/api/endpoints/types'
import type { TestModelRequest } from '@/api/endpoints/providers'
/** Pool-managed provider runs concurrent checks; single-key provider does not. */
export const POOL_TEST_CONCURRENCY = 5
export const SINGLE_TEST_CONCURRENCY = 1
const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
const MODEL_TEST_RESPONSE_PREVIEW_MAX_LENGTH = 160
type ModelTestMappingSource = {
provider_model_name: string
provider_model_mappings?: ProviderModelMapping[] | null
}
type ModelTestMappingEndpoint = {
id: string
api_format: string
}
type ModelTestEndpointSource = {
api_format: string
is_active?: boolean | null
}
type ModelTestKeySource = {
api_formats?: string[] | null
is_active?: boolean | null
}
export type ModelTestMappedModelOption = {
name: string
priority: number
}
const MODEL_TEST_UNSUPPORTED_API_FORMATS = new Set([
'openai:video',
'gemini:video',
'gemini:files',
])
const MODEL_TEST_DIAGNOSTIC_LABELS: Record<string, string> = {
pool_account_blocked: '账号已失效,需重新授权',
}
type JsonRecord = Record<string, unknown>
export function isModelTestableApiFormat(apiFormat: string | null | undefined): boolean {
const normalized = normalizeApiFormatAlias(apiFormat ?? '')
return Boolean(normalized) && !MODEL_TEST_UNSUPPORTED_API_FORMATS.has(normalized)
}
export function modelTestKeySupportsEndpoint(
key: ModelTestKeySource,
endpoint: ModelTestEndpointSource,
): boolean {
if (key.is_active === false) return false
const endpointFormat = normalizeApiFormatAlias(endpoint.api_format)
if (!isModelTestableApiFormat(endpointFormat)) return false
const keyFormats = normalizeStringList(key.api_formats ?? undefined)
if (keyFormats.length === 0) return true
return keyFormats.some(format => normalizeApiFormatAlias(format) === endpointFormat)
}
export function isModelTestableEndpoint(
endpoint: ModelTestEndpointSource,
keys: ModelTestKeySource[],
): boolean {
return endpoint.is_active !== false
&& isModelTestableApiFormat(endpoint.api_format)
&& keys.some(key => modelTestKeySupportsEndpoint(key, endpoint))
}
export function formatModelTestDiagnostic(value: string | null | undefined): string {
const normalized = value?.trim()
if (!normalized) return ''
return MODEL_TEST_DIAGNOSTIC_LABELS[normalized] ?? normalized
}
export function extractModelTestResponsePreview(responseBody: unknown): string | null {
const text = extractResponseText(responseBody)
if (text) return text
const reasoning = extractResponseReasoning(responseBody)
if (reasoning) return `推理:${reasoning}`
const summary = extractResponseSummary(responseBody)
if (summary) return summary
return null
}
function normalizeStringList(values: string[] | undefined): string[] {
return (values ?? [])
.map(value => value.trim())
.filter(Boolean)
}
function isJsonRecord(value: unknown): value is JsonRecord {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
function compactPreviewText(value: unknown): string | null {
if (typeof value !== 'string') return null
const normalized = value.replace(/\s+/g, ' ').trim()
if (!normalized) return null
if (normalized.length <= MODEL_TEST_RESPONSE_PREVIEW_MAX_LENGTH) {
return normalized
}
return `${normalized.slice(0, MODEL_TEST_RESPONSE_PREVIEW_MAX_LENGTH - 3)}...`
}
function joinPreviewParts(parts: string[]): string | null {
return compactPreviewText(parts.filter(Boolean).join(' '))
}
function extractTextFromContentParts(value: unknown, depth = 0): string | null {
if (depth > 4) return null
const directText = compactPreviewText(value)
if (directText) return directText
if (!Array.isArray(value)) return null
const parts = value.flatMap((part) => {
if (typeof part === 'string') return [part]
if (!isJsonRecord(part)) return []
const text = compactPreviewText(part.text)
?? compactPreviewText(part.content)
?? extractTextFromContentParts(part.parts, depth + 1)
return text ? [text] : []
})
return joinPreviewParts(parts)
}
function extractResponseText(responseBody: unknown, depth = 0): string | null {
if (depth > 4 || !isJsonRecord(responseBody)) return null
const wrappedText = extractResponseText(responseBody.response, depth + 1)
?? extractResponseText(responseBody.body, depth + 1)
if (wrappedText) return wrappedText
const outputText = compactPreviewText(responseBody.output_text)
if (outputText) return outputText
const topLevelContentText = extractTextFromContentParts(responseBody.content, depth + 1)
if (topLevelContentText) return topLevelContentText
const choicesText = extractChoicesText(responseBody.choices, depth + 1)
if (choicesText) return choicesText
const outputTextParts = extractOutputText(responseBody.output, depth + 1)
if (outputTextParts) return outputTextParts
const candidateText = extractGeminiCandidateText(responseBody.candidates, depth + 1)
if (candidateText) return candidateText
return null
}
function extractResponseReasoning(responseBody: unknown, depth = 0): string | null {
if (depth > 4 || !isJsonRecord(responseBody)) return null
const wrappedReasoning = extractResponseReasoning(responseBody.response, depth + 1)
?? extractResponseReasoning(responseBody.body, depth + 1)
if (wrappedReasoning) return wrappedReasoning
const directReasoning = compactPreviewText(responseBody.reasoning_content)
?? compactPreviewText(responseBody.thinking)
if (directReasoning) return directReasoning
const topLevelReasoning = extractReasoningFromContentParts(responseBody.content, depth + 1)
if (topLevelReasoning) return topLevelReasoning
const choicesReasoning = extractChoicesReasoning(responseBody.choices, depth + 1)
if (choicesReasoning) return choicesReasoning
const outputReasoning = extractOutputReasoning(responseBody.output, depth + 1)
if (outputReasoning) return outputReasoning
return null
}
function extractChoicesText(value: unknown, depth: number): string | null {
if (!Array.isArray(value)) return null
for (const choice of value) {
if (!isJsonRecord(choice)) continue
const messageText = isJsonRecord(choice.message)
? extractTextFromContentParts(choice.message.content, depth + 1)
: null
const deltaText = isJsonRecord(choice.delta)
? extractTextFromContentParts(choice.delta.content, depth + 1)
: null
const text = messageText ?? deltaText ?? extractTextFromContentParts(choice.text, depth + 1)
if (text) return text
}
return null
}
function extractChoicesReasoning(value: unknown, depth: number): string | null {
if (!Array.isArray(value)) return null
for (const choice of value) {
if (!isJsonRecord(choice)) continue
const messageReasoning = isJsonRecord(choice.message)
? extractReasoningFromMessage(choice.message, depth + 1)
: null
const deltaReasoning = isJsonRecord(choice.delta)
? extractReasoningFromMessage(choice.delta, depth + 1)
: null
const reasoning = messageReasoning ?? deltaReasoning
if (reasoning) return reasoning
}
return null
}
function extractReasoningFromMessage(message: JsonRecord, depth: number): string | null {
return compactPreviewText(message.reasoning_content)
?? compactPreviewText(message.thinking)
?? extractReasoningFromContentParts(message.content, depth + 1)
}
function extractOutputText(value: unknown, depth: number): string | null {
if (!Array.isArray(value)) return null
for (const outputItem of value) {
if (!isJsonRecord(outputItem)) continue
const contentText = extractTextFromContentParts(outputItem.content, depth + 1)
?? extractResponseText(outputItem.response, depth + 1)
if (contentText) return contentText
}
return null
}
function extractOutputReasoning(value: unknown, depth: number): string | null {
if (!Array.isArray(value)) return null
for (const outputItem of value) {
if (!isJsonRecord(outputItem)) continue
const reasoning = extractReasoningFromContentParts(outputItem.content, depth + 1)
?? compactPreviewText(outputItem.reasoning_content)
?? compactPreviewText(outputItem.thinking)
?? extractResponseReasoning(outputItem.response, depth + 1)
if (reasoning) return reasoning
}
return null
}
function extractGeminiCandidateText(value: unknown, depth: number): string | null {
if (!Array.isArray(value)) return null
for (const candidate of value) {
if (!isJsonRecord(candidate) || !isJsonRecord(candidate.content)) continue
const text = extractTextFromContentParts(candidate.content.parts, depth + 1)
if (text) return text
}
return null
}
function extractReasoningFromContentParts(value: unknown, depth = 0): string | null {
if (depth > 4 || !Array.isArray(value)) return null
const parts = value.flatMap((part) => {
if (!isJsonRecord(part)) return []
const reasoning = compactPreviewText(part.reasoning_content)
?? compactPreviewText(part.thinking)
?? compactPreviewText(part.reasoning)
?? extractReasoningFromContentParts(part.content, depth + 1)
?? extractReasoningFromContentParts(part.parts, depth + 1)
return reasoning ? [reasoning] : []
})
return joinPreviewParts(parts)
}
function extractResponseSummary(responseBody: unknown): string | null {
if (!isJsonRecord(responseBody)) return null
if (Array.isArray(responseBody.data)) {
const embeddingDimensions = responseBody.data
.map(item => isJsonRecord(item) && Array.isArray(item.embedding) ? item.embedding.length : null)
.find((size): size is number => typeof size === 'number')
if (embeddingDimensions != null) return `Embedding 维度:${embeddingDimensions}`
if (responseBody.data.length > 0) return `返回数据:${responseBody.data.length}`
}
if (Array.isArray(responseBody.results)) return `Rerank 结果:${responseBody.results.length}`
const model = compactPreviewText(responseBody.model)
if (model) return `返回模型:${model}`
return null
}
function mappingApiFormatMatches(mapping: ProviderModelMapping, endpoint: ModelTestMappingEndpoint): boolean {
const apiFormats = normalizeStringList(mapping.api_formats)
if (apiFormats.length === 0) return true
const endpointFormat = normalizeApiFormatAlias(endpoint.api_format)
return apiFormats.some(format => normalizeApiFormatAlias(format) === endpointFormat)
}
function mappingEndpointMatches(mapping: ProviderModelMapping, endpoint: ModelTestMappingEndpoint): boolean {
const endpointIds = normalizeStringList(mapping.endpoint_ids)
if (endpointIds.length === 0) return true
return endpointIds.includes(endpoint.id)
}
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 => mappingApiFormatMatches(mapping, endpoint))
.filter(mapping => mappingEndpointMatches(mapping, 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): string {
if (apiFormat?.trim().toLowerCase().endsWith(':embedding')) {
@@ -15,13 +419,15 @@ export function buildDefaultModelTestRequestBody(modelName: string, apiFormat?:
if (apiFormat?.trim().toLowerCase().endsWith(':rerank')) {
return JSON.stringify({
model: modelName,
query: 'This is a test rerank query.',
query: 'Apple',
documents: [
'This document is relevant to the test query.',
'This document is unrelated.',
'apple',
'banana',
'fruit',
'vegetable',
],
top_n: 1,
return_documents: true,
top_n: 4,
}, null, 2)
}