Fix LLM provider fields and OpenAI-compatible protocol detection

This commit is contained in:
jxxghp
2026-07-30 14:41:26 +08:00
parent 6104fb861e
commit 715da1929b
7 changed files with 116 additions and 17 deletions
@@ -0,0 +1,77 @@
import { useLlmProviderDirectory } from '@/composables/useLlmProviderDirectory'
import { mount } from '@vue/test-utils'
import { defineComponent, nextTick, ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
apiGet: vi.fn(),
}))
vi.mock('@/api', () => ({
default: {
get: (...args: unknown[]) => mocks.apiGet(...args),
},
}))
function createProvider(id: string, runtime: string) {
return {
id,
name: id,
runtime,
default_base_url: '',
base_url_presets: [],
base_url_editable: true,
requires_base_url: false,
supports_api_key: true,
api_key_label: 'API Key',
api_key_hint: '',
supports_model_refresh: true,
oauth_methods: [],
auth_status: { connected: false },
}
}
describe('useLlmProviderDirectory', () => {
beforeEach(() => {
mocks.apiGet.mockReset()
})
it('仅为 OpenAI 兼容 runtime 显示 API 协议字段', async () => {
mocks.apiGet.mockResolvedValue({
success: true,
data: [createProvider('openai', 'openai_compatible'), createProvider('deepseek', 'deepseek')],
})
const Harness = defineComponent({
setup() {
const provider = ref('openai')
const directory = useLlmProviderDirectory({
provider,
apiKey: ref(''),
baseUrl: ref(''),
model: ref(''),
})
return {
loadProviders: directory.loadProviders,
selectProvider: (value: string) => {
provider.value = value
},
showApiProtocolField: directory.showApiProtocolField,
}
},
template: '<div />',
})
const wrapper = mount(Harness)
await wrapper.vm.loadProviders()
expect(wrapper.vm.showApiProtocolField).toBe(true)
wrapper.vm.selectProvider('deepseek')
await nextTick()
expect(wrapper.vm.showApiProtocolField).toBe(false)
wrapper.unmount()
})
})
+1 -1
View File
@@ -128,7 +128,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
) )
const showApiKeyField = computed(() => selectedProvider.value?.supports_api_key !== false) const showApiKeyField = computed(() => selectedProvider.value?.supports_api_key !== false)
// OpenAI 兼容接口才需要选择 API 协议(Chat Completions / Responses)。 // OpenAI 兼容接口才需要选择 API 协议(Chat Completions / Responses)。
const showApiProtocolField = computed(() => selectedProvider.value?.runtime === 'openai') const showApiProtocolField = computed(() => selectedProvider.value?.runtime === 'openai_compatible')
const hasUsableCredential = computed(() => { const hasUsableCredential = computed(() => {
if (providerConnected.value) return true if (providerConnected.value) return true
return Boolean(normalizeValue(options.apiKey.value)) return Boolean(normalizeValue(options.apiKey.value))
+3
View File
@@ -1766,6 +1766,7 @@ export default {
llmProviderHint: 'Select the LLM service provider to use', llmProviderHint: 'Select the LLM service provider to use',
llmModel: 'LLM Model Name', llmModel: 'LLM Model Name',
llmModelHint: 'Specify the LLM model to use, such as deepseek-v4-flash, gpt-5.4, etc.', llmModelHint: 'Specify the LLM model to use, such as deepseek-v4-flash, gpt-5.4, etc.',
llmModelPlaceholder: 'Enter or select an LLM model name',
llmModelResolvedHint: 'Max context has been auto-filled to {context}K from the model catalog. Source: {source}', llmModelResolvedHint: 'Max context has been auto-filled to {context}K from the model catalog. Source: {source}',
llmThinking: 'Thinking Mode / Depth', llmThinking: 'Thinking Mode / Depth',
llmThinkingHint: llmThinkingHint:
@@ -1794,11 +1795,13 @@ export default {
llmApiKeyPlaceholder: 'Please enter API key', llmApiKeyPlaceholder: 'Please enter API key',
llmBaseUrl: 'LLM Base URL', llmBaseUrl: 'LLM Base URL',
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints', llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
llmBaseUrlPlaceholder: 'Enter the LLM API base URL',
llmUseProxy: 'Use System Proxy', llmUseProxy: 'Use System Proxy',
llmUseProxyHint: llmUseProxyHint:
'When enabled, Agent connections to the current LLM provider use the system proxy from advanced settings.', 'When enabled, Agent connections to the current LLM provider use the system proxy from advanced settings.',
llmUserAgent: 'User-Agent', llmUserAgent: 'User-Agent',
llmUserAgentHint: 'User-Agent sent to OpenAI-compatible APIs. Leave empty to use the SDK default.', llmUserAgentHint: 'User-Agent sent to OpenAI-compatible APIs. Leave empty to use the SDK default.',
llmUserAgentPlaceholder: 'Leave empty to use the SDK default User-Agent',
llmApiProtocol: 'API Protocol', llmApiProtocol: 'API Protocol',
llmApiProtocolHint: llmApiProtocolHint:
'Request protocol for OpenAI-compatible APIs: auto-select by model capability, or force Chat Completions / Responses.', 'Request protocol for OpenAI-compatible APIs: auto-select by model capability, or force Chat Completions / Responses.',
+3
View File
@@ -1753,6 +1753,7 @@ export default {
llmProviderHint: '选择使用的LLM服务提供商', llmProviderHint: '选择使用的LLM服务提供商',
llmModel: 'LLM模型名称', llmModel: 'LLM模型名称',
llmModelHint: '指定使用的LLM模型,如deepseek-v4-flash、gpt-5.4等', llmModelHint: '指定使用的LLM模型,如deepseek-v4-flash、gpt-5.4等',
llmModelPlaceholder: '请输入或选择LLM模型名称',
llmModelResolvedHint: '已根据模型目录自动回填最大上下文为 {context}K,来源:{source}', llmModelResolvedHint: '已根据模型目录自动回填最大上下文为 {context}K,来源:{source}',
llmThinking: '思考模式 / 深度', llmThinking: '思考模式 / 深度',
llmThinkingHint: llmThinkingHint:
@@ -1780,10 +1781,12 @@ export default {
llmApiKeyPlaceholder: '请输入API密钥', llmApiKeyPlaceholder: '请输入API密钥',
llmBaseUrl: 'LLM基础URL', llmBaseUrl: 'LLM基础URL',
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点', llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
llmBaseUrlPlaceholder: '请输入LLM API基础URL',
llmUseProxy: '使用系统代理', llmUseProxy: '使用系统代理',
llmUseProxyHint: '启用后,Agent 连接当前 LLM 提供商时会应用高级设置中的系统代理', llmUseProxyHint: '启用后,Agent 连接当前 LLM 提供商时会应用高级设置中的系统代理',
llmUserAgent: 'User-Agent', llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口请求使用的 User-Agent,留空则使用 SDK 默认值', llmUserAgentHint: 'OpenAI 兼容接口请求使用的 User-Agent,留空则使用 SDK 默认值',
llmUserAgentPlaceholder: '留空则使用SDK默认User-Agent',
llmApiProtocol: 'API 协议', llmApiProtocol: 'API 协议',
llmApiProtocolHint: 'OpenAI 兼容接口的请求协议:自动按模型能力选择,或强制使用 Chat Completions / Responses', llmApiProtocolHint: 'OpenAI 兼容接口的请求协议:自动按模型能力选择,或强制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自动 (auto)', llmApiProtocolAuto: '自动 (auto)',
+3
View File
@@ -1752,6 +1752,7 @@ export default {
llmProviderHint: '選擇使用的LLM服務提供商', llmProviderHint: '選擇使用的LLM服務提供商',
llmModel: 'LLM模型名稱', llmModel: 'LLM模型名稱',
llmModelHint: '指定使用的LLM模型,如deepseek-v4-flash、gpt-5.4等', llmModelHint: '指定使用的LLM模型,如deepseek-v4-flash、gpt-5.4等',
llmModelPlaceholder: '請輸入或選擇LLM模型名稱',
llmModelResolvedHint: '已根據模型目錄自動回填最大上下文為 {context}K,來源:{source}', llmModelResolvedHint: '已根據模型目錄自動回填最大上下文為 {context}K,來源:{source}',
llmThinking: '思考模式 / 深度', llmThinking: '思考模式 / 深度',
llmThinkingHint: llmThinkingHint:
@@ -1779,10 +1780,12 @@ export default {
llmApiKeyPlaceholder: '請輸入API密鑰', llmApiKeyPlaceholder: '請輸入API密鑰',
llmBaseUrl: 'LLM基礎URL', llmBaseUrl: 'LLM基礎URL',
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點', llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
llmBaseUrlPlaceholder: '請輸入LLM API基礎URL',
llmUseProxy: '使用系統代理', llmUseProxy: '使用系統代理',
llmUseProxyHint: '啟用後,Agent 連接目前 LLM 提供商時會套用進階設定中的系統代理', llmUseProxyHint: '啟用後,Agent 連接目前 LLM 提供商時會套用進階設定中的系統代理',
llmUserAgent: 'User-Agent', llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口請求使用的 User-Agent,留空則使用 SDK 預設值', llmUserAgentHint: 'OpenAI 兼容接口請求使用的 User-Agent,留空則使用 SDK 預設值',
llmUserAgentPlaceholder: '留空則使用SDK預設User-Agent',
llmApiProtocol: 'API 協議', llmApiProtocol: 'API 協議',
llmApiProtocolHint: 'OpenAI 兼容接口的請求協議:自動按模型能力選擇,或強制使用 Chat Completions / Responses', llmApiProtocolHint: 'OpenAI 兼容接口的請求協議:自動按模型能力選擇,或強制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自動 (auto)', llmApiProtocolAuto: '自動 (auto)',
+20 -13
View File
@@ -1243,7 +1243,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VCombobox <VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL" :model-value="SystemSettings.Basic.LLM_BASE_URL || null"
@update:model-value=" @update:model-value="
(value: any) => { (value: any) => {
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
@@ -1255,11 +1255,14 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
" "
:label="t('setting.system.llmBaseUrl')" :label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')" :hint="t('setting.system.llmBaseUrlHint')"
:placeholder="selectedLlmProvider?.default_base_url || 'https://api.deepseek.com'" :placeholder="
selectedLlmProvider?.default_base_url || t('setting.system.llmBaseUrlPlaceholder')
"
:items="llmBaseUrlPresetItems" :items="llmBaseUrlPresetItems"
item-title="title" item-title="title"
item-value="value" item-value="value"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-link" prepend-inner-icon="mdi-link"
> >
<template #item="{ props, item }"> <template #item="{ props, item }">
@@ -1267,14 +1270,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</template> </template>
</VCombobox> </VCombobox>
</VCol> </VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12">
<VSwitch
v-model="SystemSettings.Basic.LLM_USE_PROXY"
:label="t('setting.system.llmUseProxy')"
:hint="t('setting.system.llmUseProxyHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6">
<VTextField <VTextField
v-model="SystemSettings.Basic.LLM_API_KEY" v-model="SystemSettings.Basic.LLM_API_KEY"
@@ -1282,6 +1277,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
:hint="selectedLlmProvider?.api_key_hint || t('setting.system.llmApiKeyHint')" :hint="selectedLlmProvider?.api_key_hint || t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')" :placeholder="t('setting.system.llmApiKeyPlaceholder')"
persistent-hint persistent-hint
persistent-placeholder
type="password" type="password"
prepend-inner-icon="mdi-key-variant" prepend-inner-icon="mdi-key-variant"
/> />
@@ -1331,7 +1327,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<div> <div>
<VCombobox <VCombobox
:model-value="SystemSettings.Basic.LLM_MODEL" :model-value="SystemSettings.Basic.LLM_MODEL || null"
@update:model-value=" @update:model-value="
(val: any) => { (val: any) => {
SystemSettings.Basic.LLM_MODEL = typeof val === 'object' && val !== null ? val.id : val SystemSettings.Basic.LLM_MODEL = typeof val === 'object' && val !== null ? val.id : val
@@ -1340,8 +1336,9 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
" "
:label="t('setting.system.llmModel')" :label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')" :hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelHint')" :placeholder="t('setting.system.llmModelPlaceholder')"
persistent-hint persistent-hint
persistent-placeholder
:items="llmModels" :items="llmModels"
item-title="name" item-title="name"
item-value="id" item-value="id"
@@ -1407,7 +1404,9 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
v-model="SystemSettings.Basic.LLM_USER_AGENT" v-model="SystemSettings.Basic.LLM_USER_AGENT"
:label="t('setting.system.llmUserAgent')" :label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')" :hint="t('setting.system.llmUserAgentHint')"
:placeholder="t('setting.system.llmUserAgentPlaceholder')"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-card-account-details-outline" prepend-inner-icon="mdi-card-account-details-outline"
/> />
</VCol> </VCol>
@@ -1481,7 +1480,15 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol> </VCol>
</VRow> </VRow>
<VRow> <VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4"> <VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.LLM_USE_PROXY"
:label="t('setting.system.llmUseProxy')"
:hint="t('setting.system.llmUseProxyHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch <VSwitch
v-model="SystemSettings.Basic.LLM_SUPPORT_IMAGE_INPUT" v-model="SystemSettings.Basic.LLM_SUPPORT_IMAGE_INPUT"
:label="t('setting.system.llmSupportImageInput')" :label="t('setting.system.llmSupportImageInput')"
+9 -3
View File
@@ -353,7 +353,7 @@ onMounted(async () => {
<VCol v-if="showBaseUrlField" cols="12" md="6"> <VCol v-if="showBaseUrlField" cols="12" md="6">
<VCombobox <VCombobox
:model-value="wizardData.agent.baseUrl" :model-value="wizardData.agent.baseUrl || null"
@update:model-value=" @update:model-value="
(value: any) => { (value: any) => {
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
@@ -365,11 +365,12 @@ onMounted(async () => {
" "
:label="t('setting.system.llmBaseUrl')" :label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')" :hint="t('setting.system.llmBaseUrlHint')"
:placeholder="selectedProvider?.default_base_url || 'https://api.deepseek.com'" :placeholder="selectedProvider?.default_base_url || t('setting.system.llmBaseUrlPlaceholder')"
:items="baseUrlPresetItems" :items="baseUrlPresetItems"
item-title="title" item-title="title"
item-value="value" item-value="value"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-link-variant" prepend-inner-icon="mdi-link-variant"
> >
<template #item="{ props, item }"> <template #item="{ props, item }">
@@ -397,6 +398,7 @@ onMounted(async () => {
:error="validationErrors.agent.apiKey" :error="validationErrors.agent.apiKey"
:error-messages="validationErrors.agent.apiKey ? [t('setupWizard.agent.authOrApiKeyRequired')] : []" :error-messages="validationErrors.agent.apiKey ? [t('setupWizard.agent.authOrApiKeyRequired')] : []"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-key-variant" prepend-inner-icon="mdi-key-variant"
type="password" type="password"
/> />
@@ -445,7 +447,7 @@ onMounted(async () => {
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VCombobox <VCombobox
:model-value="wizardData.agent.model" :model-value="wizardData.agent.model || null"
@update:model-value=" @update:model-value="
(val: any) => { (val: any) => {
wizardData.agent.model = typeof val === 'object' && val !== null ? val.id : val wizardData.agent.model = typeof val === 'object' && val !== null ? val.id : val
@@ -454,6 +456,7 @@ onMounted(async () => {
" "
:label="t('setting.system.llmModel')" :label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')" :hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelPlaceholder')"
:items="llmModels" :items="llmModels"
item-title="name" item-title="name"
item-value="id" item-value="id"
@@ -461,6 +464,7 @@ onMounted(async () => {
:error="validationErrors.agent.model" :error="validationErrors.agent.model"
:error-messages="validationErrors.agent.model ? [t('setupWizard.agent.modelRequired')] : []" :error-messages="validationErrors.agent.model ? [t('setupWizard.agent.modelRequired')] : []"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-brain" prepend-inner-icon="mdi-brain"
> >
<template #append-inner> <template #append-inner>
@@ -500,7 +504,9 @@ onMounted(async () => {
v-model="wizardData.agent.userAgent" v-model="wizardData.agent.userAgent"
:label="t('setting.system.llmUserAgent')" :label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')" :hint="t('setting.system.llmUserAgentHint')"
:placeholder="t('setting.system.llmUserAgentPlaceholder')"
persistent-hint persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-card-account-details-outline" prepend-inner-icon="mdi-card-account-details-outline"
/> />
</VCol> </VCol>