@molecule/app-ai-models
v1.2.0
Published
Framework-agnostic client + UI metadata for the AI model catalog
Readme
@molecule/app-ai-models
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
App-side AI model catalog client.
Framework-agnostic loader, types, and UI-only constants (PROVIDER_BRAND_COLORS,
formatTokenCount, effort helpers). Hosts the lazy fetch of GET /ai/models.
Framework bindings (e.g. useAIModels in @molecule/app-react) wrap this loader.
Quick Start
import { getClient } from '@molecule/app-http'
import { loadAIModels, partitionByDeprecation } from '@molecule/app-ai-models'
const models = await loadAIModels(getClient())
const { current, deprecated } = partitionByDeprecation(models)Type
core
Installation
npm install @molecule/app-ai-models @molecule/app-httpAPI
Interfaces
AIModelCatalog
The full GET /ai/models payload: the model list plus (on servers that
compute them) the per-mode default model ids for the requester's tier.
interface AIModelCatalog {
/** The list of models available to the current session. */
models: AppModelDefinition[]
/** Per-mode server default model ids, when the server provides them. */
defaults?: AppModeModelDefaults
}AppModelDefinition
Client-visible model metadata. Mirrors every field of the server-side
ModelDefinition; no field is currently hidden from authenticated clients.
interface AppModelDefinition {
/** API model ID. */
id: string
/** Which AI provider serves this model. */
provider: AIProviderID
/** Human-readable label for picker display. */
label: string
/** Short description for picker display. */
description: string
/** Maximum input context window in tokens. */
contextWindow: number
/** Maximum output tokens per response. */
maxOutputTokens: number
/** Whether the model supports extended thinking / chain-of-thought. */
supportsThinking: boolean
/** Default thinking budget in tokens (only relevant when `supportsThinking` is true). */
thinkingBudgetTokens: number
/** Whether the thinking budget can be controlled via API params. */
thinkingConfigurable: boolean
/**
* The model's OWN effort levels, ordered ascending — the exact values the
* `/effort` command offers and that get persisted. Native-effort models list
* their provider values (`['low', 'high', 'xhigh', 'max']`); budget models
* list scaled-budget labels (`['4K', '8K', '16K', '32K']`); fixed-reasoning
* models omit it. A persisted value outside the set degrades to the nearest.
* Mirrors the server-side `ModelDefinition` field.
*/
supportedEffortLevels?: EffortLevel[]
/** The model's default effort value (a member of `supportedEffortLevels`). */
defaultEffortLevel?: EffortLevel
/**
* Budget-configurable models only: maps each `supportedEffortLevels` label to
* the thinking-token budget it sends. Its presence marks a model as
* budget-driven (sends `budget_tokens`) rather than native-effort (sends the
* level as the provider's effort param). Mirrors the server-side field.
*/
effortBudgetTokens?: Record<string, number>
/** Whether the model supports vision (images, documents, etc.). */
supportsVision: boolean
/** Whether the model supports prompt caching. */
supportsPromptCaching: boolean
/** Whether the model supports tool use / function calling. */
supportsTools: boolean
/** Optional provider-specific server tool type for web search. */
webSearchToolType?: string
/** Optional provider-specific server tool type for code execution. */
codeExecutionToolType?: string
/** Optional provider-specific server tool type for web fetch. */
webFetchToolType?: string
/** Whether this model is available on the free tier. */
freeTier?: boolean
/**
* Regions in which this model is free-tier selectable even though the model
* as a whole is not `freeTier` (regional hosts can price very differently).
* Ignored when `freeTier` is true; omitted → no per-region free access.
* Mirrors the API catalog's `ModelDefinition.freeTierRegions`.
*/
freeTierRegions?: string[]
/**
* Processing regions this model can run in (arbitrary region codes; FIRST
* entry is the model's default region). Omit for `['us']`. A single-entry
* list pins the model to that region. Mirrors the server-side
* `ModelDefinition.regions`.
*/
regions?: string[]
/**
* Per-region price overrides in USD per MTok for regions billed differently
* from the base rates (the native provider's list prices). A region with no
* entry bills at the base rates; omitted cache fields fall back to the
* region's `inputPricePerMTok`. Mirrors the server-side
* `ModelDefinition.regionPricing`.
*/
regionPricing?: Record<
string,
{
/** Region input price per million uncached tokens in USD. */
inputPricePerMTok: number
/** Region output price per million tokens in USD. */
outputPricePerMTok: number
/** Region prompt-cache read price per million tokens in USD. */
cacheReadPricePerMTok?: number
/** Region prompt-cache write price per million tokens in USD. */
cacheWritePricePerMTok?: number
}
>
/** Input price per million *uncached* (fresh) input tokens in USD. */
inputPricePerMTok: number
/** Output price per million tokens in USD. */
outputPricePerMTok: number
/** Price per million prompt-cache *read* (cache-hit) input tokens in USD. */
cacheReadPricePerMTok: number
/** Price per million prompt-cache *write* (cache-creation) input tokens in USD. */
cacheWritePricePerMTok: number
/**
* Provider peak-hour pricing: during these UTC windows every one of this
* model's token rates bills at `multiplier ×` the listed values. Mirrors the
* server-side `ModelDefinition.peakPricing`.
*
* Surfaced to the user because it is not a rounding error — DeepSeek's native
* windows are a flat 2×, so the same conversation costs twice as much inside
* them. A cost figure that silently ignores this tells users their allowance
* drains at a rate it does not. The rate the picker shows is always the one
* in force NOW ({@link modelUsageRate}), with the windows named alongside so
* a number that changes during the day is explained rather than surprising.
*
* Windows are minutes-since-midnight UTC, half-open `[start, end)`; a window
* may wrap midnight (`start > end`).
*
* Applies to the BASE rates only. A region with a `regionPricing` override is
* a different host billing its own card, which generally has no time-of-day
* component — so peak never applies there, matching `priceMultiplierAt`.
*/
peakPricing?: {
windows: { startMinuteUtc: number; endMinuteUtc: number }[]
multiplier: number
}
/**
* Fast-mode ("priority speed") pricing. PRESENCE marks the model as
* fast-mode capable — the ⚡ composer toggle and the picker's `fast` badge
* key off this field. Mirrors the server-side `ModelDefinition.fastPricing`.
*/
fastPricing?: {
/** Fast-mode input price per million uncached tokens in USD. */
inputPricePerMTok: number
/** Fast-mode output price per million tokens in USD. */
outputPricePerMTok: number
/** Fast-mode prompt-cache read price per million tokens in USD. */
cacheReadPricePerMTok: number
/** Fast-mode prompt-cache write price per million tokens in USD. */
cacheWritePricePerMTok: number
}
/** Reliable knowledge cutoff date (YYYY-MM-DD). */
knowledgeCutoff: string
/**
* When the model was (or will be) deprecated (YYYY-MM-DD).
*
* Past dates: still selectable, but the picker hides them in an "Older
* models" section. Future dates: treated as current. Omit entirely for
* current models.
*/
deprecatedAt?: string
/**
* Whether this model is fully disabled — removed from selection and the
* listing while remaining priceable for historical usage. Stronger than
* {@link deprecatedAt} (which keeps the model selectable in an "Older models"
* section): a disabled model is excluded from the free-tier / deprecation
* partition helpers and never offered. Kept in sync with the server-side
* `ModelDefinition.disabled`. Omit entirely for active models.
*/
disabled?: boolean
/**
* The id of the newer-generation model that replaces this one, set on the
* OLDER entry (e.g. `qwen3.7-max` carries `supersededBy: 'qwen3.8-max'`).
*
* Treated like {@link disabled} by the picker helpers — a superseded model is
* never offered, so the user sees exactly one generation of each model family
* — but it stays priceable for historical usage, and the successor id is the
* migration target for a saved selection. A server that filters properly
* never sends these; the helpers drop them anyway so an older API can't
* reintroduce them. Kept in sync with the server-side
* `ModelDefinition.supersededBy`. Omit entirely for current models.
*/
supersededBy?: string
}AppModeModelDefaults
The model ids the SERVER falls back to per mode/job when the user hasn't
picked one — already resolved for the requester's tier. Mirrors the server's
ModeModelDefaults. Lets the picker label an unset per-mode selection
"Default ()" instead of a vague "default".
interface AppModeModelDefaults {
/** Model id used in plan mode when nothing is configured. */
plan: string
/** Model id used in execute mode when nothing is configured. */
execute: string
/** Model id used for commit-message generation when nothing is configured. */
commit: string
/** Model id used for conversation compaction when nothing is configured. */
compact: string
}EffortOption
One selectable effort option for a model — its own native value.
interface EffortOption {
/** The native value the user sees, types, and that gets persisted. */
value: string
}ListAIModelsResponse
Wire-shape response returned by GET /ai/models.
interface ListAIModelsResponse {
models: AppModelDefinition[]
/**
* Per-mode server default model ids for the requester's tier. Optional —
* servers that don't compute them omit it, and clients fall back to generic
* "default" labeling.
*/
defaults?: AppModeModelDefaults
}Types
AIProviderID
AI provider identifier shipped with each model. Stays in sync with the
server-side AIProviderID by convention; any drift here will surface as a
mismatch with PROVIDER_BRAND_COLORS.
type AIProviderID =
| 'anthropic'
| 'openai'
| 'google'
| 'xai'
| 'deepseek'
| 'meta'
| 'moonshot'
| 'minimax'
| 'alibaba'
| 'zhipu'
/**
* A model served by a USER-configured endpoint + key (bring-your-own AI).
* Appears only in project-scoped listings (`GET /ai/models?projectId=…`);
* pricing fields are 0 (the user pays their own provider directly).
*/
| 'custom'EffortLevel
A reasoning-effort value — a model's OWN native effort level (e.g. 'high',
'xhigh', 'max', or a budget label like '16K'). There is no abstract
cross-model scale; the stored value is the model's real level. Mirrors the
server-side EffortLevel in @molecule/api-resource-ai-models; keep in sync.
type EffortLevel = stringFunctions
defaultEffortForModel(model)
The model's default effort value (used when the user hasn't chosen), or null
when the model has no effort levels.
function defaultEffortForModel(model: AppModelDefinition | undefined): string | nullmodel— The model (orundefined).
Returns: The default native value, or null.
effortOptionsForModel(model)
The effort options a user can pick for a model, in ascending order — the
model's own supportedEffortLevels. Empty for fixed-reasoning models (and
unknown models), which expose no effort choice.
function effortOptionsForModel(model: AppModelDefinition | undefined): EffortOption[]model— The model to build options for, orundefinedwhen unknown.
Returns: The selectable options (empty when the model has no effort levels).
formatTokenCount(tokens)
Format a token count for display (e.g. 200000 -> "200K", 1000000 -> "1M").
function formatTokenCount(tokens: number): stringtokens— Token count.
Returns: Formatted string.
isDeprecated(model, now)
Returns true when the model is deprecated as of now. A model is deprecated
if deprecatedAt is set and lexicographically <= the now date (YYYY-MM-DD
strings compare as dates). Models with a future deprecatedAt are still
current — useful for scheduling deprecations.
function isDeprecated(model: Pick<AppModelDefinition, 'deprecatedAt'>, now?: string): booleanmodel— Model to check.now— Today's date as YYYY-MM-DD. Defaults to the current UTC date.
Returns: true if the model is deprecated as of now.
isSelectableModel(model)
Whether a model may be offered in the picker: neither disabled (retired by
the provider) nor supersededBy a newer generation of its own family. The
single predicate every picker helper here uses, mirroring the server-side
isSelectableModel.
function isSelectableModel(model: Pick<AppModelDefinition, 'disabled' | 'supersededBy'>): booleanmodel— Model to check (only the two flags are read).
Returns: true when the model may be listed and chosen.
loadAIModelCatalog(http, path, projectId)
Fetches the AI model catalog from the API — the model list plus the server's per-mode default model ids (when provided).
function loadAIModelCatalog(
http: HttpClient,
path?: string,
projectId?: string,
): Promise<AIModelCatalog>http— HTTP client bonded by the host app.path— Endpoint path, defaults to'/ai/models'(the http client supplies the base URL).projectId— Optional project scope. When set, servers that support per-project custom ("bring your own AI") models append them to the catalog, flaggedprovider: 'custom'; servers that ignore the query param return the unscoped catalog unchanged.
Returns: The catalog: models plus optional per-mode defaults.
loadAIModels(http, path, projectId)
Fetches the AI model catalog from the API and returns the models array. Thin back-compat wrapper over {@link loadAIModelCatalog} for callers that don't need the per-mode defaults.
function loadAIModels(
http: HttpClient,
path?: string,
projectId?: string,
): Promise<AppModelDefinition[]>http— HTTP client bonded by the host app.path— Endpoint path, defaults to'/ai/models'(the http client supplies the base URL).projectId— Optional project scope (see {@link loadAIModelCatalog}).
Returns: The list of models available to the current session.
nativeEffortName(model, value)
Resolve a persisted effort value to the one the model will actually use — its
exact value, a legacy S|M|L|XL mapped by position, the nearest native level
by rank, or the model's default. null when the model has no effort levels
(fixed reasoning — callers show their own "fixed" copy). Mirrors the
server-side resolveEffortForModel so the display always matches what the
backend sends.
function nativeEffortName(
model: AppModelDefinition | undefined,
value: string | undefined,
): string | nullmodel— The active model (orundefined).value— The persisted effort value (orundefined).
Returns: The resolved native value, or null when reasoning is fixed.
partitionByDeprecation(models, now)
Splits a model catalog into current and deprecated entries based on each
model's deprecatedAt relative to now. Order within each partition is
preserved. Models that are not {@link isSelectableModel} — disabled or
superseded by a newer generation — are dropped entirely: they belong in
neither partition (the listing already excludes them, and they must not
surface in the picker's current or "Older models" section).
function partitionByDeprecation(
models: readonly AppModelDefinition[],
now?: string,
): { current: AppModelDefinition[]; deprecated: AppModelDefinition[] }models— Loaded model catalog.now— Today's date as YYYY-MM-DD. Defaults to the current UTC date.
Returns: Object with current and deprecated arrays.
pickFreeTierModel(models)
Returns the free-tier model from a list, or undefined if none is marked.
Models that are not {@link isSelectableModel} are ignored — a retired or
superseded model is never picked as the free-tier default even if it still
carries the flag.
function pickFreeTierModel(models: readonly AppModelDefinition[]): AppModelDefinition | undefinedmodels— Loaded model catalog.
Returns: The single selectable model with freeTier: true, or undefined.
Constants
PROVIDER_BRAND_COLORS
Brand colors keyed by provider ID. Used as accent colors in picker rows.
const PROVIDER_BRAND_COLORS: Readonly<Record<AIProviderID, string>>Injection Notes
Requirements
Peer dependencies:
@molecule/app-http^1.0.1
Runtime Dependencies
@molecule/app-httpThe server side is
@molecule/api-resource-ai-models— it serves the auth-gatedGET /ai/modelsroute this loader calls. Without that resource (or an equivalent route returningListAIModelsResponse),loadAIModelsfails. The route is session-gated: fetch with the app's authenticated HTTP client.loadAIModelsdoes NOT cache — call it once and keep the result (the framework hook does this for you). UsepickFreeTierModel/partitionByDeprecationinstead of re-deriving tier/deprecation logic;disabledmodels must never surface in a picker.
