Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cli/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- feat: adds an interactive tab command popup invoked by the `Ctrl+B` prefix — lists every open tab plus a trailing "New tab" action row, supports arrow keys / `h`/`j`/`k`/`l` navigation, `Enter` to switch or open a tab, number keys (`1`–`9`) for direct jumps, and `Esc`/`Ctrl+B` to resume
- feat: adds `Ctrl+G` as a tmux-safe alternate prefix key, giving users a one-press tab selector inside tmux sessions that already consume `Ctrl+B`
- feat: adds a Home-screen Ctrl+C quit confirmation — when no tabs are open, the first `Ctrl+C` shows a centered "Quit Bifrost?" prompt and the second press exits, preventing accidental termination
- feat: adds an interactive summary screen with arrow-key navigation across actionable rows (launch, base URL, virtual key, worktree, harness, model, dashboard, docs, issues, repo, quit) plus inline editing of base URL and virtual key without leaving the summary
- feat: masks the virtual key input with `*` characters in the chooser to avoid leaking credentials on shared screens
- feat: adds a Home-styled mandatory update prompt — when a new Bifrost CLI version is available, a centered confirmation popup is shown before the chooser opens, and declining keeps the user on Home instead of blocking startup
- feat: keeps the CLI running after a harness session ends — the tab manager now loops back to the chooser instead of exiting, so closing the last tab returns to Home rather than terminating Bifrost
- improvement: model chooser `Esc` now clears the active filter, manual-entry selection, and any error message on first press before exiting the phase, matching common picker behavior
- improvement: model chooser treats manual model names (typed into the filter) as a distinct selectable row, with arrow-key wrap-around between the filtered list and the manual entry
- improvement: hides the Bifrost logo when re-entering the harness/model/worktree phases from the summary, giving editing flows more vertical space
- fix: tab command-mode key handling now correctly distinguishes `Enter` (activate the selected row) from `Esc`/prefix (resume the active tab), and recognises both `Ctrl+B` and `Ctrl+G` as the dismiss key
- fix: arrow-key escape sequences are now mapped to the existing `h`/`j`/`k`/`l` navigation in the command overlay, so users can navigate the tab popup with cursor keys
7 changes: 2 additions & 5 deletions core/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
- feat: file scheme (file://) support for pricing URLs
- fix: corrected Bedrock outputAssessments type
- fix: added Model field to TextCompletionChunkResponse (thanks [@kuishou68](https://github.com/kuishou68)!)
- fix: accept orphaned tool results in OpenAI to Anthropic conversion flow
- fix(mcp): allow inline stdio env assignments (thanks [@Shushmitaaaa](https://github.com/Shushmitaaaa)!)
- [fix]: map upstream connection failures to 502 instead of 400 (#3929) [@chris-colinsky](https://github.com/chris-colinsky)
- chore: bumped transitive golang.org/x dependencies (crypto, net, sys, text) for Docker Scout CVE remediation (#3900)
25 changes: 25 additions & 0 deletions core/providers/utils/makerequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,14 @@ func TestMakeRequestWithContext_ClientError(t *testing.T) {
if bifrostErr == nil {
t.Fatal("expected error for nonexistent host")
}
// Upstream connectivity failures (DNS/OpError) should surface as 502 Bad Gateway,
// not the default 400 — see NewBifrostUpstreamConnectionError.
if bifrostErr.StatusCode == nil || *bifrostErr.StatusCode != 502 {
t.Fatalf("expected StatusCode 502, got %v", bifrostErr.StatusCode)
}
if bifrostErr.Error.Type == nil || *bifrostErr.Error.Type != schemas.ProviderConnectionFailed {
t.Fatalf("expected ProviderConnectionFailed type, got %v", bifrostErr.Error.Type)
}
// wait should be noop since the goroutine completed (with error)
start := time.Now()
wait()
Expand All @@ -345,6 +353,23 @@ func TestMakeRequestWithContext_ClientError(t *testing.T) {
}
}

func TestNewBifrostUpstreamConnectionError(t *testing.T) {
err := NewBifrostUpstreamConnectionError("upstream dropped connection", context.DeadlineExceeded)

if err.IsBifrostError {
t.Fatal("expected IsBifrostError to be false (upstream is at fault)")
}
if err.StatusCode == nil || *err.StatusCode != 502 {
t.Fatalf("expected StatusCode 502, got %v", err.StatusCode)
}
if err.Error.Type == nil || *err.Error.Type != schemas.ProviderConnectionFailed {
t.Fatalf("expected ProviderConnectionFailed type, got %v", err.Error.Type)
}
if err.Error.Message != "upstream dropped connection" {
t.Fatalf("expected 'upstream dropped connection', got %s", err.Error.Message)
}
}

func TestMakeRequestWithContext_DeferOrderingPattern(t *testing.T) {
// Verify the exact defer pattern used by callers works correctly under -race.
// This mirrors the real provider code pattern.
Expand Down
38 changes: 24 additions & 14 deletions core/providers/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,22 +193,10 @@ func makeRequestWithDoFunc(ctx context.Context, do func() error) (time.Duration,
var opErr *net.OpError
var dnsErr *net.DNSError
if errors.As(err, &opErr) || errors.As(err, &dnsErr) {
return latency, &schemas.BifrostError{
IsBifrostError: false,
Error: &schemas.ErrorField{
Message: schemas.ErrProviderNetworkError,
Error: err,
},
}, noop
return latency, NewBifrostUpstreamConnectionError(schemas.ErrProviderNetworkError, err), noop
}
// The HTTP request itself failed (e.g., connection error, fasthttp timeout).
return latency, &schemas.BifrostError{
IsBifrostError: false,
Error: &schemas.ErrorField{
Message: schemas.ErrProviderDoRequest,
Error: err,
},
}, noop
return latency, NewBifrostUpstreamConnectionError(schemas.ErrProviderDoRequest, err), noop
}
// HTTP request was successful from fasthttp's perspective (err is nil).
// The caller should check resp.StatusCode() for HTTP-level errors (4xx, 5xx).
Expand Down Expand Up @@ -1842,6 +1830,28 @@ func NewBifrostTimeoutError(message string, err error) *schemas.BifrostError {
}
}

// NewBifrostUpstreamConnectionError creates a standardized error for upstream
// connectivity failures where Bifrost successfully dispatched to the provider
// but the provider failed to return a response body (DNS lookup failure,
// connection refused, connection reset before the first response byte, etc.).
// Sets StatusCode to 502 (Bad Gateway) and Error.Type to ProviderConnectionFailed,
// distinguishing these retriable upstream failures from genuine HTTP 400
// client-side bad-request errors. Mirrors NewBifrostTimeoutError; IsBifrostError
// is false because the upstream provider is the cause.
func NewBifrostUpstreamConnectionError(message string, err error) *schemas.BifrostError {
statusCode := 502
errorType := schemas.ProviderConnectionFailed
return &schemas.BifrostError{
IsBifrostError: false,
StatusCode: &statusCode,
Error: &schemas.ErrorField{
Message: message,
Type: &errorType,
Error: err,
},
}
}

// NewProviderAPIError creates a standardized error for provider API errors.
// This helper reduces code duplication across providers that have provider API errors.
func NewProviderAPIError(message string, err error, statusCode int, errorType *string, eventID *string) *schemas.BifrostError {
Expand Down
5 changes: 3 additions & 2 deletions core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -1468,8 +1468,9 @@ type BifrostCacheDebug struct {
}

const (
RequestCancelled = "request_cancelled"
RequestTimedOut = "request_timed_out"
RequestCancelled = "request_cancelled"
RequestTimedOut = "request_timed_out"
ProviderConnectionFailed = "provider_connection_failed"
)

// BifrostStreamChunk represents a stream of responses from the Bifrost system.
Expand Down