Skip to content

refactor(backends)!: narrow AdapterMixin verbs, centralize option resolution, add IntrinsicMetricsPlugin skeleton#1422

Open
planetf1 wants to merge 13 commits into
generative-computing:mainfrom
planetf1:worktree-issue-1140
Open

refactor(backends)!: narrow AdapterMixin verbs, centralize option resolution, add IntrinsicMetricsPlugin skeleton#1422
planetf1 wants to merge 13 commits into
generative-computing:mainfrom
planetf1:worktree-issue-1140

Conversation

@planetf1

@planetf1 planetf1 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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 AdapterMixin verb 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)

  1. add_adapter contractadapter.py / huggingface.py / openai.py. The key design call, and the resolution of the concern raised in review: the base advertised Adapter | _AdapterCore but 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 clear TypeError. Please sanity-check this against the generic AdapterMixin[A] alternative (rationale in Details & notes).
  2. Breaking renames + silent list_adapters() shift — the ! surface. Confirm the migration table matches expectations; note list_adapters() changes meaning (registered vs loaded) without changing its signature.
  3. AC deviation (4 vs 7 verbs) — a call worth a reviewer's eye: the issue says four verbs, this ships seven, because Phase 1's 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.
  4. Metrics + hooks are a skeleton — no production caller fires them yet (that's 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), so review the contract/shape, not live behaviour.

How this fits Epic #929

The epic replaces ad-hoc adapter handling with an explicit lifecycle:

Key changes

  1. AdapterMixin → 7 verbs, split by responsibility:
    • Universal (abstract — every backend must implement): base_model_name, add_adapter, list_adapters.
    • Reality-specific (concrete default that raises 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, matching OpenAIBackend's existing contract.
  2. resolve_model_options (mellea/backends/_options.py, new) — centralises the backend_defaults < helper_defaults < call_options precedence merge previously duplicated in each backend's _simplify_and_merge. Behaviour-preserving; adopted by both backends and call_intrinsic.
  3. IntrinsicMetricsPlugin skeleton — 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 canonical adapter_function; the code symbols stay Intrinsic* — see Deviations. A further rename of these specific symbols, e.g. IntrinsicMetricsPluginAdapterFunctionMetricsPlugin, is under discussion in review and will follow as a separate commit once agreed.)

See docs/dev/adapter_observability.md for the full verb contract, option-resolution precedence, span-tree structure, MELLEA_TRACE_CONTENT content-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/, the mellea-examples repo, and mellea-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 uses DeprecationWarning shims elsewhere, e.g. Watsonx and create_bedrock_mantle_backend).

Backend (adapter reality) Old call New call
LocalHFBackend (LocalFile/PEFT) backend.load_adapter(name) backend.load_peft_adapter(name)
LocalHFBackend backend.unload_adapter(name) backend.unload_peft_adapter(name)
OpenAIBackend (Embedded/Switch) backend.load_adapter(name) backend.render_controls(name, active=True)
OpenAIBackend backend.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 descriptive NotImplementedError naming the backend — a deliberate improvement over a bare AttributeError. add_adapter now raises a clear TypeError when 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 two docs/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.

Process note: the migration mapping currently lives here and in the dev-internal docs/dev/adapter_observability.md. Where end-user breaking changes should surface (changelog, release notes) is an open question to settle separately — the repo's auto-generated changelog has no "Breaking Changes" category today. Tracked in #1436.

