Skip to main content

Crate brazen

Crate brazen 

Source
Expand description

brazen — the engine behind the bz command, and the pure, fully-tested core of a stateless LLM adapter.

cargo install brazen builds the bz binary (this crate’s [[bin]]); the library is that binary’s engine, published alongside it in case it is useful as a dependency. Its API is not yet a stability contract — pin an exact version if you build on it.

This crate holds the canonical model (the single source of truth every provider/protocol projects to and from) and the traits behind which all impurity (network, clock, credentials, browser) is injected. The bz binary and src/native/ own the native impls; the library reaches 100% coverage on its own because nothing here touches IO — a boundary tests/purity.rs keeps real now that the bin and the library share one crate.

Beyond the canonical model and error model (the dependency root), this crate defines the seams the rest of the pipeline plugs into: the Protocol, Auth, Transport, CredStore, and Clock traits, the data records they exchange (WireRequest, Provider, Cred, …), and the Registry that dispatches by id without ever matching a vendor name (§4 of the architecture). It also holds the pure pipeline — input resolution, canonical-in parsing, and the output projections + pump loop (§5). Concrete protocol/auth/transport impls land via their own tasks; the shared test doubles live in [testing].

Structs§

AmbientSpec
An ambient credential source as row DATA (auth §5.5): path is the source LOCATOR the bz impl reads — a ~/$HOME-expanded filesystem path for a file format, an environment-variable NAME for api_key_env — and format selects the pure parser that maps its bytes to a Cred. Neither lives in core code, so deleting the row’s ambient block deletes the capability (severability).
Args
The injected process inputs handed to run: the program arguments (excluding argv[0]), a snapshot of the environment, and the one bit of terminal state the pure lib can’t observe — whether stdin is an interactive tty (§5.5). main builds it from std::env/isatty; tests build it from literals — so run is exercised end-to-end without touching the real process state (arch §6.5, §9.6).
CanonicalError
A normalized error, carried in-band as Event::Error (§3.3). It stores only what cannot be computed: retryable and the exit code are queries over kind, never fields that could drift.
CanonicalRequest
The single canonical request. A field set on the wire is used as-is; a field it omits defaults (getConfigValue fills it later — §6.1). extra is the long-tail valve: an unmodelled top-level key is forwarded verbatim.
CountIo
The injected seams + writers for one bz --count-tokens — the sibling of ListIo, with a reader (the count op CONSUMES a request, unlike the listing verb). Reuses the data-plane Transport/CredStore/ModelCache/Clock: the model seed is placed against the same cache (READ-only — no write), and the one round-trip goes through the same Auth::apply. stdout gets the count; any error goes to stderr.
EnvSnapshot
A snapshot of the process environment, injected by main. A newtype over a BTreeMap so the projection is deterministic and pure (config §3.4).
HeaderSpec
The auth-header shape as data (auth §2): the only thing that names the auth header, so ApiKey/Bearer share one data-driven header write.
Host
The four impure data-plane seams, bundled (arch §1, §6.5) — the sibling of the verbs’ ListIo/LoginIo IO bundles. Every round-trip the generation path makes goes through exactly these: the Transport (the one ureq user), the credential store, the model cache, and the clock (auth-refresh expiry). The writers stay separate from the Host because run borrows stdout/stderr mutably AND simultaneously when it builds the sink — a seam reference is shared, a writer reference is exclusive, so they cannot live in one struct.
ListIo
The injected seams + writers for one bz --list-models (model-discovery §2), the sibling of LoginIo. The verb writes its listing to stdout and any error to stderr, reuses the data-plane Transport/CredStore/Clock for the one GET (auth/refresh and all, through the same Auth::apply seam), and is the WHOLESALE writer of the cache — it puts the decoded list the generation path later reads, which that path then appends learned ids to on success (§5, §5.4).
LoginIo
The injected control-plane seams + RNG for one bz --login (auth §7.2).
Message
A transcript message. content is ALWAYS a Vec<Content>; a bare wire string decodes to vec![Text(..)] (the string-vs-list distinction dies at decode, never a downstream branch).
Model
One available model, the canonical projection of a provider list entry (§3). Ordered position in the returned Vec IS the provider’s suggested order — the single source the heuristics read, so there is no rank field. default is CARRIED, not invented: a dialect that flags one sets it; today none does, so it is false and §4’s first-in-list rule governs — the seam stays so a provider that DOES flag one needs no code change.
ModelsOverride
The [provider.models] per-row model-discovery override (config §4.4, model-discovery §3.2): the bz --list-models GET’s path/query and the response list keys, OVERRIDING the protocol’s default ModelsShape (§3.1). Every key is optional — an omitted one inherits the protocol default — and the whole block is optional (absent ⇒ pure protocol default). deny_unknown_fields makes a typo’d key a MalformedFile (config §2.3), like oauth. strip is NOT here: it is protocol-only (Google’s leading models/), never row-overridable. query mirrors authorize_params (a Vec<(k, v)> URL-encoded by the same codec, auth §7.4); the skip_serializing_if keeps an omitted key out of a --dump-config round-trip and off the TOML serializer’s no-None path.
OAuthConfig
The OAuth auth-row as data (auth §7.1): endpoints, client_id, scope, and the auth-mode-dependent beta_headers (e.g. anthropic-beta: oauth-…). The provider NAME is deliberately absent — it lives once, as the row key / store_key. The pure OAuth builders take &OAuthConfig, so no vendor policy is compiled into the core. Like the [[provider]] row (config §2.3), deny_unknown_fields makes a typo’d or MISPLACED key a MalformedFile rather than a silent drop — a TOP-LEVEL row key (e.g. unsupported_body_keys) typed under [provider.oauth] would otherwise vanish and the strip never fire (bl-9649).
Provider
A resolved provider row (arch §4.2). Pure data: name is a table key never matched on in the pipeline; protocol/auth are registry keys; model_aliases drives the computed alias→wire-id lookup. Sparse user/file rows fold onto the embedded defaults before a complete Provider is resolved (config §3.2).
RedirectSpec
The loopback redirect endpoint as data (auth §10.1). The default reproduces today’s literal — 127.0.0.1 (RFC 8252), an ephemeral port (None:0), and /callback — so deleting the block restores it (severability). A provider whose registered redirect differs (OpenAI: localhost:1455/auth/callback) names it here as data; the socket still binds the IPv4 loopback 127.0.0.1.
ResolvedConfig
The one config the pipeline runs on (config §7). model is the alias- resolved WIRE id, so ProviderCtx.model is final and encode has no model logic (arch §4.1). Each gen scalar already carries the routed row’s body_defaults beneath flag/env/file (folded at resolve, config §4.1).
Secret
A plaintext secret whose Debug/Display redact and whose only plaintext reads are expose() (the single audited site) and Serialize (reached only by CredStore::put writing the 0600 file) — auth §5.3.
Timeouts
The per-request transport budgets (config §4.3), in WHOLE SECONDS; each None leaves that bound unset (the transport’s own default). ureq’s three phase budgets — the seam’s internal vocabulary, NOT the config surface: the resolved config carries ONE timeout (the silence budget), and ResolvedConfig::timeouts() FANS it onto all three here (all equal, or all None), so the collapse to one knob is a surface fact and the fan is observable at this seam (arch §13.15). Carried on the WireRequest — the one thing crossing the seam — so config- sourced policy reaches the impure transport without widening the send signature. The one number lives in config (floor data/defaults.toml), never as magic in the bin (severability — policy in config, not core).
TransportResponse
The peeked HTTP status (read even under --raw, for exit-code correctness) plus the blocking, incremental body stream (arch §4.1).
Usage
Token accounting (§3.2). Every field is Option: a provider that never reports a counter leaves it None (0 would be a lie), never fabricated. Token-explicit names — these count tokens (Anthropic input_tokens/…, OpenAI prompt_tokens/…) — frozen with the rest of the v=1 vocabulary.
WireRequest
The HTTP request that flows encode → auth → transport (arch §4.1). encode builds the body + non-auth headers; Auth::apply adds the auth headers in place; Transport::send consumes it. Header names match case-insensitively so an auth overwrite never duplicates a header. method is Post for every generation request (the default — encode builds POSTs via new) and Get for the list-models verb’s GET (§6). timeouts is the per-request transport policy (config §4): encode leaves it at the Default (all unset) and run stamps the resolved config onto it before send, so a config-driven bound reaches the impure transport without a wider send signature.

