Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/test-model-map.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ jobs:
- name: Validate model_prices_and_context_window.json
run: |
jq empty model_prices_and_context_window.json
jq empty litellm/model_prices_and_context_window_backup.json

# jq empty (and json.load) accept a repeated key and silently keep the LAST
# occurrence, so a duplicated model entry parses clean while the entry a
# reader finds first is dead text. Upstream has handed us two of these
# (gemini-omni-flash-preview, jp.anthropic.claude-sonnet-4-6); this catches
# the next one at the sync PR instead of in production pricing.
- name: Check for duplicate keys in the price maps
run: |
python3 scripts/check_model_prices_duplicate_keys.py
29 changes: 0 additions & 29 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -2382,35 +2382,6 @@
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 1024
},
"jp.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
"cache_read_input_token_cost": 3.3e-07,
"input_cost_per_token": 3.3e-06,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"output_cost_per_token": 1.65e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_assistant_prefill": true,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_max_reasoning_effort": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-sonnet-4-20250514-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_read_input_token_cost": 3e-07,
Expand Down
135 changes: 135 additions & 0 deletions scripts/check_model_prices_duplicate_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Gating guard: the model-price maps must not contain duplicate JSON keys.

``json.load`` silently keeps the LAST occurrence of a repeated key, so a duplicated
model entry parses cleanly, resolves deterministically, and still ships the wrong
pricing: whichever entry a human reads in the file may not be the one that takes
effect. Two of these have already been inherited from upstream
(``gemini-omni-flash-preview``, ``jp.anthropic.claude-sonnet-4-6``), and each cost
real time to diagnose because every ordinary validator -- ``jq empty`` included --
accepts them.

This check re-parses each price map with an ``object_pairs_hook`` that inspects the
raw key/value pairs BEFORE they collapse into a dict, so a repeat is caught instead
of swallowed. It walks every nesting level, not just the top-level model map.

Usage:
python scripts/check_model_prices_duplicate_keys.py [FILE ...]

With no arguments it checks the two tracked price maps. Exits 1 on any duplicate.
Stdlib only.
"""

from __future__ import annotations

import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Any, NamedTuple

REPO_ROOT = Path(__file__).resolve().parent.parent

DEFAULT_TARGETS: tuple[str, ...] = (
"model_prices_and_context_window.json",
"litellm/model_prices_and_context_window_backup.json",
)


class DuplicateKey(NamedTuple):
"""A key that appeared more than once inside a single JSON object."""

key: str
count: int
lines: tuple[int, ...]


def find_duplicate_keys(path: Path) -> list[DuplicateKey]:
"""Return every key repeated within one JSON object in ``path``.

Duplicates are read off the pair list handed to ``object_pairs_hook``, which
still holds both occurrences; the dict it returns does not.
"""
raw = path.read_text()
repeats: Counter[str] = Counter()

def hook(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
counts = Counter(key for key, _ in pairs)
for key, count in counts.items():
if count > 1:
# Keep the highest count seen for this key across all objects.
repeats[key] = max(repeats[key], count)
return dict(pairs)

json.loads(raw, object_pairs_hook=hook)

duplicates: list[DuplicateKey] = []
for key, count in sorted(repeats.items()):
needle = f'"{key}":'
lines = tuple(
number
for number, line in enumerate(raw.splitlines(), start=1)
if needle in line
Comment on lines +69 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track lines within the offending JSON object

When a commonly repeated nested field such as mode is duplicated in one model, this global text scan reports every "mode": line in the entire price map rather than the two occurrences in the offending object. On the current canonical map that produces roughly 20 KB of unrelated line numbers and does not identify which model contains the duplicate, making the new CI failure difficult to act on; retain the object path or source positions while parsing instead of rescanning globally by key name.

Useful? React with 👍 / 👎.

)
duplicates.append(DuplicateKey(key=key, count=count, lines=lines))
return duplicates


def check(path: Path) -> bool:
"""Check one file. True when clean, False when duplicates (or errors) found."""
try:
duplicates = find_duplicate_keys(path)
except FileNotFoundError:
print(f"FAIL {path}: file not found", file=sys.stderr)
return False
except json.JSONDecodeError as exc:
print(f"FAIL {path}: invalid JSON - {exc}", file=sys.stderr)
return False

if not duplicates:
print(f"OK {path}: no duplicate keys")
return True

print(f"FAIL {path}: {len(duplicates)} duplicate key(s)", file=sys.stderr)
for dup in duplicates:
where = ", ".join(str(line) for line in dup.lines) or "unknown"
print(
f" {dup.key!r} appears {dup.count}x in the same object "
f"(lines: {where})",
file=sys.stderr,
)
print(
"\n json.load keeps the LAST occurrence, so the earlier entry is dead text\n"
" that still reads as authoritative. Delete whichever entry is stale and\n"
" keep the one that matches the other price map and its sibling models.",
file=sys.stderr,
)
return False


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description=(
"Fail if a model-price JSON file repeats a key inside an object."
),
)
parser.add_argument(
"files",
nargs="*",
help=f"JSON files to check (default: {', '.join(DEFAULT_TARGETS)})",
)
args = parser.parse_args(argv)

paths = (
[Path(f) for f in args.files]
if args.files
else [REPO_ROOT / target for target in DEFAULT_TARGETS]
)

results = [check(path) for path in paths]
return 0 if all(results) else 1


if __name__ == "__main__":
raise SystemExit(main())
148 changes: 148 additions & 0 deletions tests/test_litellm/test_model_prices_no_duplicate_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Regression + self-test for the duplicate-key guard on the model-price maps.

Upstream has twice handed us a price map with the same model key written twice
(`gemini-omni-flash-preview` in NOL-79, `jp.anthropic.claude-sonnet-4-6` in
NOL-90). `json.load` keeps the LAST occurrence, so both parsed fine and both
shipped an entry that did not match the one a reader would find first in the
file. `jq empty` -- the only validation the price maps had -- accepts duplicates,
so nothing caught either case.

Two things are pinned here:

1. the tracked price maps carry no duplicate keys, and
2. the guard actually FAILS on a duplicate. A guard that has only ever been
observed passing is not a guard, so the negative cases are asserted too.
"""