Details & notes

  • add_adapter contract (honesty). The mixin advertises the full Adapter | _AdapterCore union. These are two disjoint adapter hierarchies co-existing during the Epic Epic: Fix Adapter Function Lifecycle & Consistency in Mellea #929 old→new migration (the legacy abc.ABC Adapter/LocalHFAdapter on one side, the core dataclass Adapter/EmbeddedIntrinsicAdapter on the other — verified: neither is a subclass of the other). Each backend accepts that full union and raises a clear TypeError for the adapter realities it doesn't implement — mirroring how the reality-specific verbs raise NotImplementedError. This keeps the advertised contract truthful and removes the # type: ignore[override] the earlier narrowing required.
    • Why runtime rejection, not a generic 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 and resolve_adapter), and the eventual old→new Adapter hierarchy unification would likely rework it. We chose the smaller, pattern-consistent runtime-rejection fix here and flag AdapterMixin[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:

Other notes

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code was added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

Attribution

  • AI coding assistants used

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.

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

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.

@planetf1

Copy link
Copy Markdown
Contributor Author

CI on this PR is currently red at the Install Ollama step — this is not caused by anything in this change. The quality workflow on main pins OLLAMA_VERSION=0.32.2-rc0, and Ollama has since removed that release-candidate tag (promoted to the v0.32.2 release), so install.sh 404s and the step fails on every PR and merge-queue run.

Fix is up in #1425 (bumps the pin to 0.32.2; tracked by #1424). Once that merges, a re-run — or a rebase onto main — will take this PR's CI green, with no change needed here.

planetf1 added 6 commits July 22, 2026 14:45
…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>
@planetf1
planetf1 force-pushed the worktree-issue-1140 branch from 2428572 to d1a067b Compare July 22, 2026 13:45
@planetf1
planetf1 marked this pull request as ready for review July 22, 2026 14:16
@planetf1
planetf1 requested a review from a team as a code owner July 22, 2026 14:16
@planetf1
planetf1 requested review from ajbozarth, akihikokuroda, jakelorocco and markstur and removed request for akihikokuroda and markstur July 22, 2026 14:16
planetf1 added 3 commits July 22, 2026 15:39
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>
Comment on lines +401 to +406
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems like these "reality-specific" verbs shouldn't live at the AdapterMixin level but should live as functions on distinct subclasses?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread mellea/plugins/hooks/intrinsic.py Outdated
# 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"] = ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason the phase can be "" here? The payload above forces it's outcome literal to not be "".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

@ajbozarth

Copy link
Copy Markdown
Contributor

Didn't find time to review today, will take a look tomorrow

planetf1 added 3 commits July 23, 2026 12:43
…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 ajbozarth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread mellea/backends/huggingface.py
Comment thread mellea/backends/huggingface.py
Comment thread mellea/backends/adapters/adapter.py
Comment thread mellea/telemetry/metrics.py
Comment thread mellea/telemetry/metrics_plugins.py

@ajbozarth ajbozarth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 new record_intrinsic_* functions aren't re-exported from the package. All sibling record_* functions are in the package __all__; these are only in metrics.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 starts None and becomes non-None after its record_* 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):

  1. binding_type needs 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 on WeightsBinding), each emission site will hand-write the string and they'll drift. adapter_type (maps to the AdapterType enum) and revision (maps to IntrinsicsCatalogEntry.revision) are already grounded — binding_type is the one that isn't.
  2. 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 in test/plugins/test_hook_call_sites.py proving the hook actually fires with the expected payload.
  3. Justify or drop parse_failures. It's fully derivable from invocations{outcome="schema_error"}, so it's a redundant series unless a pre-aggregated schema-drift alarm is specifically wanted.
  4. Phase list is duplicated in four places — the payload Literal, the two record_* docstrings, and adapter_observability.md — with no enforcement they stay in sync. Worth noting the Literal is the source of truth when emission adds phases.

record_tool_call(tool_name, status)


class IntrinsicMetricsPlugin(Plugin, name="intrinsic_metrics", priority=57):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) use adapter_function now; Python symbols stay Intrinsic* 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 stay Intrinsic* 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 catches record_intrinsic_invocation, record_intrinsic_parse_failure, record_intrinsic_phase_duration (all public, in metrics.py.__all__), the HookType enum constants (INTRINSIC_INVOCATION_COMPLETE/INTRINSIC_PHASE_COMPLETE), and the hooks/__init__.py re-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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_COMPLETEADAPTER_FUNCTION_INVOCATION_COMPLETE
  • INTRINSIC_PHASE_COMPLETEADAPTER_FUNCTION_PHASE_COMPLETE

3. Public payload types (mellea/plugins/hooks/, defined in intrinsic.py, re-exported from hooks/__init__.py __all__):

  • IntrinsicInvocationCompletePayloadAdapterFunctionInvocationCompletePayload
  • IntrinsicPhaseCompletePayloadAdapterFunctionPhaseCompletePayload
  • module file hooks/intrinsic.pyhooks/adapter_function.py (and its imports in types.py / metrics_plugins.py)

4. Plugin class + handler methods (mellea/telemetry/metrics_plugins.py):

  • IntrinsicMetricsPluginAdapterFunctionMetricsPlugin (+ the entry in _METRICS_PLUGIN_CLASSES and the module-docstring bullet)
  • handler methods record_intrinsic_invocation / record_intrinsic_phaserecord_adapter_function_invocation / record_adapter_function_phase

5. Public record functions (mellea/telemetry/metrics.py, exported from telemetry/__init__.py __all__):

  • record_intrinsic_invocationrecord_adapter_function_invocation
  • record_intrinsic_phase_durationrecord_adapter_function_phase_duration
  • record_intrinsic_parse_failurerecord_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 in test/telemetry/conftest.py's reset_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.

Comment thread mellea/telemetry/metrics_plugins.py
Comment thread mellea/telemetry/metrics.py
Comment thread mellea/telemetry/metrics.py
Comment thread test/telemetry/test_intrinsic_metrics_plugin.py Outdated
- 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>
@ajbozarth

Copy link
Copy Markdown
Contributor

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2)

3 participants