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

View File

@@ -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()
})
})

View File

@@ -128,7 +128,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
)
const showApiKeyField = computed(() => selectedProvider.value?.supports_api_key !== false)
// OpenAI 兼容接口才需要选择 API 协议Chat Completions / Responses
const showApiProtocolField = computed(() => selectedProvider.value?.runtime === 'openai')
const showApiProtocolField = computed(() => selectedProvider.value?.runtime === 'openai_compatible')
const hasUsableCredential = computed(() => {
if (providerConnected.value) return true
return Boolean(normalizeValue(options.apiKey.value))

View File

@@ -1766,6 +1766,7 @@ export default {
llmProviderHint: 'Select the LLM service provider to use',
llmModel: 'LLM Model Name',
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}',
llmThinking: 'Thinking Mode / Depth',
llmThinkingHint:
@@ -1794,11 +1795,13 @@ export default {
llmApiKeyPlaceholder: 'Please enter API key',
llmBaseUrl: 'LLM Base URL',
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
llmBaseUrlPlaceholder: 'Enter the LLM API base URL',
llmUseProxy: 'Use System Proxy',
llmUseProxyHint:
'When enabled, Agent connections to the current LLM provider use the system proxy from advanced settings.',
llmUserAgent: 'User-Agent',
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',
llmApiProtocolHint:
'Request protocol for OpenAI-compatible APIs: auto-select by model capability, or force Chat Completions / Responses.',

View File

@@ -1753,6 +1753,7 @@ export default {
llmProviderHint: '选择使用的LLM服务提供商',
llmModel: 'LLM模型名称',
llmModelHint: '指定使用的LLM模型如deepseek-v4-flash、gpt-5.4等',
llmModelPlaceholder: '请输入或选择LLM模型名称',
llmModelResolvedHint: '已根据模型目录自动回填最大上下文为 {context}K来源{source}',
llmThinking: '思考模式 / 深度',
llmThinkingHint:
@@ -1780,10 +1781,12 @@ export default {
llmApiKeyPlaceholder: '请输入API密钥',
llmBaseUrl: 'LLM基础URL',
llmBaseUrlHint: 'LLM API的基础URL地址用于自定义API端点',
llmBaseUrlPlaceholder: '请输入LLM API基础URL',
llmUseProxy: '使用系统代理',
llmUseProxyHint: '启用后Agent 连接当前 LLM 提供商时会应用高级设置中的系统代理',
llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口请求使用的 User-Agent留空则使用 SDK 默认值',
llmUserAgentPlaceholder: '留空则使用SDK默认User-Agent',
llmApiProtocol: 'API 协议',
llmApiProtocolHint: 'OpenAI 兼容接口的请求协议:自动按模型能力选择,或强制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自动 (auto)',

View File

@@ -1752,6 +1752,7 @@ export default {
llmProviderHint: '選擇使用的LLM服務提供商',
llmModel: 'LLM模型名稱',
llmModelHint: '指定使用的LLM模型如deepseek-v4-flash、gpt-5.4等',
llmModelPlaceholder: '請輸入或選擇LLM模型名稱',
llmModelResolvedHint: '已根據模型目錄自動回填最大上下文為 {context}K來源{source}',
llmThinking: '思考模式 / 深度',
llmThinkingHint:
@@ -1779,10 +1780,12 @@ export default {
llmApiKeyPlaceholder: '請輸入API密鑰',
llmBaseUrl: 'LLM基礎URL',
llmBaseUrlHint: 'LLM API的基礎URL地址用於自定義API端點',
llmBaseUrlPlaceholder: '請輸入LLM API基礎URL',
llmUseProxy: '使用系統代理',
llmUseProxyHint: '啟用後Agent 連接目前 LLM 提供商時會套用進階設定中的系統代理',
llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口請求使用的 User-Agent留空則使用 SDK 預設值',
llmUserAgentPlaceholder: '留空則使用SDK預設User-Agent',
llmApiProtocol: 'API 協議',
llmApiProtocolHint: 'OpenAI 兼容接口的請求協議:自動按模型能力選擇,或強制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自動 (auto)',

View File

@@ -1243,7 +1243,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL"
:model-value="SystemSettings.Basic.LLM_BASE_URL || null"
@update:model-value="
(value: any) => {
if (typeof value === 'object' && value !== null) {
@@ -1255,11 +1255,14 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
"
:label="t('setting.system.llmBaseUrl')"
: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"
item-title="title"
item-value="value"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-link"
>
<template #item="{ props, item }">
@@ -1267,14 +1270,6 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</template>
</VCombobox>
</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">
<VTextField
v-model="SystemSettings.Basic.LLM_API_KEY"
@@ -1282,6 +1277,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
:hint="selectedLlmProvider?.api_key_hint || t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
persistent-hint
persistent-placeholder
type="password"
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">
<div>
<VCombobox
:model-value="SystemSettings.Basic.LLM_MODEL"
:model-value="SystemSettings.Basic.LLM_MODEL || null"
@update:model-value="
(val: any) => {
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')"
:hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelPlaceholder')"
persistent-hint
persistent-placeholder
:items="llmModels"
item-title="name"
item-value="id"
@@ -1407,7 +1404,9 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
v-model="SystemSettings.Basic.LLM_USER_AGENT"
:label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')"
:placeholder="t('setting.system.llmUserAgentPlaceholder')"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-card-account-details-outline"
/>
</VCol>
@@ -1481,7 +1480,15 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
</VCol>
</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
v-model="SystemSettings.Basic.LLM_SUPPORT_IMAGE_INPUT"
:label="t('setting.system.llmSupportImageInput')"

View File

@@ -353,7 +353,7 @@ onMounted(async () => {
<VCol v-if="showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="wizardData.agent.baseUrl"
:model-value="wizardData.agent.baseUrl || null"
@update:model-value="
(value: any) => {
if (typeof value === 'object' && value !== null) {
@@ -365,11 +365,12 @@ onMounted(async () => {
"
:label="t('setting.system.llmBaseUrl')"
: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"
item-title="title"
item-value="value"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-link-variant"
>
<template #item="{ props, item }">
@@ -397,6 +398,7 @@ onMounted(async () => {
:error="validationErrors.agent.apiKey"
:error-messages="validationErrors.agent.apiKey ? [t('setupWizard.agent.authOrApiKeyRequired')] : []"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-key-variant"
type="password"
/>
@@ -445,7 +447,7 @@ onMounted(async () => {
<VCol cols="12" md="6">
<VCombobox
:model-value="wizardData.agent.model"
:model-value="wizardData.agent.model || null"
@update:model-value="
(val: any) => {
wizardData.agent.model = typeof val === 'object' && val !== null ? val.id : val
@@ -454,6 +456,7 @@ onMounted(async () => {
"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelPlaceholder')"
:items="llmModels"
item-title="name"
item-value="id"
@@ -461,6 +464,7 @@ onMounted(async () => {
:error="validationErrors.agent.model"
:error-messages="validationErrors.agent.model ? [t('setupWizard.agent.modelRequired')] : []"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-brain"
>
<template #append-inner>
@@ -500,7 +504,9 @@ onMounted(async () => {
v-model="wizardData.agent.userAgent"
:label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')"
:placeholder="t('setting.system.llmUserAgentPlaceholder')"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-card-account-details-outline"
/>
</VCol>