RxDecide-1.7B

Small, auditable clinical decision model: reads a patient record and answers structured questions (contraindications, trial eligibility, guideline checks, dosing conditions, diagnostic criteria) with calibrated confidence and a human-review flag.

License Base model Parameters GPU Research only Demo
Real notes New tasks Synthetic Missed Speed Faster Cost
Python PyTorch Transformers Inputs Outputs Pre-registered

Author: Nandhu S Kumar · Demo: nsk246/RxDecide-demo

Not for clinical use. This is a research model. It has not been validated by clinicians or in any clinical setting, and it must not be used to make or support real patient decisions.

Highlights

  • Small and fast: 1.7B parameters, one L4 GPU, 0.18 s per decision, about $0.04 per 1,000 decisions.
  • Auditable: the model answers only narrow questions; logic, thresholds, units and dates are plain code, and every answer comes with an audit trail.
  • Knows when to ask a human: calibrated confidence and a review flag; on unseen real notes it missed no contraindication among the decisions it made on its own.
  • Flexible: your own facts, rules ("at least 2 of 4"), date windows and outcome labels; free text, JSON, lab tables or FHIR in.
  • No text generation: the model never writes an answer token by token. Each question is one forward pass that reads the probabilities of the answer letters, so there is no sampling, no reasoning text and a fixed, small cost per question.
  • Measured honestly: every test was locked and run once, with the rules written before the run; failures and post-hoc fixes are reported, not hidden.

Contents

  1. What it does
  2. Quick start
  3. Inputs and outputs
  4. How it works
  5. Results
  6. Speed and cost
  7. Training
  8. Limitations and safety findings
  9. Reproducibility
  10. License and data
  11. Glossary
  12. Citation

What it does

You give it a patient record and a structure: the facts to check, how they combine, and the answers you want back. It reads the record, answers each fact, combines them in code, and returns a decision with probabilities, a confidence score, a flag that says whether a person should review it, and an audit trail of every fact it read.

It ships with a reviewed catalog of 75 FDA-label contraindication criteria for 16 drugs (amiodarone, carbamazepine, ciprofloxacin, clarithromycin, clopidogrel, dabigatran, dofetilide, fluconazole, ketorolac, metoclopramide, naproxen, propranolol, sildenafil, simvastatin, ticagrelor, verapamil). You can also pass your own structure for any yes/no, numeric, date-window or multiple-choice question, with your own outcome labels.

The design follows the pattern of small specialized decision models such as Jev: the model only answers narrow typed questions; logic, thresholds, dates and units are handled by code you can read.

Intended use

Intended Out of scope
Research on reliable, auditable clinical decision models Any real patient care, triage or prescribing decision
Pre-screening records for a human reviewer (e.g. flagging possible contraindications or trial exclusions) Unsupervised automation without review of routed cases
Benchmarking small vs large models on structured clinical questions Non-English records; pediatric-only, pregnancy, dose-dependent or time-window drug rules from the catalog
Teaching and demonstrating calibrated, decomposed decision systems Free-text answers, summaries or advice (the model does not generate text)

Hardware. A GPU with at least 12 GB is recommended (peak 10.8 GB measured on long real notes in bf16). It runs on CPU, much more slowly.

Quick start

pip install torch transformers huggingface_hub

The repository contains the model weights and the decision code, a small Python package named meddecide.

import sys, json, torch
from huggingface_hub import snapshot_download
from transformers import AutoTokenizer, AutoModelForCausalLM

repo = snapshot_download("nsk246/RxDecide-1.7B")
sys.path.insert(0, repo)
import meddecide as md

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tok = AutoTokenizer.from_pretrained(repo)
model = AutoModelForCausalLM.from_pretrained(repo, dtype=torch.bfloat16).to(device).eval()
reader = md.FastReader(model, tok, device)
config = json.load(open(f"{repo}/meddecide_config.json"))          # temperatures and review thresholds

1. Contraindications from the built-in catalog

catalog = json.load(open(f"{repo}/data/catalog.json"))              # {"catalog": {criterion_id: {...}}}
labels = json.load(open(f"{repo}/data/labels.json"))["labels"]      # FDA label text per product
products = md.products(catalog)                                      # set_id -> drug, criteria
set_id = next(s for s, p in products.items() if p["drug"] == "verapamil")
reference = next(v["contraindications"] for v in labels.values() if v.get("set_id") == set_id)

