[Stack Connectors] Add organizationId and projectId OpenAI headers, along with arbitrary headers (#213117)

This commit is contained in:
Steph Milovic 2025-03-06 12:59:15 -07:00 committed by GitHub
parent 5b8fd8f5c7
commit 1531849d6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 423 additions and 25 deletions

View file

@ -42073,7 +42073,6 @@
"xpack.stackConnectors.components.genAi.connectorTypeTitle": "OpenAI",
"xpack.stackConnectors.components.genAi.dashboardLink": "Affichez le tableau de bord d'utilisation de {apiProvider} pour le connecteur \"{ connectorName }\"",
"xpack.stackConnectors.components.genAi.defaultModelTextFieldLabel": "Modèle par défaut",
"xpack.stackConnectors.components.genAi.defaultModelTooltipContent": "Si une requête ne comprend pas de modèle, le modèle par défaut est utilisé.",
"xpack.stackConnectors.components.genAi.documentation": "documentation",
"xpack.stackConnectors.components.genAi.error.dashboardApiError": "Erreur lors de la recherche du tableau de bord d'utilisation des tokens {apiProvider}.",
"xpack.stackConnectors.components.genAi.error.requiredApiProviderText": "Un fournisseur dAPI est nécessaire.",

View file

@ -41926,7 +41926,6 @@
"xpack.stackConnectors.components.genAi.connectorTypeTitle": "OpenAI",
"xpack.stackConnectors.components.genAi.dashboardLink": "\"{ connectorName }\"コネクターの{apiProvider}使用状況ダッシュボードを表示",
"xpack.stackConnectors.components.genAi.defaultModelTextFieldLabel": "デフォルトモデル",
"xpack.stackConnectors.components.genAi.defaultModelTooltipContent": "リクエストにモデルが含まれていない場合、デフォルトが使われます。",
"xpack.stackConnectors.components.genAi.documentation": "ドキュメンテーション",
"xpack.stackConnectors.components.genAi.error.dashboardApiError": "{apiProvider}トークン使用状況ダッシュボードの検索エラー。",
"xpack.stackConnectors.components.genAi.error.requiredApiProviderText": "APIプロバイダーは必須です。",

View file

@ -41303,7 +41303,6 @@
"xpack.stackConnectors.components.genAi.bodyFieldLabel": "正文",
"xpack.stackConnectors.components.genAi.connectorTypeTitle": "OpenAI",
"xpack.stackConnectors.components.genAi.defaultModelTextFieldLabel": "默认模型",
"xpack.stackConnectors.components.genAi.defaultModelTooltipContent": "如果请求不包含模型,它将使用默认值。",
"xpack.stackConnectors.components.genAi.documentation": "文档",
"xpack.stackConnectors.components.genAi.error.dashboardApiError": "查找 {apiProvider} 令牌使用情况仪表板时出错。",
"xpack.stackConnectors.components.genAi.error.requiredApiProviderText": "'API 提供商'必填。",

View file

@ -18,6 +18,7 @@ export const allowedExperimentalValues = Object.freeze({
inferenceConnectorOff: false,
crowdstrikeConnectorRTROn: true,
microsoftDefenderEndpointOn: true,
openAIAdditionalHeadersOn: false,
});
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -23,6 +23,8 @@ export const ConfigSchema = schema.oneOf([
schema.object({
apiProvider: schema.oneOf([schema.literal(OpenAiProviderType.OpenAi)]),
apiUrl: schema.string(),
organizationId: schema.maybe(schema.string()),
projectId: schema.maybe(schema.string()),
defaultModel: schema.string({ defaultValue: DEFAULT_OPENAI_MODEL }),
headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
}),

View file

@ -8,12 +8,14 @@
import React from 'react';
import ConnectorFields from './connector';
import { ConnectorFormTestProvider } from '../lib/test_utils';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DEFAULT_OPENAI_MODEL, OpenAiProviderType } from '../../../common/openai/constants';
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
import { useGetDashboard } from '../lib/gen_ai/use_get_dashboard';
import { createStartServicesMock } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react.mock';
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
import { experimentalFeaturesMock } from '../../mocks';
const mockUseKibanaReturnValue = createStartServicesMock();
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana', () => ({
@ -38,6 +40,9 @@ const openAiConnector = {
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
__internal__: {
hasHeaders: false,
},
isDeprecated: false,
};
const azureConnector = {
@ -65,6 +70,12 @@ const otherOpenAiConnector = {
const navigateToUrl = jest.fn();
describe('ConnectorFields renders', () => {
beforeAll(() => {
ExperimentalFeaturesService.init({
// @ts-ignore force enable for testing
experimentalFeatures: { ...experimentalFeaturesMock, openAIAdditionalHeadersOn: true },
});
});
beforeEach(() => {
jest.clearAllMocks();
useKibanaMock().services.application.navigateToUrl = navigateToUrl;
@ -84,12 +95,14 @@ describe('ConnectorFields renders', () => {
expect(getAllByTestId('config.apiProvider-select')[0]).toHaveValue(
openAiConnector.config.apiProvider
);
expect(getAllByTestId('config.organizationId-input')[0]).toBeInTheDocument();
expect(getAllByTestId('config.projectId-input')[0]).toBeInTheDocument();
expect(getAllByTestId('open-ai-api-doc')[0]).toBeInTheDocument();
expect(getAllByTestId('open-ai-api-keys-doc')[0]).toBeInTheDocument();
});
test('azure ai connector fields are rendered', async () => {
const { getAllByTestId } = render(
const { getAllByTestId, queryByTestId } = render(
<ConnectorFormTestProvider connector={azureConnector}>
<ConnectorFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
@ -102,10 +115,12 @@ describe('ConnectorFields renders', () => {
);
expect(getAllByTestId('azure-ai-api-doc')[0]).toBeInTheDocument();
expect(getAllByTestId('azure-ai-api-keys-doc')[0]).toBeInTheDocument();
expect(queryByTestId('config.organizationId-input')).not.toBeInTheDocument();
expect(queryByTestId('config.projectId-input')).not.toBeInTheDocument();
});
test('other open ai connector fields are rendered', async () => {
const { getAllByTestId } = render(
const { getAllByTestId, queryByTestId } = render(
<ConnectorFormTestProvider connector={otherOpenAiConnector}>
<ConnectorFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
@ -120,6 +135,149 @@ describe('ConnectorFields renders', () => {
);
expect(getAllByTestId('other-ai-api-doc')[0]).toBeInTheDocument();
expect(getAllByTestId('other-ai-api-keys-doc')[0]).toBeInTheDocument();
expect(queryByTestId('config.organizationId-input')).not.toBeInTheDocument();
expect(queryByTestId('config.projectId-input')).not.toBeInTheDocument();
});
describe('Headers', () => {
it('toggles headers as expected', async () => {
const testFormData = {
actionTypeId: '.gen-ai',
name: 'OpenAI',
id: '123',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
__internal__: {
hasHeaders: false,
},
};
render(
<ConnectorFormTestProvider connector={testFormData}>
<ConnectorFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);
const headersToggle = await screen.findByTestId('openAIViewHeadersSwitch');
expect(headersToggle).toBeInTheDocument();
await userEvent.click(headersToggle);
expect(await screen.findByTestId('openAIHeaderText')).toBeInTheDocument();
expect(await screen.findByTestId('openAIHeadersKeyInput')).toBeInTheDocument();
expect(await screen.findByTestId('openAIHeadersValueInput')).toBeInTheDocument();
expect(await screen.findByTestId('openAIAddHeaderButton')).toBeInTheDocument();
});
it('succeeds without headers', async () => {
const testFormData = {
actionTypeId: '.gen-ai',
name: 'OpenAI',
id: '123',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
__internal__: {
hasHeaders: false,
},
};
const onSubmit = jest.fn();
render(
<ConnectorFormTestProvider connector={testFormData} onSubmit={onSubmit}>
<ConnectorFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
actionTypeId: '.gen-ai',
name: 'OpenAI',
id: '123',
isDeprecated: false,
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
__internal__: {
hasHeaders: false,
},
},
isValid: true,
});
});
});
it('succeeds with headers', async () => {
const testFormData = {
actionTypeId: '.gen-ai',
name: 'OpenAI',
id: '123',
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
isDeprecated: false,
__internal__: {
hasHeaders: false,
},
};
const onSubmit = jest.fn();
render(
<ConnectorFormTestProvider connector={testFormData} onSubmit={onSubmit}>
<ConnectorFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);
const headersToggle = await screen.findByTestId('openAIViewHeadersSwitch');
expect(headersToggle).toBeInTheDocument();
await userEvent.click(headersToggle);
await userEvent.type(screen.getByTestId('openAIHeadersKeyInput'), 'hello');
await userEvent.type(screen.getByTestId('openAIHeadersValueInput'), 'world');
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
actionTypeId: '.gen-ai',
name: 'OpenAI',
id: '123',
isDeprecated: false,
config: {
apiUrl: 'https://openaiurl.com',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
headers: [{ key: 'hello', value: 'world' }],
},
secrets: {
apiKey: 'thats-a-nice-looking-key',
},
__internal__: {
hasHeaders: true,
},
},
isValid: true,
});
});
});
});
describe('Dashboard link', () => {

View file

@ -10,14 +10,29 @@ import {
ActionConnectorFieldsProps,
SimpleConnectorForm,
} from '@kbn/triggers-actions-ui-plugin/public';
import { SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { EuiSpacer } from '@elastic/eui';
import {
SelectField,
TextField,
ToggleField,
} from '@kbn/es-ui-shared-plugin/static/forms/components';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import {
UseArray,
UseField,
useFormContext,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
import * as i18nAuth from '../../common/auth/translations';
import DashboardLink from './dashboard_link';
import { OpenAiProviderType } from '../../../common/openai/constants';
import * as i18n from './translations';
@ -26,17 +41,21 @@ import {
azureAiSecrets,
otherOpenAiConfig,
otherOpenAiSecrets,
openAiConfig,
openAiSecrets,
providerOptions,
getOpenAiConfig,
} from './constants';
const { emptyField } = fieldValidators;
const { emptyField } = fieldValidators;
const ConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
const { euiTheme } = useEuiTheme();
const { getFieldDefaultValue } = useFormContext();
const [{ config, id, name }] = useFormData({
watch: ['config.apiProvider'],
const [{ config, __internal__, id, name }] = useFormData({
watch: ['config.apiProvider', '__internal__.hasHeaders'],
});
const enabledAdditionalHeaders = ExperimentalFeaturesService.get().openAIAdditionalHeadersOn;
const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false;
const hasHeadersDefaultValue = !!getFieldDefaultValue<boolean | undefined>('config.headers');
const selectedProviderDefaultValue = useMemo(
() =>
@ -74,7 +93,7 @@ const ConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdi
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={openAiConfig}
configFormSchema={getOpenAiConfig(enabledAdditionalHeaders)}
secretsFormSchema={openAiSecrets}
/>
)}
@ -95,6 +114,86 @@ const ConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdi
secretsFormSchema={otherOpenAiSecrets}
/>
)}
<UseField
path="__internal__.hasHeaders"
component={ToggleField}
config={{
defaultValue: hasHeadersDefaultValue,
label: i18nAuth.HEADERS_SWITCH,
}}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'openAIViewHeadersSwitch',
},
}}
/>
{hasHeaders && (
<UseArray path="config.headers" initialNumberOfItems={1}>
{({ items, addItem, removeItem }) => {
return (
<>
<EuiSpacer size="s" />
<EuiTitle size="xxs" data-test-subj="openAIHeaderText">
<h5>{i18nAuth.HEADERS_TITLE}</h5>
</EuiTitle>
<EuiSpacer size="s" />
{items.map((item) => (
<EuiFlexGroup key={item.id} css={{ marginTop: euiTheme.size.s }}>
<EuiFlexItem>
<UseField
path={`${item.path}.key`}
config={{
label: i18nAuth.KEY_LABEL,
}}
component={TextField}
// This is needed because when you delete
// a row and add a new one, the stale values will appear
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: { readOnly, ['data-test-subj']: 'openAIHeadersKeyInput' },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path={`${item.path}.value`}
config={{ label: i18nAuth.VALUE_LABEL }}
component={TextField}
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: {
readOnly,
['data-test-subj']: 'openAIHeadersValueInput',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
onClick={() => removeItem(item.id)}
iconType="minusInCircle"
aria-label={i18nAuth.DELETE_BUTTON}
style={{ marginTop: '28px' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiSpacer size="m" />
<EuiButtonEmpty
iconType="plusInCircle"
onClick={addItem}
data-test-subj="openAIAddHeaderButton"
>
{i18nAuth.ADD_BUTTON}
</EuiButtonEmpty>
<EuiSpacer />
</>
);
}}
</UseArray>
)}
{isEdit && (
<DashboardLink
connectorId={id}

View file

@ -8,7 +8,7 @@
import React from 'react';
import { ConfigFieldSchema, SecretsFieldSchema } from '@kbn/triggers-actions-ui-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLink } from '@elastic/eui';
import { EuiLink, EuiText } from '@elastic/eui';
import { DEFAULT_OPENAI_MODEL, OpenAiProviderType } from '../../../common/openai/constants';
import * as i18n from './translations';
import { Config } from './types';
@ -54,7 +54,7 @@ export const getDefaultBody = (config?: Config) => {
return DEFAULT_BODY;
};
export const openAiConfig: ConfigFieldSchema[] = [
export const getOpenAiConfig = (enabledAdditionalHeaders: boolean): ConfigFieldSchema[] => [
{
id: 'apiUrl',
label: i18n.API_URL_LABEL,
@ -89,6 +89,51 @@ export const openAiConfig: ConfigFieldSchema[] = [
),
defaultValue: DEFAULT_OPENAI_MODEL,
},
...(enabledAdditionalHeaders
? [
{
id: 'organizationId',
label: i18n.ORG_ID_LABEL,
isRequired: false,
helpText: (
<FormattedMessage
defaultMessage="For users who belong to multiple organizations. Organization IDs can be found on your Organization settings page."
id="xpack.stackConnectors.components.genAi.openAiOrgId"
/>
),
euiFieldProps: {
append: (
<EuiText size="xs" color="subdued">
{i18n.OPTIONAL_LABEL}
</EuiText>
),
},
},
{
id: 'projectId',
label: i18n.PROJECT_ID_LABEL,
isRequired: false,
helpText: (
<FormattedMessage
defaultMessage="For users who are accessing their projects through their legacy user API key. Project IDs can be found on your General settings page by selecting the specific project."
id="xpack.stackConnectors.components.genAi.openAiProjectId"
/>
),
euiFieldProps: {
autocomplete: 'new-password',
autoComplete: 'new-password',
onFocus: (event: React.FocusEvent<HTMLInputElement>) => {
event.target.setAttribute('autocomplete', 'new-password');
},
append: (
<EuiText size="xs" color="subdued">
{i18n.OPTIONAL_LABEL}
</EuiText>
),
},
},
]
: []),
];
export const azureAiConfig: ConfigFieldSchema[] = [

View file

@ -25,10 +25,24 @@ export const DEFAULT_MODEL_LABEL = i18n.translate(
}
);
export const DEFAULT_MODEL_TOOLTIP_CONTENT = i18n.translate(
'xpack.stackConnectors.components.genAi.defaultModelTooltipContent',
export const ORG_ID_LABEL = i18n.translate(
'xpack.stackConnectors.components.genAi.orgIdTextFieldLabel',
{
defaultMessage: 'If a request does not include a model, it uses the default.',
defaultMessage: 'OpenAI Organization',
}
);
export const PROJECT_ID_LABEL = i18n.translate(
'xpack.stackConnectors.components.genAi.projectIdTextFieldLabel',
{
defaultMessage: 'OpenAI Project',
}
);
export const OPTIONAL_LABEL = i18n.translate(
'xpack.stackConnectors.components.genAi.optionalLabel',
{
defaultMessage: 'Optional',
}
);

View file

@ -664,6 +664,68 @@ describe('OpenAIConnector', () => {
});
});
});
describe('OpenAI with special headers', () => {
const connector = new OpenAIConnector({
configurationUtilities: actionsConfigMock.create(),
connector: { id: '1', type: OPENAI_CONNECTOR_ID },
config: {
apiUrl: 'https://api.openai.com/v1/chat/completions',
apiProvider: OpenAiProviderType.OpenAi,
defaultModel: DEFAULT_OPENAI_MODEL,
organizationId: 'org-id',
projectId: 'proj-id',
headers: {
'X-My-Custom-Header': 'foo',
Authorization: 'override',
},
},
secrets: { apiKey: '123' },
logger,
services: actionsMock.createServices(),
});
const sampleOpenAiBody = {
messages: [
{
role: 'user',
content: 'Hello world',
},
],
};
beforeEach(() => {
// @ts-ignore
connector.request = mockRequest;
jest.clearAllMocks();
});
it('the OpenAI API call is successful with correct parameters', async () => {
const response = await connector.runApi(
{ body: JSON.stringify(sampleOpenAiBody) },
connectorUsageCollector
);
expect(mockRequest).toBeCalledTimes(1);
expect(mockRequest).toHaveBeenCalledWith(
{
...mockDefaults,
data: JSON.stringify({
...sampleOpenAiBody,
stream: false,
model: DEFAULT_OPENAI_MODEL,
}),
headers: {
'OpenAI-Organization': 'org-id',
'OpenAI-Project': 'proj-id',
Authorization: 'Bearer 123',
'X-My-Custom-Header': 'foo',
'content-type': 'application/json',
},
},
connectorUsageCollector
);
expect(response).toEqual(mockResponse.data);
});
});
describe('OpenAI without headers', () => {
const connector = new OpenAIConnector({

View file

@ -59,6 +59,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
private provider;
private key;
private openAI;
private headers;
constructor(params: ServiceParams<Config, Secrets>) {
super(params);
@ -66,6 +67,13 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
this.url = this.config.apiUrl;
this.provider = this.config.apiProvider;
this.key = this.secrets.apiKey;
this.headers = {
...this.config.headers,
...('organizationId' in this.config
? { 'OpenAI-Organization': this.config.organizationId }
: {}),
...('projectId' in this.config ? { 'OpenAI-Project': this.config.projectId } : {}),
};
this.openAI =
this.config.apiProvider === OpenAiProviderType.AzureAi
@ -74,7 +82,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
baseURL: this.config.apiUrl,
defaultQuery: { 'api-version': getAzureApiVersionParameter(this.config.apiUrl) },
defaultHeaders: {
...this.config.headers,
...this.headers,
'api-key': this.secrets.apiKey,
},
})
@ -82,7 +90,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
baseURL: removeEndpointFromUrl(this.config.apiUrl),
apiKey: this.secrets.apiKey,
defaultHeaders: {
...this.config.headers,
...this.headers,
},
});
@ -180,7 +188,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
...axiosOptions,
headers: {
...this.config.headers,
...this.headers,
...axiosOptions.headers,
},
},
@ -220,7 +228,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
signal,
...axiosOptions,
headers: {
...this.config.headers,
...this.headers,
...axiosOptions.headers,
},
timeout,

View file

@ -61,7 +61,11 @@ interface Props {
// TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved
const formDeserializer = (data: ConnectorFormSchema): ConnectorFormSchema => {
if (data.actionTypeId !== '.webhook' && data.actionTypeId !== '.cases-webhook') {
if (
data.actionTypeId !== '.webhook' &&
data.actionTypeId !== '.cases-webhook' &&
data.actionTypeId !== '.gen-ai'
) {
return data;
}
@ -82,7 +86,11 @@ const formDeserializer = (data: ConnectorFormSchema): ConnectorFormSchema => {
// TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved
const formSerializer = (formData: ConnectorFormSchema): ConnectorFormSchema => {
if (formData.actionTypeId !== '.webhook' && formData.actionTypeId !== '.cases-webhook') {
if (
formData.actionTypeId !== '.webhook' &&
formData.actionTypeId !== '.cases-webhook' &&
formData.actionTypeId !== '.gen-ai'
) {
return formData;
}
@ -101,7 +109,11 @@ const formSerializer = (formData: ConnectorFormSchema): ConnectorFormSchema => {
...formData,
config: {
...formData.config,
headers: isEmpty(headers) ? null : headers,
headers: isEmpty(headers)
? formData.actionTypeId !== '.gen-ai'
? null
: undefined
: headers,
},
};
};