Skip to content

feat(OMN-10847): populate routing_outcomes.quality_score from canary grading - #605

Closed
jonahgabriel wants to merge 2 commits into
mainfrom
jonah/omn-10847-populate-routing_outcomesquality_score-from-canary-grading
Closed

jonahgabriel wants to merge 2 commits into
mainfrom
jonah/omn-10847-populate-routing_outcomesquality_score-from-canary-grading

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Extends HandlerCanaryScoreReducer.materialize() to produce rows for both capability_scores and routing_outcomes tables. Adds ModelMaterializeResult to hold both output sets. The routing_outcomes.quality_score column (migration 066) is now populated from canary grading composite scores.

node_platform_readiness already queries SELECT COUNT(*) FROM routing_outcomes WHERE quality_score IS NOT NULL — this change makes that query return real data after canary runs.

Part of the canary-to-routing feedback loop (OMN-10843).

Ticket: OMN-10847

dod_evidence

  • materialize() returns ModelMaterializeResult with both capability_score_rows and routing_outcome_rows
  • routing_outcomes.quality_score set to composite score from canary grading
  • selected=True for highest-scoring model per task type
  • Existing tests updated for new return type
  • New test verifies routing_outcome_rows content
  • 6/6 tests pass

Test plan

  • uv run pytest src/omnimarket/nodes/node_canary_score_reducer/tests/ -v — 6 tests pass
  • Full suite passes
  • pre-commit clean
  • CI green

Summary by CodeRabbit

  • New Features

    • Score reducer now returns both capability scores and routing outcomes, including model selection flags, quality scores, and per-entry latency/cost metrics to drive routing decisions.
  • Tests

    • Expanded unit and end-to-end tests validating routing outcome generation, champion selection, and integration of scores into routing decisions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 11, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

HandlerCanaryScoreReducer.materialize() return type changes from a single list to a structured ModelMaterializeResult containing both capability_score_rows and routing_outcome_rows. The handler now computes a best-scoring model key and marks selected rows accordingly. Tests are updated to reflect the new structured output.

Changes

Materialize Result Refactoring

Layer / File(s) Summary
Result Model Definition
src/omnimarket/nodes/node_canary_score_reducer/models/model_materialize_result.py
Introduces frozen Pydantic model ModelMaterializeResult with two list-of-dict fields: capability_score_rows and routing_outcome_rows, both defaulting to empty lists.
Handler Materialization Logic
src/omnimarket/nodes/node_canary_score_reducer/handlers/handler_canary_score_reducer.py
Handler imports ModelMaterializeResult and refactors materialize() to build dual outputs: capability_score_rows derived from state scores, and routing_outcome_rows with best-key selection and selected flags based on highest composite_score. Module docstring expanded with issue references.
Test Validation
src/omnimarket/nodes/node_canary_score_reducer/tests/test_handler_canary_score_reducer.py
Existing tests refactored to access result.capability_score_rows from the structured return object. New test test_materialize_produces_routing_outcome_rows validates routing outcome generation with quality_score matching composite_score and proper row selection logic.
Integration: Feedback Loop
src/omnimarket/nodes/node_canary_score_reducer/tests/test_integration_feedback_loop.py
New end-to-end test exercises canary reporting → score reduction → available-models build → routing, asserting champion selection and routing_outcome_rows contents.
Routing Package Exports
src/omnimarket/nodes/node_routing_policy_engine/__init__.py
Re-exports EnumTaskType, ModelAvailableModel, ModelRoutingPolicyRequest, and build_available_models_from_scores alongside HandlerRoutingPolicy.
Score Lookup Bridge
src/omnimarket/nodes/node_routing_policy_engine/handlers/handler_score_lookup.py
Adds build_available_models_from_scores that maps capability score rows into ModelAvailableModel objects with defaults for missing success_rate and cost_map entries, preserving input order.
Score Lookup Tests
src/omnimarket/nodes/node_routing_policy_engine/tests/test_handler_score_lookup.py
Unit tests verify proper mapping of keys/scores/costs and empty-input behavior for build_available_models_from_scores.

Sequence Diagram