note = "72M with sick sinus syndrome, no pacemaker. BP 84/52. Meds: metoprolol."
for d in md.decide_product(reader, note, catalog, set_id, reference, config):
    print(d["criterion_id"], d["decision"], d["confidence"], d["route_to_review"])

2. Your own decision: trial eligibility with a date window and custom labels

structure = {"facts": {
    "adult":      {"type": "numeric", "measure": "age", "unit": "years", "comparator": ">=", "threshold": 18,
                   "extraction_question": "What is the patient's age in years?"},
    "ecog_ok":    {"type": "numeric", "measure": "ECOG performance status", "unit": "", "comparator": "<=", "threshold": 1,
                   "extraction_question": "What is the patient's ECOG performance status?"},
    "chemo_ever": {"type": "boolean", "question": "The patient has ever received chemotherapy"},
    "chemo_date": {"type": "date_within", "event_question": "When did the patient last receive chemotherapy?", "within_days": 180}}}
outcomes = [{"label": "Not eligible", "when": "NOT adult OR NOT ecog_ok OR (chemo_ever AND chemo_date)"},
            {"label": "Eligible", "otherwise": True}]
record = {"as_of": "2026-10-01", "age": 64, "ecog_performance_status": 1, "last_chemotherapy": "2026-02-10"}
r = md.evaluate(reader, record, structure, config, outcomes=outcomes)
print(r["outcome"], r["probabilities"], r["route_to_review"], r["explanation"])
Example output (real output of this release, audit shortened)
{
  "outcome": "Eligible",
  "probabilities": {
    "Not eligible": 0.0,
    "Eligible": 1.0,
    "insufficient": 0.0
  },
  "confidence": 0.9999,
  "route_to_review": false,
  "judgment_needed": {},
  "explanation": "Result: Eligible. Facts read from the note: age 64 years (meets >= 18); ECOG performance status 1 (meets <= 1); The patient has ever received chemotherapy? yes (0.95); When did the patient last receive chemotherapy: 2026-02-10 = 2026-02-10 (not within 180 days of 2026-10-01).",
  "audit": {
    "facts": [
      {
        "name": "adult",
        "type": "numeric",
        "chosen": "64",
        "excluded_candidates": [
          "2026",
          "10",
          "01",
          "1",
          "02"
        ]
      },
      {
        "name": "ecog_ok",
        "type": "numeric",
        "chosen": "1",
        "excluded_candidates": [
          "2026",
          "10",
          "01",
          "64",
          "02"
        ]
      },
      {
        "name": "chemo_ever",
        "type": "boolean",
        "question": "The patient has ever received chemotherapy",
        "probs": {
          "true": 0.9469,
          "false": 0.0215,
          "not_stated": 0.0317
        }
      },
      {
        "name": "chemo_date",
        "type": "date_within",
        "question": "When did the patient last receive chemotherapy?",
        "chosen": "2026-02-10",
        "dates": {
          "2026-02-10": "2026-02-10"
        },
        "as_of": "2026-10-01"
      }
    ]
  }
}

3. "At least k of n" criteria (e.g. SIRS)

outcomes = [{"label": "SIRS criteria met", "when": "ATLEAST 2 OF (fever, tachy, tachypnea, leuko)"},
            {"label": "SIRS criteria not met", "otherwise": True}]

4. Single questions

md.ask_fact(reader, note, "The patient has a pacemaker", config)                       # true / false / not stated
md.ask_choice(reader, note, "What is the patient's smoking status?",
              ["Current smoker", "Former smoker", "Never smoked", "Not stated"], config)  # 2 to 60 options

A FastAPI server is included: MEDDECIDE_DIR=<repo> uvicorn server:app (endpoints /products, /decide, /evaluate, /ask/fact, /ask/value, /ask/choice).

Inputs and outputs

Accepted record formats (all turned into readable lines by code before the model reads them)

Input Example
Free-text note "72M with sick sinus syndrome. BP 84/52 ..."
Flat dict {"vitals": "BP 84/52", "medications": ["metoprolol"]}
Nested JSON {"problems": {"active": [...], "ruled_out": [...]}, "results": [...]}
Lab table (list of records) [{"name": "Creatinine", "value": 177, "unit": "µmol/L", "date": "2026-09-30"}]
FHIR resources or bundle Patient, Condition (with clinical status), MedicationStatement, Observation, AllergyIntolerance, Procedure, Encounter
Several notes prior and current notes in one string

Fact types you can ask

