bifrost

GitHub

Fastest enterprise AI gateway (50x faster than LiteLLM) with adaptive load balancer, cluster mode, guardrails, 1000+ models support & <100 ยตs overhead at 5k RPS.

RAW Rules

AGENTS.md

# AGENTS.md โ€” Bifrost AI Gateway

> Context for AI agents (Claude Code, Copilot, Cursor, etc.) working on this codebase. Read this fully before making changes.

## What is Bifrost?

Bifrost is a high-performance AI gateway that unifies 20+ LLM providers behind a single OpenAI-compatible API with ~11ยตs overhead at 5,000 RPS. It also serves as an MCP (Model Context Protocol) gateway, turning static chat models into tool-calling agents.

GitHub: `maximhq/bifrost`

---

## Repository Layout

```
bifrost/
โ”œโ”€โ”€ core/                           # Go core library โ€” the engine
โ”‚   โ”œโ”€โ”€ bifrost.go                  # Main struct, request queuing, provider lifecycle (~3.4K lines)
โ”‚   โ”œโ”€โ”€ inference.go                # Inference routing, fallbacks, streaming dispatch (~1.9K lines)
โ”‚   โ”œโ”€โ”€ mcp.go                     # MCP integration entry point
โ”‚   โ”œโ”€โ”€ schemas/                   # ALL shared Go types โ€” 41 files
โ”‚   โ”‚   โ”œโ”€โ”€ bifrost.go             # BifrostConfig, ModelProvider enum, RequestType enum, context keys
โ”‚   โ”‚   โ”œโ”€โ”€ provider.go            # Provider interface (30+ methods), NetworkConfig, ProviderConfig
โ”‚   โ”‚   โ”œโ”€โ”€ plugin.go              # LLMPlugin, MCPPlugin, HTTPTransportPlugin, ObservabilityPlugin
โ”‚   โ”‚   โ”œโ”€โ”€ context.go             # BifrostContext (custom context.Context with mutable values)
โ”‚   โ”‚   โ”œโ”€โ”€ chatcompletions.go     # Chat completion request/response types
โ”‚   โ”‚   โ”œโ”€โ”€ responses.go           # OpenAI Responses API types
โ”‚   โ”‚   โ”œโ”€โ”€ embedding.go           # Embedding types
โ”‚   โ”‚   โ”œโ”€โ”€ images.go              # Image generation types
โ”‚   โ”‚   โ”œโ”€โ”€ batch.go               # Batch operation types
โ”‚   โ”‚   โ”œโ”€โ”€ files.go               # File management types
โ”‚   โ”‚   โ”œโ”€โ”€ mcp.go                 # MCP types
โ”‚   โ”‚   โ”œโ”€โ”€ trace.go               # Tracer interface
โ”‚   โ”‚   โ””โ”€โ”€ logger.go              # Logger interface
โ”‚   โ”œโ”€โ”€ providers/                 # 20+ provider implementations
โ”‚   โ”‚   โ”œโ”€โ”€ openai/                # Reference implementation (largest, most complete)
โ”‚   โ”‚   โ”œโ”€โ”€ anthropic/             # Non-OpenAI-compatible example
โ”‚   โ”‚   โ”œโ”€โ”€ bedrock/               # AWS event-stream protocol
โ”‚   โ”‚   โ”œโ”€โ”€ gemini/                # Google-specific API shape
โ”‚   โ”‚   โ”œโ”€โ”€ groq/                  # OpenAI-compatible (minimal, delegates to openai/)
โ”‚   โ”‚   โ””โ”€โ”€ utils/                 # Shared: HTTP client, SSE parsing, error handling, scanner pool
โ”‚   โ”œโ”€โ”€ pool/                      # Generic Pool[T] โ€” dual-mode (prod: sync.Pool, debug: full tracking)
โ”‚   โ”‚   โ”œโ”€โ”€ pool_prod.go           # Zero-overhead sync.Pool wrapper (default build)
โ”‚   โ”‚   โ””โ”€โ”€ pool_debug.go          # Double-release/use-after-release/leak detection (-tags pooldebug)
โ”‚   โ”œโ”€โ”€ mcp/                       # MCP protocol implementation
โ”‚   โ”‚   โ”œโ”€โ”€ agent.go               # Agent orchestration loop (multi-turn tool calling)
โ”‚   โ”‚   โ”œโ”€โ”€ clientmanager.go       # MCP client lifecycle management
โ”‚   โ”‚   โ”œโ”€โ”€ toolmanager.go         # Tool registration, discovery, filtering
โ”‚   โ”‚   โ”œโ”€โ”€ healthmonitor.go       # Client health monitoring
โ”‚   โ”‚   โ””โ”€โ”€ codemode/starlark/     # Starlark sandbox for code-mode execution
โ”‚   โ””โ”€โ”€ internal/
โ”‚       โ”œโ”€โ”€ llmtests/              # LLM integration test infra (48 files, scenario-based)
โ”‚       โ””โ”€โ”€ mcptests/              # MCP/Agent test infra (40+ files, mock-based)
โ”‚
โ”œโ”€โ”€ framework/                     # Data persistence, streaming, ecosystem utilities
โ”‚   โ”œโ”€โ”€ configstore/               # Config storage backends (file, postgres)
โ”‚   โ”œโ”€โ”€ logstore/                  # Log storage backends (file, postgres)
โ”‚   โ”œโ”€โ”€ vectorstore/               # Vector storage (Weaviate, Qdrant, Redis, Pinecone)
โ”‚   โ”œโ”€โ”€ streaming/                 # Streaming accumulator, delta copying, response marshaling
โ”‚   โ”‚   โ”œโ”€โ”€ accumulator.go         # Chunk accumulation into full response (~24KB)
โ”‚   โ”‚   โ”œโ”€โ”€ chat.go                # Chat stream handling (~17KB)
โ”‚   โ”‚   โ””โ”€โ”€ responses.go           # Response stream marshaling (~35KB)
โ”‚   โ”œโ”€โ”€ modelcatalog/              # Model metadata registry
โ”‚   โ”œโ”€โ”€ tracing/                   # Distributed tracing helpers
โ”‚   โ””โ”€โ”€ encrypt/                   # Encryption utilities
โ”‚
โ”œโ”€โ”€ transports/
โ”‚   โ”œโ”€โ”€ config.schema.json         # JSON Schema โ€” THE source of truth for config.json (~2700 lines)
โ”‚   โ””โ”€โ”€ bifrost-http/              # HTTP gateway transport
โ”‚       โ”œโ”€โ”€ server/                # Server lifecycle, route registration
โ”‚       โ”œโ”€โ”€ handlers/              # 27 HTTP endpoint handlers
โ”‚       โ”‚   โ”œโ”€โ”€ inference.go       # Chat/text completions, responses API (~109KB)
โ”‚       โ”‚   โ”œโ”€โ”€ mcpinference.go    # MCP tool execution
โ”‚       โ”‚   โ”œโ”€โ”€ governance.go      # Virtual keys, teams, customers, budgets (~100KB)
โ”‚       โ”‚   โ”œโ”€โ”€ providers.go       # Provider CRUD, key management
โ”‚       โ”‚   โ”œโ”€โ”€ mcp.go             # MCP client registry management
โ”‚       โ”‚   โ”œโ”€โ”€ logging.go         # Log queries, stats, histograms
โ”‚       โ”‚   โ”œโ”€โ”€ config.go          # System configuration
โ”‚       โ”‚   โ”œโ”€โ”€ plugins.go         # Plugin CRUD
โ”‚       โ”‚   โ”œโ”€โ”€ cache.go           # Cache management
โ”‚       โ”‚   โ”œโ”€โ”€ session.go         # Auth/session management
โ”‚       โ”‚   โ”œโ”€โ”€ health.go          # Health checks
โ”‚       โ”‚   โ”œโ”€โ”€ mcpserver.go       # MCP server (SSE/streamable HTTP)
โ”‚       โ”‚   โ”œโ”€โ”€ websocket.go       # WebSocket handler
โ”‚       โ”‚   โ”œโ”€โ”€ devpprof.go        # Pool debug profiler endpoint (~23KB)
โ”‚       โ”‚   โ””โ”€โ”€ middlewares.go     # Middleware definitions
โ”‚       โ”œโ”€โ”€ lib/                   # ChainMiddlewares, config, context conversion
โ”‚       โ””โ”€โ”€ integrations/          # SDK compatibility layers
โ”‚           โ”œโ”€โ”€ openai.go          # OpenAI SDK drop-in compatibility
โ”‚           โ”œโ”€โ”€ anthropic.go       # Anthropic SDK compatibility
โ”‚           โ”œโ”€โ”€ bedrock.go         # AWS Bedrock SDK compatibility
โ”‚           โ”œโ”€โ”€ genai.go           # Google GenAI SDK compatibility
โ”‚           โ”œโ”€โ”€ langchain.go       # LangChain compatibility
โ”‚           โ”œโ”€โ”€ litellm.go         # LiteLLM compatibility
โ”‚           โ””โ”€โ”€ pydanticai.go      # PydanticAI compatibility
โ”‚
โ”œโ”€โ”€ plugins/                       # Go plugins โ€” each has own go.mod
โ”‚   โ”œโ”€โ”€ governance/                # Budget, rate limiting, virtual keys, routing, RBAC
โ”‚   โ”œโ”€โ”€ telemetry/                 # Prometheus metrics, push gateway
โ”‚   โ”œโ”€โ”€ logging/                   # Request/response audit logging
โ”‚   โ”œโ”€โ”€ semanticcache/             # Semantic response caching via vector store
โ”‚   โ”œโ”€โ”€ otel/                      # OpenTelemetry tracing
โ”‚   โ”œโ”€โ”€ mocker/                    # Mock responses for testing
โ”‚   โ”œโ”€โ”€ jsonparser/                # JSON extraction utilities
โ”‚   โ”œโ”€โ”€ maxim/                     # Maxim observability
โ”‚   โ””โ”€โ”€ compat/                    # LiteLLM SDK compatibility (HTTP transport)
โ”‚
โ”œโ”€โ”€ ui/                            # React + vite web interface
โ”‚   โ”œโ”€โ”€ app/workspace/             # Feature pages (20+ workspace sections)
โ”‚   โ”œโ”€โ”€ components/                # Shared React components
โ”‚   โ””โ”€โ”€ lib/                       # Constants, utilities, types
โ”‚
โ”œโ”€โ”€ tests/e2e/                     # Playwright E2E tests
โ”‚   โ”œโ”€โ”€ core/                      # Fixtures, page objects, helpers, API actions
โ”‚   โ””โ”€โ”€ features/                  # Per-feature test suites
โ”‚
โ”œโ”€โ”€ docs/                          # Mintlify MDX documentation
โ”‚   โ”œโ”€โ”€ docs.json                  # Navigation config
โ”‚   โ”œโ”€โ”€ media/                     # Screenshots (ui-*.png naming convention)
โ”‚   โ””โ”€โ”€ (architecture|features|providers|mcp|plugins|enterprise|...)
โ”‚
โ”œโ”€โ”€ .claude/skills/                # Claude Code skill definitions (4 skills)
โ”œโ”€โ”€ go.work                        # Go workspace โ€” requires Go 1.26.1
โ”œโ”€โ”€ Makefile                       # Build, test, dev commands (1300+ lines)
โ””โ”€โ”€ terraform/                     # Infrastructure as Code
```

