From 9f484d7deed6b7effec75a8e026ef29793e91e2c Mon Sep 17 00:00:00 2001 From: lakith-rambukkanage Date: Thu, 19 Sep 2024 07:18:43 +0530 Subject: [PATCH] Add AI API create flow and Backend Rate Limiting --- .../main/webapp/site/public/locales/en.json | 23 +- .../Apis/Create/AIAPI/APICreateAIAPI.jsx | 306 +++++++++++++++ .../Create/AIAPI/Steps/ProvideAIOpenAPI.jsx | 351 ++++++++++++++++++ .../Apis/Create/APICreateRoutes.jsx | 3 + .../APIDefinition/LinterUI/LinterUI.jsx | 4 +- .../Configuration/RuntimeConfiguration.jsx | 23 +- .../BackendRateLimiting.jsx | 96 +++++ .../BackendRateLimitingForm.jsx | 150 ++++++++ .../CommonRateLimitingForm.jsx | 123 ++++++ .../Details/components/APIDetailsTopMenu.jsx | 13 + .../Listing/Landing/Menus/RestAPIMenu.jsx | 18 + .../source/src/app/data/APIValidation.js | 1 + .../main/webapp/source/src/app/data/api.js | 76 ++++ 13 files changed, 1182 insertions(+), 5 deletions(-) create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/APICreateAIAPI.jsx create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/Steps/ProvideAIOpenAPI.jsx create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/BackendRateLimiting.jsx create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/BackendRateLimitingForm.jsx create mode 100644 portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/CommonRateLimitingForm.jsx diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index 2bede92a8c8..0545ae7b73d 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -10,6 +10,22 @@ "Apis.APIProductCreateWrapper.error.errorMessage.create.api.product": "Something went wrong while adding the API Product", "Apis.APIProductCreateWrapper.error.errorMessage.create.revision": "Something went wrong while creating the API Product Revision", "Apis.APIProductCreateWrapper.error.errorMessage.deploy.revision": "Something went wrong while deploying the API Product Revision", + "Apis.Create.AIAPI.ApiCreateAIAPI.back": "Back", + "Apis.Create.AIAPI.ApiCreateAIAPI.cancel": "Cancel", + "Apis.Create.AIAPI.ApiCreateAIAPI.create": "Create", + "Apis.Create.AIAPI.ApiCreateAIAPI.heading": "Create an API using an AI provider API definition.", + "Apis.Create.AIAPI.ApiCreateAIAPI.next": "Next", + "Apis.Create.AIAPI.ApiCreateAIAPI.sub.heading": "Create an API using an existing AI provider API definition.", + "Apis.Create.AIAPI.ApiCreateAIAPI.wizard.one": "Provide AI provider API", + "Apis.Create.AIAPI.ApiCreateAIAPI.wizard.two": "Create API", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model": "API version", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.empty": "No API Provider selected.", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.helper": "Select API Model version for the API", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.placeholder": "Search API version", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider": "API Provider", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.empty": "Loading API Providers...", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.helper.text": "Select AI API Provider for the API", + "Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.placeholder": "Search AI API Provider", "Apis.Create.APIProduct.APIProductCreateWrapper.back": "Back", "Apis.Create.APIProduct.APIProductCreateWrapper.cancel": "Cancel", "Apis.Create.APIProduct.APIProductCreateWrapper.create": "Create", @@ -345,6 +361,8 @@ "Apis.Details.Configurartion.components.QueryAnalysis.update.complexity": "update complexity", "Apis.Details.Configuration.ApiKeyHeader.helper.text": "ApiKey header name cannot contain spaces or special characters", "Apis.Details.Configuration.AuthHeader.helper.text": "Authorization header name cannot contain spaces or special characters", + "Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.prod": "Backend Rate Limiting", + "Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.tooltip": "This option determines the type of Backend Rate Limiting that is applied to the API.", "Apis.Details.Configuration.Components.APISecurity.Components.\n ApplicationLevel.Client.Websocket": "Client Websocket", "Apis.Details.Configuration.Components.APISecurity.Components.\n ApplicationLevel.Websocket": "Application Level Security", "Apis.Details.Configuration.Components.APISecurity.Components.ApplicationLevel.http": "Application Level Security", @@ -1510,6 +1528,7 @@ "Apis.Details.TryOutConsole.token.helper": "Generate or provide an internal API Key", "Apis.Details.TryOutConsole.token.label": "Internal API Key", "Apis.Details.components.APIDetailsTopMenu.advertise.only.label": "Third Party", + "Apis.Details.components.APIDetailsTopMenu.ai.api.label": "AI API", "Apis.Details.components.APIDetailsTopMenu.created.by": "Created by:", "Apis.Details.components.APIDetailsTopMenu.current.api": "Current API", "Apis.Details.components.APIDetailsTopMenu.error": "Something went wrong while downloading the API.", @@ -1582,6 +1601,8 @@ "Apis.Listing.ApiThumb.owners.technical": "Technical", "Apis.Listing.ApiThumb.version": "Version", "Apis.Listing.Components.Create.API": "Create API", + "Apis.Listing.SampleAPI.SampleAPI.ai.api.create.title": "Create AI API", + "Apis.Listing.SampleAPI.SampleAPI.ai.api.import.content": "Create AI APIs by importing AI vendor APIs", "Apis.Listing.SampleAPI.SampleAPI.create.new": "Let’s get started !", "Apis.Listing.SampleAPI.SampleAPI.create.new.description": "Choose your option to create an API", "Apis.Listing.SampleAPI.SampleAPI.graphql.api": "GraphQL", @@ -2030,4 +2051,4 @@ "upload.image": "Click or drag the image to upload.", "upload.image.size.error": "Uploaded File is too large. Maximum file size limit to 1MB", "upload.image.size.info": "Maximum file size limit to 1MB" -} \ No newline at end of file +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/APICreateAIAPI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/APICreateAIAPI.jsx new file mode 100644 index 00000000000..a7547ac59af --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/APICreateAIAPI.jsx @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useReducer, useState } from 'react'; +import PropTypes from 'prop-types'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { usePublisherSettings } from 'AppComponents/Shared/AppContext'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import Button from '@mui/material/Button'; +import { Link } from 'react-router-dom'; +import API from 'AppData/api'; +import Alert from 'AppComponents/Shared/Alert'; +import CircularProgress from '@mui/material/CircularProgress'; +import DefaultAPIForm from 'AppComponents/Apis/Create/Components/DefaultAPIForm'; +import APICreateBase from 'AppComponents/Apis/Create/Components/APICreateBase'; +import ProvideAIOpenAPI from './Steps/ProvideAIOpenAPI'; + + +/** + * + * Reduce the events triggered from API input fields to current state + * @param {*} currentState + * @param {*} inputAction + * @returns + */ +function apiInputsReducer(currentState, inputAction) { + const { action, value } = inputAction; + switch (action) { + case 'type': + case 'inputValue': + case 'name': + case 'version': + case 'endpoint': + case 'gatewayType': + case 'context': + case 'policies': + case 'llmProviderName': + case 'llmProviderApiVersion': + case 'isFormValid': + return { ...currentState, [action]: value }; + case 'preSetAPI': + return { + ...currentState, + name: value.name.replace(/[&/\\#,+()$~%.'":*?<>{}\s]/g, ''), + version: value.version, + context: value.context, + endpoint: value.endpoints && value.endpoints[0], + }; + default: + return currentState; + } +} +/** + * Handle API creation from AI Provider API Definition. + * + * @export + * @param {*} props + * @returns + */ +export default function ApiCreateAIAPI(props) { + const [wizardStep, setWizardStep] = useState(0); + const { history, multiGateway } = props; + const { data: settings } = usePublisherSettings(); + + const [apiInputs, inputsDispatcher] = useReducer(apiInputsReducer, { + type: 'ApiCreateAIAPI', + inputValue: '', + formValidity: false, + }); + + const intl = useIntl(); + + /** + * + * + * @param {*} event + */ + function handleOnChange(event) { + const { name: action, value } = event.target; + inputsDispatcher({ action, value }); + } + + /** + * + * Set the validity of the API Inputs form + * @param {*} isValidForm + * @param {*} validationState + */ + function handleOnValidate(isFormValid) { + inputsDispatcher({ + action: 'isFormValid', + value: isFormValid, + }); + } + + const [isCreating, setCreating] = useState(); + /** + * + * + * @param {*} params + */ + function createAPI() { + setCreating(true); + const { + name, version, context, endpoint, gatewayType, policies = ["Unlimited"], inputValue, + llmProviderName, llmProviderApiVersion, + } = apiInputs; + let defaultGatewayType; + if (settings && settings.gatewayTypes.length === 1 && settings.gatewayTypes.includes('Regular')) { + defaultGatewayType = 'wso2/synapse'; + } else if (settings && settings.gatewayTypes.length === 1 && settings.gatewayTypes.includes('APK')) { + defaultGatewayType = 'wso2/apk'; + } else { + defaultGatewayType = 'default'; + } + + const additionalProperties = { + name, + version, + context, + gatewayType: defaultGatewayType === 'default' ? gatewayType : defaultGatewayType, + policies, + aiConfiguration: { + llmProviderName, + llmProviderApiVersion, + }, + }; + if (endpoint) { + additionalProperties.endpointConfig = { + endpoint_type: 'http', + sandbox_endpoints: { + url: endpoint, + }, + production_endpoints: { + url: endpoint, + }, + }; + } + const newAPI = new API(additionalProperties); + const promisedResponse = newAPI.importOpenAPIByInlineDefinition(inputValue); + promisedResponse + .then((api) => { + Alert.info(intl.formatMessage({ + id: 'Apis.Create.OpenAPI.ApiCreateOpenAPI.created.success', + defaultMessage: 'API created successfully', + })); + history.push(`/apis/${api.id}/overview`); + }) + .catch((error) => { + if (error.response) { + Alert.error(error.response.body.description); + } else { + Alert.error(intl.formatMessage({ + id: 'Apis.Create.OpenAPI.ApiCreateOpenAPI.created.error', + defaultMessage: 'Something went wrong while adding the API', + })); + } + console.error(error); + }) + .finally(() => setCreating(false)); + } + + return ( + + + + + + + + + )} + > + + + + + + + + + + + + + + + + + + + {wizardStep === 0 && ( + + )} + {wizardStep === 1 && ( + + )} + + + + + {wizardStep === 0 && ( + + + + )} + {wizardStep === 1 && ( + + )} + + + {wizardStep === 0 && ( + + )} + {wizardStep === 1 && ( + + )} + + + + + + ); +} + +ApiCreateAIAPI.propTypes = { + history: PropTypes.shape({ push: PropTypes.func }).isRequired, + multiGateway: PropTypes.string.isRequired, +}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/Steps/ProvideAIOpenAPI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/Steps/ProvideAIOpenAPI.jsx new file mode 100644 index 00000000000..0eb9223c2be --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AIAPI/Steps/ProvideAIOpenAPI.jsx @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect, useState } from 'react'; +import API from 'AppData/api.js'; +import { styled } from '@mui/material/styles'; +import PropTypes from 'prop-types'; +import Grid from '@mui/material/Grid'; +import TextField from '@mui/material/TextField'; +import FormControl from '@mui/material/FormControl'; +import FormLabel from '@mui/material/FormLabel'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Autocomplete } from '@mui/material'; +import YAML from 'js-yaml'; + +import { + getLinterResultsFromContent +} from "../../../Details/APIDefinition/Linting/Linting"; +import ValidationResults from '../../OpenAPI/Steps/ValidationResults'; + +const PREFIX = 'ProvideAIOpenAPI'; + +const classes = { + mandatoryStar: `${PREFIX}-mandatoryStar` +}; + +const Root = styled('div')(( + { + theme + } +) => ({ + [`& .${classes.mandatoryStar}`]: { + color: theme.palette.error.main, + } +})); + + +/** + * Sub component of API Create using AI Provider OpenAPI UI + * + * @export + * @param {*} props + * @returns {React.Component} @inheritdoc + */ +export default function ProvideAIOpenAPI(props) { + const { apiInputs, inputsDispatcher, onValidate, onLinterLineSelect } = props; + const { inputValue } = apiInputs; + + // If valid value is `null`,that means valid, else an error object will be there + const [isValid, setValidity] = useState({}); + const [validationErrors, setValidationErrors] = useState([]); + const [isValidating, setIsValidating] = useState(false); + + const [llmProviders, setLLMProviders] = useState(null); + // If valid value is `null`,that means valid, else an error object will be there + const [linterResults, setLinterResults] = useState([]); + const [isLinting, setIsLinting] = useState(false); + const [selectedProvider, setSelectedProvider] = useState(null); + const [selectedModel, setSelectedModel] = useState(null); + + const intl = useIntl(); + + function getUniqueProviderList(llmProvidersResponse) { + if (!llmProvidersResponse) { + return []; + } + const uniqueProviders = []; + llmProvidersResponse.list.forEach((provider) => { + if (!uniqueProviders.includes(provider.name)) { + uniqueProviders.push(provider.name); + } + }); + return uniqueProviders; + } + + function lint(content) { + // Validate and linting + setIsLinting(true); + getLinterResultsFromContent(content).then((results) => { + if (results) { + setLinterResults(results); + } else { + setLinterResults([]); + } + }).finally(() => { setIsLinting(false); }); + } + + function hasJSONStructure(definition) { + if (typeof definition !== 'string') return false; + try { + const result = JSON.parse(definition); + return result && typeof result === 'object'; + } catch (err) { + console.log("API definition is in not in JSON format"); + return false; + } + } + + function onReceivingAPIdefinition(apiDefinition) { + setIsValidating(true); + let validFile = null; + API.validateOpenAPIByInlineDefinition(apiDefinition) + .then((response) => { + const { + body: { isValid: isValidFile, info, errors }, + } = response; + if (isValidFile) { + validFile = apiDefinition; + inputsDispatcher({ action: 'preSetAPI', value: info }); + setValidity({ ...isValid, file: null }); + } else { + setValidity({ + ...isValid, file: { + message: intl.formatMessage({ + id: 'Apis.Create.OpenAPI.create.api.openapi.content.validation.failed', + defaultMessage: 'OpenAPI content validation failed!' + }) + } + }); + setValidationErrors(errors); + } + }) + .catch((error) => { + setValidity({ + ...isValid, file: { + message: intl.formatMessage({ + id: 'Apis.Create.OpenAPI.create.api.openapi.content.validation.failed', + defaultMessage: 'OpenAPI content validation failed!' + }) + } + }); + console.error(error); + }) + .finally(() => { + setIsValidating(false); // Stop the loading animation + onValidate(validFile !== null); // If there is a valid file then validation has passed + // If the given file is valid , we set it as the inputValue else set `null` + inputsDispatcher({ action: 'inputValue', value: apiDefinition }); + }); + inputsDispatcher({ action: 'importingContent', value: apiDefinition }); + } + + function handleGetLLMProviderByIdResponse(response) { + const { + body: { + name, + apiVersion, + apiDefinition, + }, + } = response; + let formattedContent; + if (hasJSONStructure(apiDefinition)) { + formattedContent = JSON.stringify(JSON.parse(apiDefinition), null, 2); + } else { + formattedContent = JSON.stringify(YAML.load(apiDefinition), null, 2); + } + lint(formattedContent); + inputsDispatcher({ action: 'llmProviderName', value: name }); + inputsDispatcher({ action: 'llmProviderApiVersion', value: apiVersion }); + onReceivingAPIdefinition(formattedContent); + onValidate(apiDefinition !== null); + } + + + function reset() { + setSelectedModel(null); + setIsLinting(false); + setLinterResults([]); + setValidationErrors([]); + inputsDispatcher({ action: 'importingContent', value: null }); + inputsDispatcher({ action: 'inputValue', value: null }); + inputsDispatcher({ action: 'isFormValid', value: false }); + } + + useEffect(() => { + reset(); + }, [selectedProvider]); + + useEffect(() => { + API.getLLMProviders().then((response) => { + setLLMProviders(response.body); + }).catch((error) => { + console.error(error); + }); + }, []); + + return ( + + {llmProviders && ( + + + + { + setSelectedProvider(newValue); + }} + renderOption={(options, provider) => ( +
  • + {provider} +
  • + )} + renderInput={(params) => ( + + ) : ( + + ) + } + placeholder={intl.formatMessage({ + id: 'Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.placeholder', + defaultMessage: 'Search AI API Provider' + })} + helperText={( + + )} + margin='normal' + variant='outlined' + id='APIProvider' + /> + )} + /> +
    +
    +
    + + + + model.name === selectedProvider)} + noOptionsText='No API Provider selected' + getOptionLabel={(option) => + option.apiVersion + ' - ' + option.description + } + value={selectedModel} + onChange={(e, newValue) => { + setSelectedModel(newValue); + if (newValue) { + API.getLLMProviderById(newValue.id).then((response) => { + handleGetLLMProviderByIdResponse(response); + }).catch((error) => { + console.error(error); + }); + } + }} + renderOption={(options, option) => ( +
  • + {option.apiVersion + ' - ' + option.description} +
  • + )} + renderInput={(params) => ( + + ) : ( + + ) + } + placeholder={intl.formatMessage({ + id: 'Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.placeholder', + defaultMessage: 'Search API version' + })} + helperText={( + + )} + margin='normal' + variant='outlined' + id='APIModelVersion' + /> + )} + /> +
    +
    +
    + + + )} + {!llmProviders && ( + + + + + + )} +
    + ); +} + +ProvideAIOpenAPI.defaultProps = { + onValidate: () => { }, +}; + +ProvideAIOpenAPI.propTypes = { + apiInputs: PropTypes.shape({ + type: PropTypes.string, + inputValue: PropTypes.string, + }).isRequired, + inputsDispatcher: PropTypes.func.isRequired, + onValidate: PropTypes.func, +}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/APICreateRoutes.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/APICreateRoutes.jsx index b5d3db8d3f2..fcc34b11cd0 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/APICreateRoutes.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/APICreateRoutes.jsx @@ -28,6 +28,7 @@ import ApiCreateGraphQL from './GraphQL/ApiCreateGraphQL'; import ApiCreateWebSocket from './WebSocket/ApiCreateWebSocket'; import APICreateStreamingAPI from './StreamingAPI/APICreateStreamingAPI'; import APICreateAsyncAPI from './AsyncAPI/ApiCreateAsyncAPI'; +import ApiCreateAIAPI from './AIAPI/APICreateAIAPI'; const PREFIX = 'APICreateRoutes'; @@ -86,6 +87,8 @@ function APICreateRoutes() { + diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/LinterUI/LinterUI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/LinterUI/LinterUI.jsx index 6b9d8450dde..8abacf7b4e7 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/LinterUI/LinterUI.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/APIDefinition/LinterUI/LinterUI.jsx @@ -54,10 +54,10 @@ const StyledPaper = styled(Paper)(({ theme }) => ({ }, [`& .${classes.tableWrapper}`]: { - '& table tr td:first-child': { + '& table tr td:first-of-type': { width: 10, }, - '& table tr td:nth-child(2)': { + '& table tr td:nth-of-type(2)': { width: 10, }, }, diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/RuntimeConfiguration.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/RuntimeConfiguration.jsx index 779589c59f2..e07572396c5 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/RuntimeConfiguration.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/RuntimeConfiguration.jsx @@ -54,6 +54,7 @@ import { ALL_AUDIENCES_ALLOWED, } from './components/APISecurity/components/apiSecurityConstants'; import WebSubConfiguration from './components/WebSubConfiguration'; +import BackendRateLimiting from './components/AIBackendRateLimiting/BackendRateLimiting'; const PREFIX = 'RuntimeConfiguration'; @@ -203,6 +204,15 @@ function copyAPIConfig(api) { vendor: api.advertiseInfo.vendor, } } + if (api.aiConfiguration) { + apiConfigJson.aiConfiguration = { + ...api.aiConfiguration, + throttlingConfiguration: api.aiConfiguration.throttlingConfiguration ? + { ...api.aiConfiguration.throttlingConfiguration } : null, + endpointConfiguration: api.aiConfiguration.endpointConfiguration ? + { ...api.aiConfiguration.endpointConfiguration } : null, + }; + } return apiConfigJson; } @@ -378,6 +388,9 @@ export default function RuntimeConfiguration() { case 'saveButtonDisabled': setSaveButtonDisabled(value); return state; + case 'aiConfiguration': + nextState.aiConfiguration = value; + return nextState; default: return state; } @@ -632,7 +645,13 @@ export default function RuntimeConfiguration() { style={{ height: 'calc(100% - 75px)' }} elevation={0} > - {!api.isAPIProduct() && ( + {api.aiConfiguration && ( + + )} + {!api.aiConfiguration && !api.isAPIProduct() && ( <> {(!isAsyncAPI && api.gatewayType !== 'wso2/apk') && ( )} - {api.isAPIProduct() && ( + {!api.aiConfiguration && api.isAPIProduct() && ( + + + Token Based Throttling + + + + + + + + + + + ); +} + +BackendRateLimiting.propTypes = { + api: PropTypes.shape({}).isRequired, + configDispatcher: PropTypes.func.isRequired, +}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/BackendRateLimitingForm.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/BackendRateLimitingForm.jsx new file mode 100644 index 00000000000..1e18d981e2d --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/BackendRateLimitingForm.jsx @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { styled } from '@mui/material/styles'; +import PropTypes from 'prop-types'; +import { AccordionDetails, AccordionSummary, Tooltip, Typography } from '@mui/material'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { HelpOutline } from '@mui/icons-material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +import WrappedExpansionPanel from 'AppComponents/Shared/WrappedExpansionPanel'; +import CommonRateLimitingForm from './CommonRateLimitingForm'; + +/** + * Backend Rate Limiting for AI APIs + * + * @export + * @param {*} props + * @returns + */ +export default function BackendRateLimitingForm(props) { + const { api, configDispatcher, isProduction } = props; + const intl = useIntl(); + + const PREFIX = 'BackendRateLimitingForm'; + + const classes = { + expansionPanel: `${PREFIX}-expansionPanel`, + expansionPanelDetails: `${PREFIX}-expansionPanelDetails`, + iconSpace: `${PREFIX}-iconSpace`, + bottomSpace: `${PREFIX}-bottomSpace`, + subHeading: `${PREFIX}-subHeading` + }; + + const Root = styled('div')(({ theme }) => ({ + [`& .${classes.expansionPanel}`]: { + marginBottom: theme.spacing(1), + }, + + [`& .${classes.expansionPanelDetails}`]: { + flexDirection: 'column', + }, + + [`& .${classes.iconSpace}`]: { + marginLeft: theme.spacing(0.5), + }, + + [`& .${classes.bottomSpace}`]: { + marginBottom: theme.spacing(4), + }, + + [`& .${classes.subHeading}`]: { + fontSize: '1rem', + fontWeight: 400, + margin: 0, + display: 'inline-flex', + lineHeight: 1.5, + } + })); + + const titleText = (isProduction ? '[Production] ' : '[Sandbox] ') + intl.formatMessage({ + id: 'Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.prod', + defaultMessage: 'Backend Rate Limiting' + }); + + return ( + + + }> + + {titleText} + + )} + aria-label='API BE Rate limiting helper text' + placement='right-end' + interactive + > + + + + + + + + + + + + ); +} + +BackendRateLimitingForm.propTypes = { + api: PropTypes.shape({}).isRequired, + configDispatcher: PropTypes.func.isRequired, + isProduction: PropTypes.bool.isRequired, +}; \ No newline at end of file diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/CommonRateLimitingForm.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/CommonRateLimitingForm.jsx new file mode 100644 index 00000000000..1d3114c2093 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Configuration/components/AIBackendRateLimiting/CommonRateLimitingForm.jsx @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import Grid from '@mui/material/Grid'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import HelpOutline from '@mui/icons-material/HelpOutline'; +import { isRestricted } from 'AppData/AuthManager'; +import { useAPI } from 'AppComponents/Apis/Details/components/ApiContext'; +import APIValidation from 'AppData/APIValidation'; + +/** + * + * + * @export + * @param {*} props + * @returns + */ + +export default function CommonRateLimitingForm(props) { + const { api, configDispatcher, commonFormProps } = props; + const [apiFromContext] = useAPI(); + const [isValueValid, setIsValueValid] = useState(true); + + const currentValue = api.aiConfiguration.throttlingConfiguration ? + api.aiConfiguration.throttlingConfiguration[commonFormProps.key] : -1; + + function validateValue(value) { + const validity = commonFormProps.validator ? + commonFormProps.validator.validate(value, { abortEarly: false }).error + : APIValidation.isReqNumber.validate(value, { abortEarly: false }).error; + if (validity === null) { + setIsValueValid(true); + configDispatcher({ action: 'saveButtonDisabled', value: false }); + } else { + setIsValueValid(false); + configDispatcher({ action: 'saveButtonDisabled', value: true }); + } + } + + function handleOnChange({ target: { value } }) { + validateValue(value); + let throttlingConfiguration = {}; + if (api.aiConfiguration && api.aiConfiguration.throttlingConfiguration) { + throttlingConfiguration = api.aiConfiguration.throttlingConfiguration; + } + const dispatchValue = { + ...api.aiConfiguration, + throttlingConfiguration: { + ...throttlingConfiguration, + [commonFormProps.key]: value + } + } + configDispatcher({ + action: 'aiConfiguration', + value: dispatchValue + }) + } + + return ( + + + + + {commonFormProps.tooltip && ( + + + + )} + + ); +} + +CommonRateLimitingForm.propTypes = { + api: PropTypes.shape({}).isRequired, + configDispatcher: PropTypes.func.isRequired, + commonFormProps: PropTypes.shape({ + key: PropTypes.string, + label: PropTypes.string, + helperText: PropTypes.string, + placeholder: PropTypes.string, + tooltip: PropTypes.string, + validator: PropTypes.func, + }).isRequired, +}; diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx index 51794730684..83652efa746 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/components/APIDetailsTopMenu.jsx @@ -302,6 +302,19 @@ const APIDetailsTopMenu = (props) => { )}
    + {(api.aiConfiguration) && ( + + + + )} {(api.advertiseInfo && api.advertiseInfo.advertised) && ( { defaultMessage='Import Open API' /> + + + )} + > + + + {(!isCreateMenu || (isCreateMenu && alwaysShowDeploySampleButton)) && showSampleDeploy && !apkGatewayType && ( <> diff --git a/portals/publisher/src/main/webapp/source/src/app/data/APIValidation.js b/portals/publisher/src/main/webapp/source/src/app/data/APIValidation.js index 570f813be69..3e168820e73 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/APIValidation.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/APIValidation.js @@ -198,6 +198,7 @@ const definition = { websubOperationTarget: Joi.string().regex(/^[^{}]*$/).required(), name: Joi.string().min(1).max(255), email: Joi.string().email({ tlds: false }).required(), + isReqNumber: Joi.number().required(), }; export default definition; diff --git a/portals/publisher/src/main/webapp/source/src/app/data/api.js b/portals/publisher/src/main/webapp/source/src/app/data/api.js index daade3ba01f..f4a7ea23eab 100644 --- a/portals/publisher/src/main/webapp/source/src/app/data/api.js +++ b/portals/publisher/src/main/webapp/source/src/app/data/api.js @@ -143,6 +143,31 @@ class API extends Resource { return promise_create; } + importOpenAPIByInlineDefinition(inlineDefinition) { + let payload, promise_create; + + promise_create = this.client.then(client => { + const apiData = this.getDataFromSpecFields(client); + + payload = { + requestBody: { + inlineAPIDefinition: inlineDefinition, + additionalProperties: JSON.stringify(apiData), + } + }; + + const promisedResponse = client.apis['APIs'].importOpenAPIDefinition( + null, + payload, + this._requestMetaData({ + 'Content-Type': 'multipart/form-data', + }), + ); + return promisedResponse.then(response => new API(response.body)); + }); + return promise_create; + } + /** * Get list of workflow pending requests */ @@ -232,6 +257,29 @@ class API extends Resource { } + static validateOpenAPIByInlineDefinition(inlineDefinition, params = { returnContent: false }) { + const apiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + const payload = { + 'Content-Type': 'multipart/form-data', + ...params + }; + const requestBody = { + requestBody: { + inlineAPIDefinition: inlineDefinition, + }, + }; + return apiClient.then(client => { + return client.apis['Validation'].validateOpenAPIDefinition( + payload, + requestBody, + this._requestMetaData({ + 'Content-Type': 'multipart/form-data', + }), + ); + }); + + } + /** * Get API Security Audit Report */ @@ -3248,6 +3296,34 @@ class API extends Resource { ); }); } + + /** + * Get the all LLM providers + * @returns {Promise} Promise containing the list of LLM providers + */ + static getLLMProviders() { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['LLMProviders'].getLLMProviders(); + }); + } + + /** + * Get the LLM provider by ID + * @param {String} llmProviderId UUID of the LLM provider + * @returns {Promise} Promise containing the information of the requested LLM provider + */ + static getLLMProviderById(llmProviderId) { + const restApiClient = new APIClientFactory().getAPIClient(Utils.getCurrentEnvironment(), Utils.CONST.API_CLIENT).client; + return restApiClient.then(client => { + return client.apis['LLMProvider'].getLLMProvider( + {llmProviderId}, + this._requestMetaData(), + ); + }); + } + + } API.CONSTS = {