diff --git a/Cargo.lock b/Cargo.lock index 01f3693..b4e9546 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/model-connector/Cargo.toml b/crates/model-connector/Cargo.toml index 49f83db..f004be5 100644 --- a/crates/model-connector/Cargo.toml +++ b/crates/model-connector/Cargo.toml @@ -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" diff --git a/crates/model-connector/src/client.rs b/crates/model-connector/src/client.rs index a1cbcb3..ab1e3de 100644 --- a/crates/model-connector/src/client.rs +++ b/crates/model-connector/src/client.rs @@ -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(); } diff --git a/crates/model-connector/src/contract.rs b/crates/model-connector/src/contract.rs index 7ae5be5..5cfffa4 100644 --- a/crates/model-connector/src/contract.rs +++ b/crates/model-connector/src/contract.rs @@ -39,17 +39,13 @@ pub struct ProviderRequest { pub model: String, pub instructions: String, pub input: Vec, - pub reasoning_effort: Option, - pub reasoning_summary: Option, - pub service_tier: Option, - pub output_format_name: Option, - pub previous_response_id: Option, + pub reasoning_effort: Option, + pub cache_retention: Option, pub tools: Vec, pub tool_choice: ToolChoice, pub parallel_tool_calls: bool, pub output_schema_json: Option, - pub max_output_tokens: Option, - pub prompt_cache_key: Option, + pub max_output_tokens: u32, } impl ProviderRequest { @@ -58,6 +54,7 @@ impl ProviderRequest { conversation_id: impl Into, model: impl Into, instructions: impl Into, + 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, +} + #[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, - input_tokens: Option, - output_tokens: Option, - reasoning_output_tokens: Option, - cached_input_tokens: Option, + 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), diff --git a/crates/model-connector/src/fixtures.rs b/crates/model-connector/src/fixtures.rs index 9b48c1d..e94ee52 100644 --- a/crates/model-connector/src/fixtures.rs +++ b/crates/model-connector/src/fixtures.rs @@ -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), + }, }, } } diff --git a/crates/model-connector/src/serve.rs b/crates/model-connector/src/serve.rs index 8eda706..7ca0a3a 100644 --- a/crates/model-connector/src/serve.rs +++ b/crates/model-connector/src/serve.rs @@ -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(), } ) diff --git a/crates/model-connector/src/service.rs b/crates/model-connector/src/service.rs index e377c19..ace2e7f 100644 --- a/crates/model-connector/src/service.rs +++ b/crates/model-connector/src/service.rs @@ -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 TransportResult { let mut receipt = receipt(invocation); - receipt.transport = transport.to_string(); + receipt.provider = transport.to_string(); TransportResult::transported( invocation, vec![TransportEvent {