Tutorial: Adding a Custom Adapter Function
This tutorial walks through registering a custom, non-catalog adapter
function by composing an Adapter directly — the replacement for the
deprecated CustomIntrinsicAdapter shim (Epic #929, issue #1144).
By the end you will have covered:
- Composing an
Adapterfrom anIdentity, an output contract, and aLocalFileBinding - Why a custom adapter needs an explicit
revision - Registering it with a backend and invoking it through
Intrinsic - Validating output with
ALoraRequirementagainst a custom adapter name
Prerequisites: pip install "mellea[hf]", a trained LoRA/aLoRA adapter
uploaded to Hugging Face Hub with
m alora upload ... --intrinsic --io-yaml ./io.yaml (see
LoRA and aLoRA adapters for
training and uploading), and a GPU or Apple Silicon Mac.
Step 1: Compose the adapter
Mellea's built-in adapter functions (answerability, requirement-check,
etc.) are looked up from a catalog that supplies their repo_id and
revision. A custom adapter has no catalog entry, so you supply those
fields yourself:
Repo layout matters:
LocalFileBindingonly finds weights at<name>/<base-model>/<lora|alora>/insiderepo_id— the same layout Mellea's own built-in adapter functions use, and the onem alora upload --intrinsic --io-yaml ./io.yamlproduces. The plainm alora upload(no--intrinsic) pushes weights to the repo root instead, whichLocalFileBindingcannot find — if you used that command, re-upload with--intrinsicbefore continuing.
# Requires: mellea[hf]
from mellea.backends.adapters import Adapter, Identity, LocalFileBinding, get_io_contract
from mellea.backends.adapters.catalog import AdapterType
def custom_failure_check_adapter() -> Adapter:
return Adapter(
identity=Identity(name="custom-failure-check", adapter_type="alora"),
# get_io_contract falls back to a permissive dict contract for names
# outside the built-in catalog — sufficient here, since the real
# output shape is enforced by the adapter's own io.yaml, not by
# IOContract (which doesn't yet drive request/response handling).
io_contract=get_io_contract("custom-failure-check"),
weights=LocalFileBinding(
name="custom-failure-check",
adapter_type=AdapterType.ALORA,
repo_id="your-org/my-adapter", # your Hugging Face repo
# A LocalFileBinding with revision=None resolves it from the
# catalog — which has no entry for a custom name. Pin it
# explicitly (a branch, tag, or commit SHA).
revision="main",
),
)
Trap: Omitting
revisionhere raisesValueError: Unknown intrinsic name '...'from deep insideLocalFileBinding.resolved_revision()— easy to misread as a registration failure rather than what it actually is: no catalog entry to fall back to.
Step 2: Register it with a backend
from mellea.backends.huggingface import LocalHFBackend
backend = LocalHFBackend(model_id="ibm-granite/granite-3.2-8b-instruct")
backend.add_adapter(custom_failure_check_adapter()) # downloads and loads the weights
add_adapter refuses a second registration under the same qualified name, so
guard repeated calls (e.g. inside a helper function) the same way the
built-in adapters do:
qualified_name = "custom-failure-check_alora"
if qualified_name not in backend.list_adapters():
backend.add_adapter(custom_failure_check_adapter())
Step 3: Invoke it
There are two ways to invoke a registered adapter, depending on whether you need raw output or a pass/fail validation.
Directly, for raw output
from mellea.backends.adapters.catalog import AdapterType
from mellea.stdlib.components.intrinsic import Intrinsic
import mellea.stdlib.functional as mfuncs
from mellea.stdlib.components import Message
from mellea.stdlib.context import ChatContext
# adapter_types is required for a name outside the catalog — Intrinsic has
# no catalog entry to look up adapter_types from otherwise.
action = Intrinsic("custom-failure-check", adapter_types=(AdapterType.ALORA,))
ctx = ChatContext().add(Message("user", "Observed black soot on intake."))
out, _ = mfuncs.act(action, ctx, backend)
print(out)
As a validator, with ALoraRequirement
If your adapter's io.yaml transforms its output into
{"requirement_check": {"score": <float>}}, use it as a drop-in requirement
validator instead:
from mellea import MelleaSession
from mellea.stdlib.requirements import ALoraRequirement
m = MelleaSession(backend, ctx=ChatContext())
failure_check = ALoraRequirement(
"The failure mode must not be 'no_failure'.",
intrinsic_name="custom-failure-check",
# Required for the same reason as Intrinsic's adapter_types above.
adapter_types=(AdapterType.ALORA,),
)
result = m.instruct(
"Summarize this technician note: {{note}}",
user_variables={"note": "High vibration at 3100 RPM."},
requirements=[failure_check],
)
What replaced the old approach
The deprecated CustomIntrinsicAdapter shim did two things under the hood
that this tutorial's composed Adapter replaces without a class of its own:
- Adapter registration —
LocalFileBindingalready accepts an arbitraryrepo_id, so no shim was ever needed for this half. - Making the name usable by
Intrinsic/ALoraRequirement—CustomIntrinsicAdaptermonkey-patched Mellea's global intrinsics catalog to add the custom name before constructing anything.IntrinsicandALoraRequirementnow acceptadapter_typesexplicitly instead, so no catalog mutation is needed — only the caller's own process is affected, and there is no shared global state to leak between callers.
See also: Adapter functions | LoRA and aLoRA adapters | Adapter function metrics