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:
parent
8e9cd2566b
commit
d347058d6b
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
[
|
||||
'llamacpp',
|
||||
{
|
||||
inferenceUrl: 'http://localhost:1337/chat/completions',
|
||||
getSettings: vi.fn(() => Promise.resolve([
|
||||
getSettings: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
key: 'apiKey',
|
||||
title: 'API Key',
|
||||
description: 'Your API key',
|
||||
controllerType: 'input',
|
||||
controllerProps: { value: '' }
|
||||
}
|
||||
]))
|
||||
}]
|
||||
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', {
|
||||
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'
|
||||
'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: '' },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user