Merge branch 'main' into litellm_use_msft_service_principals

This commit is contained in:
Ishaan Jaff 2025-04-09 18:33:20 -07:00
commit 37f6d5f74e
8 changed files with 240 additions and 25 deletions

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

@ -51,9 +51,6 @@ def decrypt_value_helper(value: str):
# if it's not str - do not decrypt it, return the value
return value
except Exception as e:
import traceback
traceback.print_stack()
verbose_proxy_logger.error(
f"Error decrypting value, Did your master_key/salt key change recently? \nError: {str(e)}\nSet permanent salt key - https://docs.litellm.ai/docs/proxy/prod#5-set-litellm-salt-key"
)

View file

@ -1434,7 +1434,7 @@ async def get_user_daily_activity(
default=1, description="Page number for pagination", ge=1
),
page_size: int = fastapi.Query(
default=50, description="Items per page", ge=1, le=100
default=50, description="Items per page", ge=1, le=1000
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
) -> SpendAnalyticsPaginatedResponse:

View file

@ -314,6 +314,8 @@ export default function CreateKeyPage() {
<BudgetPanel accessToken={accessToken} />
) : page == "guardrails" ? (
<GuardrailsPanel accessToken={accessToken} />
): page == "transform-request" ? (
<TransformRequestPanel accessToken={accessToken} />
): page == "general-settings" ? (
<GeneralSettings
userID={userID}

View file

@ -74,6 +74,7 @@ const Sidebar: React.FC<SidebarProps> = ({
{ key: "10", page: "budgets", label: "Budgets", icon: <BankOutlined />, roles: all_admin_roles },
{ key: "11", page: "guardrails", label: "Guardrails", icon: <SafetyOutlined />, roles: all_admin_roles },
{ key: "12", page: "new_usage", label: "New Usage", icon: <BarChartOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
{ key: "20", page: "transform-request", label: "API Playground", icon: <ApiOutlined />, roles: [...all_admin_roles, ...internalUserRoles] },
{ key: "18", page: "mcp-tools", label: "MCP Tools", icon: <ToolOutlined />, roles: all_admin_roles },
{ key: "19", page: "tag-management", label: "Tag Management", icon: <TagsOutlined />, roles: all_admin_roles },
]

View file

@ -1074,8 +1074,40 @@ export const organizationDeleteCall = async (
}
};
export const transformRequestCall = async (accessToken: String, request: object) => {
/**
* Transform request
*/
export const userDailyActivityCall = async (accessToken: String, startTime: Date, endTime: Date) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/utils/transform_request` : `/utils/transform_request`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to create key:", error);
throw error;
}
}
export const userDailyActivityCall = async (accessToken: String, startTime: Date, endTime: Date, page: number = 1) => {
/**
* Get daily user activity on proxy
*/
@ -1084,6 +1116,8 @@ export const userDailyActivityCall = async (accessToken: String, startTime: Date
const queryParams = new URLSearchParams();
queryParams.append('start_date', startTime.toISOString());
queryParams.append('end_date', endTime.toISOString());
queryParams.append('page_size', '1000');
queryParams.append('page', page.toString());
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;

View file

@ -22,15 +22,13 @@ import ViewUserSpend from "./view_user_spend";
import TopKeyView from "./top_key_view";
import { ActivityMetrics, processActivityData } from './activity_metrics';
import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types';
interface NewUsagePageProps {
accessToken: string | null;
userRole: string | null;
userID: string | null;
}
const NewUsagePage: React.FC<NewUsagePageProps> = ({
accessToken,
userRole,
@ -177,8 +175,39 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
if (!accessToken || !dateValue.from || !dateValue.to) return;
const startTime = dateValue.from;
const endTime = dateValue.to;
const data = await userDailyActivityCall(accessToken, startTime, endTime);
setUserSpendData(data);
try {
// Get first page
const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime);
// Check if we need to fetch more pages
if (firstPageData.metadata.total_pages > 10) {
throw new Error("Too many pages of data (>10). Please select a smaller date range.");
}
// If only one page, just set the data
if (firstPageData.metadata.total_pages === 1) {
setUserSpendData(firstPageData);
return;
}
// Fetch all pages
const allResults = [...firstPageData.results];
for (let page = 2; page <= firstPageData.metadata.total_pages; page++) {
const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page);
allResults.push(...pageData.results);
}
// Combine all results with the first page's metadata
setUserSpendData({
results: allResults,
metadata: firstPageData.metadata
});
} catch (error) {
console.error("Error fetching user spend data:", error);
throw error;
}
};
useEffect(() => {

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Button, Select, Tabs, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { Title } from '@tremor/react';
import { transformRequestCall } from './networking';
interface TransformRequestPanelProps {
accessToken: string | null;
}
@ -79,22 +79,13 @@ ${formattedBody}
};
// 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}`);
if (!accessToken) {
message.error('No access token found');
setIsLoading(false);
return;
}
// Parse the response as JSON
const data = await response.json();
console.log("API response:", data);
const data = await transformRequestCall(accessToken, payload);
// Check if the response has the expected fields
if (data.raw_request_api_base && data.raw_request_body) {