{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifxryd6of35vzknhqiz52hoopza4am7arixkxgg5hnm4irysggnh4",
    "uri": "at://did:plc:lk3jfj3zq4k4wxnk474axylu/app.bsky.feed.post/3mniwij4ybur2"
  },
  "path": "/t/gpt-image-2-output-pricing-calculator-online-and-in-your-code/1382699#post_1",
  "publishedAt": "2026-06-05T00:36:52.000Z",
  "site": "https://community.openai.com",
  "tags": [
    "hotnova.com",
    "gpt-image-2 Price Explorer",
    "(click for more details)"
  ],
  "textContent": "OpenAI made this opaque. I’m pleased to present:\n\nhotnova.com\n\n### gpt-image-2 Price Explorer\n\nInteractive image output token and cost calculator, much easier than OpenAI’s calculator (if you can find it, that likes to say “invalid” after your input).\n\nThat’s based the algorithm behind cost calculations for gpt-image-2. If you like JavaScript, steal away there.\n\n## Simple code\n\nBringing this forum around to “stuff for developers” instead of \"complaints from developers, here’s the money shot: Python code for you to compute token output from quality and size as inputs (non-validating)\n\n\n    IMAGE_MODEL_SPECS = {\n        \"gpt-image-2\": {\n            \"size_limits\": {\n                \"step_px\": 16,\n                \"min_pixels\": 655_360,\n                \"max_pixels\": 8_294_400,\n                \"max_dimension_px\": 3_840,\n                \"max_aspect_ratio\": 3.0,\n            },\n            \"quality_axis_factors\": {\"low\": 16, \"medium\": 48, \"high\": 96},\n            \"token_area_offset_pixels\": 2_000_000,\n            \"token_area_scale_denominator\": 4_000_000,\n            \"image_output_price_per_million_tokens\": 30.00,\n        },\n    }\n\n    def calculate_image_tokens(quality, width, height, model=\"gpt-image-2\"):\n        spec = IMAGE_MODEL_SPECS[model]\n        quality_axis_factor = spec[\"quality_axis_factors\"].get(quality)\n        if quality_axis_factor is None:\n            allowed = \"', '\".join(spec[\"quality_axis_factors\"])\n            raise ValueError(f\"quality must be one of '{allowed}'; got {quality!r}\")\n\n        long_edge = max(width, height)\n        short_edge = min(width, height)\n        short_axis_factor = (\n            2 * quality_axis_factor * short_edge + long_edge\n        ) // (2 * long_edge)\n\n        return (\n            quality_axis_factor\n            * short_axis_factor\n            * (spec[\"token_area_offset_pixels\"] + width * height)\n            + spec[\"token_area_scale_denominator\"]\n            - 1\n        ) // spec[\"token_area_scale_denominator\"]\n\n\n## Code with utilities included\n\n  * **Validate image size** : return error message about the input limit “rules” violated, or use no messages as “success”\n  * **Normalize** : re-shape a desired resolution into one that works\n  * **Recommend cheaper** : in step bands, there are cheaper resolutions with a more rectangular aspect ratio to be found - we find them and recommend the size string!\n  * **Tokens to dollars** : because math is hard; this consumes from a model’s truth (and possible future models)\n\n\n\nIncludes a demo so you can exercise these all:\n\n\n    GPT image size helpers demo\n    Enter sizes like 1200x1600. Blank input exits.\n\n    Size: *333x355*\n    333x355 is invalid for gpt-image-2:\n    - Width and height must both be divisible by 16.\n    - Pixel budget must be at least 655,360 pixels, inclusive.\n    We can fix that in code, though!\n    Normalized: 333x355 -> 784x848\n    -- costs for 784x848 --\n    low: 160 tokens, $0.004800\n      cheaper larger is 784x880: 151 tokens, $0.004530\n    medium: 1,408 tokens, $0.042240\n      cheaper larger is 784x880: 1,388 tokens, $0.041640\n    high: 5,693 tokens, $0.170790\n      cheaper larger is 784x864: 5,591 tokens, $0.167730\n\n\n### Python helper utilities\n\n\n    from math import ceil, floor\n\n\n    IMAGE_MODEL_SPECS = {\n        \"gpt-image-2\": {\n            \"size_limits\": {\n                \"step_px\": 16,\n                \"min_pixels\": 655_360,\n                \"max_pixels\": 8_294_400,\n                \"max_dimension_px\": 3_840,\n                \"max_aspect_ratio\": 3.0,\n            },\n            \"quality_axis_factors\": {\"low\": 16, \"medium\": 48, \"high\": 96},\n            \"token_area_offset_pixels\": 2_000_000,\n            \"token_area_scale_denominator\": 4_000_000,\n            \"image_output_price_per_million_tokens\": 30.00,\n        },\n    }\n\n    def calculate_image_tokens(quality, width, height, model=\"gpt-image-2\"):\n        spec = IMAGE_MODEL_SPECS[model]\n        quality_axis_factor = spec[\"quality_axis_factors\"].get(quality)\n        if quality_axis_factor is None:\n            allowed = \"', '\".join(spec[\"quality_axis_factors\"])\n            raise ValueError(f\"quality must be one of '{allowed}'; got {quality!r}\")\n\n        long_edge = max(width, height)\n        short_edge = min(width, height)\n        short_axis_factor = (\n            2 * quality_axis_factor * short_edge + long_edge\n        ) // (2 * long_edge)\n\n        return (\n            quality_axis_factor\n            * short_axis_factor\n            * (spec[\"token_area_offset_pixels\"] + width * height)\n            + spec[\"token_area_scale_denominator\"]\n            - 1\n        ) // spec[\"token_area_scale_denominator\"]\n\n\n    def validate_image_size(width, height, model=\"gpt-image-2\"):\n        limits = IMAGE_MODEL_SPECS[model][\"size_limits\"]\n        step = limits[\"step_px\"]\n        min_pixels = limits[\"min_pixels\"]\n        max_pixels = limits[\"max_pixels\"]\n        max_dimension = limits[\"max_dimension_px\"]\n        max_ratio = limits[\"max_aspect_ratio\"]\n\n        if type(width) is not int or type(height) is not int or width <= 0 or height <= 0:\n            return [\"Enter whole-number width and height values greater than 0.\"]\n\n        pixels = width * height\n        long_edge = max(width, height)\n        short_edge = min(width, height)\n        errors = []\n\n        if width % step != 0 or height % step != 0:\n            errors.append(f\"Width and height must both be divisible by {step}.\")\n        if pixels > max_pixels:\n            errors.append(\n                f\"Pixel budget must be no greater than {max_pixels:,} pixels, inclusive.\"\n            )\n        if pixels < min_pixels:\n            errors.append(\n                f\"Pixel budget must be at least {min_pixels:,} pixels, inclusive.\"\n            )\n        if long_edge > max_dimension:\n            errors.append(\n                f\"Maximum edge length must be less than or equal to {max_dimension:,}px.\"\n            )\n        if long_edge > max_ratio * short_edge:\n            errors.append(f\"Aspect ratio must be no greater than {max_ratio:g}:1.\")\n\n        return errors\n\n\n    def normalize(width, height, model=\"gpt-image-2\"):\n        limits = IMAGE_MODEL_SPECS[model][\"size_limits\"]\n        step = limits[\"step_px\"]\n        min_area = ceil(limits[\"min_pixels\"] / (step * step))\n        max_area = floor(limits[\"max_pixels\"] / (step * step))\n        max_side = floor(limits[\"max_dimension_px\"] / step)\n        max_ratio = float(limits[\"max_aspect_ratio\"])\n\n        width = max(1, int(width))\n        height = max(1, int(height))\n        ratio = max(1.0 / max_ratio, min(max_ratio, width / height))\n\n        if ratio >= 1.0:\n            max_area = min(max_area, max_side * max(1, floor(max_side / ratio)))\n        else:\n            max_area = min(max_area, max_side * max(1, floor(max_side * ratio)))\n\n        pixels = width * height\n        if pixels < min_area * step * step:\n            area = min_area\n        elif pixels > max_area * step * step:\n            area = max_area\n        else:\n            area = pixels / (step * step)\n\n        target_w = (area * ratio) ** 0.5\n        target_h = (area / ratio) ** 0.5\n        choices = []\n\n        for h in {floor(target_h) - 1, floor(target_h), ceil(target_h), ceil(target_h) + 1}:\n            if 1 <= h <= max_side:\n                lo = max(1, ceil(min_area / h), ceil(h / max_ratio))\n                hi = min(max_side, floor(max_area / h), floor(h * max_ratio))\n                if lo <= hi:\n                    w = min(hi, max(lo, round(ratio * h)))\n                    choices.append((w, h))\n\n        for w in {floor(target_w) - 1, floor(target_w), ceil(target_w), ceil(target_w) + 1}:\n            if 1 <= w <= max_side:\n                lo = max(1, ceil(min_area / w), ceil(w / max_ratio))\n                hi = min(max_side, floor(max_area / w), floor(w * max_ratio))\n                if lo <= hi:\n                    h = min(hi, max(lo, round(w / ratio)))\n                    choices.append((w, h))\n\n        best = min(\n            choices,\n            key=lambda size: (\n                ((size[0] - target_w) / target_w) ** 2\n                + ((size[1] - target_h) / target_h) ** 2,\n                abs(size[0] * size[1] - area),\n            ),\n        )\n        return best[0] * step, best[1] * step\n\n\n\n\n\n\n    def recommend_cheaper_larger_size(model, size, quality):\n        if isinstance(size, str):\n            width, height = map(int, size.lower().split()[0].split(\"x\"))\n        else:\n            width, height = size\n\n        if validate_image_size(width, height, model):\n            return None\n\n        spec = IMAGE_MODEL_SPECS[model]\n        q = spec[\"quality_axis_factors\"].get(quality)\n        if q is None:\n            allowed = \"', '\".join(spec[\"quality_axis_factors\"])\n            raise ValueError(f\"quality must be one of '{allowed}'; got {quality!r}\")\n\n        limits = spec[\"size_limits\"]\n        step = limits[\"step_px\"]\n        max_dimension = (limits[\"max_dimension_px\"] // step) * step\n        long_side = max(width, height)\n        short_side = min(width, height)\n        grow_width = width >= height\n        tokens = calculate_image_tokens(quality, width, height, model)\n\n        if long_side > short_side:\n            prev_long = long_side - step\n            prev_size = (prev_long, short_side) if grow_width else (short_side, prev_long)\n            if not validate_image_size(prev_size[0], prev_size[1], model):\n                if calculate_image_tokens(quality, prev_size[0], prev_size[1], model) > tokens:\n                    return None\n\n        max_long = min(\n            max_dimension,\n            (int(limits[\"max_aspect_ratio\"] * short_side) // step) * step,\n            (limits[\"max_pixels\"] // short_side // step) * step,\n        )\n        band = (2 * q * short_side + long_side) // (2 * long_side)\n\n        for next_band in range(band - 1, 0, -1):\n            threshold = (2 * q * short_side) // (2 * next_band + 1) + 1\n            candidate_long = max(long_side + step, ((threshold + step - 1) // step) * step)\n            if candidate_long > max_long:\n                return None\n\n            candidate_size = (\n                (candidate_long, short_side)\n                if grow_width\n                else (short_side, candidate_long)\n            )\n            if calculate_image_tokens(\n                quality,\n                candidate_size[0],\n                candidate_size[1],\n                model,\n            ) < tokens:\n                return f\"{candidate_size[0]}x{candidate_size[1]}\"\n\n        return None\n\n    def image_tokens_to_dollars(tokens, model=\"gpt-image-2\"):\n        price_per_million = IMAGE_MODEL_SPECS[model][\"image_output_price_per_million_tokens\"]\n        return tokens * price_per_million / 1_000_000\n\n    def demo():\n        model = \"gpt-image-2\"\n        qualities = [\"low\", \"medium\", \"high\"]\n\n        print(\"GPT image size helper demo\")\n        print(\"Enter image sizes like 1200x1600.\")\n        print(\"Press Enter at the size prompt to choose another quality.\")\n        print(\"Press Enter at the quality prompt, or enter an invalid quality choice, to exit.\")\n\n        while True:\n            print()\n            print(\"Quality choices:\")\n            for index, quality in enumerate(qualities, start=1):\n                print(f\"  {index}. {quality}\")\n\n            try:\n                quality_choice = input(\"Choose quality 1, 2, or 3: \").strip()\n            except EOFError:\n                print()\n                return\n\n            if not quality_choice:\n                return\n\n            if quality_choice not in {\"1\", \"2\", \"3\"}:\n                return\n\n            quality = qualities[int(quality_choice) - 1]\n\n            print()\n            print(f\"Using quality: {quality}\")\n\n            while True:\n                try:\n                    size_text = input(\"Size: \").strip()\n                except EOFError:\n                    print()\n                    return\n\n                if not size_text:\n                    break\n\n                try:\n                    clean_size_text = size_text.lower().replace(\" \", \"\")\n\n                    if \"x\" in clean_size_text:\n                        parts = clean_size_text.split(\"x\")\n                        if len(parts) != 2:\n                            raise ValueError\n                        width = int(parts[0])\n                        height = int(parts[1])\n                    else:\n                        width = int(clean_size_text)\n                        height_text = input(\"Height: \").strip()\n                        if not height_text:\n                            break\n                        height = int(height_text)\n\n                except ValueError:\n                    print(\"Enter a size like 1200x1600, or enter a whole-number width.\")\n                    continue\n\n                original_width = width\n                original_height = height\n\n                errors = validate_image_size(width, height, model)\n\n                if errors:\n                    print()\n                    print(f\"{original_width}x{original_height} is not a valid request size:\")\n                    for error in errors:\n                        print(f\"  - {error}\")\n\n                    width, height = normalize(width, height, model)\n                    print(f\"Normalized size: {original_width}x{original_height} -> {width}x{height}\")\n                else:\n                    print()\n                    print(f\"{width}x{height} is valid.\")\n                    print(\"No normalization needed.\")\n\n                tokens = calculate_image_tokens(quality, width, height, model)\n                cheaper_size = recommend_cheaper_larger_size(model, (width, height), quality)\n\n                print(f\"Output tokens: {tokens:,}\")\n                print(\"Image output cost: \"\n                      f\"${image_tokens_to_dollars(tokens, model):.6f}\")\n                if cheaper_size:\n                    cheaper_tokens = calculate_image_tokens(\n                        quality,\n                        *map(int, cheaper_size.split(\"x\")),\n                        model,\n                    )\n                    print(\n                        f\"Cheaper larger size: {cheaper_size} \"\n                        f\"({cheaper_tokens:,} output tokens)\"\n                    )\n                else:\n                    print(\"Cheaper larger size: none found\")\n\n                print()\n\n    def demo():\n        model = \"gpt-image-2\"\n        qualities = [\"low\", \"medium\", \"high\"]\n        print(\"GPT image size helpers demo\")\n        print(\"Enter sizes like 1200x1600. Blank input exits.\")\n\n        while True:\n            text = input(\"\\nSize: \").strip().lower().replace(\" \", \"\")\n            if not text:\n                return\n\n            try:\n                if \"x\" in text:\n                    width, height = map(int, text.split(\"x\"))\n                else:\n                    width = int(text)\n                    height = int(input(\"Height: \").strip())\n            except ValueError:\n                print(\"Enter a size like 1200x1600, or a whole-number width.\")\n                continue\n\n            errors = validate_image_size(width, height, model)\n            if errors:\n                old_size = f\"{width}x{height}\"\n                print(f\"{old_size} is invalid for {model}:\")\n                for error in errors:\n                    print(f\"- {error}\")\n                print(\"We can fix that in code, though!\")\n                width, height = normalize(width, height, model)\n                print(f\"Normalized: {old_size} -> {width}x{height}\")\n            else:\n                print(f\"{width}x{height} is valid.\")\n\n            print(f\"-- costs for {width}x{height} --\")\n            for quality in qualities:\n                tokens = calculate_image_tokens(quality, width, height, model)\n                cost = image_tokens_to_dollars(tokens, model)\n                cheaper_size = recommend_cheaper_larger_size(model, (width, height), quality)\n\n                print(f\"{quality}: {tokens:,} tokens, ${cost:.6f}\")\n\n                if cheaper_size:\n                    cheaper_width, cheaper_height = map(int, cheaper_size.split(\"x\"))\n                    cheaper_tokens = calculate_image_tokens(\n                        quality,\n                        cheaper_width,\n                        cheaper_height,\n                        model,\n                    )\n                    cheaper_cost = image_tokens_to_dollars(cheaper_tokens, model)\n                    print(\n                        f\"  cheaper larger is {cheaper_size}: \"\n                        f\"{cheaper_tokens:,} tokens, ${cheaper_cost:.6f}\"\n                    )\n                else:\n                    print(\"  cheaper larger: none\")\n\n    if __name__ == \"__main__\":\n        demo()\n\n\nPython with a verbose breakdown of what’s being computed, reverse-engineered. Input validation and errors messages included.\n\n\n    from typing import Final, Literal\n\n    ## Validation constants\n\n    # Size validation happens before token calculation. The accepted size space is a\n    # 16 px lattice, so a dimension that is only 1 px away from a valid value can\n    # still have no token price.\n    SIZE_GRANULARITY_PX: Final[int] = 16\n\n    # The pixel budget is inclusive at both ends. These limits are checked against\n    # width * height, independent of the aspect-ratio band used later.\n    MIN_PIXEL_BUDGET: Final[int] = 655_360\n    MAX_PIXEL_BUDGET: Final[int] = 8_294_400\n\n    # The maximum edge rule is separate from total pixel budget. An image can be\n    # under the pixel budget and still be invalid if either side is too long.\n    MAX_EDGE_LENGTH_PX: Final[int] = 3_840\n\n    # Aspect ratio is checked as long_edge / short_edge <= 3. The exact 3:1 case is\n    # valid; only ratios greater than 3:1 are rejected.\n    MAX_ASPECT_RATIO: Final[int] = 3\n\n\n    def validate_image_size(width: int, height: int) -> list[str]:\n        \"\"\"\n        Return validation messages for dimensions that cannot receive a token price.\n\n        An empty list means the dimensions are eligible for token calculation.\n        \"\"\"\n        if type(width) is not int or type(height) is not int or width <= 0 or height <= 0:\n            return [\"Enter whole-number width and height values greater than 0.\"]\n\n        errors: list[str] = []\n\n        if width % SIZE_GRANULARITY_PX != 0 or height % SIZE_GRANULARITY_PX != 0:\n            errors.append(\"Width and height must both be divisible by 16.\")\n\n        pixel_budget = width * height\n\n        if pixel_budget > MAX_PIXEL_BUDGET:\n            errors.append(\n                f\"Pixel budget must be no greater than \"\n                f\"{MAX_PIXEL_BUDGET:,} pixels, inclusive.\"\n            )\n\n        if pixel_budget < MIN_PIXEL_BUDGET:\n            errors.append(\n                f\"Pixel budget must be at least \"\n                f\"{MIN_PIXEL_BUDGET:,} pixels, inclusive.\"\n            )\n\n        long_edge = max(width, height)\n        short_edge = min(width, height)\n\n        if long_edge > MAX_EDGE_LENGTH_PX:\n            errors.append(\n                f\"Maximum edge length must be less than or equal to \"\n                f\"{MAX_EDGE_LENGTH_PX:,}px.\"\n            )\n\n        if long_edge > MAX_ASPECT_RATIO * short_edge:\n            errors.append(\"Aspect ratio must be no greater than 3:1.\")\n\n        return errors\n\n    ## Algorithm constants\n\n    Quality = Literal[\"low\", \"medium\", \"high\"]\n\n    # The quality setting enters the calculation as an integer axis factor.\n    # For square images, the pre-area token grid is this value squared:\n    # low=16*16, medium=48*48, high=96*96.\n    QUALITY_AXIS_FACTORS: Final[dict[Quality, int]] = {\n        \"low\": 16,\n        \"medium\": 48,\n        \"high\": 96,\n    }\n\n    # The final area multiplier is:\n    #\n    #     (AREA_OFFSET_PIXELS + width * height) / AREA_SCALE_DENOMINATOR\n    #\n    # The positive offset means the token count is not proportional to image area\n    # alone. At the minimum valid pixel budget, the offset is larger than the image\n    # itself, so smaller valid images still carry a substantial fixed area term.\n    AREA_OFFSET_PIXELS: Final[int] = 2_000_000\n    AREA_SCALE_DENOMINATOR: Final[int] = 4_000_000\n\n\n    def _round_half_up_ratio(numerator: int, denominator: int) -> int:\n        \"\"\"\n        Round numerator / denominator to the nearest integer, with exact halves up.\n\n        The aspect component falls into integer bands rather than staying continuous.\n        Using integer arithmetic keeps boundary cases deterministic and avoids\n        moving a half-step threshold because of binary floating-point representation.\n        \"\"\"\n        return (2 * numerator + denominator) // (2 * denominator)\n\n\n    def _ceil_div(numerator: int, denominator: int) -> int:\n        \"\"\"\n        Return ceil(numerator / denominator) for positive integers.\n\n        Token totals are whole numbers. Any non-zero fractional remainder in the\n        scaled calculation increases the reported total to the next integer token.\n        \"\"\"\n        return (numerator + denominator - 1) // denominator\n\n\n    def calculate_image_tokens(quality: Quality, width: int, height: int) -> int:\n        \"\"\"\n        Return the output token count for an image setting.\n\n        The calculation is symmetric in width and height. Rotating an image does not\n        change the result because only the long edge, short edge, and total area are\n        used.\n\n        Raises:\n            ValueError: If quality is not one of \"low\", \"medium\", or \"high\", or if\n                the dimensions violate the size rules.\n        \"\"\"\n        if quality not in QUALITY_AXIS_FACTORS:\n            allowed = \", \".join(repr(value) for value in QUALITY_AXIS_FACTORS)\n            raise ValueError(f\"quality must be one of {allowed}; got {quality!r}\")\n\n        errors = validate_image_size(width, height)\n        if errors:\n            raise ValueError(\"Invalid image size:\\n- \" + \"\\n- \".join(errors))\n\n        long_edge = max(width, height)\n        short_edge = min(width, height)\n\n        quality_axis_factor = QUALITY_AXIS_FACTORS[quality]\n\n        # The longer side keeps the full quality axis factor. The shorter side is\n        # reduced according to short_edge / long_edge and then rounded into an\n        # integer band.\n        #\n        # This rounded band is the source of the visible downward jumps in resolution\n        # tables: as one edge grows, area rises gradually, but the aspect band can\n        # drop by 1 at a threshold. That one-band drop can outweigh the added pixels.\n        short_axis_factor = _round_half_up_ratio(\n            quality_axis_factor * short_edge,\n            long_edge,\n        )\n\n        # The calculation behaves like a rectangular token grid:\n        #\n        #     full quality axis factor * aspect-adjusted short-axis factor\n        #\n        # For square images, short_axis_factor equals quality_axis_factor, so the\n        # grid is exactly quality_axis_factor squared.\n        token_grid = quality_axis_factor * short_axis_factor\n\n        pixel_budget = width * height\n\n        # Area then scales the token grid with a fixed positive offset. Keeping this\n        # as integer arithmetic gives an exact final ceiling instead of depending on\n        # floating-point rounding near token boundaries.\n        scaled_token_numerator = token_grid * (AREA_OFFSET_PIXELS + pixel_budget)\n\n        return _ceil_div(scaled_token_numerator, AREA_SCALE_DENOMINATOR)\n\n\n    def main() -> None:\n        quality: Quality = \"medium\"\n        width = 1200\n        height = 1472\n        try:\n            tokens = calculate_image_tokens(quality, width, height)\n            print(f\"quality={quality}, width={width}, height={height} -> {tokens} tokens\")\n        except ValueError as v:\n            print(f\"-- ValueError --\\n{v}\")\n\n\n    if __name__ == \"__main__\":\n        main()\n\n\n### Documentation\n\nIf OpenAI were to explain “how to calculate costs” in natural language, here’s what it might look like.\n\nSummary (click for more details)\n\n* * *\n\n### Result\n\nLook forward to your favorite image creation applications telling you not just your input cost in language tokens (also the “vision” image input component), but also your output and final cost - before you push a “send” button!",
  "title": "Gpt-image-2 output pricing calculator - online.. and in your code!"
}