OpenMed delivers state-of-the-art biomedical and clinical LLMs that rival proprietary enterprise stacks, unifying model discovery, advanced extractions, and one-line orchestration.
Project description
Local-first healthcare AI that never leaves the device
Turn clinical text into structured insight with one line of code.
Entity extraction, PII de-identification, and 1,000+ specialized medical models that run entirely on
your own hardware — from a one-liner in Python to native Swift and Kotlin apps,
REST/gRPC services, React Native, and browser token classification through
Transformers.js/WebGPU.
No cloud. No vendor lock-in. No patient data leaving your network.
1,000+ models · 15 model-backed PII languages · 247 PII checkpoints · 100% on-device · Apache-2.0
English · 简体中文 · Español · Français · Deutsch · Italiano · Português · Nederlands · العربية · हिन्दी · తెలుగు · 日本語 · Türkçe · فارسی
See it in action
OpenMed runs entirely on the device — clinical text never leaves it. Here it is on iPhone, fully offline:
On iPhone via OpenMedKit — scan a clinical note, de-identify it, and extract clinical signals, all locally with Apple MLX. Nothing is uploaded.
Real-time PII de-identification — the Nemotron Privacy Filter redacting names, addresses, IDs, and billing data from a clinical discharge packet, entirely on-device. (All values shown are synthetic.)
30-second example
from openmed import analyze_text
result = analyze_text(
"Patient started on imatinib for chronic myeloid leukemia.",
model_name="disease_detection_superclinical",
)
for entity in result.entities:
print(f"{entity.label:<12} {entity.text:<28} {entity.confidence:.2f}")
# DISEASE chronic myeloid leukemia 0.98
# DRUG imatinib 0.95
A state-of-the-art clinical NER model running locally — no API key, no network call.
Why OpenMed?
| OpenMed | Cloud medical APIs | |
|---|---|---|
| Runs on your device / servers | ✅ | ❌ |
| Patient data leaves your network | Never | Sent to the vendor |
| Cost | Free & open-source | Per-call pricing |
| Specialized medical models | 1,000+ | Limited |
| Model-backed PII languages | 15 | Varies |
| Offline / air-gapped | ✅ | ❌ |
| Apple Silicon (MLX) acceleration | ✅ | n/a |
| Native iOS / macOS apps | ✅ via OpenMedKit | ❌ |
| Browser/WebGPU token classification | ✅ via Transformers.js | Varies |
| Vendor lock-in | None — Apache-2.0 | Yes |
- Specialized models — 1,000+ curated biomedical & clinical models, many outperforming proprietary stacks.
- HIPAA-aware de-identification — all 18 Safe Harbor identifiers, smart entity merging, format-preserving fakes.
- Runs everywhere — CPU, CUDA, Apple Silicon (MLX), iOS/macOS via OpenMedKit, Android/Kotlin, React Native, REST/gRPC services, and browser/WebGPU bundles via Transformers.js.
- One-line deployment — Python API, Dockerized REST service, or batch pipelines.
- Zero lock-in — Apache-2.0, your infrastructure, your data.
On-device on Apple — Swift, MLX & iOS
OpenMed is built to run where your data already lives. On Apple hardware it accelerates with MLX, and it ships straight into iPhone, iPad, and Mac apps through OpenMedKit — so PII detection and clinical extraction happen fully offline, on the device.
// Add OpenMedKit to your app
dependencies: [
.package(url: "https://github.com/maziyarpanahi/openmed.git", from: "1.8.0"),
]
Expected result: Swift Package Manager resolves OpenMedKit and makes
import OpenMedKit available to your app target.
- MLX runtime for PII token classification, the Privacy Filter family, experimental GLiNER-family zero-shot tasks, and Python MLX-LM text generation with Laneformer; includes a CoreML fallback path for supported token-classification artifacts.
- One model name, every platform — MLX model names automatically fall back to the matching PyTorch checkpoint on non-Apple hardware.
- Python on Apple Silicon too:
pip install "openmed[mlx]".
Guides: MLX backend · OpenMedKit (Swift) · CoreML export
MLX on Apple Silicon: 24–33× faster than CPU PyTorch for the Privacy Filter — median latency per inference step, lower is better.
How it works
flowchart LR
A["Clinical text"] --> B["OpenMed<br/>(100% on-device)"]
B --> C["Medical entities"]
B --> D["PII detected"]
B --> E["De-identified text"]
style B fill:#0D6E6E,stroke:#0A5656,stroke-width:2px,color:#ffffff
style C fill:#D6EBEB,stroke:#0D6E6E,color:#0E1116
style D fill:#F7DCD8,stroke:#C5453A,color:#0E1116
style E fill:#F5E27A,stroke:#A9A088,color:#0E1116
Rendered result: a local clinical-text pipeline that returns medical entities, PII findings, and de-identified text without sending data to a cloud API.
Quick start
# Core + Hugging Face runtime (Linux, macOS, Windows; CPU or CUDA)
pip install "openmed[hf]"
# Add the REST service
pip install "openmed[hf,service]"
# Apple Silicon acceleration (MLX)
pip install "openmed[mlx]"
Expected result:
Successfully installed openmed-...
|
Python API from openmed import analyze_text
result = analyze_text(
"Patient received 75mg "
"clopidogrel for NSTEMI.",
model_name=
"pharma_detection_superclinical",
)
print([(e.label, e.text) for e in result.entities])
Example output: [('DRUG', 'clopidogrel'), ('CONDITION', 'NSTEMI')]
|
REST service uvicorn openmed.service.app:app \
--host 0.0.0.0 --port 8080
Example output: INFO: Uvicorn running on http://0.0.0.0:8080
GET /health -> 200 OK
|
Batch from openmed import BatchProcessor
p = BatchProcessor(
model_name=
"disease_detection_superclinical",
group_entities=True,
)
results = p.process_texts([...])
print(len(results), sum(len(r.entities) for r in results))
print([(e.label, e.text) for e in results[0].entities[:1]])
Example output: 3 7
[('DISEASE', 'leukemia')]
|
Browser / WebGPU
Package ONNX token-classification exports for in-browser inference through Transformers.js:
python -m openmed.onnx.convert \
--model OpenMed/example-token-classifier \
--output dist/example-onnx \
--include-transformersjs
Example output:
Exported Transformers.js bundle to dist/example-onnx
import { pipeline } from "@huggingface/transformers";
const detector = await pipeline(
"token-classification",
"/models/openmed-pii/transformersjs",
{ device: "webgpu" },
);
const entities = await detector("Patient Casey Example called 212-555-0198.");
console.log(entities.slice(0, 2));
Example output:
[
{ entity: "NAME", word: "Casey Example", score: 0.99 },
{ entity: "PHONE", word: "212-555-0198", score: 0.98 },
]
Offline / air-gapped? Point model_name (or model_id) at a local directory and OpenMed loads it without contacting the Hugging Face Hub:
from openmed import OpenMedConfig, analyze_text
result = analyze_text(
"Patient presents with chronic myeloid leukemia and Type 2 diabetes.",
model_id="./models/OpenMed-NER-DiseaseDetect-SuperClinical-434M",
config=OpenMedConfig(device="cpu"),
)
for entity in result.entities:
print(f"{entity.label:<12} {entity.text:<28} {entity.confidence:.2f}")
Example output:
DISEASE chronic myeloid leukemia 0.98
DISEASE Type 2 diabetes 0.96
Because model_id points to a local directory, this example does not contact
the Hugging Face Hub or any external model provider.
Models
A curated registry of specialized medical NER models — browse the full catalog.
| Model | Specialization | Entity types | Size |
|---|---|---|---|
disease_detection_superclinical |
Disease & conditions | DISEASE, CONDITION, DIAGNOSIS | 434M |
pharma_detection_superclinical |
Drugs & medications | DRUG, MEDICATION, TREATMENT | 434M |
pii_superclinical_large |
PII & de-identification | NAME, DATE, SSN, PHONE, EMAIL, ADDRESS | 434M |
anatomy_detection_electramed |
Anatomy & body parts | ANATOMY, ORGAN, BODY_PART | 109M |
gene_detection_genecorpus |
Genes & proteins | GENE, PROTEIN | 109M |
Privacy: PII detection & de-identification
from openmed import extract_pii, deidentify
text = "Patient: John Doe, DOB: 01/15/1970, SSN: 123-45-6789"
# Extract PII with smart merging (prevents tokenization fragmentation)
result = extract_pii(text, model_name="pii_superclinical_large", use_smart_merging=True)
print([(e.label, e.text) for e in result.entities])
# De-identify with the method you need
print(deidentify(text, method="mask").deidentified_text)
print(deidentify(text, method="replace").deidentified_text)
print(deidentify(text, method="hash").deidentified_text)
print(deidentify(text, method="shift_dates", date_shift_days=180).deidentified_text)
Example output:
[('NAME', 'John Doe'), ('DATE', '01/15/1970'), ('SSN', '123-45-6789')]
Patient: [NAME], DOB: [DATE], SSN: [SSN]
Patient: Emily Chen, DOB: 03/22/1985, SSN: 456-78-9012
Patient: 6b8f...c4a1, DOB: 48b1...91de, SSN: 3f13...e912
Patient: John Doe, DOB: 07/14/1970, SSN: 123-45-6789
- Smart entity merging keeps
01/15/1970whole instead of fragmenting it. - Policy-aware pipelines add HIPAA/GDPR/research profiles, calibrated thresholds, signed audit reports, redaction previews, and minimum-necessary action selection.
- Faker-backed obfuscation with custom clinical-ID providers (CPF, CNPJ, BSN, NIR, Codice Fiscale, NIE, Aadhaar, Steuer-ID, NPI).
- HIPAA: all 18 Safe Harbor identifiers, configurable confidence thresholds.
- Batch and streaming PII: extract or de-identify across many documents with
BatchProcessor(operation="extract_pii" | "deidentify", batch_size=16)or incremental streaming helpers.
Batch processing — up to 3.3× higher throughput on CPU and 2.2× on MLX vs. one document at a time.
Complete PII notebook · Smart merging · Anonymization quickstart
Privacy Filter family — three model families on the OpenAI Privacy Filter architecture
Same model code (gpt-oss-style sparse-MoE transformer with local attention, sink tokens, RoPE+YaRN, tiktoken o200k_base), different training data. All route through the same extract_pii() / deidentify() API — only model_name= changes.
openai/privacy-filter is a Hugging Face model identifier for local weights;
using it here does not call the OpenAI API.
| Variant | PyTorch (CPU + CUDA) | MLX (Apple Silicon) | MLX 8-bit |
|---|---|---|---|
| OpenAI Privacy Filter | openai/privacy-filter |
OpenMed/privacy-filter-mlx |
…-mlx-8bit |
| Nemotron-PII fine-tune | OpenMed/privacy-filter-nemotron |
…-nemotron-mlx |
…-nemotron-mlx-8bit |
| OpenMed Multilingual | OpenMed/privacy-filter-multilingual |
…-multilingual-mlx |
…-multilingual-mlx-8bit |
from openmed import extract_pii
text = "Patient Sarah Connor (DOB: 03/15/1985) at MRN 4471882."
variants = {
"baseline": extract_pii(text, model_name="openai/privacy-filter"),
"nemotron": extract_pii(text, model_name="OpenMed/privacy-filter-nemotron"),
"mlx": extract_pii(text, model_name="OpenMed/privacy-filter-mlx"),
}
print([(e.label, e.text) for e in variants["baseline"].entities])
Example output:
[('NAME', 'Sarah Connor'), ('DATE', '03/15/1985'), ('ID', '4471882')]
On non-Apple-Silicon hosts, MLX model names are automatically substituted with the matching PyTorch checkpoint (with a one-time warning) — ship one model name, run anywhere. See Privacy Filter architecture & backend routing.
Multilingual PII (15 model-backed languages)
Extraction and de-identification support 15 supported PII language codes:
ar, de, en, es, fr, he, hi, id, it, ja, nl, pt, te, th, and tr — 247 PII checkpoints total.
These are the model-backed PII language allow-list.
OpenMed also includes validator-backed national-ID coverage for additional
ID-only locales such as Polish, Korean, Latvian, Slovak, Malay, Filipino, and
Danish.
python -c "from openmed import extract_pii; print([(e.label, e.text) for e in extract_pii('Dr. Pedro Almeida, CPF: 123.456.789-09, email: pedro@hospital.pt', lang='pt').entities])"
Example output:
[('NAME', 'Pedro Almeida'), ('ID', '123.456.789-09'), ('EMAIL', 'pedro@hospital.pt')]
Show per-language examples (Portuguese, Dutch, Hindi, Arabic, Japanese, Turkish)
from openmed import extract_pii
portuguese = extract_pii("Paciente: Pedro Almeida, CPF: 123.456.789-09, telefone: +351 912 345 678", lang="pt", use_smart_merging=True)
dutch = extract_pii("Patiënt: Eva de Vries, BSN: 123456782, telefoon: +31 6 12345678", lang="nl", use_smart_merging=True)
hindi = extract_pii("रोगी: अनीता शर्मा, फोन: +91 9876543210, पता: नई दिल्ली 110001", lang="hi", use_smart_merging=True)
arabic = extract_pii("المريضة ليلى حسن، الهاتف +20 10 1234 5678، الرقم القومي 29801011234567.", lang="ar", use_smart_merging=True)
japanese = extract_pii("患者 佐藤 花子、電話 +81 90 1234 5678、マイナンバー 1234 5678 9012.", lang="ja", use_smart_merging=True)
turkish = extract_pii("Hasta Ayşe Yılmaz, telefon +90 532 123 45 67, TCKN 10000000146.", lang="tr", use_smart_merging=True)
for r in (portuguese, dutch, hindi, arabic, japanese, turkish):
print([(e.label, e.text) for e in r.entities])
Example output:
[('NAME', 'Pedro Almeida'), ('ID', '123.456.789-09'), ('PHONE', '+351 912 345 678')]
[('NAME', 'Eva de Vries'), ('ID', '123456782'), ('PHONE', '+31 6 12345678')]
[('NAME', 'अनीता शर्मा'), ('PHONE', '+91 9876543210'), ('ADDRESS', 'नई दिल्ली 110001')]
[('NAME', 'ليلى حسن'), ('PHONE', '+20 10 1234 5678'), ('ID', '29801011234567')]
[('NAME', '佐藤 花子'), ('PHONE', '+81 90 1234 5678'), ('ID', '1234 5678 9012')]
[('NAME', 'Ayşe Yılmaz'), ('PHONE', '+90 532 123 45 67'), ('ID', '10000000146')]
REST API
A Docker-friendly FastAPI service with request validation, shared pipeline preload, and unified error envelopes.
pip install "openmed[hf,service]"
uvicorn openmed.service.app:app --host 0.0.0.0 --port 8080
# or with Docker
docker build -t openmed:1.8.0 .
docker run --rm -p 8080:8080 -e OPENMED_PROFILE=prod openmed:1.8.0
Example output:
INFO: Uvicorn running on http://0.0.0.0:8080
curl -X POST http://127.0.0.1:8080/pii/extract \
-H "Content-Type: application/json" \
-d '{"text":"Paciente: Maria Garcia, DNI: 12345678Z","lang":"es"}'
Abbreviated example response:
{
"text": "Paciente: Maria Garcia, DNI: 12345678Z",
"entities": [
{"text": "Maria Garcia", "label": "NAME", "confidence": 0.99, "start": 10, "end": 22},
{"text": "12345678Z", "label": "ID", "confidence": 0.98, "start": 29, "end": 38}
],
"model_name": "OpenMed/privacy-filter-multilingual"
}
Model lifecycle and service controls: free memory on demand with
GET /models/loaded, POST /models/unload, and a keep_alive idle window;
v1.8 also includes API-key/JWT auth, no-PHI request logging, tracing, gRPC,
async jobs, webhooks, warm pools, dynamic batching, request coalescing, rate
and concurrency limits, /livez, /readyz, and opt-in metrics:
OPENMED_SERVICE_KEEP_ALIVE=10m uvicorn openmed.service.app:app --host 0.0.0.0 --port 8080
curl -X POST http://127.0.0.1:8080/models/unload -H "Content-Type: application/json" -d '{"all":true}'
Example response:
{
"unloaded": true,
"released": {"models": 1, "tokenizers": 1, "pipelines": 1},
"active_models": {}
}
See the full REST service guide.
Documentation
Full guides at openmed.life/docs.
Meet the mascot
OpenMed's guardian is a fluffy Persian cat styled as a tiny Avicenna (Ibn Sina) — the great Persian physician whose Canon of Medicine was the world's standard medical text for some 600 years. He keeps watch over the open book of medical knowledge, in a palette built around Persian turquoise (fīrūza): a local-first guardian for your most private data.
Contributing
Contributions welcome — bug reports, feature requests, and PRs alike. Please read the Contributing guide and our Code of Conduct first.
- Open an issue
- Contributing guide · Code of Conduct · Security policy
- Translations welcome — help complete the other-language READMEs linked in the switcher at the top.
Security
Found a vulnerability? OpenMed redacts PHI, so a redaction bypass or PHI/PII leak is a security issue — please report it privately, never as a public issue. See SECURITY.md for the responsible-disclosure policy and the private reporting form. Never include real patient data in a report.
Credits
OpenMed builds on excellent open-source work — particular thanks to OpenAI (the Privacy Filter architecture), NVIDIA (the Nemotron PII dataset), Hugging Face (transformers, Transformers.js & the model ecosystem), Apple (MLX), and the Faker maintainers.
License
Released under the Apache-2.0 License. Third-party asset notices are recorded in NOTICE.
Citation
@misc{panahi2025openmedneropensourcedomainadapted,
title={OpenMed NER: Open-Source, Domain-Adapted State-of-the-Art Transformers for Biomedical NER Across 12 Public Datasets},
author={Maziyar Panahi},
year={2025},
eprint={2508.01630},
archivePrefix={arXiv},
primaryClass={cs.CL},
url={https://arxiv.org/abs/2508.01630},
}
Expected result: BibTeX-compatible citation metadata for referencing OpenMed in papers, posters, and derived documentation.
Star History
If OpenMed is useful to you, a star helps others discover it.
Built by the OpenMed team
Website · Docs · X / Twitter · LinkedIn
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openmed-1.8.0.tar.gz.
File metadata
- Download URL: openmed-1.8.0.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
811c1aad11f7e44a1d2f37d8dad6ccf033ede2f3c2e9b3f55e995470623822ba
|
|
| MD5 |
48ab430aa92f65b5ae3ed91c2cccf5dd
|
|
| BLAKE2b-256 |
573609dae3cd04c61878e9146b59d8991db20f6939b5dfda4d1ae716ccd8662f
|
File details
Details for the file openmed-1.8.0-py3-none-any.whl.
File metadata
- Download URL: openmed-1.8.0-py3-none-any.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
779fd38ba9200c016fdaa8b8391756717f834687edab2dfdc4a619b95f13950f
|
|
| MD5 |
9d73f2b5d31ee2d3109b51b4587c3917
|
|
| BLAKE2b-256 |
5f609003728d70d5c1c5cfa38bf6bc20ce066618d6db608dcdde5ca5b4c9e681
|