@@ -1998,10 +2153,15 @@ const ChatUI: React.FC = ({
onSubmit={handleSendMessage}
onCancel={handleCancelRequest}
placeholder={inputPlaceholder}
- disabled={isLoading}
+ disabled={isLoading || endpointType === EndpointType.OCR}
isLoading={isLoading}
submitDisabled={sendDisabled}
- showSuggestions={chatHistory.length === 0 && !isLoading && endpointType !== EndpointType.MCP}
+ showSuggestions={
+ chatHistory.length === 0 &&
+ !isLoading &&
+ endpointType !== EndpointType.MCP &&
+ endpointType !== EndpointType.OCR
+ }
suggestions={
endpointType === EndpointType.A2A_AGENTS
? ["What can you help me with?", "Tell me about yourself", "What tasks can you perform?"]
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx
index a1e88e51053..d09be961727 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx
@@ -26,4 +26,18 @@ describe("EndpointSelector", () => {
expect(await screen.findByText("/v1/audio/speech")).toBeInTheDocument();
expect(await screen.findByText("/v1/audio/transcriptions")).toBeInTheDocument();
});
+
+ it("should filter and show the OCR endpoint when user inputs 'ocr'", async () => {
+ const user = userEvent.setup();
+ render( {}} />);
+
+ const combobox = screen.getByRole("combobox");
+ await user.click(combobox);
+
+ const input = await screen.findByRole("combobox");
+ await user.clear(input);
+ await user.type(input, "ocr");
+
+ expect(await screen.findByText("/v1/ocr")).toBeInTheDocument();
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts
index 2b59fbad2ee..1779b92d36a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts
@@ -42,6 +42,7 @@ export const ENDPOINT_OPTIONS = [
{ value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" },
{ value: EndpointType.SPEECH, label: "/v1/audio/speech" },
{ value: EndpointType.TRANSCRIPTION, label: "/v1/audio/transcriptions" },
+ { value: EndpointType.OCR, label: "/v1/ocr" },
{ value: EndpointType.A2A_AGENTS, label: "/v1/a2a/message/send" },
{ value: EndpointType.MCP, label: "/mcp-rest/tools/call" },
{ value: EndpointType.REALTIME, label: "/v1/realtime" },
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.test.tsx
new file mode 100644
index 00000000000..d19016fae72
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.test.tsx
@@ -0,0 +1,62 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { makeOpenAIOcrRequest } from "./OcrApi";
+
+vi.mock("@/components/networking", () => ({
+ getProxyBaseUrl: vi.fn(() => "https://example.com"),
+ getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
+}));
+
+describe("ocr_api", () => {
+ const mockUpdateUI = vi.fn();
+ const mockFetch = vi.fn();
+
+ beforeEach(() => {
+ const responseBody = JSON.stringify({
+ pages: [
+ {
+ index: 0,
+ markdown: "# Extracted text",
+ },
+ ],
+ model: "mistral-ocr-latest",
+ });
+ mockFetch.mockResolvedValue({
+ ok: true,
+ text: async () => responseBody,
+ } as Response);
+
+ global.fetch = mockFetch;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("posts OCR uploads to /v1/ocr with model and file multipart fields", async () => {
+ const file = new File(["document data"], "invoice.pdf", { type: "application/pdf" });
+
+ await makeOpenAIOcrRequest({
+ file,
+ updateUI: mockUpdateUI,
+ selectedModel: "mistral-ocr-latest",
+ accessToken: "sk-1234567890",
+ tags: ["tag1", "tag2"],
+ });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(mockFetch).toHaveBeenCalledWith("https://example.com/v1/ocr", {
+ method: "POST",
+ headers: {
+ Authorization: "Bearer sk-1234567890",
+ "x-litellm-tags": "tag1,tag2",
+ },
+ body: expect.any(FormData),
+ signal: undefined,
+ });
+
+ const formData = mockFetch.mock.calls[0][1].body as FormData;
+ expect(formData.get("model")).toBe("mistral-ocr-latest");
+ expect(formData.get("file")).toBe(file);
+ expect(mockUpdateUI).toHaveBeenCalledWith("# Extracted text", "mistral-ocr-latest");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.tsx
new file mode 100644
index 00000000000..bc13af33ead
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/OcrApi.tsx
@@ -0,0 +1,66 @@
+import { toast } from "@/lib/toast";
+import { getGlobalLitellmHeaderName, getProxyBaseUrl } from "@/components/networking";
+import { createApiClient } from "@/lib/http/client";
+
+interface OcrRequestParams {
+ file: File;
+ updateUI: (text: string, model: string) => void;
+ selectedModel: string;
+ accessToken: string;
+ tags?: string[];
+ signal?: AbortSignal;
+ customBaseUrl?: string;
+}
+
+const formatOcrResponse = (response: unknown): string => {
+ if (typeof response !== "object" || response === null || !("pages" in response)) {
+ return JSON.stringify(response, null, 2);
+ }
+
+ const pages = (response as { pages?: unknown }).pages;
+ if (!Array.isArray(pages)) {
+ return JSON.stringify(response, null, 2);
+ }
+
+ const markdown = pages
+ .map((page) =>
+ typeof page === "object" && page !== null && "markdown" in page
+ ? (page as { markdown?: unknown }).markdown
+ : undefined,
+ )
+ .filter((pageMarkdown): pageMarkdown is string => typeof pageMarkdown === "string" && pageMarkdown.length > 0)
+ .join("\n\n");
+
+ return markdown || JSON.stringify(response, null, 2);
+};
+
+export async function makeOpenAIOcrRequest({
+ file,
+ updateUI,
+ selectedModel,
+ accessToken,
+ tags,
+ signal,
+ customBaseUrl,
+}: OcrRequestParams) {
+ const client = createApiClient({
+ getBaseUrl: () => customBaseUrl || getProxyBaseUrl(),
+ getAuthHeaderName: getGlobalLitellmHeaderName,
+ onError: (message) => toast.fromError(`OCR failed: ${message}`),
+ });
+ const formData = new FormData();
+ formData.append("model", selectedModel);
+ formData.append("file", file);
+
+ const responseJson = await client.post("/v1/ocr", {
+ accessToken,
+ rawBody: formData,
+ headers: {
+ ...(tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : {}),
+ },
+ signal,
+ });
+
+ updateUI(formatOcrResponse(responseJson), selectedModel);
+ toast.success("OCR completed successfully");
+}
diff --git a/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.test.ts b/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.test.ts
new file mode 100644
index 00000000000..d4a51d392b6
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest";
+import { EndpointType, getEndpointType, litellmModeMapping, ModelMode } from "./mode_endpoint_mapping";
+
+describe("mode_endpoint_mapping", () => {
+ it("maps OCR model mode to the OCR endpoint", () => {
+ expect(getEndpointType("ocr")).toBe(EndpointType.OCR);
+ expect(litellmModeMapping[ModelMode.OCR]).toBe(EndpointType.OCR);
+ });
+
+ it("preserves existing model mode mappings", () => {
+ expect(getEndpointType("chat")).toBe(EndpointType.CHAT);
+ expect(getEndpointType("responses")).toBe(EndpointType.RESPONSES);
+ expect(getEndpointType("image_generation")).toBe(EndpointType.IMAGE);
+ expect(getEndpointType("audio_transcription")).toBe(EndpointType.TRANSCRIPTION);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx b/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx
index 930ded5d1a5..1125405e714 100644
--- a/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx
+++ b/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx
@@ -13,6 +13,7 @@ export enum ModelMode {
ANTHROPIC_MESSAGES = "anthropic_messages",
EMBEDDING = "embedding",
REALTIME = "realtime",
+ OCR = "ocr",
}
// Define an enum for the endpoint types your UI calls
@@ -26,6 +27,7 @@ export enum EndpointType {
EMBEDDINGS = "embeddings",
SPEECH = "speech",
TRANSCRIPTION = "transcription",
+ OCR = "ocr",
A2A_AGENTS = "a2a_agents",
MCP = "mcp",
REALTIME = "realtime",
@@ -45,6 +47,7 @@ export const litellmModeMapping: Record = {
[ModelMode.AUDIO_TRANSCRIPTION]: EndpointType.TRANSCRIPTION,
[ModelMode.EMBEDDING]: EndpointType.EMBEDDINGS,
[ModelMode.REALTIME]: EndpointType.REALTIME,
+ [ModelMode.OCR]: EndpointType.OCR,
};
export const getEndpointType = (mode: string): EndpointType => {