mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
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:
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user