mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 02:34:29 +00:00
Add all /key/generate
api params to UI + add metadata fields on team AND org add/update (#8667)
* feat(create_key_button.tsx): initial commit using openapi.json to ensure all values via api are supported on ui for `/key/generate` Closes https://github.com/BerriAI/litellm/issues/7763 * style(create_key_button.tsx): put openapi settings inside 'advanced setting' accordion * fix(check_openapi_schema.tsx): style improvements for advanced settings * style(create_key_button.tsx): add tooltip explaining what the settings mean * fix(team_info.tsx): render metadata field on team update allow updating a team's metadata * fix(networking.tsx): add 'metadata' field to create team form * refactor: cleanup dead codeblock * fix(organization_endpoints.py): fix metadata param support on `/organization/new` * feat(organization_endpoints.py): support updating metadata for organization on api + ui * test: mark flaky test
This commit is contained in:
parent
40d1576292
commit
cc77138b37
11 changed files with 392 additions and 209 deletions
|
@ -1114,6 +1114,7 @@ class NewOrganizationRequest(LiteLLM_BudgetTable):
|
|||
organization_alias: str
|
||||
models: List = []
|
||||
budget_id: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
|
||||
class OrganizationRequest(LiteLLMPydanticObjectBase):
|
||||
|
|
|
@ -62,13 +62,12 @@ async def new_organization(
|
|||
- soft_budget: *Optional[float]* - [Not Implemented Yet] Get a slack alert when this soft budget is reached. Don't block requests.
|
||||
- model_max_budget: *Optional[dict]* - Max budget for a specific model
|
||||
- budget_duration: *Optional[str]* - Frequency of reseting org budget
|
||||
- metadata: *Optional[dict]* - Metadata for team, store information for team. Example metadata - {"extra_info": "some info"}
|
||||
- metadata: *Optional[dict]* - Metadata for organization, store information for organization. Example metadata - {"extra_info": "some info"}
|
||||
- blocked: *bool* - Flag indicating if the org is blocked or not - will stop all calls from keys with this org_id.
|
||||
- tags: *Optional[List[str]]* - Tags for [tracking spend](https://litellm.vercel.app/docs/proxy/enterprise#tracking-spend-for-custom-tags) and/or doing [tag-based routing](https://litellm.vercel.app/docs/proxy/tag_routing).
|
||||
- organization_id: *Optional[str]* - The organization id of the team. Default is None. Create via `/organization/new`.
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
|
||||
|
||||
Case 1: Create new org **without** a budget_id
|
||||
|
||||
```bash
|
||||
|
@ -103,6 +102,7 @@ async def new_organization(
|
|||
}'
|
||||
```
|
||||
"""
|
||||
|
||||
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
|
@ -174,6 +174,9 @@ async def new_organization(
|
|||
new_organization_row = prisma_client.jsonify_object(
|
||||
organization_row.json(exclude_none=True)
|
||||
)
|
||||
verbose_proxy_logger.info(
|
||||
f"new_organization_row: {json.dumps(new_organization_row, indent=2)}"
|
||||
)
|
||||
response = await prisma_client.db.litellm_organizationtable.create(
|
||||
data={
|
||||
**new_organization_row, # type: ignore
|
||||
|
@ -215,9 +218,13 @@ async def update_organization(
|
|||
if data.updated_by is None:
|
||||
data.updated_by = user_api_key_dict.user_id
|
||||
|
||||
updated_organization_row = prisma_client.jsonify_object(
|
||||
data.model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
response = await prisma_client.db.litellm_organizationtable.update(
|
||||
where={"organization_id": data.organization_id},
|
||||
data=data.model_dump(exclude_none=True),
|
||||
data=updated_organization_row,
|
||||
include={"members": True, "teams": True, "litellm_budget_table": True},
|
||||
)
|
||||
|
||||
|
|
|
@ -410,6 +410,7 @@ async def test_success_call_hook():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.flaky(retries=6, delay=1)
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_call_hook():
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, InputNumber, Select } from 'antd';
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { getOpenAPISchema } from '../networking';
|
||||
|
||||
interface SchemaProperty {
|
||||
type?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
anyOf?: Array<{ type: string }>;
|
||||
enum?: string[];
|
||||
format?: string;
|
||||
}
|
||||
|
||||
interface OpenAPISchema {
|
||||
properties: {
|
||||
[key: string]: SchemaProperty;
|
||||
};
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
interface SchemaFormFieldsProps {
|
||||
schemaComponent: string;
|
||||
excludedFields?: string[];
|
||||
form: any;
|
||||
overrideLabels?: { [key: string]: string };
|
||||
overrideTooltips?: { [key: string]: string };
|
||||
customValidation?: {
|
||||
[key: string]: (rule: any, value: any) => Promise<void>
|
||||
};
|
||||
defaultValues?: { [key: string]: any };
|
||||
}
|
||||
|
||||
// Helper function to determine if a field should be treated as JSON
|
||||
const isJSONField = (key: string, property: SchemaProperty): boolean => {
|
||||
const jsonFields = ['metadata', 'config', 'enforced_params', 'aliases'];
|
||||
return jsonFields.includes(key) || property.format === 'json';
|
||||
};
|
||||
|
||||
// Helper function to validate JSON input
|
||||
const validateJSON = (value: string): boolean => {
|
||||
if (!value) return true;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldHelp = (key: string, property: SchemaProperty, type: string): string => {
|
||||
// Default help text based on type
|
||||
const defaultHelp = {
|
||||
string: 'Text input',
|
||||
number: 'Numeric input',
|
||||
integer: 'Whole number input',
|
||||
boolean: 'True/False value',
|
||||
}[type] || 'Text input';
|
||||
|
||||
// Specific field help text
|
||||
const specificHelp: { [key: string]: string } = {
|
||||
max_budget: 'Enter maximum budget in USD (e.g., 100.50)',
|
||||
budget_duration: 'Select a time period for budget reset',
|
||||
tpm_limit: 'Enter maximum tokens per minute (whole number)',
|
||||
rpm_limit: 'Enter maximum requests per minute (whole number)',
|
||||
duration: 'Enter duration (e.g., 30s, 24h, 7d)',
|
||||
metadata: 'Enter JSON object with key-value pairs\nExample: {"team": "research", "project": "nlp"}',
|
||||
config: 'Enter configuration as JSON object\nExample: {"setting": "value"}',
|
||||
permissions: 'Enter comma-separated permission strings',
|
||||
enforced_params: 'Enter parameters as JSON object\nExample: {"param": "value"}',
|
||||
blocked: 'Enter true/false or specific block conditions',
|
||||
aliases: 'Enter aliases as JSON object\nExample: {"alias1": "value1", "alias2": "value2"}',
|
||||
models: 'Select one or more model names',
|
||||
key_alias: 'Enter a unique identifier for this key',
|
||||
tags: 'Enter comma-separated tag strings',
|
||||
};
|
||||
|
||||
// Get specific help text or use default based on type
|
||||
const helpText = specificHelp[key] || defaultHelp;
|
||||
|
||||
// Add format requirements for special cases
|
||||
if (isJSONField(key, property)) {
|
||||
return `${helpText}\nMust be valid JSON format`;
|
||||
}
|
||||
|
||||
if (property.enum) {
|
||||
return `Select from available options\nAllowed values: ${property.enum.join(', ')}`;
|
||||
}
|
||||
|
||||
return helpText;
|
||||
};
|
||||
|
||||
const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
|
||||
schemaComponent,
|
||||
excludedFields = [],
|
||||
form,
|
||||
overrideLabels = {},
|
||||
overrideTooltips = {},
|
||||
customValidation = {},
|
||||
defaultValues = {}
|
||||
}) => {
|
||||
const [schemaProperties, setSchemaProperties] = useState<OpenAPISchema | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOpenAPISchema = async () => {
|
||||
try {
|
||||
const schema = await getOpenAPISchema();
|
||||
const componentSchema = schema.components.schemas[schemaComponent];
|
||||
|
||||
if (!componentSchema) {
|
||||
throw new Error(`Schema component "${schemaComponent}" not found`);
|
||||
}
|
||||
|
||||
setSchemaProperties(componentSchema);
|
||||
|
||||
const defaultFormValues: { [key: string]: any } = {};
|
||||
Object.keys(componentSchema.properties)
|
||||
.filter(key => !excludedFields.includes(key) && defaultValues[key] !== undefined)
|
||||
.forEach(key => {
|
||||
defaultFormValues[key] = defaultValues[key];
|
||||
});
|
||||
|
||||
form.setFieldsValue(defaultFormValues);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Schema fetch error:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch schema');
|
||||
}
|
||||
};
|
||||
|
||||
fetchOpenAPISchema();
|
||||
}, [schemaComponent, form, excludedFields]);
|
||||
|
||||
const getPropertyType = (property: SchemaProperty): string => {
|
||||
if (property.type) {
|
||||
return property.type;
|
||||
}
|
||||
if (property.anyOf) {
|
||||
const types = property.anyOf.map(t => t.type);
|
||||
if (types.includes('number') || types.includes('integer')) return 'number';
|
||||
if (types.includes('string')) return 'string';
|
||||
}
|
||||
return 'string';
|
||||
};
|
||||
|
||||
const renderFormItem = (key: string, property: SchemaProperty) => {
|
||||
const type = getPropertyType(property);
|
||||
const isRequired = schemaProperties?.required?.includes(key);
|
||||
|
||||
const label = overrideLabels[key] || property.title || key;
|
||||
const tooltip = overrideTooltips[key] || property.description;
|
||||
|
||||
const rules = [];
|
||||
if (isRequired) {
|
||||
rules.push({ required: true, message: `${label} is required` });
|
||||
}
|
||||
if (customValidation[key]) {
|
||||
rules.push({ validator: customValidation[key] });
|
||||
}
|
||||
if (isJSONField(key, property)) {
|
||||
rules.push({
|
||||
validator: async (_: any, value: string) => {
|
||||
if (value && !validateJSON(value)) {
|
||||
throw new Error('Please enter valid JSON');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const formLabel = tooltip ? (
|
||||
<span>
|
||||
{label}{' '}
|
||||
<Tooltip title={tooltip}>
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : label;
|
||||
|
||||
let inputComponent;
|
||||
if (isJSONField(key, property)) {
|
||||
inputComponent = (
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Enter as JSON"
|
||||
className="font-mono"
|
||||
/>
|
||||
);
|
||||
} else if (property.enum) {
|
||||
inputComponent = (
|
||||
<Select>
|
||||
{property.enum.map(value => (
|
||||
<Select.Option key={value} value={value}>
|
||||
{value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else if (type === 'number' || type === 'integer') {
|
||||
inputComponent = (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
precision={type === 'integer' ? 0 : undefined}
|
||||
/>
|
||||
);
|
||||
} else if (key === 'duration') {
|
||||
inputComponent = (
|
||||
<TextInput
|
||||
placeholder="eg: 30s, 30h, 30d"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
inputComponent = (
|
||||
<TextInput
|
||||
placeholder={tooltip || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
label={formLabel}
|
||||
name={key}
|
||||
className="mt-8"
|
||||
rules={rules}
|
||||
initialValue={defaultValues[key]}
|
||||
help={
|
||||
<div className="text-xs text-gray-500">
|
||||
{getFieldHelp(key, property, type)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{inputComponent}
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!schemaProperties?.properties) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.entries(schemaProperties.properties)
|
||||
.filter(([key]) => !excludedFields.includes(key))
|
||||
.map(([key, property]) => renderFormItem(key, property))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchemaFormFields;
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Button, TextInput, Grid, Col } from "@tremor/react";
|
||||
import {
|
||||
|
@ -24,11 +23,13 @@ import {
|
|||
Radio,
|
||||
} from "antd";
|
||||
import { unfurlWildcardModelsInList, getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import SchemaFormFields from './common_components/check_openapi_schema';
|
||||
import {
|
||||
keyCreateCall,
|
||||
slackBudgetAlertsHealthCheck,
|
||||
modelAvailableCall,
|
||||
getGuardrailsList,
|
||||
proxyBaseUrl,
|
||||
} from "./networking";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import TeamDropdown from "./common_components/team_dropdown";
|
||||
|
@ -474,6 +475,36 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
options={predefinedTags}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Accordion className="mt-20 mb-8">
|
||||
<AccordionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<b>Advanced Settings</b>
|
||||
<Tooltip title={
|
||||
<span>
|
||||
Learn more about advanced settings in our{' '}
|
||||
<a
|
||||
href={proxyBaseUrl ? `${proxyBaseUrl}/#/key%20management/generate_key_fn_key_generate_post`: `/#/key%20management/generate_key_fn_key_generate_post`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
</span>
|
||||
}>
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-300 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
<SchemaFormFields
|
||||
schemaComponent="GenerateKeyRequest"
|
||||
form={form}
|
||||
excludedFields={['key_alias', 'team_id', 'models', 'duration', 'metadata', 'tags', 'guardrails', "max_budget", "budget_duration", "tpm_limit", "rpm_limit"]}
|
||||
/>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
</>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { message } from "antd";
|
||||
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
export const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
if (isLocal != true) {
|
||||
console.log = function() {};
|
||||
}
|
||||
|
@ -69,6 +69,12 @@ export function setGlobalLitellmHeaderName(headerName: string = "Authorization")
|
|||
globalLitellmHeaderName = headerName;
|
||||
}
|
||||
|
||||
export const getOpenAPISchema = async () => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/openapi.json` : `/openapi.json`;
|
||||
const response = await fetch(url);
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
export const modelCostMap = async (
|
||||
accessToken: string,
|
||||
|
@ -882,6 +888,17 @@ export const organizationCreateCall = async (
|
|||
try {
|
||||
console.log("Form Values in organizationCreateCall:", formValues); // Log the form values before making the API call
|
||||
|
||||
if (formValues.metadata) {
|
||||
console.log("formValues.metadata:", formValues.metadata);
|
||||
// if there's an exception JSON.parse, show it in the message
|
||||
try {
|
||||
formValues.metadata = JSON.parse(formValues.metadata);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse metadata:", error);
|
||||
throw new Error("Failed to parse metadata: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/organization/new` : `/organization/new`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
|
@ -911,6 +928,42 @@ export const organizationCreateCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const organizationUpdateCall = async (
|
||||
accessToken: string,
|
||||
formValues: Record<string, any> // Assuming formValues is an object
|
||||
) => {
|
||||
try {
|
||||
console.log("Form Values in organizationUpdateCall:", formValues); // Log the form values before making the API call
|
||||
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/organization/update` : `/organization/update`;
|
||||
const response = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...formValues, // Include formValues in the request body
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
console.error("Error response from the server:", errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("Update Team Response:", data);
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const organizationDeleteCall = async (
|
||||
accessToken: string,
|
||||
organizationID: string
|
||||
|
@ -2377,6 +2430,15 @@ export const teamCreateCall = async (
|
|||
) => {
|
||||
try {
|
||||
console.log("Form Values in teamCreateCall:", formValues); // Log the form values before making the API call
|
||||
if (formValues.metadata) {
|
||||
console.log("formValues.metadata:", formValues.metadata);
|
||||
// if there's an exception JSON.parse, show it in the message
|
||||
try {
|
||||
formValues.metadata = JSON.parse(formValues.metadata);
|
||||
} catch (error) {
|
||||
throw new Error("Failed to parse metadata: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/team/new` : `/team/new`;
|
||||
const response = await fetch(url, {
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Select as Select2,
|
||||
InputNumber,
|
||||
message,
|
||||
} from "antd";
|
||||
|
||||
import {
|
||||
TextInput,
|
||||
Button,
|
||||
} from "@tremor/react";
|
||||
|
||||
import { organizationCreateCall } from "../networking";
|
||||
|
||||
// types.ts
|
||||
export interface FormData {
|
||||
name: string;
|
||||
models: string[];
|
||||
maxBudget: number | null;
|
||||
budgetDuration: string | null;
|
||||
tpmLimit: number | null;
|
||||
rpmLimit: number | null;
|
||||
}
|
||||
|
||||
export interface OrganizationFormProps {
|
||||
title?: string;
|
||||
onCancel?: () => void;
|
||||
accessToken: string | null;
|
||||
availableModels?: string[];
|
||||
initialValues?: Partial<FormData>;
|
||||
submitButtonText?: string;
|
||||
modelSelectionType?: 'single' | 'multiple';
|
||||
}
|
||||
|
||||
// OrganizationForm.tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const onSubmit = async (formValues: Record<string, any>, accessToken: string | null, setIsModalVisible: any) => {
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
message.info("Creating Organization");
|
||||
console.log("formValues: " + JSON.stringify(formValues));
|
||||
const response: any = await organizationCreateCall(accessToken, formValues);
|
||||
console.log(`response for organization create call: ${response}`);
|
||||
message.success("Organization created");
|
||||
sessionStorage.removeItem('organizations');
|
||||
setIsModalVisible(false);
|
||||
} catch (error) {
|
||||
console.error("Error creating the organization:", error);
|
||||
message.error("Error creating the organization: " + error, 20);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const OrganizationForm: React.FC<OrganizationFormProps> = ({
|
||||
title = "Create Organization",
|
||||
onCancel,
|
||||
accessToken,
|
||||
availableModels = [],
|
||||
initialValues = {},
|
||||
submitButtonText = "Create",
|
||||
modelSelectionType = "multiple",
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: initialValues.name || '',
|
||||
models: initialValues.models || [],
|
||||
maxBudget: initialValues.maxBudget || null,
|
||||
budgetDuration: initialValues.budgetDuration || null,
|
||||
tpmLimit: initialValues.tpmLimit || null,
|
||||
rpmLimit: initialValues.rpmLimit || null
|
||||
});
|
||||
|
||||
console.log(`availableModels: ${availableModels}`)
|
||||
|
||||
const handleSubmit = async (formValues: Record<string, any>) => {
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
await onSubmit(formValues, accessToken, setIsModalVisible);
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
setIsModalVisible(false);
|
||||
if (onCancel) onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Button
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
className="mx-auto"
|
||||
type="button"
|
||||
>
|
||||
+ Create New {title}
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
title={`Create ${title}`}
|
||||
visible={isModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item
|
||||
label={`${title} Name`}
|
||||
name="organization_alias"
|
||||
rules={[
|
||||
{ required: true, message: `Please input a ${title} name` },
|
||||
]}
|
||||
>
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Models" name="models">
|
||||
<Select2
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select2.Option
|
||||
key="all-proxy-models"
|
||||
value="all-proxy-models"
|
||||
>
|
||||
All Proxy Models
|
||||
</Select2.Option>
|
||||
{availableModels.map((model) => (
|
||||
<Select2.Option key={model} value={model}>
|
||||
{model}
|
||||
</Select2.Option>
|
||||
))}
|
||||
</Select2>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label="Reset Budget"
|
||||
name="budget_duration"
|
||||
>
|
||||
<Select2 defaultValue={null} placeholder="n/a">
|
||||
<Select2.Option value="24h">daily</Select2.Option>
|
||||
<Select2.Option value="7d">weekly</Select2.Option>
|
||||
<Select2.Option value="30d">monthly</Select2.Option>
|
||||
</Select2>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Tokens per minute Limit (TPM)"
|
||||
name="tpm_limit"
|
||||
>
|
||||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Requests per minute Limit (RPM)"
|
||||
name="rpm_limit"
|
||||
>
|
||||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">{submitButtonText}</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationForm;
|
|
@ -23,7 +23,7 @@ import { Button, Form, Input, Select, message, InputNumber, Tooltip } from "antd
|
|||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { Member, Organization, organizationInfoCall, organizationMemberAddCall, organizationMemberUpdateCall, organizationMemberDeleteCall } from "../networking";
|
||||
import { Member, Organization, organizationInfoCall, organizationMemberAddCall, organizationMemberUpdateCall, organizationMemberDeleteCall, organizationUpdateCall } from "../networking";
|
||||
import UserSearchModal from "../common_components/user_search_modal";
|
||||
import MemberModal from "../team/edit_membership";
|
||||
|
||||
|
@ -146,20 +146,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
|||
rpm_limit: values.rpm_limit,
|
||||
max_budget: values.max_budget,
|
||||
budget_duration: values.budget_duration,
|
||||
}
|
||||
},
|
||||
metadata: values.metadata ? JSON.parse(values.metadata) : null,
|
||||
};
|
||||
|
||||
const response = await fetch('/organization/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
const response = await organizationUpdateCall(accessToken, updateData);
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update organization');
|
||||
|
||||
message.success("Organization settings updated successfully");
|
||||
setIsEditing(false);
|
||||
fetchOrgInfo();
|
||||
|
@ -337,6 +329,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
|||
rpm_limit: orgData.litellm_budget_table.rpm_limit,
|
||||
max_budget: orgData.litellm_budget_table.max_budget,
|
||||
budget_duration: orgData.litellm_budget_table.budget_duration,
|
||||
metadata: orgData.metadata ? JSON.stringify(orgData.metadata, null, 2) : "",
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
|
@ -384,6 +377,10 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
|||
<InputNumber step={1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<Button onClick={() => setIsEditing(false)}>
|
||||
Cancel
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
TabPanels,
|
||||
TabPanel,
|
||||
} from "@tremor/react";
|
||||
import { Input } from "antd";
|
||||
import { Modal, Form, InputNumber, Tooltip, Select as Select2 } from "antd";
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
|
@ -102,6 +103,8 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
try {
|
||||
if (!accessToken) return;
|
||||
|
||||
console.log(`values in organizations new create call: ${JSON.stringify(values)}`);
|
||||
|
||||
await organizationCreateCall(accessToken, values);
|
||||
setIsOrgModalVisible(false);
|
||||
form.resetFields();
|
||||
|
@ -334,6 +337,10 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
|||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button type="submit">Create Organization</Button>
|
||||
</div>
|
||||
|
|
|
@ -348,7 +348,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
rpm_limit: info.rpm_limit,
|
||||
max_budget: info.max_budget,
|
||||
budget_duration: info.budget_duration,
|
||||
guardrails: info.metadata?.guardrails || []
|
||||
guardrails: info.metadata?.guardrails || [],
|
||||
metadata: info.metadata ? JSON.stringify(info.metadata, null, 2) : "",
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
|
@ -420,6 +421,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
placeholder="Select or enter guardrails"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Input.TextArea rows={10} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<Button onClick={() => setIsEditing(false)}>
|
||||
|
|
|
@ -267,15 +267,9 @@ const Teams: React.FC<TeamProps> = ({
|
|||
} else {
|
||||
formValues.organization_id = organizationId.trim();
|
||||
}
|
||||
|
||||
// Create metadata object with guardrails if they exist
|
||||
formValues.metadata = {
|
||||
...(formValues.guardrails ? { guardrails: formValues.guardrails } : {})
|
||||
};
|
||||
|
||||
|
||||
// Remove guardrails from top level since it's now in metadata
|
||||
delete formValues.guardrails;
|
||||
|
||||
if (existingTeamAliases.includes(newTeamAlias)) {
|
||||
throw new Error(
|
||||
`Team alias ${newTeamAlias} already exists, please pick another alias`
|
||||
|
@ -702,6 +696,7 @@ const Teams: React.FC<TeamProps> = ({
|
|||
>
|
||||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
|
||||
<Accordion className="mt-20 mb-8">
|
||||
<AccordionHeader>
|
||||
<b>Additional Settings</b>
|
||||
|
@ -718,6 +713,9 @@ const Teams: React.FC<TeamProps> = ({
|
|||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Metadata" name="metadata" help="Additional team metadata. Enter metadata as JSON object.">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue