From 46c7ef228c1a039f4eeb7bf1db1dd250ac43ccd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 15:05:38 +0000 Subject: [PATCH] framework: bump core to v1.5.7 --skip-ci --- .gitignore | 4 +- .infisical.json | 5 + core/providers/bedrock/bedrock_test.go | 272 +++++++++++++++++++++++++ core/providers/bedrock/responses.go | 16 +- core/providers/bedrock/utils.go | 34 ++-- helm-charts/bifrost/Chart.yaml | 2 +- helm-charts/bifrost/README.md | 7 +- helm-charts/bifrost/values.yaml | 6 +- helm-charts/index.yaml | 21 ++ 9 files changed, 331 insertions(+), 36 deletions(-) create mode 100644 .infisical.json diff --git a/.gitignore b/.gitignore index b82e4df24f..a7c2e26109 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,6 @@ next-env.d.ts ui/app/routeTree.gen.ts .tanstack -.next \ No newline at end of file +.next + +.infisical diff --git a/.infisical.json b/.infisical.json new file mode 100644 index 0000000000..6ae3e19719 --- /dev/null +++ b/.infisical.json @@ -0,0 +1,5 @@ +{ + "workspaceId": "2de898b4-563f-4977-ac5f-7d66f5bb8590", + "defaultEnvironment": "", + "gitBranchToEnvironmentMapping": null +} \ No newline at end of file diff --git a/core/providers/bedrock/bedrock_test.go b/core/providers/bedrock/bedrock_test.go index 019749d8eb..c375f91689 100644 --- a/core/providers/bedrock/bedrock_test.go +++ b/core/providers/bedrock/bedrock_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/maximhq/bifrost/core/internal/llmtests" + "github.com/maximhq/bifrost/core/providers/anthropic" "github.com/maximhq/bifrost/core/providers/bedrock" "github.com/maximhq/bifrost/core/schemas" "github.com/stretchr/testify/assert" @@ -237,6 +238,120 @@ func TestBedrock(t *testing.T) { t.Run("BedrockTests", func(t *testing.T) { llmtests.RunAllComprehensiveTests(t, client, ctx, testConfig) }) + + // BedrockOpus47Tests subtree: live end-to-end repro of the user-reported + // regression on Claude Opus 4.7. GA structured outputs (output_config.format + // with json_schema) against Opus 4.7 on Bedrock currently fails with + // `output_config.format: Extra inputs are not permitted` after PR #3053 + // (commit 7df13ab45) tunneled `anthropic_beta: ["structured-outputs-2025-11-13"]` + // into additionalModelRequestFields. + // + // This subtree reuses the existing structured-output scenarios from + // core/internal/llmtests (RunStructuredOutputChatTest + + // RunStructuredOutputResponsesTest) so we exercise the SAME wire path the + // user's snippet (`client.messages.create(... output_config={"format":...})`) + // takes: Anthropic SDK -> /v1/messages -> ToBifrostResponsesRequest -> + // ToBedrockResponsesRequest. + // + // Naming places the leaf test at + // TestBedrock/BedrockOpus47Tests/TestBedrockOpus47StructuredOutputRegression + // so the Makefile's TESTCASE convention works: + // make test-core PROVIDER=bedrock TESTCASE=TestBedrockOpus47StructuredOutputRegression + // + // Skipped unless BEDROCK_OPUS_47_MODEL_ID is set to the exact Bedrock model + // id (or alias) for Claude Opus 4.7. We don't default this because per + // Anthropic's docs + // (cite: https://platform.claude.com/docs/en/docs/build-with-claude/structured-outputs) + // "Claude Opus 4.7 ... [is] available through Claude in Amazon Bedrock + // (the Messages-API Bedrock endpoint)" - i.e. not Converse - and the exact + // inference-profile id depends on the caller's Bedrock entitlements. + t.Run("BedrockOpus47Tests", func(t *testing.T) { + t.Run("TestBedrockOpus47StructuredOutputRegression", func(t *testing.T) { + modelID := strings.TrimSpace(os.Getenv("BEDROCK_OPUS_47_MODEL_ID")) + if modelID == "" { + t.Skip("Skipping Bedrock Opus 4.7 repro because BEDROCK_OPUS_47_MODEL_ID is not set (e.g. 'anthropic.claude-opus-4-7' or the inference-profile id you have entitlements for)") + } + t.Logf("Running Opus 4.7 structured-output repro against Bedrock model id: %s", modelID) + + // Mirror the user's failing Python snippet exactly: + // - Anthropic SDK call with system as a structured array (text block + // + cache_control: ephemeral) + // - user content as an array of text blocks + // - max_tokens: 4096 + // - output_config.format with json_schema and anyOf-style nullable + // fields (`{"anyOf":[{"type":"string"},{"type":"null"}]}`) + // - NO outer `anthropic-beta` HTTP header (the SDK does not auto-set + // it for GA output_config; the existing llmtests scenarios DO set + // it, which is why those scenarios pass on Opus 4.7 even today) + outputFormatJSON := json.RawMessage(`{ + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "isNewTopic": {"type": "boolean"}, + "title": {"anyOf": [{"type": "string"}, {"type": "null"}]}, + "result": {"anyOf": [{"type": "number"}, {"type": "null"}]} + }, + "required": ["isNewTopic", "title", "result"], + "additionalProperties": false + } + }`) + + anthropicReq := &anthropic.AnthropicMessageRequest{ + Model: modelID, + MaxTokens: 4096, + System: &anthropic.AnthropicContent{ + ContentBlocks: []anthropic.AnthropicContentBlock{ + { + Type: anthropic.AnthropicContentBlockTypeText, + Text: schemas.Ptr("You are an AI assistant. Analyze the user's message and respond with structured JSON."), + CacheControl: &schemas.CacheControl{Type: "ephemeral"}, + }, + }, + }, + Messages: []anthropic.AnthropicMessage{ + { + Role: anthropic.AnthropicMessageRoleUser, + Content: anthropic.AnthropicContent{ + ContentBlocks: []anthropic.AnthropicContentBlock{ + { + Type: anthropic.AnthropicContentBlockTypeText, + Text: schemas.Ptr("Hello, what's the result of 678*132?"), + }, + }, + }, + }, + }, + OutputConfig: &anthropic.AnthropicOutputConfig{ + Format: outputFormatJSON, + }, + } + + // Convert via the SAME entry point the HTTP integration uses + // (transports/bifrost-http/integrations/anthropic.go RequestConverter + // at lines 92-100 calls anthropicReq.ToBifrostResponsesRequest(ctx)). + reqCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline) + bifrostReq := anthropicReq.ToBifrostResponsesRequest(reqCtx) + require.NotNil(t, bifrostReq, "ToBifrostResponsesRequest returned nil") + bifrostReq.Provider = schemas.Bedrock + bifrostReq.Model = modelID + + // Send. NO BifrostContextKeyExtraHeaders — this is the key delta + // from llmtests.RunStructuredOutputResponsesTest (which sets + // `anthropic-beta: structured-outputs-2025-11-13` outer header + // at structured_outputs.go:411-418, masking the regression). + resp, bifrostErr := client.ResponsesRequest(reqCtx, bifrostReq) + + if bifrostErr != nil { + // Repro hit. Surface the full error for the user to confirm + // it matches the reported "output_config.format: Extra inputs + // are not permitted" Bedrock validator response. + t.Fatalf("Bedrock Opus 4.7 structured-output request failed (this is the regression repro): %s", llmtests.GetErrorMessage(bifrostErr)) + } + require.NotNil(t, resp, "expected non-nil response when error is nil") + t.Logf("Bedrock Opus 4.7 structured-output request SUCCEEDED. Response id=%v", resp.ID) + }) + }) } // TestBifrostToBedrockRequestConversion tests the conversion from Bifrost request to Bedrock request @@ -3292,6 +3407,163 @@ func TestAnthropicStructuredOutputAcceptsOrderedMaps(t *testing.T) { require.True(t, ok, "expected output_config.format.schema to remain ordered") } +// betaListContains reports whether the OrderedMap's anthropic_beta entry +// (regardless of slice element type) contains the given header value. +// Mirrors the multiple shapes appendAnthropicBetaToFields can leave behind +// (string, []string, []interface{}) so each test covers all three. +func betaListContains(t *testing.T, fields *schemas.OrderedMap, header string) bool { + t.Helper() + if fields == nil { + return false + } + raw, ok := fields.Get("anthropic_beta") + if !ok { + return false + } + switch v := raw.(type) { + case string: + return v == header + case []string: + for _, s := range v { + if s == header { + return true + } + } + case []interface{}: + for _, item := range v { + if s, ok := item.(string); ok && s == header { + return true + } + } + default: + t.Logf("unexpected anthropic_beta type %T: %#v", v, v) + } + return false +} + +// TestBedrockAnthropicChatStructuredOutputUsesSyntheticTool locks in Route A: +// Bedrock + Anthropic + json_schema response_format routes through the +// synthetic `bf_so_*` tool path (same as non-Anthropic Bedrock providers), +// not Bedrock's native `output_config.format`. Bedrock Converse's support for +// `output_config.format` is inconsistent across Claude variants (Opus 4.7 +// rejects with "output_config.format: Extra inputs are not permitted"); the +// synthetic-tool path is a regular Converse tool call that all variants +// accept reliably. +func TestBedrockAnthropicChatStructuredOutputUsesSyntheticTool(t *testing.T) { + responseFormat := any(map[string]any{ + "type": "json_schema", + "json_schema": map[string]any{ + "name": "classification", + "schema": map[string]any{ + "type": "object", + "properties": map[string]any{ + "isNewTopic": map[string]any{"type": "boolean"}, + "title": map[string]any{"type": "string"}, + "result": map[string]any{"type": "number"}, + }, + "required": []any{"isNewTopic", "title", "result"}, + }, + }, + }) + + bifrostReq := &schemas.BifrostChatRequest{ + Model: "anthropic.claude-opus-4-7-v1:0", + Input: []schemas.ChatMessage{ + { + Role: schemas.ChatMessageRoleUser, + Content: &schemas.ChatMessageContent{ + ContentStr: schemas.Ptr("Hello, what's the result of 678*132?"), + }, + }, + }, + Params: &schemas.ChatParameters{ + ResponseFormat: &responseFormat, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + result, err := bedrock.ToBedrockChatCompletionRequest(ctx, bifrostReq) + require.NoError(t, err) + require.NotNil(t, result) + + // Negative: no `output_config` and no structured-outputs beta tunnel + // in additionalModelRequestFields. PR #3053 added both; Route A removes them. + if result.AdditionalModelRequestFields != nil { + _, hasOutputConfig := result.AdditionalModelRequestFields.Get("output_config") + assert.False(t, hasOutputConfig, "expected NO output_config for Anthropic on Bedrock under Route A") + assert.False( + t, + betaListContains(t, result.AdditionalModelRequestFields, "structured-outputs-2025-11-13"), + "additionalModelRequestFields.anthropic_beta should NOT contain structured-outputs-2025-11-13", + ) + } + + // Positive: synthetic bf_so_* tool present and forced via tool_choice — + // this is the contract that replaces output_config.format on Bedrock. + require.NotNil(t, result.ToolConfig, "expected toolConfig with synthetic bf_so_* tool") + require.NotEmpty(t, result.ToolConfig.Tools, "expected at least one tool (the synthetic bf_so_*)") + require.NotNil(t, result.ToolConfig.ToolChoice, "expected forced tool_choice") + require.NotNil(t, result.ToolConfig.ToolChoice.Tool, "expected tool_choice to target a specific tool") + assert.Contains(t, result.ToolConfig.ToolChoice.Tool.Name, "bf_so_", "expected forced tool_choice to target bf_so_*") + assert.Equal(t, "bf_so_classification", result.ToolConfig.ToolChoice.Tool.Name) +} + +// TestToBedrockResponsesRequest_AnthropicStructuredOutputUsesSyntheticTool +// is the responses-path twin of TestBedrockAnthropicChatStructuredOutputUsesSyntheticTool. +// The user's failing request comes through the Anthropic Messages SDK +// (`client.messages.create`), routed via /v1/messages -> ToBifrostResponsesRequest +// -> ToBedrockResponsesRequest with Params.Text.Format set. +func TestToBedrockResponsesRequest_AnthropicStructuredOutputUsesSyntheticTool(t *testing.T) { + schemaObj := any(schemas.NewOrderedMapFromPairs( + schemas.KV("type", "object"), + schemas.KV("properties", schemas.NewOrderedMapFromPairs( + schemas.KV("isNewTopic", schemas.NewOrderedMapFromPairs(schemas.KV("type", "boolean"))), + schemas.KV("title", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))), + schemas.KV("result", schemas.NewOrderedMapFromPairs(schemas.KV("type", "number"))), + )), + schemas.KV("required", []string{"isNewTopic", "title", "result"}), + )) + + req := &schemas.BifrostResponsesRequest{ + Model: "anthropic.claude-opus-4-7-v1:0", + Params: &schemas.ResponsesParameters{ + Text: &schemas.ResponsesTextConfig{ + Format: &schemas.ResponsesTextConfigFormat{ + Type: "json_schema", + Name: schemas.Ptr("classification"), + JSONSchema: &schemas.ResponsesTextConfigFormatJSONSchema{ + Schema: &schemaObj, + }, + }, + }, + }, + } + + ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline) + bedrockReq, err := bedrock.ToBedrockResponsesRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, bedrockReq) + + // Negative: no output_config, no structured-outputs beta tunnel. + if bedrockReq.AdditionalModelRequestFields != nil { + _, hasOutputConfig := bedrockReq.AdditionalModelRequestFields.Get("output_config") + assert.False(t, hasOutputConfig, "expected NO output_config for Anthropic on Bedrock under Route A") + assert.False( + t, + betaListContains(t, bedrockReq.AdditionalModelRequestFields, "structured-outputs-2025-11-13"), + "additionalModelRequestFields.anthropic_beta should NOT contain structured-outputs-2025-11-13", + ) + } + + // Positive: synthetic bf_so_* tool injected and forced. + require.NotNil(t, bedrockReq.ToolConfig, "expected toolConfig with synthetic bf_so_* tool") + require.NotEmpty(t, bedrockReq.ToolConfig.Tools, "expected at least one tool (the synthetic bf_so_*)") + require.NotNil(t, bedrockReq.ToolConfig.ToolChoice, "expected forced tool_choice") + require.NotNil(t, bedrockReq.ToolConfig.ToolChoice.Tool, "expected tool_choice to target a specific tool") + assert.Contains(t, bedrockReq.ToolConfig.ToolChoice.Tool.Name, "bf_so_", "expected forced tool_choice to target bf_so_*") + assert.Equal(t, "bf_so_classification", bedrockReq.ToolConfig.ToolChoice.Tool.Name) +} + // TestNonAnthropicStructuredOutputStillUsesToolConversion ensures Bedrock models // other than Anthropic continue to use the legacy response_format->tool path. func TestNonAnthropicStructuredOutputStillUsesToolConversion(t *testing.T) { diff --git a/core/providers/bedrock/responses.go b/core/providers/bedrock/responses.go index d66633293f..525ccd9be7 100644 --- a/core/providers/bedrock/responses.go +++ b/core/providers/bedrock/responses.go @@ -1839,16 +1839,12 @@ func ToBedrockResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schemas. } if bifrostReq.Params.Text != nil { if bifrostReq.Params.Text.Format != nil { - responseFormatTool, anthropicOutputFormat := convertTextFormatToTool(ctx, bifrostReq.Model, bifrostReq.Params.Text) - if anthropicOutputFormat != nil { - if bedrockReq.AdditionalModelRequestFields == nil { - bedrockReq.AdditionalModelRequestFields = schemas.NewOrderedMap() - } - setOutputConfigField(bedrockReq.AdditionalModelRequestFields, "format", anthropicOutputFormat) - appendAnthropicBetaToFields(bedrockReq.AdditionalModelRequestFields, anthropic.AnthropicStructuredOutputsBetaHeader) - } - // Defer synthetic tool injection until after normal tool/tool_choice conversion - // so the structured-output tool is not overwritten by the later pass. + // Bedrock structured output goes through the synthetic `bf_so_*` + // tool path for all models, including Anthropic. We capture the + // tool here and defer injection until after normal tool/tool_choice + // conversion so the forced structured-output tool choice is not + // overwritten. + responseFormatTool, _ := convertTextFormatToTool(ctx, bifrostReq.Model, bifrostReq.Params.Text) if responseFormatTool != nil { responsesStructuredOutputTool = responseFormatTool } diff --git a/core/providers/bedrock/utils.go b/core/providers/bedrock/utils.go index 404c89fba4..f8b9bca0a6 100644 --- a/core/providers/bedrock/utils.go +++ b/core/providers/bedrock/utils.go @@ -76,20 +76,13 @@ func convertChatParameters(ctx *schemas.BifrostContext, bifrostReq *schemas.Bifr bedrockReq.InferenceConfig = inferenceConfig } - // Handle structured output conversion: - // - Anthropic models on Bedrock use native output_config.format - // - Other models keep the response_format->tool conversion. - responseFormatTool, anthropicOutputFormat := convertResponseFormatToTool(ctx, bifrostReq.Model, bifrostReq.Params) - if anthropicOutputFormat != nil { - if bedrockReq.AdditionalModelRequestFields == nil { - bedrockReq.AdditionalModelRequestFields = schemas.NewOrderedMap() - } - setOutputConfigField(bedrockReq.AdditionalModelRequestFields, "format", anthropicOutputFormat) - // The outer HTTP anthropic-beta header is consumed by Bedrock's edge and not forwarded - // to the underlying Claude model, so the beta value must also live in - // additionalModelRequestFields for the model to recognise output_config.format. - appendAnthropicBetaToFields(bedrockReq.AdditionalModelRequestFields, anthropic.AnthropicStructuredOutputsBetaHeader) - } + // Handle structured output conversion through the synthetic `bf_so_*` tool + // path for all Bedrock models, including Anthropic. We avoid native + // `output_config.format` because Bedrock Converse rejects it on some Claude + // variants (e.g. Opus 4.7 returns "output_config.format: Extra inputs are not + // permitted"), whereas the synthetic-tool path is a regular Converse tool + // call accepted by all variants. + responseFormatTool, _ := convertResponseFormatToTool(ctx, bifrostReq.Model, bifrostReq.Params) // Filter provider-unsupported server tools once; both convertToolConfig and // collectBedrockServerTools consume the same filtered set, and @@ -1060,11 +1053,9 @@ func convertResponseFormatToTool( return nil, nil } - // Anthropic Bedrock supports native output_config.format. Keep this provider-specific - // conversion encapsulated here, and let caller just apply returned values. - if schemas.IsAnthropicModel(model) { - return nil, newAnthropicOutputFormatOrderedMap(schemaObj) - } + // All Bedrock models (including Anthropic) use the synthetic `bf_so_*` tool + // path; native `output_config.format` is intentionally avoided due to + // Converse's inconsistent support across Claude variants. // Extract name and schema toolNameRaw, hasName := jsonSchemaObj.Get("name") @@ -1224,9 +1215,8 @@ func convertTextFormatToTool(ctx *schemas.BifrostContext, model string, textConf description = *format.JSONSchema.Description } - if schemas.IsAnthropicModel(model) { - return nil, newAnthropicOutputFormatOrderedMap(schemaObj) - } + // All Bedrock models use the synthetic `bf_so_*` tool path here as well. + // See convertResponseFormatToTool for the rationale. toolName = fmt.Sprintf("bf_so_%s", toolName) ctx.SetValue(schemas.BifrostContextKeyStructuredOutputToolName, toolName) diff --git a/helm-charts/bifrost/Chart.yaml b/helm-charts/bifrost/Chart.yaml index f1de608b3d..87293c2fd1 100644 --- a/helm-charts/bifrost/Chart.yaml +++ b/helm-charts/bifrost/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: bifrost description: A Helm chart for deploying Bifrost - AI Gateway with unified interface for multiple providers type: application -version: 2.1.12 +version: 2.1.13 appVersion: "1.5.0-prerelease7" keywords: - ai diff --git a/helm-charts/bifrost/README.md b/helm-charts/bifrost/README.md index 9d5485b8d8..54cdedaaef 100644 --- a/helm-charts/bifrost/README.md +++ b/helm-charts/bifrost/README.md @@ -4,10 +4,15 @@ Official Helm charts for deploying [Bifrost](https://github.com/maximhq/bifrost) - a high-performance AI gateway with unified interface for multiple providers. -**Latest Version:** 2.1.12 +**Latest Version:** 2.1.13 ## Changelog +### 2.1.13 + +- Surfaced `bifrost.client.enforceAuthOnInference` in `values.yaml` as a commented default with usage notes. The field was already wired in `_helpers.tpl` to render to `client.enforce_auth_on_inference` and declared in `values.schema.json`; this change makes the knob discoverable without altering default rendered config. +- Marked `bifrost.client.enforceGovernanceHeader` as deprecated in `values.yaml` (use `enforceAuthOnInference` instead). Schema description was already deprecated in 2.1.11. + ### 2.1.12 - Added Helm support for `storage.logsStore.objectStorageExcludeFields` and render path to `logs_store.object_storage_exclude_fields` in generated config. diff --git a/helm-charts/bifrost/values.yaml b/helm-charts/bifrost/values.yaml index b2d4d2e765..fd6a6712d7 100644 --- a/helm-charts/bifrost/values.yaml +++ b/helm-charts/bifrost/values.yaml @@ -202,7 +202,11 @@ bifrost: disableContentLogging: false disableDbPingsInHealth: false logRetentionDays: 365 + # Deprecated: use enforceAuthOnInference instead. enforceGovernanceHeader: false + # Require auth (VK, API key, or user token) on inference endpoints. + # When unset, inference endpoints follow authConfig.disableAuthOnInference behavior. + enforceAuthOnInference: true allowDirectKeys: false maxRequestBodySizeMb: 100 compat: @@ -973,4 +977,4 @@ envFrom: [] # Init containers to run before the main application container. # Provide a list of init containers using standard Kubernetes container spec. -initContainers: [] +initContainers: [] \ No newline at end of file diff --git a/helm-charts/index.yaml b/helm-charts/index.yaml index 1ac923190f..50ecd1304c 100644 --- a/helm-charts/index.yaml +++ b/helm-charts/index.yaml @@ -1,6 +1,27 @@ apiVersion: v1 entries: bifrost: + - apiVersion: v2 + appVersion: 1.5.0-prerelease7 + created: "2026-05-02T00:00:00.000000+00:00" + description: A Helm chart for deploying Bifrost - AI Gateway with unified interface for multiple providers + digest: "" + home: https://www.getmaxim.ai/bifrost + icon: https://www.getbifrost.ai/favicon.png + keywords: + - ai + - gateway + - llm + maintainers: + - email: support@getbifrost.ai + name: Bifrost Team + name: bifrost + sources: + - https://github.com/maximhq/bifrost + type: application + urls: + - https://maximhq.github.io/bifrost/helm-charts/bifrost-2.1.13.tgz + version: 2.1.13 - apiVersion: v2 appVersion: 1.5.0-prerelease7 created: "2026-04-30T12:30:00.000000+00:00"