openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
//! Net-effect accounting for a would-have transform (D-02).
//!
//! The `tokens_net` formula is **derived once in the PRD** ("`tokens_net` — the
//! derivation") and is deliberately NOT restated here — restating it is what
//! produced the B-1 defect (a dropped `+0.1·S` term that moved break-even from
//! 11.5·S to 12.5·S). This module implements the derivation as **exact
//! fixed-point arithmetic** so the fire/skip test (`tokens_net > 0`, PRD: decided
//! on the UNROUNDED value) never depends on stored precision. Correctness is
//! pinned by the net-boundary tests at both TTLs, not by re-deriving the formula
//! in a comment.
//!
//! `W` (the write multiplier in force for the request) is **read from the
//! request's own `cache_control` TTL** — 1.25 at the 5-minute TTL, 2.0 at the
//! 1-hour TTL — never assumed. See [`write_multiplier_for`].

use serde_json::Value;

/// The write multiplier `W` in force for a request, read from its `cache_control`
/// TTL (never assumed — F-25 / PRD "read it from the request; never assume").
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WriteMultiplier {
    /// 5-minute ephemeral TTL → `W = 1.25`. The default when the request pins no
    /// explicit TTL (Anthropic's `cache_control` default is the 5-minute bucket).
    FiveMinute,
    /// 1-hour ephemeral TTL → `W = 2.0`.
    OneHour,
}

impl WriteMultiplier {
    /// `W × 100` as an integer (`125` or `200`). The scale factor for exact
    /// fixed-point net arithmetic — `0.1` and `(W − 0.1)` both become integers.
    pub fn hundredths(self) -> i128 {
        match self {
            WriteMultiplier::FiveMinute => 125,
            WriteMultiplier::OneHour => 200,
        }
    }

    /// `W` as the wire numeric (`1.25` or `2.0`) for
    /// `ai.openlatch.transform.write_multiplier`.
    pub fn value(self) -> f64 {
        match self {
            WriteMultiplier::FiveMinute => 1.25,
            WriteMultiplier::OneHour => 2.0,
        }
    }
}

/// `tokens_net` scaled to **hundredths** as an exact `i128`, so the fire/skip test
/// is `tokens_net_hundredths(..) > 0` on the unrounded value (PRD requirement).
///
/// Implements the PRD derivation `tokens_net = 0.1·T − (W − 0.1)·S`; multiplying
/// through by 100 keeps every coefficient integral:
/// `100·tokens_net = 10·T − (100·W − 10)·S`, and `100·W ∈ {125, 200}`.
///
/// `tokens_gross` is `|T|` (tokens removed); `retained_tail` is `|S|` (the shifted
/// tail that must be re-written). Widened to `i128` so a large `S` can legitimately
/// drive the net negative without overflow or an unsigned wrap.
pub fn tokens_net_hundredths(tokens_gross: u64, retained_tail: u64, w: WriteMultiplier) -> i128 {
    let ten_t = 10i128 * i128::from(tokens_gross);
    let coef = w.hundredths() - 10; // (100·W − 10): 115 at 5m, 190 at 1h
    ten_t - coef * i128::from(retained_tail)
}

/// Convert an hundredths-scaled net to the wire numeric (`hundredths / 100`).
/// Exact to two decimal places, so it stores losslessly into `numeric(18,4)`.
pub fn hundredths_to_numeric(hundredths: i128) -> f64 {
    hundredths as f64 / 100.0
}

/// Determine `W` from the request body's `cache_control` TTL.
///
/// Scans the parsed body for any `cache_control` breakpoint pinned to the 1-hour
/// TTL (`"ttl": "1h"`); a single 1-hour breakpoint puts the request on the 2.0
/// write rate. **Defaults to the 5-minute multiplier (1.25)** when no explicit
/// TTL is present — matching Anthropic's own `cache_control` default bucket. `W`
/// is therefore always read from the request, never assumed.
pub fn write_multiplier_for(body: &Value) -> WriteMultiplier {
    if has_one_hour_ttl(body) {
        WriteMultiplier::OneHour
    } else {
        WriteMultiplier::FiveMinute
    }
}

/// True when the request carries a `cache_control` breakpoint pinned to the 1-hour
/// ephemeral TTL **at a structural position Anthropic actually honors** — a `system`
/// content block, a `messages[].content[]` block, or a `tools[]` entry. A
/// `cache_control` object anywhere else (buried inside a `tool_use` block's `input`,
/// inside message text, or any other arbitrary nested object) is NOT a breakpoint and
/// is ignored. Inspecting only these positions is what keeps a stray `{"ttl":"1h"}` in
/// tool arguments from wrongly flipping `W` to 2.0.
fn has_one_hour_ttl(body: &Value) -> bool {
    // `system`: only an array of content blocks carries breakpoints; a plain string
    // `system` has no breakpoint position.
    if let Some(blocks) = body.get("system").and_then(Value::as_array) {
        if blocks.iter().any(block_has_one_hour_breakpoint) {
            return true;
        }
    }
    // `messages[].content[]`: each content block may carry a breakpoint. `content` may
    // be a plain string (no breakpoint) or an array of blocks. We inspect only each
    // block's OWN `cache_control` — never recursing into a `tool_use` block's `input`.
    if let Some(messages) = body.get("messages").and_then(Value::as_array) {
        for msg in messages {
            if let Some(blocks) = msg.get("content").and_then(Value::as_array) {
                if blocks.iter().any(block_has_one_hour_breakpoint) {
                    return true;
                }
            }
        }
    }
    // `tools[]`: a tool definition may carry a breakpoint.
    if let Some(tools) = body.get("tools").and_then(Value::as_array) {
        if tools.iter().any(block_has_one_hour_breakpoint) {
            return true;
        }
    }
    false
}

/// True when a block/entry's OWN `cache_control` field is a 1-hour ephemeral
/// breakpoint (`{"type":"ephemeral","ttl":"1h"}`). Reads only the top-level
/// `cache_control` of the passed value — it never recurses into nested payloads such
/// as a `tool_use` block's `input`, so a `cache_control`-shaped object living inside
/// tool arguments is correctly ignored. Requires BOTH `type == "ephemeral"` and
/// `ttl == "1h"`.
fn block_has_one_hour_breakpoint(block: &Value) -> bool {
    let Some(cc) = block.get("cache_control") else {
        return false;
    };
    cc.get("type").and_then(Value::as_str) == Some("ephemeral")
        && cc.get("ttl").and_then(Value::as_str) == Some("1h")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn net_break_even_5m_skips_below_and_at_fires_above() {
        // 5-minute TTL (W = 1.25): break-even is T = 11.5·S (PRD). With S = 2,
        // 11.5·S = 23, so T = 23 lands exactly on break-even (net = 0 → skip),
        // T = 22 is below (net < 0 → skip), T = 24 is above (net > 0 → fire).
        let w = WriteMultiplier::FiveMinute;
        assert_eq!(
            tokens_net_hundredths(23, 2, w),
            0,
            "T = 11.5·S is break-even"
        );
        assert!(tokens_net_hundredths(22, 2, w) < 0, "just below break-even");
        assert!(tokens_net_hundredths(24, 2, w) > 0, "just above break-even");
    }

    #[test]
    fn net_break_even_1h_skips_below_and_at_fires_above() {
        // 1-hour TTL (W = 2.0): break-even is T = 19·S (PRD). With S = 2,
        // 19·S = 38, so T = 38 is break-even (net = 0 → skip), 37 below, 39 above.
        let w = WriteMultiplier::OneHour;
        assert_eq!(tokens_net_hundredths(38, 2, w), 0, "T = 19·S is break-even");
        assert!(tokens_net_hundredths(37, 2, w) < 0, "just below break-even");
        assert!(tokens_net_hundredths(39, 2, w) > 0, "just above break-even");
    }

    #[test]
    fn net_hundredths_are_exact_to_two_places() {
        // 0.1·10 − 1.15·1 = 1.0 − 1.15 = −0.15 → −15 hundredths, exactly.
        assert_eq!(
            tokens_net_hundredths(10, 1, WriteMultiplier::FiveMinute),
            -15
        );
        assert!((hundredths_to_numeric(-15) - (-0.15)).abs() < 1e-12);
    }

    #[test]
    fn w_defaults_to_five_minute_when_no_ttl() {
        // No cache_control at all → 5-minute default.
        let body = serde_json::json!({"model":"claude-opus-4-8","messages":[]});
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::FiveMinute);

        // A cache_control breakpoint with no explicit ttl → still the 5m default.
        let ephemeral = serde_json::json!({
            "system":[{"type":"text","text":"x","cache_control":{"type":"ephemeral"}}],
            "messages":[]
        });
        assert_eq!(
            write_multiplier_for(&ephemeral),
            WriteMultiplier::FiveMinute
        );
    }

    #[test]
    fn w_reads_one_hour_ttl_from_the_request() {
        // An ephemeral 1-hour breakpoint on a `system` content block → the 2.0 write
        // rate. (A `system` string carries no breakpoint; a block does.)
        let body = serde_json::json!({
            "system":[{"type":"text","text":"x","cache_control":{"type":"ephemeral","ttl":"1h"}}],
            "messages":[{"role":"user","content":"hi"}]
        });
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::OneHour);
        assert_eq!(write_multiplier_for(&body).value(), 2.0);
    }

    #[test]
    fn w_ignores_ttl_1h_inside_message_text_and_tool_use_input() {
        // A `cache_control` with a 1h ttl buried inside message TEXT content, or inside
        // a `tool_use` block's `input` object, is NOT an Anthropic-honored breakpoint —
        // it must not flip W. Both cases must resolve to the 5-minute default (1.25).
        let text_ttl = serde_json::json!({
            "messages":[
                {"role":"user","content":[
                    {"type":"text","text":"cache this {\"cache_control\":{\"type\":\"ephemeral\",\"ttl\":\"1h\"}}"}
                ]}
            ]
        });
        assert_eq!(write_multiplier_for(&text_ttl), WriteMultiplier::FiveMinute);
        assert_eq!(write_multiplier_for(&text_ttl).value(), 1.25);

        let tool_input_ttl = serde_json::json!({
            "messages":[
                {"role":"assistant","content":[
                    {"type":"tool_use","id":"t1","name":"lookup","input":{
                        "cache_control":{"type":"ephemeral","ttl":"1h"}
                    }}
                ]}
            ]
        });
        assert_eq!(
            write_multiplier_for(&tool_input_ttl),
            WriteMultiplier::FiveMinute
        );
        assert_eq!(write_multiplier_for(&tool_input_ttl).value(), 1.25);
    }

    #[test]
    fn w_reads_one_hour_ttl_from_a_message_content_block() {
        // A real ephemeral 1h breakpoint carried on a `messages[].content[]` block →
        // the 2.0 write rate.
        let body = serde_json::json!({
            "messages":[
                {"role":"user","content":[
                    {"type":"text","text":"hi","cache_control":{"type":"ephemeral","ttl":"1h"}}
                ]}
            ]
        });
        assert_eq!(write_multiplier_for(&body), WriteMultiplier::OneHour);
        assert_eq!(write_multiplier_for(&body).value(), 2.0);
    }
}