npm.io
3.3.4 • Published yesterdayCLI

@x12i/ai-tools

Licence
MIT
Version
3.3.4
Deps
6
Size
1.2 MB
Vulns
0
Weekly
720

@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):

  1. profile/choice — e.g. cheap/default, balanced/default, deep/google_deep
  2. 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 resolveAIProfilenormalizeInvokeModel
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-profilesinside 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 --> out
Profile 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"); // false
What 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-tools

Requires 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: usedModelmodelUsedmodel.

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.2deepseek/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 --verbose

Environment 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 test

License

MIT

Keywords