Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions fixtures/anthropic/simple_bedrock.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Simple Bedrock request. Tests that fields unsupported by Bedrock are removed
and adaptive thinking is converted to enabled with a budget. Includes all
bedrockUnsupportedFields (metadata, service_tier, container, inference_geo)
and beta-gated fields (output_config, context_management).

-- request --
{
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello."
}
]
}
],
"thinking": {"type": "adaptive"},
"metadata": {"user_id": "session_abc123"},
"service_tier": "auto",
"container": {"type": "ephemeral"},
"inference_geo": {"allow": ["us"]},
"output_config": {"effort": "medium"},
"context_management": {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]},
"stream": true
}

-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! How can I help?"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10}}

event: message_stop
data: {"type":"message_stop"}

-- non-streaming --
{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[{"type":"text","text":"Hello! How can I help?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}
3 changes: 3 additions & 0 deletions fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ var (

//go:embed anthropic/non_stream_error.txtar
AntNonStreamError []byte

//go:embed anthropic/simple_bedrock.txtar
AntSimpleBedrock []byte
)

var (
Expand Down
4 changes: 3 additions & 1 deletion intercept/client_headers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package intercept

import "net/http"
import (
"net/http"
)

// hopByHopHeaders are connection-level headers specific to the connection
// between client and AI Bridge, not meant for the upstream.
Expand Down
65 changes: 63 additions & 2 deletions intercept/messages/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ import (
"cdr.dev/slog/v3"
)

// bedrockSupportedBetaFlags is the set of Anthropic-Beta flags that AWS Bedrock
// accepts. Flags not in this set cause a 400 "invalid beta flag" error.
//
// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
var bedrockSupportedBetaFlags = map[string]bool{
"computer-use-2025-01-24": true,
"token-efficient-tools-2025-02-19": true,
"interleaved-thinking-2025-05-14": true,
"output-128k-2025-02-19": true,
"dev-full-thinking-2025-05-14": true,
"context-1m-2025-08-07": true,
"context-management-2025-06-27": true,
"effort-2025-11-24": true,
"tool-search-tool-2025-10-19": true,
"tool-examples-2025-10-29": true,
}

type interceptionBase struct {
id uuid.UUID
reqPayload MessagesRequestPayload
Expand Down Expand Up @@ -288,8 +305,14 @@ func (i *interceptionBase) augmentRequestForBedrock() {
i.reqPayload = updated
}

// Strip fields that Bedrock does not accept.
updated, err = i.reqPayload.removeUnsupportedBedrockFields()
// Filter Anthropic-Beta header to only include Bedrock-supported flags
// that the current model supports.
if i.clientHeaders != nil {
filterBedrockBetaFlags(i.clientHeaders, model)
}

// Strip body fields that Bedrock does not accept.
updated, err = i.reqPayload.removeUnsupportedBedrockFields(i.clientHeaders)
if err != nil {
i.logger.Warn(context.Background(), "failed to remove unsupported fields for Bedrock", slog.Error(err))
return
Expand All @@ -305,6 +328,44 @@ func bedrockModelSupportsAdaptiveThinking(model string) bool {
strings.Contains(model, "anthropic.claude-sonnet-4-6")
}

// filterBedrockBetaFlags removes unsupported beta flags from the Anthropic-Beta
// header and also removes model-gated flags the current model doesn't support.
func filterBedrockBetaFlags(headers http.Header, model string) {
raw := headers.Get("Anthropic-Beta")
if raw == "" {
return
}

flags := strings.Split(raw, ",")
var keep []string
for _, flag := range flags {
trimmed := strings.TrimSpace(flag)
if !bedrockSupportedBetaFlags[trimmed] {
continue
}

// effort is only supported for Opus 4.5 on Bedrock.
if trimmed == "effort-2025-11-24" && !strings.Contains(model, "anthropic.claude-opus-4-5") {
continue
}

// context_management is only supported for Sonnet 4.5 and Haiku 4.5 on Bedrock.
if trimmed == "context-management-2025-06-27" &&
!strings.Contains(model, "anthropic.claude-sonnet-4-5") &&
!strings.Contains(model, "anthropic.claude-haiku-4-5") {
continue
}

keep = append(keep, trimmed)
}

if len(keep) == 0 {
headers.Del("Anthropic-Beta")
} else {
headers.Set("Anthropic-Beta", strings.Join(keep, ","))
}
}

// writeUpstreamError marshals and writes a given error.
func (i *interceptionBase) writeUpstreamError(w http.ResponseWriter, antErr *ErrorResponse) {
if antErr == nil {
Expand Down
Loading
Loading