From e08ab99e8df97a9bc55e48c9e79cb3d064e9d440 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 4 Dec 2025 15:59:53 -0500 Subject: [PATCH] Add extensions for LLM providers --- extensions/anthropic/Cargo.lock | 823 +++++++++++++++++ extensions/anthropic/Cargo.toml | 17 + extensions/anthropic/extension.toml | 10 + extensions/anthropic/src/anthropic.rs | 803 +++++++++++++++++ extensions/copilot_chat/Cargo.lock | 823 +++++++++++++++++ extensions/copilot_chat/Cargo.toml | 17 + extensions/copilot_chat/extension.toml | 10 + extensions/copilot_chat/src/copilot_chat.rs | 696 +++++++++++++++ extensions/example-provider/Cargo.lock | 821 +++++++++++++++++ extensions/example-provider/Cargo.toml | 15 + extensions/example-provider/extension.toml | 10 + .../example-provider/src/example_provider.rs | 181 ++++ extensions/google-ai/Cargo.lock | 823 +++++++++++++++++ extensions/google-ai/Cargo.toml | 17 + extensions/google-ai/extension.toml | 10 + extensions/google-ai/src/google_ai.rs | 840 ++++++++++++++++++ extensions/open_router/Cargo.lock | 823 +++++++++++++++++ extensions/open_router/Cargo.toml | 17 + extensions/open_router/extension.toml | 10 + extensions/open_router/src/open_router.rs | 830 +++++++++++++++++ extensions/openai/Cargo.lock | 823 +++++++++++++++++ extensions/openai/Cargo.toml | 17 + extensions/openai/extension.toml | 10 + extensions/openai/src/openai.rs | 727 +++++++++++++++ 24 files changed, 9173 insertions(+) create mode 100644 extensions/anthropic/Cargo.lock create mode 100644 extensions/anthropic/Cargo.toml create mode 100644 extensions/anthropic/extension.toml create mode 100644 extensions/anthropic/src/anthropic.rs create mode 100644 extensions/copilot_chat/Cargo.lock create mode 100644 extensions/copilot_chat/Cargo.toml create mode 100644 extensions/copilot_chat/extension.toml create mode 100644 extensions/copilot_chat/src/copilot_chat.rs create mode 100644 extensions/example-provider/Cargo.lock create mode 100644 extensions/example-provider/Cargo.toml create mode 100644 extensions/example-provider/extension.toml create mode 100644 extensions/example-provider/src/example_provider.rs create mode 100644 extensions/google-ai/Cargo.lock create mode 100644 extensions/google-ai/Cargo.toml create mode 100644 extensions/google-ai/extension.toml create mode 100644 extensions/google-ai/src/google_ai.rs create mode 100644 extensions/open_router/Cargo.lock create mode 100644 extensions/open_router/Cargo.toml create mode 100644 extensions/open_router/extension.toml create mode 100644 extensions/open_router/src/open_router.rs create mode 100644 extensions/openai/Cargo.lock create mode 100644 extensions/openai/Cargo.toml create mode 100644 extensions/openai/extension.toml create mode 100644 extensions/openai/src/openai.rs diff --git a/extensions/anthropic/Cargo.lock b/extensions/anthropic/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..bd558d1ce1a11cafbe8ac64971fc5a554f135129 --- /dev/null +++ b/extensions/anthropic/Cargo.lock @@ -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", +] diff --git a/extensions/anthropic/Cargo.toml b/extensions/anthropic/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..89c54229513ca16af7311770869dfb1ae45a4c95 --- /dev/null +++ b/extensions/anthropic/Cargo.toml @@ -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" \ No newline at end of file diff --git a/extensions/anthropic/extension.toml b/extensions/anthropic/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..0f5973f7a9bd3647521b45594ffc14d9fca410db --- /dev/null +++ b/extensions/anthropic/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/anthropic/src/anthropic.rs b/extensions/anthropic/src/anthropic.rs new file mode 100644 index 0000000000000000000000000000000000000000..78bf1735bcd5f2c043d2e5ef725692c47ad66871 --- /dev/null +++ b/extensions/anthropic/src/anthropic.rs @@ -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>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + current_tool_use: Option, + stop_reason: Option, + pending_signature: Option, +} + +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, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop_sequences: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct AnthropicThinking { + #[serde(rename = "type")] + thinking_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + budget_tokens: Option, +} + +#[derive(Serialize)] +struct AnthropicMessage { + role: String, + content: Vec, +} + +#[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, +} + +#[derive(Deserialize, Debug, Default)] +struct AnthropicUsage { + #[serde(default)] + input_tokens: Option, + #[serde(default)] + output_tokens: Option, + #[serde(default)] + cache_creation_input_tokens: Option, + #[serde(default)] + cache_read_input_tokens: Option, +} + +#[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 { + let model_def = + get_model_definition(model_id).ok_or_else(|| format!("Unknown model: {}", model_id))?; + + let mut messages: Vec = 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 = 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 = 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 = 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 { + 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 { + vec![LlmProviderInfo { + id: "anthropic".into(), + name: "Anthropic".into(), + icon: Some("anthropic".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + 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, 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); diff --git a/extensions/copilot_chat/Cargo.lock b/extensions/copilot_chat/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..1ba4a97d7291c7661f68c87d5c5dde7d292dada8 --- /dev/null +++ b/extensions/copilot_chat/Cargo.lock @@ -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", +] diff --git a/extensions/copilot_chat/Cargo.toml b/extensions/copilot_chat/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c611e38a8451bc9ea9c88960a0791b0b2a238f4a --- /dev/null +++ b/extensions/copilot_chat/Cargo.toml @@ -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" \ No newline at end of file diff --git a/extensions/copilot_chat/extension.toml b/extensions/copilot_chat/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..213a13b463caec34c9c080ce56ded62b3c438765 --- /dev/null +++ b/extensions/copilot_chat/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/copilot_chat/src/copilot_chat.rs b/extensions/copilot_chat/src/copilot_chat.rs new file mode 100644 index 0000000000000000000000000000000000000000..482e61101f61649ae6dd516420238cc61024b21e --- /dev/null +++ b/extensions/copilot_chat/src/copilot_chat.rs @@ -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>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + 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, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + stream_options: Option, +} + +#[derive(Serialize)] +struct StreamOptions { + include_usage: bool, +} + +#[derive(Serialize)] +struct OpenAiMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenAiContent { + Text(String), + Parts(Vec), +} + +#[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, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiStreamChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = 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 = Vec::new(); + let mut tool_result_messages: Vec = 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 = 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 = 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 { + 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 { + vec![LlmProviderInfo { + id: "copilot_chat".into(), + name: "Copilot Chat".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + 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, 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); diff --git a/extensions/example-provider/Cargo.lock b/extensions/example-provider/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..08b2f0c343b1d323e067add8414906c25a02af1a --- /dev/null +++ b/extensions/example-provider/Cargo.lock @@ -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", +] diff --git a/extensions/example-provider/Cargo.toml b/extensions/example-provider/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..604d4fa6446a7aa44835bbf3db9cb1db06eacb2d --- /dev/null +++ b/extensions/example-provider/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/extensions/example-provider/extension.toml b/extensions/example-provider/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..5dc58e8d73e8da8bd1c5e5c5832e36cc368d8c2c --- /dev/null +++ b/extensions/example-provider/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/example-provider/src/example_provider.rs b/extensions/example-provider/src/example_provider.rs new file mode 100644 index 0000000000000000000000000000000000000000..190251e4d540959d145075b5e0d45db81adeb6f1 --- /dev/null +++ b/extensions/example-provider/src/example_provider.rs @@ -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>>, + /// Counter for generating unique stream IDs + next_stream_id: Mutex, +} + +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 { + vec![LlmProviderInfo { + id: "example".into(), + name: "Example Provider".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + // 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::>().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, 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); diff --git a/extensions/google-ai/Cargo.lock b/extensions/google-ai/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..2389ff51da0c24e679cec8d541860d1513227335 --- /dev/null +++ b/extensions/google-ai/Cargo.lock @@ -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", +] diff --git a/extensions/google-ai/Cargo.toml b/extensions/google-ai/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2028c191dddf6f5eb5e937efdf01ebac691ec5b1 --- /dev/null +++ b/extensions/google-ai/Cargo.toml @@ -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" \ No newline at end of file diff --git a/extensions/google-ai/extension.toml b/extensions/google-ai/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..e9c1318b54ee876996036c716a19a5bb4ad8a0f3 --- /dev/null +++ b/extensions/google-ai/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/google-ai/src/google_ai.rs b/extensions/google-ai/src/google_ai.rs new file mode 100644 index 0000000000000000000000000000000000000000..37990951581e9b593321df2d49a0cc2bd16a657d --- /dev/null +++ b/extensions/google-ai/src/google_ai.rs @@ -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>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + stop_reason: Option, + wants_tool_use: bool, +} + +struct ModelDefinition { + real_id: &'static str, + display_name: &'static str, + max_tokens: u64, + max_output_tokens: Option, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + system_instruction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + generation_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleSystemInstruction { + parts: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct GoogleContent { + parts: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + role: Option, +} + +#[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, +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleThinkingConfig { + thinking_budget: u32, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct GoogleTool { + function_declarations: Vec, +} + +#[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>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleStreamResponse { + #[serde(default)] + candidates: Vec, + #[serde(default)] + usage_metadata: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct GoogleCandidate { + #[serde(default)] + content: Option, + #[serde(default)] + finish_reason: Option, +} + +#[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 = Vec::new(); + let mut system_parts: Vec = 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 = 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 = 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> = if request.tools.is_empty() { + None + } else { + let declarations: Vec = 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 { + 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 { + vec![LlmProviderInfo { + id: "google-ai".into(), + name: "Google AI".into(), + icon: Some("google-ai".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + 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, 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); diff --git a/extensions/open_router/Cargo.lock b/extensions/open_router/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..4dea7c7a8a9cd857522214cf4ab4453d41c47e57 --- /dev/null +++ b/extensions/open_router/Cargo.lock @@ -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", +] diff --git a/extensions/open_router/Cargo.toml b/extensions/open_router/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e02e5b6d4faba9df635b11012c157bffb13e98dd --- /dev/null +++ b/extensions/open_router/Cargo.toml @@ -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" \ No newline at end of file diff --git a/extensions/open_router/extension.toml b/extensions/open_router/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..f2d48e07fcd42c4d5e7395d262c2edf9ebeb3a25 --- /dev/null +++ b/extensions/open_router/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/open_router/src/open_router.rs b/extensions/open_router/src/open_router.rs new file mode 100644 index 0000000000000000000000000000000000000000..0c34bb794f3777c98ee5fddf0ddcd19d378d32b4 --- /dev/null +++ b/extensions/open_router/src/open_router.rs @@ -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>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + 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, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + stream: bool, +} + +#[derive(Serialize)] +struct OpenRouterMessage { + role: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize, Clone)] +#[serde(untagged)] +enum OpenRouterContent { + Text(String), + Parts(Vec), +} + +#[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, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterStreamChoice { + delta: OpenRouterDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenRouterFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenRouterUsage { + prompt_tokens: u64, + completion_tokens: u64, +} + +fn convert_request( + model_id: &str, + request: &LlmCompletionRequest, +) -> Result { + let mut messages: Vec = 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 = Vec::new(); + let mut tool_result_messages: Vec = 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 = 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 = 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 { + 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 { + vec![LlmProviderInfo { + id: "open_router".into(), + name: "OpenRouter".into(), + icon: None, + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + 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, 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); diff --git a/extensions/openai/Cargo.lock b/extensions/openai/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..2ef354a2892b231676c722e48a348502d012a4a9 --- /dev/null +++ b/extensions/openai/Cargo.lock @@ -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", +] diff --git a/extensions/openai/Cargo.toml b/extensions/openai/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9477f9b9862f48c8b7c2e0385e8414173025e28e --- /dev/null +++ b/extensions/openai/Cargo.toml @@ -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" \ No newline at end of file diff --git a/extensions/openai/extension.toml b/extensions/openai/extension.toml new file mode 100644 index 0000000000000000000000000000000000000000..ca77fa684bcff861941f85f6a0887419807d7482 --- /dev/null +++ b/extensions/openai/extension.toml @@ -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" \ No newline at end of file diff --git a/extensions/openai/src/openai.rs b/extensions/openai/src/openai.rs new file mode 100644 index 0000000000000000000000000000000000000000..d464066d13f54ffbdab3dbac619f0e739492bb4b --- /dev/null +++ b/extensions/openai/src/openai.rs @@ -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>, + next_stream_id: Mutex, +} + +struct StreamState { + response_stream: Option, + buffer: String, + started: bool, + tool_calls: HashMap, + 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, + 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, + #[serde(skip_serializing_if = "Option::is_none")] + tools: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + stop: Vec, + stream: bool, + stream_options: Option, +} + +#[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 }, + #[serde(rename = "assistant")] + Assistant { + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + }, + #[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, + #[serde(default)] + usage: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiChoice { + delta: OpenAiDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug, Default)] +struct OpenAiDelta { + #[serde(default)] + content: Option, + #[serde(default)] + tool_calls: Option>, +} + +#[derive(Deserialize, Debug)] +struct OpenAiToolCallDelta { + index: usize, + #[serde(default)] + id: Option, + #[serde(default)] + function: Option, +} + +#[derive(Deserialize, Debug)] +struct OpenAiFunctionDelta { + #[serde(default)] + name: Option, + #[serde(default)] + arguments: Option, +} + +#[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 { + 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::>() + .join("\n"); + if !text.is_empty() { + messages.push(OpenAiMessage::System { content: text }); + } + } + LlmMessageRole::User => { + let parts: Vec = 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 = None; + let mut tool_calls: Vec = 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> = 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 { + 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 { + vec![LlmProviderInfo { + id: "openai".into(), + name: "OpenAI".into(), + icon: Some("openai".into()), + }] + } + + fn llm_provider_models(&self, _provider_id: &str) -> Result, 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 { + 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 { + 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, 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 = 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);