connector-usage-only: delete the dollar authorities (WIP, does not yet build)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 04:43:18 +01:00
commit 06a47bb13f
39 changed files with 1243 additions and 6010 deletions

2
.gitattributes vendored
View file

@ -1,2 +1,2 @@
schemas/*.json text eol=lf
crates/model-connector/tests/fixtures/wire_v3/* -text
crates/model-connector/tests/fixtures/wire_v4/* -text

View file

@ -21,11 +21,11 @@ Crate `crates/model-connector`:
- `src/envelope.rs` - the AES-256-GCM `TransportEnvelope` (binary ciphertext), its
identity-binding AAD, the length-prefixed framing, and the CultNet carriage: each frame is a
`cultnet.operation_request.v0` / `operation_response.v0` for service
`gamecult.model.connector`, operation `invoke` (or `spend_readout`), its `message_id` the
request id and its payload the base64 of the sealed envelope. `spend_readout` answers a
`SpendReadout` (`gamecult.model.spend_readout.v1`): each caller's spent and reserved
`gamecult.model.connector`, operation `invoke` (or `usage_readout`), its `message_id` the
request id and its payload the base64 of the sealed envelope. `usage_readout` answers a
`UsageReadout` (`gamecult.model.usage_readout.v1`): each caller's spent and reserved
micro-USD against its ceiling in its current window, and the backend's plan quota;
`ConnectorClient::spend_readout` asks.
`ConnectorClient::usage_readout` asks.
- `schemas/` - one JSON Schema per v3 document, generated from the types.
- `src/client.rs` - `ConnectorClient`, the one-request caller side.
- `src/service.rs` (feature `daemon`) - `TransportService`: caller admission, durable

View file

@ -25,3 +25,6 @@ zeroize = "1"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[dev-dependencies]
rmpv = "1"

View file

@ -5,8 +5,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use model_connector::{
CallerAdmission, ModelPrice, PriceTable, Rates, ReasoningEffort, SpendCeiling, SpendWindowMs,
StopReason, ToolDefinition, TransportAdmission, TransportDisposition, TransportInvocation,
CallerAdmission, PlanQuotaSnapshot, ReasoningEffort, StopReason, ToolDefinition, TransportAdmission, TransportDisposition, TransportInvocation,
TransportKey, TransportReceipt, TransportService, decrypt_result, encrypt_invocation,
};
@ -460,25 +459,6 @@ struct Door {
service: TransportService,
}
fn prices() -> PriceTable {
PriceTable::from([MODEL, OTHER_MODEL].map(|model| {
(
model.to_string(),
ModelPrice {
rates: Rates {
input: 3_000_000,
cache_write_short: 3_750_000,
cache_write_long: 6_000_000,
cache_read: 300_000,
output: 15_000_000,
},
long_context: None,
overhead_tokens: 100,
},
)
}))
}
impl Door {
fn new() -> Self {
let dir = std::env::temp_dir().join(format!(
@ -496,13 +476,11 @@ impl Door {
4,
FRAME / 2,
32_768,
SpendCeiling(1 << 40),
SpendWindowMs(3_600_000),
)
.unwrap()
});
let service =
TransportService::open(&dir.join("replay.cc"), PROVIDER, prices(), callers, 5_000)
TransportService::open(&dir.join("replay.cc"), PROVIDER, callers, 5_000)
.unwrap();
Self { dir, service }
}
@ -521,9 +499,12 @@ impl Door {
) -> (model_connector::ExecutionClaim, TransportInvocation) {
let key = TransportKey::from_connection_secret(&format!("{caller}-key")).unwrap();
let invocation =
TransportInvocation::new(caller, 2_000, [7; 32], 10_000_000, request).unwrap();
TransportInvocation::new(caller, 2_000, [7; 32], request).unwrap();
let envelope = encrypt_invocation(&invocation, &key).unwrap();
match self.service.begin(&envelope, 1_000, FRAME).unwrap() {
match self
.service
.begin(&envelope, 1_000, &PlanQuotaSnapshot::default(), FRAME)
.unwrap() {
TransportAdmission::Execute(claim) => (*claim, invocation),
TransportAdmission::Reply(_) => panic!("the door answered instead of admitting"),
}
@ -1458,11 +1439,6 @@ fn usage_sums_model_usage_across_models() {
};
assert_eq!(*summed, usage(12, 37, 1083, 24, Some(8)));
// The receipt is priced from usage alone: 12*3 + 37*0.3 + 1083*6 + 24*15 micro-USD, rounded up.
assert_eq!(parts(&cheap).1.cost_micro_usd, 36 + 12 + 6498 + 360);
assert_eq!(
parts(&cheap).1.cost_micro_usd,
parts(&dear).1.cost_micro_usd
);
// No model reported thinking tokens: none is claimed.
let plain = r#"{"claude-haiku-5-5":{"inputTokens":1,"outputTokens":1,"cacheReadInputTokens":0,"cacheCreationInputTokens":0}}"#;
@ -1599,7 +1575,6 @@ fn no_result_event_is_a_failure_with_unknown_usage() {
};
assert_eq!(zero_billed, Some(usage_zero()));
let mut costs = Vec::new();
for (script, expected) in ends {
let rig = rig(vec![script]);
let result = rig.run(request(vec![user("hi")]));
@ -1607,20 +1582,7 @@ fn no_result_event_is_a_failure_with_unknown_usage() {
assert_eq!(kind, expected);
assert_eq!(usage, None, "{message}");
assert!(result.transcript_items().is_empty());
costs.push(parts(&result).1.cost_micro_usd);
}
// Unknown usage is charged the reservation, which is far above a typical turn.
assert!(costs.iter().all(|cost| *cost >= 15_000), "{costs:?}");
assert_eq!(
parts(&rig(vec![]).run({
let mut bad = request(vec![user("a")]);
bad.tools.push(a_tool());
bad
}))
.1
.cost_micro_usd,
0
);
}
#[test]
@ -1719,7 +1681,6 @@ fn a_port_that_cannot_spawn_is_a_provider_failure() {
Some(usage_zero()),
"a child that never ran bills nothing"
);
assert_eq!(parts(&result).1.cost_micro_usd, 0);
assert!(
!message.contains(dir.path().to_str().unwrap()),
"no path in a message"
@ -1743,8 +1704,6 @@ fn backend_end_to_end_through_the_service() {
assert_eq!(receipt.provider, "claude-code");
assert_eq!(receipt.model, MODEL);
// USAGE priced at the table: 2*3 + 7*0.3 + 1043*6 + 4*15 micro-USD, rounded up.
assert_eq!(receipt.cost_micro_usd, 6 + 3 + 6258 + 60);
assert_eq!(receipt.overrun_micro_usd, 0);
assert_eq!(events.len(), 2);
let items = opened.transcript_items();
assert_eq!(items[0], assistant("hello back"));
@ -1757,13 +1716,6 @@ fn backend_end_to_end_through_the_service() {
const USAGE_TURN_2: &str = r#"{"claude-haiku-5-5":{"inputTokens":3,"outputTokens":5,"cacheReadInputTokens":1100,"cacheCreationInputTokens":9}}"#;
/// What a call whose usage is unknown is billed: the kit's reservation.
fn reservation() -> u64 {
let result = rig(vec![Script::lines(vec![])]).run(request(vec![user("hi")]));
assert_eq!(failure_of(&result).2, None);
parts(&result).1.cost_micro_usd
}
#[test]
fn a_resumed_turn_is_charged_only_its_own_usage() {
let rig = rig(vec![
@ -1788,13 +1740,10 @@ fn a_resumed_turn_is_charged_only_its_own_usage() {
};
assert_eq!(*billed, usage(3, 1100, 9, 5, None));
// 3*3 + 1100*0.3 + 9*6 + 5*15 micro-USD.
assert_eq!(parts(&second).1.cost_micro_usd, 9 + 330 + 54 + 75);
}
#[test]
fn usage_the_result_does_not_report_is_billed_the_reservation() {
let reservation = reservation();
assert!(reservation >= 15_000, "{reservation}");
fn usage_the_result_does_not_report_stays_unknown() {
let success = |fields: &str| {
format!(
r#"{{"type":"result","subtype":"success","is_error":false,"stop_reason":"end_turn","modelUsage":{CUMULATIVE}{fields}}}"#
@ -1838,7 +1787,6 @@ fn usage_the_result_does_not_report_is_billed_the_reservation() {
let (kind, _, usage) = failure_of(&result);
assert_eq!(kind, expected, "{line}");
assert_eq!(usage, None, "{line}");
assert_eq!(parts(&result).1.cost_micro_usd, reservation, "{line}");
}
for line in cases {
let result = rig(vec![Script::lines(vec![
@ -1850,7 +1798,6 @@ fn usage_the_result_does_not_report_is_billed_the_reservation() {
let (kind, _, usage) = failure_of(&result);
assert_eq!(kind, FailureKind::Provider, "{line}");
assert_eq!(usage, None, "{line}");
assert_eq!(parts(&result).1.cost_micro_usd, reservation, "{line}");
assert!(result.transcript_items().is_empty(), "{line}");
}
}
@ -1865,7 +1812,6 @@ fn door_set_is_exactly_401_403_429() {
// with a bogus token.
let retry = r#"{"type":"system","subtype":"api_retry","error":"authentication_failed"}"#;
let synthetic = r#"{"type":"assistant","api_error_status":401,"error":"authentication_failed","message":{"id":"e48001b7-cf4f-49b1-9810-db8fd7f73c73","model":"<synthetic>","stop_reason":"stop_sequence","content":[{"type":"text","text":"Failed to authenticate. API Error: 401 OAuth access token is invalid."}]}}"#;
let reservation = reservation();
let statuses: [Option<u16>; 9] = [
Some(400),
Some(401),
@ -1904,10 +1850,8 @@ fn door_set_is_exactly_401_403_429() {
let case = format!("{status:?} {shape}");
if matches!(status, Some(401 | 403 | 429)) && shape == "empty" {
assert_eq!(usage, Some(usage_zero()), "{case}");
assert_eq!(parts(&result).1.cost_micro_usd, 0, "{case}");
} else {
assert_eq!(usage, None, "{case}");
assert_eq!(parts(&result).1.cost_micro_usd, reservation, "{case}");
}
}
}
@ -1979,7 +1923,6 @@ fn prompt_file_lives_in_prompt_dir_exclusively() {
let result = rig.run(request(vec![user("hi")]));
let (kind, _, usage) = failure_of(&result);
assert_eq!((kind, usage), (FailureKind::Provider, Some(usage_zero())));
assert_eq!(parts(&result).1.cost_micro_usd, 0);
assert_eq!(rig.spawns(), 0);
}
@ -1996,11 +1939,6 @@ fn pin_is_checked_before_every_spawn() {
let result = rig.run(request(vec![user("hi")]));
let (kind, message, usage) = failure_of(&result);
assert_eq!((kind, usage), (FailureKind::Provider, Some(usage_zero())));
assert_eq!(
parts(&result).1.cost_micro_usd,
0,
"billed nothing, not the reservation"
);
assert!(!message.contains(rig.dir.path().to_str().unwrap()));
assert_eq!(rig.spawns(), 1, "no child for a binary off its pin");
assert!(!rig.backend.binary_matches_pin());
@ -2695,7 +2633,6 @@ fn not_logged_in_line(status: &str, api_ms: u64, extra: &str) -> String {
#[test]
fn a_result_that_made_no_api_call_bills_nothing() {
let reservation = reservation();
let empty = r#","model_usage_since_last_result":{}"#;
let cases = [
// (status field, api ms, usage field, billed nothing)
@ -2723,10 +2660,8 @@ fn a_result_that_made_no_api_call_bills_nothing() {
let (_, _, usage) = failure_of(&result);
if free {
assert_eq!(usage, Some(usage_zero()), "{line}");
assert_eq!(parts(&result).1.cost_micro_usd, 0, "{line}");
} else {
assert_eq!(usage, None, "{line}");
assert_eq!(parts(&result).1.cost_micro_usd, reservation, "{line}");
}
}
}

View file

@ -3,7 +3,7 @@
//! run with; nothing in the connector's environment, flags or JSON can change
//! them. Validation errors name a field and never echo a value.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::net::{SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
@ -11,7 +11,7 @@ use std::time::Duration;
use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore};
use model_connector::{
CallerAdmission, CallerConfig, LeaseTurn, MIN_PAYLOAD_BYTES, ModelPrice, ProviderBackend,
CallerAdmission, CallerConfig, LeaseTurn, MIN_PAYLOAD_BYTES, ProviderBackend,
ServeError, ServeLimits, ServiceError, TransportService, load_admissions, serve_service,
};
use serde::{Deserialize, Serialize};
@ -31,8 +31,8 @@ const MODEL_PREFIX: &str = "claude-";
/// A unix account that may lease the subscription token for a Self run or a
/// Persona turn, the unit its process must run in (`mind-self@<unit_instance>`
/// or `mind-persona@<unit_instance>`, by `turn`), the configured caller whose
/// spend window its leases reserve in, and what each lease reserves there.
/// or `mind-persona@<unit_instance>`, by `turn`), and the configured caller
/// whose name its leases carry.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseCallerConfig {
pub unix_user: String,
@ -41,11 +41,10 @@ pub struct LeaseCallerConfig {
/// or `mind-persona@`, its process must run in.
pub turn: LeaseTurn,
pub unit_instance: String,
pub reservation_micro_usd: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "gamecult.model_connector.claude_connector_config.v1")]
#[cultcache(type = "gamecult.model_connector.claude_connector_config.v2")]
pub struct ClaudeConnectorConfig {
/// A loopback socket address.
#[cultcache(key = 0)]
@ -86,14 +85,10 @@ pub struct ClaudeConnectorConfig {
#[cultcache(key = 16)]
pub max_expiry_skew_ms: u64,
/// Every caller of this connector: its own connection key and epoch, models,
/// concurrency, payload and output bounds, and spend ceiling and window.
/// concurrency, payload and output bounds.
/// Ids are unique. Handles are bound to the caller that was given them.
#[cultcache(key = 17)]
pub callers: Vec<CallerConfig>,
/// What each allowed model costs. The keys are the known list of models:
/// ids exactly as the CLI reports them in its init event.
#[cultcache(key = 18)]
pub prices: BTreeMap<String, ModelPrice>,
/// Where the Self-run lease socket is bound (a systemd RuntimeDirectory),
/// or none when this connector lends no token.
#[cultcache(key = 19)]
@ -143,7 +138,6 @@ impl From<ServeError> for ConfigError {
ServeError::ConnectionKey(_) => Self::Unusable("connection_key_file"),
ServeError::Service(error) => match error {
ServiceError::ReplayStore(_) => Self::Unusable("replay_store"),
ServiceError::UnpricedModel(_) => Self::Invalid("prices"),
ServiceError::InvalidAdmission
| ServiceError::DuplicateCaller
| ServiceError::SharedCallerKey => Self::Invalid("callers"),
@ -228,8 +222,6 @@ impl ClaudeConnectorConfig {
if caller.max_concurrent_requests == 0
|| caller.max_payload_bytes < MIN_PAYLOAD_BYTES
|| caller.max_output_tokens == 0
|| caller.spend_ceiling_micro_usd == 0
|| caller.spend_window_ms == 0
{
return Err(ConfigError::Invalid("caller bounds"));
}
@ -240,19 +232,11 @@ impl ClaudeConnectorConfig {
for model in &caller.allowed_models {
if !has_model_id_form(model)
|| !seen.insert(model.as_str())
|| !self.prices.contains_key(model)
{
return Err(ConfigError::Invalid("allowed_models"));
}
}
}
if !self
.prices
.iter()
.all(|(model, price)| has_model_id_form(model) && is_fully_priced(price))
{
return Err(ConfigError::Invalid("prices"));
}
self.validate_leases()?;
Ok(listen)
}
@ -278,11 +262,7 @@ impl ClaudeConnectorConfig {
&& self
.callers
.iter()
.find(|caller| caller.caller_runtime_id == lease.caller_id)
.is_some_and(|caller| {
(1..=caller.spend_ceiling_micro_usd)
.contains(&lease.reservation_micro_usd)
})
.any(|caller| caller.caller_runtime_id == lease.caller_id)
&& is_unit_instance(&lease.unit_instance)
&& users.insert(lease.unix_user.as_str())
})
@ -336,8 +316,7 @@ fn is_trimmed_nonempty(text: &str) -> bool {
/// The loose shape of a model id, only to catch typos:
/// `claude-<family>-<major>`, then an optional `-<minor>` and an optional
/// `-YYYYMMDD`, the family lowercase letters. No shape can tell an alias from
/// a canonical id, so the known list is the price table's keys (ruling
/// claude-model-ids-from-price-table); an alias that slips in fails its first
/// a canonical id, so the known list is `allowed_models` itself; an alias that slips in fails its first
/// call, when the stream's `init.model` differs from the request.
fn has_model_id_form(model: &str) -> bool {
let Some(rest) = model.strip_prefix(MODEL_PREFIX) else {
@ -365,25 +344,6 @@ fn has_model_id_form(model: &str) -> bool {
&& after_ok
}
/// A price is a fuse only if every component it bills is nonzero: a zero rate
/// lets a call cost nothing against the caller's ceiling.
fn is_fully_priced(price: &ModelPrice) -> bool {
let long = price.long_context.map(|long| long.rates);
[Some(price.rates), long]
.into_iter()
.flatten()
.all(|rates| {
[
rates.input,
rates.cache_write_short,
rates.cache_write_long,
rates.cache_read,
rates.output,
]
.iter()
.all(|rate| *rate > 0)
})
}
/// One file name under `$CREDENTIALS_DIRECTORY`, never a path.
fn is_credential_name(name: &str) -> bool {
@ -509,7 +469,6 @@ fn lease_callers_of(config: &ClaudeConnectorConfig) -> Result<Vec<LeaseCaller>,
caller_id: lease.caller_id.clone(),
turn: lease.turn,
unit_instance: lease.unit_instance.clone(),
reservation_micro_usd: lease.reservation_micro_usd,
})
.ok_or(ConfigError::Unusable("lease_callers"))
})
@ -531,6 +490,7 @@ fn start_leases(
config: &ClaudeConnectorConfig,
socket: &Path,
service: Arc<Mutex<TransportService>>,
quota: Arc<crate::QuotaCell>,
) -> Result<(), ConfigError> {
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::os::unix::net::UnixListener;
@ -559,6 +519,7 @@ fn start_leases(
callers,
limits,
service,
quota,
tokens,
Arc::new(SystemdUnits::system()),
);
@ -589,7 +550,6 @@ pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
TransportService::open(
&config.replay_store,
backend.provider(),
config.prices.clone(),
admissions,
config.max_expiry_skew_ms,
)
@ -597,7 +557,7 @@ pub fn serve_config(path: &Path) -> Result<(), ConfigError> {
));
#[cfg(target_os = "linux")]
if let Some(socket) = &config.lease_socket {
start_leases(&config, socket, service.clone())?;
start_leases(&config, socket, service.clone(), backend.quota())?;
}
serve_service(listener, config.serve_limits(), service, backend)?;
Ok(())
@ -643,10 +603,6 @@ mod tests {
ServeError::Service(ServiceError::ReplayStore(format!("{CANARY}/replay.cc"))),
"replay_store",
),
(
ServeError::Service(ServiceError::UnpricedModel(CANARY.to_string())),
"prices",
),
(
ServeError::Service(ServiceError::InvalidAdmission),
"callers",

View file

@ -1,15 +1,14 @@
//! The Self-run credential lease. The connector is the only holder of the
//! subscription token and the one owner of Self-run admission, reservation and
//! the spend ledger (rulings self-runs-through-connector,
//! self-run-credential-lease and self-run-lease-billing-and-binding). A Self
//! run asks over a unix socket for a lease. The connector decides who may ask
//! (the peer's uid, and the systemd unit invocation its process runs in),
//! reserves its own configured amount in the caller's spend window, and only
//! then hands back one pipe fd that holds the token. The run settles with the
//! per-model usage it reports, and the connector bills that at true cost,
//! uncapped; a run that vanishes or lets its lease expire is billed the whole
//! reservation. Nothing the run says about itself decides who it is, what it
//! reserves, or what the plan allows.
//! subscription token and the one owner of Self-run admission (rulings
//! self-runs-through-connector and self-run-credential-lease). A Self run asks
//! over a unix socket for a lease. The connector decides who may ask (the
//! peer's uid, and the systemd unit invocation its process runs in), and hands
//! back one pipe fd that holds the token, with the plan's usage windows. The
//! lease lasts while the holder keeps the socket open: it ends when the holder
//! closes it, when its unit invocation ends, or when its expiry passes. Nothing
//! is reserved and nothing is billed; the only usage-derived refusal is the
//! provider's own, repeated until its own reset. Nothing the run says about
//! itself decides who it is or what the plan allows.
//!
//! The token exists in this process only as the value the token source returns
//! and in the pipe. It is not in an environment, an argument, a file, a log or
@ -28,12 +27,11 @@ use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use model_connector::{
LeaseAdmission, LeaseGrant, LeaseRefusal, LeaseSettlement, LeaseTurn, LeaseUse, ModelUsage,
TransportService,
LeaseAdmission, LeaseGrant, LeaseRefusal, LeaseTurn, PlanQuota, TransportService,
};
use serde::{Deserialize, Serialize};
use crate::TokenSource;
use crate::{QuotaCell, TokenSource};
/// The largest frame either side sends or accepts, in bytes. It is wire
/// contract: a Huginn client sizes against it, so it is pinned as a literal.
@ -50,12 +48,9 @@ pub enum LeaseMessage {
/// holds the token, its write end closed.
ClaudeLeaseGrant(LeaseGranted),
ClaudeLeaseRefusal(LeaseRefused),
ClaudeLeaseSettle(LeaseSettle),
ClaudeLeaseSettled(LeaseSettled),
}
/// A request names only when the lease ends. Who asks, and what is reserved,
/// are the connector's to know.
/// A request names only when the lease ends. Who asks is the connector's to know.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseRequest {
pub expires_at_unix_ms: u64,
@ -64,16 +59,17 @@ pub struct LeaseRequest {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseGranted {
pub lease_id: String,
pub reserved_micro_usd: u64,
/// The plan's usage windows as the connector last saw them, so the run
/// can read its own burn.
pub plan_quota: Vec<PlanQuota>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LeaseRefusalKind {
SpendCeiling,
CostBound,
UnknownCaller,
Expired,
Busy,
ProviderRejected,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -82,20 +78,6 @@ pub struct LeaseRefused {
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseSettle {
pub lease_id: String,
/// What the run used of each model, as the run reports it.
pub usage: Vec<ModelUsage>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LeaseSettled {
pub billed_micro_usd: u64,
/// How far the bill passed the reservation; zero when it did not.
pub overrun_micro_usd: u64,
}
/// A frame: the length, then the named-map MessagePack of `message`.
pub fn encode_frame(message: &LeaseMessage) -> Vec<u8> {
// Every field of the message is plain data, so encoding cannot fail.
@ -180,8 +162,8 @@ fn read_message(stream: &mut impl Bounded, deadline: Instant) -> io::Result<Leas
const PIPE_BUF: usize = 4096;
/// A unix account allowed to lease, the unit its process must run in (the
/// Self or the Persona unit of its instance, by `turn`), and the caller whose
/// spend window its leases reserve in.
/// Self or the Persona unit of its instance, by `turn`), and the
/// caller whose name its leases carry.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeaseCaller {
pub uid: u32,
@ -191,8 +173,6 @@ pub struct LeaseCaller {
/// The `<instance>` of `mind-self@<instance>.service` or
/// `mind-persona@<instance>.service`.
pub unit_instance: String,
/// What each of this caller's leases reserves, whatever the run asks.
pub reservation_micro_usd: u64,
}
impl LeaseCaller {
@ -400,6 +380,7 @@ struct Lender {
callers: Vec<LeaseCaller>,
limits: LeaseLimits,
service: Arc<Mutex<TransportService>>,
quota: Arc<QuotaCell>,
tokens: Arc<dyn TokenSource>,
units: Arc<dyn PeerUnits>,
}
@ -415,6 +396,7 @@ pub fn serve_leases(
callers: Vec<LeaseCaller>,
limits: LeaseLimits,
service: Arc<Mutex<TransportService>>,
quota: Arc<QuotaCell>,
tokens: Arc<dyn TokenSource>,
units: Arc<dyn PeerUnits>,
) -> io::Result<()> {
@ -422,6 +404,7 @@ pub fn serve_leases(
callers,
limits,
service,
quota,
tokens,
units,
});
@ -484,9 +467,9 @@ impl Lender {
"the expiry is not in the future or is past the lease bound",
);
}
// The reservation is claimed in the ledger before the token is read,
// let alone lent. A ledger that fails closes the connection with no
// answer and no lease.
// The lease is claimed before the token is read, let alone lent. The
// plan the refusal reads is the one this process last observed.
let plan = self.quota.snapshot();
let admission = self
.service
.lock()
@ -495,120 +478,134 @@ impl Lender {
&caller.caller_id,
caller.turn,
&invocation.invocation_id,
caller.reservation_micro_usd,
request.expires_at_unix_ms,
now,
&plan,
)
.map_err(|_| io::Error::other("ledger"))?;
.map_err(|_| io::Error::other("lease"))?;
let grant = match admission {
LeaseAdmission::Granted(grant) => grant,
LeaseAdmission::Refused(why) => {
let (kind, detail) = match why {
LeaseRefusal::SpendCeiling => (
LeaseRefusalKind::SpendCeiling,
"the window cannot hold this reservation",
),
LeaseRefusal::CostBound => (
LeaseRefusalKind::CostBound,
"the reservation must be above zero",
),
LeaseRefusal::UnknownCaller => (
LeaseRefusalKind::UnknownCaller,
"this account is bound to no admitted caller",
),
LeaseRefusal::Busy => (
LeaseRefusalKind::Busy,
"a lease is open for this caller or this unit invocation leased",
"a lease is open for this caller or this unit invocation",
),
LeaseRefusal::ProviderRejected => (
LeaseRefusalKind::ProviderRejected,
"the provider is refusing until its own reset",
),
};
return refuse(&mut stream, kind, detail);
}
};
self.hold(stream, caller, &grant, request.expires_at_unix_ms)
self.hold(
stream,
caller,
&grant,
&invocation,
peer.process,
request.expires_at_unix_ms,
plan.windows,
)
}
/// Everything after a reservation exists: lend the token, then settle the
/// lease exactly once whatever happens.
/// Everything after a lease exists: lend the token, then keep the lease
/// while its holder keeps the socket and its unit invocation, and end it
/// exactly once whatever happens.
// One argument per fact the held lease binds.
#[allow(clippy::too_many_arguments)]
fn hold(
&self,
mut stream: UnixStream,
caller: &LeaseCaller,
grant: &LeaseGrant,
invocation: &PeerUnit,
process: Option<OwnedFd>,
expires_at_unix_ms: u64,
plan_quota: Vec<PlanQuota>,
) -> io::Result<()> {
// A token that cannot be read or piped was never lent: nothing was
// used, so nothing is billed.
let held = self.keep(
&mut stream,
caller,
grant,
invocation,
process.as_ref(),
expires_at_unix_ms,
plan_quota,
);
self.service
.lock()
.map_err(|_| io::Error::other("service lock"))?
.end_lease(&caller.caller_id, &grant.lease_id);
held
}
// One argument per fact the held lease binds.
#[allow(clippy::too_many_arguments)]
fn keep(
&self,
stream: &mut UnixStream,
caller: &LeaseCaller,
grant: &LeaseGrant,
invocation: &PeerUnit,
process: Option<&OwnedFd>,
expires_at_unix_ms: u64,
plan_quota: Vec<PlanQuota>,
) -> io::Result<()> {
// A token that cannot be read or piped was never lent: the lease ends.
let Some(read_end) = self
.tokens
.read()
.and_then(|token| token_pipe(token.as_bytes()).ok())
else {
self.settle(caller, grant, LeaseUse::NeverLent)?;
return Ok(());
};
let frame = encode_frame(&LeaseMessage::ClaudeLeaseGrant(LeaseGranted {
lease_id: grant.lease_id.clone(),
reserved_micro_usd: grant.reserved_micro_usd,
plan_quota,
}));
let sent = send_with_fd(&stream, &frame, read_end.as_raw_fd());
let sent = send_with_fd(stream, &frame, read_end.as_raw_fd());
drop(read_end);
if sent.is_err() {
// The descriptor may have been delivered: the reservation is billed.
self.settle(caller, grant, LeaseUse::Reservation)?;
return Ok(());
}
let outcome = self.await_settle(&mut stream, grant, expires_at_unix_ms);
match outcome {
Some(settle) => {
let settlement = self.settle(caller, grant, LeaseUse::Reported(&settle.usage))?;
let _ = stream.write_all(&encode_frame(&LeaseMessage::ClaudeLeaseSettled(
LeaseSettled {
billed_micro_usd: settlement.billed_micro_usd,
overrun_micro_usd: settlement.overrun_micro_usd,
},
)));
sent?;
// The holder says nothing more. The lease ends when it closes the
// socket, when its unit invocation is no longer the one that leased,
// or at the expiry.
let mut byte = [0_u8; 1];
loop {
let left = expires_at_unix_ms.saturating_sub(unix_ms());
if left == 0 {
return Ok(());
}
None => {
self.settle(caller, grant, LeaseUse::Reservation)?;
stream.set_read_timeout(Some(Duration::from_millis(left.min(HOLD_POLL_MS))))?;
match stream.read(&mut byte) {
Ok(0) => return Ok(()),
Ok(_) => {}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) => {}
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(_) => return Ok(()),
}
let still = process
.and_then(|process| self.units.invocation_in(process.as_fd(), &caller.unit()))
.is_some_and(|found| found == *invocation);
if !still {
return Ok(());
}
}
Ok(())
}
/// The run's settle, if its whole frame arrives for this lease before the
/// lease expires. Anything else (end of file, silence, a frame still
/// trickling at the expiry, a malformed frame, the wrong lease) is no
/// report.
fn await_settle(
&self,
stream: &mut UnixStream,
grant: &LeaseGrant,
expires_at_unix_ms: u64,
) -> Option<LeaseSettle> {
let remaining = expires_at_unix_ms.saturating_sub(unix_ms());
let deadline = Instant::now() + Duration::from_millis(remaining);
match read_message(stream, deadline).ok()? {
LeaseMessage::ClaudeLeaseSettle(settle) if settle.lease_id == grant.lease_id => {
Some(settle)
}
_ => None,
}
}
fn settle(
&self,
caller: &LeaseCaller,
grant: &LeaseGrant,
usage: LeaseUse<'_>,
) -> io::Result<LeaseSettlement> {
self.service
.lock()
.map_err(|_| io::Error::other("service lock"))?
.settle_lease(&caller.caller_id, &grant.lease_id, usage)
.map_err(|_| io::Error::other("ledger"))
}
}
/// How often a held lease looks again at its holder's unit invocation.
const HOLD_POLL_MS: u64 = 500;
fn refuse(stream: &mut UnixStream, kind: LeaseRefusalKind, detail: &str) -> io::Result<()> {
stream.write_all(&encode_frame(&LeaseMessage::ClaudeLeaseRefusal(
LeaseRefused {
@ -622,12 +619,12 @@ fn refuse(stream: &mut UnixStream, kind: LeaseRefusalKind, detail: &str) -> io::
mod tests {
use super::*;
/// A settle frame whose body is exactly `body_bytes` long.
fn settle_frame_of(body_bytes: usize) -> Vec<u8> {
/// A refusal frame whose body is exactly `body_bytes` long.
fn frame_of(body_bytes: usize) -> Vec<u8> {
let frame_with = |pad: usize| {
encode_frame(&LeaseMessage::ClaudeLeaseSettle(LeaseSettle {
lease_id: "x".repeat(pad),
usage: Vec::new(),
encode_frame(&LeaseMessage::ClaudeLeaseRefusal(LeaseRefused {
kind: LeaseRefusalKind::Busy,
detail: "x".repeat(pad),
}))
};
// The string header is the same width between 256 and 65535 bytes.
@ -646,10 +643,10 @@ mod tests {
#[test]
fn a_frame_is_one_whole_message_within_the_bound() {
let at_bound = settle_frame_of(MAX_LEASE_FRAME_BYTES);
let at_bound = frame_of(MAX_LEASE_FRAME_BYTES);
assert_eq!(at_bound.len(), 4 + MAX_LEASE_FRAME_BYTES);
assert!(decode_frame(&at_bound).is_some());
let past_bound = settle_frame_of(MAX_LEASE_FRAME_BYTES + 1);
let past_bound = frame_of(MAX_LEASE_FRAME_BYTES + 1);
assert!(decode_frame(&past_bound).is_none());
let mut trailing = at_bound.clone();
trailing.push(0);
@ -661,9 +658,9 @@ mod tests {
#[test]
fn a_stream_frame_over_the_bound_is_refused_before_it_is_read() {
let past_bound = settle_frame_of(MAX_LEASE_FRAME_BYTES + 1);
let past_bound = frame_of(MAX_LEASE_FRAME_BYTES + 1);
assert!(within(&past_bound).is_err());
let at_bound = settle_frame_of(MAX_LEASE_FRAME_BYTES);
let at_bound = frame_of(MAX_LEASE_FRAME_BYTES);
assert!(within(&at_bound).is_ok());
assert!(within(&[0_u8, 0, 0, 0]).is_err());
// Short of the length it announced is end of file, not a message.
@ -673,10 +670,10 @@ mod tests {
#[test]
fn a_frame_that_trickles_past_its_deadline_is_refused_however_small_each_read() {
let (mut reader, mut writer) = UnixStream::pair().unwrap();
let frame = encode_frame(&LeaseMessage::ClaudeLeaseSettle(LeaseSettle {
lease_id: "lease:x".to_string(),
usage: Vec::new(),
}));
let frame = encode_frame(&LeaseMessage::ClaudeLeaseRefusal(LeaseRefused {
kind: LeaseRefusalKind::Busy,
detail: "lease:x".to_string(),
}));
let trickle = thread::spawn(move || {
for byte in frame {
if writer.write_all(&[byte]).is_err() {

View file

@ -28,7 +28,7 @@ pub use launch::ChildEnv;
#[cfg(target_os = "linux")]
pub use lease::{
LeaseCaller, LeaseGranted, LeaseLimits, LeaseMessage, LeaseRefusalKind, LeaseRefused,
LeaseRequest, LeaseSettle, LeaseSettled, MAX_LEASE_FRAME_BYTES, PeerCredentials, PeerUnit,
LeaseRequest, MAX_LEASE_FRAME_BYTES, PeerCredentials, PeerUnit,
PeerUnits, decode_frame, encode_frame, peer_credentials, serve_leases, user_uid,
};
#[cfg(unix)]

View file

@ -18,7 +18,7 @@ use claude_connector::{
ClaudeConnectorConfig, ConfigError, LeaseCallerConfig, MIN_SECRET_BYTES, check_ready,
load_config, write_config,
};
use model_connector::{CallerConfig, LeaseTurn, ModelPrice, Rates, frame_bytes_for_payload};
use model_connector::{CallerConfig, LeaseTurn, frame_bytes_for_payload};
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
@ -72,7 +72,6 @@ fn check(path: &str) -> Result<(), Failure> {
println!("claude connector configuration is valid and ready to serve");
println!("callers: {}", config.callers.len());
println!("lease_callers: {}", config.lease_callers.len());
println!("prices: {}", config.prices.len());
Ok(())
}
@ -85,17 +84,15 @@ struct Flags {
/// The flags that describe one caller, shared by `config init` (which writes
/// the first caller) and `config add-caller`.
const CALLER: [&str; 7] = [
const CALLER: [&str; 5] = [
"--caller-id",
"--connection-key-file",
"--connection-key-epoch",
"--spend-ceiling-micro-usd",
"--spend-window-ms",
"--max-concurrent-requests",
"--max-output-tokens",
];
const INIT: [&str; 22] = [
const INIT: [&str; 21] = [
"--listen",
"--replay-store",
"--claude-binary",
@ -117,15 +114,9 @@ const INIT: [&str; 22] = [
"--lease-max-ms",
"--turn",
"--lease-unit-instance",
"--lease-reservation-micro-usd",
];
const LEASE_CALLER: [&str; 4] = [
"--caller-id",
"--turn",
"--lease-unit-instance",
"--lease-reservation-micro-usd",
];
const LEASE_CALLER: [&str; 3] = ["--caller-id", "--turn", "--lease-unit-instance"];
impl Flags {
fn parse(
@ -213,8 +204,6 @@ impl Flags {
max_concurrent_requests: self.number_or("--max-concurrent-requests", 4)?,
max_payload_bytes: self.number_or("--max-payload-bytes", 1 << 20)?,
max_output_tokens: self.number_or("--max-output-tokens", 32_768)?,
spend_ceiling_micro_usd: self.number("--spend-ceiling-micro-usd")?,
spend_window_ms: self.number("--spend-window-ms")?,
})
}
}
@ -230,33 +219,6 @@ fn sha256(hex: &str) -> Option<[u8; 32]> {
Some(digest)
}
/// MODEL:INPUT:OUTPUT:CACHE_WRITE:CACHE_READ:OVERHEAD, rates in micro-USD per
/// million tokens. The one cache-write rate prices both retentions.
fn price(text: &str) -> Option<(String, ModelPrice)> {
let mut parts = text.split(':');
let model = parts.next()?.to_string();
let mut next = || parts.next()?.parse::<u64>().ok();
let (input, output, cache_write, cache_read, overhead_tokens) =
(next()?, next()?, next()?, next()?, next()?);
if parts.next().is_some() {
return None;
}
Some((
model,
ModelPrice {
rates: Rates {
input,
cache_write_short: cache_write,
cache_write_long: cache_write,
cache_read,
output,
},
long_context: None,
overhead_tokens,
},
))
}
/// Writes 32 random bytes as 64 hex characters, mode 0600, unless the file is
/// already there: the secret is never read back or shown, and a file that
/// exists is the secret handles already depend on, so it is kept as it is when
@ -322,16 +284,9 @@ fn init(path: &str, args: &[&str]) -> Result<(), Failure> {
let flags = Flags::parse(
args,
&[&INIT, &CALLER],
&["--allowed-model", "--price", "--lease-user"],
&["--allowed-model", "--lease-user"],
)?;
let first = flags.caller()?;
let mut prices = BTreeMap::new();
for text in flags.list("--price") {
let (model, price) = price(text).ok_or(Failure::Malformed("--price"))?;
if prices.insert(model, price).is_some() {
return Err(Failure::Malformed("--price"));
}
}
let lease_callers = flags
.list("--lease-user")
.iter()
@ -341,7 +296,6 @@ fn init(path: &str, args: &[&str]) -> Result<(), Failure> {
caller_id: first.caller_runtime_id.clone(),
turn: flags.turn()?,
unit_instance: flags.text("--lease-unit-instance")?,
reservation_micro_usd: flags.number("--lease-reservation-micro-usd")?,
})
})
.collect::<Result<Vec<_>, Failure>>()?;
@ -366,7 +320,6 @@ fn init(path: &str, args: &[&str]) -> Result<(), Failure> {
socket_timeout_ms: flags.number_or("--socket-timeout-ms", 10_000)?,
max_expiry_skew_ms: 300_000,
callers: vec![first],
prices,
lease_socket: flags.single.get("--lease-socket").map(PathBuf::from),
lease_callers,
lease_max_ms: flags.number_or("--lease-max-ms", 3_600_000)?,
@ -412,7 +365,6 @@ fn add_lease_caller(path: &str, args: &[&str]) -> Result<(), Failure> {
caller_id: flags.text("--caller-id")?,
turn: flags.turn()?,
unit_instance: flags.text("--lease-unit-instance")?,
reservation_micro_usd: flags.number("--lease-reservation-micro-usd")?,
});
write_config(path.as_ref(), &config)?;
Ok(())

View file

@ -1,7 +1,7 @@
//! The plan's own usage windows, as the CLI reports them. The subscription
//! decides how much a plan may burn; the CLI's `rate_limit_event` is the only
//! place that number appears, so the latest observation of each window is kept
//! here and read out beside the connector's own spend ledger.
//! here and published as the usage readout and lent with each lease.
use std::collections::BTreeMap;
use std::sync::Mutex;

View file

@ -14,7 +14,7 @@ use crate::quota::QuotaCell;
const STRUCTURED_OUTPUT_TOOL: &str = "StructuredOutput";
/// A call that cannot complete, with the stage that failed. `usage` is `None`
/// (the kit bills its reservation) unless a result event reported it or the
/// (unknown) unless a result event reported it or the
/// call never reached the provider (zero).
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Failure {
@ -25,7 +25,7 @@ pub(crate) struct Failure {
impl Failure {
/// A failure after the child ran: what the provider billed is unknown, so
/// the kit bills its reservation.
/// the usage is unknown.
pub(crate) fn cost_unknown(kind: FailureKind, message: &'static str) -> Self {
Self {
kind,
@ -356,7 +356,7 @@ fn fail(kind: FailureKind, message: &'static str) -> Step {
/// provider cannot have billed (`empty_is_zero`: a refusal at the door, 401,
/// 403 or 429, or a call never made); elsewhere the CLI also zeroes usage on
/// crashes after real work. A call never made (`absent_is_zero`) may also
/// omit the field. `None` is billed the reservation by the kit.
/// omit the field. `None` is unknown usage.
fn read_usage(reported: Option<Value>, empty_is_zero: bool, absent_is_zero: bool) -> Option<Usage> {
let Some(reported) = reported else {
return absent_is_zero.then(|| sum_usage(std::iter::empty()));

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,11 @@ use rand_core::{OsRng, RngCore};
use crate::contract::{TransportInvocation, TransportKey, TransportResult};
use crate::envelope::{
MAX_IDENTITY_BYTES, ServiceError, TransportFrameError, decode_result_frame,
decode_spend_readout_response_frame, decrypt_result, decrypt_spend_readout,
encode_invocation_frame, encode_spend_readout_request_frame, encrypt_invocation,
encrypt_spend_readout_request, read_transport_frame, write_transport_frame,
decode_usage_readout_response_frame, decrypt_result, decrypt_usage_readout,
encode_invocation_frame, encode_usage_readout_request_frame, encrypt_invocation,
encrypt_usage_readout_request, read_transport_frame, write_transport_frame,
};
use crate::readout::SpendReadout;
use crate::readout::UsageReadout;
#[derive(Clone)]
pub struct ConnectorClient {
@ -62,25 +62,25 @@ impl ConnectorClient {
decrypt_result(&envelope, &self.security, invocation).map_err(Into::into)
}
/// Reads the connector's spend ledger and plan quota as `caller_runtime_id`,
/// Reads the plan's usage windows as `caller_runtime_id`,
/// which must be a caller whose connection key this client holds.
pub fn spend_readout(
pub fn usage_readout(
&self,
caller_runtime_id: &str,
) -> Result<SpendReadout, ConnectorClientError> {
) -> Result<UsageReadout, ConnectorClientError> {
let mut nonce = [0_u8; 16];
OsRng.fill_bytes(&mut nonce);
let request_id = format!(
"spend-readout-{}",
"usage-readout-{}",
nonce.iter().map(|b| format!("{b:02x}")).collect::<String>()
);
let sealed = encrypt_spend_readout_request(caller_runtime_id, &request_id, &self.security)?;
let response = self.exchange(&encode_spend_readout_request_frame(&sealed)?)?;
let envelope = decode_spend_readout_response_frame(&response)?;
let sealed = encrypt_usage_readout_request(caller_runtime_id, &request_id, &self.security)?;
let response = self.exchange(&encode_usage_readout_request_frame(&sealed)?)?;
let envelope = decode_usage_readout_response_frame(&response)?;
if envelope.caller_runtime_id != caller_runtime_id || envelope.request_id != request_id {
return Err(ServiceError::OuterIdentity.into());
}
decrypt_spend_readout(&envelope, &self.security).map_err(Into::into)
decrypt_usage_readout(&envelope, &self.security).map_err(Into::into)
}
/// Sends one frame on a fresh connection and reads the one answer.
@ -274,17 +274,17 @@ mod fixture_tests {
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let request = read_transport_frame(&mut stream, 64 * 1024).unwrap();
let RequestFrame::SpendReadout(asked) = decode_request_frame(&request).unwrap()
let RequestFrame::UsageReadout(asked) = decode_request_frame(&request).unwrap()
else {
panic!("the client sent something other than a readout request");
};
let request_id = answer_request.unwrap_or(&asked.request_id);
let readout = SpendReadout::new(1, Vec::new(), PlanQuotaSnapshot::default());
let readout = UsageReadout::new(1, PlanQuotaSnapshot::default());
let sealed =
encrypt_spend_readout(answer_caller, request_id, &readout, &key).unwrap();
encrypt_usage_readout(answer_caller, request_id, &readout, &key).unwrap();
write_transport_frame(
&mut stream,
&encode_spend_readout_response_frame(&sealed).unwrap(),
&encode_usage_readout_response_frame(&sealed).unwrap(),
64 * 1024,
)
.unwrap();
@ -296,7 +296,7 @@ mod fixture_tests {
Some(Duration::from_secs(1)),
)
.unwrap();
let answer = client.spend_readout("caller-1");
let answer = client.usage_readout("caller-1");
if accepted {
assert_eq!(answer.unwrap().at_unix_ms, 1);
} else {

View file

@ -9,11 +9,11 @@ use zeroize::Zeroize;
use crate::envelope::{ServiceError, binary_schema};
pub const PROVIDER_REQUEST_SCHEMA_ID: &str = "gamecult.model.provider_request.v3";
pub const INVOCATION_SCHEMA_ID: &str = "gamecult.model.transport_invocation.v3";
pub const RESULT_SCHEMA_ID: &str = "gamecult.model.transport_result.v3";
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.model.transport_receipt.v3";
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.model.transport_envelope.v3";
pub const PROVIDER_REQUEST_SCHEMA_ID: &str = "gamecult.model.provider_request.v4";
pub const INVOCATION_SCHEMA_ID: &str = "gamecult.model.transport_invocation.v4";
pub const RESULT_SCHEMA_ID: &str = "gamecult.model.transport_result.v4";
pub const RECEIPT_SCHEMA_ID: &str = "gamecult.model.transport_receipt.v4";
pub const ENVELOPE_SCHEMA_ID: &str = "gamecult.model.transport_envelope.v4";
#[derive(Clone, PartialEq, Eq)]
pub struct TransportKey(pub(crate) [u8; 32]);
@ -190,10 +190,10 @@ pub enum FailureKind {
Provider,
}
/// Token accounting for one request, as the backend reports it; the service
/// prices it. Cache reads and writes are counted apart from plain input.
/// Token accounting for one request, as the backend reports it. Cache reads
/// and writes are counted apart from plain input.
/// `output_tokens` includes reasoning tokens: `reasoning_tokens` is a detail
/// (absent when the provider does not report it) and is never priced apart.
/// (absent when the provider does not report it) and is never counted apart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Usage {
@ -252,10 +252,6 @@ pub struct TransportInvocation {
pub expires_at_unix_ms: u64,
pub native_request_sha256: [u8; 32],
pub provider_request_sha256: [u8; 32],
/// The most this request may cost, in micro-USD: the caller's own bound
/// on it. Required nonzero. It is part of the invocation's identity, so a
/// changed budget under a recorded request id is a `ReplayConflict`.
pub max_cost_micro_usd: u64,
pub request: ProviderRequest,
}
@ -264,7 +260,6 @@ impl TransportInvocation {
caller_runtime_id: impl Into<String>,
expires_at_unix_ms: u64,
native_request_sha256: [u8; 32],
max_cost_micro_usd: u64,
request: ProviderRequest,
) -> Result<Self, ContractError> {
let provider_request_sha256 = provider_request_sha256(&request)?;
@ -274,7 +269,6 @@ impl TransportInvocation {
expires_at_unix_ms,
native_request_sha256,
provider_request_sha256,
max_cost_micro_usd,
request,
})
}
@ -287,9 +281,6 @@ impl TransportInvocation {
if self.native_request_sha256 == [0; 32] {
return Err(ContractError::Invalid("native_request_sha256"));
}
if self.max_cost_micro_usd == 0 {
return Err(ContractError::Invalid("max_cost_micro_usd"));
}
self.request.validate()?;
if self.provider_request_sha256 != provider_request_sha256(&self.request)? {
return Err(ContractError::ProviderDigest);
@ -485,15 +476,6 @@ pub struct TransportReceipt {
/// How many foreign `ReasoningState` items the service removed from the
/// request before the backend saw it.
pub foreign_reasoning_dropped: u32,
/// What the request cost, in micro-USD. The service derives it from the
/// outcome's usage and its price table; a backend never writes it.
pub cost_micro_usd: u64,
/// How far `cost_micro_usd` exceeds what the service reserved for the
/// request: zero unless the backend reported usage above the worst case
/// the request allowed, which is a breach of the backend's contract. The
/// cost is still the true one and the caller's window is charged all of
/// it. The service derives this; a backend never writes it.
pub overrun_micro_usd: u64,
pub outcome: TransportOutcome,
}
@ -558,12 +540,6 @@ pub enum Refusal {
ModelNotAllowed,
OutputBound,
PayloadBound,
// The worst case of the request exceeds the invocation's own
// `max_cost_micro_usd`. Plain comments: a doc comment would split the
// published schema into a union.
CostBound,
// The caller's spend window cannot hold the request's reservation.
SpendCeiling,
Capacity,
InFlight,
Indeterminate,
@ -573,15 +549,12 @@ pub enum Refusal {
// exchange, where dropping it would break the provider's own rules. A plain
// comment: a doc comment would split the published schema into a union.
ForeignReasoningState,
// The caller's newest admission window starts after the clock's reading,
// and the request does not fit what that window holds: the host's clock
// is behind the window and the caller stays refused until it catches up.
// A plain comment: a doc comment would split the published schema into a
// union.
ClockBehindAdmissionWindow,
// The provider itself is refusing right now: a plan window it reported
// `rejected` has not reached its own reset. Plain comments, as above.
ProviderRejected,
}
/// The one wire codec of every v3 document: named MessagePack (struct fields
/// The one wire codec of every v4 document: named MessagePack (struct fields
/// are keyed by name), with unknown fields refused by the types themselves.
pub(crate) fn wire_encode<T: Serialize + ?Sized>(document: &T) -> Result<Vec<u8>, ContractError> {
rmp_serde::to_vec_named(document).map_err(|_| ContractError::Encoding)
@ -787,28 +760,16 @@ mod tests {
}
#[test]
fn max_cost_zero_is_malformed() {
let mut invocation = invocation();
invocation.max_cost_micro_usd = 0;
assert_eq!(
invocation.validate(1_000, 2_000),
Err(ContractError::Invalid("max_cost_micro_usd"))
);
invocation.max_cost_micro_usd = 1;
assert_eq!(invocation.validate(1_000, 2_000), Ok(()));
}
#[test]
fn max_cost_is_part_of_the_invocations_identity() {
fn the_invocations_identity_covers_its_request() {
let one = invocation();
let mut other = one.clone();
other.max_cost_micro_usd += 1;
other.request.max_output_tokens += 1;
assert_ne!(
canonical_invocation_bytes(&one).unwrap(),
canonical_invocation_bytes(&other).unwrap()
);
let decoded: TransportInvocation = wire_decode(&wire_encode(&one).unwrap()).unwrap();
assert_eq!(decoded.max_cost_micro_usd, one.max_cost_micro_usd);
assert_eq!(decoded, one);
}
#[test]
@ -869,7 +830,7 @@ mod tests {
}
#[test]
fn v3_documents_carry_their_v3_ids() {
fn v4_documents_carry_their_v4_ids() {
assert_eq!(
[
PROVIDER_REQUEST_SCHEMA_ID,
@ -879,11 +840,11 @@ mod tests {
ENVELOPE_SCHEMA_ID
],
[
"gamecult.model.provider_request.v3",
"gamecult.model.transport_invocation.v3",
"gamecult.model.transport_result.v3",
"gamecult.model.transport_receipt.v3",
"gamecult.model.transport_envelope.v3"
"gamecult.model.provider_request.v4",
"gamecult.model.transport_invocation.v4",
"gamecult.model.transport_result.v4",
"gamecult.model.transport_receipt.v4",
"gamecult.model.transport_envelope.v4"
]
);
let invocation = invocation();
@ -922,7 +883,7 @@ mod tests {
let bytes = wire_encode(document).unwrap();
let mut value = rmpv::decode::read_value(&mut bytes.as_slice()).unwrap();
let rmpv::Value::Map(entries) = &mut value else {
panic!("a v3 document is a named map");
panic!("a v4 document is a named map");
};
entries.push((rmpv::Value::from("unexpected_field"), rmpv::Value::from(1)));
let mut out = Vec::new();

View file

@ -17,14 +17,14 @@ use crate::contract::{
ContractError, ENVELOPE_SCHEMA_ID, TransportInvocation, TransportKey, TransportResult,
canonical_invocation_bytes, require_id, wire_decode, wire_encode,
};
use crate::readout::SpendReadout;
use crate::readout::UsageReadout;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum EnvelopeKind {
Invocation,
Result,
SpendReadoutRequest,
SpendReadoutResult,
UsageReadoutRequest,
UsageReadoutResult,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
@ -54,8 +54,8 @@ pub(crate) fn binary_schema(_: &mut schemars::SchemaGenerator) -> schemars::Sche
pub const CONNECTOR_SERVICE_ID: &str = "gamecult.model.connector";
/// The operation that runs one sealed invocation.
pub const INVOKE_OPERATION: &str = "invoke";
/// The operation that reads the spend ledger and the plan's quota.
pub const SPEND_READOUT_OPERATION: &str = "spend_readout";
/// The operation that reads the plan's usage.
pub const USAGE_READOUT_OPERATION: &str = "usage_readout";
const PAYLOAD_ENCODING: &str = "messagepack-base64";
const OK_STATUS: &str = "ok";
@ -78,19 +78,19 @@ fn seal_payload(envelope: &TransportEnvelope) -> Result<String, ServiceError> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Operation {
Invoke,
SpendReadout,
UsageReadout,
}
impl Operation {
fn name(self) -> &'static str {
match self {
Operation::Invoke => INVOKE_OPERATION,
Operation::SpendReadout => SPEND_READOUT_OPERATION,
Operation::UsageReadout => USAGE_READOUT_OPERATION,
}
}
fn from_name(name: &str) -> Option<Self> {
[Operation::Invoke, Operation::SpendReadout]
pub(crate) fn from_name(name: &str) -> Option<Self> {
[Operation::Invoke, Operation::UsageReadout]
.into_iter()
.find(|operation| operation.name() == name)
}
@ -141,10 +141,10 @@ pub(crate) fn encode_invocation_frame(
request_frame(Operation::Invoke, envelope)
}
pub(crate) fn encode_spend_readout_request_frame(
pub(crate) fn encode_usage_readout_request_frame(
envelope: &TransportEnvelope,
) -> Result<Vec<u8>, ServiceError> {
request_frame(Operation::SpendReadout, envelope)
request_frame(Operation::UsageReadout, envelope)
}
#[cfg(any(feature = "daemon", test))]
@ -153,10 +153,10 @@ pub(crate) fn encode_result_frame(envelope: &TransportEnvelope) -> Result<Vec<u8
}
#[cfg(feature = "daemon")]
pub(crate) fn encode_spend_readout_response_frame(
pub(crate) fn encode_usage_readout_response_frame(
envelope: &TransportEnvelope,
) -> Result<Vec<u8>, ServiceError> {
response_frame(Operation::SpendReadout, envelope)
response_frame(Operation::UsageReadout, envelope)
}
/// Opens the carriage of one frame. Everything the carriage says is checked
@ -193,7 +193,7 @@ fn read_carriage(bytes: &[u8]) -> Result<CultNetMessage, ServiceError> {
#[cfg(any(feature = "daemon", test))]
pub(crate) enum RequestFrame {
Invoke(TransportEnvelope),
SpendReadout(TransportEnvelope),
UsageReadout(TransportEnvelope),
}
#[cfg(any(feature = "daemon", test))]
@ -215,7 +215,7 @@ pub(crate) fn decode_request_frame(bytes: &[u8]) -> Result<RequestFrame, Service
&payload,
)? {
(Operation::Invoke, envelope) => Ok(RequestFrame::Invoke(envelope)),
(Operation::SpendReadout, envelope) => Ok(RequestFrame::SpendReadout(envelope)),
(Operation::UsageReadout, envelope) => Ok(RequestFrame::UsageReadout(envelope)),
}
}
_ => Err(ServiceError::Carriage),
@ -226,7 +226,7 @@ pub(crate) fn decode_request_frame(bytes: &[u8]) -> Result<RequestFrame, Service
pub(crate) fn decode_invocation_frame(bytes: &[u8]) -> Result<TransportEnvelope, ServiceError> {
match decode_request_frame(bytes)? {
RequestFrame::Invoke(envelope) => Ok(envelope),
RequestFrame::SpendReadout(_) => Err(ServiceError::Carriage),
RequestFrame::UsageReadout(_) => Err(ServiceError::Carriage),
}
}
@ -263,10 +263,10 @@ pub(crate) fn decode_result_frame(bytes: &[u8]) -> Result<TransportEnvelope, Ser
decode_response_frame(bytes, Operation::Invoke)
}
pub(crate) fn decode_spend_readout_response_frame(
pub(crate) fn decode_usage_readout_response_frame(
bytes: &[u8],
) -> Result<TransportEnvelope, ServiceError> {
decode_response_frame(bytes, Operation::SpendReadout)
decode_response_frame(bytes, Operation::UsageReadout)
}
/// The most bytes one frame holding a sealed envelope of `ciphertext_len`
@ -416,9 +416,9 @@ pub fn decrypt_result(
Ok(result)
}
/// A spend readout request carries no content: the sealed envelope's
/// A usage readout request carries no content: the sealed envelope's
/// authenticated identity and kind are the whole request.
pub(crate) fn encrypt_spend_readout_request(
pub(crate) fn encrypt_usage_readout_request(
caller_runtime_id: &str,
request_id: &str,
security: &TransportKey,
@ -426,45 +426,45 @@ pub(crate) fn encrypt_spend_readout_request(
encrypt_message(
caller_runtime_id,
request_id,
EnvelopeKind::SpendReadoutRequest,
EnvelopeKind::UsageReadoutRequest,
&[],
security,
)
}
#[cfg(any(feature = "daemon", test))]
pub(crate) fn decrypt_spend_readout_request(
pub(crate) fn decrypt_usage_readout_request(
envelope: &TransportEnvelope,
security: &TransportKey,
) -> Result<(), ServiceError> {
validate_envelope(envelope, EnvelopeKind::SpendReadoutRequest)?;
validate_envelope(envelope, EnvelopeKind::UsageReadoutRequest)?;
decrypt_message(envelope, security).map(drop)
}
#[cfg(feature = "daemon")]
pub(crate) fn encrypt_spend_readout(
pub(crate) fn encrypt_usage_readout(
caller_runtime_id: &str,
request_id: &str,
readout: &SpendReadout,
readout: &UsageReadout,
security: &TransportKey,
) -> Result<TransportEnvelope, ServiceError> {
let plaintext = wire_encode(readout).map_err(|_| ServiceError::Encoding)?;
encrypt_message(
caller_runtime_id,
request_id,
EnvelopeKind::SpendReadoutResult,
EnvelopeKind::UsageReadoutResult,
&plaintext,
security,
)
}
pub(crate) fn decrypt_spend_readout(
pub(crate) fn decrypt_usage_readout(
envelope: &TransportEnvelope,
security: &TransportKey,
) -> Result<SpendReadout, ServiceError> {
validate_envelope(envelope, EnvelopeKind::SpendReadoutResult)?;
) -> Result<UsageReadout, ServiceError> {
validate_envelope(envelope, EnvelopeKind::UsageReadoutResult)?;
let plaintext = decrypt_message(envelope, security)?;
let readout: SpendReadout = wire_decode(&plaintext).map_err(|_| ServiceError::Encoding)?;
let readout: UsageReadout = wire_decode(&plaintext).map_err(|_| ServiceError::Encoding)?;
readout.validate()?;
Ok(readout)
}
@ -607,8 +607,6 @@ pub enum ServiceError {
SharedCallerKey,
#[error("connector replay store failed: {0}")]
ReplayStore(String),
#[error("an allowed model has no price: {0}")]
UnpricedModel(String),
#[error("connector replay record is malformed")]
InvalidReplayRecord,
#[error("caller key changed while durable replay records still exist")]
@ -1072,7 +1070,7 @@ mod tests {
fn golden_frame() -> Vec<u8> {
std::fs::read(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/wire_v3/carriage_invocation.frame"),
.join("tests/fixtures/wire_v4/carriage_invocation.frame"),
)
.expect("carriage_invocation.frame is committed")
}
@ -1109,7 +1107,7 @@ mod tests {
assert_eq!(text(&request, "operation"), "invoke");
assert_eq!(
text(&request, "payloadSchema"),
"gamecult.model.transport_envelope.v3"
"gamecult.model.transport_envelope.v4"
);
assert_eq!(text(&request, "payloadEncoding"), "messagepack-base64");
assert_eq!(text(&request, "messageId"), envelope.request_id);
@ -1123,7 +1121,7 @@ mod tests {
assert_eq!(text(&response, "status"), "ok");
assert_eq!(
text(&response, "payloadSchema"),
"gamecult.model.transport_envelope.v3"
"gamecult.model.transport_envelope.v4"
);
assert_eq!(text(&response, "payloadEncoding"), "messagepack-base64");
assert_eq!(text(&response, "messageId"), envelope.request_id);
@ -1287,23 +1285,23 @@ mod tests {
#[test]
fn a_readout_request_is_not_an_invocation_and_names_its_own_operation() {
let key = TransportKey::from_connection_secret("readout-test-key").unwrap();
let sealed = encrypt_spend_readout_request("caller-1", "readout-1", &key).unwrap();
let frame = encode_spend_readout_request_frame(&sealed).unwrap();
let sealed = encrypt_usage_readout_request("caller-1", "readout-1", &key).unwrap();
let frame = encode_usage_readout_request_frame(&sealed).unwrap();
assert!(matches!(
decode_request_frame(&frame),
Ok(RequestFrame::SpendReadout(found)) if found == sealed
Ok(RequestFrame::UsageReadout(found)) if found == sealed
));
assert_eq!(decode_invocation_frame(&frame), Err(ServiceError::Carriage));
// Sealed as one kind, the authenticated data refuses it as another.
assert!(decrypt_spend_readout_request(&sealed, &key).is_ok());
assert!(decrypt_usage_readout_request(&sealed, &key).is_ok());
assert_eq!(
decrypt_spend_readout(&sealed, &key).unwrap_err(),
decrypt_usage_readout(&sealed, &key).unwrap_err(),
ServiceError::Envelope
);
let mut relabelled = sealed.clone();
relabelled.message_kind = EnvelopeKind::SpendReadoutResult;
relabelled.message_kind = EnvelopeKind::UsageReadoutResult;
assert_eq!(
decrypt_spend_readout(&relabelled, &key).unwrap_err(),
decrypt_usage_readout(&relabelled, &key).unwrap_err(),
ServiceError::Encryption
);
}
@ -1313,12 +1311,12 @@ mod tests {
fn a_response_is_read_as_its_own_operation_only() {
use crate::readout::PlanQuotaSnapshot;
let key = TransportKey::from_connection_secret("readout-test-key").unwrap();
let readout = SpendReadout::new(1, Vec::new(), PlanQuotaSnapshot::default());
let sealed = encrypt_spend_readout("caller-1", "readout-1", &readout, &key).unwrap();
let readout_frame = encode_spend_readout_response_frame(&sealed).unwrap();
let readout = UsageReadout::new(1, PlanQuotaSnapshot::default());
let sealed = encrypt_usage_readout("caller-1", "readout-1", &readout, &key).unwrap();
let readout_frame = encode_usage_readout_response_frame(&sealed).unwrap();
let invoke_frame = encode_result_frame(&sealed).unwrap();
assert_eq!(
decode_spend_readout_response_frame(&readout_frame).unwrap(),
decode_usage_readout_response_frame(&readout_frame).unwrap(),
sealed
);
assert_eq!(decode_result_frame(&invoke_frame).unwrap(), sealed);
@ -1327,9 +1325,9 @@ mod tests {
Err(ServiceError::Carriage)
);
assert_eq!(
decode_spend_readout_response_frame(&invoke_frame),
decode_usage_readout_response_frame(&invoke_frame),
Err(ServiceError::Carriage)
);
assert_eq!(decrypt_spend_readout(&sealed, &key).unwrap(), readout);
assert_eq!(decrypt_usage_readout(&sealed, &key).unwrap(), readout);
}
}

View file

@ -133,16 +133,9 @@ pub(crate) fn request() -> ProviderRequest {
request
}
/// Generous next to the fixture request's worst case (about 64,000), so it
/// binds only in the tests that are about spend.
pub(crate) const MAX_COST: u64 = 10_000_000;
/// What the fixture usage (10 input and 3 output tokens) costs under
/// [`fixture_price`]: 10 x 3,000,000 + 3 x 15,000,000 = 75,000,000 / 1e6.
pub(crate) const FIXTURE_USAGE_COST: u64 = 75;
pub(crate) fn invocation() -> TransportInvocation {
TransportInvocation::new("epiphany-yggdrasil", 2_000, [7; 32], MAX_COST, request()).unwrap()
TransportInvocation::new("epiphany-yggdrasil", 2_000, [7; 32], request()).unwrap()
}
pub(crate) fn receipt(invocation: &TransportInvocation) -> TransportReceipt {
@ -155,8 +148,6 @@ pub(crate) fn receipt(invocation: &TransportInvocation) -> TransportReceipt {
model: invocation.request.model.clone(),
provider: "fixture-sse".to_string(),
foreign_reasoning_dropped: 0,
cost_micro_usd: FIXTURE_USAGE_COST,
overrun_micro_usd: 0,
outcome: TransportOutcome::Completed {
provider_response_id: Some("response-1".to_string()),
stop_reason: StopReason::EndTurn,
@ -170,33 +161,6 @@ pub(crate) fn receipt(invocation: &TransportInvocation) -> TransportReceipt {
},
}
}
/// A price with a distinct nonzero rate per class, so a swapped rate changes a cost.
#[cfg(feature = "daemon")]
pub(crate) fn fixture_price() -> ModelPrice {
ModelPrice {
rates: Rates {
input: 3_000_000,
cache_write_short: 3_750_000,
cache_write_long: 6_000_000,
cache_read: 300_000,
output: 15_000_000,
},
long_context: None,
overhead_tokens: 100,
}
}
#[cfg(feature = "daemon")]
pub(crate) fn fixture_prices() -> PriceTable {
PriceTable::from([("gpt-5.4".to_string(), fixture_price())])
}
/// Far above anything a fixture request reserves: the ceiling binds only in
/// the tests that are about it.
#[cfg(feature = "daemon")]
pub(crate) const CEILING: u64 = 1 << 40;
#[cfg(feature = "daemon")]
pub(crate) const WINDOW_MS: u64 = 3_600_000;
#[cfg(feature = "daemon")]
pub(crate) fn caller(
@ -212,8 +176,6 @@ pub(crate) fn caller(
max_concurrent_requests,
64 * 1024,
32_768,
SpendCeiling(CEILING),
SpendWindowMs(WINDOW_MS),
)
.unwrap()
}
@ -223,7 +185,6 @@ pub(crate) fn open_service(store: &TestStore, max_concurrent_requests: usize) ->
TransportService::open(
&store.path,
"fixture-sse",
fixture_prices(),
[
caller(
"epiphany-yggdrasil",
@ -249,5 +210,5 @@ pub(crate) fn security(key: &str) -> TransportKey {
pub(crate) fn invocation_for(caller_runtime_id: &str, request_id: &str) -> TransportInvocation {
let mut request = request();
request.request_id = request_id.to_string();
TransportInvocation::new(caller_runtime_id, 2_000, [7; 32], MAX_COST, request).unwrap()
TransportInvocation::new(caller_runtime_id, 2_000, [7; 32], request).unwrap()
}

View file

@ -1,13 +1,14 @@
//! The spend readout: what each caller has spent and holds in its current
//! window, and the plan's own usage as the backend last saw it. The ledger is
//! the replay store's; a readout only reads it.
//! The usage readout: the subscription plan's own usage windows as the backend
//! last saw them. The connector meters nothing in dollars; it publishes what
//! the provider reports, and the minds read it and decide their own burn.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::contract::ContractError;
pub const SPEND_READOUT_SCHEMA_ID: &str = "gamecult.model.spend_readout.v1";
pub const USAGE_READOUT_SCHEMA_ID: &str = "gamecult.model.usage_readout.v1";
/// One window of the subscription plan, as the provider reported it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
@ -32,40 +33,25 @@ pub struct PlanQuotaSnapshot {
pub windows: Vec<PlanQuota>,
}
/// One caller's current spend window in micro-USD. `spent` is settled charges
/// plus what a crash left counted at its full reservation; `reserved` is what
/// calls in flight hold, and both count against the ceiling.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CallerSpend {
pub caller_id: String,
pub window_ms: u64,
pub ceiling_micro_usd: u64,
pub spent_micro_usd: u64,
pub reserved_micro_usd: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SpendReadout {
pub struct UsageReadout {
pub schema_id: String,
pub at_unix_ms: u64,
pub callers: Vec<CallerSpend>,
pub plan_quota: Vec<PlanQuota>,
}
impl SpendReadout {
pub fn new(at_unix_ms: u64, callers: Vec<CallerSpend>, plan: PlanQuotaSnapshot) -> Self {
impl UsageReadout {
pub fn new(at_unix_ms: u64, plan: PlanQuotaSnapshot) -> Self {
Self {
schema_id: SPEND_READOUT_SCHEMA_ID.to_string(),
schema_id: USAGE_READOUT_SCHEMA_ID.to_string(),
at_unix_ms,
callers,
plan_quota: plan.windows,
}
}
pub(crate) fn validate(&self) -> Result<(), ContractError> {
if self.schema_id != SPEND_READOUT_SCHEMA_ID {
if self.schema_id != USAGE_READOUT_SCHEMA_ID {
return Err(ContractError::Schema);
}
Ok(())
@ -77,16 +63,9 @@ mod tests {
use super::*;
use crate::contract::{wire_decode, wire_encode};
fn readout() -> SpendReadout {
SpendReadout::new(
fn readout() -> UsageReadout {
UsageReadout::new(
1_000,
vec![CallerSpend {
caller_id: "eureka".into(),
window_ms: 3_600_000,
ceiling_micro_usd: 50_000_000,
spent_micro_usd: 7,
reserved_micro_usd: 3,
}],
PlanQuotaSnapshot {
windows: vec![PlanQuota {
window: "five_hour".into(),
@ -107,21 +86,21 @@ mod tests {
panic!("a readout is a map, not a positional array");
};
let names: Vec<&str> = fields.iter().filter_map(|(k, _)| k.as_str()).collect();
assert_eq!(names, ["schema_id", "at_unix_ms", "callers", "plan_quota"]);
assert_eq!(wire_decode::<SpendReadout>(&bytes).unwrap(), readout());
assert_eq!(names, ["schema_id", "at_unix_ms", "plan_quota"]);
assert_eq!(wire_decode::<UsageReadout>(&bytes).unwrap(), readout());
}
#[test]
fn an_unobserved_plan_is_none_not_zero() {
let bare = SpendReadout::new(1, Vec::new(), PlanQuotaSnapshot::default());
let decoded: SpendReadout = wire_decode(&wire_encode(&bare).unwrap()).unwrap();
let bare = UsageReadout::new(1, PlanQuotaSnapshot::default());
let decoded: UsageReadout = wire_decode(&wire_encode(&bare).unwrap()).unwrap();
assert!(decoded.plan_quota.is_empty());
}
#[test]
fn a_readout_of_another_schema_is_refused() {
let mut other = readout();
other.schema_id = "gamecult.model.spend_readout.v2".into();
other.schema_id = "gamecult.model.usage_readout.v2".into();
assert!(other.validate().is_err());
assert!(readout().validate().is_ok());
}

View file

@ -13,13 +13,12 @@ use zeroize::Zeroizing;
use crate::contract::TransportResult;
use crate::envelope::{
RequestFrame, ServiceError, TransportFrameError, decode_request_frame, encode_result_frame,
encode_spend_readout_response_frame, frame_bytes_for_payload, read_transport_frame,
encode_usage_readout_response_frame, frame_bytes_for_payload, read_transport_frame,
write_transport_frame,
};
use crate::readout::PlanQuotaSnapshot;
use crate::service::{
CallerAdmission, PriceTable, ProviderCall, SpendCeiling, SpendWindowMs, TransportAdmission,
TransportService, distinct_admissions,
CallerAdmission, ProviderCall, TransportAdmission, TransportService, distinct_admissions,
};
/// The only provider seam of the connector kit. A connector binary implements
@ -34,8 +33,9 @@ pub trait ProviderBackend: Send + Sync {
/// `serve` on: it names the provider on every receipt and decides which
/// reasoning state is foreign.
fn provider(&self) -> &str;
/// What the provider's own plan allows and has used, for the spend
/// readout. A provider with no plan to report keeps the default.
/// What the provider's own plan allows and has used, for the usage
/// readout and the provider's own refusals. A provider with no plan to report
/// keeps the default.
fn plan_quota(&self) -> PlanQuotaSnapshot {
PlanQuotaSnapshot::default()
}
@ -123,12 +123,6 @@ pub struct CallerConfig {
pub max_concurrent_requests: usize,
pub max_payload_bytes: usize,
pub max_output_tokens: u32,
/// The most the caller may spend in one window, in micro-USD. Required
/// nonzero. A fuse set once, generously above what the dial allows.
pub spend_ceiling_micro_usd: u64,
/// The length of the caller's spend window, in milliseconds. Windows are
/// fixed and aligned to the unix epoch. Required nonzero.
pub spend_window_ms: u64,
}
pub fn load_admissions(callers: &[CallerConfig]) -> Result<Vec<CallerAdmission>, ServeError> {
@ -149,8 +143,6 @@ pub fn load_admissions(callers: &[CallerConfig]) -> Result<Vec<CallerAdmission>,
caller.max_concurrent_requests,
caller.max_payload_bytes,
caller.max_output_tokens,
SpendCeiling(caller.spend_ceiling_micro_usd),
SpendWindowMs(caller.spend_window_ms),
)
.map_err(ServeError::Service)?,
);
@ -159,15 +151,12 @@ pub fn load_admissions(callers: &[CallerConfig]) -> Result<Vec<CallerAdmission>,
}
/// Serves admitted callers on a loopback listener until the listener fails.
/// `prices` is the connector binary's config document's table of what every
/// allowed model costs (B4 owns that document).
/// The caller binds; a listener that is not bound to loopback is refused, and
/// peers that are not loopback are dropped.
pub fn serve(
listener: TcpListener,
limits: ServeLimits,
admissions: Vec<CallerAdmission>,
prices: PriceTable,
replay_store: &Path,
backend: Arc<dyn ProviderBackend>,
) -> Result<(), ServeError> {
@ -176,7 +165,6 @@ pub fn serve(
let service = Arc::new(Mutex::new(TransportService::open(
replay_store,
backend.provider(),
prices,
admissions,
limits.max_expiry_skew_ms,
)?));
@ -184,8 +172,8 @@ pub fn serve(
}
/// `serve` over a service the caller opened and keeps a handle to, so another
/// front of the same ledger (the Self-run credential lease) reserves in the
/// same windows. The limits must already have been validated against the
/// front of the same service (the Self-run credential lease) shares its
/// replay store and plan. The limits must already have been validated against the
/// service's admissions.
pub fn serve_service(
listener: TcpListener,
@ -197,7 +185,7 @@ pub fn serve_service(
accept_loop(|| listener.accept(), limits, service, backend)
}
/// What must be true before a serve loop touches the ledger: the listener is
/// What must be true before a serve loop touches the store: the listener is
/// on loopback and the backend can take work.
fn admit_listener(listener: &TcpListener, backend: &dyn ProviderBackend) -> Result<(), ServeError> {
if !listener
@ -272,20 +260,25 @@ fn answer_frame(
) -> Result<Vec<u8>, ServeError> {
let envelope = match decode_request_frame(request).map_err(|_| ServeError::FrameEncoding)? {
RequestFrame::Invoke(envelope) => envelope,
RequestFrame::SpendReadout(envelope) => {
RequestFrame::UsageReadout(envelope) => {
let plan = backend.plan_quota();
let response = service
.lock()
.map_err(|_| ServeError::ServicePoisoned)?
.spend_readout(&envelope, unix_ms()?, plan, limits.max_frame_bytes)?;
return encode_spend_readout_response_frame(&response)
.usage_readout(&envelope, unix_ms()?, plan, limits.max_frame_bytes)?;
return encode_usage_readout_response_frame(&response)
.map_err(|_| ServeError::FrameEncoding);
}
};
let admission = service
.lock()
.map_err(|_| ServeError::ServicePoisoned)?
.begin(&envelope, unix_ms()?, limits.max_frame_bytes)?;
.begin(
&envelope,
unix_ms()?,
&backend.plan_quota(),
limits.max_frame_bytes,
)?;
let response = match admission {
TransportAdmission::Reply(response) => response,
TransportAdmission::Execute(claim) => {
@ -390,7 +383,7 @@ mod tests {
TransportInvocation, TransportOutcome, TransportResult,
};
use crate::fixtures::{
CEILING, MAX_COST, TestStore, WINDOW_MS, caller, fixture_prices, invocation_for, receipt,
TestStore, caller, invocation_for, receipt,
request,
};
@ -472,7 +465,6 @@ mod tests {
listener,
limits,
admissions,
fixture_prices(),
&path,
backend,
));
@ -506,7 +498,6 @@ mod tests {
listener,
limits(),
admissions,
fixture_prices(),
&path,
backend,
)
@ -528,8 +519,6 @@ mod tests {
max_concurrent_requests: *capacity,
max_payload_bytes: 1_048_576,
max_output_tokens: 16_384,
spend_ceiling_micro_usd: CEILING,
spend_window_ms: WINDOW_MS,
})
.collect()
}
@ -559,7 +548,7 @@ mod tests {
}
#[test]
fn serve_reads_out_spend_and_plan_quota_without_executing_anything() {
fn serve_reads_out_plan_usage_without_executing_anything() {
let store = TestStore::new();
let inner = CountingBackend::new(true);
let plan = PlanQuotaSnapshot {
@ -580,16 +569,9 @@ mod tests {
assert_eq!(inner.executions.load(Ordering::SeqCst), 1);
let before = now_ms();
let readout = client.spend_readout(CALLER).unwrap();
let readout = client.usage_readout(CALLER).unwrap();
assert!(readout.at_unix_ms >= before && readout.at_unix_ms <= now_ms());
assert_eq!(readout.plan_quota, plan.windows);
let [row] = readout.callers.as_slice() else {
panic!("one admitted caller");
};
assert_eq!(row.caller_id, CALLER);
assert_eq!((row.ceiling_micro_usd, row.window_ms), (CEILING, WINDOW_MS));
assert!(row.spent_micro_usd > 0, "the settled call was charged");
assert_eq!(row.reserved_micro_usd, 0);
assert_eq!(
inner.executions.load(Ordering::SeqCst),
1,
@ -604,39 +586,62 @@ mod tests {
Some(Duration::from_secs(5)),
)
.unwrap();
assert!(stranger.spend_readout(CALLER).is_err());
assert!(stranger.usage_readout(CALLER).is_err());
}
#[test]
fn a_backend_with_no_plan_reads_out_none() {
let store = TestStore::new();
let (client, _) = start(CountingBackend::new(true), &store);
let readout = client.spend_readout(CALLER).unwrap();
let readout = client.usage_readout(CALLER).unwrap();
assert!(readout.plan_quota.is_empty());
}
fn window_plan(permille: u32, status: &str, resets_at_unix_ms: u64) -> PlanQuotaSnapshot {
PlanQuotaSnapshot {
windows: vec![crate::readout::PlanQuota {
window: "five_hour".into(),
utilization_permille: Some(permille),
resets_at_unix_ms,
status: status.into(),
observed_at_unix_ms: 1,
}],
}
}
#[test]
fn serve_refuses_a_request_over_its_max_cost_without_executing_it() {
fn serve_repeats_the_providers_rejection_and_executes_nothing_while_it_stands() {
let store = TestStore::new();
let backend = CountingBackend::new(true);
let (client, _) = start(backend.clone(), &store);
let mut invocation = fresh("request-costly");
invocation.max_cost_micro_usd = 1;
let refused = client.execute(&invocation).unwrap();
let inner = CountingBackend::new(true);
let backend = Arc::new(PlanBackend {
inner: inner.clone(),
plan: window_plan(1_000, "rejected", now_ms() + 3_600_000),
});
let (client, _) = start_with(backend, &store, vec![caller(CALLER, KEY, 4)]);
let refused = client.execute(&fresh("request-rejected")).unwrap();
assert_eq!(
refused.disposition,
TransportDisposition::Refused(Refusal::CostBound)
TransportDisposition::Refused(Refusal::ProviderRejected)
);
assert_eq!(backend.executions.load(Ordering::SeqCst), 0);
assert_eq!(inner.executions.load(Ordering::SeqCst), 0);
}
// The refusal was not recorded: the same id with a budget that holds
// the worst case is admitted and executed.
invocation.max_cost_micro_usd = MAX_COST;
let admitted = client.execute(&invocation).unwrap();
#[test]
fn serve_admits_a_full_window_the_provider_still_allows() {
let store = TestStore::new();
let inner = CountingBackend::new(true);
let backend = Arc::new(PlanBackend {
inner: inner.clone(),
plan: window_plan(999, "allowed", now_ms() + 3_600_000),
});
let (client, _) = start_with(backend, &store, vec![caller(CALLER, KEY, 4)]);
let admitted = client.execute(&fresh("request-full")).unwrap();
assert!(matches!(
admitted.disposition,
TransportDisposition::Transported { .. }
));
assert_eq!(inner.executions.load(Ordering::SeqCst), 1);
}
assert_eq!(backend.executions.load(Ordering::SeqCst), 1);
}
@ -676,7 +681,6 @@ mod tests {
CALLER,
invocation.expires_at_unix_ms,
[7; 32],
MAX_COST,
conflicting_request,
)
.unwrap();
@ -761,7 +765,6 @@ mod tests {
let mut service = TransportService::open(
&store.path,
"fixture-sse",
fixture_prices(),
vec![caller(CALLER, KEY, 4)],
300_000,
)
@ -770,7 +773,9 @@ mod tests {
crate::envelope::encrypt_invocation(&invocation, &crate::fixtures::security(KEY))
.unwrap();
let TransportAdmission::Execute(claim) =
service.begin(&envelope, now_ms(), 1 << 20).unwrap()
service
.begin(&envelope, now_ms(), &PlanQuotaSnapshot::default(), 1 << 20)
.unwrap()
else {
panic!("a fresh request must execute");
};
@ -793,7 +798,6 @@ mod tests {
listener,
narrowed,
vec![caller(CALLER, KEY, 4)],
fixture_prices(),
&path,
served,
)
@ -1001,8 +1005,6 @@ mod tests {
slots,
payload,
32_768,
SpendCeiling(CEILING),
SpendWindowMs(WINDOW_MS),
)
.unwrap()
}
@ -1110,7 +1112,7 @@ mod tests {
..limits()
};
let admissions = vec![admission(CALLER, KEY, 4, bound)];
thread::spawn(move || serve(listener, tight, admissions, fixture_prices(), &path, served));
thread::spawn(move || serve(listener, tight, admissions, &path, served));
let client =
ConnectorClient::new(endpoint, KEY, frame, Some(Duration::from_secs(5))).unwrap();
let reply = client.execute(&invocation).unwrap();
@ -1225,7 +1227,6 @@ mod tests {
let mut service = TransportService::open(
&store.path,
"fixture-sse",
fixture_prices(),
vec![caller(CALLER, KEY, 4)],
300_000,
)
@ -1236,7 +1237,9 @@ mod tests {
)
.unwrap();
let TransportAdmission::Execute(claim) =
service.begin(&envelope, now_ms(), 1 << 20).unwrap()
service
.begin(&envelope, now_ms(), &PlanQuotaSnapshot::default(), 1 << 20)
.unwrap()
else {
panic!("a fresh request must execute");
};
@ -1328,7 +1331,6 @@ mod tests {
TransportService::open(
&store.path,
"fixture-sse",
fixture_prices(),
vec![caller(CALLER, KEY, 4)],
300_000,
)
@ -1391,7 +1393,6 @@ mod tests {
listener,
limits(),
admissions,
fixture_prices(),
&path,
served,
)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# wire_v3 fixtures
# wire_v4 fixtures
The named-MessagePack bytes of the v3 documents and one sealed carriage frame,
The named-MessagePack bytes of the v4 documents and one sealed carriage frame,
produced by the kit at the revision that added them. They pin drift; the types in
`src/contract.rs` own the shape, and `schemas/` publishes it.
@ -10,7 +10,7 @@ produced by the kit at the revision that added them. They pin drift; the types i
the plaintext documents.
- `carriage_invocation.frame`: a `cultnet.operation_request.v0` for service
`gamecult.model.connector`, operation `invoke`, whose payload is the base64 of an envelope
sealed under the fixed test key in `tests/wire_golden_v3.rs`.
sealed under the fixed test key in `tests/wire_golden_v4.rs`.
Generator: `tests/wire_golden_v3.rs` itself. Run it with `WIRE_V3_REGENERATE=1` to rewrite
Generator: `tests/wire_golden_v4.rs` itself. Run it with `WIRE_V4_REGENERATE=1` to rewrite
the files from the kit, then review the diff: a changed byte is a wire change.

View file

@ -1,4 +1,4 @@
//! `schemas/` at the repository root publishes one JSON Schema per v3 document,
//! `schemas/` at the repository root publishes one JSON Schema per v4 document,
//! generated from the types. This test regenerates them and fails on any
//! difference; run it with `MODEL_CONNECTOR_REGENERATE_SCHEMAS=1` to rewrite the
//! files from the types.
@ -28,7 +28,7 @@ fn published_schemas_are_the_types() {
("transport_result", render::<TransportResult>()),
("transport_receipt", render::<TransportReceipt>()),
("transport_envelope", render::<TransportEnvelope>()),
("spend_readout", render::<SpendReadout>()),
("usage_readout", render::<UsageReadout>()),
];
let dir = schemas_dir();
if std::env::var_os(REGENERATE).is_some() {
@ -80,7 +80,7 @@ fn the_published_ciphertext_is_what_the_wire_sends() {
assert_eq!(&published["properties"]["ciphertext"], ciphertext);
let envelope = TransportEnvelope {
schema_id: "gamecult.model.transport_envelope.v3".to_string(),
schema_id: "gamecult.model.transport_envelope.v4".to_string(),
caller_runtime_id: "caller".to_string(),
request_id: "request".to_string(),
message_kind: EnvelopeKind::Invocation,
@ -111,9 +111,7 @@ fn the_refusal_schema_is_one_flat_string_enum() {
.iter()
.map(|value| value.as_str().unwrap())
.collect();
assert_eq!(variants.len(), 15);
assert!(variants.contains(&"ClockBehindAdmissionWindow"));
assert_eq!(variants.len(), 13);
assert!(variants.contains(&"ForeignReasoningState"));
assert!(variants.contains(&"CostBound"));
assert!(variants.contains(&"SpendCeiling"));
assert!(variants.contains(&"ProviderRejected"));
}

View file

@ -106,8 +106,6 @@ fn start_with(backend: Arc<dyn ProviderBackend>, max_concurrent: usize) -> Conne
max_concurrent,
64 * 1024,
32_768,
SpendCeiling(1 << 40),
SpendWindowMs(3_600_000),
)
.unwrap();
let limits = ServeLimits {
@ -116,21 +114,7 @@ fn start_with(backend: Arc<dyn ProviderBackend>, max_concurrent: usize) -> Conne
socket_timeout_ms: 5_000,
max_expiry_skew_ms: 300_000,
};
let prices = PriceTable::from([(
"claude-opus-4-5".to_string(),
ModelPrice {
rates: Rates {
input: 5_000_000,
cache_write_short: 6_250_000,
cache_write_long: 10_000_000,
cache_read: 500_000,
output: 25_000_000,
},
long_context: None,
overhead_tokens: 100,
},
)]);
thread::spawn(move || serve(listener, limits, vec![admission], prices, &store, backend));
thread::spawn(move || serve(listener, limits, vec![admission], &store, backend));
ConnectorClient::new(endpoint, KEY, FRAME, Some(Duration::from_secs(5))).unwrap()
}
@ -143,7 +127,7 @@ fn invocation(request_id: &str, input: Vec<InputItem>) -> TransportInvocation {
4_096,
);
request.input = input;
TransportInvocation::new(CALLER, now_ms() + 60_000, [7; 32], 10_000_000, request).unwrap()
TransportInvocation::new(CALLER, now_ms() + 60_000, [7; 32], request).unwrap()
}
fn user(text: &str) -> InputItem {
@ -332,7 +316,7 @@ fn a_refused_completion_reaches_the_caller_as_a_typed_failure_and_frees_the_slot
let edits: [fn(&mut TransportReceipt); 3] = [
|receipt| receipt.provider = "another-provider".to_string(),
|receipt| receipt.foreign_reasoning_dropped += 1,
|receipt| receipt.cost_micro_usd += 1,
|receipt| receipt.request_id = "another-request".to_string(),
];
for edit in edits {
// One concurrent request: a wedged slot would refuse the next id.

View file

@ -1,5 +1,5 @@
//! Pins the bytes of the v3 wire so a drift is a failing test. The fixtures in
//! `fixtures/wire_v3` are produced by this file (see its README); they pin the
//! Pins the bytes of the v4 wire so a drift is a failing test. The fixtures in
//! `fixtures/wire_v4` are produced by this file (see its README); they pin the
//! kit against itself and are not a second contract: the types in
//! `model_connector::contract` own the shape.
@ -17,10 +17,10 @@ use serde::de::DeserializeOwned;
const KEY: &str = "golden-distinct-test-key";
const CALLER: &str = "golden-caller";
const REGENERATE: &str = "WIRE_V3_REGENERATE";
const REGENERATE: &str = "WIRE_V4_REGENERATE";
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wire_v3")
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/wire_v4")
}
fn named<T: Serialize>(value: &T) -> Vec<u8> {
@ -28,7 +28,7 @@ fn named<T: Serialize>(value: &T) -> Vec<u8> {
}
/// The value serializes to the committed bytes and the committed bytes decode
/// back to it. With `WIRE_V3_REGENERATE` set, the committed bytes are rewritten
/// back to it. With `WIRE_V4_REGENERATE` set, the committed bytes are rewritten
/// from the value first.
fn assert_wire<T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug>(
name: &str,
@ -81,7 +81,7 @@ fn request() -> ProviderRequest {
}
fn invocation() -> TransportInvocation {
TransportInvocation::new(CALLER, 1_800_000_000_000, [7; 32], 5_000_000, request()).unwrap()
TransportInvocation::new(CALLER, 1_800_000_000_000, [7; 32], request()).unwrap()
}
fn receipt(outcome: TransportOutcome) -> TransportReceipt {
@ -95,8 +95,6 @@ fn receipt(outcome: TransportOutcome) -> TransportReceipt {
model: invocation.request.model.clone(),
provider: "anthropic-messages".to_string(),
foreign_reasoning_dropped: 2,
cost_micro_usd: 1_234_567,
overrun_micro_usd: 89,
outcome,
}
}
@ -116,7 +114,7 @@ fn completed() -> TransportOutcome {
}
#[test]
fn wire_golden_v3() {
fn wire_golden_v4() {
let invocation = invocation();
assert_wire("provider_request.mp", &invocation.request);
assert_wire("transport_invocation.mp", &invocation);
@ -233,7 +231,7 @@ fn the_golden_carriage_frame_decrypts_and_validates_under_the_fixed_key() {
};
assert_eq!(service_id, "gamecult.model.connector");
assert_eq!(operation, "invoke");
assert_eq!(payload_schema, "gamecult.model.transport_envelope.v3");
assert_eq!(payload_schema, "gamecult.model.transport_envelope.v4");
assert_eq!(message_id, invocation.request_id());
let envelope: TransportEnvelope =
rmp_serde::from_slice(&STANDARD.decode(payload).unwrap()).unwrap();

View file

@ -1,112 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SpendReadout",
"type": "object",
"properties": {
"at_unix_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"callers": {
"type": "array",
"items": {
"$ref": "#/$defs/CallerSpend"
}
},
"plan_quota": {
"type": "array",
"items": {
"$ref": "#/$defs/PlanQuota"
}
},
"schema_id": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"schema_id",
"at_unix_ms",
"callers",
"plan_quota"
],
"$defs": {
"CallerSpend": {
"description": "One caller's current spend window in micro-USD. `spent` is settled charges\nplus what a crash left counted at its full reservation; `reserved` is what\ncalls in flight hold, and both count against the ceiling.",
"type": "object",
"properties": {
"caller_id": {
"type": "string"
},
"ceiling_micro_usd": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"reserved_micro_usd": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"spent_micro_usd": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"window_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"additionalProperties": false,
"required": [
"caller_id",
"window_ms",
"ceiling_micro_usd",
"spent_micro_usd",
"reserved_micro_usd"
]
},
"PlanQuota": {
"description": "One window of the subscription plan, as the provider reported it.",
"type": "object",
"properties": {
"observed_at_unix_ms": {
"description": "When this window was last reported. Its age in a readout is the\nreadout's `at_unix_ms` minus this.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"resets_at_unix_ms": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"status": {
"type": "string"
},
"utilization_permille": {
"description": "How much of the window was used when it was observed, in thousandths;\n`None` once the window has reset (`status` is then `expired`), because\nthe old reading says nothing about the new window.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"window": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"window",
"resets_at_unix_ms",
"status",
"observed_at_unix_ms"
]
}
}
}

View file

@ -47,8 +47,8 @@
"enum": [
"Invocation",
"Result",
"SpendReadoutRequest",
"SpendReadoutResult"
"UsageReadoutRequest",
"UsageReadoutResult"
]
}
}