Enums§

AmbientFormat
Which foreign credential format an AmbientSpec names (auth §5.5). A closed enum, not a JSON-pointer DSL: each shape needs a parser anyway, so one variant per known source is less mechanism than a speculative mapping language. The variant ALSO tells the bz discover impl where to read: a file (ClaudeCode) or a process env var (ApiKeyEnv).
AuthId
Which auth model a provider uses (arch §4.2, §4.4). A registry key. ApiKey and Bearer differ only in HeaderScheme; both ship, plus OAuth2 and None. None is a keyless row (e.g. local Ollama): no credential is read and no auth header is written, so it carries no api_header — a resolve invariant mirroring the way an OAuth2 row carries an oauth block.
Content
A piece of content. Text is expressible as a bare string or a {"type":"text",…} object; other variants are tagged objects. Thinking/RedactedThinking payloads and the two server-tool variants (CR-4) round-trip verbatim — provider-executed blocks carried untouched, mirroring the RedactedThinking rule.
ContentKind
What kind of content block is opening (§3.2). Externally tagged so it renders {"text":{}} / {"tool_use":{…}} exactly as the §5.2 sample shows.
Cred
The stored secret bundle for one provider (auth §5.1). The variant IS the token-kind discriminant; expires_at is ABSOLUTE unix-seconds; there is no is_valid flag (freshness is the now + SKEW >= expires_at query) and no provider name (the file path is the name).
Delta
A streamed content fragment (§3.2). Externally tagged so a newtype variant renders {"text_delta":"Hel"}. Tool arguments ride JsonDelta as text fragments, never a parsed Value.
DocumentSource
A document source — the Image analogue for PDFs/files, kind-tagged exactly like ImageSource; a dialect that can’t express a source rejects at encode (providers §9).
ErrorKind
The taxonomy every failure normalizes to (§3.3). Provider carries the HTTP status so retryable/exit derive without a second table. Other is the forward-compat escape hatch (§3.2 v=1 contract): an error event carries no v handshake, so a future kind cannot be version-gated — instead an unrecognized snake_case kind decodes here verbatim (mirroring FinishReason::Other) so a 0.1.0-pinned consumer degrades instead of failing. Serde is hand-rolled (below) to route the unknown tag, not derived.
Event
FinishReason
Why generation stopped (§3.2). Carried flattened into Event::Finish, keyed on reason. Refusal is a Finish, never an Error. Other preserves any unknown reason string so decode never panics on a new value.
HeaderScheme
How the secret is written into the header value (auth §2). Two arms cover every shipped wire convention; a match on it is value formatting, not vendor dispatch.
ImageSource
Method
The HTTP verb a WireRequest carries (model-discovery §6): every generation request is a Post (the default — encode is unchanged), the list-models verb’s GET a Get. Data on the one struct already crossing the transport seam (mirrors timeouts), not a new send parameter — the impure HttpTransport reads it to pick the verb, MockTransport records it.
OutMode
The output projection (arch §5.1): --text default, --jsonNdjson, --rawRaw. The single enum behind both PartialConfig.output and ResolvedConfig.output — one home for “which projection” (config §7).
OutputFormat
A PORTABLE structured-output intent — one canonical knob every structured-output- capable dialect spells differently, lifted out of extra so each adapter owns its projection (the same rule as ToolChoice/reasoning). Internally tagged on type ({"type":"json"} / {"type":"json_schema",...}), so it rides the wire and config the same way. name/strict feed only the dialects whose wire has them (OpenAI); Anthropic/Google/Ollama read only schema (providers.md §6).
ProtocolId
Which wire dialect a provider speaks (arch §4.2). A registry key, never a match target; the explicit rename keeps the config spelling (openai_chat) stable regardless of the Rust identifier.
ReasoningEffort
A PORTABLE reasoning-effort intent — one canonical knob every reasoning-capable dialect spells differently, lifted out of extra so each adapter owns its projection (the same rule as ToolChoice/parallel_tool_calls). serde lowercase, so "low"/"medium"/"high" on the wire and in config (providers.md §6).
Role
Route
Which control plane the bz shim should wire (§5.10.1). Computed by the ONE authoritative [parse_args], so the coverage-excluded shim never hand-rolls an argv scan and can never disagree with the lib on flag-vs-prompt: a value whose text looks like a control flag (--system=--login) is the value, and any word after -- is the prompt, so neither is ever mistaken for a route.
Tool
A declared tool — an OPEN SET (brazen enumerates none, registers none). The enum distinguishes only NORMALIZE vs CARRY, and the harness declares which by the shape it hands over: a wire object with no type key is Custom, one with a type key is Provider (hand-rolled serde in request_de, keyed on type).
ToolChoice
All four tool-use intents, lifted explicitly rather than left in extra.

