mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(frontend): 澄清模型映射适用范围
This commit is contained in:
@@ -50,9 +50,11 @@ describe('api format display helpers', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('applies Responses to Search permissions in one direction', () => {
|
||||
it('applies Responses companion permissions in one direction', () => {
|
||||
expect(apiFormatPermissionCovers('OPENAI_RESPONSES', 'openai:search')).toBe(true)
|
||||
expect(apiFormatPermissionCovers('OPENAI_RESPONSES', 'openai:responses:compact')).toBe(true)
|
||||
expect(apiFormatPermissionCovers('openai:search', 'openai:responses')).toBe(false)
|
||||
expect(apiFormatPermissionCovers('openai:responses:compact', 'openai:responses')).toBe(false)
|
||||
})
|
||||
|
||||
it('formats embedding api format ids distinctly from chat formats', () => {
|
||||
|
||||
@@ -235,7 +235,9 @@ export function apiFormatPermissionCovers(
|
||||
return Boolean(allowed)
|
||||
&& Boolean(requested)
|
||||
&& (allowed === requested
|
||||
|| (allowed === API_FORMATS.OPENAI_RESPONSES && requested === API_FORMATS.OPENAI_SEARCH))
|
||||
|| (allowed === API_FORMATS.OPENAI_RESPONSES
|
||||
&& (requested === API_FORMATS.OPENAI_RESPONSES_COMPACT
|
||||
|| requested === API_FORMATS.OPENAI_SEARCH)))
|
||||
}
|
||||
|
||||
// 工具函数:按 family 分组并排序 API 格式数组
|
||||
|
||||
@@ -37,42 +37,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 端点限制 -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label class="text-xs">限制端点</Label>
|
||||
<span class="text-xs text-muted-foreground">{{ endpointScopeSummary }}</span>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model="selectedEndpointIds"
|
||||
:options="endpointOptions"
|
||||
placeholder="全部端点"
|
||||
empty-text="暂无端点"
|
||||
no-results-text="未找到端点"
|
||||
trigger-class="h-9 rounded-md"
|
||||
dropdown-min-width="24rem"
|
||||
:search-threshold="4"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
默认对全部端点生效;选择端点后,此映射只在选中的端点上生效
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label class="text-xs">请求操作</Label>
|
||||
<span class="text-xs text-muted-foreground">{{ operationScopeSummary }}</span>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model="selectedOperations"
|
||||
:options="operationOptions"
|
||||
placeholder="全部操作"
|
||||
empty-text="暂无可选操作"
|
||||
no-results-text="未找到操作"
|
||||
trigger-class="h-9 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 映射名称选择面板 -->
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">提供商模型</Label>
|
||||
@@ -259,6 +223,67 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 border-t border-border/60 pt-4">
|
||||
<div class="flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
||||
<div class="min-w-0 space-y-0.5">
|
||||
<h3 class="text-sm font-medium text-foreground">
|
||||
适用范围
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('providers.modelMapping.scope.matchHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="max-w-full break-words text-left text-xs text-muted-foreground sm:max-w-[55%] sm:text-right">
|
||||
{{ mappingScopeSummary }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="min-w-0 space-y-1.5">
|
||||
<Label class="text-xs">适用端点</Label>
|
||||
<MultiSelect
|
||||
v-model="selectedEndpointIds"
|
||||
:options="endpointOptions"
|
||||
:placeholder="t('providers.modelMapping.scope.allEndpoints')"
|
||||
empty-text="暂无端点"
|
||||
no-results-text="未找到端点"
|
||||
trigger-class="h-9 rounded-md"
|
||||
:search-threshold="4"
|
||||
/>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
{{ t('providers.modelMapping.scope.endpointHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 space-y-1.5">
|
||||
<Label class="text-xs">适用请求</Label>
|
||||
<div
|
||||
class="flex min-h-9 w-full flex-wrap gap-1 rounded-md bg-muted/50 p-1"
|
||||
role="radiogroup"
|
||||
aria-label="适用请求"
|
||||
>
|
||||
<Button
|
||||
v-for="option in requestScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
size="sm"
|
||||
:variant="requestScopeValue === option.value ? 'secondary' : 'ghost'"
|
||||
class="h-7 min-w-0 flex-1 basis-[9rem] px-2.5"
|
||||
role="radio"
|
||||
:aria-checked="requestScopeValue === option.value"
|
||||
:title="option.label"
|
||||
@click="handleRequestScopeChange(option.value)"
|
||||
>
|
||||
<span class="truncate">{{ option.label }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
{{ requestScopeDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -276,7 +301,7 @@
|
||||
v-if="submitting"
|
||||
class="w-4 h-4 mr-2 animate-spin"
|
||||
/>
|
||||
{{ editingGroup ? '保存' : '添加' }}
|
||||
{{ editingGroup ? '保存映射' : '添加映射' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -298,6 +323,7 @@ import {
|
||||
} from '@/components/ui'
|
||||
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
type Model,
|
||||
@@ -306,8 +332,18 @@ import {
|
||||
type UpstreamModel,
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
||||
import {
|
||||
ALL_REQUESTS_SCOPE_VALUE,
|
||||
COMPACT_REQUEST_SCOPE_VALUE,
|
||||
formatModelMappingEndpointLabel,
|
||||
formatModelMappingRequestScope,
|
||||
modelMappingOperationsKey,
|
||||
modelMappingOperationsFromScopeValue,
|
||||
modelMappingRequestScopeOptions,
|
||||
modelMappingRequestScopeValue,
|
||||
normalizeModelMappingOperations,
|
||||
} from '../utils/modelMappingScope'
|
||||
|
||||
export interface AliasGroup {
|
||||
model: Model
|
||||
@@ -340,6 +376,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess, warning: showWarning } = useToast()
|
||||
const { t } = useI18n()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
type EndpointOption = {
|
||||
@@ -347,11 +384,6 @@ type EndpointOption = {
|
||||
label: string
|
||||
}
|
||||
|
||||
type OperationOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// 状态
|
||||
const submitting = ref(false)
|
||||
const loadingModels = ref(false)
|
||||
@@ -382,54 +414,77 @@ const selectedEndpointIds = ref<string[]>([])
|
||||
|
||||
const selectedOperations = ref<string[]>([])
|
||||
|
||||
const operationOptions: OperationOption[] = [
|
||||
{ value: 'compact', label: '线程压缩' }
|
||||
]
|
||||
|
||||
// 自定义名称列表(手动添加的)
|
||||
const allCustomNames = ref<string[]>([])
|
||||
|
||||
const endpointOptions = computed<EndpointOption[]>(() => {
|
||||
return (props.endpoints ?? []).map((endpoint) => {
|
||||
const status = endpoint.is_active ? '' : '(停用)'
|
||||
return {
|
||||
value: endpoint.id,
|
||||
label: `${formatApiFormat(endpoint.api_format)}${status}`,
|
||||
}
|
||||
})
|
||||
const endpoints = props.endpoints ?? []
|
||||
return endpoints.map(endpoint => ({
|
||||
value: endpoint.id,
|
||||
label: formatModelMappingEndpointLabel(endpoint, endpoints),
|
||||
}))
|
||||
})
|
||||
|
||||
const normalizedSelectedEndpointIds = computed(() => {
|
||||
const validIds = new Set(endpointOptions.value.map(option => option.value))
|
||||
const selected = normalizeStringList(selectedEndpointIds.value)
|
||||
if (selected.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const invalidSelected = selected.filter(endpointId => !validIds.has(endpointId))
|
||||
const selectedValidCount = selected.filter(endpointId => validIds.has(endpointId)).length
|
||||
if (validIds.size > 0 && invalidSelected.length === 0 && selectedValidCount === validIds.size) {
|
||||
return undefined
|
||||
}
|
||||
return selected
|
||||
return selected.length > 0 ? selected : undefined
|
||||
})
|
||||
|
||||
const endpointScopeSummary = computed(() => {
|
||||
const selected = normalizedSelectedEndpointIds.value
|
||||
if (!selected || selected.length === 0) return '全部端点'
|
||||
return `${selected.length} 个端点`
|
||||
if (!selected || selected.length === 0) {
|
||||
return t('providers.modelMapping.scope.allEndpoints')
|
||||
}
|
||||
if (selected.length === 1) {
|
||||
return endpointOptions.value.find(option => option.value === selected[0])?.label
|
||||
?? t('providers.modelMapping.scope.endpointCount', { count: 1 })
|
||||
}
|
||||
return t('providers.modelMapping.scope.endpointCount', { count: selected.length })
|
||||
})
|
||||
|
||||
const requestScopeLabels = computed(() => ({
|
||||
allRequests: t('providers.modelMapping.scope.allRequests'),
|
||||
sessionCompactionOnly: t('providers.modelMapping.scope.sessionCompactionOnly'),
|
||||
customOperations: (operations: string[]) => t(
|
||||
'providers.modelMapping.scope.customOperations',
|
||||
{ operations: operations.join(', ') },
|
||||
),
|
||||
}))
|
||||
|
||||
const normalizedSelectedOperations = computed(() => {
|
||||
const selected = normalizeStringList(selectedOperations.value)
|
||||
const selected = normalizeModelMappingOperations(selectedOperations.value)
|
||||
return selected.length > 0 ? selected : undefined
|
||||
})
|
||||
|
||||
const operationScopeSummary = computed(() => {
|
||||
const selected = normalizedSelectedOperations.value
|
||||
if (!selected) return '全部操作'
|
||||
return selected.length === 1 && selected[0] === 'compact'
|
||||
? '线程压缩'
|
||||
: `${selected.length} 项操作`
|
||||
return formatModelMappingRequestScope(
|
||||
normalizedSelectedOperations.value,
|
||||
requestScopeLabels.value,
|
||||
)
|
||||
})
|
||||
|
||||
const mappingScopeSummary = computed(() => {
|
||||
return `${endpointScopeSummary.value} · ${operationScopeSummary.value}`
|
||||
})
|
||||
|
||||
const requestScopeValue = computed(() => {
|
||||
return modelMappingRequestScopeValue(selectedOperations.value)
|
||||
})
|
||||
|
||||
const requestScopeOptions = computed(() => {
|
||||
return modelMappingRequestScopeOptions(selectedOperations.value, requestScopeLabels.value)
|
||||
})
|
||||
|
||||
const requestScopeDescription = computed(() => {
|
||||
if (requestScopeValue.value === ALL_REQUESTS_SCOPE_VALUE) {
|
||||
return t('providers.modelMapping.scope.allRequestsDescription')
|
||||
}
|
||||
if (requestScopeValue.value === COMPACT_REQUEST_SCOPE_VALUE) {
|
||||
return t('providers.modelMapping.scope.sessionCompactionDescription')
|
||||
}
|
||||
return t('providers.modelMapping.scope.customOperationsDescription', {
|
||||
operations: normalizeModelMappingOperations(selectedOperations.value).join(', '),
|
||||
})
|
||||
})
|
||||
|
||||
// 所有已知名称集合
|
||||
@@ -559,6 +614,17 @@ function scopesOverlap(left: string[] | undefined, right: string[] | undefined):
|
||||
return leftValues.some(value => rightSet.has(value))
|
||||
}
|
||||
|
||||
function operationScopesOverlap(
|
||||
left: string[] | undefined,
|
||||
right: string[] | undefined,
|
||||
): boolean {
|
||||
const leftValues = normalizeModelMappingOperations(left)
|
||||
const rightValues = normalizeModelMappingOperations(right)
|
||||
if (leftValues.length === 0 || rightValues.length === 0) return true
|
||||
const rightSet = new Set(rightValues)
|
||||
return leftValues.some(value => rightSet.has(value))
|
||||
}
|
||||
|
||||
function findDuplicateNames(
|
||||
existingAliases: ProviderModelAlias[],
|
||||
names: string[],
|
||||
@@ -574,7 +640,7 @@ function findDuplicateNames(
|
||||
return alias.name === name
|
||||
&& scopesOverlap(alias.endpoint_ids, endpointIds)
|
||||
&& scopesOverlap(alias.api_formats, apiFormats)
|
||||
&& scopesOverlap(alias.operations, operations)
|
||||
&& operationScopesOverlap(alias.operations, operations)
|
||||
})
|
||||
if (duplicate) duplicates.add(name)
|
||||
}
|
||||
@@ -640,7 +706,7 @@ function initForm() {
|
||||
const existingNames = props.editingGroup.aliases.map(a => a.name)
|
||||
selectedNames.value = [...existingNames]
|
||||
selectedEndpointIds.value = normalizeStringList(props.editingGroup.endpointIds)
|
||||
selectedOperations.value = normalizeStringList(props.editingGroup.operations)
|
||||
selectedOperations.value = normalizeModelMappingOperations(props.editingGroup.operations)
|
||||
allCustomNames.value = [...existingNames]
|
||||
} else {
|
||||
formData.value = {
|
||||
@@ -662,6 +728,10 @@ function handleModelChange(value: string) {
|
||||
formData.value.modelId = value
|
||||
}
|
||||
|
||||
function handleRequestScopeChange(value: string) {
|
||||
selectedOperations.value = modelMappingOperationsFromScopeValue(value) ?? []
|
||||
}
|
||||
|
||||
// 生成作用域唯一键
|
||||
function getApiFormatsKey(formats: string[] | undefined): string {
|
||||
return getScopeKey(formats)
|
||||
@@ -672,7 +742,7 @@ function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
}
|
||||
|
||||
function getOperationsKey(operations: string[] | undefined): string {
|
||||
return getScopeKey(operations)
|
||||
return modelMappingOperationsKey(operations)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
@@ -712,7 +782,7 @@ async function handleSubmit() {
|
||||
if (props.editingGroup) {
|
||||
const oldApiFormatsKey = props.editingGroup.apiFormatsKey
|
||||
const oldEndpointIdsKey = props.editingGroup.endpointIdsKey
|
||||
const oldOperationsKey = props.editingGroup.operationsKey
|
||||
const oldOperationsKey = modelMappingOperationsKey(props.editingGroup.operations)
|
||||
const oldAliasNames = new Set(props.editingGroup.aliases.map(a => a.name))
|
||||
|
||||
const filteredAliases = currentAliases.filter((a: ProviderModelAlias) => {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from 'vue'
|
||||
|
||||
import ModelMappingDialog, { type AliasGroup } from '../ModelMappingDialog.vue'
|
||||
import type { Model, ProviderEndpoint } from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
const passthrough = (name: string, tag = 'div') => defineComponent({
|
||||
name,
|
||||
setup(_, { slots }) {
|
||||
return () => h(tag, [slots.default?.(), slots.footer?.()])
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Button: defineComponent({
|
||||
name: 'ButtonStub',
|
||||
setup(_, { attrs, slots }) {
|
||||
return () => h('button', { ...attrs, type: 'button' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
Dialog: passthrough('DialogStub'),
|
||||
Input: defineComponent({
|
||||
name: 'InputStub',
|
||||
props: { modelValue: String },
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { attrs, emit }) {
|
||||
return () => h('input', {
|
||||
...attrs,
|
||||
value: props.modelValue ?? '',
|
||||
onInput: (event: Event) => emit(
|
||||
'update:modelValue',
|
||||
(event.target as HTMLInputElement).value,
|
||||
),
|
||||
})
|
||||
},
|
||||
}),
|
||||
Label: passthrough('LabelStub', 'label'),
|
||||
Select: passthrough('SelectStub'),
|
||||
SelectContent: passthrough('SelectContentStub'),
|
||||
SelectItem: passthrough('SelectItemStub'),
|
||||
SelectTrigger: passthrough('SelectTriggerStub'),
|
||||
SelectValue: passthrough('SelectValueStub', 'span'),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/components/common/MultiSelect.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'MultiSelectStub',
|
||||
setup() {
|
||||
return () => h('div')
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('lucide-vue-next', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
const Icon = defineComponent({
|
||||
name: 'IconStub',
|
||||
setup() {
|
||||
return () => h('span')
|
||||
},
|
||||
})
|
||||
return {
|
||||
Check: Icon,
|
||||
ChevronDown: Icon,
|
||||
Loader2: Icon,
|
||||
Plus: Icon,
|
||||
RefreshCw: Icon,
|
||||
Search: Icon,
|
||||
Tag: Icon,
|
||||
Zap: Icon,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/api/endpoints/models', () => ({
|
||||
updateModel: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../composables/useUpstreamModelsCache', () => ({
|
||||
useUpstreamModelsCache: () => ({
|
||||
fetchModels: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(updateModel).mockClear()
|
||||
for (const { app, root } of mountedApps.splice(0)) {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
}
|
||||
})
|
||||
|
||||
describe('ModelMappingDialog', () => {
|
||||
it('normalizes and replaces an edited compact operation scope', async () => {
|
||||
const endpoint = {
|
||||
id: 'endpoint-responses',
|
||||
api_format: 'openai:responses',
|
||||
base_url: 'https://api.example.com/v1',
|
||||
is_active: true,
|
||||
} as ProviderEndpoint
|
||||
const model = {
|
||||
id: 'model-sol',
|
||||
provider_model_name: 'gpt-5.6-sol',
|
||||
global_model_display_name: 'GPT-5.6 Sol',
|
||||
provider_model_mappings: [{
|
||||
name: 'gpt-5.6-luna',
|
||||
priority: 1,
|
||||
endpoint_ids: [endpoint.id],
|
||||
operations: ['Compact'],
|
||||
}],
|
||||
} as Model
|
||||
const editingGroup: AliasGroup = {
|
||||
model,
|
||||
apiFormatsKey: '',
|
||||
apiFormats: [],
|
||||
endpointIdsKey: endpoint.id,
|
||||
endpointIds: [endpoint.id],
|
||||
operationsKey: 'Compact',
|
||||
operations: ['Compact'],
|
||||
aliases: model.provider_model_mappings ?? [],
|
||||
}
|
||||
const open = ref(false)
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(ModelMappingDialog, {
|
||||
open: open.value,
|
||||
providerId: 'provider-1',
|
||||
endpoints: [endpoint],
|
||||
models: [model],
|
||||
editingGroup,
|
||||
'onUpdate:open': (value: boolean) => { open.value = value },
|
||||
})
|
||||
},
|
||||
}))
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
|
||||
open.value = true
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
expect(root.textContent).toContain('仅会话压缩')
|
||||
|
||||
const scopeButtons = [...root.querySelectorAll('button')]
|
||||
scopeButtons.find(button => button.textContent?.includes('所有请求'))?.click()
|
||||
await nextTick()
|
||||
scopeButtons.find(button => button.textContent?.includes('仅会话压缩'))?.click()
|
||||
await nextTick()
|
||||
const saveButton = [...root.querySelectorAll('button')]
|
||||
.find(button => button.textContent?.includes('保存映射'))
|
||||
expect(saveButton).toBeDefined()
|
||||
saveButton?.click()
|
||||
await vi.waitFor(() => expect(updateModel).toHaveBeenCalledTimes(1))
|
||||
|
||||
expect(updateModel).toHaveBeenCalledWith('provider-1', 'model-sol', {
|
||||
provider_model_mappings: [{
|
||||
name: 'gpt-5.6-luna',
|
||||
priority: 1,
|
||||
endpoint_ids: [endpoint.id],
|
||||
operations: ['compact'],
|
||||
}],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -38,21 +38,21 @@
|
||||
>
|
||||
<!-- 分组头部(可点击展开) -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
class="flex items-start justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
@click="toggleAliasGroupExpand(getAliasGroupKey(group))"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1.5">
|
||||
<!-- 展开/收起图标 -->
|
||||
<ChevronRight
|
||||
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform"
|
||||
:class="{ 'rotate-90': expandedAliasGroups.has(getAliasGroupKey(group)) }"
|
||||
/>
|
||||
<!-- 模型名称 -->
|
||||
<span class="font-semibold text-sm truncate">
|
||||
<span class="min-w-0 flex-[1_1_12rem] truncate text-sm font-semibold">
|
||||
{{ group.model.global_model_display_name || group.model.provider_model_name }}
|
||||
</span>
|
||||
<!-- 作用域标签 -->
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<div class="flex min-w-0 max-w-full flex-wrap items-center gap-1">
|
||||
<Badge
|
||||
v-if="group.apiFormats.length === 0"
|
||||
variant="outline"
|
||||
@@ -76,11 +76,10 @@
|
||||
{{ getEndpointScopeLabel(group) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="group.operations.length > 0"
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
class="min-w-0 max-w-full text-xs"
|
||||
>
|
||||
{{ getOperationScopeLabel(group) }}
|
||||
<span class="truncate">{{ getOperationScopeLabel(group) }}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<!-- 映射数量 -->
|
||||
@@ -214,10 +213,16 @@ import {
|
||||
type ProviderModelAlias
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { useI18n } from '@/i18n'
|
||||
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'
|
||||
import {
|
||||
formatModelMappingRequestScope,
|
||||
modelMappingOperationsKey,
|
||||
normalizeModelMappingOperations,
|
||||
} from '../../utils/modelMappingScope'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
@@ -228,6 +233,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
@@ -278,21 +284,31 @@ function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
}
|
||||
|
||||
function getOperationsKey(operations: string[] | undefined): string {
|
||||
return getScopeKey(operations)
|
||||
return modelMappingOperationsKey(operations)
|
||||
}
|
||||
|
||||
const requestScopeLabels = computed(() => ({
|
||||
allRequests: t('providers.modelMapping.scope.allRequests'),
|
||||
sessionCompactionOnly: t('providers.modelMapping.scope.sessionCompactionOnly'),
|
||||
customOperations: (operations: string[]) => t(
|
||||
'providers.modelMapping.scope.customOperations',
|
||||
{ operations: operations.join(', ') },
|
||||
),
|
||||
}))
|
||||
|
||||
function getAliasGroupKey(group: AliasGroup): string {
|
||||
return `${group.model.id}-${group.apiFormatsKey}-${group.endpointIdsKey}-${group.operationsKey}`
|
||||
}
|
||||
|
||||
function getEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) {
|
||||
return t('providers.modelMapping.scope.allEndpoints')
|
||||
}
|
||||
return t('providers.modelMapping.scope.endpointCount', { count: group.endpointIds.length })
|
||||
}
|
||||
|
||||
function getOperationScopeLabel(group: AliasGroup): string {
|
||||
if (group.operations.length === 1 && group.operations[0] === 'compact') return '压缩'
|
||||
return `${group.operations.length} 项操作`
|
||||
return formatModelMappingRequestScope(group.operations, requestScopeLabels.value)
|
||||
}
|
||||
|
||||
// 按"模型+作用域"分组的映射列表
|
||||
@@ -317,7 +333,7 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
operationsKey,
|
||||
operations: normalizeStringList(alias.operations),
|
||||
operations: normalizeModelMappingOperations(alias.operations),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
>
|
||||
<!-- 行头部(可点击展开) -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
class="flex items-start justify-between px-4 py-3 hover:bg-muted/20 cursor-pointer"
|
||||
@click="toggleExpand(item.key)"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1.5">
|
||||
<!-- 展开/收起图标 -->
|
||||
<ChevronRight
|
||||
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform self-start mt-0.5"
|
||||
@@ -50,7 +50,7 @@
|
||||
/>
|
||||
<!-- 精确映射 -->
|
||||
<template v-if="item.type === 'exact'">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex min-w-0 flex-[1_1_12rem] flex-col">
|
||||
<span class="font-semibold text-sm truncate">
|
||||
{{ item.targetModelName }}
|
||||
</span>
|
||||
@@ -75,21 +75,23 @@
|
||||
<Badge
|
||||
v-if="item.group"
|
||||
variant="outline"
|
||||
class="text-xs shrink-0"
|
||||
class="min-w-0 max-w-full text-xs"
|
||||
:title="getGroupEndpointScopeTitle(item.group)"
|
||||
>
|
||||
{{ getGroupEndpointScopeLabel(item.group) }}
|
||||
<span class="truncate">{{ getGroupEndpointScopeLabel(item.group) }}</span>
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="item.group && item.group.operations.length > 0"
|
||||
v-if="item.group"
|
||||
variant="outline"
|
||||
class="text-xs shrink-0"
|
||||
class="min-w-0 max-w-full text-xs"
|
||||
:title="getGroupOperationScopeLabel(item.group)"
|
||||
>
|
||||
{{ getGroupOperationScopeLabel(item.group) }}
|
||||
<span class="truncate">{{ getGroupOperationScopeLabel(item.group) }}</span>
|
||||
</Badge>
|
||||
</template>
|
||||
<!-- 正则映射 -->
|
||||
<template v-else>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex min-w-0 flex-[1_1_12rem] flex-col">
|
||||
<span class="font-semibold text-sm truncate">
|
||||
{{ item.targetModelName }}
|
||||
</span>
|
||||
@@ -372,8 +374,15 @@ import {
|
||||
} from '@/api/endpoints'
|
||||
import { type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import {
|
||||
formatModelMappingEndpointLabel,
|
||||
formatModelMappingRequestScope,
|
||||
modelMappingOperationsKey,
|
||||
normalizeModelMappingOperations,
|
||||
} from '../../utils/modelMappingScope'
|
||||
import {
|
||||
buildDefaultModelTestRequestHeaders,
|
||||
buildDefaultModelTestRequestBody,
|
||||
@@ -424,6 +433,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 模型测试 composable
|
||||
const modelTest = useModelTest({ providerId: () => props.provider.id })
|
||||
@@ -501,17 +511,46 @@ function getEndpointIdsKey(endpointIds: string[] | undefined): string {
|
||||
}
|
||||
|
||||
function getOperationsKey(operations: string[] | undefined): string {
|
||||
return getScopeKey(operations)
|
||||
return modelMappingOperationsKey(operations)
|
||||
}
|
||||
|
||||
const requestScopeLabels = computed(() => ({
|
||||
allRequests: t('providers.modelMapping.scope.allRequests'),
|
||||
sessionCompactionOnly: t('providers.modelMapping.scope.sessionCompactionOnly'),
|
||||
customOperations: (operations: string[]) => t(
|
||||
'providers.modelMapping.scope.customOperations',
|
||||
{ operations: operations.join(', ') },
|
||||
),
|
||||
}))
|
||||
|
||||
function getGroupEndpointScopeLabel(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) return '全部端点'
|
||||
return `${group.endpointIds.length} 端点`
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) {
|
||||
return t('providers.modelMapping.scope.allEndpoints')
|
||||
}
|
||||
const labels = getGroupEndpointScopeLabels(group)
|
||||
return labels.length === 1
|
||||
? labels[0]
|
||||
: t('providers.modelMapping.scope.endpointCount', { count: labels.length })
|
||||
}
|
||||
|
||||
function getGroupEndpointScopeTitle(group: AliasGroup): string {
|
||||
if (!group.endpointIds || group.endpointIds.length === 0) {
|
||||
return t('providers.modelMapping.scope.allEndpoints')
|
||||
}
|
||||
return getGroupEndpointScopeLabels(group).join('、')
|
||||
}
|
||||
|
||||
function getGroupEndpointScopeLabels(group: AliasGroup): string[] {
|
||||
const endpoints = props.endpoints ?? []
|
||||
return group.endpointIds.map((endpointId) => {
|
||||
const endpoint = endpoints.find(item => item.id === endpointId)
|
||||
if (!endpoint) return endpointId
|
||||
return formatModelMappingEndpointLabel(endpoint, endpoints)
|
||||
})
|
||||
}
|
||||
|
||||
function getGroupOperationScopeLabel(group: AliasGroup): string {
|
||||
if (group.operations.length === 1 && group.operations[0] === 'compact') return '压缩'
|
||||
return `${group.operations.length} 项操作`
|
||||
return formatModelMappingRequestScope(group.operations, requestScopeLabels.value)
|
||||
}
|
||||
|
||||
// 精确映射分组(来自 provider_model_mappings)
|
||||
@@ -536,7 +575,7 @@ const exactMappingGroups = computed<AliasGroup[]>(() => {
|
||||
endpointIdsKey,
|
||||
endpointIds: normalizeStringList(alias.endpoint_ids),
|
||||
operationsKey,
|
||||
operations: normalizeStringList(alias.operations),
|
||||
operations: normalizeModelMappingOperations(alias.operations),
|
||||
aliases: []
|
||||
}
|
||||
groupMap.set(groupKey, group)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@
|
||||
v-if="getRequestTypeLabel(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-sky-500/30 bg-sky-500/5 px-1.5 text-[10px] leading-4 text-sky-700 dark:text-sky-300 flex-shrink-0"
|
||||
title="线程压缩"
|
||||
title="会话压缩"
|
||||
>
|
||||
{{ getRequestTypeLabel(record) }}
|
||||
</Badge>
|
||||
@@ -765,7 +765,7 @@
|
||||
v-if="getRequestTypeLabel(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-sky-500/30 bg-sky-500/5 px-1.5 text-[10px] leading-4 text-sky-700 dark:text-sky-300 flex-shrink-0"
|
||||
title="线程压缩"
|
||||
title="会话压缩"
|
||||
>
|
||||
{{ getRequestTypeLabel(record) }}
|
||||
</Badge>
|
||||
@@ -797,7 +797,7 @@
|
||||
v-if="getRequestTypeLabel(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full border-sky-500/30 bg-sky-500/5 px-1.5 text-[10px] leading-4 text-sky-700 dark:text-sky-300 flex-shrink-0"
|
||||
title="线程压缩"
|
||||
title="会话压缩"
|
||||
>
|
||||
{{ getRequestTypeLabel(record) }}
|
||||
</Badge>
|
||||
@@ -1630,7 +1630,7 @@ function getReasoningEffort(record: UsageRecord): string | null {
|
||||
}
|
||||
|
||||
function getRequestTypeLabel(record: UsageRecord): string | null {
|
||||
return record.request_type?.trim().toLowerCase() === 'compact' ? '压缩' : null
|
||||
return record.request_type?.trim().toLowerCase() === 'compact' ? '会话压缩' : null
|
||||
}
|
||||
|
||||
function getReasoningEffortTitle(record: UsageRecord): string {
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('UsageRecordsTable', () => {
|
||||
request_type: 'compact',
|
||||
})])
|
||||
|
||||
expect(root.textContent).toContain('压缩')
|
||||
expect(root.textContent).toContain('会话压缩')
|
||||
})
|
||||
|
||||
it('shows fast badge for priority service tier', () => {
|
||||
|
||||
@@ -295,6 +295,16 @@ export const messages = {
|
||||
'nav.cacheMonitoring': '缓存监控',
|
||||
'nav.moduleManagement': '模块管理',
|
||||
'nav.systemSettings': '系统设置',
|
||||
'providers.modelMapping.scope.allRequests': '所有请求',
|
||||
'providers.modelMapping.scope.sessionCompactionOnly': '仅会话压缩',
|
||||
'providers.modelMapping.scope.customOperations': '仅匹配:{operations}',
|
||||
'providers.modelMapping.scope.allRequestsDescription': '普通请求和会话压缩都可使用此映射。',
|
||||
'providers.modelMapping.scope.sessionCompactionDescription': '只在会话压缩时使用此映射,包括 Responses 输入中的压缩触发和 Responses Compact 端点。',
|
||||
'providers.modelMapping.scope.customOperationsDescription': '只匹配请求操作:{operations}。',
|
||||
'providers.modelMapping.scope.allEndpoints': '全部端点',
|
||||
'providers.modelMapping.scope.endpointCount': '{count} 个端点',
|
||||
'providers.modelMapping.scope.endpointHelp': '留空时不限制端点;选择后仅匹配指定端点。',
|
||||
'providers.modelMapping.scope.matchHelp': '端点范围和请求范围必须同时匹配。',
|
||||
'breadcrumb.personalSettings': '个人设置',
|
||||
'breadcrumb.routingCreate': '新建调度策略',
|
||||
'breadcrumb.routingConfig': '调度策略配置',
|
||||
@@ -595,6 +605,16 @@ export const messages = {
|
||||
'nav.cacheMonitoring': 'Cache monitoring',
|
||||
'nav.moduleManagement': 'Modules',
|
||||
'nav.systemSettings': 'System settings',
|
||||
'providers.modelMapping.scope.allRequests': 'All requests',
|
||||
'providers.modelMapping.scope.sessionCompactionOnly': 'Session compaction only',
|
||||
'providers.modelMapping.scope.customOperations': 'Match only: {operations}',
|
||||
'providers.modelMapping.scope.allRequestsDescription': 'Regular requests and session compaction can both use this mapping.',
|
||||
'providers.modelMapping.scope.sessionCompactionDescription': 'Use this mapping only for session compaction, including compaction triggers in Responses input and the Responses Compact endpoint.',
|
||||
'providers.modelMapping.scope.customOperationsDescription': 'Match only these request operations: {operations}.',
|
||||
'providers.modelMapping.scope.allEndpoints': 'All endpoints',
|
||||
'providers.modelMapping.scope.endpointCount': '{count} endpoints',
|
||||
'providers.modelMapping.scope.endpointHelp': 'Leave empty for no endpoint restriction. Selections match only those endpoints.',
|
||||
'providers.modelMapping.scope.matchHelp': 'Both the endpoint scope and request scope must match.',
|
||||
'breadcrumb.personalSettings': 'Profile settings',
|
||||
'breadcrumb.routingCreate': 'Create routing profile',
|
||||
'breadcrumb.routingConfig': 'Routing profile',
|
||||
@@ -673,6 +693,15 @@ const legacyExactEnglishMessages: Record<string, string> = {
|
||||
'全部分组': 'All groups',
|
||||
'全部归属': 'All ownership',
|
||||
'全部方式': 'All methods',
|
||||
'适用范围': 'Applies to',
|
||||
'适用端点': 'Applicable endpoints',
|
||||
'适用请求': 'Applicable requests',
|
||||
'全部端点': 'All endpoints',
|
||||
'所有请求': 'All requests',
|
||||
'仅会话压缩': 'Session compaction only',
|
||||
'会话压缩': 'Session compaction',
|
||||
'保存映射': 'Save mapping',
|
||||
'添加映射': 'Add mapping',
|
||||
'未设置': 'Not set',
|
||||
'已设置': 'Configured',
|
||||
'默认': 'Default',
|
||||
|
||||
Reference in New Issue
Block a user