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
33 changes: 31 additions & 2 deletions transports/bifrost-http/integrations/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/bytedance/sonic"
"github.com/bytedance/sonic/ast"
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/providers/anthropic"
"github.com/maximhq/bifrost/core/schemas"
Expand Down Expand Up @@ -274,6 +275,34 @@ func CreateAnthropicListModelsRouteConfigs(pathPrefix string, handlerStore lib.H
}
}

// stripModelPrefixFromBody replaces a provider-prefixed model name
// (e.g. "anthropic/claude-sonnet-4-6") with the bare model name in the raw
// HTTP request body. Uses sonic's lazy AST so only the "model" field is
// parsed and re-serialized; the rest of the body is copied as raw bytes.
func stripModelPrefixFromBody(ctx *fasthttp.RequestCtx, prefixedModel, bareModel string) {
body := ctx.Request.Body()
if len(body) == 0 {
return
}
root := ast.NewRaw(string(body))
modelNode := root.Get("model")
if modelNode == nil || modelNode.TypeSafe() != ast.V_STRING {
return
}
currentModel, err := modelNode.StrictString()
if err != nil || currentModel != prefixedModel {
return
}
if _, err := root.Set("model", ast.NewString(bareModel)); err != nil {
return
}
newBody, err := root.MarshalJSON()
if err != nil {
return
}
ctx.Request.SetBody(newBody)
}

// checkAnthropicPassthrough pre-callback checks if the request is for a claude model.
// If it is, it attaches the raw request body for direct use by the provider.
// It also checks for anthropic oauth headers and sets the bifrost context.
Expand All @@ -284,15 +313,15 @@ func checkAnthropicPassthrough(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.Bif
switch r := req.(type) {
case *anthropic.AnthropicTextRequest:
provider, model = schemas.ParseModelString(r.Model, "")
// Check if model parameter explicitly has `anthropic/` prefix
if provider == schemas.Anthropic {
stripModelPrefixFromBody(ctx, r.Model, model)
r.Model = model
}

case *anthropic.AnthropicMessageRequest:
provider, model = schemas.ParseModelString(r.Model, "")
// Check if model parameter explicitly has `anthropic/` prefix
if provider == schemas.Anthropic {
stripModelPrefixFromBody(ctx, r.Model, model)
r.Model = model
}
}
Expand Down
70 changes: 70 additions & 0 deletions transports/bifrost-http/integrations/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package integrations
import (
"testing"

"github.com/bytedance/sonic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)

func TestFilterVertexUnsupportedBetaHeaders(t *testing.T) {
Expand Down Expand Up @@ -85,3 +88,70 @@ func TestFilterVertexUnsupportedBetaHeaders(t *testing.T) {
assert.Equal(t, []string{"interleaved-thinking-2025-05-14"}, vals)
})
}

func TestStripModelPrefixFromBody(t *testing.T) {
makeCtx := func(body string) *fasthttp.RequestCtx {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetBodyString(body)
return ctx
}

bodyModel := func(ctx *fasthttp.RequestCtx) string {
var m map[string]any
require.NoError(t, sonic.Unmarshal(ctx.Request.Body(), &m))
return m["model"].(string)
}

t.Run("strips matching provider prefix from model field", func(t *testing.T) {
ctx := makeCtx(`{"model":"anthropic/claude-sonnet-4-6","max_tokens":1024}`)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
assert.Equal(t, "claude-sonnet-4-6", bodyModel(ctx))
})

t.Run("preserves other fields unchanged", func(t *testing.T) {
ctx := makeCtx(`{"model":"anthropic/claude-sonnet-4-6","max_tokens":1024,"stream":true}`)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
var m map[string]any
require.NoError(t, sonic.Unmarshal(ctx.Request.Body(), &m))
assert.Equal(t, "claude-sonnet-4-6", m["model"])
assert.Equal(t, float64(1024), m["max_tokens"])
assert.Equal(t, true, m["stream"])
})

t.Run("no-op when model does not match", func(t *testing.T) {
ctx := makeCtx(`{"model":"openai/gpt-4o","max_tokens":1024}`)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
assert.Equal(t, "openai/gpt-4o", bodyModel(ctx))
})

t.Run("no-op when body has no model field", func(t *testing.T) {
original := `{"max_tokens":1024}`
ctx := makeCtx(original)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
assert.JSONEq(t, original, string(ctx.Request.Body()))
})

t.Run("no-op on empty body", func(t *testing.T) {
ctx := makeCtx("")
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
assert.Empty(t, ctx.Request.Body())
})

t.Run("no-op on invalid JSON", func(t *testing.T) {
original := `{invalid`
ctx := makeCtx(original)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
assert.Equal(t, original, string(ctx.Request.Body()))
})

t.Run("preserves nested content containing the model name", func(t *testing.T) {
ctx := makeCtx(`{"model":"anthropic/claude-sonnet-4-6","messages":[{"content":"use anthropic/claude-sonnet-4-6"}]}`)
stripModelPrefixFromBody(ctx, "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6")
var m map[string]any
require.NoError(t, sonic.Unmarshal(ctx.Request.Body(), &m))
assert.Equal(t, "claude-sonnet-4-6", m["model"])
msgs := m["messages"].([]any)
msg := msgs[0].(map[string]any)
assert.Equal(t, "use anthropic/claude-sonnet-4-6", msg["content"], "nested content must not be modified")
})
}
5 changes: 4 additions & 1 deletion transports/bifrost-http/integrations/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,10 @@ func (g *GenericRouter) createHandler(config RouteConfig) fasthttp.RequestHandle
return
}
if sendRawRequestBody, ok := (*bifrostCtx).Value(schemas.BifrostContextKeyUseRawRequestBody).(bool); ok && sendRawRequestBody {
bifrostReq.SetRawRequestBody(rawBody)
// Re-read the body from the context: PreCallback (e.g.
// checkAnthropicPassthrough) may have updated it to strip
// the provider prefix that the governance plugin added.
bifrostReq.SetRawRequestBody(ctx.Request.Body())
}

// Extract and parse fallbacks from the request if present
Expand Down