Serving OpenMed over REST
openmed.service is a hardened FastAPI app exposing OpenMed's NER, PII
extraction, and de-identification over HTTP. It is built to be self-hosted:
models run on-device, there's no telemetry, and the request schemas reject raw
PHI from spilling into errors. Use it when callers need request/response
inference; use batch-processing-clinical-text for corpora.
When to use this skill
To put OpenMed behind an HTTP endpoint your own apps call — an internal de-id
microservice, an NER backend, a containerized inference tier. For agent/tool
integration prefer the MCP server (deploying-openmed-mcp); for offline bulk
work use batch processing.
Quick start
pip install "openmed[service]" # FastAPI + uvicorn + pydantic
# Launch the ASGI app (factory create_app, or the module-level `app`)
uvicorn openmed.service.app:app --host 127.0.0.1 --port 8000
# Or build it in-process (e.g. to mount under a parent app / add middleware):
from openmed.service import create_app
app = create_app()
curl -s localhost:8000/health
# {"status":"ok","service":"openmed-rest","version":"...","profile":"prod"}
curl -s localhost:8000/analyze -H 'content-type: application/json' -d '{
"text": "Patient received 75mg clopidogrel for NSTEMI.",
"model_name": "disease_detection_superclinical"
}'
curl -s localhost:8000/pii/deidentify -H 'content-type: application/json' -d '{
"text": "John Doe called 555-123-4567 on 01/15/2020.",
"method": "mask"
}'
Endpoints (confirmed in openmed/service/app.py)
| Method & path |
Purpose |
Request schema |
GET /health |
liveness + version + active profile |
— |
GET /models/loaded |
cache/keep-alive status of resident models |
— |
POST /models/unload |
unload one model or all inactive models |
ModelUnloadRequest (model_name or all=true) |
POST /analyze |
clinical NER |
AnalyzeRequest |
POST /pii/extract |
detect PII/PHI spans |
PIIExtractRequest |
POST /pii/deidentify |
mask/remove/replace/hash/shift-dates PHI |
PIIDeidentifyRequest |
Request fields (from openmed/service/schemas.py, strict — unknown fields are
rejected):
AnalyzeRequest: text (required), model_name
("disease_detection_superclinical"), confidence_threshold (0.0),
group_entities, aggregation_strategy (simple|first|average|max),
sentence_detection, sentence_language, sentence_clean,
use_fast_tokenizer, keep_alive.
PIIExtractRequest: text, model_name
(default OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1),
confidence_threshold (0.5), use_smart_merging, lang
(en/fr/de/it/es/nl/hi/te/pt/ar/ja/tr), normalize_accents, keep_alive.
PIIDeidentifyRequest: same base plus method
(mask|remove|replace|hash|shift_dates, default mask),
confidence_threshold (0.7), keep_year, shift_dates, date_shift_days,
keep_mapping, policy, use_smart_merging, use_safety_sweep.
Responses are the OpenMed result to_dict() (e.g. {text, entities[...], ...}).
Errors use a stable envelope: {"error": {"code", "message", "details"}} with
422 validation_error, 400 bad_request, 504 timeout, 500 internal_error.
Configuring the runtime (env vars)
ServiceRuntime.from_env() reads the process environment at startup
(openmed/service/runtime.py):
| Env var |
Effect |
OPENMED_PROFILE |
config profile (prod default) |
OPENMED_SERVICE_PRELOAD_MODELS |
comma list of models to warm at startup |
OPENMED_SERVICE_KEEP_ALIVE |
default idle keep-alive before unload |
OPENMED_SERVICE_MAX_RESIDENT_MODELS |
cap resident models (warm pool) |
OPENMED_SERVICE_BATCHING_ENABLED |
enable dynamic request batching |
OPENMED_SERVICE_BATCH_MAX_SIZE |
max dynamic batch size (default 8) |
OPENMED_SERVICE_BATCH_MAX_WAIT_MS |
batch-collection window (default 5ms) |
OPENMED_SERVICE_PRELOAD_MODELS="disease_detection_superclinical" \
OPENMED_SERVICE_BATCHING_ENABLED=true \
uvicorn openmed.service.app:app --host 0.0.0.0 --port 8000
Preloading avoids first-request latency; the warm pool keeps hot models resident
and idle-unloads the rest. /analyze and /pii/extract coalesce concurrent
requests when batching is enabled.
Workflow
- Install + launch.
pip install "openmed[service]", then run
uvicorn openmed.service.app:app (or build with create_app()).
- Configure the runtime via env vars before start: set
OPENMED_PROFILE, preload your hot models, and decide keep-alive / max
resident / batching to fit the box.
- Front it with auth/TLS. Place a reverse proxy or gateway (API keys/mTLS,
CORS allow-list) ahead of the app — it has none built in.
- Health-check + warm. Poll
GET /health; preloaded models warm during
the lifespan startup so the first real request isn't cold.
- Call the endpoints (
/analyze, /pii/extract, /pii/deidentify) with
the strict JSON schemas; handle the {"error": {...}} envelope.
- Manage memory with
GET /models/loaded and POST /models/unload as
traffic shifts between models.
Containerizing
FROM python:3.11-slim
RUN pip install --no-cache-dir "openmed[service]"
ENV OPENMED_SERVICE_PRELOAD_MODELS="disease_detection_superclinical"
EXPOSE 8000
CMD ["uvicorn", "openmed.service.app:app", "--host", "0.0.0.0", "--port", "8000"]
Bake/mount the model cache so containers don't re-download on every start; the
service runs offline after that.
Hand-off to / from OpenMed
- Same engine, different surface:
/analyze → openmed.analyze_text,
/pii/extract → openmed.extract_pii, /pii/deidentify →
openmed.deidentify. Results match the library exactly.
- Agents/tools: for Claude Code / Codex / chat clients, expose the same
capabilities as MCP tools instead (
deploying-openmed-mcp).
- Bulk: for corpora, call
batch-processing-clinical-text in a worker, not
per-request HTTP.
Edge cases & gotchas
- No built-in auth/CORS/TLS. The app ships hardened input validation but no
authentication. Put it behind your own reverse proxy / API gateway (mTLS,
API keys, CORS allow-list) before any real traffic. Bind
127.0.0.1 for
local use; only expose 0.0.0.0 behind that proxy.
- No-PHI logging. Don't add request/response body logging — that's PHI.
The error envelope is designed to avoid echoing input; keep it that way. Log
status codes, timings, and model names only.
- Strict schemas. Unknown JSON fields are rejected (
extra="forbid"); a bad
lang/method/model_name returns 422/400 with a field-level reason.
- Cold start vs memory. Preloading + a high
MAX_RESIDENT_MODELS trades RAM
for latency; tune to the box.
- Timeouts return
504 per the profile's configured timeout; long inputs
may need a larger profile or pre-chunking.
keep_mapping/policy outputs are sensitive. A de-id response with a
mapping re-identifies patients — only enable it for trusted callers and store
the mapping securely, never in service logs.
Standards & references
1---2name: serving-openmed-rest-api3description: Stand up OpenMed's FastAPI REST service for clinical NER, PII extraction, and de-identification, with health checks, model keep-alive/unload, optional dynamic batching, and no-PHI logging. Use when the user wants to serve OpenMed over HTTP, deploy a de-id/NER REST API, run an inference endpoint for clinical text, add a /analyze or /pii/deidentify route, or containerize OpenMed as a service. Covers the service extra, launching create_app with uvicorn, the real endpoints (/health, /analyze, /pii/extract, /pii/deidentify, /models/loaded, /models/unload), request/response shapes, ServiceRuntime env-var configuration, and self-hosted auth/CORS/TLS notes.4license: Apache-2.05---67# Serving OpenMed over REST89`openmed.service` is a hardened **FastAPI** app exposing OpenMed's NER, PII10extraction, and de-identification over HTTP. It is built to be **self-hosted**:11models run on-device, there's no telemetry, and the request schemas reject raw12PHI from spilling into errors. Use it when callers need request/response13inference; use `batch-processing-clinical-text` for corpora.1415## When to use this skill1617To put OpenMed behind an HTTP endpoint your own apps call — an internal de-id18microservice, an NER backend, a containerized inference tier. For agent/tool19integration prefer the MCP server (`deploying-openmed-mcp`); for offline bulk20work use batch processing.2122## Quick start2324```bash25pip install "openmed[service]" # FastAPI + uvicorn + pydantic2627# Launch the ASGI app (factory create_app, or the module-level `app`)28uvicorn openmed.service.app:app --host 127.0.0.1 --port 800029```3031```python32# Or build it in-process (e.g. to mount under a parent app / add middleware):33from openmed.service import create_app34app = create_app()35```3637```bash38curl -s localhost:8000/health39# {"status":"ok","service":"openmed-rest","version":"...","profile":"prod"}4041curl -s localhost:8000/analyze -H 'content-type: application/json' -d '{42 "text": "Patient received 75mg clopidogrel for NSTEMI.",43 "model_name": "disease_detection_superclinical"44}'4546curl -s localhost:8000/pii/deidentify -H 'content-type: application/json' -d '{47 "text": "John Doe called 555-123-4567 on 01/15/2020.",48 "method": "mask"49}'50```5152## Endpoints (confirmed in `openmed/service/app.py`)5354| Method & path | Purpose | Request schema |55| --- | --- | --- |56| `GET /health` | liveness + version + active profile | — |57| `GET /models/loaded` | cache/keep-alive status of resident models | — |58| `POST /models/unload` | unload one model or all inactive models | `ModelUnloadRequest` (`model_name` or `all=true`) |59| `POST /analyze` | clinical NER | `AnalyzeRequest` |60| `POST /pii/extract` | detect PII/PHI spans | `PIIExtractRequest` |61| `POST /pii/deidentify` | mask/remove/replace/hash/shift-dates PHI | `PIIDeidentifyRequest` |6263Request fields (from `openmed/service/schemas.py`, strict — unknown fields are64rejected):6566- **`AnalyzeRequest`**: `text` (required), `model_name`67 (`"disease_detection_superclinical"`), `confidence_threshold` (0.0),68 `group_entities`, `aggregation_strategy` (`simple|first|average|max`),69 `sentence_detection`, `sentence_language`, `sentence_clean`,70 `use_fast_tokenizer`, `keep_alive`.71- **`PIIExtractRequest`**: `text`, `model_name`72 (default `OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1`),73 `confidence_threshold` (0.5), `use_smart_merging`, `lang`74 (`en/fr/de/it/es/nl/hi/te/pt/ar/ja/tr`), `normalize_accents`, `keep_alive`.75- **`PIIDeidentifyRequest`**: same base plus `method`76 (`mask|remove|replace|hash|shift_dates`, default `mask`),77 `confidence_threshold` (0.7), `keep_year`, `shift_dates`, `date_shift_days`,78 `keep_mapping`, `policy`, `use_smart_merging`, `use_safety_sweep`.7980Responses are the OpenMed result `to_dict()` (e.g. `{text, entities[...], ...}`).81Errors use a stable envelope: `{"error": {"code", "message", "details"}}` with82`422` validation_error, `400` bad_request, `504` timeout, `500` internal_error.8384## Configuring the runtime (env vars)8586`ServiceRuntime.from_env()` reads the process environment at startup87(`openmed/service/runtime.py`):8889| Env var | Effect |90| --- | --- |91| `OPENMED_PROFILE` | config profile (`prod` default) |92| `OPENMED_SERVICE_PRELOAD_MODELS` | comma list of models to warm at startup |93| `OPENMED_SERVICE_KEEP_ALIVE` | default idle keep-alive before unload |94| `OPENMED_SERVICE_MAX_RESIDENT_MODELS` | cap resident models (warm pool) |95| `OPENMED_SERVICE_BATCHING_ENABLED` | enable dynamic request batching |96| `OPENMED_SERVICE_BATCH_MAX_SIZE` | max dynamic batch size (default 8) |97| `OPENMED_SERVICE_BATCH_MAX_WAIT_MS` | batch-collection window (default 5ms) |9899```bash100OPENMED_SERVICE_PRELOAD_MODELS="disease_detection_superclinical" \101OPENMED_SERVICE_BATCHING_ENABLED=true \102uvicorn openmed.service.app:app --host 0.0.0.0 --port 8000103```104105Preloading avoids first-request latency; the warm pool keeps hot models resident106and idle-unloads the rest. `/analyze` and `/pii/extract` coalesce concurrent107requests when batching is enabled.108109## Workflow1101111. **Install + launch.** `pip install "openmed[service]"`, then run112 `uvicorn openmed.service.app:app` (or build with `create_app()`).1132. **Configure the runtime** via env vars before start: set114 `OPENMED_PROFILE`, preload your hot models, and decide keep-alive / max115 resident / batching to fit the box.1163. **Front it with auth/TLS.** Place a reverse proxy or gateway (API keys/mTLS,117 CORS allow-list) ahead of the app — it has none built in.1184. **Health-check + warm.** Poll `GET /health`; preloaded models warm during119 the lifespan startup so the first real request isn't cold.1205. **Call the endpoints** (`/analyze`, `/pii/extract`, `/pii/deidentify`) with121 the strict JSON schemas; handle the `{"error": {...}}` envelope.1226. **Manage memory** with `GET /models/loaded` and `POST /models/unload` as123 traffic shifts between models.124125## Containerizing126127```dockerfile128FROM python:3.11-slim129RUN pip install --no-cache-dir "openmed[service]"130ENV OPENMED_SERVICE_PRELOAD_MODELS="disease_detection_superclinical"131EXPOSE 8000132CMD ["uvicorn", "openmed.service.app:app", "--host", "0.0.0.0", "--port", "8000"]133```134135Bake/mount the model cache so containers don't re-download on every start; the136service runs offline after that.137138## Hand-off to / from OpenMed139140- **Same engine, different surface:** `/analyze` → `openmed.analyze_text`,141 `/pii/extract` → `openmed.extract_pii`, `/pii/deidentify` →142 `openmed.deidentify`. Results match the library exactly.143- **Agents/tools:** for Claude Code / Codex / chat clients, expose the same144 capabilities as MCP tools instead (`deploying-openmed-mcp`).145- **Bulk:** for corpora, call `batch-processing-clinical-text` in a worker, not146 per-request HTTP.147148## Edge cases & gotchas149150- **No built-in auth/CORS/TLS.** The app ships hardened input validation but no151 authentication. Put it behind your own reverse proxy / API gateway (mTLS,152 API keys, CORS allow-list) before any real traffic. Bind `127.0.0.1` for153 local use; only expose `0.0.0.0` behind that proxy.154- **No-PHI logging.** Don't add request/response body logging — that's PHI.155 The error envelope is designed to avoid echoing input; keep it that way. Log156 status codes, timings, and model names only.157- **Strict schemas.** Unknown JSON fields are rejected (`extra="forbid"`); a bad158 `lang`/`method`/`model_name` returns `422`/`400` with a field-level reason.159- **Cold start vs memory.** Preloading + a high `MAX_RESIDENT_MODELS` trades RAM160 for latency; tune to the box.161- **Timeouts return `504`** per the profile's configured `timeout`; long inputs162 may need a larger profile or pre-chunking.163- **`keep_mapping`/`policy` outputs are sensitive.** A de-id response with a164 mapping re-identifies patients — only enable it for trusted callers and store165 the mapping securely, never in service logs.166167## Standards & references168169- FastAPI: https://fastapi.tiangolo.com/170- Uvicorn (ASGI server): https://www.uvicorn.org/171- OpenAPI (the service auto-serves `/docs` and `/openapi.json`):172 https://www.openapis.org/173- HIPAA de-identification, 45 CFR 164.514(b):174 https://www.hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification/index.html175- OpenMed source: `openmed/service/app.py` (routes), `openmed/service/runtime.py`176 (`ServiceRuntime`), `openmed/service/schemas.py` (request models).