fix(openai): clarify endpoint capability UI
This commit is contained in:
parent
ed1b57c597
commit
37044b83eb
@ -2692,10 +2692,18 @@
|
||||
<Select
|
||||
v-model="openAIResponsesMode"
|
||||
:options="openAIResponsesModeOptions"
|
||||
:disabled="!openAITextGenerationCapabilityEnabled"
|
||||
data-testid="openai-responses-mode-select"
|
||||
/>
|
||||
</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>
|
||||
<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">
|
||||
@ -3434,10 +3442,22 @@ const openAIResponsesModeOptions = computed(() => [
|
||||
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
||||
{ 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 }[]>(() => [
|
||||
{ value: 'chat_completions', label: t('admin.accounts.openai.capabilityChatCompletions') },
|
||||
{ value: 'chat_completions', label: openAITextEndpointCapabilityLabel.value },
|
||||
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
||||
])
|
||||
const openAITextGenerationCapabilityEnabled = computed(() =>
|
||||
openAIEndpointCapabilities.value.includes('chat_completions')
|
||||
)
|
||||
|
||||
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
||||
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
||||
@ -3455,6 +3475,9 @@ const toggleOpenAIEndpointCapability = (capability: OpenAIEndpointCapability, ev
|
||||
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
||||
(value) => value !== capability
|
||||
)
|
||||
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||
openAIResponsesMode.value = 'auto'
|
||||
}
|
||||
return
|
||||
}
|
||||
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
||||
@ -4268,7 +4291,11 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
||||
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
|
||||
} else {
|
||||
delete extra.openai_responses_mode
|
||||
|
||||
@ -1452,13 +1452,24 @@
|
||||
<Select
|
||||
v-model="openAIResponsesMode"
|
||||
:options="openAIResponsesModeOptions"
|
||||
:disabled="!openAITextGenerationCapabilityEnabled"
|
||||
data-testid="openai-responses-mode-select"
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
<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">
|
||||
@ -2568,10 +2579,29 @@ const openAIResponsesModeOptions = computed(() => [
|
||||
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
||||
{ 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 }[]>(() => [
|
||||
{ value: 'chat_completions', label: t('admin.accounts.openai.capabilityChatCompletions') },
|
||||
{ value: 'chat_completions', label: openAITextEndpointCapabilityLabel.value },
|
||||
{ value: 'embeddings', label: t('admin.accounts.openai.capabilityEmbeddings') }
|
||||
])
|
||||
const openAITextGenerationCapabilityEnabled = computed(() =>
|
||||
openAIEndpointCapabilities.value.includes('chat_completions')
|
||||
)
|
||||
|
||||
const normalizeOpenAIEndpointCapabilities = (values: OpenAIEndpointCapability[]) => {
|
||||
const allowed: OpenAIEndpointCapability[] = ['chat_completions', 'embeddings']
|
||||
@ -2609,6 +2639,9 @@ const toggleOpenAIEndpointCapability = (capability: OpenAIEndpointCapability, ev
|
||||
openAIEndpointCapabilities.value = openAIEndpointCapabilities.value.filter(
|
||||
(value) => value !== capability
|
||||
)
|
||||
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||
openAIResponsesMode.value = 'auto'
|
||||
}
|
||||
return
|
||||
}
|
||||
openAIEndpointCapabilities.value = normalizeOpenAIEndpointCapabilities([
|
||||
@ -2826,6 +2859,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
openAIEndpointCapabilities.value = readOpenAIEndpointCapabilities(
|
||||
newAccount.credentials as Record<string, unknown> | undefined
|
||||
)
|
||||
if (!openAITextGenerationCapabilityEnabled.value) {
|
||||
openAIResponsesMode.value = 'auto'
|
||||
}
|
||||
}
|
||||
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
|
||||
? extra.codex_image_generation_bridge
|
||||
@ -3945,7 +3981,7 @@ const handleSubmit = async () => {
|
||||
newExtra.openai_compact_mode = openAICompactMode.value
|
||||
}
|
||||
if (props.account.type === 'apikey') {
|
||||
if (openAIResponsesMode.value === 'auto') {
|
||||
if (!openAITextGenerationCapabilityEnabled.value || openAIResponsesMode.value === 'auto') {
|
||||
delete newExtra.openai_responses_mode
|
||||
} else {
|
||||
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 () => {
|
||||
const account = buildAccount()
|
||||
account.extra = {
|
||||
|
||||
@ -3349,14 +3349,20 @@ export default {
|
||||
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
|
||||
responsesMode: 'Responses API support',
|
||||
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',
|
||||
responsesModeForceResponses: 'Force Responses',
|
||||
responsesModeForceChatCompletions: 'Force Chat Completions',
|
||||
responsesModeTextDisabledHint:
|
||||
'Not applicable when the Responses / Chat Completions endpoint is not enabled.',
|
||||
endpointCapabilities: 'Endpoint capabilities',
|
||||
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',
|
||||
capabilityChatCompletionsAuto: 'Chat Completions (auto probe)',
|
||||
capabilityEmbeddings: 'Embeddings',
|
||||
responsesStatusAutoSupported: 'Auto probe: Responses',
|
||||
responsesStatusAutoUnsupported: 'Auto probe: Chat Completions',
|
||||
|
||||
@ -3495,14 +3495,19 @@ export default {
|
||||
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
|
||||
responsesMode: 'Responses API 支持',
|
||||
responsesModeDesc:
|
||||
'仅对 OpenAI API Key 生效。自动跟随探测结果,强制模式会覆盖自动探测。',
|
||||
'仅对 OpenAI API Key 的文本转发链路生效。自动跟随探测结果,强制模式会覆盖自动探测。',
|
||||
responsesModeAuto: '自动',
|
||||
responsesModeForceResponses: '强制 Responses',
|
||||
responsesModeForceChatCompletions: '强制 Chat Completions',
|
||||
responsesModeTextDisabledHint: '未启用 Responses / Chat Completions 端点时,此设置不适用。',
|
||||
endpointCapabilities: '端点能力',
|
||||
endpointCapabilitiesDesc:
|
||||
'用于调度筛选。默认两个端点都可用;如果上游只支持其中一个,请只勾选实际支持的端点。',
|
||||
'用于调度筛选。文本端点会跟随上方 Responses API 支持显示为 Responses、Chat Completions 或自动模式;Embeddings 独立控制 /v1/embeddings。',
|
||||
capabilityResponses: 'Responses',
|
||||
capabilityTextAuto: 'Responses / Chat Completions(自动)',
|
||||
capabilityResponsesAuto: 'Responses(自动探测)',
|
||||
capabilityChatCompletions: 'Chat Completions',
|
||||
capabilityChatCompletionsAuto: 'Chat Completions(自动探测)',
|
||||
capabilityEmbeddings: 'Embeddings',
|
||||
responsesStatusAutoSupported: '自动探测:Responses',
|
||||
responsesStatusAutoUnsupported: '自动探测:Chat Completions',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user