feat(api): add /v1/ocr endpoint (Mistral OCR) + Mistral moderation (#5950)

Integrated into release/v3.8.44 — /v1/ocr endpoint (Mistral OCR) + Mistral moderation (port upstream 9router#2064, co-authored @waguriagentic). Validated locally: 14 ocr-route tests + moderation/servicekind/endpoint-category suites green (CORS→Zod→handler + no-stack-leak assertion). Reds are inherited DRIFT only: cognitive-complexity ratchet (none from OCR files — pre-existing cycle drift, rebaselined at release) + environmental setup-claude base-red.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-02 17:42:31 -03:00
committed by GitHub
parent 26fd7b6a8a
commit 01d2e1a841
18 changed files with 686 additions and 23 deletions

View File

@@ -8,7 +8,7 @@
### ✨ New Features
- **feat(providers):** add ClinePass as a first-class API-key provider (Cline's BYOK gateway). (thanks @adentdk)
- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
### 🔧 Bug Fixes

View File

@@ -17,14 +17,12 @@
* still declared explicitly via `serviceKinds` on the provider entry; callers
* union the two sources.
*/
import {
AUDIO_TRANSCRIPTION_PROVIDERS,
AUDIO_SPEECH_PROVIDERS,
} from "./audioRegistry.ts";
import { AUDIO_TRANSCRIPTION_PROVIDERS, AUDIO_SPEECH_PROVIDERS } from "./audioRegistry.ts";
import { VIDEO_PROVIDERS } from "./videoRegistry.ts";
import { MUSIC_PROVIDERS } from "./musicRegistry.ts";
import { IMAGE_PROVIDERS } from "./imageRegistry.ts";
import { EMBEDDING_PROVIDERS } from "./embeddingRegistry.ts";
import { OCR_PROVIDERS } from "./ocrRegistry.ts";
/** Media kinds whose provider membership is defined by a backend registry. */
export const MEDIA_KIND_REGISTRIES = {
@@ -34,6 +32,7 @@ export const MEDIA_KIND_REGISTRIES = {
music: MUSIC_PROVIDERS,
image: IMAGE_PROVIDERS,
embedding: EMBEDDING_PROVIDERS,
ocr: OCR_PROVIDERS,
} as const satisfies Record<string, Record<string, unknown>>;
export type RegistryMediaKind = keyof typeof MEDIA_KIND_REGISTRIES;

View File

@@ -5,7 +5,25 @@
* Follows OpenAI's moderation API format.
*/
export const MODERATION_PROVIDERS = {
export interface ModerationModel {
id: string;
name: string;
}
export interface ModerationProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
models: ModerationModel[];
}
export interface ParsedModerationModel {
provider: string | null;
model: string | null;
}
export const MODERATION_PROVIDERS: Record<string, ModerationProvider> = {
openai: {
id: "openai",
baseUrl: "https://api.openai.com/v1/moderations",
@@ -16,22 +34,29 @@ export const MODERATION_PROVIDERS = {
{ id: "text-moderation-latest", name: "Text Moderation Latest" },
],
},
mistral: {
id: "mistral",
baseUrl: "https://api.mistral.ai/v1/moderations",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "mistral-moderation-latest", name: "Mistral Moderation" }],
},
};
/**
* Get moderation provider config by ID
* Get moderation provider config by ID.
*/
export function getModerationProvider(providerId) {
export function getModerationProvider(providerId: string): ModerationProvider | null {
return MODERATION_PROVIDERS[providerId] || null;
}
/**
* Parse moderation model string
* Parse a moderation model string.
*/
export function parseModerationModel(modelStr) {
export function parseModerationModel(modelStr: string | null | undefined): ParsedModerationModel {
if (!modelStr) return { provider: null, model: null };
for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) {
for (const providerId of Object.keys(MODERATION_PROVIDERS)) {
if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
}
@@ -47,10 +72,10 @@ export function parseModerationModel(modelStr) {
}
/**
* Get all moderation models as a flat list
* Get all moderation models as a flat list.
*/
export function getAllModerationModels() {
const models = [];
export function getAllModerationModels(): Array<{ id: string; name: string; provider: string }> {
const models: Array<{ id: string; name: string; provider: string }> = [];
for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) {
for (const model of config.models) {
models.push({

View File

@@ -0,0 +1,82 @@
/**
* OCR Provider Registry
*
* Defines providers that support the /v1/ocr endpoint.
* Follows Mistral's OCR API format.
*/
export interface OcrModel {
id: string;
name: string;
}
export interface OcrProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
models: OcrModel[];
}
export interface ParsedOcrModel {
provider: string | null;
model: string | null;
}
export const OCR_PROVIDERS: Record<string, OcrProvider> = {
mistral: {
id: "mistral",
baseUrl: "https://api.mistral.ai/v1/ocr",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "mistral-ocr-latest", name: "Mistral OCR" }],
},
};
/**
* Get OCR provider config by ID.
*/
export function getOcrProvider(providerId: string): OcrProvider | null {
return OCR_PROVIDERS[providerId] || null;
}
/**
* Parse an OCR model string.
*
* Accepts either a "provider/model" prefixed string or a bare model id that
* matches one of the registered OCR models.
*/
export function parseOcrModel(modelStr: string | null | undefined): ParsedOcrModel {
if (!modelStr) return { provider: null, model: null };
for (const providerId of Object.keys(OCR_PROVIDERS)) {
if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
}
}
for (const [providerId, config] of Object.entries(OCR_PROVIDERS)) {
if (config.models.some((m) => m.id === modelStr)) {
return { provider: providerId, model: modelStr };
}
}
return { provider: null, model: modelStr };
}
/**
* Get all OCR models as a flat list.
*/
export function getAllOcrModels(): Array<{ id: string; name: string; provider: string }> {
const models: Array<{ id: string; name: string; provider: string }> = [];
for (const [providerId, config] of Object.entries(OCR_PROVIDERS)) {
for (const model of config.models) {
models.push({
id: `${providerId}/${model.id}`,
name: model.name,
provider: providerId,
});
}
}
return models;
}

79
open-sse/handlers/ocr.ts Normal file
View File

@@ -0,0 +1,79 @@
import { CORS_HEADERS } from "../utils/cors.ts";
/**
* OCR Handler
*
* Handles POST /v1/ocr (Mistral OCR API format).
*/
import { getOcrProvider, parseOcrModel } from "../config/ocrRegistry.ts";
import { errorResponse } from "../utils/error.ts";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
/**
* Handle OCR request
*
* @param {Object} options
* @param {Object} options.body - JSON body { model, document }
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<unknown>} */
export async function handleOcr({ body, credentials }) {
const startTime = Date.now();
if (!body.document) {
return errorResponse(400, "document is required");
}
// Default to latest OCR model
const model = body.model || "mistral-ocr-latest";
const { provider: providerId, model: modelId } = parseOcrModel(model);
const providerConfig = providerId ? getOcrProvider(providerId) : null;
if (!providerConfig) {
return errorResponse(400, `No OCR provider found for model "${model}". Available: mistral`);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for OCR provider: ${providerId}`);
}
try {
const res = await fetch(providerConfig.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...body,
model: modelId,
}),
});
if (!res.ok) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
...CORS_HEADERS,
},
});
}
const data = await res.json();
const headers = new Headers({ ...CORS_HEADERS, "Content-Type": "application/json" });
attachOmniRouteMetaHeaders(headers, {
provider: providerId,
model: modelId,
costUsd: 0,
latencyMs: Date.now() - startTime,
requestId: generateRequestId(),
});
return new Response(JSON.stringify(data), { status: 200, headers });
} catch (err) {
return errorResponse(500, `OCR request failed: ${err.message}`);
}
}

View File

@@ -14,6 +14,7 @@ import { WebSearchExampleCard } from "../../components/WebSearchExampleCard";
import { WebFetchExampleCard } from "../../components/WebFetchExampleCard";
import { VideoExampleCard } from "../../components/VideoExampleCard";
import { MusicExampleCard } from "../../components/MusicExampleCard";
import { OcrExampleCard } from "../../components/OcrExampleCard";
interface Connection {
id: string;
@@ -53,6 +54,8 @@ function renderPlayground(kind: MediaKind, providerId: string) {
return <VideoExampleCard providerId={providerId} />;
case "music":
return <MusicExampleCard providerId={providerId} />;
case "ocr":
return <OcrExampleCard providerId={providerId} />;
case "imageToText":
// Endpoint /api/v1/images/understanding does not exist yet — omitted.
return (

View File

@@ -0,0 +1,126 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useApiKey } from "../../providers/hooks/useApiKey";
import { useProviderModels } from "../../providers/hooks/useProviderModels";
import { buildCurl } from "../../providers/utils/buildCurl";
import { PlaygroundCard } from "./PlaygroundCard";
interface Props {
providerId: string;
}
const ENDPOINT_PATH = "/api/v1/ocr";
const SAMPLE_DOCUMENT_URL = "https://arxiv.org/pdf/2201.04234";
function extractError(data: unknown): string | null {
if (!data || typeof data !== "object") return null;
const d = data as Record<string, unknown>;
const err = d.error as Record<string, unknown> | undefined;
if (err?.message) return String(err.message);
if (typeof d.message === "string") return d.message;
return null;
}
export function OcrExampleCard({ providerId }: Props) {
const t = useTranslations("miniPlayground");
const { apiKey } = useApiKey();
const { models } = useProviderModels(providerId);
const firstModel = models[0]?.id ?? "";
const [model, setModel] = useState<string>("");
const [documentUrl, setDocumentUrl] = useState<string>(SAMPLE_DOCUMENT_URL);
const [running, setRunning] = useState<boolean>(false);
const [result, setResult] = useState<{ data: unknown; latencyMs: number } | undefined>();
const [error, setError] = useState<string | null>(null);
const effectiveModel = model || firstModel;
const buildBody = () => ({
model: effectiveModel,
document: { type: "document_url", document_url: documentUrl },
});
const curlSnippet = buildCurl({
endpoint:
(typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") +
ENDPOINT_PATH,
headers: {
Authorization: `Bearer ${apiKey || "<your-api-key>"}`,
"Content-Type": "application/json",
},
body: buildBody(),
});
const handleRun = async () => {
setRunning(true);
setError(null);
setResult(undefined);
const t0 = performance.now();
try {
const res = await fetch(ENDPOINT_PATH, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"x-connection-id": providerId,
},
body: JSON.stringify(buildBody()),
});
const data: unknown = await res.json();
const latencyMs = performance.now() - t0;
const errMsg = extractError(data);
if (!res.ok || errMsg) {
setError(errMsg ?? `HTTP ${res.status}`);
} else {
setResult({ data, latencyMs });
}
} catch (err) {
setError(err instanceof Error ? err.message : "Request failed");
} finally {
setRunning(false);
}
};
const modelOptions = models.length > 0 ? models : [{ id: "mistral-ocr-latest" }];
return (
<PlaygroundCard
kindLabel="OCR"
apiEndpoint={ENDPOINT_PATH}
onRun={handleRun}
curlSnippet={curlSnippet}
running={running}
result={result}
error={error}
>
{/* Model select */}
<div>
<label className="block text-xs text-text-muted mb-1">{t("model")}</label>
<select
value={model || firstModel}
onChange={(e) => setModel(e.target.value)}
className="w-full rounded-md border border-border bg-bg-subtle text-sm px-2 py-1.5 text-text-main focus:outline-none focus:ring-1 focus:ring-primary"
>
{modelOptions.map((m) => (
<option key={m.id} value={m.id}>
{m.id}
</option>
))}
</select>
</div>
{/* Document URL */}
<div>
<label className="block text-xs text-text-muted mb-1">Document URL</label>
<input
type="text"
value={documentUrl}
onChange={(e) => setDocumentUrl(e.target.value)}
placeholder={SAMPLE_DOCUMENT_URL}
className="w-full rounded-md border border-border bg-bg-subtle text-sm px-2 py-1.5 text-text-main focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
</PlaygroundCard>
);
}

View File

@@ -15,6 +15,7 @@ const KIND_ICON: Record<ServiceKind, string> = {
webFetch: "language",
video: "videocam",
music: "music_note",
ocr: "document_scanner",
};
interface ServiceKindTabsProps {

View File

@@ -7,7 +7,8 @@ export type MediaKind =
| "webSearch"
| "webFetch"
| "video"
| "music";
| "music"
| "ocr";
export const MEDIA_KINDS: MediaKind[] = [
"embedding",
@@ -19,4 +20,5 @@ export const MEDIA_KINDS: MediaKind[] = [
"webFetch",
"video",
"music",
"ocr",
];

View File

@@ -0,0 +1,73 @@
import { handleOcr } from "@omniroute/open-sse/handlers/ocr.ts";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { parseOcrModel } from "@omniroute/open-sse/config/ocrRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
import { v1OcrSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import {
isAllRateLimitedCredentials,
rateLimitedProviderResponse,
} from "@/app/api/v1/_shared/rateLimit";
/**
* Handle CORS preflight
*/
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
});
}
/**
* POST /v1/ocr — document OCR
* Mistral OCR API compatible.
*/
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
} catch {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
const validation = validateBody(v1OcrSchema, rawBody);
if (isValidationFailure(validation)) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, validation.error.message);
}
const body = validation.data;
const model = body.model || "mistral-ocr-latest";
// Enforce API key policies (model restrictions + budget limits)
const policy = await enforceApiKeyPolicy(request, model);
if (policy.rejection) return policy.rejection;
const { provider } = parseOcrModel(model);
// Default to mistral if no provider prefix
const resolvedProvider = provider || "mistral";
const credentials = await getProviderCredentials(resolvedProvider);
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`No credentials for provider: ${resolvedProvider}`
);
}
if (isAllRateLimitedCredentials(credentials)) {
return rateLimitedProviderResponse(resolvedProvider, credentials);
}
const response = await handleOcr({ body: { ...body, model }, credentials });
if (response?.ok) {
await clearRecoveredProviderState(credentials);
}
return response;
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1856,7 +1856,8 @@
"webSearch": "Web Search",
"webFetch": "Web Fetch",
"video": "Video",
"music": "Music"
"music": "Music",
"ocr": "OCR"
},
"noProviders": "No providers configured for this kind yet.",
"addConnection": "Add Connection",

View File

@@ -22,12 +22,7 @@ export const ENDPOINT_CATEGORIES: readonly EndpointCategory[] = [
id: "chat",
label: "Chat / Messages",
description: "Chat completions, text completions, messages, and responses",
prefixes: [
"/v1/chat/completions",
"/v1/completions",
"/v1/messages",
"/v1/responses",
],
prefixes: ["/v1/chat/completions", "/v1/completions", "/v1/messages", "/v1/responses"],
},
{
id: "search",
@@ -83,6 +78,12 @@ export const ENDPOINT_CATEGORIES: readonly EndpointCategory[] = [
description: "Content moderation",
prefixes: ["/v1/moderations"],
},
{
id: "ocr",
label: "OCR",
description: "Optical character recognition",
prefixes: ["/v1/ocr"],
},
{
id: "batches",
label: "Batch Processing",

View File

@@ -16,7 +16,8 @@ export type ServiceKind =
| "webSearch"
| "webFetch"
| "video"
| "music";
| "music"
| "ocr";
export const SERVICE_KIND_VALUES: readonly ServiceKind[] = [
"llm",
@@ -29,4 +30,5 @@ export const SERVICE_KIND_VALUES: readonly ServiceKind[] = [
"webFetch",
"video",
"music",
"ocr",
];

View File

@@ -87,6 +87,31 @@ export const v1ModerationSchema = z
})
.catchall(z.unknown());
// Mistral OCR: `document` is a { type, document_url | image_url } object.
// Keep the schema permissive-but-typed — validate model + that a non-empty
// `document` object (or a document_url/image_url string shorthand) is present.
export const v1OcrDocumentSchema = z.union([
z
.object({
type: z.string().trim().min(1).optional(),
document_url: z.string().trim().min(1).optional(),
image_url: z.union([z.string().trim().min(1), z.record(z.string(), z.unknown())]).optional(),
})
.catchall(z.unknown())
.refine(
(value) => value.document_url !== undefined || value.image_url !== undefined,
"document must include document_url or image_url"
),
nonEmptyStringSchema,
]);
export const v1OcrSchema = z
.object({
model: modelIdSchema.optional(),
document: v1OcrDocumentSchema,
})
.catchall(z.unknown());
export const v1RerankSchema = z
.object({
model: modelIdSchema,

View File

@@ -80,6 +80,10 @@ test("resolveEndpointCategory: maps /v1/moderations to 'moderations'", () => {
assert.equal(resolveEndpointCategory("/v1/moderations"), "moderations");
});
test("resolveEndpointCategory: maps /v1/ocr to 'ocr'", () => {
assert.equal(resolveEndpointCategory("/v1/ocr"), "ocr");
});
test("resolveEndpointCategory: maps /v1/batches to 'batches'", () => {
assert.equal(resolveEndpointCategory("/v1/batches"), "batches");
});

View File

@@ -81,6 +81,19 @@ test("media listing filter surfaces minimax where the old declared-only filter m
assert.ok(oldListFor("tts").length < newListFor("tts").length, "fix surfaces additional tts providers");
});
test("ocr is a registry-backed media kind and mistral derives it", () => {
assert.ok(
(REGISTRY_MEDIA_KINDS as readonly string[]).includes("ocr"),
"REGISTRY_MEDIA_KINDS should include ocr once the OCR registry is wired"
);
assert.ok(
getRegistryMediaKinds("mistral").includes("ocr" as never),
"mistral should derive the ocr media kind from OCR_PROVIDERS"
);
const merged = resolveProviderServiceKinds("mistral", ["llm"]);
assert.ok(merged.includes("ocr"), `expected ocr in ${merged.join(",")}`);
});
test("derived kinds are always within the known media-kind set", () => {
for (const id of Object.keys(AI_PROVIDERS)) {
for (const kind of getRegistryMediaKinds(id)) {

View File

@@ -2,6 +2,9 @@ import test from "node:test";
import assert from "node:assert/strict";
const { handleModeration } = await import("../../open-sse/handlers/moderations.ts");
const { MODERATION_PROVIDERS, getModerationProvider, parseModerationModel } = await import(
"../../open-sse/config/moderationRegistry.ts"
);
const originalFetch = globalThis.fetch;
@@ -9,6 +12,42 @@ test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test("MODERATION_PROVIDERS registers mistral with the Mistral moderations base URL", () => {
const provider = getModerationProvider("mistral");
assert.ok(provider);
assert.equal(provider.baseUrl, "https://api.mistral.ai/v1/moderations");
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-moderation-latest"));
assert.ok(MODERATION_PROVIDERS.mistral);
});
test("parseModerationModel routes mistral moderation models to the mistral provider", () => {
assert.deepEqual(parseModerationModel("mistral/mistral-moderation-latest"), {
provider: "mistral",
model: "mistral-moderation-latest",
});
assert.deepEqual(parseModerationModel("mistral-moderation-latest"), {
provider: "mistral",
model: "mistral-moderation-latest",
});
});
test("handleModeration proxies mistral moderation requests to the mistral endpoint", async () => {
let captured: any;
globalThis.fetch = async (url: any, options: any = {}) => {
captured = { url: String(url), headers: options.headers };
return Response.json({ id: "modr-mistral", results: [{ flagged: false }] });
};
const response = await handleModeration({
body: { model: "mistral/mistral-moderation-latest", input: "check this" },
credentials: { apiKey: "sk-mistral" },
});
assert.equal(captured.url, "https://api.mistral.ai/v1/moderations");
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
assert.equal(response.status, 200);
});
test("handleModeration requires input", async () => {
const response = await handleModeration({
body: { model: "openai/omni-moderation-latest" },

View File

@@ -0,0 +1,188 @@
import test from "node:test";
import assert from "node:assert/strict";
const { POST, OPTIONS } = await import("../../src/app/api/v1/ocr/route.ts");
const { handleOcr } = await import("../../open-sse/handlers/ocr.ts");
const { OCR_PROVIDERS, getOcrProvider, parseOcrModel, getAllOcrModels } =
await import("../../open-sse/config/ocrRegistry.ts");
const { v1OcrSchema } = await import("../../src/shared/validation/schemas/apiV1.ts");
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
function ocrRequest(body: string) {
return new Request("http://localhost/v1/ocr", {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
}
// ── Registry ───────────────────────────────────────────────────────────────
test("OCR_PROVIDERS registers mistral with the Mistral OCR base URL", () => {
assert.equal(OCR_PROVIDERS.mistral.baseUrl, "https://api.mistral.ai/v1/ocr");
const provider = getOcrProvider("mistral");
assert.ok(provider);
assert.equal(provider.authHeader, "bearer");
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-ocr-latest"));
});
test("parseOcrModel routes bare and prefixed mistral models to the mistral provider", () => {
assert.deepEqual(parseOcrModel("mistral-ocr-latest"), {
provider: "mistral",
model: "mistral-ocr-latest",
});
assert.deepEqual(parseOcrModel("mistral/mistral-ocr-latest"), {
provider: "mistral",
model: "mistral-ocr-latest",
});
// Unknown model → no provider resolved
assert.deepEqual(parseOcrModel("mystery-model"), {
provider: null,
model: "mystery-model",
});
});
test("getAllOcrModels exposes the mistral OCR model with a provider prefix", () => {
const models = getAllOcrModels();
assert.ok(models.some((m: { id: string }) => m.id === "mistral/mistral-ocr-latest"));
});
// ── Schema (Zod, Rule #7) ────────────────────────────────────────────────────
test("v1OcrSchema rejects a body without a document", () => {
const result = v1OcrSchema.safeParse({ model: "mistral-ocr-latest" });
assert.equal(result.success, false);
});
test("v1OcrSchema accepts a document_url document object", () => {
const result = v1OcrSchema.safeParse({
model: "mistral-ocr-latest",
document: { type: "document_url", document_url: "https://example.com/a.pdf" },
});
assert.equal(result.success, true);
});
test("v1OcrSchema accepts an image_url document object", () => {
const result = v1OcrSchema.safeParse({
document: { type: "image_url", image_url: "https://example.com/a.png" },
});
assert.equal(result.success, true);
});
// ── Route (public /v1/ocr entry point) ───────────────────────────────────────
test("POST /v1/ocr returns 400 for invalid JSON without leaking a stack trace", async () => {
const response = await POST(ocrRequest("not json at all"));
const body = (await response.json()) as any;
assert.equal(response.status, 400);
assert.equal(body.error.message, "Invalid JSON body");
// Rule #12 — error responses must never leak stack traces.
assert.ok(!body.error.message.includes("at /"));
});
test("OPTIONS /v1/ocr answers the CORS preflight", async () => {
const response = await OPTIONS();
assert.equal(response.status, 200);
assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/);
});
// ── Handler ──────────────────────────────────────────────────────────────────
test("handleOcr requires a document", async () => {
const response = await handleOcr({
body: { model: "mistral-ocr-latest" },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 400);
assert.equal(payload.error.message, "document is required");
});
test("handleOcr rejects unknown OCR models", async () => {
const response = await handleOcr({
body: { model: "mystery/ocr", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 400);
assert.match(payload.error.message, /No OCR provider found/);
});
test("handleOcr requires credentials for the resolved provider", async () => {
const response = await handleOcr({
body: { document: { document_url: "x" } },
credentials: null,
});
const payload = (await response.json()) as any;
assert.equal(response.status, 401);
assert.equal(payload.error.message, "No credentials for OCR provider: mistral");
});
test("handleOcr proxies a successful request to the mistral OCR endpoint", async () => {
let captured: any;
globalThis.fetch = async (url: any, options: any = {}) => {
captured = {
url: String(url),
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return Response.json({ pages: [{ index: 0, markdown: "hello" }] });
};
const response = await handleOcr({
body: { document: { type: "document_url", document_url: "https://example.com/a.pdf" } },
credentials: { apiKey: "sk-mistral" },
});
assert.equal(captured.url, "https://api.mistral.ai/v1/ocr");
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
// model defaults to mistral-ocr-latest and the document is forwarded upstream.
assert.equal(captured.body.model, "mistral-ocr-latest");
assert.deepEqual(captured.body.document, {
type: "document_url",
document_url: "https://example.com/a.pdf",
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { pages: [{ index: 0, markdown: "hello" }] });
});
test("handleOcr passes upstream error payloads through with the upstream status", async () => {
globalThis.fetch = async () =>
new Response('{"error":"bad request"}', {
status: 422,
headers: { "content-type": "application/json" },
});
const response = await handleOcr({
body: { model: "mistral/mistral-ocr-latest", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
assert.equal(response.status, 422);
assert.equal(await response.text(), '{"error":"bad request"}');
});
test("handleOcr returns a sanitized 500 when the upstream request throws", async () => {
globalThis.fetch = async () => {
throw new Error("socket closed");
};
const response = await handleOcr({
body: { model: "mistral-ocr-latest", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 500);
assert.match(payload.error.message, /OCR request failed: socket closed/);
assert.ok(!payload.error.message.includes("at /"));
});