docs: initial commit adding api playground to docs

makes it easy to see how litellm transforms your request
This commit is contained in:
Krrish Dholakia 2025-04-09 16:18:17 -07:00
parent 3f3afabda9
commit 5ca93a1950
7 changed files with 1803 additions and 19 deletions

View file

@ -0,0 +1,9 @@
---
title: Transform Request Playground
description: See how LiteLLM transforms your requests for different providers
hide_table_of_contents: true
---
import TransformRequestPlayground from '@site/src/components/TransformRequestPlayground';
<TransformRequestPlayground />

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,10 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sharp": "^0.32.6",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"antd": "^4.24.0",
"@ant-design/icons": "^4.8.0",
"@tremor/react": "^2.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1"

View file

@ -519,6 +519,13 @@ const sidebars = {
],
},
"troubleshoot",
{
type: 'category',
label: 'Playground',
items: [
'playground/transform_request',
],
},
],
};

View file

@ -0,0 +1,161 @@
import React, { useState } from 'react';
import styles from './transform_request.module.css';
const DEFAULT_REQUEST = {
"model": "bedrock/gpt-4",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Explain quantum computing in simple terms"
}
],
"temperature": 0.7,
"max_tokens": 500,
"stream": true
};
type ViewMode = 'split' | 'request' | 'transformed';
const TransformRequestPlayground: React.FC = () => {
const [request, setRequest] = useState(JSON.stringify(DEFAULT_REQUEST, null, 2));
const [transformedRequest, setTransformedRequest] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('split');
const handleTransform = async () => {
try {
// Here you would make the actual API call to transform the request
// For now, we'll just set a sample response
const sampleResponse = `curl -X POST \\
https://api.openai.com/v1/chat/completions \\
-H 'Authorization: Bearer sk-xxx' \\
-H 'Content-Type: application/json' \\
-d '{
"model": "gpt-4",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
}
],
"temperature": 0.7
}'`;
setTransformedRequest(sampleResponse);
} catch (error) {
console.error('Error transforming request:', error);
}
};
const handleCopy = () => {
navigator.clipboard.writeText(transformedRequest);
};
const renderContent = () => {
switch (viewMode) {
case 'request':
return (
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Original Request</h2>
<p>The request you would send to LiteLLM /chat/completions endpoint.</p>
</div>
<textarea
className={styles['code-input']}
value={request}
onChange={(e) => setRequest(e.target.value)}
spellCheck={false}
/>
<div className={styles['panel-footer']}>
<button className={styles['transform-button']} onClick={handleTransform}>
Transform
</button>
</div>
</div>
);
case 'transformed':
return (
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Transformed Request</h2>
<p>How LiteLLM transforms your request for the specified provider.</p>
<p className={styles.note}>Note: Sensitive headers are not shown.</p>
</div>
<div className={styles['code-output-container']}>
<pre className={styles['code-output']}>{transformedRequest}</pre>
<button className={styles['copy-button']} onClick={handleCopy}>
Copy
</button>
</div>
</div>
);
default:
return (
<>
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Original Request</h2>
<p>The request you would send to LiteLLM /chat/completions endpoint.</p>
</div>
<textarea
className={styles['code-input']}
value={request}
onChange={(e) => setRequest(e.target.value)}
spellCheck={false}
/>
<div className={styles['panel-footer']}>
<button className={styles['transform-button']} onClick={handleTransform}>
Transform
</button>
</div>
</div>
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Transformed Request</h2>
<p>How LiteLLM transforms your request for the specified provider.</p>
<p className={styles.note}>Note: Sensitive headers are not shown.</p>
</div>
<div className={styles['code-output-container']}>
<pre className={styles['code-output']}>{transformedRequest}</pre>
<button className={styles['copy-button']} onClick={handleCopy}>
Copy
</button>
</div>
</div>
</>
);
}
};
return (
<div className={styles['transform-playground']}>
<div className={styles['view-toggle']}>
<button
className={viewMode === 'split' ? styles.active : ''}
onClick={() => setViewMode('split')}
>
Split View
</button>
<button
className={viewMode === 'request' ? styles.active : ''}
onClick={() => setViewMode('request')}
>
Request
</button>
<button
className={viewMode === 'transformed' ? styles.active : ''}
onClick={() => setViewMode('transformed')}
>
Transformed
</button>
</div>
<div className={styles['playground-container']}>
{renderContent()}
</div>
</div>
);
};
export default TransformRequestPlayground;

