@molecule/api-model-hosting-runpod
Provider bond · model-hosting · API (Node) · v1.0.1 · Apache-2.0
RunPod Serverless model hosting for molecule.dev — run a model server container on a load-balancing GPU endpoint that scales to zero
npm install @molecule/api-model-hosting-runpodnpm · Source on GitHub · Implements @molecule/api-model-hosting
How it works
@molecule/api-model-hosting-runpod 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-runpod'
setProvider(provider) // reads RUNPOD_API_KEY on first use
const endpoint = await requireProvider().deploy({
name: 'jeff',
image: 'ghcr.io/acme/jeff-serve:0.8b',
port: 8000,
healthPath: '/ping',
accelerator: { kind: 'gpu', minVramGb: 16 },
region: 'us',
scaling: { minInstances: 0, maxInstances: 2, idleTimeoutSeconds: 60 },
access: 'private',
})
const headers = await requireProvider().authHeaders(endpoint.id) // { authorization: 'Bearer <RUNPOD_API_KEY>' }Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-model-hosting, @molecule/api-secrets
Secrets: RUNPOD_API_KEY
Reference
RunPod Serverless model hosting for molecule.dev — run a model server
container on a load-balancing GPU endpoint (16 GB class and up) that scales
to zero, called at https://<endpoint-id>.api.runpod.ai with your RunPod
API key.
Load-balancing endpoints run YOUR HTTP server (no RunPod handler code), so
laya-serve, jeff-serve or any /v1/systemone server works unchanged.
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.tsJSDoc, not this file.
Quick Start
import { setProvider, requireProvider } from '@molecule/api-model-hosting'
import { provider } from '@molecule/api-model-hosting-runpod'
setProvider(provider) // reads RUNPOD_API_KEY on first use
const endpoint = await requireProvider().deploy({
name: 'jeff',
image: 'ghcr.io/acme/jeff-serve:0.8b',
port: 8000,
healthPath: '/ping',
accelerator: { kind: 'gpu', minVramGb: 16 },
region: 'us',
scaling: { minInstances: 0, maxInstances: 2, idleTimeoutSeconds: 60 },
access: 'private',
})
const headers = await requireProvider().authHeaders(endpoint.id) // { authorization: 'Bearer <RUNPOD_API_KEY>' }
Type
provider
Installation
npm install @molecule/api-model-hosting-runpod @molecule/api-bond @molecule/api-i18n @molecule/api-model-hosting @molecule/api-secrets
API
Interfaces
RunPodConfig
Configuration for the RunPod model hosting provider.
interface RunPodConfig {
/** API key. Defaults to `RUNPOD_API_KEY`. */
apiKey?: string
/** GraphQL `locations` for `'us'`. Default `'US'`. */
usLocations?: string
/** GraphQL `locations` for `'eu'`. Default `'CZ,FR,GB,NO,RO'`. */
euLocations?: string
/** Container disk per worker, in GB. Default 20. */
containerDiskGb?: number
/** REST base URL (tests). Default `https://rest.runpod.io/v1`. */
restBaseUrl?: string
/** GraphQL URL (tests). Default `https://api.runpod.io/graphql`. */
graphqlUrl?: string
/** Endpoint URL template (tests); `{id}` is replaced. Default `https://{id}.api.runpod.ai`. */
endpointUrlTemplate?: string
/** How often `deploy` probes the health path while workers start, in ms. Default 5000. */
pollIntervalMs?: number
}
RunPodTemplate
A RunPod template (only the fields read here).
interface RunPodTemplate {
id: string
name: string
}
RunPodEndpoint
A RunPod serverless endpoint (only the fields read here).
interface RunPodEndpoint {
id: string
name: string
templateId?: string
workersMin?: number
workersMax?: number
idleTimeout?: number
gpuTypeIds?: string[]
dataCenterIds?: string[]
createdAt?: string
workers?: unknown[]
}
Classes
RunPodApiError
A RunPod API error, carrying the HTTP status.
class RunPodApiError extends Error {
/** HTTP status. */
readonly status: number
/**
* Creates the error.
*
* @param message - What failed.
* @param status - HTTP status.
*/
constructor(message: string, status: number) {
super(message)
this.name = 'RunPodApiError'
this.status = status
}
}
RunPodClient
Thin RunPod client.
class RunPodClient {
/**
* Creates the client.
*
* @param apiKey - Returns the API key (read lazily).
* @param restBaseUrl - REST base URL.
* @param graphqlUrl - GraphQL URL.
*/
constructor(
private readonly apiKey: () => string,
private readonly restBaseUrl: string,
private readonly graphqlUrl: string,
) {}
/**
* Sends one REST request; `null` on 404.
*
* @param method - HTTP method.
* @param path - Path under the REST base.
* @param body - JSON body.
* @returns The parsed body, or `null` on 404 / 204.
*/
private async rest<T>(method: string, path: string, body?: unknown): Promise<T | null> {
const res = await fetch(`${this.restBaseUrl}${path}`, {
method,
headers: { authorization: `Bearer ${this.apiKey()}`, 'content-type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
})
if (res.status === 404 || res.status === 204) return null
const text = await res.text()
if (!res.ok) {
throw new RunPodApiError(
`RunPod ${method} ${path} failed (${res.status}): ${text.slice(0, 300)}`,
res.status,
)
}
return (text ? JSON.parse(text) : null) as T | null
}
/**
* Sends one GraphQL operation.
*
* @param query - The GraphQL document.
* @returns The `data` object.
*/
private async graphql<T>(query: string): Promise<T> {
const res = await fetch(this.graphqlUrl, {
method: 'POST',
headers: { authorization: `Bearer ${this.apiKey()}`, 'content-type': 'application/json' },
body: JSON.stringify({ query }),
})
const json = (await res.json().catch(() => ({}))) as {
data?: T
errors?: Array<{ message?: string }>
}
if (!res.ok || json.errors?.length || !json.data) {
throw new RunPodApiError(
`RunPod GraphQL failed (${res.status}): ${json.errors?.map((e) => e.message).join('; ') ?? 'no data'}`,
res.status,
)
}
return json.data
}
/**
* Lists templates.
*
* @returns The templates.
*/
async listTemplates(): Promise<RunPodTemplate[]> {
return (await this.rest<RunPodTemplate[]>('GET', '/templates')) ?? []
}
/**
* Creates a template.
*
* @param body - `TemplateCreateInput`.
* @returns The template.
*/
async createTemplate(body: Record<string, unknown>): Promise<RunPodTemplate> {
return (await this.rest<RunPodTemplate>('POST', '/templates', body))!
}
/**
* Updates a template.
*
* @param id - Template id.
* @param body - Fields to change.
*/
async updateTemplate(id: string, body: Record<string, unknown>): Promise<void> {
await this.rest('PATCH', `/templates/${id}`, body)
}
/**
* Deletes a template.
*
* @param id - Template id.
*/
async deleteTemplate(id: string): Promise<void> {
await this.rest('DELETE', `/templates/${id}`)
}
/**
* Lists endpoints.
*
* @returns The endpoints.
*/
async listEndpoints(): Promise<RunPodEndpoint[]> {
return (await this.rest<RunPodEndpoint[]>('GET', '/endpoints')) ?? []
}
/**
* Reads one endpoint with its workers.
*
* @param id - Endpoint id.
* @returns The endpoint, or `null`.
*/
getEndpoint(id: string): Promise<RunPodEndpoint | null> {
return this.rest<RunPodEndpoint>('GET', `/endpoints/${id}?includeWorkers=true`)
}
/**
* Updates an endpoint.
*
* @param id - Endpoint id.
* @param body - `EndpointUpdateInput` fields.
* @returns The endpoint.
*/
async updateEndpoint(id: string, body: Record<string, unknown>): Promise<RunPodEndpoint | null> {
return this.rest<RunPodEndpoint>('PATCH', `/endpoints/${id}`, body)
}
/**
* Deletes an endpoint.
*
* @param id - Endpoint id.
*/
async deleteEndpoint(id: string): Promise<void> {
await this.rest('DELETE', `/endpoints/${id}`)
}
/**
* Creates a load-balancing endpoint (`saveEndpoint`, `type: "LB"`).
*
* @param input - `saveEndpoint` input fields.
* @returns The new endpoint's id and name.
*/
async createLoadBalancingEndpoint(
input: Record<string, unknown>,
): Promise<{ id: string; name: string }> {
const data = await this.graphql<{ saveEndpoint: { id: string; name: string } }>(
`mutation { saveEndpoint(input: ${toGraphQLLiteral({ ...input, type: 'LB' })}) { id name } }`,
)
return data.saveEndpoint
}
}
Functions
toGraphQLLiteral(value)
Serializes a plain object as a GraphQL input literal (unquoted keys,
type values that are enums stay quoted strings — RunPod takes strings).
function toGraphQLLiteral(value: unknown): string
value— The value.
Returns: The GraphQL literal.
gpuIdsFor(spec)
The GPU pools a spec may land on: the named pool, or every pool with enough memory.
function gpuIdsFor(spec: ModelEndpointSpec): string
spec— The endpoint spec.
Returns: Comma-separated gpuIds.
toTemplate(spec, diskGb)
The serverless template body for a spec.
function toTemplate(spec: ModelEndpointSpec, diskGb: number): Record<string, unknown>
spec— The endpoint spec.diskGb— Container disk.
Returns: TemplateCreateInput.
createProvider(config)
Creates a RunPod model hosting provider.
function createProvider(config?: RunPodConfig): ModelHostingProvider
config— API key, locations, disk size.
Returns: A ModelHostingProvider backed by RunPod Serverless.
Constants
RUNPOD_REST
Default REST base URL.
const RUNPOD_REST: 'https://rest.runpod.io/v1'
RUNPOD_GRAPHQL
Default GraphQL URL.
const RUNPOD_GRAPHQL: 'https://api.runpod.io/graphql'
RUNPOD_GPU_POOLS
GPU pools by memory, smallest first (docs.runpod.io/sdks/graphql/manage-endpoints, 2026-09-30).
const RUNPOD_GPU_POOLS: readonly { id: string; vramGb: number }[]
RUNPOD_CAPABILITIES
What RunPod Serverless load-balancing endpoints offer here (GPU only).
const RUNPOD_CAPABILITIES: ProviderCapabilities
provider
The provider implementation — lazy, so env vars are read on first use.
const provider: ModelHostingProvider
modelHostingRunPodSecretDefinitions
Secret definitions used by the RunPod model hosting bond.
const modelHostingRunPodSecretDefinitions: 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-runpod'
export function setupModelHostingRunpod(): 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
RUNPOD_API_KEY(required) — runpod api key- Setup: RunPod API key — Manages templates and serverless endpoints, and authorizes calls to them (Settings → API Keys). Workers stop when the account balance reaches $0.
- Get it here: https://www.console.runpod.io/user/settings
- Example:
rpa_XXXXXXXXXXXXXXXXXXXXXXXX
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-i18n -
@molecule/api-model-hosting -
@molecule/api-secrets -
Serve
/pingon your port. RunPod's load balancer health-polls/ping(theHEALTH_CHECK_PATHoverride is documented but reported ignored — runpod/docs#853) and expects200healthy /204still loading. A server with only/healthnever becomes healthy; add/ping(or proxy it). The bond setsPORT,PORT_HEALTHandHEALTH_CHECK_PATHfrom the spec anyway, anddeploywaits on YOURhealthPaththrough the public URL. -
Cold starts return "no workers available" until a worker is up; retry 3 times 5–10 s apart (RunPod's own guidance), or keep
minInstances: 1. -
GPU only.
minVramGbpicks the smallest pool class that fits (AMPERE_16→ 16 GB; 24 GB →AMPERE_24,ADA_24; …); name one or a comma-separated set inaccelerator.model.'us'→US,'eu'→CZ,FR,GB,NO,RO, or pass RunPod location codes. -
The endpoint is created with the GraphQL
saveEndpointmutation (type: "LB"), because the REST API cannot create load-balancing endpoints; updates, reads and deletes use REST.removedrains workers to 0 before deleting, then deletes the template. -
Anyone with your RunPod API key can call every endpoint — the key is both the admin credential and the endpoint credential. Keep it server-side.
-
Workers stop when the account balance reaches $0 — a silent outage.
-
list()returns every serverless endpoint on the account (RunPod does not say which are load-balancing). -
API errors throw
RunPodApiErrorwith the HTTPstatus.