refactor(api): type OCR/moderation registries and add OCR playground card

- Add OcrProvider/ModerationProvider interfaces + Record types to the ocr
  and moderation registries, matching the typed embedding/audio registries.
- Add an OCR mini-playground card (OcrExampleCard) and wire case "ocr" so
  the new OCR provider category has UI parity with the other media kinds.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-02 15:36:43 -03:00
parent 5376f6d9a8
commit d2f3a22ecd
4 changed files with 183 additions and 18 deletions

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",
@@ -26,19 +44,19 @@ export const MODERATION_PROVIDERS = {
};
/**
* 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) };
}
@@ -54,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

@@ -5,7 +5,25 @@
* Follows Mistral's OCR API format.
*/
export const OCR_PROVIDERS = {
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",
@@ -16,22 +34,22 @@ export const OCR_PROVIDERS = {
};
/**
* Get OCR provider config by ID
* Get OCR provider config by ID.
*/
export function getOcrProvider(providerId) {
export function getOcrProvider(providerId: string): OcrProvider | null {
return OCR_PROVIDERS[providerId] || null;
}
/**
* Parse OCR model string
* 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) {
export function parseOcrModel(modelStr: string | null | undefined): ParsedOcrModel {
if (!modelStr) return { provider: null, model: null };
for (const [providerId] of Object.entries(OCR_PROVIDERS)) {
for (const providerId of Object.keys(OCR_PROVIDERS)) {
if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
}
@@ -47,10 +65,10 @@ export function parseOcrModel(modelStr) {
}
/**
* Get all OCR models as a flat list
* Get all OCR models as a flat list.
*/
export function getAllOcrModels() {
const models = [];
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({

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>
);
}