@allma/core-cdk
Version:
Core AWS CDK constructs for deploying the Allma serverless AI orchestration platform.
211 lines • 9.86 kB
JavaScript
import { BedrockRuntimeClient, InvokeModelCommand } from '@aws-sdk/client-bedrock-runtime';
// For a list of supported models and their API structures, see:
// https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html
import { LLMProviderType, PermanentStepError, TransientStepError } from '@allma/core-types';
import { log_info, log_debug, log_error } from '@allma/core-sdk';
/**
* Adapter for invoking AI models through AWS Bedrock.
* This adapter is designed to be extensible to support various model providers
* available on Bedrock (e.g., Anthropic, Cohere, Amazon) by inspecting the model ID.
*/
export class BedrockAdapter {
client;
constructor() {
this.client = new BedrockRuntimeClient({});
log_info('AWS Bedrock Adapter initialized.', {});
}
/**
* Determines the underlying model provider from the Bedrock model ID string.
* @param modelId - The Bedrock model ID (e.g., 'anthropic.claude-3-sonnet-20240229-v1:0').
* @returns The provider name (e.g., 'anthropic').
*/
getProviderFromModelId(modelId) {
const globalPrefix = 'global.';
let providerId = modelId;
if (modelId.startsWith(globalPrefix)) {
// If it's a global model, strip the prefix to identify the provider family (e.g., 'anthropic').
providerId = modelId.substring(globalPrefix.length);
}
return providerId.split('.')[0];
}
/**
* Builds the JSON payload for an Anthropic Claude model on Bedrock.
* @param request - The standardized LlmGenerationRequest.
* @returns A stringified JSON payload.
*/
buildAnthropicPayload(request) {
const { prompt, maxOutputTokens, temperature, topP, topK, stopSequences } = request;
const anthropicVersion = request.customConfig?.anthropic_version || 'bedrock-2023-05-31';
const payload = {
anthropic_version: anthropicVersion,
messages: [{ role: 'user', content: prompt }],
max_tokens: maxOutputTokens ?? 4096,
temperature: temperature ?? 0.7,
top_p: topP ?? 1.0,
top_k: topK ?? 250,
stop_sequences: stopSequences,
};
return JSON.stringify(payload);
}
/**
* Parses the response from an Anthropic Claude model on Bedrock.
* @param responseBody - The parsed JSON object from the Bedrock API response.
* @returns An object with the extracted responseText and tokenUsage.
*/
parseAnthropicResponse(responseBody) {
const responseText = responseBody.content?.[0]?.text ?? '';
const tokenUsage = {
inputTokens: responseBody.usage?.input_tokens ?? 0,
outputTokens: responseBody.usage?.output_tokens ?? 0,
};
return { responseText, tokenUsage };
}
/**
* Builds the JSON payload for an Amazon Titan Text ("Nova") model on Bedrock.
* This uses the modern conversational `messages` format.
* @param request - The standardized LlmGenerationRequest.
* @returns A stringified JSON payload. [1, 2]
*/
buildAmazonPayload(request) {
const { prompt, maxOutputTokens, temperature, topP, stopSequences, seed } = request;
const payload = {
// The top-level structure now requires a 'messages' array.
messages: [{
role: 'user',
content: [{ text: prompt }]
}],
// Inference parameters are nested under 'inferenceConfiguration'.
inferenceConfig: {
maxTokens: maxOutputTokens ?? 8192,
stopSequences: stopSequences ?? [],
temperature: temperature ?? 0.5,
topP: topP ?? 0.9,
...(seed !== undefined && { seed: seed }),
},
};
return JSON.stringify(payload);
}
/**
* Parses the response from an Amazon Titan Text ("Nova") model on Bedrock.
* This handles the modern conversational response format.
* @param responseBody - The parsed JSON object from the Bedrock API response.
* @returns An object with the extracted responseText and tokenUsage. [1, 2]
*/
parseAmazonResponse(responseBody) {
// The response text is nested inside the 'output' object.
const responseText = responseBody.output?.message?.content?.[0]?.text ?? '';
// Token metrics are in the top-level 'usage' object.
const tokenUsage = {
inputTokens: responseBody.usage?.inputTokens ?? 0,
outputTokens: responseBody.usage?.outputTokens ?? 0,
};
return { responseText, tokenUsage };
}
/**
* Builds the JSON payload for an OpenAI model on Bedrock.
* @param request - The standardized LlmGenerationRequest.
* @returns A stringified JSON payload.
*/
buildOpenaiPayload(request) {
const { prompt, maxOutputTokens, temperature, topP, stopSequences } = request;
const payload = {
messages: [{ role: 'user', content: prompt }],
max_tokens: maxOutputTokens ?? 4096,
temperature: temperature ?? 0.7,
top_p: topP ?? 1.0,
stop: stopSequences,
};
return JSON.stringify(payload);
}
/**
* Parses the response from an OpenAI model on Bedrock.
* @param responseBody - The parsed JSON object from the Bedrock API response.
* @returns An object with the extracted responseText and tokenUsage.
*/
parseOpenaiResponse(responseBody) {
const responseText = responseBody.choices?.[0]?.message?.content ?? '';
const tokenUsage = {
inputTokens: responseBody.usage?.prompt_tokens ?? 0,
outputTokens: responseBody.usage?.completion_tokens ?? 0,
};
return { responseText, tokenUsage };
}
/**
* Generates content using a Bedrock model, adhering to the standardized LlmProviderAdapter interface.
* @param request - The standardized LlmGenerationRequest object.
* @returns A promise that resolves to a standardized LlmGenerationResponse object.
*/
async generateContent(request) {
const { modelId, correlationId } = request;
const provider = this.getProviderFromModelId(modelId);
log_info(`BedrockAdapter: Requesting content from model`, { modelId, provider }, correlationId);
let body;
let responseParser;
try {
switch (provider) {
case 'anthropic':
body = this.buildAnthropicPayload(request);
responseParser = this.parseAnthropicResponse;
break;
case 'amazon':
body = this.buildAmazonPayload(request);
responseParser = this.parseAmazonResponse;
break;
case 'openai':
body = this.buildOpenaiPayload(request);
responseParser = this.parseOpenaiResponse;
break;
// To add a new provider (e.g., Cohere):
// case 'cohere':
// body = this.buildCoherePayload(request);
// responseParser = this.parseCohereResponse;
// break;
default:
throw new PermanentStepError(`Bedrock model provider '${provider}' derived from modelId '${modelId}' is not supported by the adapter.`);
}
}
catch (error) {
log_error('Failed to build payload for Bedrock model.', { error: error.message }, correlationId);
return {
success: false, provider: LLMProviderType.AWS_BEDROCK, modelUsed: modelId, responseText: null,
errorMessage: `Payload construction error: ${error.message}`,
};
}
const command = new InvokeModelCommand({
modelId, body,
contentType: 'application/json',
accept: 'application/json',
});
try {
const apiResponse = await this.client.send(command);
const responseBody = JSON.parse(new TextDecoder().decode(apiResponse.body));
log_debug('Received raw response from Bedrock', { responseBody }, correlationId);
const { responseText, tokenUsage } = responseParser(responseBody);
return {
success: true, provider: LLMProviderType.AWS_BEDROCK, modelUsed: modelId, responseText, tokenUsage,
};
}
catch (error) {
log_error('Error invoking Bedrock model', { modelId, errorName: error.name, errorMessage: error.message }, correlationId);
let finalError;
switch (error.name) {
case 'AccessDeniedException':
case 'ValidationException':
case 'ResourceNotFoundException':
finalError = new PermanentStepError(`Bedrock configuration or permission error: ${error.message}`, { errorDetails: { name: error.name } }, error);
break;
case 'ThrottlingException':
case 'ServiceQuotaExceededException':
case 'InternalServerException':
case 'ModelTimeoutException':
finalError = new TransientStepError(`Bedrock service error: ${error.message}`, { errorDetails: { name: error.name } }, error);
break;
default:
finalError = new TransientStepError(`Unknown Bedrock error: ${error.message}`, { errorDetails: { name: error.name } }, error);
}
// The handler will catch this and pass it up to the step processor for retry/failure logic.
throw finalError;
}
}
}
//# sourceMappingURL=bedrock-adapter.js.map