UNPKG

@ai-sdk/amazon-bedrock

Version:
1,888 lines • 61.4 kB
import { getAmazonBedrockLanguageModelEntry } from './amazon-bedrock-model-catalog';
import type {
  JSONObject,
  LanguageModelV4,
  LanguageModelV4CallOptions,
  LanguageModelV4Content,
  LanguageModelV4FinishReason,
  LanguageModelV4FunctionTool,
  LanguageModelV4GenerateResult,
  LanguageModelV4Reasoning,
  LanguageModelV4StreamPart,
  LanguageModelV4StreamResult,
  SharedV4ProviderMetadata,
  SharedV4Warning,
} from '@ai-sdk/provider';
import {
  lazySchema,
  zodSchema,
  combineHeaders,
  createProviderStreamError,
  createJsonErrorResponseHandler,
  createJsonResponseHandler,
  injectJsonInstructionIntoMessages,
  isCustomReasoning,
  mapReasoningToProviderBudget,
  mapReasoningToProviderEffort,
  parseProviderOptions,
  postJsonToApi,
  resolve,
  resolveProviderOptions,
  serializeModelOptions,
  WORKFLOW_SERIALIZE,
  WORKFLOW_DESERIALIZE,
  type FetchFunction,
  type ParseResult,
  type Resolvable,
} from '@ai-sdk/provider-utils';
import {
  getModelCapabilities,
  sanitizeJsonSchema,
  supportsNativeStructuredOutputSchema,
} from '@ai-sdk/anthropic/internal';
import { z } from 'zod/v4';
import {
  BEDROCK_STOP_REASONS,
  type AmazonBedrockConverseInput,
  type AmazonBedrockStopReason,
} from './amazon-bedrock-api-types';
import {
  amazonBedrockLanguageModelChatOptions,
  type AmazonBedrockLanguageModelChatOptions,
  type AmazonBedrockChatModelId,
  type AmazonBedrockChatModelSettings,
} from './amazon-bedrock-chat-language-model-options';
import {
  isAnthropicModel as detectAnthropicModel,
  supportsNativeStructuredOutput,
  supportsStrictTools,
} from './amazon-bedrock-anthropic-model-support';
import {
  amazonBedrockFailedResponseHandler,
  AmazonBedrockErrorSchema,
} from './amazon-bedrock-error';
import { createAmazonBedrockEventStreamResponseHandler } from './amazon-bedrock-event-stream-response-handler';
import {
  getAmazonBedrockStreamErrorMetadata,
  type AmazonBedrockStreamErrorType,
} from './amazon-bedrock-stream-error';
import { prepareTools } from './amazon-bedrock-prepare-tools';
import {
  convertAmazonBedrockUsage,
  type AmazonBedrockUsage,
} from './convert-amazon-bedrock-usage';
import { convertToAmazonBedrockChatMessages } from './convert-to-amazon-bedrock-chat-messages';
import { mapAmazonBedrockFinishReason } from './map-amazon-bedrock-finish-reason';
import { isMistralModel, normalizeToolCallId } from './normalize-tool-call-id';
import type { AmazonBedrockReasoningMetadata } from './amazon-bedrock-reasoning-metadata';

type AmazonBedrockChatConfig = {
  baseUrl: () => string;
  headers?: Resolvable<Record<string, string | undefined>>;
  fetch?: FetchFunction;
  generateId: () => string;
  modelFamily?: AmazonBedrockChatModelSettings['modelFamily'];
};

const anthropicProviderOptions = lazySchema(() =>
  zodSchema(
    z.object({
      disableParallelToolUse: z.boolean().optional(),
      structuredOutputMode: z
        .enum(['outputFormat', 'jsonTool', 'auto'])
        .optional(),
    }),
  ),
);

function createAmazonBedrockStreamError({
  type,
  error,
  data,
}: {
  type: AmazonBedrockStreamErrorType;
  error: Record<string, unknown>;
  data: unknown;
}) {
  const message =
    typeof error.message === 'string'
      ? error.message
      : `Amazon Bedrock stream failed with ${type}`;

  return createProviderStreamError({
    message,
    type,
    ...getAmazonBedrockStreamErrorMetadata(type),
    data,
  });
}

export class AmazonBedrockChatLanguageModel implements LanguageModelV4 {
  readonly specificationVersion = 'v4';
  readonly provider = 'amazon-bedrock';

  static [WORKFLOW_SERIALIZE](model: AmazonBedrockChatLanguageModel) {
    return serializeModelOptions({
      modelId: model.modelId,
      config: model.config,
    });
  }

  static [WORKFLOW_DESERIALIZE](options: {
    modelId: string;
    config: AmazonBedrockChatConfig;
  }) {
    return new AmazonBedrockChatLanguageModel(options.modelId, options.config);
  }

  constructor(
    readonly modelId: AmazonBedrockChatModelId,
    private readonly config: AmazonBedrockChatConfig,
  ) {}

