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-typesHello 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-coreand are loaded by name fromPRXY_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
- Lifecycle hooks — when each hook fires.
- Storage access — how to use
ctx.storage. - Publishing — distribute your module.