Skip to content

fix: de-dup jp.anthropic.claude-sonnet-4-6 and fail CI on duplicate pricing keys - #27

Merged
blackflame007 merged 1 commit into
litellm_internal_stagingfrom
admin/nol-90-dedup-dup-keys
Jul 28, 2026
Merged

fix: de-dup jp.anthropic.claude-sonnet-4-6 and fail CI on duplicate pricing keys#27
blackflame007 merged 1 commit into
litellm_internal_stagingfrom
admin/nol-90-dedup-dup-keys

Conversation

@blackflame007

Copy link
Copy Markdown

Fixes NOL-90.

The duplicate

litellm/model_prices_and_context_window_backup.json carried "jp.anthropic.claude-sonnet-4-6" twice — lines 2353 and 2385 — inherited from upstream at the base commit. Pre-existing: not introduced by the sync (NOL-74) or by the gemini-omni de-dup (NOL-79).

json.load keeps the last occurrence, so the entry a reader finds first (2353) was dead text and the one at 2385 was what actually loaded.

They were not identical, and the wrong one was winning

No shared field disagreed — every price value was the same in both. The difference was in which fields existed.

line 2353 (was losing) line 2385 (was winning)
cache_creation_input_token_cost_above_1hr 6.6e-06 absent
prompt_cache_min_tokens 1024 absent
supports_adaptive_thinking true absent
supports_output_config true absent
supports_parallel_tool_use_config true absent
supports_minimal_reasoning_effort absent true
tool_use_system_prompt_tokens absent 346

Kept the first (2353), dropped the second. Two independent reasons it is the intended entry:

  1. It is byte-identical to the jp.anthropic.claude-sonnet-4-6 entry in model_prices_and_context_window.json, the canonical map.
  2. It matches the shape of every sibling region — anthropic., global., us., eu., au. claude-sonnet-4-6 all carry supports_adaptive_thinking, the 1hr tier, prompt_cache_min_tokens: 1024, supports_output_config and supports_parallel_tool_use_config, and none of them carry supports_minimal_reasoning_effort or tool_use_system_prompt_tokens.

So this duplicate was not inert. The copy that won had diverged from the canonical map and dropped the 1-hour prompt-cache write tier, meaning anything loading the backup map priced jp. 1hr cache writes at the 5-minute rate — a ~60% undercount on that spend. tests/test_litellm/test_bedrock_anthropic_1hr_cache_pricing.py already asserts 6.6e-06 for this model but reads the root map, which is why it stayed green.

Sibling file

model_prices_and_context_window.json was not affected — single, correct entry. A full scan at every nesting depth confirms neither file has any other duplicate key.

The durable fix

Nothing could have caught this. validate-model-prices-json ran only jq empty, and jq — like json.load — accepts a repeated key and silently keeps the last. That is how two of these have now reached us from upstream.

scripts/check_model_prices_duplicate_keys.py re-parses both maps with an object_pairs_hook that inspects the raw key/value pairs before they collapse into a dict, so a repeat is reported instead of swallowed, at any nesting level. Wired into the same job, which now also validates the backup map (previously unchecked at all).

Proof it fails before it passes

A guard only ever observed passing is not a guard:

input guard jq empty
pre-fix backup (this bug) exit 1 — flags jp.anthropic.claude-sonnet-4-6 2x at lines 2353, 2385 passes (blind)
pre-NOL-79 commit 45e798ce73 exit 1 — flags gemini/gemini-omni-flash-preview and the jp. one passes (blind)
nested duplicate field ("mode" twice) exit 1 — flags mode at lines 3, 4 passes (blind)
fixed tree, both maps exit 0 passes

It would have caught NOL-79 too.

tests/test_litellm/test_model_prices_no_duplicate_keys.py pins both halves — the maps stay duplicate-free, the backup and canonical jp. entries stay in sync with the 1hr tier present, and the guard itself is asserted to fail on deliberately duplicated fixtures so it cannot rot into a no-op. Collected by the existing misc unit job (tests/test_litellm/test_*.py).