Type What the model does What code does
boolean true / false / not stated logic, medication rule, class expansion
numeric picks the number from the record (or "none") unit conversion, plausibility and label checks, threshold
date_within picks the event date (or "none") resolves relative dates ("3 months ago"), checks the window against the visit date
choice picks one of your options (2 to 60) shortlist for long lists, verification for "None of these"

Output (per decision)

Field Meaning
decision / outcome / answer applies, does_not_apply, insufficient, or your labels / option
probabilities calibrated probability of each answer
confidence (K·p_max − 1)/(K − 1) over the K answers
route_to_review true when a person should check it (see the router below)
explanation built by code from the facts the deciding rule used, e.g. "potassium 5.8 mEq/L (meets > 5.5)"
audit every fact: question, probabilities, chosen number or date, unit conversions, excluded candidates, rules applied, class annotations with FDA citations

How it works

Architecture

  1. Normalizer (code). Structured inputs become readable lines; brand names, combination products and abbreviations are annotated ("Coumadin (warfarin)"); FDA pharmacologic classes are added only when the question names that class; lists under "negative / denied / ruled out" keys and refuted FHIR conditions are written as explicit negatives.
  2. Fact reader (the model). Qwen3-1.7B-Base with LoRA adapters merged into the weights (a weight soup of three seeds). Each fact is one multiple-choice question; the model's next-token probabilities over the option letters are read in a single forward pass and averaged over all option orders, so no text is generated. FastReader runs all orders in one batch and caches the patient note once per record.
  3. Decision logic (code). Temperature-calibrated fact probabilities are combined by three-valued logic (AND, OR, NOT, ATLEAST k) over every combination of fact states, giving a probability for each answer. Code also converts units, rejects impossible or mislabelled numbers, resolves dates, applies the medication rule ("no current medications" clears a current-use fact, with guards), and expands drug classes into their members.
  4. Router (code). A decision goes to human review when confidence < 0.90, when it is "does not apply" with confidence < 0.99 (clearing a contraindication needs more certainty), when an undefined judgment term ("severe", "high risk") is read as not stated, or when a code rule and the model disagree.

No text generation at inference

Qwen3 is an autoregressive (causal) language model: normally it writes an answer one token at a time, each token depending on the ones before. RxDecide keeps the architecture but does not use it that way. At inference it never generates a single token:

  1. Code turns each fact into a multiple-choice question with lettered options (true / false / not stated; or the candidate numbers or dates that code found in the record, plus "none").
  2. The model reads the record and the question once and we take its probabilities for the option letters as the next token, restricted to those letters. That one forward pass is the whole answer.
  3. The question is scored in every option order (all orders in one batch, the patient record cached once) and the probabilities are averaged, so the answer does not depend on which option is listed first.
  4. Everything after that, the rules, the decision, the review flag and even the explanation text, is computed by code.
Usual generative LLM RxDecide
Output free text, written token by token probabilities over a fixed set of answers
Tokens generated per question tens to thousands 0
Forward passes per question one per generated token 1 (record cached and shared)
Sampling / randomness often none (deterministic)
Confidence not built in calibrated probabilities, confidence, review flag
Explanation written by the model built by code from the facts it read

This is why it is fast and cheap (see Speed and cost): a reasoning model in the same comparison wrote about 733 tokens per decision; RxDecide writes none. Training matches this use: the LoRA adapters were trained only to put probability on the correct answer letter, not to generate text.

Results

All numbers below are from evaluations run once on sets the final system never trained or tuned on, unless marked. Rules for each test were written down before it was run. Labels for real notes come from two independent language-model labelers whose answers had to agree and whose quotes had to appear verbatim in the note; no clinician reviewed them.

Real clinical notes (MTSamples, de-identified transcriptions)

Final test: 120 real notes, 1,557 contraindication decisions, 2,088 facts.

Metric Result 95% interval Target Met
Balanced accuracy .861 ≥ .85 ✅
Applies (29) .793 .616 – .902 ≥ .80 ❌ (too few cases to tell)
Does not apply (151) .861 .797 – .907 ≥ .80 ✅
Insufficient (1,377) .928 .913 – .941 ≥ .90 ✅
Accuracy of automatic answers (64% of decisions) .985 ≥ .97 ✅
Automatic missed contraindications 0 0 ✅
Automatic unsafe answers 2 (0 after the fix below) 0 not met on the test; 0 after the post-hoc fix
Fact accuracy .946

Post hoc (found after the test, reported separately): both unsafe answers came from one note where "No medications." appeared under psychiatric history while the medication list named three drugs. The fix (meddecide 0.6.2, included in this release) removes them: re-running the same test gives 0 automatic unsafe, 0 automatic missed, automatic accuracy .987; only that note's decisions changed. Because it was found on the test, this is not an independent result.

