Skip to content
Open

v1.5.16 #4518

Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
839f16a
fix: fixes redaction setting in plugins config (#4486)
roroghost17 Jun 17, 2026
3caf63d
feat: adds scripts to seed and delete bifrost and litellm entities
sammaji Jun 14, 2026
004cc14
feat: adds scripts to migrate litellm entities to bifrost entities
sammaji Jun 14, 2026
0387f60
feat: adds conformance tests
sammaji Jun 14, 2026
2278c80
fix: fix 403 errors for list models request
roroghost17 Jun 17, 2026
e25251f
chore: updated the default network config timings
roroghost17 Jun 17, 2026
a67098f
fix: helm release tag to the correct commit
BearTS Jun 18, 2026
4bfc551
[docs] : docs for source of truth
Madhuvod Jun 17, 2026
010c702
fixes canonical_model_view migration
akshaydeo Jun 18, 2026
fc0c813
docs: adds docs for Claude Desktop with inference provider
roroghost17 Jun 18, 2026
ea4fc47
feat: adds group traces by sessions to config json and helm
roroghost17 Jun 18, 2026
492c647
fix: model id in bedrock batch request
Jun 18, 2026
f66590d
fix: resolve model names for feature gating
Jun 18, 2026
a3e1992
fix: nova bedrock fixes
Jun 18, 2026
d06b94d
fix: bedrock signauture fix for minimax
Jun 18, 2026
bbfca36
fix: handle url encoded file name in url params for single file servi…
danpiths Jun 17, 2026
c238014
refactor: skills repo ui refactor
impoiler Jun 17, 2026
3052f96
feat: ability to select timezone in date selectors
impoiler Jun 16, 2026
87b35f1
chore: UI dependency updates for npn audit
impoiler Jun 16, 2026
bc1e8b0
feat: add Virtual Key Rankings tab in dashboard
impoiler Jun 16, 2026
fcd444f
fix: do not clear selection on virtual keys table on filter or search
impoiler Jun 16, 2026
99bd3c5
fix: workspace/model pages style fixes
impoiler Jun 16, 2026
abe5786
feat: dashboard export endpoint
impoiler Jun 16, 2026
7bc8bb1
fix: Seconds to minutes UI bug for provider network form
impoiler Jun 16, 2026
bb05be8
feat: allow toggling additional columns in logs and MCP logs tables
impoiler Jun 18, 2026
26ac93d
fix: return 409 for Conflict errors
impoiler Jun 18, 2026
2f96e3a
feat: show inline error for 409 errors
impoiler Jun 18, 2026
ded002a
fix: adds explicit content type header for container delete
sammaji Jun 17, 2026
79c6471
fix: e2e ui tests for providers management
sammaji Jun 18, 2026
ec3d03c
docs: evaluation mode in schemas for guardrails
Jun 12, 2026
021671e
fix: redacts emails from logs and handle wildcard provider in vk
sammaji Jun 18, 2026
5dee81f
chore: rename migration folder to migration-cli
sammaji Jun 18, 2026
fe688ab
feat: adds npx script for migration cli
sammaji Jun 19, 2026
5c0f07c
feat: adds github action scripts for migration cli
sammaji Jun 19, 2026
62d3865
fix: run governance on cc oauth requests when vk is present
Jun 19, 2026
a9e5ebf
fix: run governance on cc oauth requests when vk is present (#4557)
akshaydeo Jun 19, 2026
a3bbb4d
fix: double message start event in anthropic stream closes #4556
Jun 19, 2026
a014020
fix: double message start event in anthropic stream closes #4556 (#4559)
akshaydeo Jun 19, 2026
2ebd0ab
fix: e2e ui tests for model limts
sammaji Jun 18, 2026
bd8e1d7
fix: e2e ui tests for mcp registry
sammaji Jun 19, 2026
65fa2a2
fix: remove skip key selection check from governance pre llm hook
Jun 19, 2026
869c9c9
fix: remove skip key selection check from governance pre llm hook (#4…
akshaydeo Jun 19, 2026
971d4c6
docs: evaluation mode in schemas for guardrails (#4337)
akshaydeo Jun 19, 2026
374d6f7
chore: adds docs for litellm-to-bifrost migration script
sammaji Jun 17, 2026
c57f93e
fix: redacts emails from logs and handle wildcard provider in vk (#4550)
akshaydeo Jun 19, 2026
ce3a238
chore: adds docs for litellm-to-bifrost migration script (#4488)
akshaydeo Jun 19, 2026
e213546
chore: rename migration folder to migration-cli (#4551)
akshaydeo Jun 19, 2026
113f854
feat: adds npx script for migration cli (#4552)
akshaydeo Jun 19, 2026
cf58ea4
feat: adds github action scripts for migration cli (#4553)
akshaydeo Jun 19, 2026
6060b08
feat: adds business unit & user names & ids to tracing
roroghost17 Jun 19, 2026
5e7460b
feat: adds business unit & user names & ids to tracing (#4562)
akshaydeo Jun 19, 2026
8df5809
fix: adds explicit content type header for container delete (#4490)
akshaydeo Jun 19, 2026
fcb3939
fix: e2e ui tests for providers management (#4517)
akshaydeo Jun 19, 2026
dc7206e
fix: e2e ui tests for model limts (#4519)
akshaydeo Jun 19, 2026
e6b5301
fix: e2e ui tests for mcp registry (#4558)
akshaydeo Jun 19, 2026
24fe11f
add vkey cost compute test cases (#4561)
akshaydeo Jun 19, 2026
2cd95b5
dependabot fixes (#4570)
akshaydeo Jun 20, 2026
1dbcefc
version cut (#4571)
akshaydeo Jun 20, 2026
7058950
extra header forwarding for mcp tools (#4572)
akshaydeo Jun 20, 2026
ddb8de2
adds mcp cleanup support for bedrock models (#4573)
akshaydeo Jun 20, 2026
565750a
adds sync support from local files for airgapped deployments (#4574)
akshaydeo Jun 20, 2026
9f2440d
[fix]: Bedrock stream errors use EventStream exceptions (#4576)
jstar0 Jun 20, 2026
e587324
adds bill accounting for failed requests (#4575)
akshaydeo Jun 20, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/helm-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jobs:
cd helm-charts
gh release create "helm-chart-v${{ steps.chart-version.outputs.version }}" \
bifrost-${{ steps.chart-version.outputs.version }}.tgz \
--target ${{ github.sha }} \
--title "Helm Chart v${{ steps.chart-version.outputs.version }}" \
--notes "Helm chart release for Bifrost v${{ steps.chart-version.outputs.version }}"
env:
Expand Down
17 changes: 14 additions & 3 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,20 @@ func (bifrost *Bifrost) ListAllModels(ctx *schemas.BifrostContext, req *schemas.

response, bifrostErr := bifrost.ListModelsRequest(providerCtx, providerRequest)
if bifrostErr != nil {
// Skip logging "no keys found" and "not supported" errors as they are expected when a provider is not configured
if !strings.Contains(bifrostErr.Error.Message, "no keys found") &&
!strings.Contains(bifrostErr.Error.Message, "not supported") {
// Some per-provider failures are expected when fanning out across all
// configured providers and must not be surfaced as a top-level error
errType := ""
if bifrostErr.Type != nil {
errType = *bifrostErr.Type
}
errMsg := ""
if bifrostErr.Error != nil {
errMsg = bifrostErr.Error.Message
}
isExpected := strings.Contains(errMsg, "no keys found") ||
strings.Contains(errMsg, "not supported") ||
errType == "provider_blocked"
Comment on lines +523 to +529

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize error-message casing before expected-error matching.

strings.Contains here is case-sensitive, so equivalent messages with different capitalization can be treated as unexpected and incorrectly promote a per-provider failure.

Suggested fix
-					errMsg := ""
+					errMsg := ""
 					if bifrostErr.Error != nil {
 						errMsg = bifrostErr.Error.Message
 					}
-					isExpected := strings.Contains(errMsg, "no keys found") ||
-						strings.Contains(errMsg, "not supported") ||
+					errMsgLower := strings.ToLower(errMsg)
+					isExpected := strings.Contains(errMsgLower, "no keys found") ||
+						strings.Contains(errMsgLower, "not supported") ||
 						errType == "provider_blocked"
📝 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
errMsg := ""
if bifrostErr.Error != nil {
errMsg = bifrostErr.Error.Message
}
isExpected := strings.Contains(errMsg, "no keys found") ||
strings.Contains(errMsg, "not supported") ||
errType == "provider_blocked"
errMsg := ""
if bifrostErr.Error != nil {
errMsg = bifrostErr.Error.Message
}
errMsgLower := strings.ToLower(errMsg)
isExpected := strings.Contains(errMsgLower, "no keys found") ||
strings.Contains(errMsgLower, "not supported") ||
errType == "provider_blocked"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/bifrost.go` around lines 523 - 529, The error message matching logic in
the bifrost.go file uses case-sensitive string comparisons with strings.Contains
to check for expected error messages. To prevent equivalent error messages with
different capitalization from being incorrectly treated as unexpected errors,
normalize the errMsg variable to lowercase before performing the
strings.Contains checks against "no keys found", "not supported", and other
error message patterns. This ensures consistent matching regardless of how the
error message is capitalized in the actual bifrostErr.Error.Message response.

if !isExpected {
providerErr = bifrostErr
bifrost.logger.Warn("failed to list models for provider %s: %s", providerKey, bifrostErr.GetErrorString())
}
Expand Down
2 changes: 1 addition & 1 deletion core/bifrost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ func (ma *MockAccount) AddProviderWithBaseURL(provider schemas.ModelProvider, co
ma.configs[provider] = &schemas.ProviderConfig{
NetworkConfig: schemas.NetworkConfig{
BaseURL: baseURL,
DefaultRequestTimeoutInSeconds: 30,
DefaultRequestTimeoutInSeconds: 300,
MaxRetries: 3,
RetryBackoffInitial: 500 * time.Millisecond,
RetryBackoffMax: 5 * time.Second,
Expand Down
2 changes: 1 addition & 1 deletion core/internal/llmtests/passthrough_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func buildPassthroughChatReq(t *testing.T, provider schemas.ModelProvider, model
return passthroughChatReq{path: "/v1/messages", body: body}, true

case schemas.Gemini:
nativeReq, err := gemini.ToGeminiChatCompletionRequest(bfReq)
nativeReq, err := gemini.ToGeminiChatCompletionRequest(ctx, bfReq)
if err != nil {
return passthroughChatReq{}, false
}
Expand Down
102 changes: 102 additions & 0 deletions core/providers/anthropic/advisor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,105 @@ func TestAdvisorStream_RoundTrip(t *testing.T) {
t.Error("text block dropped")
}
}

// countPassthroughMessageStarts mirrors the transport passthrough converter
// (transports/bifrost-http/integrations/anthropic.go): for each bifrost stream
// response it forwards the raw upstream frame verbatim when present (except
// ContentPartAdded), otherwise falls back to the normalized converter. Returns
// how many message_start frames are emitted and the raw bytes of the forwarded one.
func countPassthroughMessageStarts(ctx *schemas.BifrostContext, responses []*schemas.BifrostResponsesStreamResponse) (int, string) {
count := 0
forwarded := ""
for _, r := range responses {
if r.ExtraFields.RawResponse != nil && r.Type != schemas.ResponsesStreamResponseTypeContentPartAdded {
raw, _ := r.ExtraFields.RawResponse.(string)
if gjson.Get(raw, "type").String() == "message_start" {
count++
forwarded = raw
}
continue
}
for _, ev := range ToAnthropicResponsesStreamResponse(ctx, r) {
if ev.Type == AnthropicStreamEventTypeMessageStart {
count++
}
}
}
return count, forwarded
}

// TestResponsesStream_MessageStart_NoDuplicateOnPassthrough guards the fix for the
// duplicate message_start emitted on the Anthropic passthrough responses_stream
// path. A single upstream message_start expands inbound into [response.created,
// response.in_progress]. The raw upstream frame must ride on response.created
// (which maps back to message_start) so it is forwarded once with all upstream
// fields intact; response.in_progress must carry no raw and convert to nil.
// Attaching the raw to in_progress instead (the old "last chunk" rule) produced
// two message_start frames: a lossy synthesized one from created plus the
// raw-forwarded one from in_progress.
func TestResponsesStream_MessageStart_NoDuplicateOnPassthrough(t *testing.T) {
ctx := schemas.NewBifrostContext(nil, time.Time{})

const rawMessageStart = `{"type":"message_start","message":{"id":"msg_01","type":"message","role":"assistant","model":"claude-opus-4-8","content":[],"stop_reason":null,"usage":{"input_tokens":3450,"output_tokens":4,"service_tier":"standard","inference_geo":"not_available"}}}`

var chunk AnthropicStreamEvent
if err := sonic.Unmarshal([]byte(rawMessageStart), &chunk); err != nil {
t.Fatalf("unmarshal message_start: %v", err)
}

state := acquireAnthropicResponsesStreamState()
defer releaseAnthropicResponsesStreamState(state)

responses, bErr, _ := chunk.ToBifrostResponsesStream(ctx, 0, state)
if bErr != nil {
t.Fatalf("ToBifrostResponsesStream error: %v", bErr)
}

// Inbound must expand the single message_start into [created, in_progress].
if len(responses) != 2 {
t.Fatalf("expected 2 bifrost responses (created, in_progress), got %d", len(responses))
}
createdIdx := -1
for i, r := range responses {
if r.Type == schemas.ResponsesStreamResponseTypeCreated {
createdIdx = i
}
}
if createdIdx == -1 {
t.Fatal("expected a response.created in the message_start expansion")
}
// The fix attaches the raw to response.created; the old rule attached it to the
// last response (in_progress). Confirm they differ so the cases below exercise it.
if createdIdx == len(responses)-1 {
t.Fatal("response.created is last; raw attachment would not exercise the fix")
}

t.Run("raw on created emits one full message_start", func(t *testing.T) {
for _, r := range responses {
r.ExtraFields.RawResponse = nil
}
responses[createdIdx].ExtraFields.RawResponse = rawMessageStart

count, forwarded := countPassthroughMessageStarts(ctx, responses)
if count != 1 {
t.Fatalf("expected exactly 1 message_start, got %d", count)
}
// The surviving frame must be the verbatim upstream bytes, preserving
// service_tier/inference_geo that the synthesized frame drops.
if forwarded != rawMessageStart {
t.Errorf("forwarded message_start is not the verbatim upstream frame: %s", forwarded)
}
})

t.Run("raw on in_progress (old behavior) duplicates message_start", func(t *testing.T) {
for _, r := range responses {
r.ExtraFields.RawResponse = nil
}
responses[len(responses)-1].ExtraFields.RawResponse = rawMessageStart

count, _ := countPassthroughMessageStarts(ctx, responses)
if count != 2 {
t.Fatalf("expected the old last-chunk rule to duplicate message_start (2), got %d", count)
}
})
}
17 changes: 13 additions & 4 deletions core/providers/anthropic/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ func (provider *AnthropicProvider) ChatCompletion(ctx *schemas.BifrostContext, k
// Feature gating keyed to schemas.Anthropic (not provider.GetProviderKey())
// so custom Anthropic aliases get the same feature lookup as the typed
// path above (line 445), keeping raw and typed behavior in lockstep.
sanitized, rawErr := StripUnsupportedFieldsFromRawBody(jsonData, schemas.Anthropic, request.Model)
sanitized, rawErr := StripUnsupportedFieldsFromRawBody(jsonData, schemas.Anthropic, schemas.ResolveCanonicalModel(ctx, request.Model))
if rawErr != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, rawErr)
}
Expand Down Expand Up @@ -539,7 +539,7 @@ func (provider *AnthropicProvider) ChatCompletionStream(ctx *schemas.BifrostCont
// Feature gating keyed to schemas.Anthropic (not provider.GetProviderKey())
// to keep raw and typed paths in lockstep on custom aliases — mirrors
// the typed path's hardcoded schemas.Anthropic at line 548.
sanitized, rawErr := StripUnsupportedFieldsFromRawBody(jsonData, schemas.Anthropic, request.Model)
sanitized, rawErr := StripUnsupportedFieldsFromRawBody(jsonData, schemas.Anthropic, schemas.ResolveCanonicalModel(ctx, request.Model))
if rawErr != nil {
return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRequestMarshal, rawErr)
}
Expand Down Expand Up @@ -1330,6 +1330,16 @@ func HandleAnthropicResponsesStream(
responseChan, postHookSpanFinalizer)
continue
}
// Attach the upstream raw to exactly one bifrost response. Default to the last,
// but for the message_start expansion ([created, in_progress]) attach it to
// response.created
rawIdx := len(responses) - 1
for j, r := range responses {
if r != nil && r.Type == schemas.ResponsesStreamResponseTypeCreated {
rawIdx = j
break
}
}
// Handle each response in the slice
for i, response := range responses {
if response != nil {
Expand All @@ -1347,8 +1357,7 @@ func HandleAnthropicResponsesStream(
lastChunkTime = time.Now()
chunkIndex++

// Only add raw response to the last chunk of the incoming event
if providerUtils.ShouldSendBackRawResponse(ctx, sendBackRawResponse) && i == len(responses)-1 {
if providerUtils.ShouldSendBackRawResponse(ctx, sendBackRawResponse) && i == rawIdx {
response.ExtraFields.RawResponse = eventData
}

Expand Down
25 changes: 14 additions & 11 deletions core/providers/anthropic/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
MaxTokens: providerUtils.GetMaxOutputTokensOrDefault(bifrostReq.Model, AnthropicDefaultMaxTokens),
}

// capModel is the canonical model string used only for capability/version
capModel := schemas.ResolveCanonicalModel(ctx, bifrostReq.Model)

// Convert parameters
if bifrostReq.Params != nil {
anthropicReq.ExtraParams = bifrostReq.Params.ExtraParams
Expand All @@ -257,7 +260,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif

// Opus 4.7+ and the Fable/Mythos family reject temperature, top_p, and
// top_k with a 400 error.
if !IsAdaptiveOnlyThinkingModel(bifrostReq.Model) {
if !IsAdaptiveOnlyThinkingModel(capModel) {
// Anthropic doesn't allow both temperature and top_p to be specified.
// If both are present, prefer temperature (more commonly used).
if bifrostReq.Params.Temperature != nil {
Expand All @@ -271,12 +274,12 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
// TopK — prefer the promoted neutral field; fall back to ExtraParams.
// Opus 4.7+ and the Fable/Mythos family reject top_k with a 400 error.
if bifrostReq.Params.TopK != nil {
if !IsAdaptiveOnlyThinkingModel(bifrostReq.Model) {
if !IsAdaptiveOnlyThinkingModel(capModel) {
anthropicReq.TopK = bifrostReq.Params.TopK
}
} else if topK, ok := schemas.SafeExtractIntPointer(bifrostReq.Params.ExtraParams["top_k"]); ok {
delete(anthropicReq.ExtraParams, "top_k")
if !IsAdaptiveOnlyThinkingModel(bifrostReq.Model) {
if !IsAdaptiveOnlyThinkingModel(capModel) {
anthropicReq.TopK = topK
}
}
Expand Down Expand Up @@ -458,7 +461,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
continue
}
// Non-function tool: attempt server-tool reconstruction.
if converted, ok := convertServerToolToAnthropic(tool, bifrostReq.Model); ok {
if converted, ok := convertServerToolToAnthropic(tool, capModel); ok {
tools = append(tools, converted)
}
}
Expand Down Expand Up @@ -504,7 +507,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
// Convert reasoning
if bifrostReq.Params.Reasoning != nil {
if bifrostReq.Params.Reasoning.MaxTokens != nil {
if IsAdaptiveOnlyThinkingModel(bifrostReq.Model) {
if IsAdaptiveOnlyThinkingModel(capModel) {
// Opus 4.7+ and Fable/Mythos: budget_tokens removed; adaptive thinking is the only thinking-on mode.
anthropicReq.Thinking = &AnthropicThinking{Type: "adaptive"}
} else {
Expand All @@ -524,11 +527,11 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
}
} else if bifrostReq.Params.Reasoning.Effort != nil && *bifrostReq.Params.Reasoning.Effort != "none" {
effort := MapBifrostEffortToAnthropic(*bifrostReq.Params.Reasoning.Effort)
if SupportsAdaptiveThinking(bifrostReq.Model) {
if SupportsAdaptiveThinking(capModel) {
// Opus 4.6+ and Opus 4.7+: adaptive thinking + native effort
anthropicReq.Thinking = &AnthropicThinking{Type: "adaptive"}
setEffortOnOutputConfig(anthropicReq, effort)
} else if SupportsNativeEffort(bifrostReq.Model) {
} else if SupportsNativeEffort(capModel) {
// Opus 4.5: native effort + budget_tokens thinking
setEffortOnOutputConfig(anthropicReq, effort)
budgetTokens, err := providerUtils.GetBudgetTokensFromReasoningEffort(effort, MinimumReasoningMaxTokens, anthropicReq.MaxTokens)
Expand All @@ -550,7 +553,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
BudgetTokens: schemas.Ptr(budgetTokens),
}
}
} else if !IsFableFamily(bifrostReq.Model) {
} else if !IsFableFamily(capModel) {
// Fable/Mythos reject thinking:{type:"disabled"} with a 400 —
// adaptive thinking is always on and cannot be disabled. Omit
// the thinking param entirely for that family; all other models
Expand All @@ -572,7 +575,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
if anthropicReq.Thinking != nil && anthropicReq.Thinking.Type != "disabled" {
if bifrostReq.Params.Reasoning.Display != nil {
anthropicReq.Thinking.Display = bifrostReq.Params.Reasoning.Display
} else if IsAdaptiveOnlyThinkingModel(bifrostReq.Model) {
} else if IsAdaptiveOnlyThinkingModel(capModel) {
anthropicReq.Thinking.Display = schemas.Ptr("summarized")
}
}
Expand All @@ -593,7 +596,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
// system message and is emitted as role:"system" in the messages array
// (Anthropic API + Opus 4.8+ only).
seenConversation := false
midConvSystemSupported := SupportsMidConversationSystem(bifrostReq.Provider, bifrostReq.Model)
midConvSystemSupported := SupportsMidConversationSystem(bifrostReq.Provider, capModel)

i := 0
for i < len(messages) {
Expand Down Expand Up @@ -823,7 +826,7 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
// Strip request- and tool-level fields the target Anthropic-family
// provider does not support. Fail-closed tool validation stays in
// ValidateToolsForProvider; this is strip-silently for additive fields.
stripUnsupportedAnthropicFields(anthropicReq, bifrostReq.Provider, bifrostReq.Model)
stripUnsupportedAnthropicFields(anthropicReq, bifrostReq.Provider, capModel)

return anthropicReq, nil
}
Expand Down
7 changes: 5 additions & 2 deletions core/providers/anthropic/requestbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func BuildAnthropicResponsesRequestBody(ctx *schemas.BifrostContext, request *sc
return nil, nil
}

// capModel is the canonical model used for capability gating in the raw-body
capModel := schemas.ResolveCanonicalModel(ctx, request.Model)

newErr := func(msg string, err error, reqBody []byte) *schemas.BifrostError {
return providerUtils.EnrichError(
ctx,
Expand Down Expand Up @@ -175,7 +178,7 @@ func BuildAnthropicResponsesRequestBody(ctx *schemas.BifrostContext, request *sc
// request.Model is the alias-resolved model id; pass it so
// computer-use / text-editor / bash tools get normalized to the
// canonical {type, name} pair Anthropic expects for the model's generation.
jsonBody, err = RemapRawToolVersionsForProvider(jsonBody, cfg.Provider, request.Model)
jsonBody, err = RemapRawToolVersionsForProvider(jsonBody, cfg.Provider, capModel)
if err != nil {
return nil, newErr(err.Error(), nil, jsonBody)
}
Expand All @@ -188,7 +191,7 @@ func BuildAnthropicResponsesRequestBody(ctx *schemas.BifrostContext, request *sc
}
}

jsonBody, err = StripUnsupportedFieldsFromRawBody(jsonBody, cfg.Provider, request.Model)
jsonBody, err = StripUnsupportedFieldsFromRawBody(jsonBody, cfg.Provider, capModel)
if err != nil {
return nil, newErr(schemas.ErrProviderRequestMarshal, err, jsonBody)
}
Expand Down
Loading
Loading