---

## Go Workspace

Bifrost is a **multi-module Go workspace**. Each module has its own `go.mod`:

```
go.work
โ”œโ”€โ”€ core/go.mod              # github.com/maximhq/bifrost/core
โ”œโ”€โ”€ framework/go.mod         # github.com/maximhq/bifrost/framework
โ”œโ”€โ”€ transports/go.mod        # github.com/maximhq/bifrost/transports
โ””โ”€โ”€ plugins/*/go.mod         # 9 plugin modules (governance, telemetry, logging, etc.)
```

**Rules:**
- Run `go mod tidy` in the **specific module directory**, not the root
- Cross-module imports resolve via workspace locally, but need explicit `require` in `go.mod` for releases
- The workspace requires **Go 1.26.1** (`go.work` directive)

---

## Build, Test & Dev Commands

```bash
# Development
make dev                                 # Full local dev (UI + API with hot reload via air)
make build                               # Build bifrost-http binary

# Core tests (provider integration tests โ€” hit live APIs)
make test-core                           # All providers
make test-core PROVIDER=openai           # Specific provider
make test-core PROVIDER=openai TESTCASE=TestSimpleChat  # Specific test
make test-core PATTERN=TestStreaming      # Tests matching pattern
make test-core DEBUG=1                   # With Delve debugger on :2345

# MCP/Agent tests (mock-based, no live APIs)
make test-mcp                            # All MCP tests
make test-mcp TESTCASE=TestAgentLoop     # Specific test
make test-mcp TYPE=agent                 # By category (agent|tool|connection|codemode)

# Framework tests (require local backing services โ€” bring them up FIRST)
docker compose -f tests/docker-compose.yml up -d   # postgres, weaviate, qdrant, pinecone, and the 4 redis variants
make test-framework                                # All framework packages

# Plugin tests
make test-plugins                        # All plugins
make test-governance                     # Governance plugin specifically

# Integration tests (SDK compatibility)
make test-integrations-py                # Python SDK tests
make test-integrations-ts                # TypeScript SDK tests

# E2E tests (Playwright, requires running dev server)
make run-e2e                             # All E2E tests
make run-e2e FLOW=providers              # Specific feature

# Code quality
make lint                                # Linting
make fmt                                 # Format code
```

---

## Architecture

### Request Flow

```
Client HTTP Request
  โ†’ FastHTTP Transport (parsing, validation ~2ยตs)
    โ†’ SDK Integration Layer (OpenAI/Anthropic/Bedrock format โ†’ Bifrost format)
      โ†’ Middleware Chain (lib.ChainMiddlewares, applied per-route)
        โ†’ HTTPTransportPreHook (HTTP-level plugins, can short-circuit)
          โ†’ PreLLMHook Pipeline (auth, rate-limit, cache check โ€” registration order)
            โ†’ MCP Tool Discovery & Injection (if tool_choice present)
              โ†’ Provider Queue (channel-based, per-provider isolation)
                โ†’ Worker picks up request
                  โ†’ Key Selection (~10ns weighted random)
                    โ†’ Provider API Call (fasthttp client, connection pooling)
                      โ†’ Response / SSE Stream
                โ†’ PostLLMHook Pipeline (reverse order of PreLLMHooks)
              โ†’ Tool Execution Loop (if tool_calls in response, MCP agent loop)
            โ†’ HTTPTransportPostHook (reverse order)
          โ†’ Response Serialization
        โ†’ HTTP Response to Client
```

### Design Principles

- **Provider isolation**: Each provider has its own worker pool and queue. One provider going down doesn't cascade to others.
- **Channel-based async**: Request routing uses Go channels (`chan *ChannelMessage`), not mutexes. The `ProviderQueue` struct manages channel lifecycle with atomic flags.
- **Object pooling everywhere**: `sync.Pool` wrappers reduce GC pressure. Pools exist for: channel messages, response channels, error channels, stream channels, plugin pipelines, MCP requests, HTTP request/response objects, scanner buffers.
- **Plugin pipeline symmetry**: Pre-hooks execute in registration order, post-hooks in **reverse** order (LIFO). For every pre-hook executed, the corresponding post-hook is guaranteed to run.
- **Streaming**: SSE chunks flow through `chan chan *schemas.BifrostStreamChunk`. Accumulated into full response for post-hooks via `framework/streaming/accumulator.go`.

### BifrostContext โ€” Custom Context

`BifrostContext` (`core/schemas/context.go`) is a custom `context.Context` with **thread-safe mutable values**. Unlike standard Go contexts, values can be set after creation:

```go
ctx := schemas.NewBifrostContext(parent, deadline)
ctx.SetValue(key, value)     // Thread-safe, uses RWMutex
ctx.WithValue(key, value)    // Chainable variant
```

**Reserved context keys** (set by Bifrost internals โ€” DO NOT set manually):
- `BifrostContextKeySelectedKeyID/Name` โ€” Set by governance plugin
- `BifrostContextKeyGovernance*` โ€” Set by governance plugin
- `BifrostContextKeyNumberOfRetries`, `BifrostContextKeyFallbackIndex` โ€” Set by retry/fallback logic
- `BifrostContextKeyStreamEndIndicator` โ€” Set by streaming infrastructure
- `BifrostContextKeyTrace*`, `BifrostContextKeySpan*` โ€” Set by tracing middleware

**User-settable keys** (plugins and handlers can set these):
- `BifrostContextKeyVirtualKey` (`x-bf-vk`) โ€” Virtual key for governance
- `BifrostContextKeyAPIKeyName` (`x-bf-api-key`) โ€” Explicit key selection by name
- `BifrostContextKeyAPIKeyID` (`x-bf-api-key-id`) โ€” Explicit key selection by ID (takes priority over name)
- `BifrostContextKeyRequestID` โ€” Request ID
- `BifrostContextKeyExtraHeaders` โ€” Extra headers to forward to provider
- `BifrostContextKeyURLPath` โ€” Custom URL path for provider
- `BifrostContextKeySkipKeySelection` โ€” Skip key selection (pass empty key)
- `BifrostContextKeyUseRawRequestBody` โ€” Send raw body directly to provider

**Gotcha**: `BlockRestrictedWrites()` silently drops writes to reserved keys. This prevents plugins from accidentally overwriting internal state.

**Hard rule โ€” never store stream-sized data in `BifrostContext`.** Context holds small handles only: IDs, durations, booleans, interface pointers. Any per-request state that scales with stream content (chunk buffers, accumulated payloads, replay queues, large per-request slices/maps) must live in a top-level manager keyed by `RequestID`, not in `ctx`. Reference implementations:

- `framework/streaming.Accumulator` โ€” owns a `sync.Map` of per-stream `StreamAccumulator` entries keyed by `RequestID`. Only `BifrostContextKeyAccumulatorID` (the ID string) is stored on the context; the chunk buffers live in the manager. The pause/resume gate (`gate.go`) extends the same per-stream entry with a state machine โ€” again, **no buffer in ctx**.
- The `Tracer` interface (in ctx as a small pointer) is the access path for plugins/providers to reach managers without putting bulky data on the context itself.

When in doubt: if your new ctx key would hold a slice/map that grows with request content, route the storage through a manager and keep only the ID in ctx.

---

## Core Patterns

### Provider Implementation

There are **two categories** of providers:

**Category 1: Non-OpenAI-compatible** (Anthropic, Bedrock, Gemini, Cohere, HuggingFace, Replicate, ElevenLabs):
```
core/providers/<name>/
โ”œโ”€โ”€ <name>.go              # Controller: constructor, interface methods, HTTP orchestration
โ”œโ”€โ”€ <name>_test.go         # Tests
โ”œโ”€โ”€ types.go               # ALL provider-specific structs (PascalCase prefixed with provider name)
โ”œโ”€โ”€ utils.go               # Constants, base URLs, helpers (camelCase for unexported)
โ”œโ”€โ”€ errors.go              # Error parsing: provider HTTP error โ†’ *schemas.BifrostError
โ”œโ”€โ”€ chat.go                # Chat request/response converters
โ”œโ”€โ”€ embedding.go           # Embedding converters (if supported)
โ”œโ”€โ”€ images.go              # Image generation (if supported)
โ”œโ”€โ”€ speech.go              # TTS/STT (if supported)
โ””โ”€โ”€ responses.go           # Responses API + streaming converters
```

**Category 2: OpenAI-compatible** (Groq, Cerebras, Ollama, Perplexity, OpenRouter, Parasail, Nebius, xAI, SGL):
```
core/providers/<name>/
โ”œโ”€โ”€ <name>.go              # Minimal โ€” constructor + delegates to openai.HandleOpenAI* functions
โ””โ”€โ”€ <name>_test.go         # Tests
```

**Converter function naming convention:**
- `To<ProviderName><Feature>Request()` โ€” Bifrost schema โ†’ Provider API format
- `ToBifrost<Feature>Response()` โ€” Provider API format โ†’ Bifrost schema
- These must be **pure transformation functions** โ€” no HTTP calls, no logging, no side effects

**Provider constructor pattern:**
```go
func NewProvider(config schemas.ProviderConfig) (*Provider, error) {
    // Validate config, set up fasthttp.Client with connection pooling
    client := &fasthttp.Client{
        MaxConnsPerHost:     config.NetworkConfig.MaxConnsPerHost, // configurable, default 5000
        MaxIdleConnDuration: 30 * time.Second,
    }
    // After ConfigureProxy/ConfigureDialer/ConfigureTLS, build a sibling client
    // for streaming. BuildStreamingClient zeros ReadTimeout/WriteTimeout/MaxConnDuration
    // so streams aren't killed by fasthttp's whole-response deadline; per-chunk idle
    // is enforced at the app layer via NewIdleTimeoutReader.
    streamingClient := providerUtils.BuildStreamingClient(client)
    return &Provider{client: client, streamingClient: streamingClient, ...}, nil
}
```

**Streaming vs unary client:** Every provider holds two clients โ€” `client` for unary requests (`ReadTimeout=30s` bounds the whole response) and `streamingClient` for SSE / EventStream / chunked paths (`ReadTimeout=0`; the per-chunk `NewIdleTimeoutReader` is the only governor). Pass `provider.streamingClient` to every `Handle*Streaming` / `Handle*StreamRequest` helper and to direct `Do` calls inside `*Stream` methods. For new providers, apply the same pattern โ€” missing the switch means streams get killed at 30s.

**Note:** Bedrock uses `net/http` (not fasthttp) with HTTP/2 support. Its `http.Transport` is configured with `ForceAttemptHTTP2: true` and `MaxConnsPerHost` from `NetworkConfig` to allow multiple HTTP/2 connections when the server's per-connection stream limit (100 for AWS Bedrock) is reached. Use `providerUtils.BuildStreamingHTTPClient(client)` to derive the streaming variant โ€” it shares the base `Transport` (safe for concurrent reuse) but clears `Client.Timeout`.

### The Provider Interface

`core/schemas/provider.go` defines the `Provider` interface with **30+ methods**. Every provider must implement all of them (returning "not supported" for unsupported operations). The interface covers:

- `ListModels`, `ChatCompletion`, `ChatCompletionStream`
- `Responses`, `ResponsesStream` (OpenAI Responses API)
- `TextCompletion`, `TextCompletionStream`
- `Embedding`, `Speech`, `SpeechStream`, `Transcription`, `TranscriptionStream`
- `ImageGeneration`, `ImageGenerationStream`, `ImageEdit`, `ImageEditStream`, `ImageVariation`
- `CountTokens`
- `Batch*` (Create, List, Retrieve, Cancel, Results)
- `File*` (Upload, List, Retrieve, Delete, Content)
- `Container*` and `ContainerFile*` (Create, List, Retrieve, Delete, Content)

**Streaming methods** receive a `PostHookRunner` callback and return `chan *BifrostStreamChunk`:
```go
ChatCompletionStream(ctx *BifrostContext, postHookRunner PostHookRunner, key Key, request *BifrostChatRequest) (chan *BifrostStreamChunk, *BifrostError)
```

### Error Handling

Each provider has `errors.go` with an `ErrorConverter` function:
```go
type ErrorConverter func(resp *fasthttp.Response, requestType schemas.RequestType, providerName schemas.ModelProvider, model string) *schemas.BifrostError
```

The shared utility `providerUtils.HandleProviderAPIError()` handles common HTTP error parsing. Provider-specific parsers add extra field mapping. Errors always carry metadata:
```go
bifrostErr.ExtraFields.Provider = providerName
bifrostErr.ExtraFields.ModelRequested = model
bifrostErr.ExtraFields.RequestType = requestType
```

### Plugin System

Four plugin interfaces exist:

| Interface | Hook Methods | When Called |
|-----------|-------------|------------|
| `LLMPlugin` | `PreLLMHook`, `PostLLMHook` | Every LLM request (SDK + HTTP) |
| `MCPPlugin` | `PreMCPHook`, `PostMCPHook` | Every MCP tool execution |
| `HTTPTransportPlugin` | `HTTPTransportPreHook`, `HTTPTransportPostHook`, `HTTPTransportStreamChunkHook` | HTTP gateway only (not Go SDK) |
| `ObservabilityPlugin` | `Inject(ctx, trace)` | Async, after response written to wire |

**Key plugin behaviors:**
- Plugin errors are **logged as warnings**, never returned to the caller
- Pre-hooks can **short-circuit** by returning `*LLMPluginShortCircuit` (cache hit, auth failure, rate limit)
- Post-hooks receive both response and error โ€” either can be nil. Plugins can **recover from errors** (set error to nil, provide response) or **invalidate responses** (set response to nil, provide error)
- `BifrostError.AllowFallbacks` controls whether fallback providers are tried: `nil` or `&true` = allow, `&false` = block
- `HTTPTransportStreamChunkHook` is called **per-chunk** during streaming โ€” can modify, skip, or abort the stream

### Pool System

`core/pool/` provides `Pool[T]` with two build modes:

```go
// Production (default): zero-overhead sync.Pool wrapper
// Debug (-tags pooldebug): tracks double-release, use-after-release, leaks with stack traces
p := pool.New[MyType]("descriptive-name", func() *MyType { return &MyType{} })
obj := p.Get()
// ... use obj ...
// MUST reset ALL fields before Put โ€” pool does not auto-reset
p.Put(obj)
```

**Acquire/Release pattern** for types with complex reset logic (used in `schemas/plugin.go`):
```go
req := schemas.AcquireHTTPRequest()    // Get from pool, pre-allocated maps
defer schemas.ReleaseHTTPRequest(req)  // Clears all maps and fields, returns to pool
```

### HTTP Transport Layer

**Handler pattern:** Handlers are structs with injected dependencies:
```go
type CompletionHandler struct {
    client       *bifrost.Bifrost
    handlerStore lib.HandlerStore
    config       *lib.Config
}
```

**Route registration:** Each handler implements `RegisterRoutes(router, middlewares...)` โ€” routes get middleware chains applied per-route via `lib.ChainMiddlewares()`.

**SDK integration layers** (`transports/bifrost-http/integrations/`) provide request/response converters between provider-native SDK formats and Bifrost's internal format. This enables drop-in replacement of OpenAI SDK, Anthropic SDK, AWS Bedrock SDK, Google GenAI SDK, LangChain, and LiteLLM.

---

## Gotchas

### 1. Always Reset Pooled Objects Before Put

Every pooled object must have **all** fields zeroed before `pool.Put()`. Stale data leaks between requests. The debug build catches double-release and use-after-release but **not** missing resets.

```go
// WRONG โ€” stale data from previous request leaks to next user
pool.Put(msg)

// RIGHT
msg.Response = nil
msg.Error = nil
msg.Context = nil
msg.ResponseStream = nil
pool.Put(msg)
```

### 2. Channel Lifecycle โ€” ProviderQueue Pattern

`ProviderQueue` uses atomic flags and `sync.Once` to prevent "send on closed channel" panics:
```go
type ProviderQueue struct {
    queue      chan *ChannelMessage
    done       chan struct{}
    closing    uint32         // atomic: 0=open, 1=closing
    signalOnce sync.Once      // ensure signal fires only once
    closeOnce  sync.Once      // ensure close fires only once
}
```
Always check the atomic closing flag before sending. Never close a channel without this pattern.

### 3. NetworkConfig Duration Serialization

