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
3 changes: 2 additions & 1 deletion core/changelog.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- feat: add Filename field to TranscriptionInput schema to carry original filename through the request pipeline
- fix: add AudioFilenameFromBytes utility to detect audio format from file headers with mp3 fallback
- fix: add AudioFilenameFromBytes utility to detect audio format from file headers with mp3 fallback
- feat: shifted to anthropic native API in bedrock provider for claude models
12 changes: 12 additions & 0 deletions core/providers/anthropic/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ func releaseAnthropicResponsesStreamState(state *AnthropicResponsesStreamState)
}
}

// AcquireAnthropicResponsesStreamState gets an Anthropic responses stream state from the pool.
// Exported for use by providers that wrap Anthropic-compatible endpoints (e.g. Bedrock).
func AcquireAnthropicResponsesStreamState() *AnthropicResponsesStreamState {
return acquireAnthropicResponsesStreamState()
}

// ReleaseAnthropicResponsesStreamState returns an Anthropic responses stream state to the pool.
// Exported for use by providers that wrap Anthropic-compatible endpoints (e.g. Bedrock).
func ReleaseAnthropicResponsesStreamState(state *AnthropicResponsesStreamState) {
releaseAnthropicResponsesStreamState(state)
}
Comment on lines +158 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Release wrapper now exposes a pool reset policy violation.

Line 161 delegates to a release path that does not reset all pooled fields to zero values before pool.Put() (for example, CreatedAt is set to current time and maps are reinitialized instead of zeroed). With this API exported, the risk footprint grows.

🔧 Suggested fix (zero on release, initialize on acquire)
func (state *AnthropicResponsesStreamState) flush() {
  state.ChunkIndex = nil
  state.AccumulatedJSON = ""
  state.ComputerToolID = nil
  state.WebSearchToolID = nil
  state.WebSearchOutputIndex = nil
  state.WebSearchResult = nil
- state.ContentIndexToOutputIndex = make(map[int]int)
- state.ContentIndexToBlockType = make(map[int]AnthropicContentBlockType)
- state.ToolArgumentBuffers = make(map[int]string)
- state.MCPCallOutputIndices = make(map[int]bool)
- state.ItemIDs = make(map[int]string)
- state.ReasoningSignatures = make(map[int]string)
- state.TextContentIndices = make(map[int]bool)
- state.ReasoningContentIndices = make(map[int]bool)
- state.CompactionContentIndices = make(map[int]*schemas.CacheControl)
+ state.ContentIndexToOutputIndex = nil
+ state.ContentIndexToBlockType = nil
+ state.ToolArgumentBuffers = nil
+ state.MCPCallOutputIndices = nil
+ state.ItemIDs = nil
+ state.ReasoningSignatures = nil
+ state.TextContentIndices = nil
+ state.ReasoningContentIndices = nil
+ state.CompactionContentIndices = nil
  state.CurrentOutputIndex = 0
  state.MessageID = nil
  state.StopReason = nil
  state.Model = nil
- state.CreatedAt = int(time.Now().Unix())
+ state.CreatedAt = 0
  state.HasEmittedCreated = false
  state.HasEmittedInProgress = false
  state.StructuredOutputToolName = ""
  state.StructuredOutputIndex = nil
}

