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

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.ts JSDoc, 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, or undefined when 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 (or undefined).

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 (or undefined).
  • value — The persisted effort value (or undefined).

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, 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 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 loadAIModelCatalog).

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-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.