External Async Wrapper Reference Implementation🔗
This page gives a minimum reference implementation for a customer-owned async wrapper.
Use it together with External Async Wrapper on Kubernetes. That page defines the required contract. This page shows the shortest practical implementation shape.
Minimum requirements🔗
Use this minimum interface split:
| Customer wrapper exposes to Hub | Customer wrapper calls downstream |
|---|---|
GET /weights |
GET /weights |
GET /schema |
GET /schema |
POST /ticket |
POST /api/v1/predict-async |
GET /ticket/{uuid} |
GET /api/v1/predict-async/{jobId}/status |
GET /results/{uuid} |
GET /api/v1/predict-async/{jobId}/results |
Make sure to persist Hub-facing ticket state durably.
Required storage🔗
Persist one row or record per Hub-facing ticket with at least:
{
"ticket_uuid": "ticket-123",
"downstream_job_id": "pred_abc123",
"status": "RUNNING",
"last_error": ""
}
This state should survive pod restarts.
Endpoint implementation🔗
GET /weights🔗
Minimum behavior:
- proxy downstream
GET /weightswhen available - otherwise return cached or static weights
Example response:
{
"available_weights": [
{
"model_type": "openfold3",
"version": "3.0.0",
"model_version_id": "apheris-openfold3-preview",
"model_version_tag": "3.0.0-openfold3-by-file",
"description": "OpenFold3-preview model, trained with a 2021-09-30 PDB data cutoff.",
"model_scope": ["inference", "finetuning", "affinity"]
}
]
}
GET /schema🔗
Minimum behavior:
- proxy downstream
GET /schemawhen available - otherwise return cached or static schema
Example response:
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/weights": {},
"/schema": {},
"/api/v1/predict-async": {},
"/api/v1/predict-async/{jobId}/status": {},
"/api/v1/predict-async/{jobId}/results": {}
}
}
The customer wrapper should proxy the full downstream OpenAPI document, not a reduced subset.
POST /ticket🔗
Input from Hub:
{
"request_id": "hub-request-id",
"requestParams": {
"queries": {
"ubiquitin": {
"chains": [
{
"molecule_type": "protein",
"chain_ids": ["A"],
"sequence": "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG",
"msa": "monomer.a3m"
}
]
}
}
},
"modelParams": {
"diffusion_samples": 1,
"seed": 42
},
"inputPath": "request-42/assets",
"modelName": "openfold3",
"weightVersion": "3.0.0"
}
Minimum implementation steps:
- validate request
- generate Hub-facing
ticket_uuid - resolve
inputPathfor downstream - persist ticket with
PENDING - call downstream
POST /api/v1/predict-async - persist downstream
jobId - update ticket to
RUNNING - return
202
Downstream request:
{
"requestParams": {
"queries": {
"ubiquitin": {
"chains": [
{
"molecule_type": "protein",
"chain_ids": ["A"],
"sequence": "MQIFVKTLTGKTITLEVEPSDTIENVKAKIQDKEGIPPDQQRLIFAGKQLEDGRTLSDYNIQKESTLHLVLRLRGG",
"msa": "monomer.a3m"
}
]
}
}
},
"modelParams": {
"diffusion_samples": 1,
"seed": 42
},
"inputPath": "request-42/assets",
"modelName": "openfold3",
"weightVersion": "3.0.0"
}
Downstream success response:
{
"jobId": "pred_abc123",
"jobStatus": "running"
}
Hub-facing response:
{
"uuid": "ticket-123",
"status": "RUNNING",
"request_id": "hub-request-id"
}
GET /ticket/{uuid}🔗
Minimum implementation steps:
- load persisted ticket
- if ticket is terminal, return it
- if ticket has downstream
jobId, poll downstream status - map downstream status to Hub-facing status
- persist status transition
- return Hub-facing payload
Wait/stop rules:
- keep waiting on
PENDING - keep waiting on
RUNNING - stop successfully on
COMPLETED - stop with failure on
FAILEDor anyFAILED_*
Status mapping:
| Downstream status | Hub-facing status |
|---|---|
running |
RUNNING |
completed |
COMPLETED |
failed |
FAILED |
| not yet submitted | PENDING |
Running response:
{
"uuid": "ticket-123",
"status": "RUNNING",
"request_id": "hub-request-id"
}
Failed response:
{
"uuid": "ticket-123",
"status": "FAILED",
"request_id": "hub-request-id",
"error": "FAILED_NIM_REQUEST: timeout contacting downstream wrapper"
}
Downstream failed status payload:
{
"jobId": "pred_abc123",
"jobStatus": "failed",
"outputPath": "job_openfold3_pred_abc123",
"problem": {
"type": "urn:apheris:problem:prediction-failed",
"title": "Prediction failed.",
"detail": "No such file or directory: request-42/assets/monomer.a3m"
}
}
GET /results/{uuid}🔗
Minimum implementation steps:
- load persisted ticket
- require
COMPLETED - call downstream
GET /api/v1/predict-async/{jobId}/results - return Hub-facing result envelope
Response:
{
"uuid": "ticket-123",
"status": "COMPLETED",
"request_id": "hub-request-id",
"results": {
"jobId": "pred_abc123",
"jobStatus": "completed",
"outputPath": "job_openfold3_pred_abc123",
"predictions": [
{
"query_id": "ubiquitin",
"sample_id": 0,
"cif": "data_ubiquitin\n#\n",
"top_level_stats": {
"avg_plddt": 91.2,
"ptm": 0.84,
"iptm": 0.0,
"gpde": 0.23,
"affinity_prediction": null,
"disorder": 0.05,
"has_clash": 0.0,
"sample_ranking_score": 0.84,
"chain_ptm": {},
"chain_pair_iptm": {},
"bespoke_iptm": {}
},
"top_level_metrics": {},
"ligand_metrics_per_chain": {},
"protein_metrics_per_chain": {},
"plddt": [91.2, 90.8],
"pae": [[0.1, 0.2], [0.2, 0.1]],
"pde": [[0.2, 0.3], [0.3, 0.2]],
"atoms_in_chain": {
"A": 602
},
"residues_in_chain": {
"A": 76
}
}
]
}
}
Kubernetes requirement for inputPath🔗
Recommended:
- mount the same PVC into the Hub and downstream wrapper so the same
inputPathworks in both pods
Only use path rewriting if the customer wrapper explicitly translates the Hub-facing inputPath before calling the downstream Apheris wrapper.
Build checklist🔗
Before considering the implementation complete, verify:
-
GET /weightsworks even if downstream execution is scaled down to zero -
GET /schemaworks even if downstream execution is scaled down to zero -
POST /ticketreturns202and a non-emptyuuid -
GET /ticket/{uuid}only returnsPENDING,RUNNING,COMPLETED, orFAILED -
GET /results/{uuid}returns a complete predictions array - raw failure detail is preserved in
error - ticket state survives pod restart