@molecule/api-model-hosting-koyeb

Provider bond · model-hosting · API (Node) · v1.0.1 · Apache-2.0

Koyeb model hosting for molecule.dev — run a model server container on CPU or an RTX 4000 / L4 GPU in the EU or US, with sleep when idle

npm install @molecule/api-model-hosting-koyeb

npm · Source on GitHub · Implements @molecule/api-model-hosting

How it works

@molecule/api-model-hosting-koyeb is a provider bond on the API (Node) side: it implements the model-hosting core interface (@molecule/api-model-hosting) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider, requireProvider } from '@molecule/api-model-hosting'
import { provider } from '@molecule/api-model-hosting-koyeb'

setProvider(provider) // reads KOYEB_API_TOKEN on first use

const endpoint = await requireProvider().deploy({
  name: 'laya',
  image: 'ghcr.io/acme/laya-serve:0.3.1',
  port: 8000,
  healthPath: '/health',
  secretEnv: { LAYA_API_KEY: process.env.LAYA_API_KEY ?? '' },
  accelerator: { kind: 'cpu' },
  region: 'eu',
  scaling: { minInstances: 0, maxInstances: 2, idleTimeoutSeconds: 600 },
  access: 'private',
  serverEnforcesAuth: true,
})
endpoint.url // https://laya-acme.koyeb.app

Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-model-hosting, @molecule/api-secrets

Secrets: KOYEB_API_TOKEN

Reference

Koyeb model hosting for molecule.dev — run a model server container on a CPU instance or an NVIDIA GPU (L4, A100, H100, H200) in the EU or US, with sleep-when-idle and per-second billing.

Each endpoint is one Koyeb app holding one WEB service, reachable at the app's *.koyeb.app domain.

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider, requireProvider } from '@molecule/api-model-hosting'
import { provider } from '@molecule/api-model-hosting-koyeb'

setProvider(provider) // reads KOYEB_API_TOKEN on first use

const endpoint = await requireProvider().deploy({
  name: 'laya',
  image: 'ghcr.io/acme/laya-serve:0.3.1',
  port: 8000,
  healthPath: '/health',
  secretEnv: { LAYA_API_KEY: process.env.LAYA_API_KEY ?? '' },
  accelerator: { kind: 'cpu' },
  region: 'eu',
  scaling: { minInstances: 0, maxInstances: 2, idleTimeoutSeconds: 600 },
  access: 'private',
  serverEnforcesAuth: true,
})
endpoint.url // https://laya-acme.koyeb.app

Type

provider

Installation

npm install @molecule/api-model-hosting-koyeb @molecule/api-bond @molecule/api-i18n @molecule/api-model-hosting @molecule/api-secrets

API

Interfaces

KoyebConfig

Configuration for the Koyeb model hosting provider.

interface KoyebConfig {
  /** API token. Defaults to `KOYEB_API_TOKEN`. */
  apiToken?: string
  /** Region for `'us'`. Default `'was'` (Washington D.C.). */
  usRegion?: string
  /** Region for `'eu'`. Default `'fra'` (Frankfurt). */
  euRegion?: string
  /** CPU instance type when `accelerator` is CPU. Default `'large'` (4 vCPU / 4 GB). */
  cpuInstanceType?: string
  /** API base URL (tests, proxies). Default `https://app.koyeb.com`. */
  apiBaseUrl?: string
  /** How often `deploy` polls the service while it rolls out, in ms. Default 5000. */
  pollIntervalMs?: number
}

KoyebDefinition

A Koyeb deployment definition (only the fields written here).

interface KoyebDefinition {
  name: string
  type: 'WEB'
  docker: { image: string; entrypoint?: string[]; args?: string[] }
  env: Array<{ key: string; value: string }>
  ports: Array<{ port: number; protocol: 'http' }>
  routes: Array<{ port: number; path: string }>
  regions: string[]
  instance_types: Array<{ type: string }>
  scalings: Array<{
    min: number
    max: number
    targets?: Array<{
      sleep_idle_delay?: { value?: number }
      concurrent_requests?: { value?: number }
    }>
  }>
  health_checks: Array<{ grace_period?: number; http: { port: number; path: string } }>
}

KoyebApp

A Koyeb app (only the fields read here).

interface KoyebApp {
  id: string
  name: string
  domains?: Array<{ name?: string }>
}

KoyebService

A Koyeb service (only the fields read here).

interface KoyebService {
  id: string
  name: string
  app_id: string
  created_at?: string
  status?:
    | 'STARTING'
    | 'HEALTHY'
    | 'DEGRADED'
    | 'UNHEALTHY'
    | 'DELETING'
    | 'DELETED'
    | 'PAUSING'
    | 'PAUSED'
    | 'RESUMING'
  messages?: string[]
  latest_deployment_id?: string
}

