insightfactory-databricks-langgraph-tracer 1.0.0.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databricks_langgraph_tracer/CHANGELOG.md +176 -0
- databricks_langgraph_tracer/__init__.py +28 -0
- databricks_langgraph_tracer/_generated_keys.py +43 -0
- databricks_langgraph_tracer/aggregate.py +60 -0
- databricks_langgraph_tracer/autolog_compat.py +447 -0
- databricks_langgraph_tracer/cost.py +173 -0
- databricks_langgraph_tracer/databricks_utils.py +133 -0
- databricks_langgraph_tracer/errors.py +2 -0
- databricks_langgraph_tracer/mlflow_utils.py +316 -0
- databricks_langgraph_tracer/pricing/model-pricing-overrides.json +28 -0
- databricks_langgraph_tracer/pricing/model-pricing.json +1 -0
- databricks_langgraph_tracer/py.typed +1 -0
- databricks_langgraph_tracer/tracer.py +1083 -0
- databricks_langgraph_tracer/uc_export.py +106 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5.dist-info/METADATA +224 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5.dist-info/RECORD +19 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5.dist-info/WHEEL +4 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5.dist-info/licenses/LICENSE +5 -0
- insightfactory_databricks_langgraph_tracer-1.0.0.dev5.dist-info/licenses/THIRD_PARTY_NOTICES +26 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Python package
|
|
4
|
+
(`insightfactory-databricks-langgraph-tracer`) are
|
|
5
|
+
documented here. The format is based on
|
|
6
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
|
|
7
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
|
+
|
|
9
|
+
See [`../typescript/CHANGELOG.md`](../typescript/CHANGELOG.md) for changes to the
|
|
10
|
+
separately versioned TypeScript package.
|
|
11
|
+
|
|
12
|
+
<!--
|
|
13
|
+
Releasing: add changes under [Unreleased] as you go. To cut a release, move those
|
|
14
|
+
items into a new `## [x.y.z] - YYYY-MM-DD` section, bump `version` in
|
|
15
|
+
pyproject.toml, and add the version's link reference at the bottom. CI requires a
|
|
16
|
+
matching `## [x.y.z]` section whenever pyproject.toml is version-bumped on a PR to
|
|
17
|
+
main; pushing to main then tags `python-vx.y.z` and publishes a GitHub Release
|
|
18
|
+
from this section.
|
|
19
|
+
-->
|
|
20
|
+
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - 2026-09-01
|
|
24
|
+
### Changed
|
|
25
|
+
- Renamed the PyPI distribution from `databricks-langgraph-tracer` to
|
|
26
|
+
`insightfactory-databricks-langgraph-tracer`. The Python import package remains
|
|
27
|
+
`databricks_langgraph_tracer`, so application imports do not change.
|
|
28
|
+
- Made the stable Python package available through public PyPI.
|
|
29
|
+
- Raised the minimum supported MLflow version to 3.15.0 and LangChain version to
|
|
30
|
+
1.3.9 so the public release does not resolve versions with known vulnerabilities.
|
|
31
|
+
- Added `ty` type checking to the Python development dependencies and CI gates,
|
|
32
|
+
matching the Insight Factory CLI and SDK toolchain.
|
|
33
|
+
|
|
34
|
+
## [0.6.1] - 2026-07-29
|
|
35
|
+
### Changed
|
|
36
|
+
- Refreshed the vendored LiteLLM pricing catalog (2,719 → 2,984 entries), adding cost
|
|
37
|
+
coverage for the latest Foundry / Azure OpenAI models — notably the **GPT-5.6 series**
|
|
38
|
+
(`gpt-5.6`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, plus their `azure/us/` and
|
|
39
|
+
`azure/eu/` regional variants). Deployments named after these models now resolve to a
|
|
40
|
+
pricing key and are costed, rather than being tagged `cost.unknown_model`. The manual
|
|
41
|
+
`azure_ai/deepseek-v4-flash` / `azure_ai/deepseek-v4-pro` overrides survive the refresh
|
|
42
|
+
unchanged.
|
|
43
|
+
- The refresh is not purely additive: upstream also revised rates for 21 non-Azure
|
|
44
|
+
entries (e.g. `sambanova/gpt-oss-120b`, `mistral/mistral-medium-latest`) and dropped
|
|
45
|
+
two `fireworks_ai/...whisper-v3*` keys. **No `azure/` or `azure_ai/` rate changed**, and
|
|
46
|
+
`resolve_pricing_key` only probes those two prefixes, so default-path costs are
|
|
47
|
+
unaffected; consumers passing an explicit pricing key may see reported costs move.
|
|
48
|
+
|
|
49
|
+
### Known limitations
|
|
50
|
+
- Long-context tiering is still not applied: `calculate_llm_cost` reads only the flat
|
|
51
|
+
per-token rates, never `input_cost_per_token_above_272k_tokens` or the `_priority`
|
|
52
|
+
rates. Pre-existing and affecting all of gpt-5.x, but easier to reach now that the
|
|
53
|
+
GPT-5.6 entries carry 1,050,000-token windows — a 400k-input call under-reports by
|
|
54
|
+
roughly half.
|
|
55
|
+
|
|
56
|
+
## [0.6.0] - 2026-06-25
|
|
57
|
+
### Added
|
|
58
|
+
- `max_string_chars` option on `configure_databricks_tracing(...)` and
|
|
59
|
+
`DatabricksLangGraphTracer(...)` (env fallback `DATABRICKS_TRACING_MAX_STRING_CHARS`).
|
|
60
|
+
Opt-in (default off): when set, any plain-text string leaf longer than the
|
|
61
|
+
threshold in a span's inputs **or** outputs is replaced with a compact
|
|
62
|
+
`{"_truncated": true, "chars": N, "bytes": M, "preview": "…"}` placeholder, so
|
|
63
|
+
large text (not just multimodal bytes) cannot push a Unity Catalog trace past the
|
|
64
|
+
SQL inline read limit (issue #23). It caps string values only — never keys or
|
|
65
|
+
structural fields — and is a per-leaf mitigation, not a hard per-trace budget, so
|
|
66
|
+
pair it with consumer-side payload reduction for the heaviest spans. This is a
|
|
67
|
+
**distinct** knob from the removed multimodal `max_content_part_bytes`: that gated
|
|
68
|
+
inline binary payloads; this caps arbitrary text.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- The text cap now also applies to span **outputs**, not only inputs — the spans that
|
|
72
|
+
breach the inline read limit in the reported case (`final_output` /
|
|
73
|
+
`aggregate_results`) are output-side. Multimodal payload externalization stays
|
|
74
|
+
**input-only**: outputs are arbitrary chain / tool JSON, so running the
|
|
75
|
+
`data`/`base64` probe on them would mis-strip legitimate non-binary output. Runs on
|
|
76
|
+
a copy; the live values are untouched.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
- The `max_string_chars` cap now also reaches text nested inside **structured objects**
|
|
80
|
+
in span inputs/outputs — Pydantic models, dataclasses, tuples/namedtuples, and sets.
|
|
81
|
+
Previously the walk only descended through `str`/`dict`/`list`, so a long string on a
|
|
82
|
+
Pydantic model returned by a LangGraph node (e.g. `final_output` / `aggregate_results`)
|
|
83
|
+
escaped the cap while MLflow's serializer still expanded the model and recorded the
|
|
84
|
+
field in full. Such objects are now normalized to the same shape MLflow records (e.g.
|
|
85
|
+
`model_dump()`), so their leaves are capped too; normalization runs only when the cap
|
|
86
|
+
is set and is best-effort (a failure records the value as-is). (The TypeScript package
|
|
87
|
+
already JSON-normalizes inputs/outputs before the walk, so it was unaffected.)
|
|
88
|
+
- Autolog mode: the post-`end()` attribute force-write (`mlflow.llm.*` enrichment)
|
|
89
|
+
now works on `opentelemetry-sdk >= 1.43`, which made `BoundedAttributes` immutable
|
|
90
|
+
(`__setitem__` raises). The helper writes into the mapping's backing `_dict`
|
|
91
|
+
instead, restoring the enriched reduced schema on the mlflow floor.
|
|
92
|
+
|
|
93
|
+
## [0.5.0] - 2026-06-24
|
|
94
|
+
### Added
|
|
95
|
+
- `content_ref_resolver` option on `configure_databricks_tracing(...)` and
|
|
96
|
+
`DatabricksLangGraphTracer(...)`. It is called once per multimodal content part
|
|
97
|
+
(image / PDF / file) with the part and a `ContentPartContext` (part index/page,
|
|
98
|
+
inline byte length, and a shallow copy of the run metadata), and returns the
|
|
99
|
+
reference dict to store in the trace — e.g. a Unity Catalog volume path — or
|
|
100
|
+
`None` to fall back to the placeholder (issue #21).
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
- Inline base64 payloads of multimodal parts are now **never** stored in the trace:
|
|
104
|
+
they are always stripped before recording and replaced with a lightweight
|
|
105
|
+
reference (a `content_ref_resolver` result, otherwise a
|
|
106
|
+
`{"type": ..., "_omitted": true, "bytes": N}` placeholder), keeping Unity Catalog
|
|
107
|
+
trace reads under the SQL inline limit. Remote `http(s)://` image URLs are kept
|
|
108
|
+
verbatim. The transform runs on a copy, so the cache-stable message is untouched.
|
|
109
|
+
|
|
110
|
+
### Removed
|
|
111
|
+
- The per-content-part byte cap, including the
|
|
112
|
+
`DATABRICKS_TRACING_MAX_CONTENT_PART_BYTES` env var and the
|
|
113
|
+
`max_content_part_bytes` option. Inline payloads are now always externalized, so
|
|
114
|
+
the cap is obsolete.
|
|
115
|
+
|
|
116
|
+
### Security
|
|
117
|
+
- A resolver result that would re-introduce an inline payload — a `data:` URI, a
|
|
118
|
+
recognized base64 content part, or the part's raw base64 echoed back — is rejected
|
|
119
|
+
and the placeholder is used instead, guaranteeing no inline image bytes are
|
|
120
|
+
persisted. `ctx.metadata` is a shallow copy, so a resolver cannot corrupt run
|
|
121
|
+
state.
|
|
122
|
+
|
|
123
|
+
## [0.4.0] - 2026-06-22
|
|
124
|
+
### Added
|
|
125
|
+
- Capture multimodal chat-model inputs (image / PDF / file content blocks) on LLM
|
|
126
|
+
spans.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
- Keep multimodal content inline when exporting to Unity Catalog table-backed
|
|
130
|
+
experiments, instead of dropping it to an attachment reference.
|
|
131
|
+
|
|
132
|
+
## [0.3.0] - 2026-06-17
|
|
133
|
+
### Added
|
|
134
|
+
- Trace-level per-model cost and token rollup, written to the `cost.by_model` tag
|
|
135
|
+
on the live trace before the root span ends. Unpriceable-but-resolved models
|
|
136
|
+
still contribute their token split (zero cost) so the totals stay complete.
|
|
137
|
+
|
|
138
|
+
## [0.2.2] - 2026-06-09
|
|
139
|
+
### Changed
|
|
140
|
+
- Emit OpenInference-style `llm.*` span keys so the Databricks dashboard cost
|
|
141
|
+
charts read token usage and cost correctly.
|
|
142
|
+
|
|
143
|
+
## [0.2.1] - 2026-06-04
|
|
144
|
+
### Fixed
|
|
145
|
+
- Chunk the Unity Catalog OTLP span export so payloads stay under the 10 MiB
|
|
146
|
+
ingest cap.
|
|
147
|
+
|
|
148
|
+
## [0.2.0] - 2026-06-02
|
|
149
|
+
### Changed
|
|
150
|
+
- Harden Unity Catalog configuration guards and move to experiment-id-only
|
|
151
|
+
configuration.
|
|
152
|
+
|
|
153
|
+
## [0.1.0] - 2026-06-01
|
|
154
|
+
### Added
|
|
155
|
+
- Initial release: shared LangGraph → Databricks MLflow tracer built on a custom
|
|
156
|
+
`BaseTracer` (not autolog), writing all trace fields and the cost rollup on the
|
|
157
|
+
**live** trace before the root span ends, with fail-fast configuration and an
|
|
158
|
+
autolog reduced-schema fallback. Per-model pricing comes from a vendored LiteLLM
|
|
159
|
+
catalog with overrides and a refresh script.
|
|
160
|
+
|
|
161
|
+
<!-- Historical links (0.1.0-0.4.0) use commit SHAs: those release tags were
|
|
162
|
+
reserved by the repo's immutable-releases policy and cannot be created. Restore
|
|
163
|
+
python-vX.Y.Z tag links if the names are ever reclaimed. 0.5.0's tag is created
|
|
164
|
+
out-of-band (its bump predates the release workflow); 0.5.1+ tags are minted by
|
|
165
|
+
release.yml on push to main. -->
|
|
166
|
+
[Unreleased]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v1.0.0...HEAD
|
|
167
|
+
[1.0.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.6.1...python-v1.0.0
|
|
168
|
+
[0.6.1]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.6.0...python-v0.6.1
|
|
169
|
+
[0.6.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/python-v0.5.0...python-v0.6.0
|
|
170
|
+
[0.5.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/99462dbc95c43f20085cae0bfd7a2dc0aab319b1...python-v0.5.0
|
|
171
|
+
[0.4.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/951a9299c4beffcdd23fc7442bd79b49e2d9b52f...99462dbc95c43f20085cae0bfd7a2dc0aab319b1
|
|
172
|
+
[0.3.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/1e4130acb98eb947f65302e060d6a017da0f2feb...951a9299c4beffcdd23fc7442bd79b49e2d9b52f
|
|
173
|
+
[0.2.2]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/1a868d88a6227a0528e5725c49b63848f0e03aa9...1e4130acb98eb947f65302e060d6a017da0f2feb
|
|
174
|
+
[0.2.1]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/03e34a320a8e90e046d574dab0e58cdfe58ab738...1a868d88a6227a0528e5725c49b63848f0e03aa9
|
|
175
|
+
[0.2.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/compare/83c62e28dbd04ef8e987689983e1e5a53e9d204f...03e34a320a8e90e046d574dab0e58cdfe58ab738
|
|
176
|
+
[0.1.0]: https://github.com/insightfactory-ai/if_s_langraph_mlflow_tracer/commit/83c62e28dbd04ef8e987689983e1e5a53e9d204f
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared LangGraph -> Databricks MLflow tracer (Python).
|
|
2
|
+
|
|
3
|
+
Public API. No auto-bootstrap at import (design "Patterns to NOT follow") —
|
|
4
|
+
consumers call ``configure_databricks_tracing`` explicitly and attach the
|
|
5
|
+
callbacks returned by ``get_tracing_callbacks``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .errors import DatabricksTracingConfigurationError
|
|
9
|
+
from .mlflow_utils import (
|
|
10
|
+
TracingConfiguration,
|
|
11
|
+
configure_databricks_tracing,
|
|
12
|
+
get_tracing_callbacks,
|
|
13
|
+
)
|
|
14
|
+
from .tracer import (
|
|
15
|
+
ContentPartContext,
|
|
16
|
+
ContentRefResolver,
|
|
17
|
+
DatabricksLangGraphTracer,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ContentPartContext",
|
|
22
|
+
"ContentRefResolver",
|
|
23
|
+
"DatabricksLangGraphTracer",
|
|
24
|
+
"DatabricksTracingConfigurationError",
|
|
25
|
+
"TracingConfiguration",
|
|
26
|
+
"configure_databricks_tracing",
|
|
27
|
+
"get_tracing_callbacks",
|
|
28
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Constant key names for the shared trace schema.
|
|
2
|
+
|
|
3
|
+
AUTO-GENERATED from ``schema/trace-schema.json`` — DO NOT EDIT BY HAND.
|
|
4
|
+
Regenerate with ``uv run python scripts/generate_keys.py``;
|
|
5
|
+
validate with ``uv run python scripts/generate_keys.py --check``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TraceTagKey:
|
|
12
|
+
SOURCE: Final[str] = "source"
|
|
13
|
+
COST_UNKNOWN_MODEL: Final[str] = "cost.unknown_model"
|
|
14
|
+
COST_BY_MODEL: Final[str] = "cost.by_model"
|
|
15
|
+
LANGGRAPH_RUN_ID: Final[str] = "langgraph.run_id"
|
|
16
|
+
LANGGRAPH_GRAPH_ID: Final[str] = "langgraph.graph_id"
|
|
17
|
+
LANGGRAPH_ENV: Final[str] = "langgraph.env"
|
|
18
|
+
LANGGRAPH_API_REVISION: Final[str] = "langgraph.api_revision"
|
|
19
|
+
LANGGRAPH_THREAD_ID: Final[str] = "langgraph.thread_id"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TraceMetadataKey:
|
|
23
|
+
TRACE_COST: Final[str] = "mlflow.trace.cost"
|
|
24
|
+
TRACE_SESSION: Final[str] = "mlflow.trace.session"
|
|
25
|
+
TRACE_USER: Final[str] = "mlflow.trace.user"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SpanAttrKey:
|
|
29
|
+
LLM_COST: Final[str] = "mlflow.llm.cost"
|
|
30
|
+
LLM_MODEL: Final[str] = "mlflow.llm.model"
|
|
31
|
+
LLM_PROVIDER: Final[str] = "mlflow.llm.provider"
|
|
32
|
+
TOKEN_USAGE: Final[str] = "mlflow.chat.tokenUsage"
|
|
33
|
+
INPUT_TOKENS_CACHED: Final[str] = "gen_ai.usage.input_tokens.cached"
|
|
34
|
+
INPUT_TOKENS_CACHE_CREATION: Final[str] = "gen_ai.usage.input_tokens.cache_creation"
|
|
35
|
+
OUTPUT_TOKENS_REASONING: Final[str] = "gen_ai.usage.output_tokens.reasoning"
|
|
36
|
+
INPUT_TOKENS_CACHED_COST: Final[str] = "gen_ai.usage.input_tokens.cached_cost"
|
|
37
|
+
INPUT_TOKENS_CACHE_CREATION_COST: Final[str] = "gen_ai.usage.input_tokens.cache_creation_cost"
|
|
38
|
+
LLM_MODEL_NAME: Final[str] = "llm.model_name"
|
|
39
|
+
LLM_MODEL_PROVIDER: Final[str] = "llm.model_provider"
|
|
40
|
+
LLM_PROMPT_TOKENS_COST: Final[str] = "llm.usage.prompt_tokens_cost"
|
|
41
|
+
LLM_COMPLETION_TOKENS_COST: Final[str] = "llm.usage.completion_tokens_cost"
|
|
42
|
+
LANGGRAPH_NODE: Final[str] = "langgraph.node"
|
|
43
|
+
LANGGRAPH_STEP: Final[str] = "langgraph.step"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Per-root cost accumulator.
|
|
2
|
+
|
|
3
|
+
MLflow aggregates ``mlflow.chat.tokenUsage`` span attributes itself, but it has
|
|
4
|
+
no cost aggregation — so the tracer tracks running cost totals here per root run
|
|
5
|
+
and flushes the reserved 3-key ``mlflow.trace.cost`` metadata on root end. Also
|
|
6
|
+
collects the set of unknown-model names for the ``cost.unknown_model`` trace tag.
|
|
7
|
+
|
|
8
|
+
Only the reserved 3-key totals roll up to the trace: the cache-read /
|
|
9
|
+
cache-creation cost split persists **per-span** as non-reserved attrs (Option B),
|
|
10
|
+
so it is not accumulated here.
|
|
11
|
+
|
|
12
|
+
A per-model breakdown (``by_model``) is *also* accumulated here for the
|
|
13
|
+
``cost.by_model`` trace tag: model identity is a span-level attribute and a
|
|
14
|
+
single trace mixes several models, so per-model cost/token totals don't exist at
|
|
15
|
+
the trace level otherwise. Unlike the reserved 3-key rollup, the per-model bucket
|
|
16
|
+
also carries **tokens** — MLflow's native token aggregation is whole-trace, not
|
|
17
|
+
per-model, so the tracer must accumulate them itself (issue #13).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ModelRollup:
|
|
25
|
+
"""Per-model cost + token totals for one root trace (the ``cost.by_model`` map value).
|
|
26
|
+
|
|
27
|
+
Cost fields reconcile with the reserved ``mlflow.trace.cost`` rollup; token
|
|
28
|
+
fields reconcile with the whole-trace ``mlflow.chat.tokenUsage``. A
|
|
29
|
+
resolved-but-unpriceable model still contributes tokens (with zero cost) so
|
|
30
|
+
the token split stays complete.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
input_cost: float = 0.0
|
|
34
|
+
output_cost: float = 0.0
|
|
35
|
+
total_cost: float = 0.0
|
|
36
|
+
input_tokens: int = 0
|
|
37
|
+
output_tokens: int = 0
|
|
38
|
+
cache_read_input_tokens: int = 0
|
|
39
|
+
cache_creation_input_tokens: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class TraceAggregate:
|
|
44
|
+
root_trace_id: str
|
|
45
|
+
input_cost: float = 0.0
|
|
46
|
+
output_cost: float = 0.0
|
|
47
|
+
total_cost: float = 0.0
|
|
48
|
+
unknown_models: set[str] = field(default_factory=set)
|
|
49
|
+
has_any_cost: bool = False
|
|
50
|
+
# Per-model cost/token breakdown, keyed by the resolved model identity
|
|
51
|
+
# (``mlflow.llm.model``); flushed as the ``cost.by_model`` trace tag.
|
|
52
|
+
by_model: dict[str, ModelRollup] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def model_bucket(self, model: str) -> ModelRollup:
|
|
55
|
+
"""Return the per-model rollup for ``model``, creating it on first use."""
|
|
56
|
+
bucket = self.by_model.get(model)
|
|
57
|
+
if bucket is None:
|
|
58
|
+
bucket = ModelRollup()
|
|
59
|
+
self.by_model[model] = bucket
|
|
60
|
+
return bucket
|