mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-24 10:14:26 +00:00
Merge branch 'main' into litellm_use_msft_service_principals
This commit is contained in:
commit
37f6d5f74e
8 changed files with 240 additions and 25 deletions
161
docs/my-website/src/components/TransformRequestPlayground.tsx
Normal file
161
docs/my-website/src/components/TransformRequestPlayground.tsx
Normal 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;
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 },
|
||||
]
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue