/**
 * AWS Bedrock model provider implementation.
 *
 * This module provides integration with AWS Bedrock's Converse API,
 * supporting streaming responses, tool use, and prompt caching.
 *
 * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
 */
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand, CountTokensCommand, DocumentFormat, ImageFormat, VideoFormat, } from '@aws-sdk/client-bedrock-runtime';
import { Model, resolveConfigMetadata, } from '../models/model.js';
import { ContextWindowOverflowError, ModelThrottledError, ProviderTokenCountError, normalizeError } from '../errors.js';
import { ensureDefined } from '../types/validation.js';
import { logger } from '../logging/logger.js';
import { warnOnce } from '../logging/warn-once.js';
import { NOOP_TOOL_SPEC } from '../tools/noop-tool.js';
import { MODEL_DEFAULTS, defaultModelWarningMessage } from './defaults.js';
const DEFAULT_BEDROCK_REGION_SUPPORTS_FIP = false;
/**
 * Default request timeout in milliseconds. The AWS SDK defaults to 0 (disabled), which lets
 * a stuck connection hang indefinitely — we pick 120s to bound that. Callers can override
 * via `clientConfig.requestHandler.requestTimeout`.
 */
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
/**
 * Models that require the status field in tool results.
 * According to AWS Bedrock API documentation, the status field is only supported by Anthropic Claude models.
 * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
 */
const MODELS_INCLUDE_STATUS = ['anthropic.claude'];
/**
 * Models that support the Anthropic-style prompt caching strategy.
 * Used to auto-detect when `cacheConfig.strategy` is `'auto'`.
 * @see https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
 */
const MODELS_SUPPORTING_ANTHROPIC_CACHING = ['anthropic', 'claude'];
/**
 * Error messages that indicate context window overflow.
 * Used to detect when input exceeds the model's context window.
 */
const BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES = [
    'Input is too long for requested model',
    'input length and `max_tokens` exceed context limit',
    'too many total text bytes',
    'prompt is too long',
];
/**
 * Cache of model IDs for which CountTokens API calls should be skipped.
 * Prevents repeated failing API calls that will never succeed for the lifetime of the process.
 */
const SKIP_COUNT_TOKENS_MODELS = new Set();
/**
 * Mapping of Bedrock stop reasons to SDK stop reasons.
 */
const STOP_REASON_MAP = {
    end_turn: 'endTurn',
    tool_use: 'toolUse',
    max_tokens: 'maxTokens',
    stop_sequence: 'stopSequence',
    content_filtered: 'contentFiltered',
    guardrail_intervened: 'guardrailIntervened',
};
/**
 * Default message for redacted input.
 */
const DEFAULT_REDACT_INPUT_MESSAGE = '[User input redacted.]';
/**
 * Default message for redacted output.
 */
const DEFAULT_REDACT_OUTPUT_MESSAGE = '[Assistant output redacted.]';
/**
 * Converts a snake_case string to camelCase.
 * Used for mapping unknown stop reasons from Bedrock to SDK format.
 *
 * @param str - Snake case string
 * @returns Camel case string
 */
function snakeToCamel(str) {
    return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
 * AWS Bedrock model provider implementation.
 *
 * Implements the Model interface for AWS Bedrock using the Converse Stream API.
 * Supports streaming responses, tool use, prompt caching, and comprehensive error handling.
 *
 * @example
 * ```typescript
 * const provider = new BedrockModel({
 *   modelConfig: {
 *     modelId: 'global.anthropic.claude-sonnet-4-6',
 *     maxTokens: 1024,
 *     temperature: 0.7
 *   },
 *   clientConfig: {
 *     region: 'us-west-2'
 *   }
 * })
 *
 * const messages: Message[] = [
 *   { type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello!' }] }
 * ]
 *
 * for await (const event of provider.stream(messages)) {
 *   if (event.type === 'modelContentBlockDeltaEvent' && event.delta.type === 'textDelta') {
 *     process.stdout.write(event.delta.text)
 *   }
 * }
 * ```
 */
export class BedrockModel extends Model {
    _config;
    _client;
    /**
     * Clears the cache of model IDs for which CountTokens is skipped.
     * After calling this, the next countTokens invocation will attempt the API again.
     *
     * @internal
     */
    static clearCountTokensCache() {
        SKIP_COUNT_TOKENS_MODELS.clear();
    }
    /**
     * Creates a new BedrockModel instance.
     *
     * @param options - Optional configuration for model and client
     *
     * @example
     * ```typescript
     * // Minimal configuration with defaults
     * const provider = new BedrockModel({
     *   region: 'us-west-2'
     * })
     *
     * // With model configuration
     * const provider = new BedrockModel({
     *   region: 'us-west-2',
     *   modelId: 'global.anthropic.claude-sonnet-4-6',
     *   maxTokens: 2048,
     *   temperature: 0.8,
     *   cacheConfig: { strategy: 'auto' }
     * })
     *
     * // With client configuration
     * const provider = new BedrockModel({
     *   region: 'us-east-1',
     *   clientConfig: {
     *     credentials: myCredentials
     *   }
     * })
     * ```
     */
    constructor(options) {
        super();
        const { region, clientConfig, apiKey, ...modelConfig } = options ?? {};
        // Initialize model config with default model ID if not provided
        this._config = {
            modelId: MODEL_DEFAULTS.bedrock.modelId,
            ...modelConfig,
        };
        if (modelConfig.modelId === undefined) {
            warnOnce(logger, defaultModelWarningMessage(MODEL_DEFAULTS.bedrock.modelId));
        }
        // Build user agent string (extend if provided, otherwise use SDK identifier)
        const customUserAgent = clientConfig?.customUserAgent
            ? `${clientConfig.customUserAgent} strands-agents-ts-sdk`
            : 'strands-agents-ts-sdk';
        this._client = new BedrockRuntimeClient({
            ...(clientConfig ?? {}),
            requestHandler: withDefaultRequestTimeout(clientConfig?.requestHandler),
            // region takes precedence over clientConfig
            ...(region ? { region: region } : {}),
            customUserAgent,
        });
        if (apiKey) {
            applyApiKey(this._client, apiKey);
        }
        applyDefaultRegion(this._client.config);
    }
    /**
     * Returns the cache strategy for this model based on its model ID.
     * Returns the appropriate cache strategy name, or null if automatic caching is not supported.
     *
     * @returns Cache strategy name or null
     */
    _getCacheStrategy() {
        return MODELS_SUPPORTING_ANTHROPIC_CACHING.some((pattern) => this._config.modelId?.includes(pattern))
            ? 'anthropic'
            : null;
    }
    /**
     * Determines if caching should be enabled.
     * Returns true when:
     * - strategy is 'anthropic' (explicit enable)
     * - strategy is 'auto' and model supports caching (auto-detect)
     *
     * @returns True if caching should be enabled
     */
    _shouldEnableCaching() {
        const cacheConfig = this._config.cacheConfig;
        if (!cacheConfig) {
            return false;
        }
        let strategy = cacheConfig.strategy;
        if (strategy === 'auto') {
            const detectedStrategy = this._getCacheStrategy();
            if (!detectedStrategy) {
                logger.warn(`model_id=<${this._config.modelId}> | cache_config is enabled but this model does not support automatic caching`);
                return false;
            }
            strategy = detectedStrategy;
        }
        return strategy === 'anthropic';
    }
    /**
     * Updates the model configuration.
     * Merges the provided configuration with existing settings.
     *
     * @param modelConfig - Configuration object with model-specific settings to update
     *
     * @example
     * ```typescript
     * // Update temperature and maxTokens
     * provider.updateConfig({
     *   temperature: 0.9,
     *   maxTokens: 2048
     * })
     * ```
     */
    updateConfig(modelConfig) {
        this._config = { ...this._config, ...modelConfig };
    }
    /**
     * Retrieves the current model configuration.
     *
     * @returns The current configuration object
     *
     * @example
     * ```typescript
     * const config = provider.getConfig()
     * console.log(config.modelId)
     * ```
     */
    getConfig() {
        return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.bedrock.modelId);
    }
    /**
     * Count tokens using Bedrock's native CountTokens API.
     *
     * Uses the same message format as the Converse API to get accurate token counts
     * directly from the Bedrock service. Falls back to the base class heuristic on failure.
     *
     * @param messages - Array of conversation messages to count tokens for
     * @param options - Optional options containing system prompt and tool specs
     * @returns Total input token count
     */
    async countTokens(messages, options) {
        if (this._config.useNativeTokenCount !== true)
            return super.countTokens(messages, options);
        const modelId = this._config.modelId ?? MODEL_DEFAULTS.bedrock.modelId;
        if (SKIP_COUNT_TOKENS_MODELS.has(modelId)) {
            return super.countTokens(messages, options);
        }
        try {
            const request = this._formatRequest(messages, options);
            const converseInput = {};
            if (request.messages)
                converseInput.messages = request.messages;
            if (request.system)
                converseInput.system = request.system;
            if (request.toolConfig)
                converseInput.toolConfig = request.toolConfig;
            const response = await this._client.send(new CountTokensCommand({
                modelId: this._config.modelId,
                input: { converse: converseInput },
            }));
            if (response.inputTokens == null) {
                throw new ProviderTokenCountError('Bedrock CountTokens returned undefined for inputTokens');
            }
            logger.debug(`total_tokens=<${response.inputTokens}> | native token count`);
            return response.inputTokens;
        }
        catch (error) {
            if (error instanceof Error && error.name === 'AccessDeniedException') {
                warnOnce(logger, `model_id=<${modelId}> | bedrock:CountTokens permission denied, falling back to heuristic estimation`);
                SKIP_COUNT_TOKENS_MODELS.add(modelId);
            }
            else if (error instanceof Error &&
                error.name === 'ValidationException' &&
                error.message.includes("doesn't support counting tokens")) {
                logger.debug(`model_id=<${modelId}> | model does not support CountTokens, caching for future calls, falling back to estimation`);
                SKIP_COUNT_TOKENS_MODELS.add(modelId);
            }
            else {
                logger.debug(`error=<${error}> | native token counting failed, falling back to estimation`);
            }
            return super.countTokens(messages, options);
        }
    }
    /**
     * Streams a conversation with the Bedrock model.
     * Returns an async iterable that yields streaming events as they occur.
     *
     * @param messages - Array of conversation messages
     * @param options - Optional streaming configuration
     * @returns Async iterable of streaming events
     *
     * @throws \{ContextWindowOverflowError\} When input exceeds the model's context window
     * @throws \{ModelThrottledError\} When Bedrock service throttles requests
     *
     * @example
     * ```typescript
     * const messages: Message[] = [
     *   { type: 'message', role: $1, content: [{ type: 'textBlock', text: 'What is 2+2?' }] }
     * ]
     *
     * const options: StreamOptions = {
     *   systemPrompt: 'You are a helpful math assistant.',
     *   toolSpecs: [calculatorTool]
     * }
     *
     * for await (const event of provider.stream(messages, options)) {
     *   if (event.type === 'modelContentBlockDeltaEvent') {
     *     console.log(event.delta)
     *   }
     * }
     * ```
     */
    async *stream(messages, options) {
        try {
            // Format the request for Bedrock
            const request = this._formatRequest(messages, options);
            if (this._config.stream !== false) {
                // Create and send the command
                const command = new ConverseStreamCommand(request);
                const response = await this._client.send(command);
                // Stream the response
                if (response.stream) {
                    let lastStopReason;
                    for await (const chunk of response.stream) {
                        // Map Bedrock events to SDK events
                        const result = this._mapStreamedBedrockEventToSDKEvent(chunk, lastStopReason);
                        lastStopReason = result.stopReason;
                        for (const event of result.events) {
                            yield event;
                        }
                    }
                }
            }
            else {
                const command = new ConverseCommand(request);
                const response = await this._client.send(command);
                for (const event of this._mapBedrockEventToSDKEvent(response)) {
                    yield event;
                }
            }
        }
        catch (unknownError) {
            const error = normalizeError(unknownError);
            // Check for context window overflow
            if (BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES.some((msg) => error.message.includes(msg))) {
                throw new ContextWindowOverflowError(error.message);
            }
            // Re-throw other errors as-is
            throw error;
        }
    }
    /**
     * Formats a request for the Bedrock Converse Stream API.
     *
     * @param messages - Conversation messages
     * @param options - Stream options
     * @returns Formatted Bedrock request
     */
    _formatRequest(messages, options) {
        const request = {
            modelId: this._config.modelId,
            messages: this._formatMessages(messages),
        };
        // Add system prompt
        if (options?.systemPrompt !== undefined) {
            if (typeof options.systemPrompt === 'string') {
                request.system = [{ text: options.systemPrompt }];
            }
            else if (options.systemPrompt.length > 0) {
                request.system = options.systemPrompt.map((block) => this._formatContentBlock(block));
            }
        }
        // Add tool configuration
        // Bedrock requires toolConfig when messages contain tool use/result blocks.
        // When no tools were provided but messages reference past tool usage (e.g. during
        // summarization), inject a noop tool to satisfy the API requirement.
        let toolSpecs = options?.toolSpecs ?? [];
        if (toolSpecs.length === 0) {
            const hasToolBlocks = messages.some((msg) => msg.content.some((block) => block.type === 'toolUseBlock' || block.type === 'toolResultBlock'));
            if (hasToolBlocks) {
                toolSpecs = [NOOP_TOOL_SPEC];
            }
        }
        if (toolSpecs.length > 0) {
            const tools = toolSpecs.map((spec) => ({
                toolSpec: {
                    name: spec.name,
                    description: spec.description,
                    inputSchema: { json: spec.inputSchema },
                },
            }));
            if (this._shouldEnableCaching()) {
                const cachePoint = { type: 'default' };
                const ttl = this._config.cacheConfig?.toolsTTL;
                if (ttl !== undefined) {
                    // Bedrock validates TTL values server-side, so accept any string here.
                    cachePoint.ttl = ttl;
                }
                tools.push({ cachePoint });
            }
            const toolConfig = {
                tools: tools,
            };
            if (options?.toolChoice) {
                toolConfig.toolChoice = options.toolChoice;
            }
            request.toolConfig = toolConfig;
        }
        // Add inference configuration
        const inferenceConfig = {};
        if (this._config.maxTokens !== undefined)
            inferenceConfig.maxTokens = this._config.maxTokens;
        if (this._config.temperature !== undefined)
            inferenceConfig.temperature = this._config.temperature;
        if (this._config.topP !== undefined)
            inferenceConfig.topP = this._config.topP;
        if (this._config.stopSequences !== undefined)
            inferenceConfig.stopSequences = this._config.stopSequences;
        if (Object.keys(inferenceConfig).length > 0) {
            request.inferenceConfig = inferenceConfig;
        }
        // Add additional request fields
        const additionalRequestFields = this._getAdditionalRequestFields(options);
        if (additionalRequestFields) {
            request.additionalModelRequestFields = additionalRequestFields;
        }
        // Add additional response field paths
        if (this._config.additionalResponseFieldPaths) {
            request.additionalModelResponseFieldPaths = this._config.additionalResponseFieldPaths;
        }
        // Add additional args (spread them into the request for forward compatibility)
        if (this._config.additionalArgs) {
            Object.assign(request, this._config.additionalArgs);
        }
        // Add guardrail configuration
        if (this._config.guardrailConfig) {
            request.guardrailConfig = {
                guardrailIdentifier: this._config.guardrailConfig.guardrailIdentifier,
                guardrailVersion: this._config.guardrailConfig.guardrailVersion,
                trace: this._config.guardrailConfig.trace ?? 'enabled',
                ...(this._config.guardrailConfig.streamProcessingMode && {
                    streamProcessingMode: this._config.guardrailConfig.streamProcessingMode,
                }),
            };
        }
        return request;
    }
    /**
     * Get additional request fields, adjusted for compatibility with the current stream options.
     *
     * Certain additional request fields are incompatible with specific API options. For example,
     * Bedrock does not allow thinking mode when tool_choice forces tool use.
     *
     * @param options - The stream options for the current request
     * @returns The additional request fields, or undefined if none
     */
    _getAdditionalRequestFields(options) {
        const fields = this._config.additionalRequestFields;
        if (!fields || !('thinking' in fields)) {
            return fields;
        }
        const toolChoice = options?.toolChoice;
        if (!toolChoice || 'auto' in toolChoice) {
            return fields;
        }
        const { thinking: _, ...rest } = fields;
        return Object.keys(rest).length > 0 ? rest : undefined;
    }
    /**
     * Formats messages for Bedrock API.
     *
     * @param messages - SDK messages
     * @returns Bedrock-formatted messages
     */
    _formatMessages(messages) {
        // Pre-compute the index of the last user message containing text/image content
        // This ensures guardContent wrapping is maintained across tool execution cycles
        const lastUserTextIdx = this._config.guardrailConfig?.guardLatestUserMessage
            ? this._findLastUserTextMessageIndex(messages)
            : undefined;
        const formattedMessages = messages.reduce((acc, message, idx) => {
            const shouldApplyGuardBlocks = idx === lastUserTextIdx;
            const content = message.content
                .map((block) => {
                const formattedBlock = this._formatContentBlock(block);
                return shouldApplyGuardBlocks ? this._applyGuardBlocks(formattedBlock) : formattedBlock;
            })
                .filter((block) => block !== undefined);
            if (content.length > 0) {
                acc.push({ role: message.role, content });
            }
            return acc;
        }, []);
        // Inject cache point if caching is enabled
        if (this._shouldEnableCaching()) {
            this._injectCachePoint(formattedMessages);
        }
        return formattedMessages;
    }
    /**
     * Inject a cache point at the end of the last user message.
     * Strips any existing cache points from all messages first.
     *
     * @param messages - List of messages to inject cache point into (modified in place)
     */
    _injectCachePoint(messages) {
        if (messages.length === 0) {
            return;
        }
        let lastUserIdx = null;
        // Strip existing cache points and find last user message
        for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
            const msg = messages[msgIdx];
            if (!msg)
                continue;
            const content = msg.content ?? [];
            for (let blockIdx = content.length - 1; blockIdx >= 0; blockIdx--) {
                const block = content[blockIdx];
                if (block && 'cachePoint' in block) {
                    content.splice(blockIdx, 1);
                    logger.warn(`msg_idx=<${msgIdx}>, block_idx=<${blockIdx}> | stripped existing cache point (auto mode manages cache points)`);
                }
            }
            if (msg.role === 'user') {
                lastUserIdx = msgIdx;
            }
        }
        // Add cache point to last user message
        if (lastUserIdx !== null) {
            const lastMsg = messages[lastUserIdx];
            if (lastMsg && lastMsg.content) {
                const cachePoint = { type: 'default' };
                const ttl = this._config.cacheConfig?.messagesTTL;
                if (ttl !== undefined) {
                    // Bedrock validates TTL values server-side, so accept any string here.
                    cachePoint.ttl = ttl;
                }
                lastMsg.content.push({ cachePoint });
                logger.debug(`msg_idx=<${lastUserIdx}> | added cache point to last user message`);
            }
        }
    }
    /**
     * Wraps a formatted content block in guardContent for guardrail evaluation.
     *
     * When guardLatestUserMessage is enabled, this method wraps text and image blocks
     * in guardContent blocks to signal to Bedrock's guardrails to evaluate only that content.
     * Other content types (toolUse, toolResult, etc.) pass through unchanged.
     *
     * @param formattedBlock - The formatted content block to potentially wrap
     * @returns The block wrapped in guardContent if applicable, or the original block
     */
    _applyGuardBlocks(formattedBlock) {
        if (formattedBlock === undefined) {
            return undefined;
        }
        if ('text' in formattedBlock) {
            return {
                guardContent: {
                    text: {
                        text: formattedBlock.text,
                    },
                },
            };
        }
        if ('image' in formattedBlock) {
            // Extract image data and validate for guardContent compatibility
            const imageBlock = formattedBlock.image;
            if (!imageBlock?.format || !imageBlock?.source) {
                return formattedBlock;
            }
            const format = imageBlock.format;
            // Bedrock guardrails only support png/jpeg formats
            if (format !== 'png' && format !== 'jpeg') {
                logger.warn(`image_format=<${format}> | format not supported by bedrock guardrails | skipping guardContent wrap`);
                return formattedBlock;
            }
            // Bedrock guardrails only support bytes source (not S3 or URL)
            if (!('bytes' in imageBlock.source)) {
                logger.warn('source_type=<non-bytes> | image source must be bytes for bedrock guardrails | skipping guardContent wrap');
                return formattedBlock;
            }
            return {
                guardContent: {
                    image: {
                        format: format,
                        source: imageBlock.source,
                    },
                },
            };
        }
        // Other content types (toolUse, toolResult, etc.) pass through unchanged
        return formattedBlock;
    }
    /**
     * Find the index of the last user message containing text or image content.
     *
     * This is used for guardLatestUserMessage guardrail evaluation to ensure that guardContent
     * wrapping targets the correct message even when toolResult messages (role='user') follow
     * the actual user text/image input during tool execution cycles.
     *
     * @param messages - Array of messages to search
     * @returns Index of the last user message with text/image content, or undefined if not found
     */
    _findLastUserTextMessageIndex(messages) {
        for (let idx = messages.length - 1; idx >= 0; idx--) {
            const msg = messages[idx];
            if (msg === undefined)
                continue;
            if (msg.role === 'user' &&
                msg.content.some((block) => block.type === 'textBlock' || block.type === 'imageBlock')) {
                return idx;
            }
        }
        return undefined;
    }
    /**
     * Determines whether to include the status field in tool results.
     *
     * Uses the includeToolResultStatus config option:
     * - If explicitly true, always include status
     * - If explicitly false, never include status
     * - If 'auto' (default), check if model ID matches known patterns
     *
     * @returns True if status field should be included, false otherwise
     */
    _shouldIncludeToolResultStatus() {
        const includeStatus = this._config.includeToolResultStatus ?? 'auto';
        if (includeStatus === true)
            return true;
        if (includeStatus === false)
            return false;
        // Auto-detection mode: check if modelId contains any pattern
        const shouldInclude = MODELS_INCLUDE_STATUS.some((pattern) => this._config.modelId?.includes(pattern));
        // Log debug message for auto-detection
        logger.debug(`model_id=<${this._config.modelId}>, include_tool_result_status=<${shouldInclude}> | auto-detected includeToolResultStatus`);
        return shouldInclude;
    }
    /**
     * Formats a content block for Bedrock API.
     *
     * @param block - SDK content block
     * @returns Bedrock-formatted content block
     */
    _formatContentBlock(block) {
        switch (block.type) {
            case 'textBlock':
                return { text: block.text };
            case 'toolUseBlock':
                return {
                    toolUse: {
                        toolUseId: block.toolUseId,
                        name: block.name,
                        input: block.input,
                    },
                };
            case 'toolResultBlock': {
                const content = block.content.map((content) => {
                    switch (content.type) {
                        case 'textBlock':
                            return { text: content.text };
                        case 'jsonBlock':
                            return { json: content.json };
                        case 'imageBlock':
                            return {
                                image: {
                                    format: content.format,
                                    source: this._formatMediaSource(content.source),
                                },
                            };
                        case 'videoBlock':
                            return {
                                video: {
                                    format: content.format === '3gp' ? 'three_gp' : content.format,
                                    source: this._formatMediaSource(content.source),
                                },
                            };
                        case 'documentBlock':
                            return {
                                document: {
                                    name: content.name,
                                    format: content.format,
                                    source: this._formatDocumentSource(content.source),
                                    ...(content.citations && { citations: content.citations }),
                                    ...(content.context && { context: content.context }),
                                },
                            };
                    }
                });
                return {
                    toolResult: {
                        toolUseId: block.toolUseId,
                        content,
                        ...(this._shouldIncludeToolResultStatus() && { status: block.status }),
                    },
                };
            }
            case 'reasoningBlock': {
                if (block.text !== undefined || block.signature !== undefined) {
                    return {
                        reasoningContent: {
                            reasoningText: {
                                text: block.text ?? '',
                                ...(block.signature !== undefined && { signature: block.signature }),
                            },
                        },
                    };
                }
                else if (block.redactedContent) {
                    return {
                        reasoningContent: {
                            redactedContent: block.redactedContent,
                        },
                    };
                }
                else {
                    throw Error("reasoning content requires one of 'text', 'signature', or 'redactedContent' to be set");
                }
            }
            case 'cachePointBlock': {
                const cachePoint = { type: block.cacheType };
                if (block.ttl !== undefined) {
                    // Bedrock validates TTL values server-side, so accept any string here.
                    cachePoint.ttl = block.ttl;
                }
                return { cachePoint };
            }
            case 'imageBlock':
                return {
                    image: {
                        format: block.format,
                        source: this._formatMediaSource(block.source),
                    },
                };
            case 'videoBlock':
                return {
                    video: {
                        format: block.format === '3gp' ? 'three_gp' : block.format,
                        source: this._formatMediaSource(block.source),
                    },
                };
            case 'documentBlock':
                return {
                    document: {
                        name: block.name,
                        format: block.format,
                        source: this._formatDocumentSource(block.source),
                        ...(block.citations && { citations: block.citations }),
                        ...(block.context && { context: block.context }),
                    },
                };
            case 'citationsBlock':
                return {
                    citationsContent: {
                        citations: block.citations.map((c) => this._mapCitationToBedrock(c)),
                        content: block.content,
                    },
                };
            case 'guardContentBlock': {
                if (block.text) {
                    return {
                        guardContent: {
                            text: {
                                text: block.text.text,
                                qualifiers: block.text.qualifiers,
                            },
                        },
                    };
                }
                else if (block.image) {
                    return {
                        guardContent: {
                            image: {
                                format: block.image.format,
                                source: { bytes: block.image.source.bytes },
                            },
                        },
                    };
                }
                else {
                    throw new Error('guardContent must have either text or image');
                }
            }
        }
    }
    /**
     * Format media source (image/video) for Bedrock API.
     * Handles bytes, S3 locations, and s3:// URLs.
     *
     * @param source - Media source
     * @returns Formatted source for Bedrock API
     */
    _formatMediaSource(source) {
        switch (source.type) {
            case 'imageSourceBytes':
            case 'videoSourceBytes':
                return { bytes: source.bytes };
            case 'imageSourceUrl':
                // Check if URL is actually an S3 URI
                if (source.url.startsWith('s3://')) {
                    return {
                        s3Location: {
                            uri: source.url,
                        },
                    };
                }
                logger.warn('source_type=<imageSourceUrl> | not supported by bedrock | skipping');
                return;
            case 'imageSourceS3Location':
            case 'videoSourceS3Location':
                return {
                    s3Location: {
                        uri: source.location.uri,
                        ...(source.location.bucketOwner && { bucketOwner: source.location.bucketOwner }),
                    },
                };
            default:
                throw new Error('Invalid media source');
        }
    }
    /**
     * Format document source for Bedrock API.
     * Handles bytes, text, content, and S3 locations.
     * Note: Bedrock API only accepts bytes, content, or s3Location - text is converted to bytes.
     *
     * @param source - Document source
     * @returns Formatted source for Bedrock API
     */
    _formatDocumentSource(source) {
        switch (source.type) {
            case 'documentSourceBytes':
                return {
                    bytes: source.bytes,
                };
            case 'documentSourceText': {
                // Convert text to bytes - Bedrock API doesn't accept text directly
                const encoder = new TextEncoder();
                return { bytes: encoder.encode(source.text) };
            }
            case 'documentSourceContentBlock':
                return {
                    content: source.content.map((block) => ({
                        text: block.text,
                    })),
                };
            case 'documentSourceS3Location':
                return {
                    s3Location: {
                        uri: source.location.uri,
                        ...(source.location.bucketOwner && { bucketOwner: source.location.bucketOwner }),
                    },
                };
            default:
                throw new Error('Invalid document source');
        }
    }
    _mapBedrockEventToSDKEvent(event) {
        const events = [];
        // Message start
        const output = ensureDefined(event.output, 'event.output');
        const message = ensureDefined(output.message, 'output.message');
        const role = ensureDefined(message.role, 'message.role');
        if (role !== 'user' && role !== 'assistant') {
            throw new Error(`Unexpected role from Bedrock: '${role}'`);
        }
        events.push({
            type: 'modelMessageStartEvent',
            role,
        });
        // Match on content blocks
        const blockHandlers = {
            text: (textBlock) => {
                events.push({ type: 'modelContentBlockStartEvent' });
                events.push({
                    type: 'modelContentBlockDeltaEvent',
                    delta: { type: 'textDelta', text: textBlock },
                });
                events.push({ type: 'modelContentBlockStopEvent' });
            },
            toolUse: (block) => {
                events.push({
                    type: 'modelContentBlockStartEvent',
                    start: {
                        type: 'toolUseStart',
                        name: ensureDefined(block.name, 'toolUse.name'),
                        toolUseId: ensureDefined(block.toolUseId, 'toolUse.toolUseId'),
                    },
                });
                events.push({
                    type: 'modelContentBlockDeltaEvent',
                    delta: { type: 'toolUseInputDelta', input: JSON.stringify(ensureDefined(block.input, 'toolUse.input')) },
                });
                events.push({ type: 'modelContentBlockStopEvent' });
            },
            reasoningContent: (block) => {
                if (!block)
                    return;
                events.push({ type: 'modelContentBlockStartEvent' });
                const delta = { type: 'reasoningContentDelta' };
                if (block.reasoningText) {
                    delta.text = ensureDefined(block.reasoningText.text, 'reasoningText.text');
                    if (block.reasoningText.signature)
                        delta.signature = block.reasoningText.signature;
                }
                else if (block.redactedContent) {
                    delta.redactedContent = block.redactedContent;
                }
                if (Object.keys(delta).length > 1) {
                    events.push({ type: 'modelContentBlockDeltaEvent', delta });
                }
                events.push({ type: 'modelContentBlockStopEvent' });
            },
            citationsContent: (block) => {
                if (!block)
                    return;
                events.push({ type: 'modelContentBlockStartEvent' });
                const mapped = this._mapBedrockCitationsData(block);
                const delta = {
                    type: 'citationsDelta',
                    citations: mapped.citations,
                    content: mapped.content,
                };
                events.push({ type: 'modelContentBlockDeltaEvent', delta });
                events.push({ type: 'modelContentBlockStopEvent' });
            },
        };
        const content = ensureDefined(message.content, 'message.content');
        content.forEach((block) => {
            for (const key in block) {
                if (key in blockHandlers) {
                    const handlerKey = key;
                    // @ts-expect-error - We know the value type corresponds to the handler key.
                    blockHandlers[handlerKey](block[handlerKey]);
                }
                else {
                    logger.warn(`block_key=<${key}> | skipping unsupported block key`);
                }
            }
        });
        const stopReasonRaw = ensureDefined(event.stopReason, 'event.stopReason');
        events.push({
            type: 'modelMessageStopEvent',
            stopReason: this._transformStopReason(stopReasonRaw, event),
        });
        const usage = ensureDefined(event.usage, 'output.usage');
        const metadataEvent = {
            type: 'modelMetadataEvent',
            usage: {
                inputTokens: ensureDefined(usage.inputTokens, 'usage.inputTokens'),
                outputTokens: ensureDefined(usage.outputTokens, 'usage.outputTokens'),
                totalTokens: ensureDefined(usage.totalTokens, 'usage.totalTokens'),
            },
        };
        if (event.metrics) {
            metadataEvent.metrics = {
                latencyMs: ensureDefined(event.metrics.latencyMs, 'metrics.latencyMs'),
            };
        }
        // Handle trace and guardrail check for non-streaming responses
        if (event.trace) {
            metadataEvent.trace = event.trace;
            // Check for blocked guardrails and emit redaction events
            if (this._config.guardrailConfig && event.trace.guardrail && stopReasonRaw === 'guardrail_intervened') {
                for (const redactionEvent of this._generateRedactionEvents(event.trace.guardrail)) {
                    events.push(redactionEvent);
                }
            }
        }
        events.push(metadataEvent);
        return events;
    }
    /**
     * Maps a Bedrock event to SDK streaming events.
     *
     * @param chunk - Bedrock event chunk
     * @param lastStopReason - Stop reason from previous messageStop event
     * @returns Object containing events array and optional stopReason
     */
    _mapStreamedBedrockEventToSDKEvent(chunk, lastStopReason) {
        const events = [];
        let stopReason = lastStopReason;
        // Extract the event type key
        const eventType = ensureDefined(Object.keys(chunk)[0], 'eventType');
        const eventData = chunk[eventType];
        switch (eventType) {
            case 'messageStart': {
                const data = eventData;
                const role = ensureDefined(data.role, 'messageStart.role');
                if (role !== 'user' && role !== 'assistant') {
                    throw new Error(`Unexpected role from Bedrock: '${role}'`);
                }
                events.push({
                    type: 'modelMessageStartEvent',
                    role,
                });
                break;
            }
            case 'contentBlockStart': {
                const data = eventData;
                const event = {
                    type: 'modelContentBlockStartEvent',
                };
                if (data.start?.toolUse) {
                    const toolUse = data.start.toolUse;
                    event.start = {
                        type: 'toolUseStart',
                        name: ensureDefined(toolUse.name, 'toolUse.name'),
                        toolUseId: ensureDefined(toolUse.toolUseId, 'toolUse.toolUseId'),
                    };
                }
                events.push(event);
                break;
            }
            case 'contentBlockDelta': {
                const data = eventData;
                const delta = ensureDefined(data.delta, 'contentBlockDelta.delta');
                const deltaHandlers = {
                    text: (textValue) => {
                        events.push({
                            type: 'modelContentBlockDeltaEvent',
                            delta: { type: 'textDelta', text: textValue },
                        });
                    },
                    toolUse: (toolUse) => {
                        if (!toolUse?.input)
                            return;
                        events.push({
                            type: 'modelContentBlockDeltaEvent',
                            delta: { type: 'toolUseInputDelta', input: toolUse.input },
                        });
                    },
                    reasoningContent: (reasoning) => {
                        if (!reasoning)
                            return;
                        const reasoningDelta = { type: 'reasoningContentDelta' };
                        if (reasoning.text)
                            reasoningDelta.text = reasoning.text;
                        if (reasoning.signature)
                            reasoningDelta.signature = reasoning.signature;
                        if (reasoning.redactedContent)
                            reasoningDelta.redactedContent = reasoning.redactedContent;
                        if (Object.keys(reasoningDelta).length > 1) {
                            events.push({ type: 'modelContentBlockDeltaEvent', delta: reasoningDelta });
                        }
                    },
                    citation: (citation) => {
                        const location = citation.location ? this._mapBedrockCitationLocation(citation.location) : undefined;
                        if (!location)
                            return;
                        events.push({
                            type: 'modelContentBlockDeltaEvent',
                            delta: {
                                type: 'citationsDelta',
                                citations: [
                                    {
                                        location,
                                        sourceContent: (citation.sourceContent ?? []).map((sc) => ({ text: sc.text })),
                                        source: citation.source ?? '',
                                        title: citation.title ?? '',
                                    },
                                ],
                                content: [],
                            },
                        });
                    },
                };
                for (const key in delta) {
                    if (key in deltaHandlers) {
                        const handlerKey = key;
                        // @ts-expect-error - We know the value type corresponds to the handler key.
                        deltaHandlers[handlerKey](delta[handlerKey]);
                    }
                    else {
                        logger.warn(`delta_key=<${key}> | skipping unsupported delta key`);
                    }
                }
                break;
            }
            case 'contentBlockStop': {
                events.push({
                    type: 'modelContentBlockStopEvent',
                });
                break;
            }
            case 'messageStop': {
                const data = eventData;
                const stopReasonRaw = ensureDefined(data.stopReason, 'messageStop.stopReason');
                stopReason = stopReasonRaw;
                const event = {
                    type: 'modelMessageStopEvent',
                    stopReason: this._transformStopReason(stopReasonRaw, data),
                };
                if (data.additionalModelResponseFields) {
                    event.additionalModelResponseFields = data.additionalModelResponseFields;
                }
                events.push(event);
                break;
            }
            case 'metadata': {
                const data = eventData;
                const event = {
                    type: 'modelMetadataEvent',
                };
                if (data.usage) {
                    const usage = data.usage;
                    const usageInfo = {
                        inputTokens: ensureDefined(usage.inputTokens, 'usage.inputTokens'),
                        outputTokens: ensureDefined(usage.outputTokens, 'usage.outputTokens'),
                        totalTokens: ensureDefined(usage.totalTokens, 'usage.totalTokens'),
                    };
                    if (usage.cacheReadInputTokens !== undefined) {
                        usageInfo.cacheReadInputTokens = usage.cacheReadInputTokens;
                    }
                    if (usage.cacheWriteInputTokens !== undefined) {
                        usageInfo.cacheWriteInputTokens = usage.cacheWriteInputTokens;
                    }
                    event.usage = usageInfo;
                }
                if (data.metrics) {
                    event.metrics = {
                        latencyMs: ensureDefined(data.metrics.latencyMs, 'metrics.latencyMs'),
                    };
                }
                if (data.trace) {
                    event.trace = data.trace;
                    // Check for blocked guardrails in trace and emit redaction events
                    if (this._config.guardrailConfig && data.trace.guardrail && lastStopReason === 'guardrail_intervened') {
                        for (const redactionEvent of this._generateRedactionEvents(data.trace.guardrail)) {
                            events.push(redactionEvent);
                        }
                    }
                }
                events.push(event);
                break;
            }
            case 'internalServerException':
            case 'modelStreamErrorException':
            case 'serviceUnavailableException':
            case 'validationException': {
                throw eventData;
            }
            case 'throttlingException': {
                const message = eventData.message ?? 'Request was throttled by the model provider';
                logger.debug(`throttled | error_message=<${message}>`);
                throw new ModelThrottledError(message, { cause: eventData });
            }
            default:
                // Log warning for unsupported event types (for forward compatibility)
                logger.warn(`event_type=<${eventType}> | unsupported bedrock event type`);
                break;
        }
        return stopReason !== undefined ? { events, stopReason } : { events };
    }
    /**
     * Transforms a Bedrock stop reason into the SDK's format.
     *
     * @param stopReasonRaw - The raw stop reason string from Bedrock.
     * @param event - The full event output, used to check for tool_use adjustments.
     * @returns The transformed stop reason.
     */
    _transformStopReason(stopReasonRaw, event) {
        let mappedStopReason;
        if (stopReasonRaw in STOP_REASON_MAP) {
            mappedStopReason = STOP_REASON_MAP[stopReasonRaw];
        }
        else {
            const camelCaseReason = snakeToCamel(stopReasonRaw);
            logger.warn(`stop_reason=<${stopReasonRaw}>, fallback=<${camelCaseReason}> | unknown stop reason, converting to camelCase`);
            mappedStopReason = camelCaseReason;
        }
        // Adjust for tool_use, which is sometimes incorrectly reported as end_turn
        if (mappedStopReason === 'endTurn' &&
            event &&
            'output' in event &&
            event.output?.message?.content?.some((block) => 'toolUse' in block)) {
            mappedStopReason = 'toolUse';
            logger.warn('stop_reason=<end_turn> | adjusting to tool_use due to tool use in content blocks');
        }
        return mappedStopReason;
    }
    /**
     * Maps a Bedrock object-key citation location to the SDK's type-field format.
     *
     * Bedrock uses object-key discrimination (`{ documentChar: { ... } }`) while the SDK uses
     * type-field discrimination (`{ type: 'documentChar', ... }`). Also normalizes Bedrock's
     * `searchResultLocation` key to the shorter `searchResult`.
     *
     * @param bedrockLocation - Bedrock citation location with object-key discrimination
     * @returns SDK CitationLocation with type field discrimination
     */
    _mapBedrockCitationLocation(bedrockLocation) {
        if (bedrockLocation.documentChar) {
            const loc = bedrockLocation.documentChar;
            return { type: 'documentChar', documentIndex: loc.documentIndex, start: loc.start, end: loc.end };
        }
        if (bedrockLocation.documentPage) {
            const loc = bedrockLocation.documentPage;
            return { type: 'documentPage', documentIndex: loc.documentIndex, start: loc.start, end: loc.end };
        }
        if (bedrockLocation.documentChunk) {
            const loc = bedrockLocation.documentChunk;
            return { type: 'documentChunk', documentIndex: loc.documentIndex, start: loc.start, end: loc.end };
        }
        if (bedrockLocation.searchResultLocation) {
            const loc = bedrockLocation.searchResultLocation;
            return { type: 'searchResult', searchResultIndex: loc.searchResultIndex, start: loc.start, end: loc.end };
        }
        if (bedrockLocation.web) {
            const loc = bedrockLocation.web;
            return { type: 'web', url: loc.url, ...(loc.domain && { domain: loc.domain }) };
        }
        logger.warn(`citation_location=<${JSON.stringify(bedrockLocation)}> | unknown citation location type`);
        return undefined;
    }
    /**
     * Maps a Bedrock CitationsContentBlock to SDK CitationsBlockData.
     *
     * @param bedrockData - Bedrock CitationsContentBlock
     * @returns SDK CitationsBlockData with type-field CitationLocations
     */
    _mapBedrockCitationsData(bedrockData) {
        return {
            citations: (bedrockData.citations ?? [])
                .map((citation) => {
                const location = citation.location ? this._mapBedrockCitationLocation(citation.location) : undefined;
                if (!location)
                    return undefined;
                return {
                    source: citation.source ?? '',
                    title: citation.title ?? '',
                    sourceContent: (citation.sourceContent ?? []).map((sc) => ({ text: sc.text })),
                    location,
                };
            })
                .filter((c) => c !== undefined),
            content: (bedrockData.content ?? []).map((gc) => ({ text: gc.text })),
        };
    }
    /**
     * Maps an SDK Citation to Bedrock's Citation format.
     *
     * @param citation - SDK Citation with type-field location
     * @returns Bedrock Citation with object-key location
     */
    _mapCitationToBedrock(citation) {
        return {
            location: this._mapCitationLocationToBedrock(citation.location),
            sourceContent: citation.sourceContent.map((sc) => ({ text: sc.text })),
            source: citation.source,
            title: citation.title,
        };
    }
    /**
     * Maps an SDK CitationLocation to Bedrock's object-key format.
     *
     * @param location - SDK CitationLocation with type field
     * @returns Bedrock CitationLocation with object-key discrimination
     */
    _mapCitationLocationToBedrock(location) {
        switch (location.type) {
            case 'documentChar': {
                const { type: _, ...fields } = location;
                return { documentChar: fields };
            }
            case 'documentPage': {
                const { type: _, ...fields } = location;
                return { documentPage: fields };
            }
            case 'documentChunk': {
                const { type: _, ...fields } = location;
                return { documentChunk: fields };
            }
            case 'searchResult': {
                const { type: _, ...fields } = location;
                return { searchResultLocation: fields };
            }
            case 'web':
                return { web: { url: location.url, ...(location.domain && { domain: location.domain }) } };
            default:
                return location;
        }
    }
    /**
     * Generate redaction events based on guardrail configuration.
     *
     * @param guardrailData - The guardrail trace assessment data
     * @returns Array of redaction events to emit
     */
    _generateRedactionEvents(guardrailData) {
        const events = [];
        const redaction = this._config.guardrailConfig?.redaction;
        // Default: redact input is true unless explicitly set to false
        if (redaction?.input !== false) {
            logger.debug('redacting input due to guardrail');
            events.push({
                type: 'modelRedactionEvent',
                inputRedaction: {
                    replaceContent: redaction?.inputMessage ?? DEFAULT_REDACT_INPUT_MESSAGE,
                },
            });
        }
        // Only redact output if explicitly enabled
        if (redaction?.output) {
            logger.debug('redacting output due to guardrail');
            const outputRedactionEvent = {
                type: 'modelRedactionEvent',
                outputRedaction: {
                    replaceContent: redaction?.outputMessage ?? DEFAULT_REDACT_OUTPUT_MESSAGE,
                },
            };
            // Include the original model output if available
            if (guardrailData.modelOutput && guardrailData.modelOutput.length > 0) {
                outputRedactionEvent.outputRedaction.redactedContent = guardrailData.modelOutput.join('');
            }
            events.push(outputRedactionEvent);
        }
        return events;
    }
}
/**
 * Merges a default request timeout into the caller's requestHandler options.
 *
 * The SDK's `requestHandler` slot accepts either a constructed handler instance
 * or an options bag that the SDK uses to build its default handler. We only
 * inject a default in the options-bag case: a handler instance has its timeouts
 * baked in at construction time, so we pass it through untouched.
 *
 * The handler-vs-options discriminator mirrors the SDK's own check — see
 * `NodeHttp2Handler.create` in `@smithy/node-http-handler`.
 */