`RetryBackoffInitial` and `RetryBackoffMax` are `time.Duration` (nanoseconds) in Go but **milliseconds** (integers) in JSON. Custom `MarshalJSON`/`UnmarshalJSON` handles conversion. If adding new duration fields to any config struct, follow this pattern exactly.

### 4. ExtraHeaders โ€” Defensive Map Copy

`NetworkConfig.ExtraHeaders` is deep-copied in `CheckAndSetDefaults()` to prevent data races between concurrent requests. Apply the same `maps.Copy()` pattern to any new map fields in config structs.

### 5. Provider Interface Has 30+ Methods

Adding a new operation type requires changes across the entire codebase:
1. Add method to `Provider` interface in `core/schemas/provider.go`
2. Implement in **all** 20+ providers (most return "not supported")
3. Add `RequestType` constant in `core/schemas/bifrost.go`
4. Add to `AllowedRequests` struct and `IsOperationAllowed()` switch
5. Add handler endpoint in `transports/bifrost-http/handlers/`
6. Wire up in `core/bifrost.go` and `core/inference.go`

### 6. OpenAI Provider Changes Cascade to 9+ Providers

Groq, Cerebras, Ollama, Perplexity, OpenRouter, Parasail, Nebius, xAI, and SGL all delegate to `openai.HandleOpenAI*` functions. **Any change to OpenAI converter logic affects all of them.** Always test broadly: `make test-core` (all providers).

### 7. Scanner Buffer Pool Has a Capacity Cap

The SSE scanner buffer pool in `core/providers/utils/utils.go` starts at 4KB. Buffers grow dynamically but those exceeding **64KB are discarded** (not returned to pool) to prevent memory bloat. Be aware when working with providers that send very large SSE events.

### 8. Plugin Execution Order is Meaningful

Pre-hooks: registration order (first registered โ†’ first to run). Post-hooks: **reverse** order. This creates "wrapping" semantics โ€” the first plugin registered is the outermost wrapper (its pre-hook runs first, post-hook runs last). Changing registration order changes behavior.

### 9. Fallbacks Re-execute the Full Plugin Pipeline

When a provider fails and the request falls to a fallback, the **entire plugin pipeline** re-executes from scratch. Governance checks, caching, and logging all run again for each attempt. Intentional, but surprising when debugging request counts or cost tracking.

### 10. `AllowedRequests` Nil Semantics

A **nil** `*AllowedRequests` means "all operations allowed." A **non-nil** value only allows fields explicitly set to `true`. This applies to both `ProviderConfig.AllowedRequests` and `CustomProviderConfig.AllowedRequests`.

### 11. BifrostContext Reserved Keys Are Silently Dropped

When `BlockRestrictedWrites()` is active, writes to reserved keys (governance IDs, retry counts, fallback index, etc.) are **silently ignored** โ€” no error. If your plugin needs to pass data through context, use your own custom key type.

### 12. `fasthttp`, Not `net/http`

Bifrost uses `github.com/valyala/fasthttp` for provider HTTP calls. The API is different from `net/http`:
- Use `fasthttp.AcquireRequest()`/`fasthttp.ReleaseRequest()` for lifecycle
- `fasthttp.Client` pools connections per-host (`NetworkConfig.MaxConnsPerHost`, default 5000, 30s idle)
- Request/response bodies accessed via `resp.Body()` (returns `[]byte`, not `io.Reader`)
- **Exception:** Bedrock uses `net/http` (for AWS SigV4 signing) with `http.Transport` configured for HTTP/2 multi-connection support

### 13. `sonic`, Not `encoding/json`

JSON marshaling in hot paths uses `github.com/bytedance/sonic` for performance. `core/schemas/` uses standard `encoding/json` for custom marshaling (e.g., `NetworkConfig`). Don't mix them accidentally.

For reading or writing a **single field** (or a handful) inside a larger raw JSON payload, prefer `github.com/tidwall/gjson`/`github.com/tidwall/sjson` over decoding into `map[string]interface{}` and re-encoding โ€” the shared helpers `providerUtils.GetJSONField`/`SetRawJSONField`/`DeleteJSONField`/`JSONFieldExists`/`GetJSONSubtree` (`core/providers/utils/utils.go`) wrap these and should be reused where the path is a lookup on an already-in-scope `[]byte`/`json.RawMessage`. Full-document decode into a map/struct is still correct when you need the whole shape (e.g. re-marshaling an entire object to normalize it) โ€” the point is not to round-trip an entire object through a `map[string]interface{}` just to inspect one key. When marshaling back out, always use `providerUtils.MarshalSorted` (never a raw `sonic.Marshal`/`json.Marshal`), since unsorted map keys reorder nondeterministically and break prompt-cache-relevant byte stability.

### 14. Atomic Pointer for Hot Config Reload

`Bifrost` uses `atomic.Pointer` for providers and plugins lists. On updates: create new slice โ†’ atomically swap pointer. **Never mutate the slice in place** โ€” concurrent readers would see partial state.

### 15. MCP Tool Filtering is 4 Levels Deep

Tool access follows: Global filter โ†’ Client-level filter โ†’ Tool-level filter โ†’ Per-request filter (HTTP headers). All four levels must agree for a tool to be available. Changes to filtering logic must respect this hierarchy.

### 16. `config.schema.json` is the Source of Truth

`transports/config.schema.json` (~2700 lines) is the authoritative definition for all `config.json` fields. Documentation examples must match. When adding config fields: update schema first โ†’ handlers โ†’ docs.

### 17. UI `data-testid` Attributes Are Load-Bearing

E2E tests depend on `data-testid` attributes. Convention: `data-testid="<entity>-<element>-<qualifier>"`. If you rename or remove one, search `tests/e2e/` for references. If you add new interactive elements, add `data-testid`.

### 18. E2E Tests โ€” Never Marshal Payloads to Maps

In `tests/e2e/core/`, **never marshal API payloads to a `Record`/`Map`/plain-object and then re-serialize**. Field ordering matters for backend validation and snapshot comparisons. Construct payloads as object literals with fields in the intended order and pass directly to Playwright's `request.post({ data })`. Avoid `Object.fromEntries()`, `JSON.parse(JSON.stringify(...))` round-trips, or destructuring into an intermediate `Record<string, unknown>` โ€” these can silently reorder fields.

### 19. Framework Tests Need `tests/docker-compose.yml`, Not `framework/docker-compose.yml`

`make test-framework` fails ~30 tests in `framework/vectorstore` with no services running. Bring the stack up first:

```bash
docker compose -f tests/docker-compose.yml up -d
```

Two compose files define overlapping services on the **same host ports** (9000, 6379, 6334, 5081), so only one can run at a time. Use the `tests/` one:

| | `tests/docker-compose.yml` | `framework/docker-compose.yml` |
|---|---|---|
| Redis | plain 6379, **TLS 6380, cluster 7000, cluster-TLS 7100** | plain 6379 only |
| TLS certs | `redis-certs-init` writes `tests/redis-certs/` | none |
| Weaviate | 1.32.4, pins `CLUSTER_ADVERTISE_ADDR` | 1.25.0, no advertise addr |

The differences are load-bearing, not cosmetic:

