Skip to content

Latest commit

 

History

History
139 lines (113 loc) · 7.18 KB

File metadata and controls

139 lines (113 loc) · 7.18 KB

@nodetool-ai/model-pricing

Unit price for a model chosen on a generic node's provider-model property (e.g. model on nodetool.image.TextToImage). The editor's cost preview and the server-side pre-run budget estimate both call getModelUnitPrice, so a run is gated on the same number the editor shows.

import { getModelUnitPrice } from "@nodetool-ai/model-pricing";

getModelUnitPrice({ id: "fal-ai/flux/schnell", provider: "fal_ai" });
// { unit_price: 0.003, billing_unit: "images", currency: "USD", source: "bundle" }

Where the prices come from

Looked up in order, first hit wins:

  1. FAL@nodetool-ai/fal-nodes/unit-pricing-catalog, keyed by endpoint_id.
  2. kie@nodetool-ai/kie-nodes/unit-pricing-catalog, keyed by model_id, using the USD conversion (a raw credit figure has no fixed USD value).
  3. GenSpendsrc/generated/genspend-pricing.json, keyed <provider_id>:<model_id>.

FAL and kie come from the providers themselves, so they stay ahead of GenSpend. GenSpend covers every other provider NodeTool can run and GenSpend tracks — Replicate, AtlasCloud, Together, Gemini, OpenAI, MiniMax, ElevenLabs, xAI — plus any FAL or kie model their own catalogs predate.

All three are imported as modules, not read off disk, so the estimate works identically in the browser bundle and inside the packaged Electron backend (no PACKAGE_RUNTIME_ASSETS entry needed).

The GenSpend catalog

GenSpend tracks what image/video/audio models cost across the providers that sell them, at GET /api/v1/models — public, read-only, synced from providers every ~6 h or hand-verified weekly.

scripts/sync-genspend-pricing.mjs normalizes that catalog into the shipped JSON:

npm run build:packages         # the sync reads the built providers
npm run sync:genspend          # rewrite the catalog
npm run sync:genspend:check    # exit 1 if it is stale

The GenSpend Pricing Sync workflow runs it nightly and opens a PR when a price moved. Never hand-edit the generated file.

Matching GenSpend models to NodeTool model ids

GenSpend keys models by its own slug (seedance-2); a run is priced by the provider-native id on the node (bytedance/seedance-2.0/text-to-video). Each entry records which of five routes bridged them, in descending order of trust:

  • alias — pinned by hand in scripts/genspend/aliases.json. An array of ids pins a match the name comparison cannot see; null blocks a model for that provider.
  • variant — a variants[] row whose spec names a model NodeTool ships. Best of the automatic routes, because the row carries that endpoint's own price: fal-ai/flux-pro/v1.1 bills $0.04 and …/v1.1-ultra $0.06, and both get their real number instead of sharing the model's headline price.
  • provider-id — GenSpend's providerModelId, the id the provider's own API invokes and bills with, on a receipt-or-null contract. Verified against the provider's NodeTool listing before use, so an id for a model NodeTool doesn't ship (resemble-ai/chatterbox) resolves to nothing instead of a bad key.
  • receipt — the offering's sourceUrl is a model page carrying the native id: fal.ai/models/fal-ai/flux/schnell, replicate.com/black-forest-labs/flux-dev. Ranks below provider-id because it infers the id from a URL path.
  • catalog — the model's normalized name exactly equals that of a model the provider itself enumerates in NodeTool (getAvailableImageModels and friends). That listing is the model picker's own source, so the sync can only ever emit ids NodeTool actually ships.

GenSpend prices a model once per provider, but a provider ships it as several task endpoints and only one of those is what providerModelId or a receipt names. So the name tier runs even when an exact id was found, carrying the price to the model's sibling endpoints at catalog trust — capability-checked, so a task the model cannot do gets nothing. AtlasCloud's Seedance 2.0 ends up with its text-to-video endpoint priced exactly and image-to-video and reference-to-video priced by inference, each labelled for what it is.

Five guards keep a wrong number out of a budget decision:

  • Published capabilities are respected. GenSpend's receipted task flags (t2i/i2i/t2v/i2v/r2v/v2v) drop an endpoint whose task the model cannot do — qwen-image reports i2i: false, so its price never lands on kie's image-edit endpoint. Only an explicit false blocks; null is unknown, not refuted.
  • Same modality, and speech is not music. "Gemini 3.1 Flash Image" and "Gemini 3.1 Flash TTS" normalize alike once the task word is stripped; the modality guard keeps them apart, and capabilities.kind === "music" keeps a music model off a provider's text-to-speech listing.
  • Exact key equality, never prefix or fuzzy — so seedance-2 cannot price seedance-2-mini, and kling-3-pro cannot price kling-3-turbo. A model's published aliases[] are matched too, which is how Nano Banana Pro and Gemini 3 Pro Image find each other.
  • Generation endpoints only. Upscalers, lip-sync and voice-clone endpoints bill on their own basis and never inherit a model's generation price.
  • Ambiguity is dropped, not resolved. A name hitting more than eight ids is a family name, not a model; it is reported instead of priced.
  • A video-input variant never prices a text-to-video endpoint. videoInput: true is a billing axis, not a spec note — providers charge more when a video goes in, so applying such a row to a t2v endpoint would undercharge. The headline price stands instead.
  • Tier ambiguity resolves upward. Where a provider selects tier by parameter, Pro and Standard share one callable id, so one NodeTool node could run either. Two equally-trusted models on one id keep the dearer price; the same model listed twice keeps the cheaper, which is what a run actually pays.

Anything left unresolved is printed by the sync and written to its --report file, so the gap is visible rather than silently absent.

Fetching

The sync requests ?envelope=1 (so the payload carries its own generatedAt) and sends the stored ETag as If-None-Match; a 304 ends the run as a no-op. Two wrinkles, both handled:

  • A compressing intermediary marks GenSpend's strong ETag weak in transit, and the origin then compares strongly and misses. The sync stores and sends the strong form.
  • The ETag covers the envelope's generatedAt, which turns over with the 60 s edge cache, so a nightly run usually gets a fresh body anyway. etag and catalogGeneratedAt are therefore frozen alongside the prices — otherwise the shipped file would change every night and open a PR with no price movement.

What the numbers mean

unit_price is in the provider's own unit; prices are only comparable inside one unitClass. Each entry keeps unit_class verbatim and maps it to a billing_unit label ("images", "seconds", …) matching the FAL catalog's vocabulary. Offerings that aren't available or carry no price are dropped. When two offerings land on one key, a receipt match outranks a name match, and otherwise the cheaper price wins.

Prices via genspend.io.