Skip to content

Image Token Calculator — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! Image Token Calculator — estimate the vision token cost of an image using
//! OpenAI-style tile math.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source:   CosmoDev polyglot showcase port of the Image Token Calculator
//!           tool, ported from src/lib/imageTokenCalculator.ts (the canonical
//!           TypeScript implementation).
//! Live at:  https://dev.cosmolabs.org/tools/image-token-calculator
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; invalid input is a `Result::Err`, never a panic.
//!   - Functionally equivalent to the TS reference: same inputs -> same outputs.
//!   - Self-contained: std only (no crates.io dependencies).
//!
//! Rounding note: the TS reference uses Math.round (half up). Rust's `f64::
//! round` rounds half away from zero — identical on the positive values that
//! occur here — but `shrink` spells it as `floor(x + 0.5)` for exact parity.

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

/// Requested detail mode of an image (`Auto` mirrors the TS default).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailLevel {
    Low,
    High,
    Auto,
}

/// Why an estimate was rejected: non-positive dimensions or an unknown
/// detail level (the TS reference throws for both).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageTokenError {
    /// Width/height must be integers greater than zero.
    InvalidDimensions,
    /// The detail level string was not "low", "high", or "auto".
    UnknownDetail(String),
}

impl std::fmt::Display for ImageTokenError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ImageTokenError::InvalidDimensions => {
                write!(f, "Width and height must be greater than zero")
            }
            ImageTokenError::UnknownDetail(d) => write!(f, "Unknown detail level: {d}"),
        }
    }
}

/// Mirrors the `TokenBreakdown` interface in the TS lib.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenBreakdown {
    /// Detail level actually applied (`Auto` resolves to low or high).
    pub detail: &'static str,
    /// Dimensions after the high-detail downscaling pipeline (identity for low).
    pub scaled_width: u32,
    pub scaled_height: u32,
    /// 512 px tiles along each axis (both 1 in low detail).
    pub tiles_x: u32,
    pub tiles_y: u32,
    /// Total 512 px tiles used (`tiles_x * tiles_y`).
    pub tiles: u32,
    /// Fixed base cost of the low-resolution view, in tokens.
    pub base: u32,
    /// Extra tokens for the high-resolution tile views (0 in low detail).
    pub detail_tokens: u32,
    /// Total estimated tokens: `base + detail_tokens`.
    pub total: u32,
}

/// JS `Math.round` parity, floored at 1 px: half up, never zero.
fn shrink(side: u32, scale: f64) -> u32 {
    ((side as f64 * scale) + 0.5).floor().max(1.0) as u32
}

/// 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.
pub fn preprocess_image(width: u32, height: u32) -> (u32, u32) {
    let mut w = width;
    let mut h = height;
    let longest = w.max(h);
    if longest > MAX_SIDE {
        let scale = MAX_SIDE as f64 / longest as f64;
        w = shrink(w, scale);
        h = shrink(h, scale);
    }
    let shortest = w.min(h);
    if shortest > MAX_SHORT_SIDE {
        let scale = MAX_SHORT_SIDE as f64 / shortest as f64;
        w = shrink(w, scale);
        h = shrink(h, scale);
    }
    (w, h)
}

/// Estimate the token cost of a `width` × `height` image at the given detail
/// level.
///
/// - [`DetailLevel::Low`]: fixed [`LOW_DETAIL_TOKENS`], whatever the size.
/// - [`DetailLevel::High`]: the image is downscaled by [`preprocess_image`],
///   tiled into [`TILE_SIZE`] squares, and each tile costs [`TILE_TOKENS`] on
///   top of the base.
/// - [`DetailLevel::Auto`]: low when both dimensions are ≤ [`AUTO_LOW_MAX`],
///   otherwise high.
pub fn image_tokens(
    width: u32,
    height: u32,
    detail: DetailLevel,
) -> Result<TokenBreakdown, ImageTokenError> {
    if width == 0 || height == 0 {
        return Err(ImageTokenError::InvalidDimensions);
    }

    let resolved_high = match detail {
        DetailLevel::Low => false,
        DetailLevel::High => true,
        DetailLevel::Auto => width > AUTO_LOW_MAX || height > AUTO_LOW_MAX,
    };

    if !resolved_high {
        return Ok(TokenBreakdown {
            detail: "low",
            scaled_width: width,
            scaled_height: height,
            tiles_x: 1,
            tiles_y: 1,
            tiles: 1,
            base: LOW_DETAIL_TOKENS,
            detail_tokens: 0,
            total: LOW_DETAIL_TOKENS,
        });
    }

    let (scaled_width, scaled_height) = preprocess_image(width, height);
    let tiles_x = scaled_width.div_ceil(TILE_SIZE);
    let tiles_y = scaled_height.div_ceil(TILE_SIZE);
    let tiles = tiles_x * tiles_y;
    let detail_tokens = tiles * TILE_TOKENS;
    Ok(TokenBreakdown {
        detail: "high",
        scaled_width,
        scaled_height,
        tiles_x,
        tiles_y,
        tiles,
        base: LOW_DETAIL_TOKENS,
        detail_tokens,
        total: LOW_DETAIL_TOKENS + detail_tokens,
    })
}

/// Parse a detail-level string ("low" | "high" | "auto") — the bridge from
/// the TS string union to [`DetailLevel`].
pub fn parse_detail(detail: &str) -> Result<DetailLevel, ImageTokenError> {
    match detail {
        "low" => Ok(DetailLevel::Low),
        "high" => Ok(DetailLevel::High),
        "auto" => Ok(DetailLevel::Auto),
        other => Err(ImageTokenError::UnknownDetail(other.to_string())),
    }
}

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 →