As per coding guidelines "Always reset all fields of pooled objects to their zero values before calling pool.Put(). Stale data from previous requests can leak to the next user of the pooled object."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/anthropic/responses.go` around lines 158 - 162, The exported
ReleaseAnthropicResponsesStreamState wrapper currently calls
releaseAnthropicResponsesStreamState which returns the object to the pool
without fully zeroing fields (e.g., CreatedAt left set and maps reinitialized
rather than nil), so update releaseAnthropicResponsesStreamState (the underlying
release path used by ReleaseAnthropicResponsesStreamState and any callers) to
explicitly reset all AnthropicResponsesStreamState fields to their zero values
before calling pool.Put(): set time fields like CreatedAt to zero value, set
pointer/slice/map fields to nil (not reinitialize), and clear any buffers;
ensure any necessary reinitialization happens in the acquire path (where
New/AcquireAnthropicResponsesStreamState prepares maps/buffers) rather than on
release.


// flush resets the state of the stream state to its initial values
func (state *AnthropicResponsesStreamState) flush() {
state.ChunkIndex = nil
Expand Down
132 changes: 132 additions & 0 deletions core/providers/bedrock/anthropic_compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package bedrock

import (
"fmt"

"github.com/bytedance/sonic"
"github.com/maximhq/bifrost/core/providers/anthropic"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
schemas "github.com/maximhq/bifrost/core/schemas"
)

// getBedrockAnthropicChatRequestBody prepares the Anthropic Messages API-compatible request body
// for Bedrock's InvokeModel endpoint. It adds the required anthropic_version body field and
// removes the model field (which is specified in the URL path, not the body).
// Note: streaming is determined by the URL endpoint (invoke vs invoke-with-response-stream),
// NOT by a "stream" field in the request body — so isStreaming only affects caller routing.
func getBedrockAnthropicChatRequestBody(ctx *schemas.BifrostContext, request *schemas.BifrostChatRequest, deployment string, providerName schemas.ModelProvider) ([]byte, *schemas.BifrostError) {
// Handle raw request body passthrough
if rawBody, ok := ctx.Value(schemas.BifrostContextKeyUseRawRequestBody).(bool); ok && rawBody {
rawJSON := request.GetRawRequestBody()
var requestBody map[string]interface{}
if err := sonic.Unmarshal(rawJSON, &requestBody); err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, fmt.Errorf("failed to unmarshal request body: %w", err), providerName)
}
if _, exists := requestBody["max_tokens"]; !exists {
requestBody["max_tokens"] = anthropic.AnthropicDefaultMaxTokens
}
if _, exists := requestBody["anthropic_version"]; !exists {
requestBody["anthropic_version"] = DefaultBedrockAnthropicVersion
}
delete(requestBody, "model")
delete(requestBody, "fallbacks")
// Do NOT add "stream" to the body — Bedrock uses the endpoint path for streaming
delete(requestBody, "stream")
jsonBody, err := sonic.Marshal(requestBody)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err, providerName)
}
return jsonBody, nil
}

reqBody, err := anthropic.ToAnthropicChatRequest(ctx, request)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, err, providerName)
}
if reqBody == nil {
return nil, providerUtils.NewBifrostOperationError("request body is not provided", nil, providerName)
}
reqBody.Model = deployment
// Do NOT set Stream — Bedrock uses the endpoint path for streaming

return marshalBedrockAnthropicBody(reqBody, reqBody.GetExtraParams(), ctx, providerName)
}

// getBedrockAnthropicResponsesRequestBody prepares the Anthropic Messages API-compatible request body
// for Bedrock's InvokeModel endpoint when handling Responses API requests.
// Note: streaming is determined by the URL endpoint, NOT a "stream" body field.
func getBedrockAnthropicResponsesRequestBody(ctx *schemas.BifrostContext, request *schemas.BifrostResponsesRequest, deployment string, providerName schemas.ModelProvider) ([]byte, *schemas.BifrostError) {
// Handle raw request body passthrough
if rawBody, ok := ctx.Value(schemas.BifrostContextKeyUseRawRequestBody).(bool); ok && rawBody {
rawJSON := request.GetRawRequestBody()
var requestBody map[string]interface{}
if err := sonic.Unmarshal(rawJSON, &requestBody); err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, fmt.Errorf("failed to unmarshal request body: %w", err), providerName)
}
if _, exists := requestBody["max_tokens"]; !exists {
requestBody["max_tokens"] = anthropic.AnthropicDefaultMaxTokens
}
if _, exists := requestBody["anthropic_version"]; !exists {
requestBody["anthropic_version"] = DefaultBedrockAnthropicVersion
}
delete(requestBody, "model")
delete(requestBody, "fallbacks")
// Do NOT add "stream" to the body — Bedrock uses the endpoint path for streaming
delete(requestBody, "stream")
jsonBody, err := sonic.Marshal(requestBody)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err, providerName)
}
return jsonBody, nil
}

// Mutate the model before conversion so converters see the resolved deployment name
request.Model = deployment
reqBody, err := anthropic.ToAnthropicResponsesRequest(ctx, request)
Comment on lines +83 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid mutating the inbound Responses request model.

request.Model = deployment introduces side effects on the caller-owned object and can corrupt ModelRequested metadata later in the request flow. Set the deployment on the converted Anthropic request body instead.

✅ Proposed fix
-	// Mutate the model before conversion so converters see the resolved deployment name
-	request.Model = deployment
 	reqBody, err := anthropic.ToAnthropicResponsesRequest(ctx, request)
 	if err != nil {
 		return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, err, providerName)
 	}
 	if reqBody == nil {
 		return nil, providerUtils.NewBifrostOperationError("request body is not provided", nil, providerName)
 	}
+	reqBody.Model = deployment
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mutate the model before conversion so converters see the resolved deployment name
request.Model = deployment
reqBody, err := anthropic.ToAnthropicResponsesRequest(ctx, request)
reqBody, err := anthropic.ToAnthropicResponsesRequest(ctx, request)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, err, providerName)
}
if reqBody == nil {
return nil, providerUtils.NewBifrostOperationError("request body is not provided", nil, providerName)
}
reqBody.Model = deployment
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/bedrock/anthropic_compat.go` around lines 83 - 85, The code
mutates the caller-owned Responses request by setting request.Model =
deployment; instead avoid this side-effect by leaving request unchanged and
applying the resolved deployment to the converted Anthropic request object
returned by anthropic.ToAnthropicResponsesRequest(ctx, request) (or by passing a
shallow copy of request into the converter). Locate the call site around
anthropic.ToAnthropicResponsesRequest and ensure you either pass a copy of
request (so original ModelRequested is preserved) or set the deployment on the
resulting reqBody (e.g., reqBody.Model = deployment) rather than mutating the
original request.