Classes

KoyebApiError

A Koyeb API error, carrying the HTTP status and Koyeb's error code.

class KoyebApiError extends Error {
  /** HTTP status. */
  readonly status: number
  /** Koyeb error code, when given. */
  readonly code: string | undefined

  /**
   * Creates the error.
   *
   * @param message - What failed.
   * @param status - HTTP status.
   * @param code - Koyeb error code.
   */
  constructor(message: string, status: number, code?: string) {
    super(message)
    this.name = 'KoyebApiError'
    this.status = status
    this.code = code
  }
}

KoyebClient

Thin Koyeb API client.

class KoyebClient {
  /**
   * Creates the client.
   *
   * @param token - Returns the API token (read lazily).
   * @param baseUrl - API base URL.
   */
  constructor(
    private readonly token: () => string,
    private readonly baseUrl: string,
  ) {}

  /**
   * Sends one request; `null` on 404.
   *
   * @param method - HTTP method.
   * @param path - Path under the base URL.
   * @param body - JSON body.
   * @returns The parsed body, or `null` on 404.
   */
  private async call<T>(method: string, path: string, body?: unknown): Promise<T | null> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: { authorization: `Bearer ${this.token()}`, 'content-type': 'application/json' },
      ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    })
    if (res.status === 404) return null
    const json = (await res.json().catch(() => ({}))) as Record<string, unknown>
    if (!res.ok) {
      throw new KoyebApiError(
        `Koyeb ${method} ${path} failed (${res.status}): ${String(json.message ?? json.error ?? 'no message')}`,
        res.status,
        typeof json.code === 'string' ? json.code : undefined,
      )
    }
    return json as T
  }

  /**
   * Finds an app by exact name.
   *
   * @param name - App name.
   * @returns The app, or `null`.
   */
  async findApp(name: string): Promise<KoyebApp | null> {
    const res = await this.call<{ apps?: KoyebApp[] }>(
      'GET',
      `/v1/apps?name=${encodeURIComponent(name)}`,
    )
    return res?.apps?.find((a) => a.name === name) ?? null
  }

  /**
   * Reads one app.
   *
   * @param id - App id.
   * @returns The app, or `null`.
   */
  async getApp(id: string): Promise<KoyebApp | null> {
    return (await this.call<{ app?: KoyebApp }>('GET', `/v1/apps/${id}`))?.app ?? null
  }

  /**
   * Creates an app.
   *
   * @param name - App name.
   * @returns The app.
   */
  async createApp(name: string): Promise<KoyebApp> {
    const res = await this.call<{ app: KoyebApp }>('POST', '/v1/apps', { name })
    return res!.app
  }

  /**
   * Deletes an app (and with it, its services).
   *
   * @param id - App id.
   */
  async deleteApp(id: string): Promise<void> {
    await this.call('DELETE', `/v1/apps/${id}`)
  }

  /**
   * Finds a service by app and name.
   *
   * @param appId - App id.
   * @param name - Service name.
   * @returns The service, or `null`.
   */
  async findService(appId: string, name: string): Promise<KoyebService | null> {
    const res = await this.call<{ services?: KoyebService[] }>(
      'GET',
      `/v1/services?app_id=${encodeURIComponent(appId)}&name=${encodeURIComponent(name)}`,
    )
    return res?.services?.find((s) => s.name === name) ?? null
  }

  /**
   * Lists every service in the organization.
   *
   * @returns The services.
   */
  async listServices(): Promise<KoyebService[]> {
    const out: KoyebService[] = []
    for (let offset = 0; ; offset += 100) {
      const res = await this.call<{ services?: KoyebService[]; has_next?: boolean }>(
        'GET',
        `/v1/services?limit=100&offset=${offset}`,
      )
      out.push(...(res?.services ?? []))
      if (!res?.has_next) return out
    }
  }

  /**
   * Reads one service.
   *
   * @param id - Service id.
   * @returns The service, or `null`.
   */
  async getService(id: string): Promise<KoyebService | null> {
    return (
      (await this.call<{ service?: KoyebService }>('GET', `/v1/services/${id}`))?.service ?? null
    )
  }

  /**
   * Creates a service in an app.
   *
   * @param appId - App id.
   * @param definition - Deployment definition.
   * @returns The service.
   */
  async createService(appId: string, definition: KoyebDefinition): Promise<KoyebService> {
    const res = await this.call<{ service: KoyebService }>('POST', '/v1/services', {
      app_id: appId,
      definition,
    })
    return res!.service
  }

  /**
   * Reads the definition a deployment was made from (`GET /v1/deployments/{id}`).
   *
   * @param id - Deployment id (a service's `latest_deployment_id`).
   * @returns The definition, or `null`.
   */
  async getDefinition(id: string): Promise<KoyebDefinition | null> {
    const res = await this.call<{ deployment?: { definition?: KoyebDefinition } }>(
      'GET',
      `/v1/deployments/${id}`,
    )
    return res?.deployment?.definition ?? null
  }

  /**
   * Replaces a service's definition (a new deployment rolls out).
   *
   * @param id - Service id.
   * @param definition - Deployment definition.
   * @returns The service.
   */
  async updateService(id: string, definition: KoyebDefinition): Promise<KoyebService> {
    const res = await this.call<{ service: KoyebService }>('PUT', `/v1/services/${id}`, {
      definition,
    })
    return res!.service
  }
}

Functions

instanceTypeFor(spec, cpuType)

Picks the Koyeb instance type for a spec.

function instanceTypeFor(spec: ModelEndpointSpec, cpuType: string): string
  • spec — The endpoint spec.
  • cpuType — The CPU instance type to use for CPU specs.

Returns: A Koyeb instance type id.

toDefinition(spec, region, cpuType)

Builds the Koyeb deployment definition for a spec.

function toDefinition(spec: ModelEndpointSpec, region: string, cpuType: string): KoyebDefinition
  • spec — The endpoint spec.
  • region — Koyeb region id.
  • cpuType — CPU instance type for CPU specs.

Returns: The definition.

statusOf(status)

Maps a Koyeb service status to an endpoint status.

function statusOf(
  status:
    | 'STARTING'
    | 'HEALTHY'
    | 'DEGRADED'
    | 'UNHEALTHY'
    | 'DELETING'
    | 'DELETED'
    | 'PAUSING'
    | 'PAUSED'
    | 'RESUMING'
    | undefined,
): EndpointStatus
  • status — Koyeb Service.status.

Returns: The endpoint status.

createProvider(config)

Creates a Koyeb model hosting provider.

function createProvider(config?: KoyebConfig): ModelHostingProvider
  • config — API token, region mapping, CPU instance type.

Returns: A ModelHostingProvider backed by Koyeb.

Constants

KOYEB_API

Default API base URL.

const KOYEB_API: 'https://app.koyeb.com'

KOYEB_GPUS

GPU instance ids by memory, smallest first (koyeb.com/docs/reference/instances, 2026-09-30).

const KOYEB_GPUS: readonly { id: string; vramGb: number }[]

KOYEB_CAPABILITIES

What Koyeb offers here.

const KOYEB_CAPABILITIES: ProviderCapabilities

provider

The provider implementation — lazy, so env vars are read on first use.

const provider: ModelHostingProvider

modelHostingKoyebSecretDefinitions

Secret definitions used by the Koyeb model hosting bond.

const modelHostingKoyebSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-model-hosting interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-model-hosting'
import { provider } from '@molecule/api-model-hosting-koyeb'

export function setupModelHostingKoyeb(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bond ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-model-hosting ^1.0.0
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • KOYEB_API_TOKEN (required) — koyeb api token
    • Setup: Koyeb API token — An organization API token (Settings → API). GPU instances need a paid plan and a card on file.
    • Get it here: https://app.koyeb.com/user/settings/api
    • Example: koyeb-api-token

Runtime Dependencies

  • @molecule/api-bond

  • @molecule/api-i18n

  • @molecule/api-model-hosting

  • @molecule/api-secrets

  • No endpoint auth of its own. A Koyeb URL is public. For access: 'private' you must set serverEnforcesAuth: true, pass the server's key in secretEnv (e.g. LAYA_API_KEY), and give the consumer the same key (apiKey). authHeaders() always returns {}.

  • secretEnv is stored as plain service env, visible in the Koyeb console to anyone in the organization. Use Koyeb secrets yourself for anything stricter.

  • Instance types: CPU uses large (4 vCPU / 4 GB — enough for a ~400M encoder; medium has too little memory) unless cpuInstanceType is set. GPUs are picked by minVramGb from gpu-nvidia-l4 (24 GB) up; pass accelerator.model to name one exactly (e.g. an RTX 4000 SFF Ada id from Koyeb's catalog). Which regions carry which GPUs is not documented on the regions page — a GPU in a region without it fails the deploy.

  • Sleep: minInstances: 0 sleeps after idleTimeoutSeconds (300–43,200 s; the upper end depends on your plan) and wakes in 1–5 s plus the model load. 'us' → was, 'eu' → fra.

  • GPU instances need a paid plan and a card on file (Koyeb holds a pre-authorization); the deploy fails with Koyeb's message otherwise.

  • remove() deletes the whole app. list() returns services whose app has the same name — the shape this bond creates.

  • API errors throw KoyebApiError with the HTTP status and Koyeb's code.