Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@
* 2.0.
*/

import type { ISavedObjectsRepository, Logger, SavedObject } from '@kbn/core/server';
import type {
ISavedObjectsRepository,
IUiSettingsClient,
Logger,
SavedObject,
} from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import {
type InferenceConnector,
InferenceConnectorType,
defaultInferenceEndpoints,
} from '@kbn/inference-common';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import {
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR,
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY,
} from '@kbn/management-settings-ids';
import type { InferenceFeatureConfig } from './types';
import type { InferenceSettingsAttributes } from '../common/types';
import { InferenceFeatureRegistry } from './inference_feature_registry';
import { getForFeature } from './inference_endpoints';
import { getForFeature, getForFeatureWithDefault } from './inference_endpoints';

const createValidFeature = (
overrides: Partial<InferenceFeatureConfig> = {}
Expand Down Expand Up @@ -454,3 +463,200 @@ describe('getForFeature', () => {
});
});
});

interface UiSettings {
defaultConnectorId?: string;
defaultConnectorOnly?: boolean;
}

const createUiSettingsClient = ({
defaultConnectorId,
defaultConnectorOnly,
}: UiSettings = {}): IUiSettingsClient =>
({
get: jest.fn(async (key: string) => {
if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR) return defaultConnectorId;
if (key === GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY)
return defaultConnectorOnly ?? false;
return undefined;
}),
} as unknown as IUiSettingsClient);

describe('getForFeatureWithDefault', () => {
let registry: InferenceFeatureRegistry;
let logger: Logger;

beforeEach(() => {
logger = loggingSystemMock.createLogger();
registry = new InferenceFeatureRegistry(logger);
});

it('returns only the default connector when defaultConnectorOnly is set', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({
defaultConnectorId: 'default-id',
defaultConnectorOnly: true,
}),
getConnectorById: createGetConnectorById(['default-id', 'rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('default-id')],
warnings: [],
soEntryFound: false,
});
});

it('returns an empty list when defaultConnectorOnly is set but no default is configured', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({ defaultConnectorOnly: true }),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({ endpoints: [], warnings: [], soEntryFound: false });
});

it('returns an empty list when defaultConnectorOnly is set with the NO_DEFAULT_CONNECTOR sentinel', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({
defaultConnectorId: 'NO_DEFAULT_CONNECTOR',
defaultConnectorOnly: true,
}),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({ endpoints: [], warnings: [], soEntryFound: false });
});

it('returns an empty list when defaultConnectorOnly is set but the default connector lookup fails', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({
defaultConnectorId: 'missing',
defaultConnectorOnly: true,
}),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({ endpoints: [], warnings: [], soEntryFound: false });
});

it('prepends the default connector when no SO entry is found', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({ defaultConnectorId: 'default-id' }),
getConnectorById: createGetConnectorById(['default-id', 'rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('default-id'), createConnector('rec1')],
warnings: [],
soEntryFound: false,
});
});

it('deduplicates the default connector when already present in the resolved endpoints', async () => {
registry.register(
createValidFeature({ featureId: 'f1', recommendedEndpoints: ['default-id'] })
);
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({ defaultConnectorId: 'default-id' }),
getConnectorById: createGetConnectorById(['default-id']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('default-id')],
warnings: [],
soEntryFound: false,
});
});

it('ignores the default connector when an SO entry was found', async () => {
registry.register(createValidFeature({ featureId: 'f1' }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient([{ feature_id: 'f1', endpoints: [{ id: 'so_ep' }] }]),
uiSettingsClient: createUiSettingsClient({ defaultConnectorId: 'default-id' }),
getConnectorById: createGetConnectorById(['default-id', 'so_ep']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('so_ep')],
warnings: [],
soEntryFound: true,
});
});

it('ignores the NO_DEFAULT_CONNECTOR sentinel when resolving feature endpoints', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({ defaultConnectorId: 'NO_DEFAULT_CONNECTOR' }),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('rec1')],
warnings: [],
soEntryFound: false,
});
});

it('returns the feature endpoints unchanged when the default connector lookup fails', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient({ defaultConnectorId: 'missing' }),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('rec1')],
warnings: [],
soEntryFound: false,
});
});