View file

@ -0,0 +1,215 @@
.transform-playground {
width: 100%;
margin: 0;
padding: 2rem;
background: var(--ifm-background-color);
min-height: calc(70vh - 42px);
}
.playground-container {
display: flex;
flex-direction: row;
gap: 1.5rem;
width: 100%;
margin-top: 1rem;
min-height: calc(70vh - 140px);
}
@media (min-width: 1024px) {
.playground-container {
flex-direction: row;
}
.panel {
padding: 2rem;
min-width: 45%;
}
}
.panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--ifm-background-surface-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 12px;
padding: 1.5rem;
min-height: calc(70vh - 140px);
}
.panel-header {
margin-bottom: 1rem;
}
.panel-header h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: var(--ifm-heading-color);
}
.panel-header p {
color: var(--ifm-color-emphasis-600);
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.panel-header .note {
font-size: 0.75rem;
margin-top: 0.5rem;
color: var(--ifm-color-emphasis-500);
}
.code-input {
flex: 1;
width: 100%;
padding: 1rem;
background: var(--ifm-background-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
font-family: var(--ifm-font-family-monospace);
font-size: 0.875rem;
color: var(--ifm-color-content);
resize: none;
margin-bottom: 1rem;
min-height: calc(70vh - 245px);
}
.code-output-container {
position: relative;
background: var(--ifm-background-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 8px;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: calc(70vh - 245px);
}
.code-output {
padding: 1rem;
font-family: var(--ifm-font-family-monospace);
font-size: 0.875rem;
margin: 0;
overflow: auto;
flex: 1;
color: var(--ifm-color-content);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.code-output .string { color: #a8ff60; }
.code-output .number { color: #ff9d00; }
.code-output .boolean { color: #ff628c; }
.code-output .null { color: #ff628c; }
.code-output .key { color: #5ccfe6; }
.panel-footer {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
}
.transform-button {
display: inline-flex !important;
align-items: center !important;
gap: 0.5rem !important;
background: var(--ifm-color-primary) !important;
border: none !important;
color: white !important;
padding: 0.5rem 1rem !important;
border-radius: 6px !important;
cursor: pointer !important;
font-size: 0.875rem !important;
font-weight: 500 !important;
}
.transform-button:hover {
background: var(--ifm-color-primary-darker) !important;
}
.copy-button {
position: absolute !important;
right: 0.75rem !important;
top: 0.75rem !important;
color: var(--ifm-color-emphasis-600) !important;
background: transparent !important;
border: none !important;
cursor: pointer !important;
padding: 0.25rem !important;
border-radius: 4px !important;
display: flex !important;
align-items: center !important;
gap: 0.25rem !important;
}
.copy-button:hover {
color: var(--ifm-color-primary) !important;
background: var(--ifm-color-emphasis-200) !important;
}
/* View toggle styles */
.view-toggle {
display: flex;
gap: 0.5rem;
background: var(--ifm-color-emphasis-100);
padding: 0.25rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.view-toggle button {
padding: 0.5rem 1rem;
border-radius: 4px;
border: none;
background: transparent;
color: var(--ifm-color-emphasis-700);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
}
.view-toggle button:hover {
background: var(--ifm-color-emphasis-200);
}
.view-toggle button.active {
background: var(--ifm-color-primary);
color: white;
}
/* Responsive layout */
@media (max-width: 768px) {
.playground-container {
flex-direction: column;
}
.panel {
width: 100%;
}
}
.footer {
text-align: right;
margin-top: 1.5rem;
padding: 0 1rem;
}
.footer p {
color: var(--ifm-color-emphasis-600);
font-size: 1rem;
margin: 0;
}
.footer a {
color: var(--ifm-color-primary);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}

View file

@ -0,0 +1,210 @@
import React, { useState } from 'react';
import { Button, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import styles from './transform_request.module.css';
interface TransformRequestPanelProps {
accessToken: string | null;
}
interface TransformResponse {
raw_request_api_base: string;
raw_request_body: Record<string, any>;
raw_request_headers: Record<string, string>;
}
const TransformRequestPanel: React.FC<TransformRequestPanelProps> = ({ accessToken }) => {
const [originalRequestJSON, setOriginalRequestJSON] = useState(`{
"model": "bedrock/gpt-4o",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Explain quantum computing in simple terms"
}
],
"temperature": 0.7,
"max_tokens": 500,
"stream": true
}`);
const [transformedResponse, setTransformedResponse] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Function to format curl command from API response parts
const formatCurlCommand = (apiBase: string, requestBody: Record<string, any>, requestHeaders: Record<string, string>) => {
// Format the request body as nicely indented JSON with 2 spaces
const formattedBody = JSON.stringify(requestBody, null, 2)
// Add additional indentation for the entire body
.split('\n')
.map(line => ` ${line}`)
.join('\n');
// Build headers string with consistent indentation
const headerString = Object.entries(requestHeaders)
.map(([key, value]) => `-H '${key}: ${value}'`)
.join(' \\\n ');
// Build the curl command with consistent indentation
return `curl -X POST \\
${apiBase} \\
${headerString ? `${headerString} \\\n ` : ''}-H 'Content-Type: application/json' \\
-d '{
${formattedBody}
}'`;
};
// Function to handle the transform request
const handleTransform = async () => {
setIsLoading(true);
try {
// Parse the JSON from the textarea
let requestBody;
try {
requestBody = JSON.parse(originalRequestJSON);
} catch (e) {
message.error('Invalid JSON in request body');
setIsLoading(false);
return;
}
// Create the request payload
const payload = {
call_type: "completion",
request_body: requestBody
};
// Make the API call using fetch
const response = await fetch('http://0.0.0.0:4000/utils/transform_request', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
// Parse the response as JSON
const data = await response.json();
console.log("API response:", data);
// Check if the response has the expected fields
if (data.raw_request_api_base && data.raw_request_body) {
// Format the curl command with the separate parts
const formattedCurl = formatCurlCommand(
data.raw_request_api_base,
data.raw_request_body,
data.raw_request_headers || {}
);
// Update state with the formatted curl command
setTransformedResponse(formattedCurl);
message.success('Request transformed successfully');
} else {
// Handle the case where the API returns a different format
// Try to extract the parts from a string response if needed
const rawText = typeof data === 'string' ? data : JSON.stringify(data);
setTransformedResponse(rawText);
message.info('Transformed request received in unexpected format');
}
} catch (err) {
console.error('Error transforming request:', err);
message.error('Failed to transform request');
} finally {
setIsLoading(false);
}
};
// Add this handler function near your other handlers
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault(); // Prevent default behavior
handleTransform();
}
};
return (
<div className={styles['transform-playground']}>
<div className={styles['playground-container']}>
{/* Original Request Panel */}
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Original Request</h2>
<p>The request you would send to LiteLLM /chat/completions endpoint.</p>
</div>
<textarea
className={styles['code-input']}
value={originalRequestJSON}
onChange={(e) => setOriginalRequestJSON(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Press Cmd/Ctrl + Enter to transform"
/>
<div className={styles['panel-footer']}>
<Button
type="primary"
onClick={handleTransform}
loading={isLoading}
className={styles['transform-button']}
>
<span>Transform</span>
<span></span>
</Button>
</div>
</div>
{/* Transformed Request Panel */}
<div className={styles.panel}>
<div className={styles['panel-header']}>
<h2>Transformed Request</h2>
<p>How LiteLLM transforms your request for the specified provider.</p>
<p className={styles.note}>Note: Sensitive headers are not shown.</p>
</div>
<div className={styles['code-output-container']}>
<pre className={styles['code-output']}>
{transformedResponse || `curl -X POST \\
https://api.openai.com/v1/chat/completions \\
-H 'Authorization: Bearer sk-xxx' \\
-H 'Content-Type: application/json' \\
-d '{
"model": "gpt-4",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
}
],
"temperature": 0.7
}'`}
</pre>
<Button
type="text"
icon={<CopyOutlined />}
className={styles['copy-button']}
onClick={() => {
navigator.clipboard.writeText(transformedResponse || '');
message.success('Copied to clipboard');
}}
/>
</div>
</div>
</div>
<div className={styles.footer}>
<p>Found an error? File an issue <a href="https://github.com/BerriAI/litellm/issues" target="_blank" rel="noopener noreferrer">here</a>.</p>
</div>
</div>
);
};
export default TransformRequestPanel;