Skip to main content

salvor_runtime/
model.rs

1//! The response-and-usage conversion a recorded model call writes.
2//!
3//! A [`ModelCallCompleted`](salvor_core::Event::ModelCallCompleted) event
4//! carries two derived values: the response as a JSON [`Value`] and the
5//! [`TokenUsage`] counted for the call. [`RunCtx`](crate::RunCtx) computes both
6//! at the live IO edge, and the durable log's byte form depends on exactly how
7//! it does so, so the conversion lives here as small public functions rather
8//! than inline in `ctx.rs`.
9//!
10//! # Why it is its own module
11//!
12//! The server-performed model step (`salvor-server`'s client-driven runs) has
13//! to record the *same* completion `RunCtx::model_call` records, because a run
14//! must replay identically whether the runtime drove the model call natively or
15//! the server performed it over the wire. Sharing this conversion, rather than
16//! reimplementing it in the server, is what keeps the two paths byte-identical.
17//! The functions are pure and side-effect-free: they read a
18//! [`MessageResponse`] and return a value, touching no store, clock, or
19//! provider.
20
21use salvor_core::TokenUsage;
22use salvor_llm::MessageResponse;
23use serde_json::{Value, json};
24
25/// Rebuilds the wire JSON of a response so the recorded value deserializes back
26/// into an equal [`MessageResponse`]. Built by hand because the response type
27/// is deserialize-only in `salvor-llm`.
28///
29/// This is the exact `response` field a `ModelCallCompleted` carries.
30#[must_use]
31pub fn response_value(response: &MessageResponse) -> Value {
32    json!({
33        "id": response.id,
34        "model": response.model,
35        "role": response.role,
36        "content": response.content,
37        "stop_reason": response.stop_reason,
38        "stop_sequence": response.stop_sequence,
39        "usage": {
40            "input_tokens": response.usage.input_tokens,
41            "output_tokens": response.usage.output_tokens,
42            "cache_creation_input_tokens": response.usage.cache_creation_input_tokens,
43            "cache_read_input_tokens": response.usage.cache_read_input_tokens,
44        },
45    })
46}
47
48/// Narrows a provider-reported token count to the event log's `u32`,
49/// saturating rather than failing on a count that cannot occur in practice.
50#[must_use]
51pub fn clamp_tokens(count: u64) -> u32 {
52    u32::try_from(count).unwrap_or(u32::MAX)
53}
54
55/// The [`TokenUsage`] a completion records for `response`: its input and output
56/// counts, each narrowed with [`clamp_tokens`]. This is the exact `usage` field
57/// a `ModelCallCompleted` carries.
58#[must_use]
59pub fn usage_of(response: &MessageResponse) -> TokenUsage {
60    TokenUsage {
61        input_tokens: clamp_tokens(response.usage.input_tokens),
62        output_tokens: clamp_tokens(response.usage.output_tokens),
63    }
64}