fix: HuggingFace provider should be non-deletable (#5856)

* fix: HuggingFace provider should be non-deletable

* refactor: rename const folder

* test: correct test case
This commit is contained in:
Louis 2025-07-22 23:32:37 +07:00 committed by GitHub
parent 8e9cd2566b
commit d347058d6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 122 additions and 97 deletions

View File

@ -12,12 +12,12 @@ import {
import { toast } from 'sonner'
import { CardItem } from '../Card'
import { models } from 'token.js'
import { EngineManager } from '@janhq/core'
import { useModelProvider } from '@/hooks/useModelProvider'
import { useRouter } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { predefinedProviders } from '@/consts/providers'
type Props = {
provider?: ProviderObject
@ -28,7 +28,7 @@ const DeleteProvider = ({ provider }: Props) => {
const router = useRouter()
if (
!provider ||
Object.keys(models).includes(provider.provider) ||
predefinedProviders.some((e) => e.provider === provider.provider) ||
EngineManager.instance().get(provider.provider)
)
return null

View File

@ -37,7 +37,7 @@ import { IconFolderPlus, IconLoader, IconRefresh } from '@tabler/icons-react'
import { getProviders } from '@/services/providers'
import { toast } from 'sonner'
import { useEffect, useState } from 'react'
import { predefinedProviders } from '@/mock/data'
import { predefinedProviders } from '@/consts/providers'
import { useModelLoad } from '@/hooks/useModelLoad'
// as route.threadsDetail

View File

@ -22,7 +22,7 @@ import {
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { useCallback, useState } from 'react'
import { openAIProviderSettings } from '@/mock/data'
import { openAIProviderSettings } from '@/consts/providers'
import cloneDeep from 'lodash/cloneDeep'
import { toast } from 'sonner'
@ -38,7 +38,9 @@ function ModelProviders() {
const [name, setName] = useState('')
const createProvider = useCallback(() => {
if (providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())) {
if (
providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())
) {
toast.error(t('providerAlreadyExists', { name }))
return
}

View File

@ -1,5 +1,9 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getProviders, fetchModelsFromProvider, updateSettings } from '../providers'
import {
getProviders,
fetchModelsFromProvider,
updateSettings,
} from '../providers'
import { models as providerModels } from 'token.js'
import { predefinedProviders } from '@/mock/data'
import { EngineManager } from '@janhq/core'
@ -12,9 +16,9 @@ vi.mock('token.js', () => ({
models: {
openai: {
models: ['gpt-3.5-turbo', 'gpt-4'],
supportsToolCalls: ['gpt-3.5-turbo', 'gpt-4']
}
}
supportsToolCalls: ['gpt-3.5-turbo', 'gpt-4'],
},
},
}))
vi.mock('@/mock/data', () => ({
@ -27,73 +31,80 @@ vi.mock('@/mock/data', () => ({
settings: [],
models: [
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
{ id: 'gpt-4', name: 'GPT-4' }
]
}
]
{ id: 'gpt-4', name: 'GPT-4' },
],
},
],
}))
vi.mock('@janhq/core', () => ({
EngineManager: {
instance: vi.fn(() => ({
engines: new Map([
['llamacpp', {
inferenceUrl: 'http://localhost:1337/chat/completions',
getSettings: vi.fn(() => Promise.resolve([
{
key: 'apiKey',
title: 'API Key',
description: 'Your API key',
controllerType: 'input',
controllerProps: { value: '' }
}
]))
}]
])
}))
}
[
'llamacpp',
{
inferenceUrl: 'http://localhost:1337/chat/completions',
getSettings: vi.fn(() =>
Promise.resolve([
{
key: 'apiKey',
title: 'API Key',
description: 'Your API key',
controllerType: 'input',
controllerProps: { value: '' },
},
])
),
},
],
]),
})),
},
}))
vi.mock('../models', () => ({
fetchModels: vi.fn(() => Promise.resolve([
{ id: 'llama-2-7b', name: 'Llama 2 7B', description: 'Llama model' }
]))
fetchModels: vi.fn(() =>
Promise.resolve([
{ id: 'llama-2-7b', name: 'Llama 2 7B', description: 'Llama model' },
])
),
}))
vi.mock('@/lib/extension', () => ({
ExtensionManager: {
getInstance: vi.fn(() => ({
getEngine: vi.fn()
}))
}
getEngine: vi.fn(),
})),
},
}))
vi.mock('@tauri-apps/plugin-http', () => ({
fetch: vi.fn()
fetch: vi.fn(),
}))
vi.mock('@/types/models', () => ({
ModelCapabilities: {
COMPLETION: 'completion',
TOOLS: 'tools'
TOOLS: 'tools',
},
DefaultToolUseSupportedModels: {
'gpt-4': 'gpt-4',
'gpt-3.5-turbo': 'gpt-3.5-turbo'
}
'gpt-3.5-turbo': 'gpt-3.5-turbo',
},
}))
vi.mock('@/lib/predefined', () => ({
modelSettings: {
temperature: {
key: 'temperature',
controller_props: { value: 0.7 }
controller_props: { value: 0.7 },
},
ctx_len: {
key: 'ctx_len',
controller_props: { value: 2048 }
}
}
controller_props: { value: 2048 },
},
},
}))
describe('providers service', () => {
@ -105,14 +116,14 @@ describe('providers service', () => {
it('should return builtin and runtime providers', async () => {
const providers = await getProviders()
expect(providers).toHaveLength(2) // 1 runtime + 1 builtin
expect(providers.some(p => p.provider === 'llamacpp')).toBe(true)
expect(providers.some(p => p.provider === 'openai')).toBe(true)
expect(providers).toHaveLength(9) // 8 runtime + 1 builtin
expect(providers.some((p) => p.provider === 'llamacpp')).toBe(true)
expect(providers.some((p) => p.provider === 'openai')).toBe(true)
})
it('should map builtin provider models correctly', async () => {
const providers = await getProviders()
const openaiProvider = providers.find(p => p.provider === 'openai')
const openaiProvider = providers.find((p) => p.provider === 'openai')
expect(openaiProvider).toBeDefined()
expect(openaiProvider?.models).toHaveLength(2)
@ -122,7 +133,7 @@ describe('providers service', () => {
it('should create runtime providers from engine manager', async () => {
const providers = await getProviders()
const llamacppProvider = providers.find(p => p.provider === 'llamacpp')
const llamacppProvider = providers.find((p) => p.provider === 'llamacpp')
expect(llamacppProvider).toBeDefined()
expect(llamacppProvider?.base_url).toBe('http://localhost:1337')
@ -136,44 +147,44 @@ describe('providers service', () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
data: [
{ id: 'gpt-3.5-turbo' },
{ id: 'gpt-4' }
]
})
data: [{ id: 'gpt-3.5-turbo' }, { id: 'gpt-4' }],
}),
}
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
const provider = {
provider: 'openai',
base_url: 'https://api.openai.com/v1',
api_key: 'test-key'
api_key: 'test-key',
} as ModelProvider
const models = await fetchModelsFromProvider(provider)
expect(fetchTauri).toHaveBeenCalledWith('https://api.openai.com/v1/models', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'test-key',
'Authorization': 'Bearer test-key'
expect(fetchTauri).toHaveBeenCalledWith(
'https://api.openai.com/v1/models',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'test-key',
'Authorization': 'Bearer test-key',
},
}
})
)
expect(models).toEqual(['gpt-3.5-turbo', 'gpt-4'])
})
it('should fetch models successfully with direct array format', async () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue(['model1', 'model2'])
json: vi.fn().mockResolvedValue(['model1', 'model2']),
}
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
const provider = {
provider: 'custom',
base_url: 'https://api.custom.com',
api_key: ''
api_key: '',
} as ModelProvider
const models = await fetchModelsFromProvider(provider)
@ -185,17 +196,14 @@ describe('providers service', () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
models: [
{ id: 'model1' },
'model2'
]
})
models: [{ id: 'model1' }, 'model2'],
}),
}
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
const provider = {
provider: 'custom',
base_url: 'https://api.custom.com'
base_url: 'https://api.custom.com',
} as ModelProvider
const models = await fetchModelsFromProvider(provider)
@ -205,26 +213,30 @@ describe('providers service', () => {
it('should throw error when provider has no base_url', async () => {
const provider = {
provider: 'custom'
provider: 'custom',
} as ModelProvider
await expect(fetchModelsFromProvider(provider)).rejects.toThrow('Provider must have base_url configured')
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
'Provider must have base_url configured'
)
})
it('should throw error when API response is not ok', async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found'
statusText: 'Not Found',
}
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
const provider = {
provider: 'custom',
base_url: 'https://api.custom.com'
base_url: 'https://api.custom.com',
} as ModelProvider
await expect(fetchModelsFromProvider(provider)).rejects.toThrow('Cannot connect to custom at https://api.custom.com')
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
'Cannot connect to custom at https://api.custom.com'
)
})
it('should handle network errors gracefully', async () => {
@ -232,16 +244,18 @@ describe('providers service', () => {
const provider = {
provider: 'custom',
base_url: 'https://api.custom.com'
base_url: 'https://api.custom.com',
} as ModelProvider
await expect(fetchModelsFromProvider(provider)).rejects.toThrow('Cannot connect to custom at https://api.custom.com')
await expect(fetchModelsFromProvider(provider)).rejects.toThrow(
'Cannot connect to custom at https://api.custom.com'
)
})
it('should return empty array for unexpected response format', async () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({ unexpected: 'format' })
json: vi.fn().mockResolvedValue({ unexpected: 'format' }),
}
vi.mocked(fetchTauri).mockResolvedValue(mockResponse as any)
@ -249,13 +263,16 @@ describe('providers service', () => {
const provider = {
provider: 'custom',
base_url: 'https://api.custom.com'
base_url: 'https://api.custom.com',
} as ModelProvider
const models = await fetchModelsFromProvider(provider)
expect(models).toEqual([])
expect(consoleSpy).toHaveBeenCalledWith('Unexpected response format from provider API:', { unexpected: 'format' })
expect(consoleSpy).toHaveBeenCalledWith(
'Unexpected response format from provider API:',
{ unexpected: 'format' }
)
consoleSpy.mockRestore()
})
@ -264,12 +281,14 @@ describe('providers service', () => {
describe('updateSettings', () => {
it('should update provider settings successfully', async () => {
const mockEngine = {
updateSettings: vi.fn().mockResolvedValue(undefined)
updateSettings: vi.fn().mockResolvedValue(undefined),
}
const mockExtensionManager = {
getEngine: vi.fn().mockReturnValue(mockEngine)
getEngine: vi.fn().mockReturnValue(mockEngine),
}
vi.mocked(ExtensionManager.getInstance).mockReturnValue(mockExtensionManager)
vi.mocked(ExtensionManager.getInstance).mockReturnValue(
mockExtensionManager
)
const settings = [
{
@ -277,8 +296,8 @@ describe('providers service', () => {
title: 'API Key',
description: 'Your API key',
controller_type: 'input',
controller_props: { value: 'test-key' }
}
controller_props: { value: 'test-key' },
},
] as ProviderSetting[]
await updateSettings('openai', settings)
@ -292,16 +311,18 @@ describe('providers service', () => {
controller_type: 'input',
controller_props: { value: 'test-key' },
controllerType: 'input',
controllerProps: { value: 'test-key' }
}
controllerProps: { value: 'test-key' },
},
])
})
it('should handle missing engine gracefully', async () => {
const mockExtensionManager = {
getEngine: vi.fn().mockReturnValue(null)
getEngine: vi.fn().mockReturnValue(null),
}
vi.mocked(ExtensionManager.getInstance).mockReturnValue(mockExtensionManager)
vi.mocked(ExtensionManager.getInstance).mockReturnValue(
mockExtensionManager
)
const settings = [] as ProviderSetting[]
@ -312,12 +333,14 @@ describe('providers service', () => {
it('should handle settings with undefined values', async () => {
const mockEngine = {
updateSettings: vi.fn().mockResolvedValue(undefined)
updateSettings: vi.fn().mockResolvedValue(undefined),
}
const mockExtensionManager = {
getEngine: vi.fn().mockReturnValue(mockEngine)
getEngine: vi.fn().mockReturnValue(mockEngine),
}
vi.mocked(ExtensionManager.getInstance).mockReturnValue(mockExtensionManager)
vi.mocked(ExtensionManager.getInstance).mockReturnValue(
mockExtensionManager
)
const settings = [
{
@ -325,8 +348,8 @@ describe('providers service', () => {
title: 'API Key',
description: 'Your API key',
controller_type: 'input',
controller_props: { value: undefined }
}
controller_props: { value: undefined },
},
] as ProviderSetting[]
await updateSettings('openai', settings)
@ -339,8 +362,8 @@ describe('providers service', () => {
controller_type: 'input',
controller_props: { value: undefined },
controllerType: 'input',
controllerProps: { value: '' }
}
controllerProps: { value: '' },
},
])
})
})

View File

@ -1,5 +1,5 @@
import { models as providerModels } from 'token.js'
import { predefinedProviders } from '@/mock/data'
import { predefinedProviders } from '@/consts/providers'
import { EngineManager, SettingComponentProps } from '@janhq/core'
import {
DefaultToolUseSupportedModels,