OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-qvac-models.test.js

229 lines · javascript ref:
import test from 'brittle'
import http from 'http'
import { RelayAPI } from 'p2p-hiverelay/core/relay-node/api.js'
import { ServiceRegistry } from 'p2p-hiverelay/core/services/registry.js'
import { AIService } from 'p2p-hiveservices/builtin/ai-service.js'

const API_KEY = 'qvac-api-test-key'

function mockRelayNode (serviceRegistry) {
  return {
    running: true,
    config: { storage: null },
    metrics: { getSummary () { return { uptime: 100 } } },
    seededApps: new Map(),
    appRegistry: {
      apps: new Map(),
      catalog () { return [] },
      catalogForBroadcast () { return [] }
    },
    getStats () { return { running: true, seededApps: 0, connections: 0 } },
    getHealthStatus () { return { healthy: true } },
    async stop () {},
    async start () {},
    async seedApp () { return { ok: true } },
    async unseedApp () {},
    verifyUnseedRequest () { return { ok: true } },
    broadcastUnseed () {},
    serviceRegistry,
    router: null,
    reputation: null,
    networkDiscovery: null,
    seedingRegistry: null,
    relay: null,
    seeder: null,
    swarm: null,
    on () {},
    emit () {}
  }
}

function request (port, method, path, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const opts = {
      hostname: '127.0.0.1',
      port,
      method,
      path,
      headers: {
        'Content-Type': 'application/json',
        ...headers
      }
    }
    const req = http.request(opts, (res) => {
      let data = ''
      res.on('data', (chunk) => { data += chunk })
      res.on('end', () => {
        let parsed
        try { parsed = JSON.parse(data) } catch (_) { parsed = data }
        resolve({ statusCode: res.statusCode, body: parsed })
      })
    })
    req.on('error', reject)
    if (body) req.write(JSON.stringify(body))
    req.end()
  })
}

async function makeServer (t, opts = {}) {
  const calls = []
  const sdk = {
    loadModel: async (params) => {
      calls.push({ op: 'loadModel', params })
      return 'loaded-' + params.modelSrc
    },
    completion: async (params) => {
      calls.push({ op: 'completion', params })
      return { text: 'ok', stats: { tokens: 1 } }
    },
    unloadModel: async (params) => {
      calls.push({ op: 'unloadModel', params })
    }
  }
  const registry = new ServiceRegistry({ metering: false })
  const ai = opts.provider || new AIService({ qvac: { sdk } })
  registry.register(ai)
  await registry.startAll({})

  const node = mockRelayNode(registry)
  const api = new RelayAPI(node, { apiPort: 0, apiHost: '127.0.0.1', apiKey: API_KEY })
  await api.start()
  const port = api.server.address().port

  t.teardown(async () => {
    if (api._rateLimitCleanup) clearInterval(api._rateLimitCleanup)
    if (api._dashboardFeed) {
      try { api._dashboardFeed.stop() } catch (_) {}
    }
    await new Promise(resolve => api.server.close(resolve))
    await registry.stopAll()
  })

  return { port, calls, ai }
}

test('manage api qvac models: auth required', async (t) => {
  const { port } = await makeServer(t)
  const res = await request(port, 'GET', '/api/manage/ai/models')
  t.is(res.statusCode, 401)
  t.ok(res.body.error)
})

test('manage api qvac models: register, list, remove', async (t) => {
  const { port } = await makeServer(t)
  const auth = { Authorization: 'Bearer ' + API_KEY }

  const registered = await request(port, 'POST', '/api/manage/ai/models', {
    modelId: 'operator-qvac',
    type: 'llm',
    modelSrc: '/models/operator.gguf',
    modelType: 'llm',
    modelConfig: { ctx_size: 512 }
  }, auth)
  t.is(registered.statusCode, 200)
  t.is(registered.body.ok, true)
  t.is(registered.body.action, 'registered')
  t.is(registered.body.model.modelId, 'operator-qvac')
  t.is(registered.body.model.backend, 'qvac')
  t.is(registered.body.model.status, 'registered')
  t.is(registered.body.model.qvac.loaded, false)

  const listed = await request(port, 'GET', '/api/manage/ai/models', null, auth)
  t.is(listed.statusCode, 200)
  t.is(listed.body.qvac.available, true)
  t.is(listed.body.qvacCount, 1)
  t.is(listed.body.models[0].status, 'registered')

  const removed = await request(port, 'POST', '/api/manage/ai/models/remove', {
    modelId: 'operator-qvac'
  }, auth)
  t.is(removed.statusCode, 200)
  t.is(removed.body.ok, true)
  t.is(removed.body.removed, true)
  t.is(removed.body.qvacCount, 0)
})

