mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +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
|
organization_alias: str
|
||||||
models: List = []
|
models: List = []
|
||||||
budget_id: Optional[str] = None
|
budget_id: Optional[str] = None
|
||||||
|
metadata: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRequest(LiteLLMPydanticObjectBase):
|
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.
|
- 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
|
- model_max_budget: *Optional[dict]* - Max budget for a specific model
|
||||||
- budget_duration: *Optional[str]* - Frequency of reseting org budget
|
- 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.
|
- 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).
|
- 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`.
|
- 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)
|
- 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
|
Case 1: Create new org **without** a budget_id
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -103,6 +102,7 @@ async def new_organization(
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
|
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
|
||||||
|
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
|
@ -174,6 +174,9 @@ async def new_organization(
|
||||||
new_organization_row = prisma_client.jsonify_object(
|
new_organization_row = prisma_client.jsonify_object(
|
||||||
organization_row.json(exclude_none=True)
|
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(
|
response = await prisma_client.db.litellm_organizationtable.create(
|
||||||
data={
|
data={
|
||||||
**new_organization_row, # type: ignore
|
**new_organization_row, # type: ignore
|
||||||
|
@ -215,9 +218,13 @@ async def update_organization(
|
||||||
if data.updated_by is None:
|
if data.updated_by is None:
|
||||||
data.updated_by = user_api_key_dict.user_id
|
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(
|
response = await prisma_client.db.litellm_organizationtable.update(
|
||||||
where={"organization_id": data.organization_id},
|
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},
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_failure_call_hook():
|
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";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Button, TextInput, Grid, Col } from "@tremor/react";
|
import { Button, TextInput, Grid, Col } from "@tremor/react";
|
||||||
import {
|
import {
|
||||||
|
@ -24,11 +23,13 @@ import {
|
||||||
Radio,
|
Radio,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { unfurlWildcardModelsInList, getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
import { unfurlWildcardModelsInList, getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||||
|
import SchemaFormFields from './common_components/check_openapi_schema';
|
||||||
import {
|
import {
|
||||||
keyCreateCall,
|
keyCreateCall,
|
||||||
slackBudgetAlertsHealthCheck,
|
slackBudgetAlertsHealthCheck,
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
getGuardrailsList,
|
getGuardrailsList,
|
||||||
|
proxyBaseUrl,
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
import { Team } from "./key_team_helpers/key_list";
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
import TeamDropdown from "./common_components/team_dropdown";
|
import TeamDropdown from "./common_components/team_dropdown";
|
||||||
|
@ -474,6 +475,36 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
options={predefinedTags}
|
options={predefinedTags}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
</AccordionBody>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
|
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
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) {
|
if (isLocal != true) {
|
||||||
console.log = function() {};
|
console.log = function() {};
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,12 @@ export function setGlobalLitellmHeaderName(headerName: string = "Authorization")
|
||||||
globalLitellmHeaderName = headerName;
|
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 (
|
export const modelCostMap = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
@ -882,6 +888,17 @@ export const organizationCreateCall = async (
|
||||||
try {
|
try {
|
||||||
console.log("Form Values in organizationCreateCall:", formValues); // Log the form values before making the API call
|
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 url = proxyBaseUrl ? `${proxyBaseUrl}/organization/new` : `/organization/new`;
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
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 (
|
export const organizationDeleteCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
organizationID: string
|
organizationID: string
|
||||||
|
@ -2377,6 +2430,15 @@ export const teamCreateCall = async (
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
console.log("Form Values in teamCreateCall:", formValues); // Log the form values before making the API call
|
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 url = proxyBaseUrl ? `${proxyBaseUrl}/team/new` : `/team/new`;
|
||||||
const response = await fetch(url, {
|
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 { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
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 UserSearchModal from "../common_components/user_search_modal";
|
||||||
import MemberModal from "../team/edit_membership";
|
import MemberModal from "../team/edit_membership";
|
||||||
|
|
||||||
|
@ -146,20 +146,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
||||||
rpm_limit: values.rpm_limit,
|
rpm_limit: values.rpm_limit,
|
||||||
max_budget: values.max_budget,
|
max_budget: values.max_budget,
|
||||||
budget_duration: values.budget_duration,
|
budget_duration: values.budget_duration,
|
||||||
}
|
},
|
||||||
|
metadata: values.metadata ? JSON.parse(values.metadata) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch('/organization/update', {
|
const response = await organizationUpdateCall(accessToken, updateData);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updateData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Failed to update organization');
|
|
||||||
|
|
||||||
message.success("Organization settings updated successfully");
|
message.success("Organization settings updated successfully");
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
fetchOrgInfo();
|
fetchOrgInfo();
|
||||||
|
@ -337,6 +329,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
||||||
rpm_limit: orgData.litellm_budget_table.rpm_limit,
|
rpm_limit: orgData.litellm_budget_table.rpm_limit,
|
||||||
max_budget: orgData.litellm_budget_table.max_budget,
|
max_budget: orgData.litellm_budget_table.max_budget,
|
||||||
budget_duration: orgData.litellm_budget_table.budget_duration,
|
budget_duration: orgData.litellm_budget_table.budget_duration,
|
||||||
|
metadata: orgData.metadata ? JSON.stringify(orgData.metadata, null, 2) : "",
|
||||||
}}
|
}}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
>
|
>
|
||||||
|
@ -384,6 +377,10 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
||||||
<InputNumber step={1} style={{ width: "100%" }} />
|
<InputNumber step={1} style={{ width: "100%" }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Metadata" name="metadata">
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-6">
|
<div className="flex justify-end gap-2 mt-6">
|
||||||
<Button onClick={() => setIsEditing(false)}>
|
<Button onClick={() => setIsEditing(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
TabPanels,
|
TabPanels,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
|
import { Input } from "antd";
|
||||||
import { Modal, Form, InputNumber, Tooltip, Select as Select2 } from "antd";
|
import { Modal, Form, InputNumber, Tooltip, Select as Select2 } from "antd";
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||||
|
@ -102,6 +103,8 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
||||||
try {
|
try {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
console.log(`values in organizations new create call: ${JSON.stringify(values)}`);
|
||||||
|
|
||||||
await organizationCreateCall(accessToken, values);
|
await organizationCreateCall(accessToken, values);
|
||||||
setIsOrgModalVisible(false);
|
setIsOrgModalVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
@ -334,6 +337,10 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
||||||
<InputNumber step={1} width={400} />
|
<InputNumber step={1} width={400} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Metadata" name="metadata">
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
<Button type="submit">Create Organization</Button>
|
<Button type="submit">Create Organization</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -348,7 +348,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||||
rpm_limit: info.rpm_limit,
|
rpm_limit: info.rpm_limit,
|
||||||
max_budget: info.max_budget,
|
max_budget: info.max_budget,
|
||||||
budget_duration: info.budget_duration,
|
budget_duration: info.budget_duration,
|
||||||
guardrails: info.metadata?.guardrails || []
|
guardrails: info.metadata?.guardrails || [],
|
||||||
|
metadata: info.metadata ? JSON.stringify(info.metadata, null, 2) : "",
|
||||||
}}
|
}}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
>
|
>
|
||||||
|
@ -420,6 +421,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||||
placeholder="Select or enter guardrails"
|
placeholder="Select or enter guardrails"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="Metadata" name="metadata">
|
||||||
|
<Input.TextArea rows={10} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-6">
|
<div className="flex justify-end gap-2 mt-6">
|
||||||
<Button onClick={() => setIsEditing(false)}>
|
<Button onClick={() => setIsEditing(false)}>
|
||||||
|
|
|
@ -267,15 +267,9 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
} else {
|
} else {
|
||||||
formValues.organization_id = organizationId.trim();
|
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
|
// Remove guardrails from top level since it's now in metadata
|
||||||
delete formValues.guardrails;
|
|
||||||
|
|
||||||
if (existingTeamAliases.includes(newTeamAlias)) {
|
if (existingTeamAliases.includes(newTeamAlias)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Team alias ${newTeamAlias} already exists, please pick another alias`
|
`Team alias ${newTeamAlias} already exists, please pick another alias`
|
||||||
|
@ -702,6 +696,7 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
>
|
>
|
||||||
<InputNumber step={1} width={400} />
|
<InputNumber step={1} width={400} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Accordion className="mt-20 mb-8">
|
<Accordion className="mt-20 mb-8">
|
||||||
<AccordionHeader>
|
<AccordionHeader>
|
||||||
<b>Additional Settings</b>
|
<b>Additional Settings</b>
|
||||||
|
@ -718,6 +713,9 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue