Skip to content

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

Closed

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): add complete adapter-function lifecycle observability #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.md — feat(telemetry): add complete adapter-function lifecycle observability #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

Activity

  1. self-assigned this
    on May 22, 2026
  2. added
    enhancementNew feature or request
    area/telemetryOTel spans, metrics, tracing, semconv
    area/backendsProvider-specific work: Ollama, HF, LiteLLM, OpenAI, Bedrock, vLLM
    area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Core
    on May 22, 2026
  3. planetf1 commented on Jun 11, 2026

    @planetf1
    ContributorAuthor

    This issue uses vocabulary that has been superseded by the agreed Epic #929 glossary. Please use the following terms in any new work related to this issue:

    Old term Preferred term
    intrinsic / intrinsic function adapter function
    activated-LoRA / Activated LoRA aLoRA
    Project Granite Switch Granite Switch
    cache rewrite (re: aLoRA) cache hit / cache miss

    The canonical definitions are now in the project glossary and were established as part of #1192. Python symbol names (IntrinsicAdapter, fetch_intrinsic_metadata, etc.) keep their current names until Phase 1 (#1136) and Phase 4 (#1144).

  4. added
    p1High: important bugs (workaround exists) or high-value core features. Do soon, not on fire.
    on Jun 18, 2026
  5. removed their assignment
    on Jun 24, 2026
  6. self-assigned this
    on Jul 20, 2026
  7. 16 remaining items

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

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