Skip to content

Changelog

All notable changes to Busbar are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Every release uses the same section headings, in this order: Added, Changed, Deprecated, Removed, Fixed, Security. Migration steps for a breaking change appear as a bold Migration item under Changed.

July 14, 2026

  • Finished greening the CI matrix that 1.3.1 began: three checks were still red and are now clean with no change to the binary. fmt — committed the rustfmt reformatting of render_histogram that had been applied locally but never committed. WindowsPolicyOnError is only used by the unix-only socket-gate tests, so its import is now #[cfg(unix)]. cargo-deny — the workspace’s own member crates are versionless path deps; marking them publish = false (they never go to crates.io) lets allow-wildcard-paths apply, so bans passes while external wildcards stay denied.
  • scripts/preflight.sh now fails on an uncommitted working tree (CI tests the committed state — an uncommitted cargo fmt is exactly how the fmt red slipped past 1.3.1) and runs cargo-deny (the Security job) when installed.
  • Dependency bumps (Dependabot): bytes 1.12.0 → 1.12.1; CI/release actions docker/build-push@7, docker/setup-buildx@4, docker/metadata@6, docker/login@4, actions/upload-artifact@7, actions/download-artifact@8.

July 14, 2026

A maintenance release: no behavior change to the binary, a clean CI matrix, and a pre-release gate so the config-specific breakage caught here can’t recur.

  • Restored a clean CI matrix: config-specific dead code that only -D warnings rejects under --no-default-features or on Windows now compiles cleanly. PROBE_TIMEOUT moved inside its #[cfg(unix)] scope; the admin_token_hash getter and a WarnCapture test helper are gated to the features that use them; the admin-listener split test is gated to auth-admin-tokens.
  • scripts/preflight.sh — a pre-release gate that mirrors the full CI matrix locally (fmt, structure-lint, clippy + build + test on both the default and --no-default-features feature sets, and a best-effort Windows type-check). Run it before tagging so a release can never ship red CI.

July 13, 2026

The API release. Everything you could only do by editing YAML and restarting, you can now do over an authenticated, audited API. And the routing hook grew into a hook system: gates and taps, on every request.

