Skip to content

[fix] Coerce falsy stored metric JSON columns in DayAggregatedMetrics.from_dict - #10320

Open
Lesereingrape wants to merge 1 commit into
agno-agi:mainfrom
Lesereingrape:fix/metrics-json-column-defaults
Open

Lesereingrape wants to merge 1 commit into
agno-agi:mainfrom
Lesereingrape:fix/metrics-json-column-defaults

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 19, 2026 •

Copy link
Copy Markdown

Summary

DayAggregatedMetrics.from_dict() handed the two JSON columns straight to pydantic with a fallback that contradicts their annotations:

model_metrics=metrics_dict.get("model_metrics", {}),   # field is List[Dict[str, Any]]
token_metrics=metrics_dict.get("token_metrics", {}),   # field is Dict[str, Any]

A stored row legitimately holds {} for model_metrics — that is the declared default on every adapter's JSON column (sqlite/schemas.py:130, postgres/schemas.py:131, singlestore/schemas.py:118, redis/schemas.py:46, valkey/schemas.py:46) — and a row written before per-owner metrics existed carries no such key at all. Either shape reached a List field, so GET /metrics for a scoped caller answered 500 for a day that simply has no per-model breakdown.

Both fields now normalize with or onto the container they declare. This is the rule the rest of the stack already applies — db/utils.py:806 reads [dict(m) for m in (row.get("model_metrics") or [])], and V3_MIGRATION_GUIDE.md:499,515 backfills with CASE WHEN jsonb_typeof(...) = 'array' ... ELSE '[]'::jsonb — but the per-owner read path (identify_metrics_by_owner(), which only rewrites id) never passed rows through it, so the reader is the only place left to enforce it.

A value that cannot be metrics at all (e.g. model_metrics = {"gpt-5-mini": 2}) still raises; only the falsy shapes change.

Closes #10319

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Improvement
  • Model update
  • Other:

Checklist

  • Code complies with style guidelines
  • Ran format/validation scripts (./scripts/format.sh and ./scripts/validate.sh)
  • Self-review completed
  • Documentation updated (comments, docstrings)
  • Examples and guides: Relevant cookbook examples have been included or updated (if applicable)
  • Tested in clean environment
  • Tests added/updated (if applicable)

Duplicate and AI-Generated PR Check

  • I have searched existing open pull requests and confirmed that no other PR already addresses this issue
  • If a similar PR exists, I have explained below why this PR is a better approach
  • Check if this PR was entirely AI-generated (by Copilot, Claude Code, Cursor, etc.)

Additional Notes

Verification

  • The repro from GET /metrics returns 500 when a stored row holds the JSON column default for model_metrics #10319 fails on main (85b6d1d) with ValidationError: Input should be a valid list [type=list_type, input_value={}, input_type=dict] and GET /metrics → 500; with this change it returns 200.
  • libs/agno/tests/unit/os/routers/test_metrics_router.py: 4 failed / 39 passed with only the source change reverted (the four per-owner cases), 43 passed with it applied. No existing test changed status in the other direction.
  • Added coverage: {} / None / key-absent for model_metrics, None / key-absent for token_metrics, each across both the scoped and unscoped reads, plus one case asserting a genuinely malformed model_metrics still surfaces as an error rather than being swallowed.
  • ruff check and ruff format --check pass on both changed files and across agno/os (130 files); mypy reports no issues on schemas.py.
  • Blast radius checked by symbol search: from_dict is called only at os/routers/metrics/metrics.py:152 and :333, both covered by the test file above. (agno/client/os.py:2991 validates already-serialized responses, so it is unaffected.) Note the wider tests/unit/os directory is not runnable in my environment — 222 collection errors from optional extras that aren't installed (slack_sdk, telebot, a2a, fastmcp, jwt, uvicorn) — so I verified reachability by call site rather than claiming that suite green.

Collision check

Searched open issues and PRs for model_metrics and DayAggregatedMetrics (the only hits are my own #10300/#10301, which touch os/utils.py, a different file), then scanned the changed-file lists of all 777 open PRs. Exactly one other PR touches libs/agno/agno/os/routers/metrics/schemas.py: #6072, a cookbook/timestamp sync stale since 2026-06-06 and currently CONFLICTING. Its surviving change to this file was the int → datetime shift for created_at/updated_at via to_utc_datetime, which is already on main, and its diff leaves both model_metrics=metrics_dict.get("model_metrics", {}) and the token_metrics line untouched — so it does not address this bug, and this PR does not overlap its remaining content.


AI assistance (per CONTRIBUTING.md §5): the investigation, the diff and this description were produced by an AI coding agent working under the account owner's standing instruction to find and fix defects of this kind and open PRs for them; no human read the diff before it was opened. Everything quoted in the validation section was run by that agent on a local checkout and is reproducible on a fresh clone of this branch. The commit carries no AI co-author trailer.

….from_dict

A metrics row is read straight from the JSON column, and that column can hold
its declared default `{}` for model_metrics, or no key at all for a row written
before per-owner metrics existed. Both shapes reached the field as a list and
made the /metrics endpoint answer 500 instead of reporting that day.
@Lesereingrape

Copy link
Copy Markdown
Author

Real CI has not run on this PR yet - it needs one click from someone with write access.

The 3 workflow runs on head 2f6dfb71 (PR Lint, Validation) are all completed / action_required, i.e. queued and waiting for a maintainer to press Approve and run. The only check that has executed is Triage PR (success), which is why the merge box reads blocked: a missing approval, not a failing check.

The verification I do control - the ruff and mypy runs and the test results they produced - is recorded in the description above; I am not adding new claims here.

Six sibling PRs from this account are in the same state (#10281, #10287, #10297, #10301, #10323, #10325); I have asked on 4 of them, so clearing the whole set is 20 clicks in total. Say the word and I will narrow it down.

Disclosure: this account is being operated by an AI coding assistant on the owner's instruction, and the PR bodies say so as well.

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.

GET /metrics returns 500 when a stored row holds the JSON column default for model_metrics

1 participant