if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrRequestBodyConversion, err, providerName)
}
if reqBody == nil {
return nil, providerUtils.NewBifrostOperationError("request body is not provided", nil, providerName)
}
// Do NOT set Stream — Bedrock uses the endpoint path for streaming

return marshalBedrockAnthropicBody(reqBody, reqBody.GetExtraParams(), ctx, providerName)
}

// marshalBedrockAnthropicBody converts an AnthropicMessageRequest to JSON suitable for
// Bedrock's InvokeModel endpoint. It adds anthropic_version, removes the model field
// (specified in the URL path), and merges extra params if passthrough is enabled.
func marshalBedrockAnthropicBody(reqBody *anthropic.AnthropicMessageRequest, extraParams map[string]interface{}, ctx *schemas.BifrostContext, providerName schemas.ModelProvider) ([]byte, *schemas.BifrostError) {
reqBytes, err := sonic.Marshal(reqBody)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err, providerName)
}

var requestBody map[string]interface{}
if err := sonic.Unmarshal(reqBytes, &requestBody); err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err, providerName)
}

// Add Bedrock-specific anthropic_version if not already present
if _, exists := requestBody["anthropic_version"]; !exists {
requestBody["anthropic_version"] = DefaultBedrockAnthropicVersion
}

// Remove model and stream — model is in URL path; streaming is via endpoint path, not body field
delete(requestBody, "model")
delete(requestBody, "stream")

// Merge extra params if passthrough is enabled
if ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) != nil && ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) == true {
if len(extraParams) > 0 {
providerUtils.MergeExtraParams(requestBody, extraParams)
}
}
Comment on lines +120 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-sanitize reserved fields after passthrough merge.

MergeExtraParams runs after removing reserved keys, so model/stream can be reintroduced from extraParams. That can invalidate Bedrock Anthropic requests.

✅ Proposed fix
 	// Merge extra params if passthrough is enabled
 	if ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) != nil && ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) == true {
 		if len(extraParams) > 0 {
 			providerUtils.MergeExtraParams(requestBody, extraParams)
+			// Keep Bedrock Anthropic invariants intact after merge
+			delete(requestBody, "model")
+			delete(requestBody, "stream")
+			delete(requestBody, "fallbacks")
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Merge extra params if passthrough is enabled
if ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) != nil && ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) == true {
if len(extraParams) > 0 {
providerUtils.MergeExtraParams(requestBody, extraParams)
}
}
// Merge extra params if passthrough is enabled
if ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) != nil && ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams) == true {
if len(extraParams) > 0 {
providerUtils.MergeExtraParams(requestBody, extraParams)
// Keep Bedrock Anthropic invariants intact after merge
delete(requestBody, "model")
delete(requestBody, "stream")
delete(requestBody, "fallbacks")
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/providers/bedrock/anthropic_compat.go` around lines 120 - 125,
MergeExtraParams can reintroduce reserved fields like "model" and "stream" back
into requestBody; after calling providerUtils.MergeExtraParams(requestBody,
extraParams) inside the passthrough branch, remove or re-sanitize those reserved
keys (e.g. delete requestBody["model"] and requestBody["stream"] or call the
existing sanitization helper if one exists) so Bedrock/Anthropic requests cannot
be invalidated; ensure the re-sanitization happens immediately after the
MergeExtraParams call in the same block that checks
ctx.Value(schemas.BifrostContextKeyPassthroughExtraParams).


jsonBody, err := sonic.Marshal(requestBody)
if err != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, err, providerName)
}
return jsonBody, nil
}
Loading
Loading