  private async getArgs({
    prompt,
    maxOutputTokens,
    temperature,
    topP,
    topK,
    frequencyPenalty,
    presencePenalty,
    stopSequences,
    responseFormat,
    seed,
    tools,
    toolChoice,
    reasoning,
    providerOptions,
  }: LanguageModelV4CallOptions): Promise<{
    command: AmazonBedrockConverseInput;
    warnings: SharedV4Warning[];
    usesJsonInstruction: boolean;
    usesJsonResponseTool: boolean;
    betas: Set<string>;
  }> {
    // Parse provider options. Prefer `amazonBedrock`; fall back to legacy
    // `bedrock` key for backward compatibility.
    let amazonBedrockOptions =
      (
        await resolveProviderOptions({
          providerOptions,
          namespaces: ['amazonBedrock', 'bedrock'],
          resolution: 'first',
          schema: amazonBedrockLanguageModelChatOptions,
        })
      ).options ?? {};

    const anthropicOptions = await parseProviderOptions({
      provider: 'anthropic',
      providerOptions,
      schema: anthropicProviderOptions,
    });

    const warnings: SharedV4Warning[] = [];

    const {
      supportsStructuredOutput: modelSupportsStructuredOutput,
      rejectsSamplingParameters,
      rejectsForcedToolUse,
      rejectsThinkingDisabled,
      rejectsThinkingDisabledAboveHighEffort,
      supportsBetweenToolsThinking,
      isKnownModel,
    } = getModelCapabilities(this.modelId);

    if (frequencyPenalty != null) {
      warnings.push({
        type: 'unsupported',
        feature: 'frequencyPenalty',
      });
    }

    if (presencePenalty != null) {
      warnings.push({
        type: 'unsupported',
        feature: 'presencePenalty',
      });
    }

    if (seed != null) {
      warnings.push({
        type: 'unsupported',
        feature: 'seed',
      });
    }

    if (rejectsSamplingParameters) {
      if (temperature != null) {
        warnings.push({
          type: 'unsupported',
          feature: 'temperature',
          details: `temperature is not supported by ${this.modelId} and will be ignored`,
        });
        temperature = undefined;
      }

      if (topK != null) {
        warnings.push({
          type: 'unsupported',
          feature: 'topK',
          details: `topK is not supported by ${this.modelId} and will be ignored`,
        });
        topK = undefined;
      }

      if (topP != null) {
        warnings.push({
          type: 'unsupported',
          feature: 'topP',
          details: `topP is not supported by ${this.modelId} and will be ignored`,
        });
        topP = undefined;
      }
    }

    const modelEntry = getAmazonBedrockLanguageModelEntry(this.modelId);
    const {
      openai: isOpenAIModel,
      openaiGptOss: isOpenAIGptOssModel,
      nova2Reasoning: isNovaReasoningModel,
    } = modelEntry.requestConfig.language;
    const shouldNormalizeTemperature = !isOpenAIModel || isOpenAIGptOssModel;

    if (shouldNormalizeTemperature && temperature != null && temperature > 1) {
      warnings.push({
        type: 'unsupported',
        feature: 'temperature',
        details: `${temperature} exceeds bedrock maximum of 1.0. clamped to 1.0`,
      });
      temperature = 1;
    } else if (
      shouldNormalizeTemperature &&
      temperature != null &&
      temperature < 0
    ) {
      warnings.push({
        type: 'unsupported',
        feature: 'temperature',
        details: `${temperature} is below bedrock minimum of 0. clamped to 0`,
      });
      temperature = 0;
    }

    if (
      responseFormat != null &&
      responseFormat.type !== 'text' &&
      responseFormat.type !== 'json'
    ) {
      warnings.push({
        type: 'unsupported',
        feature: 'responseFormat',
        details: 'Only text and json response formats are supported.',
      });
    }

    const isAnthropicModel = detectAnthropicModel({
      modelId: this.modelId,
      modelFamily: this.config.modelFamily,
      reasoningBudgetTokens: amazonBedrockOptions.reasoningConfig?.budgetTokens,
    });

    const hasExplicitDisabledThinking =
      amazonBedrockOptions.reasoningConfig?.type === 'disabled';
    const explicitReasoningEffort =
      amazonBedrockOptions.reasoningConfig?.maxReasoningEffort;

    amazonBedrockOptions = resolveAmazonBedrockReasoningConfig({
      reasoning,
      amazonBedrockOptions,
      warnings,
      isAnthropicModel,
      isOpenAIModel,
      modelId: this.modelId,
    });

    let thinkingType:
      | 'enabled'
      | 'disabled'
      | 'adaptive'
      | 'between_tools'
      | undefined = amazonBedrockOptions.reasoningConfig?.type;
    let maxReasoningEffort =
      amazonBedrockOptions.reasoningConfig?.maxReasoningEffort;

    if (isAnthropicModel && thinkingType === 'disabled') {
      if (
        /claude-(?:instant|v?2|3-(?:5-)?(?:haiku|sonnet|opus))(?=[-.:]|$)/.test(
          this.modelId,
        )
      ) {
        // Claude 3.5 and earlier do not accept a thinking field, even disabled.
        thinkingType = undefined;
      } else if (rejectsThinkingDisabled) {
        thinkingType = supportsBetweenToolsThinking
          ? 'between_tools'
          : 'adaptive';

        if (!hasExplicitDisabledThinking && thinkingType === 'adaptive') {
          maxReasoningEffort ??= explicitReasoningEffort;

          if (
            maxReasoningEffort == null &&
            amazonBedrockOptions.additionalModelRequestFields?.output_config
              ?.effort == null
          ) {
            maxReasoningEffort = 'low';
          }
        }

        warnings.push({
          type: hasExplicitDisabledThinking ? 'unsupported' : 'compatibility',
          feature: hasExplicitDisabledThinking
            ? 'providerOptions.amazonBedrock.reasoningConfig'
            : 'reasoning',
          details:
            `thinking cannot be disabled for ${this.modelId}. ` +
            `Using '${thinkingType}' thinking instead.`,
        });
      }

      const effort =
        maxReasoningEffort ??
        amazonBedrockOptions.additionalModelRequestFields?.output_config
          ?.effort;

      if (
        ((thinkingType === 'disabled' &&
          rejectsThinkingDisabledAboveHighEffort) ||
          thinkingType === 'between_tools') &&
        (effort === 'xhigh' || effort === 'max')
      ) {
        warnings.push({
          type: 'unsupported',
          feature:
            maxReasoningEffort != null
              ? 'providerOptions.amazonBedrock.reasoningConfig.maxReasoningEffort'
              : 'providerOptions.amazonBedrock.additionalModelRequestFields.output_config.effort',
          details:
            `effort '${effort}' is not supported by ${this.modelId} with '${thinkingType}' thinking. ` +
            `The effort has been lowered to 'high'.`,
        });
        maxReasoningEffort = 'high';
      }
    }

    const isThinkingEnabled =
      thinkingType === 'enabled' ||
      thinkingType === 'adaptive' ||
      thinkingType === 'between_tools';

    const structuredOutputMode =
      amazonBedrockOptions.structuredOutputMode ??
      anthropicOptions?.structuredOutputMode ??
      'auto';

    if (structuredOutputMode === 'jsonTool') {
      const additionalModelRequestFields = {
        ...amazonBedrockOptions.additionalModelRequestFields,
      };
      const outputConfig = additionalModelRequestFields.output_config;

      if (
        outputConfig != null &&
        typeof outputConfig === 'object' &&
        !Array.isArray(outputConfig)
      ) {
        const outputConfigWithoutFormat = { ...outputConfig };
        delete outputConfigWithoutFormat.format;

        if (Object.keys(outputConfigWithoutFormat).length > 0) {
          additionalModelRequestFields.output_config =
            outputConfigWithoutFormat;
        } else {
          delete additionalModelRequestFields.output_config;
        }

        amazonBedrockOptions = {
          ...amazonBedrockOptions,
          additionalModelRequestFields,
        };
      }
    }

    const modelSupportsNativeStructuredOutput =
      supportsNativeStructuredOutput(this.modelId) &&
      (modelSupportsStructuredOutput ||
        isThinkingEnabled ||
        this.config.modelFamily === 'anthropic');

    const useNativeStructuredOutput =
      isAnthropicModel &&
      responseFormat?.type === 'json' &&
      responseFormat.schema != null &&
      (structuredOutputMode === 'outputFormat' ||
        (structuredOutputMode === 'auto' &&
          modelSupportsNativeStructuredOutput &&
          supportsNativeStructuredOutputSchema(responseFormat.schema)));

    const useJsonInstructionForStructuredOutput =
      !useNativeStructuredOutput &&
      isAnthropicModel &&
      responseFormat?.type === 'json' &&
      responseFormat.schema != null &&
      (rejectsForcedToolUse ||
        (modelEntry.requestConfig.language.claude &&
          (!isKnownModel || !modelSupportsStructuredOutput) &&
          !supportsNativeStructuredOutput(this.modelId) &&
          structuredOutputMode !== 'jsonTool') ||
        (structuredOutputMode !== 'jsonTool' &&
          !supportsStrictTools(this.modelId) &&
          tools != null &&
          tools.length > 0));

    const jsonResponseTool: LanguageModelV4FunctionTool | undefined =
      responseFormat?.type === 'json' &&
      responseFormat.schema != null &&
      !useNativeStructuredOutput &&
      !useJsonInstructionForStructuredOutput
        ? {
            type: 'function',
            name: 'json',
            description: 'Respond with a JSON object.',
            inputSchema: responseFormat.schema,
          }
        : undefined;

    const { toolConfig, additionalTools, toolWarnings, betas } =
      await prepareTools({
        tools: jsonResponseTool
          ? [
              ...(tools ?? []).filter(
                tool =>
                  toolChoice?.type !== 'none' &&
                  (toolChoice?.type !== 'tool' ||
                    tool.name === toolChoice.toolName),
              ),
              jsonResponseTool,
            ]
          : tools,
        toolChoice:
          jsonResponseTool != null ? { type: 'required' } : toolChoice,
        modelId: this.modelId,
        modelFamily: this.config.modelFamily,
        reasoningBudgetTokens:
          amazonBedrockOptions.reasoningConfig?.budgetTokens,
        disableParallelToolUse: anthropicOptions?.disableParallelToolUse,
        rejectsForcedToolUse,
      });

    warnings.push(...toolWarnings);

    if (additionalTools) {
      amazonBedrockOptions.additionalModelRequestFields = {
        ...amazonBedrockOptions.additionalModelRequestFields,
        ...additionalTools,
      };
    }

    if (betas.size > 0 || amazonBedrockOptions.anthropicBeta) {
      const existingBetas = amazonBedrockOptions.anthropicBeta ?? [];
      const mergedBetas =
        betas.size > 0
          ? [...existingBetas, ...Array.from(betas)]
          : existingBetas;

      amazonBedrockOptions.additionalModelRequestFields = {
        ...amazonBedrockOptions.additionalModelRequestFields,
        anthropic_beta: mergedBetas,
      };
    }

    const thinkingBudget =
      thinkingType === 'enabled'
        ? amazonBedrockOptions.reasoningConfig?.budgetTokens
        : undefined;
    const thinkingDisplay =
      thinkingType === 'adaptive'
        ? amazonBedrockOptions.reasoningConfig?.display
        : undefined;
    const isAnthropicThinkingEnabled = isAnthropicModel && isThinkingEnabled;

    const inferenceConfig = {
      ...(maxOutputTokens != null && { maxTokens: maxOutputTokens }),
      ...(temperature != null && { temperature }),
      ...(topP != null && { topP }),
      ...(topK != null && { topK }),
      ...(stopSequences != null && { stopSequences }),
    };

    if (isAnthropicModel) {
      if (thinkingBudget != null) {
        if (inferenceConfig.maxTokens != null) {
          inferenceConfig.maxTokens += thinkingBudget;
        } else {
          inferenceConfig.maxTokens = thinkingBudget + 4096; // Default + thinking budget maxTokens = 4096, TODO update default in v5
        }
        amazonBedrockOptions.additionalModelRequestFields = {
          ...amazonBedrockOptions.additionalModelRequestFields,
          thinking: {
            type: 'enabled',
            budget_tokens: thinkingBudget,
          },
        };
      } else if (thinkingType === 'adaptive') {
        amazonBedrockOptions.additionalModelRequestFields = {
          ...amazonBedrockOptions.additionalModelRequestFields,
          thinking: {
            type: 'adaptive',
            ...(thinkingDisplay != null && { display: thinkingDisplay }),
          },
        };
      } else if (
        thinkingType === 'disabled' ||
        thinkingType === 'between_tools'
      ) {
        amazonBedrockOptions.additionalModelRequestFields = {
          ...amazonBedrockOptions.additionalModelRequestFields,
          thinking: { type: thinkingType },
        };
      }
    } else {
      if (amazonBedrockOptions.reasoningConfig?.budgetTokens != null) {
        warnings.push({
          type: 'unsupported',
          feature: 'budgetTokens',
          details:
            'budgetTokens applies only to Anthropic models on Bedrock and will be ignored for this model.',
        });
      }
      if (thinkingType === 'adaptive') {
        warnings.push({
          type: 'unsupported',
          feature: 'adaptive thinking',
          details:
            'adaptive thinking type applies only to Anthropic models on Bedrock.',
        });
      }
    }

    if (maxReasoningEffort != null) {
      if (isAnthropicModel) {
        amazonBedrockOptions.additionalModelRequestFields = {
          ...amazonBedrockOptions.additionalModelRequestFields,
          output_config: {
            ...amazonBedrockOptions.additionalModelRequestFields?.output_config,
            effort: maxReasoningEffort,
          },
        };
      } else if (isOpenAIModel) {
        // gpt-oss models expect `reasoning_effort` as a flat value, while
        // GPT-5.x models expect a nested `reasoning.effort` object.
        amazonBedrockOptions.additionalModelRequestFields = isOpenAIGptOssModel
          ? {
              ...amazonBedrockOptions.additionalModelRequestFields,
              reasoning_effort: maxReasoningEffort,
            }
          : {
              ...amazonBedrockOptions.additionalModelRequestFields,
              reasoning: {
                ...amazonBedrockOptions.additionalModelRequestFields?.reasoning,
                effort: maxReasoningEffort,
              },
            };
      } else {
        // other models (such as Nova 2) use reasoningConfig format
        amazonBedrockOptions.additionalModelRequestFields = {
          ...amazonBedrockOptions.additionalModelRequestFields,
          reasoningConfig: {
            ...(thinkingType != null &&
              thinkingType !== 'adaptive' && { type: thinkingType }),
            ...(thinkingBudget != null && { budgetTokens: thinkingBudget }),
            maxReasoningEffort,
          },
        };
      }
    }

    if (
      isNovaReasoningModel &&
      thinkingType === 'enabled' &&
      (maxReasoningEffort === 'high' ||
        maxReasoningEffort === 'xhigh' ||
        maxReasoningEffort === 'max') &&
      inferenceConfig.maxTokens != null
    ) {
      delete inferenceConfig.maxTokens;
      warnings.push({
        type: 'unsupported',
        feature: 'maxOutputTokens',
        details: `maxOutputTokens is not supported by ${this.modelId} when high reasoning is enabled and will be ignored`,
      });
    }

    if (useNativeStructuredOutput) {
      amazonBedrockOptions.additionalModelRequestFields = {
        ...amazonBedrockOptions.additionalModelRequestFields,
        output_config: {
          ...amazonBedrockOptions.additionalModelRequestFields?.output_config,
          format: {
            type: 'json_schema',
            schema: sanitizeJsonSchema(responseFormat!.schema!),
          },
        },
      };
    }

    if (isAnthropicThinkingEnabled && inferenceConfig.temperature != null) {
      delete inferenceConfig.temperature;
      warnings.push({
        type: 'unsupported',
        feature: 'temperature',
        details: 'temperature is not supported when thinking is enabled',
      });
    }

    if (isAnthropicThinkingEnabled && inferenceConfig.topP != null) {
      delete inferenceConfig.topP;
      warnings.push({
        type: 'unsupported',
        feature: 'topP',
        details: 'topP is not supported when thinking is enabled',
      });
    }

    if (isAnthropicThinkingEnabled && inferenceConfig.topK != null) {
      delete inferenceConfig.topK;
      warnings.push({
        type: 'unsupported',
        feature: 'topK',
        details: 'topK is not supported when thinking is enabled',
      });
    }

    // OpenAI models reject stopSequences on the Converse API. Models other
    // than gpt-oss also reject temperature and topP.
    if (isOpenAIModel) {
      const unsupportedFeatures = isOpenAIGptOssModel
        ? (['stopSequences'] as const)
        : (['temperature', 'topP', 'stopSequences'] as const);

      for (const feature of unsupportedFeatures) {
        if (inferenceConfig[feature] != null) {
          delete inferenceConfig[feature];
          warnings.push({
            type: 'unsupported',
            feature,
            details: `${feature} is not supported by this OpenAI model on the Converse API`,
          });
        }
      }
    }

    // Filter tool content from prompt when no tools are available
    const hasAnyTools = (toolConfig.tools?.length ?? 0) > 0 || additionalTools;
    let filteredPrompt = prompt;

    if (!hasAnyTools) {
      const hasToolContent = prompt.some(
        message =>
          'content' in message &&
          Array.isArray(message.content) &&
          message.content.some(
            part => part.type === 'tool-call' || part.type === 'tool-result',
          ),
      );

      if (hasToolContent) {
        filteredPrompt = prompt
          .map(message =>
            message.role === 'system'
              ? message
              : {
                  ...message,
                  content: message.content.filter(
                    part =>
                      part.type !== 'tool-call' && part.type !== 'tool-result',
                  ),
                },
          )
          .filter(
            message => message.role === 'system' || message.content.length > 0,
          ) as typeof prompt;

        warnings.push({
          type: 'unsupported',
          feature: 'toolContent',
          details:
            'Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.',
        });
      }
    }

    if (useJsonInstructionForStructuredOutput) {
      filteredPrompt = injectJsonInstructionIntoMessages({
        messages: filteredPrompt,
        schema: responseFormat!.schema,
        schemaSuffix:
          'You MUST answer with only a JSON object that matches the JSON schema above. Do not wrap it in markdown fences or include any other text.',
      });
    }

    const isMistral = isMistralModel(this.modelId);
    const { system, messages } = await convertToAmazonBedrockChatMessages(
      filteredPrompt,
      isMistral,
    );

    // Filter out reasoningConfig from amazonBedrock provider options to prevent sending it to Bedrock API
    const {
      reasoningConfig: _,
      additionalModelRequestFields: __,
      serviceTier: ___,
      structuredOutputMode: ____,
      requestMetadata: _____,
      ...filteredAmazonBedrockOptions
    } = providerOptions?.amazonBedrock ?? providerOptions?.bedrock ?? {};

    const resolvedRequestMetadata = amazonBedrockOptions.requestMetadata;

    const additionalModelResponseFieldPaths = isAnthropicModel
      ? ['/delta/stop_sequence']
      : undefined;

    return {
      command: {
        system,
        messages,
        additionalModelRequestFields:
          amazonBedrockOptions.additionalModelRequestFields,
        ...(additionalModelResponseFieldPaths && {
          additionalModelResponseFieldPaths,
        }),
        ...(Object.keys(inferenceConfig).length > 0 && {
          inferenceConfig,
        }),
        ...(amazonBedrockOptions.serviceTier != null && {
          serviceTier: {
            type: amazonBedrockOptions.serviceTier,
          },
        }),
        ...(resolvedRequestMetadata != null && {
          requestMetadata: resolvedRequestMetadata,
        }),
        ...filteredAmazonBedrockOptions,
        ...(toolConfig.tools !== undefined && toolConfig.tools.length > 0
          ? { toolConfig }
          : {}),
      },
      warnings,
      usesJsonInstruction: useJsonInstructionForStructuredOutput,
      usesJsonResponseTool: jsonResponseTool != null,
      betas,
    };
  }