Tests

  • new file: 10 passed. On the pre-fix tree, 3 of them go red (including the backup-vs-canonical drift assertion) — confirmed by restoring the old file and re-running.
  • test_bedrock_anthropic_1hr_cache_pricing.py, test_claude_sonnet_4_6_config.py, test_anthropic_sonnet_1hr_cache_pricing.py, test_bedrock_usgov_haiku_1hr_cache.py: 50 passed
  • tests/test_litellm/test_utils.py: 254 passed
  • tests/test_litellm/litellm_core_utils/llm_cost_calc/: 149 passed
  • make pre-commit: exit 0
  • zizmor on the edited workflow: no findings

Notes for review

  • No fal_ai / kling / openrouter-video entries touched.
  • No budget/spend fields touched.
  • test-server-root-path is expected red here — known, tracked in NOL-88.

…ricing keys

litellm/model_prices_and_context_window_backup.json carried "jp.anthropic.claude-
sonnet-4-6" twice, inherited from upstream at the base commit (pre-existing, not
introduced by the sync or by the gemini-omni de-dup). json.load keeps the LAST
occurrence, so the entry a reader finds first at line 2353 was dead text and the
one at line 2385 was what actually loaded.

The two copies were not identical. No shared field disagreed, but the winning
copy was an older shape missing five fields the losing copy had:

  cache_creation_input_token_cost_above_1hr (6.6e-06)
  prompt_cache_min_tokens (1024)
  supports_adaptive_thinking
  supports_output_config
  supports_parallel_tool_use_config

and carrying two the losing copy did not: supports_minimal_reasoning_effort and
tool_use_system_prompt_tokens (346).

So the duplicate was not inert. The losing first copy is byte-identical to the
entry in model_prices_and_context_window.json and matches the shape of every
sibling region (anthropic./global./us./eu./au. claude-sonnet-4-6), none of which
carry supports_minimal_reasoning_effort or tool_use_system_prompt_tokens. The
winning copy diverged from the canonical map and dropped the 1-hour prompt-cache
write tier, so anything loading the backup map priced jp. 1hr cache writes at the
5-minute rate and undercounted that spend by ~60%. Drop the stale second copy and
keep the first; the backup entry now matches the canonical map exactly.

model_prices_and_context_window.json was NOT affected -- it has a single, correct
entry. A full scan at every nesting depth confirms neither file has any other
duplicate key.

The durable half: nothing could have caught this. The validate-model-prices-json
job ran only `jq empty`, and jq -- like json.load -- accepts a repeated key and
silently keeps the last. That is how two of these have now reached us from
upstream. scripts/check_model_prices_duplicate_keys.py re-parses both maps with an
object_pairs_hook that inspects the raw key/value pairs before they collapse into
a dict, so a repeat is reported instead of swallowed, at any nesting level. It is
wired into the same job, which now also validates the backup map (previously
unchecked).

Verified the guard fails before it passes: it flags this duplicate at lines
2353/2385 on the pre-fix file, flags the gemini-omni duplicate on the pre-de-dup
commit, flags a nested duplicate field, and exits 0 on the fixed tree. jq empty
passes all three of those.

tests/test_litellm/test_model_prices_no_duplicate_keys.py pins both halves: the
maps stay duplicate-free, the backup and canonical jp. entries stay in sync with
the 1hr tier present, and the guard itself is asserted to FAIL on deliberately
duplicated fixtures so it cannot rot into a no-op.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9543c5f6e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +69 to +73
needle = f'"{key}":'
lines = tuple(
number
for number, line in enumerate(raw.splitlines(), start=1)
if needle in line

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 👍 / 👎.

@blackflame007
blackflame007 merged commit 9b19685 into litellm_internal_staging Jul 28, 2026
71 of 74 checks passed
@blackflame007
blackflame007 deleted the admin/nol-90-dedup-dup-keys branch July 28, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant