@molecule/app-ai-models
Core interface · ai-models · App (browser) · v1.4.0 · Apache-2.0
App-side client + UI metadata for the AI model catalog (loader + brand colors + token formatter)
npm install @molecule/app-ai-modelsHow it works
@molecule/app-ai-models is the ai-models core interface on the app (browser) side: the API your app calls, with no vendor inside.
Bond a provider to choose the implementation.
import { getClient } from '@molecule/app-http'
import { loadAIModels, partitionByDeprecation } from '@molecule/app-ai-models'
const models = await loadAIModels(getClient())
const { current, deprecated } = partitionByDeprecation(models)Works with: @molecule/app-http
Reference
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.
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.
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-http
API
Interfaces
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
}
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 (`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`). `daysOfWeekUtc` restricts a window to
* certain UTC weekdays (`0` = Sunday … `6` = Saturday); omitted means every
* day. Providers that price peak by BUSINESS hours qualify them by day
* (DeepSeek's card says "Monday through Friday"), and ignoring that shows a
* 2× on a weekend that is not being charged. `excludedDatesUtc` lists
* `YYYY-MM-DD` dates on which no window applies — DeepSeek's peak excludes
* Chinese public holidays — matched against the day a window STARTS on.
*
* 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; daysOfWeekUtc?: number[] }[]
multiplier: number
excludedDatesUtc?: string[]
}
/**
* 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
* `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 `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
// … truncated — the full declaration is in node_modules/@molecule/app-ai-models/dist/types.d.ts
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 (<model>)" 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
}
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 = string
Functions
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).
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 | null
model— The model (orundefined).
Returns: The default native value, or null.
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 | null
model— The active model (orundefined).value— The persisted effort value (orundefined).
Returns: The resolved native value, or null when reasoning is fixed.
formatTokenCount(tokens)
Format a token count for display (e.g. 200000 -> "200K", 1000000 -> "1M").
function formatTokenCount(tokens: number): string
tokens— Token count.
Returns: Formatted string.
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 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 (seeloadAIModelCatalog).
Returns: The list of models available to the current session.
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'>): boolean
model— Model to check (only the two flags are read).
Returns: true when the model may be listed and chosen.
pickFreeTierModel(models)
Returns the free-tier model from a list, or undefined if none is marked.
Models that are not 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 | undefined
models— Loaded model catalog.
Returns: The single selectable model with freeTier: true, or undefined.
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): boolean
model— 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.
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 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.
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-http -
The 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.