sequenceDiagram
  participant State as ModelScoreReducerState
  participant Handler as HandlerCanaryScoreReducer
  participant Result as ModelMaterializeResult
  State->>Handler: state.scores (list of rows)
  Handler->>Handler: build capability_score_rows from scores
  Handler->>Handler: compute best_key (highest composite_score)
  Handler->>Handler: build routing_outcome_rows with selected flags
  Handler->>Result: wrap both row lists
  Result->>Handler: return ModelMaterializeResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Two datasets now, so neatly bound,
Capability scores and routing found!
Composite scores lead the way,
Best models shine brighter each day. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: populating routing_outcomes.quality_score from canary grading data, matching the core feature described throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonah/omn-10847-populate-routing_outcomesquality_score-from-canary-grading

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

@jonahgabriel
jonahgabriel enabled auto-merge May 11, 2026 20:29
@github-actions

Copy link
Copy Markdown

✅ Architectural Review — PASSED

Errors: 0
Warnings: 0

What this checks

Rule Description
ARCH-TOPIC-001 No hardcoded Kafka topic strings in handler code
ARCH-DI-001 No event_bus=None bypass
ARCH-DI-002 No direct Handler instantiation outside workflow_runner/adapters
ARCH-DI-003 No reinvented DI containers
ARCH-TOPIC-002 contract.yaml topics follow `onex.{cmd

No architectural violations found.

Powered by node_pr_review_bot

@github-actions

github-actions Bot commented May 11, 2026 •

Copy link
Copy Markdown

✅ PR Review Bot — CLEAN

Findings: 0
Run ID: ``

Note: Reviewer models are running as stubs (OMN-7969–OMN-7972 pending). Architectural findings from Phase 1 (arch-lint) are the authoritative signal until model handlers land.


Verdict semantics

Verdict Meaning
clean No blocking findings
risks_noted Findings noted; threads passed verification
blocking_issue MAJOR/CRITICAL findings not resolved — do not merge

Powered by node_pr_review_bot — FSM pipeline with multi-model review fan-out

@github-actions

Copy link
Copy Markdown

⚠️ Hostile Reviewer — DEGRADED (informational)

Blocking findings (critical/error): 0
Total findings: 0
Models succeeded: none

Note: All reviewer models failed or were unavailable. Degraded results are informational during the pilot phase (OMN-8524) and do not block merge. Error: cli_review exited non-zero (all models failed or binary error)


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded All models unavailable (infra) No (pilot)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/omnimarket/nodes/node_canary_score_reducer/handlers/handler_canary_score_reducer.py (1)

79-107: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Select winners per task_type instead of a single global winner

Line 80/Line 107 use one best_key across all rows, so only one row total can be selected=True. This breaks the “highest-scoring model per task type” behavior when multiple task types are present in state.scores.

Proposed fix
-        best_key: str | None = None
-        best_score: float = -1.0
+        best_key_by_task: dict[str, str] = {}
+        best_score_by_task: dict[str, float] = {}
         for row in scored_rows:
-            if row.composite_score is not None and row.composite_score > best_score:
-                best_score = row.composite_score
-                best_key = f"{row.model_key}::{row.task_type}"
+            if row.composite_score is None:
+                continue
+            task_type = row.task_type
+            if row.composite_score > best_score_by_task.get(task_type, float("-inf")):
+                best_score_by_task[task_type] = row.composite_score
+                best_key_by_task[task_type] = f"{row.model_key}::{task_type}"
@@
-                    "selected": (f"{row.model_key}::{row.task_type}" == best_key),
+                    "selected": (
+                        f"{row.model_key}::{row.task_type}"
+                        == best_key_by_task.get(row.task_type)
+                    ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/omnimarket/nodes/node_canary_score_reducer/handlers/handler_canary_score_reducer.py`
around lines 79 - 107, The code currently computes a single global best_key so
only one row is marked selected; instead compute the highest composite_score per
task_type and mark selection per task_type. Iterate scored_rows to build a dict
mapping task_type -> (best_score, best_model_key) using row.composite_score,
then when populating capability_score_rows and routing_outcome_rows use that
dict to set "selected" by comparing f"{row.model_key}::{row.task_type}" (or
compare model_key and task_type) against the per-task best entry; update any
references to best_key accordingly (look at scored_rows, best_key,
routing_outcome_rows, capability_score_rows, row.model_key, row.task_type,
row.composite_score).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@src/omnimarket/nodes/node_canary_score_reducer/handlers/handler_canary_score_reducer.py`:
- Around line 79-107: The code currently computes a single global best_key so
only one row is marked selected; instead compute the highest composite_score per
task_type and mark selection per task_type. Iterate scored_rows to build a dict
mapping task_type -> (best_score, best_model_key) using row.composite_score,
then when populating capability_score_rows and routing_outcome_rows use that
dict to set "selected" by comparing f"{row.model_key}::{row.task_type}" (or
compare model_key and task_type) against the per-task best entry; update any
references to best_key accordingly (look at scored_rows, best_key,
routing_outcome_rows, capability_score_rows, row.model_key, row.task_type,
row.composite_score).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 552c215a-b325-49be-aafb-4c086f7fa833

📥 Commits

Reviewing files that changed from the base of the PR and between 36fc234 and f3411ac.

📒 Files selected for processing (3)
  • src/omnimarket/nodes/node_canary_score_reducer/handlers/handler_canary_score_reducer.py
  • src/omnimarket/nodes/node_canary_score_reducer/models/model_materialize_result.py
  • src/omnimarket/nodes/node_canary_score_reducer/tests/test_handler_canary_score_reducer.py

…oop (#606)

* feat(OMN-10846): bridge capability_scores to routing policy engine via score lookup

Adds build_available_models_from_scores() — a caller-side bridge that converts
capability_scores table rows into ModelAvailableModel instances accepted by the
routing policy engine. HandlerRoutingPolicy is not modified.

* fix(OMN-10846): resolve mypy strict arg-type error in score lookup

* test(OMN-10848): proof of life — canary → scores → routing feedback loop

End-to-end integration test proving the full chain works in-memory:
ModelCanaryReport → HandlerCanaryScoreReducer → ModelMaterializeResult →
build_available_models_from_scores → HandlerRoutingPolicy selects champion.

Also re-exports EnumTaskType, ModelRoutingPolicyRequest, ModelAvailableModel,
and build_available_models_from_scores from node_routing_policy_engine.__init__
to avoid cross-node .models.* reach-in violations (OMN-9263).

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/omnimarket/nodes/node_routing_policy_engine/tests/test_handler_score_lookup.py (1)

39-44: ⚡ Quick win

Add a unit test for documented fallback/default behavior.

The tests currently validate normal and empty paths, but not the contract that missing/None success_rate and missing cost_map entries default to 0.0.

Suggested test addition
 `@pytest.mark.unit`
 def test_build_available_models_empty_scores_returns_empty() -> None:
     """Empty input rows and cost_map produce an empty list."""
     result = build_available_models_from_scores([], {})
     assert result == []
+
+
+@pytest.mark.unit
+def test_build_available_models_defaults_for_missing_values() -> None:
+    rows: list[dict[str, object]] = [
+        {"model_key": "qwen3-coder", "success_rate": None},
+        {"model_key": "deepseek-r1"},
+    ]
+    result = build_available_models_from_scores(rows, {"qwen3-coder": 0.001})
+
+    assert len(result) == 2
+    assert result[0].score == pytest.approx(0.0)
+    assert result[0].cost_per_token == pytest.approx(0.001)
+    assert result[1].score == pytest.approx(0.0)
+    assert result[1].cost_per_token == pytest.approx(0.0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/omnimarket/nodes/node_routing_policy_engine/tests/test_handler_score_lookup.py`
around lines 39 - 44, Add a unit test that verifies
build_available_models_from_scores applies documented defaults: when input score
rows have missing or None success_rate and when cost_map lacks an entry for a
model, the function should treat both success_rate and cost as 0.0; create test
rows with a model missing the success_rate field and another with
success_rate=None, pass a cost_map that omits those model keys, call
build_available_models_from_scores and assert the returned entries for those
models contain success_rate == 0.0 and cost == 0.0 (and otherwise match expected
fields).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/omnimarket/nodes/node_routing_policy_engine/handlers/handler_score_lookup.py`:
- Around line 33-36: The current parsing coerces invalid model_key and can crash
on malformed success_rate; update the parsing in handler_score_lookup.py so you
first validate model_key from row (ensure it's not None, not empty, and of
expected type/string) and skip or continue when invalid instead of doing
str(None); for success_rate (raw_score) parse it safely using a try/except
around numeric conversion (handle ValueError/TypeError), default to 0.0 on
failure, and clamp the resulting score into a safe range (e.g., 0.0–1.0) before
using it; keep the cost lookup via cost_map.get(model_key, 0.0) unchanged.

---

Nitpick comments:
In
`@src/omnimarket/nodes/node_routing_policy_engine/tests/test_handler_score_lookup.py`:
- Around line 39-44: Add a unit test that verifies
build_available_models_from_scores applies documented defaults: when input score
rows have missing or None success_rate and when cost_map lacks an entry for a
model, the function should treat both success_rate and cost as 0.0; create test
rows with a model missing the success_rate field and another with
success_rate=None, pass a cost_map that omits those model keys, call
build_available_models_from_scores and assert the returned entries for those
models contain success_rate == 0.0 and cost == 0.0 (and otherwise match expected
fields).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85a43a3e-c70d-465d-9fc9-3314577470f4

📥 Commits

Reviewing files that changed from the base of the PR and between f3411ac and 1af2f16.

📒 Files selected for processing (4)
  • src/omnimarket/nodes/node_canary_score_reducer/tests/test_integration_feedback_loop.py
  • src/omnimarket/nodes/node_routing_policy_engine/__init__.py
  • src/omnimarket/nodes/node_routing_policy_engine/handlers/handler_score_lookup.py
  • src/omnimarket/nodes/node_routing_policy_engine/tests/test_handler_score_lookup.py

Comment on lines +33 to +36
model_key = str(row["model_key"])
raw_score = row.get("success_rate")
score = float(str(raw_score)) if raw_score is not None else 0.0
cost = cost_map.get(model_key, 0.0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden row parsing to avoid bad data poisoning or runtime failures.

This bridge currently coerces invalid keys (None → "None") and can throw on malformed success_rate, which can fail routing on a single bad row. Add explicit key validation and safe numeric parsing with bounded score handling.

Proposed fix
+import math
+
 def build_available_models_from_scores(
     capability_rows: list[dict[str, object]],
     cost_map: dict[str, float],
 ) -> list[ModelAvailableModel]:
@@
     models: list[ModelAvailableModel] = []
     for row in capability_rows:
-        model_key = str(row["model_key"])
+        model_key_raw = row.get("model_key")
+        if not isinstance(model_key_raw, str) or not model_key_raw.strip():
+            continue
+        model_key = model_key_raw
         raw_score = row.get("success_rate")
-        score = float(str(raw_score)) if raw_score is not None else 0.0
+        try:
+            score = float(raw_score) if raw_score is not None else 0.0
+        except (TypeError, ValueError):
+            score = 0.0
+        if not math.isfinite(score) or not (0.0 <= score <= 1.0):
+            score = 0.0
         cost = cost_map.get(model_key, 0.0)
         models.append(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/omnimarket/nodes/node_routing_policy_engine/handlers/handler_score_lookup.py`
around lines 33 - 36, The current parsing coerces invalid model_key and can
crash on malformed success_rate; update the parsing in handler_score_lookup.py
so you first validate model_key from row (ensure it's not None, not empty, and
of expected type/string) and skip or continue when invalid instead of doing
str(None); for success_rate (raw_score) parse it safely using a try/except
around numeric conversion (handle ValueError/TypeError), default to 0.0 on
failure, and clamp the resulting score into a safe range (e.g., 0.0–1.0) before
using it; keep the cost lookup via cost_map.get(model_key, 0.0) unchanged.

@jonahgabriel

Copy link
Copy Markdown
Contributor Author

Code delivered via PR #606 which cherry-picked this work. Closing as superseded.

auto-merge was automatically disabled May 11, 2026 21:23

Pull request was closed

@jonahgabriel
jonahgabriel deleted the jonah/omn-10847-populate-routing_outcomesquality_score-from-canary-grading branch June 11, 2026 17:56
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