connector-contract-v3: delete the Responses fields and Refusal::Policy; type the request, outcome and refusals

ProviderRequest loses reasoning_summary, service_tier, output_format_name,
previous_response_id and prompt_cache_key; reasoning_effort is a ReasoningEffort,
max_output_tokens is required and cache_retention is added. Completed carries a
StopReason and Usage, Failed a FailureKind, and the receipt names its provider.
Refusal::Policy becomes ModelNotAllowed, OutputBound and PayloadBound, each its
own branch in begin. cultcache-rs moves to the one CultLib pin a4eae90.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-28 11:11:53 +01:00
commit 5a4d8bd477
7 changed files with 543 additions and 104 deletions

430
Cargo.lock generated
View file

@ -52,12 +52,33 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "ar_archive_writer"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6"
dependencies = [
"object",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -113,6 +134,12 @@ dependencies = [
"inout",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -135,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"typenum",
]
@ -150,8 +177,8 @@ dependencies = [
[[package]]
name = "cultcache-rs"
version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
version = "0.3.1"
source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23"
dependencies = [
"anyhow",
"chrono",
@ -162,20 +189,84 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"serde_stacker",
"stacker",
"uuid",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "cultcache-rs-derive"
version = "0.1.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=e171eca32329baa928f4a1d810401a8b4c857029#e171eca32329baa928f4a1d810401a8b4c857029"
version = "0.2.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "cultnet-rs"
version = "0.2.0"
source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23"
dependencies = [
"aes-gcm",
"anyhow",
"base64",
"chrono",
"cultcache-rs",
"ed25519-dalek",
"hmac",
"rand",
"rmp-serde",
"rmpv",
"serde",
"serde_bytes",
"serde_json",
"sha2",
"socket2",
"uuid",
"windows-sys 0.61.2",
"zeroize",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -184,8 +275,45 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
@ -247,6 +375,18 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
@ -255,7 +395,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 6.0.0",
]
[[package]]
@ -268,6 +408,15 @@ dependencies = [
"polyval",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@ -341,10 +490,14 @@ name = "model-connector"
version = "0.1.0"
dependencies = [
"aes-gcm",
"base64",
"cultcache-rs",
"rand_core",
"cultnet-rs",
"rand_core 0.6.4",
"rmp-serde",
"schemars",
"serde",
"serde_bytes",
"serde_json",
"sha2",
"thiserror",
@ -360,6 +513,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -378,6 +540,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "polyval"
version = "0.6.2"
@ -390,6 +562,15 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@ -399,6 +580,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622"
dependencies = [
"ar_archive_writer",
"cc",
]
[[package]]
name = "quote"
version = "1.0.47"
@ -408,12 +599,38 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core 0.9.5",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.6.4"
@ -423,6 +640,15 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redb"
version = "4.2.0"
@ -432,6 +658,26 @@ dependencies = [
"libc",
]
[[package]]
name = "ref-cast"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "rmp"
version = "0.8.15"
@ -451,12 +697,63 @@ dependencies = [
"serde",
]
[[package]]
name = "rmpv"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417"
dependencies = [
"rmp",
"serde",
"serde_bytes",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "schemars"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 3.0.4",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
@ -497,6 +794,17 @@ dependencies = [
"syn 3.0.4",
]
[[package]]
name = "serde_derive_internals"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
]
[[package]]
name = "serde_json"
version = "1.0.151"
@ -510,6 +818,17 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_stacker"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a"
dependencies = [
"serde",
"serde_core",
"stacker",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -527,12 +846,54 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "stacker"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.61.2",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -626,6 +987,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
@ -752,6 +1122,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@ -761,6 +1140,15 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -825,6 +1213,32 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zeroize"
version = "1.9.0"

View file

@ -13,10 +13,14 @@ daemon = ["dep:cultcache-rs"]
[dependencies]
aes-gcm = "0.10"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "e171eca32329baa928f4a1d810401a8b4c857029", package = "cultcache-rs", optional = true }
base64 = "0.22"
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a4eae9056152335662f1115f97bf35fb00ab6d23", package = "cultnet-rs" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a4eae9056152335662f1115f97bf35fb00ab6d23", package = "cultcache-rs", optional = true }
rand_core = { version = "0.6", features = ["getrandom"] }
rmp-serde = "1"
schemars = "1"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
serde_json = "1"
sha2 = "0.10"
thiserror = "2"

View file

@ -154,7 +154,7 @@ mod fixture_tests {
let envelope: TransportEnvelope = rmp_serde::from_slice(&request).unwrap();
assert_eq!(decrypt_invocation(&envelope, &security).unwrap(), expected);
let response = encrypt_result(
&TransportResult::refused(&expected, Refusal::Policy),
&TransportResult::refused(&expected, Refusal::ModelNotAllowed),
&security,
)
.unwrap();
@ -175,7 +175,7 @@ mod fixture_tests {
.unwrap();
assert_eq!(
client.execute(&invocation).unwrap().disposition,
TransportDisposition::Refused(Refusal::Policy)
TransportDisposition::Refused(Refusal::ModelNotAllowed)
);
server.join().unwrap();
}

View file

@ -39,17 +39,13 @@ pub struct ProviderRequest {
pub model: String,
pub instructions: String,
pub input: Vec<InputItem>,
pub reasoning_effort: Option<String>,
pub reasoning_summary: Option<String>,
pub service_tier: Option<String>,
pub output_format_name: Option<String>,
pub previous_response_id: Option<String>,
pub reasoning_effort: Option<ReasoningEffort>,
pub cache_retention: Option<CacheRetention>,
pub tools: Vec<ToolDefinition>,
pub tool_choice: ToolChoice,
pub parallel_tool_calls: bool,
pub output_schema_json: Option<String>,
pub max_output_tokens: Option<u32>,
pub prompt_cache_key: Option<String>,
pub max_output_tokens: u32,
}
impl ProviderRequest {
@ -58,6 +54,7 @@ impl ProviderRequest {
conversation_id: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
max_output_tokens: u32,
) -> Self {
Self {
schema_id: PROVIDER_REQUEST_SCHEMA_ID.to_string(),
@ -67,16 +64,12 @@ impl ProviderRequest {
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_format_name: None,
previous_response_id: None,
cache_retention: None,
tools: Vec::new(),
tool_choice: ToolChoice::Auto,
parallel_tool_calls: false,
output_schema_json: None,
max_output_tokens: None,
prompt_cache_key: None,
max_output_tokens,
}
}
@ -88,38 +81,12 @@ impl ProviderRequest {
require_id(&self.conversation_id, "conversation_id")?;
require_id(&self.model, "model")?;
require_content(&self.instructions, "instructions")?;
if matches!(self.max_output_tokens, Some(0)) {
if self.max_output_tokens == 0 {
return Err(ContractError::Invalid("max_output_tokens"));
}
if self
.prompt_cache_key
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(ContractError::Invalid("prompt_cache_key"));
}
for (value, field) in [
(self.reasoning_effort.as_deref(), "reasoning_effort"),
(self.reasoning_summary.as_deref(), "reasoning_summary"),
(self.service_tier.as_deref(), "service_tier"),
(self.output_format_name.as_deref(), "output_format_name"),
(self.previous_response_id.as_deref(), "previous_response_id"),
] {
if let Some(value) = value {
require_id(value, field)?;
}
}
if let Some(schema) = &self.output_schema_json {
require_content(schema, "output_schema_json")?;
require_json_object(schema, "output_schema_json")?;
require_provider_name(
self.output_format_name
.as_deref()
.ok_or(ContractError::Invalid("output_format_name"))?,
"output_format_name",
)?;
} else if self.output_format_name.is_some() {
return Err(ContractError::Invalid("output_schema_json"));
}
if self.tools.is_empty() && self.tool_choice == ToolChoice::Required {
@ -167,6 +134,60 @@ pub enum ToolChoice {
Required,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
Low,
Medium,
High,
Max,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheRetention {
Short,
Long,
}
/// Why a provider stopped producing output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
ToolUse,
MaxOutputTokens,
StopSequence,
Refusal,
ContextWindow,
}
/// What kind of failure a provider reported. `ResultSize` is the connector's own
/// stand-in for a result whose sealed frame cannot be carried.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureKind {
ResultSize,
InvalidRequest,
Authentication,
RateLimited,
Unavailable,
Timeout,
Provider,
}
/// Token accounting for one completed request. Cache reads and writes are
/// counted apart from plain input; `reasoning_tokens` is absent when the
/// provider does not report it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
pub output_tokens: u64,
pub reasoning_tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputItem {
UserText {
@ -355,7 +376,7 @@ pub struct TransportReceipt {
pub native_request_sha256: [u8; 32],
pub provider_request_sha256: [u8; 32],
pub model: String,
pub transport: String,
pub provider: String,
pub outcome: TransportOutcome,
}
@ -376,17 +397,13 @@ impl TransportReceipt {
if self.native_request_sha256 != invocation.native_request_sha256 {
return Err(ContractError::NativeDigest);
}
require_id(&self.transport, "receipt.transport")?;
require_id(&self.provider, "receipt.provider")?;
match &self.outcome {
TransportOutcome::Completed {
provider_response_id: Some(value),
..
} => require_id(value, "receipt.provider_response_id")?,
TransportOutcome::Failed {
failure_kind,
message,
} => {
require_id(failure_kind, "receipt.failure_kind")?;
TransportOutcome::Failed { message, .. } => {
require_content(message, "receipt.failure_message")?;
}
TransportOutcome::Completed {
@ -402,13 +419,11 @@ impl TransportReceipt {
pub enum TransportOutcome {
Completed {
provider_response_id: Option<String>,
input_tokens: Option<u64>,
output_tokens: Option<u64>,
reasoning_output_tokens: Option<u64>,
cached_input_tokens: Option<u64>,
stop_reason: StopReason,
usage: Usage,
},
Failed {
failure_kind: String,
kind: FailureKind,
message: String,
},
}
@ -418,7 +433,9 @@ pub enum Refusal {
Expired,
IdentitySubstitution,
ProviderDigestSubstitution,
Policy,
ModelNotAllowed,
OutputBound,
PayloadBound,
Capacity,
InFlight,
Indeterminate,
@ -537,14 +554,14 @@ mod tests {
#[test]
fn provider_contract_refuses_transport_side_fixup_pressure() {
let mut request = request();
request.output_format_name = Some("epiphany.role.result".to_string());
request.output_schema_json = Some(r#"{"type":"object"}"#.to_string());
assert_eq!(request.validate(), Ok(()));
request.output_schema_json = Some("[]".to_string());
assert_eq!(
request.validate(),
Err(ContractError::Invalid("output_format_name"))
Err(ContractError::Invalid("output_schema_json"))
);
request.output_format_name = None;
request.output_schema_json = None;
request.input.push(InputItem::ToolResult {
call_id: "x".repeat(65),

View file

@ -43,6 +43,7 @@ pub(crate) fn request() -> ProviderRequest {
"conversation-1",
"gpt-5.4",
"Return the typed result.",
4_096,
);
request.input.push(InputItem::UserText {
text: "Projected state".to_string(),
@ -67,13 +68,17 @@ pub(crate) fn receipt(invocation: &TransportInvocation) -> TransportReceipt {
native_request_sha256: invocation.native_request_sha256,
provider_request_sha256: invocation.provider_request_sha256,
model: invocation.request.model.clone(),
transport: "fixture-sse".to_string(),
provider: "fixture-sse".to_string(),
outcome: TransportOutcome::Completed {
provider_response_id: Some("response-1".to_string()),
input_tokens: Some(10),
output_tokens: Some(3),
reasoning_output_tokens: Some(1),
cached_input_tokens: None,
stop_reason: StopReason::EndTurn,
usage: Usage {
input_tokens: 10,
cache_read_tokens: 0,
cache_write_tokens: 0,
output_tokens: 3,
reasoning_tokens: Some(1),
},
},
}
}

View file

@ -328,8 +328,8 @@ mod tests {
use super::*;
use crate::client::ConnectorClient;
use crate::contract::{
Refusal, TransportDisposition, TransportEvent, TransportEventPayload, TransportOutcome,
TransportResult,
FailureKind, Refusal, TransportDisposition, TransportEvent, TransportEventPayload,
TransportOutcome, TransportResult,
};
use crate::fixtures::{TestStore, caller, invocation_for, receipt, request};
@ -906,7 +906,7 @@ mod tests {
let reply = client.execute(&invocation).unwrap();
assert_eq!(
reply.disposition,
TransportDisposition::Refused(Refusal::Policy)
TransportDisposition::Refused(Refusal::PayloadBound)
);
assert_eq!(backend.executions.load(Ordering::SeqCst), 0);
}
@ -949,7 +949,7 @@ mod tests {
transport: &str,
) -> TransportResult {
let mut receipt = receipt(invocation);
receipt.transport = transport.to_string();
receipt.provider = transport.to_string();
TransportResult::transported(
invocation,
vec![TransportEvent {
@ -984,9 +984,9 @@ mod tests {
&result.disposition,
TransportDisposition::Transported { events, receipt }
if events.is_empty()
&& receipt.transport == "model-connector"
&& receipt.provider == "model-connector"
&& receipt.outcome == TransportOutcome::Failed {
failure_kind: "result_size".to_string(),
kind: FailureKind::ResultSize,
message: "provider result exceeded its byte bound".to_string(),
}
)

View file

@ -7,7 +7,7 @@ use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
use crate::contract::{
ContractError, RECEIPT_SCHEMA_ID, RESULT_SCHEMA_ID, Refusal, TransportDisposition,
ContractError, FailureKind, RECEIPT_SCHEMA_ID, RESULT_SCHEMA_ID, Refusal, TransportDisposition,
TransportInvocation, TransportKey, TransportOutcome, TransportReceipt, TransportResult,
canonical_invocation_bytes, require_id,
};
@ -228,18 +228,16 @@ impl TransportService {
};
}
if envelope.ciphertext.len() > caller.max_payload_bytes {
return self.reply_refusal(&invocation, Refusal::Policy, &security);
return self.reply_refusal(&invocation, Refusal::PayloadBound, &security);
}
if let Err(error) = invocation.validate(now_unix_ms, self.max_expiry_skew_ms) {
return self.reply_refusal(&invocation, contract_refusal(&error), &security);
}
if !caller.allowed_models.contains(&invocation.request.model)
|| invocation
.request
.max_output_tokens
.is_some_and(|limit| limit > caller.max_output_tokens)
{
return self.reply_refusal(&invocation, Refusal::Policy, &security);
if !caller.allowed_models.contains(&invocation.request.model) {
return self.reply_refusal(&invocation, Refusal::ModelNotAllowed, &security);
}
if invocation.request.max_output_tokens > caller.max_output_tokens {
return self.reply_refusal(&invocation, Refusal::OutputBound, &security);
}
let identity = RequestIdentity {
@ -428,8 +426,8 @@ fn fits_frame(envelope: &TransportEnvelope, max_frame_bytes: usize) -> Result<bo
.map_err(|_| ServiceError::Encoding)
}
/// The transport named on the stand-in failure, fixed so its size is bounded.
const STAND_IN_TRANSPORT: &str = "model-connector";
/// The provider named on the stand-in failure, fixed so its size is bounded.
const STAND_IN_PROVIDER: &str = "model-connector";
/// The failed result that stands in for one whose sealed frame cannot be
/// carried: the same shape a provider failure takes, with nothing a provider
@ -454,9 +452,9 @@ pub(crate) fn result_size_failure(
native_request_sha256,
provider_request_sha256,
model: model.to_string(),
transport: STAND_IN_TRANSPORT.to_string(),
provider: STAND_IN_PROVIDER.to_string(),
outcome: TransportOutcome::Failed {
failure_kind: "result_size".to_string(),
kind: FailureKind::ResultSize,
message: "provider result exceeded its byte bound".to_string(),
},
}),
@ -775,25 +773,26 @@ mod tests {
}
#[test]
fn policy_refuses_unlisted_models_and_output_tokens_over_the_caller_cap() {
fn an_unlisted_model_is_refused_model_not_allowed() {
let store = TestStore::new();
let mut service = open_service(&store, 8);
let (admission, invocation) = run(&mut service, "unlisted-model", |request| {
request.model = "other-model".to_string();
});
assert_eq!(refusal_of(admission, &invocation), Refusal::Policy);
assert_eq!(refusal_of(admission, &invocation), Refusal::ModelNotAllowed);
let (admission, _) = run(&mut service, "listed-model", |_| {});
assert!(matches!(admission, TransportAdmission::Execute(_)));
}
#[test]
fn output_tokens_over_the_caller_cap_are_refused_output_bound_inclusively() {
let store = TestStore::new();
let mut service = open_service(&store, 8);
let (admission, invocation) = run(&mut service, "over-cap", |request| {
request.max_output_tokens = Some(32_769);
request.max_output_tokens = 32_769;
});
assert_eq!(refusal_of(admission, &invocation), Refusal::Policy);
for (id, tokens) in [
("at-cap", Some(32_768)),
("under-cap", Some(100)),
("unset", None),
] {
assert_eq!(refusal_of(admission, &invocation), Refusal::OutputBound);
for (id, tokens) in [("at-cap", 32_768), ("under-cap", 100)] {
let (admission, _) = run(&mut service, id, |request| {
request.max_output_tokens = tokens;
});
@ -824,7 +823,7 @@ mod tests {
};
assert!(matches!(admit(size), Ok(TransportAdmission::Execute(_))));
let refused = admit(size - 1).unwrap();
assert_eq!(refusal_of(refused, &invocation), Refusal::Policy);
assert_eq!(refusal_of(refused, &invocation), Refusal::PayloadBound);
}
#[test]
@ -1074,7 +1073,7 @@ mod tests {
transport: &str,
) -> TransportResult {
let mut receipt = receipt(invocation);
receipt.transport = transport.to_string();
receipt.provider = transport.to_string();
TransportResult::transported(
invocation,
vec![TransportEvent {