import importlib.util
import json
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
_MODULE_PATH = REPO_ROOT / "scripts" / "check_model_prices_duplicate_keys.py"
_spec = importlib.util.spec_from_file_location(
"check_model_prices_duplicate_keys", _MODULE_PATH
)
guard = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(guard)


PRICE_MAPS = [
"model_prices_and_context_window.json",
"litellm/model_prices_and_context_window_backup.json",
]


@pytest.mark.parametrize("relative_path", PRICE_MAPS)
def test_price_map_has_no_duplicate_keys(relative_path):
"""The shipped price maps must not repeat a key inside any object."""
path = REPO_ROOT / relative_path
duplicates = guard.find_duplicate_keys(path)
assert duplicates == [], (
f"{relative_path} repeats {[d.key for d in duplicates]}. "
"json.load keeps the last occurrence, so the earlier entry is dead text "
"that still reads as authoritative - delete the stale one."
)


def test_jp_anthropic_claude_sonnet_4_6_matches_across_price_maps():
"""NOL-90: the backup's winning entry had drifted from the canonical map.

The duplicate's second (winning) copy was an older shape missing the 1-hour
cache-write tier, so the backup map silently priced jp. 1hr cache writes at
the 5-minute rate while the root map had it right.
"""
model = "jp.anthropic.claude-sonnet-4-6"
with open(REPO_ROOT / "model_prices_and_context_window.json") as f:
root = json.load(f)
with open(REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json") as f:
backup = json.load(f)

assert backup[model] == root[model], (
f"{model} differs between the price map and its backup copy"
)
assert backup[model]["cache_creation_input_token_cost_above_1hr"] == 6.6e-06


def test_guard_detects_duplicate_top_level_key(tmp_path):
"""The exact shape NOL-90 fixed: one model key written twice."""
path = tmp_path / "dup.json"
path.write_text(
'{\n'
' "a-model": {"input_cost_per_token": 1e-06},\n'
' "jp.anthropic.claude-sonnet-4-6": {"input_cost_per_token": 3.3e-06},\n'
' "jp.anthropic.claude-sonnet-4-6": {"input_cost_per_token": 9.9e-06}\n'
'}\n'
)

# json.load is blind to this - that is the whole problem.
assert json.loads(path.read_text())["jp.anthropic.claude-sonnet-4-6"] == {
"input_cost_per_token": 9.9e-06
}

duplicates = guard.find_duplicate_keys(path)
assert [d.key for d in duplicates] == ["jp.anthropic.claude-sonnet-4-6"]
assert duplicates[0].count == 2
assert duplicates[0].lines == (3, 4)
assert guard.check(path) is False
assert guard.main([str(path)]) == 1


def test_guard_detects_duplicate_nested_key(tmp_path):
"""Duplicates below the top level count too (e.g. a repeated pricing field)."""
path = tmp_path / "nested.json"
path.write_text(
'{\n'
' "a-model": {\n'
' "mode": "chat",\n'
' "mode": "video_generation"\n'
' }\n'
'}\n'
)

duplicates = guard.find_duplicate_keys(path)
assert [d.key for d in duplicates] == ["mode"]
assert guard.main([str(path)]) == 1


def test_guard_passes_on_clean_file(tmp_path):
path = tmp_path / "clean.json"
path.write_text(
'{\n'
' "a-model": {"mode": "chat"},\n'
' "b-model": {"mode": "video_generation"}\n'
'}\n'
)

assert guard.find_duplicate_keys(path) == []
assert guard.check(path) is True
assert guard.main([str(path)]) == 0


def test_guard_reports_every_offending_file(tmp_path):
"""A clean file must not mask a dirty one when several are passed."""
clean = tmp_path / "clean.json"
clean.write_text('{"a": 1}\n')
dirty = tmp_path / "dirty.json"
dirty.write_text('{"a": 1, "a": 2}\n')

assert guard.main([str(clean), str(dirty)]) == 1
assert guard.main([str(dirty), str(clean)]) == 1


def test_guard_fails_on_invalid_json(tmp_path):
path = tmp_path / "broken.json"
path.write_text("{not json")
assert guard.main([str(path)]) == 1


def test_guard_fails_on_missing_file(tmp_path):
assert guard.main([str(tmp_path / "nope.json")]) == 1


def test_guard_default_targets_are_the_tracked_price_maps():
"""Bare `python scripts/check_model_prices_duplicate_keys.py` must cover both."""
assert set(guard.DEFAULT_TARGETS) == set(PRICE_MAPS)
assert guard.main([]) == 0
Loading