This release reshapes how hooks and policies are configured. Hooks are now defined once by name and referenced everywhere; the old inline policy: block and transport-named route: values are replaced. Existing configs need a one-time update; see the 1.2.x → 1.3 migration guide. The change is a clean cut with no silent fallbacks: an old-form key reports a clear startup error telling you exactly what to write instead.

  • FinOps metering, built for third parties. GET /api/v1/admin/usage reports per-model and per-key consumption as the RAW token split (input / output / cache-read / cache-creation — each prices differently) in fixed UTC-day buckets, with spend_micros (micro-USD, integer math) derived at read time from your configured prices. The principle: Busbar exposes the inputs of cost, not just its own number — a consumer with negotiated per-model pricing reconstructs cost exactly from the split. ?window= selects past buckets; over-cap key lists carry an others remainder so every unit stays attributable; window/as_of/currency label the numbers.
  • Hooks are control-plane citizens. A hook self-reports its OBSERVED settings and its own operational metrics over the new status wire message (a Headroom compressor reports what it compressed), and GET /api/v1/admin/hooks/{name}/status surfaces it with a desired-vs-reported drift verdict. A dashboard built on Busbar sees what every plug is doing — hooks don’t need their own dashboards.
  • One professional wire contract, audited to zero. Three independent contract-audit rounds on the Admin API and two on the hook wire, all findings fixed pre-freeze: one error envelope everywhere with a frozen code taxonomy (including unauthorized, method_not_allowed, and retryable version_conflict split from terminal conflict), one {items, next_cursor} list envelope with opaque cursors, one optimistic-concurrency mechanism (RFC-7232 If-Match/ETag on every mutable resource), Idempotency-Key on both secret-minting POSTs, Retry-After on 429s, machine-readable query params + scope annotations in openapi.json, and explicit per-request op discrimination + append-only evolvability rules on the hook wire.
  • The Admin API is now a full config plane. Anything the config file can express, the API can do: read the running config, apply a validated change atomically, roll back to any previous version, register hooks, adjust pools, budgets, and rate limits. Drive Busbar from Terraform, Ansible, or CI. No SSH, no file edits, no restarts.
  • Config overlay. API-applied changes persist to a Busbar-owned overlay file; your hand-written config.yaml is never touched. The effective config is base plus overlay, both human-readable, so “who set this” is always answerable.
  • Admin audit log. Every admin mutation is recorded: who changed what, when. Scoped admin tokens let you mint credentials that can, for example, only register hooks or only read.
  • Named hooks. Define a hook once under hooks:, reference it anywhere: in a pool’s hooks: [...] list, or via global_hooks: to run on every request. One list carries both jobs: a pool names its ranking strategy (weighted, cheapest, fastest, least_busy, usage) and any gates together, e.g. hooks: [cheapest, pii-guard]. The old route: values and the inline policy: block are removed; an old-form key is a clear startup error naming its replacement (a clean cut, no silent fallback). See the 1.2.x → 1.3 migration guide (docs/migration-1.3.md).
  • Gates and taps. A gate is a blocking hook that can reject a request or restrict which pool members may serve it; a tap is fire-and-forget observation (request, route, per-attempt, and completion stages) that can never delay or fail a request. Routes rank, gates decide, taps watch.
  • The restrict verb. A gate can reply “only members carrying these tags may serve this”: compliance-constrained routing (data residency, BAA-only lanes) without teaching your router about compliance. Restrictions hold across failover.
  • Concurrent hooks. All of a request’s hooks fire at once, so added latency is the slowest hook, not the sum. Any reject wins; restrictions intersect; the route ranks what survives.
  • Pluggable auth. Authentication is now an ordered chain of modules: each module identifies the caller, rejects, or passes to the next. Today’s token auth is the first module and the default. And it is removable: list only your own module and tokens are gone. External modules speak the same hook transports; validated identities are cached (with instant admin flush), and auth always fails closed. Budgets, rate limits, pool access, and audit all follow the authenticated principal, whoever issued it.
  • Admin API lockdown. The admin API authenticates through its own pluggable chain, with scoped principals (read-only, hooks-register, full) replacing the single shared admin token, and every mutation in the audit log attributed to the person who made it. The chain itself is live-mutable (PUT /api/v1/admin/admin-auth) and guarded so a change that would lock the caller out is refused instead of applied.
  • The rewrite verb. A trusted gate (prompt: rw) can replace the request body before dispatch, context compression and redaction, across all six protocols at once, because it fires on the normalized form. Rewrites persist across failover, token accounting uses the rewritten body (the savings are real and measured), and a malformed or slow rewrite proceeds with the original body untouched; a broken compressor can never corrupt a request.
  • Live hook settings. Push a settings map to a running hook over the admin API; the change commits only when the hook acknowledges it, and a restarted hook always receives its current settings before any traffic. Hooks can also describe their own settings schema, served verbatim by the API.
  • Config reload, and health that survives everything. POST /api/v1/admin/config/reload re-reads your config files and applies them atomically, and lane health (circuit breakers, cooldowns, learned latency) is carried across by model identity, not list position, so a reorder or an added model never resets what Busbar has learned. The same health state now persists across restarts too: kill Busbar, fix the config, start it again. Sub-second, and it comes back remembering which lanes were misbehaving. --safe-mode boots from your base config alone when an API-applied overlay is the problem.
  • Group-based governance. Map identity-provider groups to authority in one place: group_map: grants admin scopes and data-plane access (allowed pools, rate limits, budgets) to groups, and a group-mapped user is governed by exactly the machinery a virtual key uses. Per-module caps bound what any auth module can assert: an allowlist of groups it may claim, and a ceiling on the admin scope obtainable through it.
  • The admin API runs on its own listener, always. The management surface (/api/v1/admin/…) is served on a dedicated admin_listen and is never mounted on the data listen — the public bind cannot serve /api/v1/admin/* at all. It carries its own admin_tls: (cert + optional client_ca_file for client-certificate mTLS), so the control plane can require client certs, bind, and firewall independently of public LLM traffic. admin_listen defaults to loopback (127.0.0.1:8081), so a zero-config deployment boots with admin reachable only on-host; point it at an exposed address (with mTLS) to manage Busbar remotely.
  • The management API lives under one root: /api/v1/admin/…. Every Busbar-native API mounts under /api/<version>/<area>/ — cleanly separated from the data-plane protocol paths (which are dictated by the vendor SDKs and don’t move). The key-management endpoints that previously lived at /admin/keys* are now /api/v1/admin/keys*; scripts calling the old paths need a one-line URL update. Future surfaces (and a future v2) slot in under the same root without new top-level paths.
  • Completion telemetry now carries usage for every operation type (chat tokens, embeddings, images, audio, rerank) plus a request id that correlates a request across hook stages.
  • The inline policy: block and transport-named route: values. A pool’s route: now takes a hook name (defined once under hooks:) or a native policy name (weighted/cheapest/fastest/least_busy/usage); the old route: socket / route: webhook + policy: form is replaced. Each removed key reports a startup error with the exact replacement. See the migration guide.
  • The embedded Rhai script routing policy (route: script). Available only behind an opt-in build flag and deprecated in 1.2.1, it is gone. A compiled hook over a socket or an HTTP webhook does the same job with real process isolation; if you want scripting, run a hook that embeds it.
  • Exposed admin plane requires mTLS — fail closed. A network-exposed admin_listen (any non-loopback bind) refuses to boot unless it is protected by client-certificate mTLS (admin_tls.client_ca_file). A loopback admin bind is exempt (unreachable off-host), and an operator fronting admin with a mesh that terminates mTLS can waive the guard explicitly with admin_insecure: true. The management plane is never silently published behind a bearer token alone.

July 11, 2026

A hardening release, plus the routing hook layer growing up: a faster transport and the payload and verbs that make screening hooks possible.

  • The socket routing hook (route: socket). Your routing policy as a compiled binary on a local Unix domain socket. Same wire contract as the HTTP webhook (a hook moves between the two without changing its logic), same hard deadline and on_error fail-safe, no HTTP stack in between: measured end to end against a real external Rust hook, a decision costs about 8 microseconds median. Busbar never spawns or supervises the hook binary; you (or your init system) run it, Busbar connects lazily, keeps the connection alive, and reconnects transparently across hook restarts. Kill the hook mid-traffic and requests keep flowing on the pool’s fallback. Unix-only; on other platforms use route: webhook.
  • Hook payload opt-ins: policy.send_prompt and policy.send_user. The hook payload stays shape-only by default; two per-pool booleans (both default false) extend it. send_prompt adds the flattened prompt content (request.system + request.messages as {role, text}) so a trusted hook can screen content — PII, guardrails, audit. send_user adds caller identity (request.user: the governance virtual-key id/name plus the body’s end-user field) so a hook can route by who is asking. The caller’s secret/token is never in the payload, under any configuration. Both transports carry the same fields; a pool that sets neither flag sends the exact pre-1.2.1 payload.
  • Member tags in the hook payload. Each candidate now carries its operator-declared free-form tags (team names, regions, compliance labels), omitted when the member declares none.
  • The hook reject verb. A hook may reply {"reject": {"status": 451, "message": "..."}} instead of an order: no upstream is dispatched and the caller receives a dialect-native error. Fail-closed and bounded: the status is clamped to 400–499 (default 403) and picks the typed error class the SDK sees, the message is sanitized, and a malformed reject still rejects — never silently routes. Counted in the new busbar_route_policy_rejections_total metric. Combined with send_prompt, this is the PII-screen primitive: a hook that sees content can stop a request before it leaves your network.
  • Default hook deadline is now 1 ms (policy.timeout_ms, was 150). The default says hooks are fast — a co-located socket hook decides in ~8 µs and a co-located webhook in ~34 µs. Raise it when your hook legitimately does I/O or crosses the network; on timeout the decision falls back per on_error and the request proceeds regardless.
  • route: script (Rhai). The embedded interpreter costs ~100x more per decision than a compiled socket hook for the same logic, and its sandbox is a weaker isolation story than a separate process. It still works behind the script-policy feature but warns at startup; migrate to route: socket (compiled hook) or route: webhook (any language).
  • Hardened throughout. Multiple rounds of extensive adversarial testing and code review over the full 1.2.0 change set and the new hook layer surfaced and fixed a broad batch of defects — protocol-translation edge cases, input validation and sanitization, error handling, and observability gaps. Every fix shipped with the regression test that catches it; the suite grew by several hundred tests this release.

July 10, 2026

Busbar now speaks more than chat. Five new operations land on top of chat: Embeddings, Moderations, Image generation, Audio (transcription and speech), and Rerank. Every one is cross-protocol, carried by the same lossless translation that already carried chat. A Gemini client can call embeddings on an Amazon Bedrock backend, an OpenAI client can route images and audio to Google Gemini, and every answer comes back in the caller’s own dialect. The round trip is lossless in both directions, errors included. Chat itself is byte-for-byte unchanged: it is simply the first operation now, not a special case.

  • Embeddings (/v1/embeddings and each protocol’s native surface), cross-protocol. An OpenAI-dialect embeddings request routes to OpenAI, Amazon Bedrock (Titan), Cohere (v2 /embed), or Google Gemini (embedContent) and returns in the caller’s own dialect. Vectors, token/usage accounting, and errors all survive the hop.
  • Per-attempt hang detection (attempt_timeout_ms). Some providers fail by hanging: the connection opens and response headers never come back, silently eating the whole failover budget on one member. attempt_timeout_ms caps a single attempt’s time to response headers; on expiry the attempt is recorded as a transient breaker failure and the request fails over to the next pool member within the same request. Set it on a model as that model’s default, and override it per pool member, so the same model can carry a 10s cap in a batch pool and a 50ms cap in a latency-critical one. The cap covers connect and headers only (a stream that has started answering is never cut off) and is always floored by the request’s remaining failover.timeout_secs. 0 is a startup error. Observable as disposition="attempt_timeout" on busbar_upstream_failures_total and reason="attempt_timeout" on busbar_failovers_total.
  • Cross-protocol logprobs (OpenAI ↔ Gemini), buffered and streaming. Per-token log probabilities are now a first-class IR concept. The ask crosses the seam in either direction (OpenAI logprobs/top_logprobs ↔ Gemini generationConfig.responseLogprobs/logprobs) and the response data comes back in the caller’s own shape (choices[].logprobs.content[]candidates[].logprobsResult, including chosen tokens, top alternatives, and synthesized UTF-8 bytes where OpenAI’s shape requires them), on buffered responses and per-chunk in streams. Backends with no logprobs concept (Anthropic, Bedrock) never receive the ask; Cohere logprobs stay same-protocol-only (its wire shape carries bare token IDs under its own tokenizer). Live-validated against real OpenAI from a Gemini-dialect client, buffered and streaming.
  • Two new cross-protocol carries: user and parallel_tool_calls. OpenAI’s user and Anthropic’s metadata.user_id are the same end-user identifier; OpenAI’s parallel_tool_calls and Anthropic’s tool_choice.disable_parallel_tool_use are the same switch, inverted. Both are now first-class in the IR and translate between the two protocols in both directions instead of being dropped at the seam. The documented field-survival table (docs/protocols.md) is now measured against real egress capture, not asserted.
  • Moderations (/v1/moderations), cross-protocol. Content-classification requests translate through the IR and return in the caller’s dialect, so a moderation call is not pinned to one vendor’s endpoint.
  • Image generation, cross-protocol. An OpenAI-dialect image request routes to OpenAI, Google Gemini (Imagen), or Amazon Bedrock (Titan) and comes back in the caller’s dialect.
  • Rerank (/v2/rerank and Bedrock rerank models), cross-protocol. The sixth operation: Cohere v2 rerank and Amazon Bedrock rerank models (via InvokeModel, detected by the query + documents body) translate exactly in both directions — the two wires share the same result shape (index + relevance_score), so a Cohere-dialect client can rerank on a Bedrock backend and vice versa, with pools, failover, and breakers like every other operation. The other four protocols ship no rerank surface and answer with the standard dialect-native 404.
  • Audio, cross-protocol. Transcription (speech-to-text, including speech-to-English translation) and Speech (text-to-speech) against OpenAI and Google Gemini backends, translated to and from the caller’s dialect like every other operation.
  • Clean, dialect-native 404 for an operation a backend lacks. Calling an operation a backend does not implement (e.g. image generation on an Anthropic backend) returns a well-formed 404 in the caller’s own protocol dialect: never a crash, never a malformed body, and never taking the lane down for other traffic.
  • Cross-protocol reasoning/thinking carry (opt-in per lane). The reasoning ask now translates between the three protocols that model it: OpenAI reasoning_effort / Responses reasoning.effort (words), Anthropic thinking.budget_tokens and Gemini thinkingConfig.thinkingBudget (token budgets). Number to number is a straight copy (a Claude/Gemini thinking pool loses nothing); words and numbers convert through a configurable effort table (limits.reasoning_effort_budgets, defaults 1024/4096/8192/16384). Because thinking support is per-MODEL, not per-protocol, the carry is gated by an operator flag: reasoning: true on a model (overridable per pool member) declares “this backend accepts thinking params”. Without the flag the ask is dropped at the seam with a warn and the request proceeds normally, so a non-reasoning model can never 400 from translation. Budgets are clamped to fit max_tokens (Anthropic requires it), and Anthropic-incompatible sampling knobs (temperature, top_k) are omitted with a warn when thinking is emitted. The response-side thinking CONTENT was already lossless and is unaffected. Gemini’s dynamic -1 round-trips to Gemini and projects elsewhere as medium.
  • License: Apache 2.0. Busbar 1.2.0 and onward is licensed under the Apache License, Version 2.0: permissive, commercial-friendly, with an explicit patent grant. Use it, modify it, and redistribute it, privately or commercially, with no copyleft obligations.
  • Every operation is lossless across protocols, errors included. Responses and error envelopes always come back in the caller’s own protocol dialect, and token/usage accounting survives the cross-protocol round trip on every operation, not just chat.
  • Four-layer operation architecture (internal). The request path is now Router → RequestHandler → OperationHandler → IR, where each operation is a small codec over the shared reliability engine and chat is operation #1 rather than a special case. Adding an operation is a codec, not a change to routing, failover, or the breaker. No user-visible behavior change to chat.
  • Billing is now a polymorphic data model (internal). Usage is metered as tokens, duration, characters, images, or a flat unit depending on the operation, so non-chat operations meter on their natural axis. A pricing engine that turns these units into cost is planned for 1.3.
  • Gemini streamed thinking no longer leaks into answer text on cross-protocol streams. The Gemini stream reader routed thought: true parts into the answer text, so a Gemini backend’s streamed reasoning was concatenated into the visible reply for every cross-protocol client (the buffered path was already correct). Thought parts now stream as proper thinking blocks (signature included) with balanced block framing on every terminal path. Caught by the new offline streaming-reasoning harness rows.

July 9, 2026

  • GET /v1/models and GET /v1beta/models: the list-models surface. Returns every routable name — configured pools first, then model entries, each sorted. This is the first call client.models.list() and self-hosted UIs (Open WebUI, LibreChat) make to build a model picker; it previously returned 404. Three protocols put list-models on the same noun, so Busbar answers in the caller’s dialect by protocol fingerprint: an anthropic-version header gets the Anthropic envelope, x-goog-api-key or the /v1beta path gets Gemini’s, otherwise OpenAI’s — the same names rendered three ways. Governance-scoped like /stats: a virtual key restricted by allowed_pools sees only the pools it may target and the models reachable through them.
  • Operations are now a first-class axis of the forward engine (internal). The request path is generic over an operation spec (OpSpec) rather than hardcoding chat’s assumptions (stream intent, upstream path, usage extraction, affinity, egress Accept). Chat is spec #1 and its behavior is byte-for-byte unchanged — the full test suite passes unmodified. This is groundwork that lets a future release add non-chat operations (embeddings, moderations, images, audio) as small spec files with no change to the reliability engine. No user-visible behavior change.
  • /metrics is no longer empty before the first request. The unlabeled counter family is pre-registered at startup and per-lane busbar_lane_state gauges are now also emitted for direct-model (pool-less) lanes — labeled with the model name as pool, matching the counter convention — so a freshly booted gateway exposes a live exposition to Prometheus immediately. Both issues were found by the user-emulated acceptance harness on its first run.
  • /stats output is now deterministic across restarts. Lanes are built sorted by model name (previously in HashMap iteration order, randomized per process), and /stats serializes pools in sorted key order. The lane/pool ordering — and therefore metric lane-series identity — is now stable boot to boot, so scrapes, dashboards, and tests are reproducible.

June 30, 2026

  • upstream_model config field: decouples a model’s config key (operator alias) from the model id sent to the provider on the wire. Lets the same model run behind two providers in one failover pool (e.g. Claude 3.5 Sonnet via Anthropic and Bedrock), where the keys must differ but each provider needs its own model string. Threaded through body rewriting, URL generation, and health probes (probes hit the same wire id as real traffic). Metrics, breaker cells, and logs continue to key off the config key. Feature contributed by @lguzzon (adopted as upstream_model; the resolver is Lane::wire_model()).
  • Documentation drift surfaced by @lguzzon and a deep doc-vs-code audit: removed dead UsageTap references, corrected same-protocol passthrough to the IR-unified model, fixed the billing_truncated metric and budget-atomicity descriptions, updated all route notation to axum 0.8 {param}/{*rest} syntax, and window_swindow_secs.

June 30, 2026

First hardened maintenance release. No request-path behavior change; the binary is functionally identical to 1.0.0, and the API, config schema, and six wire-protocol contracts are unchanged.

  • Dependency upgrades. axum 0.7 → 0.8: route path-param syntax migrated (:id{id}, *rest{*rest}), no behavior change. getrandom 0.2 → 0.3 (getrandom()fill(), same OS-CSPRNG). rcgen 0.13 → 0.14 (test-only). All build-verified: 1,667 tests pass, clippy -D warnings clean. A new credential-generator contract test pins the bearer / AWS-AKID / AWS-secret wire shapes so a future dependency change that alters them fails loudly.
  • Dependency scanning gate. A cargo-deny CI workflow checks every dependency against the RustSec advisory DB and enforces a license allow-list, crates.io-only sources, and a duplicate-version ban: on dependency changes and on a weekly schedule (an advisory can be filed after a dep is merged).
  • Signed, inventoried releases. Each release now ships a CycloneDX SBOM and a keyless (Sigstore/OIDC) build-provenance attestation, so a downloaded artifact can be verified with gh attestation verify <file> --repo MattJackson/busbarAI.

June 21, 2026

First stable release. 1.0.0 keeps the 1.0.0-rc.7 architecture (all traffic through the superset IR with a verbatim serialize short-circuit, IR-metered billing) and ships an extensive hardening pass on top of it. The HTTP API, configuration schema, and the six wire-protocol contracts are stable under Semantic Versioning: no breaking change without a major-version bump. See the rc entries below for the full pre-1.0 history.

  • Typed-IR completeness. response_format, stop_reason, image source, and redacted-reasoning are first-class IR fields rather than passthrough blobs, so each survives a cross-protocol hop losslessly and no off-spec value reaches a wire.
  • Containment refactor. Per-protocol logic moved fully behind the reader/writer vtable so the agnostic core names no protocol module; load-bearing literals named as consts; in-module-only items privatized.
  • OpenAI-family module split. proto/openai.rsopenai_chat.rs, proto/responses.rsopenai_responses.rs, with shared error/auth/id helpers in openai_family.rs. The protocol names (openai, responses) are unchanged: internal layout only.
  • Reproducible builds. CI and release builds run with --locked.
  • Migration (rc.7 → 1.0.0): governance.rate_sweep_interval must now be >= 1; 0 is rejected at boot (rc.7 silently disabled the rate-map idle-entry sweep on 0). No other config change for a default deployment.
  • Cross-protocol fidelity. Two Bedrock egress shapes that returned 400 on a valid request; consecutive same-role turn coalescing on Bedrock; Anthropic cache_control carried through on thinking/image blocks; unknown stop_reason normalized on egress; a streaming-Responses refusal data-loss.
  • Billing precision. Sub-cent carry attribution, billing of cancelled mid-stream requests, and no token-billing of a translate-aborted stream.
  • A slow-loris header-read bound on both the TLS and plain-HTTP listeners; the SigV4 inbound body buffer capped independently of the body-limit layer; circuit-breaker probe-leak / streak-inflation / jitter hardening.