function withDefaultRequestTimeout(handler) {
    if (handler && typeof handler.handle === 'function') {
        return handler;
    }
    const options = (handler ?? {});
    // Use `??` rather than spread order so an explicit `requestTimeout: undefined` still gets
    // the default (spread would otherwise overwrite the default with `undefined`, disabling it).
    return { ...options, requestTimeout: options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS };
}
/**
 * Adds middleware to override the Authorization header with a Bearer token.
 * Runs after SigV4 signing to replace the signature with the API key.
 *
 * @param client - BedrockRuntimeClient instance
 * @param apiKey - Bedrock API key
 */
function applyApiKey(client, apiKey) {
    client.middlewareStack.add(
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    (next) => async (args) => {
        const request = args.request;
        request.headers['authorization'] = `Bearer ${apiKey}`;
        return next(args);
    }, {
        step: 'finalizeRequest',
        priority: 'low',
        name: 'bedrockApiKeyMiddleware',
    });
}
/**
 * What region is used for the BedrockConfiguration can't be known at construction-time so to apply a default
 * we have to use an async function to intercept "Region is missing" errors and then apply our default (this
 * is actually how many bedrock configuration parameters are implemented).
 *
 * We need to override both region & useFipsEndpoint because the region is used in both of those places:
 * https://github.com/smithy-lang/smithy-typescript/blob/e11f7499c1bad30a515217f82a07b9e3e69a1f60/packages/config-resolver/src/regionConfig/resolveRegionConfig.ts#L42
 *
 * We do this unconditionally so that if a region is updated dynamically (environment variable or profile value) we
 * also pick up those changes and stop applying the default.
 */
function applyDefaultRegion(config) {
    // Bind original region function and wrap with error handling
    const originalRegion = config.region.bind(config);
    config.region = async () => {
        try {
            return await originalRegion();
        }
        catch (error) {
            // Note: it was observed that the browser version of the BedrockClient
            // uses a string instead of an error object - thus the normalizeError call
            if (normalizeError(error).message === 'Region is missing') {
                return MODEL_DEFAULTS.bedrock.region;
            }
            throw error;
        }
    };
    // Bind original useFipsEndpoint function and wrap with error handling
    const originalUseFipsEndpoint = config.useFipsEndpoint.bind(config);
    config.useFipsEndpoint = async () => {
        try {
            return await originalUseFipsEndpoint();
        }
        catch (error) {
            // Note: it was observed that the browser version of the BedrockClient
            // uses a string instead of an error object - thus the normalizeError call
            if (normalizeError(error).message === 'Region is missing') {
                return DEFAULT_BEDROCK_REGION_SUPPORTS_FIP;
            }
            throw error;
        }
    };
}
//# sourceMappingURL=bedrock.js.map