- `redis_test.go` dials **6380** and **7100** for the TLS and TLS-cluster client tests, and `readTestCACert` reads `tests/redis-certs/ca.crt`. The `framework/` file provides neither, so 5 tests fail against it.
- Weaviate's memberlist aborts startup with `Failed to get final advertise address: No private IP address found` unless `CLUSTER_ADVERTISE_ADDR` is set ([weaviate#7474](https://github.com/weaviate/weaviate/issues/7474)). The `tests/` file pins a static IP; the `framework/` file does not, so its Weaviate crash-loops and 4 more tests fail.

Note that `qdrant` and `pinecone` report `(unhealthy)` in `docker compose ps` under the `framework/` file because those images have no `wget` for the healthcheck. The services themselves are fine, so ignore that specific signal and probe the port instead.

Only `framework/vectorstore` needs any of this. Every other framework package passes with nothing running.

---

## Adding a New Provider โ€” Full Checklist

1. Create `core/providers/<name>/` with files per the pattern (see "Provider Implementation" above)
2. Add `ModelProvider` constant in `core/schemas/bifrost.go`
3. Add to `StandardProviders` list in `core/schemas/bifrost.go`
4. Register in `core/bifrost.go` โ€” add import + case in provider init switch
5. **UI integration** (all required):
   - `ui/lib/constants/config.ts` โ€” model placeholder + key requirement
   - `ui/lib/constants/icons.tsx` โ€” provider icon
   - `ui/lib/constants/logs.ts` โ€” provider display name (2 places)
   - `docs/openapi/openapi.json` โ€” OpenAPI spec update
   - `transports/config.schema.json` โ€” config schema (2 locations)
6. **CI/CD**: Add env vars to `.github/workflows/pr-tests.yml` and `release-pipeline.yml` (4 jobs)
7. **Docs**: Create `docs/providers/supported-providers/<name>.mdx`
8. **Test**: `make test-core PROVIDER=<name>`

---

## Testing

### Bug fixes: red before green

Before writing a fix, add (or extend) a test that reproduces the bug and confirm it fails for the expected reason โ€” a wrong assertion, not a compile error or an unrelated panic. Only then implement the fix, and confirm the same test now passes. For bugs reachable through `make run-provider-harness-test`, add the harness regression case (see `.claude/skills/harness-test-writer/SKILL.md`) alongside Go-level tests: Go tests give a fast, free red/green loop while coding; the harness case is the live end-to-end pin, expected red pre-fix and green post-fix, validated structurally (`augment-provider-harness.mjs` / `filter-collection.mjs`) without needing a live paid run during development.

### Every `core/` change ships with a provider-harness case

Any change under `core/` that a client can observe on the wire must land together with a case in `tests/e2e/api/collections/provider-harness.json` (see `.claude/skills/harness-test-writer/SKILL.md`). This covers new features and refactors, not only bug fixes โ€” the rule in the previous section is the narrower instance of this one.

`core/` is the only layer every transport, integration and provider funnels through, so its behaviour is what the harness exists to pin. A Go unit test proves the function does what you meant; only the harness proves the bytes a real client sends still come back correct through the whole stack. The gap between those two is where regressions live: a fail-soft that fires on one request shape and silently skips a sibling shape passes every unit test it has.

Write the case so it is **red before the change and green after**, and validate it structurally while developing โ€” no live paid run needed:

```bash
node tests/e2e/api/runners/augment-provider-harness.mjs --source tests/e2e/api/collections/provider-harness.json --out tmp/harness-augmented.json
node tests/e2e/api/runners/filter-collection.mjs --source tmp/harness-augmented.json --out tmp/filtered.json --feature "<keyword>"
```

Insert into the collection surgically (a script that splices the new object in, never a whole-file reserialize) โ€” the file is ~50k lines and a reformat buries the actual change.

The narrow exemptions: changes with no wire-visible effect (comments, internal renames, log lines) and behaviour no HTTP request can reach. If a change is exempt, say so explicitly in the PR rather than leaving the omission unexplained.

### Always prefer `make test-core` over raw `go test` for provider-level tests

The `make test-core` target is the canonical harness for provider tests โ€” it wires up env vars from `.env` (provider API keys), invokes the per-provider `{provider}_test.go` entrypoint in `core/providers/<provider>/`, and routes through the shared `core/internal/llmtests/` scenario suite that validates end-to-end behavior (including streaming).

Running bare `go test ./core/providers/<provider>/...` only executes unit tests and skips the llmtests scenarios โ€” so it won't catch regressions in streaming, tool-calling, or provider-specific response shapes.

```bash
make test-core PROVIDER=anthropic TESTCASE=TestChatCompletionStream   # exact test
make test-core PROVIDER=openai PATTERN=Stream                          # substring match
make test-core PROVIDER=bedrock                                        # all scenarios for one provider
make test-core DEBUG=1 PROVIDER=gemini TESTCASE=TestResponsesStream    # attach Delve on :2345
```

`PATTERN` and `TESTCASE` are mutually exclusive. Provider name must match a directory under `core/providers/` (e.g. `anthropic`, `openai`, `bedrock`, `vertex`, `azure`, `gemini`, `cohere`, `mistral`, `groq`, etc.).

### LLM Tests (`core/internal/llmtests/`)

Scenario-based tests that run against **live provider APIs** with dual-API testing (Chat Completions + Responses API):

```go
func RunMyScenarioTest(t *testing.T, client *bifrost.Bifrost, ctx context.Context, cfg ComprehensiveTestConfig) {
    // Use validation presets: BasicChatExpectations(), ToolCallExpectations(), etc.
    // Use retry framework for flaky assertions
}
```

- Register in `tests.go` `testScenarios` slice
- Add `Scenarios.MyScenario` flag to `ComprehensiveTestConfig`
- Run: `make test-core PROVIDER=<name> TESTCASE=<TestName>`

### MCP Tests (`core/internal/mcptests/`)

Mock-based tests with `DynamicLLMMocker` and declarative setup:

```go
manager, mocker, ctx := SetupAgentTest(t, AgentTestConfig{
    InProcessTools:   []string{"echo", "calculator"},
    AutoExecuteTools: []string{"*"},
    MaxDepth:         5,
})
// Queue mock LLM responses, assert tool execution order
```

Categories: `agent_*_test.go`, `tool_*_test.go`, `connection_*_test.go`, `codemode_*_test.go`

Run: `make test-mcp TESTCASE=<TestName>`

### E2E Tests (`tests/e2e/`)

Playwright tests with page objects, data factories, fixtures:

- Page objects extend `BasePage`, use `getByTestId()` as primary selector strategy
- Data factories use `Date.now()` for unique names (prevents collision in parallel runs)
- Track created resources in arrays, clean up in `afterEach`
- Import `test`/`expect` from `../../core/fixtures/base.fixture` (never from `@playwright/test`)
- **Never marshal API payloads to a `Record`/`Map`/plain-object and then re-serialize.** Field ordering matters for snapshot comparisons and some backend validations. Construct payloads as object literals with fields in the intended order and pass directly to Playwright's `request.post({ data })`. Do NOT destructure into an intermediate `Record<string, unknown>` or use `Object.fromEntries()` / `JSON.parse(JSON.stringify(...))` round-trips, as these can reorder fields.

