← All @molecule/* packages · App templates

@molecule/app-ai-models

Core interface · ai-models · App (browser) · v1.3.0 · Apache-2.0

App-side client + UI metadata for the AI model catalog (loader + brand colors + token formatter)

npm install @molecule/app-ai-models

npm · Source on GitHub

How 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

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.ts JSDoc, 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-http

API

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`). `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.
   *
   * 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
  }
  /**
   * 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 (<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
}

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 = string

Functions

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 (or undefined).

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, or undefined when 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): string
  • tokens — 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): 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.

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.

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, flagged provider: '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 | null
  • model — The active model (or undefined).
  • value — The persisted effort value (or undefined).

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 | undefined
  • models — 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-http

  • The server side is @molecule/api-resource-ai-models — it serves the auth-gated GET /ai/models route this loader calls. Without that resource (or an equivalent route returning ListAIModelsResponse), loadAIModels fails. The route is session-gated: fetch with the app's authenticated HTTP client.

  • loadAIModels does NOT cache — call it once and keep the result (the framework hook does this for you). Use pickFreeTierModel / partitionByDeprecation instead of re-deriving tier/deprecation logic; disabled models must never surface in a picker.