Constants§

EVENT_SCHEMA_VERSION
Event-schema version stamped into the first MessageStart (§3.2). The one handshake a harness pins to; a backward-incompatible change to the Event vocabulary bumps it (an additive kind/event does NOT — see the module doc).

Traits§

BrowserLauncher
Open url in the user’s browser (auth §7.2). Real impl Command::spawns the browser_argv; the fake records the argv and never execs.
Clock
The one injected time source in the data plane (auth §5.4): unix seconds. The library never calls SystemTime::now; bz wires SystemClock, tests wire FakeClock.
CodeReceiver
Capture the loopback redirect (auth §7.2, §10.1). bind binds the listener on 127.0.0.1 at the requested port (None ⇒ an OS-assigned ephemeral port, RFC 8252 §7.3) and returns the ACTUALLY-bound port, which browser_flow substitutes into the redirect_uri — single-sourcing the port through the receiver whether fixed or ephemeral. await_query then blocks until the redirect arrives and returns its raw code=…&state=… query, which parse_callback validates.
CredStore
Persist and retrieve one secret bundle per provider (auth §5.2, §5.5). get returns None for a missing cred (the no-creds path), never an error; refresh is OAuth2::apply using get+put (freshness is a query); list/delete are control-plane. discover is the third primitive (auth §5.5): read a foreign credential source named by an AmbientSpec — a file (Claude Code’s ~/.claude/…) or a process env var (a vendor key alias) — into a brazen Cred. The IO — a file read with $HOME expansion, or an env read — lives in the bz impl; the format parse is the pure parse_ambient; a store with no ambient backing returns None.
ModelCache
The per-provider model-list cache (model-discovery §5.1) — filesystem state, so like CredStore it lives behind an injected trait; the pure lib never touches the disk. A SIBLING of CredStore, not folded into it: a secret and a regenerable model list are different facts with different files. The bz bin backs it with one JSON file per provider under $XDG_CACHE_HOME/brazen/models/<provider>.json (the {"models":[{id,default}]} shape list-models --json emits, reused); the in-memory double lives in testing.
Pacer
Pace the device-flow poll loop (auth §7.3): the real bin sleeps secs; the test fake records the interval and returns instantly — so the whole flow runs offline with no real time. A control-plane concern only, kept off the data-plane Clock.
Transport
The single network seam (arch §4.1). Object-safe; Send + Sync so an impl is shareable. Exactly one round-trip per process — a caller wanting N concurrent requests spawns N bz.

