mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
[Feature] UI - Litellm test key audio (#16251)
* Add audio/transcriptions and audio/speech to Test key UI * Removed unused import in test * Fixed voice parameter not being passed in
This commit is contained in:
parent
726452dfe8
commit
615f76de88
15 changed files with 676 additions and 34 deletions
|
|
@ -0,0 +1,13 @@
|
|||
import { render } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import AudioRenderer from "./AudioRenderer";
|
||||
import { MessageType } from "./types";
|
||||
|
||||
describe("AudioRenderer", () => {
|
||||
it("should render the audio renderer", () => {
|
||||
const { container } = render(
|
||||
<AudioRenderer message={{ content: "Hello, world!", role: "user", type: "audio" } as unknown as MessageType} />,
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import { MessageType } from "./types";
|
||||
|
||||
interface AudioRendererProps {
|
||||
message: MessageType;
|
||||
}
|
||||
|
||||
const AudioRenderer: React.FC<AudioRendererProps> = ({ message }) => {
|
||||
// Check if this message contains audio
|
||||
if (!message.isAudio || typeof message.content !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<audio controls src={message.content} className="max-w-full" style={{ maxWidth: "500px" }}>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioRenderer;
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
import { render } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import ChatUI from "./ChatUI";
|
||||
|
||||
// Mock scrollIntoView which is not available in jsdom
|
||||
beforeEach(() => {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
});
|
||||
|
||||
describe("ChatUI", () => {
|
||||
it("should render the chat UI", () => {
|
||||
const { getByText } = render(
|
||||
|
|
@ -15,4 +20,54 @@ describe("ChatUI", () => {
|
|||
);
|
||||
expect(getByText("Test Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the voice selector when the endpoint type is audio_speech", async () => {
|
||||
const { getByText, container } = render(
|
||||
<ChatUI
|
||||
accessToken="1234567890"
|
||||
token="1234567890"
|
||||
userRole="user"
|
||||
userID="1234567890"
|
||||
disabledPersonalKeyCreation={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for the component to render
|
||||
await waitFor(() => {
|
||||
expect(getByText("Test Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find the endpoint selector by looking for the "Endpoint Type:" text and its associated Select
|
||||
const endpointTypeText = getByText("Endpoint Type:");
|
||||
const selectContainer = endpointTypeText.parentElement;
|
||||
const selectElement = selectContainer?.querySelector(".ant-select-selector");
|
||||
|
||||
expect(selectElement).toBeInTheDocument();
|
||||
|
||||
// Click on the select to open the dropdown
|
||||
if (selectElement) {
|
||||
fireEvent.mouseDown(selectElement);
|
||||
}
|
||||
|
||||
// Wait for the dropdown to appear and find the audio_speech option
|
||||
await waitFor(() => {
|
||||
const audioSpeechOption = screen.getByText("/v1/audio/speech");
|
||||
expect(audioSpeechOption).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the audio_speech option
|
||||
const audioSpeechOption = screen.getByText("/v1/audio/speech");
|
||||
fireEvent.click(audioSpeechOption);
|
||||
|
||||
// Verify the voice selector appears
|
||||
await waitFor(() => {
|
||||
expect(getByText("Voice")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the voice select component is present
|
||||
const voiceText = getByText("Voice");
|
||||
const voiceSelectContainer = voiceText.parentElement;
|
||||
const voiceSelectElement = voiceSelectContainer?.querySelector(".ant-select");
|
||||
expect(voiceSelectElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { Select, Spin, Typography, Tooltip, Input, Upload, Modal, Button } from
|
|||
import { makeOpenAIChatCompletionRequest } from "./llm_calls/chat_completion";
|
||||
import { makeOpenAIImageGenerationRequest } from "./llm_calls/image_generation";
|
||||
import { makeOpenAIImageEditsRequest } from "./llm_calls/image_edits";
|
||||
import { makeOpenAIAudioSpeechRequest } from "./llm_calls/audio_speech";
|
||||
import { makeOpenAIAudioTranscriptionRequest } from "./llm_calls/audio_transcriptions";
|
||||
import { makeOpenAIResponsesRequest } from "./llm_calls/responses_api";
|
||||
import { makeAnthropicMessagesRequest } from "./llm_calls/anthropic_messages";
|
||||
import { fetchAvailableModels, ModelGroup } from "./llm_calls/fetch_models";
|
||||
|
|
@ -29,6 +31,7 @@ import { createMultimodalMessage, createDisplayMessage } from "./ResponsesImageU
|
|||
import ChatImageUpload from "./ChatImageUpload";
|
||||
import ChatImageRenderer from "./ChatImageRenderer";
|
||||
import { createChatMultimodalMessage, createChatDisplayMessage } from "./ChatImageUtils";
|
||||
import AudioRenderer from "./AudioRenderer";
|
||||
import SessionManagement from "./SessionManagement";
|
||||
import MCPEventsDisplay, { MCPEvent } from "./MCPEventsDisplay";
|
||||
import { SearchResultsDisplay } from "./SearchResultsDisplay";
|
||||
|
|
@ -46,6 +49,7 @@ import {
|
|||
SafetyOutlined,
|
||||
PictureOutlined,
|
||||
CodeOutlined,
|
||||
SoundOutlined,
|
||||
ToolOutlined,
|
||||
FilePdfOutlined,
|
||||
ArrowUpOutlined,
|
||||
|
|
@ -53,6 +57,7 @@ import {
|
|||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { makeOpenAIEmbeddingsRequest } from "./llm_calls/embeddings_api";
|
||||
import { truncateString } from "./chatUtils";
|
||||
import { OPEN_AI_VOICE_SELECT_OPTIONS, OpenAIVoice } from "./chatConstants";
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Dragger } = Upload;
|
||||
|
|
@ -122,6 +127,16 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
return [];
|
||||
}
|
||||
});
|
||||
const [selectedVoice, setSelectedVoice] = useState<OpenAIVoice>(() => {
|
||||
const saved = sessionStorage.getItem("selectedVoice");
|
||||
if (!saved) return "alloy";
|
||||
try {
|
||||
return JSON.parse(saved) as OpenAIVoice;
|
||||
} catch {
|
||||
// If stored value is not valid JSON, treat it as a plain string
|
||||
return saved as OpenAIVoice;
|
||||
}
|
||||
});
|
||||
const [selectedVectorStores, setSelectedVectorStores] = useState<string[]>(() => {
|
||||
const saved = sessionStorage.getItem("selectedVectorStores");
|
||||
try {
|
||||
|
|
@ -156,6 +171,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
const [responsesImagePreviewUrl, setResponsesImagePreviewUrl] = useState<string | null>(null);
|
||||
const [chatUploadedImage, setChatUploadedImage] = useState<File | null>(null);
|
||||
const [chatImagePreviewUrl, setChatImagePreviewUrl] = useState<string | null>(null);
|
||||
const [uploadedAudio, setUploadedAudio] = useState<File | null>(null);
|
||||
const [isGetCodeModalVisible, setIsGetCodeModalVisible] = useState(false);
|
||||
const [generatedCode, setGeneratedCode] = useState("");
|
||||
const [selectedSdk, setSelectedSdk] = useState<"openai" | "azure">("openai");
|
||||
|
|
@ -200,6 +216,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
endpointType,
|
||||
selectedModel,
|
||||
selectedSdk,
|
||||
selectedVoice,
|
||||
});
|
||||
setGeneratedCode(code);
|
||||
}
|
||||
|
|
@ -237,6 +254,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
sessionStorage.setItem("selectedVectorStores", JSON.stringify(selectedVectorStores));
|
||||
sessionStorage.setItem("selectedGuardrails", JSON.stringify(selectedGuardrails));
|
||||
sessionStorage.setItem("selectedMCPTools", JSON.stringify(selectedMCPTools));
|
||||
sessionStorage.setItem("selectedVoice", selectedVoice);
|
||||
|
||||
if (selectedModel) {
|
||||
sessionStorage.setItem("selectedModel", selectedModel);
|
||||
|
|
@ -322,7 +340,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
setChatHistory((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
// if the last message is already from this same role, append
|
||||
if (last && last.role === role && !last.isImage) {
|
||||
if (last && last.role === role && !last.isImage && !last.isAudio) {
|
||||
// build a new object, but only set `model` if it wasn't there already
|
||||
const updated: MessageType = {
|
||||
...last,
|
||||
|
|
@ -348,7 +366,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
setChatHistory((prevHistory) => {
|
||||
const lastMessage = prevHistory[prevHistory.length - 1];
|
||||
|
||||
if (lastMessage && lastMessage.role === "assistant" && !lastMessage.isImage) {
|
||||
if (lastMessage && lastMessage.role === "assistant" && !lastMessage.isImage && !lastMessage.isAudio) {
|
||||
return [
|
||||
...prevHistory.slice(0, prevHistory.length - 1),
|
||||
{
|
||||
|
|
@ -500,11 +518,15 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
]);
|
||||
};
|
||||
|
||||
const updateAudioUI = (audioUrl: string, model: string) => {
|
||||
setChatHistory((prevHistory) => [...prevHistory, { role: "assistant", content: audioUrl, model, isAudio: true }]);
|
||||
};
|
||||
|
||||
const updateChatImageUI = (imageUrl: string, model?: string) => {
|
||||
setChatHistory((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
// If the last message is from assistant and has content, add image to it
|
||||
if (last && last.role === "assistant" && !last.isImage) {
|
||||
if (last && last.role === "assistant" && !last.isImage && !last.isAudio) {
|
||||
const updated = {
|
||||
...last,
|
||||
image: {
|
||||
|
|
@ -602,8 +624,17 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
setChatImagePreviewUrl(null);
|
||||
};
|
||||
|
||||
const handleAudioUpload = (file: File): false => {
|
||||
setUploadedAudio(file);
|
||||
return false; // Prevent default upload behavior
|
||||
};
|
||||
|
||||
const handleRemoveAudio = () => {
|
||||
setUploadedAudio(null);
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (inputMessage.trim() === "") return;
|
||||
if (inputMessage.trim() === "" && endpointType !== EndpointType.TRANSCRIPTION) return;
|
||||
|
||||
// For image edits, require both image and prompt
|
||||
if (endpointType === EndpointType.IMAGE_EDITS && uploadedImages.length === 0) {
|
||||
|
|
@ -611,6 +642,12 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
return;
|
||||
}
|
||||
|
||||
// For audio transcriptions, require audio file
|
||||
if (endpointType === EndpointType.TRANSCRIPTION && !uploadedAudio) {
|
||||
NotificationsManager.fromBackend("Please upload an audio file for transcription");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token || !userRole || !userID) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -672,6 +709,12 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
chatImagePreviewUrl || undefined,
|
||||
chatUploadedImage.name,
|
||||
);
|
||||
} else if (endpointType === EndpointType.TRANSCRIPTION && uploadedAudio) {
|
||||
// For audio transcription, show the audio file name and optional prompt
|
||||
const audioMessage = inputMessage
|
||||
? `🎵 Audio file: ${uploadedAudio.name}\nPrompt: ${inputMessage}`
|
||||
: `🎵 Audio file: ${uploadedAudio.name}`;
|
||||
displayMessage = createDisplayMessage(audioMessage, false);
|
||||
} else {
|
||||
displayMessage = createDisplayMessage(inputMessage, false);
|
||||
}
|
||||
|
|
@ -687,7 +730,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
// For chat completions, we preserve the multimodal content structure
|
||||
const apiChatHistory = [
|
||||
...chatHistory
|
||||
.filter((msg) => !msg.isImage)
|
||||
.filter((msg) => !msg.isImage && !msg.isAudio)
|
||||
.map(({ role, content }) => ({
|
||||
role,
|
||||
content: typeof content === "string" ? content : "",
|
||||
|
|
@ -722,6 +765,17 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
selectedTags,
|
||||
signal,
|
||||
);
|
||||
} else if (endpointType === EndpointType.SPEECH) {
|
||||
// For audio speech
|
||||
await makeOpenAIAudioSpeechRequest(
|
||||
inputMessage,
|
||||
selectedVoice,
|
||||
(audioUrl, model) => updateAudioUI(audioUrl, model),
|
||||
selectedModel || "",
|
||||
effectiveApiKey,
|
||||
selectedTags,
|
||||
signal,
|
||||
);
|
||||
} else if (endpointType === EndpointType.IMAGE_EDITS) {
|
||||
// For image edits
|
||||
if (uploadedImages.length > 0) {
|
||||
|
|
@ -745,7 +799,9 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
} else {
|
||||
// When using UI session management or starting new API session, send full history
|
||||
apiChatHistory = [
|
||||
...chatHistory.filter((msg) => !msg.isImage).map(({ role, content }) => ({ role, content })),
|
||||
...chatHistory
|
||||
.filter((msg) => !msg.isImage && !msg.isAudio)
|
||||
.map(({ role, content }) => ({ role, content })),
|
||||
newUserMessage,
|
||||
];
|
||||
}
|
||||
|
|
@ -770,7 +826,9 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
);
|
||||
} else if (endpointType === EndpointType.ANTHROPIC_MESSAGES) {
|
||||
const apiChatHistory = [
|
||||
...chatHistory.filter((msg) => !msg.isImage).map(({ role, content }) => ({ role, content })),
|
||||
...chatHistory
|
||||
.filter((msg) => !msg.isImage && !msg.isAudio)
|
||||
.map(({ role, content }) => ({ role, content })),
|
||||
newUserMessage,
|
||||
];
|
||||
|
||||
|
|
@ -797,6 +855,18 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
effectiveApiKey,
|
||||
selectedTags,
|
||||
);
|
||||
} else if (endpointType === EndpointType.TRANSCRIPTION) {
|
||||
// For audio transcriptions
|
||||
if (uploadedAudio) {
|
||||
await makeOpenAIAudioTranscriptionRequest(
|
||||
uploadedAudio,
|
||||
(transcription, model) => updateTextUI("assistant", transcription, model),
|
||||
selectedModel,
|
||||
effectiveApiKey,
|
||||
selectedTags,
|
||||
signal,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -821,12 +891,23 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
if (endpointType === EndpointType.CHAT && chatUploadedImage) {
|
||||
handleRemoveChatImage();
|
||||
}
|
||||
// Clear audio after successful request for transcription
|
||||
if (endpointType === EndpointType.TRANSCRIPTION && uploadedAudio) {
|
||||
handleRemoveAudio();
|
||||
}
|
||||
}
|
||||
|
||||
setInputMessage("");
|
||||
};
|
||||
|
||||
const clearChatHistory = () => {
|
||||
// Clean up audio object URLs before clearing history
|
||||
chatHistory.forEach((message) => {
|
||||
if (message.isAudio && typeof message.content === "string") {
|
||||
URL.revokeObjectURL(message.content);
|
||||
}
|
||||
});
|
||||
|
||||
setChatHistory([]);
|
||||
setMessageTraceId(null);
|
||||
setResponsesSessionId(null); // Clear responses session ID
|
||||
|
|
@ -834,6 +915,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
handleRemoveAllImages(); // Clear any uploaded images for image edits
|
||||
handleRemoveResponsesImage(); // Clear any uploaded images for responses
|
||||
handleRemoveChatImage(); // Clear any uploaded images for chat completions
|
||||
handleRemoveAudio(); // Clear any uploaded audio for transcription
|
||||
sessionStorage.removeItem("chatHistory");
|
||||
sessionStorage.removeItem("messageTraceId");
|
||||
sessionStorage.removeItem("responsesSessionId");
|
||||
|
|
@ -946,6 +1028,26 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
className="mb-4"
|
||||
/>
|
||||
|
||||
{/* Voice Selector for Speech Endpoint */}
|
||||
{endpointType === EndpointType.SPEECH && (
|
||||
<div className="mb-4">
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<SoundOutlined className="mr-2" />
|
||||
Voice
|
||||
</Text>
|
||||
<Select
|
||||
value={selectedVoice}
|
||||
onChange={(value) => {
|
||||
setSelectedVoice(value);
|
||||
sessionStorage.setItem("selectedVoice", value);
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
className="rounded-md"
|
||||
options={OPEN_AI_VOICE_SELECT_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Management Component */}
|
||||
<SessionManagement
|
||||
endpointType={endpointType}
|
||||
|
|
@ -1086,7 +1188,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
{chatHistory.length === 0 && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-gray-400">
|
||||
<RobotOutlined style={{ fontSize: "48px", marginBottom: "16px" }} />
|
||||
<Text>Start a conversation or generate an image</Text>
|
||||
<Text>Start a conversation, generate an image, or handle audio</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1154,6 +1256,8 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
className="max-w-full rounded-md border border-gray-200 shadow-sm"
|
||||
style={{ maxHeight: "500px" }}
|
||||
/>
|
||||
) : message.isAudio ? (
|
||||
<AudioRenderer message={message} />
|
||||
) : (
|
||||
<>
|
||||
{/* Show attached image for user messages based on current endpoint */}
|
||||
|
|
@ -1274,12 +1378,7 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
{endpointType === EndpointType.IMAGE_EDITS && (
|
||||
<div className="mb-4">
|
||||
{uploadedImages.length === 0 ? (
|
||||
<Dragger
|
||||
beforeUpload={handleImageUpload}
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
className="border-dashed border-2 border-gray-300 rounded-lg p-4"
|
||||
>
|
||||
<Dragger beforeUpload={handleImageUpload} accept="image/*" showUploadList={false}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<PictureOutlined style={{ fontSize: "24px", color: "#666" }} />
|
||||
</p>
|
||||
|
|
@ -1330,6 +1429,44 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio Upload Section for Transcriptions */}
|
||||
{endpointType === EndpointType.TRANSCRIPTION && (
|
||||
<div className="mb-4">
|
||||
{!uploadedAudio ? (
|
||||
<Dragger
|
||||
beforeUpload={handleAudioUpload}
|
||||
accept="audio/*,.mp3,.mp4,.mpeg,.mpga,.m4a,.wav,.webm"
|
||||
showUploadList={false}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<SoundOutlined style={{ fontSize: "24px", color: "#666" }} />
|
||||
</p>
|
||||
<p className="ant-upload-text text-sm">Click or drag audio file to upload</p>
|
||||
<p className="ant-upload-hint text-xs text-gray-500">
|
||||
Support for MP3, MP4, MPEG, MPGA, M4A, WAV, WEBM formats. Max file size: 25 MB.
|
||||
</p>
|
||||
</Dragger>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<SoundOutlined style={{ fontSize: "20px", color: "#666" }} />
|
||||
<span className="text-sm font-medium">{uploadedAudio.name}</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({(uploadedAudio.size / 1024 / 1024).toFixed(2)} MB)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="bg-white shadow-sm border border-gray-200 rounded px-2 py-1 text-red-500 hover:bg-red-50 text-xs"
|
||||
onClick={handleRemoveAudio}
|
||||
>
|
||||
<DeleteOutlined /> Remove
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show file previews above input when files are uploaded */}
|
||||
{endpointType === EndpointType.RESPONSES && responsesUploadedImage && (
|
||||
<div className="mb-2">
|
||||
|
|
@ -1430,7 +1567,11 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
? "Type your message... (Shift+Enter for new line)"
|
||||
: endpointType === EndpointType.IMAGE_EDITS
|
||||
? "Describe how you want to edit the image..."
|
||||
: "Describe the image you want to generate..."
|
||||
: endpointType === EndpointType.SPEECH
|
||||
? "Enter text to convert to speech..."
|
||||
: endpointType === EndpointType.TRANSCRIPTION
|
||||
? "Optional: Add context or prompt for transcription..."
|
||||
: "Describe the image you want to generate..."
|
||||
}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
|
|
@ -1449,7 +1590,9 @@ const ChatUI: React.FC<ChatUIProps> = ({ accessToken, token, userRole, userID, d
|
|||
{/* Right: send button - matching blue theme */}
|
||||
<TremorButton
|
||||
onClick={handleSendMessage}
|
||||
disabled={isLoading || !inputMessage.trim()}
|
||||
disabled={
|
||||
isLoading || (endpointType === EndpointType.TRANSCRIPTION ? !uploadedAudio : !inputMessage.trim())
|
||||
}
|
||||
className="flex-shrink-0 ml-2 !w-8 !h-8 !min-w-8 !p-0 !rounded-full !bg-blue-600 hover:!bg-blue-700 disabled:!bg-gray-300 !border-none !text-white disabled:!text-gray-500 !flex !items-center !justify-center"
|
||||
>
|
||||
<ArrowUpOutlined style={{ fontSize: "14px" }} />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ describe("CodeSnippets", () => {
|
|||
selectedGuardrails: [],
|
||||
selectedMCPTools: [],
|
||||
selectedSdk: "openai",
|
||||
selectedVoice: "alloy",
|
||||
});
|
||||
expect(code).toContain("text-embedding-3-small");
|
||||
expect(code).toContain("Hello, world!");
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface GenerateCodeParams {
|
|||
selectedVectorStores: string[];
|
||||
selectedGuardrails: string[];
|
||||
selectedMCPTools: string[];
|
||||
selectedVoice: string;
|
||||
endpointType: string;
|
||||
selectedModel: string | undefined;
|
||||
selectedSdk: "openai" | "azure";
|
||||
|
|
@ -33,6 +34,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
|||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedMCPTools,
|
||||
selectedVoice,
|
||||
endpointType,
|
||||
selectedModel,
|
||||
selectedSdk,
|
||||
|
|
@ -502,6 +504,45 @@ response = client.embeddings.create(
|
|||
)
|
||||
|
||||
print(response.data[0].embedding)
|
||||
`;
|
||||
break;
|
||||
case EndpointType.TRANSCRIPTION:
|
||||
endpointSpecificCode = `
|
||||
# Open the audio file
|
||||
audio_file = open("path/to/your/audio/file.mp3", "rb")
|
||||
|
||||
# Make the transcription request
|
||||
response = client.audio.transcriptions.create(
|
||||
model="${modelNameForCode}",
|
||||
file=audio_file${inputMessage ? `,\n prompt="${inputMessage.replace(/"/g, '\\"')}"` : ""}
|
||||
)
|
||||
|
||||
print(response.text)
|
||||
`;
|
||||
break;
|
||||
case EndpointType.SPEECH:
|
||||
endpointSpecificCode = `
|
||||
# Make the text-to-speech request
|
||||
response = client.audio.speech.create(
|
||||
model="${modelNameForCode}",
|
||||
input="${inputMessage || "Your text to convert to speech here"}",
|
||||
voice="${selectedVoice}" # Options: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer
|
||||
)
|
||||
|
||||
# Save the audio to a file
|
||||
output_filename = "output_speech.mp3"
|
||||
response.stream_to_file(output_filename)
|
||||
print(f"Audio saved to {output_filename}")
|
||||
|
||||
# Optional: Customize response format and speed
|
||||
# response = client.audio.speech.create(
|
||||
# model="${modelNameForCode}",
|
||||
# input="${inputMessage || "Your text to convert to speech here"}",
|
||||
# voice="alloy",
|
||||
# response_format="mp3", # Options: mp3, opus, aac, flac, wav, pcm
|
||||
# speed=1.0 # Range: 0.25 to 4.0
|
||||
# )
|
||||
# response.stream_to_file("output_speech.mp3")
|
||||
`;
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { render, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import EndpointSelector, { endpointOptions } from "./EndpointSelector";
|
||||
import EndpointSelector from "./EndpointSelector";
|
||||
import { ENDPOINT_OPTIONS } from "./chatConstants";
|
||||
|
||||
describe("EndpointSelector", () => {
|
||||
Object.values(endpointOptions).forEach((endpointType) => {
|
||||
Object.values(ENDPOINT_OPTIONS).forEach((endpointType) => {
|
||||
it(`should render the endpoint selector for ${endpointType.value}`, async () => {
|
||||
const { getByText } = render(<EndpointSelector endpointType={endpointType.value} onEndpointChange={() => {}} />);
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Select } from "antd";
|
||||
import { Text } from "@tremor/react";
|
||||
import { EndpointType } from "./mode_endpoint_mapping";
|
||||
import { ENDPOINT_OPTIONS } from "./chatConstants";
|
||||
|
||||
interface EndpointSelectorProps {
|
||||
endpointType: string; // Accept string to avoid type conflicts
|
||||
|
|
@ -9,18 +9,6 @@ interface EndpointSelectorProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable component for selecting API endpoints
|
||||
*/
|
||||
export const endpointOptions = [
|
||||
{ value: EndpointType.CHAT, label: "/v1/chat/completions" },
|
||||
{ value: EndpointType.RESPONSES, label: "/v1/responses" },
|
||||
{ value: EndpointType.ANTHROPIC_MESSAGES, label: "/v1/messages" },
|
||||
{ value: EndpointType.IMAGE, label: "/v1/images/generations" },
|
||||
{ value: EndpointType.IMAGE_EDITS, label: "/v1/images/edits" },
|
||||
{ value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" },
|
||||
];
|
||||
|
||||
const EndpointSelector: React.FC<EndpointSelectorProps> = ({ endpointType, onEndpointChange, className }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
|
|
@ -30,7 +18,7 @@ const EndpointSelector: React.FC<EndpointSelectorProps> = ({ endpointType, onEnd
|
|||
value={endpointType}
|
||||
style={{ width: "100%" }}
|
||||
onChange={onEndpointChange}
|
||||
options={endpointOptions}
|
||||
options={ENDPOINT_OPTIONS}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
45
ui/litellm-dashboard/src/components/chat_ui/chatConstants.ts
Normal file
45
ui/litellm-dashboard/src/components/chat_ui/chatConstants.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { EndpointType } from "./mode_endpoint_mapping";
|
||||
|
||||
export const OPEN_AI_VOICES = {
|
||||
ALLOY: "alloy",
|
||||
ASH: "ash",
|
||||
BALAD: "ballad",
|
||||
CORAL: "coral",
|
||||
ECHO: "echo",
|
||||
FABLE: "fable",
|
||||
NOVA: "nova",
|
||||
ONYX: "onyx",
|
||||
SAGE: "sage",
|
||||
SHIMMER: "shimmer",
|
||||
} as const;
|
||||
|
||||
export type OpenAIVoice = (typeof OPEN_AI_VOICES)[keyof typeof OPEN_AI_VOICES];
|
||||
|
||||
export const OPEN_AI_VOICE_LABELS = {
|
||||
ALLOY: "Alloy - Professional and confident",
|
||||
ASH: "Ash - Casual and relaxed",
|
||||
BALAD: "Ballad - Smooth and melodic",
|
||||
CORAL: "Coral - Warm and engaging",
|
||||
ECHO: "Echo - Friendly and conversational",
|
||||
FABLE: "Fable - Wise and measured",
|
||||
NOVA: "Nova - Friendly and conversational",
|
||||
ONYX: "Onyx - Deep and authoritative",
|
||||
SAGE: "Sage - Wise and measured",
|
||||
SHIMMER: "Shimmer - Bright and cheerful",
|
||||
};
|
||||
|
||||
export const OPEN_AI_VOICE_SELECT_OPTIONS = Object.entries(OPEN_AI_VOICES).map(([key, voice]) => ({
|
||||
value: voice,
|
||||
label: OPEN_AI_VOICE_LABELS[key as keyof typeof OPEN_AI_VOICE_LABELS],
|
||||
}));
|
||||
|
||||
export const ENDPOINT_OPTIONS = [
|
||||
{ value: EndpointType.CHAT, label: "/v1/chat/completions" },
|
||||
{ value: EndpointType.RESPONSES, label: "/v1/responses" },
|
||||
{ value: EndpointType.ANTHROPIC_MESSAGES, label: "/v1/messages" },
|
||||
{ value: EndpointType.IMAGE, label: "/v1/images/generations" },
|
||||
{ value: EndpointType.IMAGE_EDITS, label: "/v1/images/edits" },
|
||||
{ value: EndpointType.EMBEDDINGS, label: "/v1/embeddings" },
|
||||
{ value: EndpointType.SPEECH, label: "/v1/audio/speech" },
|
||||
{ value: EndpointType.TRANSCRIPTION, label: "/v1/audio/transcriptions" },
|
||||
];
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { makeOpenAIAudioSpeechRequest } from "./audio_speech";
|
||||
import OpenAI from "openai";
|
||||
|
||||
vi.mock("openai");
|
||||
|
||||
// Mock URL.createObjectURL
|
||||
global.URL.createObjectURL = vi.fn(() => "blob:mock-audio-url");
|
||||
|
||||
describe("audio_speech", () => {
|
||||
const mockCreate = vi.fn();
|
||||
const mockUpdateUI = vi.fn();
|
||||
const mockBlob = new Blob(["mock audio data"], { type: "audio/mpeg" });
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the response structure from OpenAI audio speech API
|
||||
mockCreate.mockResolvedValue({
|
||||
blob: vi.fn().mockResolvedValue(mockBlob),
|
||||
});
|
||||
|
||||
// Mock the OpenAI constructor and its methods
|
||||
(OpenAI as any).mockImplementation(() => ({
|
||||
audio: {
|
||||
speech: {
|
||||
create: mockCreate,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should make a request to the audio speech API with basic parameters", async () => {
|
||||
await makeOpenAIAudioSpeechRequest("Hello, world!", "alloy", mockUpdateUI, "tts-1", "sk-1234567890", []);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
model: "tts-1",
|
||||
input: "Hello, world!",
|
||||
voice: "alloy",
|
||||
},
|
||||
{ signal: undefined },
|
||||
);
|
||||
expect(mockUpdateUI).toHaveBeenCalledWith("blob:mock-audio-url", "tts-1");
|
||||
});
|
||||
|
||||
it("should include optional parameters when provided", async () => {
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await makeOpenAIAudioSpeechRequest(
|
||||
"Test input",
|
||||
"nova",
|
||||
mockUpdateUI,
|
||||
"tts-1-hd",
|
||||
"sk-1234567890",
|
||||
["tag1", "tag2"],
|
||||
signal,
|
||||
"mp3",
|
||||
1.5,
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
model: "tts-1-hd",
|
||||
input: "Test input",
|
||||
voice: "nova",
|
||||
response_format: "mp3",
|
||||
speed: 1.5,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
expect(mockUpdateUI).toHaveBeenCalledWith("blob:mock-audio-url", "tts-1-hd");
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const mockError = new Error("API Error");
|
||||
mockCreate.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
makeOpenAIAudioSpeechRequest("Hello, world!", "alloy", mockUpdateUI, "tts-1", "sk-1234567890", []),
|
||||
).rejects.toThrow("API Error");
|
||||
|
||||
expect(mockUpdateUI).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import openai from "openai";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import NotificationManager from "@/components/molecules/notifications_manager";
|
||||
import type { OpenAIVoice } from "../chatConstants";
|
||||
|
||||
export async function makeOpenAIAudioSpeechRequest(
|
||||
input: string,
|
||||
voice: OpenAIVoice,
|
||||
updateUI: (audioUrl: string, model: string) => void,
|
||||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal,
|
||||
responseFormat?: string,
|
||||
speed?: number,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal !== true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.audio.speech.create(
|
||||
{
|
||||
model: selectedModel,
|
||||
input: input,
|
||||
voice,
|
||||
...(responseFormat ? { response_format: responseFormat as any } : {}),
|
||||
...(speed ? { speed: speed } : {}),
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
// Convert the response to a blob and create an object URL
|
||||
// The response from OpenAI SDK in the browser is a Response object with a blob() method
|
||||
const blob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
|
||||
updateUI(audioUrl, selectedModel);
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
console.log("Audio speech request was cancelled");
|
||||
} else {
|
||||
NotificationManager.fromBackend(`Error occurred while generating speech. Please try again. Error: ${error}`);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { makeOpenAIAudioTranscriptionRequest } from "./audio_transcriptions";
|
||||
import OpenAI from "openai";
|
||||
|
||||
vi.mock("openai");
|
||||
|
||||
describe("audio_transcription", () => {
|
||||
const mockCreate = vi.fn();
|
||||
const mockUpdateUI = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the response structure from OpenAI audio transcription API
|
||||
mockCreate.mockResolvedValue({
|
||||
text: "This is the transcribed text from the audio file.",
|
||||
});
|
||||
|
||||
// Mock the OpenAI constructor and its methods
|
||||
(OpenAI as any).mockImplementation(() => ({
|
||||
audio: {
|
||||
transcriptions: {
|
||||
create: mockCreate,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should make a request to the audio transcription API with basic parameters", async () => {
|
||||
const mockFile = new File(["audio data"], "test.wav", {
|
||||
type: "audio/wav",
|
||||
});
|
||||
|
||||
await makeOpenAIAudioTranscriptionRequest(mockFile, mockUpdateUI, "whisper-1", "sk-1234567890", []);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
model: "whisper-1",
|
||||
file: mockFile,
|
||||
},
|
||||
{ signal: undefined },
|
||||
);
|
||||
expect(mockUpdateUI).toHaveBeenCalledWith("This is the transcribed text from the audio file.", "whisper-1");
|
||||
});
|
||||
|
||||
it("should include optional parameters when provided", async () => {
|
||||
const mockFile = new File(["audio data"], "test.mp3", {
|
||||
type: "audio/mpeg",
|
||||
});
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await makeOpenAIAudioTranscriptionRequest(
|
||||
mockFile,
|
||||
mockUpdateUI,
|
||||
"whisper-1",
|
||||
"sk-1234567890",
|
||||
["tag1", "tag2"],
|
||||
signal,
|
||||
"en",
|
||||
"This is a prompt",
|
||||
"json",
|
||||
0.5,
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
model: "whisper-1",
|
||||
file: mockFile,
|
||||
language: "en",
|
||||
prompt: "This is a prompt",
|
||||
response_format: "json",
|
||||
temperature: 0.5,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
expect(mockUpdateUI).toHaveBeenCalledWith("This is the transcribed text from the audio file.", "whisper-1");
|
||||
});
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const mockError = new Error("API Error");
|
||||
mockCreate.mockRejectedValue(mockError);
|
||||
const mockFile = new File(["audio data"], "test.wav", {
|
||||
type: "audio/wav",
|
||||
});
|
||||
|
||||
await expect(
|
||||
makeOpenAIAudioTranscriptionRequest(mockFile, mockUpdateUI, "whisper-1", "sk-1234567890", []),
|
||||
).rejects.toThrow("API Error");
|
||||
|
||||
expect(mockUpdateUI).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle missing text in response", async () => {
|
||||
mockCreate.mockResolvedValue({});
|
||||
const mockFile = new File(["audio data"], "test.wav", {
|
||||
type: "audio/wav",
|
||||
});
|
||||
|
||||
await expect(
|
||||
makeOpenAIAudioTranscriptionRequest(mockFile, mockUpdateUI, "whisper-1", "sk-1234567890", []),
|
||||
).rejects.toThrow("No transcription text in response");
|
||||
|
||||
expect(mockUpdateUI).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import openai from "openai";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import NotificationManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
export async function makeOpenAIAudioTranscriptionRequest(
|
||||
audioFile: File,
|
||||
updateUI: (transcription: string, model: string) => void,
|
||||
selectedModel: string,
|
||||
accessToken: string,
|
||||
tags?: string[],
|
||||
signal?: AbortSignal,
|
||||
language?: string,
|
||||
prompt?: string,
|
||||
responseFormat?: string,
|
||||
temperature?: number,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal !== true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("Processing audio file for transcription:", audioFile.name);
|
||||
|
||||
const response = await client.audio.transcriptions.create(
|
||||
{
|
||||
model: selectedModel,
|
||||
file: audioFile,
|
||||
...(language ? { language: language } : {}),
|
||||
...(prompt ? { prompt: prompt } : {}),
|
||||
...(responseFormat ? { response_format: responseFormat as any } : {}),
|
||||
...(temperature !== undefined ? { temperature: temperature } : {}),
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
console.log("Transcription response:", response);
|
||||
|
||||
// The response is a transcription object with a text field
|
||||
if (response && response.text) {
|
||||
updateUI(response.text, selectedModel);
|
||||
NotificationManager.success(`Audio transcribed successfully`);
|
||||
} else {
|
||||
throw new Error("No transcription text in response");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error making audio transcription request:", error);
|
||||
|
||||
if (signal?.aborted) {
|
||||
console.log("Audio transcription request was cancelled");
|
||||
} else {
|
||||
let errorMessage = "Failed to transcribe audio";
|
||||
|
||||
if (error?.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
} else if (error?.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
NotificationManager.fromBackend(`Audio transcription failed: ${errorMessage}`);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
// Define an enum for the modes as returned in model_info
|
||||
export enum ModelMode {
|
||||
AUDIO_SPEECH = "audio_speech",
|
||||
AUDIO_TRANSCRIPTION = "audio_transcription",
|
||||
IMAGE_GENERATION = "image_generation",
|
||||
CHAT = "chat",
|
||||
RESPONSES = "responses",
|
||||
|
|
@ -18,6 +20,8 @@ export enum EndpointType {
|
|||
IMAGE_EDITS = "image_edits",
|
||||
ANTHROPIC_MESSAGES = "anthropic_messages",
|
||||
EMBEDDINGS = "embeddings",
|
||||
SPEECH = "speech",
|
||||
TRANSCRIPTION = "transcription",
|
||||
// add additional endpoint types if required
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +32,8 @@ export const litellmModeMapping: Record<ModelMode, EndpointType> = {
|
|||
[ModelMode.RESPONSES]: EndpointType.RESPONSES,
|
||||
[ModelMode.IMAGE_EDITS]: EndpointType.IMAGE_EDITS,
|
||||
[ModelMode.ANTHROPIC_MESSAGES]: EndpointType.ANTHROPIC_MESSAGES,
|
||||
[ModelMode.AUDIO_SPEECH]: EndpointType.SPEECH,
|
||||
[ModelMode.AUDIO_TRANSCRIPTION]: EndpointType.TRANSCRIPTION,
|
||||
};
|
||||
|
||||
export const getEndpointType = (mode: string): EndpointType => {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export interface MessageType {
|
|||
content: string | MultimodalContent[];
|
||||
model?: string;
|
||||
isImage?: boolean;
|
||||
isAudio?: boolean;
|
||||
reasoningContent?: string;
|
||||
timeToFirstToken?: number;
|
||||
usage?: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue