brazen 0.0.4

A stateless, swiss-army-knife adapter for every LLM provider and protocol.
Documentation
//! The `google_generative_ai` Protocol impl (providers §4). `encode` projects a
//! `CanonicalRequest` onto `POST {base_url}/v1beta/models/{model}:streamGenerateContent`
//! (the model rides the URL path; roles are `user`/`model`); `decode` is a pure
//! `(frame, &mut DecodeState)` state machine over the SSE stream of
//! `GenerateContentResponse` chunks. There is no block-start/stop on the wire, so
//! `MessageStart`/`ContentStart` are synthesized and the **last chunk's non-null
//! `finishReason`** is the native terminator. The `x-goog-api-key` auth header is
//! pure row DATA read by the shared `ApiKeyAuth` — no new `Auth` impl (§4.1). No IO,
//! no clock, no creds — `&GoogleGenAi` is `&'static dyn`.

mod decode;
mod encode;

use crate::canonical::{CanonicalError, CanonicalRequest, Event};
use crate::protocol::{
    CountRequest, DecodeState, Frame, Framing, ModelKeys, ModelsShape, Protocol, ProviderCtx,
    WireRequest,
};

/// The one shared, stateless instance (arch §4.4) — registered as `&'static dyn`.
pub struct GoogleGenAi;

impl Protocol for GoogleGenAi {
    fn encode(
        &self,
        req: &CanonicalRequest,
        ctx: &ProviderCtx,
    ) -> Result<WireRequest, CanonicalError> {
        encode::encode(req, ctx)
    }

    fn path(&self, ctx: &ProviderCtx) -> String {
        // `--raw` has no parsed `stream`; target the streaming endpoint (brazen's
        // native mode), the same path `encode` builds for a streaming request.
        encode::request_path(ctx, true)
    }

    fn content_type(&self) -> &str {
        "application/json"
    }

    fn decode(&self, frame: Frame, state: &mut DecodeState) -> Result<Vec<Event>, CanonicalError> {
        decode::decode(frame, state)
    }

    fn decode_full(
        &self,
        body: &[u8],
        state: &mut DecodeState,
    ) -> Result<Vec<Event>, CanonicalError> {
        decode::decode_full(body, state)
    }

    fn framing(&self) -> Framing {
        Framing::Sse
    }

    fn models_shape(&self) -> Option<ModelsShape> {
        // `models[].name`, stripping the leading `models/` so the id is usable in
        // encode's `/v1beta/models/{model}:…` path (§3.1). `strip` is protocol-only —
        // a row never overrides it (model-discovery §3). Google's models.list is the
        // richest source: it serves `inputTokenLimit`/`outputTokenLimit`/`displayName`,
        // so all three metadata facts are lifted here (§3, §3.1).
        Some(ModelsShape {
            path: "/v1beta/models",
            keys: ModelKeys {
                array_key: "models",
                id_key: "name",
                strip: "models/",
                context_key: "inputTokenLimit",
                max_output_key: "outputTokenLimit",
                display_name_key: "displayName",
            },
        })
    }

    fn count_tokens(
        &self,
        req: &CanonicalRequest,
        ctx: &ProviderCtx,
    ) -> Option<Result<CountRequest, CanonicalError>> {
        // `POST …/models/{model}:countTokens`, response `{"totalTokens": N}` (§10.1).
        Some(encode::count_body(req, ctx).map(|wire| CountRequest {
            wire,
            token_key: "totalTokens",
        }))
    }
}