diff --git a/extensions/jan-provider-extension/package.json b/extensions/jan-provider-extension/package.json new file mode 100644 index 000000000..e30be426f --- /dev/null +++ b/extensions/jan-provider-extension/package.json @@ -0,0 +1,45 @@ +{ + "name": "@janhq/jan-provider-extension", + "productName": "Jan Provider", + "version": "1.0.0", + "description": "Provides remote model inference through Jan API", + "main": "dist/index.js", + "author": "Jan ", + "license": "MIT", + "scripts": { + "build": "rolldown -c rolldown.config.mjs", + "build:publish": "rimraf *.tgz --glob || true && yarn build && npm pack && cpx *.tgz ../../pre-install", + "test": "vitest run", + "test:watch": "vitest" + }, + "exports": { + ".": "./dist/index.js", + "./main": "./dist/module.js" + }, + "devDependencies": { + "cpx": "1.5.0", + "rimraf": "6.0.1", + "rolldown": "1.0.0-beta.1", + "ts-loader": "^9.5.0", + "typescript": "5.9.2", + "vitest": "^2.1.8" + }, + "dependencies": { + "@janhq/core": "../../core/package.tgz", + "@tauri-apps/plugin-http": "2.5.0", + "zustand": "^5.0.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist/*", + "package.json", + "README.md" + ], + "bundleDependencies": [], + "installConfig": { + "hoistingLimits": "workspaces" + }, + "packageManager": "yarn@4.5.3" +} diff --git a/extensions/jan-provider-extension/rolldown.config.mjs b/extensions/jan-provider-extension/rolldown.config.mjs new file mode 100644 index 000000000..dfc093228 --- /dev/null +++ b/extensions/jan-provider-extension/rolldown.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from 'rolldown' + +export default defineConfig({ + input: 'src/index.ts', + output: { + format: 'esm', + file: 'dist/index.js', + inlineDynamicImports: true, // Required for dynamic import of @tauri-apps/plugin-http + }, + platform: 'browser', + define: { + JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'), + }, +}) diff --git a/extensions/jan-provider-extension/src/__tests__/mobile-auth.test.ts b/extensions/jan-provider-extension/src/__tests__/mobile-auth.test.ts new file mode 100644 index 000000000..df4cba5a6 --- /dev/null +++ b/extensions/jan-provider-extension/src/__tests__/mobile-auth.test.ts @@ -0,0 +1,329 @@ +/** + * Mobile Authentication Tests + * Verifies Jan Provider can authenticate and fetch models on mobile + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' + +describe('Jan Provider Mobile Authentication', () => { + const mockFetch = vi.fn() + const originalFetch = global.fetch + + beforeEach(() => { + // Setup mobile environment + ;(globalThis as any).IS_WEB_APP = false + vi.clearAllMocks() + global.fetch = mockFetch + }) + + afterEach(() => { + global.fetch = originalFetch + delete (globalThis as any).IS_WEB_APP + }) + + describe('Guest Login Flow', () => { + it('should perform guest login and get access token', async () => { + // Mock guest login response + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'guest-token-123', + expires_in: 3600, + }), + }) + + const response = await fetch('https://api.jan.ai/v1/auth/guest-login', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.ok).toBe(true) + const data = await response.json() + expect(data.access_token).toBe('guest-token-123') + expect(data.expires_in).toBe(3600) + }) + + it('should handle guest login failure', async () => { + // Mock failed guest login + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }) + + const response = await fetch('https://api.jan.ai/v1/auth/guest-login', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.ok).toBe(false) + expect(response.status).toBe(500) + }) + }) + + describe('Authenticated API Requests', () => { + it('should fetch models with guest token', async () => { + const mockModels = { + object: 'list', + data: [ + { + id: 'gpt-4o-mini', + object: 'model', + owned_by: 'openai', + }, + { + id: 'claude-3-5-sonnet-20241022', + object: 'model', + owned_by: 'anthropic', + }, + ], + } + + // Mock models response + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => mockModels, + }) + + const response = await fetch('https://api.jan.ai/v1/conv/models', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer guest-token-123', + }, + }) + + expect(response.ok).toBe(true) + const data = await response.json() + expect(data.data).toHaveLength(2) + expect(data.data[0].id).toBe('gpt-4o-mini') + expect(data.data[1].id).toBe('claude-3-5-sonnet-20241022') + }) + + it('should handle 401 unauthorized without token', async () => { + // Mock 401 response + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: 'Unauthorized', + text: async () => JSON.stringify({ + code: '019947f0-eca1-7474-8ed2-09d6e5389b54', + error: '', + }), + }) + + const response = await fetch('https://api.jan.ai/v1/conv/models', { + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.ok).toBe(false) + expect(response.status).toBe(401) + }) + + it('should include Bearer token in Authorization header', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ object: 'list', data: [] }), + }) + + await fetch('https://api.jan.ai/v1/conv/models', { + headers: { + 'Authorization': 'Bearer guest-token-123', + }, + }) + + expect(mockFetch).toHaveBeenCalledWith( + 'https://api.jan.ai/v1/conv/models', + expect.objectContaining({ + headers: expect.objectContaining({ + 'Authorization': 'Bearer guest-token-123', + }), + }) + ) + }) + }) + + describe('Token Caching and Renewal', () => { + it('should cache token and reuse until expiry', () => { + const now = Date.now() + const expiresIn = 3600 // 1 hour + const tokenExpiryTime = now + (expiresIn * 1000) - 60000 // 1 min buffer + + // First call - should use cached token + const shouldRenew1 = Date.now() >= tokenExpiryTime + expect(shouldRenew1).toBe(false) + + // Still within expiry - should use cached token + const futureTime = now + 1800000 // 30 minutes later + const shouldRenew2 = futureTime >= tokenExpiryTime + expect(shouldRenew2).toBe(false) + + // After expiry - should renew token + const expiredTime = now + 3600000 // 1 hour later + const shouldRenew3 = expiredTime >= tokenExpiryTime + expect(shouldRenew3).toBe(true) + }) + }) + + describe('API Endpoint Configuration', () => { + it('should use production API for mobile', () => { + const apiBase = 'https://api.jan.ai/v1' + + expect(apiBase).toBe('https://api.jan.ai/v1') + expect(apiBase).not.toContain('api-dev') + }) + + it('should construct correct endpoints', () => { + const apiBase = 'https://api.jan.ai/v1' + + const guestLoginEndpoint = `${apiBase}/auth/guest-login` + const modelsEndpoint = `${apiBase}/conv/models` + const chatEndpoint = `${apiBase}/conv/chat/completions` + + expect(guestLoginEndpoint).toBe('https://api.jan.ai/v1/auth/guest-login') + expect(modelsEndpoint).toBe('https://api.jan.ai/v1/conv/models') + expect(chatEndpoint).toBe('https://api.jan.ai/v1/conv/chat/completions') + }) + }) + + describe('Platform Detection', () => { + it('should detect Tauri platform correctly', () => { + const IS_WEB_APP = (globalThis as any).IS_WEB_APP + const isTauri = + typeof IS_WEB_APP === 'undefined' || + (IS_WEB_APP !== true && IS_WEB_APP !== 'true') + + expect(isTauri).toBe(true) + }) + + it('should not detect web as Tauri', () => { + ;(globalThis as any).IS_WEB_APP = true + + const IS_WEB_APP = (globalThis as any).IS_WEB_APP + const isTauri = + typeof IS_WEB_APP === 'undefined' || + (IS_WEB_APP !== true && IS_WEB_APP !== 'true') + + expect(isTauri).toBe(false) + }) + }) + + describe('Error Handling', () => { + it('should handle network errors gracefully', async () => { + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + await expect( + fetch('https://api.jan.ai/v1/conv/models') + ).rejects.toThrow('Network error') + }) + + it('should handle malformed JSON responses', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => { + throw new Error('Invalid JSON') + }, + }) + + const response = await fetch('https://api.jan.ai/v1/conv/models') + await expect(response.json()).rejects.toThrow('Invalid JSON') + }) + + it('should provide detailed error messages', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + text: async () => 'Server error details', + }) + + const response = await fetch('https://api.jan.ai/v1/conv/models') + expect(response.ok).toBe(false) + + const errorText = await response.text() + expect(errorText).toBe('Server error details') + }) + }) +}) + +describe('Integration: Full Authentication Flow', () => { + const mockFetch = vi.fn() + const originalFetch = global.fetch + + beforeEach(() => { + ;(globalThis as any).IS_WEB_APP = false + vi.clearAllMocks() + global.fetch = mockFetch + }) + + afterEach(() => { + global.fetch = originalFetch + delete (globalThis as any).IS_WEB_APP + }) + + it('should complete full flow: guest login -> fetch models -> use models', async () => { + // Step 1: Guest login + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + access_token: 'guest-token-abc', + expires_in: 3600, + }), + }) + + const loginResponse = await fetch('https://api.jan.ai/v1/auth/guest-login', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }) + + expect(loginResponse.ok).toBe(true) + const { access_token } = await loginResponse.json() + + // Step 2: Fetch models with token + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + object: 'list', + data: [ + { id: 'gpt-4o-mini', object: 'model', owned_by: 'openai' }, + { id: 'claude-3-5-sonnet-20241022', object: 'model', owned_by: 'anthropic' }, + ], + }), + }) + + const modelsResponse = await fetch('https://api.jan.ai/v1/conv/models', { + headers: { + 'Authorization': `Bearer ${access_token}`, + }, + }) + + expect(modelsResponse.ok).toBe(true) + const models = await modelsResponse.json() + + // Step 3: Verify models can be used + expect(models.data).toHaveLength(2) + expect(models.data[0].id).toBe('gpt-4o-mini') + expect(models.data[1].id).toBe('claude-3-5-sonnet-20241022') + + // Verify provider info + const janProvider = { + provider: 'jan', + models: models.data, + active: true, + } + + expect(janProvider.provider).toBe('jan') + expect(janProvider.models.length).toBeGreaterThan(0) + expect(janProvider.active).toBe(true) + }) +}) diff --git a/extensions/jan-provider-extension/src/api.ts b/extensions/jan-provider-extension/src/api.ts new file mode 100644 index 000000000..04a5d3aad --- /dev/null +++ b/extensions/jan-provider-extension/src/api.ts @@ -0,0 +1,237 @@ +/** + * Jan Provider API Client + * Handles API requests to Jan backend for models and chat completions + */ + +import { makeAuthenticatedRequest, getAuthHeader } from './auth' +import { janProviderStore } from './store' +import type { + JanModel, + JanModelsResponse, + JanChatCompletionRequest, + JanChatCompletionResponse, + JanChatCompletionChunk, +} from './types' + +/** + * Get platform-appropriate fetch (Tauri fetch for mobile, native for web) + */ +async function getPlatformFetch(): Promise { + const IS_WEB_APP = (globalThis as any).IS_WEB_APP + const isTauri = typeof IS_WEB_APP === 'undefined' || + (IS_WEB_APP !== true && IS_WEB_APP !== 'true') + + if (isTauri) { + try { + const httpPlugin = await import('@tauri-apps/plugin-http') + return httpPlugin.fetch as typeof fetch + } catch { + return fetch + } + } + + return fetch +} + +/** + * Get Jan API base URL (production only for mobile) + */ +function getApiBase(): string { + if (typeof (globalThis as any).JAN_API_BASE !== 'undefined') { + return (globalThis as any).JAN_API_BASE + } + if (typeof import.meta !== 'undefined' && import.meta.env?.JAN_API_BASE) { + return import.meta.env.JAN_API_BASE + } + + return 'https://api.jan.ai/v1' +} + +const TEMPORARY_CHAT_ID = 'temporary-chat' + +/** + * Get chat completion endpoint and payload configuration + */ +function getChatCompletionConfig( + request: JanChatCompletionRequest, + stream: boolean = false +) { + const endpoint = `${getApiBase()}/chat/completions` + + const payload = { + ...request, + stream, + conversation_id: undefined, + } + + return { endpoint, payload, isTemporaryChat: request.conversation_id === TEMPORARY_CHAT_ID } +} + +export class JanApiClient { + private static instance: JanApiClient + + private constructor() {} + + static getInstance(): JanApiClient { + if (!JanApiClient.instance) { + JanApiClient.instance = new JanApiClient() + } + return JanApiClient.instance + } + + async getModels(): Promise { + try { + janProviderStore.setLoadingModels(true) + janProviderStore.clearError() + + const apiBase = getApiBase() + const response = await makeAuthenticatedRequest( + `${apiBase}/conv/models` + ) + + const models = response.data || [] + janProviderStore.setModels(models) + + return models + } catch (error) { + const errorMessage = + 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 makeAuthenticatedRequest( + endpoint, + { + method: 'POST', + body: JSON.stringify(payload), + } + ) + } catch (error) { + const errorMessage = + 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 getAuthHeader() + const { endpoint, payload } = getChatCompletionConfig(request, true) + + const platformFetch = await getPlatformFetch() + const response = await platformFetch(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') + + 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 Error ? error : new Error('Unknown error occurred') + janProviderStore.setError(err.message) + onError?.(err) + throw err + } + } + + async initialize(): Promise { + try { + janProviderStore.setAuthenticated(true) + await this.getModels() + console.log('Jan API client initialized successfully') + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to initialize API client' + janProviderStore.setError(errorMessage) + throw error + } finally { + janProviderStore.setInitializing(false) + } + } +} + +export const janApiClient = JanApiClient.getInstance() diff --git a/extensions/jan-provider-extension/src/auth.ts b/extensions/jan-provider-extension/src/auth.ts new file mode 100644 index 000000000..0ee7da387 --- /dev/null +++ b/extensions/jan-provider-extension/src/auth.ts @@ -0,0 +1,151 @@ +/** + * Jan Auth Client for Mobile + * Handles authentication using guest login for mobile platforms + */ + +interface AuthTokens { + access_token: string + expires_in: number +} + +interface JanAuthService { + getAuthHeader(): Promise<{ Authorization: string }> + makeAuthenticatedRequest(url: string, options?: RequestInit): Promise + initialize(): Promise + isAuthenticated(): boolean +} + +declare global { + interface Window { + janAuthService?: JanAuthService + } +} + +let guestAccessToken: string | null = null +let guestTokenExpiry: number = 0 + +function getApiBase(): string { + return 'https://api.jan.ai/v1' +} + +/** + * Perform guest login to obtain access token + */ +async function guestLogin(platformFetch: typeof fetch): Promise { + const response = await platformFetch(`${getApiBase()}/auth/guest-login`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }) + + if (!response.ok) { + throw new Error(`Guest login failed: ${response.status}`) + } + + return response.json() as Promise +} + +/** + * Ensure valid guest access token, refreshing if expired + */ +async function ensureGuestToken(platformFetch: typeof fetch): Promise { + if (guestAccessToken && Date.now() < guestTokenExpiry) { + return guestAccessToken + } + + const tokens = await guestLogin(platformFetch) + guestAccessToken = tokens.access_token + guestTokenExpiry = Date.now() + (tokens.expires_in * 1000) - 60000 + + return guestAccessToken +} + +export function getAuthService(): JanAuthService | null { + return window.janAuthService || null +} + +/** + * Wait for auth service initialization (web platform) + */ +export async function waitForAuthService( + maxWaitMs: number = 5000 +): Promise { + const startTime = Date.now() + + while (Date.now() - startTime < maxWaitMs) { + const authService = getAuthService() + if (authService) return authService + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + return null +} + +/** + * Get platform-appropriate fetch (Tauri fetch for mobile, native for web) + */ +async function getPlatformFetch(): Promise { + const IS_WEB_APP = (globalThis as any).IS_WEB_APP + const isTauri = typeof IS_WEB_APP === 'undefined' || + (IS_WEB_APP !== true && IS_WEB_APP !== 'true') + + if (isTauri) { + try { + const httpPlugin = await import('@tauri-apps/plugin-http') + return httpPlugin.fetch as typeof fetch + } catch (error) { + console.warn('Tauri fetch unavailable, using native fetch') + return fetch + } + } + + return fetch +} + +/** + * Make authenticated request to Jan API with automatic guest login + */ +export async function makeAuthenticatedRequest( + url: string, + options: RequestInit = {} +): Promise { + const authService = await waitForAuthService() + + if (authService) { + return authService.makeAuthenticatedRequest(url, options) + } + + const platformFetch = await getPlatformFetch() + const accessToken = await ensureGuestToken(platformFetch) + + const response = await platformFetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + ...options.headers, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + return response.json() +} + +/** + * Get authorization header with guest token + */ +export async function getAuthHeader(): Promise<{ Authorization?: string }> { + const authService = await waitForAuthService() + + if (authService) { + return authService.getAuthHeader() + } + + const platformFetch = await getPlatformFetch() + const accessToken = await ensureGuestToken(platformFetch) + return { Authorization: `Bearer ${accessToken}` } +} diff --git a/extensions/jan-provider-extension/src/index.ts b/extensions/jan-provider-extension/src/index.ts new file mode 100644 index 000000000..4d3a4008a --- /dev/null +++ b/extensions/jan-provider-extension/src/index.ts @@ -0,0 +1 @@ +export { default } from './provider' diff --git a/extensions/jan-provider-extension/src/provider.ts b/extensions/jan-provider-extension/src/provider.ts new file mode 100644 index 000000000..3df96f484 --- /dev/null +++ b/extensions/jan-provider-extension/src/provider.ts @@ -0,0 +1,459 @@ +/** + * Jan Provider Extension + * Provides remote model inference through Jan API + * Available on web and mobile platforms (disabled on desktop Tauri) + */ + +import { + AIEngine, + modelInfo, + SessionInfo, + UnloadResult, + chatCompletionRequest, + chatCompletion, + chatCompletionChunk, + ImportOptions, +} from '@janhq/core' +import { janApiClient } from './api' +import { janProviderStore } from './store' +import type { JanChatMessage } from './types' + +// Jan models support tools via MCP +const JAN_MODEL_CAPABILITIES = ['tools'] as const + +export default class JanProviderExtension extends AIEngine { + readonly provider = 'jan' + private activeSessions: Map = new Map() + private isDesktopTauri: boolean = false + + override async onLoad() { + // Detect if we're running on desktop Tauri (not mobile) + this.isDesktopTauri = this.detectDesktopTauri() + + // On desktop Tauri, do not load this extension + if (this.isDesktopTauri) { + console.log( + 'Jan Provider Extension: Disabled on desktop Tauri (use llamacpp instead)' + ) + return + } + + console.log('Loading Jan Provider Extension...') + + try { + // Check and clear invalid Jan models (capabilities mismatch) + this.validateJanModelsLocalStorage() + + // Don't initialize here - wait until auth service is ready + // Models will be fetched lazily when list() is called + console.log('Jan Provider Extension loaded successfully (models will be fetched on demand)') + } catch (error) { + console.error('Failed to load Jan Provider Extension:', error) + throw error + } + + super.onLoad() + } + + /** + * Detect if we're on desktop Tauri (not mobile) + * Mobile platforms (iOS/Android) will have IS_IOS or IS_ANDROID set + * Web platform will have IS_WEB_APP set to true + * Desktop Tauri will have neither (or IS_WEB_APP = false) + */ + private detectDesktopTauri(): boolean { + // Check if we're in a browser environment + if (typeof window === 'undefined') { + return false + } + + // Check for mobile-specific flags + const IS_IOS = (window as any).IS_IOS + const IS_ANDROID = (window as any).IS_ANDROID + const IS_WEB_APP = (window as any).IS_WEB_APP + + // If we're on mobile, not desktop + if (IS_IOS === true || IS_ANDROID === true) { + return false + } + + // If we're on web app, not desktop + if (IS_WEB_APP === true || IS_WEB_APP === 'true') { + return false + } + + // Check if Tauri API is available + const hasTauriAPI = !!(window as any).__TAURI__ + + // If Tauri API is available and we're not on mobile or web, we're on desktop Tauri + return hasTauriAPI + } + + // Verify Jan models capabilities in localStorage + private validateJanModelsLocalStorage() { + try { + console.log('Validating Jan models in localStorage...') + const storageKey = 'model-provider' + const data = localStorage.getItem(storageKey) + if (!data) return + + const parsed = JSON.parse(data) + if (!parsed?.state?.providers) return + + let hasInvalidModel = false + + for (const provider of parsed.state.providers) { + if (provider.provider === 'jan' && provider.models) { + for (const model of provider.models) { + console.log(`Checking Jan model: ${model.id}`, model.capabilities) + if ( + JSON.stringify(model.capabilities) !== + JSON.stringify(JAN_MODEL_CAPABILITIES) + ) { + hasInvalidModel = true + console.log( + `Found invalid Jan model: ${model.id}, clearing localStorage` + ) + break + } + } + } + if (hasInvalidModel) break + } + + if (hasInvalidModel) { + localStorage.removeItem(storageKey) + const afterRemoval = localStorage.getItem(storageKey) + if (afterRemoval) { + localStorage.setItem( + storageKey, + JSON.stringify({ + state: { providers: [] }, + version: parsed.version || 3, + }) + ) + } + console.log( + 'Cleared model-provider from localStorage due to invalid Jan capabilities' + ) + window.location.reload() + } + } catch (error) { + console.error('Failed to check Jan models:', error) + } + } + + override async onUnload() { + console.log('Unloading Jan Provider Extension...') + + for (const sessionId of this.activeSessions.keys()) { + await this.unload(sessionId) + } + + janProviderStore.reset() + console.log('Jan Provider Extension unloaded') + } + + async get(modelId: string): Promise { + if (this.isDesktopTauri) return undefined + + return janApiClient + .getModels() + .then((list) => list.find((e) => e.id === modelId)) + .then((model) => + model + ? { + id: model.id, + name: model.id, + quant_type: undefined, + providerId: this.provider, + port: 443, + sizeBytes: 0, + tags: [], + path: undefined, + owned_by: model.owned_by, + object: model.object, + capabilities: [...JAN_MODEL_CAPABILITIES], + } + : undefined + ) + } + + async list(): Promise { + if (this.isDesktopTauri) return [] + + try { + const janModels = await janApiClient.getModels() + + return janModels.map((model) => ({ + id: model.id, + name: model.id, + quant_type: undefined, + providerId: this.provider, + port: 443, + sizeBytes: 0, + tags: [], + path: undefined, + owned_by: model.owned_by, + object: model.object, + capabilities: [...JAN_MODEL_CAPABILITIES], + })) + } catch (error) { + console.error('Failed to list Jan models:', error) + throw error + } + } + + async load(modelId: string, _settings?: any): Promise { + try { + const sessionId = `jan-${modelId}-${Date.now()}` + + const sessionInfo: SessionInfo = { + pid: Date.now(), + port: 443, + model_id: modelId, + model_path: `remote:${modelId}`, + api_key: '', + } + + this.activeSessions.set(sessionId, sessionInfo) + + console.log( + `Jan model session created: ${sessionId} for model ${modelId}` + ) + return sessionInfo + } catch (error) { + console.error(`Failed to load Jan model ${modelId}:`, error) + throw error + } + } + + async unload(sessionId: string): Promise { + try { + const session = this.activeSessions.get(sessionId) + + if (!session) { + return { + success: false, + error: `Session ${sessionId} not found`, + } + } + + this.activeSessions.delete(sessionId) + console.log(`Jan model session unloaded: ${sessionId}`) + + return { success: true } + } catch (error) { + console.error(`Failed to unload Jan session ${sessionId}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } + } + + async chat( + opts: chatCompletionRequest, + abortController?: AbortController + ): Promise> { + try { + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + const modelId = opts.model + if (!modelId) { + throw new Error('Model ID is required') + } + + const janMessages: JanChatMessage[] = opts.messages.map((msg) => ({ + role: msg.role as 'system' | 'user' | 'assistant', + content: + typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content), + })) + + const janRequest = { + model: modelId, + messages: janMessages, + conversation_id: opts.thread_id, + temperature: opts.temperature ?? undefined, + max_tokens: opts.n_predict ?? undefined, + top_p: opts.top_p ?? undefined, + frequency_penalty: opts.frequency_penalty ?? undefined, + presence_penalty: opts.presence_penalty ?? undefined, + stream: opts.stream ?? false, + stop: opts.stop ?? undefined, + tools: opts.tools ?? undefined, + tool_choice: opts.tool_choice ?? undefined, + } + + if (opts.stream) { + return this.createStreamingGenerator(janRequest, abortController) + } else { + const response = await janApiClient.createChatCompletion(janRequest) + + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + return { + id: response.id, + object: 'chat.completion' as const, + created: response.created, + model: response.model, + choices: response.choices.map((choice) => ({ + index: choice.index, + message: { + role: choice.message.role, + content: choice.message.content, + reasoning: choice.message.reasoning, + reasoning_content: choice.message.reasoning_content, + tool_calls: choice.message.tool_calls, + }, + finish_reason: (choice.finish_reason || 'stop') as + | 'stop' + | 'length' + | 'tool_calls' + | 'content_filter' + | 'function_call', + })), + usage: response.usage, + } + } + } catch (error) { + console.error('Jan chat completion failed:', error) + throw error + } + } + + private async *createStreamingGenerator( + janRequest: any, + abortController?: AbortController + ) { + let resolve: () => void + let reject: (error: Error) => void + const chunks: any[] = [] + let isComplete = false + let error: Error | null = null + + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + const abortListener = () => { + error = new Error('Request was aborted') + reject(error) + } + + if (abortController?.signal) { + if (abortController.signal.aborted) { + throw new Error('Request was aborted') + } + abortController.signal.addEventListener('abort', abortListener) + } + + try { + janApiClient.createStreamingChatCompletion( + janRequest, + (chunk) => { + if (abortController?.signal?.aborted) { + return + } + const streamChunk = { + id: chunk.id, + object: chunk.object, + created: chunk.created, + model: chunk.model, + choices: chunk.choices.map((choice) => ({ + index: choice.index, + delta: { + role: choice.delta.role, + content: choice.delta.content, + reasoning: choice.delta.reasoning, + reasoning_content: choice.delta.reasoning_content, + tool_calls: choice.delta.tool_calls, + }, + finish_reason: choice.finish_reason, + })), + } + chunks.push(streamChunk) + }, + () => { + isComplete = true + resolve() + }, + (err) => { + error = err + reject(err) + } + ) + + let yieldedIndex = 0 + while (!isComplete && !error) { + if (abortController?.signal?.aborted) { + throw new Error('Request was aborted') + } + + while (yieldedIndex < chunks.length) { + yield chunks[yieldedIndex] + yieldedIndex++ + } + + await new Promise((resolve) => setTimeout(resolve, 10)) + } + + while (yieldedIndex < chunks.length) { + yield chunks[yieldedIndex] + yieldedIndex++ + } + + if (error) { + throw error + } + + await promise + } finally { + if (abortController?.signal) { + abortController.signal.removeEventListener('abort', abortListener) + } + } + } + + async delete(modelId: string): Promise { + throw new Error( + `Delete operation not supported for remote Jan API model: ${modelId}` + ) + } + + async update(modelId: string, _model: Partial): Promise { + throw new Error( + `Update operation not supported for remote Jan API model: ${modelId}` + ) + } + + async import(modelId: string, _opts: ImportOptions): Promise { + throw new Error( + `Import operation not supported for remote Jan API model: ${modelId}` + ) + } + + async abortImport(modelId: string): Promise { + throw new Error( + `Abort import operation not supported for remote Jan API model: ${modelId}` + ) + } + + async getLoadedModels(): Promise { + return Array.from(this.activeSessions.values()).map( + (session) => session.model_id + ) + } + + async isToolSupported(modelId: string): Promise { + console.log(`Checking tool support for Jan model ${modelId}: supported`) + return true + } +} diff --git a/extensions/jan-provider-extension/src/store.ts b/extensions/jan-provider-extension/src/store.ts new file mode 100644 index 000000000..435ba92f9 --- /dev/null +++ b/extensions/jan-provider-extension/src/store.ts @@ -0,0 +1,90 @@ +/** + * Jan Provider Store + * Zustand-based state management for Jan provider authentication and models + */ + +import { create } from 'zustand' +import type { JanModel } from './types' + +export interface JanProviderState { + isAuthenticated: boolean + isInitializing: boolean + models: JanModel[] + isLoadingModels: boolean + error: string | null +} + +export interface JanProviderActions { + setAuthenticated: (isAuthenticated: boolean) => void + setInitializing: (isInitializing: boolean) => void + setModels: (models: JanModel[]) => void + setLoadingModels: (isLoadingModels: boolean) => void + setError: (error: string | null) => void + clearError: () => void + reset: () => void +} + +export type JanProviderStore = JanProviderState & JanProviderActions + +const initialState: JanProviderState = { + isAuthenticated: false, + isInitializing: true, + models: [], + isLoadingModels: false, + error: null, +} + +export const useJanProviderStore = create((set) => ({ + ...initialState, + + setAuthenticated: (isAuthenticated: boolean) => + set({ isAuthenticated, error: null }), + + setInitializing: (isInitializing: boolean) => + set({ isInitializing }), + + setModels: (models: JanModel[]) => + set({ models, isLoadingModels: false }), + + setLoadingModels: (isLoadingModels: boolean) => + set({ isLoadingModels }), + + setError: (error: string | null) => + set({ error }), + + clearError: () => + set({ error: null }), + + reset: () => + set({ + isAuthenticated: false, + isInitializing: false, + models: [], + isLoadingModels: false, + error: null, + }), +})) + +// Export a store instance for non-React usage +export const janProviderStore = { + // Store access methods + getState: useJanProviderStore.getState, + setState: useJanProviderStore.setState, + subscribe: useJanProviderStore.subscribe, + + // Direct action methods + setAuthenticated: (isAuthenticated: boolean) => + useJanProviderStore.getState().setAuthenticated(isAuthenticated), + setInitializing: (isInitializing: boolean) => + useJanProviderStore.getState().setInitializing(isInitializing), + setModels: (models: JanModel[]) => + useJanProviderStore.getState().setModels(models), + setLoadingModels: (isLoadingModels: boolean) => + useJanProviderStore.getState().setLoadingModels(isLoadingModels), + setError: (error: string | null) => + useJanProviderStore.getState().setError(error), + clearError: () => + useJanProviderStore.getState().clearError(), + reset: () => + useJanProviderStore.getState().reset(), +} diff --git a/extensions/jan-provider-extension/src/types.ts b/extensions/jan-provider-extension/src/types.ts new file mode 100644 index 000000000..41925c698 --- /dev/null +++ b/extensions/jan-provider-extension/src/types.ts @@ -0,0 +1,89 @@ +/** + * Jan Provider Types + */ + +export interface JanModel { + id: string + object: string + owned_by: string +} + +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 ApiError extends Error { + constructor( + public status: number, + public statusText: string, + public details?: string + ) { + super(`API Error ${status}: ${statusText}${details ? ` - ${details}` : ''}`) + this.name = 'ApiError' + } + + isStatus(code: number): boolean { + return this.status === code + } +} diff --git a/extensions/jan-provider-extension/tsconfig.json b/extensions/jan-provider-extension/tsconfig.json new file mode 100644 index 000000000..cc350c637 --- /dev/null +++ b/extensions/jan-provider-extension/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../core/tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/extensions/jan-provider-extension/vitest.config.ts b/extensions/jan-provider-extension/vitest.config.ts new file mode 100644 index 000000000..d87fc4a69 --- /dev/null +++ b/extensions/jan-provider-extension/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}) diff --git a/extensions/yarn.lock b/extensions/yarn.lock index c5f37ba35..5a33cc2bd 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -92,6 +92,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/aix-ppc64@npm:0.25.9" @@ -99,6 +106,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-arm64@npm:0.25.9" @@ -106,6 +120,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-arm@npm:0.25.9" @@ -113,6 +134,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/android-x64@npm:0.25.9" @@ -120,6 +148,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/darwin-arm64@npm:0.25.9" @@ -127,6 +162,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/darwin-x64@npm:0.25.9" @@ -134,6 +176,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/freebsd-arm64@npm:0.25.9" @@ -141,6 +190,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/freebsd-x64@npm:0.25.9" @@ -148,6 +204,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-arm64@npm:0.25.9" @@ -155,6 +218,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-arm@npm:0.25.9" @@ -162,6 +232,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-ia32@npm:0.25.9" @@ -169,6 +246,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-loong64@npm:0.25.9" @@ -176,6 +260,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-mips64el@npm:0.25.9" @@ -183,6 +274,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-ppc64@npm:0.25.9" @@ -190,6 +288,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-riscv64@npm:0.25.9" @@ -197,6 +302,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-s390x@npm:0.25.9" @@ -204,6 +316,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/linux-x64@npm:0.25.9" @@ -218,6 +337,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/netbsd-x64@npm:0.25.9" @@ -232,6 +358,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/openbsd-x64@npm:0.25.9" @@ -246,6 +379,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/sunos-x64@npm:0.25.9" @@ -253,6 +393,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-arm64@npm:0.25.9" @@ -260,6 +407,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-ia32@npm:0.25.9" @@ -267,6 +421,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.9": version: 0.25.9 resolution: "@esbuild/win32-x64@npm:0.25.9" @@ -376,6 +537,18 @@ __metadata: languageName: node linkType: hard +"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fjan-provider-extension%40workspace%3Ajan-provider-extension": + version: 0.1.10 + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=bfbced&locator=%40janhq%2Fjan-provider-extension%40workspace%3Ajan-provider-extension" + dependencies: + rxjs: "npm:^7.8.1" + ulidx: "npm:^2.3.0" + peerDependencies: + react: 19.0.0 + checksum: 10c0/67ebb430e1e8433441ce3b24d0ce88ce3f079d99c6518bf71492edeaefbc7a774b5f17c6f34282941e466a30787b711a0779ccb0fd28fe8376f1967edb581b53 + languageName: node + linkType: hard + "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension": version: 0.1.10 resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=bfbced&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension" @@ -403,6 +576,22 @@ __metadata: languageName: unknown linkType: soft +"@janhq/jan-provider-extension@workspace:jan-provider-extension": + version: 0.0.0-use.local + resolution: "@janhq/jan-provider-extension@workspace:jan-provider-extension" + dependencies: + "@janhq/core": ../../core/package.tgz + "@tauri-apps/plugin-http": "npm:2.5.0" + cpx: "npm:1.5.0" + rimraf: "npm:6.0.1" + rolldown: "npm:1.0.0-beta.1" + ts-loader: "npm:^9.5.0" + typescript: "npm:5.9.2" + vitest: "npm:^2.1.8" + zustand: "npm:^5.0.3" + languageName: unknown + linkType: soft + "@janhq/llamacpp-extension@workspace:llamacpp-extension": version: 0.0.0-use.local resolution: "@janhq/llamacpp-extension@workspace:llamacpp-extension" @@ -445,6 +634,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.4": version: 0.2.6 resolution: "@napi-rs/wasm-runtime@npm:0.2.6" @@ -585,6 +781,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.52.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-android-arm64@npm:4.50.1" @@ -592,6 +795,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-android-arm64@npm:4.52.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-darwin-arm64@npm:4.50.1" @@ -599,6 +809,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.52.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-darwin-x64@npm:4.50.1" @@ -606,6 +823,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.52.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.1" @@ -613,6 +837,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.52.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-freebsd-x64@npm:4.50.1" @@ -620,6 +851,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.52.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1" @@ -627,6 +865,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1" @@ -634,6 +879,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.52.4" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.1" @@ -641,6 +893,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.52.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.1" @@ -648,6 +907,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.52.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.52.4" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1" @@ -662,6 +935,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.52.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1" @@ -669,6 +949,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.52.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1" @@ -676,6 +963,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.52.4" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.1" @@ -683,6 +977,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.52.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.1" @@ -690,6 +991,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.52.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.1" @@ -697,6 +1005,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.52.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-openharmony-arm64@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1" @@ -704,6 +1019,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-openharmony-arm64@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.52.4" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.1" @@ -711,6 +1033,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.52.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.1" @@ -718,6 +1047,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.52.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.52.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.50.1": version: 4.50.1 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.1" @@ -725,6 +1068,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.52.4": + version: 4.52.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.52.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@tauri-apps/api@npm:2.8.0, @tauri-apps/api@npm:^2.6.0, @tauri-apps/api@npm:^2.8.0": version: 2.8.0 resolution: "@tauri-apps/api@npm:2.8.0" @@ -789,6 +1139,18 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/expect@npm:2.1.9" + dependencies: + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/98d1cf02917316bebef9e4720723e38298a1c12b3c8f3a81f259bb822de4288edf594e69ff64f0b88afbda6d04d7a4f0c2f720f3fec16b4c45f5e2669f09fdbb + languageName: node + linkType: hard + "@vitest/expect@npm:3.2.4": version: 3.2.4 resolution: "@vitest/expect@npm:3.2.4" @@ -802,6 +1164,25 @@ __metadata: languageName: node linkType: hard +"@vitest/mocker@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/mocker@npm:2.1.9" + dependencies: + "@vitest/spy": "npm:2.1.9" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.12" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f734490d8d1206a7f44dfdfca459282f5921d73efa72935bb1dc45307578defd38a4131b14853316373ec364cbe910dbc74594ed4137e0da35aa4d9bb716f190 + languageName: node + linkType: hard + "@vitest/mocker@npm:3.2.4": version: 3.2.4 resolution: "@vitest/mocker@npm:3.2.4" @@ -821,7 +1202,7 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.1.9": +"@vitest/pretty-format@npm:2.1.9, @vitest/pretty-format@npm:^2.1.9": version: 2.1.9 resolution: "@vitest/pretty-format@npm:2.1.9" dependencies: @@ -839,6 +1220,16 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/runner@npm:2.1.9" + dependencies: + "@vitest/utils": "npm:2.1.9" + pathe: "npm:^1.1.2" + checksum: 10c0/e81f176badb12a815cbbd9bd97e19f7437a0b64e8934d680024b0f768d8670d59cad698ef0e3dada5241b6731d77a7bb3cd2c7cb29f751fd4dd35eb11c42963a + languageName: node + linkType: hard + "@vitest/runner@npm:3.2.4": version: 3.2.4 resolution: "@vitest/runner@npm:3.2.4" @@ -850,6 +1241,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/snapshot@npm:2.1.9" + dependencies: + "@vitest/pretty-format": "npm:2.1.9" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + checksum: 10c0/394974b3a1fe96186a3c87f933b2f7f1f7b7cc42f9c781d80271dbb4c987809bf035fecd7398b8a3a2d54169e3ecb49655e38a0131d0e7fea5ce88960613b526 + languageName: node + linkType: hard + "@vitest/snapshot@npm:3.2.4": version: 3.2.4 resolution: "@vitest/snapshot@npm:3.2.4" @@ -861,6 +1263,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.1.9": + version: 2.1.9 + resolution: "@vitest/spy@npm:2.1.9" + dependencies: + tinyspy: "npm:^3.0.2" + checksum: 10c0/12a59b5095e20188b819a1d797e0a513d991b4e6a57db679927c43b362a3eff52d823b34e855a6dd9e73c9fa138dcc5ef52210841a93db5cbf047957a60ca83c + languageName: node + linkType: hard + "@vitest/spy@npm:3.2.4": version: 3.2.4 resolution: "@vitest/spy@npm:3.2.4" @@ -1186,6 +1597,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.2": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/b360fd4d38861622e5010c2f709736988b05c7f31042305fa3f4e9911f6adb80ccfb4e302068bf8ed10e835c2e2520cba0f5edc13d878b886987e5aa62483f53 + languageName: node + linkType: hard + "chai@npm:^5.2.0": version: 5.2.0 resolution: "chai@npm:5.2.0" @@ -1389,6 +1813,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.7": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" @@ -1518,13 +1954,93 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.7.0": +"es-module-lexer@npm:^1.5.4, es-module-lexer@npm:^1.7.0": version: 1.7.0 resolution: "es-module-lexer@npm:1.7.0" checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b languageName: node linkType: hard +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de + languageName: node + linkType: hard + "esbuild@npm:^0.25.0": version: 0.25.9 resolution: "esbuild@npm:0.25.9" @@ -1656,7 +2172,7 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.2.1": +"expect-type@npm:^1.1.0, expect-type@npm:^1.2.1": version: 1.2.2 resolution: "expect-type@npm:1.2.2" checksum: 10c0/6019019566063bbc7a690d9281d920b1a91284a4a093c2d55d71ffade5ac890cf37a51e1da4602546c4b56569d2ad2fc175a2ccee77d1ae06cb3af91ef84f44b @@ -2485,6 +3001,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.19 + resolution: "magic-string@npm:0.30.19" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/db23fd2e2ee98a1aeb88a4cdb2353137fcf05819b883c856dd79e4c7dfb25151e2a5a4d5dbd88add5e30ed8ae5c51bcf4accbc6becb75249d924ec7b4fbcae27 + languageName: node + linkType: hard + "magic-string@npm:^0.30.17": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -3021,7 +3546,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.5.6": +"postcss@npm:^8.4.43, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -3288,6 +3813,87 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.52.4 + resolution: "rollup@npm:4.52.4" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.52.4" + "@rollup/rollup-android-arm64": "npm:4.52.4" + "@rollup/rollup-darwin-arm64": "npm:4.52.4" + "@rollup/rollup-darwin-x64": "npm:4.52.4" + "@rollup/rollup-freebsd-arm64": "npm:4.52.4" + "@rollup/rollup-freebsd-x64": "npm:4.52.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.52.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.52.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.52.4" + "@rollup/rollup-linux-loong64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-riscv64-musl": "npm:4.52.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.52.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.52.4" + "@rollup/rollup-linux-x64-musl": "npm:4.52.4" + "@rollup/rollup-openharmony-arm64": "npm:4.52.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.52.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.52.4" + "@rollup/rollup-win32-x64-gnu": "npm:4.52.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.52.4" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/aaec0f57e887d4fb37d152f93cf7133954eec79d11643e95de768ec9a377f08793b1745c648ca65a0dcc6c795c4d9ca398724d013e5745de270e88a543782aea + languageName: node + linkType: hard + "rollup@npm:^4.43.0": version: 4.50.1 resolution: "rollup@npm:4.50.1" @@ -3653,7 +4259,7 @@ __metadata: languageName: node linkType: hard -"std-env@npm:^3.9.0": +"std-env@npm:^3.8.0, std-env@npm:^3.9.0": version: 3.9.0 resolution: "std-env@npm:3.9.0" checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50 @@ -3778,7 +4384,7 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.2": +"tinyexec@npm:^0.3.1, tinyexec@npm:^0.3.2": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 @@ -3795,7 +4401,7 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^1.1.1": +"tinypool@npm:^1.0.1, tinypool@npm:^1.1.1": version: 1.1.1 resolution: "tinypool@npm:1.1.1" checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b @@ -3816,6 +4422,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^3.0.2": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10c0/55ffad24e346622b59292e097c2ee30a63919d5acb7ceca87fc0d1c223090089890587b426e20054733f97a58f20af2c349fb7cc193697203868ab7ba00bcea0 + languageName: node + linkType: hard + "tinyspy@npm:^4.0.3": version: 4.0.3 resolution: "tinyspy@npm:4.0.3" @@ -4039,6 +4652,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.9": + version: 2.1.9 + resolution: "vite-node@npm:2.1.9" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.7" + es-module-lexer: "npm:^1.5.4" + pathe: "npm:^1.1.2" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/0d3589f9f4e9cff696b5b49681fdb75d1638c75053728be52b4013f70792f38cb0120a9c15e3a4b22bdd6b795ad7c2da13bcaf47242d439f0906049e73bdd756 + languageName: node + linkType: hard + "vite-node@npm:3.2.4": version: 3.2.4 resolution: "vite-node@npm:3.2.4" @@ -4054,6 +4682,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.4.20 + resolution: "vite@npm:5.4.20" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.43" + rollup: "npm:^4.20.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/391a1fdd7e05445d60aa3b15d6c1cffcdd92c5d154da375bf06b9cd5633c2387ebee0e8f2fceed3226a63dff36c8ef18fb497662dde8c135133c46670996c7a1 + languageName: node + linkType: hard + "vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": version: 7.1.5 resolution: "vite@npm:7.1.5" @@ -4165,6 +4836,56 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.1.8": + version: 2.1.9 + resolution: "vitest@npm:2.1.9" + dependencies: + "@vitest/expect": "npm:2.1.9" + "@vitest/mocker": "npm:2.1.9" + "@vitest/pretty-format": "npm:^2.1.9" + "@vitest/runner": "npm:2.1.9" + "@vitest/snapshot": "npm:2.1.9" + "@vitest/spy": "npm:2.1.9" + "@vitest/utils": "npm:2.1.9" + chai: "npm:^5.1.2" + debug: "npm:^4.3.7" + expect-type: "npm:^1.1.0" + magic-string: "npm:^0.30.12" + pathe: "npm:^1.1.2" + std-env: "npm:^3.8.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.1" + tinypool: "npm:^1.0.1" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.1.9" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.9 + "@vitest/ui": 2.1.9 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/e339e16dccacf4589ff43cb1f38c7b4d14427956ae8ef48702af6820a9842347c2b6c77356aeddb040329759ca508a3cb2b104ddf78103ea5bc98ab8f2c3a54e + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^5.0.0": version: 5.0.0 resolution: "w3c-xmlserializer@npm:5.0.0" @@ -4319,3 +5040,24 @@ __metadata: checksum: 10c0/0223d21dbaa15d8928fe0da3b54696391d8e3e1e2d0283a1a070b5980a1dbba945ce631c2d1eccc088fdbad0f2dfa40155590bf83732d3ac4fcca2cc9237591b languageName: node linkType: hard + +"zustand@npm:^5.0.3": + version: 5.0.8 + resolution: "zustand@npm:5.0.8" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10c0/e865a6f7f1c0e03571701db5904151aaa7acefd6f85541a117085e129bf16e8f60b11f8cc82f277472de5c7ad5dcb201e1b0a89035ea0b40c9d9aab3cd24c8ad + languageName: node + linkType: hard diff --git a/src-tauri/tauri.android.conf.json b/src-tauri/tauri.android.conf.json index 2f1144c20..1a1d2cbd0 100644 --- a/src-tauri/tauri.android.conf.json +++ b/src-tauri/tauri.android.conf.json @@ -3,8 +3,8 @@ "build": { "devUrl": null, "frontendDist": "../web-app/dist", - "beforeDevCommand": "cross-env IS_DEV=true IS_ANDROID=true yarn build:web", - "beforeBuildCommand": "cross-env IS_ANDROID=true yarn build:web" + "beforeDevCommand": "cross-env IS_ANDROID=true yarn build:extensions && cross-env IS_DEV=true IS_ANDROID=true yarn build:web", + "beforeBuildCommand": "cross-env IS_ANDROID=true yarn build:extensions && cross-env IS_ANDROID=true yarn build:web" }, "app": { "security": { diff --git a/web-app/src/__tests__/jan-provider-mobile.test.ts b/web-app/src/__tests__/jan-provider-mobile.test.ts new file mode 100644 index 000000000..05ef32497 --- /dev/null +++ b/web-app/src/__tests__/jan-provider-mobile.test.ts @@ -0,0 +1,163 @@ +/** + * Jan Provider Mobile Integration Tests + * Verifies that Jan Provider extension loads correctly on mobile platforms + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest' + +describe('Jan Provider on Mobile Platforms', () => { + beforeEach(() => { + // Clear any existing global state + vi.clearAllMocks() + }) + + describe('Platform Detection', () => { + it('should enable on iOS platform', () => { + // Simulate iOS environment + ;(global as any).IS_IOS = true + ;(global as any).IS_ANDROID = false + ;(global as any).__TAURI__ = { invoke: vi.fn() } + + // Extension should detect iOS and remain enabled + const isDesktopTauri = !( + (global as any).IS_IOS === true || (global as any).IS_ANDROID === true + ) + + expect(isDesktopTauri).toBe(false) + }) + + it('should enable on Android platform', () => { + // Simulate Android environment + ;(global as any).IS_IOS = false + ;(global as any).IS_ANDROID = true + ;(global as any).__TAURI__ = { invoke: vi.fn() } + + // Extension should detect Android and remain enabled + const isDesktopTauri = !( + (global as any).IS_IOS === true || (global as any).IS_ANDROID === true + ) + + expect(isDesktopTauri).toBe(false) + }) + + it('should disable on desktop Tauri', () => { + // Simulate desktop Tauri environment + ;(global as any).IS_IOS = false + ;(global as any).IS_ANDROID = false + ;(global as any).__TAURI__ = { invoke: vi.fn() } + + // Extension should detect desktop and disable + const isDesktopTauri = !( + (global as any).IS_IOS === true || (global as any).IS_ANDROID === true + ) + + expect(isDesktopTauri).toBe(true) + }) + + it('should enable on web platform', () => { + // Simulate web environment (no Tauri) + ;(global as any).IS_IOS = false + ;(global as any).IS_ANDROID = false + delete (global as any).__TAURI__ + + // Extension should work on web + const hasTauriAPI = !!(global as any).__TAURI__ + expect(hasTauriAPI).toBe(false) + }) + }) + + describe('Authentication Requirements', () => { + it('should require auth service for API calls', () => { + const hasAuthService = typeof (global as any).window?.janAuthService !== 'undefined' + + // Auth service should be available or waited for + expect( + hasAuthService || + 'waitForAuthService should be called before API requests' + ).toBeTruthy() + }) + }) + + describe('API Configuration', () => { + it('should use correct default API base', () => { + const expectedDefault = 'https://api.jan.ai/v1' + + // The getApiBase function should return the default when not configured + const getApiBase = (): string => { + if (typeof (globalThis as any).JAN_API_BASE !== 'undefined') { + return (globalThis as any).JAN_API_BASE + } + if (typeof import.meta !== 'undefined' && import.meta.env?.JAN_API_BASE) { + return import.meta.env.JAN_API_BASE + } + return expectedDefault + } + + expect(getApiBase()).toBe(expectedDefault) + }) + + it('should respect environment variable override', () => { + const customBase = 'https://custom.api.jan.ai/v1' + ;(globalThis as any).JAN_API_BASE = customBase + + const getApiBase = (): string => { + if (typeof (globalThis as any).JAN_API_BASE !== 'undefined') { + return (globalThis as any).JAN_API_BASE + } + return 'https://api.jan.ai/v1' + } + + expect(getApiBase()).toBe(customBase) + + // Cleanup + delete (globalThis as any).JAN_API_BASE + }) + }) + + describe('Model Capabilities', () => { + it('should support tools capability', () => { + const JAN_MODEL_CAPABILITIES = ['tools'] as const + + expect(JAN_MODEL_CAPABILITIES).toContain('tools') + expect(JAN_MODEL_CAPABILITIES.length).toBe(1) + }) + }) + + describe('Extension Bundling', () => { + it('should be included in mobile bundled extensions', () => { + const bundledExtensions = [ + '@janhq/conversational-extension', + '@janhq/jan-provider-extension', // Should be present + ] + + expect(bundledExtensions).toContain('@janhq/jan-provider-extension') + }) + }) +}) + +describe('Provider Service Integration', () => { + it('should handle provider loading errors gracefully', async () => { + const mockProviders = [] + + // Simulate a provider that fails to load + const loadProvider = async (providerName: string) => { + try { + if (providerName === 'failing-provider') { + throw new Error('Provider failed to load') + } + mockProviders.push({ provider: providerName }) + } catch (error) { + console.warn(`Failed to load runtime provider ${providerName}:`, error) + // Should continue with other providers + } + } + + await loadProvider('jan') + await loadProvider('failing-provider') + await loadProvider('llamacpp') + + // Jan provider should have loaded successfully + expect(mockProviders).toHaveLength(2) + expect(mockProviders.some(p => p.provider === 'jan')).toBe(true) + }) +}) diff --git a/web-app/src/services/core/mobile.ts b/web-app/src/services/core/mobile.ts index e5aedefa0..51af412ff 100644 --- a/web-app/src/services/core/mobile.ts +++ b/web-app/src/services/core/mobile.ts @@ -9,6 +9,7 @@ import { TauriCoreService } from './tauri' import type { ExtensionManifest } from '@/lib/extension' import JanConversationalExtension from '@janhq/conversational-extension' +import JanProviderExtension from '@janhq/jan-provider-extension' export class MobileCoreService extends TauriCoreService { /** @@ -54,6 +55,15 @@ export class MobileCoreService extends TauriCoreService { '1.0.0' ) + const janProviderExt = new JanProviderExtension( + 'built-in', + '@janhq/jan-provider-extension', + 'Jan Provider', + true, + 'Provides remote model inference through Jan API', + '1.0.0' + ) + return [ { name: '@janhq/conversational-extension', @@ -64,6 +74,15 @@ export class MobileCoreService extends TauriCoreService { version: '1.0.0', extensionInstance: conversationalExt, }, + { + name: '@janhq/jan-provider-extension', + productName: 'Jan Provider', + url: 'built-in', + active: true, + description: 'Provides remote model inference through Jan API', + version: '1.0.0', + extensionInstance: janProviderExt, + }, ] } } diff --git a/web-app/tsconfig.app.json b/web-app/tsconfig.app.json index c672a79f1..74bd12aec 100644 --- a/web-app/tsconfig.app.json +++ b/web-app/tsconfig.app.json @@ -26,7 +26,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@janhq/conversational-extension": ["../extensions/conversational-extension/src/index.ts"] + "@janhq/conversational-extension": ["../extensions/conversational-extension/src/index.ts"], + "@janhq/jan-provider-extension": ["../extensions/jan-provider-extension/src/index.ts"] } }, "include": ["src"], diff --git a/web-app/tsconfig.json b/web-app/tsconfig.json index ab1a13f13..49fd2f78b 100644 --- a/web-app/tsconfig.json +++ b/web-app/tsconfig.json @@ -8,7 +8,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@janhq/conversational-extension": ["../extensions/conversational-extension/src/index.ts"] + "@janhq/conversational-extension": ["../extensions/conversational-extension/src/index.ts"], + "@janhq/jan-provider-extension": ["../extensions/jan-provider-extension/src/index.ts"] } } } diff --git a/web-app/vite.config.ts b/web-app/vite.config.ts index 298493889..e9638cda2 100644 --- a/web-app/vite.config.ts +++ b/web-app/vite.config.ts @@ -65,6 +65,7 @@ export default defineConfig(({ mode }) => { alias: { '@': path.resolve(__dirname, './src'), '@janhq/conversational-extension': path.resolve(__dirname, '../extensions/conversational-extension/src/index.ts'), + '@janhq/jan-provider-extension': path.resolve(__dirname, '../extensions/jan-provider-extension/src/index.ts'), }, }, optimizeDeps: {