feat: add openai realtime api (#2006)

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
ANKIT VARSHNEY
2025-06-20 08:52:04 +05:30
committed by GitHub
parent a6cef9c6be
commit d8ac8d385d
31 changed files with 2393 additions and 650 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@llamaindex/core": patch
"@llamaindex/google": patch
"@llamaindex/openai": patch
---
Feat: add support for openai realtime API
@@ -378,3 +378,186 @@ async function main() {
## API Reference
- [OpenAI](/docs/api/classes/OpenAI)
# OpenAI Live LLM
The OpenAI Live LLM integration in LlamaIndex provides real-time chat capabilities with support for audio streaming and tool calling.
## Basic Usage
```typescript
import { openai } from "@llamaindex/openai";
import { tool, ModalityType } from "llamaindex";
// Get the ephimeral key on the server
const serverllm = openai({
apiKey: "your-api-key",
model: "gpt-4o-realtime-preview-2025-06-03",
});
// Get an ephemeral key
// Usually this code is run on the server and the ephemeral key is passed to the
// client - the ephemeral key can be securely used on the client side
const ephemeralKey = await serverllm.live.getEphemeralKey();
// Create a client-side LLM instance with the ephemeral key
const llm = openai({
apiKey: ephemeralKey,
model: "gpt-4o-realtime-preview-2025-06-03"
});
// Create a live sessionimport { tool } from "llamaindex";
const session = await llm.live.connect({
systemInstruction: "You are a helpful assistant.",
});
// Send a message
session.sendMessage({
content: "Hello!",
role: "user",
});
```
## Tool Integration
Tools are handled server-side, making it simple to pass them to the live session:
```typescript
// Define your tools
const weatherTool = tool({
name: "weather",
description: "Get the weather for a location",
parameters: z.object({
location: z.string().describe("The location to get weather for"),
}),
execute: async ({ location }) => {
return `The weather in ${location} is sunny`;
},
});
// Create session with tools
const session = await llm.live.connect({
systemInstruction: "You are a helpful assistant.",
tools: [weatherTool],
});
```
## Audio Support
For audio capabilities:
```typescript
// Get microphone access
const userStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
// Create session with audio
const session = await llm.live.connect({
audioConfig: {
stream: userStream,
onTrack: (remoteStream) => {
// Handle incoming audio
audioElement.srcObject = remoteStream;
},
},
});
```
## Event Handling
Listen to events from the session:
```typescript
for await (const event of session.streamEvents()) {
if (liveEvents.open.include(event)) {
// Connection established
console.log("Connected!");
} else if (liveEvents.text.include(event)) {
// Received text response
console.log("Assistant:", event.text);
}
}
```
## Capabilities
The OpenAI Live LLM supports:
- Real-time text chat
- Audio streaming (if configured)
- Tool calling (server-side execution)
- Ephemeral key generation for secure sessions
## API Reference
### LiveLLM Methods
// Get an ephemeral key
// Usually this code is run on the server and the ephemeral key is passed to the
// client - the ephemeral key can be securely used on the client side
#### `connect(config?: LiveConnectConfig)`
Creates a new live session.
```typescript
interface LiveConnectConfig {
systemInstruction?: string;
tools?: BaseTool[];
audioConfig?: AudioConfig;
responseModality?: ModalityType[];
}
```
#### `getEphemeralKey()`
Gets a temporary key for the session.
### LiveLLMSession Methods
#### `sendMessage(message: ChatMessage)`
Sends a message to the assistant.
```typescript
interface ChatMessage {
content: string | MessageContentDetail[];
role: "user" | "assistant";
}
```
#### `disconnect()`
Closes the session and cleans up resources.
## Error Handling
```typescript
try {
const session = await llm.live.connect();
} catch (error) {
if (error instanceof Error) {
console.error("Connection failed:", error.message);
}
}
```
## Best Practices
1. **Tool Definition**
- Keep tool implementations server-side
- Use clear descriptions for tools
- Handle tool errors gracefully
2. **Session Management**
- Always disconnect sessions when done
- Clean up audio resources
- Handle reconnection scenarios
3. **Security**
- Use ephemeral keys for sessions
- Validate tool inputs
- Secure API key handling
@@ -1 +1,9 @@
{"root":["./src/main.ts","./vite.config.ts"],"version":"5.7.3"}
{
"root": [
"./src/main.ts",
"./vite.config.ts",
"./tsconfig.json"
],
"errors": true,
"version": "5.7.3"
}
+1 -4
View File
@@ -23,10 +23,7 @@ async function main() {
content: "Say something about you for 10 seconds",
role: "user",
});
} else if (
liveEvents.audio.include(event) &&
typeof event.data === "string"
) {
} else if (liveEvents.audio.include(event)) {
const chunk = Buffer.from(event.data, "base64");
audioChunks.push(chunk);
console.log(`Received audio chunk: ${chunk.length} bytes`);
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,54 @@
# OpenAI Realtime Chat with LlamaIndex
This is a demo application showcasing real-time audio and text chat capabilities using OpenAI's GPT-4 with voice through LlamaIndex. The application demonstrates bidirectional audio communication and text chat with an AI assistant.
## Features
- Real-time voice communication with GPT-4
- Text-based chat interface
- WebRTC-based audio streaming
- Bidirectional communication (both text and voice)
- React + TypeScript implementation
## Prerequisites
- Node.js (v18 or higher)
- OpenAI API key with access to GPT-4 voice models
- Modern browser with WebRTC support
## Getting Started
1. Install dependencies:
```bash
pnpm install
```
2. Start the development server:
```bash
pnpm run dev
```
## Usage
The application provides a simple interface where you can:
- Start/Stop a chat session
- Speak to the AI assistant through your microphone
- Receive audio responses from the assistant
- See text transcripts of the conversation
## Technical Details
This project uses:
- LlamaIndex for AI interaction management
- WebRTC for real-time audio streaming
- React for the UI
- Vite for development and building
- TypeScript for type safety
```
```
@@ -0,0 +1,28 @@
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,29 @@
{
"name": "open-ai-realtime",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.5.2",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,183 @@
import { openai } from "@llamaindex/openai";
import { liveEvents, LiveLLMSession, ModalityType } from "llamaindex";
import { useEffect, useRef, useState } from "react";
const MicIcon = ({ isConnected }: { isConnected: boolean }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{isConnected ? (
<>
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" y1="19" x2="12" y2="23" />
<line x1="8" y1="23" x2="16" y2="23" />
</>
) : (
<>
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
</>
)}
</svg>
);
const WaveAnimation = () => (
<div className="wave-animation">
{[...Array(3)].map((_, i) => (
<div key={i} className="wave" style={{ animationDelay: `${i * 0.2}s` }} />
))}
</div>
);
export const AudioChat = () => {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState<
Array<{ role: string; content: string }>
>([]);
const [status, setStatus] = useState<string>("");
const audioRef = useRef<HTMLAudioElement>(null);
const sessionRef = useRef<LiveLLMSession | null>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
return () => {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
};
}, [stream]);
const startChat = async () => {
try {
setStatus("Initializing microphone...");
const userStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
setStream(userStream);
setStatus("Connecting to AI...");
const apiKey = prompt("Please enter your OpenAI API key:");
if (!apiKey) {
throw new Error("API key is required");
}
// move this call to the server side for security reasons
// Do not store the API key in the frontend!
const serverllm = openai({
apiKey: apiKey,
model: "gpt-4o-realtime-preview-2025-06-03",
});
const tempKey = await serverllm.live.getEphemeralKey();
const llm = openai({
apiKey: tempKey,
model: "gpt-4o-realtime-preview-2025-06-03",
});
const session = await llm.live.connect({
systemInstruction: "You are a helpful assistant who speaks naturally.",
responseModality: [ModalityType.TEXT, ModalityType.AUDIO],
audioConfig: {
stream: userStream,
onTrack: (remoteStream) => {
if (audioRef.current && remoteStream) {
audioRef.current.srcObject = remoteStream;
audioRef.current.play().catch(console.error);
}
},
},
});
sessionRef.current = session;
setIsConnected(true);
setStatus("Connected! Listening...");
for await (const event of session.streamEvents()) {
if (liveEvents.open.include(event)) {
setMessages((prev) => [
...prev,
{
role: "user",
content: "Hello, I'm ready to chat!",
},
]);
session.sendMessage({
content: "Hello, I'm ready to chat!",
role: "user",
});
} else if (liveEvents.text.include(event)) {
setMessages((prev) => [
...prev,
{
role: "assistant",
content: event.text,
},
]);
}
}
} catch (error) {
console.error("Error starting chat:", error);
setStatus("Error connecting. Please try again.");
setIsConnected(false);
}
};
const stopChat = async () => {
setStatus("Disconnecting...");
if (sessionRef.current) {
await sessionRef.current.disconnect();
sessionRef.current = null;
}
if (stream) {
stream.getTracks().forEach((track) => track.stop());
setStream(null);
}
if (audioRef.current) {
audioRef.current.srcObject = null;
}
setIsConnected(false);
setStatus("");
};
return (
<div className="audio-chat-container">
<h1>AI Voice Chat</h1>
<div className="messages-container">
{messages.map((msg, idx) => (
<div key={idx} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="controls">
{status && <div className="status-indicator">{status}</div>}
<button
className={`mic-button ${isConnected ? "connected" : ""}`}
onClick={isConnected ? stopChat : startChat}
title={isConnected ? "Stop Chat" : "Start Chat"}
>
<MicIcon isConnected={isConnected} />
{isConnected && <WaveAnimation />}
</button>
<audio ref={audioRef} style={{ display: "none" }} />
</div>
</div>
);
};
@@ -0,0 +1,322 @@
:root {
--primary-color: #646cff;
--secondary-color: #535bf2;
--background-dark: #1a1a1a;
--chat-bg: #242424;
--text-primary: #ffffff;
--text-secondary: #888888;
--success-color: #4caf50;
--error-color: #f44336;
--gradient-start: #4776e6;
--gradient-end: #8e54e9;
}
body {
background-color: var(--background-dark);
color: var(--text-primary);
margin: 0;
min-height: 100vh;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
#root {
max-width: 1280px;
height: 100vh;
margin: 0 auto;
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
.audio-chat-container {
display: flex;
flex-direction: column;
gap: 2rem;
width: 100%;
max-width: 800px;
height: 80vh;
margin: 0 auto;
padding: 2rem;
background: var(--chat-bg);
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.audio-chat-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(
to right,
var(--gradient-start),
var(--gradient-end)
);
}
.audio-chat-container h1 {
font-size: 2.5rem;
margin: 0;
background: linear-gradient(
to right,
var(--gradient-start),
var(--gradient-end)
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-align: center;
}
.messages-container {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
overflow-y: auto;
padding: 1rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
margin: 1rem 0;
}
.message {
padding: 1rem 1.5rem;
border-radius: 16px;
max-width: 80%;
text-align: left;
animation: messageSlide 0.3s ease-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
background: linear-gradient(
135deg,
var(--gradient-start),
var(--gradient-end)
);
align-self: flex-end;
margin-left: 20%;
color: white;
}
.message.assistant {
background: rgba(255, 255, 255, 0.1);
align-self: flex-start;
margin-right: 20%;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.controls {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
position: relative;
}
.mic-button {
width: 80px;
height: 80px;
border-radius: 50%;
border: none;
background: linear-gradient(
135deg,
var(--gradient-start),
var(--gradient-end)
);
color: white;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.mic-button::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0)
);
border-radius: 50%;
transition: transform 0.3s ease;
}
.mic-button:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.mic-button:hover::before {
transform: translateY(-100%);
}
.mic-button.connected {
background: var(--error-color);
animation: pulseError 2s infinite;
}
.mic-button svg {
width: 32px;
height: 32px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
transition: transform 0.3s ease;
}
.mic-button:hover svg {
transform: scale(1.1);
}
@keyframes pulseError {
0% {
box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4);
}
70% {
box-shadow: 0 0 0 20px rgba(244, 67, 54, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(244, 67, 54, 0);
}
}
/* Status indicator */
.status-indicator {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 0.9rem;
color: var(--text-secondary);
opacity: 0;
transition: opacity 0.3s ease;
}
.controls:hover .status-indicator {
opacity: 1;
}
/* Scrollbar styling */
.messages-container::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb {
background: linear-gradient(var(--gradient-start), var(--gradient-end));
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: linear-gradient(var(--gradient-end), var(--gradient-start));
}
/* Wave Animation */
.wave-animation {
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
}
.wave {
width: 4px;
height: 15px;
background: currentColor;
border-radius: 2px;
animation: wave 0.5s ease-in-out infinite;
}
@keyframes wave {
0%,
100% {
transform: scaleY(0.5);
}
50% {
transform: scaleY(1.5);
}
}
/* Loading state */
.mic-button.loading {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { AudioChat } from "./audio-chat.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AudioChat />
</StrictMode>,
);
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }]
}
@@ -0,0 +1,7 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});
+7 -1
View File
@@ -18,5 +18,11 @@
"module": "commonjs"
}
},
"include": ["./**/*.ts"]
"include": ["./**/*.ts"],
"exclude": [
"node_modules",
"dist",
"models/openai/live/browser/open-ai-realtime",
"**/browser/**"
]
}
+4 -1
View File
@@ -1,6 +1,9 @@
export { BaseLLM, ToolCallLLM } from "./base";
export { LiveLLM, LiveLLMSession, liveEvents, type LiveEvent } from "./live";
export { LiveLLM, LiveLLMCapability, LiveLLMSession } from "./live/live";
export { liveEvents, type LiveEvent } from "./live/live-types";
export type { MessageSender } from "./live/sender";
export type {
AudioConfig,
BaseTool,
BaseToolWithCall,
ChatMessage,
@@ -1,9 +1,7 @@
import type {
ChatMessage,
LiveConnectConfig,
MessageContentAudioDetail,
MessageContentTextDetail,
} from "./type";
} from "../type";
export type OpenEvent = { type: "open" };
@@ -63,45 +61,3 @@ export const liveEvents = {
e.type === "turnComplete",
},
};
export abstract class LiveLLMSession {
protected eventQueue: LiveEvent[] = [];
protected eventResolvers: ((value: LiveEvent) => void)[] = [];
protected closed = false;
abstract sendMessage(message: ChatMessage): void;
async *streamEvents(): AsyncIterable<LiveEvent> {
while (true) {
const event = await this.nextEvent();
if (event === undefined) {
break;
}
yield event;
}
}
abstract disconnect(): Promise<void>;
protected async nextEvent(): Promise<LiveEvent | undefined> {
if (this.eventQueue.length) {
return Promise.resolve(this.eventQueue.shift());
}
return new Promise((resolve) => {
this.eventResolvers.push(resolve);
});
}
//Uses an async queue to send events to the client
// if the consumer is waiting for an event, it will be resolved immediately
// otherwise, the event will be queued up and sent when the consumer is ready
pushEventToQueue(event: LiveEvent) {
if (this.eventResolvers.length) {
//resolving the promise with the event
this.eventResolvers.shift()!(event);
} else {
this.eventQueue.push(event);
}
}
}
export abstract class LiveLLM {
abstract connect(config?: LiveConnectConfig): Promise<LiveLLMSession>;
}
+124
View File
@@ -0,0 +1,124 @@
import type {
ChatMessage,
LiveConnectConfig,
MessageContentAudioDetail,
MessageContentDetail,
MessageContentImageDataDetail,
MessageContentVideoDetail,
} from "../type";
import type { LiveEvent } from "./live-types";
import type { MessageSender } from "./sender";
export enum LiveLLMCapability {
EPHEMERAL_KEY = "ephemeral_key",
AUDIO_CONFIG = "audio_config",
}
export abstract class LiveLLMSession {
protected eventQueue: LiveEvent[] = [];
protected eventResolvers: ((value: LiveEvent) => void)[] = [];
closed = false;
abstract get messageSender(): MessageSender;
private isTextMessage(content: MessageContentDetail) {
return content.type === "text";
}
private isAudioMessage(
content: MessageContentDetail,
): content is MessageContentAudioDetail {
return content.type === "audio";
}
private isImageMessage(
content: MessageContentDetail,
): content is MessageContentImageDataDetail {
return content.type === "image";
}
private isVideoMessage(
content: MessageContentDetail,
): content is MessageContentVideoDetail {
return content.type === "video";
}
sendMessage(message: ChatMessage) {
const { content, role } = message;
if (!Array.isArray(content)) {
this.messageSender.sendTextMessage(content, role);
} else {
for (const item of content) {
this.processMessage(item, role);
}
}
}
private processMessage(message: MessageContentDetail, role?: string) {
if (this.isTextMessage(message)) {
this.messageSender.sendTextMessage(message.text, role);
} else if (
this.isAudioMessage(message) &&
this.messageSender.sendAudioMessage
) {
this.messageSender.sendAudioMessage(message, role);
} else if (
this.isImageMessage(message) &&
this.messageSender.sendImageMessage
) {
this.messageSender.sendImageMessage(message, role);
} else if (
this.isVideoMessage(message) &&
this.messageSender.sendVideoMessage
) {
this.messageSender.sendVideoMessage(message, role);
}
}
async *streamEvents(): AsyncIterable<LiveEvent> {
while (true) {
const event = await this.nextEvent();
if (event === undefined) {
break;
}
yield event;
}
}
abstract disconnect(): Promise<void>;
protected async nextEvent(): Promise<LiveEvent | undefined> {
if (this.eventQueue.length) {
return Promise.resolve(this.eventQueue.shift());
}
return new Promise((resolve) => {
this.eventResolvers.push(resolve);
});
}
//Uses an async queue to send events to the client
// if the consumer is waiting for an event, it will be resolved immediately
// otherwise, the event will be queued up and sent when the consumer is ready
pushEventToQueue(event: LiveEvent) {
if (this.eventResolvers.length) {
//resolving the promise with the event
this.eventResolvers.shift()!(event);
} else {
this.eventQueue.push(event);
}
}
}
export abstract class LiveLLM {
/**
* Set of capabilities supported by this implementation.
* Override in subclasses as needed.
*/
capabilities: Set<LiveLLMCapability> = new Set();
abstract connect(config?: LiveConnectConfig): Promise<LiveLLMSession>;
abstract getEphemeralKey(): Promise<string | undefined>;
hasCapability(capability: LiveLLMCapability): boolean {
return this.capabilities.has(capability);
}
}
+15
View File
@@ -0,0 +1,15 @@
import type {
MessageContentAudioDetail,
MessageContentImageDataDetail,
MessageContentVideoDetail,
} from "../type";
export interface MessageSender {
sendTextMessage(message: string, role?: string): void;
sendAudioMessage?(content: MessageContentAudioDetail, role?: string): void;
sendImageMessage?(
content: MessageContentImageDataDetail,
role?: string,
): void;
sendVideoMessage?(content: MessageContentVideoDetail, role?: string): void;
}
+6
View File
@@ -290,8 +290,14 @@ export type ToolOutput = {
isError: boolean;
};
export interface AudioConfig {
stream?: MediaStream;
onTrack?: (track: MediaStream | null) => void;
}
export interface LiveConnectConfig {
tools?: BaseTool[];
responseModality?: ModalityType[];
systemInstruction?: string;
audioConfig?: AudioConfig;
}
+18 -117
View File
@@ -12,14 +12,11 @@ import {
LiveLLM,
LiveLLMSession,
type BaseTool,
type ChatMessage,
type LiveConnectConfig,
type MessageContentAudioDetail,
type MessageContentDetail,
type MessageContentImageDataDetail,
type MessageContentVideoDetail,
type MessageSender,
} from "@llamaindex/core/llms";
import { getEnv, uint8ArrayToBase64 } from "@llamaindex/env";
import { getEnv } from "@llamaindex/env";
import { GeminiMessageSender } from "./message-sender";
import { GEMINI_MODEL, type GeminiVoiceName } from "./types";
import {
mapBaseToolToGeminiLiveFunctionDeclaration,
@@ -40,6 +37,10 @@ export class GeminiLiveSession extends LiveLLMSession {
super();
}
get messageSender(): MessageSender {
return new GeminiMessageSender(this);
}
private isInterruptedEvent(event: LiveServerMessage): boolean {
return event.serverContent?.interrupted === true;
}
@@ -70,22 +71,6 @@ export class GeminiLiveSession extends LiveLLMSession {
return event.setupComplete !== undefined;
}
private isTextMessage(content: MessageContentDetail) {
return content.type === "text";
}
private isAudioMessage(content: MessageContentDetail) {
return content.type === "audio";
}
private isImageMessage(content: MessageContentDetail) {
return content.type === "image";
}
private isVideoMessage(content: MessageContentDetail) {
return content.type === "video";
}
//for the tool call event, we need to return the response with function responses
private async handleToolCallEvent(
event: LiveServerMessage,
@@ -195,105 +180,11 @@ export class GeminiLiveSession extends LiveLLMSession {
}
}
private sendTextMessage(message: string, role?: string) {
this.session?.sendClientContent({
turns: [
{
parts: [{ text: message }],
...(role ? { role } : {}),
},
],
});
}
private sendAudioMessage(content: MessageContentAudioDetail, role?: string) {
if (typeof content.data === "string") {
this.session?.sendRealtimeInput({
audio: {
data: content.data,
mimeType: content.mimeType,
},
});
} else {
this.session?.sendRealtimeInput({
audio: {
data: uint8ArrayToBase64(content.data),
mimeType: content.mimeType,
},
});
}
}
private sendImageMessage(
content: MessageContentImageDataDetail,
role?: string,
) {
if (typeof content.data === "string") {
this.session?.sendRealtimeInput({
media: {
data: content.data,
mimeType: content.mimeType,
},
});
} else {
this.session?.sendRealtimeInput({
media: {
data: uint8ArrayToBase64(content.data),
mimeType: content.mimeType,
},
});
}
}
private sendVideoMessage(content: MessageContentVideoDetail, role?: string) {
if (typeof content.data === "string") {
this.session?.sendRealtimeInput({
video: {
data: content.data,
mimeType: content.mimeType,
},
});
} else {
this.session?.sendRealtimeInput({
video: {
data: uint8ArrayToBase64(content.data),
mimeType: content.mimeType,
},
});
}
}
private handleUserInput(message: ChatMessage) {
const { content, role } = message;
if (!Array.isArray(content)) {
this.sendTextMessage(content, role);
} else {
for (const item of content) {
if (this.isTextMessage(item)) {
this.sendTextMessage(item.text, role);
} else if (this.isAudioMessage(item)) {
this.sendAudioMessage(item as MessageContentAudioDetail, role);
} else if (this.isImageMessage(item)) {
this.sendImageMessage(item as MessageContentImageDataDetail, role);
} else if (this.isVideoMessage(item)) {
this.sendVideoMessage(item as MessageContentVideoDetail, role);
}
}
}
}
sendMessage(message: ChatMessage) {
if (!this.session) {
throw new Error("Session not connected");
}
this.handleUserInput(message);
}
async disconnect() {
if (!this.session) {
throw new Error("Session not connected");
}
this.session.close();
}
}
@@ -322,6 +213,10 @@ export class GeminiLive extends LiveLLM {
}
}
async getEphemeralKey(): Promise<string | undefined> {
throw new Error("Ephemeral key is not supported for Gemini Live");
}
async connect(config?: LiveConnectConfig) {
const liveConfig: GoogleLiveConnectConfig = {
responseModalities: config?.responseModality
@@ -356,6 +251,12 @@ export class GeminiLive extends LiveLLM {
};
}
if (config?.audioConfig) {
throw new Error(
"Audio config is not supported for Gemini Live, directly send and recieve audio events instead",
);
}
const geminiLiveSession = new GeminiLiveSession();
geminiLiveSession.session = await this.client.live.connect({
@@ -0,0 +1,53 @@
import type {
MessageContentAudioDetail,
MessageContentImageDataDetail,
MessageContentVideoDetail,
MessageSender,
} from "@llamaindex/core/llms";
import type { GeminiLiveSession } from "./live";
export class GeminiMessageSender implements MessageSender {
private geminiSession: GeminiLiveSession;
constructor(session: GeminiLiveSession) {
this.geminiSession = session;
}
sendTextMessage(message: string, role?: string) {
this.geminiSession.session?.sendClientContent({
turns: [
{
parts: [{ text: message }],
...(role ? { role } : {}),
},
],
});
}
sendAudioMessage(content: MessageContentAudioDetail, role?: string) {
this.geminiSession.session?.sendRealtimeInput({
audio: {
data: content.data,
mimeType: content.mimeType,
},
});
}
sendImageMessage(content: MessageContentImageDataDetail, role?: string) {
this.geminiSession.session?.sendRealtimeInput({
media: {
data: content.data,
mimeType: content.mimeType,
},
});
}
sendVideoMessage(content: MessageContentVideoDetail, role?: string) {
this.geminiSession.session?.sendRealtimeInput({
video: {
data: content.data,
mimeType: content.mimeType,
},
});
}
}
@@ -0,0 +1,182 @@
import type {
AudioConfig,
BaseTool,
MessageSender,
} from "@llamaindex/core/llms";
import { LiveLLMSession } from "@llamaindex/core/llms";
import { OpenAIMessageSender } from "./message-sender";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ServerEvent = Record<string, any>;
export interface FunctionCall {
name: string;
call_id: string;
args: Record<string, unknown>;
}
export class OpenAILiveSession extends LiveLLMSession {
closed = false;
audioStream: MediaStream | undefined;
peerConnection: RTCPeerConnection | undefined;
dataChannel: RTCDataChannel | undefined;
get messageSender(): MessageSender {
return new OpenAIMessageSender(this);
}
private isResponseDoneEvent(event: ServerEvent) {
return event.type === "response.done";
}
private isTextEvent(event: ServerEvent) {
return (
event.response.output &&
event.response?.output[0]?.type === "message" &&
event.response?.output[0]?.content[0]?.type === "text"
);
}
private isFunctionCallEvent(event: ServerEvent) {
return (
event.response.output &&
event.response?.output[0]?.type === "function_call"
);
}
private isAudioDeltaEvent(event: ServerEvent) {
return event.type === "response.audio.delta";
}
private isInterruptedEvent(event: ServerEvent) {
return event.type === "output_audio_buffer.cleared";
}
setupAudioTracks(config?: AudioConfig) {
if (!this.peerConnection) return;
this.peerConnection.ontrack = (event) => {
if (config?.onTrack) {
config.onTrack(event.streams[0] || null);
}
};
if (config?.stream) {
this.audioStream = config.stream;
this.audioStream.getAudioTracks().forEach((track) => {
this.peerConnection?.addTrack(track, this.audioStream!);
});
}
}
async handleEvents(event: ServerEvent, toolCalls: BaseTool[]) {
if (this.isAudioDeltaEvent(event)) {
this.pushEventToQueue({
type: "audio",
data: event.delta,
mimeType: "audio/wav",
});
}
if (this.isResponseDoneEvent(event)) {
if (this.isTextEvent(event)) {
this.pushEventToQueue({
type: "text",
text: event.response?.output[0]?.content[0]?.text,
});
}
if (this.isFunctionCallEvent(event)) {
await this.handleFunctionCall(event, toolCalls);
}
if (this.isInterruptedEvent(event)) {
this.pushEventToQueue({
type: "interrupted",
});
}
}
}
private async sendToolCallResponses(
eventToolCalls: FunctionCall[],
toolCalls: BaseTool[],
) {
for (const toolCall of eventToolCalls) {
const tool = toolCalls.find((t) => t.metadata.name === toolCall.name);
if (tool && tool.call) {
const response = await tool.call(toolCall.args);
this.sendToolCallResponse(toolCall.call_id, response);
}
}
}
async sendResponseCreateEvent() {
this.dataChannel?.send(
JSON.stringify({
type: "response.create",
}),
);
}
private sendToolCallResponse(callId: string, response: unknown) {
if (!this.dataChannel) {
throw new Error("Data channel not connected");
}
const event = {
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: callId,
output: JSON.stringify(response),
},
};
this.dataChannel.send(JSON.stringify(event));
this.sendResponseCreateEvent();
}
private async handleFunctionCall(event: ServerEvent, toolCalls: BaseTool[]) {
const eventToolCalls: FunctionCall[] = event.response.output.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(output: Record<string, any>) => {
return {
name: output.name,
call_id: output.call_id,
args:
typeof output.arguments === "string"
? JSON.parse(output.arguments)
: output.arguments,
};
},
);
if (eventToolCalls && eventToolCalls.length > 0) {
await this.sendToolCallResponses(eventToolCalls, toolCalls);
}
}
async disconnect() {
if (this.dataChannel) {
this.dataChannel.close();
this.dataChannel = undefined;
}
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = undefined;
}
this.closed = true;
if (this.audioStream) {
this.audioStream.getTracks().forEach((track) => {
track.stop();
});
this.audioStream = undefined;
}
}
setPeerConnection(pc: RTCPeerConnection) {
this.peerConnection = pc;
}
setDataChannel(dc: RTCDataChannel) {
this.dataChannel = dc;
}
}
+165
View File
@@ -0,0 +1,165 @@
import {
LiveLLM,
LiveLLMCapability,
type LiveConnectConfig,
} from "@llamaindex/core/llms";
import type { ChatModel } from "openai/resources.mjs";
import { OpenAILiveSession } from "./live-session";
import {
mapModalityToOpenAIModality,
toOpenAILiveTool,
type OpenAILiveConfig,
type OpenAIVoiceNames,
} from "./utils";
const REALTIME_MODELS = [
"gpt-4o-realtime-preview-2025-06-03",
"gpt-4o-realtime-preview-2024-12-17",
"gpt-4o-realtime-preview-2024-10-01",
];
export class OpenAILive extends LiveLLM {
private apiKey: string | undefined;
private model: ChatModel | (string & {});
voiceName?: OpenAIVoiceNames | undefined;
private baseURL: string;
constructor(init?: OpenAILiveConfig) {
super();
this.apiKey = init?.apiKey;
this.model = init?.model ?? "gpt-4o-realtime-preview-2025-06-03";
this.voiceName = init?.voiceName;
this.baseURL = "https://api.openai.com/v1/realtime";
if (!this.apiKey) {
throw new Error("OPENAI_API_KEY is not set");
}
this.capabilities.add(LiveLLMCapability.EPHEMERAL_KEY);
this.capabilities.add(LiveLLMCapability.AUDIO_CONFIG);
}
async getEphemeralKey() {
if (!REALTIME_MODELS.includes(this.model)) {
throw new Error(
"Ephemeral key is only supported for gpt-4o-realtime-preview models",
);
}
const response = await fetch(`${this.baseURL}/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: this.model,
voice: this.voiceName,
}),
});
const data = await response.json();
return data.client_secret.value;
}
private async getSDPResponse(offer: RTCSessionDescriptionInit) {
const sdpResponse = await fetch(`${this.baseURL}?model=${this.model}`, {
method: "POST",
body: offer.sdp!,
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/sdp",
},
});
return sdpResponse;
}
private async establishSDPConnection(session: OpenAILiveSession) {
const offer = await session.peerConnection!.createOffer();
await session.peerConnection!.setLocalDescription(offer);
if (!offer.sdp) {
throw new Error("Failed to create SDP offer");
}
const sdpResponse = await this.getSDPResponse(offer);
const answer = {
sdp: await sdpResponse.text(),
type: "answer" as RTCSdpType,
};
await session.peerConnection!.setRemoteDescription(answer);
}
private async initializeRTCPeerConnectionAndDataChannel(
session: OpenAILiveSession,
config?: LiveConnectConfig,
) {
session.peerConnection = new RTCPeerConnection();
session.setupAudioTracks(config?.audioConfig);
session.dataChannel =
session.peerConnection.createDataChannel("oai-events");
}
private async setupWebRTC(
session: OpenAILiveSession,
config?: LiveConnectConfig,
) {
this.initializeRTCPeerConnectionAndDataChannel(session, config);
await this.establishSDPConnection(session);
}
async connect(config?: LiveConnectConfig): Promise<OpenAILiveSession> {
const session = new OpenAILiveSession();
await this.setupWebRTC(session, config);
this.setupEventListeners(session, config);
return session;
}
private setupEventListeners(
session: OpenAILiveSession,
config?: LiveConnectConfig,
) {
this.messageEventListener(session, config);
this.openEventListener(session, config);
this.errorEventListener(session);
}
private messageEventListener(
session: OpenAILiveSession,
config?: LiveConnectConfig,
) {
session.dataChannel?.addEventListener("message", (event) => {
session.handleEvents(JSON.parse(event.data), config?.tools ?? []);
});
}
private openEventListener(
session: OpenAILiveSession,
config?: LiveConnectConfig,
) {
session.dataChannel?.addEventListener("open", () => {
const event = {
type: "session.update",
session: {
voice: this.voiceName,
instructions: config?.systemInstruction,
tools: config?.tools?.map(toOpenAILiveTool),
modalities: config?.responseModality?.map(
mapModalityToOpenAIModality,
),
},
};
session.dataChannel?.send(JSON.stringify(event));
session.pushEventToQueue({ type: "open" });
});
}
private errorEventListener(session: OpenAILiveSession) {
session.dataChannel?.addEventListener("error", (event) => {
session.pushEventToQueue({
type: "error",
error: event,
});
});
}
}
+17 -1
View File
@@ -36,6 +36,7 @@ import type {
ResponseFormatJSONObject,
ResponseFormatJSONSchema,
} from "openai/resources/index.js";
import { OpenAILive } from "./live.js";
import {
ALL_AVAILABLE_OPENAI_MODELS,
isFunctionCallingModel,
@@ -44,6 +45,7 @@ import {
type LLMInstance,
type OpenAIAdditionalChatOptions,
type OpenAIAdditionalMetadata,
type OpenAIVoiceNames,
} from "./utils.js";
export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
@@ -65,6 +67,8 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
additionalSessionOptions?:
| undefined
| Omit<Partial<OpenAIClientOptions>, "apiKey" | "maxRetries" | "timeout">;
private voiceName?: OpenAIVoiceNames | undefined;
private _live: OpenAILive | undefined;
// use lazy here to avoid check OPENAI_API_KEY immediately
lazySession: () => Promise<LLMInstance>;
@@ -79,6 +83,7 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
constructor(
init?: Omit<Partial<OpenAI>, "session"> & {
session?: LLMInstance | undefined;
voiceName?: OpenAIVoiceNames | undefined;
},
) {
super();
@@ -97,7 +102,7 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
this.additionalSessionOptions = init?.additionalSessionOptions;
this.apiKey = init?.session?.apiKey ?? init?.apiKey;
this.baseURL = init?.session?.baseURL ?? init?.baseURL;
this.voiceName = init?.voiceName;
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
@@ -115,6 +120,17 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
return isFunctionCallingModel(this);
}
get live(): OpenAILive {
if (!this._live) {
this._live = new OpenAILive({
apiKey: this.apiKey,
voiceName: this.voiceName,
model: this.model as ChatModel,
});
}
return this._live;
}
get metadata(): LLMMetadata & OpenAIAdditionalMetadata {
const contextWindow =
ALL_AVAILABLE_OPENAI_MODELS[
@@ -0,0 +1,51 @@
import {
type MessageContentAudioDetail,
type MessageSender,
} from "@llamaindex/core/llms";
import type { OpenAILiveSession } from "./live-session";
export class OpenAIMessageSender implements MessageSender {
private openaiSession: OpenAILiveSession;
constructor(session: OpenAILiveSession) {
this.openaiSession = session;
}
sendTextMessage(message: string, role?: string) {
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: message,
},
],
},
};
this.openaiSession.dataChannel?.send(JSON.stringify(event));
this.openaiSession.sendResponseCreateEvent();
}
sendAudioMessage(content: MessageContentAudioDetail, role?: string) {
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_audio",
audio: content.data,
},
],
},
};
this.openaiSession.dataChannel?.send(JSON.stringify(event));
this.openaiSession.sendResponseCreateEvent();
}
}
+51 -1
View File
@@ -1,4 +1,5 @@
import type { LLM, PartialToolCall } from "@llamaindex/core/llms";
import type { BaseTool, LLM, PartialToolCall } from "@llamaindex/core/llms";
import { ModalityType } from "@llamaindex/core/schema";
import { OpenAI as OpenAILLM } from "openai";
import type { ChatModel } from "openai/resources.mjs";
import { OpenAI } from "./llm";
@@ -34,6 +35,12 @@ export const GPT4_MODELS = {
"gpt-4o-realtime-preview-2024-10-01": {
contextWindow: 128000,
},
"gpt-4o-realtime-preview-2024-12-17": {
contextWindow: 128000,
},
"gpt-4o-realtime-preview-2025-06-03": {
contextWindow: 128000,
},
"gpt-4o-audio-preview": {
contextWindow: 128000,
},
@@ -54,6 +61,7 @@ export const GPT4_MODELS = {
},
"gpt-4o-search-preview": { contextWindow: 128000 },
"gpt-4o-mini-search-preview": { contextWindow: 128000 },
"gpt-4o-mini-realtime-preview-2024-12-17": { contextWindow: 128000 },
"gpt-4o-search-preview-2025-03-11": { contextWindow: 128000 },
"gpt-4o-mini-search-preview-2025-03-11": { contextWindow: 128000 },
"gpt-4.1": { contextWindow: 10 ** 6 },
@@ -243,3 +251,45 @@ export type ResponsesMessageContentDetail =
export type ResponseMessageContent = string | ResponsesMessageContentDetail[];
export type OpenAIResponsesRole = "user" | "assistant" | "system" | "developer";
export type Modality = "text" | "audio" | "video";
export interface OpenAILiveConfig {
apiKey?: string | undefined;
model?: ChatModel | undefined;
voiceName?: OpenAIVoiceNames | undefined;
}
export function mapModalityToOpenAIModality(
responseModalities: ModalityType,
): Modality {
return responseModalities === ModalityType.TEXT
? "text"
: responseModalities === ModalityType.AUDIO
? "audio"
: "video";
}
export type OpenAIVoiceNames =
| "alloy"
| "ash"
| "echo"
| "fable"
| "onyx"
| "nova"
| "shimmer";
export type OpenAILiveTool = {
type: "function";
name: string;
description: string;
parameters: Record<string, unknown>;
};
export const toOpenAILiveTool = (tool: BaseTool): OpenAILiveTool => {
return {
type: "function",
name: tool.metadata.name,
description: tool.metadata.description,
parameters: tool.metadata.parameters ?? {},
};
};
+797 -479
View File
File diff suppressed because it is too large Load Diff