Oracles
A refolding oracle is an independent structure-prediction model that folds each design’s sequence back down and compares the result against the design’s own coordinates. Its agreement with the design feeds two different consumers, not one:
- In-silico pass/fail criteria: eleven criteria, reported as
{criterion}.passrows in a response’saggregated_metrics, computed after generation finishes. - Reward-guided search: a beam search can weight its reward on oracle
confidence at every
step_checkpoint, scoring candidates during the generation trajectory rather than only at the end. Without a mounted oracle, search is not merely unscored on this axis; it is limited to whichever non-oracle metrics a request’s reward weights name, which is a meaningfully smaller set of signals to search against.
This NIM ships with no refolding oracle built in. Every *_refolding
metric tag is rejected with 422 and model.oracle is [] until you mount
one, which also means a reward-guided search cannot use oracle confidence as
part of its objective until one is mounted. This is a deliberate design
choice, not a missing feature: the oracle is a separate model an operator
chooses and is responsible for, reached however that operator’s deployment
reaches it (in-process, a sidecar container, or a hosted API), so the NIM
itself commits to none of those shapes.
The Mount Contract
Every image carries a single file at /opt/nim/reward.py. Its default
content declares no oracle at all:
The NIM recognizes this shipped file by content, before ever importing it: an unmodified copy is treated exactly like no file present, and its code never executes. There is no separate feature flag to also enable: the file is the switch.
To enable an oracle, replace that file with one that defines the same
oracles() function returning one or more oracle descriptions, and mount it
over the same path:
The shipped default file is exempt from NIM_ALLOW_ORACLE_PLUGIN precisely
because it is recognized before being imported and never runs. The variable
only gates code that could actually execute.
What One Oracle File Defines
oracles() itself must be cheap and free of side effects. It runs in the API
process, once per worker, before any model is built or any weight is read —
that process validates requests and must never hold a model. Importing torch,
opening a checkpoint or contacting a remote from oracles() reintroduces
exactly the problem this split exists to prevent. Put all of it in
prepare_oracle() instead.
Each OracleSpec in that list carries:
What fold Receives and Returns
cif_text and atom_array describe the same design — one is written from
the other — so use whichever suits the folding program and do not mix
conclusions drawn from both.
Three keys are also emitted once per chain pair by upstream, as
<key>_<chain_i>_<chain_j>: min_ipae, ipsae_min and iptm. Declare the
bare name in published_keys and every per-pair variant is covered;
enumerating chain labels a request has not sent yet would be meaningless.
Output Is Validated, Not Trusted
A fold result is checked before it reaches a metric row or a search score:
- every confidence must be a real, finite number — a
NaN, an infinity or aboolis an error, not a quietly dropped row, because a search ranking on a non-number is worse than one that stops; - the returned CIF must parse, contain atoms, and have finite coordinates.
Results are then written atomically under a lock, so two workers folding the same candidate cannot interleave a half-written structure.
One File, Multiple Oracles
oracles() returns a list because one mounted file is one deployment
choice, not necessarily one model. Registering the same underlying predictor
under two different names lets it answer two different roles with two
different sets of published metrics, for example a general-purpose role
plus an interface-specific role that only makes sense for a multi-chain
design:
Both entries call the same model; oracle_boltz2_ppi additionally resolves
the design’s chain of interest to a real per-chain-pair confidence value,
which is only meaningful for a request that actually names an interface.
prepare_oracle() is shared across every entry in the file, so a model
backing several roles is still loaded once per worker, not once per role.
Reference Implementations
Two working reward.py files are reproduced in full below as starting
points, rather than shipped as the default. Each calls out over HTTP to an
independent NIM hosted on build.nvidia.com;
neither runs in-process. Both are complete as printed — copy the block,
set the credential, mount it.
A variant of the OpenFold3 file that calls a self-hosted OpenFold3 NIM
container instead differs only in its base URL, its headers and a health
check in prepare_oracle().
The two NVCF examples reproduced below send their request through the NIM’s
own api.oracles.http.post_json rather than calling urllib directly. That
helper is what handles a hosted function answering 202 Accepted and
polling for the result without re-submitting, pins the poll URL to the
invocation’s origin, refuses redirects so a credential is never forwarded,
caps the response body and holds one deadline across submission, polls and
body reads. A hosted function answers 202 on exactly the long folds an
oracle exists for, so one that posts with bare urllib will appear to work
and then fail there. api.oracles.http is part of the image, available to
any mounted file.
OpenFold3
Calls the OpenFold3 NIM’s
NVCF-hosted endpoint. This is the complete, working file: copy it as-is,
set OF3_NIM_API_KEY to an NVCF invoke key, and mount it to use it.
Verified live against both a self-hosted OpenFold3 NIM and this NVCF-hosted endpoint, returning identical confidence values for the same input.
Two different NVCF credentials look similar and are not interchangeable.
OF3_NIM_API_KEY is an invoke key for a hosted function, the same
kind of credential build.nvidia.com’s own “Shell” code sample uses. It is
not the same as an NGC_API_KEY used to pull a NIM container image from a
private registry; a key scoped for one purpose commonly fails, sometimes
silently, against the other.
OpenFold3’s HTTP API returns complex-level confidence only: never a
per-chain or chain-pair breakdown, and no Predicted Aligned Error (PAE) at
all. So an oracle built on that API cannot publish min_ipae_coi,
iptm_coi, or ptm_coi, the keys the sm_binder, ppi, dna, and
antibody pass criteria all read. Mounting it still turns on
unconditional_refolding and gives a real reward signal for search, but
those four criteria stay indeterminate rather than passing or failing. This
is a limit of the hosted endpoint, not of this NIM — refer to Beyond a
Hosted API.
Boltz-2
Calls the Boltz-2 NIM’s NVCF-hosted
endpoint. Its response carries real per-chain and per-chain-pair confidence
(chains_ptm_scores and pair_chains_iptm_scores, keyed by chain
position), which lets an oracle built on it publish real ptm_coi and
iptm_coi values, something the OpenFold3 API cannot offer. This is also
the worked example of One File, Multiple Oracles:
oracle_boltz2 (general) and oracle_boltz2_ppi (interface-specific)
share every function that follows except oracles() itself and which fold
callable each entry names. This is the complete, working file:
Boltz-2 returns pLDDT/pTM/ipTM as a 0–1 fraction; this NIM’s pass
criteria are written against the 0–100 convention (for example,
ptm_coi: {min: 80.0}). Multiply every value by 100 before returning it;
this was verified against a live response of complex_plddt_scores: [0.989],
which is a near-perfect fold, not a near-zero one.
Like OpenFold3, Boltz-2’s API documents its PAE/PDE fields as always null
in the JSON response: full matrices are only ever written to the serving
container’s own local filesystem, unreachable from an HTTP client. So even
with real ptm_coi/iptm_coi, min_ipae_coi remains unavailable and
sm_binder/ppi/dna’s pass criteria stay indeterminate through this
oracle too. Both hosted APIs share this ceiling; refer to Beyond a Hosted
API.
Verified live: a two-chain design refolded through the NVCF-hosted endpoint
returned oracle_boltz2.overall_plddt: 98.92, oracle_boltz2.overall_ptm: 68.14, with model.oracle: ["oracle_boltz2"] in the response.
Beyond a Hosted API
Both files above are bounded by what a hosted HTTP API chooses to return,
which is why neither can publish min_ipae_coi and why the four
confidence-dependent pass criteria stay indeterminate under them. That
ceiling belongs to those endpoints, not to this NIM: the mount contract makes
no assumption about where the folding model runs.
A reward.py whose prepare_oracle() connects to a predictor running as its
own process — a sidecar in the same pod, or a daemon beside the container —
reads that predictor’s full output, PAE included, and can declare the
per-chain and chain-pair keys every pass criterion needs. The same
oracles() / prepare_oracle() / fold shape covers that case, an
in-process model and a remote HTTP API without changing; what differs is only
what the underlying predictor exposes and what the file therefore declares in
published_keys.
Deploying and reaching such a runtime is an operator responsibility. This NIM provides the seam, not the predictor.
Validation and Error Behavior
A reward key naming an oracle this deployment does not run, or a key that
oracle does not publish, is rejected with a 422 at request time: before
any GPU work starts, not minutes into a search that would otherwise score
-inf and abort. A design an oracle declines (for example, an unrepresentable
ligand atom) leaves that design’s pass criteria indeterminate
(all_metrics_present: 0) rather than failed; aggregated_metrics’
denominator excludes it, so a .pass row stays a measured success rate
rather than becoming a lower bound.