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 = {