Earlier real-note results, for comparison: the previous model trained only on synthetic notes scored .616 balanced on a different 60-note real test; the first real-note-trained version scored .821 on that test.

New decision types and input formats (synthetic structured records)

280 records, four tasks never used during development: trial eligibility (6 criteria, 28-day chemotherapy window), metabolic syndrome (at least 3 of 5), ACE inhibitor in heart failure (3 outcomes), ECOG choice (6 options); each in five formats.

Accuracy (pre-registered labels) Accuracy (corrected labels, post hoc)
All (280) .932 .982
Free text / flat JSON / nested JSON 1.000 / .911 / .929 1.000 / 1.000 / 1.000
FHIR / prior + current notes .893 / .929 .982 / .929
Automatic answers 77.5% at .986, 0 unsafe same

The correction: in three formats the benchmark wrote "no active infection" only as "pneumonia ruled out", which does not establish the fact; the model answered "not stated". 14 labels were corrected to match what the records support. All pre-registered targets (accuracy ≥ .90, every format ≥ .85, automatic accuracy ≥ .97, 0 automatic unsafe) were met before the correction.

Synthetic notes (earlier test sets, re-run once with the final system)

Test Result Automatic answers
Contraindications, 231 decisions .978 (balanced .979) 48% at 1.000; 0 unsafe, 0 missed
Multiple choice, 8–20 options, independently written (75) .987 51% at 1.000
Multiple choice, 7–20 options, templates (120) .883 26% at 1.000
Multiple choice, 30–60 options, templates (66) .939 6% at 1.000

These sets were each used once before to compare earlier versions of the model; the final model never trained on them and no choice was made from these runs.

Model history (each evaluated once on its own test)

Version Main change Key result
v6 facts in code, LoRA letter reader, weight soup unseen drugs .992; realistic-note holdout .967; beat Qwen3-8B prompted directly on synthetic notes (.843 vs .687)
v8A NLL + Brier training on realistic notes synthetic pool test .970
v9 trained on real notes (1,386 fact labels) real-note balanced .821 (v8A on the same test: .616)
v9 + meddecide 0.5–0.6 (this release) code fixes, class expansion, structured inputs, units, dates real-note balanced .861; new tasks .932

Tried and not kept (on record): more real-note data at the same size (v10, worse on development notes), focused reading of long notes, RLCD-style training, a frozen-feature classifier head.

Speed and cost

Same 30 real notes (398 decisions), one NVIDIA L4, bf16, timed with CUDA synchronisation.

System Accuracy Balanced Seconds per decision (median / p95) Decisions per minute GPU memory $ per 1,000 decisions* Automatic unsafe
RxDecide-1.7B .945 .874 0.18 / 0.40 325 10.8 GB $0.04 0
Same pipeline, untrained base model .224 .491 0.18 / 0.40 329 10.8 GB $0.04 0
Qwen3-8B, whole question, answer scored .925 .467 2.46 / 4.06 24 17.3 GB $0.51 0
Qwen3-8B, reasoning, writes its answer (60) .767 .918 49.1 / 70.3 1 17.3 GB $9.87 13 of 60

*At an assumed $0.71 per L4 hour. A whole-question prompt averages 1,188 input tokens and a reasoning answer 733 output tokens; multiply by any API's prices to compare.

