Detailed changes
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fanthropic"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "zed_extension_api",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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 = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.7.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,17 @@
+[package]
+name = "anthropic"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/anthropic.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
@@ -0,0 +1,10 @@
+id = "anthropic"
+name = "Anthropic"
+description = "Anthropic Claude LLM provider for Zed."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.anthropic]
+name = "Anthropic"
@@ -0,0 +1,803 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy};
+use zed_extension_api::{self as zed, *};
+
+struct AnthropicProvider {
+ streams: Mutex<HashMap<String, StreamState>>,
+ next_stream_id: Mutex<u64>,
+}
+
+struct StreamState {
+ response_stream: Option<HttpResponseStream>,
+ buffer: String,
+ started: bool,
+ current_tool_use: Option<ToolUseState>,
+ stop_reason: Option<LlmStopReason>,
+ pending_signature: Option<String>,
+}
+
+struct ToolUseState {
+ id: String,
+ name: String,
+ input_json: String,
+}
+
+struct ModelDefinition {
+ real_id: &'static str,
+ display_name: &'static str,
+ max_tokens: u64,
+ max_output_tokens: u64,
+ supports_images: bool,
+ supports_thinking: bool,
+ is_default: bool,
+ is_default_fast: bool,
+}
+
+const MODELS: &[ModelDefinition] = &[
+ ModelDefinition {
+ real_id: "claude-opus-4-5-20251101",
+ display_name: "Claude Opus 4.5",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-opus-4-5-20251101",
+ display_name: "Claude Opus 4.5 Thinking",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-sonnet-4-5-20250929",
+ display_name: "Claude Sonnet 4.5",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: true,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-sonnet-4-5-20250929",
+ display_name: "Claude Sonnet 4.5 Thinking",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-sonnet-4-20250514",
+ display_name: "Claude Sonnet 4",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-sonnet-4-20250514",
+ display_name: "Claude Sonnet 4 Thinking",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-haiku-4-5-20251001",
+ display_name: "Claude Haiku 4.5",
+ max_tokens: 200_000,
+ max_output_tokens: 64_000,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: false,
+ is_default_fast: true,
+ },
+ ModelDefinition {
+ real_id: "claude-haiku-4-5-20251001",
+ display_name: "Claude Haiku 4.5 Thinking",
+ max_tokens: 200_000,
+ max_output_tokens: 64_000,
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-3-5-sonnet-latest",
+ display_name: "Claude 3.5 Sonnet",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "claude-3-5-haiku-latest",
+ display_name: "Claude 3.5 Haiku",
+ max_tokens: 200_000,
+ max_output_tokens: 8_192,
+ supports_images: true,
+ supports_thinking: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+];
+
+fn get_model_definition(display_name: &str) -> Option<&'static ModelDefinition> {
+ MODELS.iter().find(|m| m.display_name == display_name)
+}
+
+// Anthropic API Request Types
+
+#[derive(Serialize)]
+struct AnthropicRequest {
+ model: String,
+ max_tokens: u64,
+ messages: Vec<AnthropicMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ system: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ thinking: Option<AnthropicThinking>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ tools: Vec<AnthropicTool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_choice: Option<AnthropicToolChoice>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ stop_sequences: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ temperature: Option<f32>,
+ stream: bool,
+}
+
+#[derive(Serialize)]
+struct AnthropicThinking {
+ #[serde(rename = "type")]
+ thinking_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ budget_tokens: Option<u32>,
+}
+
+#[derive(Serialize)]
+struct AnthropicMessage {
+ role: String,
+ content: Vec<AnthropicContent>,
+}
+
+#[derive(Serialize, Clone)]
+#[serde(tag = "type")]
+enum AnthropicContent {
+ #[serde(rename = "text")]
+ Text { text: String },
+ #[serde(rename = "thinking")]
+ Thinking { thinking: String, signature: String },
+ #[serde(rename = "redacted_thinking")]
+ RedactedThinking { data: String },
+ #[serde(rename = "image")]
+ Image { source: AnthropicImageSource },
+ #[serde(rename = "tool_use")]
+ ToolUse {
+ id: String,
+ name: String,
+ input: serde_json::Value,
+ },
+ #[serde(rename = "tool_result")]
+ ToolResult {
+ tool_use_id: String,
+ is_error: bool,
+ content: String,
+ },
+}
+
+#[derive(Serialize, Clone)]
+struct AnthropicImageSource {
+ #[serde(rename = "type")]
+ source_type: String,
+ media_type: String,
+ data: String,
+}
+
+#[derive(Serialize)]
+struct AnthropicTool {
+ name: String,
+ description: String,
+ input_schema: serde_json::Value,
+}
+
+#[derive(Serialize)]
+#[serde(tag = "type", rename_all = "lowercase")]
+enum AnthropicToolChoice {
+ Auto,
+ Any,
+ None,
+}
+
+// Anthropic API Response Types
+
+#[derive(Deserialize, Debug)]
+#[serde(tag = "type")]
+#[allow(dead_code)]
+enum AnthropicEvent {
+ #[serde(rename = "message_start")]
+ MessageStart { message: AnthropicMessageResponse },
+ #[serde(rename = "content_block_start")]
+ ContentBlockStart {
+ index: usize,
+ content_block: AnthropicContentBlock,
+ },
+ #[serde(rename = "content_block_delta")]
+ ContentBlockDelta { index: usize, delta: AnthropicDelta },
+ #[serde(rename = "content_block_stop")]
+ ContentBlockStop { index: usize },
+ #[serde(rename = "message_delta")]
+ MessageDelta {
+ delta: AnthropicMessageDelta,
+ usage: AnthropicUsage,
+ },
+ #[serde(rename = "message_stop")]
+ MessageStop,
+ #[serde(rename = "ping")]
+ Ping,
+ #[serde(rename = "error")]
+ Error { error: AnthropicApiError },
+}
+
+#[derive(Deserialize, Debug)]
+struct AnthropicMessageResponse {
+ #[allow(dead_code)]
+ id: String,
+ #[allow(dead_code)]
+ role: String,
+ #[serde(default)]
+ usage: AnthropicUsage,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(tag = "type")]
+enum AnthropicContentBlock {
+ #[serde(rename = "text")]
+ Text { text: String },
+ #[serde(rename = "thinking")]
+ Thinking { thinking: String },
+ #[serde(rename = "redacted_thinking")]
+ RedactedThinking { data: String },
+ #[serde(rename = "tool_use")]
+ ToolUse { id: String, name: String },
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(tag = "type")]
+enum AnthropicDelta {
+ #[serde(rename = "text_delta")]
+ TextDelta { text: String },
+ #[serde(rename = "thinking_delta")]
+ ThinkingDelta { thinking: String },
+ #[serde(rename = "signature_delta")]
+ SignatureDelta { signature: String },
+ #[serde(rename = "input_json_delta")]
+ InputJsonDelta { partial_json: String },
+}
+
+#[derive(Deserialize, Debug)]
+struct AnthropicMessageDelta {
+ stop_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct AnthropicUsage {
+ #[serde(default)]
+ input_tokens: Option<u64>,
+ #[serde(default)]
+ output_tokens: Option<u64>,
+ #[serde(default)]
+ cache_creation_input_tokens: Option<u64>,
+ #[serde(default)]
+ cache_read_input_tokens: Option<u64>,
+}
+
+#[derive(Deserialize, Debug)]
+struct AnthropicApiError {
+ #[serde(rename = "type")]
+ #[allow(dead_code)]
+ error_type: String,
+ message: String,
+}
+
+fn convert_request(
+ model_id: &str,
+ request: &LlmCompletionRequest,
+) -> Result<AnthropicRequest, String> {
+ let model_def =
+ get_model_definition(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?;
+
+ let mut messages: Vec<AnthropicMessage> = Vec::new();
+ let mut system_message = String::new();
+
+ for msg in &request.messages {
+ match msg.role {
+ LlmMessageRole::System => {
+ for content in &msg.content {
+ if let LlmMessageContent::Text(text) = content {
+ if !system_message.is_empty() {
+ system_message.push('\n');
+ }
+ system_message.push_str(text);
+ }
+ }
+ }
+ LlmMessageRole::User => {
+ let mut contents: Vec<AnthropicContent> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ contents.push(AnthropicContent::Text { text: text.clone() });
+ }
+ }
+ LlmMessageContent::Image(img) => {
+ contents.push(AnthropicContent::Image {
+ source: AnthropicImageSource {
+ source_type: "base64".to_string(),
+ media_type: "image/png".to_string(),
+ data: img.source.clone(),
+ },
+ });
+ }
+ LlmMessageContent::ToolResult(result) => {
+ let content_text = match &result.content {
+ LlmToolResultContent::Text(t) => t.clone(),
+ LlmToolResultContent::Image(_) => "[Image]".to_string(),
+ };
+ contents.push(AnthropicContent::ToolResult {
+ tool_use_id: result.tool_use_id.clone(),
+ is_error: result.is_error,
+ content: content_text,
+ });
+ }
+ _ => {}
+ }
+ }
+
+ if !contents.is_empty() {
+ messages.push(AnthropicMessage {
+ role: "user".to_string(),
+ content: contents,
+ });
+ }
+ }
+ LlmMessageRole::Assistant => {
+ let mut contents: Vec<AnthropicContent> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ contents.push(AnthropicContent::Text { text: text.clone() });
+ }
+ }
+ LlmMessageContent::ToolUse(tool_use) => {
+ let input: serde_json::Value =
+ serde_json::from_str(&tool_use.input).unwrap_or_default();
+ contents.push(AnthropicContent::ToolUse {
+ id: tool_use.id.clone(),
+ name: tool_use.name.clone(),
+ input,
+ });
+ }
+ LlmMessageContent::Thinking(thinking) => {
+ if !thinking.text.is_empty() {
+ contents.push(AnthropicContent::Thinking {
+ thinking: thinking.text.clone(),
+ signature: thinking.signature.clone().unwrap_or_default(),
+ });
+ }
+ }
+ LlmMessageContent::RedactedThinking(data) => {
+ if !data.is_empty() {
+ contents.push(AnthropicContent::RedactedThinking {
+ data: data.clone(),
+ });
+ }
+ }
+ _ => {}
+ }
+ }
+
+ if !contents.is_empty() {
+ messages.push(AnthropicMessage {
+ role: "assistant".to_string(),
+ content: contents,
+ });
+ }
+ }
+ }
+ }
+
+ let tools: Vec<AnthropicTool> = request
+ .tools
+ .iter()
+ .map(|t| AnthropicTool {
+ name: t.name.clone(),
+ description: t.description.clone(),
+ input_schema: serde_json::from_str(&t.input_schema)
+ .unwrap_or(serde_json::Value::Object(Default::default())),
+ })
+ .collect();
+
+ let tool_choice = request.tool_choice.as_ref().map(|tc| match tc {
+ LlmToolChoice::Auto => AnthropicToolChoice::Auto,
+ LlmToolChoice::Any => AnthropicToolChoice::Any,
+ LlmToolChoice::None => AnthropicToolChoice::None,
+ });
+
+ let thinking = if model_def.supports_thinking && request.thinking_allowed {
+ Some(AnthropicThinking {
+ thinking_type: "enabled".to_string(),
+ budget_tokens: Some(4096),
+ })
+ } else {
+ None
+ };
+
+ Ok(AnthropicRequest {
+ model: model_def.real_id.to_string(),
+ max_tokens: model_def.max_output_tokens,
+ messages,
+ system: if system_message.is_empty() {
+ None
+ } else {
+ Some(system_message)
+ },
+ thinking,
+ tools,
+ tool_choice,
+ stop_sequences: request.stop_sequences.clone(),
+ temperature: request.temperature,
+ stream: true,
+ })
+}
+
+fn parse_sse_line(line: &str) -> Option<AnthropicEvent> {
+ let data = line.strip_prefix("data: ")?;
+ serde_json::from_str(data).ok()
+}
+
+impl zed::Extension for AnthropicProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "anthropic".into(),
+ name: "Anthropic".into(),
+ icon: Some("anthropic".into()),
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(MODELS
+ .iter()
+ .map(|m| LlmModelInfo {
+ id: m.display_name.to_string(),
+ name: m.display_name.to_string(),
+ max_token_count: m.max_tokens,
+ max_output_tokens: Some(m.max_output_tokens),
+ capabilities: LlmModelCapabilities {
+ supports_images: m.supports_images,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: m.supports_thinking,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: m.is_default,
+ is_default_fast: m.is_default_fast,
+ })
+ .collect())
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ llm_get_credential("anthropic").is_some()
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(
+ r#"# Anthropic Setup
+
+Welcome to **Anthropic**! This extension provides access to Claude models.
+
+## Configuration
+
+Enter your Anthropic API key below. You can get your API key at [console.anthropic.com](https://console.anthropic.com/).
+
+## Available Models
+
+| Display Name | Real Model | Context | Output |
+|--------------|------------|---------|--------|
+| Claude Opus 4.5 | claude-opus-4-5 | 200K | 8K |
+| Claude Opus 4.5 Thinking | claude-opus-4-5 | 200K | 8K |
+| Claude Sonnet 4.5 | claude-sonnet-4-5 | 200K | 8K |
+| Claude Sonnet 4.5 Thinking | claude-sonnet-4-5 | 200K | 8K |
+| Claude Sonnet 4 | claude-sonnet-4 | 200K | 8K |
+| Claude Sonnet 4 Thinking | claude-sonnet-4 | 200K | 8K |
+| Claude Haiku 4.5 | claude-haiku-4-5 | 200K | 64K |
+| Claude Haiku 4.5 Thinking | claude-haiku-4-5 | 200K | 64K |
+| Claude 3.5 Sonnet | claude-3-5-sonnet | 200K | 8K |
+| Claude 3.5 Haiku | claude-3-5-haiku | 200K | 8K |
+
+## Features
+
+- ✅ Full streaming support
+- ✅ Tool/function calling
+- ✅ Vision (image inputs)
+- ✅ Extended thinking support
+- ✅ All Claude models
+
+## Pricing
+
+Uses your Anthropic API credits. See [Anthropic pricing](https://www.anthropic.com/pricing) for details.
+"#
+ .to_string(),
+ )
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ let provided = llm_request_credential(
+ "anthropic",
+ LlmCredentialType::ApiKey,
+ "Anthropic API Key",
+ "sk-ant-...",
+ )?;
+ if provided {
+ Ok(())
+ } else {
+ Err("Authentication cancelled".to_string())
+ }
+ }
+
+ fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
+ llm_delete_credential("anthropic")
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ let api_key = llm_get_credential("anthropic").ok_or_else(|| {
+ "No API key configured. Please add your Anthropic API key in settings.".to_string()
+ })?;
+
+ let anthropic_request = convert_request(model_id, request)?;
+
+ let body = serde_json::to_vec(&anthropic_request)
+ .map_err(|e| format!("Failed to serialize request: {}", e))?;
+
+ let http_request = HttpRequest {
+ method: HttpMethod::Post,
+ url: "https://api.anthropic.com/v1/messages".to_string(),
+ headers: vec![
+ ("Content-Type".to_string(), "application/json".to_string()),
+ ("x-api-key".to_string(), api_key),
+ ("anthropic-version".to_string(), "2023-06-01".to_string()),
+ ],
+ body: Some(body),
+ redirect_policy: RedirectPolicy::FollowAll,
+ };
+
+ let response_stream = http_request
+ .fetch_stream()
+ .map_err(|e| format!("HTTP request failed: {}", e))?;
+
+ let stream_id = {
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let id = format!("anthropic-stream-{}", *id_counter);
+ *id_counter += 1;
+ id
+ };
+
+ self.streams.lock().unwrap().insert(
+ stream_id.clone(),
+ StreamState {
+ response_stream: Some(response_stream),
+ buffer: String::new(),
+ started: false,
+ current_tool_use: None,
+ stop_reason: None,
+ pending_signature: None,
+ },
+ );
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ let state = streams
+ .get_mut(stream_id)
+ .ok_or_else(|| format!("Unknown stream: {}", stream_id))?;
+
+ if !state.started {
+ state.started = true;
+ return Ok(Some(LlmCompletionEvent::Started));
+ }
+
+ let response_stream = state
+ .response_stream
+ .as_mut()
+ .ok_or_else(|| "Stream already closed".to_string())?;
+
+ loop {
+ if let Some(newline_pos) = state.buffer.find('\n') {
+ let line = state.buffer[..newline_pos].to_string();
+ state.buffer = state.buffer[newline_pos + 1..].to_string();
+
+ if line.trim().is_empty() || line.starts_with("event:") {
+ continue;
+ }
+
+ if let Some(event) = parse_sse_line(&line) {
+ match event {
+ AnthropicEvent::MessageStart { message } => {
+ if let (Some(input), Some(output)) =
+ (message.usage.input_tokens, message.usage.output_tokens)
+ {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: input,
+ output_tokens: output,
+ cache_creation_input_tokens: message
+ .usage
+ .cache_creation_input_tokens,
+ cache_read_input_tokens: message.usage.cache_read_input_tokens,
+ })));
+ }
+ }
+ AnthropicEvent::ContentBlockStart { content_block, .. } => {
+ match content_block {
+ AnthropicContentBlock::Text { text } => {
+ if !text.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(text)));
+ }
+ }
+ AnthropicContentBlock::Thinking { thinking } => {
+ return Ok(Some(LlmCompletionEvent::Thinking(
+ LlmThinkingContent {
+ text: thinking,
+ signature: None,
+ },
+ )));
+ }
+ AnthropicContentBlock::RedactedThinking { data } => {
+ return Ok(Some(LlmCompletionEvent::RedactedThinking(data)));
+ }
+ AnthropicContentBlock::ToolUse { id, name } => {
+ state.current_tool_use = Some(ToolUseState {
+ id,
+ name,
+ input_json: String::new(),
+ });
+ }
+ }
+ }
+ AnthropicEvent::ContentBlockDelta { delta, .. } => match delta {
+ AnthropicDelta::TextDelta { text } => {
+ if !text.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(text)));
+ }
+ }
+ AnthropicDelta::ThinkingDelta { thinking } => {
+ return Ok(Some(LlmCompletionEvent::Thinking(
+ LlmThinkingContent {
+ text: thinking,
+ signature: None,
+ },
+ )));
+ }
+ AnthropicDelta::SignatureDelta { signature } => {
+ state.pending_signature = Some(signature.clone());
+ return Ok(Some(LlmCompletionEvent::Thinking(
+ LlmThinkingContent {
+ text: String::new(),
+ signature: Some(signature),
+ },
+ )));
+ }
+ AnthropicDelta::InputJsonDelta { partial_json } => {
+ if let Some(ref mut tool_use) = state.current_tool_use {
+ tool_use.input_json.push_str(&partial_json);
+ }
+ }
+ },
+ AnthropicEvent::ContentBlockStop { .. } => {
+ if let Some(tool_use) = state.current_tool_use.take() {
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id: tool_use.id,
+ name: tool_use.name,
+ input: tool_use.input_json,
+ thought_signature: state.pending_signature.take(),
+ })));
+ }
+ }
+ AnthropicEvent::MessageDelta { delta, usage } => {
+ if let Some(reason) = delta.stop_reason {
+ state.stop_reason = Some(match reason.as_str() {
+ "end_turn" => LlmStopReason::EndTurn,
+ "max_tokens" => LlmStopReason::MaxTokens,
+ "tool_use" => LlmStopReason::ToolUse,
+ _ => LlmStopReason::EndTurn,
+ });
+ }
+ if let Some(output) = usage.output_tokens {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.input_tokens.unwrap_or(0),
+ output_tokens: output,
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
+ cache_read_input_tokens: usage.cache_read_input_tokens,
+ })));
+ }
+ }
+ AnthropicEvent::MessageStop => {
+ if let Some(stop_reason) = state.stop_reason.take() {
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+ return Ok(Some(LlmCompletionEvent::Stop(LlmStopReason::EndTurn)));
+ }
+ AnthropicEvent::Ping => {}
+ AnthropicEvent::Error { error } => {
+ return Err(format!("API error: {}", error.message));
+ }
+ }
+ }
+
+ continue;
+ }
+
+ match response_stream.next_chunk() {
+ Ok(Some(chunk)) => {
+ let text = String::from_utf8_lossy(&chunk);
+ state.buffer.push_str(&text);
+ }
+ Ok(None) => {
+ if let Some(stop_reason) = state.stop_reason.take() {
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+ return Ok(None);
+ }
+ Err(e) => {
+ return Err(format!("Stream error: {}", e));
+ }
+ }
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(AnthropicProvider);
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "copilot_chat"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "zed_extension_api",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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 = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.8.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,17 @@
+[package]
+name = "copilot_chat"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/copilot_chat.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
@@ -0,0 +1,10 @@
+id = "copilot_chat"
+name = "Copilot Chat"
+description = "GitHub Copilot Chat LLM provider for Zed."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.copilot_chat]
+name = "Copilot Chat"
@@ -0,0 +1,696 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy};
+use zed_extension_api::{self as zed, *};
+
+struct CopilotChatProvider {
+ streams: Mutex<HashMap<String, StreamState>>,
+ next_stream_id: Mutex<u64>,
+}
+
+struct StreamState {
+ response_stream: Option<HttpResponseStream>,
+ buffer: String,
+ started: bool,
+ tool_calls: HashMap<usize, AccumulatedToolCall>,
+ tool_calls_emitted: bool,
+}
+
+#[derive(Clone, Default)]
+struct AccumulatedToolCall {
+ id: String,
+ name: String,
+ arguments: String,
+}
+
+struct ModelDefinition {
+ id: &'static str,
+ display_name: &'static str,
+ max_tokens: u64,
+ max_output_tokens: Option<u64>,
+ supports_images: bool,
+ is_default: bool,
+ is_default_fast: bool,
+}
+
+const MODELS: &[ModelDefinition] = &[
+ ModelDefinition {
+ id: "gpt-4o",
+ display_name: "GPT-4o",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ is_default: true,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "gpt-4o-mini",
+ display_name: "GPT-4o Mini",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: true,
+ },
+ ModelDefinition {
+ id: "gpt-4.1",
+ display_name: "GPT-4.1",
+ max_tokens: 1_000_000,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "o1",
+ display_name: "o1",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "o3-mini",
+ display_name: "o3-mini",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "claude-3.5-sonnet",
+ display_name: "Claude 3.5 Sonnet",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "claude-3.7-sonnet",
+ display_name: "Claude 3.7 Sonnet",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "gemini-2.0-flash-001",
+ display_name: "Gemini 2.0 Flash",
+ max_tokens: 1_000_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+];
+
+fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> {
+ MODELS.iter().find(|m| m.id == model_id)
+}
+
+#[derive(Serialize)]
+struct OpenAiRequest {
+ model: String,
+ messages: Vec<OpenAiMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ max_tokens: Option<u64>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ tools: Vec<OpenAiTool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_choice: Option<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ stop: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ temperature: Option<f32>,
+ stream: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ stream_options: Option<StreamOptions>,
+}
+
+#[derive(Serialize)]
+struct StreamOptions {
+ include_usage: bool,
+}
+
+#[derive(Serialize)]
+struct OpenAiMessage {
+ role: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ content: Option<OpenAiContent>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_calls: Option<Vec<OpenAiToolCall>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_call_id: Option<String>,
+}
+
+#[derive(Serialize, Clone)]
+#[serde(untagged)]
+enum OpenAiContent {
+ Text(String),
+ Parts(Vec<OpenAiContentPart>),
+}
+
+#[derive(Serialize, Clone)]
+#[serde(tag = "type")]
+enum OpenAiContentPart {
+ #[serde(rename = "text")]
+ Text { text: String },
+ #[serde(rename = "image_url")]
+ ImageUrl { image_url: ImageUrl },
+}
+
+#[derive(Serialize, Clone)]
+struct ImageUrl {
+ url: String,
+}
+
+#[derive(Serialize, Clone)]
+struct OpenAiToolCall {
+ id: String,
+ #[serde(rename = "type")]
+ call_type: String,
+ function: OpenAiFunctionCall,
+}
+
+#[derive(Serialize, Clone)]
+struct OpenAiFunctionCall {
+ name: String,
+ arguments: String,
+}
+
+#[derive(Serialize)]
+struct OpenAiTool {
+ #[serde(rename = "type")]
+ tool_type: String,
+ function: OpenAiFunctionDef,
+}
+
+#[derive(Serialize)]
+struct OpenAiFunctionDef {
+ name: String,
+ description: String,
+ parameters: serde_json::Value,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiStreamResponse {
+ choices: Vec<OpenAiStreamChoice>,
+ #[serde(default)]
+ usage: Option<OpenAiUsage>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiStreamChoice {
+ delta: OpenAiDelta,
+ finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct OpenAiDelta {
+ #[serde(default)]
+ content: Option<String>,
+ #[serde(default)]
+ tool_calls: Option<Vec<OpenAiToolCallDelta>>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiToolCallDelta {
+ index: usize,
+ #[serde(default)]
+ id: Option<String>,
+ #[serde(default)]
+ function: Option<OpenAiFunctionDelta>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct OpenAiFunctionDelta {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ arguments: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiUsage {
+ prompt_tokens: u64,
+ completion_tokens: u64,
+}
+
+fn convert_request(
+ model_id: &str,
+ request: &LlmCompletionRequest,
+) -> Result<OpenAiRequest, String> {
+ let mut messages: Vec<OpenAiMessage> = Vec::new();
+
+ for msg in &request.messages {
+ match msg.role {
+ LlmMessageRole::System => {
+ let mut text_content = String::new();
+ for content in &msg.content {
+ if let LlmMessageContent::Text(text) = content {
+ if !text_content.is_empty() {
+ text_content.push('\n');
+ }
+ text_content.push_str(text);
+ }
+ }
+ if !text_content.is_empty() {
+ messages.push(OpenAiMessage {
+ role: "system".to_string(),
+ content: Some(OpenAiContent::Text(text_content)),
+ tool_calls: None,
+ tool_call_id: None,
+ });
+ }
+ }
+ LlmMessageRole::User => {
+ let mut parts: Vec<OpenAiContentPart> = Vec::new();
+ let mut tool_result_messages: Vec<OpenAiMessage> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ parts.push(OpenAiContentPart::Text { text: text.clone() });
+ }
+ }
+ LlmMessageContent::Image(img) => {
+ let data_url = format!("data:image/png;base64,{}", img.source);
+ parts.push(OpenAiContentPart::ImageUrl {
+ image_url: ImageUrl { url: data_url },
+ });
+ }
+ LlmMessageContent::ToolResult(result) => {
+ let content_text = match &result.content {
+ LlmToolResultContent::Text(t) => t.clone(),
+ LlmToolResultContent::Image(_) => "[Image]".to_string(),
+ };
+ tool_result_messages.push(OpenAiMessage {
+ role: "tool".to_string(),
+ content: Some(OpenAiContent::Text(content_text)),
+ tool_calls: None,
+ tool_call_id: Some(result.tool_use_id.clone()),
+ });
+ }
+ _ => {}
+ }
+ }
+
+ if !parts.is_empty() {
+ let content = if parts.len() == 1 {
+ if let OpenAiContentPart::Text { text } = &parts[0] {
+ OpenAiContent::Text(text.clone())
+ } else {
+ OpenAiContent::Parts(parts)
+ }
+ } else {
+ OpenAiContent::Parts(parts)
+ };
+
+ messages.push(OpenAiMessage {
+ role: "user".to_string(),
+ content: Some(content),
+ tool_calls: None,
+ tool_call_id: None,
+ });
+ }
+
+ messages.extend(tool_result_messages);
+ }
+ LlmMessageRole::Assistant => {
+ let mut text_content = String::new();
+ let mut tool_calls: Vec<OpenAiToolCall> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ if !text_content.is_empty() {
+ text_content.push('\n');
+ }
+ text_content.push_str(text);
+ }
+ }
+ LlmMessageContent::ToolUse(tool_use) => {
+ tool_calls.push(OpenAiToolCall {
+ id: tool_use.id.clone(),
+ call_type: "function".to_string(),
+ function: OpenAiFunctionCall {
+ name: tool_use.name.clone(),
+ arguments: tool_use.input.clone(),
+ },
+ });
+ }
+ _ => {}
+ }
+ }
+
+ messages.push(OpenAiMessage {
+ role: "assistant".to_string(),
+ content: if text_content.is_empty() {
+ None
+ } else {
+ Some(OpenAiContent::Text(text_content))
+ },
+ tool_calls: if tool_calls.is_empty() {
+ None
+ } else {
+ Some(tool_calls)
+ },
+ tool_call_id: None,
+ });
+ }
+ }
+ }
+
+ let tools: Vec<OpenAiTool> = request
+ .tools
+ .iter()
+ .map(|t| OpenAiTool {
+ tool_type: "function".to_string(),
+ function: OpenAiFunctionDef {
+ name: t.name.clone(),
+ description: t.description.clone(),
+ parameters: serde_json::from_str(&t.input_schema)
+ .unwrap_or(serde_json::Value::Object(Default::default())),
+ },
+ })
+ .collect();
+
+ let tool_choice = request.tool_choice.as_ref().map(|tc| match tc {
+ LlmToolChoice::Auto => "auto".to_string(),
+ LlmToolChoice::Any => "required".to_string(),
+ LlmToolChoice::None => "none".to_string(),
+ });
+
+ let model_def = get_model_definition(model_id);
+ let max_tokens = request
+ .max_tokens
+ .or(model_def.and_then(|m| m.max_output_tokens));
+
+ Ok(OpenAiRequest {
+ model: model_id.to_string(),
+ messages,
+ max_tokens,
+ tools,
+ tool_choice,
+ stop: request.stop_sequences.clone(),
+ temperature: request.temperature,
+ stream: true,
+ stream_options: Some(StreamOptions {
+ include_usage: true,
+ }),
+ })
+}
+
+fn parse_sse_line(line: &str) -> Option<OpenAiStreamResponse> {
+ let data = line.strip_prefix("data: ")?;
+ if data.trim() == "[DONE]" {
+ return None;
+ }
+ serde_json::from_str(data).ok()
+}
+
+impl zed::Extension for CopilotChatProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "copilot_chat".into(),
+ name: "Copilot Chat".into(),
+ icon: None,
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(MODELS
+ .iter()
+ .map(|m| LlmModelInfo {
+ id: m.id.to_string(),
+ name: m.display_name.to_string(),
+ max_token_count: m.max_tokens,
+ max_output_tokens: m.max_output_tokens,
+ capabilities: LlmModelCapabilities {
+ supports_images: m.supports_images,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: false,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: m.is_default,
+ is_default_fast: m.is_default_fast,
+ })
+ .collect())
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ llm_get_credential("copilot_chat").is_some()
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(
+ r#"# Copilot Chat Setup
+
+Welcome to **Copilot Chat**! This extension provides access to GitHub Copilot's chat models.
+
+## Configuration
+
+Enter your GitHub Copilot token below. You need an active GitHub Copilot subscription.
+
+To get your token:
+1. Ensure you have a GitHub Copilot subscription
+2. Generate a token from your GitHub Copilot settings
+
+## Available Models
+
+| Model | Context | Output |
+|-------|---------|--------|
+| GPT-4o | 128K | 16K |
+| GPT-4o Mini | 128K | 16K |
+| GPT-4.1 | 1M | 32K |
+| o1 | 200K | 100K |
+| o3-mini | 200K | 100K |
+| Claude 3.5 Sonnet | 200K | 8K |
+| Claude 3.7 Sonnet | 200K | 8K |
+| Gemini 2.0 Flash | 1M | 8K |
+
+## Features
+
+- ✅ Full streaming support
+- ✅ Tool/function calling
+- ✅ Vision (image inputs)
+- ✅ Multiple model providers via Copilot
+
+## Note
+
+This extension requires an active GitHub Copilot subscription.
+"#
+ .to_string(),
+ )
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ let provided = llm_request_credential(
+ "copilot_chat",
+ LlmCredentialType::ApiKey,
+ "GitHub Copilot Token",
+ "ghu_...",
+ )?;
+ if provided {
+ Ok(())
+ } else {
+ Err("Authentication cancelled".to_string())
+ }
+ }
+
+ fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
+ llm_delete_credential("copilot_chat")
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ let api_key = llm_get_credential("copilot_chat").ok_or_else(|| {
+ "No token configured. Please add your GitHub Copilot token in settings.".to_string()
+ })?;
+
+ let openai_request = convert_request(model_id, request)?;
+
+ let body = serde_json::to_vec(&openai_request)
+ .map_err(|e| format!("Failed to serialize request: {}", e))?;
+
+ let http_request = HttpRequest {
+ method: HttpMethod::Post,
+ url: "https://api.githubcopilot.com/chat/completions".to_string(),
+ headers: vec![
+ ("Content-Type".to_string(), "application/json".to_string()),
+ ("Authorization".to_string(), format!("Bearer {}", api_key)),
+ (
+ "Copilot-Integration-Id".to_string(),
+ "vscode-chat".to_string(),
+ ),
+ ("Editor-Version".to_string(), "Zed/1.0.0".to_string()),
+ ],
+ body: Some(body),
+ redirect_policy: RedirectPolicy::FollowAll,
+ };
+
+ let response_stream = http_request
+ .fetch_stream()
+ .map_err(|e| format!("HTTP request failed: {}", e))?;
+
+ let stream_id = {
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let id = format!("copilot-stream-{}", *id_counter);
+ *id_counter += 1;
+ id
+ };
+
+ self.streams.lock().unwrap().insert(
+ stream_id.clone(),
+ StreamState {
+ response_stream: Some(response_stream),
+ buffer: String::new(),
+ started: false,
+ tool_calls: HashMap::new(),
+ tool_calls_emitted: false,
+ },
+ );
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ let state = streams
+ .get_mut(stream_id)
+ .ok_or_else(|| format!("Unknown stream: {}", stream_id))?;
+
+ if !state.started {
+ state.started = true;
+ return Ok(Some(LlmCompletionEvent::Started));
+ }
+
+ let response_stream = state
+ .response_stream
+ .as_mut()
+ .ok_or_else(|| "Stream already closed".to_string())?;
+
+ loop {
+ if let Some(newline_pos) = state.buffer.find('\n') {
+ let line = state.buffer[..newline_pos].to_string();
+ state.buffer = state.buffer[newline_pos + 1..].to_string();
+
+ if line.trim().is_empty() {
+ continue;
+ }
+
+ if let Some(response) = parse_sse_line(&line) {
+ if let Some(choice) = response.choices.first() {
+ if let Some(content) = &choice.delta.content {
+ if !content.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(content.clone())));
+ }
+ }
+
+ if let Some(tool_calls) = &choice.delta.tool_calls {
+ for tc in tool_calls {
+ let entry = state
+ .tool_calls
+ .entry(tc.index)
+ .or_insert_with(AccumulatedToolCall::default);
+
+ if let Some(id) = &tc.id {
+ entry.id = id.clone();
+ }
+ if let Some(func) = &tc.function {
+ if let Some(name) = &func.name {
+ entry.name = name.clone();
+ }
+ if let Some(args) = &func.arguments {
+ entry.arguments.push_str(args);
+ }
+ }
+ }
+ }
+
+ if let Some(finish_reason) = &choice.finish_reason {
+ if !state.tool_calls.is_empty() && !state.tool_calls_emitted {
+ state.tool_calls_emitted = true;
+ let mut tool_calls: Vec<_> = state.tool_calls.drain().collect();
+ tool_calls.sort_by_key(|(idx, _)| *idx);
+
+ if let Some((_, tc)) = tool_calls.into_iter().next() {
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id: tc.id,
+ name: tc.name,
+ input: tc.arguments,
+ thought_signature: None,
+ })));
+ }
+ }
+
+ let stop_reason = match finish_reason.as_str() {
+ "stop" => LlmStopReason::EndTurn,
+ "length" => LlmStopReason::MaxTokens,
+ "tool_calls" => LlmStopReason::ToolUse,
+ "content_filter" => LlmStopReason::Refusal,
+ _ => LlmStopReason::EndTurn,
+ };
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+ }
+
+ if let Some(usage) = response.usage {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.prompt_tokens,
+ output_tokens: usage.completion_tokens,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ })));
+ }
+ }
+
+ continue;
+ }
+
+ match response_stream.next_chunk() {
+ Ok(Some(chunk)) => {
+ let text = String::from_utf8_lossy(&chunk);
+ state.buffer.push_str(&text);
+ }
+ Ok(None) => {
+ return Ok(None);
+ }
+ Err(e) => {
+ return Err(format!("Stream error: {}", e));
+ }
+ }
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(CopilotChatProvider);
@@ -0,0 +1,821 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "example_provider"
+version = "0.1.0"
+dependencies = [
+ "zed_extension_api",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.7.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,15 @@
+[package]
+name = "example_provider"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/example_provider.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
@@ -0,0 +1,10 @@
+id = "example-provider"
+name = "Example Provider"
+description = "An example LLM provider extension for testing."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.example]
+name = "Example Provider"
@@ -0,0 +1,181 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+use zed_extension_api::{self as zed, *};
+
+struct ExampleProvider {
+ /// Active completion streams, keyed by stream ID
+ streams: Mutex<HashMap<String, Vec<LlmCompletionEvent>>>,
+ /// Counter for generating unique stream IDs
+ next_stream_id: Mutex<u64>,
+}
+
+impl zed::Extension for ExampleProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "example".into(),
+ name: "Example Provider".into(),
+ icon: None,
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(vec![
+ LlmModelInfo {
+ id: "example-fast".into(),
+ name: "Example Fast".into(),
+ max_token_count: 8192,
+ max_output_tokens: Some(4096),
+ capabilities: LlmModelCapabilities {
+ supports_images: false,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: false,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: false,
+ is_default_fast: true,
+ },
+ LlmModelInfo {
+ id: "example-smart".into(),
+ name: "Example Smart".into(),
+ max_token_count: 32768,
+ max_output_tokens: Some(8192),
+ capabilities: LlmModelCapabilities {
+ supports_images: true,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: true,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: true,
+ is_default_fast: false,
+ },
+ ])
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ // Example provider is always authenticated for testing
+ true
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(r#"# Example Provider Setup
+
+Welcome to the **Example Provider**! This is a demonstration LLM provider for testing purposes.
+
+## Features
+
+- 🚀 **Fast responses** - Instant echo responses for testing
+- 🛠️ **Tool support** - Full function calling capabilities
+- 🖼️ **Image support** - Vision model available (Example Smart)
+
+## Configuration
+
+No API key is required for this example provider. It echoes back your messages for testing purposes.
+
+## Models
+
+- **Example Fast** - Quick responses, 8K context
+- **Example Smart** - Extended features, 32K context, supports images and thinking
+
+## Usage
+
+Simply select this provider and start chatting! Your messages will be echoed back with the model name.
+"#.to_string())
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ // Example provider doesn't need authentication
+ Ok(())
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ // Get the last user message to echo back
+ let user_message = request
+ .messages
+ .iter()
+ .filter(|m| matches!(m.role, LlmMessageRole::User))
+ .last()
+ .and_then(|m| {
+ m.content.iter().find_map(|c| {
+ if let LlmMessageContent::Text(text) = c {
+ Some(text.clone())
+ } else {
+ None
+ }
+ })
+ })
+ .unwrap_or_else(|| "Hello!".to_string());
+
+ // Create a response based on the model
+ let response_text = format!("Hello from {}! You said: \"{}\"", model_id, user_message);
+
+ // Create events for the stream - simulate streaming by breaking into chunks
+ let mut events = vec![LlmCompletionEvent::Started];
+
+ // Stream the response in chunks
+ for chunk in response_text.chars().collect::<Vec<_>>().chunks(10) {
+ let text: String = chunk.iter().collect();
+ events.push(LlmCompletionEvent::Text(text));
+ }
+
+ events.push(LlmCompletionEvent::Stop(LlmStopReason::EndTurn));
+ events.push(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: 10,
+ output_tokens: response_text.len() as u64 / 4,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ }));
+
+ // Generate a unique stream ID
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let stream_id = format!("example-stream-{}", *id_counter);
+ *id_counter += 1;
+
+ // Store the events
+ self.streams
+ .lock()
+ .unwrap()
+ .insert(stream_id.clone(), events);
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ if let Some(events) = streams.get_mut(stream_id) {
+ if events.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(events.remove(0)))
+ }
+ } else {
+ Err(format!("Unknown stream: {}", stream_id))
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(ExampleProvider);
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foogle"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "zed_extension_api",
+]
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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 = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.7.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,17 @@
+[package]
+name = "google-ai"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/google_ai.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
@@ -0,0 +1,10 @@
+id = "google-ai"
+name = "Google AI"
+description = "Google Gemini LLM provider for Zed."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.google-ai]
+name = "Google AI"
@@ -0,0 +1,840 @@
+use std::collections::HashMap;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy};
+use zed_extension_api::{self as zed, *};
+
+static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0);
+
+struct GoogleAiProvider {
+ streams: Mutex<HashMap<String, StreamState>>,
+ next_stream_id: Mutex<u64>,
+}
+
+struct StreamState {
+ response_stream: Option<HttpResponseStream>,
+ buffer: String,
+ started: bool,
+ stop_reason: Option<LlmStopReason>,
+ wants_tool_use: bool,
+}
+
+struct ModelDefinition {
+ real_id: &'static str,
+ display_name: &'static str,
+ max_tokens: u64,
+ max_output_tokens: Option<u64>,
+ supports_images: bool,
+ supports_thinking: bool,
+ is_default: bool,
+ is_default_fast: bool,
+}
+
+const MODELS: &[ModelDefinition] = &[
+ ModelDefinition {
+ real_id: "gemini-2.5-flash-lite",
+ display_name: "Gemini 2.5 Flash-Lite",
+ max_tokens: 1_048_576,
+ max_output_tokens: Some(65_536),
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: true,
+ },
+ ModelDefinition {
+ real_id: "gemini-2.5-flash",
+ display_name: "Gemini 2.5 Flash",
+ max_tokens: 1_048_576,
+ max_output_tokens: Some(65_536),
+ supports_images: true,
+ supports_thinking: true,
+ is_default: true,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gemini-2.5-pro",
+ display_name: "Gemini 2.5 Pro",
+ max_tokens: 1_048_576,
+ max_output_tokens: Some(65_536),
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gemini-3-pro-preview",
+ display_name: "Gemini 3 Pro",
+ max_tokens: 1_048_576,
+ max_output_tokens: Some(65_536),
+ supports_images: true,
+ supports_thinking: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+];
+
+fn get_real_model_id(display_name: &str) -> Option<&'static str> {
+ MODELS
+ .iter()
+ .find(|m| m.display_name == display_name)
+ .map(|m| m.real_id)
+}
+
+fn get_model_supports_thinking(display_name: &str) -> bool {
+ MODELS
+ .iter()
+ .find(|m| m.display_name == display_name)
+ .map(|m| m.supports_thinking)
+ .unwrap_or(false)
+}
+
+/// Adapts a JSON schema to be compatible with Google's API subset.
+/// Google only supports a specific subset of JSON Schema fields.
+/// See: https://ai.google.dev/api/caching#Schema
+fn adapt_schema_for_google(json: &mut serde_json::Value) {
+ adapt_schema_for_google_impl(json, true);
+}
+
+fn adapt_schema_for_google_impl(json: &mut serde_json::Value, is_schema: bool) {
+ if let serde_json::Value::Object(obj) = json {
+ // Google's Schema only supports these fields:
+ // type, format, title, description, nullable, enum, maxItems, minItems,
+ // properties, required, minProperties, maxProperties, minLength, maxLength,
+ // pattern, example, anyOf, propertyOrdering, default, items, minimum, maximum
+ const ALLOWED_KEYS: &[&str] = &[
+ "type",
+ "format",
+ "title",
+ "description",
+ "nullable",
+ "enum",
+ "maxItems",
+ "minItems",
+ "properties",
+ "required",
+ "minProperties",
+ "maxProperties",
+ "minLength",
+ "maxLength",
+ "pattern",
+ "example",
+ "anyOf",
+ "propertyOrdering",
+ "default",
+ "items",
+ "minimum",
+ "maximum",
+ ];
+
+ // Convert oneOf to anyOf before filtering keys
+ if let Some(one_of) = obj.remove("oneOf") {
+ obj.insert("anyOf".to_string(), one_of);
+ }
+
+ // If type is an array (e.g., ["string", "null"]), take just the first type
+ if let Some(type_field) = obj.get_mut("type") {
+ if let serde_json::Value::Array(types) = type_field {
+ if let Some(first_type) = types.first().cloned() {
+ *type_field = first_type;
+ }
+ }
+ }
+
+ // Only filter keys if this is a schema object, not a properties map
+ if is_schema {
+ obj.retain(|key, _| ALLOWED_KEYS.contains(&key.as_str()));
+ }
+
+ // Recursively process nested values
+ // "properties" contains a map of property names -> schemas
+ // "items" and "anyOf" contain schemas directly
+ for (key, value) in obj.iter_mut() {
+ if key == "properties" {
+ // properties is a map of property_name -> schema
+ if let serde_json::Value::Object(props) = value {
+ for (_, prop_schema) in props.iter_mut() {
+ adapt_schema_for_google_impl(prop_schema, true);
+ }
+ }
+ } else if key == "items" {
+ // items is a schema
+ adapt_schema_for_google_impl(value, true);
+ } else if key == "anyOf" {
+ // anyOf is an array of schemas
+ if let serde_json::Value::Array(arr) = value {
+ for item in arr.iter_mut() {
+ adapt_schema_for_google_impl(item, true);
+ }
+ }
+ }
+ }
+ } else if let serde_json::Value::Array(arr) = json {
+ for item in arr.iter_mut() {
+ adapt_schema_for_google_impl(item, true);
+ }
+ }
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleRequest {
+ contents: Vec<GoogleContent>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ system_instruction: Option<GoogleSystemInstruction>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ generation_config: Option<GoogleGenerationConfig>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tools: Option<Vec<GoogleTool>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_config: Option<GoogleToolConfig>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleSystemInstruction {
+ parts: Vec<GooglePart>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleContent {
+ parts: Vec<GooglePart>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ role: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(untagged)]
+enum GooglePart {
+ Text(GoogleTextPart),
+ InlineData(GoogleInlineDataPart),
+ FunctionCall(GoogleFunctionCallPart),
+ FunctionResponse(GoogleFunctionResponsePart),
+ Thought(GoogleThoughtPart),
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleTextPart {
+ text: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleInlineDataPart {
+ inline_data: GoogleBlob,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleBlob {
+ mime_type: String,
+ data: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionCallPart {
+ function_call: GoogleFunctionCall,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ thought_signature: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionCall {
+ name: String,
+ args: serde_json::Value,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionResponsePart {
+ function_response: GoogleFunctionResponse,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionResponse {
+ name: String,
+ response: serde_json::Value,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+struct GoogleThoughtPart {
+ thought: bool,
+ thought_signature: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleGenerationConfig {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ candidate_count: Option<usize>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ stop_sequences: Option<Vec<String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ max_output_tokens: Option<usize>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ temperature: Option<f64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ thinking_config: Option<GoogleThinkingConfig>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleThinkingConfig {
+ thinking_budget: u32,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleTool {
+ function_declarations: Vec<GoogleFunctionDeclaration>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionDeclaration {
+ name: String,
+ description: String,
+ parameters: serde_json::Value,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleToolConfig {
+ function_calling_config: GoogleFunctionCallingConfig,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct GoogleFunctionCallingConfig {
+ mode: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ allowed_function_names: Option<Vec<String>>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct GoogleStreamResponse {
+ #[serde(default)]
+ candidates: Vec<GoogleCandidate>,
+ #[serde(default)]
+ usage_metadata: Option<GoogleUsageMetadata>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct GoogleCandidate {
+ #[serde(default)]
+ content: Option<GoogleContent>,
+ #[serde(default)]
+ finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct GoogleUsageMetadata {
+ #[serde(default)]
+ prompt_token_count: u64,
+ #[serde(default)]
+ candidates_token_count: u64,
+}
+
+fn convert_request(
+ model_id: &str,
+ request: &LlmCompletionRequest,
+) -> Result<(GoogleRequest, String), String> {
+ let real_model_id =
+ get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?;
+
+ let supports_thinking = get_model_supports_thinking(model_id);
+
+ let mut contents: Vec<GoogleContent> = Vec::new();
+ let mut system_parts: Vec<GooglePart> = Vec::new();
+
+ for msg in &request.messages {
+ match msg.role {
+ LlmMessageRole::System => {
+ for content in &msg.content {
+ if let LlmMessageContent::Text(text) = content {
+ if !text.is_empty() {
+ system_parts
+ .push(GooglePart::Text(GoogleTextPart { text: text.clone() }));
+ }
+ }
+ }
+ }
+ LlmMessageRole::User => {
+ let mut parts: Vec<GooglePart> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() }));
+ }
+ }
+ LlmMessageContent::Image(img) => {
+ parts.push(GooglePart::InlineData(GoogleInlineDataPart {
+ inline_data: GoogleBlob {
+ mime_type: "image/png".to_string(),
+ data: img.source.clone(),
+ },
+ }));
+ }
+ LlmMessageContent::ToolResult(result) => {
+ let response_value = match &result.content {
+ LlmToolResultContent::Text(t) => {
+ serde_json::json!({ "output": t })
+ }
+ LlmToolResultContent::Image(_) => {
+ serde_json::json!({ "output": "Tool responded with an image" })
+ }
+ };
+ parts.push(GooglePart::FunctionResponse(GoogleFunctionResponsePart {
+ function_response: GoogleFunctionResponse {
+ name: result.tool_name.clone(),
+ response: response_value,
+ },
+ }));
+ }
+ _ => {}
+ }
+ }
+
+ if !parts.is_empty() {
+ contents.push(GoogleContent {
+ parts,
+ role: Some("user".to_string()),
+ });
+ }
+ }
+ LlmMessageRole::Assistant => {
+ let mut parts: Vec<GooglePart> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ parts.push(GooglePart::Text(GoogleTextPart { text: text.clone() }));
+ }
+ }
+ LlmMessageContent::ToolUse(tool_use) => {
+ let thought_signature =
+ tool_use.thought_signature.clone().filter(|s| !s.is_empty());
+
+ let args: serde_json::Value =
+ serde_json::from_str(&tool_use.input).unwrap_or_default();
+
+ parts.push(GooglePart::FunctionCall(GoogleFunctionCallPart {
+ function_call: GoogleFunctionCall {
+ name: tool_use.name.clone(),
+ args,
+ },
+ thought_signature,
+ }));
+ }
+ LlmMessageContent::Thinking(thinking) => {
+ if let Some(ref signature) = thinking.signature {
+ if !signature.is_empty() {
+ parts.push(GooglePart::Thought(GoogleThoughtPart {
+ thought: true,
+ thought_signature: signature.clone(),
+ }));
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ if !parts.is_empty() {
+ contents.push(GoogleContent {
+ parts,
+ role: Some("model".to_string()),
+ });
+ }
+ }
+ }
+ }
+
+ let system_instruction = if system_parts.is_empty() {
+ None
+ } else {
+ Some(GoogleSystemInstruction {
+ parts: system_parts,
+ })
+ };
+
+ let tools: Option<Vec<GoogleTool>> = if request.tools.is_empty() {
+ None
+ } else {
+ let declarations: Vec<GoogleFunctionDeclaration> = request
+ .tools
+ .iter()
+ .map(|t| {
+ let mut parameters: serde_json::Value = serde_json::from_str(&t.input_schema)
+ .unwrap_or(serde_json::Value::Object(Default::default()));
+ adapt_schema_for_google(&mut parameters);
+ GoogleFunctionDeclaration {
+ name: t.name.clone(),
+ description: t.description.clone(),
+ parameters,
+ }
+ })
+ .collect();
+ Some(vec![GoogleTool {
+ function_declarations: declarations,
+ }])
+ };
+
+ let tool_config = request.tool_choice.as_ref().map(|tc| {
+ let mode = match tc {
+ LlmToolChoice::Auto => "AUTO",
+ LlmToolChoice::Any => "ANY",
+ LlmToolChoice::None => "NONE",
+ };
+ GoogleToolConfig {
+ function_calling_config: GoogleFunctionCallingConfig {
+ mode: mode.to_string(),
+ allowed_function_names: None,
+ },
+ }
+ });
+
+ let thinking_config = if supports_thinking && request.thinking_allowed {
+ Some(GoogleThinkingConfig {
+ thinking_budget: 8192,
+ })
+ } else {
+ None
+ };
+
+ let generation_config = Some(GoogleGenerationConfig {
+ candidate_count: Some(1),
+ stop_sequences: if request.stop_sequences.is_empty() {
+ None
+ } else {
+ Some(request.stop_sequences.clone())
+ },
+ max_output_tokens: None,
+ temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
+ thinking_config,
+ });
+
+ Ok((
+ GoogleRequest {
+ contents,
+ system_instruction,
+ generation_config,
+ tools,
+ tool_config,
+ },
+ real_model_id.to_string(),
+ ))
+}
+
+fn parse_stream_line(line: &str) -> Option<GoogleStreamResponse> {
+ let trimmed = line.trim();
+ if trimmed.is_empty() || trimmed == "[" || trimmed == "]" || trimmed == "," {
+ return None;
+ }
+
+ let json_str = trimmed.strip_prefix("data: ").unwrap_or(trimmed);
+ let json_str = json_str.trim_start_matches(',').trim();
+
+ if json_str.is_empty() {
+ return None;
+ }
+
+ serde_json::from_str(json_str).ok()
+}
+
+impl zed::Extension for GoogleAiProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "google-ai".into(),
+ name: "Google AI".into(),
+ icon: Some("google-ai".into()),
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(MODELS
+ .iter()
+ .map(|m| LlmModelInfo {
+ id: m.display_name.to_string(),
+ name: m.display_name.to_string(),
+ max_token_count: m.max_tokens,
+ max_output_tokens: m.max_output_tokens,
+ capabilities: LlmModelCapabilities {
+ supports_images: m.supports_images,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: m.supports_thinking,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: m.is_default,
+ is_default_fast: m.is_default_fast,
+ })
+ .collect())
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ llm_get_credential("google-ai").is_some()
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(
+ r#"# Google AI Setup
+
+Welcome to **Google AI**! This extension provides access to Google Gemini models.
+
+## Configuration
+
+Enter your Google AI API key below. You can get your API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey).
+
+## Available Models
+
+| Display Name | Real Model | Context | Output |
+|--------------|------------|---------|--------|
+| Gemini 2.5 Flash-Lite | gemini-2.5-flash-lite | 1M | 65K |
+| Gemini 2.5 Flash | gemini-2.5-flash | 1M | 65K |
+| Gemini 2.5 Pro | gemini-2.5-pro | 1M | 65K |
+| Gemini 3 Pro | gemini-3-pro-preview | 1M | 65K |
+
+## Features
+
+- ✅ Full streaming support
+- ✅ Tool/function calling with thought signatures
+- ✅ Vision (image inputs)
+- ✅ Extended thinking support
+- ✅ All Gemini models
+
+## Pricing
+
+Uses your Google AI API credits. See [Google AI pricing](https://ai.google.dev/pricing) for details.
+"#
+ .to_string(),
+ )
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ let provided = llm_request_credential(
+ "google-ai",
+ LlmCredentialType::ApiKey,
+ "Google AI API Key",
+ "AIza...",
+ )?;
+ if provided {
+ Ok(())
+ } else {
+ Err("Authentication cancelled".to_string())
+ }
+ }
+
+ fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
+ llm_delete_credential("google-ai")
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ let api_key = llm_get_credential("google-ai").ok_or_else(|| {
+ "No API key configured. Please add your Google AI API key in settings.".to_string()
+ })?;
+
+ let (google_request, real_model_id) = convert_request(model_id, request)?;
+
+ let body = serde_json::to_vec(&google_request)
+ .map_err(|e| format!("Failed to serialize request: {}", e))?;
+
+ let url = format!(
+ "https://generativelanguage.googleapis.com/v1beta/models/{}:streamGenerateContent?alt=sse&key={}",
+ real_model_id, api_key
+ );
+
+ let http_request = HttpRequest {
+ method: HttpMethod::Post,
+ url,
+ headers: vec![("Content-Type".to_string(), "application/json".to_string())],
+ body: Some(body),
+ redirect_policy: RedirectPolicy::FollowAll,
+ };
+
+ let response_stream = http_request
+ .fetch_stream()
+ .map_err(|e| format!("HTTP request failed: {}", e))?;
+
+ let stream_id = {
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let id = format!("google-ai-stream-{}", *id_counter);
+ *id_counter += 1;
+ id
+ };
+
+ self.streams.lock().unwrap().insert(
+ stream_id.clone(),
+ StreamState {
+ response_stream: Some(response_stream),
+ buffer: String::new(),
+ started: false,
+ stop_reason: None,
+ wants_tool_use: false,
+ },
+ );
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ let state = streams
+ .get_mut(stream_id)
+ .ok_or_else(|| format!("Unknown stream: {}", stream_id))?;
+
+ if !state.started {
+ state.started = true;
+ return Ok(Some(LlmCompletionEvent::Started));
+ }
+
+ let response_stream = state
+ .response_stream
+ .as_mut()
+ .ok_or_else(|| "Stream already closed".to_string())?;
+
+ loop {
+ if let Some(newline_pos) = state.buffer.find('\n') {
+ let line = state.buffer[..newline_pos].to_string();
+ state.buffer = state.buffer[newline_pos + 1..].to_string();
+
+ if let Some(response) = parse_stream_line(&line) {
+ for candidate in response.candidates {
+ if let Some(finish_reason) = &candidate.finish_reason {
+ state.stop_reason = Some(match finish_reason.as_str() {
+ "STOP" => {
+ if state.wants_tool_use {
+ LlmStopReason::ToolUse
+ } else {
+ LlmStopReason::EndTurn
+ }
+ }
+ "MAX_TOKENS" => LlmStopReason::MaxTokens,
+ "SAFETY" => LlmStopReason::Refusal,
+ _ => LlmStopReason::EndTurn,
+ });
+ }
+
+ if let Some(content) = candidate.content {
+ for part in content.parts {
+ match part {
+ GooglePart::Text(text_part) => {
+ if !text_part.text.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(
+ text_part.text,
+ )));
+ }
+ }
+ GooglePart::FunctionCall(fc_part) => {
+ state.wants_tool_use = true;
+ let next_tool_id =
+ TOOL_CALL_COUNTER.fetch_add(1, Ordering::SeqCst);
+ let id = format!(
+ "{}-{}",
+ fc_part.function_call.name, next_tool_id
+ );
+
+ let thought_signature =
+ fc_part.thought_signature.filter(|s| !s.is_empty());
+
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id,
+ name: fc_part.function_call.name,
+ input: fc_part.function_call.args.to_string(),
+ thought_signature,
+ })));
+ }
+ GooglePart::Thought(thought_part) => {
+ return Ok(Some(LlmCompletionEvent::Thinking(
+ LlmThinkingContent {
+ text: "(Encrypted thought)".to_string(),
+ signature: Some(thought_part.thought_signature),
+ },
+ )));
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ if let Some(usage) = response.usage_metadata {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.prompt_token_count,
+ output_tokens: usage.candidates_token_count,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ })));
+ }
+ }
+
+ continue;
+ }
+
+ match response_stream.next_chunk() {
+ Ok(Some(chunk)) => {
+ let text = String::from_utf8_lossy(&chunk);
+ state.buffer.push_str(&text);
+ }
+ Ok(None) => {
+ // Stream ended - check if we have a stop reason
+ if let Some(stop_reason) = state.stop_reason.take() {
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+
+ // No stop reason - this is unexpected. Check if buffer contains error info
+ let mut error_msg = String::from("Stream ended unexpectedly.");
+
+ // Try to parse remaining buffer as potential error response
+ if !state.buffer.is_empty() {
+ error_msg.push_str(&format!(
+ "\nRemaining buffer: {}",
+ &state.buffer[..state.buffer.len().min(1000)]
+ ));
+ }
+
+ return Err(error_msg);
+ }
+ Err(e) => {
+ return Err(format!("Stream error: {}", e));
+ }
+ }
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(GoogleAiProvider);
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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 = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "open_router"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "zed_extension_api",
+]
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.8.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,17 @@
+[package]
+name = "open_router"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/open_router.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
@@ -0,0 +1,10 @@
+id = "open_router"
+name = "OpenRouter"
+description = "OpenRouter LLM provider - access multiple AI models through a unified API."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.open_router]
+name = "OpenRouter"
@@ -0,0 +1,830 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy};
+use zed_extension_api::{self as zed, *};
+
+struct OpenRouterProvider {
+ streams: Mutex<HashMap<String, StreamState>>,
+ next_stream_id: Mutex<u64>,
+}
+
+struct StreamState {
+ response_stream: Option<HttpResponseStream>,
+ buffer: String,
+ started: bool,
+ tool_calls: HashMap<usize, AccumulatedToolCall>,
+ tool_calls_emitted: bool,
+}
+
+#[derive(Clone, Default)]
+struct AccumulatedToolCall {
+ id: String,
+ name: String,
+ arguments: String,
+}
+
+struct ModelDefinition {
+ id: &'static str,
+ display_name: &'static str,
+ max_tokens: u64,
+ max_output_tokens: Option<u64>,
+ supports_images: bool,
+ supports_tools: bool,
+ is_default: bool,
+ is_default_fast: bool,
+}
+
+const MODELS: &[ModelDefinition] = &[
+ // Anthropic Models
+ ModelDefinition {
+ id: "anthropic/claude-sonnet-4",
+ display_name: "Claude Sonnet 4",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: true,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "anthropic/claude-opus-4",
+ display_name: "Claude Opus 4",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "anthropic/claude-haiku-4",
+ display_name: "Claude Haiku 4",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: true,
+ },
+ ModelDefinition {
+ id: "anthropic/claude-3.5-sonnet",
+ display_name: "Claude 3.5 Sonnet",
+ max_tokens: 200_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // OpenAI Models
+ ModelDefinition {
+ id: "openai/gpt-4o",
+ display_name: "GPT-4o",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "openai/gpt-4o-mini",
+ display_name: "GPT-4o Mini",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "openai/o1",
+ display_name: "o1",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: true,
+ supports_tools: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "openai/o3-mini",
+ display_name: "o3-mini",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: false,
+ supports_tools: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // Google Models
+ ModelDefinition {
+ id: "google/gemini-2.0-flash-001",
+ display_name: "Gemini 2.0 Flash",
+ max_tokens: 1_000_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "google/gemini-2.5-pro-preview",
+ display_name: "Gemini 2.5 Pro",
+ max_tokens: 1_000_000,
+ max_output_tokens: Some(8_192),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // Meta Models
+ ModelDefinition {
+ id: "meta-llama/llama-3.3-70b-instruct",
+ display_name: "Llama 3.3 70B",
+ max_tokens: 128_000,
+ max_output_tokens: Some(4_096),
+ supports_images: false,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "meta-llama/llama-4-maverick",
+ display_name: "Llama 4 Maverick",
+ max_tokens: 128_000,
+ max_output_tokens: Some(4_096),
+ supports_images: true,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // Mistral Models
+ ModelDefinition {
+ id: "mistralai/mistral-large-2411",
+ display_name: "Mistral Large",
+ max_tokens: 128_000,
+ max_output_tokens: Some(4_096),
+ supports_images: false,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "mistralai/codestral-latest",
+ display_name: "Codestral",
+ max_tokens: 32_000,
+ max_output_tokens: Some(4_096),
+ supports_images: false,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // DeepSeek Models
+ ModelDefinition {
+ id: "deepseek/deepseek-chat-v3-0324",
+ display_name: "DeepSeek V3",
+ max_tokens: 64_000,
+ max_output_tokens: Some(8_192),
+ supports_images: false,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ id: "deepseek/deepseek-r1",
+ display_name: "DeepSeek R1",
+ max_tokens: 64_000,
+ max_output_tokens: Some(8_192),
+ supports_images: false,
+ supports_tools: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ // Qwen Models
+ ModelDefinition {
+ id: "qwen/qwen3-235b-a22b",
+ display_name: "Qwen 3 235B",
+ max_tokens: 40_000,
+ max_output_tokens: Some(8_192),
+ supports_images: false,
+ supports_tools: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+];
+
+fn get_model_definition(model_id: &str) -> Option<&'static ModelDefinition> {
+ MODELS.iter().find(|m| m.id == model_id)
+}
+
+#[derive(Serialize)]
+struct OpenRouterRequest {
+ model: String,
+ messages: Vec<OpenRouterMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ max_tokens: Option<u64>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ tools: Vec<OpenRouterTool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_choice: Option<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ stop: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ temperature: Option<f32>,
+ stream: bool,
+}
+
+#[derive(Serialize)]
+struct OpenRouterMessage {
+ role: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ content: Option<OpenRouterContent>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_calls: Option<Vec<OpenRouterToolCall>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_call_id: Option<String>,
+}
+
+#[derive(Serialize, Clone)]
+#[serde(untagged)]
+enum OpenRouterContent {
+ Text(String),
+ Parts(Vec<OpenRouterContentPart>),
+}
+
+#[derive(Serialize, Clone)]
+#[serde(tag = "type")]
+enum OpenRouterContentPart {
+ #[serde(rename = "text")]
+ Text { text: String },
+ #[serde(rename = "image_url")]
+ ImageUrl { image_url: ImageUrl },
+}
+
+#[derive(Serialize, Clone)]
+struct ImageUrl {
+ url: String,
+}
+
+#[derive(Serialize, Clone)]
+struct OpenRouterToolCall {
+ id: String,
+ #[serde(rename = "type")]
+ call_type: String,
+ function: OpenRouterFunctionCall,
+}
+
+#[derive(Serialize, Clone)]
+struct OpenRouterFunctionCall {
+ name: String,
+ arguments: String,
+}
+
+#[derive(Serialize)]
+struct OpenRouterTool {
+ #[serde(rename = "type")]
+ tool_type: String,
+ function: OpenRouterFunctionDef,
+}
+
+#[derive(Serialize)]
+struct OpenRouterFunctionDef {
+ name: String,
+ description: String,
+ parameters: serde_json::Value,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenRouterStreamResponse {
+ choices: Vec<OpenRouterStreamChoice>,
+ #[serde(default)]
+ usage: Option<OpenRouterUsage>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenRouterStreamChoice {
+ delta: OpenRouterDelta,
+ finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct OpenRouterDelta {
+ #[serde(default)]
+ content: Option<String>,
+ #[serde(default)]
+ tool_calls: Option<Vec<OpenRouterToolCallDelta>>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenRouterToolCallDelta {
+ index: usize,
+ #[serde(default)]
+ id: Option<String>,
+ #[serde(default)]
+ function: Option<OpenRouterFunctionDelta>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct OpenRouterFunctionDelta {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ arguments: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenRouterUsage {
+ prompt_tokens: u64,
+ completion_tokens: u64,
+}
+
+fn convert_request(
+ model_id: &str,
+ request: &LlmCompletionRequest,
+) -> Result<OpenRouterRequest, String> {
+ let mut messages: Vec<OpenRouterMessage> = Vec::new();
+
+ for msg in &request.messages {
+ match msg.role {
+ LlmMessageRole::System => {
+ let mut text_content = String::new();
+ for content in &msg.content {
+ if let LlmMessageContent::Text(text) = content {
+ if !text_content.is_empty() {
+ text_content.push('\n');
+ }
+ text_content.push_str(text);
+ }
+ }
+ if !text_content.is_empty() {
+ messages.push(OpenRouterMessage {
+ role: "system".to_string(),
+ content: Some(OpenRouterContent::Text(text_content)),
+ tool_calls: None,
+ tool_call_id: None,
+ });
+ }
+ }
+ LlmMessageRole::User => {
+ let mut parts: Vec<OpenRouterContentPart> = Vec::new();
+ let mut tool_result_messages: Vec<OpenRouterMessage> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ parts.push(OpenRouterContentPart::Text { text: text.clone() });
+ }
+ }
+ LlmMessageContent::Image(img) => {
+ let data_url = format!("data:image/png;base64,{}", img.source);
+ parts.push(OpenRouterContentPart::ImageUrl {
+ image_url: ImageUrl { url: data_url },
+ });
+ }
+ LlmMessageContent::ToolResult(result) => {
+ let content_text = match &result.content {
+ LlmToolResultContent::Text(t) => t.clone(),
+ LlmToolResultContent::Image(_) => "[Image]".to_string(),
+ };
+ tool_result_messages.push(OpenRouterMessage {
+ role: "tool".to_string(),
+ content: Some(OpenRouterContent::Text(content_text)),
+ tool_calls: None,
+ tool_call_id: Some(result.tool_use_id.clone()),
+ });
+ }
+ _ => {}
+ }
+ }
+
+ if !parts.is_empty() {
+ let content = if parts.len() == 1 {
+ if let OpenRouterContentPart::Text { text } = &parts[0] {
+ OpenRouterContent::Text(text.clone())
+ } else {
+ OpenRouterContent::Parts(parts)
+ }
+ } else {
+ OpenRouterContent::Parts(parts)
+ };
+
+ messages.push(OpenRouterMessage {
+ role: "user".to_string(),
+ content: Some(content),
+ tool_calls: None,
+ tool_call_id: None,
+ });
+ }
+
+ messages.extend(tool_result_messages);
+ }
+ LlmMessageRole::Assistant => {
+ let mut text_content = String::new();
+ let mut tool_calls: Vec<OpenRouterToolCall> = Vec::new();
+
+ for content in &msg.content {
+ match content {
+ LlmMessageContent::Text(text) => {
+ if !text.is_empty() {
+ if !text_content.is_empty() {
+ text_content.push('\n');
+ }
+ text_content.push_str(text);
+ }
+ }
+ LlmMessageContent::ToolUse(tool_use) => {
+ tool_calls.push(OpenRouterToolCall {
+ id: tool_use.id.clone(),
+ call_type: "function".to_string(),
+ function: OpenRouterFunctionCall {
+ name: tool_use.name.clone(),
+ arguments: tool_use.input.clone(),
+ },
+ });
+ }
+ _ => {}
+ }
+ }
+
+ messages.push(OpenRouterMessage {
+ role: "assistant".to_string(),
+ content: if text_content.is_empty() {
+ None
+ } else {
+ Some(OpenRouterContent::Text(text_content))
+ },
+ tool_calls: if tool_calls.is_empty() {
+ None
+ } else {
+ Some(tool_calls)
+ },
+ tool_call_id: None,
+ });
+ }
+ }
+ }
+
+ let model_def = get_model_definition(model_id);
+ let supports_tools = model_def.map(|m| m.supports_tools).unwrap_or(true);
+
+ let tools: Vec<OpenRouterTool> = if supports_tools {
+ request
+ .tools
+ .iter()
+ .map(|t| OpenRouterTool {
+ tool_type: "function".to_string(),
+ function: OpenRouterFunctionDef {
+ name: t.name.clone(),
+ description: t.description.clone(),
+ parameters: serde_json::from_str(&t.input_schema)
+ .unwrap_or(serde_json::Value::Object(Default::default())),
+ },
+ })
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ let tool_choice = if supports_tools {
+ request.tool_choice.as_ref().map(|tc| match tc {
+ LlmToolChoice::Auto => "auto".to_string(),
+ LlmToolChoice::Any => "required".to_string(),
+ LlmToolChoice::None => "none".to_string(),
+ })
+ } else {
+ None
+ };
+
+ let max_tokens = request
+ .max_tokens
+ .or(model_def.and_then(|m| m.max_output_tokens));
+
+ Ok(OpenRouterRequest {
+ model: model_id.to_string(),
+ messages,
+ max_tokens,
+ tools,
+ tool_choice,
+ stop: request.stop_sequences.clone(),
+ temperature: request.temperature,
+ stream: true,
+ })
+}
+
+fn parse_sse_line(line: &str) -> Option<OpenRouterStreamResponse> {
+ let data = line.strip_prefix("data: ")?;
+ if data.trim() == "[DONE]" {
+ return None;
+ }
+ serde_json::from_str(data).ok()
+}
+
+impl zed::Extension for OpenRouterProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "open_router".into(),
+ name: "OpenRouter".into(),
+ icon: None,
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(MODELS
+ .iter()
+ .map(|m| LlmModelInfo {
+ id: m.id.to_string(),
+ name: m.display_name.to_string(),
+ max_token_count: m.max_tokens,
+ max_output_tokens: m.max_output_tokens,
+ capabilities: LlmModelCapabilities {
+ supports_images: m.supports_images,
+ supports_tools: m.supports_tools,
+ supports_tool_choice_auto: m.supports_tools,
+ supports_tool_choice_any: m.supports_tools,
+ supports_tool_choice_none: m.supports_tools,
+ supports_thinking: false,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: m.is_default,
+ is_default_fast: m.is_default_fast,
+ })
+ .collect())
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ llm_get_credential("open_router").is_some()
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(
+ r#"# OpenRouter Setup
+
+Welcome to **OpenRouter**! Access multiple AI models through a single API.
+
+## Configuration
+
+Enter your OpenRouter API key below. Get your API key at [openrouter.ai/keys](https://openrouter.ai/keys).
+
+## Available Models
+
+### Anthropic
+| Model | Context | Output |
+|-------|---------|--------|
+| Claude Sonnet 4 | 200K | 8K |
+| Claude Opus 4 | 200K | 8K |
+| Claude Haiku 4 | 200K | 8K |
+| Claude 3.5 Sonnet | 200K | 8K |
+
+### OpenAI
+| Model | Context | Output |
+|-------|---------|--------|
+| GPT-4o | 128K | 16K |
+| GPT-4o Mini | 128K | 16K |
+| o1 | 200K | 100K |
+| o3-mini | 200K | 100K |
+
+### Google
+| Model | Context | Output |
+|-------|---------|--------|
+| Gemini 2.0 Flash | 1M | 8K |
+| Gemini 2.5 Pro | 1M | 8K |
+
+### Meta
+| Model | Context | Output |
+|-------|---------|--------|
+| Llama 3.3 70B | 128K | 4K |
+| Llama 4 Maverick | 128K | 4K |
+
+### Mistral
+| Model | Context | Output |
+|-------|---------|--------|
+| Mistral Large | 128K | 4K |
+| Codestral | 32K | 4K |
+
+### DeepSeek
+| Model | Context | Output |
+|-------|---------|--------|
+| DeepSeek V3 | 64K | 8K |
+| DeepSeek R1 | 64K | 8K |
+
+### Qwen
+| Model | Context | Output |
+|-------|---------|--------|
+| Qwen 3 235B | 40K | 8K |
+
+## Features
+
+- ✅ Full streaming support
+- ✅ Tool/function calling (model dependent)
+- ✅ Vision (model dependent)
+- ✅ Access to 200+ models
+- ✅ Unified billing
+
+## Pricing
+
+Pay-per-use based on model. See [openrouter.ai/models](https://openrouter.ai/models) for pricing.
+"#
+ .to_string(),
+ )
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ let provided = llm_request_credential(
+ "open_router",
+ LlmCredentialType::ApiKey,
+ "OpenRouter API Key",
+ "sk-or-v1-...",
+ )?;
+ if provided {
+ Ok(())
+ } else {
+ Err("Authentication cancelled".to_string())
+ }
+ }
+
+ fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
+ llm_delete_credential("open_router")
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ let api_key = llm_get_credential("open_router").ok_or_else(|| {
+ "No API key configured. Please add your OpenRouter API key in settings.".to_string()
+ })?;
+
+ let openrouter_request = convert_request(model_id, request)?;
+
+ let body = serde_json::to_vec(&openrouter_request)
+ .map_err(|e| format!("Failed to serialize request: {}", e))?;
+
+ let http_request = HttpRequest {
+ method: HttpMethod::Post,
+ url: "https://openrouter.ai/api/v1/chat/completions".to_string(),
+ headers: vec![
+ ("Content-Type".to_string(), "application/json".to_string()),
+ ("Authorization".to_string(), format!("Bearer {}", api_key)),
+ ("HTTP-Referer".to_string(), "https://zed.dev".to_string()),
+ ("X-Title".to_string(), "Zed Editor".to_string()),
+ ],
+ body: Some(body),
+ redirect_policy: RedirectPolicy::FollowAll,
+ };
+
+ let response_stream = http_request
+ .fetch_stream()
+ .map_err(|e| format!("HTTP request failed: {}", e))?;
+
+ let stream_id = {
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let id = format!("openrouter-stream-{}", *id_counter);
+ *id_counter += 1;
+ id
+ };
+
+ self.streams.lock().unwrap().insert(
+ stream_id.clone(),
+ StreamState {
+ response_stream: Some(response_stream),
+ buffer: String::new(),
+ started: false,
+ tool_calls: HashMap::new(),
+ tool_calls_emitted: false,
+ },
+ );
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ let state = streams
+ .get_mut(stream_id)
+ .ok_or_else(|| format!("Unknown stream: {}", stream_id))?;
+
+ if !state.started {
+ state.started = true;
+ return Ok(Some(LlmCompletionEvent::Started));
+ }
+
+ let response_stream = state
+ .response_stream
+ .as_mut()
+ .ok_or_else(|| "Stream already closed".to_string())?;
+
+ loop {
+ if let Some(newline_pos) = state.buffer.find('\n') {
+ let line = state.buffer[..newline_pos].to_string();
+ state.buffer = state.buffer[newline_pos + 1..].to_string();
+
+ if line.trim().is_empty() {
+ continue;
+ }
+
+ if let Some(response) = parse_sse_line(&line) {
+ if let Some(choice) = response.choices.first() {
+ if let Some(content) = &choice.delta.content {
+ if !content.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(content.clone())));
+ }
+ }
+
+ if let Some(tool_calls) = &choice.delta.tool_calls {
+ for tc in tool_calls {
+ let entry = state
+ .tool_calls
+ .entry(tc.index)
+ .or_insert_with(AccumulatedToolCall::default);
+
+ if let Some(id) = &tc.id {
+ entry.id = id.clone();
+ }
+ if let Some(func) = &tc.function {
+ if let Some(name) = &func.name {
+ entry.name = name.clone();
+ }
+ if let Some(args) = &func.arguments {
+ entry.arguments.push_str(args);
+ }
+ }
+ }
+ }
+
+ if let Some(finish_reason) = &choice.finish_reason {
+ if !state.tool_calls.is_empty() && !state.tool_calls_emitted {
+ state.tool_calls_emitted = true;
+ let mut tool_calls: Vec<_> = state.tool_calls.drain().collect();
+ tool_calls.sort_by_key(|(idx, _)| *idx);
+
+ if let Some((_, tc)) = tool_calls.into_iter().next() {
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id: tc.id,
+ name: tc.name,
+ input: tc.arguments,
+ thought_signature: None,
+ })));
+ }
+ }
+
+ let stop_reason = match finish_reason.as_str() {
+ "stop" => LlmStopReason::EndTurn,
+ "length" => LlmStopReason::MaxTokens,
+ "tool_calls" => LlmStopReason::ToolUse,
+ "content_filter" => LlmStopReason::Refusal,
+ _ => LlmStopReason::EndTurn,
+ };
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+ }
+
+ if let Some(usage) = response.usage {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.prompt_tokens,
+ output_tokens: usage.completion_tokens,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ })));
+ }
+ }
+
+ continue;
+ }
+
+ match response_stream.next_chunk() {
+ Ok(Some(chunk)) => {
+ let text = String::from_utf8_lossy(&chunk);
+ state.buffer.push_str(&text);
+ }
+ Ok(None) => {
+ return Ok(None);
+ }
+ Err(e) => {
+ return Err(format!("Stream error: {}", e));
+ }
+ }
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(OpenRouterProvider);
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "auditable-serde"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5"
+dependencies = [
+ "semver",
+ "serde",
+ "serde_json",
+ "topological-sort",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fopenai"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "zed_extension_api",
+]
+
+[[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 = "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 = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[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 = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[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 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[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 = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[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 = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[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 = "spdx"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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 = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[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 = "wasm-encoder"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d"
+dependencies = [
+ "anyhow",
+ "auditable-serde",
+ "flate2",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "spdx",
+ "url",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de"
+dependencies = [
+ "wit-bindgen-rt",
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621"
+dependencies = [
+ "bitflags",
+ "futures",
+ "once_cell",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.227.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[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 = "zed_extension_api"
+version = "0.7.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "wit-bindgen",
+]
+
+[[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 = "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",
+]
@@ -0,0 +1,17 @@
+[package]
+name = "openai"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[workspace]
+
+[lib]
+path = "src/openai.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+zed_extension_api = { path = "../../crates/extension_api" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
@@ -0,0 +1,10 @@
+id = "openai"
+name = "OpenAI"
+description = "OpenAI GPT LLM provider for Zed."
+version = "0.1.0"
+schema_version = 1
+authors = ["Zed Team"]
+repository = "https://github.com/zed-industries/zed"
+
+[language_model_providers.openai]
+name = "OpenAI"
@@ -0,0 +1,727 @@
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use serde::{Deserialize, Serialize};
+use zed_extension_api::http_client::{HttpMethod, HttpRequest, HttpResponseStream, RedirectPolicy};
+use zed_extension_api::{self as zed, *};
+
+struct OpenAiProvider {
+ streams: Mutex<HashMap<String, StreamState>>,
+ next_stream_id: Mutex<u64>,
+}
+
+struct StreamState {
+ response_stream: Option<HttpResponseStream>,
+ buffer: String,
+ started: bool,
+ tool_calls: HashMap<usize, AccumulatedToolCall>,
+ tool_calls_emitted: bool,
+}
+
+#[derive(Clone, Default)]
+struct AccumulatedToolCall {
+ id: String,
+ name: String,
+ arguments: String,
+}
+
+struct ModelDefinition {
+ real_id: &'static str,
+ display_name: &'static str,
+ max_tokens: u64,
+ max_output_tokens: Option<u64>,
+ supports_images: bool,
+ is_default: bool,
+ is_default_fast: bool,
+}
+
+const MODELS: &[ModelDefinition] = &[
+ ModelDefinition {
+ real_id: "gpt-4o",
+ display_name: "GPT-4o",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ is_default: true,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gpt-4o-mini",
+ display_name: "GPT-4o-mini",
+ max_tokens: 128_000,
+ max_output_tokens: Some(16_384),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: true,
+ },
+ ModelDefinition {
+ real_id: "gpt-4.1",
+ display_name: "GPT-4.1",
+ max_tokens: 1_047_576,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gpt-4.1-mini",
+ display_name: "GPT-4.1-mini",
+ max_tokens: 1_047_576,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gpt-4.1-nano",
+ display_name: "GPT-4.1-nano",
+ max_tokens: 1_047_576,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gpt-5",
+ display_name: "GPT-5",
+ max_tokens: 272_000,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "gpt-5-mini",
+ display_name: "GPT-5-mini",
+ max_tokens: 272_000,
+ max_output_tokens: Some(32_768),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "o1",
+ display_name: "o1",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "o3",
+ display_name: "o3",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "o3-mini",
+ display_name: "o3-mini",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: false,
+ is_default: false,
+ is_default_fast: false,
+ },
+ ModelDefinition {
+ real_id: "o4-mini",
+ display_name: "o4-mini",
+ max_tokens: 200_000,
+ max_output_tokens: Some(100_000),
+ supports_images: true,
+ is_default: false,
+ is_default_fast: false,
+ },
+];
+
+fn get_real_model_id(display_name: &str) -> Option<&'static str> {
+ MODELS
+ .iter()
+ .find(|m| m.display_name == display_name)
+ .map(|m| m.real_id)
+}
+
+#[derive(Serialize)]
+struct OpenAiRequest {
+ model: String,
+ messages: Vec<OpenAiMessage>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tools: Option<Vec<OpenAiTool>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_choice: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ temperature: Option<f32>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ max_tokens: Option<u64>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ stop: Vec<String>,
+ stream: bool,
+ stream_options: Option<StreamOptions>,
+}
+
+#[derive(Serialize)]
+struct StreamOptions {
+ include_usage: bool,
+}
+
+#[derive(Serialize)]
+#[serde(tag = "role")]
+enum OpenAiMessage {
+ #[serde(rename = "system")]
+ System { content: String },
+ #[serde(rename = "user")]
+ User { content: Vec<OpenAiContentPart> },
+ #[serde(rename = "assistant")]
+ Assistant {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ content: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tool_calls: Option<Vec<OpenAiToolCall>>,
+ },
+ #[serde(rename = "tool")]
+ Tool {
+ tool_call_id: String,
+ content: String,
+ },
+}
+
+#[derive(Serialize)]
+#[serde(tag = "type")]
+enum OpenAiContentPart {
+ #[serde(rename = "text")]
+ Text { text: String },
+ #[serde(rename = "image_url")]
+ ImageUrl { image_url: ImageUrl },
+}
+
+#[derive(Serialize)]
+struct ImageUrl {
+ url: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct OpenAiToolCall {
+ id: String,
+ #[serde(rename = "type")]
+ call_type: String,
+ function: OpenAiFunctionCall,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct OpenAiFunctionCall {
+ name: String,
+ arguments: String,
+}
+
+#[derive(Serialize)]
+struct OpenAiTool {
+ #[serde(rename = "type")]
+ tool_type: String,
+ function: OpenAiFunctionDef,
+}
+
+#[derive(Serialize)]
+struct OpenAiFunctionDef {
+ name: String,
+ description: String,
+ parameters: serde_json::Value,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiStreamEvent {
+ choices: Vec<OpenAiChoice>,
+ #[serde(default)]
+ usage: Option<OpenAiUsage>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiChoice {
+ delta: OpenAiDelta,
+ finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug, Default)]
+struct OpenAiDelta {
+ #[serde(default)]
+ content: Option<String>,
+ #[serde(default)]
+ tool_calls: Option<Vec<OpenAiToolCallDelta>>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiToolCallDelta {
+ index: usize,
+ #[serde(default)]
+ id: Option<String>,
+ #[serde(default)]
+ function: Option<OpenAiFunctionDelta>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiFunctionDelta {
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ arguments: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+struct OpenAiUsage {
+ prompt_tokens: u64,
+ completion_tokens: u64,
+}
+
+#[allow(dead_code)]
+#[derive(Deserialize, Debug)]
+struct OpenAiError {
+ error: OpenAiErrorDetail,
+}
+
+#[allow(dead_code)]
+#[derive(Deserialize, Debug)]
+struct OpenAiErrorDetail {
+ message: String,
+}
+
+fn convert_request(
+ model_id: &str,
+ request: &LlmCompletionRequest,
+) -> Result<OpenAiRequest, String> {
+ let real_model_id =
+ get_real_model_id(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?;
+
+ let mut messages = Vec::new();
+
+ for msg in &request.messages {
+ match msg.role {
+ LlmMessageRole::System => {
+ let text: String = msg
+ .content
+ .iter()
+ .filter_map(|c| match c {
+ LlmMessageContent::Text(t) => Some(t.as_str()),
+ _ => None,
+ })
+ .collect::<Vec<_>>()
+ .join("\n");
+ if !text.is_empty() {
+ messages.push(OpenAiMessage::System { content: text });
+ }
+ }
+ LlmMessageRole::User => {
+ let parts: Vec<OpenAiContentPart> = msg
+ .content
+ .iter()
+ .filter_map(|c| match c {
+ LlmMessageContent::Text(t) => {
+ Some(OpenAiContentPart::Text { text: t.clone() })
+ }
+ LlmMessageContent::Image(img) => Some(OpenAiContentPart::ImageUrl {
+ image_url: ImageUrl {
+ url: format!("data:image/png;base64,{}", img.source),
+ },
+ }),
+ LlmMessageContent::ToolResult(_) => None,
+ _ => None,
+ })
+ .collect();
+
+ for content in &msg.content {
+ if let LlmMessageContent::ToolResult(result) = content {
+ let content_text = match &result.content {
+ LlmToolResultContent::Text(t) => t.clone(),
+ LlmToolResultContent::Image(_) => "[Image]".to_string(),
+ };
+ messages.push(OpenAiMessage::Tool {
+ tool_call_id: result.tool_use_id.clone(),
+ content: content_text,
+ });
+ }
+ }
+
+ if !parts.is_empty() {
+ messages.push(OpenAiMessage::User { content: parts });
+ }
+ }
+ LlmMessageRole::Assistant => {
+ let mut content_text: Option<String> = None;
+ let mut tool_calls: Vec<OpenAiToolCall> = Vec::new();
+
+ for c in &msg.content {
+ match c {
+ LlmMessageContent::Text(t) => {
+ content_text = Some(t.clone());
+ }
+ LlmMessageContent::ToolUse(tool_use) => {
+ tool_calls.push(OpenAiToolCall {
+ id: tool_use.id.clone(),
+ call_type: "function".to_string(),
+ function: OpenAiFunctionCall {
+ name: tool_use.name.clone(),
+ arguments: tool_use.input.clone(),
+ },
+ });
+ }
+ _ => {}
+ }
+ }
+
+ messages.push(OpenAiMessage::Assistant {
+ content: content_text,
+ tool_calls: if tool_calls.is_empty() {
+ None
+ } else {
+ Some(tool_calls)
+ },
+ });
+ }
+ }
+ }
+
+ let tools: Option<Vec<OpenAiTool>> = if request.tools.is_empty() {
+ None
+ } else {
+ Some(
+ request
+ .tools
+ .iter()
+ .map(|t| OpenAiTool {
+ tool_type: "function".to_string(),
+ function: OpenAiFunctionDef {
+ name: t.name.clone(),
+ description: t.description.clone(),
+ parameters: serde_json::from_str(&t.input_schema)
+ .unwrap_or(serde_json::Value::Object(Default::default())),
+ },
+ })
+ .collect(),
+ )
+ };
+
+ let tool_choice = request.tool_choice.as_ref().map(|tc| match tc {
+ LlmToolChoice::Auto => "auto".to_string(),
+ LlmToolChoice::Any => "required".to_string(),
+ LlmToolChoice::None => "none".to_string(),
+ });
+
+ Ok(OpenAiRequest {
+ model: real_model_id.to_string(),
+ messages,
+ tools,
+ tool_choice,
+ temperature: request.temperature,
+ max_tokens: request.max_tokens,
+ stop: request.stop_sequences.clone(),
+ stream: true,
+ stream_options: Some(StreamOptions {
+ include_usage: true,
+ }),
+ })
+}
+
+fn parse_sse_line(line: &str) -> Option<OpenAiStreamEvent> {
+ if let Some(data) = line.strip_prefix("data: ") {
+ if data == "[DONE]" {
+ return None;
+ }
+ serde_json::from_str(data).ok()
+ } else {
+ None
+ }
+}
+
+impl zed::Extension for OpenAiProvider {
+ fn new() -> Self {
+ Self {
+ streams: Mutex::new(HashMap::new()),
+ next_stream_id: Mutex::new(0),
+ }
+ }
+
+ fn llm_providers(&self) -> Vec<LlmProviderInfo> {
+ vec![LlmProviderInfo {
+ id: "openai".into(),
+ name: "OpenAI".into(),
+ icon: Some("openai".into()),
+ }]
+ }
+
+ fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
+ Ok(MODELS
+ .iter()
+ .map(|m| LlmModelInfo {
+ id: m.display_name.to_string(),
+ name: m.display_name.to_string(),
+ max_token_count: m.max_tokens,
+ max_output_tokens: m.max_output_tokens,
+ capabilities: LlmModelCapabilities {
+ supports_images: m.supports_images,
+ supports_tools: true,
+ supports_tool_choice_auto: true,
+ supports_tool_choice_any: true,
+ supports_tool_choice_none: true,
+ supports_thinking: false,
+ tool_input_format: LlmToolInputFormat::JsonSchema,
+ },
+ is_default: m.is_default,
+ is_default_fast: m.is_default_fast,
+ })
+ .collect())
+ }
+
+ fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
+ llm_get_credential("openai").is_some()
+ }
+
+ fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
+ Some(
+ r#"# OpenAI Setup
+
+Welcome to **OpenAI**! This extension provides access to OpenAI GPT models.
+
+## Configuration
+
+Enter your OpenAI API key below. You can find your API key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys).
+
+## Available Models
+
+| Display Name | Real Model | Context | Output |
+|--------------|------------|---------|--------|
+| GPT-4o | gpt-4o | 128K | 16K |
+| GPT-4o-mini | gpt-4o-mini | 128K | 16K |
+| GPT-4.1 | gpt-4.1 | 1M | 32K |
+| GPT-4.1-mini | gpt-4.1-mini | 1M | 32K |
+| GPT-5 | gpt-5 | 272K | 32K |
+| GPT-5-mini | gpt-5-mini | 272K | 32K |
+| o1 | o1 | 200K | 100K |
+| o3 | o3 | 200K | 100K |
+| o3-mini | o3-mini | 200K | 100K |
+
+## Features
+
+- ✅ Full streaming support
+- ✅ Tool/function calling
+- ✅ Vision (image inputs)
+- ✅ All OpenAI models
+
+## Pricing
+
+Uses your OpenAI API credits. See [OpenAI pricing](https://openai.com/pricing) for details.
+"#
+ .to_string(),
+ )
+ }
+
+ fn llm_provider_authenticate(&mut self, _provider_id: &str) -> Result<(), String> {
+ let provided = llm_request_credential(
+ "openai",
+ LlmCredentialType::ApiKey,
+ "OpenAI API Key",
+ "sk-...",
+ )?;
+ if provided {
+ Ok(())
+ } else {
+ Err("Authentication cancelled".to_string())
+ }
+ }
+
+ fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
+ llm_delete_credential("openai")
+ }
+
+ fn llm_stream_completion_start(
+ &mut self,
+ _provider_id: &str,
+ model_id: &str,
+ request: &LlmCompletionRequest,
+ ) -> Result<String, String> {
+ let api_key = llm_get_credential("openai").ok_or_else(|| {
+ "No API key configured. Please add your OpenAI API key in settings.".to_string()
+ })?;
+
+ let openai_request = convert_request(model_id, request)?;
+
+ let body = serde_json::to_vec(&openai_request)
+ .map_err(|e| format!("Failed to serialize request: {}", e))?;
+
+ let http_request = HttpRequest {
+ method: HttpMethod::Post,
+ url: "https://api.openai.com/v1/chat/completions".to_string(),
+ headers: vec![
+ ("Content-Type".to_string(), "application/json".to_string()),
+ ("Authorization".to_string(), format!("Bearer {}", api_key)),
+ ],
+ body: Some(body),
+ redirect_policy: RedirectPolicy::FollowAll,
+ };
+
+ let response_stream = http_request
+ .fetch_stream()
+ .map_err(|e| format!("HTTP request failed: {}", e))?;
+
+ let stream_id = {
+ let mut id_counter = self.next_stream_id.lock().unwrap();
+ let id = format!("openai-stream-{}", *id_counter);
+ *id_counter += 1;
+ id
+ };
+
+ self.streams.lock().unwrap().insert(
+ stream_id.clone(),
+ StreamState {
+ response_stream: Some(response_stream),
+ buffer: String::new(),
+ started: false,
+ tool_calls: HashMap::new(),
+ tool_calls_emitted: false,
+ },
+ );
+
+ Ok(stream_id)
+ }
+
+ fn llm_stream_completion_next(
+ &mut self,
+ stream_id: &str,
+ ) -> Result<Option<LlmCompletionEvent>, String> {
+ let mut streams = self.streams.lock().unwrap();
+ let state = streams
+ .get_mut(stream_id)
+ .ok_or_else(|| format!("Unknown stream: {}", stream_id))?;
+
+ if !state.started {
+ state.started = true;
+ return Ok(Some(LlmCompletionEvent::Started));
+ }
+
+ let response_stream = state
+ .response_stream
+ .as_mut()
+ .ok_or_else(|| "Stream already closed".to_string())?;
+
+ loop {
+ if let Some(newline_pos) = state.buffer.find('\n') {
+ let line = state.buffer[..newline_pos].trim().to_string();
+ state.buffer = state.buffer[newline_pos + 1..].to_string();
+
+ if line.is_empty() {
+ continue;
+ }
+
+ if let Some(event) = parse_sse_line(&line) {
+ if let Some(choice) = event.choices.first() {
+ if let Some(tool_calls) = &choice.delta.tool_calls {
+ for tc in tool_calls {
+ let entry = state.tool_calls.entry(tc.index).or_default();
+
+ if let Some(id) = &tc.id {
+ entry.id = id.clone();
+ }
+
+ if let Some(func) = &tc.function {
+ if let Some(name) = &func.name {
+ entry.name = name.clone();
+ }
+ if let Some(args) = &func.arguments {
+ entry.arguments.push_str(args);
+ }
+ }
+ }
+ }
+
+ if let Some(reason) = &choice.finish_reason {
+ if reason == "tool_calls" && !state.tool_calls_emitted {
+ state.tool_calls_emitted = true;
+ if let Some((&index, _)) = state.tool_calls.iter().next() {
+ if let Some(tool_call) = state.tool_calls.remove(&index) {
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id: tool_call.id,
+ name: tool_call.name,
+ input: tool_call.arguments,
+ thought_signature: None,
+ })));
+ }
+ }
+ }
+
+ let stop_reason = match reason.as_str() {
+ "stop" => LlmStopReason::EndTurn,
+ "length" => LlmStopReason::MaxTokens,
+ "tool_calls" => LlmStopReason::ToolUse,
+ "content_filter" => LlmStopReason::Refusal,
+ _ => LlmStopReason::EndTurn,
+ };
+
+ if let Some(usage) = event.usage {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.prompt_tokens,
+ output_tokens: usage.completion_tokens,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ })));
+ }
+
+ return Ok(Some(LlmCompletionEvent::Stop(stop_reason)));
+ }
+
+ if let Some(content) = &choice.delta.content {
+ if !content.is_empty() {
+ return Ok(Some(LlmCompletionEvent::Text(content.clone())));
+ }
+ }
+ }
+
+ if event.choices.is_empty() {
+ if let Some(usage) = event.usage {
+ return Ok(Some(LlmCompletionEvent::Usage(LlmTokenUsage {
+ input_tokens: usage.prompt_tokens,
+ output_tokens: usage.completion_tokens,
+ cache_creation_input_tokens: None,
+ cache_read_input_tokens: None,
+ })));
+ }
+ }
+ }
+
+ continue;
+ }
+
+ match response_stream.next_chunk() {
+ Ok(Some(chunk)) => {
+ let text = String::from_utf8_lossy(&chunk);
+ state.buffer.push_str(&text);
+ }
+ Ok(None) => {
+ if !state.tool_calls.is_empty() && !state.tool_calls_emitted {
+ state.tool_calls_emitted = true;
+ let keys: Vec<usize> = state.tool_calls.keys().copied().collect();
+ if let Some(&key) = keys.first() {
+ if let Some(tool_call) = state.tool_calls.remove(&key) {
+ return Ok(Some(LlmCompletionEvent::ToolUse(LlmToolUse {
+ id: tool_call.id,
+ name: tool_call.name,
+ input: tool_call.arguments,
+ thought_signature: None,
+ })));
+ }
+ }
+ }
+ return Ok(None);
+ }
+ Err(e) => {
+ return Err(format!("Stream error: {}", e));
+ }
+ }
+ }
+ }
+
+ fn llm_stream_completion_close(&mut self, stream_id: &str) {
+ self.streams.lock().unwrap().remove(stream_id);
+ }
+}
+
+zed::register_extension!(OpenAiProvider);