feat: add per-provider disable_model_discovery to skip live model discovery - #4582
feat: add per-provider disable_model_discovery to skip live model discovery#4582ragokan wants to merge 68 commits into
Conversation
## Summary Fixes a bug where OTEL plugin headers were being overwritten with redacted placeholder values when saving a plugin configuration. After the multi-profile change, header values stored as plain strings inside the `profiles` array were not being restored from the database before saving, causing real credentials to be replaced with masked values like `****`. ## Changes - Extracted `restoreRedactedValue` as a standalone recursive helper, replacing the inline logic in `restoreRedactedFromExisting`. This allows the restoration logic to descend into both nested maps and slices. - Added slice traversal support (index-aligned) so that elements within arrays like the OTEL `profiles` array are individually checked and restored. - Added plain-string redaction detection so that header values stored as raw strings (rather than `EnvVar` objects) are also restored from the existing DB config when they carry a redaction artifact. Empty strings are intentionally left as-is to allow clearing a value. - Added `TestRestoreRedacted_OTELProfilesHeaders` to cover both failure modes: slice traversal and plain-string secret restoration. Also asserts that genuinely new (non-redacted) values pass through unchanged. ## Type of change - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./transports/bifrost-http/handlers/... ``` Verify that saving an OTEL plugin configuration with multiple profiles, after a GET that returns redacted header values, does not overwrite the stored credentials in the database. Confirm that providing a genuinely new header value still persists correctly. ## Screenshots/Recordings N/A ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations This fix ensures that redacted credential placeholders returned to the client are never written back over real secrets stored in the database. The restoration logic only replaces values that are confirmed redaction artifacts; empty strings and non-redacted values are always passed through as-is, preserving the ability to clear a credential intentionally. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Updates e2e tests for provider governance budgets and custom provider validation to align with recent UI changes, and fixes base URLs used in test fixtures to use real provider endpoints. ## Changes - Added `addBudgetLine()` and `getBudgetAmountInput()` helper methods to `ProvidersPage` to interact with the new budget line UI, replacing the old static `#providerBudgetMaxLimit` locator - Updated governance budget tests to call `addBudgetLine()` before interacting with the budget amount input, reflecting the new add-then-fill flow - Replaced placeholder/fake base URLs in custom provider test fixtures with real provider endpoints (`https://api.openai.com/v1`, `https://api.anthropic.com`) to avoid false failures from URL validation - Added a new test case that verifies an error toast is shown when a custom provider is saved with an invalid/non-resolvable hostname ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [x] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh cd tests/e2e pnpm test --grep "Providers|Governance" ``` Expected outcomes: - Custom provider creation tests pass with real base URLs - Governance budget tab tests correctly add a budget line before asserting input visibility - A new test confirms that submitting a custom provider with an invalid hostname (`https://api.nonexistent-provider.invalid/v1`) surfaces an "Invalid base URL" error toast and keeps the sheet open ## Screenshots/Recordings N/A ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations No security implications. Test fixtures now use real provider hostnames, but no real credentials are used. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Updates the model limits E2E page object to align with the current UI, where the model selector now defaults to "All Models" via a combobox rather than a searchable multiselect. Also introduces a reusable `setBudget` helper that handles adding a budget line, filling the amount, and optionally selecting a reset period. ## Changes - Replaced the model multiselect search-and-select flow with a single combobox click that selects "All Models", reflecting the current UI behavior. The selected model name is now hardcoded to `'*'`. - Extracted budget configuration into a private `setBudget` method used by both `createModelLimit` and `updateModelLimit`, replacing the previous inline `#modelBudgetMaxLimit` locator usage. - Added a `resetPeriodLabels` map to translate duration shorthand keys (e.g. `'1h'`, `'1d'`) into their human-readable dropdown labels (e.g. `'Hourly'`, `'Daily'`) for selecting reset periods in the budget line combobox. ## Type of change - [ ] Bug fix - [ ] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test Run the model limits E2E tests and verify that limit creation and update flows complete without errors, including budget lines with reset periods. ```sh cd tests/e2e pnpm test --grep "model-limits" ``` ## Breaking changes - [ ] Yes - [x] No ## Related issues ## Security considerations None. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Refactors the OAuth e2e test flow to use a direct API-based approach instead of relying on browser popup events. This resolves flakiness caused by waiting for popup windows to open and close during OAuth authorization in tests. ## Changes - Introduced a `completeOAuthFlow` helper that navigates to the authorization URL in a new page, completes the login form, then polls the OAuth status endpoint until `authorized` and calls the complete-oauth endpoint directly — bypassing the need to intercept popup events. - Added `createOAuthClient` method to `MCPRegistryPage` that intercepts the `pending_oauth` API response and returns the `authorize_url`, `oauth_config_id`, and related fields for use in `completeOAuthFlow`. - Updated `selectAuthType` to reflect the new UI split between auth type (e.g., `oauth`) and auth scope (e.g., `shared` vs `per_user`). `per_user_oauth` is now expressed as auth type `oauth` + scope `per_user`. - Added `selectAuthScope` method to handle the new `auth-scope-select` dropdown. - Added `expandOAuthAdvancedIfCollapsed` to expand the collapsible OAuth advanced section before interacting with fields like client ID and secret. - Updated OAuth input locators to prefer `data-testid` attributes with placeholder-based selectors as fallbacks. - Updated `viewClientDetails` to open the actions menu and click "Edit" rather than clicking the row directly. ## Type of change - [ ] Bug fix - [x] Refactor - [ ] Feature - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [x] Docs ## How to test Run the MCP registry e2e tests with the OAuth demo server running: ```sh MCP_SSE_HEADERS=1 npx playwright test tests/e2e/features/mcp-registry/mcp-registry.spec.ts ``` Verify that the OAuth and per-user OAuth client creation tests complete without flakiness, and that the created clients appear as connected in the registry table. ## Screenshots/Recordings N/A — no UI visual changes. ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations No new auth flows or secrets handling introduced. The test helper uses the existing OAuth demo server and status/complete endpoints already present in the application. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Adds a `test-cost-accuracy` CI job to the release pipeline that validates the correctness of LLM cost tracking end-to-end — from per-request log entries through aggregated stats, virtual key budget usage, and per-model quota breakdowns. ## Changes - Introduces a new `test-cost-accuracy` pipeline job that runs against any release-triggering component (core, framework, plugins, bifrost-http, or docker). - Adds `.github/workflows/scripts/cost-accuracy-test.sh`, which: - Spins up Postgres via the existing Docker Compose config and creates an isolated database. - Builds `bifrost-http`, `mocker`, and `hitter` binaries from source. - Writes a Bifrost config pointing at the mocker as the OpenAI provider backend. - Creates a virtual key with a budget and a scoped pricing override for `gpt-4o-mini` with configurable `INPUT_COST_PER_TOKEN` and `OUTPUT_COST_PER_TOKEN`. - Drives traffic through `hitter` at a configurable RPS and duration. - Validates that per-log costs, the `/api/logs/stats` total, virtual key budget `current_usage`, and per-model quota totals all match the expected value computed from token counts and the pricing override — failing with a detailed diff if any surface diverges by more than `1e-12`. - Wires `test-cost-accuracy` into the `needs` and gate conditions of all downstream release jobs (`core-release`, `framework-release`, `plugins-release`, `bifrost-http-release`, and all Docker publish jobs) so a cost accuracy failure blocks a release. - Uploads results and logs to a `cost-accuracy-results` artifact retained for 30 days. - Fixes a missing newline at the end of the workflow file. ## Type of change - [ ] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [x] Chore/CI ## Affected areas - [ ] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test The test runs automatically in CI on any branch that triggers a release. To run locally: ```sh export BENCHMARK_DIR=/path/to/bifrost-benchmarking # optional; cloned automatically if absent export COST_ACCURACY_RPS=10 export COST_ACCURACY_DURATION=10s export INPUT_COST_PER_TOKEN=0.000001 export OUTPUT_COST_PER_TOKEN=0.000002 chmod +x .github/workflows/scripts/cost-accuracy-test.sh .github/workflows/scripts/cost-accuracy-test.sh ``` Results are written to `tmp/cost-accuracy/results.json`. The script exits non-zero and prints a JSON diff if any cost surface mismatches. **Environment variables:** | Variable | Default | Description | |---|---|---| | `COST_ACCURACY_RPS` | `10` | Requests per second sent by hitter | | `COST_ACCURACY_DURATION` | `10s` | Duration of the load run | | `INPUT_COST_PER_TOKEN` | `0.000001` | Pricing override input rate | | `OUTPUT_COST_PER_TOKEN` | `0.000002` | Pricing override output rate | | `VIRTUAL_KEY_BUDGET_LIMIT` | `100` | Budget cap on the test virtual key | | `BENCHMARK_DIR` | `../bifrost-benchmarking` | Path to the benchmarking repo | ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations The job uses `egress-policy: block` with an explicit allowlist of endpoints. The mocker key and Postgres credentials are test-only values scoped to the ephemeral CI environment and are not persisted. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Bumps the indirect dependency `go.mongodb.org/mongo-driver` from `v1.17.6` to `v1.17.7` across all Go modules in the repository. ## Changes - Updated `go.mongodb.org/mongo-driver` from `v1.17.6` to `v1.17.7` in `framework`, `transports`, and all plugins (`compat`, `governance`, `logging`, `maxim`, `modelcatalogresolver`, `otel`, `semanticcache`, `telemetry`). ## Type of change - [ ] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [x] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./... ``` ## Breaking changes - [ ] Yes - [x] No ## Related issues N/A ## Security considerations Patch-level dependency update to the MongoDB Go driver. No known security implications introduced by this change; upgrading to the latest patch may include upstream bug or security fixes. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [ ] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary MCP client connections are now deferred until after all plugins are registered, ensuring `PreMCPConnectionHook` runs against the complete plugin set. Previously, `NewMCPManager` dialed clients immediately during construction, which meant plugins registered after `Init` (e.g. enterprise plugins) were silently excluded from the hook and the client would only recover on a later health-monitor reconnect cycle. ## Changes - Extracted the parallel client-dialing logic from `NewMCPManager` into a new `ConnectConfiguredClients` method on `MCPManager`. Construction now only stores the boot configs; callers must explicitly invoke `ConnectConfiguredClients` when ready. - Added `ConnectConfiguredMCPClients` on `Bifrost` as the public entry point, delegating to `MCPManager.ConnectConfiguredClients` when MCP is configured. - Added `ConnectConfiguredClients` to `MCPManagerInterface` to keep the interface consistent. - In the HTTP server's `Bootstrap`, `ConnectConfiguredMCPClients` is called after all inference routes (and therefore all plugins) are registered. - Updated the MCP test fixture helper `setupMCPManager` to call `ConnectConfiguredClients` explicitly after construction. - Renamed the `logs_add_canonical_model_columns` migration to `logs_add_canonical_model_columns_v2` to fix a previously broken migration. ## Type of change - [ ] Bug fix - [ ] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh # Core/Transports go version go test ./... go test ./core/internal/mcptests/... ``` 1. Configure one or more MCP clients in `MCPConfig.ClientConfigs` alongside at least one plugin that implements `PreMCPConnectionHook`. 2. Start the HTTP server and confirm the hook is invoked for each configured client during `Bootstrap` rather than during `Init`. 3. Simulate a connection failure for a boot client and verify the client is retained in `Disconnected` state and the health monitor recovers it automatically. ## Breaking changes - [x] Yes - [ ] No Callers that construct `MCPManager` directly via `NewMCPManager` must now call `manager.ConnectConfiguredClients(ctx)` explicitly after construction. The HTTP server transport handles this automatically. Any custom transport or embedding that relied on auto-connect during `NewMCPManager` will need to add this call. ## Related issues Closes maximhq#4556 (Anthropic duplicate `message_start` stream event, included in changelog) ## Security considerations No new auth surfaces or PII handling introduced. The change only affects the timing of MCP client connection establishment. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Per-request extra headers set via `BifrostContextKeyMCPExtraHeaders` in a `PreMCPHook` were not reaching the upstream MCP server for health-check probes (`ping` and `tools/list`). The `mcp-go` client drops `request.Header` for these internally-generated calls, so headers injected at the `CallToolRequest` level were silently lost. This PR centralizes all per-request extra header injection onto the transport layer via `WithHTTPHeaderFunc` / `WithHeaderFunc`, ensuring headers flow on every outgoing message — including `ping`, `tools/list`, and `tools/call` — while still being filtered by `AllowedExtraHeaders`. ## Changes - Registered a `headerFunc` on the `StreamableHTTP` and `SSE` transports (in `createHTTPConnection`, `createSSEConnection`, and `AcquireClientConn`) that reads `BifrostContextKeyMCPExtraHeaders` from the request context and injects only allowlisted headers per `MCPClientConfig.AllowedExtraHeaders`. This replaces the previous per-call `CallToolRequest.Header` approach. - Removed `credStore.RequestHeaders` calls and `CallToolRequest.Header` assignments from `executeToolInternal` (tool manager) and `callMCPTool` (Starlark code mode), since header injection is now handled uniformly by the transport. - Fixed `runListToolsWithHooks` and `runPingWithHooks` to pass `gateCtx` (the child context that carries `PreMCPHook` writes) instead of the outer `ctx` to the wire calls, so transport `headerFunc` can see values written during the plugin gate. - Relaxed `ExtractFilteredExtras` to accept a plain `context.Context` instead of `*schemas.BifrostContext`, enabling it to be called from the transport `headerFunc` closure. - Added end-to-end wire-level tests (`extraheaders_test.go`) using a real `httptest` streamable-HTTP server that records inbound headers per JSON-RPC method, covering: allowlisted headers reaching `ping`, `tools/list`, and `tools/call`; and non-allowlisted headers being filtered on all requests. ## Type of change - [x] Bug fix - [ ] Feature - [x] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./core/internal/mcptests/... -run TestExtraHeaders -v go test ./... ``` The three new tests validate: - `TestExtraHeadersHealthCheckPingReachWire` — allowlisted header appears on `ping` probes; non-allowlisted header never appears on any request. - `TestExtraHeadersHealthCheckListToolsReachWire` — same guarantee for `tools/list` health-check probes when ping is unavailable. - `TestExtraHeadersToolCallReachWire` — allowlisted header appears on a normal `tools/call`; non-allowlisted header is filtered. ## Breaking changes - [x] No The `CallToolRequest.Header` field is no longer populated by Bifrost internals, but this is an internal implementation detail with no public API impact. Header forwarding behavior is preserved (and extended to health-check probes). ## Security considerations `AllowedExtraHeaders` filtering is now enforced at the transport layer for all outgoing MCP requests. Non-allowlisted headers set by plugins are dropped before reaching the wire on every request type, including health-check probes that previously bypassed the per-call header path entirely. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Fixes a regression introduced in v1.5.0 (issue closes maximhq#3795) where a `/v1/responses` request carrying a Bifrost-hosted `mcp` server tool alongside function tools would fail with `"tool type 'mcp' is not supported by provider 'bedrock'"`. The Responses path now silently strips provider-unsupported tools instead of rejecting the entire request, matching the existing behavior of the Chat path. ## Changes - Introduced `ValidateResponsesToolsForProvider` in `anthropic/utils.go` — a Responses-path mirror of `ValidateChatToolsForProvider`. It partitions `[]schemas.ResponsesTool` into a keep-set and a dropped-set using the same per-type feature flags as `ValidateToolsForProvider`, but returns both sets instead of erroring, leaving policy decisions to callers. - Updated `ToBedrockResponsesRequest` in `bedrock/responses.go` to call `ValidateResponsesToolsForProvider` and use the filtered keep-set for tool conversion, rather than calling `ValidateToolsForProvider` and returning an error on the first unsupported tool. - Updated `BuildAnthropicResponsesRequestBody` in `anthropic/requestbuilder.go` to strip unsupported tools via a shallow copy of the request (so the shared/pooled inbound request and its `Params` are never mutated) instead of failing the request. - Updated the `ValidateTools` field comment to reflect the new strip-silently policy. - Added `validateresponsestools_test.go` with a dedicated test table covering Bedrock, Vertex, Anthropic, Azure, unknown providers, and forward-compat cases. - Added regression tests in `bedrock_test.go` covering the mixed mcp+function case and the all-tools-dropped case. - Updated the existing `requestbuilder_test.go` test to assert that unsupported tools are stripped (not rejected) and that the inbound request is not mutated. ## Type of change - [x] Bug fix ## Affected areas - [x] Core (Go) - [x] Providers/Integrations ## How to test ```sh go test ./core/providers/anthropic/... ./core/providers/bedrock/... ``` Expected: all tests pass, including the new regression guards for issue maximhq#3795. Specifically, a `/v1/responses` request to Bedrock with a mixed `mcp` + function tool list should succeed, with only the function tool forwarded to Bedrock and the `mcp` tool silently dropped. The inbound request's tool slice must remain unmodified. ## Breaking changes - [x] No ## Related issues Closes maximhq#3795 ## Security considerations None. The change only affects which tools are forwarded to downstream providers. Unsupported tools are dropped rather than causing a hard failure; no secrets, auth, or PII handling is affected. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
…#4574) ## Summary Adds support for loading Bifrost's pricing and model parameters datasheets from local files using the `file://` URL scheme. This resolves a connectivity issue (closes maximhq#4305) where hosts without outbound internet access, or behind HTTP proxies that block DNS resolution of `getbifrost.ai`, could not start Bifrost successfully. ## Changes - Added a new example config at `examples/configs/withlocalpricingfiles/` demonstrating how to configure `pricing_url` and `model_parameters_url` with `file://` paths, along with sample pricing and model parameters datasheets containing real entries for `gpt-4o`, `gpt-4o-mini`, `gpt-4.1`, `claude-sonnet-4-20250514`, and `text-embedding-3-small`. - Added unit tests in `framework/modelcatalog/datasheet/localfiles_test.go` that verify both datasheets load correctly from `file://` URLs without any network access or hostname resolution. A dedicated regression test (`TestLoadFromLocalFiles_NeverResolvesHostname`) guards against the `file://` scheme accidentally falling through to external URL validation. - Added `testdata/` fixtures mirroring the example datasheets for use by the tests. The `file://` scheme short-circuits external URL validation and hostname lookup entirely, reading the path directly off disk. ## Type of change - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [ ] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [x] Docs ## How to test ```sh go test ./framework/modelcatalog/datasheet/... ``` To validate the Docker example: ```sh cd examples/configs/withlocalpricingfiles docker run -p 8080:8080 \ -e OPENAI_API_KEY=sk-... \ -v "$(pwd)/config.json:/app/data/config.json" \ -v "$(pwd)/pricing.json:/opt/bifrost/pricing.json" \ -v "$(pwd)/model-parameters.json:/opt/bifrost/model-parameters.json" \ maximhq/bifrost ``` Bifrost should start without attempting to reach `getbifrost.ai`. Pricing and model parameter lookups for the bundled models should resolve correctly from the local files. **New config fields:** | Field | Example value | Description | | --- | --- | --- | | `framework.pricing.pricing_url` | `file:///opt/bifrost/pricing.json` | Local path to the pricing datasheet | | `framework.pricing.model_parameters_url` | `file:///opt/bifrost/model-parameters.json` | Local path to the model parameters datasheet | ## Breaking changes - [ ] Yes - [x] No ## Related issues Closes maximhq#4305 ## Security considerations The `file://` scheme reads arbitrary paths off disk as the process user. Operators should ensure the mounted files are not world-writable and that the container or process runs with appropriate least-privilege permissions. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [x] I updated documentation where needed - [ ] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
Affected packages: - transports/bifrost-http/integrations/ - transports/changelog.md
## Summary Providers (Anthropic, OpenAI, Gemini, Cohere, Bedrock) bill for tokens they process regardless of whether the client receives the response. Previously, if a streaming request was cancelled mid-flight or a non-streaming request timed out after the provider had already consumed input tokens, Bifrost recorded zero cost and zero tokens — silently under-billing. This PR closes that gap by propagating partial/full usage from failed and cancelled requests through the post-LLM hook pipeline so governance budgets and logging reflect what the provider actually charged. Closes maximhq#3357 ## Changes - **Streaming providers** (`anthropic`, `openai`, `gemini`, `cohere`, `bedrock`) now register an in-place `*BifrostLLMUsage` handle on the context (`BifrostContextKeyStreamAccumulatedUsage`) at the start of each stream. The handle is mutated as usage chunks arrive, so `HandleStreamCancellation` and `HandleStreamTimeout` in `providers/utils` can read the latest accumulated usage and attach it to the `BifrostError.ExtraFields.BilledUsage` field before running post-hooks. - **Non-streaming cancellation** (`core/bifrost.go`) — when `requestWorker` detects that the client context was cancelled after the provider had already returned a result or error (the `ctx.Done` branch of the response-send select), it now calls `billAbandonedTerminal`, which runs terminal post-LLM hooks for that result/error. A channel rendezvous guarantee ensures `tryRequest` cannot also receive the same value, so hooks never fire twice for one call. - **`BifrostError.ExtraFields.BilledUsage`** (`schemas/bifrost.go`) — new optional field carrying provider-reported token usage on failed/cancelled requests. Nil when the failure consumed no tokens (e.g. 401/403/429 before the model ran). - **`BifrostContextKeyStreamAccumulatedUsage`** (`schemas/bifrost.go`) — new context key for the streaming usage handle. - **Governance plugin** (`plugins/governance/main.go`, `tracker.go`) — `postHookWorker` now accepts the `BifrostError` and bills `BilledUsage` when present on a failed request. `UpdateUsage` no longer skips all failed requests; it skips only those with zero tokens and zero cost. A billing idempotency set (`RequestID + AttemptNumber`) prevents the same physical provider call from being billed twice when both the core cancellation path and the provider goroutine's terminal hook fire. The idempotency map is swept on the existing reset-worker tick using a 5-minute TTL. Request counts are only incremented for successful requests. - **Logging plugin** (`plugins/logging/main.go`) — fills `TokenUsageParsed`, `PromptTokens`, `CompletionTokens`, `TotalTokens`, and `Cost` from `BilledUsage` on error entries when stream accumulation did not already capture usage. - **Model catalog** (`framework/modelcatalog`, `datasheet/cost.go`) — new `CalculateCostForUsage` helper computes cost from a bare `BifrostLLMUsage` object (provider + model + request type) using the same pricing path as `CalculateCost`, so success and failure billing use identical rates. `calculateBaseCost` is refactored to share a `computeCostFromInput` helper with the new function. - **E2E test collection** (`tests/e2e/api/collections/provider-harness.json`) — new "Costing — Failed/Cancelled Requests" folder with four cases (streaming chat, non-streaming chat, embeddings, transcription) that pin `x-request-id` and `x-bf-expect-cost: true`. - **Newman DB-verify reporter** (`newman-reporter-dbverify/index.js`) — new `verifyCostingRequest` function polls the logs table with exponential backoff and asserts `cost > 0 && total_tokens > 0` for the pinned request ID, covering the async write + deferred usage path. ## Type of change - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [x] Core (Go) - [ ] Transports (HTTP) - [x] Providers/Integrations - [x] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./core/... go test ./plugins/governance/... go test ./framework/modelcatalog/... ``` Key unit tests added: - `TestCalculateCostForUsage_MatchesCalculateCost` — bare-usage cost equals full-response cost for the same tokens. - `TestCalculateCostForUsage_NilUsageIsZero` — nil usage bills nothing. - `TestUsageTracker_FailedRequestWithUsage_IsBilled` — a failed request that consumed tokens updates the budget. - `TestUsageTracker_FailedRequestNoUsage_IsSkipped` — a failed request with no tokens does not update the budget. - `TestUsageTracker_Idempotency_SameAttemptBilledOnce` — duplicate settlement for the same `RequestID + AttemptNumber` bills exactly once. - `TestUsageTracker_Idempotency_DifferentAttemptsBothBilled` — distinct attempts under one `RequestID` each bill independently. For streaming cancellation, use `tests/e2e/api/runners/run-stream-cancellation.mjs` to abort a stream mid-response and verify the logs row for `costing-stream-chat` shows `cost > 0` and `total_tokens > 0`. ## Breaking changes - [x] No `BifrostError.ExtraFields.BilledUsage` is a new optional JSON field (`omitempty`). Existing consumers that ignore unknown fields are unaffected. The governance `UsageUpdate` struct gains `AttemptNumber` and `BilledReason` fields, also `omitempty`. ## Security considerations No new auth surfaces, secrets, or PII handling. The billing idempotency map is in-process and bounded by TTL sweep; it does not persist across restarts, which is acceptable since in-flight requests do not survive a restart. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
## Summary Adds an end-to-end regression test suite covering virtual key (VK) scoped model list restrictions. When a VK is configured to allow only a subset of providers, both the `/api/models` and `/v1/models` endpoints must return exclusively those providers — no leakage from other configured providers (e.g. mistral, gemini, groq). ## Changes - Added a new Postman collection folder `Governance - VK List Models Restriction` that covers regression maximhq#2887 / PRs maximhq#3187 and maximhq#3094. - The test flow creates a VK scoped to `openai` and `anthropic` only, then validates both list-models endpoints (`/api/models` and `/v1/models`) return only those providers and explicitly exclude `mistral`, `gemini`, and `groq`. - A teardown step deletes the VK after the tests complete to keep the environment clean. ## Type of change - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [x] Transports (HTTP) - [ ] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test Run the Postman collection against a live Bifrost instance that has multiple providers configured (including openai, anthropic, mistral, gemini, and groq): ```sh newman run tests/e2e/api/collections/bifrost-api-management.postman_collection.json \ --folder "Governance - VK List Models Restriction" \ --env-var "base_url=http://localhost:8080" ``` Expected outcome: all tests in the folder pass — allowed providers appear in both endpoints, disallowed providers do not. ## Screenshots/Recordings N/A ## Breaking changes - [ ] Yes - [x] No ## Related issues Closes maximhq#2887 Related: maximhq#3187, maximhq#3094 ## Security considerations This test validates that VK-scoped provider restrictions are correctly enforced on model listing endpoints, preventing information leakage about providers a VK is not authorized to use. ## Checklist - [x] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [x] I verified the CI pipeline passes locally if applicable
## Summary Fixes a streaming buffering issue (Closes maximhq#4542) where Bedrock streaming responses arrived in a single burst at the end of generation rather than incrementally. Go's `net/http` transport automatically negotiates gzip encoding, which causes the eventstream to be buffered until the stream completes, collapsing time-to-first-byte (TTFB) to the total generation time. ## Changes - Added `Accept-Encoding: identity` header to all Bedrock streaming requests, preventing Go's `net/http` transport from auto-negotiating gzip compression on the eventstream connection - Removed a duplicate `Accept` header set that was only applied to the IAM auth path - Added `TestChatCompletionStream_StreamsIncrementally_NotBuffered`: a deterministic integration test using a channel-gated fake Bedrock server that verifies the first chunk arrives while the upstream is still held open (pre-fix this times out; post-fix it succeeds) - Added `TestMakeStreamingRequest_SendsIdentityAcceptEncoding`: a guard test that asserts the outbound request carries `Accept-Encoding: identity` rather than Go's default `gzip` ## Type of change - [x] Bug fix - [ ] Feature - [ ] Refactor - [ ] Documentation - [ ] Chore/CI ## Affected areas - [ ] Core (Go) - [x] Transports (HTTP) - [x] Providers/Integrations - [ ] Plugins - [ ] UI (React) - [ ] Docs ## How to test ```sh go test ./core/providers/bedrock/... -v -run TestChatCompletionStream_StreamsIncrementally_NotBuffered go test ./core/providers/bedrock/... -v -run TestMakeStreamingRequest_SendsIdentityAcceptEncoding go test ./... ``` The buffering test is deterministic and channel-gated — no wall-clock sleeps. Pre-fix, `TestChatCompletionStream_StreamsIncrementally_NotBuffered` fails with a 2-second timeout; post-fix it passes immediately upon receiving the first flushed chunk. ## Screenshots/Recordings N/A ## Breaking changes - [x] No ## Related issues Closes maximhq#4542 ## Security considerations No security implications. The change only affects HTTP content-encoding negotiation for the Bedrock streaming endpoint. ## Checklist - [ ] I read `docs/contributing/README.md` and followed the guidelines - [x] I added/updated tests where appropriate - [ ] I updated documentation where needed - [x] I verified builds succeed (Go and UI) - [ ] I verified the CI pipeline passes locally if applicable
…eb_search) in filterUnsupportedTools (maximhq#4532) * fix(openai/responses): preserve OpenRouter server tools in filterUnsupportedTools filterUnsupportedTools stripped tools whose type was not in the OpenAI-native whitelist, including OpenRouter server tools (the "openrouter:" namespace: web_search, web_fetch, datetime, image_generation, apply_patch, subagent). For provider=openrouter on /v1/responses this dropped the tool, so the upstream call ran with tools:[] and no server tool executed (tool_choice:"required" then caused a 400 from the upstream). Allow any "openrouter:"-prefixed tool type through the filter when provider == OpenRouter, mirroring the existing XAI x_search handling. This covers all current and future OpenRouter server tools without per-tool additions. Fixes maximhq#4530 * removes scripts * test case fixes --------- Co-authored-by: akshaydeo <akshay@akshaydeo.com>
…#4563) * fix: cache compiled regexps in CORS wildcard origin matching matchesWildcardPattern was calling regexp.Compile on every HTTP request for each wildcard pattern in AllowedOrigins. Add a sync.Map cache keyed by the raw pattern string so each regexp is compiled once and reused. Benchmarks show ~36x speedup sequential, ~350x under concurrency, with zero allocations on the hot path. Also adds unit tests for matchesWildcardPattern and IsOriginAllowed. Signed-off-by: Matías Insaurralde <matias@insaurral.de> * fix: use LoadOrStore return value in wildcard regexp cache Use the actual stored value from LoadOrStore rather than the locally compiled regexp, making the concurrent-safety intent explicit without requiring readers to reason about functional equivalence of duplicate compiles. Signed-off-by: Matías Insaurralde <matias@insaurral.de> * fix: rename misleading test case for scheme-less wildcard pattern Rename "scheme-less match" (want: false) to "scheme-less no match with scheme prefix" so the test name reflects the expected outcome. Signed-off-by: Matías Insaurralde <matias@insaurral.de> --------- Signed-off-by: Matías Insaurralde <matias@insaurral.de> Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
…iscovery GET /v1/models triggers live model discovery (list-models) against every configured provider. For built-in providers like Azure with many regional keys this fans out per key, and the connections can pile up until /v1/models hangs. Built-in providers cannot opt out via custom_provider_config.allowed_requests (they are not in SupportedBaseProviders), so there was previously no way to disable discovery for them. This adds a per-provider disable_model_discovery flag. When set, FetchAndStoreLiveForKey early-returns, so /v1/models serves only the statically configured models for that provider and the per-key list-models fan-out is skipped. It composes with the existing allowed_requests skip for custom providers. Affected packages: - core/schemas/provider.go - ProviderConfig.DisableModelDiscovery field - framework/configstore - persist the field: configstore ProviderConfig, the providers table column, the config hash, the load/save conversions, and an add-column migration (defaults to false, which contributes nothing to the config hash, so existing rows need no backfill) - transports/bifrost-http/server/server.go - skip discovery when the flag is set - transports/config.schema.json - document the field on every provider block - transports/schema_test - schema coverage test - core/changelog.md, framework/changelog.md, transports/changelog.md Closes maximhq#4581
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a per-provider ChangesPer-provider disable_model_discovery option
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Hey not the right solution again - we will take over it |
Confidence Score: 4/5Safe to merge. The flag is additive with a false default, all read/write paths and the config hash are updated consistently, and the early-return guard covers every call site that would otherwise open network connections. The implementation is complete and correct end-to-end. The two non-blocking observations are: RefreshLiveModelsForProvider still creates per-key goroutines before the guard fires (they return immediately, no network I/O), and there is no idempotency migration test comparable to the one added for store_raw_request_response. Neither affects correctness or introduces regressions. transports/bifrost-http/server/server.go (goroutine fan-out before guard) and framework/configstore/migrations.go (missing migration test) are worth a second look, though neither blocks merging. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GET /v1/models or key event] --> B[RefreshLiveModelsForProvider]
B --> C{No keys?}
C -- keyless --> D[FetchAndStoreLiveForKey]
C -- has keys --> E[Fan-out: goroutine per key]
E --> D
D --> F{s.Config not nil?}
F -- no --> G[Proceed to ListModels]
F -- yes --> H[GetProviderConfigRaw]
H --> I{DisableModelDiscovery?}
I -- true --> J[Return early - no HTTP call]
I -- false --> K{CustomProviderConfig blocks ListModels?}
K -- true --> J
K -- false --> G
G --> L[ListModelsRequest x2 filtered + unfiltered]
L --> M[UpsertLiveFromResponse model catalog]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[GET /v1/models or key event] --> B[RefreshLiveModelsForProvider]
B --> C{No keys?}
C -- keyless --> D[FetchAndStoreLiveForKey]
C -- has keys --> E[Fan-out: goroutine per key]
E --> D
D --> F{s.Config not nil?}
F -- no --> G[Proceed to ListModels]
F -- yes --> H[GetProviderConfigRaw]
H --> I{DisableModelDiscovery?}
I -- true --> J[Return early - no HTTP call]
I -- false --> K{CustomProviderConfig blocks ListModels?}
K -- true --> J
K -- false --> G
G --> L[ListModelsRequest x2 filtered + unfiltered]
L --> M[UpsertLiveFromResponse model catalog]
|
| func migrationAddDisableModelDiscoveryColumn(ctx context.Context, db *gorm.DB, logger schemas.Logger) error { | ||
| migrationName := "add_disable_model_discovery_column" | ||
| logger.Info("[configstore] starting migration %s", migrationName) | ||
| defer logger.Info("[configstore] finished migration %s", migrationName) | ||
| m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{ | ||
| ID: migrationName, | ||
| Migrate: func(tx *gorm.DB) error { | ||
| tx = tx.WithContext(ctx) | ||
| migrator := tx.Migrator() | ||
| // DisableModelDiscovery defaults to false, which contributes nothing to | ||
| // GenerateConfigHash, so existing rows keep a valid config_hash and need no | ||
| // backfill — only the column is added. | ||
| if !migrator.HasColumn(&tables.TableProvider{}, "disable_model_discovery") { | ||
| logger.Info("[configstore] %s: adding column disable_model_discovery to TableProvider", migrationName) | ||
| if err := migrator.AddColumn(&tables.TableProvider{}, "disable_model_discovery"); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| }, | ||
| Rollback: func(tx *gorm.DB) error { | ||
| tx = tx.WithContext(ctx) | ||
| migrator := tx.Migrator() | ||
| logger.Info("[configstore] %s: dropping column disable_model_discovery from TableProvider", migrationName) | ||
| if err := migrator.DropColumn(&tables.TableProvider{}, "disable_model_discovery"); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| }, | ||
| }}) | ||
| if err := m.Migrate(); err != nil { | ||
| return fmt.Errorf("error while running add disable model discovery column migration: %s", err.Error()) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
No migration test for
migrationAddDisableModelDiscoveryColumn
The structurally identical predecessor, migrationAddStoreRawRequestResponseColumn, has TestMigrationAddStoreRawRequestResponseColumn and TestMigrationAddStoreRawRequestResponseColumn_Idempotent (in migrations_test.go) which cover both the data-preservation path and the idempotency guard. This migration has neither. Since the migration only adds a column (no backfill logic), a minimal idempotency test — running migrationAddDisableModelDiscoveryColumn twice on the same in-memory SQLite DB and asserting no error — would protect against regression if the HasColumn guard is accidentally removed in a future edit.
akshaydeo
left a comment
There was a problem hiding this comment.
This is not the right way - we just need to use cached table and trigger a background sync to make sure they are in the right state.
This does not require these many changes
The merge-base changed after approval.
Summary
GET /v1/modelstriggers live model discovery (list-models) against every configured provider. For built-in providers like Azure with many regional keys this fans out per key, and the connections can pile up until/v1/modelshangs (and can starve inference). Built-in providers can't opt out viacustom_provider_config.allowed_requestsbecause they aren't inSupportedBaseProviders, so there was previously no way to disable discovery for them. Full investigation in #4581.This adds a per-provider
disable_model_discoveryflag. When set,FetchAndStoreLiveForKeyearly-returns, so/v1/modelsserves only the statically configured models for that provider and the per-key list-models fan-out is skipped. It composes with the existingallowed_requestsskip used by custom providers.This implements option 2 from #4581 (a per-provider opt-out for built-in providers), not the global env var. Default behaviour is unchanged — discovery stays on unless explicitly disabled.
Changes
core/schemas/provider.go— addProviderConfig.DisableModelDiscovery(disable_model_discovery, defaultfalse).transports/bifrost-http/server/server.go—FetchAndStoreLiveForKeyskips the live fetch when the flag is set, alongside the existing custom-providerallowed_requestsskip.framework/configstore— persist the field end-to-end: the configstoreProviderConfig, theproviderstable column,GenerateConfigHash, the load/save conversions, and anadd_disable_model_discovery_columnmigration. Because the field defaults tofalse(which writes nothing to the config hash), existing rows keep a valid hash and the migration only adds the column — no backfill needed.transports/config.schema.json— document the field on every provider block.transports/schema_test/config_schema_test.go— assert the schema documents it as a boolean on every provider block.core/,framework/,transports/.Type of change
Affected areas
How to test
Runtime: set
disable_model_discovery: trueon a provider inconfig.json.GET /v1/modelsthen returns only that provider's statically configured models, and the gateway no longer opens list-models connections for it on startup or per request.Breaking changes
Defaults to
false; existing configs and storedconfig_hashvalues are unaffected.Related issues
Closes #4581
Security considerations
None. The flag only suppresses outbound list-models calls; it doesn't touch auth, secrets, or request handling (and can reduce outbound connection pressure).
Checklist
docs/contributing/raising-a-pr.mdx)