diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bb41626 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ANTHROPIC_API_KEY=your_api_key \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d5df85..f3c4cae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .direnv +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8c8e218..b02a9bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,767 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.179" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[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.104" @@ -23,6 +772,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -32,6 +837,239 @@ 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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "reqwest" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -75,6 +1113,46 @@ dependencies = [ "zmij", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.112" @@ -86,13 +1164,76 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -106,18 +1247,610 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "worker" version = "0.1.0" dependencies = [ + "async-trait", + "eventsource-stream", + "futures", + "reqwest", + "serde", "serde_json", - "thiserror", + "tempfile", + "thiserror 1.0.69", + "tokio", "worker-macros", "worker-types", ] @@ -140,6 +1873,115 @@ dependencies = [ "serde_json", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.7" diff --git a/worker-types/src/event.rs b/worker-types/src/event.rs index 066713e..043fd2a 100644 --- a/worker-types/src/event.rs +++ b/worker-types/src/event.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; /// llm_client層が出力するフラットなイベント列挙 /// /// Timeline層がこのイベントストリームを受け取り、ブロック構造化を行う -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Event { // Meta events (not tied to a block) Ping(PingEvent), @@ -31,7 +31,7 @@ pub enum Event { // ============================================================================= /// Pingイベント(ハートビート) -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct PingEvent { pub timestamp: Option, } @@ -52,13 +52,13 @@ pub struct UsageEvent { } /// ステータスイベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct StatusEvent { pub status: ResponseStatus, } /// レスポンスステータス -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ResponseStatus { /// ストリーム開始 Started, @@ -71,7 +71,7 @@ pub enum ResponseStatus { } /// エラーイベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ErrorEvent { pub code: Option, pub message: String, @@ -82,7 +82,7 @@ pub struct ErrorEvent { // ============================================================================= /// ブロックの種別 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BlockType { /// テキスト生成 Text, @@ -95,7 +95,7 @@ pub enum BlockType { } /// ブロック開始イベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlockStart { /// ブロックのインデックス pub index: usize, @@ -112,7 +112,7 @@ impl BlockStart { } /// ブロックのメタデータ -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum BlockMetadata { Text, Thinking, @@ -121,7 +121,7 @@ pub enum BlockMetadata { } /// ブロックデルタイベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlockDelta { /// ブロックのインデックス pub index: usize, @@ -130,7 +130,7 @@ pub struct BlockDelta { } /// デルタの内容 -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum DeltaContent { /// テキストデルタ Text(String), @@ -152,7 +152,7 @@ impl DeltaContent { } /// ブロック停止イベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlockStop { /// ブロックのインデックス pub index: usize, @@ -169,7 +169,7 @@ impl BlockStop { } /// ブロック中断イベント -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlockAbort { /// ブロックのインデックス pub index: usize, @@ -186,7 +186,7 @@ impl BlockAbort { } /// 停止理由 -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum StopReason { /// 自然終了 EndTurn, diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 5dcdbe0..d70c9e0 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -4,7 +4,16 @@ version = "0.1.0" edition = "2024" [dependencies] +async-trait = "0.1.89" +eventsource-stream = "0.2.3" +futures = "0.3.31" +reqwest = { version = "0.13.1", features = ["stream", "json"] } +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" +tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] } worker-macros = { path = "../worker-macros" } worker-types = { path = "../worker-types" } + +[dev-dependencies] +tempfile = "3.24.0" diff --git a/worker/examples/llm_client_anthropic.rs b/worker/examples/llm_client_anthropic.rs new file mode 100644 index 0000000..64ee4cd --- /dev/null +++ b/worker/examples/llm_client_anthropic.rs @@ -0,0 +1,176 @@ +//! LLMクライアント + Timeline統合サンプル +//! +//! Anthropic Claude APIにリクエストを送信し、Timelineでイベントを処理するサンプル +//! +//! ## 使用方法 +//! +//! ```bash +//! # .envファイルにAPIキーを設定 +//! echo "ANTHROPIC_API_KEY=your-api-key" > .env +//! +//! # 実行 +//! cargo run --example llm_client_anthropic +//! ``` + +use std::sync::{Arc, Mutex}; + +use futures::StreamExt; +use worker::{ + Handler, TextBlockEvent, TextBlockKind, Timeline, ToolUseBlockEvent, ToolUseBlockKind, + UsageEvent, UsageKind, + llm_client::{LlmClient, Request, providers::anthropic::AnthropicClient}, +}; + +/// テキスト出力をリアルタイムで表示するハンドラー +struct PrintHandler; + +impl Handler for PrintHandler { + type Scope = (); + + fn on_event(&mut self, _scope: &mut (), event: &TextBlockEvent) { + match event { + TextBlockEvent::Start(_) => { + print!("\n🤖 Assistant: "); + } + TextBlockEvent::Delta(text) => { + print!("{}", text); + // 即時出力をフラッシュ + use std::io::Write; + std::io::stdout().flush().ok(); + } + TextBlockEvent::Stop(_) => { + println!("\n"); + } + } + } +} + +/// テキストを蓄積するハンドラー +struct TextCollector { + texts: Arc>>, +} + +impl Handler for TextCollector { + type Scope = String; + + fn on_event(&mut self, buffer: &mut String, event: &TextBlockEvent) { + match event { + TextBlockEvent::Start(_) => {} + TextBlockEvent::Delta(text) => { + buffer.push_str(text); + } + TextBlockEvent::Stop(_) => { + let text = std::mem::take(buffer); + self.texts.lock().unwrap().push(text); + } + } + } +} + +/// ツール使用を検出するハンドラー +struct ToolUseDetector; + +impl Handler for ToolUseDetector { + type Scope = String; // JSON accumulator + + fn on_event(&mut self, json_buffer: &mut String, event: &ToolUseBlockEvent) { + match event { + ToolUseBlockEvent::Start(start) => { + println!("\n🔧 Tool Call: {} (id: {})", start.name, start.id); + } + ToolUseBlockEvent::InputJsonDelta(json) => { + json_buffer.push_str(json); + } + ToolUseBlockEvent::Stop(stop) => { + println!(" Arguments: {}", json_buffer); + println!(" Tool {} completed\n", stop.name); + } + } + } +} + +/// 使用量を追跡するハンドラー +struct UsageTracker { + total_input: Arc>, + total_output: Arc>, +} + +impl Handler for UsageTracker { + type Scope = (); + + fn on_event(&mut self, _scope: &mut (), event: &UsageEvent) { + if let Some(input) = event.input_tokens { + *self.total_input.lock().unwrap() += input; + } + if let Some(output) = event.output_tokens { + *self.total_output.lock().unwrap() += output; + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // APIキーを環境変数から取得 + let api_key = std::env::var("ANTHROPIC_API_KEY") + .expect("ANTHROPIC_API_KEY environment variable must be set"); + + println!("=== LLM Client + Timeline Integration Example ===\n"); + + // クライアントを作成 + let client = AnthropicClient::new(api_key, "claude-sonnet-4-20250514"); + + // 共有状態 + let collected_texts = Arc::new(Mutex::new(Vec::new())); + let total_input = Arc::new(Mutex::new(0u64)); + let total_output = Arc::new(Mutex::new(0u64)); + + // タイムラインを構築 + let mut timeline = Timeline::new(); + timeline + .on_text_block(PrintHandler) + .on_text_block(TextCollector { + texts: collected_texts.clone(), + }) + .on_tool_use_block(ToolUseDetector) + .on_usage(UsageTracker { + total_input: total_input.clone(), + total_output: total_output.clone(), + }); + + // リクエストを作成 + let request = Request::new() + .system("You are a helpful assistant. Be concise.") + .user("What is the capital of Japan? Answer in one sentence.") + .max_tokens(100); + + println!("📤 Sending request...\n"); + + // ストリーミングリクエストを送信 + let mut stream = client.stream(request).await?; + + // イベントを処理 + while let Some(result) = stream.next().await { + match result { + Ok(event) => { + timeline.dispatch(&event); + } + Err(e) => { + eprintln!("Error: {}", e); + break; + } + } + } + + // 結果を表示 + println!("=== Summary ==="); + println!( + "📊 Token Usage: {} input, {} output", + total_input.lock().unwrap(), + total_output.lock().unwrap() + ); + + let texts = collected_texts.lock().unwrap(); + println!("📝 Collected {} text block(s)", texts.len()); + + Ok(()) +} diff --git a/worker/examples/record_anthropic.rs b/worker/examples/record_anthropic.rs new file mode 100644 index 0000000..0cfa02a --- /dev/null +++ b/worker/examples/record_anthropic.rs @@ -0,0 +1,118 @@ +//! APIレスポンス記録ツール +//! +//! 実際のAnthropicAPIからのレスポンスをファイルに記録する。 +//! 後でテストフィクスチャとして使用可能。 +//! +//! ## 使用方法 +//! +//! ```bash +//! # 記録モード (APIを呼び出して記録) +//! ANTHROPIC_API_KEY=your-key cargo run --example record_anthropic +//! +//! # 記録されたファイルは worker/tests/fixtures/ に保存される +//! ``` + +use std::fs::{self, File}; +use std::io::{BufWriter, Write}; +use std::path::Path; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use futures::StreamExt; +use worker::llm_client::{LlmClient, Request, providers::anthropic::AnthropicClient}; + +/// 記録されたSSEイベント +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct RecordedEvent { + elapsed_ms: u64, + event_type: String, + data: String, +} + +/// セッションメタデータ +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct SessionMetadata { + timestamp: u64, + model: String, + description: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let api_key = std::env::var("ANTHROPIC_API_KEY") + .expect("ANTHROPIC_API_KEY environment variable must be set"); + + let model = "claude-sonnet-4-20250514"; + let description = "Simple greeting test"; + + println!("=== Anthropic API Response Recorder ===\n"); + println!("Model: {}", model); + println!("Description: {}\n", description); + + // クライアントを作成 + let client = AnthropicClient::new(&api_key, model); + + // シンプルなリクエスト + let request = Request::new() + .system("You are a helpful assistant. Be very concise.") + .user("Say hello in one word.") + .max_tokens(50); + + println!("📤 Sending request...\n"); + + // レスポンスを記録 + let start_time = Instant::now(); + let mut events: Vec = Vec::new(); + + let mut stream = client.stream(request).await?; + + while let Some(result) = stream.next().await { + let elapsed = start_time.elapsed().as_millis() as u64; + match result { + Ok(event) => { + // Eventをシリアライズして記録 + let event_json = serde_json::to_string(&event)?; + println!("[{:>6}ms] {:?}", elapsed, event); + events.push(RecordedEvent { + elapsed_ms: elapsed, + event_type: format!("{:?}", std::mem::discriminant(&event)), + data: event_json, + }); + } + Err(e) => { + eprintln!("Error: {}", e); + break; + } + } + } + + println!("\n📊 Recorded {} events", events.len()); + + // ファイルに保存 + let fixtures_dir = Path::new("worker/tests/fixtures"); + fs::create_dir_all(fixtures_dir)?; + + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let filename = format!("anthropic_{}.jsonl", timestamp); + let filepath = fixtures_dir.join(&filename); + + let file = File::create(&filepath)?; + let mut writer = BufWriter::new(file); + + // メタデータを書き込み + let metadata = SessionMetadata { + timestamp, + model: model.to_string(), + description: description.to_string(), + }; + writeln!(writer, "{}", serde_json::to_string(&metadata)?)?; + + // イベントを書き込み + for event in &events { + writeln!(writer, "{}", serde_json::to_string(event)?)?; + } + writer.flush()?; + + println!("💾 Saved to: {}", filepath.display()); + + Ok(()) +} diff --git a/worker/examples/timeline_basic.rs b/worker/examples/timeline_basic.rs index bb3cff2..7074a87 100644 --- a/worker/examples/timeline_basic.rs +++ b/worker/examples/timeline_basic.rs @@ -3,8 +3,8 @@ //! 設計ドキュメントに基づいたTimelineの使用パターンを示すサンプル use worker::{ - Event, Handler, TextBlockEvent, TextBlockKind, Timeline, - ToolUseBlockEvent, ToolUseBlockKind, UsageEvent, UsageKind, + Event, Handler, TextBlockEvent, TextBlockKind, Timeline, ToolUseBlockEvent, ToolUseBlockKind, + UsageEvent, UsageKind, }; fn main() { @@ -81,7 +81,9 @@ struct TextCollector { impl TextCollector { fn new() -> Self { - Self { results: Vec::new() } + Self { + results: Vec::new(), + } } } diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 3b13109..0bcb17b 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -2,8 +2,10 @@ //! //! このクレートは以下を提供します: //! - Timeline: イベントストリームの状態管理とハンドラーへのディスパッチ +//! - LlmClient: LLMプロバイダとの通信 //! - 型消去されたHandler実装 +pub mod llm_client; mod timeline; pub use timeline::*; diff --git a/worker/src/llm_client/client.rs b/worker/src/llm_client/client.rs new file mode 100644 index 0000000..5ee83e1 --- /dev/null +++ b/worker/src/llm_client/client.rs @@ -0,0 +1,28 @@ +//! LLMクライアント共通trait定義 + +use std::pin::Pin; + +use async_trait::async_trait; +use futures::Stream; +use worker_types::Event; + +use crate::llm_client::{ClientError, Request}; + +/// LLMクライアントのtrait +/// +/// 各プロバイダはこのtraitを実装し、統一されたインターフェースを提供する。 +#[async_trait] +pub trait LlmClient: Send + Sync { + /// ストリーミングリクエストを送信し、Eventストリームを返す + /// + /// # Arguments + /// * `request` - リクエスト情報 + /// + /// # Returns + /// * `Ok(Stream)` - イベントストリーム + /// * `Err(ClientError)` - エラー + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError>; +} diff --git a/worker/src/llm_client/error.rs b/worker/src/llm_client/error.rs new file mode 100644 index 0000000..02ecbf1 --- /dev/null +++ b/worker/src/llm_client/error.rs @@ -0,0 +1,69 @@ +//! LLMクライアントエラー型 + +use std::fmt; + +/// LLMクライアントのエラー +#[derive(Debug)] +pub enum ClientError { + /// HTTPリクエストエラー + Http(reqwest::Error), + /// JSONパースエラー + Json(serde_json::Error), + /// SSEパースエラー + Sse(String), + /// APIエラー (プロバイダからのエラーレスポンス) + Api { + status: Option, + code: Option, + message: String, + }, + /// 設定エラー + Config(String), +} + +impl fmt::Display for ClientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ClientError::Http(e) => write!(f, "HTTP error: {}", e), + ClientError::Json(e) => write!(f, "JSON parse error: {}", e), + ClientError::Sse(msg) => write!(f, "SSE parse error: {}", msg), + ClientError::Api { + status, + code, + message, + } => { + write!(f, "API error")?; + if let Some(s) = status { + write!(f, " (status: {})", s)?; + } + if let Some(c) = code { + write!(f, " [{}]", c)?; + } + write!(f, ": {}", message) + } + ClientError::Config(msg) => write!(f, "Config error: {}", msg), + } + } +} + +impl std::error::Error for ClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ClientError::Http(e) => Some(e), + ClientError::Json(e) => Some(e), + _ => None, + } + } +} + +impl From for ClientError { + fn from(err: reqwest::Error) -> Self { + ClientError::Http(err) + } +} + +impl From for ClientError { + fn from(err: serde_json::Error) -> Self { + ClientError::Json(err) + } +} diff --git a/worker/src/llm_client/mod.rs b/worker/src/llm_client/mod.rs new file mode 100644 index 0000000..404b2e6 --- /dev/null +++ b/worker/src/llm_client/mod.rs @@ -0,0 +1,24 @@ +//! LLMクライアント層 +//! +//! LLMプロバイダと通信し、統一された`Event`ストリームを出力する。 +//! +//! # アーキテクチャ +//! +//! - **client**: `LlmClient` trait定義 +//! - **scheme**: APIスキーマ(リクエスト/レスポンス変換) +//! - **providers**: プロバイダ固有のHTTPクライアント実装 +//! - **testing**: テスト用のAPIレスポンス記録・再生機能 + +pub mod client; +pub mod error; +pub mod types; + +pub mod providers; +pub(crate) mod scheme; + +#[cfg(test)] +pub mod testing; + +pub use client::*; +pub use error::*; +pub use types::*; diff --git a/worker/src/llm_client/providers/anthropic.rs b/worker/src/llm_client/providers/anthropic.rs new file mode 100644 index 0000000..0f87782 --- /dev/null +++ b/worker/src/llm_client/providers/anthropic.rs @@ -0,0 +1,193 @@ +//! Anthropic プロバイダ実装 +//! +//! Anthropic Messages APIと通信し、Eventストリームを出力 + +use std::pin::Pin; + +use async_trait::async_trait; +use eventsource_stream::Eventsource; +use futures::{Stream, StreamExt, TryStreamExt}; +use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; +use worker_types::Event; + +use crate::llm_client::{ClientError, LlmClient, Request, scheme::anthropic::AnthropicScheme}; + +/// Anthropic クライアント +pub struct AnthropicClient { + /// HTTPクライアント + http_client: reqwest::Client, + /// APIキー + api_key: String, + /// モデル名 + model: String, + /// スキーマ + scheme: AnthropicScheme, + /// ベースURL + base_url: String, +} + +impl AnthropicClient { + /// 新しいAnthropicクライアントを作成 + pub fn new(api_key: impl Into, model: impl Into) -> Self { + Self { + http_client: reqwest::Client::new(), + api_key: api_key.into(), + model: model.into(), + scheme: AnthropicScheme::default(), + base_url: "https://api.anthropic.com".to_string(), + } + } + + /// カスタムHTTPクライアントを設定 + pub fn with_http_client(mut self, client: reqwest::Client) -> Self { + self.http_client = client; + self + } + + /// スキーマを設定 + pub fn with_scheme(mut self, scheme: AnthropicScheme) -> Self { + self.scheme = scheme; + self + } + + /// ベースURLを設定 + pub fn with_base_url(mut self, url: impl Into) -> Self { + self.base_url = url.into(); + self + } + + /// リクエストヘッダーを構築 + fn build_headers(&self) -> Result { + let mut headers = HeaderMap::new(); + + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert( + "x-api-key", + HeaderValue::from_str(&self.api_key) + .map_err(|e| ClientError::Config(format!("Invalid API key: {}", e)))?, + ); + headers.insert( + "anthropic-version", + HeaderValue::from_str(&self.scheme.api_version) + .map_err(|e| ClientError::Config(format!("Invalid API version: {}", e)))?, + ); + + // 細粒度ツールストリーミングを有効にする場合 + if self.scheme.fine_grained_tool_streaming { + headers.insert( + "anthropic-beta", + HeaderValue::from_static("fine-grained-tool-streaming-2025-05-14"), + ); + } + + Ok(headers) + } +} + +#[async_trait] +impl LlmClient for AnthropicClient { + async fn stream( + &self, + request: Request, + ) -> Result> + Send>>, ClientError> { + let url = format!("{}/v1/messages", self.base_url); + let headers = self.build_headers()?; + let body = self.scheme.build_request(&self.model, &request); + + let response = self + .http_client + .post(&url) + .headers(headers) + .json(&body) + .send() + .await?; + + // エラーレスポンスをチェック + if !response.status().is_success() { + let status = response.status().as_u16(); + let text = response.text().await.unwrap_or_default(); + + // JSONでエラーをパースしてみる + if let Ok(json) = serde_json::from_str::(&text) { + let error = json.get("error").unwrap_or(&json); + let code = error.get("type").and_then(|v| v.as_str()).map(String::from); + let message = error + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or(&text) + .to_string(); + return Err(ClientError::Api { + status: Some(status), + code, + message, + }); + } + + return Err(ClientError::Api { + status: Some(status), + code: None, + message: text, + }); + } + + // SSEストリームを構築 + let scheme = self.scheme.clone(); + let byte_stream = response + .bytes_stream() + .map_err(|e| std::io::Error::other(e)); + let event_stream = byte_stream.eventsource(); + + // 現在のブロックタイプを追跡するための状態 + // Note: Streamではmutableな状態を直接保持できないため、 + // BlockStopイベントでblock_typeを正しく設定するには追加の処理が必要 + let stream = event_stream.map(move |result| { + match result { + Ok(event) => { + // SSEイベントをパース + match scheme.parse_event(&event.event, &event.data) { + Ok(Some(evt)) => Ok(evt), + Ok(None) => { + // イベントを無視(空のStatusで代用し、後でフィルタ) + // 実際にはOptionを返すべきだが、Stream型の都合上こうする + Ok(Event::Ping(worker_types::PingEvent { timestamp: None })) + } + Err(e) => Err(e), + } + } + Err(e) => Err(ClientError::Sse(e.to_string())), + } + }); + + Ok(Box::pin(stream)) + } +} + +impl Clone for AnthropicScheme { + fn clone(&self) -> Self { + Self { + api_version: self.api_version.clone(), + fine_grained_tool_streaming: self.fine_grained_tool_streaming, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_creation() { + let client = AnthropicClient::new("test-key", "claude-sonnet-4-20250514"); + assert_eq!(client.model, "claude-sonnet-4-20250514"); + } + + #[test] + fn test_build_headers() { + let client = AnthropicClient::new("test-key", "claude-sonnet-4-20250514"); + let headers = client.build_headers().unwrap(); + + assert!(headers.contains_key("x-api-key")); + assert!(headers.contains_key("anthropic-version")); + assert!(headers.contains_key("anthropic-beta")); + } +} diff --git a/worker/src/llm_client/providers/mod.rs b/worker/src/llm_client/providers/mod.rs new file mode 100644 index 0000000..4351076 --- /dev/null +++ b/worker/src/llm_client/providers/mod.rs @@ -0,0 +1,5 @@ +//! プロバイダ実装 +//! +//! 各プロバイダ固有のHTTPクライアント実装 + +pub mod anthropic; diff --git a/worker/src/llm_client/scheme/anthropic/events.rs b/worker/src/llm_client/scheme/anthropic/events.rs new file mode 100644 index 0000000..5bb0748 --- /dev/null +++ b/worker/src/llm_client/scheme/anthropic/events.rs @@ -0,0 +1,372 @@ +//! Anthropic SSEイベントパース +//! +//! Anthropic Messages APIのSSEイベントをパースし、統一Event型に変換 + +use serde::Deserialize; +use worker_types::{ + BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent, ErrorEvent, Event, + PingEvent, ResponseStatus, StatusEvent, UsageEvent, +}; + +use crate::llm_client::ClientError; + +use super::AnthropicScheme; + +/// Anthropic SSEイベントタイプ +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum AnthropicEventType { + MessageStart, + ContentBlockStart, + ContentBlockDelta, + ContentBlockStop, + MessageDelta, + MessageStop, + Ping, + Error, +} + +impl AnthropicEventType { + /// イベントタイプ文字列からパース + pub(crate) fn parse(s: &str) -> Option { + match s { + "message_start" => Some(Self::MessageStart), + "content_block_start" => Some(Self::ContentBlockStart), + "content_block_delta" => Some(Self::ContentBlockDelta), + "content_block_stop" => Some(Self::ContentBlockStop), + "message_delta" => Some(Self::MessageDelta), + "message_stop" => Some(Self::MessageStop), + "ping" => Some(Self::Ping), + "error" => Some(Self::Error), + _ => None, + } + } +} + +// ============================================================================ +// SSEイベントのJSON構造 +// ============================================================================ + +/// message_start イベント +#[derive(Debug, Deserialize)] +pub(crate) struct MessageStartEvent { + pub message: MessageStartMessage, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct MessageStartMessage { + pub id: String, + pub model: String, + pub usage: Option, +} + +/// content_block_start イベント +#[derive(Debug, Deserialize)] +pub(crate) struct ContentBlockStartEvent { + pub index: usize, + pub content_block: ContentBlock, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub(crate) enum ContentBlock { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "thinking")] + Thinking { thinking: String }, + #[serde(rename = "tool_use")] + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, +} + +/// content_block_delta イベント +#[derive(Debug, Deserialize)] +pub(crate) struct ContentBlockDeltaEvent { + pub index: usize, + pub delta: DeltaBlock, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub(crate) enum DeltaBlock { + #[serde(rename = "text_delta")] + TextDelta { text: String }, + #[serde(rename = "thinking_delta")] + ThinkingDelta { thinking: String }, + #[serde(rename = "input_json_delta")] + InputJsonDelta { partial_json: String }, + #[serde(rename = "signature_delta")] + SignatureDelta { signature: String }, +} + +/// content_block_stop イベント +#[derive(Debug, Deserialize)] +pub(crate) struct ContentBlockStopEvent { + pub index: usize, +} + +/// message_delta イベント +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct MessageDeltaEvent { + pub delta: MessageDeltaData, + pub usage: Option, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct MessageDeltaData { + pub stop_reason: Option, + pub stop_sequence: Option, +} + +/// 使用量データ +#[derive(Debug, Deserialize)] +pub(crate) struct UsageData { + pub input_tokens: Option, + pub output_tokens: Option, + pub cache_read_input_tokens: Option, + pub cache_creation_input_tokens: Option, +} + +/// エラーイベント +#[derive(Debug, Deserialize)] +pub(crate) struct ErrorEventData { + pub error: ErrorDetail, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct ErrorDetail { + #[serde(rename = "type")] + pub error_type: String, + pub message: String, +} + +// ============================================================================ +// イベント変換 +// ============================================================================ + +impl AnthropicScheme { + /// SSEイベントをEvent型に変換 + /// + /// # Arguments + /// * `event_type` - SSEイベントタイプ + /// * `data` - イベントデータJSON文字列 + /// + /// # Returns + /// * `Ok(Some(Event))` - 変換成功 + /// * `Ok(None)` - イベントを無視(unknown event等) + /// * `Err(ClientError)` - パースエラー + pub(crate) fn parse_event( + &self, + event_type: &str, + data: &str, + ) -> Result, ClientError> { + let Some(event_type) = AnthropicEventType::parse(event_type) else { + // Unknown event type, ignore + return Ok(None); + }; + + match event_type { + AnthropicEventType::MessageStart => { + let event: MessageStartEvent = serde_json::from_str(data)?; + // message_start時にUsageイベントがあれば出力 + if let Some(usage) = event.message.usage { + return Ok(Some(Event::Usage(self.convert_usage(&usage)))); + } + // Statusイベントとして開始を通知 + Ok(Some(Event::Status(StatusEvent { + status: ResponseStatus::Started, + }))) + } + AnthropicEventType::ContentBlockStart => { + let event: ContentBlockStartEvent = serde_json::from_str(data)?; + Ok(Some(self.convert_block_start(&event))) + } + AnthropicEventType::ContentBlockDelta => { + let event: ContentBlockDeltaEvent = serde_json::from_str(data)?; + Ok(self.convert_block_delta(&event)) + } + AnthropicEventType::ContentBlockStop => { + let event: ContentBlockStopEvent = serde_json::from_str(data)?; + // Note: BlockStopにはblock_typeが必要だが、ここでは追跡していない + // プロバイダ層で状態を追跡する必要がある + Ok(Some(Event::BlockStop(BlockStop { + index: event.index, + block_type: BlockType::Text, // プロバイダ層で上書きされる + stop_reason: None, + }))) + } + AnthropicEventType::MessageDelta => { + let event: MessageDeltaEvent = serde_json::from_str(data)?; + // Usage情報があれば出力 + if let Some(usage) = event.usage { + return Ok(Some(Event::Usage(self.convert_usage(&usage)))); + } + Ok(None) + } + AnthropicEventType::MessageStop => Ok(Some(Event::Status(StatusEvent { + status: ResponseStatus::Completed, + }))), + AnthropicEventType::Ping => Ok(Some(Event::Ping(PingEvent { timestamp: None }))), + AnthropicEventType::Error => { + let event: ErrorEventData = serde_json::from_str(data)?; + Ok(Some(Event::Error(ErrorEvent { + code: Some(event.error.error_type), + message: event.error.message, + }))) + } + } + } + + fn convert_block_start(&self, event: &ContentBlockStartEvent) -> Event { + let (block_type, metadata) = match &event.content_block { + ContentBlock::Text { .. } => (BlockType::Text, BlockMetadata::Text), + ContentBlock::Thinking { .. } => (BlockType::Thinking, BlockMetadata::Thinking), + ContentBlock::ToolUse { id, name, .. } => ( + BlockType::ToolUse, + BlockMetadata::ToolUse { + id: id.clone(), + name: name.clone(), + }, + ), + }; + + Event::BlockStart(BlockStart { + index: event.index, + block_type, + metadata, + }) + } + + fn convert_block_delta(&self, event: &ContentBlockDeltaEvent) -> Option { + let delta = match &event.delta { + DeltaBlock::TextDelta { text } => DeltaContent::Text(text.clone()), + DeltaBlock::ThinkingDelta { thinking } => DeltaContent::Thinking(thinking.clone()), + DeltaBlock::InputJsonDelta { partial_json } => { + DeltaContent::InputJson(partial_json.clone()) + } + DeltaBlock::SignatureDelta { .. } => { + // signature_delta は無視 + return None; + } + }; + + Some(Event::BlockDelta(BlockDelta { + index: event.index, + delta, + })) + } + + fn convert_usage(&self, usage: &UsageData) -> UsageEvent { + let input = usage.input_tokens.unwrap_or(0); + let output = usage.output_tokens.unwrap_or(0); + UsageEvent { + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: Some(input + output), + cache_read_input_tokens: usage.cache_read_input_tokens, + cache_creation_input_tokens: usage.cache_creation_input_tokens, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_message_start() { + let scheme = AnthropicScheme::new(); + let data = r#"{"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-20250514","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0}}}"#; + + let event = scheme.parse_event("message_start", data).unwrap().unwrap(); + match event { + Event::Usage(u) => { + assert_eq!(u.input_tokens, Some(10)); + } + _ => panic!("Expected Usage event"), + } + } + + #[test] + fn test_parse_content_block_start_text() { + let scheme = AnthropicScheme::new(); + let data = + r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#; + + let event = scheme + .parse_event("content_block_start", data) + .unwrap() + .unwrap(); + match event { + Event::BlockStart(s) => { + assert_eq!(s.index, 0); + assert_eq!(s.block_type, BlockType::Text); + } + _ => panic!("Expected BlockStart event"), + } + } + + #[test] + fn test_parse_content_block_delta_text() { + let scheme = AnthropicScheme::new(); + let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#; + + let event = scheme + .parse_event("content_block_delta", data) + .unwrap() + .unwrap(); + match event { + Event::BlockDelta(d) => { + assert_eq!(d.index, 0); + match d.delta { + DeltaContent::Text(t) => assert_eq!(t, "Hello"), + _ => panic!("Expected Text delta"), + } + } + _ => panic!("Expected BlockDelta event"), + } + } + + #[test] + fn test_parse_tool_use_start() { + let scheme = AnthropicScheme::new(); + let data = r#"{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_123","name":"get_weather","input":{}}}"#; + + let event = scheme + .parse_event("content_block_start", data) + .unwrap() + .unwrap(); + match event { + Event::BlockStart(s) => { + assert_eq!(s.block_type, BlockType::ToolUse); + match s.metadata { + BlockMetadata::ToolUse { id, name } => { + assert_eq!(id, "toolu_123"); + assert_eq!(name, "get_weather"); + } + _ => panic!("Expected ToolUse metadata"), + } + } + _ => panic!("Expected BlockStart event"), + } + } + + #[test] + fn test_parse_ping() { + let scheme = AnthropicScheme::new(); + let data = r#"{"type":"ping"}"#; + + let event = scheme.parse_event("ping", data).unwrap().unwrap(); + match event { + Event::Ping(_) => {} + _ => panic!("Expected Ping event"), + } + } +} diff --git a/worker/src/llm_client/scheme/anthropic/mod.rs b/worker/src/llm_client/scheme/anthropic/mod.rs new file mode 100644 index 0000000..997da4b --- /dev/null +++ b/worker/src/llm_client/scheme/anthropic/mod.rs @@ -0,0 +1,39 @@ +//! Anthropic Messages API スキーマ +//! +//! - リクエストJSON生成 +//! - SSEイベントパース → Event変換 + +mod events; +mod request; + +/// Anthropicスキーマ +/// +/// Anthropic Messages APIのリクエスト/レスポンス変換を担当 +pub struct AnthropicScheme { + /// APIバージョン + pub api_version: String, + /// 細粒度ツールストリーミングを有効にするか + pub fine_grained_tool_streaming: bool, +} + +impl Default for AnthropicScheme { + fn default() -> Self { + Self { + api_version: "2023-06-01".to_string(), + fine_grained_tool_streaming: true, + } + } +} + +impl AnthropicScheme { + /// 新しいスキーマを作成 + pub fn new() -> Self { + Self::default() + } + + /// 細粒度ツールストリーミングを有効/無効にする + pub fn with_fine_grained_tool_streaming(mut self, enabled: bool) -> Self { + self.fine_grained_tool_streaming = enabled; + self + } +} diff --git a/worker/src/llm_client/scheme/anthropic/request.rs b/worker/src/llm_client/scheme/anthropic/request.rs new file mode 100644 index 0000000..c554257 --- /dev/null +++ b/worker/src/llm_client/scheme/anthropic/request.rs @@ -0,0 +1,195 @@ +//! Anthropic リクエスト生成 + +use serde::Serialize; + +use crate::llm_client::{ + Request, + types::{ContentPart, Message, MessageContent, Role, ToolDefinition}, +}; + +use super::AnthropicScheme; + +/// Anthropic APIへのリクエストボディ +#[derive(Debug, Serialize)] +pub(crate) struct AnthropicRequest { + pub model: String, + pub max_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub system: Option, + pub messages: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub top_p: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub stop_sequences: Vec, + pub stream: bool, +} + +/// Anthropic メッセージ +#[derive(Debug, Serialize)] +pub(crate) struct AnthropicMessage { + pub role: String, + pub content: AnthropicContent, +} + +/// Anthropic コンテンツ +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum AnthropicContent { + Text(String), + Parts(Vec), +} + +/// Anthropic コンテンツパーツ +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +pub(crate) enum AnthropicContentPart { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "tool_use")] + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, + #[serde(rename = "tool_result")] + ToolResult { + tool_use_id: String, + content: String, + }, +} + +/// Anthropic ツール定義 +#[derive(Debug, Serialize)] +pub(crate) struct AnthropicTool { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub input_schema: serde_json::Value, +} + +impl AnthropicScheme { + /// RequestからAnthropicのリクエストボディを構築 + pub(crate) fn build_request(&self, model: &str, request: &Request) -> AnthropicRequest { + let messages = request + .messages + .iter() + .map(|m| self.convert_message(m)) + .collect(); + + let tools = request.tools.iter().map(|t| self.convert_tool(t)).collect(); + + AnthropicRequest { + model: model.to_string(), + max_tokens: request.config.max_tokens.unwrap_or(4096), + system: request.system_prompt.clone(), + messages, + tools, + temperature: request.config.temperature, + top_p: request.config.top_p, + stop_sequences: request.config.stop_sequences.clone(), + stream: true, + } + } + + fn convert_message(&self, message: &Message) -> AnthropicMessage { + let role = match message.role { + Role::User => "user", + Role::Assistant => "assistant", + }; + + let content = match &message.content { + MessageContent::Text(text) => AnthropicContent::Text(text.clone()), + MessageContent::ToolResult { + tool_use_id, + content, + } => AnthropicContent::Parts(vec![AnthropicContentPart::ToolResult { + tool_use_id: tool_use_id.clone(), + content: content.clone(), + }]), + MessageContent::Parts(parts) => { + let converted: Vec<_> = parts + .iter() + .map(|p| match p { + ContentPart::Text { text } => { + AnthropicContentPart::Text { text: text.clone() } + } + ContentPart::ToolUse { id, name, input } => AnthropicContentPart::ToolUse { + id: id.clone(), + name: name.clone(), + input: input.clone(), + }, + ContentPart::ToolResult { + tool_use_id, + content, + } => AnthropicContentPart::ToolResult { + tool_use_id: tool_use_id.clone(), + content: content.clone(), + }, + }) + .collect(); + AnthropicContent::Parts(converted) + } + }; + + AnthropicMessage { + role: role.to_string(), + content, + } + } + + fn convert_tool(&self, tool: &ToolDefinition) -> AnthropicTool { + AnthropicTool { + name: tool.name.clone(), + description: tool.description.clone(), + input_schema: tool.input_schema.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_simple_request() { + let scheme = AnthropicScheme::new(); + let request = Request::new() + .system("You are a helpful assistant.") + .user("Hello!"); + + let anthropic_req = scheme.build_request("claude-sonnet-4-20250514", &request); + + assert_eq!(anthropic_req.model, "claude-sonnet-4-20250514"); + assert_eq!( + anthropic_req.system, + Some("You are a helpful assistant.".to_string()) + ); + assert_eq!(anthropic_req.messages.len(), 1); + assert!(anthropic_req.stream); + } + + #[test] + fn test_build_request_with_tool() { + let scheme = AnthropicScheme::new(); + let request = Request::new().user("What's the weather?").tool( + ToolDefinition::new("get_weather") + .description("Get current weather") + .input_schema(serde_json::json!({ + "type": "object", + "properties": { + "location": { "type": "string" } + }, + "required": ["location"] + })), + ); + + let anthropic_req = scheme.build_request("claude-sonnet-4-20250514", &request); + + assert_eq!(anthropic_req.tools.len(), 1); + assert_eq!(anthropic_req.tools[0].name, "get_weather"); + } +} diff --git a/worker/src/llm_client/scheme/mod.rs b/worker/src/llm_client/scheme/mod.rs new file mode 100644 index 0000000..64c5e4a --- /dev/null +++ b/worker/src/llm_client/scheme/mod.rs @@ -0,0 +1,7 @@ +//! APIスキーマ定義 +//! +//! 各APIスキーマごとの変換ロジック +//! - リクエスト変換: Request → プロバイダ固有JSON +//! - レスポンス変換: SSEイベント → Event + +pub mod anthropic; diff --git a/worker/src/llm_client/testing.rs b/worker/src/llm_client/testing.rs new file mode 100644 index 0000000..1613708 --- /dev/null +++ b/worker/src/llm_client/testing.rs @@ -0,0 +1,238 @@ +//! テスト用のAPIレスポンス記録・再生機能 +//! +//! 実際のAPIレスポンスをタイムスタンプ付きで記録し、 +//! テスト時に再生できるようにする。 + +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::Path; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +/// 記録されたSSEイベント +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecordedEvent { + /// イベント受信からの経過時間 (ミリ秒) + pub elapsed_ms: u64, + /// SSEイベントタイプ + pub event_type: String, + /// SSEイベントデータ + pub data: String, +} + +/// セッションメタデータ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionMetadata { + /// 記録開始タイムスタンプ (Unix epoch秒) + pub timestamp: u64, + /// モデル名 + pub model: String, + /// リクエストの説明 + pub description: String, +} + +/// SSEイベントレコーダー +/// +/// 実際のAPIレスポンスを記録し、後でテストに使用できるようにする +pub struct EventRecorder { + start_time: Instant, + events: Vec, + metadata: SessionMetadata, +} + +impl EventRecorder { + /// 新しいレコーダーを作成 + pub fn new(model: impl Into, description: impl Into) -> Self { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + Self { + start_time: Instant::now(), + events: Vec::new(), + metadata: SessionMetadata { + timestamp, + model: model.into(), + description: description.into(), + }, + } + } + + /// イベントを記録 + pub fn record(&mut self, event_type: &str, data: &str) { + let elapsed = self.start_time.elapsed(); + self.events.push(RecordedEvent { + elapsed_ms: elapsed.as_millis() as u64, + event_type: event_type.to_string(), + data: data.to_string(), + }); + } + + /// 記録をファイルに保存 + /// + /// フォーマット: JSONL (1行目: metadata, 2行目以降: events) + pub fn save(&self, path: impl AsRef) -> std::io::Result<()> { + let file = File::create(path)?; + let mut writer = BufWriter::new(file); + + // メタデータを書き込み + let metadata_json = serde_json::to_string(&self.metadata)?; + writeln!(writer, "{}", metadata_json)?; + + // イベントを書き込み + for event in &self.events { + let event_json = serde_json::to_string(event)?; + writeln!(writer, "{}", event_json)?; + } + + writer.flush()?; + Ok(()) + } + + /// 記録されたイベント数を取得 + pub fn event_count(&self) -> usize { + self.events.len() + } +} + +/// SSEイベントプレイヤー +/// +/// 記録されたイベントを読み込み、テストで使用する +pub struct EventPlayer { + metadata: SessionMetadata, + events: Vec, + current_index: usize, +} + +impl EventPlayer { + /// ファイルから読み込み + pub fn load(path: impl AsRef) -> std::io::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + // メタデータを読み込み + let metadata_line = lines + .next() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Empty file"))??; + let metadata: SessionMetadata = serde_json::from_str(&metadata_line)?; + + // イベントを読み込み + let mut events = Vec::new(); + for line in lines { + let line = line?; + if !line.is_empty() { + let event: RecordedEvent = serde_json::from_str(&line)?; + events.push(event); + } + } + + Ok(Self { + metadata, + events, + current_index: 0, + }) + } + + /// メタデータを取得 + pub fn metadata(&self) -> &SessionMetadata { + &self.metadata + } + + /// 全イベントを取得 + pub fn events(&self) -> &[RecordedEvent] { + &self.events + } + + /// イベント数を取得 + pub fn event_count(&self) -> usize { + self.events.len() + } + + /// 次のイベントを取得(Iterator的に使用) + pub fn next_event(&mut self) -> Option<&RecordedEvent> { + if self.current_index < self.events.len() { + let event = &self.events[self.current_index]; + self.current_index += 1; + Some(event) + } else { + None + } + } + + /// インデックスをリセット + pub fn reset(&mut self) { + self.current_index = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_record_and_playback() { + // レコーダーを作成して記録 + let mut recorder = EventRecorder::new("claude-sonnet-4-20250514", "Test recording"); + recorder.record("message_start", r#"{"type":"message_start"}"#); + recorder.record( + "content_block_start", + r#"{"type":"content_block_start","index":0}"#, + ); + recorder.record( + "content_block_delta", + r#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}"#, + ); + + // 一時ファイルに保存 + let temp_file = NamedTempFile::new().unwrap(); + recorder.save(temp_file.path()).unwrap(); + + // 読み込んで確認 + let player = EventPlayer::load(temp_file.path()).unwrap(); + assert_eq!(player.metadata().model, "claude-sonnet-4-20250514"); + assert_eq!(player.event_count(), 3); + assert_eq!(player.events()[0].event_type, "message_start"); + assert_eq!(player.events()[2].event_type, "content_block_delta"); + } + + #[test] + fn test_player_iteration() { + // テストデータを直接作成 + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!( + temp_file, + r#"{{"timestamp":1704067200,"model":"test","description":"test"}}"# + ) + .unwrap(); + writeln!( + temp_file, + r#"{{"elapsed_ms":0,"event_type":"ping","data":"{{}}"}}"# + ) + .unwrap(); + writeln!( + temp_file, + r#"{{"elapsed_ms":100,"event_type":"message_stop","data":"{{}}"}}"# + ) + .unwrap(); + temp_file.flush().unwrap(); + + let mut player = EventPlayer::load(temp_file.path()).unwrap(); + + let first = player.next_event().unwrap(); + assert_eq!(first.event_type, "ping"); + + let second = player.next_event().unwrap(); + assert_eq!(second.event_type, "message_stop"); + + assert!(player.next_event().is_none()); + + // リセット後は最初から + player.reset(); + assert_eq!(player.next_event().unwrap().event_type, "ping"); + } +} diff --git a/worker/src/llm_client/types.rs b/worker/src/llm_client/types.rs new file mode 100644 index 0000000..ae71c57 --- /dev/null +++ b/worker/src/llm_client/types.rs @@ -0,0 +1,198 @@ +//! LLMクライアント共通型定義 + +use serde::{Deserialize, Serialize}; + +/// リクエスト構造体 +#[derive(Debug, Clone, Default)] +pub struct Request { + /// システムプロンプト + pub system_prompt: Option, + /// メッセージ履歴 + pub messages: Vec, + /// ツール定義 + pub tools: Vec, + /// リクエスト設定 + pub config: RequestConfig, +} + +impl Request { + /// 新しいリクエストを作成 + pub fn new() -> Self { + Self::default() + } + + /// システムプロンプトを設定 + pub fn system(mut self, prompt: impl Into) -> Self { + self.system_prompt = Some(prompt.into()); + self + } + + /// ユーザーメッセージを追加 + pub fn user(mut self, content: impl Into) -> Self { + self.messages.push(Message::user(content)); + self + } + + /// アシスタントメッセージを追加 + pub fn assistant(mut self, content: impl Into) -> Self { + self.messages.push(Message::assistant(content)); + self + } + + /// メッセージを追加 + pub fn message(mut self, message: Message) -> Self { + self.messages.push(message); + self + } + + /// ツールを追加 + pub fn tool(mut self, tool: ToolDefinition) -> Self { + self.tools.push(tool); + self + } + + /// 設定を適用 + pub fn config(mut self, config: RequestConfig) -> Self { + self.config = config; + self + } + + /// max_tokensを設定 + pub fn max_tokens(mut self, max_tokens: u32) -> Self { + self.config.max_tokens = Some(max_tokens); + self + } +} + +/// メッセージ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + /// ロール + pub role: Role, + /// コンテンツ + pub content: MessageContent, +} + +impl Message { + /// ユーザーメッセージを作成 + pub fn user(content: impl Into) -> Self { + Self { + role: Role::User, + content: MessageContent::Text(content.into()), + } + } + + /// アシスタントメッセージを作成 + pub fn assistant(content: impl Into) -> Self { + Self { + role: Role::Assistant, + content: MessageContent::Text(content.into()), + } + } + + /// ツール結果メッセージを作成 + pub fn tool_result(tool_use_id: impl Into, content: impl Into) -> Self { + Self { + role: Role::User, + content: MessageContent::ToolResult { + tool_use_id: tool_use_id.into(), + content: content.into(), + }, + } + } +} + +/// ロール +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Role { + User, + Assistant, +} + +/// メッセージコンテンツ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageContent { + /// テキストコンテンツ + Text(String), + /// ツール結果 + ToolResult { + tool_use_id: String, + content: String, + }, + /// 複合コンテンツ (テキスト + ツール使用等) + Parts(Vec), +} + +/// コンテンツパーツ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ContentPart { + /// テキスト + #[serde(rename = "text")] + Text { text: String }, + /// ツール使用 + #[serde(rename = "tool_use")] + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, + /// ツール結果 + #[serde(rename = "tool_result")] + ToolResult { + tool_use_id: String, + content: String, + }, +} + +/// ツール定義 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolDefinition { + /// ツール名 + pub name: String, + /// 説明 + pub description: Option, + /// 入力スキーマ (JSON Schema) + pub input_schema: serde_json::Value, +} + +impl ToolDefinition { + /// 新しいツール定義を作成 + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + description: None, + input_schema: serde_json::json!({ + "type": "object", + "properties": {} + }), + } + } + + /// 説明を設定 + pub fn description(mut self, desc: impl Into) -> Self { + self.description = Some(desc.into()); + self + } + + /// 入力スキーマを設定 + pub fn input_schema(mut self, schema: serde_json::Value) -> Self { + self.input_schema = schema; + self + } +} + +/// リクエスト設定 +#[derive(Debug, Clone, Default)] +pub struct RequestConfig { + /// 最大トークン数 + pub max_tokens: Option, + /// Temperature + pub temperature: Option, + /// Top P + pub top_p: Option, + /// ストップシーケンス + pub stop_sequences: Vec, +} diff --git a/worker/src/timeline.rs b/worker/src/timeline.rs index f6e7612..36870d5 100644 --- a/worker/src/timeline.rs +++ b/worker/src/timeline.rs @@ -121,7 +121,8 @@ where fn dispatch_delta(&mut self, delta: &BlockDelta) { if let Some(scope) = &mut self.scope { if let DeltaContent::Text(text) = &delta.delta { - self.handler.on_event(scope, &TextBlockEvent::Delta(text.clone())); + self.handler + .on_event(scope, &TextBlockEvent::Delta(text.clone())); } } } @@ -189,7 +190,8 @@ where fn dispatch_delta(&mut self, delta: &BlockDelta) { if let Some(scope) = &mut self.scope { if let DeltaContent::Thinking(text) = &delta.delta { - self.handler.on_event(scope, &ThinkingBlockEvent::Delta(text.clone())); + self.handler + .on_event(scope, &ThinkingBlockEvent::Delta(text.clone())); } } } @@ -510,7 +512,10 @@ impl Timeline { self.current_block = None; } - fn get_block_handlers_mut(&mut self, block_type: BlockType) -> &mut Vec> { + fn get_block_handlers_mut( + &mut self, + block_type: BlockType, + ) -> &mut Vec> { match block_type { BlockType::Text => &mut self.text_block_handlers, BlockType::Thinking => &mut self.thinking_block_handlers, @@ -551,7 +556,9 @@ mod tests { } let calls = Arc::new(Mutex::new(Vec::new())); - let handler = TestUsageHandler { calls: calls.clone() }; + let handler = TestUsageHandler { + calls: calls.clone(), + }; let mut timeline = Timeline::new(); timeline.on_usage(handler); diff --git a/worker/tests/anthropic_fixtures.rs b/worker/tests/anthropic_fixtures.rs new file mode 100644 index 0000000..431c5bb --- /dev/null +++ b/worker/tests/anthropic_fixtures.rs @@ -0,0 +1,228 @@ +//! Anthropic フィクスチャベースの統合テスト +//! +//! 記録されたAPIレスポンスを使ってイベントパースをテストする + +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; + +use worker_types::{BlockType, DeltaContent, Event, ResponseStatus}; + +/// フィクスチャファイルからEventを読み込む +fn load_events_from_fixture(path: impl AsRef) -> Vec { + let file = File::open(path).expect("Failed to open fixture file"); + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + // 最初の行はメタデータ、スキップ + let _metadata = lines.next().expect("Empty fixture file").unwrap(); + + // 残りはイベント + let mut events = Vec::new(); + for line in lines { + let line = line.unwrap(); + if line.is_empty() { + continue; + } + + // RecordedEvent構造体をパース + let recorded: serde_json::Value = serde_json::from_str(&line).unwrap(); + let data = recorded["data"].as_str().unwrap(); + + // data フィールドからEventをデシリアライズ + let event: Event = serde_json::from_str(data).unwrap(); + events.push(event); + } + + events +} + +/// フィクスチャディレクトリからanthropic_*ファイルを検索 +fn find_anthropic_fixtures() -> Vec { + let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + + if !fixtures_dir.exists() { + return Vec::new(); + } + + std::fs::read_dir(&fixtures_dir) + .unwrap() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("anthropic_") && n.ends_with(".jsonl")) + }) + .collect() +} + +#[test] +fn test_fixture_events_deserialize() { + let fixtures = find_anthropic_fixtures(); + assert!(!fixtures.is_empty(), "No anthropic fixtures found"); + + for fixture_path in fixtures { + println!("Testing fixture: {:?}", fixture_path); + let events = load_events_from_fixture(&fixture_path); + + assert!(!events.is_empty(), "Fixture should contain events"); + + // 各イベントが正しくデシリアライズされているか確認 + for event in &events { + // Debugトレイトで出力可能か確認 + let _ = format!("{:?}", event); + } + + println!(" Loaded {} events", events.len()); + } +} + +#[test] +fn test_fixture_event_sequence() { + let fixtures = find_anthropic_fixtures(); + if fixtures.is_empty() { + println!("No fixtures found, skipping test"); + return; + } + + // 最初のフィクスチャをテスト + let events = load_events_from_fixture(&fixtures[0]); + + // 期待されるイベントシーケンスを検証 + // Usage -> BlockStart -> BlockDelta -> BlockStop -> Usage -> Status + + // 最初のUsageイベント + assert!( + matches!(&events[0], Event::Usage(_)), + "First event should be Usage" + ); + + // BlockStartイベント + if let Event::BlockStart(start) = &events[1] { + assert_eq!(start.block_type, BlockType::Text); + assert_eq!(start.index, 0); + } else { + panic!("Second event should be BlockStart"); + } + + // BlockDeltaイベント + if let Event::BlockDelta(delta) = &events[2] { + assert_eq!(delta.index, 0); + if let DeltaContent::Text(text) = &delta.delta { + assert!(!text.is_empty(), "Delta text should not be empty"); + println!(" Text content: {}", text); + } else { + panic!("Delta should be Text"); + } + } else { + panic!("Third event should be BlockDelta"); + } + + // BlockStopイベント + if let Event::BlockStop(stop) = &events[3] { + assert_eq!(stop.block_type, BlockType::Text); + assert_eq!(stop.index, 0); + } else { + panic!("Fourth event should be BlockStop"); + } + + // 最後のStatusイベント + if let Event::Status(status) = events.last().unwrap() { + assert_eq!(status.status, ResponseStatus::Completed); + } else { + panic!("Last event should be Status(Completed)"); + } +} + +#[test] +fn test_fixture_usage_tokens() { + let fixtures = find_anthropic_fixtures(); + if fixtures.is_empty() { + println!("No fixtures found, skipping test"); + return; + } + + let events = load_events_from_fixture(&fixtures[0]); + + // Usageイベントを収集 + let usage_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let Event::Usage(u) = e { + Some(u) + } else { + None + } + }) + .collect(); + + assert!( + !usage_events.is_empty(), + "Should have at least one Usage event" + ); + + // 最後のUsageイベントはトークン数を持つはず + let last_usage = usage_events.last().unwrap(); + assert!(last_usage.input_tokens.is_some()); + assert!(last_usage.output_tokens.is_some()); + assert!(last_usage.total_tokens.is_some()); + + println!( + " Token usage: {} input, {} output, {} total", + last_usage.input_tokens.unwrap(), + last_usage.output_tokens.unwrap(), + last_usage.total_tokens.unwrap() + ); +} + +#[test] +fn test_fixture_with_timeline() { + use std::sync::{Arc, Mutex}; + use worker::{Handler, TextBlockEvent, TextBlockKind, Timeline}; + + let fixtures = find_anthropic_fixtures(); + if fixtures.is_empty() { + println!("No fixtures found, skipping test"); + return; + } + + let events = load_events_from_fixture(&fixtures[0]); + + // テスト用ハンドラー + struct TestCollector { + texts: Arc>>, + } + + impl Handler for TestCollector { + type Scope = String; + + fn on_event(&mut self, buffer: &mut String, event: &TextBlockEvent) { + match event { + TextBlockEvent::Start(_) => {} + TextBlockEvent::Delta(text) => buffer.push_str(text), + TextBlockEvent::Stop(_) => { + let text = std::mem::take(buffer); + self.texts.lock().unwrap().push(text); + } + } + } + } + + let collected = Arc::new(Mutex::new(Vec::new())); + let mut timeline = Timeline::new(); + timeline.on_text_block(TestCollector { + texts: collected.clone(), + }); + + // フィクスチャからのイベントをTimelineにディスパッチ + for event in &events { + timeline.dispatch(event); + } + + // テキストが収集されたことを確認 + let texts = collected.lock().unwrap(); + assert_eq!(texts.len(), 1, "Should have collected one text block"); + assert!(!texts[0].is_empty(), "Collected text should not be empty"); + println!(" Collected text: {}", texts[0]); +} diff --git a/worker/tests/fixtures/anthropic_1767624445.jsonl b/worker/tests/fixtures/anthropic_1767624445.jsonl new file mode 100644 index 0000000..ac4ebc5 --- /dev/null +++ b/worker/tests/fixtures/anthropic_1767624445.jsonl @@ -0,0 +1,7 @@ +{"timestamp":1767624445,"model":"claude-sonnet-4-20250514","description":"Simple greeting test"} +{"elapsed_ms":1697,"event_type":"Discriminant(1)","data":"{\"Usage\":{\"input_tokens\":24,\"output_tokens\":2,\"total_tokens\":26,\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}"} +{"elapsed_ms":1697,"event_type":"Discriminant(4)","data":"{\"BlockStart\":{\"index\":0,\"block_type\":\"Text\",\"metadata\":\"Text\"}}"} +{"elapsed_ms":1697,"event_type":"Discriminant(5)","data":"{\"BlockDelta\":{\"index\":0,\"delta\":{\"Text\":\"Hello!\"}}}"} +{"elapsed_ms":1885,"event_type":"Discriminant(6)","data":"{\"BlockStop\":{\"index\":0,\"block_type\":\"Text\",\"stop_reason\":null}}"} +{"elapsed_ms":1929,"event_type":"Discriminant(1)","data":"{\"Usage\":{\"input_tokens\":24,\"output_tokens\":5,\"total_tokens\":29,\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}"} +{"elapsed_ms":1929,"event_type":"Discriminant(2)","data":"{\"Status\":{\"status\":\"Completed\"}}"}