/** * Jan Provider API Client * Handles API requests to Jan backend for models and chat completions */ import { getSharedAuthService, JanAuthService } from '../shared' import { JanModel, janProviderStore } from './store' import { ApiError } from '../shared/types/errors' // JAN_API_BASE is defined in vite.config.ts // Constants const TEMPORARY_CHAT_ID = 'temporary-chat' /** * Determines the appropriate API endpoint and request payload based on chat type * @param request - The chat completion request * @returns Object containing endpoint URL and processed request payload */ function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) { const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID // For temporary chats, use the stateless /chat/completions endpoint // For regular conversations, use the stateful /conv/chat/completions endpoint const endpoint = isTemporaryChat ? `${JAN_API_BASE}/chat/completions` : `${JAN_API_BASE}/conv/chat/completions` const payload = { ...request, stream, ...(isTemporaryChat ? { // For temporary chat: don't store anything, remove conversation metadata conversation_id: undefined, } : { // For regular chat: store everything, use conversation metadata store: true, store_reasoning: true, conversation: request.conversation_id, conversation_id: undefined, }) } return { endpoint, payload, isTemporaryChat } } export interface JanModelsResponse { object: string data: JanModel[] } export interface JanChatMessage { role: 'system' | 'user' | 'assistant' content: string reasoning?: string reasoning_content?: string tool_calls?: any[] } export interface JanChatCompletionRequest { model: string messages: JanChatMessage[] conversation_id?: string temperature?: number max_tokens?: number top_p?: number frequency_penalty?: number presence_penalty?: number stream?: boolean stop?: string | string[] tools?: any[] tool_choice?: any } export interface JanChatCompletionChoice { index: number message: JanChatMessage finish_reason: string | null } export interface JanChatCompletionResponse { id: string object: string created: number model: string choices: JanChatCompletionChoice[] usage?: { prompt_tokens: number completion_tokens: number total_tokens: number } } export interface JanChatCompletionChunk { id: string object: string created: number model: string choices: Array<{ index: number delta: { role?: string content?: string reasoning?: string reasoning_content?: string tool_calls?: any[] } finish_reason: string | null }> } export class JanApiClient { private static instance: JanApiClient private authService: JanAuthService private constructor() { this.authService = getSharedAuthService() } static getInstance(): JanApiClient { if (!JanApiClient.instance) { JanApiClient.instance = new JanApiClient() } return JanApiClient.instance } async getModels(): Promise { try { janProviderStore.setLoadingModels(true) janProviderStore.clearError() const response = await this.authService.makeAuthenticatedRequest( `${JAN_API_BASE}/conv/models` ) const models = response.data || [] janProviderStore.setModels(models) return models } catch (error) { const errorMessage = error instanceof ApiError ? error.message : error instanceof Error ? error.message : 'Failed to fetch models' janProviderStore.setError(errorMessage) janProviderStore.setLoadingModels(false) throw error } } async createChatCompletion( request: JanChatCompletionRequest ): Promise { try { janProviderStore.clearError() const { endpoint, payload } = getChatCompletionConfig(request, false) return await this.authService.makeAuthenticatedRequest( endpoint, { method: 'POST', body: JSON.stringify(payload), } ) } catch (error) { const errorMessage = error instanceof ApiError ? error.message : error instanceof Error ? error.message : 'Failed to create chat completion' janProviderStore.setError(errorMessage) throw error } } async createStreamingChatCompletion( request: JanChatCompletionRequest, onChunk: (chunk: JanChatCompletionChunk) => void, onComplete?: () => void, onError?: (error: Error) => void ): Promise { try { janProviderStore.clearError() const authHeader = await this.authService.getAuthHeader() const { endpoint, payload } = getChatCompletionConfig(request, true) const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeader, }, body: JSON.stringify(payload), }) if (!response.ok) { const errorText = await response.text() throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`) } if (!response.body) { throw new Error('Response body is null') } const reader = response.body.getReader() const decoder = new TextDecoder() try { let buffer = '' while (true) { const { done, value } = await reader.read() if (done) { break } buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') // Keep the last incomplete line in buffer buffer = lines.pop() || '' for (const line of lines) { const trimmedLine = line.trim() if (trimmedLine.startsWith('data: ')) { const data = trimmedLine.slice(6).trim() if (data === '[DONE]') { onComplete?.() return } try { const parsedChunk: JanChatCompletionChunk = JSON.parse(data) onChunk(parsedChunk) } catch (parseError) { console.warn('Failed to parse SSE chunk:', parseError, 'Data:', data) } } } } onComplete?.() } finally { reader.releaseLock() } } catch (error) { const err = error instanceof ApiError ? error : error instanceof Error ? error : new Error('Unknown error occurred') janProviderStore.setError(err.message) onError?.(err) throw err } } async initialize(): Promise { try { janProviderStore.setAuthenticated(true) // Fetch initial models await this.getModels() console.log('Jan API client initialized successfully') } catch (error) { const errorMessage = error instanceof ApiError ? error.message : error instanceof Error ? error.message : 'Failed to initialize API client' janProviderStore.setError(errorMessage) throw error } finally { janProviderStore.setInitializing(false) } } } export const janApiClient = JanApiClient.getInstance()