mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Stack Connectors] Add organizationId
and projectId
OpenAI headers, along with arbitrary headers (#213117)
This commit is contained in:
parent
5b8fd8f5c7
commit
1531849d6f
12 changed files with 423 additions and 25 deletions
|
@ -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 d’API est nécessaire.",
|
||||
|
|
|
@ -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プロバイダーは必須です。",
|
||||
|
|
|
@ -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 提供商'必填。",
|
||||
|
|
|
@ -18,6 +18,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
inferenceConnectorOff: false,
|
||||
crowdstrikeConnectorRTROn: true,
|
||||
microsoftDefenderEndpointOn: true,
|
||||
openAIAdditionalHeadersOn: false,
|
||||
});
|
||||
|
||||
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -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())),
|
||||
}),
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue