Skip to main content

Model Registry

To calculate execution costs and select correct routing paths, workflow-engine maintains a central directory of model pricing, context sizes, and provider keys called the Model Registry.


Global Custom Registrations​

The engine includes a list of predefined models (like gemini-2.5-flash), but you can register any model (including private or newly-released models) using registerModels():

import { registerModels } from "@bratsos/workflow-engine";

registerModels({
"custom-llama-3": {
id: "meta-llama/llama-3-8b-instruct", // Provider ID (OpenRouter / Gemini)
name: "Llama 3 8B",
provider: "openrouter",
inputCostPerMillion: 0.2, // USD per 1M input tokens
outputCostPerMillion: 0.4, // USD per 1M output tokens
contextLength: 8192,
maxCompletionTokens: 2048,
supportsTools: true,
supportsStructuredOutputs: true,
supportsAsyncBatch: true,
batchProvider: "openrouter", // where ctx.step.ai.map batches it; defaults to the native vendor
batchModelId: "meta-llama/llama-3-8b-instruct:batch",
batchInputCostPerMillion: 0.1,
batchOutputCostPerMillion: 0.2,
},
"custom-voyage-embed": {
id: "voyage-large-2-instruct",
name: "Voyage Large 2",
provider: "voyage", // Custom provider resolver
inputCostPerMillion: 0.12,
outputCostPerMillion: 0,
isEmbeddingModel: true,
}
});

TypeScript Autocomplete via Module Augmentation​

By default, model key parameters in AIHelper methods (like generateText) accept any string. To enable autocomplete and compile-time verification, you can augment the ModelRegistry interface:

// model-registry-env.d.ts
import "@bratsos/workflow-engine/client";

declare module "@bratsos/workflow-engine/client" {
interface ModelRegistry {
"custom-llama-3": true;
"custom-voyage-embed": true;
}
}

Once augmented, calling ai.generateText("custom-llama-3", ...) is verified by the compiler, and typos will throw TypeScript build errors.

The ModelKey type stays open (any string is accepted, augmentation only adds autocomplete), and the exported ModelKey zod schema is z.string().min(1) — a schemas.config field typed with it no longer rejects an unregistered key at run.create. Validation happens where the model is resolved: getModel(key) throws with the registered keys in the message. Check batch capability with getModel(key).supportsAsyncBatch; the 0.x helpers (getModelById, getRegisteredModel, listRegisteredModels, getDefaultModel, modelSupportsBatch, printAvailableModels) were removed in 1.0.


The Model Sync CLI​

Manually compiling model prices and capacities is tedious, especially with OpenRouter's frequent updates. workflow-engine ships with a sync CLI to automate this.

The CLI fetches all active models from OpenRouter (including pricing details and parameter capabilities), filters them, and exports an auto-generated model registry file with full TypeScript module augmentations.

1. Configuration​

Create a configuration file in your project root named workflow-engine.models.ts:

// workflow-engine.models.ts
import { type ModelSyncConfig } from "@bratsos/workflow-engine";

const config: ModelSyncConfig = {
outputPath: "src/generated/models.ts",
include: [
/^google\/gemini-2.5/,
/^anthropic\/claude-3.5/,
/^openai\/gpt-4o/
],
exclude: [
/-preview$/,
/gpt-oss/
],
customModels: {
"my-internal-llm": {
id: "custom/my-internal-llm",
name: "Internal Llama",
inputCostPerMillion: 0.1,
outputCostPerMillion: 0.2,
provider: "openrouter"
}
}
};

export default config;

2. Execution​

Run the sync command. The OpenRouter catalog endpoint is public, so no API key is required (an optional OPENROUTER_API_KEY will be sent if present):

# Run unauthenticated (default)
npx workflow-engine-sync

# Or optionally with your API key
export OPENROUTER_API_KEY="your-openrouter-key"
npx workflow-engine-sync

This generates src/generated/models.ts. Import this file once in your application entrypoint (e.g. index.ts) to register the models and load autocomplete keys globally:

import "./generated/models";

OpenRouter max_price Guardrails​

When routing requests through OpenRouter (which aggregates dozens of independent host providers for the same model), prices can fluctuate or spike if cheaper hosts experience downtime.

To protect against unexpected charges, workflow-engine enforces strict max_price guardrails at the API payload layer:

  • When dispatching a request to an "openrouter" provider, the engine automatically extracts the inputCostPerMillion and outputCostPerMillion values declared in the model config.
  • It injects these values directly into the OpenRouter provider body:
    {
    "provider": {
    "sort": "throughput",
    "require_parameters": true,
    "max_price": {
    "prompt": modelConfig.inputCostPerMillion,
    "completion": modelConfig.outputCostPerMillion
    }
    }
    }
  • If OpenRouter tries to route your call to a host provider that charges more than your model configuration defines, OpenRouter rejects the call rather than serving it at the higher price. That bounds the per-token price of a routed call to what the registry says; it is not a spend cap, and it does not apply to non-token charges, which OpenRouter documents as not discounted and not covered by max_price.