@x12i/ai-tools
v3.3.3
Published
AI model catalogs (open-assets JSON), cost calculation, and LLM utilities
Readme
@x12i/ai-tools
TypeScript package for AI model catalogs, runtime cost calculation, and LLM utilities.
Pricing data is loaded from two JSON catalogs on open-assets.x12i.com, with bundled fallbacks shipped in the published package (src/data):
| Catalog | URL | Used when |
|---------|-----|-----------|
| Direct vendor | models-catalog.json | provider is openai, anthropic, google, … |
| OpenRouter | openrouter-models-catalog.json | provider is openrouter, or as fallback |
Remote catalogs are always treated as the latest source. The JSON version field (e.g. "0.1.0") is publisher metadata only — this library does not read or compare it.
v3.0 breaking changes
Aligned with @x12i/ai-profiles v3.3.0.
Accepted model strings
Only two forms are supported everywhere (model, modelUsed, usedModel, CLI --model, invoke config):
profile/choice— e.g.cheap/default,balanced/default,deep/google_deep- Concrete model id — e.g.
deepseek-v3.2,deepseek/deepseek-v3.2,gpt-5.5,openai/gpt-5.5,gemini-2.5-flash-lite
Removed (v3)
| Removed | Replacement |
|---------|-------------|
| ai-profiles shortcuts (standard, cheapest, economy, …) | Full profile/choice keys (balanced/default, cheap/default, …) |
| Bare profile names (cheap, balanced) | cheap/default or { profile: "cheap", choice: "default" } |
| Project-local aliases (@x12i/ai-tools/aliases, ai-tools alias CLI, AliasRegistry) | Store profile/choice or concrete model ids in config |
| Legacy model shorthands (deepseekv32, claude-sonnet, …) | Catalog ids (deepseek-v3.2, deepseek/deepseek-v3.2) |
| listAIShortcuts | listAIProfiles / listAIProfileChoices |
| applyOpenRouterInvokePolicy | applyOpenRouterInvokeRouting |
| ModelProfileUnroutableError.profileAlias | profileKey |
Migration examples
| Before (v2.x) | After (v3) |
|---------------|------------|
| standard, cheapest | balanced/default, cheap/default |
| cheap (bare) | cheap/default |
| ai-tools alias set best … | Store concrete id or profile/choice in config |
| deepseekv32 | deepseek-v3.2 or deepseek/deepseek-v3.2 |
| applyOpenRouterInvokePolicy(...) | applyOpenRouterInvokeRouting(...) |
Dependency: @x12i/ai-profiles ^3.3.0 (minimum 3.3.0).
Invoke model resolution
Host-agnostic pipeline for AI engines that call LLMProviderRouter.invoke(). Pass { provider?, model }; get router-ready { provider, model, allowOpenRouterProxy?, providerProxy? } plus structured diagnostics.
import { resolveInvokeModel } from "@x12i/ai-tools";
// or: import { resolveInvokeModel } from "@x12i/ai-tools/invoke";
const { router, diagnostics, resolution } = await resolveInvokeModel(
{ provider: invokeConfig.provider, model: invokeConfig.model },
{
preferOpenRouter: true,
openRouterApiKey: process.env.OPENROUTER_API_KEY,
defaultProvider: "openai",
modelsOnly: false, // set true to reject profile/choice inputs
},
);
Object.assign(invokeConfig, router);
// diagnostics.profileKey / invokedModelId for logging when routedViaOpenRouter| Export | Role |
|--------|------|
| resolveInvokeModel | Full pipeline (catalog → vendor map → OR routing) |
| buildInvokeModelResolverOptions | Build ModelResolverOptions with routeViaOpenRouter when preferred |
| mapResolutionToRouterConfig | Map ModelResolutionSuccess → { provider, model } |
| applyOpenRouterInvokeRouting | Set allowOpenRouterProxy / providerProxy on router config |
| normalizeInvokeModel | Idempotent gateway invoke wire shape (from ai-profiles v3.2) |
| resolveAndNormalizeInvokeModel | resolveAIProfile → normalizeInvokeModel |
| createAiToolsInvokeClient | Shared catalog + cost calculator + routing env |
| ModelProfileUnroutableError | Profile/choice input that failed to route |
Responsibility split: ai-tools owns model strings, catalogs, OpenRouter vs direct transport, and cost. The AI engine owns sampling defaults, maxTokens, message templates, router registration, and activity tracking.
Set resolveModels: false for passthrough (no catalog lookup, no resolution errors).
AI profiles (@x12i/ai-profiles v3)
Profile resolution follows the same strict contract as @x12i/ai-profiles — inside and outside this repo (configs, graph nodes, AI engines, any service that calls resolveAIProfile).
Two kinds of model strings
| Intent | What you pass | How ai-tools resolves it |
|--------|---------------|---------------------------|
| Profile (capability tier) | profile/choice only | @x12i/ai-profiles → concrete provider + modelId |
| Concrete model (vendor SKU) | gemini-2.5-flash-lite, deepseek/deepseek-v3.2, … | Catalog / profile-choice index |
flowchart TD
input["model / modelUsed / usedModel string"]
input --> composite{"profile/choice?"}
composite -->|yes| profiles["@x12i/ai-profiles\nresolveAIProfile"]
composite -->|no| concrete["catalog + registry index\n(concrete model SKU)"]
profiles --> out["provider + modelId + pricing"]
concrete --> outProfile keys: profile/choice (required)
Canonical resolve key: {profile}/{choice} with a forward slash /.
"cheap/default"
"balanced/default"
"deep/google_deep"
"local/llama_cpp_gguf"- Profile = capability contract (
cheap,balanced,deep,json, …) — not a model name. - Choice = implementation variant within that profile (
default,google_floor, …).
Bare profile names are not accepted (cheap, balanced, deep alone). ai-tools does not infer default for you.
| Valid | Invalid |
|-------|---------|
| cheap/default | cheap |
| deep/default | standard, cheapest (removed shortcuts) |
| balanced/default | balanced (without /choice) |
resolveProfileForAsk — split fields allowed
For ask-node / FuncX config, pass either the combined key or explicit fields (still requires a choice):
import { resolveProfileForAsk, TEXT_CATALOG_LANE } from "@x12i/ai-tools";
await resolveProfileForAsk({
profile: "cheap/default",
catalogLane: TEXT_CATALOG_LANE,
});
await resolveProfileForAsk({
profile: "cheap",
choice: "default",
catalogLane: TEXT_CATALOG_LANE,
});catalogLane is required ("text" for most tiers; "image" for vision).
Concrete model names
When the string is a vendor or OpenRouter model id, resolution uses the catalog and the profile registry index:
import { CostCalculator, AiModelsCatalogClient } from "@x12i/ai-tools";
const calculator = new CostCalculator(catalog);
await calculator.calculate({
tokens: { prompt: 1_000_000, completion: 0, total: 1_000_000 },
provider: "google",
modelUsed: "gemini-2.5-flash-lite",
});
await calculator.calculate({
tokens: { prompt: 1_000_000, completion: 0, total: 1_000_000 },
provider: "google",
modelUsed: "cheap/default",
});const catalog = new AiModelsCatalogClient();
await catalog.resolveModel({ model: "gemini-2.5-flash-lite", provider: "google" });
await catalog.resolveModel({ model: "cheap/default" });
// await catalog.resolveModel({ model: "cheap" }); // not found
// await catalog.resolveModel({ model: "standard" }); // not found (shortcut removed)Errors and helpers
import {
requireProfileResolveKey,
profileResolveKey,
ProfileResolveKeyRequiredError,
formatProfileChoiceKey,
isKnownProfileChoice,
ensureConcreteModel,
} from "@x12i/ai-tools";
await requireProfileResolveKey("cheap", { choice: undefined }); // throws
await profileResolveKey("gemini-2.5-flash-lite"); // null — concrete SKU
await profileResolveKey("cheap/default"); // "cheap/default"
isKnownProfileChoice("cheap/default"); // true
isKnownProfileChoice("standard"); // falseWhat to put in configs
| Stored value | Use when |
|--------------|----------|
| cheap/default, deep/default | Task / graph tier (profile intent) |
| gemini-2.5-flash-lite, deepseek/deepseek-v3.2, openai/gpt-5.5 | Explicit model SKU |
| ~~cheap~~, ~~standard~~, ~~deepseekv32~~ | Do not use — rejected in v3 |
Re-exports
import {
resolveAIProfile,
listAIProfiles,
listAIProfileChoices,
resolveAIProfileByTags,
resolvePreferOpenRouter,
readPreferOpenRouterFromEnv,
ensureConcreteModel,
resolveProfileForAsk,
TEXT_CATALOG_LANE,
} from "@x12i/ai-tools";
// or
import { resolveProfileForAsk, requireProfileResolveKey } from "@x12i/ai-tools/profiles";Install
npm install @x12i/ai-toolsRequires Node.js 20+.
Optional peers: @x12i/helpers, openai, better-sqlite3 (toolbox SQLite storage).
Quick start
import { AiModelsCatalogClient, CostCalculator } from "@x12i/ai-tools";
const catalog = new AiModelsCatalogClient();
const calculator = new CostCalculator(catalog);
const result = await calculator.calculate({
tokens: { prompt: 1000, completion: 500, total: 1500 },
provider: "openai",
usedModel: "gpt-5.5-2026-04-23",
});
console.log(result.cost);
console.log(result.resolvedModelId);
console.log(result.usedModel);
console.log(result.usage);From activity or invoke records:
const result = await calculator.calculateFromRecord(activityDocument);Catalog loading and cache
Catalogs are fetched on first use and cached in memory for 24 hours. Override TTL with AI_TOOLS_CACHE_TTL_MS. If a fetch fails, bundled src/data/*.json is used automatically.
npx ai-tools catalog refresh
npx ai-tools catalog verify --json
npx ai-tools catalog refresh --bundled-only| npm script | Command |
|------------|---------|
| npm run catalog:refresh | Fetch catalogs and warm cache |
| npm run catalog:verify | Verify both catalogs load |
Cost calculation
await calculator.calculate({
tokens: { prompt: 1000, completion: 500, total: 1500 },
provider: "openrouter",
usedModel: "deepseek/deepseek-v3.2",
});Runtime model priority: usedModel → modelUsed → model.
Profile vs model in usedModel: pass cheap/default for tier-based pricing; pass gemini-2.5-flash-lite (or vendor/slug) for a concrete SKU.
Smart model resolution
ModelNameResolver normalises provider + model input against the cached catalog. Catalog record aliases (metadata in JSON, e.g. deepseek-v3.2 → deepseek/deepseek-v3.2) are supported; shorthand tokens (deepseekv32, etc.) are not.
import { AiModelsCatalogClient, ModelNameResolver } from "@x12i/ai-tools";
const catalog = new AiModelsCatalogClient();
const models = await catalog.getAllModels();
const resolver = new ModelNameResolver(models);
const result = await catalog.resolveModel({
provider: "openrouter",
model: "deepseek-v3.2",
});| Input | Typical resolution |
|-------|-------------------|
| openrouter + deepseek-v3.2 | deepseek/deepseek-v3.2 |
| cheap/default | ai-profiles → e.g. google/gemini-2.5-flash-lite |
| standard, cheapest | not found (use balanced/default, cheap/default) |
| deepseekv32 | not found (use deepseek-v3.2) |
| gemini-2.5-flash-lite | registry index / catalog |
| ollama + llama3:8b | local passthrough |
OpenRouter vs direct routing
| Condition | Routes via OpenRouter |
|-----------|----------------------|
| PREFER_OPENROUTER true (default) and OPENROUTER_API_KEY set | Yes |
| OPENROUTER_API_KEY set, vendor {VENDOR}_API_KEY missing | Yes |
| Vendor key present, PREFER_OPENROUTER=false or 0 | Direct |
import {
loadOpenRouterRoutingEnv,
shouldDefaultRouteViaOpenRouter,
resolvePreferOpenRouter,
} from "@x12i/ai-tools";CLI
npx ai-tools catalog refresh
npx ai-tools catalog verify --json
npx ai-tools models list --provider openai
npx ai-tools models resolve --model deepseek-v3.2 --provider openrouter --verbose
npx ai-tools cost --model openai/gpt-5.5 --prompt-tokens 1000 --completion-tokens 500 --provider openai
npx ai-tools cost --model cheap/default --prompt-tokens 1000000 --completion-tokens 0 --provider google
npx ai-tools models resolve --model cheap/default --verboseEnvironment variables
Copy .env.example to .env in your app.
| Variable | Purpose |
|----------|---------|
| OPENROUTER_API_KEY | OpenRouter routing defaults; enables invoke proxy routing when set |
| PREFER_OPENROUTER | true / 1 / false / 0 — prefer OpenRouter when OR + vendor keys both set (default true when unset; parsed via @x12i/ai-profiles) |
| OPENAI_API_KEY, ANTHROPIC_API_KEY, … | Direct vendor keys ({PROVIDER_ID}_API_KEY) |
| AI_TOOLS_CACHE_TTL_MS | Catalog in-memory cache TTL (default: 86400000 = 24h) |
Package exports
| Subpath | Contents |
|---------|----------|
| @x12i/ai-tools | Catalog client, cost calculator, resolver, profiles, invoke |
| @x12i/ai-tools/cost | Cost types, extractUsageInput, CostCalculator |
| @x12i/ai-tools/catalog | loadCatalogSources, refresh/verify |
| @x12i/ai-tools/models | Model listing and filters |
| @x12i/ai-tools/sync | ModelNameResolver, OpenRouter fetch helper |
| @x12i/ai-tools/profiles | resolveProfileForAsk, ai-profiles v3 re-exports |
| @x12i/ai-tools/invoke | resolveInvokeModel, OpenRouter invoke routing helpers |
| @x12i/ai-tools/toolbox | Tracker, router, guard |
Dependency: @x12i/ai-profiles ^3.3.0.
Tests
npm test
npm run build
npm run test:live # optional OpenRouter API live testLicense
MIT
