fix(openai): clarify endpoint capability UI
This commit is contained in:
parent
ed1b57c597
commit
37044b83eb
@ -2692,10 +2692,18 @@
|
|||||||
<Select
|
<Select
|
||||||
v-model="openAIResponsesMode"
|
v-model="openAIResponsesMode"
|
||||||
:options="openAIResponsesModeOptions"
|
:options="openAIResponsesModeOptions"
|
||||||
|
:disabled="!openAITextGenerationCapabilityEnabled"
|
||||||
data-testid="openai-responses-mode-select"
|
data-testid="openai-responses-mode-select"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="!openAITextGenerationCapabilityEnabled"
|
||||||
|
class="rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
|
||||||
|
data-testid="openai-responses-mode-not-applicable"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.openai.responsesModeTextDisabledHint') }}
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-2 block">{{ t('admin.accounts.openai.endpointCapabilities') }}</label>
|
<label class="input-label mb-2 block">{{ t('admin.accounts.openai.endpointCapabilities') }}</label>
|
||||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
@ -3434,10 +3442,22 @@ const openAIResponsesModeOptions = computed(() => [
|
|||||||
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
||||||
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
|
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
|
||||||
])
|
])
|
||||||
|
const openAITextEndpointCapabilityLabel = computed(() => {
|
||||||
|
if (openAIResponsesMode.value === 'force_responses') {
|
||||||
|
return t('admin.accounts.openai.capabilityResponses')
|
||||||
|
}
|
||||||
|
if (openAIResponsesMode.value === 'force_chat_completions') {
|
||||||
|
return t('admin.accounts.openai.capabilityChatCompletions')
|
||||||
|
}
|
||||||
|
return t('admin.accounts.openai.capabilityTextAuto')
|
||||||
|
})
|
||||||
const openAIEndpointCapabilityOptions = computed<{ value: OpenAIEndpointCapability; label: string }[]>(() => [
|
const openAIEndpointCapabilityOptions = computed<{ value: OpenAIEndpointCapability; label: string }[]>(() => [
|
||||||
{ value: 'chat_completions', label: t('admin.accounts.openai.capabilityChatCompletions') },
|
{ value: 'chat_completions', label: openAITextEndpointCapabilityLabel.value },
|
||||||
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
||||||
])
|
])
|
||||||
|
const openAITextGenerationCapabilityEnabled = computed(() =>
|
||||||
|
openAIEndpointCapabilities.value.includes('chat_completions')
|
||||||
|
)
|
||||||
|
|
||||||
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
||||||
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
||||||
@ -3455,6 +3475,9 @@ const toggleOpenAIEndpointCapability = (capability: OpenAIEndpointCapability, ev
|
|||||||
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
||||||
(value) => value !== capability
|
(value) => value !== capability
|
||||||
)
|
)
|
||||||
|
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||||
|
openAIResponsesMode.value = 'auto'
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
||||||
@ -4268,7 +4291,11 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
|||||||
delete extra.openai_compact_mode
|
delete extra.openai_compact_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountCategory.value === 'apikey' && openAIResponsesMode.value !== 'auto') {
|
if (
|
||||||
|
accountCategory.value === 'apikey' &&
|
||||||
|
openAITextGenerationCapabilityEnabled.value &&
|
||||||
|
openAIResponsesMode.value !== 'auto'
|
||||||
|
) {
|
||||||
extra.openai_responses_mode = openAIResponsesMode.value
|
extra.openai_responses_mode = openAIResponsesMode.value
|
||||||
} else {
|
} else {
|
||||||
delete extra.openai_responses_mode
|
delete extra.openai_responses_mode
|
||||||
|
|||||||
@ -1452,13 +1452,24 @@
|
|||||||
<Select
|
<Select
|
||||||
v-model="openAIResponsesMode"
|
v-model="openAIResponsesMode"
|
||||||
:options="openAIResponsesModeOptions"
|
:options="openAIResponsesModeOptions"
|
||||||
|
:disabled="!openAITextGenerationCapabilityEnabled"
|
||||||
data-testid="openai-responses-mode-select"
|
data-testid="openai-responses-mode-select"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300">
|
<div
|
||||||
|
v-if="openAITextGenerationCapabilityEnabled"
|
||||||
|
class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
<span class="font-medium">{{ t(openAIResponsesStatusKey) }}</span>
|
<span class="font-medium">{{ t(openAIResponsesStatusKey) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
|
||||||
|
data-testid="openai-responses-mode-not-applicable"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.openai.responsesModeTextDisabledHint') }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-2 block">{{ t('admin.accounts.openai.endpointCapabilities') }}</label>
|
<label class="input-label mb-2 block">{{ t('admin.accounts.openai.endpointCapabilities') }}</label>
|
||||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
@ -2568,10 +2579,29 @@ const openAIResponsesModeOptions = computed(() => [
|
|||||||
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
||||||
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
|
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
|
||||||
])
|
])
|
||||||
|
const openAITextEndpointCapabilityLabel = computed(() => {
|
||||||
|
if (openAIResponsesMode.value === 'force_responses') {
|
||||||
|
return t('admin.accounts.openai.capabilityResponses')
|
||||||
|
}
|
||||||
|
if (openAIResponsesMode.value === 'force_chat_completions') {
|
||||||
|
return t('admin.accounts.openai.capabilityChatCompletions')
|
||||||
|
}
|
||||||
|
const extra = props.account?.extra as Record<string, unknown> | undefined
|
||||||
|
if (extra?.openai_responses_supported === true) {
|
||||||
|
return t('admin.accounts.openai.capabilityResponsesAuto')
|
||||||
|
}
|
||||||
|
if (extra?.openai_responses_supported === false) {
|
||||||
|
return t('admin.accounts.openai.capabilityChatCompletionsAuto')
|
||||||
|
}
|
||||||
|
return t('admin.accounts.openai.capabilityTextAuto')
|
||||||
|
})
|
||||||
const openAIEndpointCapabilityOptions = computed<{ value: OpenAIEndpointCapability; label: string }[]>(() => [
|
const openAIEndpointCapabilityOptions = computed<{ value: OpenAIEndpointCapability; label: string }[]>(() => [
|
||||||
{ value: 'chat_completions', label: t('admin.accounts.openai.capabilityChatCompletions') },
|
{ value: 'chat_completions', label: openAITextEndpointCapabilityLabel.value },
|
||||||
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
||||||
])
|
])
|
||||||
|
const openAITextGenerationCapabilityEnabled = computed(() =>
|
||||||
|
openAIEndpointCapabilities.value.includes('chat_completions')
|
||||||
|
)
|
||||||
|
|
||||||
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
||||||
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
||||||
@ -2609,6 +2639,9 @@ const toggleOpenAIEndpointCapability = (capability: OpenAIEndpointCapability, ev
|
|||||||
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
||||||
(value) => value !== capability
|
(value) => value !== capability
|
||||||
)
|
)
|
||||||
|
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||||
|
openAIResponsesMode.value = 'auto'
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
||||||
@ -2826,6 +2859,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
openAIEndpointCapabilities.value = readOpenAIEndpointCapabilities(
|
openAIEndpointCapabilities.value = readOpenAIEndpointCapabilities(
|
||||||
newAccount.credentials as Record<string, unknown> | undefined
|
newAccount.credentials as Record<string, unknown> | undefined
|
||||||
)
|
)
|
||||||
|
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||||
|
openAIResponsesMode.value = 'auto'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
|
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
|
||||||
? extra.codex_image_generation_bridge
|
? extra.codex_image_generation_bridge
|
||||||
@ -3945,7 +3981,7 @@ const handleSubmit = async () => {
|
|||||||
newExtra.openai_compact_mode = openAICompactMode.value
|
newExtra.openai_compact_mode = openAICompactMode.value
|
||||||
}
|
}
|
||||||
if (props.account.type === 'apikey') {
|
if (props.account.type === 'apikey') {
|
||||||
if (openAIResponsesMode.value === 'auto') {
|
if (!openAITextGenerationCapabilityEnabled.value || openAIResponsesMode.value === 'auto') {
|
||||||
delete newExtra.openai_responses_mode
|
delete newExtra.openai_responses_mode
|
||||||
} else {
|
} else {
|
||||||
newExtra.openai_responses_mode = openAIResponsesMode.value
|
newExtra.openai_responses_mode = openAIResponsesMode.value
|
||||||
|
|||||||
@ -367,6 +367,37 @@ describe('EditAccountModal', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('disables text generation protocol when only embeddings requests are accepted', async () => {
|
||||||
|
const account = buildAccount()
|
||||||
|
account.credentials.openai_capabilities = ['embeddings']
|
||||||
|
account.extra = {
|
||||||
|
openai_responses_mode: 'force_responses',
|
||||||
|
openai_responses_supported: true
|
||||||
|
}
|
||||||
|
updateAccountMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||||
|
updateAccountMock.mockResolvedValue(account)
|
||||||
|
|
||||||
|
const wrapper = mountModal(account)
|
||||||
|
|
||||||
|
const responsesModeSelect = wrapper.get<HTMLSelectElement>(
|
||||||
|
'[data-testid="openai-responses-mode-select"]'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(responsesModeSelect.element.disabled).toBe(true)
|
||||||
|
expect(wrapper.find('[data-testid="openai-responses-mode-not-applicable"]').exists()).toBe(true)
|
||||||
|
|
||||||
|
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||||
|
|
||||||
|
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.openai_capabilities).toEqual([
|
||||||
|
'embeddings'
|
||||||
|
])
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('openai_responses_mode')
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_supported).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('submits account-level Codex image generation bridge override', async () => {
|
it('submits account-level Codex image generation bridge override', async () => {
|
||||||
const account = buildAccount()
|
const account = buildAccount()
|
||||||
account.extra = {
|
account.extra = {
|
||||||
|
|||||||
@ -3349,14 +3349,20 @@ export default {
|
|||||||
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
|
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
|
||||||
responsesMode: 'Responses API support',
|
responsesMode: 'Responses API support',
|
||||||
responsesModeDesc:
|
responsesModeDesc:
|
||||||
'Only applies to OpenAI API Key accounts. Auto follows probe results; force modes override probing.',
|
'Only applies to the OpenAI API Key text forwarding path. Auto follows probe results; force modes override probing.',
|
||||||
responsesModeAuto: 'Auto',
|
responsesModeAuto: 'Auto',
|
||||||
responsesModeForceResponses: 'Force Responses',
|
responsesModeForceResponses: 'Force Responses',
|
||||||
responsesModeForceChatCompletions: 'Force Chat Completions',
|
responsesModeForceChatCompletions: 'Force Chat Completions',
|
||||||
|
responsesModeTextDisabledHint:
|
||||||
|
'Not applicable when the Responses / Chat Completions endpoint is not enabled.',
|
||||||
endpointCapabilities: 'Endpoint capabilities',
|
endpointCapabilities: 'Endpoint capabilities',
|
||||||
endpointCapabilitiesDesc:
|
endpointCapabilitiesDesc:
|
||||||
'Used by account routing. Both endpoints are allowed by default; if the upstream only supports one, select only the supported endpoint.',
|
'Used by account routing. The text endpoint follows the Responses API support setting above and is shown as Responses, Chat Completions, or auto mode; Embeddings independently controls /v1/embeddings.',
|
||||||
|
capabilityResponses: 'Responses',
|
||||||
|
capabilityTextAuto: 'Responses / Chat Completions (Auto)',
|
||||||
|
capabilityResponsesAuto: 'Responses (auto probe)',
|
||||||
capabilityChatCompletions: 'Chat Completions',
|
capabilityChatCompletions: 'Chat Completions',
|
||||||
|
capabilityChatCompletionsAuto: 'Chat Completions (auto probe)',
|
||||||
capabilityEmbeddings: 'Embeddings',
|
capabilityEmbeddings: 'Embeddings',
|
||||||
responsesStatusAutoSupported: 'Auto probe: Responses',
|
responsesStatusAutoSupported: 'Auto probe: Responses',
|
||||||
responsesStatusAutoUnsupported: 'Auto probe: Chat Completions',
|
responsesStatusAutoUnsupported: 'Auto probe: Chat Completions',
|
||||||
|
|||||||
@ -3495,14 +3495,19 @@ export default {
|
|||||||
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
|
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
|
||||||
responsesMode: 'Responses API 支持',
|
responsesMode: 'Responses API 支持',
|
||||||
responsesModeDesc:
|
responsesModeDesc:
|
||||||
'仅对 OpenAI API Key 生效。自动跟随探测结果,强制模式会覆盖自动探测。',
|
'仅对 OpenAI API Key 的文本转发链路生效。自动跟随探测结果,强制模式会覆盖自动探测。',
|
||||||
responsesModeAuto: '自动',
|
responsesModeAuto: '自动',
|
||||||
responsesModeForceResponses: '强制 Responses',
|
responsesModeForceResponses: '强制 Responses',
|
||||||
responsesModeForceChatCompletions: '强制 Chat Completions',
|
responsesModeForceChatCompletions: '强制 Chat Completions',
|
||||||
|
responsesModeTextDisabledHint: '未启用 Responses / Chat Completions 端点时,此设置不适用。',
|
||||||
endpointCapabilities: '端点能力',
|
endpointCapabilities: '端点能力',
|
||||||
endpointCapabilitiesDesc:
|
endpointCapabilitiesDesc:
|
||||||
'用于调度筛选。默认两个端点都可用;如果上游只支持其中一个,请只勾选实际支持的端点。',
|
'用于调度筛选。文本端点会跟随上方 Responses API 支持显示为 Responses、Chat Completions 或自动模式;Embeddings 独立控制 /v1/embeddings。',
|
||||||
|
capabilityResponses: 'Responses',
|
||||||
|
capabilityTextAuto: 'Responses / Chat Completions(自动)',
|
||||||
|
capabilityResponsesAuto: 'Responses(自动探测)',
|
||||||
capabilityChatCompletions: 'Chat Completions',
|
capabilityChatCompletions: 'Chat Completions',
|
||||||
|
capabilityChatCompletionsAuto: 'Chat Completions(自动探测)',
|
||||||
capabilityEmbeddings: 'Embeddings',
|
capabilityEmbeddings: 'Embeddings',
|
||||||
responsesStatusAutoSupported: '自动探测:Responses',
|
responsesStatusAutoSupported: '自动探测:Responses',
|
||||||
responsesStatusAutoUnsupported: '自动探测:Chat Completions',
|
responsesStatusAutoUnsupported: '自动探测:Chat Completions',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user