Skip to Content
prxy.monster v1 is in early access. See what shipped →
SdkModule interface

Module interface

A module is a TypeScript object with a stable shape. Two required fields, four optional hooks.

Install

pnpm add @prxy/module-sdk @prxy/shared-types

Hello world

import type { Module } from '@prxy/module-sdk'; export const hello: Module = { name: 'hello', version: '1.0.0', async pre(ctx) { ctx.logger.info('hello from a module'); return { continue: true }; }, };

That’s a complete, valid module. It does nothing useful but the gateway will load and run it.

The full interface

export interface Module { /** Stable module name. Must match the string used in PRXY_PIPE entries. */ name: string; /** Semver. Surfaced in /v1/pipeline responses for debugging. */ version: string; /** Optional: one-shot setup at gateway start. Runs once per process. */ init?(storage: StorageAdapter): Promise<void>; /** Optional: pre-request hook. Can mutate the request or short-circuit. */ pre?(ctx: RequestContext): Promise<PreResult>; /** Optional: per-chunk hook for streaming responses. */ stream?(chunk: CanonicalChunk, ctx: ResponseContext): Promise<CanonicalChunk>; /** Optional: post-response hook. Side effects only. */ post?(ctx: ResponseContext): Promise<void>; }

RequestContext

Passed to pre. Read-write.

export interface RequestContext { request: CanonicalRequest; // mutable — change anything metadata: Map<string, unknown>; // shared across modules storage: StorageAdapter; // KV / DB / blob apiKey: ApiKeyInfo; // { id, userId, tier, ... } logger: Logger; // pino-style startTime: number; // ms timestamp }

ResponseContext

Passed to stream and post. Adds response and timing.

export interface ResponseContext extends RequestContext { response: CanonicalResponse; durationMs: number; }

PreResult

The return type of pre. Two variants:

export type PreResult = | { continue: true } | { continue: false; response: CanonicalResponse };

Returning continue: false short-circuits the pipeline — remaining pre hooks and the provider call are skipped. Post hooks still run on the supplied response (this is how cost-guard returns 429s and how caches return hits).

A real example: a counter module

import type { Module } from '@prxy/module-sdk'; export function requestCounter(): Module { return { name: 'request-counter', version: '1.0.0', async init(storage) { // initialize the count if it doesn't exist const existing = await storage.kv.get('counter:total'); if (!existing) await storage.kv.set('counter:total', '0'); }, async pre(ctx) { const total = await ctx.storage.kv.get('counter:total'); ctx.metadata.set('request_number', Number.parseInt(total ?? '0', 10) + 1); return { continue: true }; }, async post(ctx) { // increment in the post hook so failed pre-hooks don't inflate the count const total = await ctx.storage.kv.get('counter:total'); const n = Number.parseInt(total ?? '0', 10) + 1; await ctx.storage.kv.set('counter:total', n.toString()); ctx.logger.info({ total: n }, 'request counted'); }, }; }

A real example: the cost-guard module

This is the actual cost-guard from @prxy/modules-core, abridged:

import type { Module } from '@prxy/module-sdk'; import { calculateActualCost, estimateRequestCost } from './lib/cost.js'; import { errorResponse } from './lib/errors.js'; export interface CostGuardConfig { perRequest?: number; perDay?: number; perMonth?: number; } export function costGuard(config: CostGuardConfig = {}): Module { return { name: 'cost-guard', version: '1.0.0', async pre(ctx) { const estimated = estimateRequestCost(ctx.request); ctx.metadata.set('cost.estimated', estimated); if (config.perRequest != null && estimated > config.perRequest) { return { continue: false, response: errorResponse('cost_limit_per_request', 'Request exceeds cap', { limit: config.perRequest, estimated, status: 429, }), }; } // ...per-day, per-month checks omitted... return { continue: true }; }, async post(ctx) { if (ctx.response.stopReason === 'error') return; const actual = calculateActualCost(ctx.request, ctx.response); ctx.metadata.set('cost.actual', actual); // ...write spend counters to KV... }, }; }

The same shape your custom modules will follow.

Where modules live

  • Built-in modules ship in @prxy/modules-core and are loaded by name from PRXY_PIPE.
  • Custom modules can be loaded from a local file path or an npm package (planned for v1.1 — see Publishing).

Module factory functions (the costGuard(config) shape) are the convention. Direct singleton exports work too — but a factory makes per-key config cleaner.

See also

Last updated on