refactor(backends)!: narrow AdapterMixin verbs, centralize option resolution, add IntrinsicMetricsPlugin skeleton#1422
Conversation
|
CI on this PR is currently red at the Install Ollama step — this is not caused by anything in this change. The Fix is up in #1425 (bumps the pin to |
…olution, add IntrinsicMetricsPlugin skeleton Epic generative-computing#929 Phase 2 (issue generative-computing#1140). Three changes shipped together: - AdapterMixin narrows to 7 reality-specific verbs: universal base_model_name/add_adapter/list_adapters, plus load_peft_adapter/ unload_peft_adapter (LocalFile/PEFT), render_controls (Embedded/Granite Switch), and set_request_adapter (ServerMediated, not yet implemented by any backend). list_adapters on LocalHFBackend now reports registered adapters rather than only loaded ones, matching OpenAIBackend's existing contract. Note: the issue's acceptance criteria state "exactly four verbs" three times; that's unsatisfiable now that Phase 1 (generative-computing#1269) added resolve_adapter(), which depends on base_model_name/add_adapter staying on the mixin. Flagging this AC conflict for a follow-up comment on generative-computing#1140. - resolve_model_options (mellea/backends/_options.py) centralizes the backend_defaults < helper_defaults < call_options merge previously duplicated in each backend's _simplify_and_merge, and call_intrinsic now routes its TEMPERATURE default through the same helper. - IntrinsicMetricsPlugin skeleton records three OTel metrics (invocations, phase_duration_ms, parse_failures) via two new hook types. No production call site fires these hooks yet; real wiring lands with generative-computing#1141/generative-computing#1142. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: Claude Code
Comment-and-docstring clarifications from the PR self-review; no behavioural change. - hook_system.md: update the aspirational adapter-hook table to name the renamed verbs (load_peft_adapter/unload_peft_adapter). - huggingface.py, openai.py: explain why add_adapter's override ignore is intentional (contravariant parameter narrowing under LSP). - metrics_plugins.py: note why the revision label is normalised to a string. - _util.py: document call_intrinsic's default TEMPERATURE of 0.0 and how to override it. - intrinsic.py: record why the error payload field stays typed Any and that the metrics plugin classifies on outcome rather than reading it. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Follow-up to a second review pass on this PR: - Export IntrinsicInvocationCompletePayload and IntrinsicPhaseCompletePayload from mellea.plugins.hooks, matching every other hook payload module so plugin authors can import them from the package like the rest. - Type IntrinsicInvocationCompletePayload.outcome as a Literal of its three valid values, so a typo is caught at construction rather than silently creating a new metric-label cardinality entry. phase keeps its str type: its "" default sits outside the valid set, so a Literal there needs a separate contract decision. - Note that self._model.load_adapter() in load_peft_adapter is PEFT's own PreTrainedModel method, not the renamed AdapterMixin verb. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Align the intrinsic phase-duration histogram with the base-unit convention the latency histograms already follow (mellea.llm.request.duration / .ttfb use seconds): rename mellea.intrinsic.phase_duration_ms -> mellea.intrinsic.phase_ duration, unit s, and convert the payload's milliseconds to seconds in the plugin — mirroring LatencyMetricsPlugin. Drops the unit from the metric name per OTel naming guidance. No released consumer: the metric has no production producer yet (wired in generative-computing#1141/generative-computing#1142). Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
The base add_adapter advertised `Adapter | _AdapterCore`, but each backend narrowed the parameter (with `# type: ignore[override]`) to a disjoint slice of that union — so a call that type-checks against AdapterMixin could fail at runtime, and LocalHF failed with a confusing `AttributeError: 'Adapter' object has no attribute 'backend'`. Both concrete backends now accept the full union (via a shared `AdapterInput` alias) and reject unsupported adapter realities at runtime with a clear TypeError — the same "reject unsupported reality" contract the reality-specific verbs use. This removes both override type-ignores and makes the advertised contract truthful. A fully type-safe alternative (generic AdapterMixin[A]) is noted in the PR for the eventual hierarchy-unification work. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Address non-blocking review follow-ups:
- Type IntrinsicPhaseCompletePayload.phase as a Literal so a typo cannot
silently create a new metric-label series ("" kept as the unset sentinel;
the field can't be made required without breaking dataclass default ordering).
- Reset the three cached intrinsic instruments in the telemetry conftest, so
metric tests don't leak instrument state bound to a previous MeterProvider.
Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
2428572 to
d1a067b
Compare
Close the two adapter_observability.md topics generative-computing#1140's acceptance criteria required but the first draft omitted: the span-tree structure (parent invoke span + per-phase children, aligned one-to-one with the phase_duration metric) and the MELLEA_TRACE_CONTENT content-capture gate (off by default; metadata always recorded, input/output content only when opted in). Both are documented as structure/contract — emission is wired with the Bindings (generative-computing#1141/generative-computing#1142) — and the span section defers concrete names to Mellea's existing tracing conventions. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…orter generative-computing#1140's test plan calls for a synthetic OTel exporter, but the plugin tests only mocked the record_* functions (verifying dispatch, not emission). Add an InMemoryMetricReader-based test — matching the pattern the sibling metrics-plugin tests use — that fires the plugin and asserts the invocations counter, parse_failures counter (schema_error path), and phase_duration histogram all emit with the correct labels and the ms->s conversion. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…nventions Two convention fixes to the observability doc: - Span tree: drop the invented `intrinsic.invoke`/`intrinsic.prepare` span names; spans follow the existing `start_*_span` helpers (operation-named, `gen_ai.*` where applicable) with Mellea fields under `mellea.intrinsic.*`, mirroring `mellea.action_type`. Documents the deliberate metric-labels-bare vs span-attributes-`mellea.*` split already used across Mellea. - Content capture: the gate is the existing `MELLEA_TRACES_CONTENT` (plural), not the `MELLEA_TRACE_CONTENT` named in generative-computing#1140 (which does not exist). It is already implemented and also honours OTEL_INSTRUMENTATION_GENAI_CAPTURE_ MESSAGE_CONTENT; the intrinsic spans reuse it rather than adding a new gate. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
| def load_peft_adapter(self, adapter_qualified_name: str) -> None: | ||
| """Load a previously registered PEFT adapter into the underlying model. | ||
|
|
||
| LocalFile/PEFT reality only (e.g. a locally hosted Hugging Face | ||
| model). The adapter must have been registered via ``add_adapter`` | ||
| before calling this method. |
There was a problem hiding this comment.
It seems like these "reality-specific" verbs shouldn't live at the AdapterMixin level but should live as functions on distinct subclasses?
There was a problem hiding this comment.
I'm actually okay with leaving it as is for now. I think we should see how users interact with these interfaces, but it may eventually make sense to add subclassing to the AdapterMixin type so that users get better type completion, etc...
There was a problem hiding this comment.
This was the main thing I wanted a view on - there's a note in the PR description ("Why not a generic AdapterMixin[A]?") with the tradeoff. The generic/subclassed version would give the better type completion you mention, but it's more invasive and the old/new Adapter hierarchy is still being unified, so I think we'd end up redoing it once that settles.
I could make the change here (and change it again later if needed), or open an issue and add it to the epic to do at the end. Or we leave it as is?
There was a problem hiding this comment.
I'm okay with postponing for now. I think we can see if the type completion / typical use cases are fine with this approach and if not, iterate.
| with self._generation_lock: | ||
| if adapter_name != "": | ||
| self.load_adapter(adapter_name) | ||
| self.load_peft_adapter(adapter_name) |
There was a problem hiding this comment.
This seems to lock us into the hf backend supporting only peft adapters. I wonder if we should return these back to being generic (ie load_adapter or activate_adapter) and then have each adapter subclass implement these functions if necessary?
For instance, if we want to support both embedded adapters and peft adapters, we need the implementation to live somewhere. I guess either the backend can do the if/else or the adapter can be aware of the backend. I suppose it's cleaner for the backend to make these decisions.
There was a problem hiding this comment.
Agreed, the backend making the decision is what's here. It's not locked to peft though - a backend can support both, so when we add embedded adapters to HF (#1018) it'd just implement render_controls alongside the peft verbs. The mixin only raises for the verbs a given backend doesn't implement.
| # series (the phase becomes a metric dimension). "" is the unset sentinel: the | ||
| # field can't be made required because the base payload's fields are all | ||
| # defaulted, and a non-defaulted field after them breaks dataclass ordering. | ||
| phase: Literal["", "prepare", "activate", "generate", "parse", "deactivate"] = "" |
There was a problem hiding this comment.
Is there a reason the phase can be "" here? The payload above forces it's outcome literal to not be "".
There was a problem hiding this comment.
Fair point - I only just added this in the previous commit, so no need to preserve the "". The default was unused anyway. Went through the other fields and came up with:
- phase - dropped the "", now required
- name (both) and duration_ms - same dead default, made required since a completion event always has them
- outcome - required too. "success" felt like a dangerous default, a forgotten emit would just look like it succeeded - and the caller always knows the real outcome anyway, so no default needed
- revision - left optional (None = genuinely unpinned), but the label now says "unpinned" rather than "unknown" so it's not muddled with a revision we couldn't work out
- error - left as None, since that's just "no error", and it's the one default that's actually used
|
Didn't find time to review today, will take a look tomorrow |
…pinned revisions Address review feedback on the intrinsic hook payloads. The phase Literal carried an unused "" default behind a wrong justification (I claimed a required field couldn't follow the defaulted ones — but these are pydantic models, where it can). Reviewing both payloads for the same smell: - phase: drop "" from the Literal and make it required - name (both payloads) and duration_ms: same dead default, made required — a completion event always has them - outcome: made required, dropping the "success" default — a forgotten emit would silently record success, and the emit site always knows the outcome - revision: kept optional (None = genuinely unpinned), but the metric label now reads "unpinned" rather than "unknown", so it isn't conflated with a revision that couldn't be determined Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
get_type_hints(OpenAIBackend.add_adapter) raised NameError: the AdapterInput
alias was a forward-ref string ("Adapter | _AdapterCore"), but openai.py imports
only AdapterInput, not the underlying names, so resolving the annotation failed.
Both names are already in scope where the alias is defined, so the quotes served
no purpose. Dropping them makes the alias a real union object that resolves
without the underlying names in the caller's namespace.
Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Rename the user-facing metric names (and the documented span-attribute keys) from mellea.intrinsic.* to mellea.adapter_function.*, and fix the one prose use of "intrinsic" in the observability doc. The glossary makes "adapter function" the canonical term for user-facing surfaces (metric/label/span names, prose) while keeping the Python symbol `Intrinsic` as the "current implementation name" — so the code identifiers (IntrinsicMetricsPlugin, the payloads, the hook module, record_intrinsic_*) deliberately stay as they are, matching the existing call_intrinsic / Intrinsic / components/intrinsic code. A comment at the metric definitions records the prose-vs-code split. This deviates from generative-computing#1140, which specified mellea.intrinsic.*; done now because the metrics have no producer or dashboard yet, so the names cost nothing to get right, and there is no separate issue tracking the terminology alignment. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
ajbozarth
left a comment
There was a problem hiding this comment.
Some feedback from Claude.
Action items inline. I'll do a separate deeper pass on the metrics/hooks side — the telemetry notes below are shape-level only for now.
ajbozarth
left a comment
There was a problem hiding this comment.
Follow-up from my telemetry deep dive — requesting changes. The public metric/plugin naming (inline below) is the highest-stakes item, since renaming after merge breaks the API. Also a couple of missing-test items inline.
Two more in-this-PR items that land on files the diff doesn't touch (so no inline anchor):
mellea/telemetry/__init__.py— the three newrecord_intrinsic_*functions aren't re-exported from the package. All siblingrecord_*functions are in the package__all__; these are only inmetrics.py's__all__. Add them — the emission wiring will likely import them.test/telemetry/test_metrics.py::test_metric_instruments_lazy_initialization— asserts every instrument global startsNoneand becomes non-Noneafter itsrecord_*call, but the three new_intrinsic_*globals aren't in it. Extend it to cover them, matching the pattern for every other instrument.
Follow-up items — out of scope for this PR, should become acceptance criteria on the emission-wiring follow-ups (#1141 / #1142):
binding_typeneeds a canonical source. The label values ("local_file"/"embedded"/"server_mediated") currently exist only in docstrings and the payload's"unknown"default. Without a single source of truth (e.g. a property onWeightsBinding), each emission site will hand-write the string and they'll drift.adapter_type(maps to theAdapterTypeenum) andrevision(maps toIntrinsicsCatalogEntry.revision) are already grounded —binding_typeis the one that isn't.- End-to-end hook-firing test. The unit tests drive the plugin handlers directly; none go through
invoke_hook. The follow-up that adds emission should add the call-site test intest/plugins/test_hook_call_sites.pyproving the hook actually fires with the expected payload. - Justify or drop
parse_failures. It's fully derivable frominvocations{outcome="schema_error"}, so it's a redundant series unless a pre-aggregated schema-drift alarm is specifically wanted. - Phase list is duplicated in four places — the payload
Literal, the tworecord_*docstrings, andadapter_observability.md— with no enforcement they stay in sync. Worth noting theLiteralis the source of truth when emission adds phases.
| record_tool_call(tool_name, status) | ||
|
|
||
|
|
||
| class IntrinsicMetricsPlugin(Plugin, name="intrinsic_metrics", priority=57): |
There was a problem hiding this comment.
Telemetry naming should move off the deprecated Intrinsic* term. Keeping the hooks as intrinsic_* is defensible if they'll be emitted from functions still named that (call_intrinsic etc.) — matching the emitting code. But that reasoning doesn't extend to the telemetry surface, and this is public API: IntrinsicMetricsPlugin, the registration name "intrinsic_metrics", and the IntrinsicInvocationCompletePayload / IntrinsicPhaseCompletePayload payload types should be AdapterFunction* / "adapter_function_metrics". The metric/label names already made this move (mellea.adapter_function.*); the public Python symbols should follow. Renaming a public plugin/payload after consumers depend on it is a breaking change — cheapest to fix now.
There was a problem hiding this comment.
Agreed, and after digging into the glossary more carefully, here's the fuller picture — and the scope should be a bit wider than either of us first proposed.
- The actual documented policy (
docs/docs/reference/glossary.md) is: string surfaces (metric names, labels, prose) useadapter_functionnow; Python symbols stayIntrinsic*until a coordinated, shim-backed rename in a later Epic Epic: Fix Adapter Function Lifecycle & Consistency in Mellea #929 phase. So "new code doesn't need to follow the old-symbol rule" isn't really the right justification — by that policy, these symbols would normally stayIntrinsic*too. - The justification that does hold: none of this has shipped yet. Renaming now costs nothing and avoids ever releasing the deprecated name or needing a future shim for it — that's a narrower, cleaner reason than "it's new code," and it only applies because nothing depends on these names today.
- Scope needs to be complete, not partial. Beyond
IntrinsicMetricsPlugin/"intrinsic_metrics"/the two payload types, this also catchesrecord_intrinsic_invocation,record_intrinsic_parse_failure,record_intrinsic_phase_duration(all public, inmetrics.py.__all__), theHookTypeenum constants (INTRINSIC_INVOCATION_COMPLETE/INTRINSIC_PHASE_COMPLETE), and thehooks/__init__.pyre-exports. Renaming the plugin but leaving these would just move the inconsistency rather than fix it. - Hook name strings too — deciding now rather than deferring to feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() + OTel spans (Epic #929 Phase 2) #1141/feat(backends): EmbeddedBinding implements verbs (Granite Switch path) + OTel spans (Epic #929 Phase 2) #1142. There's no live emitter either way, so there's nothing to defer for, and leaving it open just risks a different author defaulting back to
intrinsic_*out of habit.
Will do the full rename across all of the above once we're agreed on the approach.
There was a problem hiding this comment.
Agreed — let's do the full rename, all in this PR since it's all net-new and nothing's shipped. Here's the complete breakdown I'd expect so we're aligned on scope before you start.
Convention: Intrinsic* → AdapterFunction* for symbols, intrinsic_* → adapter_function_* for strings. This matches the metric names that already went that way (mellea.adapter_function.*).
1. Hook string values (the wire contract — the reason to do this now):
"intrinsic_invocation_complete"→"adapter_function_invocation_complete""intrinsic_phase_complete"→"adapter_function_phase_complete"- plugin registration name
"intrinsic_metrics"→"adapter_function_metrics"
2. HookType enum constants (mellea/plugins/types.py, both the definitions at 74-75 and the registry uses at 184/188):
INTRINSIC_INVOCATION_COMPLETE→ADAPTER_FUNCTION_INVOCATION_COMPLETEINTRINSIC_PHASE_COMPLETE→ADAPTER_FUNCTION_PHASE_COMPLETE
3. Public payload types (mellea/plugins/hooks/, defined in intrinsic.py, re-exported from hooks/__init__.py __all__):
IntrinsicInvocationCompletePayload→AdapterFunctionInvocationCompletePayloadIntrinsicPhaseCompletePayload→AdapterFunctionPhaseCompletePayload- module file
hooks/intrinsic.py→hooks/adapter_function.py(and its imports intypes.py/metrics_plugins.py)
4. Plugin class + handler methods (mellea/telemetry/metrics_plugins.py):
IntrinsicMetricsPlugin→AdapterFunctionMetricsPlugin(+ the entry in_METRICS_PLUGIN_CLASSESand the module-docstring bullet)- handler methods
record_intrinsic_invocation/record_intrinsic_phase→record_adapter_function_invocation/record_adapter_function_phase
5. Public record functions (mellea/telemetry/metrics.py, exported from telemetry/__init__.py __all__):
record_intrinsic_invocation→record_adapter_function_invocationrecord_intrinsic_phase_duration→record_adapter_function_phase_durationrecord_intrinsic_parse_failure→record_adapter_function_parse_failure
6. Private module globals + accessors (metrics.py, consistency only — not public):
_intrinsic_*_counter/_histogram→_adapter_function_*(+ the matching_get_*accessors, and the resets intest/telemetry/conftest.py'sreset_metrics_state)
Downstream: the three telemetry test files (test_metrics.py, test_metrics_intrinsic.py, test_metrics_plugins.py) update to the new names.
Explicitly out of scope (stays Intrinsic*, owned by the coordinated shim-backed rename in #1136): the Intrinsic class, IntrinsicAdapter, components/intrinsic/, call_intrinsic. Also unchanged: the mellea.adapter_function.* metric name strings (already correct).
One knock-on we're accepting: the telemetry surface becomes AdapterFunction* while the emitter code stays intrinsic-named until #1136 — a short-lived, self-correcting mismatch, which I'm fine with. If that all matches your plan, go ahead.
- Add missing TypeError test for LocalHFBackend.add_adapter (mirrors the existing OpenAI equivalent) - Re-export AdapterInput from adapters/__init__.py - Register an explicit View/bucket boundaries for the phase_duration histogram, matching the other duration histograms - Re-export the three record_intrinsic_* functions from telemetry/__init__.py - Cover the new intrinsic instruments in the lazy-initialization test - Split test_intrinsic_metrics_plugin.py into the established per-file convention: mocked-dispatch tests in test_metrics_plugins.py, emission tests in a new test_metrics_intrinsic.py - Link adapter_observability.md from the naming-split comment in metrics.py Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
|
@planetf1 thanks for the fixes, I resolved all the items you fixed/gave good pushback on and left a detailed plan comment on the naming item you left open. This LGTM now pending that naming refactor for the telemetry surface |
Pull Request
Issue
Fixes #1140
Summary
What this unlocks: it turns Mellea's ad-hoc, per-backend adapter handling into a uniform, reality-aware lifecycle — the clean verb surface, shared option-merge, and observability contract that the upcoming Binding types (#1141/#1142) plug into. This is the Phase 2 seam that makes the rest of Epic #929's first-class adapter/intrinsic support buildable.
What it does (Epic #929, Phase 2): narrows the
AdapterMixinverb surface so each backend exposes only the adapter operations that are real for its deployment ("adapter reality"), centralises the model-option merge shared across backends, and adds an observability skeleton (metrics + hooks) for adapter-function invocations. There is no behavioural change beyond the declared breaking renames, and the metrics skeleton has no production callers yet.What to look at (review focus)
add_adaptercontract —adapter.py/huggingface.py/openai.py. The key design call, and the resolution of the concern raised in review: the base advertisedAdapter | _AdapterCorebut each backend narrowed it, so a statically valid call could fail at runtime. Fix = accept the full union, reject unsupported realities at runtime with a clearTypeError. Please sanity-check this against the genericAdapterMixin[A]alternative (rationale in Details & notes).list_adapters()shift — the!surface. Confirm the migration table matches expectations; notelist_adapters()changes meaning (registered vs loaded) without changing its signature.resolve_adapter()depends on two of the universal verbs staying on the mixin. See Deviations from refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2) #1140 below for the full list of intentional departures.How this fits Epic #929
The epic replaces ad-hoc adapter handling with an explicit lifecycle:
call_intrinsicrewrite, and the RAG intrinsic migration.render_controls(name, active)will backEmbeddedBinding.activate()/deactivate()(feat(backends): EmbeddedBinding implements verbs (Granite Switch path) + OTel spans (Epic #929 Phase 2) #1142), andload_peft_adapter/unload_peft_adapterback the LocalFile/PEFT binding (feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() + OTel spans (Epic #929 Phase 2) #1141).Key changes
AdapterMixin→ 7 verbs, split by responsibility:base_model_name,add_adapter,list_adapters.NotImplementedError; only the relevant backend overrides):load_peft_adapter/unload_peft_adapter(LocalFile/PEFT),render_controls(name, active)(Embedded/Granite Switch),set_request_adapter(ServerMediated — not yet implemented by any backend).LocalHFBackend.list_adapters()now reports registered adapters rather than only loaded ones, matchingOpenAIBackend's existing contract.resolve_model_options(mellea/backends/_options.py, new) — centralises thebackend_defaults < helper_defaults < call_optionsprecedence merge previously duplicated in each backend's_simplify_and_merge. Behaviour-preserving; adopted by both backends andcall_intrinsic.IntrinsicMetricsPluginskeleton — three OTel metrics (mellea.adapter_function.invocations,mellea.adapter_function.phase_duration,mellea.adapter_function.parse_failures) via two new hook types. Unit-tested against synthetic payloads plus an in-memory OTel exporter; no production call site fires these hooks yet (wired in feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() + OTel spans (Epic #929 Phase 2) #1141/feat(backends): EmbeddedBinding implements verbs (Granite Switch path) + OTel spans (Epic #929 Phase 2) #1142). (Metric names use the glossary's canonicaladapter_function; the code symbols stayIntrinsic*— see Deviations. A further rename of these specific symbols, e.g.IntrinsicMetricsPlugin→AdapterFunctionMetricsPlugin, is under discussion in review and will follow as a separate commit once agreed.)See
docs/dev/adapter_observability.mdfor the full verb contract, option-resolution precedence, span-tree structure,MELLEA_TRACE_CONTENTcontent-capture gate, and metric/label reference.Breaking changes (
refactor(backends)!)Removing the old verb names outright — rather than keeping deprecated aliases — is a deliberate design choice: no first-party code calls them (verified across the core repo,
docs/examples/, themellea-examplesrepo, andmellea-website), and aliases would re-introduce the reality-agnostic names this narrowing is meant to retire. Whether backend-API breaks should follow a formal deprecation window in future is raised for reviewer discussion (Mellea usesDeprecationWarningshims elsewhere, e.g. Watsonx andcreate_bedrock_mantle_backend).LocalHFBackend(LocalFile/PEFT)backend.load_adapter(name)backend.load_peft_adapter(name)LocalHFBackendbackend.unload_adapter(name)backend.unload_peft_adapter(name)OpenAIBackend(Embedded/Switch)backend.load_adapter(name)backend.render_controls(name, active=True)OpenAIBackendbackend.unload_adapter(name)backend.render_controls(name, active=False)A removed verb now raises
AttributeError. A reality-specific verb called on a backend whose reality doesn't support it raises a descriptiveNotImplementedErrornaming the backend — a deliberate improvement over a bareAttributeError.add_adapternow raises a clearTypeErrorwhen handed an adapter type the backend doesn't support (previously a mismatched type failed less clearly) — a runtime-contract tightening, not a new restriction.list_adapters()changes silently: same signature (() -> list[str]), but it now returns registered rather than only loaded adapters. First-party callers are unaffected (the twodocs/examples/uses are "is it registered?" guards the new meaning makes more correct); external callers relying on the old "loaded" meaning should review their usage.Details & notes
add_adaptercontract (honesty). The mixin advertises the fullAdapter | _AdapterCoreunion. These are two disjoint adapter hierarchies co-existing during the Epic Epic: Fix Adapter Function Lifecycle & Consistency in Mellea #929 old→new migration (the legacyabc.ABCAdapter/LocalHFAdapteron one side, the core dataclassAdapter/EmbeddedIntrinsicAdapteron the other — verified: neither is a subclass of the other). Each backend accepts that full union and raises a clearTypeErrorfor the adapter realities it doesn't implement — mirroring how the reality-specific verbs raiseNotImplementedError. This keeps the advertised contract truthful and removes the# type: ignore[override]the earlier narrowing required.AdapterMixin[A]? A generic mixin parameterised per backend would give compile-time safety and also drop the type-ignores, but it's more invasive (touches class definitions andresolve_adapter), and the eventual old→newAdapterhierarchy unification would likely rework it. We chose the smaller, pattern-consistent runtime-rejection fix here and flagAdapterMixin[A]as the type-safe direction for that later hierarchy-consolidation work.Deviations from #1140 (all deliberate)
Every difference from the issue as written is intentional and traceable:
resolve_adapter(), which depends onbase_model_name/add_adapterstaying on the mixin. Corrected in a comment on refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2) #1140 and documented inadapter_observability.md.mellea/telemetry/metrics_plugins.py, not the issue'smellea/core/plugins/intrinsic_metrics.py— co-located with the sibling metrics plugins (TokenMetricsPlugin,LatencyMetricsPlugin, …) whose registration pattern it mirrors.mellea.adapter_function.*(e.g.…phase_duration, units), not the issue'smellea.intrinsic.*/phase_duration_ms. Two deliberate alignments: (a) the glossary makes "adapter function" the canonical term for user-facing surfaces (metric/label/span names, prose), while the Python symbols stayIntrinsic*— the glossary's "current implementation name" — matching the existingcall_intrinsic/Intrinsic/components/intrinsiccode; (b) durations use the OTel base units, matchingmellea.llm.request.duration/ttfb. Done now because the metrics have no producer/dashboard yet and no separate issue tracks the terminology. The payload still carries ms and converts at the recording boundary. (Under review discussion: whether theIntrinsic*code symbols introduced in this PR specifically — since they're unreleased — should be renamed toAdapterFunction*now rather than waiting for the coordinated future-phase rename.)MELLEA_TRACES_CONTENT(plural, already implemented), not theMELLEA_TRACE_CONTENTnamed in refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2) #1140's AC — that variable doesn't exist. The intrinsic spans reuse the existing gate (which also honoursOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT) rather than introducing a new one.start_*_spanhelpers,mellea.*attributes) rather than a bespoke scheme; the doc documents the structure, emission lands with feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() + OTel spans (Epic #929 Phase 2) #1141/feat(backends): EmbeddedBinding implements verbs (Granite Switch path) + OTel spans (Epic #929 Phase 2) #1142.Other notes
render_controls(name, active: bool)signature is a design judgement (the issue names the verb, not its shape); expected to map onto feat(backends): EmbeddedBinding implements verbs (Granite Switch path) + OTel spans (Epic #929 Phase 2) #1142'sactivate()/deactivate().Testing
Attribution
Adding a new component, requirement, sampling strategy, or tool?
If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.
NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.