Skip to content

feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142

Description

@planetf1

Parent epic: #929
Design proposal: PR #1080 §8.2, §9.2, §10
Design discussion: #1486 (rescoped this issue — see the note below)
Phase: 2 (Wave 4 — start after 2.1 merges, in parallel with 2.2)
Depends on: 0.1, 2.1
Blocks: 4.1


🔄 Rescoped 2026-08-10 following discussion #1486.

This issue previously asked EmbeddedBinding to implement the four weights verbs
(prepare/activate/deactivate/release). It no longer does. The conclusion in
#1486, agreed by @psschwei and @jakelorocco, is that an embedded adapter has no weights
lifecycle to manage — it is already in the model, and turning it on is a field on the
outgoing request. So EmbeddedBinding gets one method, apply_activation, not four
verbs.

The two dead activation methods that the old scope was built around
(render_controls, set_request_adapter) are removed here — see "Removals" below.

The contract/reformatting half of #1486 is a separate axis and is tracked in #1516;
the two are independent and can proceed in parallel.

⚠️ Update 2026-08-11: #1464's doc/CI half has landed; span emission still awaits #1466.

The rule — library code fires a hook, a plugin in
mellea/telemetry/tracing_plugins.py emits the span — is now written down in
docs/docs/observability/tracing.md,
and test/telemetry/test_tracing_import_boundary.py enforces in CI that
nothing under mellea/backends/ imports mellea.telemetry.tracing
directly. docs/dev/adapter_observability.md — the stale note that taught
#1141 the wrong pattern, leading to the PR #1454 revert — has been deleted
entirely (#1483); its still-current content moved into code docstrings, the
published docs, and the mellea-telemetry internal reference. Every
reference to that file below is stale as a result — see the inline notes.

There is still a structural blocker: ADAPTER_FUNCTION_INVOCATION_COMPLETE and
ADAPTER_FUNCTION_PHASE_COMPLETE have no pre/start sibling, so no plugin can
open these spans until #1466 adds the start hooks.

Practical effect: the implementation half of this issue remains
unblocked and can proceed. The span naming below is now settled (see the
2026-08-11 note under "OTel spans") — what's still missing is the mechanism,
which is #1466's job. Do not implement the span criteria as written until
#1466 adds the missing start hooks.

⚠️ Correction, 2026-08-18: the doc/CI citations two paragraphs up are dead.
PR #1545 — which would have added the #how-spans-are-produced section to
docs/docs/observability/tracing.md and the new
test/telemetry/test_tracing_import_boundary.py CI guard — was closed
unmerged
on 2026-08-17. Neither exists on main: tracing.md has no such
section and doesn't mention adapters, and the test file isn't in the repo.
The epic's own tracker (#929) already records #1464 as intentionally closed
without that doc/CI addition, since the underlying source guidance was judged
sufficient — this issue's 2026-08-11 note was simply never updated to match.
The docs/dev/adapter_observability.md deletion (#1483) is real and
unaffected by this correction. Practical effect is unchanged: the
implementation half of this issue is still unblocked; there is just no
written doc/CI enforcement of the hook/plugin rule to point to.

Problem

Activating an embedded adapter has no home in the codebase, so it was written inline into a
backend. mellea/backends/openai.py:699 carries the whole of it:

if isinstance(adapter, EmbeddedIntrinsicAdapter):
    chat_template_kwargs = extra_body.pop("chat_template_kwargs", {}) or {}
    chat_template_kwargs["adapter_name"] = action.intrinsic_name
    extra_body["chat_template_kwargs"] = chat_template_kwargs
    # The rewriter config may set `model` to the adapter name, but
    # for embedded adapters the actual model is self._model_id.
    api_params.pop("model", None)

Any new backend supporting embedded adapters has to copy that block — including the
non-obvious api_params.pop("model", None) — or embedded adapters silently fail to
activate. EmbeddedBinding is meanwhile a stub raising NotImplementedError.

Reality B (Granite Switch's embedded adapters; today's EmbeddedIntrinsicAdapter, used by
the OpenAI backend per PR #881) needs that block to become a method on the binding.

Agreed design

One method (Reality B — adapters are part of the base-model weights, no separate file)

apply_activation(request, identity) — edits the outgoing request so the model uses the
named adapter. For Granite Switch that is the chat_template_kwargs["adapter_name"] write
plus the model removal currently inlined at openai.py:699.

There is no prepare (nothing to download), no release (nothing to unload), and no
activate/deactivate pair (nothing is toggled — each request either carries the
activation field or does not). Activation varies by deployment, which is why it lives on
the binding; request reformatting varies by capability and comes from the adapter's
io_contract instead (#1516).

Currently only the OpenAI backend supports Reality B (per PR #881). HF backend will gain
support via #1018, which is the acceptance test for this shape: the same capability on a
different deployment should need only a binding, with no reformatting written twice.

Removals

render_controls and set_request_adapter were the previous attempt at a home for
activation. Neither does anything, and what actually activates an embedded adapter today is
the inline block above, bypassing both. apply_activation replaces them. Remove:

  • AdapterMixin.render_controls (mellea/backends/adapters/adapter.py:432) and
    AdapterMixin.set_request_adapter (adapter.py:454) — both raise NotImplementedError
  • The one override, an explicit no-op that only logs (mellea/backends/openai.py:278)
  • Their tests — test/backends/test_adapters/test_adapter_mixin.py (in the verb list and
    in test_openai_backend_overrides_only_render_controls,
    test_set_request_adapter_has_no_implementation) and
    test/backends/test_adapters/test_embedded_adapter.py::test_render_controls_is_noop
  • Their entries in docs/dev/adapter_observability.md — moot; that file
    was deleted entirely by docs: delete docs/dev/, migrate current content, close #1464's telemetry-doc gap #1483, so there's nothing left to strike from it

load_peft_adapter and unload_peft_adapter are unaffected and stay as they are.

OTel spans

Mechanism pending #1466 — see the note at the top. The span shape below
is what we want; how it is produced is what changes. Note the span set shrinks
with the scope — there is no prepare/deactivate to instrument.
Child spans under the adapter_function parent (set up in 2.2):

  • adapter_function.activate — attributes: mellea.adapter_function.name,
    mellea.adapter_function.binding_type="embedded", mellea.adapter_function.source
    (base model identifier), and the name of the activation field written into the request

Naming, corrected 2026-08-11. This section previously said intrinsic.call /
intrinsic.activate with intrinsic.* attributes. That spelling has never existed in
the codebase — it originated in #1141 and propagated. The merged convention, from
#1140 / PR #1422, is adapter_function for the parent, adapter_function.<phase>
for children, mellea.adapter_function.* for attributes — matching the existing
mellea.adapter_function.invocations/.phase_duration/.parse_failures metrics in
mellea/telemetry/metrics.py. Metric labels are bare (name, phase, revision)
while span attributes carry the mellea.* prefix — same values, different surface,
each following its own signal type's convention; don't harmonize the two. Do not
reintroduce the intrinsic.* names. #1466 owns the mechanism and should document the
emitted spans in docs/docs/observability/tracing.md alongside the other span
families, now that #1483 has deleted docs/dev/adapter_observability.md.

Docs updates (ship with this PR)

  • docs/docs/advanced/intrinsics.md — add Embedded construction example: Adapter(weights=EmbeddedBinding.from_base_model(backend)); note which backends support which bindings (backend × reality matrix)
  • AGENTS.md §14 — update with normalised post-parse shape reference table for both bindings

Out of scope

HF backend embedded adapters (#1018), other realities (2.2, 3.1), the output-contract
resolution change (#1516).

Acceptance criteria

  • apply_activation implemented for EmbeddedBinding
  • The inline isinstance(adapter, EmbeddedIntrinsicAdapter) block at openai.py:699 is gone; the OpenAI backend activates through the binding
  • The api_params.pop("model", None) behaviour is preserved by the binding, with a test that fails if it is dropped
  • render_controls and set_request_adapter removed, along with the openai.py override and their tests (no adapter_observability.md entries to remove — the file no longer exists)
  • No prepare/activate/deactivate/release methods added to EmbeddedBinding
  • OpenAI backend continues to support embedded adapters via the new binding
  • Existing Granite Switch tests pass through the new binding
  • adapter_function.activate span emitted with mellea.adapter_function.binding_type="embedded"emitted from a tracing plugin, not from library code; blocked on feat(telemetry): emit adapter-function spans from a tracing plugin (Epic #929 Phase 2) #1466 (naming settled — see the 2026-08-11 note above)
  • AdapterFunctionMetricsPlugin counters (mellea.adapter_function.invocations, .parse_failures, .phase_duration) increment correctly for Embedded calls
  • Embedded-specific span attributes documented in docs/docs/observability/tracing.mdfeat(telemetry): emit adapter-function spans from a tracing plugin (Epic #929 Phase 2) #1466's responsibility, once it implements emission
  • docs/docs/advanced/intrinsics.md updated: Embedded construction example + backend × reality matrix
  • AGENTS.md §14 updated with normalised post-parse shape reference table
  • ruff format, ruff check, mypy clean

Test plan

Unit tests (test/backends/test_adapters/test_embedded_binding.py) with mocked OpenAI backend:

  • test_apply_activation_sets_adapter_name — writes chat_template_kwargs["adapter_name"]
  • test_apply_activation_removes_model_param — the api_params.pop("model") behaviour; fails loudly if regressed
  • test_apply_activation_preserves_existing_chat_template_kwargs — does not clobber caller-supplied keys
  • test_no_weights_verbs_on_embedded_binding — asserts prepare/activate/deactivate/release are absent
  • test_multi_call_isolation — activation from call N does not leak into call N+1
  • test_activate_span_binding_type_is_embedded
  • test_metrics_invocation_counter_increments_for_embedded

Removal gate:

  • grep for render_controls and set_request_adapter across mellea/, test/, docs/ — no hits

Integration test (test/backends/test_adapters/test_embedded_integration.py, @pytest.mark.integration, @pytest.mark.openai):

  • OpenAIBackend × EmbeddedBinding × lora/alora against Granite Switch — activation through the binding; existing Granite Switch tests pass

Docs validation:
npx markdownlint-cli2 "docs/docs/advanced/intrinsics.md" "docs/docs/observability/tracing.md" "AGENTS.md"

Breaking changes

None for end users. Replaces internal stubs. OpenAI-backend Granite Switch behaviour
preserved. render_controls and set_request_adapter are removed, but neither has a
working implementation or any caller, so nothing can depend on them.

References

Metadata

Metadata

Assignees

Labels

area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Corearea/backendsProvider-specific work: Ollama, HF, LiteLLM, OpenAI, Bedrock, vLLMarea/telemetryOTel spans, metrics, tracing, semconvenhancementNew feature or requestp1High: important bugs (workaround exists) or high-value core features. Do soon, not on fire.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions