OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / backend / ai / qvac-ollama-catalog.mjs

82 lines · javascript ref:
const MODEL_MEDIA_TYPE = 'application/vnd.ollama.image.model'

export function discoverOllamaQwenModels (opts = {}) {
  const fs = opts.fs
  const path = opts.path
  if (!fs || !path) throw new TypeError('Ollama discovery requires fs and path adapters')

  const homeDir = opts.homeDir
  const modelsRoot = opts.modelsRoot || (homeDir ? path.join(homeDir, '.ollama', 'models') : null)
  if (!modelsRoot) return {}
  const manifestsRoot = path.join(modelsRoot, 'manifests', 'registry.ollama.ai')
  const blobsRoot = path.join(modelsRoot, 'blobs')
  const device = opts.device === 'gpu' ? 'gpu' : 'cpu'
  const entries = []

  let candidates
  try {
    candidates = fs.readdirSync(manifestsRoot, { recursive: true })
  } catch {
    return {}
  }

  for (const relativePath of candidates) {
    try {
      const manifestPath = path.join(manifestsRoot, relativePath)
      const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
      const configPath = blobPath(blobsRoot, manifest?.config?.digest, path)
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
      const family = String(config.model_family || '').toLowerCase()
      if (!family.startsWith('qwen')) continue

      const layer = manifest.layers?.find(item => item?.mediaType === MODEL_MEDIA_TYPE)
      const modelPath = blobPath(blobsRoot, layer?.digest, path)
      if (!modelPath || !fs.existsSync(modelPath)) continue

      const modelName = ollamaName(relativePath)
      if (!modelName || !/qwen/i.test(modelName)) continue
      const alias = `ollama:${modelName}`
      const expectedSize = Number.isFinite(layer.size) ? layer.size : undefined
      entries.push([alias, Object.freeze({
        modelSrc: modelPath,
        modelType: 'llamacpp-completion',
        modelConfig: Object.freeze({
          device,
          gpu_layers: device === 'gpu' ? 99 : 0,
          ctx_size: 8192
        }),
        expectedSize,
        label: modelName,
        provider: 'ollama',
        family,
        params: typeof config.model_type === 'string' ? config.model_type : undefined,
        quantization: typeof config.file_type === 'string' ? config.file_type : undefined,
        recommended: family === 'qwen3'
      })])
    } catch {
      // Directories, malformed manifests, and incomplete blobs are ignored.
    }
  }

  entries.sort((a, b) => {
    const recommended = Number(!!b[1].recommended) - Number(!!a[1].recommended)
    if (recommended) return recommended
    return (a[1].expectedSize || Infinity) - (b[1].expectedSize || Infinity) || a[0].localeCompare(b[0])
  })
  return Object.freeze(Object.fromEntries(entries))
}

function blobPath (blobsRoot, digest, path) {
  if (typeof digest !== 'string' || !/^sha256:[0-9a-f]{64}$/i.test(digest)) return null
  return path.join(blobsRoot, digest.replace(':', '-').toLowerCase())
}

function ollamaName (relativePath) {
  const parts = String(relativePath || '').split(/[\\/]/).filter(Boolean)
  if (parts.length < 2) return null
  const tag = parts.pop()
  if (parts[0] === 'library') parts.shift()
  if (!parts.length || !tag) return null
  return `${parts.join('/')}:${tag}`
}