From bf6b19370b839759a13feffd9c01af8f865a9af5 Mon Sep 17 00:00:00 2001 From: bensonz Date: Tue, 10 Feb 2026 12:26:27 +0800 Subject: [PATCH 1/6] fix: strip unsupported cache_control fields for Bedrock compatibility Claude Code v2.1.38+ sends cache_control with a 'scope' field (e.g. {type: 'ephemeral', scope: 'global'}) on system prompt blocks. AWS Bedrock only accepts {type: 'ephemeral'} and rejects the extra 'scope' field with a ValidationException. Added _sanitizeCacheControl() that recursively strips unsupported fields from cache_control objects in system, messages, and tools before forwarding to Bedrock. --- src/services/bedrockRelayService.js | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index 87e65105..49982dbb 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -511,6 +511,33 @@ class BedrockRelayService { return this.defaultRegion } + // Sanitize cache_control fields for Bedrock compatibility. + // Bedrock only supports { type: "ephemeral" } — extra fields like "scope" + // (added in Claude Code v2.1.38+) cause ValidationException. + _sanitizeCacheControl(obj) { + if (obj == null || typeof obj !== 'object') return obj + + if (Array.isArray(obj)) { + obj.forEach((item) => this._sanitizeCacheControl(item)) + return obj + } + + if (obj.cache_control && typeof obj.cache_control === 'object') { + // Keep only the "type" field that Bedrock accepts + obj.cache_control = { type: obj.cache_control.type || 'ephemeral' } + } + + // Recurse into known nested structures (messages[].content, tool input_schema, etc.) + for (const key of Object.keys(obj)) { + const val = obj[key] + if (val && typeof val === 'object') { + this._sanitizeCacheControl(val) + } + } + + return obj + } + // 转换Claude格式请求到Bedrock格式 _convertToBedrockFormat(requestBody) { const bedrockPayload = { @@ -550,6 +577,9 @@ class BedrockRelayService { bedrockPayload.tool_choice = requestBody.tool_choice } + // Sanitize cache_control for Bedrock compatibility (strip unsupported fields like "scope") + this._sanitizeCacheControl(bedrockPayload) + return bedrockPayload } From 6acdd0ba5f16205e6f75e58ebd8f92a0dd7b3e34 Mon Sep 17 00:00:00 2001 From: bensonz Date: Tue, 10 Feb 2026 14:12:41 +0800 Subject: [PATCH 2/6] fix: add claude-opus-4-6 mapping and strip [1m] suffix for Bedrock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add model mapping: claude-opus-4-6 → us.anthropic.claude-opus-4-6-v1 - Strip [1m] suffix before model lookup (Claude Code sends e.g. claude-opus-4-6[1m] for 1M context variant) - Bedrock supports 1M context natively for Opus 4.6, so the same model ID works — the [1m] suffix just needs to be removed --- src/services/bedrockRelayService.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index 49982dbb..fa85a2ff 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -433,8 +433,15 @@ class BedrockRelayService { // 将标准Claude模型名映射为Bedrock格式 _mapToBedrockModel(modelName) { + // Strip [1m] suffix (long context variant) — Bedrock uses the same model ID + // but supports 1M context natively for models that have it + const cleanModelName = modelName.replace(/\[1m\]$/, '') + // 标准Claude模型名到Bedrock模型名的映射表 const modelMapping = { + // Claude Opus 4.6 + 'claude-opus-4-6': 'us.anthropic.claude-opus-4-6-v1', + // Claude 4.5 Opus 'claude-opus-4-5': 'us.anthropic.claude-opus-4-5-20251101-v1:0', 'claude-opus-4-5-20251101': 'us.anthropic.claude-opus-4-5-20251101-v1:0', @@ -479,21 +486,21 @@ class BedrockRelayService { // 如果已经是Bedrock格式,直接返回 // Bedrock模型格式:{region}.anthropic.{model-name} 或 anthropic.{model-name} - if (modelName.includes('.anthropic.') || modelName.startsWith('anthropic.')) { - return modelName + if (cleanModelName.includes('.anthropic.') || cleanModelName.startsWith('anthropic.')) { + return cleanModelName } // 查找映射 - const mappedModel = modelMapping[modelName] + const mappedModel = modelMapping[cleanModelName] if (mappedModel) { return mappedModel } // 如果没有找到映射,返回原始模型名(可能会导致错误,但保持向后兼容) - logger.warn(`⚠️ 未找到模型映射: ${modelName},使用原始模型名`, { + logger.warn(`⚠️ 未找到模型映射: ${cleanModelName},使用原始模型名`, { metadata: { originalModel: modelName } }) - return modelName + return cleanModelName } // 选择使用的区域 From 7aa80156fc2096c4cc58958e0c9a8f013c35c9ee Mon Sep 17 00:00:00 2001 From: bensonz Date: Tue, 10 Feb 2026 14:14:38 +0800 Subject: [PATCH 3/6] fix: use global inference profile for claude-opus-4-6 mapping Use global.anthropic.claude-opus-4-6-v1 (cross-region inference profile) instead of us.anthropic prefix for better availability. --- src/services/bedrockRelayService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index fa85a2ff..81a6d3a7 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -440,7 +440,7 @@ class BedrockRelayService { // 标准Claude模型名到Bedrock模型名的映射表 const modelMapping = { // Claude Opus 4.6 - 'claude-opus-4-6': 'us.anthropic.claude-opus-4-6-v1', + 'claude-opus-4-6': 'global.anthropic.claude-opus-4-6-v1', // Claude 4.5 Opus 'claude-opus-4-5': 'us.anthropic.claude-opus-4-5-20251101-v1:0', From 8967e866ed983db82cb2cb5ac02e63496b81ead5 Mon Sep 17 00:00:00 2001 From: bensonz Date: Tue, 10 Feb 2026 14:25:16 +0800 Subject: [PATCH 4/6] feat: support 1M context window via anthropic_beta header for Bedrock When Claude Code sends a model with [1m] suffix (e.g. claude-opus-4-6[1m]), the relay now adds 'context-1m-2025-08-07' to the anthropic_beta array in the Bedrock request body. This enables the 1M context window preview on Bedrock for supported models (Opus 4.6, Sonnet 4.5, Sonnet 4). Per Anthropic docs: the 1M context requires the beta header on all platforms including Amazon Bedrock. --- src/services/bedrockRelayService.js | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index 81a6d3a7..bd8bc3d3 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -136,12 +136,12 @@ class BedrockRelayService { } } - const modelId = this._selectModel(requestBody, bedrockAccount) + const { modelId, isLongContext } = this._selectModel(requestBody, bedrockAccount) const region = this._selectRegion(modelId, bedrockAccount) const client = this._getBedrockClient(region, bedrockAccount) // 转换请求格式为Bedrock格式 - const bedrockPayload = this._convertToBedrockFormat(requestBody) + const bedrockPayload = this._convertToBedrockFormat(requestBody, { isLongContext }) const command = new InvokeModelCommand({ modelId, @@ -277,12 +277,12 @@ class BedrockRelayService { } } - const modelId = this._selectModel(requestBody, bedrockAccount) + const { modelId, isLongContext } = this._selectModel(requestBody, bedrockAccount) const region = this._selectRegion(modelId, bedrockAccount) const client = this._getBedrockClient(region, bedrockAccount) // 转换请求格式为Bedrock格式 - const bedrockPayload = this._convertToBedrockFormat(requestBody) + const bedrockPayload = this._convertToBedrockFormat(requestBody, { isLongContext }) const command = new InvokeModelWithResponseStreamCommand({ modelId, @@ -399,6 +399,7 @@ class BedrockRelayService { } // 选择使用的模型 + // Returns { modelId, isLongContext } _selectModel(requestBody, bedrockAccount) { let selectedModel @@ -420,15 +421,18 @@ class BedrockRelayService { logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } }) } + // Detect [1m] long context variant before mapping + const isLongContext = selectedModel.includes('[1m]') + // 如果是标准Claude模型名,需要映射为Bedrock格式 const bedrockModel = this._mapToBedrockModel(selectedModel) - if (bedrockModel !== selectedModel) { + if (bedrockModel !== selectedModel.replace(/\[1m\]$/, '')) { logger.info(`🔄 模型映射: ${selectedModel} → ${bedrockModel}`, { - metadata: { originalModel: selectedModel, bedrockModel } + metadata: { originalModel: selectedModel, bedrockModel, isLongContext } }) } - return bedrockModel + return { modelId: bedrockModel, isLongContext } } // 将标准Claude模型名映射为Bedrock格式 @@ -546,13 +550,23 @@ class BedrockRelayService { } // 转换Claude格式请求到Bedrock格式 - _convertToBedrockFormat(requestBody) { + _convertToBedrockFormat(requestBody, { isLongContext = false } = {}) { const bedrockPayload = { anthropic_version: 'bedrock-2023-05-31', max_tokens: Math.min(requestBody.max_tokens || this.maxOutputTokens, this.maxOutputTokens), messages: requestBody.messages || [] } + // Enable 1M context window beta when [1m] model variant is requested + if (isLongContext) { + bedrockPayload.anthropic_beta = [ + ...(requestBody.anthropic_beta || []), + 'context-1m-2025-08-07' + ] + } else if (requestBody.anthropic_beta) { + bedrockPayload.anthropic_beta = requestBody.anthropic_beta + } + // 添加系统提示词 if (requestBody.system) { bedrockPayload.system = requestBody.system From c6d30c8cd62096b5617773270687fa54b14b2da9 Mon Sep 17 00:00:00 2001 From: bensonz Date: Tue, 10 Feb 2026 15:26:34 +0800 Subject: [PATCH 5/6] Revert "feat: support 1M context window via anthropic_beta header for Bedrock" This reverts commit 8967e866ed983db82cb2cb5ac02e63496b81ead5. --- src/services/bedrockRelayService.js | 30 ++++++++--------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index bd8bc3d3..81a6d3a7 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -136,12 +136,12 @@ class BedrockRelayService { } } - const { modelId, isLongContext } = this._selectModel(requestBody, bedrockAccount) + const modelId = this._selectModel(requestBody, bedrockAccount) const region = this._selectRegion(modelId, bedrockAccount) const client = this._getBedrockClient(region, bedrockAccount) // 转换请求格式为Bedrock格式 - const bedrockPayload = this._convertToBedrockFormat(requestBody, { isLongContext }) + const bedrockPayload = this._convertToBedrockFormat(requestBody) const command = new InvokeModelCommand({ modelId, @@ -277,12 +277,12 @@ class BedrockRelayService { } } - const { modelId, isLongContext } = this._selectModel(requestBody, bedrockAccount) + const modelId = this._selectModel(requestBody, bedrockAccount) const region = this._selectRegion(modelId, bedrockAccount) const client = this._getBedrockClient(region, bedrockAccount) // 转换请求格式为Bedrock格式 - const bedrockPayload = this._convertToBedrockFormat(requestBody, { isLongContext }) + const bedrockPayload = this._convertToBedrockFormat(requestBody) const command = new InvokeModelWithResponseStreamCommand({ modelId, @@ -399,7 +399,6 @@ class BedrockRelayService { } // 选择使用的模型 - // Returns { modelId, isLongContext } _selectModel(requestBody, bedrockAccount) { let selectedModel @@ -421,18 +420,15 @@ class BedrockRelayService { logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } }) } - // Detect [1m] long context variant before mapping - const isLongContext = selectedModel.includes('[1m]') - // 如果是标准Claude模型名,需要映射为Bedrock格式 const bedrockModel = this._mapToBedrockModel(selectedModel) - if (bedrockModel !== selectedModel.replace(/\[1m\]$/, '')) { + if (bedrockModel !== selectedModel) { logger.info(`🔄 模型映射: ${selectedModel} → ${bedrockModel}`, { - metadata: { originalModel: selectedModel, bedrockModel, isLongContext } + metadata: { originalModel: selectedModel, bedrockModel } }) } - return { modelId: bedrockModel, isLongContext } + return bedrockModel } // 将标准Claude模型名映射为Bedrock格式 @@ -550,23 +546,13 @@ class BedrockRelayService { } // 转换Claude格式请求到Bedrock格式 - _convertToBedrockFormat(requestBody, { isLongContext = false } = {}) { + _convertToBedrockFormat(requestBody) { const bedrockPayload = { anthropic_version: 'bedrock-2023-05-31', max_tokens: Math.min(requestBody.max_tokens || this.maxOutputTokens, this.maxOutputTokens), messages: requestBody.messages || [] } - // Enable 1M context window beta when [1m] model variant is requested - if (isLongContext) { - bedrockPayload.anthropic_beta = [ - ...(requestBody.anthropic_beta || []), - 'context-1m-2025-08-07' - ] - } else if (requestBody.anthropic_beta) { - bedrockPayload.anthropic_beta = requestBody.anthropic_beta - } - // 添加系统提示词 if (requestBody.system) { bedrockPayload.system = requestBody.system From 9256d02e89d9bc5a1a607d56bd68ada70f43ab41 Mon Sep 17 00:00:00 2001 From: bensonz Date: Sat, 21 Feb 2026 12:11:07 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20ESLint=20errors=20in=20=5FsanitizeCa?= =?UTF-8?q?cheControl=20=E2=80=94=20strict=20equality=20and=20curly=20brac?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- src/services/relay/bedrockRelayService.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/relay/bedrockRelayService.js b/src/services/relay/bedrockRelayService.js index 8269e2b4..83d0a732 100644 --- a/src/services/relay/bedrockRelayService.js +++ b/src/services/relay/bedrockRelayService.js @@ -525,7 +525,9 @@ class BedrockRelayService { // Bedrock only supports { type: "ephemeral" } — extra fields like "scope" // (added in Claude Code v2.1.38+) cause ValidationException. _sanitizeCacheControl(obj) { - if (obj == null || typeof obj !== 'object') return obj + if (obj === null || obj === undefined || typeof obj !== 'object') { + return obj + } if (Array.isArray(obj)) { obj.forEach((item) => this._sanitizeCacheControl(item))