External Adapter SDK#
External adapters make researcher code a first-class Noema operation without editing the core repository. An adapter is a small folder with:
noema_adapter.yaml: operation IDs, wrapped Noema contract, callable location, and visible params.adapter.py: Python functions that implement the model or payload transform.
The registered operations appear in noema ops, recipe validation, graph rendering, run manifests,
dashboard block labels, timing summaries, and result artifacts exactly like built-in operations.
Quick Start#
Create a starter payload-bit image codec adapter:
uv run noema adapter scaffold adapters/my_codec --name my_codec --kind bits
uv run noema adapter validate adapters/my_codec/noema_adapter.yaml
Create a non-codec task metric or dataset adapter:
uv run noema adapter scaffold adapters/my_metric --name my_metric --kind classification_metric
uv run noema adapter scaffold adapters/my_dataset --name my_dataset --kind classification_dataset
Use it in CLI commands:
uv run noema --adapter adapters/my_codec recipe validate recipes/my_recipe.yaml
uv run noema --adapter adapters/my_codec recipe lint recipes/my_recipe.yaml
uv run noema --adapter adapters/my_codec recipe run recipes/my_recipe.yaml
Use it in the dashboard:
uv run noema --adapter adapters/my_codec ui serve --host 127.0.0.1 --port 8766
You can also set NOEMA_ADAPTER_PATHS to one or more manifest files or adapter directories,
separated by the platform path separator (: on Linux).
Manifest Contract#
Minimal manifest:
schema_version: 1
name: my_codec
version: 0.1.0
operations:
- id: model.my_codec_encode_bits
name: My codec encoder
wraps: model.external_encode_bits
adapter:
path: adapter.py
callable: encode_bits
call_style: array_params
fixed_params:
bit_storage: unpacked_bits
bit_order: big
differentiability:
framework: blackbox
gradient: none
trainable_params: false
exportable: false
reason: "Declare torch/sionna + full/surrogate only when the adapter can be exported for training."
params_schema:
type: object
properties:
model_path:
type: string
default: ""
additionalProperties: true
Checkpoint-backed adapter manifests can also declare how a trained model returned to Noema:
schema_version: 1
name: my_deepjscc_method
version: 0.2.0
description: DeepJSCC checkpoint returned from a Noema differentiable export.
training:
source_recipe: recipes/deepjscc_kodak_awgn_train.yaml
source_recipe_sha256: "<sha256-of-source-recipe>"
differentiable_export_id: 20260710T120000Z_deepjscc_export
capture_id: optional_capture_id
framework: torch
checkpoint_path: checkpoints/model.pt
checkpoint_sha256: optional_known_sha256
input_schema:
dtype: float32
shape: [N, H, W, C]
output_schema:
dtype: complex64
shape: [N, channel_uses]
model_card: MODEL_CARD.md
operations:
- id: model.my_deepjscc_encode
name: My DeepJSCC encoder
wraps: model.deepjscc_external_encode
adapter:
path: adapter.py
callable: encode_symbols
call_style: dict
- id: model.my_deepjscc_decode
name: My DeepJSCC decoder
wraps: model.deepjscc_external_decode
adapter:
path: adapter.py
callable: decode_symbols
call_style: dict
Relative training.checkpoint_path, training.source_recipe, and training.model_card values are
resolved relative to noema_adapter.yaml. When training.checkpoint_path is present, validation
requires the file to exist and computes training.checkpoint_sha256. If you provide
checkpoint_sha256, Noema checks that it matches the file. The normalized training metadata is passed
to adapter callables as params["training"]; params["checkpoint_path"] and
params["checkpoint_sha256"] are also populated when they are not already supplied by operation
params. Run summaries, artifacts, and manifests record the same metadata so benchmark results can be
traced back to the differentiable export and checkpoint.
wraps chooses the built-in contract and artifact handling:
Wrapped operation |
Input artifact |
Output artifact |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
none |
|
|
|
|
Relative adapter.path values are resolved relative to the manifest file. adapter.module can be
used instead of adapter.path for importable Python packages.
Adapter registration imports and resolves every declared callable before an execution plan can create a run directory. Registration is all-or-nothing across the discovered manifests: one missing import does not leave earlier adapter operations partially installed. Provenance binds the manifest, callable file, and a deterministic inventory of local Python sources under the adapter package root; any byte change requires rebuilding the registry and plan.
This preflight is a trusted-code boundary, not a sandbox. Importing an adapter executes Python in
the Noema process. Only load adapters and Python/pickle-based checkpoints from trusted producers.
Prefer data-only artifacts such as NPZ, ONNX, or Safetensors; when a PyTorch checkpoint is unavoidable,
use a supported weights_only=True loader and independently pin its digest. The adapter source-tree
fingerprint detects drift but does not make imported code safe or authenticate its author.
Callable Signatures#
The default call_style is array_params:
def encode_bits(images, params):
...
return {"array": bits, "metadata": {"bit_count": int(bits.size)}}
Other supported call styles:
array:fn(array)dict:fn({"array": array, "metadata": metadata, "params": params})
The callable may return either a NumPy-compatible array or:
{"array": array, "metadata": {...}}
For image decoders, return uint8 images shaped [N, H, W, 3].
Differentiability Metadata#
External adapter operations may include optional differentiability metadata with the same fields as
built-in operations:
Field |
Values |
|---|---|
|
|
|
|
|
|
|
|
|
optional human-readable explanation |
If omitted, the wrapped operation’s metadata is inherited. If neither the adapter nor wrapped
operation declares metadata, Noema reports the safe default: NumPy, no gradient, no trainable params,
not exportable. trainable_params is legacy compatibility metadata: setting it to true grants
neither Built-in fine-tuning nor Portable replacement. Built-in fine-tuning requires both an
implementation-owned fine_tuning_supported declaration and a callable fine_tuning_provider;
portable replacement requires a
validated operation trained_artifact_abi plus a compatible runtime binding. External manifest
differentiability metadata is useful only for describing an adapter retained as unchanged support on
a live gradient route.
A portable ABI must bind both artifact_manifest_path and artifact_entrypoint, and the bound
entrypoint must equal the ABI’s entrypoint_id. It must also declare a non-empty component_id and
component_role; this matches the artifact import gate. Parameter metadata alone cannot advertise
portable replacement.
Task dataset and metric wrappers use call_style: dict by default:
def load_classification_examples(request):
return {
"examples": [
{"id": "a", "label": "clear", "prediction": "clear"},
{"id": "b", "label": "faded", "prediction": "clear"},
],
"metadata": {"dataset": "my_dataset"},
}
def score_classification(request):
reference = request["reference"]
candidate = request["candidate"]
return {
"metrics": {"external.classification.accuracy": 0.5, "task.accuracy": 0.5},
"per_example": [],
}
Run the bundled non-codec adapter example:
uv run noema --adapter examples/adapters/classification_task \
benchmark run examples/benchmarks/external_classification_adapter_smoke.yaml
Bit Payloads#
For model.external_encode_bits, Noema always stores and transmits canonical unpacked channel bits:
dtype:
np.uint8shape: flat vector
values:
0or1one array element is one bit
If your encoder naturally emits packed bytes, set:
fixed_params:
bit_storage: packed_bytes
bit_order: big
Then return byte data and provide metadata.bit_count when the last byte contains padding. Noema
will unpack before channel transmission and repack before model.external_decode_bits.
Timing#
Manifest operations inherit the timing behavior of the wrapped external operation. Today that is a single local-Python adapter-call timing:
encoder wrappers record
encoder.external_calldecoder wrappers record
decoder.external_call
If your adapter internally measures model inference, payload packing, or native runtime calls, return those detailed measurements in metadata in a later SDK revision. The current SDK intentionally records the whole adapter call so no external model is silently treated as a built-in runtime.
Validation#
uv run noema adapter validate adapters/my_codec/noema_adapter.yaml --json
Validation checks:
manifest schema version
unique operation IDs inside the manifest
wrapped operation IDs exist
checkpoint-backed training metadata is well formed
training.checkpoint_pathexists when declaredtraining.checkpoint_sha256is computed, and checked if a value is declaredcallable path/module can be imported
callable object exists and is callable
visible operation input/output kinds match the wrapped Noema contract
Use --no-import only when you want a structural check without importing researcher code.
After adapter validation, run recipe lint on the recipe that uses the adapter. Lint checks the
platform-level invariants around the adapter, including canonical uint8 unpacked bit boundaries,
fixed channel count checks, symbol boundaries for DeepJSCC-style adapters, and explicit model
conversion artifacts for non-native runtimes.