Run: `make run-e2e FLOW=<feature>`

---

## Claude Code Skills

Four skills are available via `/skill-name`:

### `/docs-writer <feature-name>`
Write, update, or review Mintlify MDX documentation. Researches UI code, Go handlers, and config schema. Validates `config.json` examples against `transports/config.schema.json`. Outputs docs with Web UI / API / config.json tabs.

Variants: `/docs-writer update <doc-path>`, `/docs-writer review <doc-path>`

### `/e2e-test <feature-name>`
Create, run, debug, audit, or auto-update Playwright E2E tests.

Variants:
- `/e2e-test fix <spec>` โ€” Debug and fix a failing test
- `/e2e-test sync` โ€” Detect UI changes, update affected tests automatically
- `/e2e-test audit` โ€” Scan specs for incorrect/weak assertions (P0-P6 severity scale)

### `/investigate-issue <issue-id>`
Investigate a GitHub issue from `maximhq/bifrost`. Fetches issue details, classifies by type/area, searches codebase, traces dependencies, analyzes side effects, suggests tests (LLM/MCP/E2E), and presents an implementation plan with per-change approval gates.

### `/resolve-pr-comments <pr-number>`
Systematically address unresolved PR review comments. Uses GraphQL to get unresolved threads, presents each with FIX/REPLY/SKIP options, collects fixes locally, and only posts replies **after code is pushed** to remote.

---

## Common Workflows

### Modify chat completions across all providers
1. Change types in `core/schemas/chatcompletions.go`
2. Update converter functions in each provider's `chat.go`
3. If streaming affected, update `framework/streaming/` (accumulator, delta copy)
4. Run `make test-core` (all providers)

### Add a new field to API responses
1. Add to schema type in `core/schemas/`
2. Map in provider response converter (`ToBifrost*Response`)
3. Handle in streaming accumulator if applicable
4. Update HTTP handler if field needs special serialization
5. Update `transports/config.schema.json` if configurable

### Add a new plugin
1. Create `plugins/<name>/` with its own `go.mod`
2. Implement `LLMPlugin`, `MCPPlugin`, or `HTTPTransportPlugin` interface
3. Add to `go.work`
4. Register in transport layer or Bifrost config
5. Add test targets to `Makefile`

### Modify a UI feature
1. Find workspace page: `ui/app/workspace/<feature>/`
2. Check existing `data-testid` attributes โ€” E2E tests depend on them
3. Add `data-testid` to new interactive elements
4. Run `make run-e2e FLOW=<feature>` to verify
5. If E2E tests break, use `/e2e-test sync` to update them

---

## Key Files Quick Reference

| What | Where |
|------|-------|
| Main Bifrost struct & queuing | `core/bifrost.go` |
| Inference routing & fallbacks | `core/inference.go` |
| Provider interface (30+ methods) | `core/schemas/provider.go` |
| ModelProvider enum & context keys | `core/schemas/bifrost.go` |
| Plugin interfaces & pooled HTTP types | `core/schemas/plugin.go` |
| BifrostContext (mutable context) | `core/schemas/context.go` |
| Chat completion types | `core/schemas/chatcompletions.go` |
| Responses API types | `core/schemas/responses.go` |
| Object pool (prod + debug) | `core/pool/pool_prod.go`, `pool_debug.go` |
| Shared provider utils & SSE parsing | `core/providers/utils/utils.go` |
| Streaming accumulator | `framework/streaming/accumulator.go` |
| HTTP inference handler | `transports/bifrost-http/handlers/inference.go` |
| Governance handler | `transports/bifrost-http/handlers/governance.go` |
| Config schema (source of truth) | `transports/config.schema.json` |
| Pool debug profiler | `transports/bifrost-http/handlers/devpprof.go` |
| LLM test infrastructure | `core/internal/llmtests/` |
| MCP test infrastructure | `core/internal/mcptests/` |
| E2E test infrastructure | `tests/e2e/core/` |
| Docs navigation config | `docs/docs.json` |
| CI/CD workflows | `.github/workflows/` |

---

## Code Style

- **Go**: `gofmt`/`goimports`. No custom linter config.
- **TypeScript/React**: Oxfmt. TanStack Router.
- **JSON tags**: `snake_case` matching provider API conventions.
- **Error strings**: Lowercase, no trailing punctuation (Go convention).
- **Provider types**: Prefixed with provider name in PascalCase (`AnthropicChatRequest`, `GeminiEmbeddingResponse`).
- **Converter functions**: Pure โ€” no side effects, no logging, no HTTP.
- **Pool names**: Descriptive string passed to `pool.New()` (e.g., `"channel-message"`, `"response-stream"`).
- **Context keys**: Use `BifrostContextKey` type. Custom plugins should define their own key types to avoid collisions.
- **Go filenames**: No underscores. The only permitted underscore is the `_test.go` suffix. Examples: `pluginpipeline.go`, `pluginpipeline_test.go` โ€” never `plugin_pipeline.go` or `plugin_pipeline_race_test.go`. Concatenate words (lowercase, no separators) for multi-word filenames.

# Frontend Code Guidelines & Patterns

This document defines the standards, structure, and best practices for writing frontend code in this project.

---

## Tech Stack

- **React** (with Vite)
- **TypeScript**
- **@tanstack/react-router** (type-safe routing)
- **Tailwind CSS v4**
- **Radix UI** (primitives)
- **Local UI component library** (`ui/components/ui/`) built on Radix primitives

---

## Folder Structure

```text

/ui
โ”œโ”€โ”€ app                # Routes & pages
โ”œโ”€โ”€ components        # Shared components
โ”‚   โ””โ”€โ”€ ui            # Core design system components
โ”œโ”€โ”€ hooks             # Custom React hooks
โ”œโ”€โ”€ lib               # Utilities, helpers, shared logic
โ””โ”€โ”€ app/enterprise    # Enterprise-specific code (via symlink)

```

### Rules

- All frontend code must live inside `/ui`
- Routes and pages โ†’ `ui/app`
- Shared/reusable components โ†’ `ui/components`
- Core UI primitives โ†’ `ui/components/ui`
- Utilities and libraries โ†’ `ui/lib`
- Custom hooks โ†’ `ui/hooks`

---

## Libraries & Usage

### Core Libraries

- `react` โ†’ UI library
- `typescript` โ†’ Type safety
- `tailwindcss` โ†’ Styling
- `@tanstack/react-router` โ†’ Routing

### UI & Visualization

- `@radix-ui/react-*` โ†’ UI primitives
- `ui/components/ui/*` โ†’ Project's Radix-based component system
- `recharts` โ†’ Charts
- `monaco-editor` โ†’ Code editor

### Utilities

- `date-fns` โ†’ Date/time formatting
- `nuqs` โ†’ Query param state management

### Tooling

