fixes ocr for newer open ai models

This commit is contained in:
Timon_bed committed 2026-08-30 17:37:52 +02:00
1 parent 320788a984
commit 6ab3b02935
5 files changed
+49 -11

No files matched your search

@@ -136,6 +136,16 @@ describe("POST /api/labs/ocr/extract — text mode budget", () => {
"2026-06-26",
);
});
it("identifies provider failures instead of blaming image quality", async () => {
vi.mocked(runOcrExtraction).mockRejectedValue(new Error("provider down"));
const res = await POST(textReq());
const body = (await res.json()) as { error: string };
expect(res.status).toBe(502);
expect(body.error).toContain("configured AI provider");
});
});
describe("POST /api/labs/ocr/extract — vision PDF rasterization", () => {
+31 -8
View File
@@ -63,6 +63,25 @@ const EXTRACT_WINDOW_MS = 60 * 60 * 1000;
/** OCR'd text is bounded in the schema; cap the JSON body proportionally. */
const TEXT_BODY_MAX_BYTES = 512 * 1024;
function providerFailureMeta(error: unknown, mode?: "text") {
const err = error as {
httpStatus?: unknown;
model?: unknown;
bodyExcerpt?: unknown;
};
return {
reason: "provider_error",
...(mode ? { mode } : {}),
...(typeof err.httpStatus === "number"
? { upstreamStatus: err.httpStatus }
: {}),
...(typeof err.model === "string" ? { model: err.model } : {}),
...(typeof err.bodyExcerpt === "string"
? { upstreamError: err.bodyExcerpt }
: {}),
};
}
export const POST = apiHandler(async (request: Request) => {
const { user } = await requireAuth();
@@ -191,11 +210,13 @@ async function handleTextExtract(
}
annotate({
action: { name: "labs.ocr.extractFailed" },
meta: { reason: "provider_error", mode: "text" },
});
return apiError("Couldn't read the report. Try a clearer photo.", 502, {
errorCode: "labs.ocr.extractFailed",
meta: providerFailureMeta(err, "text"),
});
return apiError(
"The configured AI provider could not process this report. Check the provider configuration and retry.",
502,
{ errorCode: "labs.ocr.extractFailed" },
);
}
}
@@ -397,11 +418,13 @@ async function handleVisionExtract(
}
annotate({
action: { name: "labs.ocr.extractFailed" },
meta: { reason: "provider_error" },
});
return apiError("Couldn't read the report. Try a clearer photo.", 502, {
errorCode: "labs.ocr.extractFailed",
meta: providerFailureMeta(err),
});
return apiError(
"The configured AI provider could not process this report. Check the provider configuration and retry.",
502,
{ errorCode: "labs.ocr.extractFailed" },
);
}
} catch (err) {
// A guard threw after the reservation (e.g. consent races) — refund fully.
@@ -37,6 +37,9 @@ describe("supportsVisionForConfig", () => {
] as const) {
expect(supportsVisionForConfig(providerType, "gpt-4o")).toBe(true);
expect(supportsVisionForConfig(providerType, "gpt-4o-mini")).toBe(true);
expect(supportsVisionForConfig(providerType, "gpt-5.6-terra")).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.
+4 -3
View File
@@ -189,13 +189,14 @@ export const AI_BUDGETS = {
* v1.18.9 — Lab-OCR structured extraction. ONE vision call transcribes a
* photo / PDF of a paper lab report into a JSON array of analyte rows
* (analyte + value/valueText + unit + reference range + date + per-field
* confidence). A full panel is a dozen-plus rows of compact JSON; 4000
* tokens covers a dense report plus the envelope. Temperature 0 — the task
* confidence). A full panel is a dozen-plus rows of compact JSON; 8000
* tokens leaves room for GPT-5 reasoning plus the dense report envelope.
* Temperature 0 — the task
* is faithful transcription, not generation, so the lowest setting minimises
* invented values. Vision input tokens are expensive, so the route also
* gates with a tight 6/hour rate bucket + reserveBudget + upload-size cap.
*/
ocrExtract: { temperature: 0, maxTokens: 4000 },
ocrExtract: { temperature: 0, maxTokens: 8000 },
/**
* v1.20.1 — Lab-OCR text-mode structuring. The browser OCR's the image
+1
View File
@@ -63,6 +63,7 @@ function isVisionReasoningSlug(m: string): boolean {
function openaiSupportsVision(model: string): boolean {
const m = model.toLowerCase();
return (
m.startsWith("gpt-5") ||
m.startsWith("gpt-4o") ||
m.startsWith("gpt-4.1") ||
m.startsWith("gpt-4-turbo") ||