How to read it: 13× faster than an 8B model asked the same question and about 270× faster than an 8B model that reasons first, at 1/12 and 1/230 of the cost, with the best balanced accuracy of the systems run on all 398 decisions (the reasoning model's .918 comes from only 60 decisions and is not directly comparable). The 8B direct model's plain accuracy is high only because most decisions are "insufficient" and it nearly always says so (balanced .467). The reasoning model has no calibrated confidence, so its wrong clearances are not routed to review. Fine-tuning changes accuracy, not speed: the untrained base model runs at the same speed and is far less accurate. The speed comes from the design: no generated tokens, one forward pass per question with the record cached, and a 1.7B model. The 8B "direct" baseline also scores answer letters without generating, but reads a long whole-question prompt with a model about 5× larger; the 8B reasoning baseline generates its answer token by token.

Training

Base Qwen/Qwen3-1.7B-Base (revision ea980cb0…)
Adapters LoRA r 16, alpha 32, dropout .05 on q, k, v, o, gate, up, down projections; merged into the weights
Objective negative log-likelihood + 0.1 × Brier score over the option letters (a single next-token choice; no text-generation training), random option orders
Schedule AdamW, lr 1e-4, 3 epochs, gradient accumulation 8; seeds 42, 43, 44; weight soup of the three
Data 535 synthetic rows (written by language models, checked by a verifier and hallucination guard) + 1,386 fact labels from 240 real MTSamples notes
Calibration fact temperature 1.85 (fitted on real development notes), number/date extraction temperature 0.25
Hardware one NVIDIA L4 (24 GB), under 1 hour per seed

Real-note labels: two independent Claude labelers per batch answered every fact as true (with a verbatim quote), false (explicit denial, normal value or complete medication list, with a quote) or not stated; a label was kept only if both agreed and both quotes were found in the note by code (agreement .94–.97). Criterion labels are derived from the fact labels by the same logic the system uses. Train, development and test notes never overlap.

Limitations and safety findings

  • Not clinically validated. No clinician reviewed the labels or the outputs. Real-note labels are language-model labels, cross-checked but not expert-verified.
  • Missed "applies" (6 of 29 on the final test, all sent to review): vocabulary gaps such as "melena" not read as GI hemorrhage, "bowel obstruction" not read as mechanical obstruction, "blood pressure absent" not read as severe hypotension, "still bleeding" not read as incomplete hemostasis.
  • Two automatic unsafe answers on the final test (one note, a code rule; fixed in this release, see above).
  • Small "applies" count. 29 real cases; the interval (.62–.90) is wide.
  • Prior + current notes are the weakest input format (.929): the model sometimes takes an older value.
  • Coverage trade-off. The 0.99 bar for "does not apply" sends more decisions to review (about 36% of real-note decisions, 52% of synthetic ones).
  • Scope. English only; notes up to about 6,000 characters tested; the catalog excludes time-window, dose-dependent, pregnancy, pediatric-only and self-hypersensitivity criteria. Drug classes are annotated only from FDA sources.
  • Data source. MTSamples is the only real-note source used; other institutions, note styles and languages are untested.

Reproducibility

Every experiment, data hash, pre-registration and verdict, including failures, is recorded in the project reference document (sections 70–207). Each test set was locked by SHA-256 before it was run, and evaluation scripts refuse to run twice. meddecide_config.json records the adapter hashes and temperatures; MANIFEST.json lists the hash of every file in this release.

License and data

Model weights and code: Apache 2.0 (base model Qwen3-1.7B-Base, Apache 2.0). Real notes: MTSamples (public Kaggle "Medical Transcriptions" dataset). Drug label text, pharmacologic classes and brand names: U.S. FDA labels via openFDA (public). CYP and transporter classes: FDA table of drugs that interact with CYP enzymes and transporter systems.

Glossary

Term Meaning
Applies / does not apply / insufficient the criterion is met / explicitly ruled out by the record / the record does not settle it
Balanced accuracy the average of the accuracies on each answer class, so a model cannot score well by always giving the most common answer
Automatic answer a decision not sent to review (confidence high enough and no review rule triggered)
Unsafe answer "does not apply" when the record does not settle the question (a contraindication cleared without evidence)
Missed contraindication "does not apply" when the contraindication is present
Routed / review the system hands the case to a person instead of answering on its own
Pre-registered the test, its data and its pass/fail rules were fixed and hashed before the system was run on it
Post hoc found or changed after seeing a test result; reported separately and not counted as independent evidence
Weight soup averaging the weights of several fine-tuned copies (different seeds) into one model

Citation

@misc{kumar2026rxdecide,
  title  = {RxDecide-1.7B: a small, auditable model for structured clinical decisions},
  author = {Kumar, Nandhu S},
  year   = {2026},
  url    = {https://huggingface.co/nsk246/RxDecide-1.7B}
}
Downloads last month
83
Safetensors
Model size
2B params
Tensor type
BF16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for nsk246/RxDecide-1.7B

Adapter
(77)
this model
Adapters
1 model

Space using nsk246/RxDecide-1.7B 1

Evaluation results

  • Balanced accuracy on MTSamples real-note test (120 notes, 1,557 decisions, evaluated once)
    self-reported
    0.861
  • Accuracy of automatic (non-routed) decisions on MTSamples real-note test (120 notes, 1,557 decisions, evaluated once)
    self-reported
    0.985
  • Accuracy (pre-registered labels) on gen_v1 TEST (280 synthetic records, unseen tasks, evaluated once)
    self-reported
    0.932
  • Accuracy on Synthetic pool test (231 decisions)
    self-reported
    0.978