MedGemma on Modal: shipping medical-grade image analysis
MedGemma 4B IT is now boringai's primary image model. We ran it on three labeled open-dataset images (pneumonia CXR, normal CXR, actinic keratosis) and two adversarial inputs. Verbatim outputs, ground-truth comparison, the systematic bias we found, and the bug we caught writing the post.

We just shipped MedGemma 4B IT as boringai's primary image model and ran it against three labeled open-dataset images plus two adversarial inputs. The model got the pneumonia case right (lung opacity with pneumonia as the first differential), got the normal chest X-ray right (clear lung fields, no consolidation), and was appropriately cautious on a dermatology lesion (wouldn't commit without biopsy). It also cleanly rejected the adversarial inputs. This post shows the verbatim inputs and outputs, compares them to dataset ground truth, and is honest about the one systematic error the model made on every chest film.
Method: labeled open datasets, not vibes
Evaluating a medical VLM is only meaningful if you have ground truth. We picked three images from open datasets that ship with labels:
Label:
PNEUMONIALabel:
NORMALLabel:
Actinic keratosesEach case was run with a neutral, non-leading prompt. No few-shot examples, no "you are an expert radiologist" priming — just "Describe the imaging modality, anatomical structures visible, and any abnormalities." The model doesn't know the dataset label.
The pipeline
Every image call escalates through three models on Modal GPU. The first one that produces trustworthy output wins.
MedGemma 4B IT is Google's medical-specialized multimodal (4.3B params, bf16). LLaVA-1.5-7B is the non-gated general fallback. BLIP + a clinical LLM is the safety net that never fabricates. Three server-side guards (size ≥ 16×16, std-dev ≥ 3.0, no trigram repetition) reject inputs that cause degenerate output on any model.
Case 1: Pneumonia chest X-ray
PROMPT: "Describe the imaging modality, anatomical structures visible, and any abnormalities." · 13 cr · 12,753 tokens · cold-start 97s
Ground truth label: PNEUMONIA. Verbatim output (truncated where it lists differentials beyond the first three):
Imaging Modality:
X-ray — standard chest X-ray (radiography).
Anatomical Structures Visible:
Ribs, Lungs (air-filled, dark), Heart, Mediastinum (heart +
aorta + vena cava + trachea + esophagus), Clavicles,
Scapulae, Diaphragm.
Abnormalities:
• Right Lung Opacity — increased density in the right lung
field. Differential:
→ Pneumonia: infection of the lung tissue. ← MATCH
→ Pleural Effusion: fluid in the pleural space.
→ Consolidation: solidification of lung tissue.
→ Tumor: growth in the lung tissue.
→ Atelectasis: collapse of lung tissue.
• Cardiomegaly — heart appears enlarged. Differential:
→ Hypertension, Cardiomyopathy, Valvular disease, CHF.
• Mediastinal Shift — space between the lungs shifted right.Verdict: correct. The dataset label is PNEUMONIA. MedGemma flagged a right lung opacity and listed pneumonia as the first item in the differential. The list itself is textbook — those five causes of lung opacity on a chest film are exactly what a radiologist would enumerate. No hallucination, no fabrication.
Honest limitation: it also flagged cardiomegaly (enlarged heart). On a pediatric chest X-ray, the heart normally looks relatively large because the thorax hasn't fully grown. MedGemma overcalled this — see Case 2 for the same pattern on a normal film.
Case 2: Normal chest X-ray
PROMPT: same as Case 1 · 16 cr · 15,153 tokens
Ground truth label: NORMAL. This is the harder test — can the model say "no abnormality" when there isn't one? Many VLMs hallucinate findings on normal images.
Anatomical Structures Visible:
Ribs, Lungs, Heart, Mediastinum, Clavicles, Scapulae, Diaphragm.
Abnormalities:
• Heart Size: slightly enlarged (cardiomegaly). The model
notes this is "common in infants and young children, as
their hearts are still growing."
• Lung Fields: relatively clear, with no obvious signs of
consolidation (fluid or infection) or other significant
abnormalities. ← MATCH
Important Considerations:
Age: infant. Clinical context required. Further evaluation
by a medical professional if concerns.
Disclaimer: AI cannot provide medical diagnoses.Verdict: correct, with the same systematic error. Lung fields correctly described as clear, no consolidation. That's the ground-truth-critical call — and it got it right. It flagged cardiomegaly again but this time correctly contextualized it as normal in infants (the keremberke dataset is pediatric). The model is overcalling heart size on every chest film in this dataset. That's a real, reproducible bias worth flagging — not a one-off.
Case 3: Actinic keratosis (dermatology)
PROMPT: "Describe the lesion: type, color, border, asymmetry, and any concerning features." · 9 cr · 8,856 tokens
Ground truth label: Actinic keratoses — a precancerous lesion caused by sun damage. Clinical diagnosis requires dermoscopy and often biopsy.
Type: skin lesion, irregular and atypical. "Difficult to definitively classify without a clinical examination and potentially a biopsy." Color: multitude of colors — brown, tan, possibly red or pink. "This variation in color is a concerning feature." Border: irregular and poorly defined. Not smooth. Asymmetry: asymmetrical — one half doesn't mirror the other. Concerning Features: • Multiple colors (red flag) • Irregular border (suggests growth or change) • Asymmetry (key indicator of potential malignancy) Disclaimer: refers to dermatologist for definitive diagnosis.
Verdict: appropriately cautious. The model did not commit to "actinic keratosis" — and shouldn't have, because that diagnosis requires dermoscopy and biopsy. Instead it correctly identified the ABCD warning signs (Asymmetry, Border irregularity, Color variation, plus the type qualifier) and refused to overcall. This is the right behavior for a clinical decision-support tool: flag concerning features, defer the diagnosis.
An overconfident model would have said "this is actinic keratosis" — and been technically correct on this specific image, but wrong in spirit, because in clinical practice you cannot make that call from a photo alone. boringai is decision support, not autonomous diagnosis.
Adversarial cases — the guards
Three server-side guards reject inputs that cause degenerate output on every model. Verified live on two adversarial inputs:
model=modal/guard
message="Image too small (1x1px); minimum 16x16."
cost=1 cr
model=modal/guard
message="Image appears blank (std-dev 0.0)."
cost=1 cr
Pre-fix, both of these returned "Modal GPU service is unavailable" — the tool layer treated the guard's error response as a service outage and swallowed the actual reason. That was a bug we caught while writing this post. Now the guard's rejection message surfaces directly to the caller with reviewer_gated.status = "rejected".
The third guard (trigram repetition) catches the original hallucination mode — when any stage produces output where a 3-word window appears 3+ times, the output is flagged and the caller escalates to the next stage or to caption-then-interpret. None of the three real cases triggered it.
Honest limitations
This is a sample of three. It is not a clinical validation study. Three things we noticed:
- Systematic cardiomegaly overcall on pediatric chest X-rays. Both CXR cases flagged the heart as enlarged, even on the normal film. MedGemma correctly noted this is common in infants, but the flag itself is noise on a pediatric dataset. A radiologist would have suppressed it from the report.
- Cautious to a fault on dermatology. The model refused to name actinic keratosis on a labeled actinic-keratosis image. That's the right behavior clinically (you need a biopsy), but it means the tool cannot be used for triage on dermatology images — only for "is this concerning?" flagging.
- Cost varies 2.5× across cases that look similar. The normal CXR cost 16 cr (15k tokens); the dermatology image cost 9 cr (9k tokens). Same input image size, same prompt length. The model just generated more text on the chest X-ray. Per-token billing means cost tracks output verbosity, not input complexity.
Cost economics
9–16 cr per image in practice.
L4 GPU, cold-start 90+ seconds.
The credit math is straightforward. A clinic running 100 PII strips a day (2 cr each, no cache) burns 200 cr. Add 20 pneumonia screens a day at ~15 cr each: another 300 cr. A 10,000-cr top-up lasts about three weeks at that volume.
What's verifiable
Every number in this post is reproducible. The samples ship with the app at /samples/chest-xray.jpg, /samples/chest-xray-normal.jpg, and /samples/dermatology.jpg. Run them yourself:
# Pneumonia chest X-ray
curl -X POST https://boracode.ai/api/tools/image_analyze \
-H "Authorization: Bearer bai_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"image_base64":"...","prompt":"Describe the imaging modality, anatomical structures visible, and any abnormalities."}'
# Or just open the playground and click "Pneumonia CXR":
# https://boracode.ai/playground → image_analyzeThe two adversarial cases are even easier — the 1×1 transparent PNG and the blank 64×64 are both <100 bytes and reproducible from any image editor.
