Compare commits
3 Commits
dev
...
mobile/jan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6a71828da | ||
|
|
ef1e990d40 | ||
|
|
79e915d97d |
45
extensions/jan-provider-extension/package.json
Normal file
45
extensions/jan-provider-extension/package.json
Normal file
@ -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 <service@jan.ai>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
14
extensions/jan-provider-extension/rolldown.config.mjs
Normal file
14
extensions/jan-provider-extension/rolldown.config.mjs
Normal file
@ -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'),
|
||||||
|
},
|
||||||
|
})
|
||||||
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
237
extensions/jan-provider-extension/src/api.ts
Normal file
237
extensions/jan-provider-extension/src/api.ts
Normal file
@ -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<typeof fetch> {
|
||||||
|
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<JanModel[]> {
|
||||||
|
try {
|
||||||
|
janProviderStore.setLoadingModels(true)
|
||||||
|
janProviderStore.clearError()
|
||||||
|
|
||||||
|
const apiBase = getApiBase()
|
||||||
|
const response = await makeAuthenticatedRequest<JanModelsResponse>(
|
||||||
|
`${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<JanChatCompletionResponse> {
|
||||||
|
try {
|
||||||
|
janProviderStore.clearError()
|
||||||
|
|
||||||
|
const { endpoint, payload } = getChatCompletionConfig(request, false)
|
||||||
|
|
||||||
|
return await makeAuthenticatedRequest<JanChatCompletionResponse>(
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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()
|
||||||
151
extensions/jan-provider-extension/src/auth.ts
Normal file
151
extensions/jan-provider-extension/src/auth.ts
Normal file
@ -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<T>(url: string, options?: RequestInit): Promise<T>
|
||||||
|
initialize(): Promise<void>
|
||||||
|
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<AuthTokens> {
|
||||||
|
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<AuthTokens>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure valid guest access token, refreshing if expired
|
||||||
|
*/
|
||||||
|
async function ensureGuestToken(platformFetch: typeof fetch): Promise<string> {
|
||||||
|
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<JanAuthService | null> {
|
||||||
|
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<typeof fetch> {
|
||||||
|
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<T>(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const authService = await waitForAuthService()
|
||||||
|
|
||||||
|
if (authService) {
|
||||||
|
return authService.makeAuthenticatedRequest<T>(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}` }
|
||||||
|
}
|
||||||
1
extensions/jan-provider-extension/src/index.ts
Normal file
1
extensions/jan-provider-extension/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './provider'
|
||||||
459
extensions/jan-provider-extension/src/provider.ts
Normal file
459
extensions/jan-provider-extension/src/provider.ts
Normal file
@ -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<string, SessionInfo> = 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<modelInfo | undefined> {
|
||||||
|
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<modelInfo[]> {
|
||||||
|
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<SessionInfo> {
|
||||||
|
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<UnloadResult> {
|
||||||
|
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<chatCompletion | AsyncIterable<chatCompletionChunk>> {
|
||||||
|
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<void>((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<void> {
|
||||||
|
throw new Error(
|
||||||
|
`Delete operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(modelId: string, _model: Partial<modelInfo>): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
`Update operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(modelId: string, _opts: ImportOptions): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
`Import operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async abortImport(modelId: string): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
`Abort import operation not supported for remote Jan API model: ${modelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLoadedModels(): Promise<string[]> {
|
||||||
|
return Array.from(this.activeSessions.values()).map(
|
||||||
|
(session) => session.model_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async isToolSupported(modelId: string): Promise<boolean> {
|
||||||
|
console.log(`Checking tool support for Jan model ${modelId}: supported`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
90
extensions/jan-provider-extension/src/store.ts
Normal file
90
extensions/jan-provider-extension/src/store.ts
Normal file
@ -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<JanProviderStore>((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(),
|
||||||
|
}
|
||||||
89
extensions/jan-provider-extension/src/types.ts
Normal file
89
extensions/jan-provider-extension/src/types.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
7
extensions/jan-provider-extension/tsconfig.json
Normal file
7
extensions/jan-provider-extension/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../core/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"]
|
||||||
|
}
|
||||||
8
extensions/jan-provider-extension/vitest.config.ts
Normal file
8
extensions/jan-provider-extension/vitest.config.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@ -3,8 +3,8 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"devUrl": null,
|
"devUrl": null,
|
||||||
"frontendDist": "../web-app/dist",
|
"frontendDist": "../web-app/dist",
|
||||||
"beforeDevCommand": "cross-env IS_DEV=true 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:web"
|
"beforeBuildCommand": "cross-env IS_ANDROID=true yarn build:extensions && cross-env IS_ANDROID=true yarn build:web"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
163
web-app/src/__tests__/jan-provider-mobile.test.ts
Normal file
163
web-app/src/__tests__/jan-provider-mobile.test.ts
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -9,6 +9,7 @@
|
|||||||
import { TauriCoreService } from './tauri'
|
import { TauriCoreService } from './tauri'
|
||||||
import type { ExtensionManifest } from '@/lib/extension'
|
import type { ExtensionManifest } from '@/lib/extension'
|
||||||
import JanConversationalExtension from '@janhq/conversational-extension'
|
import JanConversationalExtension from '@janhq/conversational-extension'
|
||||||
|
import JanProviderExtension from '@janhq/jan-provider-extension'
|
||||||
|
|
||||||
export class MobileCoreService extends TauriCoreService {
|
export class MobileCoreService extends TauriCoreService {
|
||||||
/**
|
/**
|
||||||
@ -54,6 +55,15 @@ export class MobileCoreService extends TauriCoreService {
|
|||||||
'1.0.0'
|
'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 [
|
return [
|
||||||
{
|
{
|
||||||
name: '@janhq/conversational-extension',
|
name: '@janhq/conversational-extension',
|
||||||
@ -64,6 +74,15 @@ export class MobileCoreService extends TauriCoreService {
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
extensionInstance: conversationalExt,
|
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,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./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"],
|
"include": ["src"],
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
'@janhq/conversational-extension': path.resolve(__dirname, '../extensions/conversational-extension/src/index.ts'),
|
'@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: {
|
optimizeDeps: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user