mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
merge(FIX-C): v1.20.2 audit fixes
This commit is contained in:
commit
9bffa80950
4 files changed
+131
-15
No files matched your search
@@ -39,7 +39,10 @@ describe("supportsVisionForConfig", () => {
|
||||
expect(supportsVisionForConfig(providerType, "gpt-4o-mini")).toBe(true);
|
||||
expect(supportsVisionForConfig(providerType, "gpt-4.1")).toBe(true);
|
||||
expect(supportsVisionForConfig(providerType, "gpt-4-turbo")).toBe(true);
|
||||
// Full o-series reasoning models read images.
|
||||
expect(supportsVisionForConfig(providerType, "o1")).toBe(true);
|
||||
expect(supportsVisionForConfig(providerType, "o3")).toBe(true);
|
||||
expect(supportsVisionForConfig(providerType, "o4")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,6 +52,20 @@ describe("supportsVisionForConfig", () => {
|
||||
expect(supportsVisionForConfig("openai", null)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects text-only o-series variants (-mini / -preview) for openai", () => {
|
||||
// o1-mini / o1-preview / o3-mini accept the `o*` prefix but have no image
|
||||
// input — claiming vision here 400s the user at the image block.
|
||||
for (const providerType of [
|
||||
"openai",
|
||||
"admin-openai",
|
||||
"admin-key",
|
||||
] as const) {
|
||||
expect(supportsVisionForConfig(providerType, "o1-mini")).toBe(false);
|
||||
expect(supportsVisionForConfig(providerType, "o1-preview")).toBe(false);
|
||||
expect(supportsVisionForConfig(providerType, "o3-mini")).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("trusts local (operator opted in) regardless of model string", () => {
|
||||
expect(supportsVisionForConfig("local", "llava")).toBe(true);
|
||||
expect(supportsVisionForConfig("local", null)).toBe(true);
|
||||
@@ -72,6 +89,12 @@ describe("supportsVisionForConfig", () => {
|
||||
// The `-codex` specialist slugs are text-only despite the gpt-5 prefix.
|
||||
expect(supportsVisionForConfig("codex", "gpt-5-codex")).toBe(false);
|
||||
expect(supportsVisionForConfig("codex", "gpt-5.1-codex-mini")).toBe(false);
|
||||
// Text-only o-series variants are excluded for codex too.
|
||||
expect(supportsVisionForConfig("codex", "o1-mini")).toBe(false);
|
||||
expect(supportsVisionForConfig("codex", "o3-mini")).toBe(false);
|
||||
expect(supportsVisionForConfig("codex", "o1-preview")).toBe(false);
|
||||
// Full o-series stays vision-capable.
|
||||
expect(supportsVisionForConfig("codex", "o3")).toBe(true);
|
||||
});
|
||||
|
||||
it("never reports vision for none", () => {
|
||||
|
||||
@@ -41,18 +41,32 @@ function anthropicSupportsVision(model: string): boolean {
|
||||
/**
|
||||
* OpenAI vision models (also covers the operator's admin key + Codex when it
|
||||
* runs an OpenAI model — though Codex is excluded by `providerType`). `gpt-4o`,
|
||||
* `gpt-4.1`, `gpt-4-turbo`, and the `o*` reasoning models read image content;
|
||||
* `gpt-3.5*` and the legacy `gpt-4` (non-turbo) 400 on an image block.
|
||||
* `gpt-4.1`, `gpt-4-turbo`, and the full `o1` / `o3` / `o4` reasoning models
|
||||
* read image content; `gpt-3.5*` and the legacy `gpt-4` (non-turbo) 400 on an
|
||||
* image block.
|
||||
*
|
||||
* The `o*` reasoning line has text-only variants that DO accept the `o*` prefix
|
||||
* but reject an image block: `o1-mini` / `o1-preview` and `o3-mini` have no
|
||||
* vision input (only the full `o1` / `o3` are multimodal). They must be
|
||||
* excluded or a user pinned to one is told OCR works, then the provider 400s at
|
||||
* the image block. We treat an `o1` / `o3` / `o4` slug as vision-capable only
|
||||
* when it is NOT a `-mini` / `-preview` variant — the conservative default for
|
||||
* any unverified narrow slug.
|
||||
*/
|
||||
function isVisionReasoningSlug(m: string): boolean {
|
||||
if (!(m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4"))) {
|
||||
return false;
|
||||
}
|
||||
return !m.includes("-mini") && !m.includes("-preview");
|
||||
}
|
||||
|
||||
function openaiSupportsVision(model: string): boolean {
|
||||
const m = model.toLowerCase();
|
||||
return (
|
||||
m.startsWith("gpt-4o") ||
|
||||
m.startsWith("gpt-4.1") ||
|
||||
m.startsWith("gpt-4-turbo") ||
|
||||
m.startsWith("o1") ||
|
||||
m.startsWith("o3") ||
|
||||
m.startsWith("o4")
|
||||
isVisionReasoningSlug(m)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,9 +92,9 @@ function codexSupportsVision(model: string): boolean {
|
||||
m.startsWith("gpt-4o") ||
|
||||
m.startsWith("gpt-4.1") ||
|
||||
m.startsWith("gpt-4-turbo") ||
|
||||
m.startsWith("o1") ||
|
||||
m.startsWith("o3") ||
|
||||
m.startsWith("o4")
|
||||
// Same `-mini` / `-preview` text-only exclusion as the OpenAI gate: a
|
||||
// future codex slug rotation onto e.g. `o3-mini` must not claim vision.
|
||||
isVisionReasoningSlug(m)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -669,6 +669,70 @@ describe("computeIllnessCorrelation — red-flag escalation", () => {
|
||||
expect(fever?.days).toBe(3);
|
||||
expect(fever?.worstValue).toBe(39.0);
|
||||
});
|
||||
|
||||
it("does NOT escalate a sparse febrile series with calendar GAPS (cries-wolf guard)", () => {
|
||||
// Symptom/vital points are present-days-only (never zero-filled). A user
|
||||
// who logs a fever on 01-10/01-15/01-20/01-25 has FOUR isolated febrile
|
||||
// days with multi-day gaps between them — NOT a sustained run. The run
|
||||
// scan must compare calendar-day deltas, not array adjacency, or it falsely
|
||||
// fires sustained_fever (which surfaces a "seek medical care" string AND an
|
||||
// urgent cross-channel push). No two of these days are calendar-adjacent →
|
||||
// the longest run is 1 → NO escalation.
|
||||
const rhr: MeasurementType = "RESTING_HEART_RATE";
|
||||
const out = computeIllnessCorrelation(
|
||||
input({
|
||||
series: [
|
||||
{
|
||||
type: rhr,
|
||||
baselineDays: jitteredBaseline(55, 1, 21, "2026-01-02"),
|
||||
episodeDays: flatBaseline(55, 5, "2026-01-15"),
|
||||
},
|
||||
],
|
||||
dayLogFever: [
|
||||
{ day: "2026-01-10", feverC: 39.2 },
|
||||
{ day: "2026-01-15", feverC: 39.0 },
|
||||
{ day: "2026-01-20", feverC: 38.8 },
|
||||
{ day: "2026-01-25", feverC: 38.7 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(out.status).toBe("ok");
|
||||
if (out.status !== "ok") return;
|
||||
const fever = out.value.redFlags.find(
|
||||
(f) => f.reason === "sustained_fever",
|
||||
);
|
||||
expect(fever).toBeUndefined();
|
||||
});
|
||||
|
||||
it("DOES escalate a genuinely consecutive 3-calendar-day febrile run", () => {
|
||||
// The mirror of the gap guard: three truly adjacent calendar days
|
||||
// (01-10/01-11/01-12) all ≥ 38.5 → a real sustained fever → escalate.
|
||||
const rhr: MeasurementType = "RESTING_HEART_RATE";
|
||||
const out = computeIllnessCorrelation(
|
||||
input({
|
||||
series: [
|
||||
{
|
||||
type: rhr,
|
||||
baselineDays: jitteredBaseline(55, 1, 21, "2026-01-02"),
|
||||
episodeDays: flatBaseline(55, 5, "2026-01-15"),
|
||||
},
|
||||
],
|
||||
dayLogFever: [
|
||||
{ day: "2026-01-10", feverC: 39.2 },
|
||||
{ day: "2026-01-11", feverC: 38.9 },
|
||||
{ day: "2026-01-12", feverC: 38.6 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(out.status).toBe("ok");
|
||||
if (out.status !== "ok") return;
|
||||
const fever = out.value.redFlags.find(
|
||||
(f) => f.reason === "sustained_fever",
|
||||
);
|
||||
expect(fever).toBeDefined();
|
||||
expect(fever?.days).toBe(3);
|
||||
expect(fever?.worstValue).toBe(39.2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAdverseDeviation", () => {
|
||||
|
||||
@@ -968,22 +968,37 @@ function runFlag(
|
||||
let bestWorst: number | null = null;
|
||||
let run = 0;
|
||||
let runWorst: number | null = null;
|
||||
let prevDay: string | null = null;
|
||||
for (const p of sorted) {
|
||||
if (predicate(p.mean)) {
|
||||
run++;
|
||||
runWorst =
|
||||
runWorst === null
|
||||
? p.mean
|
||||
: worst === "min"
|
||||
? Math.min(runWorst, p.mean)
|
||||
: Math.max(runWorst, p.mean);
|
||||
// A run is CALENDAR-consecutive days. Symptom/vital points are
|
||||
// present-days-only (never zero-filled), so a sparse febrile series
|
||||
// (e.g. days 01/10/15/20) must NOT count as one long run — that fires a
|
||||
// false sustained_fever escalation. Continue the run only when this
|
||||
// point is the calendar day immediately after the previous matching one;
|
||||
// otherwise the current point starts a fresh run of 1.
|
||||
const adjacent = prevDay !== null && dayDiff(prevDay, p.day) === 1;
|
||||
if (adjacent) {
|
||||
run++;
|
||||
runWorst =
|
||||
runWorst === null
|
||||
? p.mean
|
||||
: worst === "min"
|
||||
? Math.min(runWorst, p.mean)
|
||||
: Math.max(runWorst, p.mean);
|
||||
} else {
|
||||
run = 1;
|
||||
runWorst = p.mean;
|
||||
}
|
||||
if (run > bestRun) {
|
||||
bestRun = run;
|
||||
bestWorst = runWorst;
|
||||
}
|
||||
prevDay = p.day;
|
||||
} else {
|
||||
run = 0;
|
||||
runWorst = null;
|
||||
prevDay = null;
|
||||
}
|
||||
}
|
||||
if (bestRun >= RED_FLAG_RUN_DAYS && bestWorst !== null) {
|
||||
|
||||
Reference in new issue
Block a user