Local backends and typed outcomes¶
Configure a local backend explicitly for summarization, NLI verification and a guarded brief. Existing independently qualified local checkpoints or caller-owned providers can satisfy these APIs; training or publishing a new model is not a library-release prerequisite. Missing providers fail closed. Remote provider names and URLs are rejected, with no cloud fallback.
The examples below use only synthetic text, metadata and runtime doubles. They run without model downloads or optional inference packages. Synthetic scores, calibration identifiers, review transitions and tiny artifact files demonstrate contracts; they are not qualified models, empirical calibration, clinical validation or permission to approve real evidence.
Explicit extraction and trusted local summarizers¶
For standalone summarization, model="extractive" selects the deterministic CPU baseline. Within a reviewed brief it selects whole sentences by fact coverage and the independent omission and length rules; an infeasible selection is refused without partial output. Omitting model selects the registered, pinned MLX summarizer; extraction is never an automatic fallback for missing MLX assets. For raw input, summarize() first de-identifies locally and may therefore require a cached PII model even in extractive mode. Use summarize_deidentified() only with a genuine completed DeidentificationResult; a plain string is rejected by its ordering guard.
This synthetic fixture contains no identifiers and needs no PII model:
from datetime import datetime
from openmed.clinical.summarize import summarize_deidentified
from openmed.core.pii import DeidentificationResult
text = "A cough is present. Symptoms improved. Follow-up was arranged."
deidentified = DeidentificationResult(
original_text=text,
deidentified_text=text,
pii_entities=[],
method="mask",
timestamp=datetime(2026, 1, 1),
)
extractive = summarize_deidentified(deidentified, model="extractive")
assert extractive.summary == text
assert extractive.backend == "deterministic-extractive"
class SyntheticLocalSummarizer:
def summarize(self, source, *, mode="bhc"):
assert mode == "bhc" and source == text
return "A cough is present."
custom = summarize_deidentified(deidentified, model=SyntheticLocalSummarizer())
assert custom.summary == "A cough is present."
assert custom.backend == "caller-supplied-local"
assert custom.leakage_check.passed
A callable (deidentified_text) -> str or an object exposing summarize(deidentified_text, *, mode=...) -> str wraps an existing local runtime. OpenMed passes only de-identified text, checks output size and source identifier leakage, and blocks outbound Python socket calls during execution. The callable is trusted application code, with the process's authority; this guard does not isolate native code, subprocesses, files or other services. Keep it local and quiet, bound its resource use, and qualify its actual model.
summary and SummarizationResult.to_dict() contain generated text and need protected handling. For diagnostics, project controlled backend identifiers, template digests and leakage_check.to_dict(); never log original text, prompts, model responses or arbitrary upstream exception messages.
Registered MLX package provisioning¶
model="mlx", model="maple" and the registered summarizer alias resolve to the same reviewed repository and immutable revision. MLXSummarizerBackend accepts a memory budget; arbitrary paths and unregistered model strings are rejected at this clinical entry point. Constructing it does not load weights. The first inference requires the optional MLX runtime and an explicitly registered local package. There is no Hub cache lookup, download or automatic extraction fallback at inference time.
Provision the exact registry-selected package before processing clinical input, using the SLM manifest guide. Register its absolute directory and the manifest digest from an independently trusted provisioning record with register_summarizer_package; deriving the trusted pin from an untrusted package defeats that trust boundary. Registration itself does not read files or contact a service. The PII loader's OpenMed cache remains a separate input; provisioning it does not register summarizer artifacts.
Before model construction, MLX admission verifies the trusted manifest digest, model identity and revision, the complete declared file inventory and every component's digest and size. Package files must be regular files; symlinks and undeclared files are refused. Config and template bytes are checked again when read. Manifest task, context, runtime-feature and quantization metadata must match the runtime contract. Unsupported secure local-read platforms fail closed.
Admission reserves output context, enforces a conservative memory budget and checks actual tokenizer length before generation. Digest checks establish integrity against the supplied trusted pin; they do not establish model quality, provenance authenticity or measured peak memory. Keep provisioned files immutable through verification and runtime loading: an external writer is not locked out for the whole inference operation. No Hugging Face Space visibility change is involved.
Local encoder NLI with an explicit class mapping¶
EncoderNLIBackend needs the checkpoint's class-index mapping and NLIThresholds from its independently validated calibration. Do not infer that class zero means entailment, or treat default thresholds or calibrated=True as empirical proof. The three mapped states must be entailment, neutral and contradiction at indices 0, 1 and 2. The encoder produces a selective decision: low-confidence or overlong pairs become abstention, preserving human review.
This injected loader mimics the local Torch adapter without importing Torch:
from types import SimpleNamespace
from openmed.clinical.nli import verify
from openmed.clinical.nli_backends import EncoderNLIBackend
from openmed.clinical.nli_gate import NLIThresholds
class SyntheticEncoderLoader:
def __init__(self):
self.calls = []
def load_local_sequence_classifier(self, model_ref, *, revision, runtime):
self.calls.append((model_ref, revision, runtime))
return {
"tokenizer": lambda *args, **kwargs: {"input_ids": [[1, 2]]},
"model": lambda **kwargs: SimpleNamespace(
logits=[SimpleNamespace(tolist=lambda: [5.0, 0.0, 0.0])]
),
}
loader = SyntheticEncoderLoader()
encoder = EncoderNLIBackend(
"synthetic-local-checkpoint",
revision="a" * 40,
runtime="torch",
label_mapping={"0": "entailment", "1": "neutral", "2": "contradiction"},
thresholds=NLIThresholds(
entailment=0.9,
contradiction=0.9,
margin=0.05,
calibration_id="synthetic-docs-only",
calibration_method="synthetic-fixture",
),
loader=loader,
)
assert loader.calls == [] # Loading is lazy.
verdicts = verify(["A cough is present."], "A cough is present.", backend=encoder)
assert verdicts[0]["label"] == "entailment"
assert verdicts[0]["backend_id"] == "local-encoder"
assert loader.calls == [("synthetic-local-checkpoint", "a" * 40, "torch")]
Without an injected loader, the existing ModelLoader uses its cache-only local sequence-classifier path with runtime="torch" or runtime="onnx". For a Hub model, supply a pinned revision and provision that snapshot separately; local checkpoint directories also need protected permissions and verified contents. Registry resolution requires a released, permissively licensed NLI entry with a pinned revision, mapping and calibration metadata. If no qualified default is registered, verify(..., backend=None) raises LocalNLIError. It does not switch to the development heuristic automatically.
verify() accepts trusted local callables or .predict() objects returning label, score and a safe backend_id. It returns controlled decisions and review flags, omitting source and claim text. Explicit backend="heuristic" is a development baseline, not a qualified encoder or a brief provider. Empty claims return an empty list without resolving a backend; absence of a verdict is not a successful verification.
Artifact, capability and memory gates before construction¶
For a caller-owned runtime, verify the artifact manifest, probe the separate capability metadata and perform memory preflight before constructing a tokenizer, weight tensor or runtime. The strict artifact manifest and capability manifest have different schemas; do not add context/runtime fields to the artifact manifest. Pin the expected manifest digest through a trusted distribution channel. A self-digest is not authenticity.
The following tiny files are synthetic contract fixtures, not loadable weights:
import hashlib
from pathlib import Path
from tempfile import TemporaryDirectory
from openmed.models.clinical_slm_capabilities import probe_clinical_slm_capabilities
from openmed.models.clinical_slm_manifest import (
ClinicalSLMArtifact,
ClinicalSLMArtifactManifest,
ClinicalSLMQuantization,
load_clinical_slm_manifest,
verify_clinical_slm_package,
)
from openmed.models.clinical_slm_memory import (
ClinicalSLMRuntimeProfile,
preflight_clinical_slm_memory,
)
payloads = {
"weights": b"synthetic-weights",
"tokenizer": b"synthetic-tokenizer",
"templates": b"synthetic-template",
"quantization": b"synthetic-int4",
}
manifest = ClinicalSLMArtifactManifest(
model_id="OpenMed/Synthetic-Clinical-SLM",
revision="a" * 40,
components=tuple(
ClinicalSLMArtifact(
component=name,
path=f"{name}.bin",
sha256=hashlib.sha256(payload).hexdigest(),
size_bytes=len(payload),
)
for name, payload in payloads.items()
),
quantization=ClinicalSLMQuantization(scheme="int4", bits=4),
licenses={name: "MIT" for name in payloads},
supported_tasks=("clinical-summarization",),
)
capabilities = {
"supported_tasks": ["clinical-summarization"],
"context_limits": {
"max_context_tokens": 512,
"max_input_tokens": 384,
"max_output_tokens": 128,
},
"quantization": {"scheme": "int4", "bits": 4},
"required_runtime_features": ["tokenizers"],
"offline": True,
"human_review_required": True,
"cloud_fallback": False,
}
with TemporaryDirectory(prefix="openmed-backend-docs-") as directory:
package = Path(directory)
for name, payload in payloads.items():
(package / f"{name}.bin").write_bytes(payload)
(package / "clinical-slm-manifest.json").write_text(manifest.to_json())
checked = load_clinical_slm_manifest(package)
assert checked.manifest_digest == manifest.manifest_digest
integrity = verify_clinical_slm_package(package, checked)
capability = probe_clinical_slm_capabilities(
capabilities,
required_tasks=["clinical-summarization"],
available_runtime_features=["tokenizers"],
)
memory = preflight_clinical_slm_memory(
{"weights_bytes": len(payloads["weights"])},
ClinicalSLMRuntimeProfile(
memory_budget_bytes=1_000_000,
headroom_bytes=100_000,
context_tokens=512,
cache_bytes_per_token=16,
context_bytes_per_token=8,
batch_bytes=0,
runtime_overhead_bytes=1_000,
),
)
if not (integrity.verified and capability.supported and memory.accepted):
raise RuntimeError("synthetic_backend_admission_refused")
constructed = {"backend_id": "synthetic-docs-only"} # Trusted runtime double.
assert constructed == {"backend_id": "synthetic-docs-only"}
assert not package.exists()
Disk artifact verification requires POSIX no-follow/directory-descriptor support and fails closed on unsupported platforms. Capability probing validates declared metadata, not execution or model quality; memory preflight estimates resource use, not measured peak allocation. Rejected capability/memory reports are normal decisions with fixed reason codes, distinct from malformed-input exceptions. Keep artifacts immutable until loading completes and measure the actual device.
Wire reviewed evidence into BriefContext¶
Prefer a callback returning three calibrated class probabilities and the matching calibration_id. The current composer also accepts the NLI gate's legacy label/score mapping with that matching identifier. In that compatibility path, the gate splits the residual probability equally between the other two classes; those residuals are not measured class probabilities or evidence of empirical calibration. A standalone verifier decision can contain abstention labels and does not automatically satisfy this callback contract. Use an independently qualified probability adapter instead of manufacturing calibration from a selected label or score. An existing local runtime can expose a separately qualified probability adapter. The offline NLI qualification protocol binds caller-owned artifact bytes, mapping, runtime, separated development and held-out data, and policy. Only a live qualifier-issued receipt can construct bind_qualified_nli(); decoded audit JSON cannot authorize a callback. Supply a local privacy detector too; an empty detector result is meaningful only when the detector itself has been qualified.
This self-contained example creates review transitions only for synthetic evidence. Production application review stores must provide actual reviewed history and enforce their own record authorization. The current evidence packet remains synthetic-only. The separate, opt-in ReviewedLocalBriefContext requires current source custody, independently stored review authority and an unexpired, unrevoked receipt; see reviewed-local admission. This example does not manufacture that authority.
import hashlib
import json
from datetime import datetime
from openmed.clinical.brief import (
BriefContext,
BriefFact,
brief_policy_fingerprint,
build_clinical_brief,
)
from openmed.clinical.evidence_packet import (
build_evidence_packet,
fingerprint_evidence_review,
)
from openmed.clinical.nli_gate import NLIThresholds
from openmed.clinical.review_state_machine import (
ReviewState,
ReviewStateMachine,
make_opaque_event_id,
)
from openmed.core.pii import DeidentificationResult
sentences = (
"The admission problem was dehydration.",
"The discharge diagnosis was dehydration.",
"Symptoms improved after fluids.",
)
fields = ("admission_reason", "discharge_diagnoses", "hospital_course")
text = " ".join(sentences)
facts = tuple(
BriefFact(
f"synthetic:ref-{index}", field, "affirmed", "certain", "recent", "patient"
)
for index, field in enumerate(fields)
)
policy = brief_policy_fingerprint(text, facts)
rows = []
for index, sentence in enumerate(sentences):
reference_id = f"synthetic:ref-{index}"
start = text.index(sentence)
row = {
"reference_id": reference_id,
"source_id": "synthetic:document",
"start": start,
"end": start + len(sentence),
"policy_fingerprint": policy,
"review_state": "approved",
"synthetic": True,
"verified": True,
}
fingerprint = fingerprint_evidence_review(
**{
key: row[key]
for key in (
"reference_id",
"source_id",
"start",
"end",
"policy_fingerprint",
)
}
)
machine = ReviewStateMachine()
for state in (ReviewState.IN_REVIEW, ReviewState.APPROVED):
machine.transition(
state, make_opaque_event_id((reference_id, state.value)), fingerprint
)
row["review_transitions"] = machine.transitions
rows.append(row)
thresholds = NLIThresholds(
calibration_id="synthetic-docs-only", calibration_method="synthetic-fixture"
)
def synthetic_probabilities(premise, hypothesis):
assert premise == hypothesis
return {
"entailment": 1.0,
"contradiction": 0.0,
"neutral": 0.0,
"calibration_id": thresholds.calibration_id,
}
content_digest = (
"sha256:"
+ hashlib.sha256(
json.dumps(
text, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode()
).hexdigest()
)
context = BriefContext(
packet=build_evidence_packet(rows, policy_fingerprint=policy),
content_digest=content_digest,
facts=facts,
nli_predict=synthetic_probabilities,
thresholds=thresholds,
privacy_detector=lambda value: [], # Synthetic test-only detector double.
)
deidentified = DeidentificationResult(text, text, [], "mask", datetime(2026, 1, 1))
brief = build_clinical_brief(deidentified, model="extractive", context=context)
assert brief.refusal_reason is None
audit = brief.to_dict()
assert audit["status"] == "needs_review"
assert audit["envelope"]["requires_human_review"]
assert text not in json.dumps(audit)
The content digest follows the composer's canonical JSON-string encoding. The policy fingerprint binds text, reviewed axes and profile version; changed content or annotations require new review. Generation must match unique reviewed atomic source spans. Successful output still has needs_review; it is not approval or a clinical decision. brief.summary and to_response() are protected output, while to_dict() is the value-free audit view. Digests bind records; they are not authorization or anonymization.
Backend exceptions¶
The table covers public exception classes defined by summarize, summarize_backends, nli and nli_backends, plus the separate admission modules and optional-runtime error. Its drift test discovers exports rather than keeping a second list of backend errors. Catch subclasses before base classes. Never serialize arbitrary exception objects or their upstream context.
| Exception | Trigger at this boundary | Remediation |
|---|---|---|
ExtractiveSelectionError | Reviewed fact coverage cannot satisfy the independent omission/length rules, or bounded selection is exhausted | Inspect its value-free selection diagnostics and re-review the budget/evidence; no partial summary is returned |
NLIQualificationError | Invalid, unavailable, drifting or unqualified caller-owned artifact/receipt inputs | Reproduce the offline qualification protocol with trusted local inputs; do not promote an audit record into authority |
BriefInterrupted | Direct summarization reaches a cancelled or expired cooperative checkpoint | Discard the late result; inspect only cancelled or deadline_exceeded and start a new request if appropriate |
LocalSummarizerPackageError | Missing trusted package pin, unsafe or changed files, incompatible manifest/config/template metadata, or unsupported secure local reads | Inspect its fixed reason code; re-provision and register an independently trusted package before retrying; do not log upstream errors |
LocalSummarizerError | Unregistered/invalid backend, rejected admission or inference, invalid output, or trusted callback failure | Check fixed local configuration, artifact/runtime availability, limits and provider contract; keep the failure closed |
RemoteSummarizerError | A remote provider name or colon-bearing backend string at summarizer resolution | Select a registered local alias or trusted local callable; never send the note to that provider |
SummarizationOrderError | Guarded stage receives raw/invalid de-identification input or retained source identifiers | Complete and validate local de-identification before generation |
SummarizationLeakageError | Generated output contains detected source identifiers | Discard output and investigate the local provider using synthetic reproduction |
LocalNLIError | Missing/unqualified default or alias, invalid encoder configuration, unavailable checkpoint or inference failure | Provision and qualify the local checkpoint, mapping, calibration and runtime; keep claims unverified |
RemoteNLIBackendError | A remote NLI provider name, URL or api. reference | Use a registered local checkpoint or trusted local predictor |
MissingOptionalDependencyError | Built-in MLX runtime dependencies are absent | Install the documented optional extra before clinical execution; do not switch backend implicitly |
ClinicalSLMManifestError | Base class for artifact-manifest failures | Refuse runtime construction and inspect fixed reason codes |
ClinicalSLMValidationError | Invalid manifest metadata/schema, paths, licenses or revision | Correct trusted deployment metadata without weakening validation |
ClinicalSLMArtifactError | Local filesystem verification cannot safely complete | Repair the protected immutable package/platform and reverify |
ClinicalSLMArtifactMissingError | A required declared artifact is absent | Provision the exact declared package; do not fetch during clinical execution |
ClinicalSLMArtifactDigestMismatchError | File size or digest differs from the manifest | Reject the package and restore its trusted immutable contents |
ClinicalSLMCapabilityError | Malformed capability metadata or probe requirements | Correct the separate capability schema; an unsupported report instead carries reason codes |
ClinicalSLMMemoryError | Base class for invalid memory artifact/profile metadata | Fix the conservative local profile; resource shortfall instead returns a rejected report |
ClinicalSLMMemoryValidationError | Conflicting/invalid size, profile or report metadata | Correct the measurements and reject unknown assumptions |
Other public validation failures can be standard TypeError or ValueError (for example invalid modes, claim/source alignment or malformed predictor labels/scores). Do not relabel all such cases as LocalNLIError. The standalone verify() result is a decision, not a BriefRefusal and not a clinical approval.
Brief refusals¶
build_clinical_brief() returns a ClinicalBrief with a typed refusal and an empty summary, rather than exposing backend exceptions. The table covers every BriefRefusal member. Missing runtime packages, unknown local aliases and unavailable local de-identification artifacts become MODEL_UNAVAILABLE at de-identification or generation. Remote providers remain prohibited; other unexpected exceptions, including NLI callback failures, become STAGE_FAILED. A missing/incompatible NLI callback or calibration ID reaches NLI_UNAVAILABLE. Record the enum and controlled stage trace, not raw provider errors. The trace records entered stages, not proof that each passed.
LocalSummarizerError.reason carries the closed reason vocabulary described in Summarization. Built-in errors preserve their fixed code; foreign subclasses and callback errors become a fresh execution_failed without reading foreign reason properties or retaining private exception chains. Registered MLX package admission uses the separate closed LocalSummarizerPackageError.code vocabulary. Direct summarization preserves that typed error; the brief composer reports package admission failures as STAGE_FAILED, with no source or path details.
| Member | Value | Current trigger | Remediation |
|---|---|---|---|
INVALID_INPUT | invalid_input | Input type/size or guarded profile input is invalid | Supply bounded input and a supported profile |
REVIEW_REQUIRED | review_required | No reviewed context was supplied | Resolve actually reviewed evidence from the application's authorized store |
EMPTY_EVIDENCE | empty_evidence | The reviewed packet has no references | Stop generation and obtain appropriate reviewed evidence |
INVALID_EVIDENCE | invalid_evidence | Content/policy binding, packet/fact limits or reference alignment is inconsistent | Rebuild and re-review the changed evidence; never copy old approval |
UNSUPPORTED_CLAIM | unsupported_claim | A claim is not exactly and uniquely supported, or downstream support/coverage checks fail | Discard the summary and review the supported source spans |
NLI_UNAVAILABLE | nli_unavailable | Missing/incompatible probability callback, thresholds or calibration binding | Supply a qualified local three-probability adapter and matching thresholds |
NLI_REJECTED | nli_rejected | A claim does not pass selective entailment, including contradiction or abstention | Retain human review; do not force entailment or relax thresholds silently |
PRIVACY | privacy | Leakage or final privacy validation rejects output | Discard output and investigate with protected handling and synthetic reproduction |
LENGTH_BUDGET_EXCEEDED | length_budget_exceeded | Reviewed evidence exceeds a mapped class allocation before generation | Reduce admitted evidence with independent review; keep policy caps intact |
MODEL_UNAVAILABLE | model_unavailable | A local alias, runtime package or cached artifact is unavailable at generation/de-identification | Provision the approved local runtime/artifact separately; do not substitute a remote provider |
STAGE_FAILED | stage_failed | Other unexpected exception or incomplete stage contract | Inspect controlled configuration/stage information and restore the missing local contract |
INVALID_REVIEWED_EVIDENCE | invalid_reviewed_evidence | The separately versioned local evidence contract is malformed | Obtain bounded exact reviewed evidence; do not reuse a malformed record |
REVIEW_RECEIPT_MISSING | review_receipt_missing | No independently stored matching review receipt is available | Resolve the receipt through the trusted review store |
REVIEW_RECEIPT_EXPIRED | review_receipt_expired | The receipt is outside its exclusive validity interval | Obtain fresh independent review |
REVIEW_RECEIPT_MISMATCHED | review_receipt_mismatched | Receipt, current review state or evidence binding differs | Re-review the current evidence without copying old authority |
REVIEW_RECEIPT_REVOKED | review_receipt_revoked | The trusted authority reports revocation | Stop generation and obtain current authorized review |
REVIEW_AUTHORITY_UNAVAILABLE | review_authority_unavailable | The trusted current-review lookup fails or is unavailable | Restore the application-owned authority; never trust wire metadata alone |
REVIEW_SOURCE_UNAVAILABLE | review_source_unavailable | Current source custody cannot be verified | Restore the independent source lookup |
REVIEW_SOURCE_CHANGED | review_source_changed | Current source identity/content/offset binding has drifted | Rebuild evidence from the current source and obtain fresh review |
REVIEW_POLICY_CHANGED | review_policy_changed | Current policy digest differs from the reviewed policy | Review the evidence against the current policy |
CANCELLED | cancelled | Explicit cancellation or native task cancellation reaches a checkpoint | Discard output and any late callback result; cancellation does not forcibly kill a legacy callback |
DEADLINE_EXCEEDED | deadline_exceeded | The started request budget expires at a checkpoint | Discard output; use an appropriate new request budget without treating the old result as complete |
These tables describe the current implementation. They do not alter exception typing, refusal values, model qualification or the review/privacy requirements.