From efcfe18c7c7bd354ce96a9a427ad9c6189339739 Mon Sep 17 00:00:00 2001 From: Universe Date: Thu, 5 Nov 2026 22:49:50 +0900 Subject: [PATCH] schema: preserve published image token rounding --- catalogue/README.md | 7 ++++- catalogue/schemas/models.schema.json | 19 ++++++++++++ scripts/check.py | 17 ++++++++++ tests/test_check.py | 46 ++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/catalogue/README.md b/catalogue/README.md index d9f9339..861dd7c 100644 --- a/catalogue/README.md +++ b/catalogue/README.md @@ -226,7 +226,12 @@ Missing fields mean unrecorded facts, never unsupported features or zero prices. records the number of decimal places in a published rounded price. A rounded price is evidence of that price, not a basis for inventing an exact token count. The checker reproduces any recorded token count from the formula and rejects - duplicate size/quality examples. + duplicate size/quality examples. Optional `quality_multipliers` supplies exact positive + decimal strings for every quality axis, together with `result_rounding: +"nearest_ties_up"`. Apply the multiplier after the base output-token ceiling, then + round to the nearest integer with half ties upward. This preserves the GPT Image + 2.6 calculator's second step; older calculators omit both fields and keep their + original formula. The multiplier map must match the quality-axis keys exactly. - **Vision tokenization.** `vision_tokenization` is a sourced set of the rules the vendor actually publishes: `patch_size_px`, `token_multiplier`, `detail_patch_budgets`, `detail_token_budgets`, `detail_maximum_edges`, diff --git a/catalogue/schemas/models.schema.json b/catalogue/schemas/models.schema.json index 779de8f..f439cc9 100644 --- a/catalogue/schemas/models.schema.json +++ b/catalogue/schemas/models.schema.json @@ -485,7 +485,26 @@ "type": "string", "format": "uri", "pattern": "^https://[^/]+(?:/|$)" + }, + "quality_multipliers": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$" + }, + "additionalProperties": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$", + "description": "Exact positive decimal multiplier; no exponent notation." + } + }, + "result_rounding": { + "enum": ["nearest_ties_up"] } + }, + "dependentRequired": { + "quality_multipliers": ["result_rounding"], + "result_rounding": ["quality_multipliers"] } }, "output_token_example": { diff --git a/scripts/check.py b/scripts/check.py index 7abed67..2baac56 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -175,6 +175,20 @@ def numeric_range_errors(value: dict[str, Any], where: str) -> Iterator[str]: def calculator_errors(calculator: dict[str, Any], where: str) -> Iterator[str]: axes = calculator.get("quality_axes", {}) axes = axes if isinstance(axes, dict) else {} + multipliers = calculator.get("quality_multipliers") + valid_multipliers: dict[str, Fraction] = {} + if isinstance(multipliers, dict): + if multipliers.keys() != axes.keys(): + yield f"{where}/quality_multipliers: qualities must match quality_axes exactly" + for quality, raw in multipliers.items(): + try: + number = Fraction(raw) if isinstance(raw, str) else Fraction(0) + except (ValueError, ZeroDivisionError): + number = Fraction(0) + if number <= 0: + yield f"{where}/quality_multipliers/{quality}: multiplier must be a positive decimal string" + else: + valid_multipliers[quality] = number seen = set() examples = calculator.get("examples", []) if not isinstance(examples, list): @@ -206,6 +220,9 @@ def calculator_errors(calculator: dict[str, Any], where: str) -> Iterator[str]: short_axis = round(Fraction(axis * min(width, height), max(width, height))) numerator = axis * short_axis * (offset + width * height) expected = -(-numerator // divisor) + if quality in valid_multipliers and calculator.get("result_rounding") == "nearest_ties_up": + scaled = expected * valid_multipliers[quality] + expected = (scaled.numerator * 2 + scaled.denominator) // (scaled.denominator * 2) if output_tokens != expected: yield f"{example_where}: output_tokens {output_tokens} does not match calculator {expected}" diff --git a/tests/test_check.py b/tests/test_check.py index 3d0c55c..f7359f2 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -441,6 +441,52 @@ def test_verified_output_token_example_must_match_calculator(): assert any("does not match calculator" in error for error in enriched(lambda card: card["output_token_calculator"]["examples"][0].update(output_tokens=197))) +def scaled_calculator(card, multiplier="4.2", tokens=823): + calc = card["output_token_calculator"] + calc["quality_multipliers"] = {"low": multiplier, "medium": "1", "high": "1"} + calc["result_rounding"] = "nearest_ties_up" + calc["examples"][0]["output_tokens"] = tokens + + +def test_calculator_applies_multiplier_after_base_ceiling(): + assert enriched(scaled_calculator) == [] + + +def test_calculator_multiplier_rounds_half_ties_up(): + # Base low square output is 196; 196 * 0.125 is 24.5, not ties-to-even 24. + assert enriched(lambda card: scaled_calculator(card, "0.125", 25)) == [] + assert any("does not match calculator" in error for error in enriched(lambda card: scaled_calculator(card, "0.125", 24))) + + +@pytest.mark.parametrize("multiplier", ["0", "-1", "nan", "1e3", 4.2]) +def test_calculator_multiplier_requires_positive_decimal_string(multiplier): + assert enriched(lambda card: scaled_calculator(card, multiplier)) + + +@pytest.mark.parametrize("missing", ["result_rounding", "quality_multipliers"]) +def test_calculator_multiplier_and_rounding_are_a_pair(missing): + def mutate(card): + scaled_calculator(card) + card["output_token_calculator"].pop(missing) + assert enriched(mutate) + + +@pytest.mark.parametrize("change", ["missing", "extra"]) +def test_calculator_multiplier_qualities_match_axes(change): + def mutate(card): + scaled_calculator(card) + multipliers = card["output_token_calculator"]["quality_multipliers"] + if change == "missing": + multipliers.pop("medium") + else: + multipliers["unknown"] = "1" + assert any("match quality_axes" in error for error in enriched(mutate)) + + +def test_calculator_rejects_unscaled_example_for_scaled_formula(): + assert any("does not match calculator" in error for error in enriched(lambda card: scaled_calculator(card, tokens=196))) + + def test_rounded_price_example_never_requires_inferred_tokens(): def mutate(card): example = card["output_token_calculator"]["examples"][0]