Skip to content

fix(model): one same-model rule for baseline, self-judge and repo compare - #273

Open
napetrov wants to merge 12 commits into
intel:mainfrom
napetrov:fix/one-model-match-rule
Open

napetrov wants to merge 12 commits into
intel:mainfrom
napetrov:fix/one-model-match-rule

Conversation

@napetrov

Copy link
Copy Markdown
Contributor

Summary

Item P2-3 of #229 (Refs #229). Two rules decided "same model" and they disagreed:

baseline compare, self-judge warning   utils.normalize_model_ref(provider, model)
  identity = provider + lowercased id; only a bare Bedrock alias maps to its profile

repo compare                            repos/report.model_matches(requested, resolved)
  identity = id after region, revision and @date rules; provider prefix ignored, case kept

After the change there is one function, utils.same_model(a, b), and all three call sites use it:

 utils.py
-def normalize_model_ref(provider, model) -> str:   # provider + lowercased id
+def same_model(a: str, b: str) -> bool:
+    # route, region profile, -vN:M revision and case are not identity
+    # dated id == its undated alias; two dated ids equal only on the same date
 
 commands/baseline.py   _model_mismatches: normalize_model_ref(*b) != normalize_model_ref(*c)
+                       _same_recorded(b, c)  -> same_model per pair; panels: equal size, judges paired one to one
 commands/evaluate.py   _warn_judge_independence: normalize_model_ref(...) == ...
+                       same_model(answer_model, judge_model)
 repos/report.py        model_matches (and its _strip_region, _normalize_model_id helpers) removed
+                       model_mismatch uses same_model
 repos/compare.py       _models_equivalent: model_matches -> same_model

Rules chosen for each case (the table is tests/test_same_model.py):

Case Old baseline/self-judge Old repo compare Now
claude-sonnet-4-5-20250929 vs claude-sonnet-4-5 differ same same (alias runs some snapshot of it)
claude-3-5-sonnet-20240620 vs …-20241022 differ differ differ (two snapshots)
us.anthropic.x vs eu.anthropic.x (bare) differ same same (regional profile of one model)
anthropic/x vs amazon-bedrock/x (same model, two routes) differ differ same
openrouter/anthropic/x vs anthropic/x differ same same
m-v1:0 vs m-v3:0 differ same same
Claude-Sonnet-4-5 vs claude-sonnet-4-5 same differ same
haiku vs claude-haiku-3, deepseek-v2 vs deepseek-v3 differ differ differ

Decision points (for review):

  • Provider is not identity. The same weights on the direct API and on Bedrock are one model for the self-judge warning, which is about shared bias. The old baseline check counted a route change as a judge change; it now does not. The existing test that pinned ("anthropic", "us.anthropic.claude-sonnet-5-5") as a different model was changed: it now warns.
  • Dated vs undated is "same". The undated id is an alias for some snapshot; the recorded strings cannot show snapshot drift, so the rule accepts the alias and refuses two different dated snapshots.
  • The BEDROCK_MODEL_IDS alias table is now covered by the rule without a table lookup; test_a_bedrock_alias_and_its_profile_are_one_model runs over that table.

Evidence

Step 1, the case table run against both functions on intel/main (e2147b17; 27 rows, case_table.py in scratch): the two functions disagree on 13 rows, and both are wrong on 6 of them. Rows 11, 12, 14, 15, 17 and 18 pass a route on both sides (amazon-bedrock/us.anthropic… vs amazon-bedrock/eu.anthropic…); the old model_matches strips a region only from the start of the string, so it says differ there. With the route on one side only (the bare-name row in the Summary table) it says same. Excerpt:

row expected normalize model_matches  why
  1     True      True         False  case only  <-- functions disagree  (wrong: model_matches)
  3     True     False          True  dated vs undated alias  <-- functions disagree  (wrong: normalize)
  5     True     False          True  vertex @date form  <-- functions disagree  (wrong: normalize)
 11     True     False         False  regional profiles  (wrong: normalize, model_matches)
 19     True     False          True  route prefix vs bare  <-- functions disagree  (wrong: normalize)
 23     True     False          True  same route on both sides, dated  <-- functions disagree  (wrong: normalize)
