diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e1889daad243..0e3f898864da 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -4,7 +4,7 @@ # - canary.yml # # Platform Build Strategy: -# - Linux standard: Uses native Ubuntu 22.04 runners to keep glibc compatibility with Ubuntu 22.04 LTS +# - Linux standard (x86_64 + aarch64): Builds inside manylinux_2_28 container for glibc 2.28+ compat # - Linux Vulkan: Uses native Ubuntu 24.04 runners for newer Vulkan headers/tooling # - Linux musl: Uses native Ubuntu 22.04 runners with reduced features for musl compatibility # - macOS: Uses native macOS runners for each architecture @@ -27,6 +27,7 @@ jobs: build-cli: name: Build CLI runs-on: ${{ matrix.build-on }} + container: ${{ matrix.container }} env: MACOSX_DEPLOYMENT_TARGET: "12.0" strategy: @@ -37,11 +38,15 @@ jobs: architecture: x86_64 target-suffix: unknown-linux-gnu build-on: ubuntu-22.04 + # Pinned by digest for reproducible builds; bump explicitly when newer manylinux_2_28 images ship. + container: quay.io/pypa/manylinux_2_28_x86_64@sha256:441c35fdc6ee809ff9260894f8468ab4fea8c15dc880f8700a3f81b7922c1cda variant: standard - platform: linux architecture: aarch64 target-suffix: unknown-linux-gnu build-on: ubuntu-22.04-arm + # Pinned by digest for reproducible builds; bump explicitly when newer manylinux_2_28 images ship. + container: quay.io/pypa/manylinux_2_28_aarch64@sha256:8b5f2b4e8c072ae5aefeb659f22c03e1ff46e6a82f154b6c904b106c87e65ff7 variant: standard - platform: linux architecture: x86_64 @@ -97,8 +102,8 @@ jobs: sed -i.bak 's/^version = ".*"/version = "'${{ inputs.version }}'"/' Cargo.toml rm -f Cargo.toml.bak - - name: Install Linux build dependencies - if: matrix.platform == 'linux' + - name: Install Linux build dependencies (host runner) + if: matrix.platform == 'linux' && matrix.container == '' run: | sudo apt-get update sudo apt-get install -y \ @@ -119,11 +124,28 @@ jobs: sudo apt-get install -y musl-tools fi + - name: Install Linux build dependencies (manylinux container) + if: matrix.platform == 'linux' && matrix.container != '' + run: | + # perl-core provides FindBin, File::Compare, etc. that openssl-sys's + # vendored openssl build needs; in AlmaLinux 8 these aren't standalone packages. + # clang provides libclang.so for bindgen (used by llama-cpp-sys-2). + # Defensive: avoid actions/checkout falling back to a tarball download if base image changes. + dnf install -y --setopt=install_weak_deps=False \ + openssl-devel \ + dbus-devel \ + libxcb-devel \ + cmake \ + perl-core \ + clang \ + git \ + tar + - name: Cache Cargo artifacts (Linux/macOS) if: matrix.platform != 'windows' uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - key: ${{ matrix.architecture }}-${{ matrix.target-suffix }}-${{ matrix.build-on }}-native-macos-deployment-target-12 + key: ${{ matrix.architecture }}-${{ matrix.target-suffix }}-${{ matrix.build-on }}-${{ matrix.container || 'native' }}-macos-deployment-target-12 - name: Cache Cargo artifacts (Windows) if: matrix.platform == 'windows' @@ -131,8 +153,8 @@ jobs: with: key: windows-msvc-cli-${{ matrix.variant }} - - name: Build CLI (Linux/macOS) - if: matrix.platform != 'windows' + - name: Build CLI (Linux/macOS host runner) + if: matrix.platform != 'windows' && matrix.container == '' env: RUST_LOG: debug RUST_BACKTRACE: 1 @@ -157,6 +179,27 @@ jobs: cargo build --release --target ${TARGET} -p goose-cli "${FEATURE_ARGS[@]}" fi + - name: Build CLI (manylinux container) + if: matrix.platform == 'linux' && matrix.container != '' + env: + RUST_BACKTRACE: 1 + run: | + # Hermit's tool cache is host-runner-scoped; inside the container we + # bootstrap rustup directly and let rust-toolchain.toml pin the channel. + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain none --profile minimal --no-modify-path + export PATH="$HOME/.cargo/bin:$PATH" + TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}" + RUST_CHANNEL=$(grep '^channel' rust-toolchain.toml | cut -d'"' -f2) + if [ -z "$RUST_CHANNEL" ]; then + echo "Could not parse channel from rust-toolchain.toml" >&2 + exit 1 + fi + rustup toolchain install "$RUST_CHANNEL" --profile minimal \ + --component rustc,cargo --target "$TARGET" + rustup show + cargo build --release --target "$TARGET" -p goose-cli + - name: Setup Rust (Windows) if: matrix.platform == 'windows' shell: bash @@ -215,7 +258,10 @@ jobs: - name: Package CLI (Linux/macOS) if: matrix.platform != 'windows' run: | - source ./bin/activate-hermit + # Hermit isn't installed in the manylinux container; tar is all this step needs. + if [ "${{ matrix.container }}" = '' ]; then + source ./bin/activate-hermit + fi export TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}" export VARIANT_SUFFIX="" if [ "${{ matrix.variant }}" = "vulkan" ]; then diff --git a/Cargo.lock b/Cargo.lock index e1423fe66404..c8f2a81843d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,7 @@ dependencies = [ "futures-concurrency", "jsonrpcmsg", "rmcp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "schemars 1.2.1", "serde", "serde_json", @@ -64,11 +64,10 @@ dependencies = [ [[package]] name = "agent-client-protocol-derive" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce42c2d3c048c12897eef2e577dfff1e3355c632c9f1625cc953b9df48b44631" +checksum = "cabdc9d845d08ec7ed2d0c9de1ae4a1b198301407d55855261572761be90ec9f" dependencies = [ - "proc-macro2", "quote", "syn 2.0.117", ] @@ -232,12 +231,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", - "image 0.25.10", "log", "objc2", "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", "objc2-foundation", "parking_lot", "percent-encoding", @@ -247,19 +243,13 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "arrayref" version = "0.3.9" @@ -280,9 +270,9 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -340,9 +330,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -443,15 +433,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" -version = "1.8.16" +version = "1.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -463,12 +453,13 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", "hex", - "http 1.4.0", + "http 1.4.1", "sha1", "time", "tokio", @@ -530,7 +521,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "percent-encoding", "pin-project-lite", @@ -559,7 +550,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body-util", "regex-lite", "tracing", @@ -585,16 +576,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "1.98.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" dependencies = [ "aws-credential-types", "aws-runtime", @@ -609,16 +600,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.100.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" dependencies = [ "aws-credential-types", "aws-runtime", @@ -633,16 +624,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.103.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" dependencies = [ "aws-credential-types", "aws-runtime", @@ -658,7 +649,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "regex-lite", "tracing", ] @@ -679,7 +670,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "percent-encoding", "sha2 0.11.0", "time", @@ -721,7 +712,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "percent-encoding", @@ -730,30 +721,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "aws-smithy-http-client" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2", - "http 1.4.0", - "hyper", - "hyper-rustls", - "hyper-util", - "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower", - "tracing", -] - [[package]] name = "aws-smithy-json" version = "0.62.6" @@ -792,7 +759,6 @@ checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", - "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", "aws-smithy-schema", @@ -800,7 +766,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -821,7 +787,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "pin-project-lite", "tokio", "tracing", @@ -847,7 +813,7 @@ checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -861,7 +827,7 @@ dependencies = [ "bytes-utils", "futures-core", "http 0.2.12", - "http 1.4.0", + "http 1.4.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -912,7 +878,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -934,7 +900,6 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -945,7 +910,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "mime", @@ -953,7 +918,6 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -977,7 +941,7 @@ dependencies = [ "bytes", "either", "fs-err", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper", "hyper-util", @@ -1130,7 +1094,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1139,8 +1103,8 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", - "shlex 1.3.0", + "rustc-hash 2.1.2", + "shlex", "syn 2.0.117", ] @@ -1170,7 +1134,7 @@ dependencies = [ "biome_json_parser", "biome_json_syntax", "biome_rowan", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap 1.9.3", "serde", "serde_json", @@ -1190,7 +1154,7 @@ dependencies = [ "biome_rowan", "biome_text_edit", "biome_text_size", - "bitflags 2.11.0", + "bitflags 2.11.1", "bpaf", "serde", "termcolor", @@ -1283,7 +1247,7 @@ dependencies = [ "biome_js_unicode_table", "biome_parser", "biome_rowan", - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "drop_bomb", "indexmap 1.9.3", @@ -1368,7 +1332,7 @@ dependencies = [ "biome_console", "biome_diagnostics", "biome_rowan", - "bitflags 2.11.0", + "bitflags 2.11.1", "drop_bomb", ] @@ -1473,9 +1437,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] @@ -1566,18 +1530,18 @@ dependencies = [ [[package]] name = "bpaf" -version = "0.9.24" +version = "0.9.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2435ff2f08be8436bdcd06a3de2bd7696fd10e45eb630ecfc09af7fbfa3e69a" +checksum = "0b86829876e7e200161a5aa6ea688d46c32d64d70ee3100127790dde84688d6e" dependencies = [ "bpaf_derive", ] [[package]] name = "bpaf_derive" -version = "0.5.23" +version = "0.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549ca9c364fdc06f9f36d1356980193d930abc80db848193c2dd4d0e9a83de1b" +checksum = "2f7e98cee839b19076cb3ce1afdb62bb182e04ff5f71f70188827002fae91094" dependencies = [ "proc-macro2", "quote", @@ -1605,6 +1569,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1618,9 +1591,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ "allocator-api2", ] @@ -1712,9 +1685,9 @@ checksum = "1bf2a5fb3207c12b5d208ebc145f967fea5cac41a021c37417ccc31ba40f39ee" [[package]] name = "calendrical_calculations" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" dependencies = [ "core_maths", "displaydoc", @@ -1739,7 +1712,7 @@ dependencies = [ "candle-kernels", "candle-metal-kernels", "candle-ug", - "cudarc 0.19.4", + "cudarc 0.19.7", "float8", "gemm 0.19.0", "half", @@ -1755,7 +1728,7 @@ dependencies = [ "safetensors 0.7.0", "thiserror 2.0.18", "tokenizers 0.22.2", - "yoke 0.8.1", + "yoke 0.8.2", "zip 7.2.0", ] @@ -1873,22 +1846,16 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 1.3.0", + "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -1940,7 +1907,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -2063,9 +2030,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.3.0" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" dependencies = [ "clap", "roff", @@ -2106,9 +2073,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -2171,7 +2138,6 @@ version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ - "crossterm", "unicode-segmentation", "unicode-width 0.2.2", ] @@ -2206,9 +2172,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -2220,9 +2186,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -2239,18 +2205,10 @@ version = "0.15.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8" dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", "pathdiff", - "ron", - "rust-ini", - "serde-untagged", "serde_core", - "serde_json", - "toml 1.1.0+spec-1.1.0", - "winnow 1.0.0", - "yaml-rust2", + "toml 1.1.2+spec-1.1.0", + "winnow 1.0.3", ] [[package]] @@ -2277,26 +2235,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - [[package]] name = "constant_time_eq" version = "0.4.2" @@ -2312,15 +2250,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "convert_case" version = "0.10.0" @@ -2446,9 +2375,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -2525,29 +2454,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "document-features", - "parking_lot", - "rustix 1.1.4", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - [[package]] name = "crunchy" version = "0.2.4" @@ -2579,21 +2485,21 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] [[package]] name = "ctor" -version = "1.0.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d765eb1c0bda10d31e0ea185f5ee15da532d60b0912d2bd1441783439e749c5" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ - "link-section", - "linktime-proc-macro", + "quote", + "syn 2.0.117", ] [[package]] @@ -2635,9 +2541,9 @@ dependencies = [ [[package]] name = "cudarc" -version = "0.19.4" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f071cd6a7b5d51607df76aa2d426aaabc7a74bc6bdb885b8afa63a880572ad9b" +checksum = "1cea5f10a99e025c1b44ae2354c2d8326b25ddbd0baf76bde8e55cfd4018a2cc" dependencies = [ "float8", "half", @@ -2742,9 +2648,9 @@ dependencies = [ [[package]] name = "dary_heap" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" dependencies = [ "serde", ] @@ -2778,9 +2684,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-url" @@ -2790,13 +2696,13 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "dbus" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2896,7 +2802,7 @@ dependencies = [ "deno_error", "deno_media_type", "deno_path_util", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "log", "once_cell", @@ -3293,7 +3199,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -3327,7 +3233,7 @@ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] @@ -3411,7 +3317,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", ] @@ -3432,15 +3338,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "doc-comment" version = "0.3.4" @@ -3490,7 +3387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33175ddb7a6d418589cab2966bd14a710b3b1139459d3d5ca9edf783c4833f4c" dependencies = [ "num-bigint", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -3600,9 +3497,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -3708,17 +3605,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.14" @@ -3818,29 +3704,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -3869,13 +3741,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -4021,9 +3892,9 @@ dependencies = [ [[package]] name = "fraction" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" dependencies = [ "lazy_static", "num", @@ -4031,9 +3902,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "from_variant" @@ -4471,15 +4345,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width 0.2.2", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -4516,7 +4381,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -4533,9 +4398,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -4580,7 +4445,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.35.0" +version = "1.36.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-schema", @@ -4618,8 +4483,10 @@ dependencies = [ "goose-mcp", "goose-sdk", "goose-test-support", - "http 1.4.0", + "http 1.4.1", "http-body-util", + "icu_calendar", + "icu_locale", "ignore", "include_dir", "indexmap 2.14.0", @@ -4630,7 +4497,8 @@ dependencies = [ "keyring", "libc", "llama-cpp-2", - "lru 0.18.0", + "llama-cpp-sys-2", + "lru", "minijinja", "mockall", "nanoid", @@ -4638,11 +4506,11 @@ dependencies = [ "nostr-sdk", "oauth2", "once_cell", - "opentelemetry", + "opentelemetry 0.32.0", "opentelemetry-appender-tracing", - "opentelemetry-otlp", + "opentelemetry-otlp 0.32.0", "opentelemetry-stdout", - "opentelemetry_sdk", + "opentelemetry_sdk 0.32.0", "pastey", "pctx_code_mode", "pem", @@ -4650,10 +4518,10 @@ dependencies = [ "pkcs8", "process-wrap", "pulldown-cmark", - "rand 0.8.5", + "rand 0.8.6", "rayon", "regex", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp", "rubato", "rustls", @@ -4664,11 +4532,12 @@ dependencies = [ "serde_urlencoded", "serde_yaml", "serial_test", - "sha2 0.10.9", + "sha2 0.11.0", "shell-words", "shellexpand", + "smithy-transport-reqwest", "sqlx", - "strum 0.28.0", + "strum 0.27.2", "symphonia", "sys-info", "tempfile", @@ -4711,7 +4580,7 @@ dependencies = [ [[package]] name = "goose-acp-macros" -version = "1.35.0" +version = "1.36.0" dependencies = [ "quote", "syn 2.0.117", @@ -4719,7 +4588,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.35.0" +version = "1.36.0" dependencies = [ "anstream", "anyhow", @@ -4737,24 +4606,25 @@ dependencies = [ "comfy-table", "console", "dotenvy", + "env-lock", "etcetera 0.11.0", "futures", "goose", "goose-mcp", "indicatif", "open", - "rand 0.8.5", + "rand 0.8.6", "regex", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp", "rustyline", "serde", "serde_json", "serde_yaml", - "sha2 0.10.9", - "shlex 2.0.1", + "sha2 0.11.0", + "shlex", "sigstore-verify", - "strum 0.28.0", + "strum 0.27.2", "tar", "tempfile", "test-case", @@ -4771,7 +4641,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.35.0" +version = "1.36.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -4784,7 +4654,7 @@ dependencies = [ "lopdf", "once_cell", "process-wrap", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp", "schemars 1.2.1", "serde", @@ -4801,7 +4671,7 @@ dependencies = [ [[package]] name = "goose-sdk" -version = "1.35.0" +version = "1.36.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-schema", @@ -4814,7 +4684,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.35.0" +version = "1.36.0" dependencies = [ "anyhow", "aws-lc-rs", @@ -4830,12 +4700,12 @@ dependencies = [ "goose", "goose-mcp", "hex", - "http 1.4.0", + "http 1.4.1", "openssl", "pem", - "rand 0.8.5", + "rand 0.8.6", "rcgen", - "reqwest 0.13.3", + "reqwest 0.13.4", "rmcp", "rustls", "serde", @@ -4861,7 +4731,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.35.0" +version = "1.36.0" dependencies = [ "clap", "serde_json", @@ -4869,11 +4739,11 @@ dependencies = [ [[package]] name = "goose-test-support" -version = "1.35.0" +version = "1.36.0" dependencies = [ "axum", "env-lock", - "opentelemetry", + "opentelemetry 0.32.0", "rmcp", "serde_json", "tokio", @@ -4892,25 +4762,25 @@ dependencies = [ [[package]] name = "gzip-header" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +checksum = "86848f4fd157d91041a62c78046fb7b248bcc2dce78376d436a1756e9a038577" dependencies = [ "crc32fast", ] [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "slab", "tokio", @@ -4935,9 +4805,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" +checksum = "d43ccdfe15a81ab0a8af639e90254227c9a46afd9c5f5b6ec7efaa345c4b0f00" dependencies = [ "derive_builder", "log", @@ -4991,14 +4861,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -5009,15 +4874,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "heck" version = "0.5.0" @@ -5094,14 +4950,14 @@ dependencies = [ [[package]] name = "hstr" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa57007c3c9dab34df2fa4c1fb52fe9c34ec5a27ed9d8edea53254b50cd7887" +checksum = "c1b94e40256e78ddd4e30490aa931bec17e65e9413a6ad11f64ec67815da9323" dependencies = [ "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "triomphe", ] @@ -5134,9 +4990,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -5160,7 +5016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -5171,7 +5027,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "pin-project-lite", ] @@ -5199,22 +5055,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -5222,20 +5077,18 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.1", "hyper", "hyper-util", "rustls", - "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -5277,7 +5130,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "hyper", "ipnet", @@ -5346,7 +5199,7 @@ checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerovec", ] @@ -5368,9 +5221,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -5428,16 +5281,16 @@ checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", "serde", "stable_deref_trait", "writeable", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", @@ -5512,7 +5365,6 @@ dependencies = [ "jpeg-decoder", "num-traits", "png 0.17.16", - "qoi", "tiff 0.9.1", ] @@ -5525,7 +5377,7 @@ dependencies = [ "bytemuck", "byteorder-lite", "color_quant", - "gif 0.14.1", + "gif 0.14.2", "moxcms", "num-traits", "png 0.18.1", @@ -5594,7 +5446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -5637,7 +5489,6 @@ version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ - "console", "once_cell", "similar", "tempfile", @@ -5724,15 +5575,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "ixdtf" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" +checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -5740,14 +5591,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" dependencies = [ "proc-macro2", "quote", @@ -5769,22 +5620,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -5794,7 +5629,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -5815,15 +5650,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -5864,25 +5690,16 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "jsonc-parser" version = "0.27.1" @@ -5923,7 +5740,6 @@ dependencies = [ "referencing", "regex", "regex-syntax", - "reqwest 0.12.28", "serde", "serde_json", "uuid-simd", @@ -5944,7 +5760,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -6017,9 +5833,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libbz2-rs-sys" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" @@ -6065,14 +5881,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] @@ -6086,12 +5902,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-section" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1e908a416d6e9f725743b84a36feea40c4c131e805fbc26d61f9f451f36080" - [[package]] name = "linktime-proc-macro" version = "0.1.0" @@ -6104,7 +5914,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", ] @@ -6122,9 +5932,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -6171,9 +5981,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lopdf" @@ -6182,27 +5992,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fdcbab5b237a03984f83b1394dc534e0b1960675c7f3ec4d04dccc9032b56d" dependencies = [ "aes", - "bitflags 2.11.0", + "bitflags 2.11.1", "cbc", - "chrono", "ecb", "encoding_rs", "flate2", "getrandom 0.4.2", "indexmap 2.14.0", "itoa", - "jiff", "log", "md-5", "nom 8.0.0", "nom_locate", - "rand 0.10.0", + "rand 0.10.1", "rangemap", - "rayon", "sha2 0.10.9", "stringprep", "thiserror 2.0.18", - "time", "ttf-parser", "weezl", ] @@ -6213,15 +6019,6 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" -[[package]] -name = "lru" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" -dependencies = [ - "hashbrown 0.17.0", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -6315,7 +6112,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -6368,9 +6165,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -6379,9 +6176,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" dependencies = [ "cfg-if", "downcast", @@ -6393,9 +6190,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.14.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" dependencies = [ "cfg-if", "proc-macro2", @@ -6496,11 +6293,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -6597,7 +6394,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ - "lru 0.16.4", + "lru", "nostr", "tokio", ] @@ -6613,15 +6410,15 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +checksum = "91b2c039df4f96c4bf7dae52a74fd5516ad6dda83a11c0c69dea91b5255a4f37" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", "hex", - "lru 0.16.4", + "lru", "negentropy", "nostr", "nostr-database", @@ -6675,7 +6472,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -6690,7 +6487,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -6713,9 +6510,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -6803,8 +6600,8 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.0", - "rand 0.8.5", + "http 1.4.1", + "rand 0.8.6", "reqwest 0.12.28", "serde", "serde_json", @@ -6838,7 +6635,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -6850,7 +6647,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] @@ -6861,7 +6658,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -6880,7 +6677,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -6893,7 +6690,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -6904,7 +6701,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "dispatch2", "objc2", @@ -6944,11 +6741,11 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" -version = "6.5.1" +version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -6956,9 +6753,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.9.1" +version = "69.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" dependencies = [ "cc", "pkg-config", @@ -6987,7 +6784,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -7014,9 +6811,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] @@ -7037,7 +6834,22 @@ dependencies = [ [[package]] name = "opentelemetry" version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -7049,10 +6861,11 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.31.1" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" dependencies = [ - "opentelemetry", + "opentelemetry 0.32.0", "tracing", "tracing-core", "tracing-subscriber", @@ -7061,64 +6874,124 @@ dependencies = [ [[package]] name = "opentelemetry-http" version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http 1.4.0", - "opentelemetry", - "reqwest 0.13.3", + "http 1.4.1", + "opentelemetry 0.31.0", + "reqwest 0.12.28", +] + +[[package]] +name = "opentelemetry-http" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" +dependencies = [ + "async-trait", + "bytes", + "http 1.4.1", + "opentelemetry 0.32.0", + "reqwest 0.13.4", ] [[package]] name = "opentelemetry-otlp" -version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ - "http 1.4.0", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", + "http 1.4.1", + "opentelemetry 0.31.0", + "opentelemetry-http 0.31.0", + "opentelemetry-proto 0.31.0", + "opentelemetry_sdk 0.31.0", "prost", - "reqwest 0.13.3", + "reqwest 0.12.28", "thiserror 2.0.18", "tokio", "tonic", - "tonic-types", + "tracing", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" +dependencies = [ + "http 1.4.1", + "opentelemetry 0.32.0", + "opentelemetry-http 0.32.0", + "opentelemetry-proto 0.32.0", + "opentelemetry_sdk 0.32.0", + "prost", + "reqwest 0.13.4", + "thiserror 2.0.18", ] [[package]] name = "opentelemetry-proto" version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", "prost", "tonic", "tonic-prost", ] +[[package]] +name = "opentelemetry-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.0", + "prost", +] + [[package]] name = "opentelemetry-stdout" -version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b1c6a247d79091f0062a5f4bd058589525cf987a8d4c169440d9c1be72f0ad" dependencies = [ "chrono", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.0", ] [[package]] name = "opentelemetry_sdk" version = "0.31.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a#345cd74a9c88ad1a47435d3d063c12d47235e803" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry", + "opentelemetry 0.31.0", + "percent-encoding", + "rand 0.9.4", + "thiserror 2.0.18", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368afaed344110f40b179bb8fbe54bc52d98f9bd2b281799ef32487c2650c956" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.32.0", "percent-encoding", "portable-atomic", "rand 0.9.4", @@ -7132,16 +7005,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "outref" version = "0.5.2" @@ -7301,7 +7164,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "utoipa 5.4.0", + "utoipa 5.5.0", ] [[package]] @@ -7335,16 +7198,16 @@ dependencies = [ "anyhow", "base64 0.22.1", "camino", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "keyring", - "opentelemetry-otlp", - "opentelemetry_sdk", - "reqwest 0.13.3", + "opentelemetry-otlp 0.31.1", + "opentelemetry_sdk 0.31.0", + "reqwest 0.13.4", "rmcp", "serde", "serde_json", - "shlex 1.3.0", + "shlex", "thiserror 2.0.18", "tokio", "tonic", @@ -7511,7 +7374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -7533,7 +7396,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.2", + "siphasher 1.0.3", ] [[package]] @@ -7542,23 +7405,23 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ - "siphasher 1.0.2", + "siphasher 1.0.3", ] [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -7600,9 +7463,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -7612,13 +7475,13 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml 0.38.4", + "quick-xml 0.39.4", "serde", "time", ] @@ -7642,7 +7505,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -7668,18 +7531,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "serde_core", "writeable", @@ -7747,15 +7610,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -7835,15 +7689,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "prost-types" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" -dependencies = [ - "prost", -] - [[package]] name = "psl-types" version = "2.0.11" @@ -7852,9 +7697,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -7876,19 +7721,11 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.11.0", - "getopts", + "bitflags 2.11.1", "memchr", - "pulldown-cmark-escape", "unicase", ] -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "pulp" version = "0.21.5" @@ -7928,18 +7765,9 @@ checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0" [[package]] name = "pxfm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" - -[[package]] -name = "qoi" -version = "0.4.1" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "quick-error" @@ -7969,9 +7797,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -7987,7 +7815,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "socket2", "thiserror 2.0.18", @@ -8008,7 +7836,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -8071,9 +7899,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -8092,13 +7920,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -8141,9 +7969,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_distr" @@ -8167,7 +7995,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -8207,23 +8035,14 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ + "aws-lc-rs", "pem", - "ring", "rustls-pki-types", "time", "x509-parser", "yasna", ] -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - [[package]] name = "reborrow" version = "0.5.5" @@ -8236,16 +8055,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -8350,7 +8169,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -8379,14 +8198,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -8397,7 +8216,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -8435,9 +8254,9 @@ dependencies = [ [[package]] name = "resb" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76" +checksum = "22d392791f3c6802a1905a509e9d1a6039cbbcb5e9e00e5a6d3661f7c874f390" dependencies = [ "potential_utf", "serde_core", @@ -8487,7 +8306,7 @@ dependencies = [ "bytes", "chrono", "futures", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -8496,8 +8315,8 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "rand 0.10.0", - "reqwest 0.13.3", + "rand 0.10.1", + "reqwest 0.13.4", "rmcp-macros", "schemars 1.2.1", "serde", @@ -8528,23 +8347,9 @@ dependencies = [ [[package]] name = "roff" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" - -[[package]] -name = "ron" -version = "0.12.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" -dependencies = [ - "bitflags 2.11.0", - "once_cell", - "serde", - "serde_derive", - "typeid", - "unicode-ident", -] +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rsa" @@ -8572,20 +8377,8 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" dependencies = [ - "num-complex", "num-integer", "num-traits", - "realfft", -] - -[[package]] -name = "rust-ini" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = [ - "cfg-if", - "ordered-multimap", ] [[package]] @@ -8602,9 +8395,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -8615,20 +8408,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rusticata-macros" version = "4.1.0" @@ -8644,7 +8423,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -8657,7 +8436,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -8671,7 +8450,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -8694,9 +8472,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -8704,13 +8482,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -8753,7 +8531,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "clipboard-win", "home", @@ -8950,7 +8728,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", "serde", ] @@ -8970,7 +8748,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -8983,7 +8761,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -9002,9 +8780,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "seq-macro" @@ -9022,18 +8800,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - [[package]] name = "serde_bytes" version = "0.11.19" @@ -9122,9 +8888,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -9157,11 +8923,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -9176,9 +8943,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -9205,9 +8972,6 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ - "futures-executor", - "futures-util", - "log", "once_cell", "parking_lot", "scc", @@ -9288,12 +9052,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -9375,7 +9133,7 @@ checksum = "fbe0bf948de89bed9b3b5c9d62940264a4da60db3518006b952b107d0e71926f" dependencies = [ "base64 0.22.1", "hex", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "sigstore-crypto", @@ -9418,7 +9176,7 @@ dependencies = [ "hex", "jiff", "rand 0.9.4", - "reqwest 0.13.3", + "reqwest 0.13.4", "rustls-pki-types", "rustls-webpki", "sigstore-crypto", @@ -9475,9 +9233,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" @@ -9525,9 +9283,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -9561,6 +9319,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smithy-transport-reqwest" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566dc85be03a09c384f77a122188d9af000e1f1bd23551b346a9b555838da7e1" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.1", + "parking_lot", + "reqwest 0.13.4", +] + [[package]] name = "socket2" version = "0.6.3" @@ -9582,7 +9353,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -9657,7 +9428,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink 0.10.0", + "hashlink", "indexmap 2.14.0", "log", "memchr", @@ -9723,7 +9494,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -9745,7 +9516,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -9766,7 +9537,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -9784,7 +9555,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2 0.10.9", @@ -9823,9 +9594,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" +checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72" dependencies = [ "bytes", "futures-util", @@ -9842,15 +9613,15 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9880,12 +9651,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "string_enum" version = "1.0.2" @@ -9977,7 +9742,7 @@ dependencies = [ "allocator-api2", "bumpalo", "hashbrown 0.14.5", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -10006,7 +9771,7 @@ dependencies = [ "new_debug_unreachable", "num-bigint", "once_cell", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "siphasher 0.3.11", "swc_atoms", @@ -10050,12 +9815,12 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a573a0c72850dec8d4d8085f152d5778af35a2520c3093b242d2d1d50776da7c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "is-macro", "num-bigint", "once_cell", "phf 0.11.3", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "string_enum", "swc_atoms", @@ -10076,7 +9841,7 @@ dependencies = [ "num-bigint", "once_cell", "regex", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "ryu-js", "serde", "swc_allocator", @@ -10105,10 +9870,10 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e82f7747e052c6ff6e111fa4adeb14e33b46ee6e94fe5ef717601f651db48fc" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "either", "num-bigint", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "seq-macro", "serde", "smallvec", @@ -10129,7 +9894,7 @@ checksum = "fbcababb48f0d46587a0a854b2c577eb3a56fa99687de558338021e93cd2c8f5" dependencies = [ "anyhow", "pathdiff", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10142,11 +9907,11 @@ version = "27.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1a51af1a92cd4904c073b293e491bbc0918400a45d58227b34c961dd6f52d7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "either", "num-bigint", "phf 0.11.3", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "seq-macro", "serde", "smartstring", @@ -10168,7 +9933,7 @@ dependencies = [ "once_cell", "par-core", "phf 0.11.3", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10211,7 +9976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d7748d4112c87ce1885260035e4a43cebfe7661a40174b7d77a0a04760a257" dependencies = [ "either", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10232,7 +9997,7 @@ dependencies = [ "bytes-str", "indexmap 2.14.0", "once_cell", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "sha1", "string_enum", @@ -10253,7 +10018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4408800fdeb541fabf3659db622189a0aeb386f57b6103f9294ff19dfde4f7b0" dependencies = [ "bytes-str", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10274,7 +10039,7 @@ dependencies = [ "num_cpus", "once_cell", "par-core", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "ryu-js", "swc_atoms", "swc_common", @@ -10332,7 +10097,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -10362,7 +10127,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", - "symphonia-bundle-flac", "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-adpcm", @@ -10370,26 +10134,12 @@ dependencies = [ "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", - "symphonia-format-caf", "symphonia-format-isomp4", "symphonia-format-mkv", - "symphonia-format-ogg", "symphonia-format-riff", "symphonia-metadata", ] -[[package]] -name = "symphonia-bundle-flac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - [[package]] name = "symphonia-bundle-mp3" version = "0.5.5" @@ -10467,17 +10217,6 @@ dependencies = [ "log", ] -[[package]] -name = "symphonia-format-caf" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", -] - [[package]] name = "symphonia-format-isomp4" version = "0.5.5" @@ -10504,18 +10243,6 @@ dependencies = [ "symphonia-utils-xiph", ] -[[package]] -name = "symphonia-format-ogg" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - [[package]] name = "symphonia-format-riff" version = "0.5.5" @@ -10657,7 +10384,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "enum-as-inner", "libc", @@ -10671,7 +10398,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -10700,7 +10427,6 @@ checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", - "xattr", ] [[package]] @@ -10845,9 +10571,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259cdf8ed4e4aca6f1e9d011e10bd53f524a2d0637d7b28450f6c64ac298c4c6" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" [[package]] name = "thiserror" @@ -10987,20 +10713,11 @@ dependencies = [ "zoneinfo64", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -11111,9 +10828,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -11144,9 +10861,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -11273,15 +10990,15 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "serde_core", "serde_spanned", - "toml_datetime 1.1.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] @@ -11295,40 +11012,40 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "hyper", @@ -11348,26 +11065,15 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", "tonic", ] -[[package]] -name = "tonic-types" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399" -dependencies = [ - "prost", - "prost-types", - "tonic", -] - [[package]] name = "tower" version = "0.5.3" @@ -11394,11 +11100,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body 1.0.1", "http-body-util", "pin-project-lite", @@ -11480,29 +11186,17 @@ dependencies = [ "tracing", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", - "opentelemetry", + "opentelemetry 0.32.0", "smallvec", "tracing", "tracing-core", - "tracing-log", "tracing-subscriber", "web-time", ] @@ -11530,25 +11224,13 @@ dependencies = [ "serde", "serde_json", "sharded-slab", - "smallvec", "thread_local", "time", "tracing", "tracing-core", - "tracing-log", "tracing-serde", ] -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - [[package]] name = "tree-sitter" version = "0.26.9" @@ -11689,7 +11371,7 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "rand 0.9.4", @@ -11708,7 +11390,7 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.1", "httparse", "log", "native-tls", @@ -11731,12 +11413,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.20.0" @@ -12001,14 +11677,14 @@ dependencies = [ [[package]] name = "utoipa" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ "indexmap 2.14.0", "serde", "serde_json", - "utoipa-gen 5.4.0", + "utoipa-gen 5.5.0", ] [[package]] @@ -12026,9 +11702,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", @@ -12071,7 +11747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f84468f393984251db025e944544590a8fb429d5088b78102bd72f09232f0da" dependencies = [ "bindgen", - "bitflags 2.11.0", + "bitflags 2.11.1", "fslock", "gzip-header", "home", @@ -12081,11 +11757,20 @@ dependencies = [ "which 6.0.3", ] +[[package]] +name = "v_escape-base" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1212fce830b75af194b578e55b3db9049f2c8c45f58d397fb25602fdb50fb3d" + [[package]] name = "v_htmlescape" -version = "0.15.8" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +checksum = "befb3d53c9e3ec641417685896cbc8cc5bd264d6a2e190c56aaef1af24740d99" +dependencies = [ + "v_escape-base", +] [[package]] name = "valuable" @@ -12138,11 +11823,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -12151,7 +11836,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -12162,9 +11847,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -12175,23 +11860,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12199,9 +11880,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -12212,9 +11893,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -12270,7 +11951,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -12278,9 +11959,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -12303,7 +11984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni 0.22.4", + "jni", "log", "ndk-context", "objc2", @@ -12314,9 +11995,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -12327,14 +12008,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -12584,15 +12265,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -12638,21 +12310,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -12710,12 +12367,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -12734,12 +12385,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -12758,12 +12403,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -12794,12 +12433,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -12818,12 +12451,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -12842,12 +12469,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -12866,12 +12487,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -12898,9 +12513,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -12931,7 +12546,7 @@ dependencies = [ "base64 0.22.1", "deadpool", "futures", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper", "hyper-util", @@ -12953,6 +12568,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -13002,7 +12623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", @@ -13034,9 +12655,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -13085,12 +12706,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", + "aws-lc-rs", "data-encoding", "der-parser", "lazy_static", "nom 7.1.3", "oid-registry", - "ring", "rusticata-macros", "thiserror 2.0.18", "time", @@ -13107,16 +12728,6 @@ dependencies = [ "der", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.4", -] - [[package]] name = "xmlparser" version = "0.13.6" @@ -13129,17 +12740,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7008a9d8ba97a7e47d9b2df63fcdb8dade303010c5a7cd5bf2469d4da6eba673" -[[package]] -name = "yaml-rust2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.11.0", -] - [[package]] name = "yansi" version = "1.0.1" @@ -13170,12 +12770,12 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive 0.8.2", "zerofrom", ] @@ -13193,9 +12793,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -13205,18 +12805,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -13225,18 +12825,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -13266,32 +12866,33 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -13435,9 +13036,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 6b09a19e4dbe..2774b0a4b51a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.35.0" +version = "1.36.0" rust-version = "1.91.1" authors = ["AAIF "] license = "Apache-2.0" @@ -20,88 +20,96 @@ uninlined_format_args = "allow" string_slice = "warn" [workspace.dependencies] -rmcp = { version = "1.7.0", features = ["schemars", "auth"] } -agent-client-protocol-schema = { version = "0.12", features = ["unstable"] } -agent-client-protocol = "0.11" -arboard = "3" -anyhow = "1.0" -async-stream = "0.3" -async-trait = "0.1" -axum = "0.8" -base64 = "0.22.1" -bytes = "1" -chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["derive"] } -dirs = "5.0" -dotenvy = "0.15" -env-lock = "1.0.1" -etcetera = "0.11.0" -fs2 = "0.4" -futures = "0.3" -http = "1.0" -ignore = "0.4.25" -include_dir = "0.7.4" -indoc = "2.0" -lru = "0.18" -once_cell = "1.20" -rand = "0.8" -regex = "1.12" -reqwest = { version = "0.13", default-features = false, features = ["multipart", "form"] } -schemars = { default-features = false, version = "1.0" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.9" -shellexpand = "3.1" -strum = { version = "0.28", features = ["derive"] } -tempfile = "3" -thiserror = "1.0" -tokio = { version = "1.49", features = ["full"] } -tokio-stream = "0.1" -tokio-util = "0.7" -tower-http = "0.6.11" -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = "0.3" -urlencoding = "2.1" -utoipa = "4.1" -uuid = { version = "1.23", features = ["v4"] } -webbrowser = "1.2" -which = "8.0.0" -winapi = { version = "0.3", features = ["wincred"] } -wiremock = "0.6" -zip = { version = "^8.6", default-features = false, features = ["deflate"] } -serial_test = "3.2.0" -sha2 = "0.10" -shell-words = "1.1.1" -test-case = "3.3.1" -url = "2.5.8" -opentelemetry = "0.31" -opentelemetry_sdk = { version = "0.31", features = ["metrics"] } -opentelemetry-otlp = "0.31" -opentelemetry-appender-tracing = { version = "0.31", features = ["experimental_span_attributes"] } -opentelemetry-stdout = { version = "0.31", features = ["trace", "metrics", "logs"] } -tracing-futures = { version = "0.2", features = ["futures-03"] } -tracing-opentelemetry = "0.32" +rmcp = { version = "1.4", default-features = false, features = ["schemars", "auth"] } +agent-client-protocol-schema = { version = "0.12", default-features = false, features = ["unstable"] } +agent-client-protocol = { version = "0.11", default-features = false } +arboard = { version = "3", default-features = false } +anyhow = { version = "1.0.102", default-features = false, features = ["std"] } +async-stream = { version = "0.3.6", default-features = false } +async-trait = { version = "0.1.89", default-features = false } +axum = { version = "0.8", default-features = false, features = ["http1", "http2", "json", "tokio", "query"] } +base64 = { version = "0.22.1", default-features = false, features = ["std"] } +bytes = { version = "1.10.1", default-features = false, features = ["std"] } +candle-core = { version = "0.10", default-features = false } +candle-nn = { version = "0.10", default-features = false } +chrono = { version = "0.4.44", default-features = false, features = ["serde", "std"] } +clap = { version = "4.1.14", default-features = false, features = ["derive", "std", "help", "suggestions", "usage", "color", "error-context"] } +dirs = { version = "5", default-features = false } +dotenvy = { version = "0.15.7", default-features = false } +env-lock = { version = "1", default-features = false } +etcetera = { version = "0.11", default-features = false } +fs2 = { version = "0.4", default-features = false } +futures = { version = "0.3.32", default-features = false, features = ["std"] } +http = { version = "1.1", default-features = false, features = ["std"] } +ignore = { version = "0.4.12", default-features = false } +include_dir = { version = "0.7", default-features = false } +indoc = { version = "2", default-features = false } +keyring = { version = "3.6.3", default-features = false, features = ["vendored"] } +lru = { version = "0.16", default-features = false } +once_cell = { version = "1.21.3", default-features = false, features = ["std"] } +rand = { version = "0.8.5", default-features = false, features = ["std"] } +regex = { version = "1.12.3", default-features = false, features = ["std"] } +reqwest = { version = "0.13.2", default-features = false, features = ["multipart", "form"] } +rustls = { version = "0.23.31", default-features = false, features = ["aws_lc_rs", "std"] } +schemars = { version = "1.0.2", default-features = false, features = ["std"] } +serde = { version = "1.0.228", default-features = false, features = ["derive", "std"] } +serde_json = { version = "1.0.145", default-features = false, features = ["std"] } +serde_yaml = { version = "0.9.32", default-features = false } +shellexpand = { version = "3", default-features = false, features = ["base-0", "tilde"] } +strum = { version = "0.27.1", default-features = false, features = ["derive", "std"] } +tempfile = { version = "3.10.1", default-features = false } +thiserror = { version = "1.0.49", default-features = false } +tokio = { version = "1.48", default-features = false } +tokio-stream = { version = "0.1.16", default-features = false } +tokio-util = { version = "0.7.12", default-features = false } +tower-http = { version = "0.6.8", default-features = false } +tracing = { version = "0.1.43", default-features = false, features = ["std"] } +tracing-appender = { version = "0.2.1", default-features = false } +tracing-futures = { version = "0.2.4", default-features = false, features = ["futures-03", "std", "std-future"] } +tracing-subscriber = { version = "0.3.22", default-features = false, features = ["std"] } +urlencoding = { version = "2.1", default-features = false } +utoipa = { version = "4.2", default-features = false } +uuid = { version = "1.18", default-features = false, features = ["v4", "std"] } +webbrowser = { version = "1", default-features = false } +which = { version = "8", default-features = false, features = ["real-sys"] } +winapi = { version = "0.3.9", default-features = false, features = ["wincred", "std"] } +wiremock = { version = "0.6", default-features = false } +zip = { version = "8", default-features = false, features = ["deflate"] } +serial_test = { version = "3", default-features = false } +sha2 = { version = "0.11", default-features = false } +shell-words = { version = "1", default-features = false, features = ["std"] } +test-case = { version = "3", default-features = false } +url = { version = "2.5.4", default-features = false, features = ["std"] } +opentelemetry = { version = "0.32", default-features = false } +opentelemetry_sdk = { version = "0.32", default-features = false, features = ["metrics"] } +opentelemetry-http = { version = "0.32", default-features = false, features = ["internal-logs", "reqwest"] } +opentelemetry-otlp = { version = "0.32", default-features = false, features = ["http-proto", "internal-logs", "logs", "metrics", "reqwest-client", "trace"] } +opentelemetry-appender-tracing = { version = "0.32", default-features = false, features = ["experimental_span_attributes"] } +opentelemetry-stdout = { version = "0.32", default-features = false, features = ["trace", "metrics", "logs"] } +tracing-opentelemetry = { version = "0.33", default-features = false, features = ["metrics"] } -rayon = "1.12" -tree-sitter = "0.26" -tree-sitter-go = "0.25" -tree-sitter-java = "0.23" -tree-sitter-javascript = "0.25" -tree-sitter-kotlin-ng = "1.1" -tree-sitter-python = "0.25" -tree-sitter-ruby = "0.23" -tree-sitter-rust = "0.24" -tree-sitter-swift = "0.7" -tree-sitter-typescript = "0.23" +rayon = { version = "1.10", default-features = false } +tree-sitter = { version = "0.26", default-features = false, features = ["std"] } +tree-sitter-go = { version = "0.25", default-features = false } +tree-sitter-java = { version = "0.23", default-features = false } +tree-sitter-javascript = { version = "0.25", default-features = false } +tree-sitter-kotlin-ng = { version = "1", default-features = false } +tree-sitter-python = { version = "0.25", default-features = false } +tree-sitter-ruby = { version = "0.23", default-features = false } +tree-sitter-rust = { version = "0.24", default-features = false } +tree-sitter-swift = { version = "0.7", default-features = false } +tree-sitter-typescript = { version = "0.23", default-features = false } + +# llama-cpp-2 doesn't pin the version of its sys crate, so we do it here. it also has breaking changes in patch releases, so we use an exact version pin +llama-cpp-2 = { version = "=0.1.146", default-features = false, features = ["sampler", "mtmd"] } +llama-cpp-sys-2 = { version = "=0.1.146", default-features = false } + +# These are needed because temporal_rs 0.1 (a transitive dep via PCTX) enables unstable features on icu_calendar without pinning the dependency version +# A fix is available in temporal_rs 0.2 but PCTX has not updated +# They are just here to pin the version, and can be removed if PCTX updates temporal_rs +icu_calendar = { version = "=2.1.1", default-features = false } +icu_locale = { version = "=2.1.1", default-features = false } [patch.crates-io] v8 = { path = "vendor/v8" } cudaforge = { git = "https://github.com/jbg/cudaforge", rev = "e7c1967340e40673db98dc9e17da0f04834a456f" } -# TODO: switch to released version in opentelemetry 0.32.0 -# https://github.com/open-telemetry/opentelemetry-rust/issues/3408 -opentelemetry = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } -opentelemetry_sdk = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } -opentelemetry-appender-tracing = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } -opentelemetry-otlp = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } -opentelemetry-stdout = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } diff --git a/Justfile b/Justfile index f615a25ce3e6..8e8f6e49f354 100644 --- a/Justfile +++ b/Justfile @@ -188,7 +188,7 @@ check-acp-schema: generate-acp-types # Generate ACP JSON schema from Rust types generate-acp-schema: @echo "Generating ACP schema..." - cd crates/goose && cargo run --bin generate-acp-schema + cd crates/goose && cargo run --features code-mode,local-inference,aws-providers,telemetry,otel,rustls-tls,system-keyring --bin generate-acp-schema @echo "ACP schema generated: crates/goose/acp-schema.json, crates/goose/acp-meta.json" # Generate ACP TypeScript types from JSON schema (requires generate-acp-schema first) diff --git a/crates/goose-acp-macros/Cargo.toml b/crates/goose-acp-macros/Cargo.toml index 982f87638d2b..760397ddcdb2 100644 --- a/crates/goose-acp-macros/Cargo.toml +++ b/crates/goose-acp-macros/Cargo.toml @@ -12,8 +12,8 @@ description.workspace = true proc-macro = true [dependencies] -quote = "1" -syn = { version = "2", features = ["full", "extra-traits"] } +quote = { version = "1.0.40", default-features = false } +syn = { version = "2.0.104", default-features = false, features = ["full", "extra-traits"] } [lints] workspace = true diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 479957527031..7dadbd32c8a3 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -20,15 +20,15 @@ name = "generate_manpages" path = "src/bin/generate_manpages.rs" [dependencies] -clap_mangen = "0.3.0" +clap_mangen = { version = "0.2", default-features = false } goose = { path = "../goose", default-features = false } -goose-mcp = { path = "../goose-mcp" } +goose-mcp = { path = "../goose-mcp", default-features = false } rmcp = { workspace = true } clap = { workspace = true } -cliclack = "0.5.4" -console = "0.16.1" +cliclack = { version = "0.5", default-features = false } +console = { version = "0.16", default-features = false, features = ["std"] } dotenvy = { workspace = true } -bat = { version = "0.26.1", default-features = false, features = ["regex-onig"] } +bat = { version = "0.26", default-features = false, features = ["regex-onig"] } anyhow = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } @@ -39,44 +39,48 @@ strum = { workspace = true } tempfile = { workspace = true } etcetera = { workspace = true } rand = { workspace = true } -rustyline = "18.0.0" +rustyline = { version = "18", default-features = false, features = ["custom-bindings", "with-dirs", "with-file-history"] } tracing = { workspace = true } chrono = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "time"] } -shlex = "2.0.1" +shlex = { version = "1.3", default-features = false, features = ["std"] } async-trait = { workspace = true } base64 = { workspace = true } regex = { workspace = true } -tar = "0.4.46" -reqwest = { workspace = true, features = ["blocking"], default-features = false } +tar = { version = "0.4.46", default-features = false } +reqwest = { workspace = true, features = ["blocking"] } zip = { workspace = true } -bzip2 = "0.6" +bzip2 = { version = "0.6", default-features = false, features = ["default"] } webbrowser = { workspace = true } -indicatif = "0.18.1" +indicatif = { version = "0.18", default-features = false } tokio-util = { workspace = true, features = ["compat", "rt"] } -anstream = "1.0.0" -open = "5.3.5" +anstream = { version = "1", default-features = false, features = ["auto"] } +open = { version = "5", default-features = false } url = { workspace = true } urlencoding = { workspace = true } -clap_complete = "4.6.5" -comfy-table = "7.2.2" +clap_complete = { version = "4", default-features = false } +comfy-table = { version = "7", default-features = false } sha2 = { workspace = true } -sigstore-verify = { version = "=0.8.0", default-features = false } +sigstore-verify = { version = "0.8", default-features = false, optional = true } axum.workspace = true -clap_complete_nushell = "4.6.0" +clap_complete_nushell = { version = "4", default-features = false } [target.'cfg(target_os = "windows")'.dependencies] +anstream = { version = "1", default-features = false, features = ["wincon"] } winapi = { workspace = true } [features] default = [ "code-mode", "local-inference", + "tui", "aws-providers", "telemetry", + "nostr", "otel", "rustls-tls", "system-keyring", + "update", ] code-mode = ["goose/code-mode"] local-inference = ["goose/local-inference"] @@ -84,25 +88,29 @@ aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] vulkan = ["goose/vulkan", "local-inference"] telemetry = ["goose/telemetry"] +nostr = ["goose/nostr"] otel = ["goose/otel"] system-keyring = ["goose/system-keyring"] -portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] +tui = [] +update = ["dep:sigstore-verify"] +portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel", "tui"] # disables the update command disable-update = [] rustls-tls = [ "reqwest/rustls", - "sigstore-verify/rustls", + "sigstore-verify?/rustls", "goose/rustls-tls", "goose-mcp/rustls-tls", ] native-tls = [ "reqwest/native-tls", - "sigstore-verify/native-tls", + "sigstore-verify?/native-tls", "goose/native-tls", "goose-mcp/native-tls", ] [dev-dependencies] +env-lock.workspace = true tempfile = { workspace = true } test-case = { workspace = true } tokio = { workspace = true } diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index abc08a7406a4..aac8b77c3b17 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -29,6 +29,7 @@ use crate::commands::schedule::{ handle_schedule_sessions, }; use crate::commands::session::{handle_session_list, handle_session_remove}; +use crate::commands::skills::handle_skills_list; use crate::recipes::extract_from_cli::extract_recipe_info_from_cli; use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml}; use crate::session::{build_session, SessionBuilderConfig}; @@ -699,6 +700,13 @@ enum PluginCommand { }, } +#[derive(Subcommand)] +enum SkillsCommand { + /// List all skills available to the goose agent + #[command(about = "List all skills available to the goose agent")] + List, +} + #[derive(Subcommand)] enum RecipeCommand { /// Validate a recipe file @@ -910,6 +918,13 @@ enum Command { command: RecipeCommand, }, + /// Skill utilities + #[command(about = "Skill utilities")] + Skills { + #[command(subcommand)] + command: SkillsCommand, + }, + /// Manage plugins #[command(about = "Manage plugins")] Plugin { @@ -935,6 +950,7 @@ enum Command { }, /// Update the goose CLI version + #[cfg(feature = "update")] #[command(about = "Update the goose CLI version")] Update { /// Update to canary version @@ -970,6 +986,7 @@ enum Command { }, /// Launch the goose terminal UI (TUI) + #[cfg(feature = "tui")] #[command( about = "Launch the goose terminal UI", long_about = "Launch the goose terminal UI (the @aaif/goose npm package).\n\ @@ -1269,10 +1286,13 @@ fn get_command_name(command: &Option) -> &'static str { Some(Command::Run { .. }) => "run", Some(Command::Gateway { .. }) => "gateway", Some(Command::Schedule { .. }) => "schedule", + #[cfg(feature = "update")] Some(Command::Update { .. }) => "update", Some(Command::Recipe { .. }) => "recipe", + Some(Command::Skills { .. }) => "skills", Some(Command::Plugin { .. }) => "plugin", Some(Command::Term { .. }) => "term", + #[cfg(feature = "tui")] Some(Command::Tui { .. }) => "tui", #[cfg(feature = "local-inference")] Some(Command::LocalModels { .. }) => "local-models", @@ -1797,6 +1817,12 @@ fn handle_recipe_subcommand(command: RecipeCommand) -> Result<()> { } } +async fn handle_skills_subcommand(command: SkillsCommand) -> Result<()> { + match command { + SkillsCommand::List => handle_skills_list().await, + } +} + async fn handle_term_subcommand(command: TermCommand) -> Result<()> { match command { TermCommand::Init { @@ -1814,7 +1840,7 @@ async fn handle_term_subcommand(command: TermCommand) -> Result<()> { async fn handle_local_models_command(command: LocalModelsCommand) -> Result<()> { use goose::providers::local_inference::hf_models; use goose::providers::local_inference::local_model_registry::{ - get_registry, model_id_from_repo, LocalModelEntry, + get_registry, mmproj_local_path, model_id_from_repo, LocalModelEntry, }; match command { @@ -1851,10 +1877,28 @@ async fn handle_local_models_command(command: LocalModelsCommand) -> Result<()> } LocalModelsCommand::Download { spec } => { println!("Resolving {}...", spec); - let (repo_id, file) = hf_models::resolve_model_spec(&spec).await?; + let (repo_id, resolved) = hf_models::resolve_model_spec_full(&spec).await?; + if resolved.files.len() > 1 { + anyhow::bail!( + "Model '{}' is sharded ({} files) — download it from the desktop UI", + spec, + resolved.files.len() + ); + } + let mmproj = resolved.mmproj; + let file = resolved.files.into_iter().next().unwrap(); let model_id = model_id_from_repo(&repo_id, &file.quantization); let local_path = goose::config::paths::Paths::in_data_dir("models").join(&file.filename); + let mmproj_path = mmproj + .as_ref() + .map(|mmproj| mmproj_local_path(&repo_id, &mmproj.filename)); + let mmproj_source_url = mmproj.as_ref().map(|mmproj| mmproj.download_url.clone()); + let mmproj_size_bytes = mmproj.as_ref().map_or(0, |mmproj| mmproj.size_bytes); + let mut download_files = vec![(file.download_url.clone(), local_path.clone())]; + if let Some(mmproj) = mmproj { + download_files.push((mmproj.download_url, mmproj_path.clone().unwrap())); + } println!( "Downloading {} ({})...", @@ -1879,9 +1923,10 @@ async fn handle_local_models_command(command: LocalModelsCommand) -> Result<()> source_url: file.download_url.clone(), settings: Default::default(), size_bytes: file.size_bytes, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, + mmproj_path, + mmproj_source_url, + mmproj_size_bytes, + mmproj_checked: true, shard_files: vec![], }; @@ -1895,10 +1940,10 @@ async fn handle_local_models_command(command: LocalModelsCommand) -> Result<()> // Download let manager = goose::download_manager::get_download_manager(); manager - .download_model( + .download_model_sharded( format!("{}-model", model_id), - file.download_url, - local_path, + download_files, + file.size_bytes + mmproj_size_bytes, None, ) .await?; @@ -2099,6 +2144,7 @@ pub async fn cli() -> anyhow::Result<()> { } Some(Command::Gateway { command }) => handle_gateway_command(command).await, Some(Command::Schedule { command }) => handle_schedule_command(command).await, + #[cfg(feature = "update")] Some(Command::Update { canary, reconfigure, @@ -2107,8 +2153,10 @@ pub async fn cli() -> anyhow::Result<()> { Ok(()) } Some(Command::Recipe { command }) => handle_recipe_subcommand(command), + Some(Command::Skills { command }) => handle_skills_subcommand(command).await, Some(Command::Plugin { command }) => handle_plugin_subcommand(command), Some(Command::Term { command }) => handle_term_subcommand(command).await, + #[cfg(feature = "tui")] Some(Command::Tui { args }) => crate::commands::tui::handle_tui(args), #[cfg(feature = "local-inference")] Some(Command::LocalModels { command }) => handle_local_models_command(command).await, diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index 76d4d79cbcc8..d687e13e668d 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -8,6 +8,9 @@ pub mod recipe; pub mod review; pub mod schedule; pub mod session; +pub mod skills; pub mod term; +#[cfg(feature = "tui")] pub mod tui; +#[cfg(feature = "update")] pub mod update; diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 9990d376e8cb..01efebf21a82 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -3,8 +3,11 @@ use anyhow::{Context, Result}; use cliclack::{confirm, multiselect, select}; use etcetera::home_dir; +#[cfg(feature = "nostr")] use goose::config::Config; -use goose::session::{generate_diagnostics, nostr_share, Session, SessionManager, SessionType}; +#[cfg(feature = "nostr")] +use goose::session::nostr_share; +use goose::session::{generate_diagnostics, Session, SessionManager, SessionType}; use goose::utils::safe_truncate; use regex::Regex; use std::fs; @@ -218,7 +221,7 @@ pub async fn handle_session_export( output_path: Option, format: String, nostr: bool, - relays: Vec, + #[cfg_attr(not(feature = "nostr"), allow(unused_variables))] relays: Vec, ) -> Result<()> { let session_manager = SessionManager::instance(); let session = match session_manager.get_session(&session_id, true).await { @@ -244,6 +247,7 @@ pub async fn handle_session_export( _ => return Err(anyhow::anyhow!("Unsupported format: {}", format)), }; + #[cfg(feature = "nostr")] if nostr { if format != "json" { return Err(anyhow::anyhow!( @@ -266,6 +270,10 @@ pub async fn handle_session_export( println!("{}", share.deeplink); return Ok(()); } + #[cfg(not(feature = "nostr"))] + if nostr { + return Err(anyhow::anyhow!("goose was not built with nostr support")); + } if let Some(output_path) = output_path { fs::write(&output_path, output).with_context(|| { @@ -281,7 +289,12 @@ pub async fn handle_session_export( pub async fn handle_session_import(input: String, nostr: bool) -> Result<()> { let json = if nostr || input.starts_with("goose://sessions/nostr") { - nostr_share::import_session_json_from_deeplink(&input).await? + #[cfg(feature = "nostr")] + { + nostr_share::import_session_json_from_deeplink(&input).await? + } + #[cfg(not(feature = "nostr"))] + return Err(anyhow::anyhow!("goose was not built with nostr support")); } else { fs::read_to_string(&input) .with_context(|| format!("Failed to read session import file: {input}"))? diff --git a/crates/goose-cli/src/commands/skills.rs b/crates/goose-cli/src/commands/skills.rs new file mode 100644 index 000000000000..59737c13ff18 --- /dev/null +++ b/crates/goose-cli/src/commands/skills.rs @@ -0,0 +1,228 @@ +use anyhow::Result; +use console::{measure_text_width, Term}; +use goose::skills::list_installed_skills; +use goose::token_counter::create_token_counter; + +const DESCRIPTION_PREVIEW_CHARS: usize = 50; +const SEPARATOR: &str = " | "; +const MIN_DESCRIPTION_WIDTH: usize = 4; +const MIN_LOCATION_WIDTH: usize = 4; +const NAME_HEADER: &str = "Name"; +const DESCRIPTION_HEADER: &str = "Description"; +const DESCRIPTION_TOKENS_HEADER: &str = "Description tokens"; +const CONTENT_TOKENS_HEADER: &str = "Content tokens"; +const LOCATION_HEADER: &str = "Location"; + +struct SkillRow { + name: String, + description: String, + description_tokens: String, + content_tokens: String, + location: String, +} + +struct ColumnWidths { + name: usize, + description: usize, + description_tokens: usize, + content_tokens: usize, + location: usize, +} + +pub async fn handle_skills_list() -> Result<()> { + let cwd = std::env::current_dir()?; + let terminal_width = terminal_width(); + let token_counter = create_token_counter().await.map_err(anyhow::Error::msg)?; + let mut skills = list_installed_skills(Some(&cwd)); + skills.sort_by(|a, b| a.name.cmp(&b.name)); + + let rows = skills + .iter() + .map(|skill| SkillRow { + name: skill.name.clone(), + description: description_preview(&skill.description), + description_tokens: token_counter.count_tokens(&skill.description).to_string(), + content_tokens: token_counter.count_tokens(&skill.content).to_string(), + location: skill.path.clone(), + }) + .collect::>(); + let widths = column_widths(&rows, terminal_width); + + println!("{}", header_line(&widths, terminal_width)); + for row in rows { + println!("{}", skill_line(&row, &widths, terminal_width)); + } + + Ok(()) +} + +fn terminal_width() -> Option { + Term::stdout() + .size_checked() + .map(|(_height, width)| width as usize) +} + +fn column_widths(rows: &[SkillRow], max_display_width: Option) -> ColumnWidths { + let description_tokens = max_width( + DESCRIPTION_TOKENS_HEADER, + rows.iter().map(|row| row.description_tokens.as_str()), + ); + let content_tokens = max_width( + CONTENT_TOKENS_HEADER, + rows.iter().map(|row| row.content_tokens.as_str()), + ); + let longest_name = max_width(NAME_HEADER, rows.iter().map(|row| row.name.as_str())); + let description = max_width( + DESCRIPTION_HEADER, + rows.iter().map(|row| row.description.as_str()), + ); + let location = max_width( + LOCATION_HEADER, + rows.iter().map(|row| row.location.as_str()), + ); + + let Some(width) = max_display_width else { + return ColumnWidths { + name: longest_name, + description, + description_tokens, + content_tokens, + location, + }; + }; + + let separator_width = measure_text_width(SEPARATOR) * 4; + let available_width = width.saturating_sub(separator_width); + let dynamic_width = available_width.saturating_sub(description_tokens + content_tokens); + + let name = + longest_name.min(dynamic_width.saturating_sub(MIN_DESCRIPTION_WIDTH + MIN_LOCATION_WIDTH)); + let remaining_after_name = dynamic_width.saturating_sub(name); + let description = description.min(remaining_after_name.saturating_sub(MIN_LOCATION_WIDTH)); + let remaining_after_description = remaining_after_name.saturating_sub(description); + let location = location.min(remaining_after_description); + + ColumnWidths { + name, + description, + description_tokens, + content_tokens, + location, + } +} + +fn max_width<'a>(header: &str, values: impl Iterator) -> usize { + values + .map(measure_text_width) + .chain(std::iter::once(measure_text_width(header))) + .max() + .unwrap_or(0) +} + +fn header_line(widths: &ColumnWidths, max_display_width: Option) -> String { + let line = format_line( + NAME_HEADER, + DESCRIPTION_HEADER, + DESCRIPTION_TOKENS_HEADER, + CONTENT_TOKENS_HEADER, + LOCATION_HEADER, + widths, + ); + + match max_display_width { + Some(width) => truncate_to_display_width(&line, width), + None => line, + } +} + +fn skill_line(row: &SkillRow, widths: &ColumnWidths, max_display_width: Option) -> String { + let line = format_line( + &row.name, + &row.description, + &row.description_tokens, + &row.content_tokens, + &row.location, + widths, + ); + + match max_display_width { + Some(width) => truncate_to_display_width(&line, width), + None => line, + } +} + +fn format_line( + name: &str, + description: &str, + description_tokens: &str, + content_tokens: &str, + location: &str, + widths: &ColumnWidths, +) -> String { + format!( + "{}{}{}{}{}{}{}{}{}", + pad_to_display_width(&truncate_to_display_width(name, widths.name), widths.name), + SEPARATOR, + pad_to_display_width( + &truncate_to_display_width(description, widths.description), + widths.description + ), + SEPARATOR, + pad_to_display_width(description_tokens, widths.description_tokens), + SEPARATOR, + pad_to_display_width(content_tokens, widths.content_tokens), + SEPARATOR, + pad_to_display_width( + &truncate_to_display_width(location, widths.location), + widths.location + ), + ) +} + +fn description_preview(description: &str) -> String { + let normalized = description.split_whitespace().collect::>().join(" "); + truncate_to_chars(&normalized, DESCRIPTION_PREVIEW_CHARS) +} + +fn truncate_to_chars(text: &str, max_chars: usize) -> String { + if text.chars().count() <= max_chars { + return text.to_string(); + } + + if max_chars <= 3 { + return ".".repeat(max_chars); + } + + let mut output = text.chars().take(max_chars - 3).collect::(); + output.push_str("..."); + output +} + +fn truncate_to_display_width(text: &str, max_width: usize) -> String { + if measure_text_width(text) <= max_width { + return text.to_string(); + } + + if max_width <= 3 { + return ".".repeat(max_width); + } + + let mut output = String::new(); + let suffix_width = measure_text_width("..."); + + for ch in text.chars() { + output.push(ch); + if measure_text_width(&output) + suffix_width > max_width { + output.pop(); + break; + } + } + + output.push_str("..."); + output +} + +fn pad_to_display_width(text: &str, width: usize) -> String { + let padding = width.saturating_sub(measure_text_width(text)); + format!("{}{}", text, " ".repeat(padding)) +} diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index 73a69503b0da..04272d2059ab 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -64,7 +64,7 @@ fn binary_name() -> &'static str { fn sha256_hex(data: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(data); - format!("{:x}", hasher.finalize()) + goose::utils::bytes_to_hex(hasher.finalize()) } #[derive(serde::Deserialize)] diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index f9c3d2bf6795..69bffe1a7d87 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -129,7 +129,6 @@ impl GooseCompleter { let skills = list_installed_skills(Some(&cwd)); let skill_names: Vec = skills.iter().map(|s| s.name.clone()).collect(); - // Complete the last letter being typed (e.g. "/skills coding in") let last = line.rsplit_once(' ').map_or("", |(_, w)| w); let pos = line.len() - last.len(); @@ -146,6 +145,11 @@ impl GooseCompleter { Ok((pos, candidates)) } + /// Complete model names for the /model command. + fn complete_model_names(&self, line: &str) -> Result<(usize, Vec)> { + Ok((line.len(), vec![])) + } + /// Complete slash commands fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec)> { // Define available slash commands @@ -160,6 +164,7 @@ impl GooseCompleter { "/prompts", "/prompt", "/mode", + "/model", "/recipe", "/skills", ]; @@ -396,6 +401,10 @@ impl Completer for GooseCompleter { } } + if line.starts_with("/model") { + return self.complete_model_names(line); + } + if line.starts_with("/mode") { return self.complete_mode_flags(line); } @@ -574,6 +583,20 @@ mod tests { assert_eq!(candidates.len(), 0); } + #[test] + fn test_complete_model_names() { + let cache = create_test_cache(); + let completer = GooseCompleter::new(cache); + + let (pos, candidates) = completer.complete_model_names("/model ").unwrap(); + assert_eq!(pos, "/model ".len()); + assert!(candidates.is_empty()); + + let (pos, candidates) = completer.complete_model_names("/model gpt").unwrap(); + assert_eq!(pos, "/model gpt".len()); + assert!(candidates.is_empty()); + } + #[test] fn test_complete_prompt_names() { let cache = create_test_cache(); diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index ba60ea763d08..729428b29413 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -20,6 +20,7 @@ pub enum InputResult { ListPrompts(Option), PromptCommand(PromptCommandOptions), GooseMode(String), + Model(Option), Plan(PlanCommandOptions), EndPlan, Clear, @@ -196,6 +197,8 @@ fn handle_slash_command(input: &str) -> Option { const CMD_EXTENSION: &str = "/extension "; const CMD_BUILTIN: &str = "/builtin "; const CMD_MODE: &str = "/mode "; + const CMD_MODEL: &str = "/model"; + const CMD_MODEL_WITH_SPACE: &str = "/model "; const CMD_PLAN: &str = "/plan"; const CMD_ENDPLAN: &str = "/endplan"; const CMD_CLEAR: &str = "/clear"; @@ -261,6 +264,19 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => Some(InputResult::GooseMode( s.get(CMD_MODE.len()..).unwrap_or("").to_string(), )), + s if s == CMD_MODEL => Some(InputResult::Model(None)), + s if s.starts_with(CMD_MODEL_WITH_SPACE) => { + let model = s + .get(CMD_MODEL_WITH_SPACE.len()..) + .unwrap_or("") + .trim() + .to_string(); + if model.is_empty() { + Some(InputResult::Model(None)) + } else { + Some(InputResult::Model(Some(model))) + } + } s if s.starts_with(CMD_PLAN) => { parse_plan_command(s.get(CMD_PLAN.len()..).unwrap_or("").trim().to_string()) } @@ -399,6 +415,7 @@ fn print_help() { /prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt /mode - Set the goose mode to use ({modes}) +/model [name] - Show the current model, or switch models for this session while keeping the same provider /plan - Enters 'plan' mode with optional message. Create a plan based on the current messages and asks user if they want to act on it. If user acts on the plan, goose mode is set to 'auto' and returns to 'normal' goose mode. To warm up goose before using '/plan', we recommend setting '/mode approve' & putting appropriate context into goose. @@ -498,6 +515,21 @@ mod tests { panic!("Expected AddBuiltin"); } + // Test model command + assert!(matches!( + handle_slash_command("/model"), + Some(InputResult::Model(None)) + )); + assert!(matches!( + handle_slash_command("/model "), + Some(InputResult::Model(None)) + )); + if let Some(InputResult::Model(Some(model))) = handle_slash_command("/model gpt-4.1") { + assert_eq!(model, "gpt-4.1"); + } else { + panic!("Expected Model"); + } + // Test unknown commands assert!(handle_slash_command("/unknown").is_none()); } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 11713ed9f520..cd7e63b74098 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -608,6 +608,10 @@ impl CliSession { history.save(editor); self.handle_goose_mode(&mode).await?; } + InputResult::Model(model) => { + history.save(editor); + self.handle_model(model.as_deref()).await?; + } InputResult::Plan(options) => { self.handle_plan_mode(options).await?; } @@ -795,6 +799,73 @@ impl CliSession { Ok(()) } + async fn handle_model(&self, model: Option<&str>) -> Result<()> { + let provider = self.agent.provider().await?; + let current_provider_name = provider.get_name().to_string(); + let current_model_config = provider.get_model_config(); + let current_model_name = current_model_config.model_name.clone(); + + if model.is_none() { + output::goose_mode_message(&format!( + "Current session model: '{}' (provider '{}')", + current_model_name, current_provider_name + )); + return Ok(()); + } + + let model_name = model.unwrap_or_default().trim(); + if model_name.is_empty() { + output::render_error("Model name cannot be empty"); + return Ok(()); + } + + if current_provider_name.ends_with("-acp") { + output::render_error( + "Session model switching is not supported for ACP providers in the CLI.", + ); + return Ok(()); + } + + if provider.manages_own_context() { + output::render_error(&format!( + "Session model switching is not supported for provider '{}' because it manages its own conversation context.", + current_provider_name + )); + return Ok(()); + } + + let new_model_config = + build_switched_model_config(¤t_provider_name, model_name, ¤t_model_config)?; + + if new_model_config.model_name == current_model_config.model_name + && new_model_config.thinking_effort() == current_model_config.thinking_effort() + { + output::goose_mode_message(&format!( + "Session already using model '{}' for provider '{}'", + current_model_name, current_provider_name + )); + return Ok(()); + } + + let extensions = self.agent.get_extension_configs().await; + let new_provider = + goose::providers::create(¤t_provider_name, new_model_config, extensions) + .await + .map_err(|e| anyhow::anyhow!("Failed to create provider: {e}"))?; + + self.agent + .update_provider(new_provider, &self.session_id) + .await?; + + let mode = self.agent.goose_mode().await; + self.agent.update_goose_mode(mode, &self.session_id).await?; + output::goose_mode_message(&format!( + "Session model switched from '{}' to '{}' for provider '{}'", + current_model_name, model_name, current_provider_name + )); + Ok(()) + } + async fn handle_plan_mode(&mut self, options: input::PlanCommandOptions) -> Result<()> { self.run_mode = RunMode::Plan; output::render_enter_plan_mode(); @@ -2091,11 +2162,28 @@ fn format_elapsed_time(duration: std::time::Duration) -> String { } } +fn build_switched_model_config( + provider_name: &str, + model_name: &str, + current_model_config: &goose::model::ModelConfig, +) -> Result { + goose::model::ModelConfig::new(model_name) + .map(|config| { + config + .with_canonical_limits(provider_name) + .with_temperature(current_model_config.temperature) + .with_toolshim(current_model_config.toolshim) + .with_toolshim_model(current_model_config.toolshim_model.clone()) + }) + .map_err(|e| anyhow::anyhow!("Failed to create model configuration: {e}")) +} + #[cfg(test)] mod tests { use super::*; use goose::agents::extension::Envs; use goose::config::ExtensionConfig; + use std::collections::HashMap; use std::time::Duration; use test_case::test_case; @@ -2219,6 +2307,74 @@ mod tests { assert!(CliSession::parse_stdio_extension("").is_err()); } + #[test] + fn test_build_switched_model_config_rebuilds_target_model_settings() { + let _guard = env_lock::lock_env([ + ("GOOSE_MAX_TOKENS", None::<&str>), + ("GOOSE_TEMPERATURE", None::<&str>), + ("GOOSE_CONTEXT_LIMIT", None::<&str>), + ("GOOSE_TOOLSHIM", None::<&str>), + ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), + ]); + + let current_model_config = goose::model::ModelConfig { + model_name: "gpt-4o".to_string(), + context_limit: Some(128_000), + temperature: Some(0.25), + max_tokens: Some(16_384), + toolshim: true, + toolshim_model: Some("qwen2.5-coder".to_string()), + fast_model_config: None, + request_params: Some(HashMap::from([( + "anthropic_beta".to_string(), + serde_json::json!(["output-128k-2025-02-19"]), + )])), + reasoning: Some(false), + }; + + let switched = + build_switched_model_config("openai", "gpt-5.4", ¤t_model_config).unwrap(); + let expected = goose::model::ModelConfig::new_or_fail("gpt-5.4") + .with_canonical_limits("openai") + .with_temperature(Some(0.25)) + .with_toolshim(true) + .with_toolshim_model(Some("qwen2.5-coder".to_string())); + + assert_eq!(switched.model_name, expected.model_name); + assert_eq!(switched.context_limit, expected.context_limit); + assert_eq!(switched.max_tokens, expected.max_tokens); + assert_eq!(switched.request_params, expected.request_params); + assert_eq!(switched.reasoning, expected.reasoning); + assert_eq!(switched.temperature, Some(0.25)); + assert!(switched.toolshim); + assert_eq!(switched.toolshim_model.as_deref(), Some("qwen2.5-coder")); + } + + #[test] + fn test_build_switched_model_config_detects_effort_suffix_change() { + let _guard = env_lock::lock_env([ + ("GOOSE_MAX_TOKENS", None::<&str>), + ("GOOSE_TEMPERATURE", None::<&str>), + ("GOOSE_CONTEXT_LIMIT", None::<&str>), + ("GOOSE_TOOLSHIM", None::<&str>), + ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), + ("GOOSE_THINKING_EFFORT", None::<&str>), + ]); + + let current = + goose::model::ModelConfig::new_or_fail("gpt-5.4-high").with_canonical_limits("openai"); + assert_eq!(current.model_name, "gpt-5.4"); + assert_eq!( + current.thinking_effort(), + Some(goose::model::ThinkingEffort::High) + ); + + let switched = build_switched_model_config("openai", "gpt-5.4", ¤t).unwrap(); + + assert_eq!(switched.model_name, current.model_name); + assert_ne!(switched.thinking_effort(), current.thinking_effort()); + } + #[test] fn test_split_command_args_windows_paths() { assert_eq!( diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index f028f094b2ac..610e9986b68c 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -12,6 +12,7 @@ description.workspace = true workspace = true [features] +default = [] rustls-tls = ["reqwest/rustls"] native-tls = ["reqwest/native-tls"] @@ -28,15 +29,15 @@ serde = { workspace = true } serde_json = { workspace = true } schemars = { workspace = true } indoc = { workspace = true } -reqwest = { workspace = true, features = ["json", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "system-proxy"] } chrono = { workspace = true } etcetera = { workspace = true } tempfile = { workspace = true } include_dir = { workspace = true } once_cell = { workspace = true } -lopdf = "0.40.0" -docx-rs = "0.4.20" -image = { version = "0.24.9", features = ["jpeg"] } -umya-spreadsheet = "2.2.3" +lopdf = { version = "0.40", default-features = false } +docx-rs = { version = "0.4.18", default-features = false, features = ["image"] } +image = { version = "0.24.4", default-features = false, features = ["bmp", "dds", "dxt", "farbfeld", "gif", "hdr", "ico", "jpeg", "jpeg_rayon", "openexr", "png", "pnm", "tga", "tiff", "webp"] } +umya-spreadsheet = { version = "2", default-features = false } shell-words = { workspace = true } -process-wrap = { version = "9.1.0", features = ["std"] } +process-wrap = { version = "9", default-features = false, features = ["std"] } diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 991e5f475bce..c1f04fc6dddf 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -17,6 +17,7 @@ default = [ "local-inference", "aws-providers", "telemetry", + "nostr", "otel", "rustls-tls", "system-keyring", @@ -27,6 +28,7 @@ aws-providers = ["goose/aws-providers"] cuda = ["goose/cuda", "local-inference"] vulkan = ["goose/vulkan", "local-inference"] telemetry = ["goose/telemetry"] +nostr = ["goose/nostr"] otel = ["goose/otel"] system-keyring = ["goose/system-keyring"] portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] @@ -50,7 +52,7 @@ native-tls = [ [dependencies] goose = { path = "../goose", default-features = false } -goose-mcp = { path = "../goose-mcp" } +goose-mcp = { path = "../goose-mcp", default-features = false } rmcp = { workspace = true } axum = { workspace = true, features = ["ws", "macros"] } tokio = { workspace = true } @@ -66,31 +68,31 @@ anyhow = { workspace = true } bytes = { workspace = true } http = { workspace = true } base64 = { workspace = true } -config = { version = "0.15.23", features = ["toml"] } +config = { version = "0.15", default-features = false, features = ["toml"] } thiserror = { workspace = true } clap = { workspace = true } serde_yaml = { workspace = true } utoipa = { workspace = true, features = ["axum_extras", "chrono"] } -reqwest = { workspace = true, features = ["json", "blocking", "multipart", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "blocking", "multipart", "system-proxy"] } tokio-util = { workspace = true } -serde_path_to_error = "0.1.20" -tokio-tungstenite = { version = "0.29.0" } +serde_path_to_error = { version = "0.1.8", default-features = false } +tokio-tungstenite = { version = "0.29", default-features = false, features = ["connect"] } url = { workspace = true } rand = { workspace = true } -hex = "0.4.3" -subtle = "2.6" -socket2 = "0.6.1" +hex = { version = "0.4.3", default-features = false, features = ["std"] } +subtle = { version = "2.5", default-features = false, features = ["std"] } +socket2 = { version = "0.6", default-features = false } fs2 = { workspace = true } -rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true } +rustls = { workspace = true, optional = true } uuid = { workspace = true } -rcgen = "0.14" -axum-server = { version = "0.8.0" } -aws-lc-rs = { version = "1.17.0", optional = true } -openssl = { version = "0.10", optional = true } -pem = "3.0.6" +rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] } +axum-server = { version = "0.8", default-features = false } +aws-lc-rs = { version = "1.17", default-features = false, optional = true } +openssl = { version = "0.10.66", default-features = false, optional = true } +pem = { version = "3.0.2", default-features = false, features = ["std"] } [target.'cfg(windows)'.dependencies] -winreg = { version = "0.56.0" } +winreg = { version = "0.56", default-features = false } [[bin]] name = "goosed" @@ -101,7 +103,7 @@ name = "generate_schema" path = "src/bin/generate_schema.rs" [dev-dependencies] -tower = "0.5.2" +tower = { version = "0.5.2", default-features = false } [package.metadata.cargo-machete] ignored = [ diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 79d2301b87a1..afff2d7c93af 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -392,9 +392,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::upsert_config, super::routes::config_management::remove_config, super::routes::config_management::read_config, - super::routes::config_management::add_extension, - super::routes::config_management::remove_extension, - super::routes::config_management::get_extensions, super::routes::config_management::read_all_config, super::routes::config_management::providers, super::routes::config_management::get_provider_models, @@ -428,8 +425,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::agent::export_app, super::routes::agent::import_app, super::routes::agent::update_from_session, - super::routes::agent::agent_add_extension, - super::routes::agent::agent_remove_extension, super::routes::agent::update_agent_provider, super::routes::agent::update_session, super::routes::action_required::confirm_tool_action, @@ -449,7 +444,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session::import_session_nostr, super::routes::session::update_session_user_recipe_values, super::routes::session::fork_session, - super::routes::session::get_session_extensions, super::routes::schedule::create_schedule, super::routes::schedule::list_schedules, super::routes::schedule::delete_schedule, @@ -491,8 +485,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::config_management::SlashCommandsResponse, super::routes::config_management::SlashCommand, super::routes::config_management::CommandType, - super::routes::config_management::ExtensionResponse, - super::routes::config_management::ExtensionQuery, super::routes::config_management::ToolPermission, super::routes::config_management::UpsertPermissionsQuery, super::routes::config_management::UpdateCustomProviderRequest, @@ -525,7 +517,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::session::UpdateSessionUserRecipeValuesResponse, super::routes::session::ForkRequest, super::routes::session::ForkResponse, - super::routes::session::SessionExtensionsResponse, Message, MessageContent, MessageMetadata, @@ -644,8 +635,6 @@ derive_utoipa!(IconTheme as IconThemeSchema); super::routes::agent::RestartAgentRequest, super::routes::agent::UpdateWorkingDirRequest, super::routes::agent::UpdateFromSessionRequest, - super::routes::agent::AddExtensionRequest, - super::routes::agent::RemoveExtensionRequest, super::routes::agent::ResumeAgentResponse, super::routes::agent::RestartAgentResponse, goose::agents::ExtensionLoadResult, @@ -683,6 +672,7 @@ pub struct ApiDoc; super::routes::local_inference::list_local_models, super::routes::local_inference::sync_featured_models, super::routes::local_inference::search_hf_models, + super::routes::local_inference::list_builtin_chat_templates, super::routes::local_inference::get_repo_files, super::routes::local_inference::download_hf_model, super::routes::local_inference::get_local_model_download_progress, @@ -701,7 +691,9 @@ pub struct ApiDoc; goose::providers::local_inference::hf_models::HfQuantVariant, super::routes::local_inference::RepoVariantsResponse, goose::providers::local_inference::local_model_registry::ModelSettings, + goose::providers::local_inference::local_model_registry::ChatTemplate, goose::providers::local_inference::local_model_registry::SamplingConfig, + goose::providers::local_inference::local_model_registry::ToolCallingMode, )) )] pub struct LocalInferenceApiDoc; diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 1b9f83913b76..be5793f1bbb4 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -99,18 +99,6 @@ pub struct ResumeAgentRequest { load_model_and_extensions: bool, } -#[derive(Deserialize, utoipa::ToSchema)] -pub struct AddExtensionRequest { - session_id: String, - config: ExtensionConfig, -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct RemoveExtensionRequest { - name: String, - session_id: String, -} - #[derive(Deserialize, utoipa::ToSchema)] pub struct SetContainerRequest { session_id: String, @@ -693,70 +681,6 @@ async fn update_session( Ok(()) } -#[utoipa::path( - post, - path = "/agent/add_extension", - request_body = AddExtensionRequest, - responses( - (status = 200, description = "Extension added", body = String), - (status = 401, description = "Unauthorized - invalid secret key"), - (status = 424, description = "Agent not initialized"), - (status = 500, description = "Internal server error") - ) -)] -async fn agent_add_extension( - State(state): State>, - Json(request): Json, -) -> Result { - let extension_name = request.config.name(); - let agent = state.get_agent(request.session_id.clone()).await?; - - agent - .add_extension(request.config, &request.session_id) - .await - .map_err(|e| { - #[cfg(feature = "telemetry")] - goose::posthog::emit_error( - "extension_add_failed", - &format!("{}: {}", extension_name, e), - ); - ErrorResponse::internal(format!("Failed to add extension: {}", e)) - })?; - - Ok(StatusCode::OK) -} - -#[utoipa::path( - post, - path = "/agent/remove_extension", - request_body = RemoveExtensionRequest, - responses( - (status = 200, description = "Extension removed", body = String), - (status = 401, description = "Unauthorized - invalid secret key"), - (status = 424, description = "Agent not initialized"), - (status = 500, description = "Internal server error") - ) -)] -async fn agent_remove_extension( - State(state): State>, - Json(request): Json, -) -> Result { - let agent = state.get_agent(request.session_id.clone()).await?; - - agent - .remove_extension(&request.name, &request.session_id) - .await - .map_err(|e| { - error!("Failed to remove extension: {}", e); - ErrorResponse { - message: format!("Failed to remove extension: {}", e), - status: StatusCode::INTERNAL_SERVER_ERROR, - } - })?; - - Ok(StatusCode::OK) -} - #[utoipa::path( post, path = "/agent/set_container", @@ -1356,8 +1280,6 @@ pub fn routes(state: Arc) -> Router { .route("/agent/update_provider", post(update_agent_provider)) .route("/agent/update_session", post(update_session)) .route("/agent/update_from_session", post(update_from_session)) - .route("/agent/add_extension", post(agent_add_extension)) - .route("/agent/remove_extension", post(agent_remove_extension)) .route("/agent/set_container", post(set_container)) .route("/agent/stop", post(stop_agent)) .with_state(state) @@ -1406,15 +1328,11 @@ mod tests { .await .unwrap(); - agent_add_extension( - State(state.clone()), - Json(AddExtensionRequest { - session_id: session.id.clone(), - config: frontend_extension(), - }), - ) - .await - .unwrap(); + let agent = state.get_agent(session.id.clone()).await.unwrap(); + agent + .add_extension(frontend_extension(), &session.id) + .await + .unwrap(); let Json(tools) = get_tools( State(state.clone()), diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 69ed487d6445..a31f079b5f5f 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -9,7 +9,6 @@ use axum::{ }; use goose::config::declarative_providers::LoadedProvider; use goose::config::paths::Paths; -use goose::config::ExtensionEntry; use goose::config::{Config, ConfigError}; use goose::custom_requests::SourceType; use goose::model::ModelConfig; @@ -22,7 +21,7 @@ use goose::providers::catalog::{ use goose::providers::create_with_default_model; use goose::providers::providers as get_providers; use goose::{ - agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel, + agents::execute_commands, config::permission::PermissionLevel, slash_commands::recipe_slash_command, }; use serde::{Deserialize, Serialize}; @@ -31,20 +30,6 @@ use serde_yaml; use std::{collections::HashMap, sync::Arc}; use utoipa::ToSchema; -#[derive(Serialize, ToSchema)] -pub struct ExtensionResponse { - pub extensions: Vec, - #[serde(default)] - pub warnings: Vec, -} - -#[derive(Deserialize, ToSchema)] -pub struct ExtensionQuery { - pub name: String, - pub config: ExtensionConfig, - pub enabled: bool, -} - #[derive(Deserialize, ToSchema)] pub struct UpsertConfigQuery { pub key: String, @@ -299,72 +284,6 @@ pub async fn read_config( Ok(Json(response_value)) } -#[utoipa::path( - get, - path = "/config/extensions", - responses( - (status = 200, description = "All extensions retrieved successfully", body = ExtensionResponse), - (status = 500, description = "Internal server error") - ) -)] -pub async fn get_extensions() -> Result, ErrorResponse> { - let extensions = goose::config::get_all_extensions() - .into_iter() - .filter(|ext| !goose::agents::extension_manager::is_hidden_extension(&ext.config.name())) - .collect(); - let warnings = goose::config::get_warnings(); - Ok(Json(ExtensionResponse { - extensions, - warnings, - })) -} - -#[utoipa::path( - post, - path = "/config/extensions", - request_body = ExtensionQuery, - responses( - (status = 200, description = "Extension added or updated successfully", body = String), - (status = 400, description = "Invalid request"), - (status = 422, description = "Could not serialize config.yaml"), - (status = 500, description = "Internal server error") - ) -)] -pub async fn add_extension( - Json(extension_query): Json, -) -> Result, ErrorResponse> { - let extensions = goose::config::get_all_extensions(); - let key = goose::config::extensions::name_to_key(&extension_query.name); - - let is_update = extensions.iter().any(|e| e.config.key() == key); - - goose::config::set_extension(ExtensionEntry { - enabled: extension_query.enabled, - config: extension_query.config, - }); - - if is_update { - Ok(Json(format!("Updated extension {}", extension_query.name))) - } else { - Ok(Json(format!("Added extension {}", extension_query.name))) - } -} - -#[utoipa::path( - delete, - path = "/config/extensions/{name}", - responses( - (status = 200, description = "Extension removed successfully", body = String), - (status = 404, description = "Extension not found"), - (status = 500, description = "Internal server error") - ) -)] -pub async fn remove_extension(Path(name): Path) -> Result, ErrorResponse> { - let key = goose::config::extensions::name_to_key(&name); - goose::config::remove_extension(&key); - Ok(Json(format!("Removed extension {}", name))) -} - #[utoipa::path( get, path = "/config", @@ -989,9 +908,6 @@ pub fn routes(state: Arc) -> Router { .route("/config/upsert", post(upsert_config)) .route("/config/remove", post(remove_config)) .route("/config/read", post(read_config)) - .route("/config/extensions", get(get_extensions)) - .route("/config/extensions", post(add_extension)) - .route("/config/extensions/{name}", delete(remove_extension)) .route("/config/providers", get(providers)) .route("/config/providers/{name}/models", get(get_provider_models)) .route( diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs index b7a7dfbde6ab..8ee9bc4eebcf 100644 --- a/crates/goose-server/src/routes/local_inference.rs +++ b/crates/goose-server/src/routes/local_inference.rs @@ -13,10 +13,10 @@ use goose::config::paths::Paths; use goose::download_manager::{get_download_manager, DownloadProgress}; use goose::providers::local_inference::hf_models::{self, HfModelInfo, HfQuantVariant}; use goose::providers::local_inference::{ - available_inference_memory_bytes, - hf_models::{resolve_model_spec, resolve_model_spec_full, HfGgufFile}, + available_inference_memory_bytes, builtin_chat_template_names, + hf_models::{resolve_model_spec_full, HfGgufFile}, local_model_registry::{ - default_settings_for_model, featured_mmproj_spec, get_registry, is_featured_model, + default_settings_for_model, get_registry, is_featured_model, mmproj_local_path, model_id_from_repo, LocalModelEntry, ModelDownloadStatus as RegistryDownloadStatus, ModelSettings, ShardFile, FEATURED_MODELS, }, @@ -79,26 +79,18 @@ async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { .lock() .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; if let Some(existing) = registry.get_model(&model_id) { - let needs_backfill = existing.mmproj_path.is_none() && featured.mmproj.is_some(); - let needs_download = existing.is_downloaded() - && featured.mmproj.is_some() - && !existing.mmproj_path.as_ref().is_some_and(|p| p.exists()); - - if needs_download { - if let Some(mmproj) = featured.mmproj.as_ref() { - let path = mmproj.local_path(); - let url = format!( - "https://huggingface.co/{}/resolve/main/{}", - mmproj.repo, mmproj.filename - ); - mmproj_downloads_needed.push((model_id.clone(), url, path)); + if let Some(path) = &existing.mmproj_path { + if existing.is_downloaded() && !path.exists() { + if let Some(url) = &existing.mmproj_source_url { + mmproj_downloads_needed.push(( + model_id.clone(), + url.clone(), + path.clone(), + )); + } } - } - - if !needs_backfill { continue; } - // Fall through to resolve for backfill } } @@ -110,36 +102,45 @@ async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { }); } - let resolved: Vec<(PendingResolve, HfGgufFile)> = + let resolved: Vec<(PendingResolve, HfGgufFile, Option)> = join_all(to_resolve.into_iter().map(|pending| async move { - let hf_file = match resolve_model_spec(pending.spec).await { - Ok((_repo, file)) => file, + let (hf_file, mmproj) = match resolve_model_spec_full(pending.spec).await { + Ok((_repo, resolved)) => (resolved.files[0].clone(), resolved.mmproj), Err(_) => { let filename = format!( "{}-{}.gguf", pending.repo_id.split('/').next_back().unwrap_or("model"), pending.quantization ); - HfGgufFile { - filename: filename.clone(), - size_bytes: 0, - quantization: pending.quantization.to_string(), - download_url: format!( - "https://huggingface.co/{}/resolve/main/{}", - pending.repo_id, filename - ), - } + ( + HfGgufFile { + filename: filename.clone(), + size_bytes: 0, + quantization: pending.quantization.to_string(), + download_url: format!( + "https://huggingface.co/{}/resolve/main/{}", + pending.repo_id, filename + ), + }, + None, + ) } }; - (pending, hf_file) + (pending, hf_file, mmproj) })) .await; let entries_to_add: Vec = resolved .into_iter() - .map(|(pending, hf_file)| { + .map(|(pending, hf_file, mmproj)| { let local_path = Paths::in_data_dir("models").join(&hf_file.filename); let settings = default_settings_for_model(&pending.model_id); + let mmproj_path = mmproj + .as_ref() + .map(|mmproj| mmproj_local_path(&pending.repo_id, &mmproj.filename)); + let mmproj_source_url = mmproj.as_ref().map(|mmproj| mmproj.download_url.clone()); + let mmproj_size_bytes = mmproj.as_ref().map_or(0, |mmproj| mmproj.size_bytes); + let mmproj_checked = mmproj.is_some(); LocalModelEntry { id: pending.model_id, repo_id: pending.repo_id, @@ -149,9 +150,10 @@ async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { source_url: hf_file.download_url, settings, size_bytes: hf_file.size_bytes, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, + mmproj_path, + mmproj_source_url, + mmproj_size_bytes, + mmproj_checked, shard_files: vec![], } }) @@ -165,20 +167,80 @@ async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { if !entries_to_add.is_empty() { registry.sync_with_featured(entries_to_add); } + } + + let to_backfill: Vec<(String, String, String)> = { + let registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + registry + .list_models() + .iter() + .filter(|model| model.is_downloaded()) + .filter(|model| model.mmproj_path.is_none()) + .filter(|model| !model.mmproj_checked) + .map(|model| { + ( + model.id.clone(), + model.repo_id.clone(), + model.quantization.clone(), + ) + }) + .collect() + }; + + let mmproj_backfills: Vec<(String, String, Option>)> = join_all( + to_backfill + .into_iter() + .map(|(id, repo_id, quantization)| async move { + let spec = format!("{repo_id}:{quantization}"); + let mmproj = resolve_model_spec_full(&spec) + .await + .ok() + .map(|(_, resolved)| resolved.mmproj); + (id, repo_id, mmproj) + }), + ) + .await; + + { + let mut registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + for (model_id, repo_id, mmproj_result) in mmproj_backfills { + if let Some(model) = registry + .list_models_mut() + .iter_mut() + .find(|model| model.id == model_id) + { + let Some(mmproj) = mmproj_result else { + continue; + }; + + model.mmproj_checked = true; + if let Some(mmproj) = mmproj { + model.mmproj_path = Some(mmproj_local_path(&repo_id, &mmproj.filename)); + model.mmproj_source_url = Some(mmproj.download_url); + model.mmproj_size_bytes = mmproj.size_bytes; + } + model.refresh_mmproj_metadata(); + } + } - // Backfill mmproj data for all registry models and collect any - // needed mmproj downloads for models already on disk. for model in registry.list_models_mut() { - model.enrich_with_featured_mmproj(); + model.refresh_mmproj_metadata(); if model.is_downloaded() { - if let Some(mmproj) = featured_mmproj_spec(&model.id) { - let path = mmproj.local_path(); + if let Some(path) = &model.mmproj_path { if !path.exists() { - let url = format!( - "https://huggingface.co/{}/resolve/main/{}", - mmproj.repo, mmproj.filename - ); - mmproj_downloads_needed.push((model.id.clone(), url, path)); + if let Some(url) = &model.mmproj_source_url { + mmproj_downloads_needed.push(( + model.id.clone(), + url.clone(), + path.clone(), + )); + } } } } @@ -431,6 +493,20 @@ pub async fn download_hf_model( vec![] }; + let mmproj_path = resolved + .mmproj + .as_ref() + .map(|mmproj| mmproj_local_path(&repo_id, &mmproj.filename)); + let mmproj_source_url = resolved + .mmproj + .as_ref() + .map(|mmproj| mmproj.download_url.clone()); + let mmproj_size_bytes = resolved + .mmproj + .as_ref() + .map_or(0, |mmproj| mmproj.size_bytes); + let mmproj_checked = true; + let entry = LocalModelEntry { id: model_id.clone(), repo_id, @@ -440,13 +516,13 @@ pub async fn download_hf_model( source_url: first_file.download_url.clone(), settings: default_settings_for_model(&model_id), size_bytes: resolved.total_size, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, + mmproj_path, + mmproj_source_url, + mmproj_size_bytes, + mmproj_checked, shard_files: shard_files.clone(), }; - // add_model enriches the entry with mmproj metadata from the featured table let mmproj_path = { let mut registry = get_registry() .lock() @@ -649,6 +725,17 @@ pub async fn update_model_settings( Ok(Json(settings)) } +#[utoipa::path( + get, + path = "/local-inference/chat-templates/builtin", + responses( + (status = 200, description = "llama.cpp built-in chat template names", body = Vec) + ) +)] +pub async fn list_builtin_chat_templates() -> Json> { + Json(builtin_chat_template_names()) +} + pub fn routes(state: Arc) -> Router { let registered_paths: std::collections::HashSet = get_registry() .lock() @@ -672,6 +759,10 @@ pub fn routes(state: Arc) -> Router { .route("/local-inference/models", get(list_local_models)) .route("/local-inference/sync-featured", post(sync_featured_models)) .route("/local-inference/search", get(search_hf_models)) + .route( + "/local-inference/chat-templates/builtin", + get(list_builtin_chat_templates), + ) .route( "/local-inference/repo/{author}/{repo}/files", get(get_repo_files), diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 5b0d89a88abe..408e800591b8 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -9,11 +9,11 @@ use axum::{ routing::{delete, get, put}, Json, Router, }; -use goose::agents::ExtensionConfig; use goose::recipe::Recipe; +#[cfg(feature = "nostr")] use goose::session::nostr_share; use goose::session::session_manager::{SessionInsights, SessionType}; -use goose::session::{EnabledExtensionsState, Session}; +use goose::session::Session; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -51,6 +51,7 @@ pub struct ImportSessionRequest { json: String, } +#[cfg_attr(not(feature = "nostr"), allow(dead_code))] #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ShareSessionNostrRequest { @@ -67,6 +68,7 @@ pub struct ShareSessionNostrResponse { relays: Vec, } +#[cfg_attr(not(feature = "nostr"), allow(dead_code))] #[derive(Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ImportSessionNostrRequest { @@ -387,6 +389,7 @@ async fn import_session( Ok(Json(session)) } +#[cfg_attr(not(feature = "nostr"), allow(unused_variables))] #[utoipa::path( post, path = "/sessions/{session_id}/share/nostr", @@ -410,25 +413,32 @@ async fn share_session_nostr( Path(session_id): Path, Json(request): Json, ) -> Result, StatusCode> { - let exported = state - .session_manager() - .export_session(&session_id) - .await - .map_err(|_| StatusCode::NOT_FOUND)?; + #[cfg(feature = "nostr")] + { + let exported = state + .session_manager() + .export_session(&session_id) + .await + .map_err(|_| StatusCode::NOT_FOUND)?; - let relays = nostr_share::resolve_relays(request.relays, goose::config::Config::global()); - let share = nostr_share::publish_session_json(&exported, relays) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let relays = nostr_share::resolve_relays(request.relays, goose::config::Config::global()); + let share = nostr_share::publish_session_json(&exported, relays) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(ShareSessionNostrResponse { + deeplink: share.deeplink, + nevent: share.nevent, + event_id: share.event_id, + relays: share.relays, + })) + } - Ok(Json(ShareSessionNostrResponse { - deeplink: share.deeplink, - nevent: share.nevent, - event_id: share.event_id, - relays: share.relays, - })) + #[cfg(not(feature = "nostr"))] + Err(StatusCode::NOT_FOUND) } +#[cfg_attr(not(feature = "nostr"), allow(unused_variables))] #[utoipa::path( post, path = "/sessions/import/nostr", @@ -448,16 +458,22 @@ async fn import_session_nostr( State(state): State>, Json(request): Json, ) -> Result, StatusCode> { - let json = nostr_share::import_session_json_from_deeplink(&request.deeplink) - .await - .map_err(|_| StatusCode::BAD_REQUEST)?; - let session = state - .session_manager() - .import_session(&json, Some(SessionType::User)) - .await - .map_err(|_| StatusCode::BAD_REQUEST)?; + #[cfg(feature = "nostr")] + { + let json = nostr_share::import_session_json_from_deeplink(&request.deeplink) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + let session = state + .session_manager() + .import_session(&json, Some(SessionType::User)) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; - Ok(Json(session)) + Ok(Json(session)) + } + + #[cfg(not(feature = "nostr"))] + Err(StatusCode::NOT_FOUND) } #[utoipa::path( @@ -553,47 +569,6 @@ async fn fork_session( })) } -#[derive(Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct SessionExtensionsResponse { - extensions: Vec, -} - -#[utoipa::path( - get, - path = "/sessions/{session_id}/extensions", - params( - ("session_id" = String, Path, description = "Unique identifier for the session") - ), - responses( - (status = 200, description = "Session extensions retrieved successfully", body = SessionExtensionsResponse), - (status = 401, description = "Unauthorized - Invalid or missing API key"), - (status = 404, description = "Session not found"), - (status = 500, description = "Internal server error") - ), - security( - ("api_key" = []) - ), - tag = "Session Management" -)] -async fn get_session_extensions( - State(state): State>, - Path(session_id): Path, -) -> Result, StatusCode> { - let session = state - .session_manager() - .get_session(&session_id, false) - .await - .map_err(|_| StatusCode::NOT_FOUND)?; - - let extensions = EnabledExtensionsState::extensions_or_default( - Some(&session.extension_data), - goose::config::Config::global(), - ); - - Ok(Json(SessionExtensionsResponse { extensions })) -} - pub fn routes(state: Arc) -> Router { Router::new() .route("/sessions", get(list_sessions)) @@ -620,10 +595,6 @@ pub fn routes(state: Arc) -> Router { put(update_session_user_recipe_values), ) .route("/sessions/{session_id}/fork", post(fork_session)) - .route( - "/sessions/{session_id}/extensions", - get(get_session_extensions), - ) .with_state(state) } #[derive(Deserialize, ToSchema)] @@ -685,9 +656,9 @@ async fn search_sessions( .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); - let search_results = state + let sessions = state .session_manager() - .search_chat_history( + .search_chat_sessions( query, Some(limit), after_date, @@ -698,23 +669,5 @@ async fn search_sessions( .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - // Get full Session objects for matching session IDs - let session_ids: Vec = search_results - .results - .into_iter() - .map(|r| r.session_id) - .collect(); - - let all_sessions = state - .session_manager() - .list_sessions() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let matching_sessions: Vec = all_sessions - .into_iter() - .filter(|s| session_ids.contains(&s.id)) - .collect(); - - Ok(Json(matching_sessions)) + Ok(Json(sessions)) } diff --git a/crates/goose-server/src/routes/telemetry.rs b/crates/goose-server/src/routes/telemetry.rs index 697f579cf049..222449ecbf3a 100644 --- a/crates/goose-server/src/routes/telemetry.rs +++ b/crates/goose-server/src/routes/telemetry.rs @@ -8,6 +8,7 @@ use utoipa::ToSchema; use crate::state::AppState; +#[cfg_attr(not(feature = "telemetry"), allow(dead_code))] #[derive(Debug, Deserialize, ToSchema)] pub struct TelemetryEventRequest { pub event_name: String, @@ -15,6 +16,7 @@ pub struct TelemetryEventRequest { pub properties: HashMap, } +#[cfg_attr(not(feature = "telemetry"), allow(unused_variables))] #[utoipa::path( post, path = "/telemetry/event", @@ -27,15 +29,17 @@ async fn send_telemetry_event( State(_state): State>, Json(request): Json, ) -> StatusCode { - let event_name = request.event_name; - let properties = request.properties; - #[cfg(feature = "telemetry")] - tokio::spawn(async move { - if let Err(e) = emit_event(&event_name, properties).await { - tracing::debug!("Failed to send telemetry event: {}", e); - } - }); + { + let event_name = request.event_name; + let properties = request.properties; + + tokio::spawn(async move { + if let Err(e) = emit_event(&event_name, properties).await { + tracing::debug!("Failed to send telemetry event: {}", e); + } + }); + } StatusCode::ACCEPTED } diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index 3be8934adbce..a9dee95f65ce 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -5,7 +5,9 @@ use goose::scheduler_trait::SchedulerTrait; use goose::session::SessionManager; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; +#[cfg(feature = "local-inference")] +use std::sync::OnceLock; use tokio::sync::Mutex; use tokio::task::JoinHandle; diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index e3d5b42efe7d..fccd7dbbc6d8 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -9,15 +9,7 @@ repository.workspace = true description.workspace = true [features] -default = [ - "code-mode", - "local-inference", - "aws-providers", - "telemetry", - "otel", - "rustls-tls", - "system-keyring", -] +default = [] telemetry = [] otel = [ "dep:tracing-opentelemetry", @@ -33,6 +25,7 @@ local-inference = [ "dep:candle-nn", "dep:candle-transformers", "dep:llama-cpp-2", + "dep:llama-cpp-sys-2", "dep:tokenizers", "dep:symphonia", "dep:rubato", @@ -43,6 +36,7 @@ aws-providers = [ "dep:aws-smithy-types", "dep:aws-sdk-bedrockruntime", "dep:aws-sdk-sagemakerruntime", + "dep:smithy-transport-reqwest", ] cuda = ["local-inference", "candle-core/cuda", "candle-nn/cuda", "llama-cpp-2/cuda"] vulkan = ["local-inference", "llama-cpp-2/vulkan"] @@ -50,6 +44,7 @@ rustls-tls = [ "dep:rustls", "reqwest/rustls", "rmcp/reqwest", + "smithy-transport-reqwest?/rustls", "sqlx/runtime-tokio-rustls", "jsonwebtoken/aws_lc_rs", "oauth2/reqwest", @@ -62,6 +57,7 @@ native-tls = [ "dep:sec1", "reqwest/native-tls", "rmcp/reqwest-native-tls", + "smithy-transport-reqwest?/native-tls", "sqlx/runtime-tokio-native-tls", "jsonwebtoken/rust_crypto", "oauth2/reqwest", @@ -69,7 +65,7 @@ native-tls = [ ] system-keyring = ["dep:keyring"] portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] - +nostr = ["dep:nostr", "dep:nostr-sdk"] [lints] workspace = true @@ -89,30 +85,30 @@ anyhow = { workspace = true } thiserror = { workspace = true } futures = { workspace = true } dirs = { workspace = true } -reqwest = { workspace = true, features = ["json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"], default-features = false } +reqwest = { workspace = true, features = ["json", "cookies", "gzip", "brotli", "deflate", "zstd", "charset", "http2", "stream", "blocking", "multipart", "system-proxy"] } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde_urlencoded = "0.7" -jsonschema = "0.30.0" +serde_urlencoded = { version = "0.7.1", default-features = false } +jsonschema = { version = "0.30", default-features = false } uuid = { workspace = true, features = ["v7"] } regex = { workspace = true } async-trait = { workspace = true } async-stream = { workspace = true } -minijinja = { version = "2.20.0", features = ["loader"] } +minijinja = { version = "2.18", default-features = false, features = ["loader", "multi_template", "serde"] } include_dir = { workspace = true } -tiktoken-rs = "0.11.0" +tiktoken-rs = { version = "0.11", default-features = false } chrono = { workspace = true } clap = { workspace = true } indoc = { workspace = true } -nanoid = "0.5" +nanoid = { version = "0.5", default-features = false } sha2 = { workspace = true } base64 = { workspace = true } url = { workspace = true } axum = { workspace = true, features = ["ws"] } webbrowser = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "time"] } +tracing-subscriber = { workspace = true, features = ["ansi", "env-filter", "fmt", "json", "time"] } tracing-futures = { workspace = true } tracing-opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } @@ -120,19 +116,19 @@ opentelemetry_sdk = { workspace = true, optional = true } opentelemetry-appender-tracing = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true } opentelemetry-stdout = { workspace = true, optional = true } -keyring = { version = "3.6.2", features = ["vendored"], optional = true } +keyring = { workspace = true, optional = true } serde_yaml = { workspace = true } strum = { workspace = true } once_cell = { workspace = true } etcetera = { workspace = true } -fs-err = "3" -goose-sdk = { path = "../goose-sdk" } +fs-err = { version = "3.1", default-features = false } +goose-sdk = { path = "../goose-sdk", default-features = false } rand = { workspace = true } utoipa = { workspace = true, features = ["chrono"] } -tokio-cron-scheduler = "0.15.1" +tokio-cron-scheduler = { version = "0.15", default-features = false } urlencoding = { workspace = true } -v_htmlescape = "0.15" -sqlx = { version = "0.8", default-features = false, features = [ +v_htmlescape = { version = "0.17", default-features = false, features = ["std"] } +sqlx = { version = "0.8.5", default-features = false, features = [ "sqlite", "chrono", "json", @@ -141,43 +137,46 @@ sqlx = { version = "0.8", default-features = false, features = [ ] } # For Bedrock provider (optional, behind "aws-providers" feature) -aws-config = { version = "=1.8.16", features = ["behavior-version-latest"], optional = true } -aws-smithy-types = { version = "=1.4.8", optional = true } -aws-sdk-bedrockruntime = { version = "=1.131.0", default-features = false, features = ["default-https-client", "rt-tokio"], optional = true } +aws-config = { version = "1.6", default-features = false, features = ["credentials-process", "rt-tokio", "sso", "behavior-version-latest"], optional = true } +aws-smithy-types = { version = "1.3.4", default-features = false, features = ["rt-tokio"], optional = true } +aws-sdk-bedrockruntime = { version = "1.119", default-features = false, features = ["rt-tokio"], optional = true } +smithy-transport-reqwest = { version = "0.1", default-features = false, features = ["http2", "system-proxy"], optional = true } # For SageMaker TGI provider (optional, behind "aws-providers" feature) -aws-sdk-sagemakerruntime = { version = "1.102.0", default-features = false, features = ["default-https-client", "rt-tokio"], optional = true } +aws-sdk-sagemakerruntime = { version = "1.64", default-features = false, features = ["rt-tokio"], optional = true } # For GCP Vertex AI provider auth -jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem"] } +jsonwebtoken = { version = "10.2", default-features = false, features = ["use_pem"] } -blake3 = "1.8" +blake3 = { version = "1", default-features = false, features = ["std"] } fs2 = { workspace = true } tokio-stream = { workspace = true, features = ["io-util"] } -tempfile.workspace = true -dashmap = "6.2" -ahash = "0.8" +tempfile = { workspace = true } +dashmap = { version = "6", default-features = false } +ahash = { version = "0.8.11", default-features = false, features = ["std"] } tokio-util = { workspace = true, features = ["compat"] } agent-client-protocol-schema = { workspace = true } agent-client-protocol = { workspace = true, features = ["unstable"] } -unicode-normalization = "0.1" +unicode-normalization = { version = "0.1.22", default-features = false, features = ["std"] } # For local Whisper transcription (optional, behind "local-inference" feature) -candle-core = { version = "0.10.2", default-features = false, optional = true } -candle-nn = { version = "0.10.2", default-features = false, optional = true } -candle-transformers = { version = "0.10.2", default-features = false, optional = true } -byteorder = { version = "1.5.0", optional = true } -tokenizers = { version = "0.21.0", default-features = false, features = ["onig"], optional = true } -symphonia = { version = "0.5", features = ["all"], optional = true } -rubato = { version = "0.16", optional = true } +candle-core = { workspace = true, optional = true } +candle-nn = { workspace = true, optional = true } +candle-transformers = { version = "0.10", default-features = false, optional = true } +byteorder = { version = "1.5", default-features = false, features = ["std"], optional = true } +tokenizers = { version = "0.21", default-features = false, features = ["onig"], optional = true } +symphonia = { version = "0.5", default-features = false, features = ["aac", "adpcm", "alac", "isomp4", "mkv", "mp3", "pcm", "vorbis", "wav"], optional = true } +rubato = { version = "0.16", default-features = false, optional = true } zip = { workspace = true } -sys-info = "0.9" +sys-info = { version = "0.9", default-features = false } + +llama-cpp-2 = { workspace = true, optional = true } schemars = { workspace = true, features = [ "derive", ] } shellexpand = { workspace = true } -indexmap = "2.14.0" +indexmap = { version = "2.9", default-features = false, features = ["std"] } ignore = { workspace = true } rayon = { workspace = true } tree-sitter = { workspace = true } @@ -191,59 +190,65 @@ tree-sitter-rust = { workspace = true } tree-sitter-swift = { workspace = true } tree-sitter-typescript = { workspace = true } which = { workspace = true } -pctx_code_mode = { version = "^0.3.0", optional = true } -pulldown-cmark = "0.13.4" -llama-cpp-2 = { version = "0.1.145", features = ["sampler", "mtmd"], optional = true } -encoding_rs = "0.8.35" -pastey = "0.2.3" +pulldown-cmark = { version = "0.13", default-features = false } +encoding_rs = { version = "0.8.35", default-features = false } +pastey = { version = "0.2", default-features = false } shell-words = { workspace = true } -pem = { version = "3", optional = true } -pkcs1 = { version = "0.7", default-features = false, features = ["pkcs8"], optional = true } -pkcs8 = { version = "0.10", default-features = false, features = ["alloc"], optional = true } -sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8"], optional = true } -goose-acp-macros = { path = "../goose-acp-macros" } +pem = { version = "3.0.2", default-features = false, features = ["std"], optional = true } +pkcs1 = { version = "0.7.5", default-features = false, features = ["pkcs8", "std"], optional = true } +pkcs8 = { version = "0.10.2", default-features = false, features = ["alloc", "std"], optional = true } +sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8", "std"], optional = true } +goose-acp-macros = { path = "../goose-acp-macros", default-features = false } tower-http = { workspace = true, features = ["cors"] } -http-body-util = "0.1.3" -tracing-appender.workspace = true -process-wrap = { version = "9.1.0", features = ["std"] } -nostr = { version = "0.44.3", features = ["nip44"] } -nostr-sdk = { version = "0.44.1", features = ["nip44"] } -rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true } +http-body-util = { version = "0.1.2", default-features = false } +tracing-appender = { workspace = true } +process-wrap = { version = "9", default-features = false, features = ["std"] } +nostr = { version = "0.44", default-features = false, features = ["nip44", "std"], optional = true } +nostr-sdk = { version = "0.44", default-features = false, features = ["nip44"], optional = true } +rustls = { workspace = true, optional = true } +pctx_code_mode = { version = "0.3", default-features = false, optional = true } + +# These are needed because temporal_rs 0.1 (a transitive dep via PCTX) enables unstable features on icu_calendar without pinning the dependency version +# A fix is available in temporal_rs 0.2 but PCTX has not updated +# They are just here to pin the version, and can be removed if PCTX updates temporal_rs +icu_calendar = { version = "=2.1.1", default-features = false } +icu_locale = { version = "=2.1.1", default-features = false } +llama-cpp-sys-2 = { workspace = true, optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } -keyring = { version = "3.6.2", features = ["windows-native"], optional = true } +keyring = { workspace = true, features = ["windows-native"], optional = true } # Platform-specific GPU acceleration for Whisper and local inference [target.'cfg(target_os = "macos")'.dependencies] -candle-core = { version = "0.10.2", default-features = false, features = ["metal"], optional = true } -candle-nn = { version = "0.10.2", default-features = false, features = ["metal"], optional = true } -llama-cpp-2 = { version = "0.1.145", features = ["sampler", "metal", "mtmd"], optional = true } -keyring = { version = "3.6.2", features = ["apple-native"], optional = true } +candle-core = { workspace = true, features = ["metal"], optional = true } +candle-nn = { workspace = true, features = ["metal"], optional = true } +llama-cpp-2 = { workspace = true, features = ["sampler", "metal", "mtmd"], optional = true } +keyring = { workspace = true, features = ["apple-native"], optional = true } [target.'cfg(target_os = "linux")'.dependencies] -keyring = { version = "3.6.2", features = ["sync-secret-service"], optional = true } -libc = "0.2.186" +keyring = { workspace = true, features = ["sync-secret-service"], optional = true } +libc = { version = "0.2.182", default-features = false, features = ["std"] } [dev-dependencies] serial_test = { workspace = true } -mockall = "0.14.0" +mockall = { version = "0.13", default-features = false } wiremock = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } dotenvy = { workspace = true } -ctor = "1.0.6" +ctor = { version = "0.2", default-features = false } test-case = { workspace = true } env-lock = { workspace = true } rmcp = { workspace = true, features = ["transport-streamable-http-server"] } opentelemetry_sdk = { workspace = true, features = ["testing"] } -goose-test-support = { path = "../goose-test-support" } -bytes.workspace = true -http.workspace = true -goose-mcp = { path = "../goose-mcp" } -insta = "1.47.2" -dtor = "1.0.3" +goose-test-support = { path = "../goose-test-support", default-features = false } +bytes = { workspace = true } +http = { workspace = true } +goose-mcp = { path = "../goose-mcp", default-features = false } +insta = { version = "1", default-features = false } +dtor = { version = "1.0.3", default-features = false, features = ["proc_macro"] } [[example]] name = "agent" @@ -271,12 +276,11 @@ name = "generate-acp-schema" path = "src/bin/generate_acp_schema.rs" [package.metadata.cargo-machete] - ignored = [ # Used only on windows "winapi", - # Used to provide extras imports for agent-client-protocol - "agent-client-protocol-schema", - # Used via http transport - "http-body-util", + + # Included only to pin version + "icu_calendar", + "icu_locale", ] diff --git a/crates/goose/src/acp/server/extensions.rs b/crates/goose/src/acp/server/extensions.rs index b45e0407b02f..c44b446d36d3 100644 --- a/crates/goose/src/acp/server/extensions.rs +++ b/crates/goose/src/acp/server/extensions.rs @@ -93,12 +93,13 @@ impl GooseAcpAgent { &self, req: RemoveConfigExtensionRequest, ) -> Result { + let key = crate::config::extensions::name_to_key(&req.config_key); let keys = crate::config::extensions::get_all_extension_names(); - if !keys.iter().any(|k| k == &req.config_key) { + if !keys.iter().any(|k| k == &key) { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Extension '{}' not found", req.config_key))); } - crate::config::extensions::remove_extension(&req.config_key); + crate::config::extensions::remove_extension(&key); Ok(EmptyResponse {}) } @@ -106,12 +107,13 @@ impl GooseAcpAgent { &self, req: ToggleConfigExtensionRequest, ) -> Result { + let key = crate::config::extensions::name_to_key(&req.config_key); let keys = crate::config::extensions::get_all_extension_names(); - if !keys.iter().any(|k| k == &req.config_key) { + if !keys.iter().any(|k| k == &key) { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Extension '{}' not found", req.config_key))); } - crate::config::extensions::set_extension_enabled(&req.config_key, req.enabled); + crate::config::extensions::set_extension_enabled(&key, req.enabled); Ok(EmptyResponse {}) } diff --git a/crates/goose/src/agents/large_response_handler.rs b/crates/goose/src/agents/large_response_handler.rs index 2ac5480d41aa..b3f3e0babc92 100644 --- a/crates/goose/src/agents/large_response_handler.rs +++ b/crates/goose/src/agents/large_response_handler.rs @@ -1,14 +1,22 @@ +use crate::config::Config; use chrono::Utc; use rmcp::model::{CallToolResult, Content, ErrorData}; use std::fs::File; use std::io::Write; -const LARGE_TEXT_THRESHOLD: usize = 200_000; +const DEFAULT_LARGE_TEXT_THRESHOLD: usize = 200_000; + +fn large_text_threshold() -> usize { + Config::global() + .get_param::("GOOSE_MAX_TOOL_RESPONSE_SIZE") + .unwrap_or(DEFAULT_LARGE_TEXT_THRESHOLD) +} /// Process tool response and handle large text content pub fn process_tool_response( response: Result, ) -> Result { + let threshold = large_text_threshold(); match response { Ok(mut result) => { let mut processed_contents = Vec::new(); @@ -17,7 +25,7 @@ pub fn process_tool_response( match content.as_text() { Some(text_content) => { // Check if text exceeds threshold - if text_content.text.chars().count() > LARGE_TEXT_THRESHOLD { + if text_content.text.chars().count() > threshold { // Write to temp file match write_large_text_to_file(&text_content.text) { Ok(file_path) => { @@ -107,7 +115,7 @@ mod tests { #[test] fn test_large_text_response_redirected_to_file() { // Create a text larger than the threshold - let large_text = "a".repeat(LARGE_TEXT_THRESHOLD + 1000); + let large_text = "a".repeat(DEFAULT_LARGE_TEXT_THRESHOLD + 1000); let content = Content::text(large_text.clone()); let response = Ok(CallToolResult::success(vec![content])); @@ -166,7 +174,7 @@ mod tests { fn test_mixed_content_handled_correctly() { // Create a response with mixed content types let small_text = Content::text("Small text"); - let large_text = Content::text("a".repeat(LARGE_TEXT_THRESHOLD + 1000)); + let large_text = Content::text("a".repeat(DEFAULT_LARGE_TEXT_THRESHOLD + 1000)); let image = Content::image("image_data".to_string(), "image/jpeg".to_string()); let response = Ok(CallToolResult::success(vec![small_text, large_text, image])); diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index e1edf2607a39..2c67d38dd716 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -428,11 +428,8 @@ impl Drop for SummonClient { impl SummonClient { pub fn new(context: PlatformExtensionContext) -> Result { - let instructions = build_subagent_instructions(context.session.as_deref()); - let info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) - .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Summon")) - .with_instructions(instructions); + .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Summon")); Ok(Self { info, @@ -1744,6 +1741,15 @@ impl McpClientTrait for SummonClient { Some(&self.info) } + fn get_instructions(&self) -> Option { + let instructions = build_subagent_instructions(self.context.session.as_deref()); + if instructions.is_empty() { + None + } else { + Some(instructions) + } + } + async fn subscribe(&self) -> mpsc::Receiver { let (tx, rx) = mpsc::channel(16); self.notification_subscribers.lock().await.push(tx); diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index d590ca53a51b..302dd7d9571d 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -10,7 +10,7 @@ use lru::LruCache; use std::collections::HashMap; use std::num::NonZeroUsize; use std::sync::Arc; -use tokio::sync::{OnceCell, RwLock}; +use tokio::sync::{Mutex, OnceCell, RwLock}; use tokio_util::sync::CancellationToken; use tracing::{debug, info}; @@ -25,6 +25,15 @@ pub struct AgentManager { default_provider: Arc>>>, default_mode: GooseMode, cancel_tokens: Arc>>, + /// Per-session creation locks. When `get_or_create_agent` misses the + /// `sessions` cache it acquires the per-session lock before doing the + /// expensive work (provider restore, MCP extension initialization) so + /// concurrent callers for the same session never race into doing the + /// work twice. Entries are inserted on demand and pruned when the + /// session is removed *or* evicted by the LRU; the underlying + /// `Arc>` stays alive as long as any caller still holds it, + /// even after the HashMap entry is removed. + creation_locks: Arc>>>>, } impl AgentManager { @@ -46,6 +55,7 @@ impl AgentManager { default_provider: Arc::new(RwLock::new(None)), default_mode, cancel_tokens: Arc::new(RwLock::new(HashMap::new())), + creation_locks: Arc::new(Mutex::new(HashMap::new())), }; Ok(manager) @@ -89,6 +99,7 @@ impl AgentManager { } pub async fn get_or_create_agent(&self, session_id: String) -> Result> { + // Fast path: agent already cached. { let mut sessions = self.sessions.write().await; if let Some(existing) = sessions.get(&session_id) { @@ -96,10 +107,62 @@ impl AgentManager { } } + // Slow path: serialize creation per session so concurrent callers + // (e.g. start_agent's background extension-loading task and a + // resume_agent request racing through the frontend) cannot each + // construct their own Agent and independently send `initialize` to + // every MCP server. See issue #9031. + let creation_lock = { + let mut locks = self.creation_locks.lock().await; + Arc::clone( + locks + .entry(session_id.clone()) + .or_insert_with(|| Arc::new(Mutex::new(()))), + ) + }; + let creation_guard = creation_lock.lock().await; + + // Funnel the fallible work through a helper so we can prune the + // per-session creation lock on every error exit. Without this + // the provider-setup path (update_provider / update_mode) could + // bail out via `?`, leaving a permanent `creation_locks` entry + // for a session that never made it into the LRU cache and that + // no one will ever call `remove_session` on. + let result = self.create_agent_locked(&session_id).await; + + if result.is_err() { + // Release BOTH the guard and our local Arc clone of the + // creation lock before pruning. `prune_creation_lock` + // gates removal on `Arc::strong_count == 1`; if we kept + // `creation_lock` alive the count would still be at least + // two (HashMap + this local) and the failed session would + // leak its lock entry forever. In-flight waiters keep the + // Arc alive on their own and prune correctly skips while + // they hold it. + drop(creation_guard); + drop(creation_lock); + self.prune_creation_lock(&session_id).await; + } + + result + } + + /// Slow-path body for `get_or_create_agent`. Must be called with the + /// per-session creation lock held by the caller. + async fn create_agent_locked(&self, session_id: &str) -> Result> { + // Re-check under the creation lock: another caller may have + // finished creating the agent while we were waiting. + { + let mut sessions = self.sessions.write().await; + if let Some(existing) = sessions.get(session_id) { + return Ok(Arc::clone(existing)); + } + } + let mut mode = self.default_mode; let permission_manager = PermissionManager::instance(); - if let Ok(session) = self.session_manager.get_session(&session_id, false).await { + if let Ok(session) = self.session_manager.get_session(session_id, false).await { mode = session.goose_mode; info!(goose_mode = %mode, session_id = %session_id, "Session loaded"); } @@ -116,7 +179,7 @@ impl AgentManager { ); let agent = Arc::new(Agent::with_config(config)); - if let Ok(session) = self.session_manager.get_session(&session_id, false).await { + if let Ok(session) = self.session_manager.get_session(session_id, false).await { if session.provider_name.is_some() { info!( "Restoring evicted session {} (provider: {:?})", @@ -136,21 +199,53 @@ impl AgentManager { if agent.provider().await.is_err() { if let Some(provider) = &*self.default_provider.read().await { agent - .update_provider(Arc::clone(provider), &session_id) + .update_provider(Arc::clone(provider), session_id) .await?; provider - .update_mode(&session_id, mode) + .update_mode(session_id, mode) .await .map_err(|e| anyhow::anyhow!("Failed to propagate mode to provider: {}", e))?; } } let mut sessions = self.sessions.write().await; - if let Some(existing) = sessions.get(&session_id) { - Ok(Arc::clone(existing)) - } else { - sessions.put(session_id, agent.clone()); - Ok(agent) + if let Some(existing) = sessions.get(session_id) { + return Ok(Arc::clone(existing)); + } + // `push` returns the LRU-evicted entry when the cache is at + // capacity, which `put` does not surface. We need the evicted + // key so we can also drop its creation lock below, otherwise the + // `creation_locks` HashMap would grow without bound in long-lived + // processes that churn through many sessions. + let evicted = sessions + .push(session_id.to_string(), agent.clone()) + .map(|(k, _)| k); + drop(sessions); + + if let Some(evicted_id) = evicted { + self.prune_creation_lock(&evicted_id).await; + } + + Ok(agent) + } + + /// Drop the per-session creation lock for `session_id` if no other + /// caller is currently holding a clone of its `Arc`. Holding the + /// `creation_locks` mutex while we both check `Arc::strong_count` and + /// remove guarantees no new waiter can race in between the check and + /// the removal: any new caller would need to acquire the outer mutex + /// first to clone the inner `Arc`. + /// + /// If a waiter is still in flight (strong_count > 1) we leave the + /// entry in place so the in-flight callers continue to serialize + /// through the same lock; a later removal or eviction will sweep it. + async fn prune_creation_lock(&self, session_id: &str) { + let mut locks = self.creation_locks.lock().await; + let in_use = locks + .get(session_id) + .is_some_and(|lock| Arc::strong_count(lock) > 1); + if !in_use { + locks.remove(session_id); } } @@ -162,6 +257,12 @@ impl AgentManager { sessions .pop(session_id) .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?; + drop(sessions); + // Best-effort prune of the per-session creation lock so the + // HashMap doesn't grow unbounded. Any caller still holding a + // clone of the Arc keeps the underlying Mutex alive until it + // releases its guard. + self.prune_creation_lock(session_id).await; info!("Removed session {}", session_id); Ok(()) } @@ -428,6 +529,140 @@ mod tests { assert!(result.unwrap_err().to_string().contains("not found")); } + #[tokio::test] + async fn test_remove_session_prunes_creation_lock() { + // remove_session must drop the per-session creation lock so the + // HashMap doesn't grow unboundedly. + let temp_dir = TempDir::new().unwrap(); + let manager = create_test_manager(&temp_dir).await; + let session = String::from("to-be-removed"); + + manager.get_or_create_agent(session.clone()).await.unwrap(); + assert_eq!(manager.creation_locks.lock().await.len(), 1); + + manager.remove_session(&session).await.unwrap(); + assert!( + manager.creation_locks.lock().await.is_empty(), + "remove_session must prune the creation lock for the removed session" + ); + } + + #[tokio::test] + async fn test_failed_creation_prunes_creation_lock() { + // Regression test for the Codex review note on PR #9357: when the + // provider-setup path in `create_agent_locked` returns Err, the + // outer `get_or_create_agent` must also drop its local Arc clone + // of the creation lock before pruning. Otherwise + // `Arc::strong_count` stays > 1 and the failed session leaks a + // permanent entry in `creation_locks`. + use async_trait::async_trait; + use rmcp::model::Tool; + + use crate::conversation::message::Message; + use crate::model::ModelConfig; + use crate::providers::base::{MessageStream, Provider, ProviderUsage, Usage}; + use crate::providers::errors::ProviderError; + + struct FailingProvider; + + #[async_trait] + impl Provider for FailingProvider { + fn get_name(&self) -> &str { + "failing-test-provider" + } + + fn get_model_config(&self) -> ModelConfig { + ModelConfig::new_or_fail("test-model") + } + + async fn stream( + &self, + _model_config: &ModelConfig, + _session_id: &str, + _system: &str, + _messages: &[Message], + _tools: &[Tool], + ) -> std::result::Result { + Ok(crate::providers::base::stream_from_single_message( + Message::assistant().with_text("unused"), + ProviderUsage::new("failing-test-provider".into(), Usage::default()), + )) + } + + async fn update_mode( + &self, + _session_id: &str, + _mode: GooseMode, + ) -> std::result::Result<(), ProviderError> { + Err(ProviderError::ExecutionError( + "intentional failure for test".into(), + )) + } + } + + let temp_dir = TempDir::new().unwrap(); + let manager = create_test_manager(&temp_dir).await; + manager + .set_default_provider(Arc::new(FailingProvider)) + .await; + + let session_id = String::from("failed-creation-test"); + let result = manager.get_or_create_agent(session_id.clone()).await; + + assert!( + result.is_err(), + "expected provider mode-update failure to propagate" + ); + assert!( + manager.creation_locks.lock().await.is_empty(), + "creation_locks must be empty after a failed agent creation" + ); + assert!( + !manager.has_session(&session_id).await, + "failed creation must not insert into the LRU cache" + ); + } + + #[tokio::test] + async fn test_lru_eviction_prunes_creation_lock() { + // Sessions can disappear from the LRU cache without going through + // remove_session. When that happens the matching creation lock + // must also be pruned, otherwise long-lived processes that churn + // through many session IDs would accumulate stale lock entries + // even though only `max_sessions` agents remain cached. + let temp_dir = TempDir::new().unwrap(); + let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf())); + let schedule_path = temp_dir.path().join("schedule.json"); + let manager = AgentManager::new( + session_manager, + schedule_path, + Some(2), + GooseMode::default(), + ) + .await + .unwrap(); + + manager.get_or_create_agent("a".into()).await.unwrap(); + manager.get_or_create_agent("b".into()).await.unwrap(); + assert_eq!(manager.creation_locks.lock().await.len(), 2); + + // Inserting a third session evicts the LRU entry ("a"). + manager.get_or_create_agent("c".into()).await.unwrap(); + + let locks = manager.creation_locks.lock().await; + assert_eq!( + locks.len(), + 2, + "creation_locks must stay bounded by max_sessions after LRU eviction" + ); + assert!( + !locks.contains_key("a"), + "LRU-evicted session's creation lock should be pruned" + ); + assert!(locks.contains_key("b")); + assert!(locks.contains_key("c")); + } + #[test_case(GooseMode::Approve ; "approve")] #[test_case(GooseMode::Chat ; "chat")] #[test_case(GooseMode::SmartApprove ; "smart_approve")] diff --git a/crates/goose/src/goose_apps/cache.rs b/crates/goose/src/goose_apps/cache.rs index c63640304537..36582829fe43 100644 --- a/crates/goose/src/goose_apps/cache.rs +++ b/crates/goose/src/goose_apps/cache.rs @@ -1,4 +1,5 @@ use crate::config::paths::Paths; +use crate::utils::bytes_to_hex; use sha2::{Digest, Sha256}; use std::fs; use std::path::PathBuf; @@ -36,8 +37,8 @@ impl McpAppCache { fn cache_key(extension_name: &str, resource_uri: &str) -> String { let input = format!("{}::{}", extension_name, resource_uri); - let hash = Sha256::digest(input.as_bytes()); - format!("{}_{:x}", extension_name, hash) + let hash = bytes_to_hex(Sha256::digest(input.as_bytes())); + format!("{}_{}", extension_name, hash) } pub fn list_apps(&self) -> Result, std::io::Error> { diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 02696312c942..aaee33764927 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -1,6 +1,3 @@ -#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] -compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled"); - #[cfg(all(feature = "rustls-tls", feature = "native-tls"))] compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive"); diff --git a/crates/goose/src/otel/otlp.rs b/crates/goose/src/otel/otlp.rs index 234617941ec9..c93049679b65 100644 --- a/crates/goose/src/otel/otlp.rs +++ b/crates/goose/src/otel/otlp.rs @@ -1,6 +1,6 @@ use opentelemetry::trace::TracerProvider; use opentelemetry::{global, KeyValue}; -use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_appender_tracing::layer::{OpenTelemetryTracingBridge, TracingSpanAttributes}; use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider}; use opentelemetry_sdk::metrics::{SdkMeterProvider, Temporality}; use opentelemetry_sdk::propagation::TraceContextPropagator; @@ -242,7 +242,7 @@ fn create_otlp_logs_layer() -> OtlpResult { }; let bridge = OpenTelemetryTracingBridge::builder(&logger_provider) - .with_span_attribute_allowlist(["session.id"]) + .with_tracing_span_attributes(TracingSpanAttributes::allowlist(["session.id"])) .build(); *LOGGER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(logger_provider); diff --git a/crates/goose/src/providers/api_client.rs b/crates/goose/src/providers/api_client.rs index 86109073ad86..f6bc62d47083 100644 --- a/crates/goose/src/providers/api_client.rs +++ b/crates/goose/src/providers/api_client.rs @@ -4,10 +4,13 @@ use anyhow::Result; use async_trait::async_trait; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, - Certificate, Client, Identity, Response, StatusCode, + Client, Response, StatusCode, }; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] +use reqwest::{Certificate, Identity}; use serde_json::Value; use std::fmt; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] use std::fs::read_to_string; use std::path::PathBuf; use std::time::Duration; @@ -113,7 +116,8 @@ impl TlsConfig { self.client_identity.is_some() || self.ca_cert_path.is_some() } - pub fn load_identity(&self) -> Result> { + #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] + fn load_identity(&self) -> Result> { if let Some(cert_key_pair) = &self.client_identity { let cert_pem = read_to_string(&cert_key_pair.cert_path) .map_err(|e| anyhow::anyhow!("Failed to read client certificate: {}", e))?; @@ -142,7 +146,8 @@ impl TlsConfig { } } - pub fn load_ca_certificates(&self) -> Result> { + #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] + fn load_ca_certificates(&self) -> Result> { match &self.ca_cert_path { Some(ca_path) => { let ca_pem = read_to_string(ca_path) @@ -323,6 +328,7 @@ impl ApiClient { } /// Configure TLS settings on a reqwest ClientBuilder + #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] fn configure_tls( mut client_builder: reqwest::ClientBuilder, tls_config: &TlsConfig, @@ -342,6 +348,20 @@ impl ApiClient { Ok(client_builder) } + /// Reject custom TLS settings when goose is compiled without a TLS backend. + #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] + fn configure_tls( + client_builder: reqwest::ClientBuilder, + tls_config: &TlsConfig, + ) -> Result { + if tls_config.is_configured() { + return Err(anyhow::anyhow!( + "Custom TLS configuration requires the `rustls-tls` or `native-tls` feature" + )); + } + Ok(client_builder) + } + pub fn with_headers(mut self, headers: HeaderMap) -> Result { self.default_headers = headers; self.rebuild_client()?; diff --git a/crates/goose/src/providers/azure.rs b/crates/goose/src/providers/azure.rs index 4072ae68234d..74c966b7ff6c 100644 --- a/crates/goose/src/providers/azure.rs +++ b/crates/goose/src/providers/azure.rs @@ -12,9 +12,15 @@ const AZURE_PROVIDER_NAME: &str = "azure_openai"; pub const AZURE_DEFAULT_MODEL: &str = "gpt-4o"; pub const AZURE_DOC_URL: &str = "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models"; -pub const AZURE_DEFAULT_API_VERSION: &str = "2024-10-21"; +const AZURE_DEFAULT_API_VERSION: &str = "2024-10-21"; pub const AZURE_OPENAI_KNOWN_MODELS: &[&str] = &["gpt-4o", "gpt-4o-mini", "gpt-4"]; +/// New-style Azure AI endpoints use `/v1/` paths and reject the `api-version` query param. +fn is_v1_endpoint(endpoint: &str) -> bool { + let normalized = endpoint.trim_end_matches('/'); + normalized.ends_with("/v1") || endpoint.contains("/v1/") +} + pub struct AzureProvider; // Custom auth provider that wraps AzureAuth @@ -57,13 +63,7 @@ impl ProviderDef for AzureProvider { vec![ ConfigKey::new("AZURE_OPENAI_ENDPOINT", true, false, None, true), ConfigKey::new("AZURE_OPENAI_DEPLOYMENT_NAME", true, false, None, true), - ConfigKey::new( - "AZURE_OPENAI_API_VERSION", - true, - false, - Some("2024-10-21"), - false, - ), + ConfigKey::new("AZURE_OPENAI_API_VERSION", false, false, None, false), ConfigKey::new("AZURE_OPENAI_API_KEY", false, true, Some(""), true), ], ) @@ -77,9 +77,16 @@ impl ProviderDef for AzureProvider { let config = crate::config::Config::global(); let endpoint: String = config.get_param("AZURE_OPENAI_ENDPOINT")?; let deployment_name: String = config.get_param("AZURE_OPENAI_DEPLOYMENT_NAME")?; - let api_version: String = config + let api_version: Option = config .get_param("AZURE_OPENAI_API_VERSION") - .unwrap_or_else(|_| AZURE_DEFAULT_API_VERSION.to_string()); + .ok() + .or_else(|| { + if is_v1_endpoint(&endpoint) { + None + } else { + Some(AZURE_DEFAULT_API_VERSION.to_string()) + } + }); let api_key = config .get_secret("AZURE_OPENAI_API_KEY") @@ -92,8 +99,10 @@ impl ProviderDef for AzureProvider { let auth_provider = AzureAuthProvider { auth }; let host = format!("{}/openai", endpoint.trim_end_matches('/')); - let api_client = ApiClient::new(host, AuthMethod::Custom(Box::new(auth_provider)))? - .with_query(vec![("api-version".to_string(), api_version)]); + let mut api_client = ApiClient::new(host, AuthMethod::Custom(Box::new(auth_provider)))?; + if let Some(version) = api_version { + api_client = api_client.with_query(vec![("api-version".to_string(), version)]); + } Ok(OpenAiCompatibleProvider::new( AZURE_PROVIDER_NAME.to_string(), @@ -104,3 +113,27 @@ impl ProviderDef for AzureProvider { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_v1_endpoint() { + assert!(is_v1_endpoint( + "https://my-resource.services.ai.azure.com/api/projects/my-proj/openai/v1" + )); + assert!(is_v1_endpoint( + "https://my-resource.services.ai.azure.com/api/projects/my-proj/openai/v1/" + )); + assert!(is_v1_endpoint( + "https://my-resource.services.ai.azure.com/v1/some/path" + )); + + assert!(!is_v1_endpoint("https://my-resource.openai.azure.com")); + assert!(!is_v1_endpoint("https://my-resource.openai.azure.com/")); + assert!(!is_v1_endpoint( + "https://my-resource.openai.azure.com/openai" + )); + } +} diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 0737cbed6b95..f79a3a3f3fb2 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -1419,6 +1419,19 @@ mod tests { assert_eq!(out.thinking, "unfinished"); } + #[test] + fn test_think_filter_tracks_generation_prompt_open_block() { + let mut filter = ThinkFilter::new(); + let _ = filter.push("<|assistant|>\n"); + let mut out = filter.push("hidden reasoningvisible answer"); + let final_out = filter.finish(); + out.content.push_str(&final_out.content); + out.thinking.push_str(&final_out.thinking); + + assert_eq!(out.content, "visible answer"); + assert_eq!(out.thinking, "hidden reasoning"); + } + #[test] fn test_think_filter_preserves_tags_with_think_prefix() { for input in [ diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index 4a5a733ba741..13be0e79554d 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -17,6 +17,7 @@ use futures::future::BoxFuture; use reqwest::header::HeaderValue; use rmcp::model::Tool; use serde_json::Value; +use smithy_transport_reqwest::ReqwestHttpClient; use super::formats::bedrock::{ from_bedrock_message, from_bedrock_usage, to_bedrock_message_with_caching, @@ -98,7 +99,8 @@ impl BedrockProvider { }; // Use load_defaults() which supports AWS SSO, profiles, and environment variables - let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()); + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .http_client(ReqwestHttpClient::new()); if let Ok(profile_name) = config.get_param::("AWS_PROFILE") { if !profile_name.is_empty() { diff --git a/crates/goose/src/providers/canonical/data/canonical_models.json b/crates/goose/src/providers/canonical/data/canonical_models.json index 38969d61a536..b1cdba60b302 100644 --- a/crates/goose/src/providers/canonical/data/canonical_models.json +++ b/crates/goose/src/providers/canonical/data/canonical_models.json @@ -6562,7 +6562,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -6590,9 +6590,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -8260,6 +8260,37 @@ "output": 65536 } }, + { + "id": "alibaba-cn/qwen3.6-flash", + "name": "Qwen3.6 Flash", + "family": "qwen3.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-04-27", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1875, + "output": 1.125, + "cache_write": 0.234375 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba-cn/qwen3.6-max-preview", "name": "Qwen3.6 Max Preview", @@ -8322,6 +8353,36 @@ "output": 65536 } }, + { + "id": "alibaba-cn/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba-cn/qwq-32b", "name": "QwQ 32B", @@ -8769,140 +8830,15 @@ } }, { - "id": "alibaba-coding-plan-cn/qwen3.6-plus", - "name": "Qwen3.6 Plus", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-04-02", - "last_updated": "2026-04-02", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "alibaba-coding-plan/MiniMax-M2.5", - "name": "MiniMax-M2.5", - "family": "minimax", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-12", - "last_updated": "2026-02-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 196608, - "output": 24576 - } - }, - { - "id": "alibaba-coding-plan/glm-4.7", - "name": "GLM-4.7", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-12-22", - "last_updated": "2025-12-22", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 202752, - "output": 16384 - } - }, - { - "id": "alibaba-coding-plan/glm-5", - "name": "GLM-5", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-11", - "last_updated": "2026-02-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 202752, - "output": 16384 - } - }, - { - "id": "alibaba-coding-plan/kimi-k2.5", - "name": "Kimi K2.5", - "family": "kimi", + "id": "alibaba-coding-plan-cn/qwen3.6-flash", + "name": "Qwen3.6 Flash", + "family": "qwen3.6", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01-27", - "last_updated": "2026-01-27", + "release_date": "2026-04-27", + "last_updated": "2026-04-27", "modalities": { "input": [ "text", @@ -8913,73 +8849,11 @@ "text" ] }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "alibaba-coding-plan/qwen3-coder-next", - "name": "Qwen3 Coder Next", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-03", - "last_updated": "2026-02-03", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "alibaba-coding-plan/qwen3-coder-plus", - "name": "Qwen3 Coder Plus", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-07-23", - "last_updated": "2025-07-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 + "input": 0.1875, + "output": 1.125, + "cache_write": 0.234375 }, "limit": { "context": 1000000, @@ -8987,47 +8861,16 @@ } }, { - "id": "alibaba-coding-plan/qwen3-max", - "name": "Qwen3 Max", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-01-23", - "last_updated": "2026-01-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "alibaba-coding-plan/qwen3.5-plus", - "name": "Qwen3.5 Plus", + "id": "alibaba-coding-plan-cn/qwen3.6-plus", + "name": "Qwen3.6 Plus", "family": "qwen", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-04", - "release_date": "2026-02-16", - "last_updated": "2026-02-16", + "release_date": "2026-04-02", + "last_updated": "2026-04-02", "modalities": { "input": [ "text", @@ -9050,6 +8893,316 @@ "output": 65536 } }, + { + "id": "alibaba-coding-plan-cn/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, + { + "id": "alibaba-coding-plan/MiniMax-M2.5", + "name": "MiniMax-M2.5", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-12", + "last_updated": "2026-02-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 196608, + "output": 24576 + } + }, + { + "id": "alibaba-coding-plan/glm-4.7", + "name": "GLM-4.7", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2025-12-22", + "last_updated": "2025-12-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 16384 + } + }, + { + "id": "alibaba-coding-plan/glm-5", + "name": "GLM-5", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-11", + "last_updated": "2026-02-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 16384 + } + }, + { + "id": "alibaba-coding-plan/kimi-k2.5", + "name": "Kimi K2.5", + "family": "kimi", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-01-27", + "last_updated": "2026-01-27", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + { + "id": "alibaba-coding-plan/qwen3-coder-next", + "name": "Qwen3 Coder Next", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-03", + "last_updated": "2026-02-03", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, + { + "id": "alibaba-coding-plan/qwen3-coder-plus", + "name": "Qwen3 Coder Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2025-07-23", + "last_updated": "2025-07-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, + { + "id": "alibaba-coding-plan/qwen3-max", + "name": "Qwen3 Max", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-01-23", + "last_updated": "2026-01-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + { + "id": "alibaba-coding-plan/qwen3.5-plus", + "name": "Qwen3.5 Plus", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-02-16", + "last_updated": "2026-02-16", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, + { + "id": "alibaba-coding-plan/qwen3.6-flash", + "name": "Qwen3.6 Flash", + "family": "qwen3.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-04-27", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1875, + "output": 1.125, + "cache_write": 0.234375 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba-coding-plan/qwen3.6-plus", "name": "Qwen3.6 Plus", @@ -9083,6 +9236,36 @@ "output": 65536 } }, + { + "id": "alibaba-coding-plan/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba/qvq-max", "name": "QVQ Max", @@ -10434,6 +10617,37 @@ "output": 65536 } }, + { + "id": "alibaba/qwen3.6-flash", + "name": "Qwen3.6 Flash", + "family": "qwen3.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-27", + "last_updated": "2026-04-27", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1875, + "output": 1.125, + "cache_write": 0.234375 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba/qwen3.6-max-preview", "name": "Qwen3.6 Max Preview", @@ -10498,6 +10712,36 @@ "output": 65536 } }, + { + "id": "alibaba/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "alibaba/qwq-plus", "name": "QwQ Plus", @@ -14182,12 +14426,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-05-01", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -24631,6 +24876,90 @@ "output": 131072 } }, + { + "id": "cloudflare-workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it", + "name": "Gemma Sea Lion V4 27B It", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-09-23", + "last_updated": "2025-09-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.351, + "output": 0.555 + }, + "limit": { + "context": 128000, + "output": 128000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", + "name": "Deepseek R1 Distill Qwen 32B", + "family": "deepseek", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-22", + "last_updated": "2025-01-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.497, + "output": 4.881 + }, + "limit": { + "context": 80000, + "output": 80000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/google/gemma-3-12b-it", + "name": "Gemma 3 12B It", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-18", + "last_updated": "2025-03-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.345, + "output": 0.556 + }, + "limit": { + "context": 80000, + "output": 80000 + } + }, { "id": "cloudflare-workers-ai/@cf/google/gemma-4-26b-a4b-it", "name": "Gemma 4 26B A4B IT", @@ -24660,6 +24989,287 @@ "output": 16384 } }, + { + "id": "cloudflare-workers-ai/@cf/ibm-granite/granite-4.0-h-micro", + "name": "Granite 4.0 H Micro", + "family": "granite", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-07", + "last_updated": "2025-10-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.017, + "output": 0.112 + }, + "limit": { + "context": 131000, + "output": 131000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-2-7b-chat-fp16", + "name": "Llama 2 7B Chat fp16", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2023-11-07", + "last_updated": "2023-11-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.556, + "output": 6.667 + }, + "limit": { + "context": 4096, + "output": 4096 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3-8b-instruct", + "name": "Llama 3 8B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-04-18", + "last_updated": "2024-04-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.282, + "output": 0.827 + }, + "limit": { + "context": 7968, + "output": 7968 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3-8b-instruct-awq", + "name": "Llama 3 8B Instruct Awq", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-05-09", + "last_updated": "2024-05-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.123, + "output": 0.266 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.1-8b-instruct-awq", + "name": "Llama 3.1 8B Instruct Awq", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-07-25", + "last_updated": "2024-07-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.123, + "output": 0.266 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8", + "name": "Llama 3.1 8B Instruct fp8", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-07-25", + "last_updated": "2024-07-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.152, + "output": 0.287 + }, + "limit": { + "context": 32000, + "output": 32000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.2-11b-vision-instruct", + "name": "Llama 3.2 11B Vision Instruct", + "family": "llama", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-25", + "last_updated": "2024-09-25", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0485, + "output": 0.676 + }, + "limit": { + "context": 128000, + "output": 128000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.2-1b-instruct", + "name": "Llama 3.2 1B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-25", + "last_updated": "2024-09-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.027, + "output": 0.201 + }, + "limit": { + "context": 60000, + "output": 60000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.2-3b-instruct", + "name": "Llama 3.2 3B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-25", + "last_updated": "2024-09-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0509, + "output": 0.335 + }, + "limit": { + "context": 80000, + "output": 80000 + } + }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast", + "name": "Llama 3.3 70B Instruct fp8 Fast", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-12-06", + "last_updated": "2024-12-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.293, + "output": 2.253 + }, + "limit": { + "context": 24000, + "output": 24000 + } + }, { "id": "cloudflare-workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout 17B 16E Instruct", @@ -24685,10 +25295,94 @@ "output": 0.85 }, "limit": { - "context": 128000, + "context": 131000, "output": 16384 } }, + { + "id": "cloudflare-workers-ai/@cf/meta/llama-guard-3-8b", + "name": "Llama Guard 3 8B", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-22", + "last_updated": "2025-01-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.484, + "output": 0.03 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, + { + "id": "cloudflare-workers-ai/@cf/mistral/mistral-7b-instruct-v0.1", + "name": "Mistral 7B Instruct V0.1", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2023-11-07", + "last_updated": "2023-11-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.11, + "output": 0.19 + }, + "limit": { + "context": 2824, + "output": 2824 + } + }, + { + "id": "cloudflare-workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct", + "name": "Mistral Small 3.1 24B Instruct", + "family": "mistral-small", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-03-18", + "last_updated": "2025-03-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.351, + "output": 0.555 + }, + "limit": { + "context": 128000, + "output": 128000 + } + }, { "id": "cloudflare-workers-ai/@cf/moonshotai/kimi-k2.5", "name": "Kimi K2.5", @@ -24747,7 +25441,7 @@ "cache_read": 0.16 }, "limit": { - "context": 256000, + "context": 262144, "output": 256000 } }, @@ -24782,6 +25476,7 @@ { "id": "cloudflare-workers-ai/@cf/openai/gpt-oss-120b", "name": "GPT OSS 120B", + "family": "gpt-oss", "attachment": false, "reasoning": true, "tool_call": true, @@ -24809,6 +25504,7 @@ { "id": "cloudflare-workers-ai/@cf/openai/gpt-oss-20b", "name": "GPT OSS 20B", + "family": "gpt-oss", "attachment": false, "reasoning": true, "tool_call": true, @@ -24833,6 +25529,90 @@ "output": 16384 } }, + { + "id": "cloudflare-workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct", + "name": "Qwen2.5 Coder 32B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-27", + "last_updated": "2025-02-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.66, + "output": 1.0 + }, + "limit": { + "context": 32768, + "output": 32768 + } + }, + { + "id": "cloudflare-workers-ai/@cf/qwen/qwen3-30b-a3b-fp8", + "name": "Qwen3 30B A3b fp8", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-30", + "last_updated": "2025-04-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0509, + "output": 0.335 + }, + "limit": { + "context": 32768, + "output": 32768 + } + }, + { + "id": "cloudflare-workers-ai/@cf/qwen/qwq-32b", + "name": "Qwq 32B", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-05", + "last_updated": "2025-03-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.66, + "output": 1.0 + }, + "limit": { + "context": 24000, + "output": 24000 + } + }, { "id": "cloudflare-workers-ai/@cf/zai-org/glm-4.7-flash", "name": "GLM-4.7-Flash", @@ -24854,7 +25634,7 @@ }, "open_weights": true, "cost": { - "input": 0.06, + "input": 0.0605, "output": 0.4 }, "limit": { @@ -25552,7 +26332,7 @@ "cost": { "input": 0.133, "output": 0.266, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1048576, @@ -25582,7 +26362,7 @@ "cost": { "input": 1.553, "output": 3.106, - "cache_read": 0.145 + "cache_read": 0.003625 }, "limit": { "context": 1048576, @@ -26630,6 +27410,644 @@ "output": 250000 } }, + { + "id": "crof/deepseek-v3.2", + "name": "DeepSeek V3.2", + "family": "deepseek", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-22", + "last_updated": "2025-07-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.28, + "output": 0.38, + "cache_read": 0.06 + }, + "limit": { + "context": 163840, + "output": 163840 + } + }, + { + "id": "crof/deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "family": "deepseek-flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.12, + "output": 0.21, + "cache_read": 0.02 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "crof/deepseek-v4-pro", + "name": "DeepSeek V4 Pro", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.4, + "output": 0.85, + "cache_read": 0.003 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "crof/deepseek-v4-pro-precision", + "name": "DeepSeek V4 Pro (Precision)", + "family": "deepseek-thinking", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-24", + "last_updated": "2026-04-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.1 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "crof/gemma-4-31b-it", + "name": "Gemma 4 31B IT", + "family": "gemma", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-02", + "last_updated": "2026-04-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3, + "cache_read": 0.02 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/glm-4.7", + "name": "GLM-4.7", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2025-12-22", + "last_updated": "2025-12-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.25, + "output": 1.1, + "cache_read": 0.05, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, + { + "id": "crof/glm-4.7-flash", + "name": "GLM-4.7-Flash", + "family": "glm-flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-01-19", + "last_updated": "2026-01-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.04, + "output": 0.3, + "cache_read": 0.008, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, + { + "id": "crof/glm-5", + "name": "GLM-5", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-11", + "last_updated": "2026-02-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.48, + "output": 1.9, + "cache_read": 0.1, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, + { + "id": "crof/glm-5.1", + "name": "GLM-5.1", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.45, + "output": 2.1, + "cache_read": 0.09, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, + { + "id": "crof/glm-5.1-precision", + "name": "GLM 5.1 (Precision)", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.75, + "output": 2.9, + "cache_read": 0.15 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, + { + "id": "crof/greg", + "name": "Experiment!: Greg", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-01-27", + "last_updated": "2026-01-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.2, + "cache_read": 0.02 + }, + "limit": { + "context": 229376, + "output": 229376 + } + }, + { + "id": "crof/kimi-k2.5", + "name": "Kimi K2.5", + "family": "kimi-k2.5", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": false, + "knowledge": "2025-01", + "release_date": "2026-01", + "last_updated": "2026-01", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.35, + "output": 1.7, + "cache_read": 0.07 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/kimi-k2.5-lightning", + "name": "Kimi K2.5 (Lightning)", + "family": "kimi-k2.5", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-02-06", + "last_updated": "2026-02-06", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.0, + "output": 3.0, + "cache_read": 0.2 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "crof/kimi-k2.6", + "name": "Kimi K2.6", + "family": "kimi-k2.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.99, + "cache_read": 0.1 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/kimi-k2.6-precision", + "name": "Kimi K2.6 (Precision)", + "family": "kimi-k2.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.55, + "output": 2.7, + "cache_read": 0.11 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/mimo-v2.5-pro", + "name": "MiMo-V2.5-Pro", + "family": "mimo", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-12", + "release_date": "2026-04-22", + "last_updated": "2026-04-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.5, + "cache_read": 0.1 + }, + "limit": { + "context": 1048576, + "output": 131072 + } + }, + { + "id": "crof/mimo-v2.5-pro-precision", + "name": "MiMo-V2.5-Pro (Precision)", + "family": "mimo", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-04-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.8, + "output": 2.5, + "cache_read": 0.16 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "crof/minimax-m2.5", + "name": "MiniMax-M2.5", + "family": "minimax", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-12", + "last_updated": "2026-02-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.11, + "output": 0.95, + "cache_read": 0.02, + "cache_write": 0.375 + }, + "limit": { + "context": 204800, + "output": 131072 + } + }, + { + "id": "crof/qwen3.5-397b-a17b", + "name": "Qwen3.5 397B-A17B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-15", + "last_updated": "2026-02-15", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.35, + "output": 1.75, + "cache_read": 0.07 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/qwen3.5-9b", + "name": "Qwen3.5 9B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-13", + "last_updated": "2026-03-13", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.04, + "output": 0.15, + "cache_read": 0.008 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "crof/qwen3.6-27b", + "name": "Qwen3.6 27B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-22", + "last_updated": "2026-04-22", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 1.5, + "cache_read": 0.04 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, { "id": "databricks/databricks-claude-haiku-4.5", "name": "Claude Haiku 4.5 (latest)", @@ -27026,8 +28444,8 @@ "cache_read": 0.2 }, "limit": { - "context": 1000000, - "output": 64000 + "context": 1048576, + "output": 65536 } }, { @@ -27815,7 +29233,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -27843,9 +29261,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 65536, @@ -27854,9 +29272,9 @@ }, { "id": "deepinfra/google/gemma-4-26B-A4B-it", - "name": "Gemma 4 26B", + "name": "Gemma 4 26B A4B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -27877,15 +29295,15 @@ "output": 0.34 }, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { "id": "deepinfra/google/gemma-4-31B-it", - "name": "Gemma 4 31B", + "name": "Gemma 4 31B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -27906,8 +29324,8 @@ "output": 0.38 }, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { @@ -28633,7 +30051,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -28661,9 +30079,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -31983,65 +33401,6 @@ "output": 262000 } }, - { - "id": "fireworks-ai/accounts/fireworks/models/deepseek-v3p1", - "name": "DeepSeek V3.1", - "family": "deepseek", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-07", - "release_date": "2025-08-21", - "last_updated": "2025-08-21", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.56, - "output": 1.68 - }, - "limit": { - "context": 163840, - "output": 163840 - } - }, - { - "id": "fireworks-ai/accounts/fireworks/models/deepseek-v3p2", - "name": "DeepSeek V3.2", - "family": "deepseek", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-09", - "release_date": "2025-12-01", - "last_updated": "2025-12-01", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.56, - "output": 1.68, - "cache_read": 0.28 - }, - "limit": { - "context": 160000, - "output": 160000 - } - }, { "id": "fireworks-ai/accounts/fireworks/models/deepseek-v4-flash", "name": "DeepSeek V4 Flash", @@ -32095,130 +33454,13 @@ "cost": { "input": 1.74, "output": 3.48, - "cache_read": 0.15 + "cache_read": 0.145 }, "limit": { "context": 1000000, "output": 384000 } }, - { - "id": "fireworks-ai/accounts/fireworks/models/glm-4p5", - "name": "GLM 4.5", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-07-29", - "last_updated": "2025-07-29", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.55, - "output": 2.19 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, - { - "id": "fireworks-ai/accounts/fireworks/models/glm-4p5-air", - "name": "GLM 4.5 Air", - "family": "glm-air", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-08-01", - "last_updated": "2025-08-01", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.22, - "output": 0.88 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, - { - "id": "fireworks-ai/accounts/fireworks/models/glm-4p7", - "name": "GLM 4.7", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-12-22", - "last_updated": "2025-12-22", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.6, - "output": 2.2, - "cache_read": 0.3 - }, - "limit": { - "context": 198000, - "output": 198000 - } - }, - { - "id": "fireworks-ai/accounts/fireworks/models/glm-5", - "name": "GLM 5", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-11", - "last_updated": "2026-02-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.0, - "output": 3.2, - "cache_read": 0.5 - }, - "limit": { - "context": 202752, - "output": 131072 - } - }, { "id": "fireworks-ai/accounts/fireworks/models/glm-5p1", "name": "GLM 5.1", @@ -32269,7 +33511,8 @@ "open_weights": true, "cost": { "input": 0.15, - "output": 0.6 + "output": 0.6, + "cache_read": 0.015 }, "limit": { "context": 131072, @@ -32296,72 +33539,15 @@ }, "open_weights": true, "cost": { - "input": 0.05, - "output": 0.2 + "input": 0.07, + "output": 0.3, + "cache_read": 0.035 }, "limit": { "context": 131072, "output": 32768 } }, - { - "id": "fireworks-ai/accounts/fireworks/models/kimi-k2-instruct", - "name": "Kimi K2 Instruct", - "family": "kimi", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-07-11", - "last_updated": "2025-07-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.0, - "output": 3.0 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, - { - "id": "fireworks-ai/accounts/fireworks/models/kimi-k2-thinking", - "name": "Kimi K2 Thinking", - "family": "kimi-thinking", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-11-06", - "last_updated": "2025-11-06", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.6, - "output": 2.5, - "cache_read": 0.3 - }, - "limit": { - "context": 256000, - "output": 256000 - } - }, { "id": "fireworks-ai/accounts/fireworks/models/kimi-k2p5", "name": "Kimi K2.5", @@ -32424,35 +33610,6 @@ "output": 262000 } }, - { - "id": "fireworks-ai/accounts/fireworks/models/minimax-m2p1", - "name": "MiniMax-M2.1", - "family": "minimax", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-12-23", - "last_updated": "2025-12-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.03 - }, - "limit": { - "context": 200000, - "output": 200000 - } - }, { "id": "fireworks-ai/accounts/fireworks/models/minimax-m2p5", "name": "MiniMax-M2.5", @@ -32504,7 +33661,7 @@ "cost": { "input": 0.3, "output": 1.2, - "cache_read": 0.03 + "cache_read": 0.06 }, "limit": { "context": 196608, @@ -32542,16 +33699,44 @@ } }, { - "id": "fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo", - "name": "Kimi K2.5 Turbo", + "id": "fireworks-ai/accounts/fireworks/routers/glm-5p1-fast", + "name": "GLM 5.1 Fast", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-01", + "last_updated": "2026-04-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 2.8, + "output": 8.8, + "cache_read": 0.52 + }, + "limit": { + "context": 202800, + "output": 131072 + } + }, + { + "id": "fireworks-ai/accounts/fireworks/routers/kimi-k2p6-turbo", + "name": "Kimi K2.6 Turbo", "family": "kimi-thinking", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01-27", - "last_updated": "2026-01-27", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", @@ -32563,13 +33748,13 @@ }, "open_weights": true, "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0 + "input": 2.0, + "output": 8.0, + "cache_read": 0.3 }, "limit": { - "context": 256000, - "output": 256000 + "context": 262000, + "output": 262000 } }, { @@ -33828,22 +35013,84 @@ } }, { - "id": "github-copilot/gemini-3-flash-preview", - "name": "Gemini 3 Flash", - "family": "gemini-flash", + "id": "github-copilot/gemini-3-flash-preview", + "name": "Gemini 3 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2025-12-17", + "last_updated": "2025-12-17", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 64000 + } + }, + { + "id": "github-copilot/gemini-3-pro-preview", + "name": "Gemini 3 Pro Preview", + "family": "gemini-pro", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2025-11-18", + "last_updated": "2025-11-18", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 64000 + } + }, + { + "id": "github-copilot/gemini-3.1-pro-preview", + "name": "Gemini 3.1 Pro Preview", + "family": "gemini-pro", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-12-17", - "last_updated": "2025-12-17", + "release_date": "2026-02-19", + "last_updated": "2026-02-19", "modalities": { "input": [ "text", - "image", - "audio", - "video" + "image" ], "output": [ "text" @@ -33860,16 +35107,16 @@ } }, { - "id": "github-copilot/gemini-3-pro-preview", - "name": "Gemini 3 Pro Preview", - "family": "gemini-pro", + "id": "github-copilot/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-11-18", - "last_updated": "2025-11-18", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", "modalities": { "input": [ "text", @@ -33891,36 +35138,6 @@ "output": 64000 } }, - { - "id": "github-copilot/gemini-3.1-pro-preview", - "name": "Gemini 3.1 Pro Preview", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-02-19", - "last_updated": "2026-02-19", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 128000, - "output": 64000 - } - }, { "id": "github-copilot/gpt-4.1", "name": "GPT-4.1", @@ -37060,8 +38277,8 @@ "cache_write": 3.75 }, "limit": { - "context": 200000, - "output": 64000 + "context": 1000000, + "output": 128000 } }, { @@ -37456,8 +38673,8 @@ "cache_write": 3.75 }, "limit": { - "context": 200000, - "output": 64000 + "context": 1000000, + "output": 128000 } }, { @@ -37555,7 +38772,7 @@ }, { "id": "google-vertex/gemini-2.0-flash-lite", - "name": "Gemini 2.0 Flash Lite", + "name": "Gemini 2.0 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": false, @@ -37623,7 +38840,7 @@ }, { "id": "google-vertex/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, @@ -37689,108 +38906,6 @@ "output": 65536 } }, - { - "id": "google-vertex/gemini-2.5-flash-lite-preview-09", - "name": "Gemini 2.5 Flash Lite Preview 09-25", - "family": "gemini-flash-lite", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-09-25", - "last_updated": "2025-09-25", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.025 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google-vertex/gemini-2.5-flash-preview-04-17", - "name": "Gemini 2.5 Flash Preview 04-17", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-04-17", - "last_updated": "2025-04-17", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.0375 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google-vertex/gemini-2.5-flash-preview-05-20", - "name": "Gemini 2.5 Flash Preview 05-20", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-05-20", - "last_updated": "2025-05-20", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.0375 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, { "id": "google-vertex/gemini-2.5-flash-preview-09", "name": "Gemini 2.5 Flash Preview 09-25", @@ -37860,74 +38975,6 @@ "output": 65536 } }, - { - "id": "google-vertex/gemini-2.5-pro-preview-05-06", - "name": "Gemini 2.5 Pro Preview 05-06", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-05-06", - "last_updated": "2025-05-06", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.31 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google-vertex/gemini-2.5-pro-preview-06-05", - "name": "Gemini 2.5 Pro Preview 06-05", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-06-05", - "last_updated": "2025-06-05", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.31 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, { "id": "google-vertex/gemini-3-flash-preview", "name": "Gemini 3 Flash Preview", @@ -38132,6 +39179,40 @@ "output": 65536 } }, + { + "id": "google-vertex/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "google-vertex/gemini-embedding-001", "name": "Gemini Embedding 001", @@ -38158,7 +39239,7 @@ }, "limit": { "context": 2048, - "output": 3072 + "output": 1 } }, { @@ -38461,105 +39542,6 @@ "output": 131072 } }, - { - "id": "google/gemini-1.5-flash", - "name": "Gemini 1.5 Flash", - "family": "gemini-flash", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2024-05-14", - "last_updated": "2024-05-14", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.075, - "output": 0.3, - "cache_read": 0.01875 - }, - "limit": { - "context": 1000000, - "output": 8192 - } - }, - { - "id": "google/gemini-1.5-flash-8b", - "name": "Gemini 1.5 Flash-8B", - "family": "gemini-flash", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2024-10-03", - "last_updated": "2024-10-03", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0375, - "output": 0.15, - "cache_read": 0.01 - }, - "limit": { - "context": 1000000, - "output": 8192 - } - }, - { - "id": "google/gemini-1.5-pro", - "name": "Gemini 1.5 Pro", - "family": "gemini-pro", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2024-02-15", - "last_updated": "2024-02-15", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 5.0, - "cache_read": 0.3125 - }, - "limit": { - "context": 1000000, - "output": 8192 - } - }, { "id": "google/gemini-2.0-flash", "name": "Gemini 2.0 Flash", @@ -38596,7 +39578,7 @@ }, { "id": "google/gemini-2.0-flash-lite", - "name": "Gemini 2.0 Flash Lite", + "name": "Gemini 2.0 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": false, @@ -38663,39 +39645,7 @@ }, { "id": "google/gemini-2.5-flash-image", - "name": "Gemini 2.5 Flash Image", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-06", - "release_date": "2025-08-26", - "last_updated": "2025-08-26", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 0.3, - "output": 30.0, - "cache_read": 0.075 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, - { - "id": "google/gemini-2.5-flash-image-preview", - "name": "Gemini 2.5 Flash Image (Preview)", + "name": "Nano Banana", "family": "gemini-flash", "attachment": true, "reasoning": true, @@ -38727,7 +39677,7 @@ }, { "id": "google/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, @@ -38759,176 +39709,6 @@ "output": 65536 } }, - { - "id": "google/gemini-2.5-flash-lite-preview-06-17", - "name": "Gemini 2.5 Flash Lite Preview 06-17", - "family": "gemini-flash-lite", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.025 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-2.5-flash-lite-preview-09", - "name": "Gemini 2.5 Flash Lite Preview 09-25", - "family": "gemini-flash-lite", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-09-25", - "last_updated": "2025-09-25", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.025 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-2.5-flash-preview-04-17", - "name": "Gemini 2.5 Flash Preview 04-17", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-04-17", - "last_updated": "2025-04-17", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.0375 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-2.5-flash-preview-05-20", - "name": "Gemini 2.5 Flash Preview 05-20", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-05-20", - "last_updated": "2025-05-20", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.0375 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-2.5-flash-preview-09", - "name": "Gemini 2.5 Flash Preview 09-25", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-09-25", - "last_updated": "2025-09-25", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.075 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, { "id": "google/gemini-2.5-flash-preview-tts", "name": "Gemini 2.5 Flash Preview TTS", @@ -38936,7 +39716,7 @@ "attachment": false, "reasoning": false, "tool_call": false, - "temperature": false, + "temperature": true, "knowledge": "2025-01", "release_date": "2025-05-01", "last_updated": "2025-05-01", @@ -38954,8 +39734,8 @@ "output": 10.0 }, "limit": { - "context": 8000, - "output": 16000 + "context": 8192, + "output": 16384 } }, { @@ -38992,74 +39772,6 @@ "output": 65536 } }, - { - "id": "google/gemini-2.5-pro-preview-05-06", - "name": "Gemini 2.5 Pro Preview 05-06", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-05-06", - "last_updated": "2025-05-06", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.31 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-2.5-pro-preview-06-05", - "name": "Gemini 2.5 Pro Preview 06-05", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-06-05", - "last_updated": "2025-06-05", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.31 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, { "id": "google/gemini-2.5-pro-preview-tts", "name": "Gemini 2.5 Pro Preview TTS", @@ -39067,7 +39779,7 @@ "attachment": false, "reasoning": false, "tool_call": false, - "temperature": false, + "temperature": true, "knowledge": "2025-01", "release_date": "2025-05-01", "last_updated": "2025-05-01", @@ -39085,8 +39797,8 @@ "output": 20.0 }, "limit": { - "context": 8000, - "output": 16000 + "context": 8192, + "output": 16384 } }, { @@ -39153,13 +39865,13 @@ "cache_read": 0.2 }, "limit": { - "context": 1000000, - "output": 64000 + "context": 1048576, + "output": 65536 } }, { "id": "google/gemini-3.1-flash-image-preview", - "name": "Gemini 3.1 Flash Image (Preview)", + "name": "Nano Banana 2", "family": "gemini-flash", "attachment": true, "reasoning": true, @@ -39185,8 +39897,8 @@ "output": 60.0 }, "limit": { - "context": 131072, - "output": 32768 + "context": 65536, + "output": 65536 } }, { @@ -39385,7 +40097,7 @@ }, "limit": { "context": 2048, - "output": 3072 + "output": 1 } }, { @@ -39413,43 +40125,9 @@ }, "open_weights": false, "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.075 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "google/gemini-flash-lite", - "name": "Gemini Flash-Lite Latest", - "family": "gemini-flash-lite", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-09-25", - "last_updated": "2025-09-25", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.025 + "input": 0.3, + "output": 2.5, + "cache_read": 0.075 }, "limit": { "context": 1048576, @@ -39457,223 +40135,44 @@ } }, { - "id": "google/gemini-live-2.5-flash", - "name": "Gemini Live 2.5 Flash", - "family": "gemini-flash", + "id": "google/gemini-flash-lite", + "name": "Gemini Flash-Lite Latest", + "family": "gemini-flash-lite", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-09-01", - "last_updated": "2025-09-01", + "release_date": "2025-09-25", + "last_updated": "2025-09-25", "modalities": { "input": [ "text", "image", "audio", - "video" - ], - "output": [ - "text", - "audio" - ] - }, - "open_weights": false, - "cost": { - "input": 0.5, - "output": 2.0 - }, - "limit": { - "context": 128000, - "output": 8000 - } - }, - { - "id": "google/gemini-live-2.5-flash-preview-native-audio", - "name": "Gemini Live 2.5 Flash Preview Native Audio", - "family": "gemini-flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2025-06-17", - "last_updated": "2025-09-18", - "modalities": { - "input": [ - "text", - "audio", - "video" + "video", + "pdf" ], "output": [ - "text", - "audio" + "text" ] }, "open_weights": false, "cost": { - "input": 0.5, - "output": 2.0 + "input": 0.1, + "output": 0.4, + "cache_read": 0.025 }, "limit": { - "context": 131072, + "context": 1048576, "output": 65536 } }, - { - "id": "google/gemma-3-12b-it", - "name": "Gemma 3 12B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-03-13", - "last_updated": "2025-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 32768, - "output": 8192 - } - }, - { - "id": "google/gemma-3-27b-it", - "name": "Gemma 3 27B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-03-12", - "last_updated": "2025-03-12", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 131072, - "output": 8192 - } - }, - { - "id": "google/gemma-3-4b-it", - "name": "Gemma 3 4B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-03-13", - "last_updated": "2025-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 32768, - "output": 8192 - } - }, - { - "id": "google/gemma-3n-e2b-it", - "name": "Gemma 3n 2B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-07-09", - "last_updated": "2025-07-09", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 8192, - "output": 2000 - } - }, - { - "id": "google/gemma-3n-e4b-it", - "name": "Gemma 3n 4B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-05-20", - "last_updated": "2025-05-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 8192, - "output": 2000 - } - }, { "id": "google/gemma-4-26b-a4b-it", - "name": "Gemma 4 26B", + "name": "Gemma 4 26B A4B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -39691,15 +40190,15 @@ "open_weights": true, "cost": {}, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { "id": "google/gemma-4-31b-it", - "name": "Gemma 4 31B", + "name": "Gemma 4 31B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -39717,8 +40216,8 @@ "open_weights": true, "cost": {}, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { @@ -43609,9 +44108,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1048576, @@ -44294,6 +44793,129 @@ "output": 8192 } }, + { + "id": "inceptron/MiniMaxAI/MiniMax-M2.5", + "name": "MiniMax M2.5", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-12", + "last_updated": "2026-02-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.24, + "output": 0.9, + "cache_read": 0.03, + "cache_write": 0.0 + }, + "limit": { + "context": 196608, + "output": 196608 + } + }, + { + "id": "inceptron/moonshotai/Kimi-K2.6", + "name": "Kimi K2.6", + "family": "kimi-k2.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.78, + "output": 3.5, + "cache_read": 0.2, + "cache_write": 0.0 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "inceptron/nvidia/llama-3.3-70b-instruct-fp8", + "name": "Llama 3.3 70B Instruct", + "family": "llama", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2023-12", + "release_date": "2024-12-06", + "last_updated": "2024-12-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.12, + "output": 0.38, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, + { + "id": "inceptron/zai-org/GLM-5.1-FP8", + "name": "GLM 5.1", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.4, + "output": 4.4, + "cache_read": 0.26, + "cache_write": 0.0 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, { "id": "inference/google/gemma-3", "name": "Google Gemma 3", @@ -57120,7 +57742,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -57148,9 +57770,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -57251,7 +57873,7 @@ }, { "id": "llmgateway/gemini-2.0-flash-lite", - "name": "Gemini 2.0 Flash Lite", + "name": "Gemini 2.0 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": false, @@ -57318,7 +57940,7 @@ }, { "id": "llmgateway/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, @@ -57350,40 +57972,6 @@ "output": 65536 } }, - { - "id": "llmgateway/gemini-2.5-flash-lite-preview-09", - "name": "Gemini 2.5 Flash Lite Preview 09-25", - "family": "gemini-flash-lite", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-09-25", - "last_updated": "2025-09-25", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.025 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, { "id": "llmgateway/gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -57554,6 +58142,40 @@ "output": 65536 } }, + { + "id": "llmgateway/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "llmgateway/gemini-pro", "name": "Gemini Pro Latest", @@ -57612,36 +58234,6 @@ "output": 16384 } }, - { - "id": "llmgateway/gemma-3-12b-it", - "name": "Gemma 3 12B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-03-13", - "last_updated": "2025-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 32768, - "output": 8192 - } - }, { "id": "llmgateway/gemma-3-1b-it", "name": "Gemma 3 1B IT", @@ -57699,94 +58291,6 @@ "output": 16384 } }, - { - "id": "llmgateway/gemma-3-4b-it", - "name": "Gemma 3 4B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-03-13", - "last_updated": "2025-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 32768, - "output": 8192 - } - }, - { - "id": "llmgateway/gemma-3n-e2b-it", - "name": "Gemma 3n 2B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-07-09", - "last_updated": "2025-07-09", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 8192, - "output": 2000 - } - }, - { - "id": "llmgateway/gemma-3n-e4b-it", - "name": "Gemma 3n 4B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-05-20", - "last_updated": "2025-05-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 8192, - "output": 2000 - } - }, { "id": "llmgateway/glm-4-32b-0414-128k", "name": "GLM-4 32B (0414-128k)", @@ -59281,6 +59785,37 @@ "output": 256000 } }, + { + "id": "llmgateway/grok-4-20", + "name": "Grok 4.20 (Reasoning)", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-09", + "last_updated": "2026-03-09", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, + "cache_read": 0.2 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, { "id": "llmgateway/grok-4-20-beta", "name": "Grok 4.20 (Reasoning)", @@ -59294,7 +59829,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -59302,8 +59838,8 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 6.0, + "input": 1.25, + "output": 2.5, "cache_read": 0.2 }, "limit": { @@ -59324,7 +59860,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -59332,8 +59869,39 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 6.0, + "input": 1.25, + "output": 2.5, + "cache_read": 0.2 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4-20-non", + "name": "Grok 4.20 (Non-Reasoning)", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-09", + "last_updated": "2026-03-09", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 2.5, "cache_read": 0.2 }, "limit": { @@ -59409,12 +59977,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-05-01", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -61722,6 +62291,36 @@ "output": 65536 } }, + { + "id": "llmgateway/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "llmgateway/qwen35-397b-a17b", "name": "Qwen3.5 397B-A17B", @@ -79097,6 +79696,35 @@ "output": 32768 } }, + { + "id": "nearai/Qwen/Qwen3.6-35B-A3B-FP8", + "name": "Qwen 3.6 35B A3B FP8", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-17", + "last_updated": "2026-04-17", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.17, + "output": 1.1, + "cache_read": 0.056 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, { "id": "nearai/anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 (latest)", @@ -79327,7 +79955,7 @@ }, { "id": "nearai/google/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, @@ -79393,6 +80021,40 @@ "output": 65536 } }, + { + "id": "nearai/google/gemini-3-pro", + "name": "Gemini 3 Pro Preview", + "family": "gemini-pro", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2025-11-18", + "last_updated": "2025-11-18", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 15.0, + "cache_read": 0.0 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "nearai/google/gemini-3.1-flash-lite", "name": "Gemini 3.1 Flash Lite", @@ -79427,6 +80089,69 @@ "output": 65536 } }, + { + "id": "nearai/google/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + { + "id": "nearai/google/gemma-4-31B-it", + "name": "Gemma 4 31B IT", + "family": "gemma", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-02", + "last_updated": "2026-04-02", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.13, + "output": 0.4, + "cache_read": 0.026 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, { "id": "nearai/openai/gpt-4.1", "name": "GPT-4.1", @@ -80989,8 +81714,8 @@ }, "open_weights": true, "cost": { - "input": 0.05, - "output": 0.1 + "input": 0.29, + "output": 1.15 }, "limit": { "context": 131056, @@ -81275,8 +82000,8 @@ }, "open_weights": true, "cost": { - "input": 0.05, - "output": 0.1 + "input": 0.29, + "output": 1.15 }, "limit": { "context": 131056, @@ -84480,7 +85205,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1048576, @@ -84508,9 +85233,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1048576, @@ -89685,7 +90410,7 @@ }, "limit": { "context": 200000, - "output": 128000 + "output": 32000 } }, { @@ -90117,6 +90842,40 @@ "output": 65536 } }, + { + "id": "opencode/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "video", + "audio", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "opencode/glm-4.6", "name": "GLM-4.6", @@ -90831,6 +91590,36 @@ "output": 128000 } }, + { + "id": "opencode/grok-build-0.1", + "name": "Grok Build 0.1", + "family": "grok-build", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-20", + "last_updated": "2026-05-20", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 2.0, + "cache_read": 0.2 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "opencode/grok-code", "name": "Grok Code Fast 1", @@ -91703,36 +92492,6 @@ "output": 4096 } }, - { - "id": "openrouter/alibaba/tongyi-deepresearch-30b-a3b", - "name": "Tongyi DeepResearch 30B A3B", - "family": "yi", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-03-31", - "release_date": "2025-09-18", - "last_updated": "2025-09-18", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.09, - "output": 0.45, - "cache_read": 0.09 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, { "id": "openrouter/allenai/olmo-3-32b-think", "name": "Olmo 3 32B Think", @@ -92005,7 +92764,7 @@ }, { "id": "openrouter/anthropic/claude-haiku-4.5", - "name": "Claude Haiku 4.5", + "name": "Claude Haiku 4.5 (latest)", "family": "claude-haiku", "attachment": true, "reasoning": true, @@ -92071,13 +92830,13 @@ }, { "id": "openrouter/anthropic/claude-opus-4.1", - "name": "Claude Opus 4.1", + "name": "Claude Opus 4.1 (latest)", "family": "claude-opus", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01-31", + "knowledge": "2025-03-31", "release_date": "2025-08-05", "last_updated": "2025-08-05", "modalities": { @@ -92104,13 +92863,13 @@ }, { "id": "openrouter/anthropic/claude-opus-4.5", - "name": "Claude Opus 4.5", + "name": "Claude Opus 4.5 (latest)", "family": "claude-opus", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05-30", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -92144,8 +92903,8 @@ "tool_call": true, "temperature": true, "knowledge": "2025-05-31", - "release_date": "2026-02-04", - "last_updated": "2026-02-04", + "release_date": "2026-02-05", + "last_updated": "2026-03-13", "modalities": { "input": [ "text", @@ -92300,13 +93059,13 @@ }, { "id": "openrouter/anthropic/claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "name": "Claude Sonnet 4.5 (latest)", "family": "claude-sonnet", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01-31", + "knowledge": "2025-07-31", "release_date": "2025-09-29", "last_updated": "2025-09-29", "modalities": { @@ -92341,7 +93100,7 @@ "temperature": true, "knowledge": "2025-08-31", "release_date": "2026-02-17", - "last_updated": "2026-02-17", + "last_updated": "2026-03-13", "modalities": { "input": [ "text", @@ -92449,34 +93208,6 @@ "output": 65537 } }, - { - "id": "openrouter/arcee-ai/trinity-large-preview", - "name": "Trinity Large Preview", - "family": "trinity", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-01-27", - "last_updated": "2026-01-27", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.45 - }, - "limit": { - "context": 131000, - "output": 131000 - } - }, { "id": "openrouter/arcee-ai/trinity-large-thinking", "name": "Trinity Large Thinking", @@ -93002,13 +93733,13 @@ }, { "id": "openrouter/cohere/command-r-08", - "name": "Command R (08-2024)", + "name": "Command R", "family": "command-r", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-03-31", + "knowledge": "2024-06-01", "release_date": "2024-08-30", "last_updated": "2024-08-30", "modalities": { @@ -93019,7 +93750,7 @@ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { "input": 0.15, "output": 0.6 @@ -93031,13 +93762,13 @@ }, { "id": "openrouter/cohere/command-r-plus-08", - "name": "Command R+ (08-2024)", + "name": "Command R+", "family": "command-r", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-03-31", + "knowledge": "2024-06-01", "release_date": "2024-08-30", "last_updated": "2024-08-30", "modalities": { @@ -93048,7 +93779,7 @@ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { "input": 2.5, "output": 10.0 @@ -93060,15 +93791,15 @@ }, { "id": "openrouter/cohere/command-r7b-12", - "name": "Command R7B (12-2024)", + "name": "Command R7B", "family": "command-r", "attachment": false, "reasoning": false, "tool_call": false, "temperature": true, - "knowledge": "2024-08-31", - "release_date": "2024-12-14", - "last_updated": "2024-12-14", + "knowledge": "2024-06-01", + "release_date": "2024-02-27", + "last_updated": "2024-02-27", "modalities": { "input": [ "text" @@ -93077,7 +93808,7 @@ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { "input": 0.0375, "output": 0.15 @@ -93117,15 +93848,15 @@ }, { "id": "openrouter/deepseek/deepseek-chat", - "name": "DeepSeek V3", + "name": "DeepSeek Chat", "family": "deepseek", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-07-31", - "release_date": "2024-12-26", - "last_updated": "2024-12-26", + "knowledge": "2025-09", + "release_date": "2025-12-01", + "last_updated": "2026-02-28", "modalities": { "input": [ "text" @@ -93136,12 +93867,12 @@ }, "open_weights": true, "cost": { - "input": 0.32, - "output": 0.89 + "input": 0.2288, + "output": 0.9144 }, "limit": { - "context": 163840, - "output": 16384 + "context": 128000, + "output": 16000 } }, { @@ -93414,11 +94145,12 @@ { "id": "openrouter/deepseek/deepseek-v4-flash", "name": "DeepSeek V4 Flash", - "family": "deepseek", + "family": "deepseek-flash", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-05", "release_date": "2026-04-24", "last_updated": "2026-04-24", "modalities": { @@ -93431,23 +94163,24 @@ }, "open_weights": true, "cost": { - "input": 0.112, - "output": 0.224, - "cache_read": 0.022 + "input": 0.1, + "output": 0.2, + "cache_read": 0.02 }, "limit": { - "context": 1048575, - "output": 131072 + "context": 1048576, + "output": 16384 } }, { "id": "openrouter/deepseek/deepseek-v4-flash:free", "name": "DeepSeek V4 Flash (free)", - "family": "deepseek", + "family": "deepseek-flash", "attachment": false, "reasoning": true, "tool_call": true, "temperature": false, + "knowledge": "2025-05", "release_date": "2026-04-24", "last_updated": "2026-04-24", "modalities": { @@ -93471,11 +94204,12 @@ { "id": "openrouter/deepseek/deepseek-v4-pro", "name": "DeepSeek V4 Pro", - "family": "deepseek", + "family": "deepseek-thinking", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-05", "release_date": "2026-04-24", "last_updated": "2026-04-24", "modalities": { @@ -93556,7 +94290,7 @@ "cache_write": 0.083333 }, "limit": { - "context": 1048576, + "context": 1000000, "output": 8192 } }, @@ -93601,9 +94335,9 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01-31", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", + "knowledge": "2025-01", + "release_date": "2025-03-20", + "last_updated": "2025-06-05", "modalities": { "input": [ "pdf", @@ -93630,15 +94364,15 @@ }, { "id": "openrouter/google/gemini-2.5-flash-image", - "name": "Nano Banana (Gemini 2.5 Flash Image)", - "family": "gemini", + "name": "Nano Banana", + "family": "gemini-flash", "attachment": true, "reasoning": false, "tool_call": false, "temperature": true, - "knowledge": "2025-01-31", - "release_date": "2025-10-07", - "last_updated": "2025-10-07", + "knowledge": "2025-06", + "release_date": "2025-08-26", + "last_updated": "2025-08-26", "modalities": { "input": [ "image", @@ -93663,15 +94397,15 @@ }, { "id": "openrouter/google/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01-31", - "release_date": "2025-07-22", - "last_updated": "2025-07-22", + "knowledge": "2025-01", + "release_date": "2025-06-17", + "last_updated": "2025-06-17", "modalities": { "input": [ "text", @@ -93734,14 +94468,14 @@ { "id": "openrouter/google/gemini-2.5-pro", "name": "Gemini 2.5 Pro", - "family": "gemini", + "family": "gemini-pro", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01-31", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", + "knowledge": "2025-01", + "release_date": "2025-03-20", + "last_updated": "2025-06-05", "modalities": { "input": [ "text", @@ -93904,7 +94638,7 @@ }, { "id": "openrouter/google/gemini-3.1-flash-image-preview", - "name": "Nano Banana 2 (Gemini 3.1 Flash Image Preview)", + "name": "Nano Banana 2", "family": "gemini-flash", "attachment": true, "reasoning": true, @@ -93936,11 +94670,12 @@ { "id": "openrouter/google/gemini-3.1-flash-lite", "name": "Gemini 3.1 Flash Lite", - "family": "gemini", + "family": "gemini-flash-lite", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-01", "release_date": "2026-05-07", "last_updated": "2026-05-07", "modalities": { @@ -93975,6 +94710,7 @@ "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-01", "release_date": "2026-03-03", "last_updated": "2026-03-03", "modalities": { @@ -94045,8 +94781,8 @@ "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2026-02-25", - "last_updated": "2026-02-25", + "release_date": "2026-02-19", + "last_updated": "2026-02-19", "modalities": { "input": [ "text", @@ -94071,6 +94807,41 @@ "output": 65536 } }, + { + "id": "openrouter/google/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "video", + "pdf", + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15, + "cache_write": 0.083333 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "openrouter/google/gemma-2-27b-it", "name": "Gemma 2 27B", @@ -94221,15 +94992,14 @@ }, { "id": "openrouter/google/gemma-4-26b-a4b-it", - "name": "Gemma 4 26B A4B ", + "name": "Gemma 4 26B A4B IT", "family": "gemma", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-04-03", - "last_updated": "2026-04-03", + "release_date": "2026-04-02", + "last_updated": "2026-04-02", "modalities": { "input": [ "image", @@ -94258,9 +95028,8 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-04-03", - "last_updated": "2026-04-03", + "release_date": "2026-04-02", + "last_updated": "2026-04-02", "modalities": { "input": [ "image", @@ -94283,13 +95052,12 @@ }, { "id": "openrouter/google/gemma-4-31b-it", - "name": "Gemma 4 31B", + "name": "Gemma 4 31B IT", "family": "gemma", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", "release_date": "2026-04-02", "last_updated": "2026-04-02", "modalities": { @@ -94320,7 +95088,6 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", "release_date": "2026-04-02", "last_updated": "2026-04-02", "modalities": { @@ -94537,9 +95304,9 @@ }, "open_weights": false, "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.06 + "input": 0.075, + "output": 0.625, + "cache_read": 0.015 }, "limit": { "context": 262144, @@ -95039,13 +95806,13 @@ }, { "id": "openrouter/meta-llama/llama-3.3-70b-instruct", - "name": "Llama 3.3 70B Instruct", + "name": "Llama-3.3-70B-Instruct", "family": "llama", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-12-31", + "knowledge": "2023-12", "release_date": "2024-12-06", "last_updated": "2024-12-06", "modalities": { @@ -95074,7 +95841,7 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-12-31", + "knowledge": "2023-12", "release_date": "2024-12-06", "last_updated": "2024-12-06", "modalities": { @@ -95361,14 +96128,14 @@ }, { "id": "openrouter/minimax/minimax-m2", - "name": "MiniMax M2", + "name": "MiniMax-M2", "family": "minimax", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-10-23", - "last_updated": "2025-10-23", + "release_date": "2025-10-27", + "last_updated": "2025-10-27", "modalities": { "input": [ "text" @@ -95419,7 +96186,7 @@ }, { "id": "openrouter/minimax/minimax-m2.1", - "name": "MiniMax M2.1", + "name": "MiniMax-M2.1", "family": "minimax", "attachment": false, "reasoning": true, @@ -95448,7 +96215,7 @@ }, { "id": "openrouter/minimax/minimax-m2.5", - "name": "MiniMax M2.5", + "name": "MiniMax-M2.5", "family": "minimax", "attachment": false, "reasoning": true, @@ -95504,7 +96271,7 @@ }, { "id": "openrouter/minimax/minimax-m2.7", - "name": "MiniMax M2.7", + "name": "MiniMax-M2.7", "family": "minimax", "attachment": false, "reasoning": true, @@ -95563,7 +96330,7 @@ }, { "id": "openrouter/mistralai/devstral", - "name": "Devstral 2 2512", + "name": "Devstral 2", "family": "devstral", "attachment": true, "reasoning": false, @@ -95775,15 +96542,15 @@ }, { "id": "openrouter/mistralai/mistral-large", - "name": "Mistral Large", + "name": "Mistral Large 2.1", "family": "mistral-large", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-11-30", - "release_date": "2024-02-26", - "last_updated": "2024-02-26", + "knowledge": "2024-11", + "release_date": "2024-11-01", + "last_updated": "2024-11-04", "modalities": { "input": [ "text", @@ -95793,15 +96560,15 @@ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { "input": 2.0, "output": 6.0, "cache_read": 0.2 }, "limit": { - "context": 128000, - "output": 128000 + "context": 131072, + "output": 131072 } }, { @@ -95906,9 +96673,9 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-04-30", - "release_date": "2024-07-19", - "last_updated": "2024-07-19", + "knowledge": "2024-07", + "release_date": "2024-07-01", + "last_updated": "2024-07-01", "modalities": { "input": [ "text" @@ -96233,14 +97000,14 @@ { "id": "openrouter/moonshotai/kimi-k2.5", "name": "Kimi K2.5", - "family": "kimi", + "family": "kimi-k2.5", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2026-01-27", - "last_updated": "2026-01-27", + "release_date": "2026-01", + "last_updated": "2026-01", "modalities": { "input": [ "text", @@ -96264,13 +97031,14 @@ { "id": "openrouter/moonshotai/kimi-k2.6", "name": "Kimi K2.6", - "family": "kimi", + "family": "kimi-k2.6", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-04-20", - "last_updated": "2026-04-20", + "knowledge": "2025-01", + "release_date": "2026-04-21", + "last_updated": "2026-04-21", "modalities": { "input": [ "text", @@ -96815,15 +97583,15 @@ }, { "id": "openrouter/openai/gpt-3.5-turbo", - "name": "GPT-3.5 Turbo (older v0613)", + "name": "GPT-3.5-turbo", "family": "gpt", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2021-09-30", - "release_date": "2024-01-25", - "last_updated": "2024-01-25", + "knowledge": "2021-09-01", + "release_date": "2023-03-01", + "last_updated": "2023-11-06", "modalities": { "input": [ "text" @@ -96834,11 +97602,11 @@ }, "open_weights": false, "cost": { - "input": 1.0, - "output": 2.0 + "input": 0.5, + "output": 1.5 }, "limit": { - "context": 4095, + "context": 16385, "output": 4096 } }, @@ -96908,9 +97676,9 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2021-09-30", - "release_date": "2023-05-28", - "last_updated": "2023-05-28", + "knowledge": "2023-11", + "release_date": "2023-11-06", + "last_updated": "2024-04-09", "modalities": { "input": [ "text" @@ -96966,8 +97734,8 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-12-31", - "release_date": "2024-04-09", + "knowledge": "2023-12", + "release_date": "2023-11-06", "last_updated": "2024-04-09", "modalities": { "input": [ @@ -97025,7 +97793,7 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-06-30", + "knowledge": "2024-04", "release_date": "2025-04-14", "last_updated": "2025-04-14", "modalities": { @@ -97051,13 +97819,13 @@ }, { "id": "openrouter/openai/gpt-4.1-mini", - "name": "GPT-4.1 Mini", + "name": "GPT-4.1 mini", "family": "gpt-mini", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-06-30", + "knowledge": "2024-04", "release_date": "2025-04-14", "last_updated": "2025-04-14", "modalities": { @@ -97083,13 +97851,13 @@ }, { "id": "openrouter/openai/gpt-4.1-nano", - "name": "GPT-4.1 Nano", - "family": "gpt", + "name": "GPT-4.1 nano", + "family": "gpt-nano", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-06-30", + "knowledge": "2024-04", "release_date": "2025-04-14", "last_updated": "2025-04-14", "modalities": { @@ -97121,7 +97889,7 @@ "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-10-31", + "knowledge": "2023-09", "release_date": "2024-08-06", "last_updated": "2024-08-06", "modalities": { @@ -97178,13 +97946,13 @@ }, { "id": "openrouter/openai/gpt-4o-mini", - "name": "GPT-4o-mini (2024-07-18)", - "family": "o-mini", + "name": "GPT-4o mini", + "family": "gpt-mini", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-10-31", + "knowledge": "2023-09", "release_date": "2024-07-18", "last_updated": "2024-07-18", "modalities": { @@ -97332,15 +98100,15 @@ }, { "id": "openrouter/openai/gpt-5-codex", - "name": "GPT-5 Codex", + "name": "GPT-5-Codex", "family": "gpt-codex", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, "knowledge": "2024-09-30", - "release_date": "2025-09-23", - "last_updated": "2025-09-23", + "release_date": "2025-09-15", + "last_updated": "2025-09-15", "modalities": { "input": [ "text", @@ -97434,7 +98202,7 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2024-05-31", + "knowledge": "2024-05-30", "release_date": "2025-08-07", "last_updated": "2025-08-07", "modalities": { @@ -97466,7 +98234,7 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2024-05-31", + "knowledge": "2024-05-30", "release_date": "2025-08-07", "last_updated": "2025-08-07", "modalities": { @@ -97587,7 +98355,7 @@ }, { "id": "openrouter/openai/gpt-5.1-codex", - "name": "GPT-5.1-Codex", + "name": "GPT-5.1 Codex", "family": "gpt-codex", "attachment": true, "reasoning": true, @@ -97618,15 +98386,15 @@ }, { "id": "openrouter/openai/gpt-5.1-codex-max", - "name": "GPT-5.1-Codex-Max", + "name": "GPT-5.1 Codex Max", "family": "gpt-codex", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, "knowledge": "2024-09-30", - "release_date": "2025-12-04", - "last_updated": "2025-12-04", + "release_date": "2025-11-13", + "last_updated": "2025-11-13", "modalities": { "input": [ "text", @@ -97649,7 +98417,7 @@ }, { "id": "openrouter/openai/gpt-5.1-codex-mini", - "name": "GPT-5.1-Codex-Mini", + "name": "GPT-5.1 Codex mini", "family": "gpt-codex", "attachment": true, "reasoning": true, @@ -97687,8 +98455,8 @@ "tool_call": true, "temperature": false, "knowledge": "2025-08-31", - "release_date": "2025-12-10", - "last_updated": "2025-12-10", + "release_date": "2025-12-11", + "last_updated": "2025-12-11", "modalities": { "input": [ "pdf", @@ -97744,15 +98512,15 @@ }, { "id": "openrouter/openai/gpt-5.2-codex", - "name": "GPT-5.2-Codex", + "name": "GPT-5.2 Codex", "family": "gpt-codex", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, "knowledge": "2025-08-31", - "release_date": "2026-01-14", - "last_updated": "2026-01-14", + "release_date": "2025-12-11", + "last_updated": "2025-12-11", "modalities": { "input": [ "text", @@ -97782,8 +98550,8 @@ "tool_call": true, "temperature": false, "knowledge": "2025-08-31", - "release_date": "2025-12-10", - "last_updated": "2025-12-10", + "release_date": "2025-12-11", + "last_updated": "2025-12-11", "modalities": { "input": [ "image", @@ -97837,15 +98605,15 @@ }, { "id": "openrouter/openai/gpt-5.3-codex", - "name": "GPT-5.3-Codex", + "name": "GPT-5.3 Codex", "family": "gpt-codex", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, "knowledge": "2025-08-31", - "release_date": "2026-02-24", - "last_updated": "2026-02-24", + "release_date": "2026-02-05", + "last_updated": "2026-02-05", "modalities": { "input": [ "text", @@ -97875,6 +98643,7 @@ "reasoning": true, "tool_call": true, "temperature": false, + "knowledge": "2025-08-31", "release_date": "2026-03-05", "last_updated": "2026-03-05", "modalities": { @@ -97932,7 +98701,7 @@ }, { "id": "openrouter/openai/gpt-5.4-mini", - "name": "GPT-5.4 Mini", + "name": "GPT-5.4 mini", "family": "gpt-mini", "attachment": true, "reasoning": true, @@ -97964,7 +98733,7 @@ }, { "id": "openrouter/openai/gpt-5.4-nano", - "name": "GPT-5.4 Nano", + "name": "GPT-5.4 nano", "family": "gpt-nano", "attachment": true, "reasoning": true, @@ -98034,8 +98803,8 @@ "tool_call": true, "temperature": false, "knowledge": "2025-12-01", - "release_date": "2026-04-24", - "last_updated": "2026-04-24", + "release_date": "2026-04-23", + "last_updated": "2026-04-23", "modalities": { "input": [ "pdf", @@ -98060,14 +98829,14 @@ { "id": "openrouter/openai/gpt-5.5-pro", "name": "GPT-5.5 Pro", - "family": "gpt", + "family": "gpt-pro", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, "knowledge": "2025-12-01", - "release_date": "2026-04-24", - "last_updated": "2026-04-24", + "release_date": "2026-04-23", + "last_updated": "2026-04-23", "modalities": { "input": [ "pdf", @@ -98332,9 +99101,9 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2023-10-31", - "release_date": "2024-12-17", - "last_updated": "2024-12-17", + "knowledge": "2023-09", + "release_date": "2024-12-05", + "last_updated": "2024-12-05", "modalities": { "input": [ "text", @@ -98359,12 +99128,12 @@ { "id": "openrouter/openai/o1-pro", "name": "o1-pro", - "family": "o", + "family": "o-pro", "attachment": true, "reasoning": true, "tool_call": false, "temperature": false, - "knowledge": "2023-10-31", + "knowledge": "2023-09", "release_date": "2025-03-19", "last_updated": "2025-03-19", "modalities": { @@ -98395,7 +99164,7 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2024-06-30", + "knowledge": "2024-05", "release_date": "2025-04-16", "last_updated": "2025-04-16", "modalities": { @@ -98421,14 +99190,15 @@ }, { "id": "openrouter/openai/o3-deep-research", - "name": "o3 Deep Research", + "name": "o3-deep-research", "family": "o", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-10-10", - "last_updated": "2025-10-10", + "knowledge": "2024-05", + "release_date": "2024-06-26", + "last_updated": "2024-06-26", "modalities": { "input": [ "image", @@ -98452,15 +99222,15 @@ }, { "id": "openrouter/openai/o3-mini", - "name": "o3 Mini", - "family": "o", + "name": "o3-mini", + "family": "o-mini", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2023-10-31", - "release_date": "2025-01-31", - "last_updated": "2025-01-31", + "knowledge": "2024-05", + "release_date": "2024-12-20", + "last_updated": "2025-01-29", "modalities": { "input": [ "text", @@ -98514,13 +99284,13 @@ }, { "id": "openrouter/openai/o3-pro", - "name": "o3 Pro", - "family": "o", + "name": "o3-pro", + "family": "o-pro", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2024-06-30", + "knowledge": "2024-05", "release_date": "2025-06-10", "last_updated": "2025-06-10", "modalities": { @@ -98545,13 +99315,13 @@ }, { "id": "openrouter/openai/o4-mini", - "name": "o4 Mini", + "name": "o4-mini", "family": "o-mini", "attachment": true, "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2024-06-30", + "knowledge": "2024-05", "release_date": "2025-04-16", "last_updated": "2025-04-16", "modalities": { @@ -98577,14 +99347,15 @@ }, { "id": "openrouter/openai/o4-mini-deep-research", - "name": "o4 Mini Deep Research", - "family": "o", + "name": "o4-mini-deep-research", + "family": "o-mini", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-10-10", - "last_updated": "2025-10-10", + "knowledge": "2024-05", + "release_date": "2024-06-26", + "last_updated": "2024-06-26", "modalities": { "input": [ "pdf", @@ -99999,7 +100770,7 @@ }, "limit": { "context": 262144, - "output": 65536 + "output": 262144 } }, { @@ -100054,9 +100825,8 @@ }, "open_weights": true, "cost": { - "input": 0.14, - "output": 1.0, - "cache_read": 0.05 + "input": 0.139, + "output": 1.0 }, "limit": { "context": 262144, @@ -100087,8 +100857,7 @@ "open_weights": true, "cost": { "input": 0.39, - "output": 2.34, - "cache_read": 0.195 + "output": 2.34 }, "limit": { "context": 262144, @@ -100240,12 +101009,12 @@ }, "open_weights": true, "cost": { - "input": 0.32, + "input": 0.3, "output": 3.2 }, "limit": { "context": 262144, - "output": 81920 + "output": 262144 } }, { @@ -100271,12 +101040,11 @@ "open_weights": true, "cost": { "input": 0.15, - "output": 1.0, - "cache_read": 0.05 + "output": 1.0 }, "limit": { - "context": 262144, - "output": 262144 + "context": 262140, + "output": 262140 } }, { @@ -100371,6 +101139,35 @@ "output": 65536 } }, + { + "id": "openrouter/qwen/qwen3.7-max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_write": 3.125 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "openrouter/rekaai/reka-edge", "name": "Reka Edge", @@ -100650,12 +101447,13 @@ }, "open_weights": true, "cost": { - "input": 0.1, - "output": 0.3 + "input": 0.09, + "output": 0.3, + "cache_read": 0.02 }, "limit": { "context": 262144, - "output": 65536 + "output": 16384 } }, { @@ -101014,8 +101812,8 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-04-30", - "last_updated": "2026-04-30", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", @@ -101036,6 +101834,36 @@ "output": 1000000 } }, + { + "id": "openrouter/x-ai/grok-build-0.1", + "name": "Grok Build 0.1", + "family": "grok-build", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-16", + "last_updated": "2026-04-16", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 2.0, + "cache_read": 0.2 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "openrouter/xiaomi/mimo-v2-flash", "name": "MiMo-V2-Flash", @@ -101044,8 +101872,9 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-12-14", - "last_updated": "2025-12-14", + "knowledge": "2024-12-01", + "release_date": "2025-12-16", + "last_updated": "2026-02-04", "modalities": { "input": [ "text" @@ -101068,11 +101897,12 @@ { "id": "openrouter/xiaomi/mimo-v2-omni", "name": "MiMo-V2-Omni", - "family": "mimo-v2-omni", + "family": "mimo", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-12", "release_date": "2026-03-18", "last_updated": "2026-03-18", "modalities": { @@ -101100,11 +101930,12 @@ { "id": "openrouter/xiaomi/mimo-v2-pro", "name": "MiMo-V2-Pro", - "family": "mimo-v2-pro", + "family": "mimo", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-12", "release_date": "2026-03-18", "last_updated": "2026-03-18", "modalities": { @@ -101129,11 +101960,12 @@ { "id": "openrouter/xiaomi/mimo-v2.5", "name": "MiMo-V2.5", - "family": "mimo-v2.5", + "family": "mimo", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-12", "release_date": "2026-04-22", "last_updated": "2026-04-22", "modalities": { @@ -101161,11 +101993,12 @@ { "id": "openrouter/xiaomi/mimo-v2.5-pro", "name": "MiMo-V2.5-Pro", - "family": "mimo-v2.5-pro", + "family": "mimo", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-12", "release_date": "2026-04-22", "last_updated": "2026-04-22", "modalities": { @@ -101218,15 +102051,15 @@ }, { "id": "openrouter/z-ai/glm-4.5", - "name": "GLM 4.5", + "name": "GLM-4.5", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-12-31", - "release_date": "2025-07-25", - "last_updated": "2025-07-25", + "knowledge": "2025-04", + "release_date": "2025-07-28", + "last_updated": "2025-07-28", "modalities": { "input": [ "text" @@ -101248,15 +102081,15 @@ }, { "id": "openrouter/z-ai/glm-4.5-air", - "name": "GLM 4.5 Air", + "name": "GLM-4.5-Air", "family": "glm-air", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-12-31", - "release_date": "2025-07-25", - "last_updated": "2025-07-25", + "knowledge": "2025-04", + "release_date": "2025-07-28", + "last_updated": "2025-07-28", "modalities": { "input": [ "text" @@ -101284,9 +102117,9 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-12-31", - "release_date": "2025-07-25", - "last_updated": "2025-07-25", + "knowledge": "2025-04", + "release_date": "2025-07-28", + "last_updated": "2025-07-28", "modalities": { "input": [ "text" @@ -101307,13 +102140,13 @@ }, { "id": "openrouter/z-ai/glm-4.5v", - "name": "GLM 4.5V", + "name": "GLM-4.5V", "family": "glm", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-12-31", + "knowledge": "2025-04", "release_date": "2025-08-11", "last_updated": "2025-08-11", "modalities": { @@ -101338,13 +102171,13 @@ }, { "id": "openrouter/z-ai/glm-4.6", - "name": "GLM 4.6", + "name": "GLM-4.6", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-03-31", + "knowledge": "2025-04", "release_date": "2025-09-30", "last_updated": "2025-09-30", "modalities": { @@ -101368,12 +102201,13 @@ }, { "id": "openrouter/z-ai/glm-4.6v", - "name": "GLM 4.6V", + "name": "GLM-4.6V", "family": "glm", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-04", "release_date": "2025-12-08", "last_updated": "2025-12-08", "modalities": { @@ -101399,7 +102233,7 @@ }, { "id": "openrouter/z-ai/glm-4.7", - "name": "GLM 4.7", + "name": "GLM-4.7", "family": "glm", "attachment": false, "reasoning": true, @@ -101429,12 +102263,13 @@ }, { "id": "openrouter/z-ai/glm-4.7-flash", - "name": "GLM 4.7 Flash", - "family": "glm", + "name": "GLM-4.7-Flash", + "family": "glm-flash", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-04", "release_date": "2026-01-19", "last_updated": "2026-01-19", "modalities": { @@ -101458,7 +102293,7 @@ }, { "id": "openrouter/z-ai/glm-5", - "name": "GLM 5", + "name": "GLM-5", "family": "glm", "attachment": false, "reasoning": true, @@ -101487,14 +102322,14 @@ }, { "id": "openrouter/z-ai/glm-5-turbo", - "name": "GLM 5 Turbo", + "name": "GLM-5-Turbo", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-03-15", - "last_updated": "2026-03-15", + "release_date": "2026-03-16", + "last_updated": "2026-03-16", "modalities": { "input": [ "text" @@ -101516,14 +102351,14 @@ }, { "id": "openrouter/z-ai/glm-5.1", - "name": "GLM 5.1", + "name": "GLM-5.1", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-04-07", - "last_updated": "2026-04-07", + "release_date": "2026-03-27", + "last_updated": "2026-03-27", "modalities": { "input": [ "text" @@ -101532,7 +102367,7 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { "input": 0.98, "output": 3.08, @@ -101540,12 +102375,12 @@ }, "limit": { "context": 202752, - "output": 131072 + "output": 202800 } }, { "id": "openrouter/z-ai/glm-5v-turbo", - "name": "GLM 5V Turbo", + "name": "GLM-5V-Turbo", "family": "glm", "attachment": true, "reasoning": true, @@ -101678,15 +102513,16 @@ "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-01-01", "release_date": "2026-04-27", "last_updated": "2026-04-27", "modalities": { "input": [ "text", "image", + "video", "pdf", - "audio", - "video" + "audio" ], "output": [ "text" @@ -101694,9 +102530,9 @@ }, "open_weights": false, "cost": { - "input": 0.5, - "output": 3.0, - "cache_read": 0.05, + "input": 1.5, + "output": 9.0, + "cache_read": 0.15, "cache_write": 0.083333 }, "limit": { @@ -102212,7 +103048,7 @@ "cost": { "input": 0.19, "output": 0.37, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -102242,7 +103078,7 @@ "cost": { "input": 0.56, "output": 1.12, - "cache_read": 0.145 + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -102285,7 +103121,7 @@ }, { "id": "orcarouter/google/gemini-2.5-flash-lite", - "name": "Gemini 2.5 Flash Lite", + "name": "Gemini 2.5 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": true, @@ -102415,8 +103251,8 @@ "cache_read": 0.2 }, "limit": { - "context": 1000000, - "output": 64000 + "context": 1048576, + "output": 65536 } }, { @@ -102591,9 +103427,9 @@ }, { "id": "orcarouter/google/gemma-4-26b-a4b-it", - "name": "Gemma 4 26B", + "name": "Gemma 4 26B A4B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -102614,15 +103450,15 @@ "output": 0.33 }, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { "id": "orcarouter/google/gemma-4-31b-it", - "name": "Gemma 4 31B", + "name": "Gemma 4 31B IT", "family": "gemma", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -102643,8 +103479,8 @@ "output": 0.38 }, "limit": { - "context": 256000, - "output": 8192 + "context": 262144, + "output": 32768 } }, { @@ -102655,12 +103491,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-05-01", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -106278,6 +107115,38 @@ "output": 65536 } }, + { + "id": "poe/google/gemini-3.5-flash", + "name": "Gemini-3.5-Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "knowledge": "2025-01", + "release_date": "2026-05-19", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image", + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5152, + "output": 9.0909, + "cache_read": 0.1515 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, { "id": "poe/google/gemini-deep-research", "name": "gemini-deep-research", @@ -109055,6 +109924,64 @@ "output": 128000 } }, + { + "id": "poolside/poolside/laguna-m.1", + "name": "Laguna M.1", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 131040, + "output": 8192 + } + }, + { + "id": "poolside/poolside/laguna-xs.2", + "name": "Laguna XS.2", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-28", + "last_updated": "2026-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 131040, + "output": 8192 + } + }, { "id": "privatemode-ai/gemma-3-27b", "name": "Gemma 3 27B", @@ -113298,7 +114225,7 @@ "cost": { "input": 0.4928, "output": 0.7392, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -113328,7 +114255,7 @@ "cost": { "input": 0.4928, "output": 0.7392, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -113358,7 +114285,7 @@ "cost": { "input": 0.4928, "output": 0.7392, - "cache_read": 0.145 + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -113388,7 +114315,7 @@ "cost": { "input": 0.4928, "output": 0.7392, - "cache_read": 0.145 + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -114830,35 +115757,6 @@ "output": 3072 } }, - { - "id": "scaleway/deepseek-r1-distill-llama-70b", - "name": "DeepSeek R1 Distill Llama 70B", - "family": "deepseek-thinking", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-07", - "release_date": "2025-01-20", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.9, - "output": 0.9 - }, - "limit": { - "context": 32000, - "output": 8196 - } - }, { "id": "scaleway/devstral-2-123b-instruct", "name": "Devstral 2 123B Instruct (2512)", @@ -114867,6 +115765,7 @@ "reasoning": false, "tool_call": true, "temperature": true, + "knowledge": "2025-12", "release_date": "2026-01-07", "last_updated": "2026-03-17", "modalities": { @@ -114918,18 +115817,20 @@ } }, { - "id": "scaleway/gpt-oss-120b", - "name": "GPT-OSS 120B", - "family": "gpt-oss", + "id": "scaleway/gemma-4-26b-a4b-it", + "name": "Gemma 4 26B A4B IT", + "family": "gemma", "attachment": true, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2024-01-01", - "last_updated": "2026-03-17", + "knowledge": "2025-04", + "release_date": "2026-04-01", + "last_updated": "2026-05-22", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -114937,24 +115838,23 @@ }, "open_weights": true, "cost": { - "input": 0.15, - "output": 0.6 + "input": 0.25, + "output": 0.5 }, "limit": { - "context": 128000, - "output": 32768 + "context": 256000, + "output": 16384 } }, { - "id": "scaleway/llama-3.1-8b-instruct", - "name": "Llama 3.1 8B Instruct", - "family": "llama", - "attachment": false, + "id": "scaleway/gpt-oss-120b", + "name": "GPT-OSS 120B", + "family": "gpt-oss", + "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-12", - "release_date": "2025-01-01", + "release_date": "2024-01-01", "last_updated": "2026-03-17", "modalities": { "input": [ @@ -114966,12 +115866,12 @@ }, "open_weights": true, "cost": { - "input": 0.2, - "output": 0.2 + "input": 0.15, + "output": 0.6 }, "limit": { "context": 128000, - "output": 16384 + "output": 32768 } }, { @@ -115004,18 +115904,19 @@ } }, { - "id": "scaleway/mistral-nemo-instruct", - "name": "Mistral Nemo Instruct 2407", - "family": "mistral-nemo", + "id": "scaleway/mistral-medium-3.5-128b", + "name": "Mistral Medium 3.5 128B", + "family": "mistral-medium", "attachment": true, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2024-07-25", - "last_updated": "2026-03-17", + "release_date": "2026-04-29", + "last_updated": "2026-04-29", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -115023,12 +115924,12 @@ }, "open_weights": true, "cost": { - "input": 0.2, - "output": 0.2 + "input": 1.5, + "output": 7.5 }, "limit": { - "context": 128000, - "output": 8192 + "context": 256000, + "output": 16384 } }, { @@ -115039,6 +115940,7 @@ "reasoning": false, "tool_call": true, "temperature": true, + "knowledge": "2025-03", "release_date": "2025-06-20", "last_updated": "2026-03-17", "modalities": { @@ -115068,6 +115970,7 @@ "reasoning": false, "tool_call": true, "temperature": true, + "knowledge": "2024-09", "release_date": "2024-09-25", "last_updated": "2026-03-17", "modalities": { @@ -115093,10 +115996,11 @@ "id": "scaleway/qwen3-235b-a22b-instruct", "name": "Qwen3 235B A22B Instruct 2507", "family": "qwen", - "attachment": true, - "reasoning": false, + "attachment": false, + "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2025-04", "release_date": "2025-07-01", "last_updated": "2026-03-17", "modalities": { @@ -115178,7 +116082,7 @@ "id": "scaleway/qwen3.5-397b-a17b", "name": "Qwen3.5 397B A17B", "family": "qwen", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -115205,6 +116109,36 @@ "output": 16384 } }, + { + "id": "scaleway/qwen3.6-35b-a3b", + "name": "Qwen3.6 35B A3B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-05-01", + "last_updated": "2026-05-22", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.25, + "output": 1.5 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, { "id": "scaleway/voxtral-small-24b", "name": "Voxtral Small 24B 2507", @@ -119928,6 +120862,35 @@ "output": 8192 } }, + { + "id": "stepfun-ai/step-3.5-flash", + "name": "Step 3.5 Flash 2603", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-04-02", + "last_updated": "2026-04-02", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3, + "cache_read": 0.02 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "stepfun/step-1-32k", "name": "Step 1 (32K)", @@ -121442,6 +122405,152 @@ "output": 64000 } }, + { + "id": "the-grid-ai/agent-max", + "name": "Agent Max", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 1000000, + "output": 128000 + } + }, + { + "id": "the-grid-ai/agent-prime", + "name": "Agent Prime", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 128000, + "output": 64000 + } + }, + { + "id": "the-grid-ai/agent-standard", + "name": "Agent Standard", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 128000, + "output": 16000 + } + }, + { + "id": "the-grid-ai/code-max", + "name": "Code Max", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 1000000, + "output": 128000 + } + }, + { + "id": "the-grid-ai/code-prime", + "name": "Code Prime", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 128000, + "output": 64000 + } + }, + { + "id": "the-grid-ai/code-standard", + "name": "Code Standard", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-04", + "last_updated": "2026-05-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": {}, + "limit": { + "context": 128000, + "output": 16000 + } + }, { "id": "the-grid-ai/text-max", "name": "Text Max", @@ -121450,10 +122559,11 @@ "tool_call": true, "temperature": true, "release_date": "2026-03-24", - "last_updated": "2026-03-24", + "last_updated": "2026-05-19", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -121474,7 +122584,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-02-26", - "last_updated": "2026-02-26", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -121498,7 +122608,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-02-26", - "last_updated": "2026-02-26", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -121716,6 +122826,34 @@ "output": 500000 } }, + { + "id": "togetherai/Qwen/Qwen3.7-Max", + "name": "Qwen3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5 + }, + "limit": { + "context": 1000000, + "output": 500000 + } + }, { "id": "togetherai/deepseek-ai/DeepSeek-R1", "name": "DeepSeek R1", @@ -122803,6 +123941,39 @@ "output": 32768 } }, + { + "id": "venice/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini-flash", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-22", + "last_updated": "2026-05-25", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.55, + "output": 9.45, + "cache_read": 0.155, + "cache_write": 0.086 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "venice/gemma-4-uncensored", "name": "Gemma 4 Uncensored", @@ -122901,7 +124072,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-04-03", - "last_updated": "2026-04-12", + "last_updated": "2026-05-25", "modalities": { "input": [ "text", @@ -122914,8 +124085,8 @@ }, "open_weights": true, "cost": { - "input": 0.175, - "output": 0.5 + "input": 0.155, + "output": 0.44 }, "limit": { "context": 256000, @@ -123013,16 +124184,15 @@ } }, { - "id": "venice/grok-41-fast", - "name": "Grok 4.1 Fast", - "family": "grok", + "id": "venice/grok-build-0.1", + "name": "Grok Build 0.1", + "family": "grok-build", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-07", - "release_date": "2025-12-01", - "last_updated": "2026-04-09", + "release_date": "2026-05-21", + "last_updated": "2026-05-22", "modalities": { "input": [ "text", @@ -123034,13 +124204,13 @@ }, "open_weights": false, "cost": { - "input": 0.23, - "output": 0.57, - "cache_read": 0.06 + "input": 1.0, + "output": 2.0, + "cache_read": 0.2 }, "limit": { - "context": 1000000, - "output": 30000 + "context": 256000, + "output": 65536 } }, { @@ -123778,6 +124948,36 @@ "output": 65536 } }, + { + "id": "venice/qwen-3.7-max", + "name": "Qwen 3.7 Max", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-22", + "last_updated": "2026-05-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.7, + "output": 8.05, + "cache_read": 0.27, + "cache_write": 3.35 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "venice/qwen3-235b-a22b-instruct", "name": "Qwen 3 235B A22B Instruct 2507", @@ -123845,7 +125045,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-02-25", - "last_updated": "2026-04-16", + "last_updated": "2026-05-25", "modalities": { "input": [ "text", @@ -123864,7 +125064,7 @@ }, "limit": { "context": 256000, - "output": 65536 + "output": 16384 } }, { @@ -125045,6 +126245,38 @@ "output": 64000 } }, + { + "id": "vercel/alibaba/qwen3.7-max", + "name": "Qwen 3.7 Max", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 7.5, + "cache_read": 0.5, + "cache_write": 3.125 + }, + "limit": { + "context": 991000, + "output": 64000 + } + }, { "id": "vercel/amazon/nova-2-lite", "name": "Nova 2 Lite", @@ -126286,7 +127518,7 @@ }, { "id": "vercel/google/gemini-2.0-flash-lite", - "name": "Gemini 2.0 Flash Lite", + "name": "Gemini 2.0 Flash-Lite", "family": "gemini-flash-lite", "attachment": true, "reasoning": false, @@ -126768,6 +128000,37 @@ "output": 64000 } }, + { + "id": "vercel/google/gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-19", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 9.0, + "cache_read": 0.15 + }, + "limit": { + "context": 1000000, + "output": 64000 + } + }, { "id": "vercel/google/gemini-embedding-001", "name": "Gemini Embedding 001", @@ -128084,6 +129347,34 @@ "output": 64000 } }, + { + "id": "vercel/mistral/mistral-medium-3.5", + "name": "Mistral Medium Latest", + "family": "mistral-medium", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.5, + "output": 7.5 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "vercel/mistral/mistral-nemo", "name": "Mistral Nemo", @@ -130473,37 +131764,6 @@ "output": 1536 } }, - { - "id": "vercel/xai/grok-2-vision", - "name": "Grok 2 Vision", - "family": "grok", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08", - "release_date": "2024-08-20", - "last_updated": "2024-08-20", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 2.0 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, { "id": "vercel/xai/grok-4-fast", "name": "Grok 4 Fast Reasoning", @@ -130807,6 +132067,36 @@ "output": 1000000 } }, + { + "id": "vercel/xai/grok-build-0.1", + "name": "Grok Build 0.1", + "family": "grok-build", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-20", + "last_updated": "2026-05-21", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 2.0, + "cache_read": 0.19999999999999998 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "vercel/xai/grok-imagine-image", "name": "Grok Imagine Image", @@ -131413,9 +132703,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1000000, @@ -131775,11 +133065,68 @@ } }, { - "id": "vultr/DeepSeek-V3.2", + "id": "vultr/MiniMaxAI/MiniMax-M2.7", + "name": "MiniMax-M2.7", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-18", + "last_updated": "2026-03-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 1.2 + }, + "limit": { + "context": 204800, + "output": 131072 + } + }, + { + "id": "vultr/moonshotai/Kimi-K2.6", + "name": "Kimi K2.6", + "family": "kimi-k2.6", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-04-21", + "last_updated": "2026-04-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.15, + "output": 0.6 + }, + "limit": { + "context": 262144, + "output": 131072 + } + }, + { + "id": "vultr/nvidia/DeepSeek-V3.2-NVFP4", "name": "DeepSeek V3.2", "family": "deepseek", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2024-07", @@ -131799,21 +133146,21 @@ "output": 1.65 }, "limit": { - "context": 127000, - "output": 4096 + "context": 131072, + "output": 131072 } }, { - "id": "vultr/GLM-5-FP8", - "name": "GLM 5 FP8", - "family": "glm", + "id": "vultr/nvidia/Llama-3.1-Nemotron-Safety-Guard-8B-v3", + "name": "Llama 3.1 Nemotron Safety Guard", + "family": "llama", "attachment": false, "reasoning": false, - "tool_call": true, + "tool_call": false, "temperature": true, - "knowledge": "2025-05", - "release_date": "2026-02-11", - "last_updated": "2026-02-11", + "knowledge": "2023-12", + "release_date": "2025-10-28", + "last_updated": "2025-10-28", "modalities": { "input": [ "text" @@ -131824,25 +133171,25 @@ }, "open_weights": true, "cost": { - "input": 0.85, - "output": 3.1 + "input": 0.01, + "output": 0.01 }, "limit": { - "context": 200000, - "output": 131072 + "context": 8192, + "output": 4096 } }, { - "id": "vultr/Kimi-K2.5", - "name": "Kimi K2 Instruct", - "family": "kimi", + "id": "vultr/nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16", + "name": "NVIDIA Nemotron 3 Nano Omni", + "family": "nemotron", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-04", - "release_date": "2026-01-27", - "last_updated": "2026-01-27", + "knowledge": "2025-05", + "release_date": "2026-04-28", + "last_updated": "2026-04-28", "modalities": { "input": [ "text" @@ -131853,25 +133200,25 @@ }, "open_weights": true, "cost": { - "input": 0.55, - "output": 2.75 + "input": 0.13, + "output": 0.38 }, "limit": { - "context": 254000, - "output": 32768 + "context": 262144, + "output": 131072 } }, { - "id": "vultr/MiniMax-M2.5", - "name": "MiniMax M2.5", - "family": "minimax", + "id": "vultr/nvidia/Nemotron-Cascade-2-30B-A3B", + "name": "NVIDIA Nemotron Cascade 2", + "family": "nemotron", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-09", - "release_date": "2025-02-11", - "last_updated": "2025-02-11", + "knowledge": "2024-07", + "release_date": "2025-12-01", + "last_updated": "2025-12-01", "modalities": { "input": [ "text" @@ -131882,25 +133229,24 @@ }, "open_weights": true, "cost": { - "input": 0.3, - "output": 1.2 + "input": 0.15, + "output": 0.6 }, "limit": { - "context": 194000, - "output": 4096 + "context": 262144, + "output": 131072 } }, { - "id": "vultr/gpt-oss-120b", - "name": "GPT OSS 120B", - "family": "gpt-oss", + "id": "vultr/zai-org/GLM-5.1-FP8", + "name": "GLM-5.1", + "family": "glm", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-06", - "release_date": "2025-08-05", - "last_updated": "2025-08-05", + "release_date": "2026-03-27", + "last_updated": "2026-03-27", "modalities": { "input": [ "text" @@ -131909,14 +133255,14 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.15, - "output": 0.6 + "input": 0.85, + "output": 3.1 }, "limit": { - "context": 129000, - "output": 4096 + "context": 200000, + "output": 131072 } }, { @@ -131950,6 +133296,38 @@ "output": 131072 } }, + { + "id": "wafer.ai/Kimi-K2.6", + "name": "Kimi K2.6", + "family": "kimi", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-05-13", + "last_updated": "2026-05-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.1, + "output": 4.8, + "cache_read": 0.11, + "cache_write": 0.0 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, { "id": "wafer.ai/Qwen3.5-397B-A17B", "name": "Qwen3.5 397B A17B", @@ -131983,6 +133361,39 @@ "output": 65536 } }, + { + "id": "wafer.ai/Qwen3.6-35B-A3B", + "name": "Qwen3.6 35B A3B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-04", + "release_date": "2026-05-11", + "last_updated": "2026-05-11", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.19, + "output": 1.25, + "cache_read": 0.02, + "cache_write": 0.0 + }, + "limit": { + "context": 32768, + "output": 16384 + } + }, { "id": "wandb/MiniMaxAI/MiniMax-M2.5", "name": "MiniMax M2.5", @@ -132498,67 +133909,6 @@ "output": 131072 } }, - { - "id": "x-ai/grok-2", - "name": "Grok 2 Latest", - "family": "grok", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08", - "release_date": "2024-08-20", - "last_updated": "2024-12-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 2.0 - }, - "limit": { - "context": 131072, - "output": 8192 - } - }, - { - "id": "x-ai/grok-2-vision", - "name": "Grok 2 Vision", - "family": "grok", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08", - "release_date": "2024-08-20", - "last_updated": "2024-08-20", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 2.0 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, { "id": "x-ai/grok-4.20", "name": "Grok 4.20 (Reasoning)", @@ -132572,7 +133922,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -132580,8 +133931,8 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 6.0, + "input": 1.25, + "output": 2.5, "cache_read": 0.2 }, "limit": { @@ -132602,7 +133953,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -132610,8 +133962,8 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 6.0, + "input": 1.25, + "output": 2.5, "cache_read": 0.2 }, "limit": { @@ -132632,7 +133984,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -132640,8 +133993,8 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 6.0, + "input": 1.25, + "output": 2.5, "cache_read": 0.2 }, "limit": { @@ -132657,12 +134010,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-05-01", + "release_date": "2026-04-17", + "last_updated": "2026-04-17", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -132680,19 +134034,20 @@ } }, { - "id": "x-ai/grok-beta", - "name": "Grok Beta", - "family": "grok-beta", - "attachment": false, - "reasoning": false, + "id": "x-ai/grok-build-0.1", + "name": "Grok Build 0.1", + "family": "grok-build", + "attachment": true, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-08", - "release_date": "2024-11-01", - "last_updated": "2024-11-01", + "release_date": "2026-04-16", + "last_updated": "2026-04-16", "modalities": { "input": [ - "text" + "text", + "image", + "pdf" ], "output": [ "text" @@ -132700,13 +134055,13 @@ }, "open_weights": false, "cost": { - "input": 5.0, - "output": 15.0, - "cache_read": 5.0 + "input": 1.0, + "output": 2.0, + "cache_read": 0.2 }, "limit": { - "context": 131072, - "output": 4096 + "context": 256000, + "output": 256000 } }, { @@ -132717,21 +134072,23 @@ "reasoning": false, "tool_call": false, "temperature": false, - "release_date": "2026-03", - "last_updated": "2026-05-16", + "release_date": "2026-01-28", + "last_updated": "2026-01-28", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ - "image" + "image", + "pdf" ] }, "open_weights": false, "cost": {}, "limit": { - "context": 1024, + "context": 8000, "output": 0 } }, @@ -132743,21 +134100,23 @@ "reasoning": false, "tool_call": false, "temperature": false, - "release_date": "2026-04", - "last_updated": "2026-05-16", + "release_date": "2026-04-03", + "last_updated": "2026-04-03", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ - "image" + "image", + "pdf" ] }, "open_weights": false, "cost": {}, "limit": { - "context": 1024, + "context": 8000, "output": 0 } }, @@ -132769,13 +134128,13 @@ "reasoning": false, "tool_call": false, "temperature": false, - "release_date": "2026-03", - "last_updated": "2026-05-16", + "release_date": "2026-01-28", + "last_updated": "2026-01-28", "modalities": { "input": [ "text", "image", - "video" + "pdf" ], "output": [ "video" @@ -132788,37 +134147,6 @@ "output": 0 } }, - { - "id": "x-ai/grok-vision-beta", - "name": "Grok Vision Beta", - "family": "grok-vision", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08", - "release_date": "2024-11-01", - "last_updated": "2024-11-01", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 15.0, - "cache_read": 5.0 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, { "id": "xiaomi-token-plan-ams/mimo-v2-flash", "name": "MiMo-V2-Flash", @@ -133537,10 +134865,11 @@ "temperature": false, "knowledge": "2025-12-30", "release_date": "2026-05-01", - "last_updated": "2026-05-15", + "last_updated": "2026-05-25", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -133553,8 +134882,8 @@ "cache_read": 0.15 }, "limit": { - "context": 400000, - "output": 128000 + "context": 1000000, + "output": 384000 } }, { @@ -134600,7 +135929,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -134628,9 +135957,9 @@ }, "open_weights": true, "cost": { - "input": 1.74, - "output": 3.48, - "cache_read": 0.145 + "input": 0.435, + "output": 0.87, + "cache_read": 0.003625 }, "limit": { "context": 1000000, diff --git a/crates/goose/src/providers/canonical/data/provider_metadata.json b/crates/goose/src/providers/canonical/data/provider_metadata.json index c19ab1449c78..d16fc1bf045f 100644 --- a/crates/goose/src/providers/canonical/data/provider_metadata.json +++ b/crates/goose/src/providers/canonical/data/provider_metadata.json @@ -164,6 +164,17 @@ ], "model_count": 8 }, + { + "id": "crof", + "display_name": "CrofAI", + "npm": "@ai-sdk/openai-compatible", + "api": "https://crof.ai/v1", + "doc": "https://crof.ai/docs", + "env": [ + "CROF_API_KEY" + ], + "model_count": 21 + }, { "id": "ambient", "display_name": "Ambient", @@ -195,7 +206,7 @@ "env": [ "THEGRIDAI_API_KEY" ], - "model_count": 3 + "model_count": 9 }, { "id": "fastrouter", @@ -306,7 +317,7 @@ "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_KEY" ], - "model_count": 8 + "model_count": 27 }, { "id": "lmstudio", @@ -350,7 +361,7 @@ "env": [ "NEARAI_API_KEY" ], - "model_count": 33 + "model_count": 37 }, { "id": "abacus", @@ -439,7 +450,7 @@ "env": [ "VULTR_API_KEY" ], - "model_count": 5 + "model_count": 7 }, { "id": "kuae-cloud-coding-plan", @@ -582,7 +593,7 @@ "env": [ "LLMGATEWAY_API_KEY" ], - "model_count": 189 + "model_count": 188 }, { "id": "poe", @@ -593,7 +604,7 @@ "env": [ "POE_API_KEY" ], - "model_count": 135 + "model_count": 136 }, { "id": "minimax", @@ -682,7 +693,7 @@ "env": [ "ALIBABA_CODING_PLAN_API_KEY" ], - "model_count": 9 + "model_count": 11 }, { "id": "minimax-cn", @@ -792,7 +803,7 @@ "env": [ "OPENCODE_API_KEY" ], - "model_count": 60 + "model_count": 62 }, { "id": "mixlayer", @@ -836,7 +847,7 @@ "env": [ "ALIBABA_CODING_PLAN_API_KEY" ], - "model_count": 9 + "model_count": 11 }, { "id": "meganova", @@ -860,6 +871,17 @@ ], "model_count": 33 }, + { + "id": "inceptron", + "display_name": "Inceptron", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.inceptron.io/v1", + "doc": "https://docs.inceptron.io", + "env": [ + "INCEPTRON_API_KEY" + ], + "model_count": 4 + }, { "id": "minimax-coding-plan", "display_name": "MiniMax Token Plan (minimax.io)", @@ -935,7 +957,7 @@ "env": [ "WAFER_API_KEY" ], - "model_count": 2 + "model_count": 4 }, { "id": "evroc", @@ -968,7 +990,7 @@ "env": [ "FIREWORKS_API_KEY" ], - "model_count": 20 + "model_count": 12 }, { "id": "alibaba", @@ -979,7 +1001,7 @@ "env": [ "DASHSCOPE_API_KEY" ], - "model_count": 48 + "model_count": 50 }, { "id": "302ai", @@ -1078,7 +1100,7 @@ "env": [ "DASHSCOPE_API_KEY" ], - "model_count": 80 + "model_count": 82 }, { "id": "drun", @@ -1166,7 +1188,18 @@ "env": [ "GITHUB_TOKEN" ], - "model_count": 27 + "model_count": 28 + }, + { + "id": "stepfun-ai", + "display_name": "StepFun", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.stepfun.ai/step_plan/v1", + "doc": "https://platform.stepfun.ai/docs/en/step-plan/integrations/open-code", + "env": [ + "STEPFUN_API_KEY" + ], + "model_count": 2 }, { "id": "inference", @@ -1178,5 +1211,16 @@ "INFERENCE_API_KEY" ], "model_count": 9 + }, + { + "id": "poolside", + "display_name": "Poolside", + "npm": "@ai-sdk/openai-compatible", + "api": "https://inference.poolside.ai/v1", + "doc": "https://platform.poolside.ai", + "env": [ + "POOLSIDE_API_KEY" + ], + "model_count": 2 } ] \ No newline at end of file diff --git a/crates/goose/src/providers/catalog.rs b/crates/goose/src/providers/catalog.rs index e1dfe6d0f526..2cfe698de5d5 100644 --- a/crates/goose/src/providers/catalog.rs +++ b/crates/goose/src/providers/catalog.rs @@ -495,6 +495,36 @@ const SETUP_METADATA: &[CuratedSetupMetadata] = &[ }, ], }, + CuratedSetupMetadata { + provider_id: "databricks_v2", + category: ProviderSetupCategory::Model, + setup_method: ProviderSetupMethod::HostWithOauthFallback, + group: ProviderSetupGroup::Additional, + display_name: Some("Databricks AI Gateway"), + description: Some("Models on Databricks AI Gateway v2"), + docs_url: Some("https://docs.databricks.com/en/generative-ai/ai-gateway/"), + aliases: &["databricks_ai_gateway"], + native_connect_query: None, + binary_name: None, + setup_capabilities: setup_capabilities(false, true, false), + show_only_when_installed: false, + synthetic: false, + secret_field_default: None, + field_overrides: &[ + CuratedFieldMetadata { + key: "DATABRICKS_HOST", + label: "Host URL", + placeholder: Some("https://dbc-...cloud.databricks.com"), + default_value: None, + }, + CuratedFieldMetadata { + key: "DATABRICKS_TOKEN", + label: "Access Token", + placeholder: Some("Paste your access token"), + default_value: None, + }, + ], + }, CuratedSetupMetadata { provider_id: "github_copilot", category: ProviderSetupCategory::Model, diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs index 1aa29862b01b..17cd31bc07fe 100644 --- a/crates/goose/src/providers/chatgpt_codex.rs +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -624,7 +624,7 @@ fn html_success() -> String { } fn html_error(error: &str) -> String { - let safe_error = v_htmlescape::escape(error).to_string(); + let safe_error = v_htmlescape::escape_fmt(error); format!( r#" diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 0eff8b1963d3..76b9bc45942a 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -1,23 +1,22 @@ use anyhow::Result; use async_trait::async_trait; use futures::future::BoxFuture; -use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashSet; use std::sync::LazyLock; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use super::api_client::{ApiClient, AuthMethod, AuthProvider}; +use super::api_client::{ApiClient, AuthMethod}; use super::base::{ ConfigKey, MessageStream, ModelInfo, Provider, ProviderDef, ProviderMetadata, DEFAULT_PROVIDER_TIMEOUT_SECS, }; +use super::databricks_auth::{DatabricksAuth, DatabricksAuthProvider}; use super::embedding::EmbeddingCapable; use super::errors::ProviderError; use super::formats::databricks::create_request; use super::formats::openai_responses::create_responses_request; -use super::oauth; use super::openai_compatible::{ handle_response_openai_compat, handle_status, map_http_error_to_provider_error, sanitize_url, stream_openai_compat, stream_responses_compat, @@ -55,10 +54,6 @@ struct CachedDatabricksEndpointInfo { fetched_at: Instant, } -const DEFAULT_CLIENT_ID: &str = "databricks-cli"; -const DEFAULT_REDIRECT_URL: &str = "http://localhost"; -const DEFAULT_SCOPES: &[&str] = &["all-apis", "offline_access"]; - const DATABRICKS_PROVIDER_NAME: &str = "databricks"; const DATABRICKS_ENDPOINT_METADATA_TTL_SECS: u64 = 60; static DATABRICKS_ENDPOINT_INFO_CACHE: LazyLock< @@ -75,69 +70,6 @@ pub const DATABRICKS_KNOWN_MODELS: &[&str] = &[ pub const DATABRICKS_DOC_URL: &str = "https://docs.databricks.com/en/generative-ai/external-models/index.html"; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DatabricksAuth { - Token(String), - OAuth { - host: String, - client_id: String, - redirect_url: String, - scopes: Vec, - }, -} - -impl DatabricksAuth { - pub fn oauth(host: String) -> Self { - Self::OAuth { - host, - client_id: DEFAULT_CLIENT_ID.to_string(), - redirect_url: DEFAULT_REDIRECT_URL.to_string(), - scopes: DEFAULT_SCOPES.iter().map(|s| s.to_string()).collect(), - } - } - - pub fn token(token: String) -> Self { - Self::Token(token) - } -} - -struct DatabricksAuthProvider { - auth: DatabricksAuth, - token_cache: Arc>>, -} - -#[async_trait] -impl AuthProvider for DatabricksAuthProvider { - async fn get_auth_header(&self) -> Result<(String, String)> { - let token = match &self.auth { - DatabricksAuth::Token(original) => { - let cached = self.token_cache.lock().unwrap().clone(); - match cached { - Some(t) => t, - None => { - // Cache was cleared by refresh_credentials(); re-read - // from config which may have a sidecar-rotated token. - // Fall back to the constructor-provided token if config - // lookup fails (e.g. from_params usage). - let fresh = crate::config::Config::global() - .get_secret::("DATABRICKS_TOKEN") - .unwrap_or_else(|_| original.clone()); - *self.token_cache.lock().unwrap() = Some(fresh.clone()); - fresh - } - } - } - DatabricksAuth::OAuth { - host, - client_id, - redirect_url, - scopes, - } => oauth::get_oauth_token_async(host, client_id, redirect_url, scopes).await?, - }; - Ok(("Authorization".to_string(), format!("Bearer {}", token))) - } -} - #[derive(Debug, serde::Serialize)] pub struct DatabricksProvider { #[serde(skip)] diff --git a/crates/goose/src/providers/databricks_auth.rs b/crates/goose/src/providers/databricks_auth.rs new file mode 100644 index 000000000000..e2795e0ef501 --- /dev/null +++ b/crates/goose/src/providers/databricks_auth.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; + +use super::api_client::AuthProvider; +use super::oauth; + +const DEFAULT_CLIENT_ID: &str = "databricks-cli"; +const DEFAULT_REDIRECT_URL: &str = "http://localhost"; +const DEFAULT_SCOPES: &[&str] = &["all-apis", "offline_access"]; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DatabricksAuth { + Token(String), + OAuth { + host: String, + client_id: String, + redirect_url: String, + scopes: Vec, + }, +} + +impl DatabricksAuth { + pub fn oauth(host: String) -> Self { + Self::OAuth { + host, + client_id: DEFAULT_CLIENT_ID.to_string(), + redirect_url: DEFAULT_REDIRECT_URL.to_string(), + scopes: DEFAULT_SCOPES.iter().map(|s| s.to_string()).collect(), + } + } + + pub fn token(token: String) -> Self { + Self::Token(token) + } +} + +pub(crate) struct DatabricksAuthProvider { + pub auth: DatabricksAuth, + pub token_cache: Arc>>, +} + +#[async_trait] +impl AuthProvider for DatabricksAuthProvider { + async fn get_auth_header(&self) -> Result<(String, String)> { + let token = match &self.auth { + DatabricksAuth::Token(original) => { + let cached = self.token_cache.lock().unwrap().clone(); + match cached { + Some(t) => t, + None => { + let fresh = crate::config::Config::global() + .get_secret::("DATABRICKS_TOKEN") + .unwrap_or_else(|_| original.clone()); + *self.token_cache.lock().unwrap() = Some(fresh.clone()); + fresh + } + } + } + DatabricksAuth::OAuth { + host, + client_id, + redirect_url, + scopes, + } => oauth::get_oauth_token_async(host, client_id, redirect_url, scopes).await?, + }; + Ok(("Authorization".to_string(), format!("Bearer {}", token))) + } +} diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs new file mode 100644 index 000000000000..2385ae9bde8c --- /dev/null +++ b/crates/goose/src/providers/databricks_v2.rs @@ -0,0 +1,499 @@ +use anyhow::Result; +use async_stream::try_stream; +use async_trait::async_trait; +use futures::future::BoxFuture; +use futures::TryStreamExt; +use serde::Serialize; +use serde_json::Value; +use std::io; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::pin; +use tokio_util::io::StreamReader; + +use super::api_client::{ApiClient, AuthMethod}; +use super::base::{ + ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, + DEFAULT_PROVIDER_TIMEOUT_SECS, +}; +use super::databricks_auth::{DatabricksAuth, DatabricksAuthProvider}; +use super::errors::ProviderError; +use super::formats::{anthropic, openai, openai_responses}; +use super::openai_compatible::{handle_status, stream_openai_compat, stream_responses_compat}; +use super::retry::ProviderRetry; +use super::utils::{extract_reasoning_effort, is_openai_responses_model, ImageFormat, RequestLog}; +use crate::config::ConfigError; +use crate::conversation::message::Message; +use crate::model::ModelConfig; +use crate::providers::retry::{ + RetryConfig, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_INITIAL_RETRY_INTERVAL_MS, + DEFAULT_MAX_RETRIES, DEFAULT_MAX_RETRY_INTERVAL_MS, +}; +use rmcp::model::Tool; + +const DATABRICKS_V2_PROVIDER_NAME: &str = "databricks_v2"; +const DATABRICKS_V2_LIST_ENDPOINTS_PATH: &str = "api/ai-gateway/v2/endpoints"; +pub const DATABRICKS_V2_DEFAULT_MODEL: &str = "databricks-gpt-5-5"; +pub const DATABRICKS_V2_KNOWN_MODELS: &[&str] = + &["databricks-gpt-5-5", "databricks-claude-opus-4-7"]; + +pub const DATABRICKS_V2_DOC_URL: &str = "https://docs.databricks.com/en/generative-ai/ai-gateway/"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DatabricksV2Route { + OpenAiResponses, + AnthropicMessages, + MlflowChatCompletions, +} + +#[derive(Debug, Serialize)] +pub struct DatabricksV2Provider { + #[serde(skip)] + api_client: ApiClient, + model: ModelConfig, + #[serde(skip)] + retry_config: RetryConfig, + #[serde(skip)] + name: String, + #[serde(skip)] + token_cache: Arc>>, +} + +impl DatabricksV2Provider { + pub async fn cleanup() -> Result<()> { + super::oauth::cleanup_oauth_cache() + } + + pub async fn from_env(model: ModelConfig) -> Result { + let config = crate::config::Config::global(); + + let mut host: Result = config.get_param("DATABRICKS_HOST"); + if host.is_err() { + host = config.get_secret("DATABRICKS_HOST") + } + + if host.is_err() { + return Err(ConfigError::NotFound( + "Did not find DATABRICKS_HOST in either config file or keyring".to_string(), + ) + .into()); + } + + let host = host?; + let retry_config = Self::load_retry_config(config); + + let auth = if let Ok(api_key) = config.get_secret("DATABRICKS_TOKEN") { + DatabricksAuth::token(api_key) + } else { + DatabricksAuth::oauth(host.clone()) + }; + + Self::new(host, auth, model, retry_config) + } + + pub fn from_params(host: String, api_key: String, model: ModelConfig) -> Result { + Self::new( + host, + DatabricksAuth::token(api_key), + model, + RetryConfig::default(), + ) + } + + fn new( + host: String, + auth: DatabricksAuth, + model: ModelConfig, + retry_config: RetryConfig, + ) -> Result { + let token_cache = Arc::new(Mutex::new(match &auth { + DatabricksAuth::Token(t) => Some(t.clone()), + _ => None, + })); + + let auth_method = AuthMethod::Custom(Box::new(DatabricksAuthProvider { + auth: auth.clone(), + token_cache: token_cache.clone(), + })); + + let api_client = ApiClient::with_timeout( + host, + auth_method, + Duration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS), + )?; + + Ok(Self { + api_client, + model, + retry_config, + name: DATABRICKS_V2_PROVIDER_NAME.to_string(), + token_cache, + }) + } + + fn load_retry_config(config: &crate::config::Config) -> RetryConfig { + let max_retries = config + .get_param("DATABRICKS_MAX_RETRIES") + .ok() + .and_then(|v: String| v.parse::().ok()) + .unwrap_or(DEFAULT_MAX_RETRIES); + + let initial_interval_ms = config + .get_param("DATABRICKS_INITIAL_RETRY_INTERVAL_MS") + .ok() + .and_then(|v: String| v.parse::().ok()) + .unwrap_or(DEFAULT_INITIAL_RETRY_INTERVAL_MS); + + let backoff_multiplier = config + .get_param("DATABRICKS_BACKOFF_MULTIPLIER") + .ok() + .and_then(|v: String| v.parse::().ok()) + .unwrap_or(DEFAULT_BACKOFF_MULTIPLIER); + + let max_interval_ms = config + .get_param("DATABRICKS_MAX_RETRY_INTERVAL_MS") + .ok() + .and_then(|v: String| v.parse::().ok()) + .unwrap_or(DEFAULT_MAX_RETRY_INTERVAL_MS); + + RetryConfig::new( + max_retries, + initial_interval_ms, + backoff_multiplier, + max_interval_ms, + ) + } + + fn route_for_model(model_name: &str) -> DatabricksV2Route { + let (clean_name, _) = extract_reasoning_effort(model_name); + let lower = clean_name.to_lowercase(); + + if is_openai_responses_model(&clean_name) || Self::looks_like_gpt5(&lower) { + DatabricksV2Route::OpenAiResponses + } else if Self::is_claude_model(&lower) { + DatabricksV2Route::AnthropicMessages + } else { + DatabricksV2Route::MlflowChatCompletions + } + } + + fn looks_like_gpt5(model_name: &str) -> bool { + model_name.contains("gpt-5") || model_name.contains("gpt5") + } + + fn is_claude_model(model_name: &str) -> bool { + model_name.contains("claude") + } + + fn parse_list_endpoints_response(json: &Value) -> Result, ProviderError> { + let endpoints = json + .get("endpoints") + .and_then(|v| v.as_array()) + .ok_or_else(|| { + ProviderError::RequestFailed( + "Unexpected response format from Databricks AI Gateway endpoints API" + .to_string(), + ) + })?; + + let mut models: Vec = endpoints + .iter() + .filter_map(|endpoint| { + endpoint + .get("name") + .and_then(|v| v.as_str()) + .map(str::to_string) + }) + .collect(); + models.sort(); + Ok(models) + } + + async fn stream_openai_responses( + &self, + model_config: &ModelConfig, + session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + let mut payload = + openai_responses::create_responses_request(model_config, system, messages, tools)?; + payload["stream"] = Value::Bool(true); + let mut log = RequestLog::start(model_config, &payload)?; + + let response = self + .with_retry(|| async { + let resp = self + .api_client + .response_post(Some(session_id), "ai-gateway/openai/v1/responses", &payload) + .await?; + handle_status(resp).await + }) + .await + .inspect_err(|e| { + let _ = log.error(e); + })?; + + stream_responses_compat(response, log) + } + + async fn stream_mlflow_chat_completions( + &self, + model_config: &ModelConfig, + session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + let mut payload = openai::create_request( + model_config, + system, + messages, + tools, + &ImageFormat::OpenAi, + true, + )?; + if payload.get("max_tokens").is_none() { + payload["max_tokens"] = Value::from(model_config.max_output_tokens()); + } + let mut log = RequestLog::start(model_config, &payload)?; + + let response = self + .with_retry(|| async { + let resp = self + .api_client + .response_post( + Some(session_id), + "ai-gateway/mlflow/v1/chat/completions", + &payload, + ) + .await?; + handle_status(resp).await + }) + .await + .inspect_err(|e| { + let _ = log.error(e); + })?; + + stream_openai_compat(response, log) + } + + async fn stream_anthropic_messages( + &self, + model_config: &ModelConfig, + session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + let mut payload = anthropic::create_request(model_config, system, messages, tools)?; + payload["stream"] = Value::Bool(true); + let mut log = RequestLog::start(model_config, &payload)?; + + let response = self + .with_retry(|| async { + let resp = self + .api_client + .response_post( + Some(session_id), + "ai-gateway/anthropic/v1/messages", + &payload, + ) + .await?; + handle_status(resp).await + }) + .await + .inspect_err(|e| { + let _ = log.error(e); + })?; + + let stream = response.bytes_stream().map_err(io::Error::other); + + Ok(Box::pin(try_stream! { + let stream_reader = StreamReader::new(stream); + let framed = tokio_util::codec::FramedRead::new(stream_reader, tokio_util::codec::LinesCodec::new()) + .map_err(anyhow::Error::from); + + let message_stream = anthropic::response_to_streaming_message(framed); + pin!(message_stream); + while let Some(message) = futures::StreamExt::next(&mut message_stream).await { + let (message, usage) = message.map_err(|e| ProviderError::RequestFailed(format!("Stream decode error: {e}")))?; + log.write(&message, usage.as_ref().map(|f| f.usage).as_ref())?; + yield (message, usage); + } + })) + } +} + +impl ProviderDef for DatabricksV2Provider { + type Provider = Self; + + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + DATABRICKS_V2_PROVIDER_NAME, + "Databricks AI Gateway", + "Models on Databricks AI Gateway v2", + DATABRICKS_V2_DEFAULT_MODEL, + DATABRICKS_V2_KNOWN_MODELS.to_vec(), + DATABRICKS_V2_DOC_URL, + vec![ + ConfigKey::new("DATABRICKS_HOST", true, false, None, true), + ConfigKey::new("DATABRICKS_TOKEN", false, true, None, true), + ], + ) + } + + fn from_env( + model: ModelConfig, + _extensions: Vec, + ) -> BoxFuture<'static, Result> { + Box::pin(Self::from_env(model)) + } + + fn supports_inventory_refresh() -> bool { + true + } +} + +#[async_trait] +impl Provider for DatabricksV2Provider { + fn get_name(&self) -> &str { + &self.name + } + + fn retry_config(&self) -> RetryConfig { + self.retry_config.clone() + } + + async fn refresh_credentials(&self) -> Result<(), ProviderError> { + crate::config::Config::global().invalidate_secrets_cache(); + *self.token_cache.lock().unwrap() = None; + Ok(()) + } + + fn get_model_config(&self) -> ModelConfig { + self.model.clone() + } + + async fn stream( + &self, + model_config: &ModelConfig, + session_id: &str, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result { + match Self::route_for_model(&model_config.model_name) { + DatabricksV2Route::OpenAiResponses => { + self.stream_openai_responses(model_config, session_id, system, messages, tools) + .await + } + DatabricksV2Route::AnthropicMessages => { + self.stream_anthropic_messages(model_config, session_id, system, messages, tools) + .await + } + DatabricksV2Route::MlflowChatCompletions => { + self.stream_mlflow_chat_completions( + model_config, + session_id, + system, + messages, + tools, + ) + .await + } + } + } + + async fn fetch_supported_models(&self) -> Result, ProviderError> { + let response = self + .api_client + .response_get(None, DATABRICKS_V2_LIST_ENDPOINTS_PATH) + .await + .map_err(|e| { + ProviderError::RequestFailed(format!( + "Failed to fetch Databricks AI Gateway endpoints: {e}" + )) + })?; + + if !response.status().is_success() { + let status = response.status(); + let detail = response.text().await.unwrap_or_default(); + return Err(ProviderError::RequestFailed(format!( + "Failed to fetch Databricks AI Gateway endpoints: {status} {detail}" + ))); + } + + let json: Value = response.json().await.map_err(|e| { + ProviderError::RequestFailed(format!( + "Failed to parse Databricks AI Gateway endpoints response: {e}" + )) + })?; + + Self::parse_list_endpoints_response(&json) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn routes_known_model_families() { + for model in ["databricks-gpt-5-5", "databricks-gpt5"] { + assert_eq!( + DatabricksV2Provider::route_for_model(model), + DatabricksV2Route::OpenAiResponses, + "unexpected route for {model}" + ); + } + + for model in ["databricks-claude-opus-4-7", "databricks-claude-sonnet-4-6"] { + assert_eq!( + DatabricksV2Provider::route_for_model(model), + DatabricksV2Route::AnthropicMessages, + "unexpected route for {model}" + ); + } + + assert_eq!( + DatabricksV2Provider::route_for_model("custom-model"), + DatabricksV2Route::MlflowChatCompletions + ); + } + + #[test] + fn parses_list_endpoints_response() { + let json = serde_json::json!({ + "endpoints": [ + {"name": "databricks-claude-opus-4-7"}, + {"name": "databricks-gpt-5-5"}, + {"name": "custom-model"} + ] + }); + + let models = DatabricksV2Provider::parse_list_endpoints_response(&json).unwrap(); + + assert_eq!( + models, + vec![ + "custom-model".to_string(), + "databricks-claude-opus-4-7".to_string(), + "databricks-gpt-5-5".to_string(), + ] + ); + } + + #[test] + fn errors_when_list_endpoints_response_has_no_endpoints_array() { + let json = serde_json::json!({"data": []}); + + let error = DatabricksV2Provider::parse_list_endpoints_response(&json).unwrap_err(); + + assert!(matches!(error, ProviderError::RequestFailed(_))); + assert!(error + .to_string() + .contains("Unexpected response format from Databricks AI Gateway endpoints API")); + } +} diff --git a/crates/goose/src/providers/declarative/alibaba.json b/crates/goose/src/providers/declarative/alibaba.json new file mode 100644 index 000000000000..d9867aec38f1 --- /dev/null +++ b/crates/goose/src/providers/declarative/alibaba.json @@ -0,0 +1,60 @@ +{ + "name": "alibaba", + "engine": "openai", + "display_name": "Alibaba (Qwen)", + "description": "Alibaba Qwen models via DashScope's OpenAI-compatible API.", + "api_key_env": "DASHSCOPE_API_KEY", + "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + "catalog_provider_id": "alibaba", + "dynamic_models": true, + "models": [ + { + "name": "qwen3.7-max", + "context_limit": 262144 + }, + { + "name": "qwen3.7-max-preview", + "context_limit": 262144 + }, + { + "name": "qwen3.6-max-preview", + "context_limit": 262144 + }, + { + "name": "qwen3.6-plus", + "context_limit": 1000000 + }, + { + "name": "qwen3-max", + "context_limit": 262144 + }, + { + "name": "qwen-plus", + "context_limit": 1000000 + }, + { + "name": "qwen-turbo", + "context_limit": 1000000 + }, + { + "name": "qwen-flash", + "context_limit": 1000000 + }, + { + "name": "qwen3-coder-plus", + "context_limit": 1048576 + }, + { + "name": "qwen3-coder-flash", + "context_limit": 1000000 + } + ], + "preserves_thinking": true, + "supports_streaming": true, + "model_doc_link": "https://www.alibabacloud.com/help/en/model-studio/models", + "setup_steps": [ + "Sign in to https://modelstudio.console.alibabacloud.com (international) or https://bailian.console.aliyun.com (China)", + "Open API Keys in the left sidebar and create a new key", + "Copy the key and paste it above" + ] +} diff --git a/crates/goose/src/providers/declarative/perplexity.json b/crates/goose/src/providers/declarative/perplexity.json new file mode 100644 index 000000000000..bcda3402431b --- /dev/null +++ b/crates/goose/src/providers/declarative/perplexity.json @@ -0,0 +1,21 @@ +{ + "name": "perplexity", + "engine": "openai", + "display_name": "Perplexity", + "description": "Chat models with built-in real-time web search grounding", + "api_key_env": "PERPLEXITY_API_KEY", + "base_url": "https://api.perplexity.ai", + "models": [ + { "name": "sonar", "context_limit": 128000 }, + { "name": "sonar-pro", "context_limit": 128000 }, + { "name": "sonar-reasoning", "context_limit": 128000 }, + { "name": "sonar-reasoning-pro", "context_limit": 128000 } + ], + "supports_streaming": true, + "model_doc_link": "https://docs.perplexity.ai/docs/getting-started", + "setup_steps": [ + "Go to https://www.perplexity.ai/account/api/keys", + "Create or copy an existing API key", + "Paste the key above as PERPLEXITY_API_KEY" + ] +} diff --git a/crates/goose/src/providers/formats/openai_responses.rs b/crates/goose/src/providers/formats/openai_responses.rs index ef39fb8b20e9..d5c2e15a3e35 100644 --- a/crates/goose/src/providers/formats/openai_responses.rs +++ b/crates/goose/src/providers/formats/openai_responses.rs @@ -273,6 +273,7 @@ pub struct ResponseMetadata { pub created_at: i64, pub status: String, pub model: String, + #[serde(default)] pub output: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub usage: Option, @@ -981,6 +982,47 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_responses_stream_completed_allows_missing_output() -> anyhow::Result<()> { + let lines = vec![ + r#"data: {"type":"response.created","sequence_number":1,"response":{"id":"resp_1","object":"response","created_at":1737368310,"status":"in_progress","model":"gpt-5.2-pro","output":[]}}"#.to_string(), + r#"data: {"type":"response.output_text.delta","sequence_number":2,"item_id":"msg_1","output_index":0,"content_index":0,"delta":"Hello"}"#.to_string(), + r#"data: {"type":"response.output_text.delta","sequence_number":3,"item_id":"msg_1","output_index":0,"content_index":0,"delta":" world"}"#.to_string(), + r#"data: {"type":"response.completed","sequence_number":4,"response":{"id":"resp_1","object":"response","created_at":1737368310,"status":"completed","model":"gpt-5.2-pro","usage":{"input_tokens":10,"output_tokens":4,"total_tokens":14}}}"#.to_string(), + "data: [DONE]".to_string(), + ]; + + let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); + let messages = responses_api_to_streaming_message(response_stream); + futures::pin_mut!(messages); + + let mut text_parts = Vec::new(); + let mut usage: Option = None; + + while let Some(item) = messages.next().await { + let (message, maybe_usage) = item?; + if let Some(msg) = message { + for content in msg.content { + if let MessageContent::Text(text) = content { + text_parts.push(text.text.clone()); + } + } + } + if let Some(final_usage) = maybe_usage { + usage = Some(final_usage); + } + } + + assert_eq!(text_parts.concat(), "Hello world"); + let usage = usage.expect("usage should be present at completion"); + assert_eq!(usage.model, "gpt-5.2-pro"); + assert_eq!(usage.usage.input_tokens, Some(10)); + assert_eq!(usage.usage.output_tokens, Some(4)); + assert_eq!(usage.usage.total_tokens, Some(14)); + + Ok(()) + } + #[test] fn test_responses_api_to_message_captures_reasoning_summary() -> anyhow::Result<()> { let response: ResponsesApiResponse = serde_json::from_value(serde_json::json!({ diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index cb4ef5b4b6c5..2126c624f09f 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -531,7 +531,7 @@ fn html_success() -> String { } fn html_error(error: &str) -> String { - let safe_error = v_htmlescape::escape(error).to_string(); + let safe_error = v_htmlescape::escape_fmt(error); format!( r#" diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 24364399ccf1..3340f438478f 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -21,6 +21,7 @@ use super::{ copilot_acp::CopilotAcpProvider, cursor_agent::CursorAgentProvider, databricks::DatabricksProvider, + databricks_v2::DatabricksV2Provider, gcpvertexai::GcpVertexAIProvider, gemini_cli::GeminiCliProvider, gemini_oauth::GeminiOAuthProvider, @@ -68,6 +69,7 @@ async fn init_registry() -> RwLock { registry.register::(true); registry.register::(false); registry.register::(true); + registry.register::(false); registry.register::(false); registry.register::(false); registry.register::(true); @@ -95,6 +97,10 @@ async fn init_registry() -> RwLock { "databricks", Arc::new(|| Box::pin(DatabricksProvider::cleanup())), ); + registry.set_cleanup( + "databricks_v2", + Arc::new(|| Box::pin(DatabricksV2Provider::cleanup())), + ); registry.set_cleanup( "kimi_code", Arc::new(|| Box::pin(KimiCodeProvider::cleanup())), @@ -308,6 +314,33 @@ mod tests { assert!(api_key.primary, "NEARAI_API_KEY should be primary"); } + #[tokio::test] + async fn test_alibaba_declarative_provider_registry_wiring() { + let alibaba = get_from_registry("alibaba") + .await + .expect("alibaba provider should be registered"); + let meta = alibaba.metadata(); + + assert_eq!(alibaba.provider_type(), ProviderType::Declarative); + assert!(alibaba.supports_inventory_refresh()); + assert_eq!(meta.display_name, "Alibaba (Qwen)"); + assert_eq!(meta.default_model, "qwen3.7-max"); + assert_eq!( + meta.model_doc_link, + "https://www.alibabacloud.com/help/en/model-studio/models" + ); + assert!(!meta.setup_steps.is_empty()); + + let api_key = meta + .config_keys + .iter() + .find(|k| k.name == "DASHSCOPE_API_KEY") + .expect("DASHSCOPE_API_KEY config key should exist"); + assert!(api_key.required, "DASHSCOPE_API_KEY should be required"); + assert!(api_key.secret, "DASHSCOPE_API_KEY should be secret"); + assert!(api_key.primary, "DASHSCOPE_API_KEY should be primary"); + } + #[tokio::test] async fn test_openai_compatible_providers_config_keys() { let providers_list = providers().await; diff --git a/crates/goose/src/providers/inventory/mod.rs b/crates/goose/src/providers/inventory/mod.rs index 3b76a59c4f7b..f363d6963e1a 100644 --- a/crates/goose/src/providers/inventory/mod.rs +++ b/crates/goose/src/providers/inventory/mod.rs @@ -4,6 +4,7 @@ use super::catalog::ProviderSetupCategory; use crate::config::declarative_providers::{DeclarativeProviderConfig, ProviderEngine}; use crate::config::Config; use crate::session::session_manager::SessionStorage; +use crate::utils::bytes_to_hex; use anyhow::{Context, Result}; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; @@ -126,7 +127,7 @@ impl InventoryIdentityInput { Ok(InventoryIdentity { provider_id, provider_family, - inventory_key: format!("{digest:x}"), + inventory_key: bytes_to_hex(digest), }) } } @@ -881,10 +882,10 @@ fn enrich_model_ids_with_canonical( models.push(model); } - // For databricks, prefer goose- prefixed model_ids when there are duplicates. + // For Databricks providers, prefer goose- prefixed model_ids when there are duplicates. // Re-scan: if a later model_id with "goose-" prefix maps to the same display name, // swap it in. - if provider_family == "databricks" { + if matches!(provider_family, "databricks" | "databricks_v2") { let mut name_to_idx: HashMap = HashMap::new(); for (idx, model) in models.iter().enumerate() { name_to_idx.insert(model.name.clone(), idx); @@ -1178,6 +1179,26 @@ mod tests { assert!(models[0].name.contains("Claude")); } + #[test] + fn databricks_v2_inventory_prefers_goose_model_ids_for_duplicate_names() { + let models = enrich_model_ids_with_canonical( + "databricks_v2", + &[ + "databricks-gpt-5-5".to_string(), + "goose-gpt-5-5".to_string(), + ], + ); + + assert!( + models.iter().any(|model| model.id == "goose-gpt-5-5"), + "expected goose-gpt-5-5 to win duplicate canonical-name tie, got {models:?}" + ); + assert!( + !models.iter().any(|model| model.id == "databricks-gpt-5-5"), + "expected databricks-gpt-5-5 to be replaced by goose-gpt-5-5, got {models:?}" + ); + } + #[test] fn inventory_uses_configured_models_before_first_successful_refresh() { let configured_models = [ModelInfo::new("claude-sonnet-4-5", 0)]; diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 970a35311fa2..e7229f5fdd8f 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -19,6 +19,7 @@ use async_trait::async_trait; use backend::{BackendLoadedModel, LocalInferenceBackend}; use futures::future::BoxFuture; use llamacpp::{LlamaCppBackend, LLAMACPP_BACKEND_ID}; +use local_model_registry::ChatTemplate; use rmcp::model::Tool; use serde_json::{json, Value}; use std::collections::HashMap; @@ -33,13 +34,19 @@ type ModelSlot = Arc>>>; struct ModelCacheKey { backend_id: &'static str, model_id: String, + chat_template: ChatTemplate, } impl ModelCacheKey { - fn new(backend_id: &'static str, model_id: impl Into) -> Self { + fn new( + backend_id: &'static str, + model_id: impl Into, + chat_template: ChatTemplate, + ) -> Self { Self { backend_id, model_id: model_id.into(), + chat_template, } } } @@ -49,6 +56,10 @@ pub struct InferenceRuntime { backends: HashMap<&'static str, Arc>, } +pub fn builtin_chat_template_names() -> Vec { + llamacpp::builtin_chat_template_names() +} + /// Global weak reference used to share a single `InferenceRuntime` across /// all providers and server routes. Only a `Weak` is stored — strong `Arc`s /// live in providers and `AppState`. When all strong refs drop (normal @@ -123,20 +134,12 @@ pub(super) struct ResolvedModelPaths { /// Resolve model path, context limit, settings, and mmproj path for a model ID from the registry. fn resolve_model_path(model_id: &str) -> Option { - use crate::providers::local_inference::local_model_registry::{ - default_settings_for_model, get_registry, - }; + use crate::providers::local_inference::local_model_registry::get_registry; if let Ok(registry) = get_registry().lock() { if let Some(entry) = registry.get_model(model_id) { let ctx = entry.settings.context_size.unwrap_or(0) as usize; let mut settings = entry.settings.clone(); - // Capability flags are inherent to the model family, not user-configurable. - // Re-derive them so that registry entries persisted before a model was - // recognized (or with a different quantization) still get the right behavior. - let defaults = default_settings_for_model(model_id); - settings.native_tool_calling = defaults.native_tool_calling; - settings.vision_capable = defaults.vision_capable; settings.mmproj_size_bytes = entry.mmproj_size_bytes; let mmproj_path = entry.mmproj_path.as_ref().filter(|p| p.exists()).cloned(); return Some(ResolvedModelPaths { @@ -195,6 +198,22 @@ fn build_openai_messages_json(system: &str, messages: &[Message]) -> String { serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()) } +fn build_openai_text_messages_json(system: &str, messages: &[Message]) -> String { + let mut arr: Vec = vec![json!({"role": "system", "content": system})]; + arr.extend(messages.iter().filter_map(|m| { + let content = extract_text_content(m); + if content.trim().is_empty() { + return None; + } + let role = match m.role { + rmcp::model::Role::User => "user", + rmcp::model::Role::Assistant => "assistant", + }; + Some(json!({"role": role, "content": content})) + })); + serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()) +} + /// Remove `image_url` content parts from OpenAI-format messages JSON, replacing /// each with a text note. This prevents an FFI crash in llama.cpp which does not /// accept `image_url` content-part types. @@ -422,7 +441,11 @@ impl Provider for LocalInferenceProvider { let backend = self.runtime.backend_for_model(&resolved)?; let model_context_limit = resolved.context_limit; let model_settings = resolved.settings.clone(); - let cache_key = ModelCacheKey::new(backend.id(), model_config.model_name.clone()); + let cache_key = ModelCacheKey::new( + backend.id(), + model_config.model_name.clone(), + model_settings.chat_template.clone(), + ); let model_slot = self.runtime.get_or_create_model_slot(cache_key.clone()); // Ensure model is loaded — unload any other models first to free memory. @@ -477,8 +500,8 @@ impl Provider for LocalInferenceProvider { }).collect::>(), "tools": tools.iter().map(|t| &t.name).collect::>(), "settings": { - "use_jinja": settings.use_jinja, - "native_tool_calling": settings.native_tool_calling, + "tool_calling": settings.tool_calling, + "chat_template": settings.chat_template, "context_size": settings.context_size, "sampling": settings.sampling, }, diff --git a/crates/goose/src/providers/local_inference/hf_models.rs b/crates/goose/src/providers/local_inference/hf_models.rs index 510cd63812ac..5767193ae2ea 100644 --- a/crates/goose/src/providers/local_inference/hf_models.rs +++ b/crates/goose/src/providers/local_inference/hf_models.rs @@ -42,6 +42,7 @@ pub struct HfQuantVariant { pub struct ResolvedModel { pub files: Vec, pub total_size: u64, + pub mmproj: Option, } #[derive(Debug, Deserialize)] @@ -183,6 +184,24 @@ fn parse_quantization(filename: &str) -> String { "unknown".to_string() } +fn quant_bits(quantization: &str) -> u8 { + let digits: String = quantization + .chars() + .skip_while(|c| !c.is_ascii_digit()) + .take_while(|c| c.is_ascii_digit()) + .collect(); + digits.parse().unwrap_or(0) +} + +fn mmproj_precision_preference(quantization: &str) -> u8 { + match quantization.to_uppercase().as_str() { + "BF16" => 3, + "F16" => 2, + "F32" => 1, + _ => 0, + } +} + fn looks_like_quant(s: &str) -> bool { let upper = s.to_uppercase(); upper.starts_with("Q") @@ -226,6 +245,64 @@ fn build_download_url(repo_id: &str, filename: &str) -> String { format!("{}/{}/resolve/main/{}", HF_DOWNLOAD_BASE, repo_id, filename) } +fn parent_components(filename: &str) -> Vec<&str> { + filename.rsplit_once('/').map_or(Vec::new(), |(parent, _)| { + parent.split('/').filter(|part| !part.is_empty()).collect() + }) +} + +fn is_prefix(prefix: &[&str], parts: &[&str]) -> bool { + prefix.len() <= parts.len() && prefix.iter().zip(parts).all(|(a, b)| a == b) +} + +fn select_best_mmproj( + repo_id: &str, + siblings: &[HfApiSibling], + model_filename: &str, + model_quantization: &str, +) -> Option { + let model_dir = parent_components(model_filename); + let model_bits = quant_bits(model_quantization); + + siblings + .iter() + .filter(|s| { + let lowercase = s.rfilename.to_lowercase(); + lowercase.ends_with(".gguf") && lowercase.contains("mmproj") + }) + .filter_map(|s| { + let mmproj_dir = parent_components(&s.rfilename); + if !is_prefix(&mmproj_dir, &model_dir) { + return None; + } + + let quantization = parse_quantization(&s.rfilename); + let bits = quant_bits(&quantization); + let diff = bits.abs_diff(model_bits); + let proximity = u8::MAX - diff; + + Some(( + mmproj_dir.len(), + proximity, + mmproj_precision_preference(&quantization), + s, + quantization, + )) + }) + .max_by(|a, b| { + a.0.cmp(&b.0) + .then_with(|| a.1.cmp(&b.1)) + .then_with(|| a.2.cmp(&b.2)) + .then_with(|| b.3.rfilename.cmp(&a.3.rfilename)) + }) + .map(|(_, _, _, sibling, quantization)| HfGgufFile { + filename: sibling.rfilename.clone(), + size_bytes: sibling.size.unwrap_or(0), + quantization, + download_url: build_download_url(repo_id, &sibling.rfilename), + }) +} + /// Derive the expected model filename stem from a repo_id. /// e.g. "unsloth/gemma-4-26B-A4B-it-GGUF" → "gemma-4-26b-a4b-it" (lowercased) fn model_stem_from_repo(repo_id: &str) -> String { @@ -500,7 +577,7 @@ pub async fn resolve_model_spec_full(spec: &str) -> Result<(String, ResolvedMode // Collect all GGUF files matching the quantization let matching: Vec<_> = siblings - .into_iter() + .iter() .filter(|s| { s.rfilename.ends_with(".gguf") && is_model_file(&s.rfilename, &stem) @@ -519,7 +596,7 @@ pub async fn resolve_model_spec_full(spec: &str) -> Result<(String, ResolvedMode // Separate single files from shards let mut single_files: Vec<&HfApiSibling> = Vec::new(); let mut shard_files: Vec<&HfApiSibling> = Vec::new(); - for f in &matching { + for &f in &matching { if is_shard_file(&f.rfilename) { shard_files.push(f); } else { @@ -529,6 +606,7 @@ pub async fn resolve_model_spec_full(spec: &str) -> Result<(String, ResolvedMode // Prefer single file if available if let Some(single) = single_files.first() { + let mmproj = select_best_mmproj(&repo_id, &siblings, &single.rfilename, &quant); let file = HfGgufFile { filename: single.rfilename.clone(), size_bytes: single.size.unwrap_or(0), @@ -541,6 +619,7 @@ pub async fn resolve_model_spec_full(spec: &str) -> Result<(String, ResolvedMode ResolvedModel { files: vec![file], total_size, + mmproj, }, )); } @@ -600,7 +679,16 @@ pub async fn resolve_model_spec_full(spec: &str) -> Result<(String, ResolvedMode .collect(); let total_size: u64 = files.iter().map(|f| f.size_bytes).sum(); - Ok((repo_id, ResolvedModel { files, total_size })) + let mmproj = select_best_mmproj(&repo_id, &siblings, &files[0].filename, &quant); + + Ok(( + repo_id, + ResolvedModel { + files, + total_size, + mmproj, + }, + )) } /// Resolve a model spec to a specific GGUF file from the repo. @@ -816,4 +904,92 @@ mod tests { assert_eq!(variants[1].quantization, "Q4_K_M"); assert_eq!(variants[2].quantization, "IQ1_S"); } + + #[test] + fn test_select_best_mmproj_prefers_closest_precision() { + let files = vec![ + HfApiSibling { + rfilename: "mmproj-F32.gguf".into(), + size: Some(3_000), + }, + HfApiSibling { + rfilename: "mmproj-BF16.gguf".into(), + size: Some(2_000), + }, + ]; + + let mmproj = + select_best_mmproj("someone/model-GGUF", &files, "model-Q4_K_M.gguf", "Q4_K_M") + .unwrap(); + + assert_eq!(mmproj.filename, "mmproj-BF16.gguf"); + assert_eq!(mmproj.quantization, "BF16"); + } + + #[test] + fn test_select_best_mmproj_prefers_bf16_over_f16_tie() { + let files = vec![ + HfApiSibling { + rfilename: "mmproj-F16.gguf".into(), + size: Some(2_000), + }, + HfApiSibling { + rfilename: "mmproj-BF16.gguf".into(), + size: Some(2_000), + }, + ]; + + let mmproj = + select_best_mmproj("someone/model-GGUF", &files, "model-Q8_0.gguf", "Q8_0").unwrap(); + + assert_eq!(mmproj.filename, "mmproj-BF16.gguf"); + } + + #[test] + fn test_select_best_mmproj_prefers_nearest_directory() { + let files = vec![ + HfApiSibling { + rfilename: "mmproj-BF16.gguf".into(), + size: Some(2_000), + }, + HfApiSibling { + rfilename: "Q4_K_M/mmproj-F32.gguf".into(), + size: Some(3_000), + }, + ]; + + let mmproj = select_best_mmproj( + "someone/model-GGUF", + &files, + "Q4_K_M/model-Q4_K_M.gguf", + "Q4_K_M", + ) + .unwrap(); + + assert_eq!(mmproj.filename, "Q4_K_M/mmproj-F32.gguf"); + } + + #[test] + fn test_select_best_mmproj_ignores_sibling_directories() { + let files = vec![ + HfApiSibling { + rfilename: "Q8_0/mmproj-BF16.gguf".into(), + size: Some(2_000), + }, + HfApiSibling { + rfilename: "mmproj-F32.gguf".into(), + size: Some(3_000), + }, + ]; + + let mmproj = select_best_mmproj( + "someone/model-GGUF", + &files, + "Q4_K_M/model-Q4_K_M.gguf", + "Q4_K_M", + ) + .unwrap(); + + assert_eq!(mmproj.filename, "mmproj-F32.gguf"); + } } diff --git a/crates/goose/src/providers/local_inference/llamacpp/inference_emulated_tools.rs b/crates/goose/src/providers/local_inference/llamacpp/inference_emulated_tools.rs index 1f18612a68a3..bcde00435989 100644 --- a/crates/goose/src/providers/local_inference/llamacpp/inference_emulated_tools.rs +++ b/crates/goose/src/providers/local_inference/llamacpp/inference_emulated_tools.rs @@ -22,7 +22,6 @@ use crate::conversation::message::{Message, MessageContent}; use crate::providers::errors::ProviderError; -use llama_cpp_2::model::AddBos; use rmcp::model::{CallToolRequestParams, Tool}; use serde_json::json; use std::borrow::Cow; @@ -30,8 +29,8 @@ use uuid::Uuid; use super::super::{finalize_usage, StreamSender}; use super::inference_engine::{ - create_and_prefill_context, create_and_prefill_multimodal, generation_loop, - validate_and_compute_context, GenerationContext, TokenAction, + generation_loop, prepare_generation, GenerationContext, StopSuffixTrimmer, + ThinkingOutputFilter, TokenAction, }; const SHELL_TOOL: &str = "developer__shell"; @@ -355,56 +354,26 @@ fn send_emulator_action( pub(super) fn generate_with_emulated_tools( ctx: &mut GenerationContext<'_>, code_mode_enabled: bool, + oai_messages_json: &str, ) -> Result<(), ProviderError> { - // Use oaicompat variant — its C++ wrapper catches exceptions that would - // otherwise abort the process when other native libs disturb the C++ ABI. - let prompt = ctx - .loaded - .model - .apply_chat_template_with_tools_oaicompat( - &ctx.loaded.template, - ctx.chat_messages, - None, // no tools for emulated path - None, // no json_schema - true, // add_generation_prompt - ) - .map(|r| r.prompt) - .map_err(|e| { - ProviderError::ExecutionError(format!("Failed to apply chat template: {}", e)) - })?; - - let (mut llama_ctx, prompt_token_count, effective_ctx) = if !ctx.images.is_empty() { - create_and_prefill_multimodal( - ctx.loaded, - ctx.backend, - &prompt, - ctx.images, - ctx.context_limit, - ctx.settings, - )? - } else { - let tokens = ctx - .loaded - .model - .str_to_token(&prompt, AddBos::Never) - .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; - let (ptc, ectx) = validate_and_compute_context( - ctx.loaded, - ctx.backend, - tokens.len(), - ctx.context_limit, - ctx.settings, - )?; - let lctx = - create_and_prefill_context(ctx.loaded, ctx.backend, &tokens, ectx, ctx.settings)?; - (lctx, ptc, ectx) - }; + let prepared = prepare_generation(ctx, oai_messages_json, None, None)?; + let template_result = prepared.template_result; + let mut llama_ctx = prepared.llama_ctx; + let prompt_token_count = prepared.prompt_token_count; + let effective_ctx = prepared.effective_ctx; let message_id = ctx.message_id; let tx = ctx.tx; let mut emulator_parser = StreamingEmulatorParser::new(code_mode_enabled); + let mut output_filter = ThinkingOutputFilter::new( + ctx.settings.enable_thinking, + &template_result.generation_prompt, + ); + let mut stop_trimmer = StopSuffixTrimmer::new(&template_result.additional_stops); + let mut generated_text = String::new(); let mut tool_call_emitted = false; let mut send_failed = false; + let mut stop_string_emitted = false; let output_token_count = generation_loop( &ctx.loaded.model, @@ -413,7 +382,10 @@ pub(super) fn generate_with_emulated_tools( prompt_token_count, effective_ctx, |piece| { - let actions = emulator_parser.process_chunk(piece); + generated_text.push_str(piece); + let filtered = output_filter.push_text(piece); + let (content, stop_seen) = stop_trimmer.push(&filtered.content); + let actions = emulator_parser.process_chunk(&content); for action in actions { match send_emulator_action(&action, message_id, tx) { Ok(is_tool) => { @@ -429,12 +401,47 @@ pub(super) fn generate_with_emulated_tools( } if tool_call_emitted { Ok(TokenAction::Stop) + } else if stop_seen + || template_result + .additional_stops + .iter() + .any(|stop| generated_text.ends_with(stop)) + { + stop_string_emitted = true; + Ok(TokenAction::Stop) } else { Ok(TokenAction::Continue) } }, )?; + if !send_failed { + let filtered = output_filter.finish(); + if !filtered.thinking.is_empty() { + let mut message = Message::assistant().with_thinking(filtered.thinking, ""); + message.id = Some(message_id.to_string()); + send_failed = tx.blocking_send(Ok((Some(message), None))).is_err(); + } + if !send_failed { + let content = if stop_string_emitted { + String::new() + } else { + let (content, stop_seen) = stop_trimmer.push(&filtered.content); + let mut content = content; + if !stop_seen { + content.push_str(&stop_trimmer.finish()); + } + content + }; + for action in emulator_parser.process_chunk(&content) { + if send_emulator_action(&action, message_id, tx).is_err() { + send_failed = true; + break; + } + } + } + } + if !send_failed { for action in emulator_parser.flush() { if send_emulator_action(&action, message_id, tx).is_err() { @@ -474,6 +481,50 @@ mod tests { parse_chunks(&[input], code_mode) } + fn trim_chunks(chunks: &[&str], stops: &[String]) -> (String, bool) { + let mut trimmer = StopSuffixTrimmer::new(stops); + let mut output = String::new(); + let mut stopped = false; + + for chunk in chunks { + let (content, stop_seen) = trimmer.push(chunk); + output.push_str(&content); + if stop_seen { + stopped = true; + break; + } + } + + if !stopped { + output.push_str(&trimmer.finish()); + } + + (output, stopped) + } + + fn parse_with_seeded_thinking( + chunks: &[&str], + code_mode: bool, + ) -> (String, Vec) { + let mut output_filter = ThinkingOutputFilter::new(true, "<|assistant|>\n"); + let mut parser = StreamingEmulatorParser::new(code_mode); + let mut thinking = String::new(); + let mut actions = Vec::new(); + + for chunk in chunks { + let filtered = output_filter.push_text(chunk); + thinking.push_str(&filtered.thinking); + actions.extend(parser.process_chunk(&filtered.content)); + } + + let filtered = output_filter.finish(); + thinking.push_str(&filtered.thinking); + actions.extend(parser.process_chunk(&filtered.content)); + actions.extend(parser.flush()); + + (thinking, actions) + } + fn assert_text(action: &EmulatorAction, expected: &str) { match action { EmulatorAction::Text(t) => assert_eq!(t.trim(), expected.trim(), "text mismatch"), @@ -507,6 +558,24 @@ mod tests { } } + #[test] + fn stop_suffix_trimmer_strips_split_stop() { + let stops = vec!["<|eom_id|>".to_string()]; + let (content, stopped) = trim_chunks(&["The answer", "<|e", "om_id|>"], &stops); + + assert!(stopped); + assert_eq!(content, "The answer"); + } + + #[test] + fn stop_suffix_trimmer_flushes_partial_non_stop() { + let stops = vec!["<|eom_id|>".to_string()]; + let (content, stopped) = trim_chunks(&["Use the <", " symbol"], &stops); + + assert!(!stopped); + assert_eq!(content, "Use the < symbol"); + } + #[test] fn plain_text_no_tools() { let actions = parse_all("Hello, world!", false); @@ -667,6 +736,25 @@ mod tests { assert_shell(shells[0], "echo hello"); } + #[test] + fn thinking_seeded_from_generation_prompt_is_not_emulated_text() { + let (thinking, actions) = + parse_with_seeded_thinking(&["reasoning\n$ echo hidden\nThe answer."], false); + + assert_eq!(thinking.trim(), "reasoning\n$ echo hidden"); + assert!(actions + .iter() + .all(|action| matches!(action, EmulatorAction::Text(_)))); + let text: String = actions + .iter() + .filter_map(|action| match action { + EmulatorAction::Text(text) => Some(text.as_str()), + _ => None, + }) + .collect(); + assert_eq!(text.trim(), "The answer."); + } + #[test] fn execute_block_with_multiline_code() { let input = "```execute_typescript\nasync function run() {\n const r = await Developer.shell({ command: \"ls\" });\n return r;\n}\n```\n"; diff --git a/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs b/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs index 86d2988c05bc..2a19f5fe7595 100644 --- a/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs +++ b/crates/goose/src/providers/local_inference/llamacpp/inference_engine.rs @@ -1,3 +1,4 @@ +use crate::providers::base::{FilterOut, ThinkFilter}; use crate::providers::errors::ProviderError; use crate::providers::local_inference::backend::LocalInferenceBackend; use crate::providers::local_inference::local_model_registry::ModelSettings; @@ -5,8 +6,9 @@ use crate::providers::local_inference::multimodal::ExtractedImage; use crate::providers::utils::RequestLog; use llama_cpp_2::context::params::LlamaContextParams; use llama_cpp_2::llama_batch::LlamaBatch; -use llama_cpp_2::model::{LlamaChatMessage, LlamaChatTemplate, LlamaModel}; +use llama_cpp_2::model::{AddBos, ChatTemplateResult, LlamaChatTemplate, LlamaModel}; use llama_cpp_2::mtmd::{MtmdBitmap, MtmdContext, MtmdInputText}; +use llama_cpp_2::openai::OpenAIChatTemplateParams; use llama_cpp_2::sampling::LlamaSampler; use std::num::NonZeroU32; @@ -16,7 +18,7 @@ use super::LlamaCppBackend; pub(super) struct GenerationContext<'a> { pub loaded: &'a LoadedModel, pub backend: &'a LlamaCppBackend, - pub chat_messages: &'a [LlamaChatMessage], + pub template: &'a LlamaChatTemplate, pub settings: &'a ModelSettings, pub context_limit: usize, pub model_name: String, @@ -28,11 +30,165 @@ pub(super) struct GenerationContext<'a> { pub(super) struct LoadedModel { pub model: LlamaModel, - pub template: LlamaChatTemplate, + pub templates: LoadedChatTemplates, /// Multimodal context for vision models. None for text-only models. pub mtmd_ctx: Option, } +pub(super) struct LoadedChatTemplates { + pub default: Option, + pub tool_use: Option, + pub force_default: bool, +} + +pub(super) struct PreparedGeneration<'model> { + pub template_result: ChatTemplateResult, + pub llama_ctx: llama_cpp_2::context::LlamaContext<'model>, + pub prompt_token_count: usize, + pub effective_ctx: usize, +} + +pub(super) struct ThinkingOutputFilter { + enabled: bool, + saw_structured_reasoning: bool, + think_filter: ThinkFilter, + pending_inline_thinking: String, + accumulated_thinking: String, +} + +impl ThinkingOutputFilter { + pub(super) fn new(enable_thinking: bool, generation_prompt: &str) -> Self { + let mut think_filter = ThinkFilter::new(); + if enable_thinking && !generation_prompt.is_empty() { + let _ = think_filter.push(generation_prompt); + } + + Self { + enabled: enable_thinking, + saw_structured_reasoning: false, + think_filter, + pending_inline_thinking: String::new(), + accumulated_thinking: String::new(), + } + } + + pub(super) fn push_structured_reasoning(&mut self, reasoning: &str) -> Option { + if reasoning.is_empty() { + return None; + } + + self.saw_structured_reasoning = true; + self.pending_inline_thinking.clear(); + self.think_filter = ThinkFilter::new(); + self.accumulated_thinking.push_str(reasoning); + Some(reasoning.to_string()) + } + + pub(super) fn push_text(&mut self, text: &str) -> FilterOut { + if !self.enabled { + return FilterOut { + content: text.to_string(), + thinking: String::new(), + }; + } + + let mut filtered = self.think_filter.push(text); + if self.saw_structured_reasoning { + filtered.thinking.clear(); + } else if !filtered.thinking.is_empty() { + self.pending_inline_thinking.push_str(&filtered.thinking); + filtered.thinking.clear(); + } + filtered + } + + pub(super) fn finish(&mut self) -> FilterOut { + let mut filtered = if self.enabled && !self.saw_structured_reasoning { + std::mem::take(&mut self.think_filter).finish() + } else { + FilterOut::default() + }; + + if !self.saw_structured_reasoning { + let mut thinking = std::mem::take(&mut self.pending_inline_thinking); + thinking.push_str(&filtered.thinking); + if !thinking.is_empty() { + self.accumulated_thinking.push_str(&thinking); + } + filtered.thinking = thinking; + } else { + filtered.thinking.clear(); + } + + filtered + } + + pub(super) fn accumulated_thinking(&self) -> &str { + &self.accumulated_thinking + } +} + +pub(super) struct StopSuffixTrimmer { + pending: String, + stops: Vec, +} + +impl StopSuffixTrimmer { + pub(super) fn new(stops: &[String]) -> Self { + Self { + pending: String::new(), + stops: stops + .iter() + .filter(|stop| !stop.is_empty()) + .cloned() + .collect(), + } + } + + pub(super) fn push(&mut self, chunk: &str) -> (String, bool) { + if self.stops.is_empty() { + return (chunk.to_string(), false); + } + + self.pending.push_str(chunk); + + if let Some(stop) = self + .stops + .iter() + .filter(|stop| self.pending.ends_with(stop.as_str())) + .max_by_key(|stop| stop.len()) + { + let emit_len = self.pending.len() - stop.len(); + let _stop = self.pending.split_off(emit_len); + let emit = std::mem::take(&mut self.pending); + return (emit, true); + } + + let hold_len = self + .pending + .char_indices() + .map(|(idx, _)| idx) + .chain(std::iter::once(self.pending.len())) + .filter(|idx| { + self.pending + .get(*idx..) + .is_some_and(|suffix| self.stops.iter().any(|stop| stop.starts_with(suffix))) + }) + .map(|idx| self.pending.len() - idx) + .max() + .unwrap_or(0); + + let emit_len = self.pending.len() - hold_len; + let keep = self.pending.split_off(emit_len); + let emit = std::mem::replace(&mut self.pending, keep); + (emit, false) + } + + pub(super) fn finish(&mut self) -> String { + std::mem::take(&mut self.pending) + } +} + /// Estimate the maximum context length that can fit in available accelerator/CPU /// memory based on the model's KV cache requirements. /// @@ -349,6 +505,121 @@ pub(super) fn create_and_prefill_multimodal<'model>( Ok((llama_ctx, prompt_token_count, effective_ctx)) } +pub(super) fn prepare_generation<'model>( + ctx: &mut GenerationContext<'model>, + oai_messages_json: &str, + full_tools_json: Option<&str>, + compact_tools_json: Option<&str>, +) -> Result, ProviderError> { + let apply_template = |tools: Option<&str>| { + let params = OpenAIChatTemplateParams { + messages_json: oai_messages_json, + tools_json: tools, + tool_choice: None, + json_schema: None, + grammar: None, + reasoning_format: if ctx.settings.enable_thinking { + Some("auto") + } else { + None + }, + chat_template_kwargs: None, + add_generation_prompt: true, + use_jinja: true, + parallel_tool_calls: false, + enable_thinking: ctx.settings.enable_thinking, + add_bos: false, + add_eos: false, + parse_tool_calls: true, + }; + ctx.loaded + .model + .apply_chat_template_oaicompat(ctx.template, ¶ms) + }; + + let min_generation_headroom = 512; + let n_ctx_train = ctx.loaded.model.n_ctx_train() as usize; + let mmproj_overhead = if ctx.loaded.mtmd_ctx.is_some() { + ctx.settings.mmproj_size_bytes + } else { + 0 + }; + let memory_max_ctx = + estimate_max_context_for_memory(&ctx.loaded.model, ctx.backend, mmproj_overhead); + let cap = context_cap(ctx.settings, ctx.context_limit, n_ctx_train, memory_max_ctx); + let token_budget = cap.saturating_sub(min_generation_headroom); + let estimated_image_tokens = ctx.images.len() * ctx.settings.image_token_estimate; + + let template_result = match apply_template(full_tools_json) { + Ok(r) => { + let token_count = ctx + .loaded + .model + .str_to_token(&r.prompt, AddBos::Never) + .map(|t| t.len()) + .unwrap_or(0); + if token_count + estimated_image_tokens > token_budget { + apply_template(compact_tools_json).unwrap_or(r) + } else { + r + } + } + Err(e) => { + tracing::warn!( + error = %e, + "Failed to apply llama.cpp OpenAI-compatible chat template" + ); + match apply_template(compact_tools_json) { + Ok(r) => r, + Err(compact_err) => { + return Err(ProviderError::ExecutionError(format!( + "Failed to apply chat template with llama.cpp's Jinja renderer. This usually means the selected built-in template name does not exist, the embedded or custom template is invalid, or the template is incompatible with the current message shape. Select a valid llama.cpp built-in template name, configure a custom inline Jinja template, or use a GGUF with valid tokenizer.chat_template metadata. Full tools error: {e}; compact tools error: {compact_err}" + ))); + } + } + } + }; + + let _ = ctx.log.write( + &serde_json::json!({"applied_prompt": &template_result.prompt}), + None, + ); + + let (llama_ctx, prompt_token_count, effective_ctx) = if !ctx.images.is_empty() { + create_and_prefill_multimodal( + ctx.loaded, + ctx.backend, + &template_result.prompt, + ctx.images, + ctx.context_limit, + ctx.settings, + )? + } else { + let tokens = ctx + .loaded + .model + .str_to_token(&template_result.prompt, AddBos::Never) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + let (ptc, ectx) = validate_and_compute_context( + ctx.loaded, + ctx.backend, + tokens.len(), + ctx.context_limit, + ctx.settings, + )?; + let lctx = + create_and_prefill_context(ctx.loaded, ctx.backend, &tokens, ectx, ctx.settings)?; + (lctx, ptc, ectx) + }; + + Ok(PreparedGeneration { + template_result, + llama_ctx, + prompt_token_count, + effective_ctx, + }) +} + /// Action to take after processing a generated token piece. pub(super) enum TokenAction { Continue, diff --git a/crates/goose/src/providers/local_inference/llamacpp/inference_native_tools.rs b/crates/goose/src/providers/local_inference/llamacpp/inference_native_tools.rs index 662b80244549..501ad6b8497c 100644 --- a/crates/goose/src/providers/local_inference/llamacpp/inference_native_tools.rs +++ b/crates/goose/src/providers/local_inference/llamacpp/inference_native_tools.rs @@ -1,7 +1,5 @@ use crate::conversation::message::{Message, MessageContent}; use crate::providers::errors::ProviderError; -use llama_cpp_2::model::AddBos; -use llama_cpp_2::openai::OpenAIChatTemplateParams; use rmcp::model::CallToolRequestParams; use serde_json::Value; use std::borrow::Cow; @@ -9,121 +7,27 @@ use uuid::Uuid; use super::super::finalize_usage; use super::inference_engine::{ - context_cap, create_and_prefill_context, create_and_prefill_multimodal, - estimate_max_context_for_memory, generation_loop, validate_and_compute_context, - GenerationContext, TokenAction, + generation_loop, prepare_generation, GenerationContext, StopSuffixTrimmer, + ThinkingOutputFilter, TokenAction, }; pub(super) fn generate_with_native_tools( ctx: &mut GenerationContext<'_>, - oai_messages_json: &Option, + oai_messages_json: &str, full_tools_json: Option<&str>, compact_tools: Option<&str>, ) -> Result<(), ProviderError> { - let min_generation_headroom = 512; - let n_ctx_train = ctx.loaded.model.n_ctx_train() as usize; - let mmproj_overhead = if ctx.loaded.mtmd_ctx.is_some() { - ctx.settings.mmproj_size_bytes - } else { - 0 - }; - let memory_max_ctx = - estimate_max_context_for_memory(&ctx.loaded.model, ctx.backend, mmproj_overhead); - let cap = context_cap(ctx.settings, ctx.context_limit, n_ctx_train, memory_max_ctx); - let token_budget = cap.saturating_sub(min_generation_headroom); - - let apply_template = |tools: Option<&str>| { - if let Some(ref messages_json) = oai_messages_json { - let params = OpenAIChatTemplateParams { - messages_json: messages_json.as_str(), - tools_json: tools, - tool_choice: None, - json_schema: None, - grammar: None, - reasoning_format: if ctx.settings.enable_thinking { - Some("auto") - } else { - None - }, - chat_template_kwargs: None, - add_generation_prompt: true, - use_jinja: true, - parallel_tool_calls: false, - enable_thinking: ctx.settings.enable_thinking, - add_bos: false, - add_eos: false, - parse_tool_calls: true, - }; - ctx.loaded - .model - .apply_chat_template_oaicompat(&ctx.loaded.template, ¶ms) - } else { - ctx.loaded.model.apply_chat_template_with_tools_oaicompat( - &ctx.loaded.template, - ctx.chat_messages, - tools, - None, - true, - ) - } - }; - - let estimated_image_tokens = ctx.images.len() * ctx.settings.image_token_estimate; - - let template_result = match apply_template(full_tools_json) { - Ok(r) => { - let token_count = ctx - .loaded - .model - .str_to_token(&r.prompt, AddBos::Never) - .map(|t| t.len()) - .unwrap_or(0); - if token_count + estimated_image_tokens > token_budget { - apply_template(compact_tools).unwrap_or(r) - } else { - r - } - } - Err(_) => apply_template(compact_tools).map_err(|e| { - ProviderError::ExecutionError(format!("Failed to apply chat template: {}", e)) - })?, - }; - - let _ = ctx.log.write( - &serde_json::json!({"applied_prompt": &template_result.prompt}), - None, - ); - - let (mut llama_ctx, prompt_token_count, effective_ctx) = if !ctx.images.is_empty() { - create_and_prefill_multimodal( - ctx.loaded, - ctx.backend, - &template_result.prompt, - ctx.images, - ctx.context_limit, - ctx.settings, - )? - } else { - let tokens = ctx - .loaded - .model - .str_to_token(&template_result.prompt, AddBos::Never) - .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; - let (ptc, ectx) = validate_and_compute_context( - ctx.loaded, - ctx.backend, - tokens.len(), - ctx.context_limit, - ctx.settings, - )?; - let lctx = - create_and_prefill_context(ctx.loaded, ctx.backend, &tokens, ectx, ctx.settings)?; - (lctx, ptc, ectx) - }; + let prepared = prepare_generation(ctx, oai_messages_json, full_tools_json, compact_tools)?; + let template_result = prepared.template_result; + let mut llama_ctx = prepared.llama_ctx; + let prompt_token_count = prepared.prompt_token_count; + let effective_ctx = prepared.effective_ctx; let message_id = ctx.message_id; let tx = ctx.tx; let mut generated_text = String::new(); + let mut stop_trimmer = StopSuffixTrimmer::new(&template_result.additional_stops); + let mut stop_string_emitted = false; // Initialize streaming parser — handles thinking tokens, tool calls, etc. let mut stream_parser = template_result.streaming_state_oaicompat().map_err(|e| { @@ -141,7 +45,10 @@ pub(super) fn generate_with_native_tools( // Accumulate thinking/reasoning across the entire generation so we can // attach it to the final tool-call message (mirroring what the OpenAI // streaming path does). Streaming chunks are still sent for UI display. - let mut accumulated_thinking = String::new(); + let mut output_filter = ThinkingOutputFilter::new( + ctx.settings.enable_thinking, + &template_result.generation_prompt, + ); let output_token_count = generation_loop( &ctx.loaded.model, @@ -151,6 +58,7 @@ pub(super) fn generate_with_native_tools( effective_ctx, |piece| { generated_text.push_str(piece); + let mut stop_seen = false; // Feed the new piece to the streaming parser match stream_parser.update(piece, true) { @@ -161,9 +69,10 @@ pub(super) fn generate_with_native_tools( if let Some(reasoning) = delta.get("reasoning_content").and_then(|v| v.as_str()) { - if !reasoning.is_empty() { - accumulated_thinking.push_str(reasoning); - let mut msg = Message::assistant().with_thinking(reasoning, ""); + if let Some(thinking) = + output_filter.push_structured_reasoning(reasoning) + { + let mut msg = Message::assistant().with_thinking(thinking, ""); msg.id = Some(message_id.to_string()); if tx.blocking_send(Ok((Some(msg), None))).is_err() { return Ok(TokenAction::Stop); @@ -173,10 +82,15 @@ pub(super) fn generate_with_native_tools( // Stream content text to the UI if let Some(content) = delta.get("content").and_then(|v| v.as_str()) { if !content.is_empty() { - let mut msg = Message::assistant().with_text(content); - msg.id = Some(message_id.to_string()); - if tx.blocking_send(Ok((Some(msg), None))).is_err() { - return Ok(TokenAction::Stop); + let filtered = output_filter.push_text(content); + let (content, seen) = stop_trimmer.push(&filtered.content); + stop_seen |= seen; + if !content.is_empty() { + let mut msg = Message::assistant().with_text(content); + msg.id = Some(message_id.to_string()); + if tx.blocking_send(Ok((Some(msg), None))).is_err() { + return Ok(TokenAction::Stop); + } } } } @@ -193,19 +107,26 @@ pub(super) fn generate_with_native_tools( } Err(e) => { tracing::warn!("Streaming parser error: {}", e); - let mut msg = Message::assistant().with_text(piece); - msg.id = Some(message_id.to_string()); - if tx.blocking_send(Ok((Some(msg), None))).is_err() { - return Ok(TokenAction::Stop); + let filtered = output_filter.push_text(piece); + let (content, seen) = stop_trimmer.push(&filtered.content); + stop_seen |= seen; + if !content.is_empty() { + let mut msg = Message::assistant().with_text(content); + msg.id = Some(message_id.to_string()); + if tx.blocking_send(Ok((Some(msg), None))).is_err() { + return Ok(TokenAction::Stop); + } } } } - let should_stop = template_result - .additional_stops - .iter() - .any(|stop| generated_text.ends_with(stop)); + let should_stop = stop_seen + || template_result + .additional_stops + .iter() + .any(|stop| generated_text.ends_with(stop)); if should_stop { + stop_string_emitted = true; Ok(TokenAction::Stop) } else { Ok(TokenAction::Continue) @@ -218,18 +139,22 @@ pub(super) fn generate_with_native_tools( for delta_json in final_deltas { if let Ok(delta) = serde_json::from_str::(&delta_json) { if let Some(reasoning) = delta.get("reasoning_content").and_then(|v| v.as_str()) { - if !reasoning.is_empty() { - accumulated_thinking.push_str(reasoning); - let mut msg = Message::assistant().with_thinking(reasoning, ""); + if let Some(thinking) = output_filter.push_structured_reasoning(reasoning) { + let mut msg = Message::assistant().with_thinking(thinking, ""); msg.id = Some(message_id.to_string()); let _ = tx.blocking_send(Ok((Some(msg), None))); } } if let Some(content) = delta.get("content").and_then(|v| v.as_str()) { if !content.is_empty() { - let mut msg = Message::assistant().with_text(content); - msg.id = Some(message_id.to_string()); - let _ = tx.blocking_send(Ok((Some(msg), None))); + let filtered = output_filter.push_text(content); + let (content, stop_seen) = stop_trimmer.push(&filtered.content); + stop_string_emitted |= stop_seen; + if !content.is_empty() { + let mut msg = Message::assistant().with_text(content); + msg.id = Some(message_id.to_string()); + let _ = tx.blocking_send(Ok((Some(msg), None))); + } } } if let Some(tool_calls) = delta.get("tool_calls").and_then(|v| v.as_array()) { @@ -241,6 +166,28 @@ pub(super) fn generate_with_native_tools( } } + let filtered = output_filter.finish(); + if !filtered.thinking.is_empty() { + let mut msg = Message::assistant().with_thinking(&filtered.thinking, ""); + msg.id = Some(message_id.to_string()); + let _ = tx.blocking_send(Ok((Some(msg), None))); + } + let content = if stop_string_emitted { + String::new() + } else { + let (content, stop_seen) = stop_trimmer.push(&filtered.content); + let mut content = content; + if !stop_seen { + content.push_str(&stop_trimmer.finish()); + } + content + }; + if !content.is_empty() { + let mut msg = Message::assistant().with_text(content); + msg.id = Some(message_id.to_string()); + let _ = tx.blocking_send(Ok((Some(msg), None))); + } + // Build a single message combining thinking + all tool calls, mirroring // the structure produced by the OpenAI streaming path. The agent relies // on this combined message to: @@ -250,8 +197,11 @@ pub(super) fn generate_with_native_tools( let tool_call_contents = extract_oai_tool_call_contents(&accumulated_tool_calls); if !tool_call_contents.is_empty() { let mut contents: Vec = Vec::new(); - if !accumulated_thinking.is_empty() { - contents.push(MessageContent::thinking(&accumulated_thinking, "")); + if !output_filter.accumulated_thinking().is_empty() { + contents.push(MessageContent::thinking( + output_filter.accumulated_thinking(), + "", + )); } contents.extend(tool_call_contents); let mut msg = Message::new( diff --git a/crates/goose/src/providers/local_inference/llamacpp/mod.rs b/crates/goose/src/providers/local_inference/llamacpp/mod.rs index 0b4141ad72fc..4e076f459325 100644 --- a/crates/goose/src/providers/local_inference/llamacpp/mod.rs +++ b/crates/goose/src/providers/local_inference/llamacpp/mod.rs @@ -3,35 +3,312 @@ mod inference_engine; mod inference_native_tools; use std::any::Any; +use std::ffi::CStr; use std::path::PathBuf; use anyhow::Result; use llama_cpp_2::llama_backend::LlamaBackend; use llama_cpp_2::model::params::LlamaModelParams; -use llama_cpp_2::model::{LlamaChatMessage, LlamaChatTemplate, LlamaModel}; +use llama_cpp_2::model::{ChatTemplateResult, LlamaChatTemplate, LlamaModel}; +use llama_cpp_2::openai::OpenAIChatTemplateParams; use llama_cpp_2::{list_llama_ggml_backend_devices, LlamaBackendDeviceType, LogOptions}; -use rmcp::model::Role; use self::inference_emulated_tools::{ build_emulator_tool_description, generate_with_emulated_tools, load_tiny_model_prompt, }; -use self::inference_engine::{GenerationContext, LoadedModel}; +use self::inference_engine::{GenerationContext, LoadedChatTemplates, LoadedModel}; use self::inference_native_tools::generate_with_native_tools; use crate::providers::errors::ProviderError; use crate::providers::formats::openai::format_tools; use crate::providers::local_inference::backend::{ BackendLoadedModel, LocalGenerationRequest, LocalInferenceBackend, }; +use crate::providers::local_inference::local_model_registry::{ + ChatTemplate, ModelSettings, ToolCallingMode, +}; use crate::providers::local_inference::multimodal::ExtractedImage; use crate::providers::local_inference::tool_parsing::compact_tools_json; use crate::providers::local_inference::{ - build_openai_messages_json, extract_text_content, ResolvedModelPaths, + build_openai_messages_json, build_openai_text_messages_json, ResolvedModelPaths, }; pub(super) const LLAMACPP_BACKEND_ID: &str = "llamacpp"; const CODE_EXECUTION_TOOL: &str = "code_execution__execute_typescript"; +pub(super) fn builtin_chat_template_names() -> Vec { + let count = unsafe { llama_cpp_sys_2::llama_chat_builtin_templates(std::ptr::null_mut(), 0) }; + if count <= 0 { + return Vec::new(); + } + + let mut templates = vec![std::ptr::null(); count as usize]; + let written = unsafe { + llama_cpp_sys_2::llama_chat_builtin_templates(templates.as_mut_ptr(), templates.len()) + }; + templates.truncate(written.max(0) as usize); + + templates + .into_iter() + .filter(|ptr| !ptr.is_null()) + .filter_map(|ptr| { + unsafe { CStr::from_ptr(ptr) } + .to_str() + .ok() + .map(str::to_string) + }) + .collect() +} + +fn template_result_supports_native_tool_calling(result: &ChatTemplateResult) -> bool { + result.parse_tool_calls + && result + .parser + .as_deref() + .is_some_and(|parser| !parser.trim().is_empty()) +} + +fn supports_native_tool_calling( + loaded: &LoadedModel, + settings: &ModelSettings, + template: &LlamaChatTemplate, + oai_messages_json: &str, + tools_json: Option<&str>, +) -> bool { + let Some(tools_json) = tools_json.filter(|tools| !tools.trim().is_empty()) else { + return false; + }; + + // llama.cpp exposes common_chat_templates_get_caps in C++, but llama-cpp-2 + // 0.1.146 does not bind it yet. Replace this dry-run with that capability + // map once it is available through the Rust wrapper. + let params = OpenAIChatTemplateParams { + messages_json: oai_messages_json, + tools_json: Some(tools_json), + tool_choice: None, + json_schema: None, + grammar: None, + reasoning_format: if settings.enable_thinking { + Some("auto") + } else { + None + }, + chat_template_kwargs: None, + add_generation_prompt: true, + use_jinja: true, + parallel_tool_calls: false, + enable_thinking: settings.enable_thinking, + add_bos: false, + add_eos: false, + parse_tool_calls: true, + }; + + match loaded + .model + .apply_chat_template_oaicompat(template, ¶ms) + { + Ok(result) => template_result_supports_native_tool_calling(&result), + Err(e) => { + tracing::debug!( + error = %e, + "llama.cpp chat template dry-run did not support native tool calling" + ); + false + } + } +} + +fn should_use_native_tool_calling( + mode: ToolCallingMode, + has_tools: bool, + template_supports_native: bool, +) -> bool { + has_tools + && match mode { + ToolCallingMode::Auto => template_supports_native, + ToolCallingMode::ForceNative => true, + ToolCallingMode::ForceEmulated => false, + } +} + +fn is_legacy_builtin_template_name(template: &str) -> bool { + matches!( + template.trim(), + "bailing" + | "bailing-think" + | "bailing2" + | "chatglm3" + | "chatglm4" + | "command-r" + | "deepseek" + | "deepseek-ocr" + | "deepseek2" + | "deepseek3" + | "exaone-moe" + | "exaone3" + | "exaone4" + | "falcon3" + | "gemma" + | "gigachat" + | "glmedge" + | "gpt-oss" + | "granite" + | "granite-4.0" + | "grok-2" + | "hunyuan-dense" + | "hunyuan-moe" + | "hunyuan-ocr" + | "kimi-k2" + | "llama2" + | "llama2-sys" + | "llama2-sys-bos" + | "llama2-sys-strip" + | "llama3" + | "llama4" + | "megrez" + | "minicpm" + | "mistral-v1" + | "mistral-v3" + | "mistral-v3-tekken" + | "mistral-v7" + | "mistral-v7-tekken" + | "monarch" + | "openchat" + | "orion" + | "pangu-embedded" + | "phi3" + | "phi4" + | "rwkv-world" + | "seed_oss" + | "smolvlm" + | "solar-open" + | "vicuna" + | "vicuna-orca" + | "yandex" + | "zephyr" + ) +} + +fn missing_chat_template_error( + model_id: &str, + architecture: Option<&str>, + context: &str, + has_tool_use_template: bool, +) -> ProviderError { + let architecture = architecture + .map(str::trim) + .filter(|arch| !arch.is_empty()) + .map(|arch| format!(" Detected GGUF general.architecture={arch}.")) + .unwrap_or_default(); + let tool_use_note = if has_tool_use_template { + " A named tool_use chat template is present, but that template is only used for native tool calls with tools present." + } else { + "" + }; + + ProviderError::ExecutionError(format!( + "Model {model_id} does not contain GGUF tokenizer.chat_template metadata required for {context}.{architecture}{tool_use_note} \ + Goose cannot safely infer the correct prompt format from architecture alone. Select a \ + llama.cpp built-in chat template name, configure a custom inline chat template containing \ + the full Jinja template source, or use a GGUF that includes tokenizer.chat_template metadata." + )) +} + +fn load_chat_templates( + model: &LlamaModel, + settings: &ModelSettings, +) -> Result { + match &settings.chat_template { + ChatTemplate::Embedded => Ok(LoadedChatTemplates { + default: model.chat_template(None).ok(), + tool_use: model.chat_template(Some("tool_use")).ok(), + force_default: false, + }), + ChatTemplate::Builtin { name } => { + let trimmed = name.trim(); + if trimmed.is_empty() { + return Err(ProviderError::ExecutionError( + "Built-in chat template name is empty. Enter a llama.cpp built-in template name such as 'chatml', or use embedded chat template metadata.".to_string(), + )); + } + LlamaChatTemplate::new(trimmed) + .map_err(|e| { + ProviderError::ExecutionError(format!( + "Built-in chat template name contains an invalid NUL byte: {e}" + )) + }) + .map(|template| LoadedChatTemplates { + default: Some(template), + tool_use: None, + force_default: true, + }) + } + ChatTemplate::CustomInline { template } => { + let trimmed = template.trim(); + if trimmed.is_empty() { + return Err(ProviderError::ExecutionError( + "Custom inline chat template is empty. Paste the full Jinja chat template source, use a llama.cpp built-in template name, or use embedded chat template metadata.".to_string(), + )); + } + if trimmed == "chatml" || is_legacy_builtin_template_name(trimmed) { + return Err(ProviderError::ExecutionError(format!( + "Custom inline chat template is set to '{trimmed}', which is a llama.cpp template name rather than Jinja template source. Paste the full Jinja chat template source instead, or select Built-in and enter '{trimmed}' if that built-in template is intended." + ))); + } + LlamaChatTemplate::new(template) + .map_err(|e| { + ProviderError::ExecutionError(format!( + "Custom inline chat template contains an invalid NUL byte: {e}" + )) + }) + .map(|template| LoadedChatTemplates { + default: Some(template), + tool_use: None, + force_default: true, + }) + } + } +} + +fn select_generation_template<'a>( + model_id: &str, + model: &LlamaModel, + templates: &'a LoadedChatTemplates, + native_tool_calling: bool, + has_tools: bool, +) -> Result<&'a LlamaChatTemplate, ProviderError> { + if templates.force_default { + return templates.default.as_ref().ok_or_else(|| { + ProviderError::ExecutionError( + "Configured chat template was not loaded correctly".to_string(), + ) + }); + } + + if native_tool_calling && has_tools { + if let Some(template) = templates.tool_use.as_ref() { + return Ok(template); + } + } + + templates.default.as_ref().ok_or_else(|| { + let architecture = model.meta_val_str("general.architecture").ok(); + let context = if has_tools && native_tool_calling { + "native tool calling because no tool_use template is available" + } else if has_tools { + "emulated tool calling" + } else { + "chat without tools" + }; + missing_chat_template_error( + model_id, + architecture.as_deref(), + context, + templates.tool_use.is_some(), + ) + }) +} + pub(super) struct LlamaCppBackend { backend: LlamaBackend, } @@ -134,18 +411,7 @@ impl LocalInferenceBackend for LlamaCppBackend { let model = LlamaModel::load_from_file(&self.backend, model_path, ¶ms) .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; - let template = match model.chat_template(None) { - Ok(t) => t, - Err(_) => { - tracing::warn!("Model has no embedded chat template, falling back to chatml"); - LlamaChatTemplate::new("chatml").map_err(|e| { - ProviderError::ExecutionError(format!( - "Failed to create fallback chat template: {}", - e - )) - })? - } - }; + let templates = load_chat_templates(&model, settings)?; let mtmd_ctx = Self::init_mtmd_context(&model, &resolved.mmproj_path, settings); @@ -157,7 +423,7 @@ impl LocalInferenceBackend for LlamaCppBackend { Ok(Box::new(LoadedModel { model, - template, + templates, mtmd_ctx, })) } @@ -174,14 +440,6 @@ impl LocalInferenceBackend for LlamaCppBackend { ProviderError::ExecutionError("Loaded model backend mismatch".to_string()) })?; - let native_tool_calling = request.settings.native_tool_calling; - let use_emulator = !native_tool_calling && !request.tools.is_empty(); - let system_prompt = if use_emulator { - load_tiny_model_prompt() - } else { - request.system.to_string() - }; - let has_vision = request.resolved_model.mmproj_path.is_some(); let marker = llama_cpp_2::mtmd::mtmd_default_marker(); let (images, vision_messages): (Vec, Option>) = if has_vision { @@ -193,45 +451,8 @@ impl LocalInferenceBackend for LlamaCppBackend { }; let effective_messages = vision_messages.as_deref().unwrap_or(request.messages); - let mut chat_messages = - vec![ - LlamaChatMessage::new("system".to_string(), system_prompt.clone()).map_err( - |e| { - ProviderError::ExecutionError(format!( - "Failed to create system message: {}", - e - )) - }, - )?, - ]; - let code_mode_enabled = request.tools.iter().any(|t| t.name == CODE_EXECUTION_TOOL); - - if use_emulator && !request.tools.is_empty() { - let tool_desc = build_emulator_tool_description(request.tools, code_mode_enabled); - chat_messages = vec![LlamaChatMessage::new( - "system".to_string(), - format!("{}{}", system_prompt, tool_desc), - ) - .map_err(|e| { - ProviderError::ExecutionError(format!("Failed to create system message: {}", e)) - })?]; - } - - for msg in effective_messages { - let role = match msg.role { - Role::User => "user", - Role::Assistant => "assistant", - }; - let content = extract_text_content(msg); - if !content.trim().is_empty() { - chat_messages.push(LlamaChatMessage::new(role.to_string(), content).map_err( - |e| ProviderError::ExecutionError(format!("Failed to create message: {}", e)), - )?); - } - } - - let (full_tools_json, compact_tools) = if !use_emulator && !request.tools.is_empty() { + let (full_tools_json, compact_tools) = if !request.tools.is_empty() { let full = format_tools(request.tools) .ok() .and_then(|spec| serde_json::to_string(&spec).ok()); @@ -241,13 +462,53 @@ impl LocalInferenceBackend for LlamaCppBackend { (None, None) }; - let oai_messages_json = if request.settings.use_jinja || native_tool_calling { - Some(build_openai_messages_json( - &system_prompt, - effective_messages, - )) + let has_native_tool_payload = full_tools_json + .as_deref() + .is_some_and(|tools| !tools.trim().is_empty()); + let template_supports_native = + if matches!(request.settings.tool_calling, ToolCallingMode::Auto) + && has_native_tool_payload + { + let messages_json = build_openai_messages_json(request.system, effective_messages); + if let Some(template) = loaded.templates.tool_use.as_ref() { + supports_native_tool_calling( + loaded, + request.settings, + template, + &messages_json, + full_tools_json.as_deref(), + ) + } else { + loaded.templates.default.as_ref().is_some_and(|template| { + supports_native_tool_calling( + loaded, + request.settings, + template, + &messages_json, + full_tools_json.as_deref(), + ) + }) + } + } else { + false + }; + let native_tool_calling = should_use_native_tool_calling( + request.settings.tool_calling, + !request.tools.is_empty(), + template_supports_native, + ); + let use_emulator = !native_tool_calling && !request.tools.is_empty(); + let system_prompt = if use_emulator { + let tool_desc = build_emulator_tool_description(request.tools, code_mode_enabled); + format!("{}{}", load_tiny_model_prompt(), tool_desc) } else { - None + request.system.to_string() + }; + + let oai_messages_json = if use_emulator { + build_openai_text_messages_json(&system_prompt, effective_messages) + } else { + build_openai_messages_json(&system_prompt, effective_messages) }; if !images.is_empty() && loaded.mtmd_ctx.is_none() { @@ -258,10 +519,18 @@ impl LocalInferenceBackend for LlamaCppBackend { ); } + let template = select_generation_template( + &request.model_name, + &loaded.model, + &loaded.templates, + native_tool_calling, + !request.tools.is_empty(), + )?; + let mut gen_ctx = GenerationContext { loaded, backend: self, - chat_messages: &chat_messages, + template, settings: request.settings, context_limit: request.context_limit, model_name: request.model_name, @@ -272,7 +541,7 @@ impl LocalInferenceBackend for LlamaCppBackend { }; if use_emulator { - generate_with_emulated_tools(&mut gen_ctx, code_mode_enabled) + generate_with_emulated_tools(&mut gen_ctx, code_mode_enabled, &oai_messages_json) } else { generate_with_native_tools( &mut gen_ctx, @@ -353,3 +622,78 @@ fn log_inference_backend_devices() { ); } } + +#[cfg(test)] +mod tests { + use super::*; + + fn template_result(parser: Option<&str>, parse_tool_calls: bool) -> ChatTemplateResult { + ChatTemplateResult { + prompt: String::new(), + grammar: None, + grammar_lazy: false, + grammar_triggers: Vec::new(), + preserved_tokens: Vec::new(), + additional_stops: Vec::new(), + chat_format: 0, + parser: parser.map(str::to_string), + generation_prompt: String::new(), + parse_tool_calls, + } + } + + #[test] + fn native_tool_calling_requires_generated_parser() { + assert!(template_result_supports_native_tool_calling( + &template_result(Some("parser"), true) + )); + assert!(!template_result_supports_native_tool_calling( + &template_result(None, true) + )); + assert!(!template_result_supports_native_tool_calling( + &template_result(Some("parser"), false) + )); + assert!(!template_result_supports_native_tool_calling( + &template_result(Some(" "), true) + )); + } + + #[test] + fn tool_calling_mode_controls_path_selection() { + assert!(should_use_native_tool_calling( + ToolCallingMode::Auto, + true, + true + )); + assert!(!should_use_native_tool_calling( + ToolCallingMode::Auto, + true, + false + )); + assert!(should_use_native_tool_calling( + ToolCallingMode::ForceNative, + true, + false + )); + assert!(!should_use_native_tool_calling( + ToolCallingMode::ForceEmulated, + true, + true + )); + assert!(!should_use_native_tool_calling( + ToolCallingMode::ForceNative, + false, + true + )); + } + + #[test] + fn rejects_legacy_builtin_names_as_inline_templates() { + assert!(is_legacy_builtin_template_name("gemma")); + assert!(is_legacy_builtin_template_name("llama3")); + assert!(!is_legacy_builtin_template_name("chatml")); + assert!(!is_legacy_builtin_template_name( + "{% for message in messages %}{{ message.content }}{% endfor %}" + )); + } +} diff --git a/crates/goose/src/providers/local_inference/local_model_registry.rs b/crates/goose/src/providers/local_inference/local_model_registry.rs index b6a4a6f306f7..abe9ee635568 100644 --- a/crates/goose/src/providers/local_inference/local_model_registry.rs +++ b/crates/goose/src/providers/local_inference/local_model_registry.rs @@ -36,6 +36,29 @@ impl Default for SamplingConfig { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ToolCallingMode { + #[default] + Auto, + ForceNative, + ForceEmulated, +} + +#[derive(Debug, Clone, Default, Hash, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChatTemplate { + #[serde(alias = "auto")] + #[default] + Embedded, + Builtin { + name: String, + }, + CustomInline { + template: String, + }, +} + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ModelSettings { pub context_size: Option, @@ -57,13 +80,13 @@ pub struct ModelSettings { pub flash_attention: Option, pub n_threads: Option, #[serde(default)] - pub native_tool_calling: bool, + pub tool_calling: ToolCallingMode, #[serde(default)] - pub use_jinja: bool, + pub chat_template: ChatTemplate, #[serde(default = "default_true")] pub enable_thinking: bool, /// Whether this model architecture supports vision input. - /// Derived from the featured model table, not user-configurable. + /// Derived from associated mmproj metadata, not user-configurable. #[serde(default)] pub vision_capable: bool, /// Estimated tokens per image for budget planning before mtmd tokenization. @@ -106,8 +129,8 @@ impl Default for ModelSettings { use_mlock: false, flash_attention: None, n_threads: None, - native_tool_calling: false, - use_jinja: false, + tool_calling: ToolCallingMode::Auto, + chat_template: ChatTemplate::Embedded, enable_thinking: true, vision_capable: false, image_token_estimate: default_image_token_estimate(), @@ -116,100 +139,43 @@ impl Default for ModelSettings { } } -/// HuggingFace repo + filename for multimodal projection weights (vision encoder). -pub struct MmprojSpec { - pub repo: &'static str, - pub filename: &'static str, -} - -impl MmprojSpec { - /// Local path for this mmproj, namespaced by repo to avoid collisions - /// between different models that use the same filename. - pub fn local_path(&self) -> std::path::PathBuf { - let repo_name = self.repo.split('/').next_back().unwrap_or(self.repo); - Paths::in_data_dir("models") - .join(repo_name) - .join(self.filename) - } -} - pub struct FeaturedModel { /// HuggingFace spec in "author/repo-GGUF:quantization" format. pub spec: &'static str, - /// Whether this model's GGUF template supports native tool calling via llama.cpp. - pub native_tool_calling: bool, - /// Multimodal projection weights spec. None for text-only models. - pub mmproj: Option, } pub const FEATURED_MODELS: &[FeaturedModel] = &[ FeaturedModel { spec: "bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M", - native_tool_calling: false, - mmproj: None, }, FeaturedModel { spec: "bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M", - native_tool_calling: false, - mmproj: None, }, FeaturedModel { spec: "bartowski/Hermes-2-Pro-Mistral-7B-GGUF:Q4_K_M", - native_tool_calling: false, - mmproj: None, }, FeaturedModel { spec: "bartowski/Mistral-Small-24B-Instruct-2501-GGUF:Q4_K_M", - native_tool_calling: false, - mmproj: None, }, FeaturedModel { spec: "unsloth/gemma-4-E4B-it-GGUF:Q4_K_M", - native_tool_calling: true, - mmproj: Some(MmprojSpec { - repo: "unsloth/gemma-4-E4B-it-GGUF", - filename: "mmproj-BF16.gguf", - }), }, FeaturedModel { spec: "unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M", - native_tool_calling: true, - mmproj: Some(MmprojSpec { - repo: "unsloth/gemma-4-26B-A4B-it-GGUF", - filename: "mmproj-BF16.gguf", - }), }, ]; -pub fn default_settings_for_model(model_id: &str) -> ModelSettings { - use super::hf_models::parse_model_spec; - let model_repo = model_id.split(':').next().unwrap_or(model_id); - let featured = FEATURED_MODELS.iter().find(|m| { - if let Ok((repo_id, _quant)) = parse_model_spec(m.spec) { - repo_id == model_repo - } else { - false - } - }); +pub fn default_settings_for_model(_model_id: &str) -> ModelSettings { ModelSettings { - native_tool_calling: featured.is_some_and(|m| m.native_tool_calling), - vision_capable: featured.is_some_and(|m| m.mmproj.is_some()), ..ModelSettings::default() } } -/// Look up the `MmprojSpec` for a featured model by its model ID. -pub fn featured_mmproj_spec(model_id: &str) -> Option<&'static MmprojSpec> { - use super::hf_models::parse_model_spec; - let model_repo = model_id.split(':').next().unwrap_or(model_id); - FEATURED_MODELS.iter().find_map(|m| { - if let Ok((repo_id, _quant)) = parse_model_spec(m.spec) { - if repo_id == model_repo { - return m.mmproj.as_ref(); - } - } - None - }) +/// Local path for an mmproj file, namespaced by repo to avoid collisions +/// between different models that use the same filename. +pub fn mmproj_local_path(repo_id: &str, filename: &str) -> PathBuf { + let repo_name = repo_id.split('/').next_back().unwrap_or(repo_id); + Paths::in_data_dir("models").join(repo_name).join(filename) } /// Check if a model ID corresponds to a featured model. @@ -263,32 +229,27 @@ pub struct LocalModelEntry { #[serde(default)] pub mmproj_size_bytes: u64, #[serde(default)] + pub mmproj_checked: bool, + #[serde(default)] pub shard_files: Vec, } impl LocalModelEntry { - /// Populate mmproj metadata and vision settings from the featured model - /// table if this model's repo has a known vision encoder. - pub fn enrich_with_featured_mmproj(&mut self) { - if let Some(mmproj) = featured_mmproj_spec(&self.id) { - let path = mmproj.local_path(); - if self.mmproj_path.as_ref() != Some(&path) { - self.mmproj_path = Some(path.clone()); - self.mmproj_source_url = Some(format!( - "https://huggingface.co/{}/resolve/main/{}", - mmproj.repo, mmproj.filename - )); - } + pub fn refresh_mmproj_metadata(&mut self) { + self.settings.vision_capable = self.mmproj_path.is_some(); + if let Some(path) = &self.mmproj_path { + self.mmproj_checked = true; self.settings.vision_capable = true; if self.mmproj_size_bytes == 0 || self.settings.mmproj_size_bytes == 0 { - if let Ok(meta) = std::fs::metadata(&path) { + if let Ok(meta) = std::fs::metadata(path) { self.mmproj_size_bytes = meta.len(); self.settings.mmproj_size_bytes = meta.len(); } } + } else { + self.mmproj_size_bytes = 0; + self.settings.mmproj_size_bytes = 0; } - let defaults = default_settings_for_model(&self.id); - self.settings.native_tool_calling = defaults.native_tool_calling; } pub fn is_downloaded(&self) -> bool { @@ -436,7 +397,7 @@ impl LocalModelRegistry { for mut entry in featured_entries { if !self.models.iter().any(|m| m.id == entry.id) { - entry.enrich_with_featured_mmproj(); + entry.refresh_mmproj_metadata(); self.models.push(entry); changed = true; } @@ -455,7 +416,7 @@ impl LocalModelRegistry { } pub fn add_model(&mut self, mut entry: LocalModelEntry) -> Result<()> { - entry.enrich_with_featured_mmproj(); + entry.refresh_mmproj_metadata(); if let Some(existing) = self.models.iter_mut().find(|m| m.id == entry.id) { *existing = entry; } else { diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 44f7dd0a96a5..f5e4f952001d 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -19,6 +19,8 @@ pub mod codex_acp; pub mod copilot_acp; pub mod cursor_agent; pub mod databricks; +pub mod databricks_auth; +pub mod databricks_v2; pub mod embedding; pub mod errors; pub mod formats; diff --git a/crates/goose/src/providers/oauth.rs b/crates/goose/src/providers/oauth.rs index e27ba808bee2..432dd014c22b 100644 --- a/crates/goose/src/providers/oauth.rs +++ b/crates/goose/src/providers/oauth.rs @@ -1,4 +1,5 @@ use crate::config::paths::Paths; +use crate::utils::bytes_to_hex; use anyhow::Result; use axum::{extract::Query, response::Html, routing::get, Router}; use base64::Engine; @@ -56,7 +57,7 @@ impl TokenCache { hasher.update(host.as_bytes()); hasher.update(client_id.as_bytes()); hasher.update(scopes.join(",").as_bytes()); - let hash = format!("{:x}", hasher.finalize()); + let hash = bytes_to_hex(hasher.finalize()); fs::create_dir_all(get_base_path()).unwrap(); let cache_path = get_base_path().join(format!("{}.json", hash)); diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 89fe5424cf2d..d05e15113170 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -8,6 +8,7 @@ use aws_sdk_bedrockruntime::config::ProvideCredentials; use aws_sdk_sagemakerruntime::Client as SageMakerClient; use rmcp::model::Tool; use serde_json::{json, Value}; +use smithy_transport_reqwest::ReqwestHttpClient; use super::base::{ ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, ProviderUsage, Usage, @@ -61,7 +62,10 @@ impl SageMakerTgiProvider { set_aws_env_vars(config.all_values()); set_aws_env_vars(config.all_secrets()); - let aws_config = aws_config::load_from_env().await; + let aws_config = aws_config::from_env() + .http_client(ReqwestHttpClient::new()) + .load() + .await; // Validate credentials aws_config diff --git a/crates/goose/src/providers/testprovider.rs b/crates/goose/src/providers/testprovider.rs index 5f58c2f9c7e7..e06d1f27057f 100644 --- a/crates/goose/src/providers/testprovider.rs +++ b/crates/goose/src/providers/testprovider.rs @@ -13,6 +13,7 @@ use super::base::{MessageStream, Provider, ProviderDef, ProviderMetadata, Provid use super::errors::ProviderError; use crate::conversation::message::{Message, ToolResponse}; use crate::model::ModelConfig; +use crate::utils::bytes_to_hex; use futures::future::BoxFuture; use rmcp::model::{CallToolResult, Tool}; @@ -111,7 +112,7 @@ impl TestProvider { let serialized = serde_json::to_string(&stable_messages).unwrap_or_default(); let mut hasher = Sha256::new(); hasher.update(serialized.as_bytes()); - format!("{:x}", hasher.finalize()) + bytes_to_hex(hasher.finalize()) } fn load_records(file_path: &str) -> Result> { diff --git a/crates/goose/src/session/chat_history_search.rs b/crates/goose/src/session/chat_history_search.rs index c06abb846459..22a89807e792 100644 --- a/crates/goose/src/session/chat_history_search.rs +++ b/crates/goose/src/session/chat_history_search.rs @@ -302,3 +302,137 @@ impl<'a> ChatHistorySearch<'a> { } } } + +pub(crate) struct ChatSessionSearch<'a> { + pool: &'a Pool, + query: &'a str, + limit: usize, + after_date: Option>, + before_date: Option>, + exclude_session_id: Option, + session_types: Vec, +} + +impl<'a> ChatSessionSearch<'a> { + pub fn new( + pool: &'a Pool, + query: &'a str, + limit: Option, + after_date: Option>, + before_date: Option>, + exclude_session_id: Option, + session_types: Vec, + ) -> Self { + Self { + pool, + query, + limit: limit.unwrap_or(10), + after_date, + before_date, + exclude_session_id, + session_types, + } + } + + pub async fn execute(self) -> Result> { + let keywords = self.parse_keywords(); + if keywords.is_empty() { + return Ok(Vec::new()); + } + + let sql = self.build_sql(&keywords); + let mut query_builder = sqlx::query_scalar::<_, String>(&sql); + + for keyword in &keywords { + query_builder = query_builder.bind(keyword); + } + + if let Some(after) = self.after_date { + query_builder = query_builder.bind(after); + } + if let Some(before) = self.before_date { + query_builder = query_builder.bind(before); + } + + if let Some(exclude_id) = &self.exclude_session_id { + query_builder = query_builder.bind(exclude_id); + } + + for t in &self.session_types { + query_builder = query_builder.bind(t.to_string()); + } + + query_builder = query_builder.bind(self.limit as i64); + + Ok(query_builder.fetch_all(self.pool).await?) + } + + fn parse_keywords(&self) -> Vec { + self.query + .split_whitespace() + .map(|word| format!("%{}%", word.to_lowercase())) + .collect() + } + + fn build_sql(&self, keywords: &[String]) -> String { + let mut sql = String::from( + r#" + SELECT s.id + FROM sessions s + WHERE EXISTS ( + SELECT 1 + FROM messages m + WHERE m.session_id = s.id + AND EXISTS ( + SELECT 1 FROM json_each(m.content_json) + WHERE json_extract(value, '$.type') = 'text' + AND ( + "#, + ); + + for (i, _) in keywords.iter().enumerate() { + if i > 0 { + sql.push_str(" OR "); + } + sql.push_str("LOWER(json_extract(value, '$.text')) LIKE ?"); + } + + sql.push_str( + r#" + ) + ) + "#, + ); + + if self.after_date.is_some() { + sql.push_str(" AND m.timestamp >= ?"); + } + if self.before_date.is_some() { + sql.push_str(" AND m.timestamp <= ?"); + } + + sql.push_str( + r#" + ) + "#, + ); + + if self.exclude_session_id.is_some() { + sql.push_str(" AND s.id != ?"); + } + + if !self.session_types.is_empty() { + let placeholders: String = self + .session_types + .iter() + .map(|_| "?") + .collect::>() + .join(", "); + sql.push_str(&format!(" AND s.session_type IN ({})", placeholders)); + } + + sql.push_str(" ORDER BY s.updated_at DESC, s.id DESC LIMIT ?"); + + sql + } +} diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index bf58fa8970db..8f4bb474cf12 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -2,6 +2,7 @@ mod chat_history_search; mod diagnostics; pub mod extension_data; mod legacy; +#[cfg(feature = "nostr")] pub mod nostr_share; pub mod session_manager; diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 6b04183d3ed5..20caf461d609 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -472,6 +472,27 @@ impl SessionManager { .await } + pub async fn search_chat_sessions( + &self, + query: &str, + limit: Option, + after_date: Option>, + before_date: Option>, + exclude_session_id: Option, + session_types: Vec, + ) -> Result> { + self.storage + .search_chat_sessions( + query, + limit, + after_date, + before_date, + exclude_session_id, + session_types, + ) + .await + } + pub async fn update_message_metadata(id: &str, message_id: &str, f: F) -> Result<()> where F: FnOnce( @@ -1845,6 +1866,41 @@ impl SessionStorage { .await } + async fn search_chat_sessions( + &self, + query: &str, + limit: Option, + after_date: Option>, + before_date: Option>, + exclude_session_id: Option, + session_types: Vec, + ) -> Result> { + use crate::session::chat_history_search::ChatSessionSearch; + + let pool = self.pool().await?; + let session_ids = ChatSessionSearch::new( + pool, + query, + limit, + after_date, + before_date, + exclude_session_id, + session_types, + ) + .execute() + .await?; + + let mut sessions = Vec::with_capacity(session_ids.len()); + for session_id in session_ids { + match self.get_session(&session_id, false).await { + Ok(session) => sessions.push(session), + Err(err) if err.to_string() == "Session not found" => continue, + Err(err) => return Err(err), + } + } + Ok(sessions) + } + async fn update_message_metadata( &self, session_id: &str, @@ -2015,6 +2071,219 @@ mod tests { } } + async fn add_message_at(sm: &SessionManager, session_id: &str, text: &str, timestamp: &str) { + sm.add_message(session_id, &Message::user().with_text(text)) + .await + .unwrap(); + + let pool = sm.storage().pool().await.unwrap(); + let timestamp = chrono::DateTime::parse_from_rfc3339(timestamp).unwrap(); + let timestamp_string = timestamp.format("%Y-%m-%d %H:%M:%S").to_string(); + + sqlx::query( + "UPDATE messages SET timestamp = ?, created_timestamp = ? WHERE id = (SELECT MAX(id) FROM messages WHERE session_id = ?)", + ) + .bind(×tamp_string) + .bind(timestamp.timestamp()) + .bind(session_id) + .execute(pool) + .await + .unwrap(); + } + + async fn create_search_session( + sm: &SessionManager, + name: &str, + session_type: SessionType, + updated_at: &str, + messages: &[(&str, &str)], + ) -> String { + let session = sm + .create_session( + PathBuf::from("/tmp/search-test"), + name.to_string(), + session_type, + GooseMode::default(), + ) + .await + .unwrap(); + + for (text, timestamp) in messages { + add_message_at(sm, &session.id, text, timestamp).await; + } + set_sessions_updated_at(sm, std::slice::from_ref(&session.id), updated_at).await; + + session.id + } + + #[tokio::test] + async fn test_search_chat_history_preserves_message_limited_behavior() { + let temp_dir = TempDir::new().unwrap(); + let sm = SessionManager::new(temp_dir.path().to_path_buf()); + + let _older_target = create_search_session( + &sm, + "Older target", + SessionType::User, + "2026-05-01T00:00:00Z", + &[( + "does Acme have an email address for John Doe", + "2026-05-01T00:00:00Z", + )], + ) + .await; + + let newer_noise = create_search_session( + &sm, + "Newer noise", + SessionType::User, + "2026-05-22T00:00:00Z", + &[ + ("Acme person name looking for Acme", "2026-05-22T00:00:00Z"), + ( + "another Acme person name looking for Acme", + "2026-05-22T00:01:00Z", + ), + ], + ) + .await; + + let results = sm + .search_chat_history("Acme", Some(2), None, None, None, vec![SessionType::User]) + .await + .unwrap(); + + assert_eq!(results.results.len(), 1); + assert_eq!(results.results[0].session_id, newer_noise); + assert_eq!(results.results[0].messages.len(), 2); + } + + #[tokio::test] + async fn test_search_chat_sessions_limits_distinct_sessions() { + let temp_dir = TempDir::new().unwrap(); + let sm = SessionManager::new(temp_dir.path().to_path_buf()); + + let older_target = create_search_session( + &sm, + "Older target", + SessionType::User, + "2026-05-01T00:00:00Z", + &[( + "does Acme have an email address for John Doe", + "2026-05-01T00:00:00Z", + )], + ) + .await; + + let newer_noise = create_search_session( + &sm, + "Newer noise", + SessionType::User, + "2026-05-22T00:00:00Z", + &[ + ("Acme person name looking for Acme", "2026-05-22T00:00:00Z"), + ( + "another Acme person name looking for Acme", + "2026-05-22T00:01:00Z", + ), + ], + ) + .await; + + let results = sm + .search_chat_sessions("Acme", Some(2), None, None, None, vec![SessionType::User]) + .await + .unwrap(); + let ids = results + .iter() + .map(|session| session.id.clone()) + .collect::>(); + + assert_eq!(ids, vec![newer_noise, older_target]); + } + + #[tokio::test] + async fn test_search_chat_sessions_applies_all_filters() { + let temp_dir = TempDir::new().unwrap(); + let sm = SessionManager::new(temp_dir.path().to_path_buf()); + + let excluded = create_search_session( + &sm, + "Excluded user", + SessionType::User, + "2026-05-20T00:00:00Z", + &[("Acme John excluded session", "2026-05-15T00:00:00Z")], + ) + .await; + + let scheduled_target = create_search_session( + &sm, + "Scheduled target", + SessionType::Scheduled, + "2026-05-19T00:00:00Z", + &[( + "John appears in scheduled Acme work", + "2026-05-16T00:00:00Z", + )], + ) + .await; + + let user_target = create_search_session( + &sm, + "User target", + SessionType::User, + "2026-05-18T00:00:00Z", + &[( + "Acme has an email address question for John Doe", + "2026-05-14T00:00:00Z", + )], + ) + .await; + + let _before_window = create_search_session( + &sm, + "Before window", + SessionType::User, + "2026-05-17T00:00:00Z", + &[("Acme John before date window", "2026-05-09T00:00:00Z")], + ) + .await; + + let _wrong_type = create_search_session( + &sm, + "ACP target", + SessionType::Acp, + "2026-05-16T00:00:00Z", + &[("Acme John wrong session type", "2026-05-15T00:00:00Z")], + ) + .await; + + let after = chrono::DateTime::parse_from_rfc3339("2026-05-10T00:00:00Z") + .unwrap() + .with_timezone(&chrono::Utc); + let before = chrono::DateTime::parse_from_rfc3339("2026-05-17T00:00:00Z") + .unwrap() + .with_timezone(&chrono::Utc); + + let results = sm + .search_chat_sessions( + "Acme John", + Some(10), + Some(after), + Some(before), + Some(excluded), + vec![SessionType::User, SessionType::Scheduled], + ) + .await + .unwrap(); + let ids = results + .iter() + .map(|session| session.id.clone()) + .collect::>(); + + assert_eq!(ids, vec![scheduled_target, user_target]); + } + async fn expected_session_list_ids(sm: &SessionManager, session_ids: &[String]) -> Vec { let mut sessions = Vec::new(); for session_id in session_ids { diff --git a/crates/goose/src/utils.rs b/crates/goose/src/utils.rs index b78d4a4a6960..dee7fc4ecc71 100644 --- a/crates/goose/src/utils.rs +++ b/crates/goose/src/utils.rs @@ -1,6 +1,23 @@ use tokio_util::sync::CancellationToken; use unicode_normalization::UnicodeNormalization; +/// Encode bytes as a lowercase hexadecimal string. +/// +/// This avoids relying on digest output types implementing `LowerHex`, which +/// changed in sha2 0.11. +pub fn bytes_to_hex(bytes: impl AsRef<[u8]>) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let bytes = bytes.as_ref(); + let mut output = String::with_capacity(bytes.len() * 2); + + for &byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + + output +} + /// Check if a character is in the Unicode Tags Block range (U+E0000-U+E007F) /// These characters are invisible and can be used for steganographic attacks fn is_in_unicode_tag_range(c: char) -> bool { @@ -83,6 +100,13 @@ pub fn split_command_args(input: &str) -> anyhow::Result> { mod tests { use super::*; + #[test] + fn test_bytes_to_hex() { + assert_eq!(bytes_to_hex([]), ""); + assert_eq!(bytes_to_hex([0x00, 0x0f, 0x10, 0xab, 0xff]), "000f10abff"); + assert_eq!(bytes_to_hex(b"hello world"), "68656c6c6f20776f726c64"); + } + #[test] fn test_contains_unicode_tags() { // Test detection of Unicode Tags Block characters diff --git a/crates/goose/tests/session_id_propagation_test.rs b/crates/goose/tests/session_id_propagation_test.rs index be26868475d1..c8c069f154a6 100644 --- a/crates/goose/tests/session_id_propagation_test.rs +++ b/crates/goose/tests/session_id_propagation_test.rs @@ -161,7 +161,9 @@ async fn make_request(provider: &dyn Provider, session_id: &str) { async fn test_session_id_propagates_to_log_records() { use opentelemetry::logs::AnyValue; use opentelemetry::Key; - use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; + use opentelemetry_appender_tracing::layer::{ + OpenTelemetryTracingBridge, TracingSpanAttributes, + }; use opentelemetry_sdk::logs::{InMemoryLogExporterBuilder, SdkLoggerProvider}; use tracing_subscriber::prelude::*; @@ -171,7 +173,7 @@ async fn test_session_id_propagates_to_log_records() { .build(); let layer = OpenTelemetryTracingBridge::builder(&provider) - .with_span_attribute_allowlist(["session.id"]) + .with_tracing_span_attributes(TracingSpanAttributes::allowlist(["session.id"])) .build(); let subscriber = tracing_subscriber::registry().with(layer); let _guard = tracing::subscriber::set_default(subscriber); diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index 90367355c743..0eba35b6eef1 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -45,10 +45,12 @@ goose is compatible with a wide range of LLM providers, allowing you to choose a | [Ollama Cloud](https://ollama.com/) | Access hosted models on ollama.com via OpenAI-compatible API. Requires an Ollama account and API key. | `OLLAMA_CLOUD_API_KEY` | | [OpenAI](https://platform.openai.com/api-keys) | Provides gpt-4o, o1, and other advanced language models. Also supports OpenAI-compatible endpoints (e.g., self-hosted LLaMA, vLLM, KServe). **o1-mini and o1-preview are not supported because goose uses tool calling.** | `OPENAI_API_KEY`, `OPENAI_HOST` (optional), `OPENAI_ORGANIZATION` (optional), `OPENAI_PROJECT` (optional), `OPENAI_CUSTOM_HEADERS` (optional) | | [OpenRouter](https://openrouter.ai/) | API gateway for unified access to various models with features like rate-limiting management. | `OPENROUTER_API_KEY` | +| [Perplexity](https://www.perplexity.ai/) | Chat models with built-in real-time web search grounding. OpenAI-compatible chat completions API at `https://api.perplexity.ai`. | `PERPLEXITY_API_KEY` | | [OVHcloud AI](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/) | Provides access to open-source models including Qwen, Llama, Mistral, and DeepSeek through AI Endpoints service. | `OVHCLOUD_API_KEY` | | [Ramalama](https://ramalama.ai/) | Local model using native [OCI](https://opencontainers.org/) container runtimes, [CNCF](https://www.cncf.io/) tools, and supporting models as OCI artifacts. Ramalama API is a compatible alternative to Ollama and can be used with the goose Ollama provider. Supports Qwen, Llama, DeepSeek, and other open-source models. **Because this provider runs locally, you must first [download and run a model](#local-llms).** | `OLLAMA_HOST` | | [Routstr](https://routstr.com/) | OpenAI-compatible aggregator that fronts dozens of upstream providers (Anthropic, OpenAI, Google, DeepSeek, Llama, …) behind a single API. Authenticate with an `sk-...` bearer issued by your Routstr instance — payment is handled outside goose. | `ROUTSTR_API_KEY`, `ROUTSTR_HOST` (optional, default `https://api.routstr.com`) | | [SaladCloud AI Gateway](https://salad.com/) | OpenAI-compatible access to SaladCloud-hosted open-source models, including Qwen, Gemma, and others. | `SALAD_CLOUD_API_KEY` | +| [Scaleway](https://www.scaleway.com/en/generative-apis/) | European cloud offering OpenAI-compatible access to models like Mistral, Qwen, and open-source weights. Ensures data residency and GDPR compliance. | `SCW_SECRET_KEY` | | [Snowflake](https://docs.snowflake.com/user-guide/snowflake-cortex/aisql#choosing-a-model) | Access the latest models using Snowflake Cortex services, including Claude models. **Requires a Snowflake account and programmatic access token (PAT)**. | `SNOWFLAKE_HOST`, `SNOWFLAKE_TOKEN` | | [VMware Tanzu Platform](https://techdocs.broadcom.com/us/en/vmware-tanzu/platform/ai-services/10-3/ai/index.html) | Enterprise-managed LLM access through AI Services on VMware Tanzu Platform. Models are fetched dynamically from the endpoint. | `TANZU_AI_API_KEY`, `TANZU_AI_ENDPOINT` | | [Tetrate Agent Router Service](https://router.tetrate.ai) | Unified API gateway for AI models including Claude, Gemini, GPT, open-weight models, and others. Supports PKCE authentication flow for secure API key generation. | `TETRATE_API_KEY`, `TETRATE_HOST` (optional) | diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 2f2108515a25..f63c35346290 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -364,6 +364,7 @@ These variables control how goose handles [tool execution](/docs/guides/managing | `GOOSE_CLI_TOOL_PARAMS_TRUNCATION_MAX_LENGTH` | Maximum length for tool parameter values before truncation in CLI output (not in debug mode) | Integer | 40 | | `GOOSE_DEBUG` | Enables debug mode to show full tool parameters without truncation. Can also be toggled during a session using the `/r` [slash command](/docs/guides/goose-cli-commands#slash-commands) | "1", "true" (case-insensitive) to enable | false | | `GOOSE_SEARCH_PATHS` | Prepends additional directories to PATH for extension commands | JSON array of paths (for example, `["/usr/local/bin", "~/custom/bin"]`) | System PATH only | +| `GOOSE_MAX_TOOL_RESPONSE_SIZE` | Maximum character count for a single tool response before it is written to a temporary file instead of being included inline in the conversation | Positive integer (e.g., 100000, 200000) | 200000 | | `GOOSE_SHELL` | Overrides the shell used for Developer extension shell commands | Shell executable path or name (for example, `/bin/zsh`, `pwsh`, `C:\cygwin64\bin\bash.exe`) | Unix: `/bin/bash` if present, otherwise `$SHELL`, otherwise `sh`. Windows: `cmd` | **Examples** @@ -379,6 +380,9 @@ export GOOSE_CLI_TOOL_PARAMS_MAX_LENGTH=100 # Show up to 100 characters for too # Add custom tool directories for extensions export GOOSE_SEARCH_PATHS='["/usr/local/bin", "~/custom/tools", "/opt/homebrew/bin"]' +# Lower the tool response size limit for smaller-context models +export GOOSE_MAX_TOOL_RESPONSE_SIZE=100000 + # Use zsh for Developer extension shell commands export GOOSE_SHELL=/bin/zsh ``` diff --git a/documentation/src/pages/index.tsx b/documentation/src/pages/index.tsx index 051eff70e599..5db2d352e209 100644 --- a/documentation/src/pages/index.tsx +++ b/documentation/src/pages/index.tsx @@ -36,12 +36,12 @@ function HeroSection() {
- 38k+ + 45k+ GitHub stars
- 400+ + 500+ Contributors
diff --git a/documentation/static/servers.json b/documentation/static/servers.json index b03b41f4f7f1..8c4de0011ddf 100644 --- a/documentation/static/servers.json +++ b/documentation/static/servers.json @@ -845,5 +845,23 @@ "is_builtin": false, "endorsed": true, "environmentVariables": [] - } + }, + { + "id": "scholar-sidekick", + "name": "Scholar Sidekick", + "description": "Resolve, format, export, and verify academic citations (DOI, PMID, ISBN, arXiv) plus retraction and open-access checks", + "command": "npx -y scholar-sidekick-mcp@latest", + "link": "https://github.com/mlava/scholar-sidekick-mcp", + "installation_notes": "Requires a free RapidAPI key. Get one at https://rapidapi.com/scholar-sidekick-scholar-sidekick-api/api/scholar-sidekick and set RAPIDAPI_KEY.", + "is_builtin": false, + "endorsed": false, + "environmentVariables": [ + { + "name": "RAPIDAPI_KEY", + "description": "RapidAPI key for the Scholar Sidekick API (free tier available)", + "required": true + } + ] +} + ] diff --git a/evals/open-model-gym/mcp-harness/package-lock.json b/evals/open-model-gym/mcp-harness/package-lock.json index 0abb8b0f26d1..845f45748a2d 100644 --- a/evals/open-model-gym/mcp-harness/package-lock.json +++ b/evals/open-model-gym/mcp-harness/package-lock.json @@ -846,9 +846,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f2ac75833f65..d1219992567a 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.35.0" + "version": "1.36.0" }, "paths": { "/action-required/tool-confirmation": { @@ -47,45 +47,6 @@ } } }, - "/agent/add_extension": { - "post": { - "tags": [ - "super::routes::agent" - ], - "operationId": "agent_add_extension", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddExtensionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Extension added", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized - invalid secret key" - }, - "424": { - "description": "Agent not initialized" - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/agent/call_tool": { "post": { "tags": [ @@ -368,45 +329,6 @@ } } }, - "/agent/remove_extension": { - "post": { - "tags": [ - "super::routes::agent" - ], - "operationId": "agent_remove_extension", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RemoveExtensionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Extension removed", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Unauthorized - invalid secret key" - }, - "424": { - "description": "Agent not initialized" - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/agent/restart": { "post": { "tags": [ @@ -977,102 +899,6 @@ } } }, - "/config/extensions": { - "get": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "get_extensions", - "responses": { - "200": { - "description": "All extensions retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExtensionResponse" - } - } - } - }, - "500": { - "description": "Internal server error" - } - } - }, - "post": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "add_extension", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExtensionQuery" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Extension added or updated successfully", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid request" - }, - "422": { - "description": "Could not serialize config.yaml" - }, - "500": { - "description": "Internal server error" - } - } - } - }, - "/config/extensions/{name}": { - "delete": { - "tags": [ - "super::routes::config_management" - ], - "operationId": "remove_extension", - "parameters": [ - { - "name": "name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Extension removed successfully", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "404": { - "description": "Extension not found" - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/config/permissions": { "post": { "tags": [ @@ -1993,6 +1819,29 @@ } } }, + "/local-inference/chat-templates/builtin": { + "get": { + "tags": [ + "super::routes::local_inference" + ], + "operationId": "list_builtin_chat_templates", + "responses": { + "200": { + "description": "llama.cpp built-in chat template names", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, "/local-inference/download": { "post": { "tags": [ @@ -3596,51 +3445,6 @@ ] } }, - "/sessions/{session_id}/extensions": { - "get": { - "tags": [ - "Session Management" - ], - "operationId": "get_session_extensions", - "parameters": [ - { - "name": "session_id", - "in": "path", - "description": "Unique identifier for the session", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Session extensions retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionExtensionsResponse" - } - } - } - }, - "401": { - "description": "Unauthorized - Invalid or missing API key" - }, - "404": { - "description": "Session not found" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, "/sessions/{session_id}/fork": { "post": { "tags": [ @@ -4118,21 +3922,6 @@ "propertyName": "actionType" } }, - "AddExtensionRequest": { - "type": "object", - "required": [ - "session_id", - "config" - ], - "properties": { - "config": { - "$ref": "#/components/schemas/ExtensionConfig" - }, - "session_id": { - "type": "string" - } - } - }, "Annotations": { "type": "object", "properties": { @@ -4260,6 +4049,63 @@ } } }, + "ChatTemplate": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "embedded" + ] + } + } + }, + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "builtin" + ] + } + } + }, + { + "type": "object", + "required": [ + "template", + "type" + ], + "properties": { + "template": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "custom_inline" + ] + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "CheckProviderRequest": { "type": "object", "required": [ @@ -5425,45 +5271,6 @@ } } }, - "ExtensionQuery": { - "type": "object", - "required": [ - "name", - "config", - "enabled" - ], - "properties": { - "config": { - "$ref": "#/components/schemas/ExtensionConfig" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "type": "string" - } - } - }, - "ExtensionResponse": { - "type": "object", - "required": [ - "extensions" - ], - "properties": { - "extensions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExtensionEntry" - } - }, - "warnings": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, "FeaturesResponse": { "type": "object", "required": [ @@ -6716,6 +6523,9 @@ "ModelSettings": { "type": "object", "properties": { + "chat_template": { + "$ref": "#/components/schemas/ChatTemplate" + }, "context_size": { "type": "integer", "format": "int32", @@ -6766,9 +6576,6 @@ "format": "int32", "nullable": true }, - "native_tool_calling": { - "type": "boolean" - }, "presence_penalty": { "type": "number", "format": "float" @@ -6784,15 +6591,15 @@ "sampling": { "$ref": "#/components/schemas/SamplingConfig" }, - "use_jinja": { - "type": "boolean" + "tool_calling": { + "$ref": "#/components/schemas/ToolCallingMode" }, "use_mlock": { "type": "boolean" }, "vision_capable": { "type": "boolean", - "description": "Whether this model architecture supports vision input.\nDerived from the featured model table, not user-configurable." + "description": "Whether this model architecture supports vision input.\nDerived from associated mmproj metadata, not user-configurable." } } }, @@ -7487,21 +7294,6 @@ } } }, - "RemoveExtensionRequest": { - "type": "object", - "required": [ - "name", - "session_id" - ], - "properties": { - "name": { - "type": "string" - }, - "session_id": { - "type": "string" - } - } - }, "RepoVariantsResponse": { "type": "object", "required": [ @@ -8156,20 +7948,6 @@ } } }, - "SessionExtensionsResponse": { - "type": "object", - "required": [ - "extensions" - ], - "properties": { - "extensions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExtensionConfig" - } - } - } - }, "SessionInsights": { "type": "object", "required": [ @@ -8797,6 +8575,14 @@ } } }, + "ToolCallingMode": { + "type": "string", + "enum": [ + "auto", + "force_native", + "force_emulated" + ] + }, "ToolConfirmationRequest": { "type": "object", "required": [ diff --git a/ui/desktop/package.json b/ui/desktop/package.json index ca762b463116..67a4afc78af0 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "ApeMind Agent", - "version": "1.35.0", + "version": "1.36.0", "description": "ApeMind Agent Desktop", "engines": { "node": "^24.10.0", diff --git a/ui/desktop/src/acp/extensions.ts b/ui/desktop/src/acp/extensions.ts index 4edcff17c52c..076a7a3d4467 100644 --- a/ui/desktop/src/acp/extensions.ts +++ b/ui/desktop/src/acp/extensions.ts @@ -1,11 +1,96 @@ -import type { ExtensionResponse, ExtensionEntry } from '../api'; +import type { ExtensionEntry, ExtensionConfig } from '../api'; import { getAcpClient } from './acpConnection'; -export async function getConfiguredExtensions(): Promise { +export interface ConfiguredExtensionsResponse { + extensions: ExtensionEntry[]; + warnings: string[]; +} + +/** + * Fetch all configured extensions via ACP (`_goose/unstable/config/extensions/list`). + */ +export async function getConfiguredExtensions(): Promise { const client = await getAcpClient(); const response = await client.goose.configExtensionsList_unstable({}); return { extensions: response.extensions as ExtensionEntry[], - warnings: response.warnings, + warnings: response.warnings ?? [], }; } + +/** + * Add (or update) an extension in the user's global goose config via ACP + * (`_goose/unstable/config/extensions/add`). + */ +export async function addConfiguredExtension( + name: string, + config: ExtensionConfig, + enabled: boolean +): Promise { + const client = await getAcpClient(); + // Server expects a JSON object matching one of the ExtensionConfig variants, + // and injects `name` itself. We strip `name` from the body to match that shape. + const extensionConfig = { ...config } as Record; + delete extensionConfig.name; + + await client.goose.configExtensionsAdd_unstable({ + name, + extensionConfig, + enabled, + }); +} + +/** + * Remove an extension from the user's global goose config via ACP + * (`_goose/unstable/config/extensions/remove`). The server normalizes the + * supplied `configKey` via `name_to_key`, so passing the raw extension name + * is sufficient and matches how the previous REST route worked. + */ +export async function removeConfiguredExtension(name: string): Promise { + const client = await getAcpClient(); + await client.goose.configExtensionsRemove_unstable({ + configKey: name, + }); +} + +/** + * Add an extension to a running session's agent via ACP + * (`_goose/unstable/session/extensions/add`). + */ +export async function addSessionExtension( + sessionId: string, + config: ExtensionConfig +): Promise { + const client = await getAcpClient(); + await client.goose.sessionExtensionsAdd_unstable({ + sessionId, + config, + }); +} + +/** + * Remove an extension from a running session's agent via ACP + * (`_goose/unstable/session/extensions/remove`). + */ +export async function removeSessionExtension( + sessionId: string, + name: string +): Promise { + const client = await getAcpClient(); + await client.goose.sessionExtensionsRemove_unstable({ + sessionId, + name, + }); +} + +/** + * Fetch the list of extensions associated with a given session via ACP + * (`_goose/unstable/session/extensions/list`). + */ +export async function getSessionExtensions( + sessionId: string +): Promise { + const client = await getAcpClient(); + const response = await client.goose.sessionExtensionsList_unstable({ sessionId }); + return response.extensions as ExtensionEntry[]; +} diff --git a/ui/desktop/src/api/index.ts b/ui/desktop/src/api/index.ts index d42bc29fc7f3..d30ca646c9af 100644 --- a/ui/desktop/src/api/index.ts +++ b/ui/desktop/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { addExtension, agentAddExtension, agentRemoveExtension, callTool, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getExtensions, getFeatures, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getRepoFiles, getSession, getSessionExtensions, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, importSessionNostr, inspectRunningJob, killRunningJob, listApps, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, removeConfig, removeCustomProvider, removeExtension, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, shareSessionNostr, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; -export type { ActionRequired, ActionRequiredData, AddExtensionData, AddExtensionErrors, AddExtensionRequest, AddExtensionResponse, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponse, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponse, AgentRemoveExtensionResponses, Annotations, Author, AuthorRequest, CallToolData, CallToolError, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, ExtensionQuery, ExtensionResponse, FeaturesResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponse, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponse, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponse, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, IconTheme, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrRequest, ImportSessionNostrResponse, ImportSessionNostrResponses, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionRequest, RemoveExtensionResponse, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionExtensionsResponse, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrRequest, ShareSessionNostrResponse, ShareSessionNostrResponse2, ShareSessionNostrResponses, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; +export { callTool, cancelDownload, cancelLocalModelDownload, checkProvider, cleanupProviderCache, configureProviderOauth, confirmToolAction, createCustomProvider, createRecipe, createSchedule, decodeRecipe, deleteLocalModel, deleteModel, deleteRecipe, deleteSchedule, deleteSession, diagnostics, downloadHfModel, downloadModel, encodeRecipe, exportApp, exportSession, forkSession, getCanonicalModelInfo, getCustomProvider, getDictationConfig, getDownloadProgress, getFeatures, getLocalModelDownloadProgress, getModelSettings, getPrompt, getPrompts, getProviderCatalog, getProviderCatalogTemplate, getProviderModelInfo, getProviderModels, getRepoFiles, getSession, getSessionInsights, getSlashCommands, getTools, getTunnelStatus, importApp, importSession, importSessionNostr, inspectRunningJob, killRunningJob, listApps, listBuiltinChatTemplates, listLocalModels, listModels, listRecipes, listSchedules, listSessions, mcpUiProxy, type Options, parseRecipe, pauseSchedule, providers, readAllConfig, readConfig, readResource, recipeToYaml, removeConfig, removeCustomProvider, reply, resetPrompt, restartAgent, resumeAgent, runNowHandler, savePrompt, saveRecipe, scanRecipe, scheduleRecipe, searchHfModels, searchSessions, sendTelemetryEvent, sessionCancel, sessionEvents, sessionReply, sessionsHandler, setConfigProvider, setRecipeSlashCommand, shareSessionNostr, startAgent, startNanogptSetup, startOpenrouterSetup, startTetrateSetup, startTunnel, status, stopAgent, stopTunnel, syncFeaturedModels, systemInfo, transcribeDictation, unpauseSchedule, updateAgentProvider, updateCustomProvider, updateFromSession, updateModelSettings, updateSchedule, updateSession, updateSessionName, updateSessionUserRecipeValues, updateWorkingDir, upsertConfig, upsertPermissions, validateConfig } from './sdk.gen'; +export type { ActionRequired, ActionRequiredData, Annotations, Author, AuthorRequest, CallToolData, CallToolError, CallToolErrors, CallToolRequest, CallToolResponse, CallToolResponse2, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CancelRequest, ChatRequest, ChatTemplate, CheckProviderData, CheckProviderRequest, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponse, CleanupProviderCacheResponses, ClientOptions, CommandType, ConfigKey, ConfigKeyQuery, ConfigResponse, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionRequest, ConfirmToolActionResponses, Content, ContentBlock, Conversation, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponse, CreateCustomProviderResponse2, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeRequest, CreateRecipeResponse, CreateRecipeResponse2, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleRequest, CreateScheduleResponse, CreateScheduleResponses, CspMetadata, DeclarativeProviderConfig, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeRequest, DecodeRecipeResponse, DecodeRecipeResponse2, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeRequest, DeleteRecipeResponse, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponse, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponse, DiagnosticsResponses, DictationProvider, DictationProviderStatus, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponse, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelRequest, DownloadModelResponses, DownloadProgress, DownloadStatus, EmbeddedResource, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeRequest, EncodeRecipeResponse, EncodeRecipeResponse2, EncodeRecipeResponses, Envs, EnvVarConfig, ErrorResponse, ExportAppData, ExportAppError, ExportAppErrors, ExportAppResponse, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponse, ExportSessionResponses, ExtensionConfig, ExtensionData, ExtensionEntry, ExtensionLoadResult, FeaturesResponse, ForkRequest, ForkResponse, ForkSessionData, ForkSessionErrors, ForkSessionResponse, ForkSessionResponses, FrontendToolRequest, GetCanonicalModelInfoData, GetCanonicalModelInfoResponse, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponse, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponse, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponse, GetDownloadProgressResponses, GetFeaturesData, GetFeaturesResponse, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponse, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponse, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponse, GetPromptResponses, GetPromptsData, GetPromptsResponse, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponse, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponse, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponse, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponse, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponse, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponse, GetSessionInsightsResponses, GetSessionResponse, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponse, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsQuery, GetToolsResponse, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponse, GetTunnelStatusResponses, GooseApp, GooseMode, HfGgufFile, HfModelInfo, HfQuantVariant, Icon, IconTheme, ImageContent, ImportAppData, ImportAppError, ImportAppErrors, ImportAppRequest, ImportAppResponse, ImportAppResponse2, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrRequest, ImportSessionNostrResponse, ImportSessionNostrResponses, ImportSessionRequest, ImportSessionResponse, ImportSessionResponses, InferenceMetadata, InspectJobResponse, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponse, InspectRunningJobResponses, JsonObject, KillJobResponse, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsError, ListAppsErrors, ListAppsRequest, ListAppsResponse, ListAppsResponse2, ListAppsResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponse, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponse, ListLocalModelsResponses, ListModelsData, ListModelsResponse, ListModelsResponses, ListRecipeResponse, ListRecipesData, ListRecipesErrors, ListRecipesResponse, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponse, ListSchedulesResponse2, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponse, ListSessionsResponses, LoadedProvider, LocalModelResponse, McpAppResource, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, Message, MessageContent, MessageEvent, MessageMetadata, ModelCapabilities, ModelConfig, ModelDownloadStatus, ModelInfo, ModelInfoData, ModelInfoQuery, ModelInfoResponse, ModelSettings, ModelTemplate, ParseRecipeData, ParseRecipeError, ParseRecipeErrors, ParseRecipeRequest, ParseRecipeResponse, ParseRecipeResponse2, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponse, PauseScheduleResponses, Permission, PermissionLevel, PermissionsMetadata, PrincipalType, PromptContentResponse, PromptsListResponse, ProviderCatalogEntry, ProviderDetails, ProviderEngine, ProviderMetadata, ProviderModelInfoQuery, ProvidersData, ProvidersResponse, ProvidersResponse2, ProvidersResponses, ProviderTemplate, ProviderType, RawAudioContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ReadAllConfigData, ReadAllConfigResponse, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceRequest, ReadResourceResponse, ReadResourceResponse2, ReadResourceResponses, Recipe, RecipeManifest, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement, RecipeToYamlData, RecipeToYamlError, RecipeToYamlErrors, RecipeToYamlRequest, RecipeToYamlResponse, RecipeToYamlResponse2, RecipeToYamlResponses, RedactedThinkingContent, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponse, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponse, RemoveCustomProviderResponses, ReplyData, ReplyErrors, ReplyResponse, ReplyResponses, RepoVariantsResponse, ResetPromptData, ResetPromptErrors, ResetPromptResponse, ResetPromptResponses, ResourceContents, ResourceMetadata, Response, RestartAgentData, RestartAgentErrors, RestartAgentRequest, RestartAgentResponse, RestartAgentResponse2, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentRequest, ResumeAgentResponse, ResumeAgentResponse2, ResumeAgentResponses, RetryConfig, Role, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponse, RunNowHandlerResponses, RunNowResponse, SamplingConfig, SavePromptData, SavePromptErrors, SavePromptRequest, SavePromptResponse, SavePromptResponses, SaveRecipeData, SaveRecipeError, SaveRecipeErrors, SaveRecipeRequest, SaveRecipeResponse, SaveRecipeResponse2, SaveRecipeResponses, ScanRecipeData, ScanRecipeRequest, ScanRecipeResponse, ScanRecipeResponse2, ScanRecipeResponses, ScheduledJob, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeRequest, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponse, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponse, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, Session, SessionCancelData, SessionCancelResponses, SessionDisplayInfo, SessionEventsData, SessionEventsErrors, SessionEventsResponse, SessionEventsResponses, SessionInsights, SessionListResponse, SessionReplyData, SessionReplyErrors, SessionReplyRequest, SessionReplyResponse, SessionReplyResponse2, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponse, SessionsHandlerResponses, SessionsQuery, SessionType, SetConfigProviderData, SetProviderRequest, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, SetSlashCommandRequest, Settings, SetupResponse, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrRequest, ShareSessionNostrResponse, ShareSessionNostrResponse2, ShareSessionNostrResponses, SlashCommand, SlashCommandsResponse, StartAgentData, StartAgentError, StartAgentErrors, StartAgentRequest, StartAgentResponse, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponse, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponse, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponse, StartTetrateSetupResponses, StartTunnelData, StartTunnelError, StartTunnelErrors, StartTunnelResponse, StartTunnelResponses, StatusData, StatusResponse, StatusResponses, StopAgentData, StopAgentErrors, StopAgentRequest, StopAgentResponse, StopAgentResponses, StopTunnelData, StopTunnelError, StopTunnelErrors, StopTunnelResponses, SubRecipe, SuccessCheck, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfo, SystemInfoData, SystemInfoResponse, SystemInfoResponses, SystemNotificationContent, SystemNotificationType, TaskSupport, TelemetryEventRequest, Template, TextContent, ThinkingContent, ThinkingEffort, TokenState, Tool, ToolAnnotations, ToolCallingMode, ToolConfirmationRequest, ToolExecution, ToolInfo, ToolPermission, ToolRequest, ToolResponse, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponse, TranscribeDictationResponses, TranscribeRequest, TranscribeResponse, TunnelInfo, TunnelState, UiMetadata, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponse, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderRequest, UpdateCustomProviderResponse, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionRequest, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponse, UpdateModelSettingsResponses, UpdateProviderRequest, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleRequest, UpdateScheduleResponse, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameRequest, UpdateSessionNameResponses, UpdateSessionRequest, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesError, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesRequest, UpdateSessionUserRecipeValuesResponse, UpdateSessionUserRecipeValuesResponse2, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirRequest, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigQuery, UpsertConfigResponse, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsQuery, UpsertPermissionsResponse, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponse, ValidateConfigResponses, WhisperModelResponse, WindowProps } from './types.gen'; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index c98c91640854..29948c84e9ff 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetFeaturesData, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionExtensionsData, GetSessionExtensionsErrors, GetSessionExtensionsResponses, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrResponses, ImportSessionResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { CallToolData, CallToolErrors, CallToolResponses, CancelDownloadData, CancelDownloadErrors, CancelDownloadResponses, CancelLocalModelDownloadData, CancelLocalModelDownloadErrors, CancelLocalModelDownloadResponses, CheckProviderData, CleanupProviderCacheData, CleanupProviderCacheErrors, CleanupProviderCacheResponses, ConfigureProviderOauthData, ConfigureProviderOauthErrors, ConfigureProviderOauthResponses, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteLocalModelData, DeleteLocalModelErrors, DeleteLocalModelResponses, DeleteModelData, DeleteModelErrors, DeleteModelResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, DownloadHfModelData, DownloadHfModelErrors, DownloadHfModelResponses, DownloadModelData, DownloadModelErrors, DownloadModelResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportAppData, ExportAppErrors, ExportAppResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, ForkSessionData, ForkSessionErrors, ForkSessionResponses, GetCanonicalModelInfoData, GetCanonicalModelInfoResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetDictationConfigData, GetDictationConfigResponses, GetDownloadProgressData, GetDownloadProgressErrors, GetDownloadProgressResponses, GetFeaturesData, GetFeaturesResponses, GetLocalModelDownloadProgressData, GetLocalModelDownloadProgressErrors, GetLocalModelDownloadProgressResponses, GetModelSettingsData, GetModelSettingsErrors, GetModelSettingsResponses, GetPromptData, GetPromptErrors, GetPromptResponses, GetPromptsData, GetPromptsResponses, GetProviderCatalogData, GetProviderCatalogErrors, GetProviderCatalogResponses, GetProviderCatalogTemplateData, GetProviderCatalogTemplateErrors, GetProviderCatalogTemplateResponses, GetProviderModelInfoData, GetProviderModelInfoErrors, GetProviderModelInfoResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetRepoFilesData, GetRepoFilesResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportAppData, ImportAppErrors, ImportAppResponses, ImportSessionData, ImportSessionErrors, ImportSessionNostrData, ImportSessionNostrErrors, ImportSessionNostrResponses, ImportSessionResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListAppsData, ListAppsErrors, ListAppsResponses, ListBuiltinChatTemplatesData, ListBuiltinChatTemplatesResponses, ListLocalModelsData, ListLocalModelsResponses, ListModelsData, ListModelsResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, ReplyData, ReplyErrors, ReplyResponses, ResetPromptData, ResetPromptErrors, ResetPromptResponses, RestartAgentData, RestartAgentErrors, RestartAgentResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SavePromptData, SavePromptErrors, SavePromptResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SearchHfModelsData, SearchHfModelsErrors, SearchHfModelsResponses, SearchSessionsData, SearchSessionsErrors, SearchSessionsResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionCancelData, SessionCancelResponses, SessionEventsData, SessionEventsErrors, SessionEventsResponses, SessionReplyData, SessionReplyErrors, SessionReplyResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, ShareSessionNostrData, ShareSessionNostrErrors, ShareSessionNostrResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartNanogptSetupData, StartNanogptSetupResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopAgentData, StopAgentErrors, StopAgentResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, SyncFeaturedModelsData, SyncFeaturedModelsResponses, SystemInfoData, SystemInfoResponses, TranscribeDictationData, TranscribeDictationErrors, TranscribeDictationResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateModelSettingsData, UpdateModelSettingsErrors, UpdateModelSettingsResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionData, UpdateSessionErrors, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpdateWorkingDirData, UpdateWorkingDirErrors, UpdateWorkingDirResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -27,15 +27,6 @@ export const confirmToolAction = (options: } }); -export const agentAddExtension = (options: Options) => (options.client ?? client).post({ - url: '/agent/add_extension', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const callTool = (options: Options) => (options.client ?? client).post({ url: '/agent/call_tool', ...options, @@ -67,15 +58,6 @@ export const readResource = (options: Opti } }); -export const agentRemoveExtension = (options: Options) => (options.client ?? client).post({ - url: '/agent/remove_extension', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - export const restartAgent = (options: Options) => (options.client ?? client).post({ url: '/agent/restart', ...options, @@ -192,19 +174,6 @@ export const updateCustomProvider = (optio } }); -export const getExtensions = (options?: Options) => (options?.client ?? client).get({ url: '/config/extensions', ...options }); - -export const addExtension = (options: Options) => (options.client ?? client).post({ - url: '/config/extensions', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -export const removeExtension = (options: Options) => (options.client ?? client).delete({ url: '/config/extensions/{name}', ...options }); - export const upsertPermissions = (options: Options) => (options.client ?? client).post({ url: '/config/permissions', ...options, @@ -321,6 +290,8 @@ export const startOpenrouterSetup = (optio export const startTetrateSetup = (options?: Options) => (options?.client ?? client).post({ url: '/handle_tetrate', ...options }); +export const listBuiltinChatTemplates = (options?: Options) => (options?.client ?? client).get({ url: '/local-inference/chat-templates/builtin', ...options }); + export const downloadHfModel = (options: Options) => (options.client ?? client).post({ url: '/local-inference/download', ...options, @@ -542,8 +513,6 @@ export const getSession = (options: Option export const exportSession = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}/export', ...options }); -export const getSessionExtensions = (options: Options) => (options.client ?? client).get({ url: '/sessions/{session_id}/extensions', ...options }); - export const forkSession = (options: Options) => (options.client ?? client).post({ url: '/sessions/{session_id}/fork', ...options, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index b5535a239fb6..09e243ee2bd2 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -25,11 +25,6 @@ export type ActionRequiredData = { user_data: unknown; }; -export type AddExtensionRequest = { - config: ExtensionConfig; - session_id: string; -}; - export type Annotations = { audience?: Array; lastModified?: string; @@ -76,6 +71,16 @@ export type ChatRequest = { user_message: Message; }; +export type ChatTemplate = { + type: 'embedded'; +} | { + name: string; + type: 'builtin'; +} | { + template: string; + type: 'custom_inline'; +}; + export type CheckProviderRequest = { provider: string; }; @@ -483,17 +488,6 @@ export type ExtensionLoadResult = { success: boolean; }; -export type ExtensionQuery = { - config: ExtensionConfig; - enabled: boolean; - name: string; -}; - -export type ExtensionResponse = { - extensions: Array; - warnings?: Array; -}; - export type FeaturesResponse = { /** * Map of feature name to enabled status @@ -863,6 +857,7 @@ export type ModelInfoResponse = { }; export type ModelSettings = { + chat_template?: ChatTemplate; context_size?: number | null; enable_thinking?: boolean; flash_attention?: boolean | null; @@ -880,16 +875,15 @@ export type ModelSettings = { n_batch?: number | null; n_gpu_layers?: number | null; n_threads?: number | null; - native_tool_calling?: boolean; presence_penalty?: number; repeat_last_n?: number; repeat_penalty?: number; sampling?: SamplingConfig; - use_jinja?: boolean; + tool_calling?: ToolCallingMode; use_mlock?: boolean; /** * Whether this model architecture supports vision input. - * Derived from the featured model table, not user-configurable. + * Derived from associated mmproj metadata, not user-configurable. */ vision_capable?: boolean; }; @@ -1142,11 +1136,6 @@ export type RedactedThinkingContent = { data: string; }; -export type RemoveExtensionRequest = { - name: string; - session_id: string; -}; - export type RepoVariantsResponse = { available_memory_bytes: number; downloaded_quants: Array; @@ -1338,10 +1327,6 @@ export type SessionDisplayInfo = { workingDir: string; }; -export type SessionExtensionsResponse = { - extensions: Array; -}; - export type SessionInsights = { totalSessions: number; totalTokens: number; @@ -1544,6 +1529,8 @@ export type ToolAnnotations = { title?: string; }; +export type ToolCallingMode = 'auto' | 'force_native' | 'force_emulated'; + export type ToolConfirmationRequest = { arguments: JsonObject; id: string; @@ -1769,37 +1756,6 @@ export type ConfirmToolActionResponses = { 200: unknown; }; -export type AgentAddExtensionData = { - body: AddExtensionRequest; - path?: never; - query?: never; - url: '/agent/add_extension'; -}; - -export type AgentAddExtensionErrors = { - /** - * Unauthorized - invalid secret key - */ - 401: unknown; - /** - * Agent not initialized - */ - 424: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type AgentAddExtensionResponses = { - /** - * Extension added - */ - 200: string; -}; - -export type AgentAddExtensionResponse = AgentAddExtensionResponses[keyof AgentAddExtensionResponses]; - export type CallToolData = { body: CallToolRequest; path?: never; @@ -1970,37 +1926,6 @@ export type ReadResourceResponses = { export type ReadResourceResponse2 = ReadResourceResponses[keyof ReadResourceResponses]; -export type AgentRemoveExtensionData = { - body: RemoveExtensionRequest; - path?: never; - query?: never; - url: '/agent/remove_extension'; -}; - -export type AgentRemoveExtensionErrors = { - /** - * Unauthorized - invalid secret key - */ - 401: unknown; - /** - * Agent not initialized - */ - 424: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type AgentRemoveExtensionResponses = { - /** - * Extension removed - */ - 200: string; -}; - -export type AgentRemoveExtensionResponse = AgentRemoveExtensionResponses[keyof AgentRemoveExtensionResponses]; - export type RestartAgentData = { body: RestartAgentRequest; path?: never; @@ -2436,89 +2361,6 @@ export type UpdateCustomProviderResponses = { export type UpdateCustomProviderResponse = UpdateCustomProviderResponses[keyof UpdateCustomProviderResponses]; -export type GetExtensionsData = { - body?: never; - path?: never; - query?: never; - url: '/config/extensions'; -}; - -export type GetExtensionsErrors = { - /** - * Internal server error - */ - 500: unknown; -}; - -export type GetExtensionsResponses = { - /** - * All extensions retrieved successfully - */ - 200: ExtensionResponse; -}; - -export type GetExtensionsResponse = GetExtensionsResponses[keyof GetExtensionsResponses]; - -export type AddExtensionData = { - body: ExtensionQuery; - path?: never; - query?: never; - url: '/config/extensions'; -}; - -export type AddExtensionErrors = { - /** - * Invalid request - */ - 400: unknown; - /** - * Could not serialize config.yaml - */ - 422: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type AddExtensionResponses = { - /** - * Extension added or updated successfully - */ - 200: string; -}; - -export type AddExtensionResponse = AddExtensionResponses[keyof AddExtensionResponses]; - -export type RemoveExtensionData = { - body?: never; - path: { - name: string; - }; - query?: never; - url: '/config/extensions/{name}'; -}; - -export type RemoveExtensionErrors = { - /** - * Extension not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type RemoveExtensionResponses = { - /** - * Extension removed successfully - */ - 200: string; -}; - -export type RemoveExtensionResponse = RemoveExtensionResponses[keyof RemoveExtensionResponses]; - export type UpsertPermissionsData = { body: UpsertPermissionsQuery; path?: never; @@ -3241,6 +3083,22 @@ export type StartTetrateSetupResponses = { export type StartTetrateSetupResponse = StartTetrateSetupResponses[keyof StartTetrateSetupResponses]; +export type ListBuiltinChatTemplatesData = { + body?: never; + path?: never; + query?: never; + url: '/local-inference/chat-templates/builtin'; +}; + +export type ListBuiltinChatTemplatesResponses = { + /** + * llama.cpp built-in chat template names + */ + 200: Array; +}; + +export type ListBuiltinChatTemplatesResponse = ListBuiltinChatTemplatesResponses[keyof ListBuiltinChatTemplatesResponses]; + export type DownloadHfModelData = { body: DownloadModelRequest; path?: never; @@ -4468,42 +4326,6 @@ export type ExportSessionResponses = { export type ExportSessionResponse = ExportSessionResponses[keyof ExportSessionResponses]; -export type GetSessionExtensionsData = { - body?: never; - path: { - /** - * Unique identifier for the session - */ - session_id: string; - }; - query?: never; - url: '/sessions/{session_id}/extensions'; -}; - -export type GetSessionExtensionsErrors = { - /** - * Unauthorized - Invalid or missing API key - */ - 401: unknown; - /** - * Session not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type GetSessionExtensionsResponses = { - /** - * Session extensions retrieved successfully - */ - 200: SessionExtensionsResponse; -}; - -export type GetSessionExtensionsResponse = GetSessionExtensionsResponses[keyof GetSessionExtensionsResponses]; - export type ForkSessionData = { body: ForkRequest; path: { diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index ef3ef23d04f1..f6a36faa6ae9 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -7,6 +7,7 @@ import LoadingGoose from './LoadingGoose'; import ProgressiveMessageList from './ProgressiveMessageList'; import { MainPanelLayout } from './Layout/MainPanelLayout'; import ChatInput from './ChatInput'; +import { ChatInputCard } from './ChatInputCard'; import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area'; import { useFileDrop } from '../hooks/useFileDrop'; import { Message } from '../api'; @@ -371,13 +372,13 @@ export default function BaseChat({ return (
{renderHeader && renderHeader()} -
-
+
+

@@ -404,7 +405,7 @@ export default function BaseChat({ return (
@@ -412,7 +413,7 @@ export default function BaseChat({ {renderHeader && renderHeader()} {/* Chat container with sticky recipe header */} -
+
{/* Product watermark - top right */}
@@ -426,7 +427,7 @@ export default function BaseChat({ -
-
+ {RECIPE_TRUST_WARNINGS_ENABLED && recipe && isActiveSession && ( diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 7fe9c86c6b91..0b3240809883 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1,18 +1,18 @@ import { AppEvents } from '../constants/events'; import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react'; -import { Bug, ChefHat, ScrollText } from 'lucide-react'; +import { ArrowUp, Bug, ScrollText } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; import { Button } from './ui/button'; import type { View } from '../utils/navigationUtils'; import Stop from './ui/Stop'; -import { Attach, Send, Close, Microphone } from './icons'; +import { Attach, Close, Microphone } from './icons'; import { ChatState } from '../types/chatState'; import debounce from 'lodash/debounce'; import { LocalMessageStorage } from '../utils/localMessageStorage'; import { DirSwitcher } from './bottom_menu/DirSwitcher'; import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar'; -import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection'; import { BottomMenuExtensionSelection } from './bottom_menu/BottomMenuExtensionSelection'; +import { cn } from '../utils'; import { AlertType, useAlerts } from './alerts'; import { useConfig } from './ConfigContext'; import { useModelAndProvider } from './ModelAndProviderContext'; @@ -28,16 +28,12 @@ import { MessageQueue, QueuedMessage } from './MessageQueue'; import { detectInterruption } from '../utils/interruptionDetector'; import { DiagnosticsModal } from './ui/Diagnostics'; import { getSession, Message } from '../api'; -import CreateRecipeFromSessionModal from './recipes/CreateRecipeFromSessionModal'; -import CreateEditRecipeModal from './recipes/CreateEditRecipeModal'; import { getInitialWorkingDir } from '../utils/workingDir'; import { getPredefinedModelsFromEnv } from './settings/models/predefinedModelsUtils'; import { trackFileAttached, trackVoiceDictation, trackDiagnosticsOpened, - trackCreateRecipeOpened, - trackEditRecipeOpened, } from '../utils/analytics'; import { getNavigationShortcutText } from '../utils/keyboardShortcuts'; import { UserInput, ImageData } from '../types/message'; @@ -222,8 +218,8 @@ export default function ChatInput({ accumulatedCost, messages = [], disableAnimation = false, - recipe, - recipeId, + recipe: _recipe, + recipeId: _recipeId, recipeAccepted, initialPrompt, toolCount, @@ -305,10 +301,23 @@ export default function ChatInput({ const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); const [diagnosticsOpen, setDiagnosticsOpen] = useState(false); - const [showCreateRecipeModal, setShowCreateRecipeModal] = useState(false); - const [showEditRecipeModal, setShowEditRecipeModal] = useState(false); const [sessionWorkingDir, setSessionWorkingDir] = useState(null); + // Hide non-essential bottom-bar controls when the chat input is narrow. + // Only the model selector, mic, and send button remain visible. + const bottomBarRef = useRef(null); + const [isBottomBarNarrow, setIsBottomBarNarrow] = useState(false); + useEffect(() => { + const el = bottomBarRef.current; + if (!el) return; + const observer = new ResizeObserver((entries) => { + const width = entries[0]?.contentRect.width ?? 0; + setIsBottomBarNarrow(width < 480); + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + useEffect(() => { if (!sessionId) { return; @@ -1191,7 +1200,7 @@ export default function ChatInput({ } }; - const onFormSubmit = (e: React.FormEvent) => { + const onFormSubmit = (e: React.FormEvent | React.MouseEvent) => { e.preventDefault(); if (isLoading && hasSubmittableContent) { handleInterruptionAndQueue(); @@ -1394,7 +1403,7 @@ export default function ChatInput({ isFocused ? 'border-border-secondary hover:border-border-secondary' : 'border-border-primary hover:border-border-primary' - } bg-background-primary z-10 rounded-t-2xl`} + } bg-background-primary z-10`} data-drop-zone="true" onDrop={handleLocalDrop} onDragOver={handleLocalDragOver} @@ -1444,154 +1453,30 @@ export default function ChatInput({ minHeight: `${minTextareaHeight}px`, maxHeight: `${maxHeight}px`, overflowY: 'auto', - paddingRight: dictationProvider ? '180px' : '120px', }} className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 text-sm resize-none text-text-primary placeholder:text-text-secondary" /> - {/* Inline action buttons - absolutely positioned on the right */} -
- {/* Microphone button - show only if provider is selected */} - {dictationProvider && ( - <> - {!isEnabled ? ( - - - - - - - - {dictationProvider === 'openai' ? ( -

- OpenAI API key is not configured. Set it up in Settings {'>'}{' '} - Models. -

- ) : dictationProvider === 'elevenlabs' ? ( -

- ElevenLabs API key is not configured. Set it up in Settings {'>'}{' '} - Chat {'>'} Voice Dictation. -

- ) : dictationProvider === 'local' ? ( -

- Local Whisper model not found. Download a model in{' '} - Settings > Dictation > Local (Offline) -

- ) : ( -

Dictation provider is not properly configured.

- )} -
-
- ) : ( - - - - - -

- Voice dictation - {isRecording ? '' : ' • Say "submit" to send'} -

-
-
+ {/* Recording/transcribing status indicator (floats above the bottom bar) */} + {(isRecording || isTranscribing) && ( +
+ + {isRecording && ( + + + Listening + )} - - )} - - {/* Send/Stop button */} - {isLoading && !hasSubmittableContent ? ( - - ) : ( - - - - + {isRecording && isTranscribing && } + {isTranscribing && ( + + + Transcribing - - -

{getSubmitButtonTooltip()}

-
-
- )} - - {/* Recording/transcribing status indicator - positioned above the button row */} - {(isRecording || isTranscribing) && ( -
- - {isRecording && ( - - - Listening - - )} - {isRecording && isTranscribing && } - {isTranscribing && ( - - - Transcribing - - )} - -
- )} -
+ )} + +
+ )}
@@ -1696,129 +1581,206 @@ export default function ChatInput({
)} - {/* Secondary actions and controls row below input */} -
- { - setSessionWorkingDir(newDir); - if (onWorkingDirChange) { - onWorkingDirChange(newDir); - } - }} - onRestartStart={() => setChatState?.(ChatState.RestartingAgent)} - onRestartEnd={() => setChatState?.(ChatState.Idle)} - /> -
+ {/* Bottom action bar. Single flat row; no dividers. Left side: model + + working dir. Right side (after spacer): context indicator, + extensions, diagnostics, attach, mic, send. When the bar is narrow + (e.g. on a small window), the secondary controls drop out so the + model selector + send button always stay visible. */} +
+ {/* Left: model selector */} - - - - Attach file +
+ +
-
- {/* Model selector, mode selector, alerts, summarize button */} -
- {/* Cost Tracker */} - {COST_TRACKING_ENABLED && ( - <> -
- -
- - )} - { + setSessionWorkingDir(newDir); + if (onWorkingDirChange) { + onWorkingDirChange(newDir); + } + }} + onRestartStart={() => setChatState?.(ChatState.RestartingAgent)} + onRestartEnd={() => setChatState?.(ChatState.Idle)} /> - -
- + + {!isBottomBarNarrow && ( + <> + {/* Right: cost tracker (when enabled) */} + {COST_TRACKING_ENABLED && ( + -
-
-
- -
- - {sessionId && messages.length > 0 && ( - <> -
-
- - - - - - {recipe - ? intl.formatMessage(i18n.viewEditRecipe) - : intl.formatMessage(i18n.createRecipeFromSession)} - - -
- - )} - {sessionId && ( + )} + + {/* Right: context window indicator */} + + + {/* Right: extension selector */} + + + {/* Right: diagnostics */} + {sessionId && ( + + + + + Generate diagnostics bundle + + )} + + {/* Right: attach */} - Generate diagnostics bundle + Attach file - )} -
+ + )} + + {/* Right: mic — ghost icon, no background when idle */} + {dictationProvider && ( + + + + + + {!isEnabled ? ( +

Dictation not configured (Settings)

+ ) : ( +

Voice dictation{isRecording ? '' : ' • Say "submit" to send'}

+ )} +
+
+ )} + + {/* Right: send / stop — soft gray circle with up-arrow */} + {isLoading && !hasSubmittableContent ? ( + + ) : ( + + + + + + + +

{getSubmitButtonTooltip()}

+
+
+ )} {sessionId && diagnosticsOpen && ( - {sessionId && showCreateRecipeModal && ( - setShowCreateRecipeModal(false)} - sessionId={sessionId} - /> - )} - - {recipe && showEditRecipeModal && ( - setShowEditRecipeModal(false)} - recipe={recipe} - recipeId={recipeId} - /> - )}
); diff --git a/ui/desktop/src/components/ChatInputCard.tsx b/ui/desktop/src/components/ChatInputCard.tsx new file mode 100644 index 000000000000..f632f4af296b --- /dev/null +++ b/ui/desktop/src/components/ChatInputCard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { cn } from '../utils'; + +/** + * Shared visual wrapper for the ChatInput. + * + * Both the Hub (empty-chat landing) and the BaseChat (active session) + * present ChatInput as a floating rounded outlined card on the canvas. + * Centralizing it here keeps the look in sync and gives a single place + * to tweak the recipe. + */ +export const ChatInputCard: React.FC<{ + className?: string; + children: React.ReactNode; +}> = ({ className, children }) => ( +
+ {children} +
+); diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 018f2f872e5b..df80183efd3e 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -1,21 +1,16 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import { readAllConfig, readConfig, removeConfig, upsertConfig, providers } from '../api'; import { - readAllConfig, - readConfig, - removeConfig, - upsertConfig, - addExtension as apiAddExtension, - removeExtension as apiRemoveExtension, - providers, -} from '../api'; -import { getConfiguredExtensions } from '../acp/extensions'; + getConfiguredExtensions, + addConfiguredExtension, + removeConfiguredExtension, +} from '../acp/extensions'; import { pruneDeprecatedBundledExtensions, syncBundledExtensions } from './settings/extensions'; import type { ConfigResponse, UpsertConfigQuery, ConfigKeyQuery, ProviderDetails, - ExtensionQuery, ExtensionConfig, } from '../api'; @@ -113,10 +108,7 @@ export const ConfigProvider: React.FC = ({ children }) => { const addExtension = useCallback( async (name: string, config: ExtensionConfig, enabled: boolean) => { - const query: ExtensionQuery = { name, config, enabled }; - await apiAddExtension({ - body: query, - }); + await addConfiguredExtension(name, config, enabled); await reloadConfig(); // Refresh extensions list after successful addition await refreshExtensions(); @@ -126,7 +118,7 @@ export const ConfigProvider: React.FC = ({ children }) => { const removeExtension = useCallback( async (name: string) => { - await apiRemoveExtension({ path: { name: name } }); + await removeConfiguredExtension(name); await reloadConfig(); // Refresh extensions list after successful removal await refreshExtensions(); @@ -206,11 +198,10 @@ export const ConfigProvider: React.FC = ({ children }) => { config: ExtensionConfig, enabled: boolean ) => { - const query: ExtensionQuery = { name, config, enabled }; - await apiAddExtension({ body: query }); + await addConfiguredExtension(name, config, enabled); }; const removeExtensionForSync = async (name: string) => { - await apiRemoveExtension({ path: { name } }); + await removeConfiguredExtension(name); }; extensions = await pruneDeprecatedBundledExtensions(extensions, removeExtensionForSync); await syncBundledExtensions(extensions, addExtensionForSync); diff --git a/ui/desktop/src/components/Hub.tsx b/ui/desktop/src/components/Hub.tsx index 3cdeb21aa6fa..0fd38f32d892 100644 --- a/ui/desktop/src/components/Hub.tsx +++ b/ui/desktop/src/components/Hub.tsx @@ -1,47 +1,70 @@ -import { AppEvents } from '../constants/events'; /** * Hub Component * - * The Hub is the main landing page and entry point for the Goose Desktop application. - * It serves as the welcome screen where users can start new conversations. - * - * Key Responsibilities: - * - Displays SessionInsights to show session statistics and recent chats - * - Provides a ChatInput for users to start new conversations - * - Creates a new session and navigates to Pair with the session ID - * - Shows loading state while session is being created - * - * Navigation Flow: - * Hub (input submission) → Create Session → Pair (with session ID and initial message) + * The empty-chat landing screen. Visually it's "Pair with no messages yet" — + * a large time + greeting above a centered, narrower ChatInput. Submitting + * creates a session and navigates to /pair so the rest of the chat lifecycle + * lives there. */ -import { useEffect, useRef, useState } from 'react'; -import { SessionInsights } from './sessions/SessionsInsights'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { defineMessages, useIntl } from '../i18n'; +import { AppEvents } from '../constants/events'; import ChatInput from './ChatInput'; +import { ChatInputCard } from './ChatInputCard'; import { ChatState } from '../types/chatState'; import 'react-toastify/dist/ReactToastify.css'; import { View, ViewOptions } from '../utils/navigationUtils'; import { useConfig } from './ConfigContext'; import { - getExtensionConfigsWithOverrides, clearExtensionOverrides, + getExtensionConfigsWithOverrides, } from '../store/extensionOverrides'; import { getInitialWorkingDir } from '../utils/workingDir'; import { createSession } from '../sessions'; import LoadingGoose from './LoadingGoose'; import { UserInput } from '../types/message'; +const i18n = defineMessages({ + goodMorning: { id: 'hub.goodMorning', defaultMessage: 'Good morning' }, + goodAfternoon: { id: 'hub.goodAfternoon', defaultMessage: 'Good afternoon' }, + goodEvening: { id: 'hub.goodEvening', defaultMessage: 'Good evening' }, +}); + +function useClock(): { time: string; meridiem: string; hour: number } { + const [now, setNow] = useState(() => new Date()); + useEffect(() => { + const interval = setInterval(() => setNow(new Date()), 30_000); + return () => clearInterval(interval); + }, []); + + const hour = now.getHours(); + const minutes = now.getMinutes(); + const meridiem = hour >= 12 ? 'PM' : 'AM'; + const displayHour = ((hour + 11) % 12) + 1; + const time = `${displayHour}:${String(minutes).padStart(2, '0')}`; + return { time, meridiem, hour }; +} + export default function Hub({ setView, }: { setView: (view: View, viewOptions?: ViewOptions) => void; }) { + const intl = useIntl(); const { extensionsList } = useConfig(); const [workingDir, setWorkingDir] = useState(getInitialWorkingDir()); const [isCreatingSession, setIsCreatingSession] = useState(false); const inputRef = useRef(null); + const { time, meridiem, hour } = useClock(); - // rAF is more reliable than autoFocus across async render boundaries (Suspense, OnboardingGuard, etc.) + const greeting = useMemo(() => { + if (hour < 12) return intl.formatMessage(i18n.goodMorning); + if (hour < 18) return intl.formatMessage(i18n.goodAfternoon); + return intl.formatMessage(i18n.goodEvening); + }, [intl, hour]); + + // rAF is more reliable than autoFocus across async render boundaries. useEffect(() => { const frameId = requestAnimationFrame(() => { inputRef.current?.focus(); @@ -51,67 +74,74 @@ export default function Hub({ const handleSubmit = async (input: UserInput) => { const { msg: userMessage, images } = input; - if ((images.length > 0 || userMessage.trim()) && !isCreatingSession) { - const extensionConfigs = getExtensionConfigsWithOverrides(extensionsList); - clearExtensionOverrides(); - setIsCreatingSession(true); + if (!(images.length > 0 || userMessage.trim()) || isCreatingSession) return; + + const extensionConfigs = getExtensionConfigsWithOverrides(extensionsList); + clearExtensionOverrides(); + setIsCreatingSession(true); - try { - const session = await createSession(workingDir, { - extensionConfigs, - allExtensions: extensionConfigs.length > 0 ? undefined : extensionsList, - }); + try { + const session = await createSession(workingDir, { + extensionConfigs, + allExtensions: extensionConfigs.length > 0 ? undefined : extensionsList, + }); - window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED)); - window.dispatchEvent( - new CustomEvent(AppEvents.ADD_ACTIVE_SESSION, { - detail: { sessionId: session.id, initialMessage: { msg: userMessage, images } }, - }) - ); + window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED)); + window.dispatchEvent( + new CustomEvent(AppEvents.ADD_ACTIVE_SESSION, { + detail: { sessionId: session.id, initialMessage: { msg: userMessage, images } }, + }) + ); - setView('pair', { - disableAnimation: true, - resumeSessionId: session.id, - initialMessage: { msg: userMessage, images }, - }); - } catch (error) { - console.error('Failed to create session:', error); - setIsCreatingSession(false); - } + setView('pair', { + disableAnimation: true, + resumeSessionId: session.id, + initialMessage: { msg: userMessage, images }, + }); + } catch (error) { + console.error('Failed to create session:', error); + setIsCreatingSession(false); } }; return ( -
-
- - {isCreatingSession && ( -
- -
- )} -
+
+
+
+ + {time} + + {meridiem} +
+

{greeting}

-
- {}} - initialValue="" - setView={setView} - totalTokens={0} - accumulatedInputTokens={0} - accumulatedOutputTokens={0} - droppedFiles={[]} - onFilesProcessed={() => {}} - messages={[]} - disableAnimation={false} - toolCount={0} - onWorkingDirChange={setWorkingDir} - inputRef={inputRef} - /> + + {}} + initialValue="" + setView={setView} + totalTokens={0} + accumulatedInputTokens={0} + accumulatedOutputTokens={0} + droppedFiles={[]} + onFilesProcessed={() => {}} + messages={[]} + disableAnimation={false} + toolCount={0} + onWorkingDirChange={setWorkingDir} + inputRef={inputRef} + /> +
+ + {isCreatingSession && ( +
+ +
+ )}
); } diff --git a/ui/desktop/src/components/Layout/AppLayout.tsx b/ui/desktop/src/components/Layout/AppLayout.tsx index 845d525ae9b6..e28a47eb2274 100644 --- a/ui/desktop/src/components/Layout/AppLayout.tsx +++ b/ui/desktop/src/components/Layout/AppLayout.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { IpcRendererEvent } from 'electron'; import { Outlet, useLocation } from 'react-router-dom'; import { motion } from 'framer-motion'; @@ -14,10 +14,6 @@ import { cn } from '../../utils'; import { UserInput } from '../../types/message'; const i18n = defineMessages({ - closeNavigation: { - id: 'appLayout.closeNavigation', - defaultMessage: 'Close navigation', - }, openNavigation: { id: 'appLayout.openNavigation', defaultMessage: 'Open navigation', @@ -53,91 +49,7 @@ const AppLayoutContent: React.FC = ({ activeSessions }) = return () => window.electron.off('fullscreen-change', handler); }, [safeIsMacOS]); - const { - isNavExpanded, - setIsNavExpanded, - effectiveNavigationMode, - effectiveNavigationStyle, - navigationPosition, - isHorizontalNav, - isCondensedIconOnly, - } = useNavigationContext(); - - const [navWidth, setNavWidth] = useState(null); - const navWidthRef = useRef(null); - - useEffect(() => { - window.electron.getSetting('navExpandedWidth').then((delta) => { - if (delta !== null) { - setNavWidth( - Math.min( - NAV_DIMENSIONS.MAX_NAV_WIDTH, - Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, NAV_DIMENSIONS.CONDENSED_WIDTH + delta) - ) - ); - } - }); - }, []); - - const isResizable = - !isHorizontalNav && !isCondensedIconOnly && effectiveNavigationMode === 'push' && isNavExpanded; - - const dragStateRef = useRef<{ startX: number; startWidth: number; direction: 1 | -1 } | null>( - null - ); - const navRef = useRef(null); - - const onMouseMove = useCallback((e: MouseEvent) => { - if (!dragStateRef.current) return; - const delta = (e.clientX - dragStateRef.current.startX) * dragStateRef.current.direction; - const newWidth = Math.min( - NAV_DIMENSIONS.MAX_NAV_WIDTH, - Math.max(NAV_DIMENSIONS.MIN_NAV_WIDTH, dragStateRef.current.startWidth + delta) - ); - navWidthRef.current = newWidth; - setNavWidth(newWidth); - }, []); - - const onMouseUp = useCallback(() => { - dragStateRef.current = null; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('mouseup', onMouseUp); - if (navWidthRef.current !== null) { - window.electron.setSetting( - 'navExpandedWidth', - navWidthRef.current - NAV_DIMENSIONS.CONDENSED_WIDTH - ); - } - }, [onMouseMove]); - - const onHandleMouseDown = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - const currentWidth = - navRef.current?.getBoundingClientRect().width ?? NAV_DIMENSIONS.CONDENSED_WIDTH; - dragStateRef.current = { - startX: e.clientX, - startWidth: currentWidth, - direction: navigationPosition === 'right' ? -1 : 1, - }; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - window.addEventListener('mousemove', onMouseMove); - window.addEventListener('mouseup', onMouseUp); - }, - [navigationPosition, onMouseMove, onMouseUp] - ); - - useEffect(() => { - return () => { - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('mouseup', onMouseUp); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - }, [onMouseMove, onMouseUp]); + const { isNavExpanded, setIsNavExpanded } = useNavigationContext(); if (!chatContext) { throw new Error('AppLayoutContent must be used within ChatProvider'); @@ -145,181 +57,57 @@ const AppLayoutContent: React.FC = ({ activeSessions }) = const { setChat } = chatContext; - // Hide the titlebar drag region when nav is at the top in push mode, - // since the nav occupies that space and the drag region blocks interactions - const isPushTopNav = - effectiveNavigationMode === 'push' && navigationPosition === 'top' && isNavExpanded; - React.useEffect(() => { - const dragRegion = document.querySelector('.titlebar-drag-region') as HTMLElement | null; - if (!dragRegion) return; - if (isPushTopNav) { - dragRegion.style.display = 'none'; - } else { - dragRegion.style.display = ''; - } - return () => { - dragRegion.style.display = ''; - }; - }, [isPushTopNav]); - const needsTrafficLightInset = safeIsMacOS && !isFullScreen; const headerPadding = needsTrafficLightInset ? 'pl-[96px]' : 'pl-4'; const headerTop = needsTrafficLightInset ? 'top-[15px]' : 'top-[11px]'; - // Determine flex direction based on navigation position (for push mode) - const getLayoutClass = () => { - if (effectiveNavigationMode === 'overlay') { - return 'flex-row'; - } - - switch (navigationPosition) { - case 'top': - return 'flex-col'; - case 'bottom': - return 'flex-col-reverse'; - case 'left': - return 'flex-row'; - case 'right': - return 'flex-row-reverse'; - default: - return 'flex-row'; - } - }; - - // Main content area - const mainContent = ( -
-
- - {/* Always render ChatSessionsContainer to keep SSE connections alive. - When navigating away from /pair, hide it with CSS */} -
- -
-
-
- ); - return ( -
- {/* Header controls */} -
- {/* Navigation trigger */} - -
- - {/* Main content with navigation */} -
- {/* Push mode navigation (inline) with animation */} - {effectiveNavigationMode === 'push' && ( - setIsNavExpanded(true)} + className="no-drag hover:!bg-background-tertiary" + variant="ghost" + size="xs" + title={intl.formatMessage(i18n.openNavigation)} > -
- -
- {isResizable && ( -
-
-
- )} - - )} + + +
+ )} - {/* Main content */} - {mainContent} + {/* Main content with navigation. Shared white canvas; the sidebar is a + rounded outlined card floating on it with breathing room. */} +
+ +
+ +
+
+ + {/* Main content — no border / no card; just flows on the canvas. */} +
+ + {/* Always render ChatSessionsContainer to keep SSE connections alive. + When navigating away from /pair, hide it with CSS */} +
+ +
+
- - {/* Overlay mode navigation */} - {effectiveNavigationMode === 'overlay' && }
); }; diff --git a/ui/desktop/src/components/Layout/CondensedRenderer.tsx b/ui/desktop/src/components/Layout/CondensedRenderer.tsx deleted file mode 100644 index f6f7feb3c9b4..000000000000 --- a/ui/desktop/src/components/Layout/CondensedRenderer.tsx +++ /dev/null @@ -1,377 +0,0 @@ -import React, { useState } from 'react'; -import { GripVertical, ChevronDown, ChevronRight, Plus } from 'lucide-react'; -import { motion } from 'framer-motion'; -import { defineMessages, useIntl } from '../../i18n'; -import { cn } from '../../utils'; -import { DropdownMenu, DropdownMenuTrigger } from '../ui/dropdown-menu'; -import { ChatSessionsDropdown, SessionsList } from './navigation'; -import { ChatHistorySearch } from '../conversation/ChatHistorySearch'; -import type { NavigationRendererProps } from './navigation/types'; -import { getNavItemLabel } from '../../hooks/useNavigationItems'; - -const i18n = defineMessages({ - newChat: { - id: 'condensedRenderer.newChat', - defaultMessage: 'New Chat', - }, -}); - -export const CondensedRenderer: React.FC = ({ - isOverlayMode, - navigationPosition, - isCondensedIconOnly, - className, - visibleItems, - isActive, - recentSessions, - activeSessionId, - onNavClick, - onNewChat, - onSessionClick, - onFetchSessions, - getSessionStatus, - clearUnread, - isChatExpanded, - onToggleChatExpanded, - drag, - navFocusRef, -}) => { - const intl = useIntl(); - const [chatPopoverOpen, setChatPopoverOpen] = useState(false); - - const isVertical = navigationPosition === 'left' || navigationPosition === 'right'; - const isTopPosition = navigationPosition === 'top'; - const isBottomPosition = navigationPosition === 'bottom'; - - return ( - - {/* Top spacer (vertical only) */} - {isVertical && ( -
- )} - - {/* Left spacer (horizontal top position only) */} - {!isVertical && isTopPosition && ( -
- )} - - {/* Search bar — skip mount entirely in icon-only mode so the - document-level Cmd/Ctrl+K handler inside ChatHistorySearch - does not intercept the shortcut when no UI is visible. */} - {!isCondensedIconOnly && ( -
- -
- )} - - {/* Navigation items */} - {isVertical ? ( -
- {visibleItems.map((item, index) => { - const Icon = item.icon; - const active = isActive(item.path); - const isDragging = drag.draggedItem === item.id; - const isDragOver = drag.dragOverItem === item.id; - const isChatItem = item.id === 'chat'; - - return ( - drag.onDragStart(e as unknown as React.DragEvent, item.id)} - onDragOver={(e) => drag.onDragOver(e as unknown as React.DragEvent, item.id)} - onDrop={(e) => drag.onDrop(e as unknown as React.DragEvent, item.id)} - onDragEnd={drag.onDragEnd} - initial={{ opacity: 0 }} - animate={{ opacity: isDragging ? 0.5 : 1 }} - transition={{ duration: 0.15, delay: index * 0.02 }} - className={cn( - 'relative cursor-move group', - isCondensedIconOnly ? 'flex-shrink-0' : 'w-full flex-shrink-0', - isDragOver && 'ring-2 ring-blue-500 rounded-lg', - isChatItem && !isCondensedIconOnly && 'overflow-visible' - )} - > -
- {/* Chat item with dropdown in icon-only mode */} - {isChatItem && isCondensedIconOnly ? ( - - - - - onNavClick('/sessions')} - /> - - ) : ( - <> - {isChatItem && !isCondensedIconOnly ? ( -
- -
- -
- - - {getNavItemLabel(item, intl)} - -
- {isChatExpanded ? ( - - ) : ( - - )} -
-
- {!isChatExpanded && ( - { - e.stopPropagation(); - onNewChat(); - }} - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.95 }} - className={cn( - 'absolute -right-9 top-1/2 -translate-y-1/2 p-1.5 rounded-md z-10', - 'opacity-0 group-hover:opacity-100 transition-opacity', - 'bg-background-tertiary hover:bg-background-inverse hover:text-text-inverse', - 'flex items-center justify-center' - )} - title={intl.formatMessage(i18n.newChat)} - > - - - )} -
- ) : ( - onNavClick(item.path)} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - className={cn( - 'flex flex-row items-center gap-2', - 'relative rounded-lg transition-colors duration-200 no-drag', - isCondensedIconOnly - ? 'justify-center p-2.5' - : 'w-full pl-2 pr-4 py-2.5', - active - ? 'bg-background-inverse text-text-inverse' - : 'bg-background-primary hover:bg-background-tertiary' - )} - > - {!isCondensedIconOnly && ( -
- -
- )} - - {!isCondensedIconOnly && ( - - {getNavItemLabel(item, intl)} - - )} - {!isCondensedIconOnly && item.getTag && ( -
- - {item.getTag()} - -
- )} -
- )} - - )} - {isChatItem && !isCondensedIconOnly && ( - onNavClick('/sessions')} - /> - )} -
-
- ); - })} - -
-
- ) : ( - /* Horizontal navigation items */ - visibleItems.map((item, index) => { - const Icon = item.icon; - const active = isActive(item.path); - const isDragging = drag.draggedItem === item.id; - const isDragOver = drag.dragOverItem === item.id; - const isChatItem = item.id === 'chat'; - - return ( - drag.onDragStart(e as unknown as React.DragEvent, item.id)} - onDragOver={(e) => drag.onDragOver(e as unknown as React.DragEvent, item.id)} - onDrop={(e) => drag.onDrop(e as unknown as React.DragEvent, item.id)} - onDragEnd={drag.onDragEnd} - initial={{ opacity: 0 }} - animate={{ opacity: isDragging ? 0.5 : 1 }} - transition={{ duration: 0.15, delay: index * 0.02 }} - className={cn( - 'relative cursor-move group flex-shrink-0', - isDragOver && 'ring-2 ring-blue-500 rounded-lg', - isChatItem && !isCondensedIconOnly && 'overflow-visible' - )} - > -
- {isChatItem ? ( - - - - - - {getNavItemLabel(item, intl)} - - - - onNavClick('/sessions')} - /> - - ) : ( - onNavClick(item.path)} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - className={cn( - 'flex flex-row items-center gap-2 px-3 py-2.5', - 'relative rounded-lg transition-colors duration-200 no-drag', - active - ? 'bg-background-inverse text-text-inverse' - : 'bg-background-primary hover:bg-background-tertiary' - )} - > - - - {getNavItemLabel(item, intl)} - - - )} -
-
- ); - }) - )} - - {/* Right spacer (horizontal only) */} - {!isVertical && ( -
- )} - - ); -}; diff --git a/ui/desktop/src/components/Layout/ExpandedRenderer.tsx b/ui/desktop/src/components/Layout/ExpandedRenderer.tsx deleted file mode 100644 index d69a169437bb..000000000000 --- a/ui/desktop/src/components/Layout/ExpandedRenderer.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { GripVertical } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Z_INDEX } from './constants'; -import { cn } from '../../utils'; -import { DropdownMenu, DropdownMenuTrigger } from '../ui/dropdown-menu'; -import { ChatSessionsDropdown } from './navigation'; -import { ChatHistorySearch } from '../conversation/ChatHistorySearch'; -import type { NavigationRendererProps } from './navigation/types'; -import { useIntl } from '../../i18n'; -import { getNavItemLabel } from '../../hooks/useNavigationItems'; - -export const ExpandedRenderer: React.FC = ({ - isNavExpanded, - isOverlayMode, - navigationPosition, - onClose, - className, - visibleItems, - isActive, - recentSessions, - activeSessionId, - onNavClick, - onNewChat, - onSessionClick, - getSessionStatus, - clearUnread, - drag, - navFocusRef, -}) => { - const intl = useIntl(); - const [chatDropdownOpen, setChatDropdownOpen] = useState(false); - const [gridColumns, setGridColumns] = useState(2); - const [gridMeasured, setGridMeasured] = useState(false); - const [tilesReady, setTilesReady] = useState(false); - const [isClosing, setIsClosing] = useState(false); - const prevIsNavExpandedRef = useRef(isNavExpanded); - const gridRef = useRef(null); - - // Detect when nav is closing - useEffect(() => { - if (prevIsNavExpandedRef.current && !isNavExpanded) { - setIsClosing(true); - setTilesReady(false); - } else if (!prevIsNavExpandedRef.current && isNavExpanded) { - setIsClosing(false); - } - prevIsNavExpandedRef.current = isNavExpanded; - }, [isNavExpanded]); - - // Delay tiles animation until panel opens - useEffect(() => { - if (!isNavExpanded) { - setTilesReady(false); - return; - } - const timeoutId = setTimeout(() => setTilesReady(true), 150); - return () => clearTimeout(timeoutId); - }, [isNavExpanded]); - - // Track grid columns for spacer tiles - useEffect(() => { - if (!isNavExpanded) { - setGridMeasured(false); - return; - } - - setGridMeasured(false); - let rafId: number; - - const updateGridColumns = () => { - if (!gridRef.current) return; - const parent = gridRef.current.parentElement; - if (!parent) return; - - const parentStyle = window.getComputedStyle(parent); - const availableWidth = - parent.clientWidth - - parseFloat(parentStyle.paddingLeft) - - parseFloat(parentStyle.paddingRight); - - const minSize = navigationPosition === 'left' || navigationPosition === 'right' ? 140 : 160; - const gap = isOverlayMode ? 12 : 2; - const cols = Math.max(1, Math.floor((availableWidth + gap) / (minSize + gap))); - - setGridColumns(cols); - setGridMeasured(true); - }; - - const timeoutId = setTimeout(() => { - rafId = requestAnimationFrame(updateGridColumns); - }, 100); - - const resizeObserver = new ResizeObserver(() => { - cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(updateGridColumns); - }); - - const parent = gridRef.current?.parentElement; - if (parent) resizeObserver.observe(parent); - - return () => { - clearTimeout(timeoutId); - cancelAnimationFrame(rafId); - resizeObserver.disconnect(); - }; - }, [isNavExpanded, navigationPosition, isOverlayMode]); - - const isPushTopNav = !isOverlayMode && navigationPosition === 'top'; - const dragStyle = isPushTopNav ? ({ WebkitAppRegion: 'drag' } as React.CSSProperties) : undefined; - const showContent = !isClosing || isOverlayMode; - - const navContent = ( - - {showContent ? ( -
- {/* Search bar spanning full width */} -
- -
- - {visibleItems.map((item, index) => { - const Icon = item.icon; - const active = isActive(item.path); - const isDragging = drag.draggedItem === item.id; - const isDragOver = drag.dragOverItem === item.id; - const isChatItem = item.id === 'chat'; - - if (isChatItem) { - return ( - - drag.onDragStart(e as unknown as React.DragEvent, item.id)} - onDragOver={(e) => drag.onDragOver(e as unknown as React.DragEvent, item.id)} - onDrop={(e) => drag.onDrop(e as unknown as React.DragEvent, item.id)} - onDragEnd={drag.onDragEnd} - initial={{ opacity: 0 }} - animate={{ opacity: tilesReady ? (isDragging ? 0.5 : 1) : 0 }} - transition={{ duration: 0.15, delay: tilesReady ? index * 0.03 : 0 }} - className={cn( - 'relative cursor-move group', - isDragOver && 'ring-2 ring-blue-500 rounded-lg' - )} - > -
- - -
-
- -
- {item.getTag && ( -
- - {item.getTag()} - -
- )} -
- -

{getNavItemLabel(item, intl)}

-
-
-
-
-
- onNavClick('/sessions')} - /> -
-
- ); - } - - return ( - drag.onDragStart(e as unknown as React.DragEvent, item.id)} - onDragOver={(e) => drag.onDragOver(e as unknown as React.DragEvent, item.id)} - onDrop={(e) => drag.onDrop(e as unknown as React.DragEvent, item.id)} - onDragEnd={drag.onDragEnd} - initial={{ opacity: 0 }} - animate={{ opacity: tilesReady ? (isDragging ? 0.5 : 1) : 0 }} - transition={{ duration: 0.15, delay: tilesReady ? index * 0.03 : 0 }} - className={cn( - 'relative cursor-move group', - isDragOver && 'ring-2 ring-blue-500 rounded-lg' - )} - > - - - - - ); - })} - - {/* Spacer tiles */} - {!isOverlayMode && - gridMeasured && - gridColumns >= 2 && - Array.from({ - length: - navigationPosition === 'left' || navigationPosition === 'right' - ? ((gridColumns - (visibleItems.length % gridColumns)) % gridColumns) + - gridColumns * 6 - : (gridColumns - (visibleItems.length % gridColumns)) % gridColumns, - }).map((_, index) => ( -
-
-
- ))} -
- ) : null} - - ); - - // Expanded overlay uses its own AnimatePresence - if (isOverlayMode) { - return ( - - {isNavExpanded && ( -
- -
-
-
{navContent}
-
-
-
- )} -
- ); - } - - return navContent; -}; diff --git a/ui/desktop/src/components/Layout/NavigationContext.tsx b/ui/desktop/src/components/Layout/NavigationContext.tsx index c295328f79c7..0a14d7d63c38 100644 --- a/ui/desktop/src/components/Layout/NavigationContext.tsx +++ b/ui/desktop/src/components/Layout/NavigationContext.tsx @@ -8,49 +8,16 @@ import React, { useState, } from 'react'; -export type NavigationMode = 'push' | 'overlay'; -export type NavigationStyle = 'expanded' | 'condensed'; -export type NavigationPosition = 'top' | 'bottom' | 'left' | 'right'; - -export interface NavigationPreferences { - itemOrder: string[]; - enabledItems: string[]; -} - -export const HIDDEN_NAV_ITEM_IDS = ['apps']; - -export const DEFAULT_ITEM_ORDER = [ - 'home', - 'chat', - 'recipes', - 'skills', - 'scheduler', - 'extensions', - 'settings', -]; - -export const DEFAULT_ENABLED_ITEMS = [...DEFAULT_ITEM_ORDER]; - -const RESPONSIVE_BREAKPOINT = 700; +/** + * When the window is narrower than this many CSS pixels, we auto-collapse + * the sidebar. The user can re-expand it via the menu button; it will only + * auto-collapse again if they go below the threshold from above. + */ +const NARROW_WINDOW_THRESHOLD = 700; interface NavigationContextValue { isNavExpanded: boolean; setIsNavExpanded: (expanded: boolean) => void; - navigationMode: NavigationMode; - setNavigationMode: (mode: NavigationMode) => void; - effectiveNavigationMode: NavigationMode; - navigationStyle: NavigationStyle; - setNavigationStyle: (style: NavigationStyle) => void; - effectiveNavigationStyle: NavigationStyle; - navigationPosition: NavigationPosition; - setNavigationPosition: (position: NavigationPosition) => void; - preferences: NavigationPreferences; - updatePreferences: (prefs: NavigationPreferences) => void; - isHorizontalNav: boolean; - isCondensedIconOnly: boolean; - isOverlayMode: boolean; - isChatExpanded: boolean; - setIsChatExpanded: (expanded: boolean) => void; } const NavigationContext = createContext(null); @@ -77,101 +44,11 @@ export const NavigationProvider: React.FC = ({ children return stored !== 'false'; }); - const [isBelowBreakpoint, setIsBelowBreakpoint] = useState( - () => window.innerWidth < RESPONSIVE_BREAKPOINT - ); - - const [navigationMode, setNavigationModeState] = useState(() => { - const stored = localStorage.getItem('navigation_mode'); - return (stored as NavigationMode) || 'push'; - }); - - const [navigationStyle, setNavigationStyleState] = useState(() => { - const stored = localStorage.getItem('navigation_style'); - return (stored as NavigationStyle) || 'condensed'; - }); - - const [navigationPosition, setNavigationPositionState] = useState(() => { - const stored = localStorage.getItem('navigation_position'); - return (stored as NavigationPosition) || 'left'; - }); - - const [preferences, setPreferences] = useState(() => { - const stored = localStorage.getItem('navigation_preferences'); - if (stored) { - try { - const parsed = JSON.parse(stored); - // Only backfill truly new default IDs (not previously known to the user). - // Using itemOrder as the source of truth ensures items the user - // intentionally disabled stay disabled. - const itemOrder = (parsed.itemOrder ?? []).filter( - (id: string) => !HIDDEN_NAV_ITEM_IDS.includes(id) - ); - const enabledItems = (parsed.enabledItems ?? []).filter( - (id: string) => !HIDDEN_NAV_ITEM_IDS.includes(id) - ); - const newIds = DEFAULT_ITEM_ORDER.filter((id) => !itemOrder.includes(id)); - return { - itemOrder: [...itemOrder, ...newIds], - enabledItems: [...enabledItems, ...newIds], - }; - } catch { - console.error('Failed to parse navigation preferences'); - } - } - return { - itemOrder: DEFAULT_ITEM_ORDER, - enabledItems: DEFAULT_ENABLED_ITEMS, - }; - }); - - const [isChatExpanded, setIsChatExpandedState] = useState(() => { - const stored = localStorage.getItem('navigation_chat_expanded'); - return stored !== 'false'; - }); - - useEffect(() => { - const mql = window.matchMedia(`(max-width: ${RESPONSIVE_BREAKPOINT - 1}px)`); - const onChange = () => setIsBelowBreakpoint(window.innerWidth < RESPONSIVE_BREAKPOINT); - mql.addEventListener('change', onChange); - setIsBelowBreakpoint(window.innerWidth < RESPONSIVE_BREAKPOINT); - return () => mql.removeEventListener('change', onChange); - }, []); - const setIsNavExpanded = useCallback((expanded: boolean) => { setIsNavExpandedState(expanded); localStorage.setItem('navigation_expanded', String(expanded)); }, []); - const setNavigationMode = useCallback((mode: NavigationMode) => { - setNavigationModeState(mode); - localStorage.setItem('navigation_mode', mode); - window.dispatchEvent(new CustomEvent('navigation-mode-changed', { detail: { mode } })); - }, []); - - const setNavigationStyle = useCallback((style: NavigationStyle) => { - setNavigationStyleState(style); - localStorage.setItem('navigation_style', style); - window.dispatchEvent(new CustomEvent('navigation-style-changed', { detail: { style } })); - }, []); - - const setNavigationPosition = useCallback((position: NavigationPosition) => { - setNavigationPositionState(position); - localStorage.setItem('navigation_position', position); - window.dispatchEvent(new CustomEvent('navigation-position-changed', { detail: { position } })); - }, []); - - const updatePreferences = useCallback((newPrefs: NavigationPreferences) => { - setPreferences(newPrefs); - localStorage.setItem('navigation_preferences', JSON.stringify(newPrefs)); - window.dispatchEvent(new CustomEvent('navigation-preferences-updated', { detail: newPrefs })); - }, []); - - const setIsChatExpanded = useCallback((expanded: boolean) => { - setIsChatExpandedState(expanded); - localStorage.setItem('navigation_chat_expanded', String(expanded)); - }, []); - const isNavExpandedRef = useRef(isNavExpanded); useEffect(() => { isNavExpandedRef.current = isNavExpanded; @@ -187,53 +64,32 @@ export const NavigationProvider: React.FC = ({ children }; }, [setIsNavExpanded]); + // Auto-collapse the sidebar when the window becomes narrow. Track the + // previous width so we only fire on the downward crossing — the user can + // re-expand it manually without us fighting them on the next resize. useEffect(() => { - const handleModeChange = (e: Event) => setNavigationModeState((e as CustomEvent).detail.mode); - const handleStyleChange = (e: Event) => - setNavigationStyleState((e as CustomEvent).detail.style); - const handlePositionChange = (e: Event) => - setNavigationPositionState((e as CustomEvent).detail.position); - const handlePrefsChange = (e: Event) => setPreferences((e as CustomEvent).detail); - - window.addEventListener('navigation-mode-changed', handleModeChange); - window.addEventListener('navigation-style-changed', handleStyleChange); - window.addEventListener('navigation-position-changed', handlePositionChange); - window.addEventListener('navigation-preferences-updated', handlePrefsChange); - - return () => { - window.removeEventListener('navigation-mode-changed', handleModeChange); - window.removeEventListener('navigation-style-changed', handleStyleChange); - window.removeEventListener('navigation-position-changed', handlePositionChange); - window.removeEventListener('navigation-preferences-updated', handlePrefsChange); + let lastWidth = window.innerWidth; + if (lastWidth < NARROW_WINDOW_THRESHOLD && isNavExpandedRef.current) { + setIsNavExpanded(false); + } + const onResize = () => { + const width = window.innerWidth; + if ( + width < NARROW_WINDOW_THRESHOLD && + lastWidth >= NARROW_WINDOW_THRESHOLD && + isNavExpandedRef.current + ) { + setIsNavExpanded(false); + } + lastWidth = width; }; - }, []); - - const isHorizontalNav = navigationPosition === 'top' || navigationPosition === 'bottom'; - const effectiveNavigationMode: NavigationMode = - navigationStyle === 'expanded' && isBelowBreakpoint ? 'overlay' : navigationMode; - const effectiveNavigationStyle: NavigationStyle = - navigationMode === 'overlay' ? 'expanded' : navigationStyle; - const isCondensedIconOnly = !isHorizontalNav && isBelowBreakpoint; - const isOverlayMode = effectiveNavigationMode === 'overlay'; + window.addEventListener('resize', onResize); + return () => window.removeEventListener('resize', onResize); + }, [setIsNavExpanded]); const value: NavigationContextValue = { isNavExpanded, setIsNavExpanded, - navigationMode, - setNavigationMode, - effectiveNavigationMode, - navigationStyle, - setNavigationStyle, - effectiveNavigationStyle, - navigationPosition, - setNavigationPosition, - preferences, - updatePreferences, - isHorizontalNav, - isCondensedIconOnly, - isOverlayMode, - isChatExpanded, - setIsChatExpanded, }; return {children}; diff --git a/ui/desktop/src/components/Layout/NavigationPanel.tsx b/ui/desktop/src/components/Layout/NavigationPanel.tsx index f8e1b63012ba..bce83155ac5c 100644 --- a/ui/desktop/src/components/Layout/NavigationPanel.tsx +++ b/ui/desktop/src/components/Layout/NavigationPanel.tsx @@ -1,44 +1,139 @@ -import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { ChevronDown, ChevronRight, PanelLeft } from 'lucide-react'; +import { motion } from 'framer-motion'; import { useNavigationContext } from './NavigationContext'; import { useConfig } from '../ConfigContext'; -import { useNavigationSessions } from '../../hooks/useNavigationSessions'; -import { getNavItemById, type NavItem } from '../../hooks/useNavigationItems'; +import { useNavigationSessions, getSessionDisplayName } from '../../hooks/useNavigationSessions'; +import { + NAV_ITEMS, + SETTINGS_NAV_ITEM, + getNavItemLabel, + type NavItem, +} from '../../hooks/useNavigationItems'; import { AppEvents } from '../../constants/events'; -import { CondensedRenderer } from './CondensedRenderer'; -import { ExpandedRenderer } from './ExpandedRenderer'; -import { NavigationOverlay } from './navigation'; -import type { SessionStatus, DragHandlers } from './navigation/types'; +import { Goose } from '../icons/Goose'; +import { InlineEditText } from '../common/InlineEditText'; +import { SessionIndicators } from '../SessionIndicators'; +import { updateSessionName, type Session } from '../../api'; +import { cn } from '../../utils'; +import { defineMessages, useIntl } from '../../i18n'; -export const Navigation: React.FC<{ className?: string }> = ({ className }) => { - const { - isNavExpanded, - setIsNavExpanded, - navigationPosition, - preferences, - updatePreferences, - isCondensedIconOnly, - isOverlayMode, - effectiveNavigationStyle, - isChatExpanded, - setIsChatExpanded, - } = useNavigationContext(); +type StreamState = 'idle' | 'loading' | 'streaming' | 'error'; + +interface SessionStatus { + streamState: StreamState; + hasUnreadActivity: boolean; +} + +const i18n = defineMessages({ + chats: { + id: 'navigationPanel.chats', + defaultMessage: 'Chats', + }, + noChats: { + id: 'navigationPanel.noChats', + defaultMessage: 'No recent chats', + }, + untitledSession: { + id: 'navigationPanel.untitledSession', + defaultMessage: 'Untitled session', + }, + collapseSidebar: { + id: 'navigationPanel.collapseSidebar', + defaultMessage: 'Collapse sidebar', + }, +}); + +const navItemClass = (active: boolean) => + cn( + 'flex flex-row items-center gap-3 outline-none no-drag w-full', + 'rounded-full px-3 py-2 text-sm font-medium transition-colors', + active + ? 'bg-background-tertiary text-text-primary' + : 'text-text-primary hover:bg-background-tertiary/60' + ); + +interface NavRowProps { + item: NavItem; + active: boolean; + onClick: () => void; +} + +const NavRow: React.FC = ({ item, active, onClick }) => { + const intl = useIntl(); + const Icon = item.icon; + return ( + + ); +}; + +interface SessionRowProps { + session: Session; + active: boolean; + status: SessionStatus | undefined; + onClick: () => void; + onRenamed: () => void; +} + +const SessionRow: React.FC = ({ session, active, status, onClick, onRenamed }) => { + const intl = useIntl(); + const [isEditing, setIsEditing] = useState(false); + const isStreaming = status?.streamState === 'streaming'; + const hasError = status?.streamState === 'error'; + const hasUnread = status?.hasUnreadActivity ?? false; + return ( +
!isEditing && onClick()} + className={cn( + 'flex items-center gap-2 px-3 py-1.5 rounded-full cursor-pointer text-sm', + 'hover:bg-background-tertiary/60 transition-colors', + active && 'bg-background-tertiary' + )} + > + { + await updateSessionName({ + path: { session_id: session.id }, + body: { name: newName }, + }); + onRenamed(); + }} + placeholder={intl.formatMessage(i18n.untitledSession)} + disabled={isStreaming} + singleClickEdit={false} + className="truncate text-text-primary flex-1 !px-0 !py-0 hover:bg-transparent" + editClassName="!text-sm" + onEditStart={() => setIsEditing(true)} + onEditEnd={() => setIsEditing(false)} + /> + +
+ ); +}; + +export const Navigation: React.FC<{ className?: string }> = ({ className }) => { + const intl = useIntl(); + const { isNavExpanded, setIsNavExpanded } = useNavigationContext(); const location = useLocation(); const { extensionsList } = useConfig(); const appsExtensionEnabled = !!extensionsList?.find((ext) => ext.name === 'apps')?.enabled; - const visibleItems = useMemo(() => { - return preferences.itemOrder - .filter((id) => preferences.enabledItems.includes(id)) - .map((id) => getNavItemById(id)) - .filter((item): item is NavItem => item !== undefined) - .filter((item) => { - if (item.path === '/apps') return appsExtensionEnabled; - return true; - }); - }, [preferences.itemOrder, preferences.enabledItems, appsExtensionEnabled]); + const visibleItems = useMemo(() => { + return NAV_ITEMS.filter((item) => { + if (item.path === '/apps') return appsExtensionEnabled; + return true; + }); + }, [appsExtensionEnabled]); const isActive = useCallback((path: string) => location.pathname === path, [location.pathname]); @@ -47,61 +142,8 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => { activeSessionId, fetchSessions, handleNavClick, - handleNewChat, handleSessionClick, - } = useNavigationSessions({ - onNavigate: isOverlayMode ? () => setIsNavExpanded(false) : undefined, - }); - - const [draggedItem, setDraggedItem] = useState(null); - const [dragOverItem, setDragOverItem] = useState(null); - - const onDragStart = useCallback((e: React.DragEvent, itemId: string) => { - setDraggedItem(itemId); - e.dataTransfer.effectAllowed = 'move'; - }, []); - - const onDragOver = useCallback( - (e: React.DragEvent, itemId: string) => { - e.preventDefault(); - if (draggedItem && draggedItem !== itemId) setDragOverItem(itemId); - }, - [draggedItem] - ); - - const onDrop = useCallback( - (e: React.DragEvent, dropItemId: string) => { - e.preventDefault(); - if (!draggedItem || draggedItem === dropItemId) return; - - const newOrder = [...preferences.itemOrder]; - const draggedIndex = newOrder.indexOf(draggedItem); - const dropIndex = newOrder.indexOf(dropItemId); - if (draggedIndex === -1 || dropIndex === -1) return; - - newOrder.splice(draggedIndex, 1); - newOrder.splice(dropIndex, 0, draggedItem); - updatePreferences({ ...preferences, itemOrder: newOrder }); - - setDraggedItem(null); - setDragOverItem(null); - }, - [draggedItem, preferences, updatePreferences] - ); - - const onDragEnd = useCallback(() => { - setDraggedItem(null); - setDragOverItem(null); - }, []); - - const drag: DragHandlers = { - draggedItem, - dragOverItem, - onDragStart, - onDragOver, - onDrop, - onDragEnd, - }; + } = useNavigationSessions(); const [sessionStatuses, setSessionStatuses] = useState>(new Map()); @@ -124,11 +166,6 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => { return () => window.removeEventListener(AppEvents.SESSION_STATUS_UPDATE, handleStatusUpdate); }, []); - const getSessionStatus = useCallback( - (sessionId: string) => sessionStatuses.get(sessionId), - [sessionStatuses] - ); - const clearUnread = useCallback((sessionId: string) => { setSessionStatuses((prev) => { const status = prev.get(sessionId); @@ -141,20 +178,6 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => { }); }, []); - useEffect(() => { - if (!(isOverlayMode && isNavExpanded)) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - setIsNavExpanded(false); - } - }; - - document.addEventListener('keydown', handleKeyDown, { capture: true }); - return () => document.removeEventListener('keydown', handleKeyDown, { capture: true }); - }, [isNavExpanded, isOverlayMode, setIsNavExpanded]); - const navFocusRef = useRef(null); useEffect(() => { @@ -164,58 +187,93 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => { } }, [isNavExpanded, fetchSessions]); - const onToggleChatExpanded = useCallback(() => { - setIsChatExpanded(!isChatExpanded); - }, [isChatExpanded, setIsChatExpanded]); - - const onClose = useCallback(() => setIsNavExpanded(false), [setIsNavExpanded]); - - const rendererProps = { - isNavExpanded, - isOverlayMode, - navigationPosition, - isCondensedIconOnly, - onClose, - className, - visibleItems, - isActive, - recentSessions, - activeSessionId, - onNavClick: handleNavClick, - onNewChat: handleNewChat, - onSessionClick: handleSessionClick, - onFetchSessions: fetchSessions, - getSessionStatus, - clearUnread, - isChatExpanded, - onToggleChatExpanded, - drag, - navFocusRef, - }; - - const content = - effectiveNavigationStyle === 'expanded' ? ( - - ) : ( - - ); - - if (isOverlayMode) { - if (effectiveNavigationStyle === 'expanded') { - // Expanded overlay uses its own AnimatePresence layout - return content; - } - return ( - setIsNavExpanded(false)} - > - {content} - - ); - } + const [isChatsExpanded, setIsChatsExpanded] = useState(true); if (!isNavExpanded) return null; - return content; + + return ( + + {/* Header: logo + collapse button. Top padding clears the macOS traffic lights. */} +
+ + +
+ + {/* Nav items */} +
+ {visibleItems.map((item) => ( + handleNavClick(item.path)} + /> + ))} +
+ + {/* Chats section — takes remaining vertical space */} +
+ + {isChatsExpanded && ( +
+ {recentSessions.length === 0 ? ( +
+ {intl.formatMessage(i18n.noChats)} +
+ ) : ( + recentSessions.map((session) => ( + { + clearUnread(session.id); + handleSessionClick(session.id); + }} + onRenamed={fetchSessions} + /> + )) + )} +
+ )} +
+ + {/* Settings pinned to bottom */} +
+ handleNavClick(SETTINGS_NAV_ITEM.path)} + /> +
+
+ ); }; diff --git a/ui/desktop/src/components/Layout/constants.ts b/ui/desktop/src/components/Layout/constants.ts index 81d3acdf9d43..41c525ea541b 100644 --- a/ui/desktop/src/components/Layout/constants.ts +++ b/ui/desktop/src/components/Layout/constants.ts @@ -1,16 +1,6 @@ export const NAV_DIMENSIONS = { - /** Width of condensed navigation in icon-only mode */ - CONDENSED_ICON_ONLY_WIDTH: 44, - /** Width of condensed navigation with labels */ - CONDENSED_WIDTH: 200, - /** Height of expanded navigation (horizontal mode) */ - EXPANDED_HEIGHT: 180, - /** Height of condensed navigation (horizontal mode) */ - CONDENSED_HEIGHT: 46, - /** Minimum width when resizing the navigation panel */ - MIN_NAV_WIDTH: 200, - /** Maximum width when resizing the navigation panel */ - MAX_NAV_WIDTH: 600, + /** Width of the navigation sidebar */ + NAV_WIDTH: 240, } as const; export const Z_INDEX = { diff --git a/ui/desktop/src/components/Layout/navigation/ChatSessionsDropdown.tsx b/ui/desktop/src/components/Layout/navigation/ChatSessionsDropdown.tsx deleted file mode 100644 index 2f2006a7afea..000000000000 --- a/ui/desktop/src/components/Layout/navigation/ChatSessionsDropdown.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import { MessageSquare, History, Plus, ChefHat } from 'lucide-react'; -import { - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, -} from '../../ui/dropdown-menu'; -import { SessionIndicators } from '../../SessionIndicators'; -import { cn } from '../../../utils'; -import { getSessionDisplayName, truncateMessage } from '../../../hooks/useNavigationSessions'; -import { defineMessages, useIntl } from '../../../i18n'; -import type { Session } from '../../../api'; -import type { SessionStatus } from './types'; - -const i18n = defineMessages({ - newChat: { - id: 'chatSessionsDropdown.newChat', - defaultMessage: 'New Chat', - }, - showAll: { - id: 'chatSessionsDropdown.showAll', - defaultMessage: 'Show All', - }, -}); - -interface ChatSessionsDropdownProps { - sessions: Session[]; - activeSessionId?: string; - side?: 'top' | 'bottom' | 'left' | 'right'; - zIndex?: number; - getSessionStatus: (sessionId: string) => SessionStatus | undefined; - clearUnread: (sessionId: string) => void; - onNewChat: () => void; - onSessionClick: (sessionId: string) => void; - onShowAll: () => void; -} - -export const ChatSessionsDropdown: React.FC = ({ - sessions, - activeSessionId, - side = 'right', - zIndex, - getSessionStatus, - clearUnread, - onNewChat, - onSessionClick, - onShowAll, -}) => { - const intl = useIntl(); - return ( - - - - {intl.formatMessage(i18n.newChat)} - - - {sessions.length > 0 && } - - {sessions.map((session) => { - const status = getSessionStatus(session.id); - const isStreaming = status?.streamState === 'streaming'; - const hasError = status?.streamState === 'error'; - const hasUnread = status?.hasUnreadActivity ?? false; - const isActiveSession = session.id === activeSessionId; - - return ( - { - clearUnread(session.id); - onSessionClick(session.id); - }} - className={cn( - 'flex items-center gap-2 px-3 py-2 text-sm rounded-lg cursor-pointer', - isActiveSession && 'bg-background-tertiary' - )} - > - {session.recipe ? ( - - ) : ( - - )} - - {truncateMessage(getSessionDisplayName(session), 30)} - - - - ); - })} - - {sessions.length > 0 && ( - <> - - - - {intl.formatMessage(i18n.showAll)} - - - )} - - ); -}; diff --git a/ui/desktop/src/components/Layout/navigation/NavigationOverlay.tsx b/ui/desktop/src/components/Layout/navigation/NavigationOverlay.tsx deleted file mode 100644 index e9ac6d34f7c6..000000000000 --- a/ui/desktop/src/components/Layout/navigation/NavigationOverlay.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { cn } from '../../../utils'; -import { Z_INDEX } from '../constants'; - -type NavigationPosition = 'top' | 'bottom' | 'left' | 'right'; - -interface NavigationOverlayProps { - isOpen: boolean; - position: NavigationPosition; - onClose: () => void; - children: React.ReactNode; -} - -export const NavigationOverlay: React.FC = ({ - isOpen, - position, - onClose, - children, -}) => { - return ( - - {isOpen && ( -
- {/* Backdrop */} - - - {/* Scrollable container for navigation panel */} -
-
-
{children}
-
-
-
- )} -
- ); -}; diff --git a/ui/desktop/src/components/Layout/navigation/SessionsList.tsx b/ui/desktop/src/components/Layout/navigation/SessionsList.tsx deleted file mode 100644 index 42705d24e410..000000000000 --- a/ui/desktop/src/components/Layout/navigation/SessionsList.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { MessageSquare, ChefHat, Plus, History } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { SessionIndicators } from '../../SessionIndicators'; -import { InlineEditText } from '../../common/InlineEditText'; -import { cn } from '../../../utils'; -import { getSessionDisplayName } from '../../../hooks/useNavigationSessions'; -import { updateSessionName } from '../../../api'; -import type { Session } from '../../../api'; -import type { SessionStatus } from './types'; -import { defineMessages, useIntl } from '../../../i18n'; - -const i18n = defineMessages({ - startNewChat: { - id: 'sessionsList.startNewChat', - defaultMessage: 'Start New Chat', - }, - untitledSession: { - id: 'sessionsList.untitledSession', - defaultMessage: 'Untitled session', - }, - showAll: { - id: 'sessionsList.showAll', - defaultMessage: 'Show All', - }, -}); - -interface SessionsListProps { - sessions: Session[]; - activeSessionId?: string; - isExpanded: boolean; - getSessionStatus: (sessionId: string) => SessionStatus | undefined; - clearUnread: (sessionId: string) => void; - onSessionClick: (sessionId: string) => void; - onSessionRenamed?: () => void; - onNewChat?: () => void; - onShowAll?: () => void; -} - -export const SessionsList: React.FC = ({ - sessions, - activeSessionId, - isExpanded, - getSessionStatus, - clearUnread, - onSessionClick, - onSessionRenamed, - onNewChat, - onShowAll, -}) => { - const intl = useIntl(); - const [editingSessionId, setEditingSessionId] = useState(null); - - const handleSaveSessionName = useCallback( - async (sessionId: string, newName: string) => { - await updateSessionName({ - path: { session_id: sessionId }, - body: { name: newName }, - }); - onSessionRenamed?.(); - }, - [onSessionRenamed] - ); - - return ( - - {isExpanded && ( - -
- {/* New Chat button as first item */} - {onNewChat && ( -
-
- - {intl.formatMessage(i18n.startNewChat)} -
- )} - - {sessions.map((session) => { - const status = getSessionStatus(session.id); - const isStreaming = status?.streamState === 'streaming'; - const hasError = status?.streamState === 'error'; - const hasUnread = status?.hasUnreadActivity ?? false; - const isActiveSession = session.id === activeSessionId; - const isEditing = editingSessionId === session.id; - - return ( -
{ - if (!isEditing) { - clearUnread(session.id); - onSessionClick(session.id); - } - }} - className={cn( - 'w-full text-left py-1.5 px-2 text-xs rounded-md', - 'hover:bg-background-tertiary transition-colors', - 'flex items-center gap-2 cursor-pointer', - isActiveSession && 'bg-background-tertiary' - )} - > -
- {session.recipe ? ( - - ) : ( - - )} - handleSaveSessionName(session.id, newName)} - placeholder={intl.formatMessage(i18n.untitledSession)} - disabled={isStreaming} - singleClickEdit={false} - className="truncate text-text-primary flex-1 !px-0 !py-0 hover:bg-transparent" - editClassName="!text-xs" - onEditStart={() => setEditingSessionId(session.id)} - onEditEnd={() => setEditingSessionId(null)} - /> - -
- ); - })} - - {/* Show All button at bottom */} - {onShowAll && sessions.length > 0 && ( -
-
- - {intl.formatMessage(i18n.showAll)} -
- )} -
- - )} - - ); -}; diff --git a/ui/desktop/src/components/Layout/navigation/index.ts b/ui/desktop/src/components/Layout/navigation/index.ts deleted file mode 100644 index 63a77a7b82ca..000000000000 --- a/ui/desktop/src/components/Layout/navigation/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ChatSessionsDropdown } from './ChatSessionsDropdown'; -export { NavigationOverlay } from './NavigationOverlay'; -export { SessionsList } from './SessionsList'; diff --git a/ui/desktop/src/components/Layout/navigation/types.ts b/ui/desktop/src/components/Layout/navigation/types.ts deleted file mode 100644 index bca2cf5bbb9a..000000000000 --- a/ui/desktop/src/components/Layout/navigation/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { NavItem } from '../../../hooks/useNavigationItems'; -import type { Session } from '../../../api'; -import type { NavigationPosition } from '../NavigationContext'; - -export type StreamState = 'idle' | 'loading' | 'streaming' | 'error'; - -export interface SessionStatus { - streamState: StreamState; - hasUnreadActivity: boolean; -} - -export interface DragHandlers { - draggedItem: string | null; - dragOverItem: string | null; - onDragStart: (e: React.DragEvent, itemId: string) => void; - onDragOver: (e: React.DragEvent, itemId: string) => void; - onDrop: (e: React.DragEvent, dropItemId: string) => void; - onDragEnd: () => void; -} - -export interface NavigationRendererProps { - isNavExpanded: boolean; - isOverlayMode: boolean; - navigationPosition: NavigationPosition; - isCondensedIconOnly: boolean; - onClose: () => void; - className?: string; - - // Items - visibleItems: NavItem[]; - isActive: (path: string) => boolean; - - // Sessions - recentSessions: Session[]; - activeSessionId?: string; - onNavClick: (path: string) => void; - onNewChat: () => void; - onSessionClick: (sessionId: string) => void; - onFetchSessions: () => void; - - // Session status - getSessionStatus: (sessionId: string) => SessionStatus | undefined; - clearUnread: (sessionId: string) => void; - - // Chat expand (condensed only, but simpler to keep uniform) - isChatExpanded: boolean; - onToggleChatExpanded: () => void; - - // Drag and drop - drag: DragHandlers; - - // Ref for focus management - navFocusRef: React.RefObject; -} diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 2128b134631c..295ad9b20af6 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -8,7 +8,8 @@ import { FixedExtensionEntry, useConfig } from '../ConfigContext'; import { toastService } from '../../toasts'; import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList'; import { nameToKey } from '../settings/extensions/utils'; -import { ExtensionConfig, getSessionExtensions } from '../../api'; +import { ExtensionConfig } from '../../api'; +import { getSessionExtensions } from '../../acp/extensions'; import { addToAgent, removeFromAgent } from '../settings/extensions/agent-api'; import { setExtensionOverride, @@ -119,14 +120,9 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS } try { - const response = await getSessionExtensions({ - path: { session_id: sessionId }, - }); - - if (response.data?.extensions) { - setSessionExtensions(response.data.extensions); - setIsSessionExtensionsLoaded(true); - } + const extensions = await getSessionExtensions(sessionId); + setSessionExtensions(extensions); + setIsSessionExtensionsLoaded(true); } catch (error) { console.error('Failed to fetch session extensions:', error); setIsSessionExtensionsLoaded(true); @@ -197,13 +193,8 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS } sortTimeoutRef.current = setTimeout(async () => { - const response = await getSessionExtensions({ - path: { session_id: sessionId }, - }); - - if (response.data?.extensions) { - setSessionExtensions(response.data.extensions); - } + const extensions = await getSessionExtensions(sessionId); + setSessionExtensions(extensions); setPendingSort(false); setIsTransitioning(false); setTogglingExtension(null); diff --git a/ui/desktop/src/components/bottom_menu/ContextWindowIndicator.tsx b/ui/desktop/src/components/bottom_menu/ContextWindowIndicator.tsx index 1e74de093527..f734d1b0ee1e 100644 --- a/ui/desktop/src/components/bottom_menu/ContextWindowIndicator.tsx +++ b/ui/desktop/src/components/bottom_menu/ContextWindowIndicator.tsx @@ -8,13 +8,8 @@ interface ContextWindowIndicatorProps { } const formatTokenCount = (count: number): string => { - if (count >= 1000000) { - const millions = count / 1000000; - return millions % 1 === 0 ? `${millions.toFixed(0)}M` : `${millions.toFixed(1)}M`; - } else if (count >= 1000) { - const thousands = count / 1000; - return thousands % 1 === 0 ? `${thousands.toFixed(0)}k` : `${thousands.toFixed(1)}k`; - } + if (count >= 1_000_000) return `${Math.round(count / 1_000_000)}M`; + if (count >= 1_000) return `${Math.round(count / 1_000)}k`; return count.toString(); }; @@ -35,15 +30,12 @@ export function ContextWindowIndicator({ const colorClass = getProgressColor(percentage); return ( - <> -
- - - {formatTokenCount(totalTokens)} / {formatTokenCount(tokenLimit)} - - -
-
- +
+ + + {formatTokenCount(totalTokens)} / {formatTokenCount(tokenLimit)} + + +
); } diff --git a/ui/desktop/src/components/bottom_menu/CostTracker.tsx b/ui/desktop/src/components/bottom_menu/CostTracker.tsx index 1ade649f544e..323beb3ee4d6 100644 --- a/ui/desktop/src/components/bottom_menu/CostTracker.tsx +++ b/ui/desktop/src/components/bottom_menu/CostTracker.tsx @@ -99,10 +99,7 @@ export function CostTracker({ return accumulatedCost ?? 0; }; - const formatCost = (cost: number): string => { - // Always show 4 decimal places for consistency - return cost.toFixed(4); - }; + const formatCost = (cost: number): string => cost.toFixed(2); // Show loading state or when we don't have model/provider info if (!currentModel || !currentProvider) { @@ -112,12 +109,9 @@ export function CostTracker({ // If still loading, show a placeholder if (isLoading) { return ( - <> -
- ... -
-
- +
+ ... +
); } @@ -129,14 +123,11 @@ export function CostTracker({ const freeProviders = ['ollama', 'local', 'localhost']; if (freeProviders.includes(currentProvider.toLowerCase())) { return ( - <> -
- - {inputTokens.toLocaleString()}↑ {outputTokens.toLocaleString()}↓ - -
-
- +
+ + {inputTokens.toLocaleString()}↑ {outputTokens.toLocaleString()}↓ + +
); } @@ -153,18 +144,15 @@ export function CostTracker({ }; return ( - <> - - -
- - 0.0000 -
-
- {getUnavailableTooltip()} -
-
- + + +
+ + 0.0000 +
+
+ {getUnavailableTooltip()} +
); } @@ -199,17 +187,14 @@ export function CostTracker({ }; return ( - <> - - -
- - {formatCost(totalCost)} -
-
- {getTooltipContent()} -
-
- + + +
+ + {formatCost(totalCost)} +
+
+ {getTooltipContent()} +
); } diff --git a/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx b/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx index 13b1b7a06566..10b24c886af1 100644 --- a/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx +++ b/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx @@ -188,7 +188,9 @@ export const DirSwitcher: React.FC = ({ disabled={isDirectoryChooserOpen} > -
{workingDir}
+
+ {workingDir.replace(/\/+$/, '').split('/').pop() || workingDir} +
diff --git a/ui/desktop/src/components/common/Greeting.tsx b/ui/desktop/src/components/common/Greeting.tsx deleted file mode 100644 index 66bc99633051..000000000000 --- a/ui/desktop/src/components/common/Greeting.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState } from 'react'; -import { useTextAnimator } from '../../hooks/use-text-animator'; -import { defineMessages, useIntl } from '../../i18n'; - -const i18n = defineMessages({ - readyToGetStarted: { - id: 'greeting.readyToGetStarted', - defaultMessage: 'Ready to get started?', - }, - whatToWorkOn: { - id: 'greeting.whatToWorkOn', - defaultMessage: 'What would you like to work on?', - }, - readyToBuild: { - id: 'greeting.readyToBuild', - defaultMessage: 'Ready to build something amazing?', - }, - whatToExplore: { - id: 'greeting.whatToExplore', - defaultMessage: 'What would you like to explore?', - }, - whatsOnYourMind: { - id: 'greeting.whatsOnYourMind', - defaultMessage: "What's on your mind?", - }, - whatShallWeCreate: { - id: 'greeting.whatShallWeCreate', - defaultMessage: 'What shall we create today?', - }, - whatProjectNeedsAttention: { - id: 'greeting.whatProjectNeedsAttention', - defaultMessage: 'What project needs attention?', - }, - whatToTackle: { - id: 'greeting.whatToTackle', - defaultMessage: 'What would you like to tackle?', - }, - whatNeedsToBeDone: { - id: 'greeting.whatNeedsToBeDone', - defaultMessage: 'What needs to be done?', - }, - whatsThePlan: { - id: 'greeting.whatsThePlan', - defaultMessage: "What's the plan for today?", - }, - readyToCreateGreat: { - id: 'greeting.readyToCreateGreat', - defaultMessage: 'Ready to create something great?', - }, - whatCanBeBuilt: { - id: 'greeting.whatCanBeBuilt', - defaultMessage: 'What can be built today?', - }, - whatsNextChallenge: { - id: 'greeting.whatsNextChallenge', - defaultMessage: "What's the next challenge?", - }, - whatProgress: { - id: 'greeting.whatProgress', - defaultMessage: 'What progress can be made?', - }, - whatToAccomplish: { - id: 'greeting.whatToAccomplish', - defaultMessage: 'What would you like to accomplish?', - }, - whatTaskAwaits: { - id: 'greeting.whatTaskAwaits', - defaultMessage: 'What task awaits?', - }, - whatsTheMission: { - id: 'greeting.whatsTheMission', - defaultMessage: "What's the mission today?", - }, - whatCanBeAchieved: { - id: 'greeting.whatCanBeAchieved', - defaultMessage: 'What can be achieved?', - }, - whatProjectReadyToBegin: { - id: 'greeting.whatProjectReadyToBegin', - defaultMessage: 'What project is ready to begin?', - }, -}); - -interface GreetingProps { - className?: string; - forceRefresh?: boolean; -} - -export function Greeting({ - className = 'mt-1 text-4xl font-light animate-in fade-in duration-300', - forceRefresh = false, -}: GreetingProps) { - const intl = useIntl(); - - const messageDescriptors = [ - i18n.readyToGetStarted, - i18n.whatToWorkOn, - i18n.readyToBuild, - i18n.whatToExplore, - i18n.whatsOnYourMind, - i18n.whatShallWeCreate, - i18n.whatProjectNeedsAttention, - i18n.whatToTackle, - i18n.whatToExplore, - i18n.whatNeedsToBeDone, - i18n.whatsThePlan, - i18n.readyToCreateGreat, - i18n.whatCanBeBuilt, - i18n.whatsNextChallenge, - i18n.whatProgress, - i18n.whatToAccomplish, - i18n.whatTaskAwaits, - i18n.whatsTheMission, - i18n.whatCanBeAchieved, - i18n.whatProjectReadyToBegin, - ]; - - // Using lazy initializer to generate random greeting on each component instance - const greeting = useState(() => { - const randomMessageIndex = Math.floor(Math.random() * messageDescriptors.length); - return messageDescriptors[randomMessageIndex]; - })[0]; - - const greetingText = intl.formatMessage(greeting); - const messageRef = useTextAnimator({ text: greetingText }); - - return ( -

- {greetingText} -

- ); -} diff --git a/ui/desktop/src/components/conversation/ChatHistorySearch.tsx b/ui/desktop/src/components/conversation/ChatHistorySearch.tsx deleted file mode 100644 index 5d332e7408f3..000000000000 --- a/ui/desktop/src/components/conversation/ChatHistorySearch.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react'; -import { Search, X, MessageSquare, ChefHat, Clock } from 'lucide-react'; -import { AnimatePresence, motion } from 'framer-motion'; -import { defineMessages, useIntl } from '../../i18n'; -import { searchSessions } from '../../api'; -import { cn } from '../../utils'; -import { ScrollArea } from '../ui/scroll-area'; -import { Skeleton } from '../ui/skeleton'; -import { SessionIndicators } from '../SessionIndicators'; -import { getSessionDisplayName, truncateMessage } from '../../hooks/useNavigationSessions'; -import type { Session } from '../../api'; -import type { SessionStatus } from '../Layout/navigation/types'; - -const i18n = defineMessages({ - searchPlaceholder: { - id: 'chatHistorySearch.searchPlaceholder', - defaultMessage: 'Search chats…', - }, - noResults: { - id: 'chatHistorySearch.noResults', - defaultMessage: 'No chats found', - }, - searchError: { - id: 'chatHistorySearch.searchError', - defaultMessage: 'Search failed', - }, - messageCount: { - id: 'chatHistorySearch.messageCount', - defaultMessage: '{count, plural, one {# message} other {# messages}}', - }, - keyboardHintMac: { - id: 'chatHistorySearch.keyboardHintMac', - defaultMessage: '⌘K', - }, - keyboardHintOther: { - id: 'chatHistorySearch.keyboardHintOther', - defaultMessage: 'Ctrl+K', - }, -}); - -function formatRelativeTime(dateStr: string): string { - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMin = Math.floor(diffMs / 60000); - const diffHr = Math.floor(diffMs / 3600000); - const diffDay = Math.floor(diffMs / 86400000); - - if (diffMin < 1) return 'just now'; - if (diffMin < 60) return `${diffMin}m ago`; - if (diffHr < 24) return `${diffHr}h ago`; - if (diffDay < 7) return `${diffDay}d ago`; - return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); -} - -interface ChatHistorySearchProps { - onSessionClick: (sessionId: string) => void; - getSessionStatus: (sessionId: string) => SessionStatus | undefined; - clearUnread: (sessionId: string) => void; - activeSessionId?: string; - className?: string; -} - -const SEARCH_RESULTS_LIMIT = 8; - -export const ChatHistorySearch: React.FC = ({ - onSessionClick, - getSessionStatus, - clearUnread, - activeSessionId, - className, -}) => { - const intl = useIntl(); - const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [hasError, setHasError] = useState(false); - const [isFocused, setIsFocused] = useState(false); - const [highlightedIndex, setHighlightedIndex] = useState(-1); - const containerRef = useRef(null); - const inputRef = useRef(null); - const searchTimeoutRef = useRef | undefined>(undefined); - const latestRequestIdRef = useRef(0); - - const showDropdown = - isFocused && (query.trim().length > 0 || isSearching || hasError || results.length > 0); - - const performSearch = useCallback(async (searchQuery: string) => { - if (!searchQuery.trim()) { - latestRequestIdRef.current += 1; - setResults([]); - setHasError(false); - return; - } - - const requestId = ++latestRequestIdRef.current; - setIsSearching(true); - setHasError(false); - try { - const response = await searchSessions({ - query: { query: searchQuery, limit: SEARCH_RESULTS_LIMIT }, - throwOnError: false, - client: undefined, - }); - - if (requestId !== latestRequestIdRef.current) return; - - if (response.error || !response.data) { - setResults([]); - setHasError(true); - } else { - setResults(response.data); - } - } catch { - if (requestId !== latestRequestIdRef.current) return; - setResults([]); - setHasError(true); - } finally { - if (requestId === latestRequestIdRef.current) { - setIsSearching(false); - setHighlightedIndex(-1); - } - } - }, []); - - useEffect(() => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - - // Invalidate any in-flight request and reset selection so Enter - // cannot pick a stale result during the debounce window. - latestRequestIdRef.current += 1; - setHighlightedIndex(-1); - - if (query.trim()) { - // Drop previous results and flip to the searching state immediately - // so (a) an item from the previous query cannot be clicked during the - // 250ms debounce window and (b) the empty-state does not flicker - // before the new request actually runs. - setResults([]); - setHasError(false); - setIsSearching(true); - searchTimeoutRef.current = setTimeout(() => { - performSearch(query); - }, 250); - } else { - setResults([]); - setHasError(false); - setIsSearching(false); - } - - return () => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - }; - }, [query, performSearch]); - - const handleClear = useCallback(() => { - setQuery(''); - setResults([]); - setHasError(false); - setHighlightedIndex(-1); - inputRef.current?.focus(); - }, []); - - const handleSelectSession = useCallback( - (sessionId: string) => { - clearUnread(sessionId); - onSessionClick(sessionId); - setQuery(''); - setResults([]); - setHighlightedIndex(-1); - inputRef.current?.blur(); - }, - [onSessionClick, clearUnread] - ); - - const handleContainerBlur = useCallback((event: React.FocusEvent) => { - const nextFocusedElement = event.relatedTarget; - if (!containerRef.current || !nextFocusedElement) { - setIsFocused(false); - return; - } - - if (!containerRef.current.contains(nextFocusedElement)) { - setIsFocused(false); - } - }, []); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(event.target as Node)) { - setIsFocused(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - useEffect(() => { - const isMac = window.electron?.platform === 'darwin'; - const handleKeyDown = (e: KeyboardEvent) => { - if ((isMac ? e.metaKey : e.ctrlKey) && !e.shiftKey && !e.altKey && e.key === 'k') { - e.preventDefault(); - inputRef.current?.focus(); - setIsFocused(true); - } - }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, []); - - const handleInputKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (!showDropdown) return; - - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setHighlightedIndex((prev) => (prev < results.length - 1 ? prev + 1 : 0)); - break; - case 'ArrowUp': - e.preventDefault(); - setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : results.length - 1)); - break; - case 'Enter': - e.preventDefault(); - // Ignore Enter while a search is pending/in-flight so we don't - // select a result from the previous query. - if (isSearching) break; - if (highlightedIndex >= 0 && highlightedIndex < results.length) { - handleSelectSession(results[highlightedIndex].id); - } - break; - case 'Escape': - e.preventDefault(); - setIsFocused(false); - inputRef.current?.blur(); - break; - } - }, - [showDropdown, results, highlightedIndex, isSearching, handleSelectSession] - ); - - return ( -
-
- - setQuery(e.target.value)} - onFocus={() => setIsFocused(true)} - onKeyDown={handleInputKeyDown} - placeholder={intl.formatMessage(i18n.searchPlaceholder)} - aria-label={intl.formatMessage(i18n.searchPlaceholder)} - aria-expanded={showDropdown} - aria-autocomplete="list" - aria-controls="chat-search-results" - role="combobox" - className={cn( - 'w-full pl-8 pr-14 py-1.5 text-sm', - 'bg-background-secondary border border-border-primary rounded-lg', - 'text-text-primary placeholder-text-secondary', - 'focus:outline-none focus:ring-1 focus:ring-border-tertiary focus:border-border-tertiary', - 'transition-all duration-150' - )} - /> - {query ? ( - - ) : ( - - {intl.formatMessage( - window.electron?.platform === 'darwin' ? i18n.keyboardHintMac : i18n.keyboardHintOther - )} - - )} -
- - - {showDropdown && ( - - {isSearching ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
- -
- - -
-
- ))} -
- ) : hasError ? ( -
- {intl.formatMessage(i18n.searchError)} -
- ) : results.length > 0 ? ( - -
- {results.map((session, index) => { - const status = getSessionStatus(session.id); - const isStreaming = status?.streamState === 'streaming'; - const hasSessionError = status?.streamState === 'error'; - const hasUnread = status?.hasUnreadActivity ?? false; - const isActive = session.id === activeSessionId; - const isHighlighted = index === highlightedIndex; - const displayName = truncateMessage(getSessionDisplayName(session), 40); - const isRecipe = !!session.recipe; - - return ( - - ); - })} -
-
- ) : query.trim() ? ( -
- {intl.formatMessage(i18n.noResults)} -
- ) : null} -
- )} -
-
- ); -}; diff --git a/ui/desktop/src/components/sessions/SessionsInsights.tsx b/ui/desktop/src/components/sessions/SessionsInsights.tsx deleted file mode 100644 index e136e21ec2c3..000000000000 --- a/ui/desktop/src/components/sessions/SessionsInsights.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineMessages, useIntl } from '../../i18n'; -import { errorMessage } from '../../utils/conversionUtils'; -import { Card, CardContent, CardDescription } from '../ui/card'; -import { Greeting } from '../common/Greeting'; -import { useNavigate } from 'react-router-dom'; -import { Button } from '../ui/button'; -import { ChatSmart } from '../icons/'; -import GooseLogo from '../GooseLogo'; -import { Skeleton } from '../ui/skeleton'; -import { - getSessionInsights, - listSessions, - Session, - SessionInsights as ApiSessionInsights, -} from '../../api'; -import { resumeSession } from '../../sessions'; -import { useNavigation } from '../../hooks/useNavigation'; - -const i18n = defineMessages({ - totalSessions: { - id: 'sessionsInsights.totalSessions', - defaultMessage: 'Total sessions', - }, - totalTokens: { - id: 'sessionsInsights.totalTokens', - defaultMessage: 'Total tokens', - }, - recentChats: { - id: 'sessionsInsights.recentChats', - defaultMessage: 'Recent chats', - }, - seeAll: { - id: 'sessionsInsights.seeAll', - defaultMessage: 'See all', - }, - noRecentChats: { - id: 'sessionsInsights.noRecentChats', - defaultMessage: 'No recent chat sessions found.', - }, - failedToLoadInsights: { - id: 'sessionsInsights.failedToLoad', - defaultMessage: 'Failed to load insights', - }, -}); - -export function SessionInsights() { - const intl = useIntl(); - const [insights, setInsights] = useState(null); - const [error, setError] = useState(null); - const [recentSessions, setRecentSessions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [isLoadingSessions, setIsLoadingSessions] = useState(true); - const navigate = useNavigate(); - const setView = useNavigation(); - - useEffect(() => { - let loadingTimeout: ReturnType; - - const loadInsights = async () => { - try { - const response = await getSessionInsights({ throwOnError: true }); - setInsights(response.data); - setError(null); - } catch (error) { - console.error('Failed to load insights:', error); - setError(errorMessage(error, 'Failed to load insights')); - setInsights({ - totalSessions: 0, - totalTokens: 0, - }); - } finally { - setIsLoading(false); - } - }; - - const loadRecentSessions = async () => { - try { - const response = await listSessions({ throwOnError: true }); - setRecentSessions(response.data.sessions.slice(0, 3)); - } finally { - setIsLoadingSessions(false); - } - }; - - // Set a maximum loading time to prevent infinite skeleton - loadingTimeout = setTimeout(() => { - // Only apply fallback if we still don't have insights data - setInsights((currentInsights) => { - if (!currentInsights) { - console.warn('Loading timeout reached, showing fallback content'); - setError('Failed to load insights'); - setIsLoading(false); - return { - totalSessions: 0, - mostActiveDirs: [], - avgSessionDuration: 0, - totalTokens: 0, - recentActivity: [], - }; - } - // If we already have insights, just make sure loading is false - setIsLoading(false); - return currentInsights; - }); - }, 10000); // 10 second timeout - - loadInsights(); - loadRecentSessions(); - - return () => { - if (loadingTimeout) { - window.clearTimeout(loadingTimeout); - } - }; - }, []); - - const handleSessionClick = async (session: Session) => { - try { - resumeSession(session, setView); - } catch (error) { - console.error('Failed to start session:', error); - navigate('/sessions', { - state: { selectedSessionId: session.id }, - replace: true, - }); - } - }; - - const navigateToSessionHistory = () => { - navigate('/sessions'); - }; - - // Format date to show only the date part (without time) - const formatDateOnly = (dateStr: string) => { - const date = new Date(dateStr); - return date - .toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }) - .replace(/\//g, '/'); - }; - - const formatTokens = (tokens: number | undefined): string => { - return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }).format( - tokens || 0 - ); - }; - - // Render skeleton loader while data is loading - const renderSkeleton = () => ( -
- {/* Header container with rounded bottom */} -
-
-
- -
- -
-
- - {/* Stats containers - full bleed with 2px gaps */} -
- {/* Top row with three equal columns */} -
- {/* Total Sessions Card Skeleton */} - - -
- - {intl.formatMessage(i18n.totalSessions)} -
-
-
- - {/* Total Tokens Card Skeleton */} - - -
- - {intl.formatMessage(i18n.totalTokens)} -
-
-
-
- - {/* Recent Chats Card Skeleton */} -
- - -
- - {intl.formatMessage(i18n.recentChats)} - - -
-
- {/* Skeleton chat items */} -
-
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
-
-
- - {/* Filler container - extends to fill remaining space */} -
-
-
- ); - - // Show skeleton while loading, then show actual content - if (isLoading) { - return renderSkeleton(); - } - - return ( -
- {/* Header container with rounded bottom */} -
-
-
- -
- -
-
- - {/* Stats containers - full bleed with 2px gaps */} -
- {/* Error notice if insights failed to load */} - {error && ( -
-
-
- - {intl.formatMessage(i18n.failedToLoadInsights)} - -
-
- )} - - {/* Top row with three equal columns */} -
- {/* Total Sessions Card */} - - -
-

- {Math.max(insights?.totalSessions ?? 0, 0)} -

- {intl.formatMessage(i18n.totalSessions)} -
-
-
- - {/* Average Duration Card */} - {/**/} - {/* */} - {/*
*/} - {/*

*/} - {/* {insights?.avgSessionDuration*/} - {/* ? `${insights.avgSessionDuration.toFixed(1)}m`*/} - {/* : '0.0m'}*/} - {/*

*/} - {/* Avg. chat length*/} - {/*
*/} - {/*
*/} - {/*
*/} - - {/* Total Tokens Card */} - - -
-

- {formatTokens(insights?.totalTokens)} -

- {intl.formatMessage(i18n.totalTokens)} -
-
-
-
- - {/* Recent Chats Card */} -
- {/* Recent Chats Card */} - - -
- - {intl.formatMessage(i18n.recentChats)} - - -
-
- {isLoadingSessions ? ( - <> -
-
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
- - ) : recentSessions.length > 0 ? ( - recentSessions.map((session, index) => ( -
handleSessionClick(session)} - role="button" - tabIndex={0} - style={{ animationDelay: `${index * 0.1}s` }} - onKeyDown={async (e) => { - if (e.key === 'Enter' || e.key === ' ') { - await handleSessionClick(session); - } - }} - > -
- - {session.name} -
- - {formatDateOnly(session.updated_at)} - -
- )) - ) : ( -
- {intl.formatMessage(i18n.noRecentChats)} -
- )} -
-
-
-
- - {/* Filler container - extends to fill remaining space */} -
-
-
- ); -} diff --git a/ui/desktop/src/components/sessions/SessionsView.tsx b/ui/desktop/src/components/sessions/SessionsView.tsx index a3e8c19a82d7..a7eb513604e4 100644 --- a/ui/desktop/src/components/sessions/SessionsView.tsx +++ b/ui/desktop/src/components/sessions/SessionsView.tsx @@ -59,7 +59,7 @@ const SessionsView: React.FC = () => { [setView] ); - // Check if a session ID was passed in the location state (from SessionsInsights) + // Check if a session ID was passed in the location state. useEffect(() => { const state = location.state as { selectedSessionId?: string } | null; if (state?.selectedSessionId) { diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index 55ed35168946..320d5035daa6 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { defineMessages, useIntl } from '../../../i18n'; import { Switch } from '../../ui/switch'; import { Button } from '../../ui/button'; -import { Settings, ChevronDown, ChevronUp } from 'lucide-react'; +import { Settings } from 'lucide-react'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog'; import UpdateSection from './UpdateSection'; @@ -13,11 +13,6 @@ import ApeCloudLogoDark from '../../../images/logo_dark_en_US.png'; import ApeCloudLogoLight from '../../../images/logo_light_en_US.png'; import TelemetrySettings from './TelemetrySettings'; import { trackSettingToggled } from '../../../utils/analytics'; -import { NavigationModeSelector } from './NavigationModeSelector'; -import { NavigationStyleSelector } from './NavigationStyleSelector'; -import { NavigationPositionSelector } from './NavigationPositionSelector'; -import { NavigationCustomizationSettings } from './NavigationCustomizationSettings'; -import { NavigationProvider, useNavigationContextSafe } from '../../Layout/NavigationContext'; const i18n = defineMessages({ appearanceTitle: { id: 'settings.appearance.title', defaultMessage: 'Appearance' }, @@ -67,15 +62,6 @@ const i18n = defineMessages({ id: 'settings.theme.description', defaultMessage: 'Customize the look and feel of ApeMind Agent', }, - navigationTitle: { id: 'settings.navigation.title', defaultMessage: 'Navigation' }, - navigationDesc: { - id: 'settings.navigation.description', - defaultMessage: 'Customize navigation layout and behavior', - }, - navMode: { id: 'settings.navigation.mode', defaultMessage: 'Mode' }, - navStyle: { id: 'settings.navigation.style', defaultMessage: 'Style' }, - navPosition: { id: 'settings.navigation.position', defaultMessage: 'Position' }, - navCustomize: { id: 'settings.navigation.customize', defaultMessage: 'Customize Items' }, helpTitle: { id: 'settings.help.title', defaultMessage: 'Help & feedback' }, helpDesc: { id: 'settings.help.description', @@ -140,83 +126,6 @@ interface AppSettingsSectionProps { scrollToSection?: string; } -const NavigationSettingsContent: React.FC = () => { - const [isExpanded, setIsExpanded] = useState(false); - const navContext = useNavigationContextSafe(); - const isOverlayMode = navContext?.navigationMode === 'overlay'; - const intl = useIntl(); - - return ( - - - - - {isExpanded && ( - -
-

- {intl.formatMessage(i18n.navMode)} -

- -
- {!isOverlayMode && ( -
-

- {intl.formatMessage(i18n.navStyle)} -

- -
- )} - {!isOverlayMode && ( -
-

- {intl.formatMessage(i18n.navPosition)} -

- -
- )} -
-

- {intl.formatMessage(i18n.navCustomize)} -

- -
-
- )} -
- ); -}; - -// Navigation Settings Card - wrapped in its own provider for settings page -const NavigationSettingsCard: React.FC = () => { - const navContext = useNavigationContextSafe(); - - // If already in a NavigationProvider context, render directly - if (navContext) { - return ; - } - - // Otherwise wrap with provider - return ( - - - - ); -}; - export default function AppSettingsSection({ scrollToSection }: AppSettingsSectionProps) { const [menuBarIconEnabled, setMenuBarIconEnabled] = useState(true); const [dockIconEnabled, setDockIconEnabled] = useState(true); @@ -495,9 +404,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti - {/* Navigation Settings */} - - diff --git a/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx b/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx deleted file mode 100644 index 4ea25298b8e5..000000000000 --- a/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState } from 'react'; -import { GripVertical, Eye, EyeOff } from 'lucide-react'; -import { defineMessages, useIntl } from '../../../i18n'; -import { - useNavigationContext, - DEFAULT_ITEM_ORDER, - DEFAULT_ENABLED_ITEMS, - HIDDEN_NAV_ITEM_IDS, -} from '../../Layout/NavigationContext'; -import { cn } from '../../../utils'; - -const i18n = defineMessages({ - dragInstructions: { - id: 'navigationCustomization.dragInstructions', - defaultMessage: 'Drag to reorder, click the eye icon to show/hide items', - }, - resetToDefaults: { - id: 'navigationCustomization.resetToDefaults', - defaultMessage: 'Reset to defaults', - }, - hideItem: { - id: 'navigationCustomization.hideItem', - defaultMessage: 'Hide item', - }, - showItem: { - id: 'navigationCustomization.showItem', - defaultMessage: 'Show item', - }, - itemHome: { - id: 'navigationCustomization.itemHome', - defaultMessage: 'Home', - }, - itemChat: { - id: 'navigationCustomization.itemChat', - defaultMessage: 'Chat', - }, - itemRecipes: { - id: 'navigationCustomization.itemRecipes', - defaultMessage: 'Workflows', - }, - itemScheduler: { - id: 'navigationCustomization.itemScheduler', - defaultMessage: 'Scheduler', - }, - itemExtensions: { - id: 'navigationCustomization.itemExtensions', - defaultMessage: 'Extensions', - }, - itemSettings: { - id: 'navigationCustomization.itemSettings', - defaultMessage: 'Settings', - }, -}); - -const ITEM_LABEL_KEYS: Record = { - home: 'itemHome', - chat: 'itemChat', - recipes: 'itemRecipes', - scheduler: 'itemScheduler', - extensions: 'itemExtensions', - settings: 'itemSettings', -}; - -interface NavigationCustomizationSettingsProps { - className?: string; -} - -export const NavigationCustomizationSettings: React.FC = ({ - className, -}) => { - const { preferences, updatePreferences } = useNavigationContext(); - const [draggedItem, setDraggedItem] = useState(null); - const [dragOverItem, setDragOverItem] = useState(null); - const intl = useIntl(); - - const handleDragStart = (e: React.DragEvent, itemId: string) => { - setDraggedItem(itemId); - e.dataTransfer.effectAllowed = 'move'; - }; - - const handleDragOver = (e: React.DragEvent, itemId: string) => { - e.preventDefault(); - if (draggedItem && draggedItem !== itemId) { - setDragOverItem(itemId); - } - }; - - const handleDrop = (e: React.DragEvent, dropItemId: string) => { - e.preventDefault(); - if (!draggedItem || draggedItem === dropItemId) return; - - const newOrder = [...preferences.itemOrder]; - const draggedIndex = newOrder.indexOf(draggedItem); - const dropIndex = newOrder.indexOf(dropItemId); - - if (draggedIndex === -1 || dropIndex === -1) return; - - newOrder.splice(draggedIndex, 1); - newOrder.splice(dropIndex, 0, draggedItem); - - updatePreferences({ - ...preferences, - itemOrder: newOrder, - }); - - setDraggedItem(null); - setDragOverItem(null); - }; - - const handleDragEnd = () => { - setDraggedItem(null); - setDragOverItem(null); - }; - - const toggleItemEnabled = (itemId: string) => { - const newEnabledItems = preferences.enabledItems.includes(itemId) - ? preferences.enabledItems.filter((id) => id !== itemId) - : [...preferences.enabledItems, itemId]; - - updatePreferences({ - ...preferences, - enabledItems: newEnabledItems, - }); - }; - - const resetToDefaults = () => { - updatePreferences({ - itemOrder: DEFAULT_ITEM_ORDER, - enabledItems: DEFAULT_ENABLED_ITEMS, - }); - }; - - const getItemLabel = (itemId: string): string => { - const key = ITEM_LABEL_KEYS[itemId]; - if (key) { - return intl.formatMessage(i18n[key]); - } - return itemId; - }; - - return ( -
-
-
-

{intl.formatMessage(i18n.dragInstructions)}

- -
- - {preferences.itemOrder - .filter((itemId) => !HIDDEN_NAV_ITEM_IDS.includes(itemId)) - .map((itemId) => { - const isEnabled = preferences.enabledItems.includes(itemId); - const isDragging = draggedItem === itemId; - const isDragOver = dragOverItem === itemId; - const label = getItemLabel(itemId); - - return ( -
handleDragStart(e, itemId)} - onDragOver={(e) => handleDragOver(e, itemId)} - onDrop={(e) => handleDrop(e, itemId)} - onDragEnd={handleDragEnd} - className={cn( - 'flex items-center gap-3 p-3 rounded-lg border transition-all', - isDragging && 'opacity-50', - isDragOver - ? 'border-border-primary bg-background-tertiary' - : 'border-border-secondary bg-background-primary', - !isEnabled && 'opacity-50' - )} - > - - {label} - -
- ); - })} -
-
- ); -}; diff --git a/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx b/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx deleted file mode 100644 index 7206965e3abc..000000000000 --- a/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { Columns2, Layers } from 'lucide-react'; -import { defineMessages, useIntl } from '../../../i18n'; -import { useNavigationContext, NavigationMode } from '../../Layout/NavigationContext'; -import { cn } from '../../../utils'; - -const i18n = defineMessages({ - pushLabel: { - id: 'navigationModeSelector.pushLabel', - defaultMessage: 'Push', - }, - pushDescription: { - id: 'navigationModeSelector.pushDescription', - defaultMessage: 'Navigation pushes content', - }, - overlayLabel: { - id: 'navigationModeSelector.overlayLabel', - defaultMessage: 'Overlay', - }, - overlayDescription: { - id: 'navigationModeSelector.overlayDescription', - defaultMessage: 'Full-screen overlay', - }, -}); - -interface NavigationModeSelectorProps { - className?: string; -} - -export const NavigationModeSelector: React.FC = ({ className }) => { - const { navigationMode, setNavigationMode } = useNavigationContext(); - const intl = useIntl(); - - const modes: { - value: NavigationMode; - label: string; - icon: React.ReactNode; - description: string; - }[] = [ - { - value: 'push', - label: intl.formatMessage(i18n.pushLabel), - icon: , - description: intl.formatMessage(i18n.pushDescription), - }, - { - value: 'overlay', - label: intl.formatMessage(i18n.overlayLabel), - icon: , - description: intl.formatMessage(i18n.overlayDescription), - }, - ]; - - return ( -
-
- {modes.map((mode) => ( - - ))} -
-
- ); -}; diff --git a/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx b/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx deleted file mode 100644 index fee468685f5c..000000000000 --- a/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from 'lucide-react'; -import { defineMessages, useIntl } from '../../../i18n'; -import { useNavigationContext, NavigationPosition } from '../../Layout/NavigationContext'; -import { cn } from '../../../utils'; - -const i18n = defineMessages({ - topLabel: { - id: 'navigationPositionSelector.topLabel', - defaultMessage: 'Top', - }, - bottomLabel: { - id: 'navigationPositionSelector.bottomLabel', - defaultMessage: 'Bottom', - }, - leftLabel: { - id: 'navigationPositionSelector.leftLabel', - defaultMessage: 'Left', - }, - rightLabel: { - id: 'navigationPositionSelector.rightLabel', - defaultMessage: 'Right', - }, -}); - -interface NavigationPositionSelectorProps { - className?: string; -} - -export const NavigationPositionSelector: React.FC = ({ - className, -}) => { - const { navigationPosition, setNavigationPosition } = useNavigationContext(); - const intl = useIntl(); - - const positions: { value: NavigationPosition; label: string; icon: React.ReactNode }[] = [ - { value: 'top', label: intl.formatMessage(i18n.topLabel), icon: }, - { value: 'bottom', label: intl.formatMessage(i18n.bottomLabel), icon: }, - { value: 'left', label: intl.formatMessage(i18n.leftLabel), icon: }, - { value: 'right', label: intl.formatMessage(i18n.rightLabel), icon: }, - ]; - - return ( -
-
- {positions.map((position) => ( - - ))} -
-
- ); -}; diff --git a/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx b/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx deleted file mode 100644 index cd5b79c3b841..000000000000 --- a/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { LayoutGrid, List } from 'lucide-react'; -import { defineMessages, useIntl } from '../../../i18n'; -import { useNavigationContext, NavigationStyle } from '../../Layout/NavigationContext'; -import { cn } from '../../../utils'; - -const i18n = defineMessages({ - tileLabel: { - id: 'navigationStyleSelector.tileLabel', - defaultMessage: 'Tile', - }, - tileDescription: { - id: 'navigationStyleSelector.tileDescription', - defaultMessage: 'Enlarged tile view', - }, - listLabel: { - id: 'navigationStyleSelector.listLabel', - defaultMessage: 'List', - }, - listDescription: { - id: 'navigationStyleSelector.listDescription', - defaultMessage: 'Classic condensed view', - }, -}); - -interface NavigationStyleSelectorProps { - className?: string; -} - -export const NavigationStyleSelector: React.FC = ({ className }) => { - const { navigationStyle, setNavigationStyle } = useNavigationContext(); - const intl = useIntl(); - - const styles: { - value: NavigationStyle; - label: string; - icon: React.ReactNode; - description: string; - }[] = [ - { - value: 'expanded', - label: intl.formatMessage(i18n.tileLabel), - icon: , - description: intl.formatMessage(i18n.tileDescription), - }, - { - value: 'condensed', - label: intl.formatMessage(i18n.listLabel), - icon: , - description: intl.formatMessage(i18n.listDescription), - }, - ]; - - return ( -
-
- {styles.map((style) => ( - - ))} -
-
- ); -}; diff --git a/ui/desktop/src/components/settings/extensions/agent-api.ts b/ui/desktop/src/components/settings/extensions/agent-api.ts index bd38284873c4..064491f58ddf 100644 --- a/ui/desktop/src/components/settings/extensions/agent-api.ts +++ b/ui/desktop/src/components/settings/extensions/agent-api.ts @@ -1,5 +1,6 @@ import { toastService } from '../../../toasts'; -import { agentAddExtension, ExtensionConfig, agentRemoveExtension } from '../../../api'; +import { ExtensionConfig } from '../../../api'; +import { addSessionExtension, removeSessionExtension } from '../../../acp/extensions'; import { errorMessage } from '../../../utils/conversionUtils'; import { createExtensionRecoverHints, @@ -20,10 +21,7 @@ export async function addToAgent( : 0; try { - await agentAddExtension({ - body: { session_id: sessionId, config: extensionConfig }, - throwOnError: true, - }); + await addSessionExtension(sessionId, extensionConfig); if (showToast) { toastService.dismiss(toastId); toastService.success({ @@ -61,10 +59,7 @@ export async function removeFromAgent( : 0; try { - await agentRemoveExtension({ - body: { session_id: sessionId, name: extensionName }, - throwOnError: true, - }); + await removeSessionExtension(sessionId, extensionName); if (showToast) { toastService.dismiss(toastId); toastService.success({ diff --git a/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx b/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx index b894574693b9..bd6d27534aed 100644 --- a/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx @@ -42,7 +42,10 @@ interface EnvVarsSectionProps { onRemove: (index: number) => void; onChange: (index: number, field: 'key' | 'value', value: string) => void; submitAttempted: boolean; - onPendingInputChange?: (hasPendingInput: boolean) => void; + onPendingInputChange: ( + hasPendingInput: boolean, + pendingEnvVar: { key: string; value: string } | null + ) => void; } export default function EnvVarsSection({ @@ -65,7 +68,9 @@ export default function EnvVarsSection({ // Notify parent when pending input changes React.useEffect(() => { const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== ''; - onPendingInputChange?.(hasPendingInput); + const pendingEnvVar = + newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : null; + onPendingInputChange(hasPendingInput, pendingEnvVar); }, [newKey, newValue, onPendingInputChange]); const handleAdd = () => { diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx index 1ae077570f97..a18ebbe2feb2 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx @@ -1,9 +1,20 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, type RenderOptions, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ExtensionModal from './ExtensionModal'; import { ExtensionFormData } from '../utils'; import { IntlTestWrapper } from '../../../../i18n/test-utils'; +import { upsertConfig } from '../../../../api'; + +vi.mock('../../../../api', async () => { + const actual = await vi.importActual('../../../../api'); + return { + ...actual, + upsertConfig: vi.fn().mockResolvedValue({ data: 'ok' }), + }; +}); + +const mockedUpsertConfig = vi.mocked(upsertConfig); const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) => render(ui, { wrapper: IntlTestWrapper, ...options }); @@ -246,4 +257,140 @@ describe('ExtensionModal', () => { { key: 'Authorization', value: 'Bearer abc123', isEdited: true }, ]); }); + + describe('pending env var capture (fix for #8969)', () => { + beforeEach(() => { + mockedUpsertConfig.mockClear(); + mockedUpsertConfig.mockResolvedValue({ + data: 'ok', + error: undefined, + request: new globalThis.Request('http://localhost/test'), + response: new globalThis.Response(), + }); + }); + + const emptyInitialData: ExtensionFormData = { + name: '', + description: '', + type: 'stdio', + cmd: '', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [], + headers: [], + }; + + // Returns the env-var key+value inputs (scoped to the "Environment Variables" section, + // disambiguated from the header inputs which share the "Value" placeholder). + function getEnvVarInputs() { + const envVarKeyInput = screen.getByPlaceholderText('Variable name'); + const envVarValueInput = screen + .getAllByPlaceholderText('Value') + .find((input) => + input.parentElement?.parentElement?.parentElement?.textContent?.includes( + 'Environment Variables' + ) + ); + return { envVarKeyInput, envVarValueInput }; + } + + it('captures a pending env var typed but not "+ Added" when Submit is clicked', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + renderWithIntl( + + ); + + await user.type(screen.getByPlaceholderText('Enter extension name...'), 'WooMCP'); + await user.type( + screen.getByPlaceholderText(/^e\.g\. npx/), + 'npx -y @automattic/mcp-wordpress-remote@latest' + ); + + const { envVarKeyInput, envVarValueInput } = getEnvVarInputs(); + await user.type(envVarKeyInput, 'JWT_TOKEN'); + if (envVarValueInput) { + await user.type(envVarValueInput, 'my_very_long_token'); + } + + // Note: intentionally NOT clicking the "+ Add" button — this is the #8969 repro. + await user.click(screen.getByTestId('extension-submit-btn')); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + expect(mockedUpsertConfig).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + is_secret: true, + key: 'JWT_TOKEN', + value: 'my_very_long_token', + }), + }) + ); + + const submittedData = mockOnSubmit.mock.calls[0][0]; + expect(submittedData.envVars).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: 'JWT_TOKEN', + value: 'my_very_long_token', + isEdited: true, + }), + ]) + ); + }); + + it('does not capture a pending env var when only the key is filled', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + renderWithIntl( + + ); + + await user.type(screen.getByPlaceholderText('Enter extension name...'), 'WooMCP'); + await user.type(screen.getByPlaceholderText(/^e\.g\. npx/), 'npx -y something'); + + const { envVarKeyInput } = getEnvVarInputs(); + await user.type(envVarKeyInput, 'LONELY_KEY'); + // Intentionally leaving the value field empty. + + await user.click(screen.getByTestId('extension-submit-btn')); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + expect(mockedUpsertConfig).not.toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ key: 'LONELY_KEY' }), + }) + ); + + const submittedData = mockOnSubmit.mock.calls[0][0]; + expect(submittedData.envVars).not.toEqual( + expect.arrayContaining([expect.objectContaining({ key: 'LONELY_KEY' })]) + ); + }); + }); }); diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index c9051f0acc94..97acdaab8e55 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -83,6 +83,7 @@ export default function ExtensionModal({ const [submitAttempted, setSubmitAttempted] = useState(false); const [showCloseConfirmation, setShowCloseConfirmation] = useState(false); const [hasPendingEnvVars, setHasPendingEnvVars] = useState(false); + const [pendingEnvVar, setPendingEnvVar] = useState<{ key: string; value: string } | null>(null); const [hasPendingHeaders, setHasPendingHeaders] = useState(false); const [pendingHeader, setPendingHeader] = useState<{ key: string; value: string } | null>(null); @@ -232,6 +233,14 @@ export default function ExtensionModal({ [] ); + const handlePendingEnvVarChange = useCallback( + (hasPending: boolean, envVar: { key: string; value: string } | null) => { + setHasPendingEnvVars(hasPending); + setPendingEnvVar(envVar); + }, + [] + ); + // Function to store a secret value const storeSecret = async (key: string, value: string) => { try { @@ -289,6 +298,19 @@ export default function ExtensionModal({ return finalHeaders; }; + const getFinalEnvVars = () => { + const finalEnvVars = [...formData.envVars]; + if ( + pendingEnvVar && + pendingEnvVar.key.trim() !== '' && + pendingEnvVar.value.trim() !== '' && + !pendingEnvVar.key.includes(' ') + ) { + finalEnvVars.push({ ...pendingEnvVar, isEdited: true }); + } + return finalEnvVars; + }; + const isHeadersValid = () => { return getFinalHeaders().every( ({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '') @@ -323,6 +345,7 @@ export default function ExtensionModal({ if (isFormValid()) { const finalFormData = { ...formData, + envVars: getFinalEnvVars(), headers: getFinalHeaders(), }; @@ -437,7 +460,7 @@ export default function ExtensionModal({ onRemove={handleRemoveEnvVar} onChange={handleEnvVarChange} submitAttempted={submitAttempted} - onPendingInputChange={setHasPendingEnvVars} + onPendingInputChange={handlePendingEnvVarChange} />
diff --git a/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx b/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx index 8887ad65d5ce..e1b0661a798d 100644 --- a/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx +++ b/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx @@ -4,9 +4,12 @@ import { Button } from '../../ui/button'; import { Switch } from '../../ui/switch'; import { getModelSettings, + listBuiltinChatTemplates, updateModelSettings, + type ChatTemplate, type ModelSettings, type SamplingConfig, + type ToolCallingMode, } from '../../../api'; import { defineMessages, useIntl } from '../../../i18n'; @@ -161,16 +164,59 @@ const i18n = defineMessages({ }, toolCalling: { id: 'modelSettingsPanel.toolCalling', - defaultMessage: 'Tool Calling', + defaultMessage: 'Tool calling', }, - nativeToolCalling: { - id: 'modelSettingsPanel.nativeToolCalling', - defaultMessage: 'Native tool calling', + toolCallingDescription: { + id: 'modelSettingsPanel.toolCallingDescription', + defaultMessage: 'Choose how local models select native or emulated tool calling', }, - nativeToolCallingDescription: { - id: 'modelSettingsPanel.nativeToolCallingDescription', - defaultMessage: - "Use the model's built-in tool-call format instead of the shell-command emulator. Enable for large models that reliably support tool calling.", + toolCallingAuto: { + id: 'modelSettingsPanel.toolCallingAuto', + defaultMessage: 'Auto', + }, + toolCallingForceNative: { + id: 'modelSettingsPanel.toolCallingForceNative', + defaultMessage: 'Force native', + }, + toolCallingForceEmulated: { + id: 'modelSettingsPanel.toolCallingForceEmulated', + defaultMessage: 'Force emulated', + }, + chatTemplate: { + id: 'modelSettingsPanel.chatTemplate', + defaultMessage: 'Chat template', + }, + chatTemplateDescription: { + id: 'modelSettingsPanel.chatTemplateDescription', + defaultMessage: 'Use embedded GGUF metadata, a llama.cpp built-in template, or inline Jinja', + }, + chatTemplateEmbedded: { + id: 'modelSettingsPanel.chatTemplateEmbedded', + defaultMessage: 'Embedded', + }, + chatTemplateBuiltin: { + id: 'modelSettingsPanel.chatTemplateBuiltin', + defaultMessage: 'Built-in', + }, + chatTemplateCustomInline: { + id: 'modelSettingsPanel.chatTemplateCustomInline', + defaultMessage: 'Custom inline', + }, + builtinChatTemplate: { + id: 'modelSettingsPanel.builtinChatTemplate', + defaultMessage: 'Built-in template', + }, + builtinChatTemplateDescription: { + id: 'modelSettingsPanel.builtinChatTemplateDescription', + defaultMessage: 'Select a llama.cpp built-in template name', + }, + customChatTemplate: { + id: 'modelSettingsPanel.customChatTemplate', + defaultMessage: 'Custom chat template', + }, + customChatTemplateDescription: { + id: 'modelSettingsPanel.customChatTemplateDescription', + defaultMessage: 'Paste the full Jinja chat template source', }, }); @@ -194,10 +240,12 @@ const DEFAULT_SETTINGS: ModelSettings = { use_mlock: false, flash_attention: null, n_threads: null, - native_tool_calling: false, + tool_calling: 'auto', + chat_template: { type: 'embedded' }, }; type SamplingType = SamplingConfig['type']; +type ChatTemplateMode = 'embedded' | 'builtin' | 'custom_inline'; function NumberField({ label, @@ -302,16 +350,59 @@ function SelectField({ ); } +function TextAreaField({ + label, + description, + value, + onChange, + onBlur, +}: { + label: string; + description?: string; + value: string; + onChange: (v: string) => void; + onBlur: () => void; +}) { + return ( +
+ + {description && {description}} +