  readonly supportedUrls: Record<string, RegExp[]> = {
    'image/*': [/^s3:\/\//],
    'video/*': [/^s3:\/\//],
  };

  private async getHeaders({
    headers,
  }: {
    headers: Record<string, string | undefined> | undefined;
  }) {
    return combineHeaders(
      this.config.headers ? await resolve(this.config.headers) : undefined,
      headers,
    );
  }

  async doGenerate(
    options: LanguageModelV4CallOptions,
  ): Promise<LanguageModelV4GenerateResult> {
    const {
      command: args,
      warnings,
      usesJsonInstruction,
      usesJsonResponseTool,
    } = await this.getArgs(options);

    const url = `${this.getUrl(this.modelId)}/converse`;
    const { value: response, responseHeaders } = await postJsonToApi({
      url,
      headers: await this.getHeaders({ headers: options.headers }),
      body: args,
      failedResponseHandler: createJsonErrorResponseHandler({
        errorSchema: AmazonBedrockErrorSchema,
        errorToMessage: error => `${error.message ?? 'Unknown error'}`,
      }),
      successfulResponseHandler: createJsonResponseHandler(
        AmazonBedrockResponseSchema,
      ),
      abortSignal: options.abortSignal,
      fetch: this.config.fetch,
    });

    const content: Array<LanguageModelV4Content> = [];
    let isJsonResponseFromTool = false;
    const jsonObjectTextExtractor = usesJsonInstruction
      ? new JsonObjectTextExtractor()
      : undefined;

    // map response content to content array
    for (const part of response.output.message.content) {
      // text
      const textParts =
        part.text != null
          ? [part.text]
          : (part.citationsContent?.content?.flatMap(content =>
              content.text == null ? [] : [content.text],
            ) ?? []);

      for (const text of textParts) {
        content.push({
          type: 'text',
          text: jsonObjectTextExtractor?.process(text) ?? text,
        });
      }

      // reasoning
      if (part.reasoningContent) {
        if ('reasoningText' in part.reasoningContent) {
          const reasoning: LanguageModelV4Reasoning = {
            type: 'reasoning',
            text: part.reasoningContent.reasoningText.text,
          };

          if (part.reasoningContent.reasoningText.signature) {
            const reasoningPayload: AmazonBedrockReasoningMetadata = {
              signature: part.reasoningContent.reasoningText.signature,
            };
            reasoning.providerMetadata = {
              amazonBedrock: reasoningPayload,
              bedrock: reasoningPayload,
            };
          }

          content.push(reasoning);
        } else if ('redactedReasoning' in part.reasoningContent) {
          const redactedPayload: AmazonBedrockReasoningMetadata = {
            redactedData: part.reasoningContent.redactedReasoning.data ?? '',
          };
          content.push({
            type: 'reasoning',
            text: '',
            providerMetadata: {
              amazonBedrock: redactedPayload,
              bedrock: redactedPayload,
            },
          });
        } else if ('redactedContent' in part.reasoningContent) {
          const redactedPayload: AmazonBedrockReasoningMetadata = {
            redactedContent: part.reasoningContent.redactedContent,
          };
          content.push({
            type: 'reasoning',
            text: '',
            providerMetadata: {
              amazonBedrock: redactedPayload,
              bedrock: redactedPayload,
            },
          });
        }
      }

      // tool calls
      if (part.toolUse) {
        const isJsonResponseTool =
          usesJsonResponseTool && part.toolUse.name === 'json';

        if (isJsonResponseTool) {
          isJsonResponseFromTool = true;
          // when a json response tool is used, the tool call becomes the text:
          content.push({
            type: 'text',
            text: JSON.stringify(part.toolUse.input),
          });
        } else {
          const isMistral = isMistralModel(this.modelId);
          const rawToolCallId =
            part.toolUse?.toolUseId || this.config.generateId();
          content.push({
            type: 'tool-call' as const,
            toolCallId: normalizeToolCallId(rawToolCallId, isMistral),
            toolName: part.toolUse?.name ?? `tool-${this.config.generateId()}`,
            input: JSON.stringify(part.toolUse?.input ?? {}),
          });
        }
      }
    }

    // provider metadata:
    const stopSequence =
      response.additionalModelResponseFields?.delta?.stop_sequence ?? null;

    const providerMetadataPayload =
      response.trace ||
      response.usage ||
      response.performanceConfig ||
      response.serviceTier ||
      isJsonResponseFromTool ||
      stopSequence
        ? {
            ...(response.trace && typeof response.trace === 'object'
              ? { trace: response.trace as JSONObject }
              : {}),
            ...(response.performanceConfig && {
              performanceConfig: response.performanceConfig,
            }),
            ...(response.serviceTier && {
              serviceTier: response.serviceTier,
            }),
            ...((response.usage?.cacheWriteInputTokens != null ||
              response.usage?.cacheDetails != null) && {
              usage: {
                ...(response.usage.cacheWriteInputTokens != null && {
                  cacheWriteInputTokens: response.usage.cacheWriteInputTokens,
                }),
                ...(response.usage.cacheDetails != null && {
                  cacheDetails: response.usage.cacheDetails,
                }),
              },
            }),
            ...(isJsonResponseFromTool && { isJsonResponseFromTool: true }),
            stopSequence,
          }
        : undefined;

    const providerMetadata = providerMetadataPayload
      ? {
          amazonBedrock: providerMetadataPayload,
          bedrock: providerMetadataPayload,
        }
      : undefined;

    return {
      content,
      finishReason: {
        unified: mapAmazonBedrockFinishReason(
          response.stopReason as AmazonBedrockStopReason,
          isJsonResponseFromTool,
        ),
        raw: response.stopReason ?? undefined,
      },
      usage: convertAmazonBedrockUsage(response.usage),
      response: {
        id: responseHeaders?.['x-amzn-requestid'] ?? undefined,
        timestamp:
          responseHeaders?.['date'] != null
            ? new Date(responseHeaders['date'])
            : undefined,
        modelId: this.modelId,
        headers: responseHeaders,
      },
      warnings,
      request: { body: args },
      ...(providerMetadata && { providerMetadata }),
    };
  }

  async doStream(
    options: LanguageModelV4CallOptions,
  ): Promise<LanguageModelV4StreamResult> {
    const {
      command: args,
      warnings,
      usesJsonInstruction,
      usesJsonResponseTool,
    } = await this.getArgs(options);
    const modelId = this.modelId;
    const isMistral = isMistralModel(modelId);
    const url = `${this.getUrl(modelId)}/converse-stream`;

    const { value: response, responseHeaders } = await postJsonToApi({
      url,
      headers: await this.getHeaders({ headers: options.headers }),
      body: args,
      failedResponseHandler: amazonBedrockFailedResponseHandler,
      successfulResponseHandler: createAmazonBedrockEventStreamResponseHandler(
        AmazonBedrockStreamSchema,
      ),
      abortSignal: options.abortSignal,
      fetch: this.config.fetch,
    });

    // A clean end without the provider's completion signal is interrupted.
    let finishReason: LanguageModelV4FinishReason = {
      unified: 'interrupted',
      raw: undefined,
    };
    let usage: AmazonBedrockUsage | undefined = undefined;
    let providerMetadata: SharedV4ProviderMetadata | undefined = undefined;
    let isJsonResponseFromTool = false;
    let stopSequence: string | null = null;
    const jsonObjectTextExtractor = usesJsonInstruction
      ? new JsonObjectTextExtractor()
      : undefined;

    const contentBlocks: Record<
      number,
      | {
          type: 'tool-call';
          toolCallId: string;
          toolName: string;
          jsonText: string;
          isJsonResponseTool?: boolean;
        }
      | { type: 'text' }
      | { type: 'reasoning'; redactedContent?: string }
    > = {};

    return {
      stream: response.pipeThrough(
        new TransformStream<
          ParseResult<z.infer<typeof AmazonBedrockStreamSchema>>,
          LanguageModelV4StreamPart
        >({
          start(controller) {
            controller.enqueue({ type: 'stream-start', warnings });
            controller.enqueue({
              type: 'response-metadata',
              id: responseHeaders?.['x-amzn-requestid'] ?? undefined,
              timestamp:
                responseHeaders?.['date'] != null
                  ? new Date(responseHeaders['date'])
                  : undefined,
              modelId,
            });
          },

          transform(chunk, controller) {
            function enqueueError(
              type: AmazonBedrockStreamErrorType,
              amazonBedrockError: Record<string, unknown>,
            ) {
              finishReason = { unified: 'error', raw: undefined };
              controller.enqueue({
                type: 'error',
                error: createAmazonBedrockStreamError({
                  type,
                  error: amazonBedrockError,
                  data: chunk.rawValue,
                }),
              });
            }

            // Emit raw chunk if requested (before anything else)
            if (options.includeRawChunks) {
              controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
            }

            // handle failed chunk parsing / validation:
            if (!chunk.success) {
              finishReason = { unified: 'error', raw: undefined };
              controller.enqueue({ type: 'error', error: chunk.error });
              return;
            }

            const value = chunk.value;

            // handle errors:
            if (value.internalServerException) {
              enqueueError(
                'internalServerException',
                value.internalServerException,
              );
              return;
            }
            if (value.modelStreamErrorException) {
              enqueueError(
                'modelStreamErrorException',
                value.modelStreamErrorException,
              );
              return;
            }
            if (value.serviceUnavailableException) {
              enqueueError(
                'serviceUnavailableException',
                value.serviceUnavailableException,
              );
              return;
            }
            if (value.throttlingException) {
              enqueueError('throttlingException', value.throttlingException);
              return;
            }
            if (value.validationException) {
              enqueueError('validationException', value.validationException);
              return;
            }

            if (value.messageStop) {
              finishReason = {
                unified: mapAmazonBedrockFinishReason(
                  value.messageStop.stopReason as AmazonBedrockStopReason,
                  isJsonResponseFromTool,
                ),
                raw: value.messageStop.stopReason ?? undefined,
              };
              stopSequence =
                value.messageStop.additionalModelResponseFields?.delta
                  ?.stop_sequence ?? null;
            }

            if (value.metadata) {
              if (value.metadata.usage) {
                usage = value.metadata.usage;
              }

              const cacheUsage =
                value.metadata.usage?.cacheWriteInputTokens != null ||
                value.metadata.usage?.cacheDetails != null
                  ? {
                      usage: {
                        ...(value.metadata.usage?.cacheWriteInputTokens !=
                          null && {
                          cacheWriteInputTokens:
                            value.metadata.usage.cacheWriteInputTokens,
                        }),
                        ...(value.metadata.usage?.cacheDetails != null && {
                          cacheDetails: value.metadata.usage.cacheDetails,
                        }),
                      },
                    }
                  : undefined;

              const trace = value.metadata.trace
                ? {
                    trace: value.metadata.trace as JSONObject,
                  }
                : undefined;

              if (
                cacheUsage ||
                trace ||
                value.metadata.performanceConfig ||
                value.metadata.serviceTier
              ) {
                const metadataPayload = {
                  ...cacheUsage,
                  ...trace,
                  ...(value.metadata.performanceConfig && {
                    performanceConfig: value.metadata.performanceConfig,
                  }),
                  ...(value.metadata.serviceTier && {
                    serviceTier: value.metadata.serviceTier,
                  }),
                };
                providerMetadata = {
                  amazonBedrock: metadataPayload,
                  bedrock: metadataPayload,
                };
              }
            }

            if (
              value.contentBlockStart?.contentBlockIndex != null &&
              !value.contentBlockStart?.start?.toolUse
            ) {
              const blockIndex = value.contentBlockStart.contentBlockIndex;
              contentBlocks[blockIndex] = { type: 'text' };
              controller.enqueue({
                type: 'text-start',
                id: String(blockIndex),
              });
            }

            if (
              value.contentBlockDelta?.delta &&
              'text' in value.contentBlockDelta.delta &&
              value.contentBlockDelta.delta.text
            ) {
              const blockIndex = value.contentBlockDelta.contentBlockIndex || 0;

              if (contentBlocks[blockIndex] == null) {
                contentBlocks[blockIndex] = { type: 'text' };

                controller.enqueue({
                  type: 'text-start',
                  id: String(blockIndex),
                });
              }

              const textDelta =
                jsonObjectTextExtractor?.process(
                  value.contentBlockDelta.delta.text,
                ) ?? value.contentBlockDelta.delta.text;

              if (textDelta.length > 0) {
                controller.enqueue({
                  type: 'text-delta',
                  id: String(blockIndex),
                  delta: textDelta,
                });
              }
            }

            if (value.contentBlockStop?.contentBlockIndex != null) {
              const blockIndex = value.contentBlockStop.contentBlockIndex;
              const contentBlock = contentBlocks[blockIndex];

              if (contentBlock != null) {
                if (contentBlock.type === 'reasoning') {
                  const redactedPayload: AmazonBedrockReasoningMetadata | null =
                    contentBlock.redactedContent != null
                      ? { redactedContent: contentBlock.redactedContent }
                      : null;
                  controller.enqueue({
                    type: 'reasoning-end',
                    id: String(blockIndex),
                    ...(redactedPayload != null
                      ? {
                          providerMetadata: {
                            amazonBedrock: redactedPayload,
                            bedrock: redactedPayload,
                          },
                        }
                      : {}),
                  });
                } else if (contentBlock.type === 'text') {
                  controller.enqueue({
                    type: 'text-end',
                    id: String(blockIndex),
                  });
                } else if (contentBlock.type === 'tool-call') {
                  if (contentBlock.isJsonResponseTool) {
                    isJsonResponseFromTool = true;
                    // when this specific tool is the json response tool, emit the tool input as text
                    controller.enqueue({
                      type: 'text-start',
                      id: String(blockIndex),
                    });
                    controller.enqueue({
                      type: 'text-delta',
                      id: String(blockIndex),
                      delta: contentBlock.jsonText,
                    });
                    controller.enqueue({
                      type: 'text-end',
                      id: String(blockIndex),
                    });
                  } else {
                    controller.enqueue({
                      type: 'tool-input-end',
                      id: contentBlock.toolCallId,
                    });
                    controller.enqueue({
                      type: 'tool-call',
                      toolCallId: contentBlock.toolCallId,
                      toolName: contentBlock.toolName,
                      input:
                        contentBlock.jsonText === ''
                          ? '{}'
                          : contentBlock.jsonText,
                    });
                  }
                }

                delete contentBlocks[blockIndex];
              }
            }

            if (
              value.contentBlockDelta?.delta &&
              'reasoningContent' in value.contentBlockDelta.delta &&
              value.contentBlockDelta.delta.reasoningContent
            ) {
              const blockIndex = value.contentBlockDelta.contentBlockIndex || 0;
              const reasoningContent =
                value.contentBlockDelta.delta.reasoningContent;

              if ('text' in reasoningContent && reasoningContent.text) {
                if (contentBlocks[blockIndex] == null) {
                  contentBlocks[blockIndex] = { type: 'reasoning' };
                  controller.enqueue({
                    type: 'reasoning-start',
                    id: String(blockIndex),
                  });
                }

                controller.enqueue({
                  type: 'reasoning-delta',
                  id: String(blockIndex),
                  delta: reasoningContent.text,
                });
              } else if (
                'signature' in reasoningContent &&
                reasoningContent.signature
              ) {
                if (contentBlocks[blockIndex] == null) {
                  contentBlocks[blockIndex] = { type: 'reasoning' };
                  controller.enqueue({
                    type: 'reasoning-start',
                    id: String(blockIndex),
                  });
                }
                {
                  const signaturePayload: AmazonBedrockReasoningMetadata = {
                    signature: reasoningContent.signature,
                  };
                  controller.enqueue({
                    type: 'reasoning-delta',
                    id: String(blockIndex),
                    delta: '',
                    providerMetadata: {
                      amazonBedrock: signaturePayload,
                      bedrock: signaturePayload,
                    },
                  });
                }
              } else if ('data' in reasoningContent && reasoningContent.data) {
                if (contentBlocks[blockIndex] == null) {
                  contentBlocks[blockIndex] = { type: 'reasoning' };
                  controller.enqueue({
                    type: 'reasoning-start',
                    id: String(blockIndex),
                  });
                }
                {
                  const redactedPayload: AmazonBedrockReasoningMetadata = {
                    redactedData: reasoningContent.data,
                  };
                  controller.enqueue({
                    type: 'reasoning-delta',
                    id: String(blockIndex),
                    delta: '',
                    providerMetadata: {
                      amazonBedrock: redactedPayload,
                      bedrock: redactedPayload,
                    },
                  });
                }
              } else if (
                'redactedContent' in reasoningContent &&
                reasoningContent.redactedContent
              ) {
                if (contentBlocks[blockIndex] == null) {
                  contentBlocks[blockIndex] = { type: 'reasoning' };
                  controller.enqueue({
                    type: 'reasoning-start',
                    id: String(blockIndex),
                  });
                }

                const contentBlock = contentBlocks[blockIndex];
                if (contentBlock.type === 'reasoning') {
                  // accumulate and attach once via reasoning-end: the merged
                  // provider metadata of a reasoning part is last-write-wins,
                  // so per-delta metadata would drop earlier chunks
                  contentBlock.redactedContent =
                    (contentBlock.redactedContent ?? '') +
                    reasoningContent.redactedContent;
                }
              }
            }

            const contentBlockStart = value.contentBlockStart;
            if (contentBlockStart?.start?.toolUse != null) {
              const toolUse = contentBlockStart.start.toolUse;
              const blockIndex = contentBlockStart.contentBlockIndex!;
              const isJsonResponseTool =
                usesJsonResponseTool && toolUse.name === 'json';

              const normalizedToolCallId = normalizeToolCallId(
                toolUse.toolUseId!,
                isMistral,
              );
              contentBlocks[blockIndex] = {
                type: 'tool-call',
                toolCallId: normalizedToolCallId,
                toolName: toolUse.name!,
                jsonText: '',
                isJsonResponseTool,
              };

              // when this specific tool is the json response tool, we don't emit tool events
              if (!isJsonResponseTool) {
                controller.enqueue({
                  type: 'tool-input-start',
                  id: normalizedToolCallId,
                  toolName: toolUse.name!,
                });
              }
            }

            const contentBlockDelta = value.contentBlockDelta;
            if (
              contentBlockDelta?.delta &&
              'toolUse' in contentBlockDelta.delta &&
              contentBlockDelta.delta.toolUse
            ) {
              const blockIndex = contentBlockDelta.contentBlockIndex!;
              const contentBlock = contentBlocks[blockIndex];

              if (contentBlock?.type === 'tool-call') {
                const delta = contentBlockDelta.delta.toolUse.input ?? '';

                // when this specific tool is the json response tool, we don't emit tool events
                if (!contentBlock.isJsonResponseTool) {
                  controller.enqueue({
                    type: 'tool-input-delta',
                    id: contentBlock.toolCallId,
                    delta: delta,
                  });
                }

                contentBlock.jsonText += delta;
              }
            }
          },
          flush(controller) {
            // Update provider metadata with isJsonResponseFromTool and stopSequence if needed
            if (isJsonResponseFromTool || stopSequence != null) {
              const updatePayload = {
                ...(providerMetadata?.amazonBedrock ??
                  providerMetadata?.bedrock),
                ...(isJsonResponseFromTool && {
                  isJsonResponseFromTool: true,
                }),
                stopSequence,
              };
              providerMetadata = {
                ...providerMetadata,
                amazonBedrock: updatePayload,
                bedrock: updatePayload,
              };
            }

            controller.enqueue({
              type: 'finish',
              finishReason,
              usage: convertAmazonBedrockUsage(usage),
              ...(providerMetadata && { providerMetadata }),
            });
          },
        }),
      ),
      request: { body: args },
      response: { headers: responseHeaders },
    };
  }

  private getUrl(modelId: string) {
    return `${this.config.baseUrl()}/model/${encodeURIComponent(modelId)}`;
  }
}

class JsonObjectTextExtractor {
  private started = false;
  private completed = false;
  private depth = 0;
  private inString = false;
  private escaped = false;

  process(text: string): string {
    let result = '';

    for (const character of text) {
      if (this.completed) {
        break;
      }

      if (!this.started) {
        if (character !== '{') {
          continue;
        }

        this.started = true;
        this.depth = 1;
        result += character;
        continue;
      }

      result += character;

      if (this.escaped) {
        this.escaped = false;
        continue;
      }

      if (character === '\\' && this.inString) {
        this.escaped = true;
        continue;
      }

      if (character === '"') {
        this.inString = !this.inString;
        continue;
      }

      if (this.inString) {
        continue;
      }

      if (character === '{') {
        this.depth++;
      } else if (character === '}') {
        this.depth--;

        if (this.depth === 0) {
          this.completed = true;
        }
      }
    }

    return result;
  }
}

const AmazonBedrockStopReasonSchema = z.union([
  z.enum(BEDROCK_STOP_REASONS),
  z.string(),
]);

const AmazonBedrockAdditionalModelResponseFieldsSchema = z
  .object({
    delta: z
      .object({
        stop_sequence: z.string().nullish(),
      })
      .nullish(),
  })
  .catchall(z.unknown());

const AmazonBedrockToolUseSchema = z.object({
  toolUseId: z.string(),
  name: z.string(),
  input: z.unknown().optional(),
});

const AmazonBedrockReasoningTextSchema = () =>
  z.object({
    signature: z.string().nullish(),
    text: z.string(),
  });

const AmazonBedrockRedactedReasoningSchema = () =>
  z.object({
    data: z.string(),
  });

const AmazonBedrockCacheDetailSchema = z
  .object({
    inputTokens: z.number(),
    ttl: z.string(),
  })
  .catchall(z.json());

// limited version of the schema, focused on what is needed for the implementation
// this approach limits breakages when the API changes and increases efficiency
const AmazonBedrockResponseSchema = lazySchema(() =>
  zodSchema(
    z.object({
      metrics: z
        .object({
          latencyMs: z.number(),
        })
        .nullish(),
      output: z.object({
        message: z.object({
          content: z.array(
            z.object({
              text: z.string().nullish(),
              citationsContent: z
                .object({
                  content: z
                    .array(
                      z.object({
                        text: z.string().nullish(),
                      }),
                    )
                    .nullish(),
                })
                .nullish(),
              toolUse: AmazonBedrockToolUseSchema.nullish(),
              reasoningContent: z
                .union([
                  z.object({
                    reasoningText: AmazonBedrockReasoningTextSchema(),
                  }),
                  z.object({
                    redactedReasoning: AmazonBedrockRedactedReasoningSchema(),
                  }),
                  // `redactedContent` is a member of the documented
                  // ReasoningContentBlock union. OpenAI models on Bedrock
                  // (e.g. `us.openai.gpt-5.6-luna`) return their encrypted
                  // reasoning in this shape.
                  z.object({
                    redactedContent: z.string(),
                  }),
                ])
                .nullish(),
            }),
          ),
          role: z.string(),
        }),
      }),
      stopReason: AmazonBedrockStopReasonSchema,
      additionalModelResponseFields:
        AmazonBedrockAdditionalModelResponseFieldsSchema.nullish(),
      trace: z.unknown().nullish(),
      performanceConfig: z.object({ latency: z.string() }).nullish(),
      serviceTier: z.object({ type: z.string() }).nullish(),
      usage: z
        .object({
          inputTokens: z.number(),
          outputTokens: z.number(),
          totalTokens: z.number(),
          cacheReadInputTokens: z.number().nullish(),
          cacheWriteInputTokens: z.number().nullish(),
          cacheDetails: z.array(AmazonBedrockCacheDetailSchema).nullish(),
        })
        .catchall(z.json()),
    }),
  ),
);

// limited version of the schema, focussed on what is needed for the implementation
// this approach limits breakages when the API changes and increases efficiency
const AmazonBedrockStreamSchema = z.object({
  contentBlockDelta: z
    .object({
      contentBlockIndex: z.number(),
      delta: z
        .union([
          z.object({ text: z.string() }),
          z.object({
            citation: z.record(z.string(), z.unknown()),
          }),
          z.object({ toolUse: z.object({ input: z.string() }) }),
          z.object({
            reasoningContent: z.object({ text: z.string() }),
          }),
          z.object({
            reasoningContent: z.object({
              signature: z.string(),
            }),
          }),
          z.object({
            reasoningContent: z.object({ data: z.string() }),
          }),
          // `redactedContent` is a member of the documented
          // ReasoningContentBlockDelta union. OpenAI models on Bedrock stream
          // their encrypted reasoning in this shape.
          z.object({
            reasoningContent: z.object({ redactedContent: z.string() }),
          }),
        ])
        .nullish(),
    })
    .nullish(),
  contentBlockStart: z
    .object({
      contentBlockIndex: z.number(),
      start: z
        .object({
          toolUse: AmazonBedrockToolUseSchema.nullish(),
        })
        .nullish(),
    })
    .nullish(),
  contentBlockStop: z
    .object({
      contentBlockIndex: z.number(),
    })
    .nullish(),
  internalServerException: z.record(z.string(), z.unknown()).nullish(),
  messageStop: z
    .object({
      additionalModelResponseFields:
        AmazonBedrockAdditionalModelResponseFieldsSchema.nullish(),
      stopReason: AmazonBedrockStopReasonSchema,
    })
    .nullish(),
  metadata: z
    .object({
      trace: z.unknown().nullish(),
      performanceConfig: z.object({ latency: z.string() }).nullish(),
      serviceTier: z.object({ type: z.string() }).nullish(),
      usage: z
        .object({
          cacheReadInputTokens: z.number().nullish(),
          cacheWriteInputTokens: z.number().nullish(),
          cacheDetails: z.array(AmazonBedrockCacheDetailSchema).nullish(),
          inputTokens: z.number(),
          outputTokens: z.number(),
          totalTokens: z.number().optional(),
        })
        .catchall(z.json())
        .nullish(),
    })
    .nullish(),
  modelStreamErrorException: z.record(z.string(), z.unknown()).nullish(),
  serviceUnavailableException: z.record(z.string(), z.unknown()).nullish(),
  throttlingException: z.record(z.string(), z.unknown()).nullish(),
  validationException: z.record(z.string(), z.unknown()).nullish(),
});

export {
  amazonBedrockReasoningMetadataSchema,
  type AmazonBedrockReasoningMetadata,
} from './amazon-bedrock-reasoning-metadata';

// Effort maps only list the supported levels. Other levels are mapped to the
// closest supported level by mapReasoningToProviderEffort.
const amazonBedrockReasoningEffortMap = {
  low: 'low',
  medium: 'medium',
  high: 'high',
  max: 'max',
} as const;

const amazonNovaReasoningEffortMap = {
  low: 'low',
  medium: 'medium',
  high: 'high',
} as const;

const openAIGptOssReasoningEffortMap = {
  low: 'low',
  medium: 'medium',
  high: 'high',
} as const;

// The reasoning efforts of other OpenAI models are not known, so reasoning
// values are sent directly.
const openAIReasoningEffortMap = {
  none: 'none',
  minimal: 'minimal',
  low: 'low',
  medium: 'medium',
  high: 'high',
  xhigh: 'xhigh',
  max: 'max',
} as const;

function isNova2ReasoningModel(modelId: string): boolean {
  return getAmazonBedrockLanguageModelEntry(modelId).requestConfig.language
    .nova2Reasoning;
}

function resolveAmazonBedrockReasoningConfig({
  reasoning,
  amazonBedrockOptions,
  warnings,
  isAnthropicModel,
  isOpenAIModel,
  modelId,
}: {
  reasoning: LanguageModelV4CallOptions['reasoning'];
  amazonBedrockOptions: AmazonBedrockLanguageModelChatOptions;
  warnings: SharedV4Warning[];
  isAnthropicModel: boolean;
  isOpenAIModel: boolean;
  modelId: string;
}): AmazonBedrockLanguageModelChatOptions {
  const reasoningConfig = amazonBedrockOptions.reasoningConfig;

  if (!isCustomReasoning(reasoning)) {
    return amazonBedrockOptions;
  }

  const result = { ...amazonBedrockOptions };

  // Do not derive effort or budget from portable reasoning when thinking is
  // explicitly disabled. Clone the configuration before removing those fields.
  if (reasoningConfig?.type === 'disabled') {
    result.reasoningConfig = { ...reasoningConfig };
    delete result.reasoningConfig.maxReasoningEffort;
    delete result.reasoningConfig.budgetTokens;
    return result;
  }

  const isNovaReasoningModel = isNova2ReasoningModel(modelId);
  const supportsPortableReasoning =
    isOpenAIModel || isNovaReasoningModel || reasoningConfig != null;

  if (isAnthropicModel) {
    const capabilities = getModelCapabilities(modelId);

    if (reasoning === 'none') {
      result.reasoningConfig = { type: 'disabled' };
    } else if (capabilities.supportsAdaptiveThinking) {
      const effort =
        reasoningConfig?.maxReasoningEffort ??
        mapReasoningToProviderEffort({
          reasoning,
          effortMap: {
            ...amazonBedrockReasoningEffortMap,
            ...(capabilities.supportsXhighEffort && {
              xhigh: 'xhigh' as const,
            }),
          },
          warnings,
        });
      result.reasoningConfig = {
        ...reasoningConfig,
        type: reasoningConfig?.type ?? 'adaptive',
        maxReasoningEffort: effort,
      };
    } else {
      const budgetTokens =
        reasoningConfig?.budgetTokens ??
        mapReasoningToProviderBudget({
          reasoning,
          maxOutputTokens: capabilities.maxOutputTokens,
          maxReasoningBudget: capabilities.maxOutputTokens,
          warnings,
        });
      result.reasoningConfig = {
        ...reasoningConfig,
        type: reasoningConfig?.type ?? 'enabled',
        budgetTokens,
      };
    }
  } else if (reasoning === 'none' && isNovaReasoningModel) {
    // Nova 2 reasoning is disabled unless a reasoning effort is sent, so
    // there is nothing to add.
  } else {
    if (supportsPortableReasoning) {
      const effort =
        reasoningConfig?.maxReasoningEffort ??
        // OpenAI efforts such as 'none' are sent as `reasoning_effort` even
        // though they are not accepted in the reasoningConfig provider option.
        (mapReasoningToProviderEffort({
          reasoning,
          effortMap: isNovaReasoningModel
            ? amazonNovaReasoningEffortMap
            : isOpenAIModel
              ? getAmazonBedrockLanguageModelEntry(modelId).requestConfig
                  .language.openaiGptOssReasoning
                ? openAIGptOssReasoningEffortMap
                : openAIReasoningEffortMap
              : amazonBedrockReasoningEffortMap,
          warnings,
        }) as NonNullable<
          AmazonBedrockLanguageModelChatOptions['reasoningConfig']
        >['maxReasoningEffort']);
      result.reasoningConfig = {
        ...reasoningConfig,
        ...(isNovaReasoningModel && {
          type: reasoningConfig?.type ?? 'enabled',
        }),
        maxReasoningEffort: effort,
      };
    } else {
      warnings.push({
        type: 'unsupported',
        feature: 'reasoning',
        details:
          'Portable reasoning is not supported for this model and will be ignored. If the model supports a provider-specific reasoning configuration, use providerOptions.amazonBedrock.reasoningConfig.',
      });
    }
  }

  return result;
}