Functions§

browser_argv
The argv to open url in the user’s default browser on the build target (arch §7.3). A thin pass of the compile-time OS to the pure argv_for; the caller (bz --login --browser) Command::spawns the result.
count_tokens
Run bz --count-tokens and return the POSIX exit code (§5.10.1). Reads the request from reader (stdin / --input) or the positional prompt, resolves provider/model as the data plane does, does ONE round-trip, and prints {"input_tokens": N} under --json else the bare N. A failure is written to stderr and mapped to its exit (usage 64 / no-input 66 / config 78 / auth 77 / non-2xx 69-70 / malformed body 70).
generate
Generate against a resolved config (arch §1): drive ONE round-trip and yield the canonical event stream, terminated by a single End. THE pure typed core — run wraps it in byte I/O, an embedder consumes the events directly. The model SEED is resolved against the per-provider cache here (a local file read via host.cache, model-discovery §5.2), then the request is encoded, authenticated, and sent over the one Transport. Every failure is an in-band Event::Error, so the call is total. This is the normalized-in / canonical-out composition — [send_encoded] (the request half) then [canonical_events] (the response half); the --raw=out variant reuses send_encoded under a RawSink (arch §5.4, §13.14).
list_models
Run bz --list-models and return the POSIX exit code (model-discovery §2). Reuses the full flag parser + into_resolved(None) to pick the provider (an explicit --provider, else the row owning a configured model; neither → NoProvider/78), does ONE GET to models_path, and prints — --json the {"models":[…]} object, else the ids one per line with (default) on the default. The listing goes to stdout; any failure is written to stderr and mapped to its exit (config 78 / auth 77 / non-2xx 69-70 / a malformed body 70 — the same run-level table).
login
Run bz --login and return the POSIX exit code (auth §7). Resolves the provider’s OAuthConfig, runs the selected flow, and persists the resulting Cred::OAuth2. Any failure is written to STDERR and mapped to its exit (login failure → 77, unresolvable / no-oauth / no-device-endpoint provider → 78, bad flag → 64).
parse_ambient
Map a foreign credential source’s bytes to a brazen Cred (auth §5.5) — the pure half of discovery, so the bz impl does only the IO. None for malformed or incomplete input (the no-creds path, like get). The claude_code case reads claudeAiOauth into a Cred::OAuth2: expiresAt is MILLISECONDS, divided to absolute unix-seconds once here (the single home for that unit mismatch), and scopes join into the scope string (None when empty); account_id is None (Anthropic binds no account id). The api_key_env case is the env var’s value as a raw Cred::ApiKey (trimmed; empty/non-UTF-8 ⇒ None).
query_from_request_line
Extract the query string from an HTTP request line, e.g. GET /callback?code=x&state=y HTTP/1.1code=x&state=y (auth §7.4). PURE, so the bin’s loopback receiver reads the line over the socket and defers the parse here; None when the request-target carries no ?query.
route
Read the routing decision from argv (§5.10.1). A parse error (an unknown flag, two combined control ops) routes to Route::Run, whose lib entry re-parses and surfaces the same error as the authoritative 64 — so routing owns no error path.
run
The binary in one call (arch §1). Resolves config, reads the request (positional XOR stdin), encodes, authenticates, sends one round-trip, decodes the framed response into canonical events, and projects them through the mode’s sink — returning the POSIX exit code (main materializes the ExitCode).

Type Aliases§

Bytes
One transport body chunk. An alias, so the framers’ Vec<u8> and the body stream speak the same type.