it('returns the feature endpoints when no default is configured', async () => {
registry.register(createValidFeature({ featureId: 'f1', recommendedEndpoints: ['rec1'] }));
const result = await getForFeatureWithDefault({
registry,
soClient: createSoClient(),
uiSettingsClient: createUiSettingsClient(),
getConnectorById: createGetConnectorById(['rec1']),
featureId: 'f1',
logger,
});
expect(result).toEqual({
endpoints: [createConnector('rec1')],
warnings: [],
soEntryFound: false,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
* 2.0.
*/

import type { ISavedObjectsRepository, Logger } from '@kbn/core/server';
import type { ISavedObjectsRepository, IUiSettingsClient, Logger } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { type InferenceConnector, defaultInferenceEndpoints } from '@kbn/inference-common';
import {
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR,
GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY,
} from '@kbn/management-settings-ids';
import { INFERENCE_SETTINGS_SO_TYPE, INFERENCE_SETTINGS_ID } from '../common/constants';
import type { InferenceSettingsAttributes } from '../common/types';
import type { InferenceFeatureRegistry } from './inference_feature_registry';
import type { ResolvedInferenceEndpoints } from './types';

const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR';

/**
* Returns the resolved inference endpoints for a feature.
* Walks the fallback chain (admin SO override → recommendedEndpoints → parent feature)
Expand Down Expand Up @@ -48,6 +54,83 @@ export const getForFeature = async (
};
};

/**
* Resolves endpoints for a feature and layers on the global default AI connector
* configured via advanced settings (`GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR` and
* `GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY`).
*
* - When `defaultOnly` is set, only the default connector is returned (or an empty list).
* - When the feature has no admin-configured SO override, the default connector is
* prepended to the resolved endpoints.
* - When an SO override exists for the feature, the default is ignored.
*
* Kept in sync with the HTTP route at `GET /internal/search_inference_endpoints/connectors`.
*/
export const getForFeatureWithDefault = async ({
registry,
soClient,
uiSettingsClient,
getConnectorById,
featureId,
logger,
}: {
registry: InferenceFeatureRegistry;
soClient: ISavedObjectsRepository;
uiSettingsClient: IUiSettingsClient;
getConnectorById: (id: string) => Promise<InferenceConnector>;
featureId: string;
logger: Logger;
}): Promise<ResolvedInferenceEndpoints> => {
const [defaultConnectorId, defaultConnectorOnly] = await Promise.all([
uiSettingsClient.get<string>(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR),
uiSettingsClient.get<boolean>(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY),
]);

const hasDefault =
typeof defaultConnectorId === 'string' &&
defaultConnectorId.length > 0 &&
defaultConnectorId !== NO_DEFAULT_CONNECTOR;

const fetchDefault = async (): Promise<InferenceConnector | undefined> => {
if (!hasDefault) return undefined;
try {
return await getConnectorById(defaultConnectorId);
} catch (e) {
logger.warn(`Failed to load default connector "${defaultConnectorId}": ${e.message}`);
return undefined;
}
};

if (defaultConnectorOnly) {
const defaultConnector = await fetchDefault();
return {
endpoints: defaultConnector ? [defaultConnector] : [],
warnings: [],
soEntryFound: false,
};
}

const result = await getForFeature(registry, soClient, getConnectorById, featureId, logger);

if (result.soEntryFound || !hasDefault) {
return result;
}

const defaultConnector = await fetchDefault();
if (!defaultConnector) {
return result;
}

return {
endpoints: [
defaultConnector,
...result.endpoints.filter((c) => c.connectorId !== defaultConnector.connectorId),
],
warnings: result.warnings,
soEntryFound: false,
};
};

/**
* Fetches connectors by their IDs using getConnectorById.
* Returns warnings for any IDs that were not found.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import type { SearchInferenceEndpointsConfig } from './config';
import { DynamicConnectorsPoller } from './lib/dynamic_connectors';
import { defineRoutes } from './routes';
import { InferenceFeatureRegistry } from './inference_feature_registry';
import { getForFeature as getForFeatureFn } from './inference_endpoints';
import {
getForFeature as getForFeatureFn,
getForFeatureWithDefault as getForFeatureWithDefaultFn,
} from './inference_endpoints';
import { createInferenceSettingsSavedObjectType } from './saved_objects/inference_settings';
import type {
SearchInferenceEndpointsPluginSetup,
Expand Down Expand Up @@ -179,14 +182,18 @@ export class SearchInferenceEndpointsPlugin
endpoints: {
getForFeature: (featureId: string, request: KibanaRequest) => {
const soClient = core.savedObjects.createInternalRepository([INFERENCE_SETTINGS_SO_TYPE]);
const uiSettingsClient = core.uiSettings.asScopedToClient(
core.savedObjects.getScopedClient(request)
);
const getConnectorById = (id: string) => plugins.inference.getConnectorById(id, request);
return getForFeatureFn(
featureRegistry,
return getForFeatureWithDefaultFn({
registry: featureRegistry,
soClient,
uiSettingsClient,
getConnectorById,
featureId,
this.logger
);
logger: this.logger,
});
},
},
};
Expand Down
Loading