brazen 0.0.4

A stateless, swiss-army-knife adapter for every LLM provider and protocol.
Documentation
//! The typed generation core (arch §1, §4.4): a `CanonicalRequest` in, an
//! `Iterator<Item = Event>` out — the pure pipeline minus byte-serialization. `run` is
//! the byte adapter over it (parse stdin → request, fold config, `pump` the events into
//! the sink); an embedder calls it directly and `match`es the typed events. Errors are
//! in-band: a request-half failure (model resolution, encode, auth, transport) yields
//! one `Event::Error` then the terminal `End`, exactly as the streamed path does
//! (§5.9, §8) — so the signature is total, never a `Result` the caller must thread.
//! `--raw` is NOT typed (it never decodes); it lives in [`stream_raw`](super::raw).

use crate::canonical::{select_model, CanonicalError, CanonicalRequest, Event, Model, Provenance};
use crate::config::{fill_absent, lead_with_preamble, strip_unsupported, ResolvedConfig};
use crate::registry::Registry;

use super::drive::{canonical_events, Sent};
use super::events::is_2xx;
use super::Host;

/// 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).
pub fn generate(
    request: CanonicalRequest,
    config: ResolvedConfig,
    host: &Host,
) -> impl Iterator<Item = Event> {
    match send_encoded(request, config, host) {
        // `host.clock.now()` (the injected `Clock` seam) lets `response_events` parse a
        // `Retry-After` HTTP-date to seconds (§3.3) — never a wall-clock read in the lib.
        Ok(sent) => canonical_events(sent, host.clock.now()),
        Err(e) => Box::new(std::iter::once(Event::Error(e)).chain(std::iter::once(Event::End))),
    }
}

/// The normalized (encoded) request half (arch §4.4): resolve the model against the
/// cache, dispatch the protocol/auth over the closed key-enums (no vendor-name match,
/// §4.4), fill/preamble/strip the request, encode, stamp headers/timeouts, authenticate,
/// and send — returning one prepared [`Sent`] (the response, the streaming intent, the
/// protocol, and the §5.3 model-provenance hint). Any step's error rides back as a
/// `CanonicalError` the caller surfaces in-band. Shared by [`generate`] (canonical out)
/// and `run`'s `--raw=out` path (raw out) — the request half is one home, the response
/// half toggles above it (§5.4, §13.14).
pub(super) fn send_encoded(
    mut request: CanonicalRequest,
    mut config: ResolvedConfig,
    host: &Host,
) -> Result<Sent, CanonicalError> {
    // Resolve the model SEED against the per-provider cache (model-discovery §5.2): a
    // LOCAL FILE READ, every request, never a round-trip. `select_model` is TOTAL — a
    // hit expands the seed to its wire id (`Cached`), a cold/absent cache passes it
    // verbatim (`Verbatim`); the lone error is an absent model with an empty cache (78).
    let cached = host.cache.get(&config.provider.name).unwrap_or_default();
    let (wire_model, prov) = select_model(&cached, &config.model, &config.provider.name)?;
    config.model = wire_model;
    config.model_from_cache = matches!(prov, Provenance::Cached);

    let registry = Registry::builtin();
    let proto = registry.protocol(config.provider.protocol);

    let beta: Vec<(&str, &str)> = config
        .provider
        .beta_headers
        .iter()
        .map(|(k, v)| (k.as_str(), v.as_str()))
        .collect();
    let ctx = config.provider_ctx(&beta);

    fill_absent(&mut request, &config);
    // An auth mode may mandate a leading system preamble (auth §4.1) — a BODY fact, so
    // resolution prepends it here and `encode` carries it like any other `req.system`.
    lead_with_preamble(&mut request, &config);
    // Drop fields the routed backend can't accept (config §4.1.1) AFTER the fill, so an
    // explicit --temperature/--top-p/--max-tokens is cleared too (the Codex 400 set).
    strip_unsupported(&mut request, &config);
    // The streaming intent the body carries (architecture §3.2), resolved by `fill_absent`
    // to a concrete bool: a bare request defaults to brazen's stream-native `true`;
    // --no-stream / body_defaults={stream=false} honor `false`. Carried to the fold.
    let streamed = request.stream.unwrap_or(true);

    let wire = proto.encode(&request, &ctx)?;
    // Query + content-type/static headers/timeouts/auth + send share one tail with
    // `--raw=in` (config §4.3.1, arch §4.4); encode owns only protocol URL/body.
    let resp = super::request::send(wire, &config, proto, &ctx, host)?;
    // Learn from the model that worked (model-discovery §5.4) — ONE write site keeping
    // TWO facts in step, which is why they can never drift: MEMBERSHIP (a VERBATIM id the
    // cache could not place, yet the provider accepted, joins the list tail — so partial
    // matching and routing's ownership tier can see it) and RECENCY (`last_used` names it,
    // so a later bare `bz` defaults to it — §4 rung 2). A Cached model is already in the
    // list, so only the verbatim case appends (dedup-free: Verbatim proves it is absent),
    // but EVERY 2xx moves the pointer — using a listed model is using it. Guarded on an
    // ACTUAL CHANGE, so the steady state (the same model, over and over) writes nothing.
    // `list-models` stays the authoritative WHOLESALE writer of the list.
    let learned = !config.model_from_cache;
    if is_2xx(resp.status) && (learned || cached.last_used.as_deref() != Some(&config.model)) {
        let mut next = cached;
        if learned {
            next.models.push(Model {
                id: config.model.clone(),
                default: false,
                // A verbatim-learned id carries no provider metadata — the data plane
                // never lists (§5.4); `--list-models` fills it in when it REPLACES.
                ..Default::default()
            });
        }
        next.last_used = Some(config.model.clone());
        host.cache.put(&config.provider.name, &next);
    }
    // The §5.3 404 hint, carried by the cache provenance: a Cached model that 404s means
    // a stale cache; a Verbatim one means a cold cache or a typo. `Some` iff this is a
    // 404 — `response_events` appends it to the decoded error's message.
    let hint = (resp.status == 404).then(|| model_hint(&config.model, config.model_from_cache));
    Ok(Sent {
        proto,
        resp,
        streamed,
        hint,
    })
}

/// The §5.3 provenance hint for a 404 on the generation request: a Cached model the
/// provider rejected points at a STALE cache; a Verbatim one (the cache could not place
/// it) at a COLD cache or a typo. The one message-construction home.
fn model_hint(model: &str, from_cache: bool) -> String {
    if from_cache {
        format!(
            "`{model}` was in the cache but the provider rejected it; \
             the cache may be stale — re-run `bz --list-models`"
        )
    } else {
        format!(
            "`{model}` is not in the model cache; \
             run `bz --list-models` to refresh or enable partial matching"
        )
    }
}