diff --git a/web-app/src/mock/data.ts b/web-app/src/consts/providers.ts similarity index 100% rename from web-app/src/mock/data.ts rename to web-app/src/consts/providers.ts diff --git a/web-app/src/containers/dialogs/DeleteProvider.tsx b/web-app/src/containers/dialogs/DeleteProvider.tsx index 6afba39d3..fede8e97d 100644 --- a/web-app/src/containers/dialogs/DeleteProvider.tsx +++ b/web-app/src/containers/dialogs/DeleteProvider.tsx @@ -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 diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index 9b1066aa6..2681054ac 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -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 diff --git a/web-app/src/routes/settings/providers/index.tsx b/web-app/src/routes/settings/providers/index.tsx index 163f82951..f2056b73a 100644 --- a/web-app/src/routes/settings/providers/index.tsx +++ b/web-app/src/routes/settings/providers/index.tsx @@ -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 } diff --git a/web-app/src/services/__tests__/providers.test.ts b/web-app/src/services/__tests__/providers.test.ts index 6f15ef28a..848399df4 100644 --- a/web-app/src/services/__tests__/providers.test.ts +++ b/web-app/src/services/__tests__/providers.test.ts @@ -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,14 +263,17 @@ 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,9 +362,9 @@ describe('providers service', () => { controller_type: 'input', controller_props: { value: undefined }, controllerType: 'input', - controllerProps: { value: '' } - } + controllerProps: { value: '' }, + }, ]) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/providers.ts b/web-app/src/services/providers.ts index 2b70dba74..a57df0a9c 100644 --- a/web-app/src/services/providers.ts +++ b/web-app/src/services/providers.ts @@ -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,