@sap-ai-sdk/orchestration
Version:
SAP Cloud SDK for AI is the official Software Development Kit (SDK) for **SAP AI Core**, **SAP Generative AI Hub**, and **Orchestration Service**.
323 lines • 13 kB
JavaScript
import { createLogger } from '@sap-cloud-sdk/util';
const logger = createLogger({
package: 'orchestration',
messageContext: 'orchestration-utils'
});
/**
* @internal
*/
export function constructCompletionPostRequestFromJsonModuleConfig(config, prompt, stream) {
if (stream) {
config = {
...config,
stream: {
...(config.stream || {}),
enabled: true
}
};
}
else {
delete config.stream;
}
return {
messages_history: prompt?.messagesHistory || [],
placeholder_values: prompt?.placeholderValues || {},
config
};
}
/**
* @internal
*/
export function constructCompletionPostRequestFromConfigReference(configRef, request) {
// Route request.messages into messages_history since there is no local
// prompt.template to merge them into for config references.
const messagesHistory = request?.messages?.length
? [...(request.messagesHistory || []), ...request.messages]
: request?.messagesHistory;
return {
config_ref: configRef,
...(request?.placeholderValues && {
placeholder_values: request.placeholderValues
}),
...(messagesHistory && {
messages_history: messagesHistory
})
};
}
/**
* @internal
*/
export function addStreamOptionsToPromptTemplatingModuleConfig(promptTemplatingModuleConfig, streamOptions) {
if (streamOptions?.promptTemplating === null) {
return promptTemplatingModuleConfig;
}
return {
...promptTemplatingModuleConfig,
model: {
...promptTemplatingModuleConfig.model,
params: {
...(promptTemplatingModuleConfig.model.params || {}),
...(streamOptions?.promptTemplating !== null && {
stream_options: {
include_usage: true,
...(promptTemplatingModuleConfig.model.params?.stream_options ||
{}),
...(streamOptions?.promptTemplating || {})
}
})
}
}
};
}
/**
* @internal
*/
export function addStreamOptionsToOutputFilteringConfig(outputFilteringConfig, filteringStreamOptions) {
return {
...outputFilteringConfig,
stream_options: {
...(outputFilteringConfig.stream_options || {}),
...filteringStreamOptions
}
};
}
function warnAboutUnusedOverrides(streamOptions, configurationCount) {
const overrideKeys = Object.keys(streamOptions?.overrides || []);
if (!overrideKeys.length) {
return;
}
const unusedOverrides = overrideKeys.filter(key => {
const index = parseFloat(key);
return !Number.isInteger(index) || index < 0 || index >= configurationCount;
});
if (unusedOverrides.length) {
logger.debug(`The following override keys do not correspond to any module configuration and will be ignored: ${unusedOverrides.join(', ')}.`);
}
}
function warnAboutShortOverridesArray(streamOptions, configurationCount) {
if (!Array.isArray(streamOptions?.overrides) ||
!streamOptions.overrides.length) {
return;
}
const arrayLength = streamOptions.overrides.length;
logger.debug(`Override array has ${arrayLength} element(s) but there are ${configurationCount} module configuration(s). ` +
`Configs at indices ${arrayLength}-${configurationCount - 1} will use shared options. ` +
'If this is intentional, use object input to silence this warning: `{...streamOptionsArray}`');
}
/**
* Gets stream options for a specific module configuration.
* Returns the override for the given index if it exists, otherwise returns shared options.
* @param streamOptions - The stream options containing shared settings and optional overrides.
* @param index - The index of the module configuration.
* @returns Override options for this index if present, otherwise shared options, or undefined if streamOptions is not provided.
*/
function getStreamOptionsForItem(streamOptions, index) {
if (!streamOptions) {
return undefined;
}
const { global: _global, overrides, ...shared } = streamOptions;
return (overrides && overrides[index]) || shared;
}
/**
* @internal
* Warns if output filtering stream options are provided but some configs lack output filtering.
* Checks both shared options and per-config overrides.
* @param moduleConfigs - Single or array of module configurations.
* @param streamOptions - Stream options (shared settings and optional per-config overrides).
*/
function warnAboutMissingOutputFiltering(moduleConfigs, streamOptions) {
const configs = Array.isArray(moduleConfigs)
? moduleConfigs
: [moduleConfigs];
// Collect indices where output filtering options are set but config lacks output filtering
const configsWithoutFilter = configs
.map((cfg, idx) => {
const opts = getStreamOptionsForItem(streamOptions, idx);
// Only flag if stream options request output filtering but config doesn't have it
if (opts?.outputFiltering && !cfg.filtering?.output) {
return idx;
}
return undefined;
})
.filter(idx => idx !== undefined);
if (configsWithoutFilter.length === 0) {
return;
}
// Three scenarios:
// 1. All configs with output filtering options lack output filtering - warn that options are unused
// 2. Some configs lack output filtering - warn about specific configs affected
// 3. All configs have output filtering - no warning needed
if (configsWithoutFilter.length === configs.length) {
logger.warn('Output filter stream options are not applied because no module configuration has output filtering enabled.');
}
else if (configsWithoutFilter.length > 0) {
const configWord = configsWithoutFilter.length > 1 ? 'configurations' : 'configuration';
const positions = configsWithoutFilter.map(i => `#${i + 1}`).join(', ');
logger.warn(`Output filter stream options will not be applied to ${configWord} ${positions} because output filtering is not configured for those modules.`);
}
}
/**
* @internal
* Adds stream options to module configurations for streaming requests.
*/
function buildModules(configs, options) {
return configs.map((config, index) => {
const itemStreamOptions = getStreamOptionsForItem(options, index);
return addStreamOptionsToSingleModuleConfig(config, itemStreamOptions);
});
}
/**
* @internal
* Adds stream options to module configurations and returns an orchestration config for streaming requests.
* Validates overrides and warns about unused or invalid override indices.
* @param moduleConfigs - Single or array of module configurations.
* @param streamOptions - Stream options with optional per-config overrides.
* @returns Orchestration config with stream enabled and processed modules.
*/
export function addStreamOptions(moduleConfigs, streamOptions) {
const configs = Array.isArray(moduleConfigs)
? moduleConfigs
: [moduleConfigs];
if (!Array.isArray(moduleConfigs) && streamOptions?.overrides) {
throw new Error('Overrides in stream options are not supported when a single module configuration is provided.');
}
warnAboutUnusedOverrides(streamOptions, configs.length);
warnAboutShortOverridesArray(streamOptions, configs.length);
warnAboutMissingOutputFiltering(moduleConfigs, streamOptions);
const modules = buildModules(configs, streamOptions);
return {
stream: {
enabled: true,
...(streamOptions?.global || {})
},
modules: Array.isArray(moduleConfigs) ? modules : modules[0]
};
}
function addStreamOptionsToSingleModuleConfig(moduleConfigs, streamOptions) {
const { prompt_templating, filtering } = moduleConfigs;
const outputFiltering = streamOptions?.outputFiltering;
return {
...moduleConfigs,
prompt_templating: addStreamOptionsToPromptTemplatingModuleConfig(prompt_templating, streamOptions),
...(outputFiltering &&
filtering?.output && {
filtering: {
...filtering,
output: addStreamOptionsToOutputFilteringConfig(filtering.output, outputFiltering)
}
})
};
}
/** @internal */
export function constructCompletionPostRequest(config, request, stream, streamOptions) {
// Preserve format: single config → ModuleConfigs, array → ModuleConfigs[]
// The orchestration service expects the config structure to match the input:
// - Single config (OrchestrationModuleConfig) → single ModuleConfigs object
// - Config array (OrchestrationModuleConfigList) → array of ModuleConfigs for fallback behavior
// When any config uses a TemplateRef, messages cannot be merged into prompt.template
// (the template lives remotely). Route them to messages_history instead.
const configs = Array.isArray(config) ? config : [config];
const routeMessagesToHistory = configs.some(c => isTemplateRef(c?.promptTemplating?.prompt || {}));
const moduleRequest = routeMessagesToHistory && request
? { ...request, messages: undefined }
: request;
/**
* Module configurations for the orchestration request.
*/
const moduleConfigurations = Array.isArray(config)
? config.map(c => buildCompletionModulesConfig(c, moduleRequest))
: buildCompletionModulesConfig(config, moduleRequest);
/**
* Orchestration configuration with or without streaming enabled.
*/
const configWithStream = stream
? Array.isArray(moduleConfigurations)
? addStreamOptions(moduleConfigurations, streamOptions)
: addStreamOptions(moduleConfigurations, streamOptions)
: { modules: moduleConfigurations };
// When routing messages to history, append request.messages after messagesHistory
const messagesHistory = routeMessagesToHistory && request?.messages?.length
? [...(request.messagesHistory || []), ...request.messages]
: request?.messagesHistory;
return {
config: configWithStream,
...(request?.placeholderValues && {
placeholder_values: request.placeholderValues
}),
...(messagesHistory && {
messages_history: messagesHistory
})
};
}
function buildCompletionModulesConfig(config, request) {
const { promptTemplating, filtering, masking, grounding, translation } = config;
// prompt is not a string here as it is already parsed in `parseAndMergeTemplating` method
const prompt = {
...promptTemplating.prompt
};
// If promptTemplating.prompt is not defined, we initialize it with an empty Template object
promptTemplating.prompt = promptTemplating.prompt || { template: [] };
if (isTemplate(prompt)) {
if (!prompt.template?.length && !request?.messages?.length) {
throw new Error('Either a prompt template or messages must be defined.');
}
prompt.template = [
...(prompt.template || []),
...(request?.messages || [])
];
}
return {
prompt_templating: {
...promptTemplating,
prompt
},
...(filtering && Object.keys(filtering).length && { filtering }),
...(masking && Object.keys(masking).length && { masking }),
...(grounding && Object.keys(grounding).length && { grounding }),
...(translation && Object.keys(translation).length && { translation })
};
}
function isTemplate(templating) {
return (!!templating &&
typeof templating === 'object' &&
!('template_ref' in templating));
}
function isTemplateRef(templating) {
return (!!templating &&
typeof templating === 'object' &&
'template_ref' in templating);
}
/**
* Constructs an embedding post request from the given configuration and request.
* @internal
*/
export function constructEmbeddingPostRequest(config, request) {
const orchestrationConfig = {
modules: buildEmbeddingModulesConfig(config)
};
const embeddingRequest = {
config: orchestrationConfig,
input: {
text: request.input,
...(request.type && { type: request.type })
}
};
return embeddingRequest;
}
function buildEmbeddingModulesConfig(config) {
const { embeddings, masking } = config;
const { model } = embeddings;
const { name, version, params } = model;
const modules = {
embeddings: {
model: {
name,
...(version && { version }),
...(params && { params })
}
},
...(masking && Object.keys(masking).length && { masking })
};
return modules;
}
//# sourceMappingURL=module-config.js.map