Repository navigation
feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142
Description
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
EmbeddedBindingto 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. SoEmbeddedBindinggets 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.pyemits the span — is now written down in
docs/docs/observability/tracing.md,
andtest/telemetry/test_tracing_import_boundary.pyenforces in CI that
nothing undermellea/backends/importsmellea.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 themellea-telemetryinternal 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_COMPLETEand
ADAPTER_FUNCTION_PHASE_COMPLETEhave 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-producedsection to
docs/docs/observability/tracing.mdand the new
test/telemetry/test_tracing_import_boundary.pyCI guard — was closed
unmerged on 2026-08-17. Neither exists onmain:tracing.mdhas 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.
Thedocs/dev/adapter_observability.mddeletion (#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 raiseNotImplementedError- 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
intest_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— moot; that filedocs/dev/adapter_observability.md
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.activatewithintrinsic.*attributes. That spelling has never existed in
the codebase — it originated in #1141 and propagated. The merged convention, from
#1140 / PR #1422, isadapter_functionfor the parent,adapter_function.<phase>
for children,mellea.adapter_function.*for attributes — matching the existing
mellea.adapter_function.invocations/.phase_duration/.parse_failuresmetrics in
mellea/telemetry/metrics.py. Metric labels are bare (name,phase,revision)
while span attributes carry themellea.*prefix — same values, different surface,
each following its own signal type's convention; don't harmonize the two. Do not
reintroduce theintrinsic.*names. #1466 owns the mechanism and should document the
emitted spans indocs/docs/observability/tracing.mdalongside the other span
families, now that #1483 has deleteddocs/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_activationimplemented forEmbeddedBinding - The inline
isinstance(adapter, EmbeddedIntrinsicAdapter)block atopenai.py:699is 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_controlsandset_request_adapterremoved, along with theopenai.pyoverride and their tests (noadapter_observability.mdentries to remove — the file no longer exists) - No
prepare/activate/deactivate/releasemethods added toEmbeddedBinding - OpenAI backend continues to support embedded adapters via the new binding
- Existing Granite Switch tests pass through the new binding
-
adapter_function.activatespan emitted withmellea.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) -
AdapterFunctionMetricsPlugincounters (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.mdupdated: Embedded construction example + backend × reality matrix -
AGENTS.md §14updated with normalised post-parse shape reference table -
ruff format,ruff check,mypyclean
Test plan
Unit tests (test/backends/test_adapters/test_embedded_binding.py) with mocked OpenAI backend:
test_apply_activation_sets_adapter_name— writeschat_template_kwargs["adapter_name"]test_apply_activation_removes_model_param— theapi_params.pop("model")behaviour; fails loudly if regressedtest_apply_activation_preserves_existing_chat_template_kwargs— does not clobber caller-supplied keystest_no_weights_verbs_on_embedded_binding— assertsprepare/activate/deactivate/releaseare absenttest_multi_call_isolation— activation from call N does not leak into call N+1test_activate_span_binding_type_is_embeddedtest_metrics_invocation_counter_increments_for_embedded
Removal gate:
grepforrender_controlsandset_request_adapteracrossmellea/,test/,docs/— no hits
Integration test (test/backends/test_adapters/test_embedded_integration.py, @pytest.mark.integration, @pytest.mark.openai):
OpenAIBackend × EmbeddedBinding × lora/aloraagainst 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
- Adapter API: one lifecycle, or two shapes? #1486 — design discussion that rescoped this issue
- refactor(intrinsics): resolve the adapter's output contract from the adapter, not a parallel argument (Epic #929 Phase 2) #1516 — the contract/reformatting half; independent axis, runs in parallel
- PR docs: design proposal for adapter lifecycle (#929) — review channel, do not merge #1080 §8.2, §9.2, §10
- PR feat: add embedded adapters (granite switch) to openai backend #881 — Reality B added to OpenAI backend; this binding is the structural home
- add support for granite-switch / embedded adapters on our huggingface backend #1018 — HF embedded adapters; sequenced after this issue; will reuse
EmbeddedBindingagainstLocalHFBackend
Activity
planetf1 commentedon Jun 11, 2026
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:
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).refactor(telemetry): record intrinsic phase duration in seconds
refactor(backends)!: narrow AdapterMixin verbs, centralize option res…
refactor(telemetry): record intrinsic phase duration in seconds
16 remaining items
fix(backends): address review findings on EmbeddedBinding.apply_activ…
fixup: leave io_contract bullet untouched (owned by generative-comput…
chore(adapters): split EmbeddedIntrinsicAdapter's io_contract/weights…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
docs: document EmbeddedBinding construction and weights-binding shapes (
fix(backends): address review findings on EmbeddedBinding.apply_activ…
fixup: leave io_contract bullet untouched (owned by generative-comput…
refactor(intrinsics): resolve the adapter's output contract from the …
docs: document EmbeddedBinding construction and weights-binding shapes (
fix(backends): address review findings on EmbeddedBinding.apply_activ…
fix(backends): route intrinsic generation through adapter_scope, fix …
feat(backends): EmbeddedBinding implements apply_activation (Granite …
fix(backends): fire adapter_function_invocation_complete for embedded…