Add local model provider

Richard Feldman created

Change summary

Cargo.lock                                         | 330 ++++++-----
Cargo.toml                                         |   2 
crates/language_models/Cargo.toml                  |   2 
crates/language_models/src/language_models.rs      |   5 
crates/language_models/src/provider.rs             |   1 
crates/language_models/src/provider/local.rs       | 440 ++++++++++++++++
crates/language_models/src/provider/local/tests.rs | 259 +++++++++
7 files changed, 892 insertions(+), 147 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2,6 +2,16 @@
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+dependencies = [
+ "lazy_static",
+ "regex",
+]
+
 [[package]]
 name = "acp_thread"
 version = "0.1.0"
@@ -63,9 +73,9 @@ dependencies = [
 
 [[package]]
 name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
 [[package]]
 name = "aes"
@@ -117,7 +127,7 @@ dependencies = [
  "rand 0.8.5",
  "ref-cast",
  "rope",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
@@ -129,7 +139,7 @@ dependencies = [
  "thiserror 2.0.12",
  "time",
  "util",
- "uuid",
+ "uuid 1.17.0",
  "workspace",
  "workspace-hack",
  "zed_llm_client",
@@ -142,7 +152,7 @@ version = "0.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b"
 dependencies = [
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
 ]
@@ -170,16 +180,16 @@ dependencies = [
  "paths",
  "project",
  "rand 0.8.5",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
  "smol",
- "strum 0.27.1",
+ "strum 0.27.2",
  "tempfile",
  "ui",
  "util",
- "uuid",
+ "uuid 1.17.0",
  "watch",
  "which 6.0.3",
  "workspace-hack",
@@ -195,7 +205,7 @@ dependencies = [
  "gpui",
  "language_model",
  "paths",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -267,7 +277,7 @@ dependencies = [
  "release_channel",
  "rope",
  "rules_library",
- "schemars",
+ "schemars 1.0.4",
  "search",
  "serde",
  "serde_json",
@@ -290,7 +300,7 @@ dependencies = [
  "unindent",
  "urlencoding",
  "util",
- "uuid",
+ "uuid 1.17.0",
  "watch",
  "workspace",
  "workspace-hack",
@@ -310,7 +320,7 @@ dependencies = [
  "futures 0.3.31",
  "log",
  "parking_lot",
- "schemars",
+ "schemars 1.0.4",
  "semver",
  "serde",
  "serde_json",
@@ -322,24 +332,24 @@ version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
  "once_cell",
  "version_check",
 ]
 
 [[package]]
 name = "ahash"
-version = "0.8.11"
+version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
 dependencies = [
  "cfg-if",
  "const-random",
- "getrandom 0.2.15",
+ "getrandom 0.3.3",
  "once_cell",
  "serde",
  "version_check",
- "zerocopy 0.7.35",
+ "zerocopy",
 ]
 
 [[package]]
@@ -368,13 +378,19 @@ dependencies = [
  "zed_actions",
 ]
 
+[[package]]
+name = "akin"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1763692fc1416554cf051efc56a3de5595eca47299d731cc5c2b583adf8b4d2f"
+
 [[package]]
 name = "alacritty_terminal"
 version = "0.25.1-dev"
 source = "git+https://github.com/zed-industries/alacritty.git?branch=add-hush-login-flag#828457c9ff1f7ea0a0469337cc8a37ee3a1b0590"
 dependencies = [
  "base64 0.22.1",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "home",
  "libc",
  "log",
@@ -399,9 +415,12 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
 [[package]]
 name = "aligned-vec"
-version = "0.5.0"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
 
 [[package]]
 name = "allocator-api2"
@@ -416,7 +435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
 dependencies = [
  "alsa-sys",
- "bitflags 2.9.0",
+ "bitflags 2.9.1",
  "cfg-if",
  "libc",
 ]
@@ -439,12 +458,12 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
 
 [[package]]
 name = "ammonia"
-version = "4.1.0"
+version = "4.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364"
+checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
 dependencies = [
- "cssparser",
- "html5ever 0.31.0",
+ "cssparser 0.35.0",
+ "html5ever 0.35.0",
  "maplit",
  "tendril",
  "url",
@@ -473,9 +492,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.18"
+version = "0.6.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -488,36 +507,36 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
 dependencies = [
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
 dependencies = [
  "anstyle",
- "once_cell",
+ "once_cell_polyfill",
  "windows-sys 0.59.0",
 ]
 
@@ -529,10 +548,10 @@ dependencies = [
  "chrono",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
- "strum 0.27.1",
+ "strum 0.27.2",
  "thiserror 2.0.12",
  "workspace-hack",
 ]
@@ -549,6 +568,12 @@ version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
 
+[[package]]
+name = "apodize"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca387cdc0a1f9c7a7c26556d584aa2d07fc529843082e4861003cde4ab914ed"
+
 [[package]]
 name = "approx"
 version = "0.5.1"
@@ -563,6 +588,9 @@ name = "arbitrary"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
 
 [[package]]
 name = "arc-swap"
@@ -578,7 +606,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.104",
 ]
 
 [[package]]
@@ -602,6 +630,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "as-any"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063"
+
 [[package]]
 name = "as-raw-xcb-connection"
 version = "1.0.1"
@@ -645,7 +679,7 @@ dependencies = [
  "enumflags2",
  "futures-channel",
  "futures-util",
- "rand 0.9.1",
+ "rand 0.9.2",
  "serde",
  "serde_repr",
  "url",
@@ -718,7 +752,7 @@ dependencies = [
  "ui",
  "unindent",
  "util",
- "uuid",
+ "uuid 1.17.0",
  "workspace",
  "workspace-hack",
  "zed_llm_client",
@@ -731,7 +765,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "collections",
- "derive_more 0.99.19",
+ "derive_more 0.99.20",
  "extension",
  "futures 0.3.31",
  "gpui",
@@ -776,7 +810,7 @@ dependencies = [
  "settings",
  "smol",
  "text",
- "toml 0.8.20",
+ "toml 0.8.23",
  "ui",
  "util",
  "workspace",
@@ -794,7 +828,7 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
- "derive_more 0.99.19",
+ "derive_more 0.99.20",
  "futures 0.3.31",
  "gpui",
  "icons",
@@ -831,7 +865,7 @@ dependencies = [
  "clock",
  "collections",
  "component",
- "derive_more 0.99.19",
+ "derive_more 0.99.20",
  "diffy",
  "editor",
  "feature_flags",
@@ -860,14 +894,14 @@ dependencies = [
  "regex",
  "reqwest_client",
  "rust-embed",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
  "smallvec",
  "smol",
  "streaming_diff",
- "strsim",
+ "strsim 0.11.1",
  "task",
  "tempfile",
  "terminal",
@@ -921,9 +955,9 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "2.3.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
 dependencies = [
  "concurrent-queue",
  "event-listener-strategy",
@@ -946,9 +980,9 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.4.22"
+version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
+checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
 dependencies = [
  "deflate64",
  "flate2",
@@ -970,22 +1004,23 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.13.1"
+version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
+checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
 dependencies = [
  "async-task",
  "concurrent-queue",
  "fastrand 2.3.0",
  "futures-lite 2.6.0",
+ "pin-project-lite",
  "slab",
 ]
 
 [[package]]
 name = "async-fs"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
+checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50"
 dependencies = [
  "async-lock",
  "blocking",
@@ -998,7 +1033,7 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
 dependencies = [
- "async-channel 2.3.1",
+ "async-channel 2.5.0",
  "async-executor",
  "async-io",
  "async-lock",
@@ -1009,9 +1044,9 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca"
 dependencies = [
  "async-lock",
  "cfg-if",
@@ -1020,10 +1055,9 @@ dependencies = [
  "futures-lite 2.6.0",
  "parking",
  "polling",
- "rustix 0.38.44",
+ "rustix 1.0.8",
  "slab",
- "tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -1059,11 +1093,11 @@ dependencies = [
 
 [[package]]
 name = "async-process"
-version = "2.3.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
+checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00"
 dependencies = [
- "async-channel 2.3.1",
+ "async-channel 2.5.0",
  "async-io",
  "async-lock",
  "async-signal",
@@ -1072,8 +1106,7 @@ dependencies = [
  "cfg-if",
  "event-listener 5.4.0",
  "futures-lite 2.6.0",
- "rustix 0.38.44",
- "tracing",
+ "rustix 1.0.8",
 ]
 
 [[package]]
@@ -1095,14 +1128,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.104",
 ]
 
 [[package]]
 name = "async-signal"
-version = "0.2.10"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
+checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1"
 dependencies = [
  "async-io",
  "async-lock",
@@ -1110,10 +1143,10 @@ dependencies = [
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.44",
+ "rustix 1.0.8",
  "signal-hook-registry",
  "slab",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -1163,7 +1196,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.104",
 ]
 
 [[package]]
@@ -1214,7 +1247,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.101",
+ "syn 2.0.104",
 ]
 
 [[package]]
@@ -1236,6 +1269,23 @@ dependencies = [
  "tungstenite 0.26.2",
 ]
 
+[[package]]
+name = "async-tungstenite"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e308e9866b891743e3fdf9dfd6b57f85c5062ca01ce4fed6f393e76eb5accea4"
+dependencies = [
+ "atomic-waker",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "futures-util",
+ "log",
+ "pin-project-lite",
+ "tokio",
+ "tungstenite 0.27.0",
+]
+
 [[package]]
 name = "async_zip"
 version = "0.0.17"
@@ -1289,7 +1339,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
- "derive_more 0.99.19",
+ "derive_more 0.99.20",
  "gpui",
  "parking_lot",
  "rodio",
@@ -1321,7 +1371,7 @@ dependencies = [
  "log",
  "paths",
  "release_channel",
- "schemars",
+ "schemars 1.0.4",
  "serde",
  "serde_json",
  "settings",
@@ -1339,7 +1389,7 @@ dependencies = [
  "anyhow",
  "log",
  "simplelog",
- "windows 0.61.1",
+ "windows 0.61.3",
  "winresource",
  "workspace-hack",
 ]
@@ -1366,15 +1416,15 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
 name = "av1-grain"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8"
 dependencies = [
  "anyhow",
  "arrayvec",
@@ -1386,18 +1436,18 @@ dependencies = [
 
 [[package]]
 name = "avif-serialize"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
+checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42"
 dependencies = [
  "arrayvec",
 ]
 
 [[package]]
 name = "aws-config"
-version = "1.6.1"
+version = "1.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c39646d1a6b51240a1a23bb57ea4eebede7e16fbc237fdc876980233dcecb4f"
+checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1425,9 +1475,9 @@ dependencies = [
 
 [[package]]
 name = "aws-credential-types"
-version = "1.2.2"
+version = "1.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14"
+checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-runtime-api",
@@ -1437,9 +1487,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.13.1"
+version = "1.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
+checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
 dependencies = [
  "aws-lc-sys",
  "zeroize",
@@ -1447,9 +1497,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.29.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
+checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff"
 dependencies = [
  "bindgen 0.69.5",
  "cc",
@@ -1460,9 +1510,9 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.5.6"
+version = "1.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad"
+checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
@@ -1477,18 +1527,17 @@ dependencies = [
  "fastrand 2.3.0",
  "http 0.2.12",
  "http-body 0.4.6",
- "once_cell",
  "percent-encoding",
  "pin-project-lite",
  "tracing",
- "uuid",
+ "uuid 1.17.0",
 ]
 
 [[package]]
 name = "aws-sdk-bedrockruntime"
-version = "1.82.0"
+version = "1.99.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cb95f77abd4321348dd2f52a25e1de199732f54d2a35860ad20f5df21c66b44"
+checksum = "13630ddba535c5e3fc9ee5dbe4942e2203a48a4dddfdf2d864cbb82f8fd46a72"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1505,16 +1554,15 @@ dependencies = [
  "fastrand 2.3.0",
  "http 0.2.12",
  "hyper 0.14.32",
- "once_cell",
  "regex-lite",
  "tracing",
 ]
 
 [[package]]
 name = "aws-sdk-kinesis"
-version = "1.66.0"
+version = "1.82.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e43e5fb05c78cdad4fef5be4503465e4b42292f472fc991823ea4c50078208e4"
+checksum = "0e4835e87d8844261326f9837440f58d90d25d94bceca8264e8ba02b9731a25c"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1529,16 +1577,15 @@ dependencies = [
  "bytes 1.10.1",
  "fastrand 2.3.0",
  "http 0.2.12",
- "once_cell",
  "regex-lite",
  "tracing",
 ]
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.82.0"
+version = "1.100.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6eab2900764411ab01c8e91a76fd11a63b4e12bc3da97d9e14a0ce1343d86d3"
+checksum = "8c5eafbdcd898114b839ba68ac628e31c4cfc3e11dfca38dc1b2de2f35bb6270"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1561,7 +1608,6 @@ dependencies = [
  "http 1.3.1",
  "http-body 0.4.6",
  "lru",
- "once_cell",
  "percent-encoding",
  "regex-lite",
  "sha2",
@@ -1571,9 +1617,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.64.0"
+version = "1.78.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02d4bdb0e5f80f0689e61c77ab678b2b9304af329616af38aef5b6b967b8e736"
+checksum = "dbd7bc4bd34303733bded362c4c997a39130eac4310257c79aae8484b1c4b724"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1587,16 +1633,15 @@ dependencies = [
  "bytes 1.10.1",
  "fastrand 2.3.0",
  "http 0.2.12",
- "once_cell",
  "regex-lite",
  "tracing",
 ]
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.65.0"
+version = "1.79.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbbb3ce8da257aedbccdcb1aadafbbb6a5fe9adf445db0e1ea897bdc7e22d08"
+checksum = "77358d25f781bb106c1a69531231d4fd12c6be904edb0c47198c604df5a2dbca"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1610,16 +1655,15 @@ dependencies = [
  "bytes 1.10.1",
  "fastrand 2.3.0",
  "http 0.2.12",
- "once_cell",
  "regex-lite",
  "tracing",
 ]
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.65.0"
+version = "1.80.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96a78a8f50a1630db757b60f679c8226a8a70ee2ab5f5e6e51dc67f6c61c7cfd"
+checksum = "06e3ed2a9b828ae7763ddaed41d51724d2661a50c45f845b08967e52f4939cfc"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1634,16 +1678,15 @@ dependencies = [
  "aws-types",
  "fastrand 2.3.0",
  "http 0.2.12",
- "once_cell",
  "regex-lite",
  "tracing",
 ]
 
 [[package]]
 name = "aws-sigv4"
-version = "1.3.0"
+version = "1.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db"
+checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-eventstream",
@@ -1657,7 +1700,6 @@ dependencies = [
  "hmac",
  "http 0.2.12",
  "http 1.3.1",
- "once_cell",
  "p256",
  "percent-encoding",
  "ring",
@@ -1681,16 +1723,14 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-checksums"
-version = "0.63.1"
+version = "0.63.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b65d21e1ba6f2cdec92044f904356a19f5ad86961acf015741106cdfafd747c0"
+checksum = "5ab9472f7a8ec259ddb5681d2ef1cb1cf16c0411890063e67cdc7b62562cc496"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
  "bytes 1.10.1",
- "crc32c",
- "crc32fast",
- "crc64fast-nvme",
+ "crc-fast",
  "hex",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1703,9 +1743,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-eventstream"
-version = "0.60.8"
+version = "0.60.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a"
+checksum = "604c7aec361252b8f1c871a7641d5e0ba3a7f5a586e51b66bc9510a5519594d9"
 dependencies = [
  "aws-smithy-types",
  "bytes 1.10.1",
@@ -1714,9 +1754,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http"
-version = "0.62.0"
+version = "0.62.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166"
+checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f"
 dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
@@ -1727,7 +1767,6 @@ dependencies = [
  "http 0.2.12",
  "http 1.3.1",
  "http-body 0.4.6",
- "once_cell",
  "percent-encoding",
  "pin-project-lite",
  "pin-utils",
@@ -1736,25 +1775,26 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http-client"
-version = "1.0.1"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8aff1159006441d02e57204bf57a1b890ba68bedb6904ffd2873c1c4c11c546b"
+checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "h2 0.4.9",
+ "h2 0.3.27",
+ "h2 0.4.11",
  "http 0.2.12",
  "http 1.3.1",
  "http-body 0.4.6",
  "hyper 0.14.32",
  "hyper 1.6.0",
  "hyper-rustls 0.24.2",
- "hyper-rustls 0.27.5",
+ "hyper-rustls 0.27.7",
  "hyper-util",
  "pin-project-lite",
  "rustls 0.21.12",
- "rustls 0.23.26",
+ "rustls 0.23.31",
  "rustls-native-certs 0.8.1",
  "rustls-pki-types",
  "tokio",
@@ -1764,21 +1804,20 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-json"
-version = "0.61.3"
+version = "0.61.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07"
+checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9"
 dependencies = [
  "aws-smithy-types",
 ]
 
 [[package]]
 name = "aws-smithy-observability"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445d065e76bc1ef54963db400319f1dd3ebb3e0a74af20f7f7630625b0cc7cc0"
+checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393"
 dependencies = [
  "aws-smithy-runtime-api",
- "once_cell",
 ]
 
 [[package]]
@@ -1793,9 +1832,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.8.1"
+version = "1.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0152749e17ce4d1b47c7747bdfec09dac1ccafdcbc741ebf9daa2a373356730f"
+checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -1809,7 +1848,6 @@ dependencies = [
  "http 1.3.1",
  "http-body 0.4.6",
  "http-body 1.0.1",
- "once_cell",
  "pin-project-lite",
  "pin-utils",
  "tokio",
@@ -1818,9 +1856,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime-api"
-version = "1.7.4"
+version = "1.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f"
+checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
@@ -1835,9 +1873,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.3.0"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f"
+checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8"
 dependencies = [
  "base64-simd",
  "bytes 1.10.1",
@@ -1861,18 +1899,18 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.60.9"
+version = "0.60.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
+checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728"
 dependencies = [
  "xmlparser",
 ]
 
 [[package]]
 name = "aws-types"
-version = "1.3.6"
+version = "1.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125"
+checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-async",

Cargo.toml 🔗

@@ -515,7 +515,7 @@ objc = "0.2"
 open = "5.0.0"
 ordered-float = "2.1.1"
 palette = { version = "0.7.5", default-features = false, features = ["std"] }
-parking_lot = "0.12.1"
+parking_lot = "0.12.4"
 partial-json-fixer = "0.5.3"
 parse_int = "0.9"
 pathdiff = "0.2"

crates/language_models/Cargo.toml 🔗

@@ -15,6 +15,7 @@ path = "src/language_models.rs"
 ai_onboarding.workspace = true
 anthropic = { workspace = true, features = ["schemars"] }
 anyhow.workspace = true
+
 aws-config = { workspace = true, features = ["behavior-version-latest"] }
 aws-credential-types = { workspace = true, features = [
     "hardcoded-credentials",
@@ -64,6 +65,7 @@ util.workspace = true
 workspace-hack.workspace = true
 zed_llm_client.workspace = true
 language.workspace = true
+mistralrs = { git = "https://github.com/EricLBuehler/mistral.rs", rev = "d256806c6", features = [] }
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/language_models/src/language_models.rs 🔗

@@ -17,6 +17,7 @@ use crate::provider::cloud::CloudLanguageModelProvider;
 use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
 use crate::provider::google::GoogleLanguageModelProvider;
 use crate::provider::lmstudio::LmStudioLanguageModelProvider;
+use crate::provider::local::LocalLanguageModelProvider;
 use crate::provider::mistral::MistralLanguageModelProvider;
 use crate::provider::ollama::OllamaLanguageModelProvider;
 use crate::provider::open_ai::OpenAiLanguageModelProvider;
@@ -150,4 +151,8 @@ fn register_language_model_providers(
     );
     registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
     registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
+    registry.register_provider(
+        LocalLanguageModelProvider::new(client.http_client(), cx),
+        cx,
+    );
 }

crates/language_models/src/provider/local.rs 🔗

@@ -0,0 +1,440 @@
+use anyhow::{Result, anyhow};
+use futures::{FutureExt, SinkExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
+use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
+use http_client::HttpClient;
+use language_model::{
+    AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
+    LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
+    LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
+    LanguageModelToolChoice, MessageContent, RateLimiter, Role, StopReason,
+};
+use mistralrs::{
+    IsqType, Model as MistralModel, Response as MistralResponse, TextMessageRole, TextMessages,
+    TextModelBuilder,
+};
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use ui::{ButtonLike, IconName, Indicator, prelude::*};
+
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("local");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Local");
+const DEFAULT_MODEL: &str = "mlx-community/GLM-4.5-Air-3bit";
+
+#[derive(Default, Debug, Clone, PartialEq)]
+pub struct LocalSettings {
+    pub available_models: Vec<AvailableModel>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct AvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+}
+
+pub struct LocalLanguageModelProvider {
+    state: Entity<State>,
+}
+
+pub struct State {
+    model: Option<Arc<MistralModel>>,
+    status: ModelStatus,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum ModelStatus {
+    NotLoaded,
+    Loading,
+    Loaded,
+    Error(String),
+}
+
+impl State {
+    fn new(_cx: &mut Context<Self>) -> Self {
+        Self {
+            model: None,
+            status: ModelStatus::NotLoaded,
+        }
+    }
+
+    fn is_authenticated(&self) -> bool {
+        matches!(self.status, ModelStatus::Loaded)
+    }
+
+    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
+        if self.is_authenticated() {
+            return Task::ready(Ok(()));
+        }
+
+        if matches!(self.status, ModelStatus::Loading) {
+            return Task::ready(Ok(()));
+        }
+
+        self.status = ModelStatus::Loading;
+        cx.notify();
+
+        cx.spawn(async move |this, cx| match load_mistral_model().await {
+            Ok(model) => {
+                this.update(cx, |state, cx| {
+                    state.model = Some(model);
+                    state.status = ModelStatus::Loaded;
+                    cx.notify();
+                })?;
+                Ok(())
+            }
+            Err(e) => {
+                let error_msg = e.to_string();
+                this.update(cx, |state, cx| {
+                    state.status = ModelStatus::Error(error_msg.clone());
+                    cx.notify();
+                })?;
+                Err(AuthenticateError::Other(anyhow!(
+                    "Failed to load model: {}",
+                    error_msg
+                )))
+            }
+        })
+    }
+}
+
+async fn load_mistral_model() -> Result<Arc<MistralModel>> {
+    let model = TextModelBuilder::new(DEFAULT_MODEL)
+        .with_isq(IsqType::Q4_0)
+        .with_logging()
+        .build()
+        .await?;
+
+    Ok(Arc::new(model))
+}
+
+impl LocalLanguageModelProvider {
+    pub fn new(_http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
+        let state = cx.new(State::new);
+        Self { state }
+    }
+}
+
+impl LanguageModelProviderState for LocalLanguageModelProvider {
+    type ObservableEntity = State;
+
+    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
+        Some(self.state.clone())
+    }
+}
+
+impl LanguageModelProvider for LocalLanguageModelProvider {
+    fn id(&self) -> LanguageModelProviderId {
+        PROVIDER_ID
+    }
+
+    fn name(&self) -> LanguageModelProviderName {
+        PROVIDER_NAME
+    }
+
+    fn icon(&self) -> IconName {
+        IconName::Ai
+    }
+
+    fn provided_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
+        vec![Arc::new(LocalLanguageModel {
+            state: self.state.clone(),
+            request_limiter: RateLimiter::new(4),
+        })]
+    }
+
+    fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
+        self.provided_models(cx).into_iter().next()
+    }
+
+    fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
+        self.default_model(cx)
+    }
+
+    fn is_authenticated(&self, cx: &App) -> bool {
+        self.state.read(cx).is_authenticated()
+    }
+
+    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
+        self.state.update(cx, |state, cx| state.authenticate(cx))
+    }
+
+    fn configuration_view(&self, _window: &mut gpui::Window, cx: &mut App) -> AnyView {
+        cx.new(|_cx| ConfigurationView {
+            state: self.state.clone(),
+        })
+        .into()
+    }
+
+    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
+        self.state.update(cx, |state, cx| {
+            state.model = None;
+            state.status = ModelStatus::NotLoaded;
+            cx.notify();
+        });
+        Task::ready(Ok(()))
+    }
+}
+
+pub struct LocalLanguageModel {
+    state: Entity<State>,
+    request_limiter: RateLimiter,
+}
+
+impl LocalLanguageModel {
+    fn to_mistral_messages(&self, request: &LanguageModelRequest) -> TextMessages {
+        let mut messages = TextMessages::new();
+
+        for message in &request.messages {
+            let mut text_content = String::new();
+
+            for content in &message.content {
+                match content {
+                    MessageContent::Text(text) => {
+                        text_content.push_str(text);
+                    }
+                    MessageContent::Image { .. } => {
+                        // For now, skip image content
+                        continue;
+                    }
+                    MessageContent::ToolResult { .. } => {
+                        // Skip tool results for now
+                        continue;
+                    }
+                    MessageContent::Thinking { .. } => {
+                        // Skip thinking content
+                        continue;
+                    }
+                    MessageContent::RedactedThinking(_) => {
+                        // Skip redacted thinking
+                        continue;
+                    }
+                    MessageContent::ToolUse(_) => {
+                        // Skip tool use
+                        continue;
+                    }
+                }
+            }
+
+            if text_content.is_empty() {
+                continue;
+            }
+
+            let role = match message.role {
+                Role::User => TextMessageRole::User,
+                Role::Assistant => TextMessageRole::Assistant,
+                Role::System => TextMessageRole::System,
+            };
+
+            messages = messages.add_message(role, text_content);
+        }
+
+        messages
+    }
+}
+
+impl LanguageModel for LocalLanguageModel {
+    fn id(&self) -> LanguageModelId {
+        LanguageModelId(DEFAULT_MODEL.into())
+    }
+
+    fn name(&self) -> LanguageModelName {
+        LanguageModelName(DEFAULT_MODEL.into())
+    }
+
+    fn provider_id(&self) -> LanguageModelProviderId {
+        PROVIDER_ID
+    }
+
+    fn provider_name(&self) -> LanguageModelProviderName {
+        PROVIDER_NAME
+    }
+
+    fn telemetry_id(&self) -> String {
+        format!("local/{}", DEFAULT_MODEL)
+    }
+
+    fn supports_tools(&self) -> bool {
+        false
+    }
+
+    fn supports_images(&self) -> bool {
+        false
+    }
+
+    fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {
+        false
+    }
+
+    fn max_token_count(&self) -> u64 {
+        128000 // GLM-4.5-Air supports 128k context
+    }
+
+    fn count_tokens(
+        &self,
+        request: LanguageModelRequest,
+        _cx: &App,
+    ) -> BoxFuture<'static, Result<u64>> {
+        // Rough estimation: 1 token ≈ 4 characters
+        let mut total_chars = 0;
+        for message in request.messages {
+            for content in message.content {
+                match content {
+                    MessageContent::Text(text) => total_chars += text.len(),
+                    _ => {}
+                }
+            }
+        }
+        let tokens = (total_chars / 4) as u64;
+        futures::future::ready(Ok(tokens)).boxed()
+    }
+
+    fn stream_completion(
+        &self,
+        request: LanguageModelRequest,
+        cx: &AsyncApp,
+    ) -> BoxFuture<
+        'static,
+        Result<
+            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
+            LanguageModelCompletionError,
+        >,
+    > {
+        let messages = self.to_mistral_messages(&request);
+        let state = self.state.clone();
+        let limiter = self.request_limiter.clone();
+
+        cx.spawn(async move |cx| {
+            let result: Result<
+                BoxStream<
+                    'static,
+                    Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+                >,
+                LanguageModelCompletionError,
+            > = limiter
+                .run(async move {
+                    let model = cx
+                        .read_entity(&state, |state, _| state.model.clone())
+                        .map_err(|_| {
+                            LanguageModelCompletionError::Other(anyhow!("App state dropped"))
+                        })?
+                        .ok_or_else(|| {
+                            LanguageModelCompletionError::Other(anyhow!("Model not loaded"))
+                        })?;
+
+                    let (mut tx, rx) = mpsc::channel(32);
+
+                    // Spawn a task to handle the stream
+                    let _ = smol::spawn(async move {
+                        let mut stream = match model.stream_chat_request(messages).await {
+                            Ok(stream) => stream,
+                            Err(e) => {
+                                let _ = tx
+                                    .send(Err(LanguageModelCompletionError::Other(anyhow!(
+                                        "Failed to start stream: {}",
+                                        e
+                                    ))))
+                                    .await;
+                                return;
+                            }
+                        };
+
+                        while let Some(response) = stream.next().await {
+                            let event = match response {
+                                MistralResponse::Chunk(chunk) => {
+                                    if let Some(choice) = chunk.choices.first() {
+                                        if let Some(content) = &choice.delta.content {
+                                            Some(Ok(LanguageModelCompletionEvent::Text(
+                                                content.clone(),
+                                            )))
+                                        } else if let Some(finish_reason) = &choice.finish_reason {
+                                            let stop_reason = match finish_reason.as_str() {
+                                                "stop" => StopReason::EndTurn,
+                                                "length" => StopReason::MaxTokens,
+                                                _ => StopReason::EndTurn,
+                                            };
+                                            Some(Ok(LanguageModelCompletionEvent::Stop(
+                                                stop_reason,
+                                            )))
+                                        } else {
+                                            None
+                                        }
+                                    } else {
+                                        None
+                                    }
+                                }
+                                MistralResponse::Done(_response) => {
+                                    // For now, we don't emit usage events since the format doesn't match
+                                    None
+                                }
+                                _ => None,
+                            };
+
+                            if let Some(event) = event {
+                                if tx.send(event).await.is_err() {
+                                    break;
+                                }
+                            }
+                        }
+                    })
+                    .detach();
+
+                    Ok(rx.boxed())
+                })
+                .await;
+
+            result
+        })
+        .boxed()
+    }
+}
+
+struct ConfigurationView {
+    state: Entity<State>,
+}
+
+impl Render for ConfigurationView {
+    fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let status = self.state.read(cx).status.clone();
+
+        div().size_full().child(
+            div()
+                .p_4()
+                .child(
+                    div()
+                        .flex()
+                        .gap_2()
+                        .items_center()
+                        .child(match &status {
+                            ModelStatus::NotLoaded => Label::new("Model not loaded"),
+                            ModelStatus::Loading => Label::new("Loading model..."),
+                            ModelStatus::Loaded => Label::new("Model loaded"),
+                            ModelStatus::Error(e) => Label::new(format!("Error: {}", e)),
+                        })
+                        .child(match &status {
+                            ModelStatus::NotLoaded => Indicator::dot().color(Color::Disabled),
+                            ModelStatus::Loading => Indicator::dot().color(Color::Modified),
+                            ModelStatus::Loaded => Indicator::dot().color(Color::Success),
+                            ModelStatus::Error(_) => Indicator::dot().color(Color::Error),
+                        }),
+                )
+                .when(!matches!(status, ModelStatus::Loading), |this| {
+                    this.child(
+                        ButtonLike::new("load_model")
+                            .child(Label::new(if matches!(status, ModelStatus::Loaded) {
+                                "Reload Model"
+                            } else {
+                                "Load Model"
+                            }))
+                            .on_click(cx.listener(|this, _, _window, cx| {
+                                this.state.update(cx, |state, cx| {
+                                    state.authenticate(cx).detach();
+                                });
+                            })),
+                    )
+                }),
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests;

crates/language_models/src/provider/local/tests.rs 🔗

@@ -0,0 +1,259 @@
+use super::*;
+use gpui::TestAppContext;
+use http_client::FakeHttpClient;
+use language_model::{LanguageModelRequest, MessageContent, Role};
+
+#[gpui::test]
+fn test_local_provider_creation(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    cx.read(|cx| {
+        assert_eq!(provider.id(), PROVIDER_ID);
+        assert_eq!(provider.name(), PROVIDER_NAME);
+        assert!(!provider.is_authenticated(cx));
+        assert_eq!(provider.provided_models(cx).len(), 1);
+    });
+}
+
+#[gpui::test]
+fn test_state_initialization(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        let state = cx.new(State::new);
+
+        assert!(!state.read(cx).is_authenticated());
+        assert_eq!(state.read(cx).status, ModelStatus::NotLoaded);
+        assert!(state.read(cx).model.is_none());
+    });
+}
+
+#[gpui::test]
+fn test_model_properties(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    // Create a model directly for testing (bypassing authentication)
+    let model = LocalLanguageModel {
+        state: provider.state.clone(),
+        request_limiter: RateLimiter::new(4),
+    };
+
+    assert_eq!(model.id(), LanguageModelId(DEFAULT_MODEL.into()));
+    assert_eq!(model.name(), LanguageModelName(DEFAULT_MODEL.into()));
+    assert_eq!(model.provider_id(), PROVIDER_ID);
+    assert_eq!(model.provider_name(), PROVIDER_NAME);
+    assert_eq!(model.max_token_count(), 128000);
+    assert!(!model.supports_tools());
+    assert!(!model.supports_images());
+}
+
+#[gpui::test]
+async fn test_token_counting(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    let model = LocalLanguageModel {
+        state: provider.state.clone(),
+        request_limiter: RateLimiter::new(4),
+    };
+
+    let request = LanguageModelRequest {
+        thread_id: None,
+        prompt_id: None,
+        intent: None,
+        mode: None,
+        messages: vec![language_model::LanguageModelRequestMessage {
+            role: Role::User,
+            content: vec![MessageContent::Text("Hello, world!".to_string())],
+            cache: false,
+        }],
+        tools: Vec::new(),
+        tool_choice: None,
+        stop: Vec::new(),
+        temperature: None,
+        thinking_allowed: false,
+    };
+
+    let count = cx
+        .update(|cx| model.count_tokens(request, cx))
+        .await
+        .unwrap();
+
+    // "Hello, world!" is 13 characters, so ~3 tokens
+    assert!(count > 0);
+    assert!(count < 10);
+}
+
+#[gpui::test]
+async fn test_message_conversion(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    let model = LocalLanguageModel {
+        state: provider.state.clone(),
+        request_limiter: RateLimiter::new(4),
+    };
+
+    let request = LanguageModelRequest {
+        thread_id: None,
+        prompt_id: None,
+        intent: None,
+        mode: None,
+        messages: vec![
+            language_model::LanguageModelRequestMessage {
+                role: Role::System,
+                content: vec![MessageContent::Text(
+                    "You are a helpful assistant.".to_string(),
+                )],
+                cache: false,
+            },
+            language_model::LanguageModelRequestMessage {
+                role: Role::User,
+                content: vec![MessageContent::Text("Hello!".to_string())],
+                cache: false,
+            },
+            language_model::LanguageModelRequestMessage {
+                role: Role::Assistant,
+                content: vec![MessageContent::Text("Hi there!".to_string())],
+                cache: false,
+            },
+        ],
+        tools: Vec::new(),
+        tool_choice: None,
+        stop: Vec::new(),
+        temperature: None,
+        thinking_allowed: false,
+    };
+
+    let _messages = model.to_mistral_messages(&request);
+    // We can't directly inspect TextMessages, but we can verify it doesn't panic
+    assert!(true); // Placeholder assertion
+}
+
+#[gpui::test]
+async fn test_reset_credentials(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    // Simulate loading a model by just setting the status
+    cx.update(|cx| {
+        provider.state.update(cx, |state, cx| {
+            state.status = ModelStatus::Loaded;
+            // We don't actually set a model since we can't mock it safely
+            cx.notify();
+        });
+    });
+
+    cx.read(|cx| {
+        // Since is_authenticated checks for model presence, we need to check status directly
+        assert_eq!(provider.state.read(cx).status, ModelStatus::Loaded);
+    });
+
+    // Reset credentials
+    let task = cx.update(|cx| provider.reset_credentials(cx));
+    task.await.unwrap();
+
+    cx.read(|cx| {
+        assert!(!provider.is_authenticated(cx));
+        assert_eq!(provider.state.read(cx).status, ModelStatus::NotLoaded);
+        assert!(provider.state.read(cx).model.is_none());
+    });
+}
+
+// TODO: Fix this test - need to handle window creation in tests
+// #[gpui::test]
+// async fn test_configuration_view_rendering(cx: &mut TestAppContext) {
+//     let http_client = FakeHttpClient::with_200_response();
+//     let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+//     let view = cx.update(|cx| provider.configuration_view(cx.window(), cx));
+
+//     // Basic test to ensure the view can be created without panicking
+//     assert!(view.entity_type() == std::any::TypeId::of::<ConfigurationView>());
+// }
+
+#[gpui::test]
+fn test_status_transitions(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        let state = cx.new(State::new);
+
+        // Initial state
+        assert_eq!(state.read(cx).status, ModelStatus::NotLoaded);
+
+        // Transition to loading
+        state.update(cx, |state, cx| {
+            state.status = ModelStatus::Loading;
+            cx.notify();
+        });
+        assert_eq!(state.read(cx).status, ModelStatus::Loading);
+
+        // Transition to loaded
+        state.update(cx, |state, cx| {
+            state.status = ModelStatus::Loaded;
+            cx.notify();
+        });
+        assert_eq!(state.read(cx).status, ModelStatus::Loaded);
+
+        // Transition to error
+        state.update(cx, |state, cx| {
+            state.status = ModelStatus::Error("Test error".to_string());
+            cx.notify();
+        });
+        match &state.read(cx).status {
+            ModelStatus::Error(msg) => assert_eq!(msg, "Test error"),
+            _ => panic!("Expected error status"),
+        }
+    });
+}
+
+#[gpui::test]
+fn test_provider_shows_models_without_authentication(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    cx.read(|cx| {
+        // Provider should show models even when not authenticated
+        let models = provider.provided_models(cx);
+        assert_eq!(models.len(), 1);
+
+        let model = &models[0];
+        assert_eq!(model.id(), LanguageModelId(DEFAULT_MODEL.into()));
+        assert_eq!(model.name(), LanguageModelName(DEFAULT_MODEL.into()));
+        assert_eq!(model.provider_id(), PROVIDER_ID);
+        assert_eq!(model.provider_name(), PROVIDER_NAME);
+    });
+}
+
+#[gpui::test]
+fn test_provider_has_icon(cx: &mut TestAppContext) {
+    let http_client = FakeHttpClient::with_200_response();
+    let provider = cx.update(|cx| LocalLanguageModelProvider::new(Arc::new(http_client), cx));
+
+    assert_eq!(provider.icon(), IconName::Ai);
+}
+
+#[gpui::test]
+fn test_provider_appears_in_registry(cx: &mut TestAppContext) {
+    use language_model::LanguageModelRegistry;
+
+    cx.update(|cx| {
+        let registry = cx.new(|_| LanguageModelRegistry::default());
+        let http_client = FakeHttpClient::with_200_response();
+
+        // Register the local provider
+        registry.update(cx, |registry, cx| {
+            let provider = LocalLanguageModelProvider::new(Arc::new(http_client), cx);
+            registry.register_provider(provider, cx);
+        });
+
+        // Verify the provider is registered
+        let provider = registry.read(cx).provider(&PROVIDER_ID).unwrap();
+        assert_eq!(provider.name(), PROVIDER_NAME);
+        assert_eq!(provider.icon(), IconName::Ai);
+
+        // Verify it provides models even without authentication
+        let models = provider.provided_models(cx);
+        assert_eq!(models.len(), 1);
+        assert_eq!(models[0].id(), LanguageModelId(DEFAULT_MODEL.into()));
+    });
+}