- `Oxfmt` โ†’ Code formatting
- `vitest` โ†’ Testing

---

## Routing Convention

For every new route:

```text

ui/app/<route-name>/
โ”œโ”€โ”€ layout.tsx   # Route definition using createFileRoute
โ”œโ”€โ”€ page.tsx     # Page content
โ””โ”€โ”€ views/       # Optional: route-specific components

```

### Rules

- Folder name must match route name
- Always use `createFileRoute` in `layout.tsx`
- `page.tsx` should only handle composition (not heavy logic)
- Route-specific components go inside `views/`

---

## Component Guidelines

### Reusability First

- Always check if similar components/functions already exist
- Prefer extending or refactoring existing code over duplication
- Only create new components if reuse is not feasible

---

### Component Placement

- Shared โ†’ `ui/components`
- Route-specific โ†’ `views/` inside route folder

---

### Entity Selectors โ€” never hand-roll an entity picker

Any UI that lets a user pick an existing entity (virtual key, team, customer, user, business unit, โ€ฆ) **must** go through `ui/components/entitySelectors/`. Do not build a new `Select`/`Combobox` + `useState` + debounce + fetch stack for this โ€” that pattern was already duplicated across surfaces and consolidated here.

**Use an existing selector** โ€” import it and pass one of the three modes:

```tsx
import { VirtualKeySelector } from "@/components/entitySelectors/virtualKeySelector";

<VirtualKeySelector value={id} onChange={setId} fallbackOption={{ value: row.id, label: row.name }} />   // single
<VirtualKeySelector multiple value={ids} onChange={setIds} />                                            // multi (chips inside the control)
<VirtualKeySelector mode="add" onSelect={(o) => appendRow(o)} />                                         // fire-and-forget add
```

Available today: `virtualKeySelector`, `teamSelector`, `customerSelector` (OSS); `userSelector`, `businessUnitSelector` (enterprise โ€” reached via registry, see below).

**Always pass `fallbackOption` / `fallbackOptions`** when editing an existing row. Selectors fetch nothing until the popover opens, so a preselected id renders as a raw UUID otherwise.

**Adding a selector for a new entity** โ€” write a thin wrapper, never a new picker. Copy `customerSelector.tsx` (the simplest one) and change only what genuinely differs: the list query, the by-id label resolver, and the label/description fields. The wrapper must:

1. Call `useEntitySelectorSearch()` for open/search/debounce state, and pass `skip` to the RTK Query hook โ€” nothing is fetched until the picker opens.
2. `useMemo` the `options` array. Multi mode feeds it to react-select as `defaultOptions`, which re-syncs on identity change and will loop if the identity churns.
3. Ship a `LabelResolver` component (`EntityLabelResolverProps`) that fetches one entity by id and calls `onResolved` โ€” this is what keeps selected-but-unfetched ids from rendering as UUIDs.
4. Type its props as `OwnProps & EntitySelectorModeProps` and extend `EntitySelectorCommonProps`, so all three modes and the shared prop surface come for free.
5. Default `limit` to `ENTITY_SELECTOR_PAGE_SIZE`; expose a `filters` prop only if the endpoint supports server-side scoping.
6. Search is **server-side** โ€” never fetch a page and filter it client-side.

Do not edit `entitySelector.tsx` to accommodate one surface. It only carries behaviour identical across every entity; per-entity differences belong in the wrapper, per-surface differences in props (`trigger`, `triggerClassName`, `excludeIds`, `noPortal`, `className`).

**OSS โ†” enterprise placement.** `entitySelector.tsx` and any selector whose API is OSS live in `ui/components/entitySelectors/`. A selector for an enterprise-only API lives in `bifrost-enterprise/enterprise-ui/app/components/entitySelectors/` and OSS must never import it directly โ€” OSS reaches it through a runtime registry (`ui/lib/registries/userPicker.tsx`, `ui/lib/registries/modelLimitScopes.tsx`), with an empty fallback under `ui/app/_fallbacks/enterprise/` so OSS-only builds simply hide the option. Keep single mode prop-compatible with the registry contract (`{ value, onChange, disabled, fallbackOption }`) so the selector can be registered as-is.

---

### JSX & Rendering

- Avoid deeply nested conditional rendering
- Break complex UI into smaller components
- Keep components readable and maintainable

---

### Lists & Keys

- Always use **stable, unique keys**
- Never use array index as key (unless unavoidable)

---

## React Best Practices

- Avoid unnecessary or unstable dependencies in hooks
- Prevent infinite loops in `useEffect`
- Keep dependency arrays accurate and minimal
- Prefer derived state over duplicated state

---

## State Management

### Priority Order

1. Query Params (`nuqs`) โ†’ for persistent/shareable state
2. Local State โ†’ for UI-only state
3. Redux โ†’ only when truly necessary

---

### Query Params (`nuqs`)

- Use for state that should persist across refresh/navigation
- Use proper parsers like `parseAsString` or `parseAsInteger`
- Do NOT mix query param state with local/redux state
- Follow a single consistent pattern across the codebase

---

### Redux

- Use only when global/shared state is required
- Avoid unnecessary slices
- Prefer simpler alternatives when possible

---

### RTK Query (`@reduxjs/toolkit/query`)

- Use for API calls and caching
- Use **granular tags** for cache invalidation
- Avoid invalidating entire datasets unnecessarily
- Implement **optimistic updates** where applicable

---

## Forms

We use:

- `react-hook-form`
- `zod v4` (for schema validation)

### Rules

- Always define a Zod schema
- Include meaningful validation messages
- Prefer **inline field errors** (not toast notifications)
- Use `refine` / `superRefine` for complex validation
- Store schemas in: `ui/lib/types/schemas.ts`

---

## Tables

- Use `@tanstack/react-table` **only for large/complex datasets**
- For simple tables โ†’ build custom lightweight components
- Prioritize performance over abstraction

---

## โšก Performance Guidelines

- Lazy load heavy or rarely-used libraries
- Avoid unnecessary re-renders
- Split large components into smaller ones
- Keep bundle size minimal

---

## Dependency Rules

- Do NOT add new dependencies unless absolutely necessary
- Always pin exact versions (no `^` or `~`)
- Prefer existing libraries in the codebase

---

## TypeScript Guidelines

- Avoid using `any` unless absolutely unavoidable
- Prefer strict typing and inference
- Define reusable types in shared locations

---

## Code Quality & Formatting

After writing code:

```bash
cd ui && npm run format
````

Then verify build:

```bash
cd ui && npm run build
```

* Code must pass formatting and build checks
* Follow consistent naming and structure conventions

---

## Anti-Patterns to Avoid

* Duplicate components without considering reuse
* Mixing multiple state management approaches unnecessarily
* Overusing Redux
* Using unstable hook dependencies
* Adding heavy libraries for simple use cases
* Poorly structured or deeply nested JSX

---

## Summary

* Prioritize **reusability, performance, and consistency**
* Follow **strict folder structure and routing conventions**
* Use **the right tool for the right problem**
* Keep code **simple, predictable, and maintainable**