mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-10 16:13:28 +08:00
Fix LLM provider fields and OpenAI-compatible protocol detection
This commit is contained in:
77
src/composables/__tests__/useLlmProviderDirectory.spec.ts
Normal file
77
src/composables/__tests__/useLlmProviderDirectory.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user