test('manage api qvac models: rejects non-qvac registration', async (t) => {
  const { port } = await makeServer(t)
  const res = await request(port, 'POST', '/api/manage/ai/models', {
    modelId: 'http-model',
    type: 'llm',
    backend: 'http',
    endpoint: 'http://127.0.0.1:11434/api/generate'
  }, { Authorization: 'Bearer ' + API_KEY })
  t.is(res.statusCode, 400)
  t.ok(res.body.error.includes('qvac-backed'))
})

test('manage api qvac models: redacts unexpected provider errors', async (t) => {
  const secretPath = '/data/models/private/qvac-secret'
  const provider = {
    manifest () {
      return { name: 'ai', version: '1.0.0', capabilities: ['list-models', 'register-model', 'remove-model'] }
    },
    async 'list-models' () {
      throw new Error('list failed at ' + secretPath)
    },
    async 'register-model' () {
      throw new Error('register failed at ' + secretPath)
    },
    async 'remove-model' () {
      throw new Error('remove failed at ' + secretPath)
    }
  }
  const { port } = await makeServer(t, { provider })
  const auth = { Authorization: 'Bearer ' + API_KEY }

  const listed = await request(port, 'GET', '/api/manage/ai/models', null, auth)
  t.is(listed.statusCode, 500)
  t.alike(listed.body, { error: 'AI model list failed' })
  t.absent(JSON.stringify(listed.body).includes(secretPath))

  const registered = await request(port, 'POST', '/api/manage/ai/models', {
    modelId: 'operator-qvac',
    modelSrc: '/models/operator.gguf'
  }, auth)
  t.is(registered.statusCode, 500)
  t.alike(registered.body, { error: 'AI model registration failed' })
  t.absent(JSON.stringify(registered.body).includes(secretPath))

  const removed = await request(port, 'POST', '/api/manage/ai/models/remove', {
    modelId: 'operator-qvac'
  }, auth)
  t.is(removed.statusCode, 500)
  t.alike(removed.body, { error: 'AI model removal failed' })
  t.absent(JSON.stringify(removed.body).includes(secretPath))
})

test('manage api qvac models: preserves known AI model errors', async (t) => {
  const provider = {
    manifest () {
      return { name: 'ai', version: '1.0.0', capabilities: ['list-models', 'register-model', 'remove-model'] }
    },
    async 'list-models' () {
      return []
    },
    async 'register-model' () {
      throw new Error('AI_MODEL_EXISTS: operator-qvac')
    },
    async 'remove-model' () {
      throw new Error('AI_MODEL_NOT_FOUND: operator-qvac')
    }
  }
  const { port } = await makeServer(t, { provider })
  const auth = { Authorization: 'Bearer ' + API_KEY }

  const registered = await request(port, 'POST', '/api/manage/ai/models', {
    modelId: 'operator-qvac',
    modelSrc: '/models/operator.gguf'
  }, auth)
  t.is(registered.statusCode, 400)
  t.alike(registered.body, { error: 'AI_MODEL_EXISTS: operator-qvac' })

  const removed = await request(port, 'POST', '/api/manage/ai/models/remove', {
    modelId: 'operator-qvac'
  }, auth)
  t.is(removed.statusCode, 400)
  t.alike(removed.body, { error: 'AI_MODEL_NOT_FOUND: operator-qvac' })
})