Skip to content

Image Token Calculator — TypeScript source

Estimate the vision token cost of an image before sending it to an LLM - low/high/auto detail modes, the 512px tile math, the 2048/768 downscaling steps, and a full base + tiles + detail breakdown. Runs entirely in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Vision image token-cost calculator (OpenAI-style tile math).
// Pure + deterministic, throws on invalid input - the unit-test surface for
// the Image Token Calculator tool.

export type DetailLevel = 'low' | 'high' | 'auto';

export interface TokenBreakdown {
  /** Detail level actually applied. `auto` resolves to `low` or `high`. */
  detail: 'low' | 'high';
  /** Dimensions after the high-detail downscaling pipeline (identity for low). */
  scaledWidth: number;
  scaledHeight: number;
  /** 512 px tiles along each axis (both 1 in low detail). */
  tilesX: number;
  tilesY: number;
  /** Total 512 px tiles used (tilesX * tilesY). */
  tiles: number;
  /** Fixed base cost of the low-resolution view, in tokens. */
  base: number;
  /** Extra tokens for the high-resolution tile views (0 in low detail). */
  detailTokens: number;
  /** Total estimated tokens: base + detailTokens. */
  total: number;
}

/** Fixed token cost of the low-resolution image view. */
export const LOW_DETAIL_TOKENS = 85;
/** Token cost of one high-resolution 512 px tile. */
export const TILE_TOKENS = 170;
/** Images are first scaled to fit inside this square. */
export const MAX_SIDE = 2048;
/** Then the shortest side is capped at this length. */
export const MAX_SHORT_SIDE = 768;
/** Tile edge length in pixels. */
export const TILE_SIZE = 512;
/** Both dimensions at or under this → `auto` stays low detail. */
export const AUTO_LOW_MAX = 512;

/**
 * Scale (width, height) per the vision preprocessing pipeline:
 * 1. fit inside a MAX_SIDE × MAX_SIDE square (longest side capped), then
 * 2. cap the shortest side at MAX_SHORT_SIDE.
 * Aspect ratio is preserved; each step is skipped when already satisfied.
 */
export function preprocessImage(width: number, height: number): { width: number; height: number } {
  const longest = Math.max(width, height);
  if (longest > MAX_SIDE) {
    const scale = MAX_SIDE / longest;
    width = Math.max(1, Math.round(width * scale));
    height = Math.max(1, Math.round(height * scale));
  }
  const shortest = Math.min(width, height);
  if (shortest > MAX_SHORT_SIDE) {
    const scale = MAX_SHORT_SIDE / shortest;
    width = Math.max(1, Math.round(width * scale));
    height = Math.max(1, Math.round(height * scale));
  }
  return { width, height };
}

/**
 * Estimate the token cost of a width × height image at the given detail level.
 *
 * - `low`: fixed LOW_DETAIL_TOKENS, whatever the size.
 * - `high`: the image is downscaled by {@link preprocessImage}, tiled into
 *   TILE_SIZE squares, and each tile costs TILE_TOKENS on top of the base.
 * - `auto`: low when both dimensions are ≤ AUTO_LOW_MAX, otherwise high.
 *
 * Throws when dimensions are not finite positive integers or the detail level
 * is unknown.
 */
export function imageTokens(
  width: number,
  height: number,
  detail: DetailLevel = 'auto'
): TokenBreakdown {
  if (!Number.isFinite(width) || !Number.isFinite(height)) {
    throw new Error('Width and height must be finite numbers');
  }
  if (!Number.isInteger(width) || !Number.isInteger(height)) {
    throw new Error('Width and height must be whole pixels');
  }
  if (width <= 0 || height <= 0) {
    throw new Error('Width and height must be greater than zero');
  }

  let resolved: 'low' | 'high';
  switch (detail) {
    case 'low':
      resolved = 'low';
      break;
    case 'high':
      resolved = 'high';
      break;
    case 'auto':
      resolved = width <= AUTO_LOW_MAX && height <= AUTO_LOW_MAX ? 'low' : 'high';
      break;
    default:
      throw new Error(`Unknown detail level: ${String(detail)}`);
  }

  if (resolved === 'low') {
    return {
      detail: 'low',
      scaledWidth: width,
      scaledHeight: height,
      tilesX: 1,
      tilesY: 1,
      tiles: 1,
      base: LOW_DETAIL_TOKENS,
      detailTokens: 0,
      total: LOW_DETAIL_TOKENS,
    };
  }

  const scaled = preprocessImage(width, height);
  const tilesX = Math.ceil(scaled.width / TILE_SIZE);
  const tilesY = Math.ceil(scaled.height / TILE_SIZE);
  const tiles = tilesX * tilesY;
  const detailTokens = tiles * TILE_TOKENS;
  return {
    detail: 'high',
    scaledWidth: scaled.width,
    scaledHeight: scaled.height,
    tilesX,
    tilesY,
    tiles,
    base: LOW_DETAIL_TOKENS,
    detailTokens,
    total: LOW_DETAIL_TOKENS + detailTokens,
  };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →