diff --git a/web-app/package.json b/web-app/package.json index 8b3193817..44d027623 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -32,6 +32,7 @@ "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-dialog": "^2.2.1", + "@tauri-apps/plugin-http": "^2.2.1", "@tauri-apps/plugin-opener": "^2.2.7", "@tauri-apps/plugin-os": "^2.2.1", "@tauri-apps/plugin-updater": "^2.7.1", diff --git a/web-app/src/lib/completion.ts b/web-app/src/lib/completion.ts index 24daec3cd..5ffd4fa4b 100644 --- a/web-app/src/lib/completion.ts +++ b/web-app/src/lib/completion.ts @@ -7,6 +7,7 @@ import { ModelManager, } from '@janhq/core' import { invoke } from '@tauri-apps/api/core' +import { fetch as fetchTauri } from '@tauri-apps/plugin-http' import { ChatCompletionMessageParam, ChatCompletionTool, @@ -15,7 +16,13 @@ import { models, StreamCompletionResponse, TokenJS, + ConfigOptions, } from 'token.js' + +// Extended config options to include custom fetch function +type ExtendedConfigOptions = ConfigOptions & { + fetch?: typeof fetch +} import { ulid } from 'ulidx' import { normalizeProvider } from './models' import { MCPTool } from '@/types/completion' @@ -129,7 +136,9 @@ export const sendCompletion = async ( apiKey: provider.api_key ?? (await invoke('app_token')), // TODO: Retrieve from extension settings baseURL: provider.base_url, - }) + // Use Tauri's fetch to avoid CORS issues only for openai-compatible provider + ...(providerName === 'openai-compatible' && { fetch: fetchTauri }), + } as ExtendedConfigOptions) if ( thread.model.id && !(thread.model.id in Object.values(models).flat()) && diff --git a/web-app/src/services/providers.ts b/web-app/src/services/providers.ts index 6bd2b63f0..358d06a72 100644 --- a/web-app/src/services/providers.ts +++ b/web-app/src/services/providers.ts @@ -13,6 +13,8 @@ import { import { modelSettings } from '@/lib/predefined' import { fetchModels } from './models' import { ExtensionManager } from '@/lib/extension' +import { fetch as fetchTauri } from '@tauri-apps/plugin-http' + export const getProviders = async (): Promise => { const engines = !localStorage.getItem('migration_completed') @@ -163,26 +165,35 @@ export const getProviders = async (): Promise => { return runtimeProviders.concat(builtinProviders as ModelProvider[]) } + /** * Fetches models from a provider's API endpoint + * Always uses Tauri's HTTP client to bypass CORS issues * @param provider The provider object containing base_url and api_key * @returns Promise Array of model IDs */ export const fetchModelsFromProvider = async ( provider: ModelProvider ): Promise => { - if (!provider.base_url || !provider.api_key) { - throw new Error('Provider must have base_url and api_key configured') + if (!provider.base_url) { + throw new Error('Provider must have base_url configured') } try { - const response = await fetch(`${provider.base_url}/models`, { + const headers: Record = { + 'Content-Type': 'application/json', + } + + // Only add authentication headers if API key is provided + if (provider.api_key) { + headers['x-api-key'] = provider.api_key + headers['Authorization'] = `Bearer ${provider.api_key}` + } + + // Always use Tauri's fetch to avoid CORS issues + const response = await fetchTauri(`${provider.base_url}/models`, { method: 'GET', - headers: { - 'x-api-key': provider.api_key, - 'Authorization': `Bearer ${provider.api_key}`, - 'Content-Type': 'application/json', - }, + headers, }) if (!response.ok) { @@ -213,6 +224,14 @@ export const fetchModelsFromProvider = async ( } } catch (error) { console.error('Error fetching models from provider:', error) + + // Provide helpful error message + if (error instanceof Error && error.message.includes('fetch')) { + throw new Error( + `Cannot connect to ${provider.provider} at ${provider.base_url}. Please check that the service is running and accessible.` + ) + } + throw error } }