feat: improve local provider connectivity with CORS bypass (#5458)

* feat: improve local provider connectivity with CORS bypass

- Add @tauri-apps/plugin-http dependency
- Implement dual fetch strategy for local vs remote providers
- Auto-detect local providers (localhost, Ollama:11434, LM Studio:1234)
- Make API key optional for local providers
- Add comprehensive test coverage for provider fetching

refactor: simplify fetchModelsFromProvider by removing preflight check logic

* feat: extend config options to include custom fetch function for CORS handling

* feat: conditionally use Tauri's fetch for openai-compatible providers to handle CORS
This commit is contained in:
Sam Hoang Van 2025-06-25 15:42:14 +07:00 committed by GitHub
parent 52d15802d9
commit 0890de1869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 9 deletions

View File

@ -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",

View File

@ -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()) &&

View File

@ -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<ModelProvider[]> => {
const engines = !localStorage.getItem('migration_completed')
@ -163,26 +165,35 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
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<string[]> Array of model IDs
*/
export const fetchModelsFromProvider = async (
provider: ModelProvider
): Promise<string[]> => {
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<string, string> = {
'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
}
}