rows=27 disagree=13

Regression tests before the fix (pytest tests/test_baseline_compare.py tests/test_model_defaults.py), the two that encode the new rule:

FAILED tests/test_baseline_compare.py::test_a_snapshot_its_alias_and_another_route_are_one_judge_model
E   AssertionError: assert (['judge model: baseline anthropic/claude-sonnet-4-5, current anthropic/claude-sonnet-4-5-20250929'], []) == ([], [])
FAILED tests/test_model_defaults.py::test_independence_warning_sees_a_bedrock_alias_and_its_profile_as_one_model
E   AssertionError: assert False
E    +  where False = _warn_judge_independence('anthropic', 'us.anthropic.claude-sonnet-5-5', 'anthropic', 'claude-sonnet-5-5')
2 failed, 64 passed

tests/test_same_model.py fails to collect on the old code (ImportError: cannot import name 'same_model').

After the fix:

tests/test_same_model.py + test_baseline_compare + test_model_defaults + test_repo_run + test_repo_compare: 324 passed
full suite (python -m pytest -q): 9736 passed, 26 skipped
ruff check .: All checks passed!   mypy: Success: no issues found in 271 source files
deptry .: Success! No dependency issues found.

Numbers: no score formula changes. Which runs count as the same model does change: repo compare arm pairing (_models_equivalent), the self-judge warning, and baseline compare judge/answer model mismatch lines and --strict refusals. Cases that used to refuse or warn about a snapshot, region or route change now pass, and the independence warning fires for the same model on another provider.

Merge Danger

Door: two-way. Reverting restores the two old rules; no data is rewritten.

Blast Radius: comparison pairing and warnings in repo compare, baseline compare and the self-judge check. No score or stored artifact changes.

Ramifications:

  • baseline compare --strict refuses fewer pairs (route, region, alias changes no longer count as a model change). Any pipeline that relied on a refusal to catch a judge route change will not see it.
  • The self-judge warning fires in more cases (same model on another provider).
  • Overlap: feat(llm): claude-cli provider: run the Q&A track on a logged-in Claude Code CLI #255 touches dokimi/utils.py (get_llm hunk) and fix(cli): run, compare, arms, tasks, explore and repo exit with a message on bad input or output #238 touches dokimi/repos/report.py (hunks above model_matches). Neither hunk is edited here; the textual overlap is the removal of model_matches and its helpers in report.py and the new function in utils.py. Whichever lands second needs a rebase.
  • Changelog: changelog.d/20261028_130000_napetrov_one-model-match-rule.md (Fixed). Docs: docs/benchmarking-and-comparison.md describes the new rule and the panel pairing.
  • Known limits, unchanged from before: an OpenRouter or Hugging Face org is dropped with the route, so org-a/x and org-b/x are one model; claude-3-5-sonnet-v2@20241022 (Vertex) and claude-3-5-sonnet-20241022 (direct) are not matched, because the -v2 belongs to the id. A set of two snapshots against one alias is accepted by the set comparison in repo compare (its docstring says set sizes may differ), as before.
  • An unset model on both sides ("") is no longer "the same"; a malformed cell that records no resolved model now gets a mismatch line where the old code printed none.

Refs #229

🤖 Generated with Claude Code

…pare

utils.normalize_model_ref (baseline check, self-judge warning) and repos/report.model_matches (repo compare) disagreed on dated ids, regional profiles, provider routes, -vN:M revisions and case. Replace both with utils.same_model: route, region profile, revision and case are not identity; a dated id matches its undated alias; two dated ids match only on the same date. Refs intel#229 (P2-3).

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@codecov-commenter

codecov-commenter commented Oct 29, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.49%. Comparing base (f0f95a2) to head (7140e6d).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #273   +/-   ##
=======================================
  Coverage   93.48%   93.49%           
=======================================
  Files         272      272           
  Lines       35363    35388   +25     
  Branches     6001     6009    +8     
=======================================
+ Hits        33060    33085   +25     
  Misses       1489     1489           
  Partials      814      814           
