{"repo": "chatboxai/chatbox", "files": {"src/renderer/utils/provider-config.test.ts": {"f90fc31afd634494bdf8f074eca3e38fcf8da740": {"t": 1785768987.26589, "n": 14845, "h": "0da94434847d67b1", "c": "import { ModelProviderEnum, ModelProviderType } from '@shared/types'\nimport { describe, expect, it } from 'vitest'\nimport { parseProviderFromJson, validateProviderConfig } from './provider-config'\n\ndescribe('provider-config', () => {\n describe('parseProviderFromJson', () => {\n it('should parse a valid custom provider config', () => {\n const configJson = JSON.stringify({\n id: 'custom-provider',\n name: 'Custom Provider',\n type: 'openai',\n iconUrl: 'https://example.com/icon.png',\n urls: {\n website: 'https://example.com',\n getApiKey: 'https://example.com/api-key',\n docs: 'https://example.com/docs',\n models: 'https://example.com/models',\n },\n settings: {\n apiHost: 'https://api.example.com',\n apiPath: '/v1/chat/completions',\n apiKey: 'test-api-key',\n models: [\n {\n modelId: 'model-1',\n nickname: 'Model One',\n type: 'chat',\n capabilities: ['vision', 'tool_use'],\n contextWindow: 32000,\n maxOutput: 4096,\n },\n ],\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('custom-provider')\n // For custom providers, name should be present\n expect(result && 'name' in result).toBe(true)\n if (result && 'name' in result) {\n expect(result.name).toBe('Custom Provider')\n expect(result.type).toBe(ModelProviderType.OpenAI)\n // For custom providers, isCustom should be true\n expect('isCustom' in result && result.isCustom).toBe(true)\n // iconUrl should be present in this test case for custom provider\n if ('isCustom' in result && result.isCustom) {\n expect(result.iconUrl).toBe('https://example.com/icon.png')\n }\n }\n expect(result?.apiHost).toBe('https://api.example.com')\n expect(result?.apiPath).toBe('/v1/chat/completions')\n expect(result?.apiKey).toBe('test-api-key')\n expect(result?.models).toHaveLength(1)\n expect(result?.models?.[0].modelId).toBe('model-1')\n })\n\n it('should parse a valid builtin provider config', () => {\n const configJson = JSON.stringify({\n id: ModelProviderEnum.OpenAI,\n settings: {\n apiHost: 'https://api.openai.com',\n apiKey: 'sk-test-key',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe(ModelProviderEnum.OpenAI)\n expect(result?.apiHost).toBe('https://api.openai.com')\n expect(result?.apiKey).toBe('sk-test-key')\n })\n\n it('should reject explicitly custom configs when the id matches a builtin provider', () => {\n const configJson = JSON.stringify({\n isCustom: true,\n id: ModelProviderEnum.OpenAI,\n name: 'My OpenAI Mirror',\n type: 'openai',\n urls: {\n website: 'https://example.com',\n },\n settings: {\n apiHost: 'https://mirror.example.com',\n apiKey: 'mirror-key',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeUndefined()\n })\n\n it('should handle minimal valid provider config', () => {\n const configJson = JSON.stringify({\n id: 'minimal-provider',\n name: 'Minimal Provider',\n type: 'openai',\n settings: {\n apiHost: 'https://api.minimal.com',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('minimal-provider')\n if (result && 'name' in result) {\n expect(result.name).toBe('Minimal Provider')\n }\n expect(result?.apiHost).toBe('https://api.minimal.com')\n expect(result?.apiKey).toBeUndefined()\n expect(result?.models).toBeUndefined()\n })\n\n it('should handle provider config with anthropic type', () => {\n const configJson = JSON.stringify({\n id: 'anthropic-custom',\n name: 'Anthropic Custom',\n type: 'anthropic',\n settings: {\n apiHost: 'https://api.anthropic.com',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('anthropic-custom')\n if (result && 'type' in result) {\n expect(result.type).toBe(ModelProviderType.Claude) // anthropic type should map to Claude\n }\n })\n\n it('should handle provider config with openai-responses type', () => {\n const configJson = JSON.stringify({\n id: 'openai-responses-custom',\n name: 'OpenAI Responses Custom',\n type: 'openai-responses',\n settings: {\n apiHost: 'https://api.openai.com',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('openai-responses-custom')\n if (result && 'type' in result) {\n expect(result.type).toBe(ModelProviderType.OpenAIResponses) // openai-responses type should map to OpenAIResponses\n }\n })\n\n it('should return undefined for invalid JSON', () => {\n const result = parseProviderFromJson('invalid json')\n expect(result).toBeUndefined()\n })\n\n it('should return undefined for missing required fields', () => {\n const configJson = JSON.stringify({\n name: 'Missing ID',\n type: 'openai',\n settings: {\n apiHost: 'https://api.example.com',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n expect(result).toBeUndefined()\n })\n\n it('should return undefined for invalid type field', () => {\n const configJson = JSON.stringify({\n id: 'invalid-type',\n name: 'Invalid Type',\n type: 'invalid',\n settings: {\n apiHost: 'https://api.example.com',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n expect(result).toBeUndefined()\n })\n\n it('should handle models with various capabilities', () => {\n const configJson = JSON.stringify({\n id: 'multi-model',\n name: 'Multi Model Provider',\n type: 'openai',\n settings: {\n apiHost: 'https://api.example.com',\n models: [\n {\n modelId: 'chat-model',\n type: 'chat',\n capabilities: ['vision'],\n },\n {\n modelId: 'embedding-model',\n type: 'embedding',\n },\n {\n modelId: 'reasoning-model',\n type: 'chat',\n capabilities: ['reasoning', 'tool_use'],\n contextWindow: 128000,\n maxOutput: 8192,\n },\n ],\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.models).toHaveLength(3)\n expect(result?.models?.[0].capabilities).toEqual(['vision'])\n expect(result?.models?.[1].type).toBe('embedding')\n expect(result?.models?.[2].capabilities).toEqual(['reasoning', 'tool_use'])\n expect(result?.models?.[2].contextWindow).toBe(128000)\n })\n\n it('should handle empty models array', () => {\n const configJson = JSON.stringify({\n id: 'no-models',\n name: 'No Models Provider',\n type: 'openai',\n settings: {\n apiHost: 'https://api.example.com',\n models: [],\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n expect(result?.models).toEqual([])\n })\n\n it('should reject invalid builtin provider enum', () => {\n const configJson = JSON.stringify({\n id: 'NotAValidEnum',\n settings: {\n apiHost: 'https://api.example.com',\n apiKey: 'test-key',\n },\n })\n\n const result = parseProviderFromJson(configJson)\n expect(result).toBeUndefined()\n })\n\n it('should handle provider with all optional fields', () => {\n const configJson = JSON.stringify({\n id: 'full-provider',\n name: 'Full Provider',\n type: 'openai',\n iconUrl: 'https://icon.url',\n urls: {\n website: 'https://website.com',\n getApiKey: 'https://get-key.com',\n docs: 'https://docs.com',\n models: 'https://models.com',\n },\n settings: {\n apiHost: 'https://api.example.com',\n apiPath: '/custom/path',\n apiKey: 'full-key',\n models: [\n {\n modelId: 'full-model',\n nickname: 'Full Model',\n type: 'chat',\n capabilities: ['vision', 'reasoning', 'tool_use'],\n contextWindow: 200000,\n maxOutput: 16384,\n },\n ],\n },\n })\n\n const result = parseProviderFromJson(configJson)\n\n expect(result).toBeDefined()\n if (result && 'urls' in result) {\n expect(result.urls?.website).toBe('https://website.com')\n if (result.urls && 'getApiKey' in result.urls) {\n expect(result.urls.getApiKey).toBe('https://get-key.com')\n }\n expect(result.urls?.docs).toBe('https://docs.com')\n expect(result.urls?.models).toBe('https://models.com')\n }\n expect(result?.models?.[0].nickname).toBe('Full Model')\n expect(result?.models?.[0].maxOutput).toBe(16384)\n })\n })\n\n describe('validateProviderConfig', () => {\n it('should validate and return valid provider config', () => {\n const config = {\n id: 'valid-provider',\n name: 'Valid Provider',\n type: 'openai',\n settings: {\n apiHost: 'https://api.valid.com',\n },\n }\n\n const result = validateProviderConfig(config)\n\n expect(result).toEqual({\n ...config,\n isCustom: true, // The schema adds this automatically\n })\n })\n\n it('should return undefined for invalid config', () => {\n const config = {\n id: 'invalid-provider',\n // missing required fields\n }\n\n const result = validateProviderConfig(config)\n\n expect(result).toBeUndefined()\n })\n\n it('should validate config with invalid model type', () => {\n const config = {\n id: 'invalid-model-type',\n name: 'Invalid Model Type',\n type: 'openai',\n settings: {\n apiHost: 'https://api.example.com',\n models: [\n {\n modelId: 'model',\n type: 'invalid-type', // should be chat, embedding, or rerank\n },\n ],\n },\n }\n\n const result = validateProviderConfig(config)\n\n expect(result).toBeUndefined()\n })\n\n it('should validate config with invalid capability', () => {\n const config = {\n id: 'invalid-capability',\n name: 'Invalid Capability',\n type: 'openai',\n settings: {\n apiHost: 'https://api.example.com',\n models: [\n {\n modelId: 'model',\n capabilities: ['invalid-capability'], // should be vision, reasoning, or tool_use\n },\n ],\n },\n }\n\n const result = validateProviderConfig(config)\n\n expect(result).toBeUndefined()\n })\n\n it('should reject custom configs that reuse builtin provider ids', () => {\n const result = validateProviderConfig({\n isCustom: true,\n id: ModelProviderEnum.OpenAI,\n name: 'Conflicting OpenAI Mirror',\n type: 'openai',\n settings: {\n apiHost: 'https://mirror.example.com',\n },\n })\n\n expect(result).toBeUndefined()\n })\n })\n\n describe('Base64 encoded config (for deep links)', () => {\n it('should parse base64 encoded provider config', () => {\n const config = {\n id: 'base64-provider',\n name: 'Base64 Provider',\n type: 'openai',\n settings: {\n apiHost: 'https://api.base64.com',\n },\n }\n\n const base64Config = Buffer.from(JSON.stringify(config)).toString('base64')\n const decodedJson = Buffer.from(base64Config, 'base64').toString('utf-8')\n const result = parseProviderFromJson(decodedJson)\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('base64-provider')\n if (result && 'name' in result) {\n expect(result.name).toBe('Base64 Provider')\n }\n })\n\n it('should handle malformed base64 data', () => {\n const invalidBase64 = 'not-valid-base64-json'\n try {\n const decodedJson = Buffer.from(invalidBase64, 'base64').toString('utf-8')\n const result = parseProviderFromJson(decodedJson)\n expect(result).toBeUndefined()\n } catch {\n // Expected to fail\n expect(true).toBe(true)\n }\n })\n })\n\n describe('Real-world provider configs from documentation', () => {\n it('should parse 302.AI provider config', () => {\n const config302AI = {\n id: '302ai',\n name: '302.AI',\n type: 'openai',\n iconUrl: 'https://file.302.ai/favicon.ico',\n urls: {\n website: 'https://302.ai',\n getApiKey: 'https://302.ai',\n },\n settings: {\n apiHost: 'https://api.302.ai',\n models: [\n {\n modelId: 'gpt-4o',\n nickname: 'GPT-4o',\n capabilities: ['vision'],\n },\n {\n modelId: 'claude-3-5-sonnet-20241022',\n nickname: 'Claude 3.5 Sonnet',\n },\n ],\n },\n }\n\n const result = parseProviderFromJson(JSON.stringify(config302AI))\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('302ai')\n if (result && 'name' in result) {\n expect(result.name).toBe('302.AI')\n }\n expect(result?.models).toHaveLength(2)\n })\n\n it('should parse AiHubMix provider config', () => {\n const configAiHubMix = {\n id: 'aihubmix',\n name: 'AiHubMix',\n type: 'openai',\n iconUrl: 'https://aihubmix.com/logo.png',\n urls: {\n website: 'https://aihubmix.com',\n getApiKey: 'https://aihubmix.com/dashboard',\n },\n settings: {\n apiHost: 'https://api.aihubmix.com',\n models: [\n {\n modelId: 'gpt-4',\n contextWindow: 8192,\n },\n {\n modelId: 'claude-2',\n contextWindow: 100000,\n },\n ],\n },\n }\n\n const result = parseProviderFromJson(JSON.stringify(configAiHubMix))\n\n expect(result).toBeDefined()\n expect(result?.id).toBe('aihubmix')\n expect(result?.models?.[0].contextWindow).toBe(8192)\n expect(result?.models?.[1].contextWindow).toBe(100000)\n })\n })\n})\n"}}}}