Skip to content

fix(router): match routing-memory scores on canonical model identity - #547

Merged
njbrake merged 1 commit into
mainfrom
fix/canonical-router-score-keys
Aug 10, 2026
Merged

fix(router): match routing-memory scores on canonical model identity#547
njbrake merged 1 commit into
mainfrom
fix/canonical-router-score-keys

Conversation

@njbrake

@njbrake njbrake commented Aug 10, 2026

Copy link
Copy Markdown
Member

Description

rank stored each score key in the spelling its policy used, and knn._score matched qualities keys against candidate selectors by exact string. That holds for one policy, but a user with two learned policies that spell one model differently gets a single stored spelling, so the other policy's lookup misses: the candidate scores nothing, the strong model wins unopposed at confidence 1.0, and /status reports the pool warm. The only signal is a log line.

Store the canonical instance:model instead, and canonicalize the pool and each neighbor's stored keys in _score, so spelling cannot decide whether a candidate matches anywhere. Canonicalizing on read also picks up records written before this change, so no migration is needed, and aliases resolve through the same path. A record holding two spellings of one model (which rank refuses) keeps the first, so the collision resolves the same way on every request.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #541

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

Tests: a unit test where a record keyed in the other spelling still scores its candidate, and an integration test where a second policy naming the same pool as provider/model routes on examples taught through the instance:model policy. Both fail on main. Full suite: 2617 passed, 10 skipped.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

Claude Opus 5 via Claude Code.

Any additional AI details you'd like to share:

Implemented from the issue write-up via back-and-forth with @njbrake; the reasoning and decisions are his.

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Store routing-memory scores using canonical instance:model identities.
  • Match equivalent model spellings and aliases during routing.
  • Preserve compatibility with existing records without migration.
  • Keep the first score when a record contains duplicate model spellings.
  • Add unit and integration coverage for canonical model matching.

Validation

  • 2,617 tests passed.
  • 10 tests skipped.

`rank` stored each score key in the spelling its policy used, and `knn._score`
matched `qualities` keys against candidate selectors by exact string. That holds
for one policy. It breaks as soon as a user has two learned policies that spell
one model differently: normalization picks a single spelling, so the other
policy's lookup misses, the candidate scores nothing, and the strong model wins
unopposed at confidence 1.0 while `/status` reports the pool warm. The only
signal is a log line.

Store the canonical `instance:model` instead, and canonicalize the pool and each
neighbor's stored keys in `_score`, so spelling cannot decide whether a candidate
matches anywhere. Canonicalizing on read also picks up records written before
this change, and aliases resolve through the same path. A record holding two
spellings of one model (which `rank` refuses) keeps the first, so the collision
resolves the same way on every request.

Fixes #541
@njbrake
njbrake deployed to integration-tests August 10, 2026 16:48 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 51ccb28b-caaa-4a6d-bc18-bf03dde8bb9a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b7e1d7 and 9056144.

⛔ Files ignored due to path filters (1)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
📒 Files selected for processing (6)
  • docs/public/otari.postman_collection.json
  • src/gateway/api/routes/routing_memory.py
  • src/gateway/models/entities.py
  • src/gateway/services/routing/knn.py
  • tests/integration/test_routing_learned.py
  • tests/unit/test_knn_router.py

Walkthrough

Routing-memory validation and kNN scoring now canonicalize model selectors to instance:model keys. The change supports equivalent slash and colon spellings across learned policies, with integration and unit test coverage.

Changes

Routing-memory canonicalization

Layer / File(s) Summary
Canonical score-key validation
src/gateway/api/routes/routing_memory.py, src/gateway/models/entities.py
Score validation stores resolvable selectors as canonical instance:model keys. RoutingMemory documentation describes canonical keys and preference-collection records.
Canonical kNN matching
src/gateway/services/routing/knn.py
kNN scoring canonicalizes candidates and stored qualities. Selector resolution is cached, unresolved selectors retain their spelling, and duplicate canonical keys keep the first score.
Cross-spelling routing coverage
tests/integration/test_routing_learned.py, tests/unit/test_knn_router.py, docs/public/otari.postman_collection.json
Tests cover slash and colon model spellings, shared learned memory, candidate selection, confidence, and deterministic first-match behavior. The ranking endpoint documentation describes canonical key storage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: tbille

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the fix Conventional Commit prefix, imperative mood, and stays within the 70-character limit while describing the canonical model identity fix.
Description check ✅ Passed The description explains the bug and solution, links issue #541, records testing and documentation updates, and completes the required checklist and AI disclosure.
Linked Issues check ✅ Passed The implementation satisfies issue #541 by canonicalizing stored and lookup keys, supporting aliases and legacy records, and adding unit and integration coverage.
Out of Scope Changes check ✅ Passed The code, documentation, and tests directly support canonical routing-memory identity matching and remain within the linked issue scope.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/canonical-router-score-keys
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/canonical-router-score-keys

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from tbille August 10, 2026 16:49
@njbrake
njbrake merged commit f75e5b0 into main Aug 10, 2026
14 checks passed
@njbrake
njbrake deleted the fix/canonical-router-score-keys branch August 10, 2026 17:44
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.

Canonicalize routing-memory score keys so model spelling cannot matter

1 participant