Files with missing lines Coverage Δ
dokimi/dokimi/commands/baseline.py 93.61% <100.00%> (+0.56%) ⬆️
dokimi/dokimi/commands/evaluate.py 89.87% <100.00%> (ø)
dokimi/dokimi/orchestrator/pipeline.py 83.89% <100.00%> (+0.06%) ⬆️
dokimi/dokimi/repos/compare.py 99.48% <100.00%> (+<0.01%) ⬆️
dokimi/dokimi/repos/report.py 97.27% <100.00%> (-0.19%) ⬇️
dokimi/dokimi/utils.py 98.43% <100.00%> (+0.28%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vlad-perevezentsev vlad-perevezentsev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at e159659. One same_model helper for the self-judge warning, baseline compare and repo compare is the right direction, and the panel pairing is correct. But dokimi run still stores the self-judge flag by the old rule. The looser rule also lets baseline compare --strict pass two cases it refused before: an undated alias and a different org. Details are in the line comments.

What I ran, at e159659:

  • make ci-local --offline: 0 failed, 17 passed, 13 skipped (network/docker jobs).
  • same_model on edge cases: alias vs dated snapshot (True), meta-llama/X vs myorg/X (True), openai.gpt-oss-120b-1:0 vs gpt-oss-120b (False), gpt-4o-2024-05-13 vs gpt-4o-2024-08-06 (False).

Checked, no issues

  • Panel matching: _same_recorded pairs the judges one to one by augmenting paths, so a non-transitive match (alias vs two snapshots) cannot pair two judges with one. The tests cover unequal sizes and the two-snapshot case.
  • repo compare / repo report: the old model_matches cases in test_a_provider_prefix_is_not_another_model still pass under same_model, and the cjm-model-answered outcome test is only renamed.
  • Bedrock aliases: the BEDROCK_MODEL_IDS entries map a bare name to us.anthropic.<same name>, so dropping the lookup loses no match.
  • Docs and changelog: the baseline compare section and the fragment describe the new rule. No scores change.

Comment thread dokimi/dokimi/commands/evaluate.py
Comment thread dokimi/dokimi/utils.py
Comment on lines +144 to +155
some snapshot of it), or the same id behind another route or region profile. Two dated
ids match only on the same date, and a bare family name (``haiku``) is not a full id.
The provider is not part of the identity: the same weights on Bedrock and on the direct
API are still one model for the self-judge warning and for a baseline's model check.
"""
prov, name = (provider or "").strip().lower(), (model or "").strip()
if prov == "amazon-bedrock":
name = BEDROCK_MODEL_IDS.get(name, name)
return f"{prov}/{name.lower()}"
(base_a, date_a), (base_b, date_b) = _model_core(a), _model_core(b)
if not (base_a and base_b) or (date_a and date_b and date_a != date_b):
return False
# a lowercase word before the first dot is taken as a vendor prefix (``anthropic.``, ``amazon.``) and
# dropped; a dot after a digit or a dash (``gpt-4.1``, ``qwen2.5``) is part of the name
return re.sub(r"^[a-z]+\.", "", base_a) == re.sub(r"^[a-z]+\.", "", base_b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

baseline compare --strict now passes when one side records only an undated alias, so a judge or answer model change behind the alias is no longer caught.

same_model('anthropic/claude-sonnet-4-5-20240620', 'claude-sonnet-4-5') is True. The alias does not record which snapshot answered, so this is a role that was not measured, and --strict now reads it as a match. Before this PR it was a mismatch. For the self-judge warning a loose rule is the safe side (it warns more), but for baseline compare it is the unsafe side (it refuses less).

Suggestion: keep the alias-matches-snapshot rule for the warning and repo compare, but in baseline compare treat dated vs undated as "not verified" (named on stderr, refused by --strict), the same way a role that is not recorded is handled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Waiting on a decision before I change this: the undated alias matching its dated snapshot was a deliberate part of this PR, but you are right that before it baseline compare --strict refused that pair, and that this is the unsafe direction for a gate. Options: (a) keep one rule everywhere, as now; (b) as you suggest, keep the alias match for the warning and repo compare, and make baseline compare read dated against undated as "not verified" (named on stderr, refused by --strict). (b) also means a panel in baseline compare pairs by equality of the dated or undated form, so the augmenting-path pairing and the panel cases added in 8a2f952 would change. My recommendation: (b), since --strict is a gate and refusing more is the safe side. The thread stays open until then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(b), as you recommend. The panel pairing does not have to go. Compare in two passes: first by exact normalized id and date (for a panel: the same multiset). If that matches, it is a match. If it fails but same_model pairs everything (your augmenting-path pairing, unchanged), the role is "not verified": it is named on stderr and --strict refuses it. Anything else is a mismatch. So a dated-vs-undated pair is never a match and never a false mismatch. The m,m-20240101 vs m-20240101,m-20250101 cases then expect "not verified" instead of a match.

Comment thread dokimi/dokimi/utils.py Outdated
Comment thread dokimi/dokimi/utils.py Outdated
Comment thread dokimi/dokimi/commands/baseline.py Outdated

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at e159659. The single same_model rule is the right direction, and the augmenting-path panel pairing is correct. Two test gaps in the panel pairing are worth fixing before merge, and @vlad-perevezentsev's open points still hold.

I agree with @vlad-perevezentsev's points on the stored evaluator_independence_warning in dokimi run, the undated alias passing baseline compare --strict, the dropped org in rsplit, openai.gpt-oss-120b-1:0, and the docstring wrap. I reproduced each at this head.

What I ran, on a Linux review host at e159659:

  • make ci-local: 27 passed, 1 failed, 7 skipped. The failure is pip-audit, which lists filelock advisories on the lock files; this PR does not touch them.
  • 9,758 tests passed, changed-line coverage 100%.
  • Reproductions for the code findings, with the LLM transport mocked, and mutations of _same_recorded.

Checked, no issues

  • Panel/non-panel and one-to-one pairing: removing the panel check, dropping tried.add, any for all and == for same_model are all caught by the suite.
  • repo compare / repo report: same_model is symmetric, and each loop in _models_equivalent covers one direction.
  • Changelog and growth ratchet: the fragment is in place and ci-local passes both.

Comment thread dokimi/dokimi/commands/baseline.py
Comment thread dokimi/dokimi/commands/baseline.py
@vchamarthi

vchamarthi commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

✅ OpenCodeReview: Review complete: 0 finding(s) across 14 selected item(s).

Review details
  • Commit reviewed: 7140e6d against merge base f0f95a2
  • Coverage: 14 files in 5 groups
  • Engine: OpenCodeReview v1.13.3, us.anthropic.claude-sonnet-5-5
  • Usage: 485k tokens (378k from cache), 44s
  • Tool calls: 34 (18 code_search, 14 file_read, 2 file_read_diff)
  • Changelog and docs for one-model match rule: 2 files
  • Core model-matching utility and its tests: 3 files
  • Baseline command and baseline compare tests: 2 files
  • Evaluate command and pipeline independence flag: 3 files
  • Repo compare/report changes and tests: 4 files

Comment thread dokimi/dokimi/commands/baseline.py Outdated
Comment thread dokimi/dokimi/utils.py Outdated
Comment thread dokimi/tests/test_baseline_compare.py
Comment thread dokimi/tests/test_model_defaults.py
…odel-match-rule

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

# Conflicts:
#	dokimi/dokimi/repos/compare.py
#	dokimi/dokimi/repos/report.py
The pipeline compared provider and model as exact strings, so the printed
warning and run_metadata.evaluator_independence_warning disagreed for one
model on two routes. Use same_model, as eval does.

Thread: PRRT_kwDOVBg7g86xgsTm
Thread: PRR_kwDOVBg7g88AAAABU4ae8g
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Only a known route (provider or gateway prefix, an ARN's inference-profile/)
is dropped; the rest of the path stays in the id, so meta-llama/X and
myorg/X are two models.

Thread: PRRT_kwDOVBg7g86xgsT1
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
openai.gpt-oss-120b-1:0 did not match gpt-oss-120b, so the self-judge
warning missed it and repo compare reported a change that did not happen.

Thread: PRRT_kwDOVBg7g86xgsT-
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
gpt-4o-2024-08-06 did not match gpt-4o, so the self-judge warning was
skipped and a baseline check reported a false model mismatch. A dashed
and a compact date of one day compare equal.

Thread: PRRT_kwDOVBg7g86xlsyt
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The edited line was 123 characters and the next one started at column 0.

Thread: PRRT_kwDOVBg7g86xgsUD
Thread: PRRT_kwDOVBg7g86xlsyi
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A baseline panel of two judges against a current panel of three, where every
baseline judge pairs, was accepted when the size check was deleted.

Thread: PRRT_kwDOVBg7g86xiXNo
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Baseline m,m-20240101 against current m-20240101,m-20250101 is the same
panel, and only an augmenting path finds the pairing: a first-match pairing
strands the second judge. Replacing the augment step with a first match left
the suite green; the new case, in both orders, fails it.

Thread: PRRT_kwDOVBg7g86xiXN0
Thread: PRRT_kwDOVBg7g86xlsy9
Thread: PRR_kwDOVBg7g88AAAABU5SrZA
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The assertions checked only the return value. A warning printed for every
pair passed the old test; the new one fails it. It also covers an unset
answer or judge model, which must not warn.

Thread: PRRT_kwDOVBg7g86xlszG
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…del id

Thread: PRRT_kwDOVBg7g86xgsT1
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… nine

Found by the self-review of this round: with the organization kept, a route
missing from the list (azure/, gemini/, groq/, models/, a foundation-model
ARN) read as an organization, so azure/gpt-4o did not match gpt-4o. Add the
litellm routes, strip a foundation-model ARN like an inference profile, and
pin that every provider in llm.PROVIDERS and its aliases is a route.

Thread: PRRT_kwDOVBg7g86xgsT1
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@napetrov

Copy link
Copy Markdown
Contributor Author

Your points are answered in their threads: the stored flag of dokimi run (58eafba), the organization, the -N:M revision and the docstring are fixed there; the undated alias under --strict is waiting on a decision in its thread.

@napetrov

Copy link
Copy Markdown
Contributor Author

Both test gaps are fixed (8a2f952, panel size and augmenting pairing, each failing on your mutation); the points you share with the earlier review are answered in their threads.

@napetrov

Copy link
Copy Markdown
Contributor Author

This review carries only the OpenCodeReview run marker and no claim of its own; its four findings are the inline threads, each answered there.

@napetrov

Copy link
Copy Markdown
Contributor Author

Codecov report, no finding to act on: it reports all modified and coverable lines covered.

@napetrov

Copy link
Copy Markdown
Contributor Author

OpenCodeReview summary, no finding of its own: its four inline findings are answered in their threads.

@vlad-perevezentsev vlad-perevezentsev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review at 7140e6d. The fixes since e159659 are right. dokimi run stores the self-judge flag by same_model, an org/name id keeps its organization, and plain -N:M revisions and OpenAI's dashed snapshots are handled. One point is still open: an undated alias passing baseline compare --strict. My answer to your question is in that thread.

What I ran, at 7140e6d:

  • make ci-local --offline: 0 failed, 17 passed, 13 skipped (network/docker jobs).
  • same_model on the new route list: meta-llama/X vs myorg/X and meta-llama/X vs X are False, openrouter/google/gemma-2-9b vs gemma-2-9b and ollama/qwen2.5:7b vs qwen2.5:7b are True.

Checked, no issues

  • Stored flag: pipeline.py and evaluate.py now use one rule. Under the old exact provider-and-model check, test_pipeline_independence_flag.py fails on the cross-route pair.
  • Routes: only the leading segments in _ROUTES are dropped, plus an ARN's inference-profile/ or foundation-model/ tail. A test pins every llm.PROVIDERS name and alias as a route.
  • Panel tests: the augmenting-path and panel-size cases fail under a greedy pairing and with the size check removed.
  • Docs and changelog: both now say the organization is part of the id and that run stores the flag by the new rule.

This branch has not been deployed

No deployments
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.

5 participants