mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat(frontend): 澄清模型映射适用范围
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ALL_REQUESTS_SCOPE_VALUE,
|
||||
COMPACT_REQUEST_SCOPE_VALUE,
|
||||
formatModelMappingEndpointLabel,
|
||||
formatModelMappingRequestScope,
|
||||
modelMappingOperationsKey,
|
||||
modelMappingOperationsFromScopeValue,
|
||||
modelMappingRequestScopeOptions,
|
||||
modelMappingRequestScopeValue,
|
||||
normalizeModelMappingOperations,
|
||||
} from '../modelMappingScope'
|
||||
|
||||
describe('model mapping request scope', () => {
|
||||
it('represents an omitted operation filter as all requests', () => {
|
||||
expect(modelMappingRequestScopeValue(undefined)).toBe(ALL_REQUESTS_SCOPE_VALUE)
|
||||
expect(modelMappingOperationsFromScopeValue(ALL_REQUESTS_SCOPE_VALUE)).toBeUndefined()
|
||||
expect(formatModelMappingRequestScope(undefined)).toBe('所有请求')
|
||||
})
|
||||
|
||||
it('round-trips the compact operation as a dedicated request scope', () => {
|
||||
expect(modelMappingRequestScopeValue(['compact'])).toBe(COMPACT_REQUEST_SCOPE_VALUE)
|
||||
expect(modelMappingOperationsFromScopeValue(COMPACT_REQUEST_SCOPE_VALUE)).toEqual(['compact'])
|
||||
expect(formatModelMappingRequestScope(['compact'])).toBe('仅会话压缩')
|
||||
})
|
||||
|
||||
it('normalizes operation values using the backend matching semantics', () => {
|
||||
expect(normalizeModelMappingOperations([' Compact ', 'compact', '', 'SEARCH'])).toEqual([
|
||||
'compact',
|
||||
'search',
|
||||
])
|
||||
expect(modelMappingOperationsKey(['SEARCH', ' compact ', 'Compact'])).toBe('compact,search')
|
||||
})
|
||||
|
||||
it('preserves an unknown operation scope while editing', () => {
|
||||
const operations = ['future_operation', 'compact']
|
||||
const value = modelMappingRequestScopeValue(operations)
|
||||
const options = modelMappingRequestScopeOptions(operations)
|
||||
|
||||
expect(modelMappingOperationsFromScopeValue(value)).toEqual(operations)
|
||||
expect(options).toContainEqual({ value, label: '仅匹配:future_operation, compact' })
|
||||
})
|
||||
|
||||
it('rejects malformed scope values without constructing operations', () => {
|
||||
expect(modelMappingOperationsFromScopeValue('compact')).toBeUndefined()
|
||||
expect(modelMappingOperationsFromScopeValue('{"compact":true}')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disambiguates endpoints that share an API format', () => {
|
||||
const endpoints = [
|
||||
{
|
||||
id: 'endpoint-1',
|
||||
api_format: 'openai:responses',
|
||||
base_url: 'https://api.example.com/v1',
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
id: 'endpoint-2',
|
||||
api_format: 'openai:responses',
|
||||
base_url: 'https://backup.example.com/v1',
|
||||
custom_path: '/backend-api/codex/responses',
|
||||
is_active: false,
|
||||
},
|
||||
]
|
||||
|
||||
expect(formatModelMappingEndpointLabel(endpoints[0], endpoints)).toBe(
|
||||
'OpenAI Responses · api.example.com/v1',
|
||||
)
|
||||
expect(formatModelMappingEndpointLabel(endpoints[1], endpoints)).toBe(
|
||||
'OpenAI Responses · backup.example.com/backend-api/codex/responses(停用)',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
|
||||
export const MODEL_MAPPING_OPERATION_COMPACT = 'compact'
|
||||
|
||||
export const ALL_REQUESTS_SCOPE_VALUE = '[]'
|
||||
export const COMPACT_REQUEST_SCOPE_VALUE = JSON.stringify([
|
||||
MODEL_MAPPING_OPERATION_COMPACT,
|
||||
])
|
||||
|
||||
export interface ModelMappingRequestScopeOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ModelMappingRequestScopeLabels {
|
||||
allRequests: string
|
||||
sessionCompactionOnly: string
|
||||
customOperations: (operations: string[]) => string
|
||||
}
|
||||
|
||||
export interface ModelMappingEndpoint {
|
||||
id: string
|
||||
api_format: string
|
||||
base_url: string
|
||||
custom_path?: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_REQUEST_SCOPE_LABELS: ModelMappingRequestScopeLabels = {
|
||||
allRequests: '所有请求',
|
||||
sessionCompactionOnly: '仅会话压缩',
|
||||
customOperations: operations => `仅匹配:${operations.join(', ')}`,
|
||||
}
|
||||
|
||||
export function normalizeModelMappingOperations(
|
||||
operations: string[] | undefined,
|
||||
): string[] {
|
||||
const seen = new Set<string>()
|
||||
const normalized: string[] = []
|
||||
for (const operation of operations ?? []) {
|
||||
const value = operation.trim().toLowerCase()
|
||||
if (!value || seen.has(value)) continue
|
||||
seen.add(value)
|
||||
normalized.push(value)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function modelMappingRequestScopeValue(
|
||||
operations: string[] | undefined,
|
||||
): string {
|
||||
return JSON.stringify(normalizeModelMappingOperations(operations))
|
||||
}
|
||||
|
||||
export function modelMappingOperationsKey(
|
||||
operations: string[] | undefined,
|
||||
): string {
|
||||
return normalizeModelMappingOperations(operations).sort().join(',')
|
||||
}
|
||||
|
||||
export function modelMappingOperationsFromScopeValue(
|
||||
value: string,
|
||||
): string[] | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (!Array.isArray(parsed) || parsed.some(item => typeof item !== 'string')) {
|
||||
return undefined
|
||||
}
|
||||
const operations = normalizeModelMappingOperations(parsed)
|
||||
return operations.length > 0 ? operations : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function formatModelMappingRequestScope(
|
||||
operations: string[] | undefined,
|
||||
labels: ModelMappingRequestScopeLabels = DEFAULT_REQUEST_SCOPE_LABELS,
|
||||
): string {
|
||||
const normalized = normalizeModelMappingOperations(operations)
|
||||
if (normalized.length === 0) return labels.allRequests
|
||||
if (
|
||||
normalized.length === 1
|
||||
&& normalized[0] === MODEL_MAPPING_OPERATION_COMPACT
|
||||
) {
|
||||
return labels.sessionCompactionOnly
|
||||
}
|
||||
return labels.customOperations(normalized)
|
||||
}
|
||||
|
||||
export function modelMappingRequestScopeOptions(
|
||||
operations: string[] | undefined,
|
||||
labels: ModelMappingRequestScopeLabels = DEFAULT_REQUEST_SCOPE_LABELS,
|
||||
): ModelMappingRequestScopeOption[] {
|
||||
const options: ModelMappingRequestScopeOption[] = [
|
||||
{ value: ALL_REQUESTS_SCOPE_VALUE, label: labels.allRequests },
|
||||
{ value: COMPACT_REQUEST_SCOPE_VALUE, label: labels.sessionCompactionOnly },
|
||||
]
|
||||
const currentValue = modelMappingRequestScopeValue(operations)
|
||||
if (!options.some(option => option.value === currentValue)) {
|
||||
options.push({
|
||||
value: currentValue,
|
||||
label: formatModelMappingRequestScope(operations, labels),
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
export function formatModelMappingEndpointLabel(
|
||||
endpoint: ModelMappingEndpoint,
|
||||
endpoints: ModelMappingEndpoint[],
|
||||
): string {
|
||||
const sameFormatCount = endpoints.filter(item => item.api_format === endpoint.api_format).length
|
||||
const format = formatApiFormat(endpoint.api_format)
|
||||
const discriminator = sameFormatCount > 1
|
||||
? formatModelMappingEndpointDiscriminator(endpoint)
|
||||
: ''
|
||||
const status = endpoint.is_active ? '' : '(停用)'
|
||||
return `${format}${discriminator ? ` · ${discriminator}` : ''}${status}`
|
||||
}
|
||||
|
||||
function formatModelMappingEndpointDiscriminator(endpoint: ModelMappingEndpoint): string {
|
||||
const baseUrl = endpoint.base_url.trim()
|
||||
try {
|
||||
const parsed = new URL(baseUrl)
|
||||
const customPath = endpoint.custom_path?.trim()
|
||||
const path = (customPath || parsed.pathname).replace(/\/$/, '')
|
||||
return `${parsed.host}${path && path !== '/' ? path : ''}`
|
||||
} catch {
|
||||
return baseUrl || endpoint.id.slice(0, 8)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user