Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/conversational-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
} from '@janhq/core'

/**
* JSONConversationalExtension is a ConversationalExtension implementation that provides
* JanConversationalExtension is a ConversationalExtension implementation that provides
* functionality for managing threads.
*/
export default class CortexConversationalExtension extends ConversationalExtension {
export default class JanConversationalExtension extends ConversationalExtension {
/**
* Called when the extension is loaded.
*/
Expand Down
93 changes: 92 additions & 1 deletion extensions/llamacpp-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { invoke } from '@tauri-apps/api/core'
import { basename } from '@tauri-apps/api/path'
import { getProxyConfig } from './util'
import { basename } from '@tauri-apps/api/path'

type LlamacppConfig = {
version_backend: string
Expand Down Expand Up @@ -538,9 +539,11 @@ export default class llamacpp_extension extends AIEngine {
override async list(): Promise<modelInfo[]> {
const modelsDir = await joinPath([await this.getProviderPath(), 'models'])
if (!(await fs.existsSync(modelsDir))) {
return []
await fs.mkdir(modelsDir)
}

await this.migrateLegacyModels()

let modelIds: string[] = []

// DFS
Expand Down Expand Up @@ -589,6 +592,94 @@ export default class llamacpp_extension extends AIEngine {
return modelInfos
}

private async migrateLegacyModels() {
const janDataFolderPath = await getJanDataFolderPath()
const modelsDir = await joinPath([janDataFolderPath, 'models'])
if (!(await fs.existsSync(modelsDir))) return

// DFS
let stack = [modelsDir]
while (stack.length > 0) {
const currentDir = stack.pop()

const files = await fs.readdirSync(currentDir)
for (const child of files) {
const childPath = await joinPath([currentDir, child])
const stat = await fs.fileStat(childPath)
if (
files.some((e) => e.endsWith('model.yml')) &&
!child.endsWith('model.yml')
)
continue
if (!stat.isDirectory && child.endsWith('.yml')) {
// check if model.yml exists
const modelConfigPath = child
if (await fs.existsSync(modelConfigPath)) {
const legacyModelConfig = await invoke<{
files: string[]
model: string
}>('read_yaml', {
path: modelConfigPath,
})
const legacyModelPath = legacyModelConfig.files?.[0]
if (!legacyModelPath) continue
// +1 to remove the leading slash
// NOTE: this does not handle Windows path \\
let modelId = currentDir.slice(modelsDir.length + 1)

modelId =
modelId !== 'imported'
? modelId
: (await basename(child)).replace('.yml', '')

const modelName = legacyModelConfig.model ?? modelId
const configPath = await joinPath([
await this.getProviderPath(),
'models',
modelId,
'model.yml',
])
if (await fs.existsSync(configPath)) continue // Don't reimport

// this is relative to Jan's data folder
const modelDir = `${this.providerId}/models/${modelId}`

let size_bytes = (
await fs.fileStat(
await joinPath([janDataFolderPath, legacyModelPath])
)
).size

const modelConfig = {
model_path: legacyModelPath,
mmproj_path: undefined, // legacy models do not have mmproj
name: modelName,
size_bytes,
} as ModelConfig
await fs.mkdir(await joinPath([janDataFolderPath, modelDir]))
await invoke<void>('write_yaml', {
data: modelConfig,
savePath: configPath,
})
continue
}
}
}

// otherwise, look into subdirectories
const children = await fs.readdirSync(currentDir)
for (const child of children) {
// skip files
const dirInfo = await fs.fileStat(child)
if (!dirInfo.isDirectory) {
continue
}

stack.push(child)
}
}
}

override async import(modelId: string, opts: ImportOptions): Promise<void> {
const isValidModelId = (id: string) => {
// only allow alphanumeric, underscore, hyphen, and dot characters in modelId
Expand Down
14 changes: 12 additions & 2 deletions extensions/llamacpp-extension/src/test/backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,21 @@ describe('Backend functions', () => {
os_type: 'windows',
})

const { getJanDataFolderPath, joinPath } = await import('@janhq/core')
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')

vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
vi.mocked(joinPath)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/win-avx2-x64'
)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/win-avx2-x64/build'
)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/win-avx2-x64/build/bin/llama-server.exe'
)

vi.mocked(fs.existsSync).mockResolvedValue(true)

const result = await getBackendExePath('win-avx2-x64', 'v1.0.0')

Expand All @@ -130,16 +135,21 @@ describe('Backend functions', () => {
os_type: 'linux',
})

const { getJanDataFolderPath, joinPath } = await import('@janhq/core')
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')

vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
vi.mocked(joinPath)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/linux-avx2-x64'
)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/linux-avx2-x64/build'
)
.mockResolvedValueOnce(
'/path/to/jan/llamacpp/backends/v1.0.0/linux-avx2-x64/build/bin/llama-server'
)

vi.mocked(fs.existsSync).mockResolvedValue(true)

const result = await getBackendExePath('linux-avx2-x64', 'v1.0.0')

Expand Down
28 changes: 25 additions & 3 deletions extensions/llamacpp-extension/src/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import llamacpp_extension from '../index'
// Mock fetch globally
global.fetch = vi.fn()

// Mock backend functions
vi.mock('../backend', () => ({
isBackendInstalled: vi.fn(),
getBackendExePath: vi.fn(),
downloadBackend: vi.fn(),
listSupportedBackends: vi.fn(),
getBackendDir: vi.fn(),
}))

describe('llamacpp_extension', () => {
let extension: llamacpp_extension

Expand Down Expand Up @@ -43,7 +52,11 @@ describe('llamacpp_extension', () => {

vi.mocked(getJanDataFolderPath).mockResolvedValue('/path/to/jan')
vi.mocked(joinPath).mockResolvedValue('/path/to/jan/llamacpp/models')
vi.mocked(fs.existsSync).mockResolvedValue(false)
vi.mocked(fs.existsSync)
.mockResolvedValueOnce(false) // models directory doesn't exist initially
.mockResolvedValue(false) // no model.yml files exist
vi.mocked(fs.mkdir).mockResolvedValue(undefined)
vi.mocked(fs.readdirSync).mockResolvedValue([]) // empty directory after creation

const result = await extension.list()

Expand Down Expand Up @@ -158,14 +171,22 @@ describe('llamacpp_extension', () => {
})

it('should load model successfully', async () => {
const { getJanDataFolderPath, joinPath } = await import('@janhq/core')
const { getJanDataFolderPath, joinPath, fs } = await import('@janhq/core')
const { invoke } = await import('@tauri-apps/api/core')

// Mock system info for getBackendExePath
window.core.api.getSystemInfo = vi.fn().mockResolvedValue({
os_type: 'linux'
})

// Mock backend functions to avoid download
const backendModule = await import('../backend')
vi.mocked(backendModule.isBackendInstalled).mockResolvedValue(true)
vi.mocked(backendModule.getBackendExePath).mockResolvedValue('/path/to/backend/executable')

// Mock fs for backend check
vi.mocked(fs.existsSync).mockResolvedValue(true)

// Mock configuration
extension['config'] = {
version_backend: 'v1.0.0/win-avx2-x64',
Expand Down Expand Up @@ -220,7 +241,8 @@ describe('llamacpp_extension', () => {

// Mock successful health check
global.fetch = vi.fn().mockResolvedValue({
ok: true
ok: true,
json: vi.fn().mockResolvedValue({ status: 'ok' })
})

const result = await extension.load('test-model')
Expand Down
Loading
Loading