diff --git a/.claude/skills/code-review b/.claude/skills/code-review new file mode 120000 index 0000000000..2d88efdf94 --- /dev/null +++ b/.claude/skills/code-review @@ -0,0 +1 @@ +../../.agents/skills/code-review \ No newline at end of file diff --git a/.claude/skills/create-app-e2e-test b/.claude/skills/create-app-e2e-test new file mode 120000 index 0000000000..3e2e55d8d4 --- /dev/null +++ b/.claude/skills/create-app-e2e-test @@ -0,0 +1 @@ +../../.agents/skills/create-app-e2e-test \ No newline at end of file diff --git a/.claude/skills/create-pr b/.claude/skills/create-pr new file mode 120000 index 0000000000..d5b42b7251 --- /dev/null +++ b/.claude/skills/create-pr @@ -0,0 +1 @@ +../../.agents/skills/create-pr \ No newline at end of file diff --git a/.claude/skills/edge-case-finder b/.claude/skills/edge-case-finder new file mode 120000 index 0000000000..93db35644c --- /dev/null +++ b/.claude/skills/edge-case-finder @@ -0,0 +1 @@ +../../.agents/skills/edge-case-finder \ No newline at end of file diff --git a/.codex/skills/code-review b/.codex/skills/code-review new file mode 120000 index 0000000000..2d88efdf94 --- /dev/null +++ b/.codex/skills/code-review @@ -0,0 +1 @@ +../../.agents/skills/code-review \ No newline at end of file diff --git a/.codex/skills/create-app-e2e-test b/.codex/skills/create-app-e2e-test new file mode 120000 index 0000000000..3e2e55d8d4 --- /dev/null +++ b/.codex/skills/create-app-e2e-test @@ -0,0 +1 @@ +../../.agents/skills/create-app-e2e-test \ No newline at end of file diff --git a/.codex/skills/create-pr b/.codex/skills/create-pr new file mode 120000 index 0000000000..d5b42b7251 --- /dev/null +++ b/.codex/skills/create-pr @@ -0,0 +1 @@ +../../.agents/skills/create-pr \ No newline at end of file diff --git a/.codex/skills/edge-case-finder b/.codex/skills/edge-case-finder new file mode 120000 index 0000000000..93db35644c --- /dev/null +++ b/.codex/skills/edge-case-finder @@ -0,0 +1 @@ +../../.agents/skills/edge-case-finder \ No newline at end of file diff --git a/.cursor/skills/code-review b/.cursor/skills/code-review new file mode 120000 index 0000000000..2d88efdf94 --- /dev/null +++ b/.cursor/skills/code-review @@ -0,0 +1 @@ +../../.agents/skills/code-review \ No newline at end of file diff --git a/.cursor/skills/create-app-e2e-test b/.cursor/skills/create-app-e2e-test new file mode 120000 index 0000000000..3e2e55d8d4 --- /dev/null +++ b/.cursor/skills/create-app-e2e-test @@ -0,0 +1 @@ +../../.agents/skills/create-app-e2e-test \ No newline at end of file diff --git a/.cursor/skills/create-pr b/.cursor/skills/create-pr new file mode 120000 index 0000000000..d5b42b7251 --- /dev/null +++ b/.cursor/skills/create-pr @@ -0,0 +1 @@ +../../.agents/skills/create-pr \ No newline at end of file diff --git a/.cursor/skills/edge-case-finder b/.cursor/skills/edge-case-finder new file mode 120000 index 0000000000..93db35644c --- /dev/null +++ b/.cursor/skills/edge-case-finder @@ -0,0 +1 @@ +../../.agents/skills/edge-case-finder \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d58649e65d..6f464634ad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -type: bug +labels: bug assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0cec..5c2d2a4572 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,8 @@ blank_issues_enabled: false +contact_links: + - name: goose Discord discussion + url: https://discord.gg/goose-oss + about: Please ask and answer questions here. + - name: Report a security vulnerability + url: https://github.com/aaif-goose/goose/security/policy + about: Please report security vulnerabilities here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 2d5bb5e360..fbbcc255b8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,14 +2,11 @@ name: Feature request about: Suggest an idea for this project title: '' -type: 'feature' +labels: 'enhancement' assignees: '' --- -**Before opening a feature request** -Please start a discussion in the [goose-eng Discord channel](https://discord.com/channels/1287729918100246654/1514412780504088677) before creating a feature request or beginning implementation. This helps align on direction before anyone spends time building. - **Please explain the motivation behind the feature request.** Does this feature solve a particular problem you have been experiencing? What opportunities or use cases would be unlocked with this feature? diff --git a/.github/ISSUE_TEMPLATE/submit-recipe.yml b/.github/ISSUE_TEMPLATE/submit-recipe.yml new file mode 100644 index 0000000000..4c632582d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/submit-recipe.yml @@ -0,0 +1,99 @@ +name: ๐Ÿง‘โ€๐Ÿณ Submit a recipe to the goose cookbook +description: Share a reusable goose recipe with the community! +title: "[Recipe] " +labels: ["recipe submission"] +body: + - type: markdown + attributes: + value: | + Thanks for contributing to the goose cookbook! ๐Ÿณ + Recipes are reusable sessions created in goose desktop or CLI and shared with the community to help others vibe code faster. + + ๐Ÿ“Œ **How to Submit** + - Create your recipe using goose ("Make recipe from this session") + - Fill out the YAML below using the format provided + - Paste it into the field and submit the issue โ€” we'll review and add it to the cookbook! + + ๐Ÿช„ **What Happens After?** + - If accepted, we'll publish your recipe to the [goose recipes cookbook](https://goose-docs.ai/recipes) + - Your GitHub handle will be displayed and linked on the recipe card + - If the YAML has any issues, goose will comment with validation errors so you can fix and resubmit. + + ๐Ÿงช **Pro Tip:** You can test your recipe locally in your terminal with: + `goose recipe validate your-recipe.yaml` + + - type: textarea + id: recipe-yaml + attributes: + label: Paste Your Full Recipe YAML Below + description: Use the structure below and we'll auto-fill your GitHub handle for `author.contact` after submission. + placeholder: | + version: "1.0.0" + id: clean-up-feature-flag + title: Clean Up Feature Flag + description: Automatically clean up all references of a fully rolled out feature flag from a codebase and make the new behavior the default. + instructions: | + Your job is to systematically remove a fully rolled out feature flag and ensure the new behavior is now the default. Use code search tools like ripgrep to identify all references to the flag, clean up definition files, usage sites, tests, and configuration files. Then create a commit and push changes with clear commit messages documenting the flag removal. + prompt: | + Task: Remove a feature flag that has been fully rolled out, where the feature flag's functionality should become the default behavior. + + Context: + Feature flag key: {{ feature_flag_key }} + Project: {{ repo_dir }} + + Steps to follow: + 1. Check out a *new* branch from main or master named using the feature flag key. + 2. Find the feature flag constant/object that wraps the key. + 3. Search for all references to the constant/object using ripgrep or equivalent tools. + 4. Remove all conditional logic and make the new behavior default. + 5. Remove unused imports, mocks, config, and tests. + 6. Commit your changes and push the branch. + 7. Open a GitHub PR. + + Use commit messages like: + chore(flag-cleanup): remove flag from codebase + + parameters: + - key: feature_flag_key + input_type: string + requirement: required + description: Key of the feature flag + + - key: repo_dir + input_type: string + requirement: optional + default: ./ + description: Directory of the codebase + + extensions: + - type: stdio + name: developer + cmd: uvx + args: + - developer-mcp@latest + timeout: 300 + bundled: true + description: Access developer tools + + activities: + - Remove feature flag definitions + - Clean up feature flag usage sites + - Update affected tests + - Remove flag configurations + - Document flag removal + validations: + required: true + + - type: markdown + attributes: + value: | + ๐Ÿ›  **Recipe Field Tips** + - `version` must be "1.0.0" for now + - `id` should be lowercase, hyphenated, and unique (e.g. `my-awesome-recipe`) + - `title` is the display name of your recipe + - `description` should clearly explain what the recipe does + - `instructions` are specific steps goose should follow โ€” supports template variables like `{{ variable_name }}` + - `prompt` is the first thing goose sees when the recipe is launched + - `parameters` should include required or optional inputs โ€” optional ones must have `default` + - `extensions` must follow the full format with `type`, `cmd`, `args`, `timeout`, etc. + - `activities` describe the main actions the recipe performs diff --git a/.github/actions/generate-release-pr-body/pr_body_template.txt b/.github/actions/generate-release-pr-body/pr_body_template.txt index 48ffe753ab..e0c7e4d15d 100644 --- a/.github/actions/generate-release-pr-body/pr_body_template.txt +++ b/.github/actions/generate-release-pr-body/pr_body_template.txt @@ -2,7 +2,7 @@ ## Test before Release -1. Approve the workflows for this PR (scroll to bottom) to trigger CI and produce the Desktop bundle for testing. +1. Close and reopen this PR to trigger CI and produce the Desktop bundle for testing. Reason: workflows don't run on PRs opened by `GITHUB_TOKEN` ([docs](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow)). 2. Make sure all check workflows pass. 3. Install the Desktop bundle from the download links (posted as a comment below when it is ready). 4. Complete the goose Release Manual Testing Checklist (posted as a comment below). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5d902fbe55..51d3018cbb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -40,7 +40,7 @@ ## Project-Specific Context - This is a Rust project using cargo workspaces -- Core crates: `goose` (agent logic and ACP server), `goose-cli` (CLI), `goose-mcp` (MCP servers) +- Core crates: `goose` (agent logic), `goose-cli` (CLI), `goose-server` (backend), `goose-mcp` (MCP servers) - Error handling: Use `anyhow::Result`, not `unwrap()` in production code - Async runtime: tokio - MCP protocol implementations require extra scrutiny @@ -56,6 +56,7 @@ - `cargo fmt --check` - Code formatting (rustfmt) - `cargo test --jobs 2` - All tests - `cargo clippy --all-targets -- -D warnings` - Linting (clippy) +- `just check-openapi-schema` - OpenAPI schema validation **Desktop app checks:** - `pnpm install --frozen-lockfile` - Fresh dependency install (in `ui/desktop/`) diff --git a/.github/workflows/autoclose b/.github/workflows/autoclose new file mode 100644 index 0000000000..651fd3111c --- /dev/null +++ b/.github/workflows/autoclose @@ -0,0 +1,23 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 60 + days-before-issue-close: 21 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + any-of-labels: "bug,Bug" + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-notify.yml b/.github/workflows/build-notify.yml index 552aa275c5..8b2bf465eb 100644 --- a/.github/workflows/build-notify.yml +++ b/.github/workflows/build-notify.yml @@ -7,8 +7,6 @@ on: - Release - Canary types: [completed] - branches-ignore: - - "release/**" jobs: @@ -36,4 +34,4 @@ jobs: "color": 15158332 }] }' \ - "$DISCORD_WEBHOOK_URL" + "$DISCORD_WEBHOOK_URL" \ No newline at end of file diff --git a/.github/workflows/bundle-desktop-intel.yml b/.github/workflows/bundle-desktop-intel.yml index 8cda0961bb..311ec965da 100644 --- a/.github/workflows/bundle-desktop-intel.yml +++ b/.github/workflows/bundle-desktop-intel.yml @@ -73,11 +73,11 @@ jobs: key: intel-macos-deployment-target-12 - - name: Build desktop backend for Intel macOS (x86_64) + - name: Build goose-server for Intel macOS (x86_64) run: | source ./bin/activate-hermit rustup target add x86_64-apple-darwin - cargo build --release -p goose-cli --bin goose --target x86_64-apple-darwin + cargo build --release -p goose-server --target x86_64-apple-darwin @@ -95,16 +95,12 @@ jobs: # Check disk space after cleanup df -h - - name: Copy backend binary into Electron folder + - name: Copy binaries into Electron folder run: | - mkdir -p ui/desktop/src/bin - rm -f ui/desktop/src/bin/goose - cp target/x86_64-apple-darwin/release/goose ui/desktop/src/bin/goose - chmod +x ui/desktop/src/bin/goose - ls -la ui/desktop/src/bin/ + cp target/x86_64-apple-darwin/release/goosed ui/desktop/src/bin/goosed - name: Cache pnpm dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ui/desktop/node_modules diff --git a/.github/workflows/bundle-desktop-linux.yml b/.github/workflows/bundle-desktop-linux.yml index 7f24f22af4..dd1a88fcc9 100644 --- a/.github/workflows/bundle-desktop-linux.yml +++ b/.github/workflows/bundle-desktop-linux.yml @@ -124,7 +124,7 @@ jobs: with: key: linux-${{ matrix.build-on }}-${{ matrix.variant }} - - name: Build desktop backend binary + - name: Build goosed binary env: RUST_LOG: debug RUST_BACKTRACE: 1 @@ -137,25 +137,19 @@ jobs: FEATURE_ARGS=(--features vulkan) fi - cargo build --release --target ${TARGET} -p goose-cli --bin goose "${FEATURE_ARGS[@]}" + cargo build --release --target ${TARGET} -p goose-server "${FEATURE_ARGS[@]}" - - name: Copy backend binary into Electron folder + - name: Copy binaries into Electron folder run: | - echo "Copying backend binary to ui/desktop/src/bin/" + echo "Copying binaries to ui/desktop/src/bin/" export TARGET="x86_64-unknown-linux-gnu" mkdir -p ui/desktop/src/bin - rm -f ui/desktop/src/bin/goose - cp target/$TARGET/release/goose ui/desktop/src/bin/ - chmod +x ui/desktop/src/bin/goose + cp target/$TARGET/release/goosed ui/desktop/src/bin/ + chmod +x ui/desktop/src/bin/goosed ls -la ui/desktop/src/bin/ - - name: Free Rust build artifacts before packaging - run: | - source ./bin/activate-hermit - cargo clean - - name: Cache pnpm dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ui/desktop/node_modules diff --git a/.github/workflows/bundle-desktop-windows.yml b/.github/workflows/bundle-desktop-windows.yml index a5c53c793f..f18af1025d 100644 --- a/.github/workflows/bundle-desktop-windows.yml +++ b/.github/workflows/bundle-desktop-windows.yml @@ -61,7 +61,7 @@ jobs: run: npm install -g pnpm@10.30.3 - name: Cache node_modules - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | node_modules @@ -113,33 +113,28 @@ jobs: env: CUDA_COMPUTE_CAP: ${{ inputs.windows_variant == 'cuda' && '80' || '' }} run: | - $isCuda = "${{ inputs.windows_variant }}" -eq "cuda" - - Write-Output "Building Windows ACP backend" - if ($isCuda) { - cargo build --release --target x86_64-pc-windows-msvc -p goose-cli --bin goose --features cuda + Write-Output "Building Windows executable..." + if ("${{ inputs.windows_variant }}" -eq "cuda") { + cargo build --release --target x86_64-pc-windows-msvc -p goose-server --features cuda } else { - cargo build --release --target x86_64-pc-windows-msvc -p goose-cli --bin goose + cargo build --release --target x86_64-pc-windows-msvc -p goose-server } - $binaryPath = "./target/x86_64-pc-windows-msvc/release/goose.exe" # Verify build succeeded - if (-not (Test-Path $binaryPath)) { - Write-Error "Windows backend binary not found: $binaryPath" + if (-not (Test-Path "./target/x86_64-pc-windows-msvc/release/goosed.exe")) { + Write-Error "Windows binary not found." Get-ChildItem ./target/x86_64-pc-windows-msvc/release/ -ErrorAction SilentlyContinue exit 1 } - Write-Output "Windows backend binary found." - Get-Item $binaryPath + Write-Output "Windows binary found." + Get-Item ./target/x86_64-pc-windows-msvc/release/goosed.exe - name: Prepare Windows binary shell: bash run: | - BACKEND_BINARY="./target/x86_64-pc-windows-msvc/release/goose.exe" - - if [ ! -f "$BACKEND_BINARY" ]; then - echo "Windows backend binary not found: $BACKEND_BINARY" + if [ ! -f "./target/x86_64-pc-windows-msvc/release/goosed.exe" ]; then + echo "Windows binary not found." exit 1 fi @@ -147,14 +142,13 @@ jobs: rm -rf ./ui/desktop/src/bin mkdir -p ./ui/desktop/src/bin - echo "Copying Windows backend binary..." - cp -f "$BACKEND_BINARY" ./ui/desktop/src/bin/ + echo "Copying Windows binary..." + cp -f ./target/x86_64-pc-windows-msvc/release/goosed.exe ./ui/desktop/src/bin/ if [ -d "./ui/desktop/src/platform/windows/bin" ]; then echo "Copying Windows platform files..." for file in ./ui/desktop/src/platform/windows/bin/*.{exe,dll,cmd}; do - filename="$(basename "$file")" - if [ -f "$file" ] && [ "$filename" != "goose.exe" ]; then + if [ -f "$file" ] && [ "$(basename "$file")" != "goosed.exe" ]; then cp -f "$file" ./ui/desktop/src/bin/ fi done @@ -235,14 +229,14 @@ jobs: certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} files: | ${{ github.workspace }}/dist-windows/Goose.exe - ${{ github.workspace }}/dist-windows/resources/bin/goose.exe + ${{ github.workspace }}/dist-windows/resources/bin/goosed.exe - name: Verify signed executables shell: pwsh run: | $files = @( "dist-windows/Goose.exe", - "dist-windows/resources/bin/goose.exe" + "dist-windows/resources/bin/goosed.exe" ) foreach ($file in $files) { Write-Output "Verifying signature: $file" diff --git a/.github/workflows/bundle-desktop.yml b/.github/workflows/bundle-desktop.yml index 93940e52be..c60df16a96 100644 --- a/.github/workflows/bundle-desktop.yml +++ b/.github/workflows/bundle-desktop.yml @@ -118,10 +118,8 @@ jobs: key: macos-deployment-target-12 # Build the project - - name: Build desktop backend - run: | - source ./bin/activate-hermit - cargo build --release -p goose-cli --bin goose + - name: Build goosed + run: source ./bin/activate-hermit && cargo build --release -p goose-server # Post-build cleanup to free space - name: Post-build cleanup @@ -136,16 +134,12 @@ jobs: # Check disk space after cleanup df -h - - name: Copy backend binary into Electron folder + - name: Copy binaries into Electron folder run: | - mkdir -p ui/desktop/src/bin - rm -f ui/desktop/src/bin/goose - cp target/release/goose ui/desktop/src/bin/goose - chmod +x ui/desktop/src/bin/goose - ls -la ui/desktop/src/bin/ + cp target/release/goosed ui/desktop/src/bin/goosed - name: Cache pnpm dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ui/desktop/node_modules diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index f93599880c..58ac9fb5a8 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -67,7 +67,7 @@ jobs: path: download_cli.sh # ------------------------------------------------------------ - # 4) Bundle Desktop App (macOS only) + # 4) Bundle Desktop App (macOS only) - builds goosed and Electron app # ------------------------------------------------------------ bundle-desktop: needs: [prepare-version] @@ -80,7 +80,7 @@ jobs: signing: false # ------------------------------------------------------------ - # 5) Bundle Desktop App (macOS Intel) + # 5) Bundle Desktop App (macOS Intel) - builds goosed and Electron app # ------------------------------------------------------------ bundle-desktop-intel: needs: [prepare-version] @@ -93,7 +93,7 @@ jobs: signing: false # ------------------------------------------------------------ - # 6) Bundle Desktop App (Linux) + # 6) Bundle Desktop App (Linux) - builds goosed and Electron app # ------------------------------------------------------------ bundle-desktop-linux: needs: [prepare-version] @@ -102,7 +102,7 @@ jobs: version: ${{ needs.prepare-version.outputs.version }} # ------------------------------------------------------------ - # 6) Bundle Desktop App (Windows) + # 6) Bundle Desktop App (Windows) - builds goosed and Electron app # ------------------------------------------------------------ bundle-desktop-windows: needs: [prepare-version] @@ -138,7 +138,7 @@ jobs: merge-multiple: true - name: Attest build provenance - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: | goose-*.tar.bz2 diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index 0e917b04e8..9c2625b852 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -22,7 +22,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@v7 # https://github.com/EmbarkStudios/cargo-deny-action v2.0.15 - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 913909c56e..6106534c79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check for file changes - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # pin@v3 + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v3 id: filter with: filters: | @@ -41,7 +41,7 @@ jobs: - name: Checkout Code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - name: Run cargo fmt run: cargo fmt --check @@ -55,7 +55,7 @@ jobs: - name: Checkout Code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - name: Install Dependencies run: | @@ -75,44 +75,6 @@ jobs: env: RUST_MIN_STACK: 8388608 - rust-build-and-test-tls: - name: Build and Test TLS Backend (${{ matrix.tls-feature }}) - runs-on: ubuntu-latest - needs: changes - if: needs.changes.outputs.code == 'true' || github.event_name != 'pull_request' - strategy: - fail-fast: false - matrix: - tls-feature: - - rustls-tls - - native-tls - steps: - - name: Checkout Code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 - - - name: Install Dependencies - run: | - sudo apt update -y - sudo apt install -y libdbus-1-dev gnome-keyring libxcb1-dev - - - name: Cache Cargo artifacts - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - key: ${{ matrix.tls-feature }} - - - name: Build and Test with ${{ matrix.tls-feature }} - run: | - gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar' - export CARGO_INCREMENTAL=0 - cargo test -p goose --no-default-features --features ${{ matrix.tls-feature }},code-mode - cargo test -p goose-providers --no-default-features --features ${{ matrix.tls-feature }} - cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode -- --skip scenario_tests::scenarios::tests - cargo test -p goose-cli --no-default-features --features ${{ matrix.tls-feature }},code-mode --jobs 1 scenario_tests::scenarios::tests - env: - RUST_MIN_STACK: 8388608 - rust-build-windows: name: Build Rust Project on Windows @@ -157,7 +119,7 @@ jobs: echo "msrv=$msrv" >> "$GITHUB_OUTPUT" echo "MSRV: $msrv" - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1 with: toolchain: ${{ steps.msrv.outputs.msrv }} @@ -183,7 +145,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -205,7 +167,7 @@ jobs: - name: Checkout Code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - name: Install Dependencies run: | @@ -221,6 +183,12 @@ jobs: cd ui/desktop && pnpm install --frozen-lockfile cd ../sdk && pnpm install --frozen-lockfile + - name: Check OpenAPI Schema is Up-to-Date + run: | + source ./bin/activate-hermit + hermit uninstall rustup + just check-openapi-schema + - name: Check ACP Schema is Up-to-Date run: | source ./bin/activate-hermit diff --git a/.github/workflows/create-release-branch.yaml b/.github/workflows/create-release-branch.yaml index bca8171ebf..cb92ae2c31 100644 --- a/.github/workflows/create-release-branch.yaml +++ b/.github/workflows/create-release-branch.yaml @@ -23,7 +23,7 @@ jobs: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 0 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: Extract version and base branch diff --git a/.github/workflows/create-version-bump-pr.yaml b/.github/workflows/create-version-bump-pr.yaml index 0e4d070ded..9f1403ff57 100644 --- a/.github/workflows/create-version-bump-pr.yaml +++ b/.github/workflows/create-version-bump-pr.yaml @@ -30,7 +30,7 @@ jobs: ref: main fetch-depth: 0 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: install dependencies @@ -85,7 +85,7 @@ jobs: **Please follow these steps:** - 1. Approve workflows for this PR to trigger CI checks. + 1. Close and reopen this PR to trigger CI checks. Reason: workflows don't run on PRs opened by `GITHUB_TOKEN` ([docs](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow)). 2. Review and resolve any merge conflicts. 3. Approve and merge this PR. 4. The `release/${{ env.version }}` PR will be created automatically. diff --git a/.github/workflows/deploy-docs-and-extensions.yml b/.github/workflows/deploy-docs-and-extensions.yml index 62cd22772e..4eed0ff625 100644 --- a/.github/workflows/deploy-docs-and-extensions.yml +++ b/.github/workflows/deploy-docs-and-extensions.yml @@ -32,7 +32,7 @@ jobs: node-version: 20 - name: Cache Node.js modules (documentation) - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ./documentation/node_modules key: ${{ runner.os }}-documentation-${{ hashFiles('./documentation/package-lock.json') }} diff --git a/.github/workflows/docs-update-cli-ref.yml b/.github/workflows/docs-update-cli-ref.yml index 9ac5cfca7d..cdb4b42534 100644 --- a/.github/workflows/docs-update-cli-ref.yml +++ b/.github/workflows/docs-update-cli-ref.yml @@ -63,7 +63,7 @@ jobs: sudo apt-get install -y jq ripgrep - name: Set up Rust - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: toolchain: stable diff --git a/.github/workflows/docs-update-recipe-ref.yml b/.github/workflows/docs-update-recipe-ref.yml new file mode 100644 index 0000000000..e0c9e86a85 --- /dev/null +++ b/.github/workflows/docs-update-recipe-ref.yml @@ -0,0 +1,292 @@ +# Automatically updates the Recipe Reference Guide when recipe struct fields, +# validation rules, or schema constraints change between releases. +# +# Triggers: Manual (for testing) or on release (production) +# Testing: Use dry_run mode to review outputs without creating PRs +# See: documentation/automation/recipe-schema-tracking/TESTING.md + +name: Update Recipe Documentation + +on: + workflow_dispatch: # Manual trigger for testing + inputs: + old_version: + description: 'Previous version (e.g., v1.14.0). Leave empty to auto-detect.' + required: false + type: string + new_version: + description: 'New version (e.g., v1.15.0). Leave empty to use HEAD.' + required: false + type: string + dry_run: + description: 'Dry run mode - generate files but do not create PR' + required: false + type: boolean + default: true + + release: + types: [published] + +permissions: + contents: write # Create branches and commit files + pull-requests: write # Create PRs + +jobs: + update-docs: + name: Update Recipe Documentation + runs-on: ubuntu-latest + + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 # Fetch all history for version comparison + fetch-tags: true # Fetch all tags so we can checkout version tags + + - name: Fetch upstream tags (for forks) + if: github.repository != 'aaif-goose/goose' + run: | + # Add upstream remote and fetch tags (only needed when testing in forks) + git remote add upstream https://github.com/aaif-goose/goose.git || git remote set-url upstream https://github.com/aaif-goose/goose.git + git fetch upstream --tags --force + echo "โœ… Fetched tags from upstream (fork mode)" + echo "Total tags available: $(git tag | wc -l)" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq ripgrep + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '20' + + - name: Install goose CLI + run: | + mkdir -p /home/runner/.local/bin + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh \ + | CONFIGURE=false GOOSE_BIN_DIR=/home/runner/.local/bin bash + echo "/home/runner/.local/bin" >> $GITHUB_PATH + goose --version + + - name: Configure goose for CI + env: + GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openai' }} + GOOSE_MODEL: ${{ vars.GOOSE_MODEL || 'gpt-4o' }} + run: | + mkdir -p ~/.config/goose + cat < ~/.config/goose/config.yaml + GOOSE_PROVIDER: $GOOSE_PROVIDER + GOOSE_MODEL: $GOOSE_MODEL + keyring: false + EOF + echo "โœ… Created goose config:" + cat ~/.config/goose/config.yaml + + - name: Determine versions to compare + id: versions + env: + GH_TOKEN: ${{ github.token }} + INPUT_OLD_VERSION: ${{ github.event.inputs.old_version }} + INPUT_NEW_VERSION: ${{ github.event.inputs.new_version }} + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + get_previous_release() { + gh release list --limit 2 --json tagName --jq '.[].tagName' | sed -n '2p' + } + + if [ -n "$INPUT_OLD_VERSION" ]; then + OLD_VERSION="$INPUT_OLD_VERSION" + else + OLD_VERSION=$(get_previous_release) + fi + + if [ -n "$INPUT_NEW_VERSION" ]; then + NEW_VERSION="$INPUT_NEW_VERSION" + elif [ "$EVENT_NAME" = "release" ]; then + NEW_VERSION="$RELEASE_TAG" + else + NEW_VERSION="HEAD" # For testing unreleased changes + fi + + if [ -z "$OLD_VERSION" ] || [ -z "$NEW_VERSION" ]; then + echo "Error: Could not determine versions to compare" + exit 1 + fi + + echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "OLD_VERSION=$OLD_VERSION" >> $GITHUB_ENV + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + echo "โœ… Comparing $OLD_VERSION โ†’ $NEW_VERSION" + + - name: Extract and compare schemas + id: extract + timeout-minutes: 15 + working-directory: documentation/automation/recipe-schema-tracking + env: + GOOSE_REPO: ${{ github.workspace }} + run: | + set -o pipefail # Ensure pipeline failures are caught + + mkdir -p output + ./scripts/run-pipeline.sh "$OLD_VERSION" "$NEW_VERSION" 2>&1 | tee output/pipeline.log + + HAS_CHANGES=$(jq -r '.has_changes' output/validation-changes.json) + echo "has_changes=$HAS_CHANGES" >> $GITHUB_OUTPUT + + if [ "$HAS_CHANGES" = "false" ]; then + echo "โœ… No changes detected" + else + echo "โœ… Changes detected" + fi + + - name: Update recipe-reference.md (AI synthesis) + if: steps.extract.outputs.has_changes == 'true' + timeout-minutes: 10 + working-directory: documentation/automation/recipe-schema-tracking/output + env: + RECIPE_REF_PATH: ${{ github.workspace }}/documentation/docs/guides/recipes/recipe-reference.md + run: | + echo "๐Ÿ” Environment diagnostics:" + echo " GOOSE_PROVIDER: $GOOSE_PROVIDER" + echo " GOOSE_MODEL: $GOOSE_MODEL" + echo " OPENAI_API_KEY: ${OPENAI_API_KEY:0:8}..." # Show first 8 chars only + echo " RECIPE_REF_PATH: $RECIPE_REF_PATH" + echo " HOME: $HOME" + echo " PATH: $PATH" + echo "" + echo "๐Ÿ“ Goose config file:" + cat ~/.config/goose/config.yaml || echo "Config file not found!" + echo "" + echo "๐Ÿ“ Current directory:" + pwd + ls -la + echo "" + echo "๐Ÿค– Step 1: Running validation changes synthesis..." + goose run --recipe ../recipes/synthesize-validation-changes.yaml + + echo "" + echo "๐Ÿค– Step 2: Applying changes to recipe-reference.md..." + goose run --recipe ../recipes/update-recipe-reference.yaml + + - name: Upload automation outputs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: recipe-docs-update-${{ steps.versions.outputs.old_version }}-to-${{ steps.versions.outputs.new_version }} + path: | + documentation/automation/recipe-schema-tracking/output/*.json + documentation/automation/recipe-schema-tracking/output/*.md + documentation/automation/recipe-schema-tracking/output/*.log + retention-days: 30 + + - name: Create Pull Request + if: | + steps.extract.outputs.has_changes == 'true' && + (github.event.inputs.dry_run != 'true' || github.event_name == 'release') + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: docs/auto-recipe-reference-${{ steps.versions.outputs.new_version }} + delete-branch: true + + commit-message: | + docs: Update recipe reference for ${{ steps.versions.outputs.new_version }} + + Automated update based on schema changes between ${{ steps.versions.outputs.old_version }} and ${{ steps.versions.outputs.new_version }}. + + title: "docs: Update Recipe Reference Guide for ${{ steps.versions.outputs.new_version }}" + body: | + ## Summary + + This PR updates the Recipe Reference Guide based on schema and validation changes detected between **${{ steps.versions.outputs.old_version }}** and **${{ steps.versions.outputs.new_version }}**. + + ### Type of Change + - [x] Documentation + + ### AI Assistance + - [x] This PR was created or reviewed with AI assistance + + #### ๐Ÿค– Automation Details + + - **Workflow**: `docs-update-recipe-ref.yml` + - **Triggered by**: ${{ github.event_name }} + - **Previous version**: ${{ steps.versions.outputs.old_version }} + - **New version**: ${{ steps.versions.outputs.new_version }} + + #### ๐Ÿ“‹ Changes Detected + + Review the workflow artifacts for detailed change analysis: + - `validation-changes.json` - Structured diff of changes + - `validation-changes.md` - Human-readable change documentation + - `update-summary.md` - Summary of documentation updates applied + + Download artifacts from the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). + + ### โœ… Review Checklist + + - [ ] Verify all schema changes are accurately documented + - [ ] Check that examples are updated correctly + - [ ] Ensure validation rules are clearly explained + - [ ] Confirm no unintended changes were made + - [ ] Do changes require additional updates in this or other recipe topics? + + ### ๐Ÿ”— Related + + - Release: ${{ github.event.release.html_url || 'N/A' }} + + --- + + *This PR was automatically generated by the Recipe Documentation Automation workflow.* + + labels: | + documentation + automated + recipe-reference + + - name: Workflow summary + if: always() + env: + OLD_VERSION: ${{ steps.versions.outputs.old_version }} + NEW_VERSION: ${{ steps.versions.outputs.new_version }} + HAS_CHANGES: ${{ steps.extract.outputs.has_changes }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + run: | + echo "## ๐Ÿ“Š Recipe Documentation Update Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version Comparison**: $OLD_VERSION โ†’ $NEW_VERSION" >> $GITHUB_STEP_SUMMARY + echo "**Changes Detected**: $HAS_CHANGES" >> $GITHUB_STEP_SUMMARY + echo "**Dry Run Mode**: $DRY_RUN" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_CHANGES" = "true" ]; then + echo "### โœ… Documentation Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The Recipe Reference Guide has been updated to reflect changes in $NEW_VERSION." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$DRY_RUN" = "true" ]; then + echo "**Note**: Running in dry-run mode - no PR was created. Review the artifacts to see the generated changes." >> $GITHUB_STEP_SUMMARY + else + echo "A pull request has been created with the documentation updates." >> $GITHUB_STEP_SUMMARY + fi + else + echo "### โ„น๏ธ No Changes Needed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "No recipe schema or validation changes were detected between $OLD_VERSION and $NEW_VERSION." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“ฆ Artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Download the workflow artifacts to review:" >> $GITHUB_STEP_SUMMARY + echo "- Extracted schemas and validation structures" >> $GITHUB_STEP_SUMMARY + echo "- Change detection results" >> $GITHUB_STEP_SUMMARY + echo "- Human-readable change documentation" >> $GITHUB_STEP_SUMMARY + echo "- Documentation update summary" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/goose-issue-solver.yml b/.github/workflows/goose-issue-solver.yml index 8ef17abb30..b75a77d9b6 100644 --- a/.github/workflows/goose-issue-solver.yml +++ b/.github/workflows/goose-issue-solver.yml @@ -30,7 +30,7 @@ env: - Deletion is a feature: ask "can I delete code instead of adding?" - Complete changes: update ALL usages when changing a type. No exceptions. - When changing message types, update ALL provider format functions. - - When changing ACP request/response types, run `just generate-acp-types`. + - When changing server routes, run `just generate-openapi`. - Your context degrades. The TODO is your memory. Update it after each step. Types: diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 9a4ffd2c7e..866a54cbbd 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -15,8 +15,6 @@ on: - Release types: - completed - branches-ignore: - - "release/**" # Allow manual trigger for testing workflow_dispatch: @@ -78,7 +76,7 @@ jobs: generate-release-notes: name: Generate Release Notes runs-on: ubuntu-latest - # For workflow_run: release branch preflight builds are ignored by the trigger. + # For workflow_run: only run if Release succeeded and tag is not 'stable' # For workflow_dispatch: only run if tag is not 'stable' if: | (github.event_name == 'workflow_dispatch' && inputs.tag != 'stable') || diff --git a/.github/workflows/maven-sdk.yml b/.github/workflows/maven-sdk.yml deleted file mode 100644 index f96739ef9c..0000000000 --- a/.github/workflows/maven-sdk.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: Maven SDK - -on: - workflow_dispatch: - inputs: - publish: - description: "Publish gdk to Maven Central after building" - required: true - default: false - type: boolean - -permissions: - contents: read - -jobs: - build-native: - name: Build native library (${{ matrix.name }}) - runs-on: ${{ matrix.os }} - container: ${{ matrix.container || null }} - strategy: - fail-fast: false - matrix: - include: - - name: darwin-aarch64 - os: macos-14 - resource-prefix: darwin-aarch64 - - name: linux-x86-64 - os: ubuntu-latest - container: quay.io/pypa/manylinux_2_28_x86_64@sha256:441c35fdc6ee809ff9260894f8468ab4fea8c15dc880f8700a3f81b7922c1cda - resource-prefix: linux-x86-64 - - name: win32-x86-64 - os: windows-latest - resource-prefix: win32-x86-64 - - steps: - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Set up Rust - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 - - - name: Cache Cargo artifacts - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - - - name: Build native library - shell: bash - run: cargo build -p goose-sdk --features uniffi --release - - - name: Stage native library - shell: bash - run: | - case "${{ runner.os }}" in - Linux) lib="target/release/libgoose_sdk.so" ;; - macOS) lib="target/release/libgoose_sdk.dylib" ;; - Windows) lib="target/release/goose_sdk.dll" ;; - *) echo "unsupported runner OS: ${{ runner.os }}" >&2; exit 1 ;; - esac - mkdir -p "maven-native/${{ matrix.resource-prefix }}" - cp "$lib" "maven-native/${{ matrix.resource-prefix }}/" - - - name: Upload native library artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: goose-sdk-native-${{ matrix.name }} - path: maven-native/** - if-no-files-found: error - - package: - name: Package Maven artifact - needs: build-native - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Set up Rust - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 - - - name: Set up Java - uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5 - with: - distribution: temurin - java-version: "17" - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4 - - - name: Download native library artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - pattern: goose-sdk-native-* - path: native-artifacts - merge-multiple: true - - - name: Generate Kotlin bindings - shell: bash - run: | - cargo build -p goose-sdk --features uniffi --release - crates/goose-sdk/scripts/prepare-maven-package.sh \ - target/release/libgoose_sdk.so \ - linux-x86-64 - - - name: Restore downloaded native libraries - shell: bash - run: cp -R native-artifacts/* crates/goose-sdk/maven/src/main/resources/ - - - name: Build Maven publication - working-directory: crates/goose-sdk/maven - run: gradle --no-daemon publishToMavenLocal - - - name: Stage Maven local artifacts - shell: bash - run: | - version=$(grep -m1 '^version =' crates/goose-sdk/Cargo.toml | sed -E 's/version = "([^"]+)"/\1/') - mkdir -p maven-artifacts - cp -R "$HOME/.m2/repository/io/github/aaif-goose/gdk/$version" maven-artifacts/ - - - name: Upload Maven artifact bundle - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: goose-sdk-maven - path: maven-artifacts/** - if-no-files-found: error - - publish: - name: Publish to Maven Central - needs: package - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' && inputs.publish - environment: maven-central - steps: - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - - name: Set up Rust - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1 - - - name: Set up Java - uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5 - with: - distribution: temurin - java-version: "17" - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4 - - - name: Download native library artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - pattern: goose-sdk-native-* - path: native-artifacts - merge-multiple: true - - - name: Generate Kotlin bindings - shell: bash - run: | - cargo build -p goose-sdk --features uniffi --release - crates/goose-sdk/scripts/prepare-maven-package.sh \ - target/release/libgoose_sdk.so \ - linux-x86-64 - - - name: Restore downloaded native libraries - shell: bash - run: cp -R native-artifacts/* crates/goose-sdk/maven/src/main/resources/ - - - name: Publish to Maven Central - working-directory: crates/goose-sdk/maven - env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }} - run: gradle --no-daemon publishAndReleaseToMavenCentral diff --git a/.github/workflows/patch-release.yaml b/.github/workflows/patch-release.yaml index b9936551ca..7204206df7 100644 --- a/.github/workflows/patch-release.yaml +++ b/.github/workflows/patch-release.yaml @@ -24,7 +24,7 @@ jobs: ref: ${{ inputs.target_branch }} fetch-depth: 0 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: install dependencies diff --git a/.github/workflows/pr-smoke-test.yml b/.github/workflows/pr-smoke-test.yml index 7c89f66775..405cb8d3ec 100644 --- a/.github/workflows/pr-smoke-test.yml +++ b/.github/workflows/pr-smoke-test.yml @@ -36,7 +36,7 @@ jobs: fetch-depth: 0 - name: Check for code changes - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # pin@v3 + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v3 id: filter with: base: ${{ github.event.before || github.event.pull_request.base.sha }} @@ -55,7 +55,7 @@ jobs: with: ref: ${{ github.event.inputs.branch || github.ref }} - - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 - name: Install Dependencies run: | @@ -67,7 +67,7 @@ jobs: - name: Build Binary for Smoke Tests run: | - cargo build --bin goose + cargo build --bin goose --bin goosed - name: Upload goose binary uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -76,6 +76,13 @@ jobs: path: target/debug/goose retention-days: 1 + - name: Upload goosed binary + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: goosed-binary + path: target/debug/goosed + retention-days: 1 + smoke-tests: name: Smoke Tests runs-on: ubuntu-latest @@ -246,3 +253,39 @@ jobs: mkdir -p $HOME/.local/share/goose/sessions mkdir -p $HOME/.config/goose bash scripts/test_compaction.sh + + goosed-integration-tests: + name: goose server HTTP integration tests + runs-on: ubuntu-latest + needs: build-binary + steps: + - name: Checkout Code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Download Binary + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: goosed-binary + path: target/debug + + - name: Make Binary Executable + run: chmod +x target/debug/goosed + + - name: Install Node.js Dependencies + run: source ../../bin/activate-hermit && pnpm install --frozen-lockfile + working-directory: ui/desktop + + - name: Run Integration Tests + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + GOOSED_BINARY: ../../target/debug/goosed + GOOSE_PROVIDER: anthropic + GOOSE_MODEL: claude-sonnet-4-5-20250929 + SHELL: /bin/bash + SKIP_BUILD: 1 + run: | + echo 'export PATH=/some/fake/path:$PATH' >> $HOME/.bash_profile + source ../../bin/activate-hermit && pnpm run test:integration:goosed + working-directory: ui/desktop diff --git a/.github/workflows/publish-ask-ai-bot.yml b/.github/workflows/publish-ask-ai-bot.yml index 3410da014e..22b779893e 100644 --- a/.github/workflows/publish-ask-ai-bot.yml +++ b/.github/workflows/publish-ask-ai-bot.yml @@ -22,10 +22,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -33,7 +33,7 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository_owner }}/ask-ai-bot tags: | @@ -42,7 +42,7 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # pin@v7.3.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # pin@v7.2.0 with: context: . file: services/ask-ai-bot/Dockerfile diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 20764cc0e8..f163c8c153 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -26,10 +26,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GitHub Container Registry - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -37,7 +37,7 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository_owner }}/goose tags: | @@ -54,7 +54,7 @@ jobs: - name: Build and push Docker image id: docker-push - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # pin@v7.3.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # pin@v7.2.0 with: context: . push: true @@ -65,7 +65,7 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Attest Docker image - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ghcr.io/${{ github.repository_owner }}/goose subject-digest: ${{ steps.docker-push.outputs.digest }} diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 1653b1dc30..d3a7e1a42d 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -32,7 +32,7 @@ jobs: always-auth: true - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 with: version: 10.30.3 @@ -164,7 +164,7 @@ jobs: always-auth: true - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 with: version: 10.30.3 diff --git a/.github/workflows/python-sdk-wheels.yml b/.github/workflows/python-sdk-wheels.yml deleted file mode 100644 index d01d4008c0..0000000000 --- a/.github/workflows/python-sdk-wheels.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Python SDK Wheels - -on: - workflow_dispatch: - inputs: - publish: - description: "Publish wheels to PyPI after building" - required: true - default: false - type: boolean - -permissions: - contents: read - id-token: write - -jobs: - build-wheels: - name: Build wheel (${{ matrix.name }}) - runs-on: ${{ matrix.os }} - container: ${{ matrix.container || null }} - strategy: - fail-fast: false - matrix: - include: - - name: macos-arm64 - os: macos-14 - - name: macos-x86_64 - os: macos-13 - - name: linux-x86_64 - os: ubuntu-latest - container: quay.io/pypa/manylinux_2_28_x86_64@sha256:441c35fdc6ee809ff9260894f8468ab4fea8c15dc880f8700a3f81b7922c1cda - - name: windows-x86_64 - os: windows-latest - - steps: - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Set up Rust - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: "3.12" - - - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - - - name: Install Linux tools - if: runner.os == 'Linux' - shell: bash - run: | - python3 -m pip install --upgrade pip - python3 -m pip install uv - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin - - - name: Install just - if: runner.os != 'Linux' - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - - - name: Cache Cargo artifacts - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - - name: Build wheel - shell: bash - run: just --justfile crates/goose-sdk/justfile python-wheel - - - name: Repair Linux wheel - if: runner.os == 'Linux' - shell: bash - run: | - python3 -m pip install auditwheel - mkdir -p crates/goose-sdk/python/wheelhouse - auditwheel repair --plat manylinux_2_28_x86_64 -w crates/goose-sdk/python/wheelhouse crates/goose-sdk/python/dist/*.whl - rm crates/goose-sdk/python/dist/*.whl - mv crates/goose-sdk/python/wheelhouse/*.whl crates/goose-sdk/python/dist/ - - - name: Smoke test wheel - shell: bash - run: | - python -m venv .venv-wheel-test - if [ "${{ runner.os }}" = "Windows" ]; then - python_bin=".venv-wheel-test/Scripts/python.exe" - else - python_bin=".venv-wheel-test/bin/python" - fi - UV_NO_CONFIG=1 uv pip install --default-index https://pypi.org/simple --python "$python_bin" crates/goose-sdk/python/dist/*.whl - "$python_bin" -c 'import goose; print(goose.__name__)' - - - name: Upload wheel artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: goose-sdk-wheel-${{ matrix.name }} - path: crates/goose-sdk/python/dist/*.whl - if-no-files-found: error - - publish: - name: Publish wheels to PyPI - needs: build-wheels - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' && inputs.publish - environment: pypi - steps: - - name: Download wheel artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - pattern: goose-sdk-wheel-* - path: dist - merge-multiple: true - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - packages-dir: dist diff --git a/.github/workflows/rebuild-skills-marketplace.yml b/.github/workflows/rebuild-skills-marketplace.yml index 08b6b05623..2964d36f19 100644 --- a/.github/workflows/rebuild-skills-marketplace.yml +++ b/.github/workflows/rebuild-skills-marketplace.yml @@ -53,7 +53,7 @@ jobs: node-version: 20 - name: Cache Node.js modules (documentation) - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ./documentation/node_modules key: ${{ runner.os }}-documentation-${{ hashFiles('./documentation/package-lock.json') }} diff --git a/.github/workflows/recipe-security-scanner.yml b/.github/workflows/recipe-security-scanner.yml index 717f1137cd..f24d4ee02a 100644 --- a/.github/workflows/recipe-security-scanner.yml +++ b/.github/workflows/recipe-security-scanner.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit @@ -119,7 +119,7 @@ jobs: - name: Set up Docker Buildx if: steps.find_recipes.outputs.has_recipes == 'true' && steps.recipe_changes.outputs.recipe_files_changed == 'true' - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Prune Docker caches if: steps.find_recipes.outputs.has_recipes == 'true' && steps.recipe_changes.outputs.recipe_files_changed == 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e4c140bd3..158d940aec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: id-token: write # Required for Sigstore OIDC signing attestations: write # Required for SLSA build provenance attestations steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download all artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -129,12 +129,12 @@ jobs: - name: Attest macOS update manifest if: ${{ env.ENABLE_MAC_NATIVE_AUTO_UPDATE == 'true' }} - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: latest-mac.yml - name: Attest build provenance - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-path: | goose-*.tar.bz2 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 156676141f..dbe008a54e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -29,7 +29,7 @@ jobs: steps: # Use the official stale action from GitHub - name: 'Close Stale PRs' - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: # Authentication token with required permissions repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-release-pr.yaml b/.github/workflows/update-release-pr.yaml index 0c5e54a877..06f6508138 100644 --- a/.github/workflows/update-release-pr.yaml +++ b/.github/workflows/update-release-pr.yaml @@ -21,7 +21,7 @@ jobs: with: fetch-depth: 0 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - name: Extract version from branch name diff --git a/.gitignore b/.gitignore index c25c7a8dc6..fb5d128862 100644 --- a/.gitignore +++ b/.gitignore @@ -84,14 +84,3 @@ result # Goose self-test artifacts gooseselftest/ .tasks/ - -# Generated goose-sdk Maven bindings and native libraries -/crates/goose-sdk/maven/src/main/ -/crates/goose-sdk/maven/.gradle/ -/crates/goose-sdk/maven/.kotlin/ -/crates/goose-sdk/maven/build/ - -# Generated goose-sdk Kotlin example build artifacts -/crates/goose-sdk/examples/uniffi/kotlin/.gradle/ -/crates/goose-sdk/examples/uniffi/kotlin/.kotlin/ -/crates/goose-sdk/examples/uniffi/kotlin/build/ diff --git a/AGENTS.md b/AGENTS.md index 0f01fcde55..e0fda6d5e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ cargo build ```bash cargo build # debug cargo build --release # release -just release-binary # release binary +just release-binary # release + openapi ``` ### Test @@ -33,8 +33,8 @@ cargo clippy --all-targets -- -D warnings ### UI ```bash +just generate-openapi # after server changes just run-ui # start desktop -cd ui/desktop && pnpm run typecheck cd ui/desktop && pnpm test # test UI ``` @@ -44,10 +44,12 @@ crates/ โ”œโ”€โ”€ goose # core logic โ”œโ”€โ”€ goose-acp-macros # ACP proc macros โ”œโ”€โ”€ goose-cli # CLI entry +โ”œโ”€โ”€ goose-server # backend (binary: goosed) โ”œโ”€โ”€ goose-mcp # MCP extensions โ”œโ”€โ”€ goose-test # test utilities โ””โ”€โ”€ goose-test-support # test helpers +evals/open-model-gym/ # benchmarking / evals ui/desktop/ # Electron app ``` @@ -63,6 +65,7 @@ ui/desktop/ # Electron app # 1. cargo build # 2. cargo test -p # 3. cargo clippy --all-targets -- -D warnings +# 4. [if server] just generate-openapi ``` ## Rules @@ -72,7 +75,7 @@ ui/desktop/ # Electron app - Error: Use anyhow::Result - Provider: Implement Provider trait see providers/base.rs - MCP: Extensions in crates/goose-mcp/ -- UI Desktop: Use ACP SDK types or local `src/types/*` types. Do not import generated OpenAPI types/client code from `ui/desktop/src/api` +- Server: Changes need just generate-openapi ## Code Quality @@ -104,7 +107,7 @@ remaining space for dynamic text. ## Never -- Never: Recreate `ui/desktop/src/api` or add `@hey-api/openapi-ts` to `ui/desktop` +- Never: Edit ui/desktop/openapi.json manually - Cargo.toml: For human-authored dependency changes, use `cargo add` instead of manually editing dependency entries unless there is a specific reason not to. - Cargo.toml: Automated dependency bump PRs are exempt; when manual edits are necessary, keep `Cargo.lock` consistent. - Never: Skip cargo fmt @@ -113,5 +116,6 @@ remaining space for dynamic text. ## Entry Points - CLI: crates/goose-cli/src/main.rs +- Server: crates/goose-server/src/main.rs - UI: ui/desktop/src/main.ts - Agent: crates/goose/src/agents/agent.rs diff --git a/BUILDING_LINUX.md b/BUILDING_LINUX.md index 0b242a359f..4e1b9c4114 100644 --- a/BUILDING_LINUX.md +++ b/BUILDING_LINUX.md @@ -59,7 +59,13 @@ cd goose Build Goose CLI: ```bash -cargo build --release -p goose-cli --bin goose +cargo build --release -p goose-cli +``` + +Build Goose Server: + +```bash +cargo build --release -p goose-server ``` This command should give you a list of possible packages in the @@ -74,9 +80,9 @@ cargo test -p cd ui/desktop pnpm install -# Copy the goose binary to the expected location +# Copy the server binary to the expected location mkdir -p src/bin -cp ../../target/release/goose src/bin/ +cp ../../target/release/goosed src/bin/ ``` ### 4. Build the Application @@ -137,10 +143,10 @@ cd /path/to/goose/ui/desktop/out/goose-linux-x64 ./goose 2>&1 | grep -v "GLib-GObject" | grep -v "browser_main_loop" ``` -#### Goose Binary Not Found -If you see "Goose binary not found", ensure you've: -1. Built the Rust binary: `cargo build --release -p goose-cli --bin goose` -2. Copied it to the right location: `cp ../../target/release/goose src/bin/` +#### Server Binary Not Found +If you see "Could not find goosed binary", ensure you've: +1. Built the Rust backend: `cargo build --release -p goose-server` +2. Copied it to the right location: `cp ../../target/release/goosed src/bin/` 3. Rebuilt the application: `pnpm run make` ### Distribution-Specific Notes @@ -171,7 +177,7 @@ Building as Snap packages is not currently supported but may be added in the fut For active development: -1. **Backend changes**: Rebuild with `cargo build --release -p goose-cli --bin goose` and copy the binary +1. **Backend changes**: Rebuild with `cargo build --release -p goose-server` and copy the binary 2. **Frontend changes**: Use `pnpm run start` for hot reload during development 3. **Full rebuild**: Run the complete build process above diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index a102b29a09..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,90 +0,0 @@ - -# Contributor Covenant 3.0 Code of Conduct - -## Our Pledge - -We pledge to make our community welcoming, safe, and equitable for all. - -We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. - -## Encouraged Behaviors - -While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. - -With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: - -1. Respecting the **purpose of our community**, our activities, and our ways of gathering. -2. Engaging **kindly and honestly** with others. -3. Respecting **different viewpoints** and experiences. -4. **Taking responsibility** for our actions and contributions. -5. Gracefully giving and accepting **constructive feedback**. -6. Committing to **repairing harm** when it occurs. -7. Behaving in other ways that promote and sustain the **well-being of our community**. - - -## Restricted Behaviors - -We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. - -1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. -2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. -3. **Stereotyping or discrimination.** Characterizing anyoneโ€™s personality or behavior on the basis of immutable identities or traits. -4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. -5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. -6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. -7. Behaving in other ways that **threaten the well-being** of our community. - -### Other Restrictions - -1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. -2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. -3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. -4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. - - -## Reporting an Issue - -Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. - -When an incident does occur, it is important to report it promptly. To report a possible violation, **Send a message to @goose-admin** - -Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. - - -## Addressing and Repairing Harm - -If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. - -1) Warning - 1) Event: A violation involving a single incident or series of incidents. - 2) Consequence: A private, written warning from the Community Moderators. - 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. -2) Temporarily Limited Activities - 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. - 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. - 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. -3) Temporary Suspension - 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. - 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. - 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. -4) Permanent Ban - 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. - 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. - 3) Repair: There is no possible repair in cases of this severity. - -This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. - - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - - -## Attribution - -This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). - -Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) - -For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozillaโ€™s code of conduct team](https://github.com/mozilla/inclusion). - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee36c03dc3..49e3dcd56e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,17 +29,16 @@ It just means the change was too large for a first contribution. Start with some ### Issues -If you spot a bug, please open an issue. This shows the community and the maintainers the direction of your -thinking. +If you spot a bug or have a concrete proposal for a feature, please open an issue. This shows the community and +the maintainers the direction of your thinking. For bugs, describe how to reproduce the problem as clearly as possible. If the issue involves an interaction with an LLM, include a diagnostics report if possible. ### Discussions -Before opening a feature request or beginning implementation, please start with a discussion in the -[goose-eng Discord channel](https://discord.com/channels/1287729918100246654/1514412780504088677). Discussions -are a good place to explore design questions, alternatives, and whether something fits the goals of the project. +If you have an idea but are not yet sure how it should work, open a discussion instead. Discussions are a good +place to explore design questions, alternatives, and whether something fits the goals of the project. If a change is large or touches multiple parts of the codebase, please start with a discussion before opening a PR. This helps us align on direction before you spend time implementing something. @@ -185,27 +184,41 @@ cd ui && pnpm install See #8757. +### Regenerating the OpenAPI schema + +The file `ui/desktop/openapi.json` is automatically generated during the build. +It is written by the `generate_schema` binary in `crates/goose-server`. +To update the spec without starting the UI, run: + +``` +just generate-openapi +``` + +This command regenerates `ui/desktop/openapi.json` and then runs the UI's +`generate-api` script to rebuild the TypeScript client from that spec. + +API changes should be made in the Rust source under `crates/goose-server/src/`. + ### Debugging -To debug the external ACP backend, run it from an IDE. The configuration will depend on the IDE. The command to run is: +To debug the Goose server, run it from an IDE. The configuration will depend on the IDE. The command to run is: ``` export GOOSE_SERVER__SECRET_KEY=test -cargo run --package goose-cli --bin goose -- serve --platform desktop --host 127.0.0.1 --port 3000 +cargo run --package goose-server --bin goosed -- agent # or: `just run-server` ``` -The `debug-ui` recipe connects to `http://127.0.0.1:3000` by default. If the -backend uses another port, set `GOOSE_PORT` when starting the UI, or set -`GOOSE_EXTERNAL_BACKEND_URL` to the backend's HTTP base URL. +The server listens on port `3000` by default; this can be changed by setting the +`GOOSE_PORT` environment variable. -Once the backend is running, start a UI and connect it to the backend by running: +Once the server is running, start a UI and connect it to the server by running: ``` just debug-ui ``` -The UI connects to the backend started in the IDE, allowing breakpoints -and stepping through the backend code while interacting with the UI. +The UI connects to the server started in the IDE, allowing breakpoints +and stepping through the server code while interacting with the UI. ## Creating a fork diff --git a/CUSTOM_DISTROS.md b/CUSTOM_DISTROS.md index 74ace5fc5b..ad59da656e 100644 --- a/CUSTOM_DISTROS.md +++ b/CUSTOM_DISTROS.md @@ -26,8 +26,8 @@ goose's architecture is designed for extensibility. Organizations can create "re โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ goose serve (ACP) โ”‚ -โ”‚ ACP HTTP/WebSocket server for custom clients โ”‚ +โ”‚ goose-server (goosed) โ”‚ +โ”‚ REST API for all goose functionality โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ @@ -49,7 +49,7 @@ goose's architecture is designed for extensibility. Organizations can create "re | Bundle custom MCP extensions | `config.yaml` extensions section, `ui/desktop/src/built-in-extensions.json`, `ui/desktop/src/components/settings/extensions/bundled-extensions.json` | Medium | | Modify system prompts | `crates/goose/src/prompts/` | Low | | Customize desktop branding | `ui/desktop/` (icons, names, colors) | Medium | -| Build a new UI (web, mobile) | Integrate with `goose serve` over ACP | High | +| Build a new UI (web, mobile) | Integrate with `goose-server` REST API | High | | Create guided workflows | Recipes (YAML-based task definitions) | Low | | Build complex multi-step workflows | Recipes with sub-recipes and subagents | Medium | @@ -304,32 +304,40 @@ export GOOSE_BUNDLE_NAME="InsightStream-goose" **Goal**: Create an entirely new frontend while leveraging goose's backend. -goose provides two ACP transport options for building custom clients: +goose provides two integration options for building custom UIs: -### Option 1: ACP HTTP/WebSocket (`goose serve`) +### Option 1: REST API (goose-server) -Use `goose serve` for process-separated integrations such as web apps, desktop shells, and other clients: +Use goose-server for HTTP-based integrations (web apps, simple clients): ```bash # Start the server -GOOSE_SERVER__SECRET_KEY='a-long-random-secret' goose serve +./target/release/goosed -# ACP endpoint available at http://localhost:3284/acp +# API available at http://localhost:3000 ``` -HTTP clients authenticate with the `X-Secret-Key` header. Browser WebSocket clients use the same secret as a `?token=` query parameter because the browser WebSocket API cannot set custom headers: +**Reference the OpenAPI spec** at `ui/desktop/openapi.json` for available endpoints: +- Session management +- Message streaming +- Extension control +- Configuration -```text -ws://localhost:3284/acp?token=a-long-random-secret +**Key endpoints** for a minimal integration: + +``` +POST /sessions # Create a new session +POST /sessions/{id}/messages # Send a message (streaming response) +GET /sessions/{id} # Get session state +GET /extensions # List available extensions +POST /extensions/{name}/enable # Enable an extension ``` -For browser clients served from a non-loopback origin, pass the exact UI origin with `--allowed-origin`. When you pass `--allowed-origin`, it replaces the default loopback origin allowlist, so include every origin the client needs. +**Handle streaming responses** - goose uses Server-Sent Events (SSE) for real-time responses. -For the ACP protocol and client flow, see [Agent Client Protocol clients](documentation/docs/guides/acp-clients.md). +### Option 2: Agent Client Protocol (ACP) -### Option 2: Agent Client Protocol (ACP) over stdio - -For richer local integrations (IDEs and embedded agents), run goose as an ACP agent over stdio. +For richer integrations (IDEs, desktop apps, embedded agents), use the **Agent Client Protocol (ACP)**โ€”a standardized JSON-RPC protocol for AI agent communication over stdio or other transports. ACP provides: - **Bidirectional communication**: Agents can request permissions, stream updates, and receive cancellations @@ -402,6 +410,11 @@ For the full ACP specification, see the [Agent Client Protocol documentation](ht ### Technical Details +**REST API (goose-server)**: +- Server implementation: `crates/goose-server/src/routes/` +- OpenAPI generation: `just generate-openapi` +- API client example: `ui/desktop/src/api/` (generated TypeScript client) + **ACP**: - ACP server implementation: `crates/goose/src/acp/server.rs` - CLI integration: `crates/goose-cli/src/cli.rs` (Command::Acp) @@ -642,10 +655,10 @@ prompt: | - find_patterns: Find similar features to model after ``` -Recipes that define `sub_recipes` get the Summon extension automatically. The AI can invoke sub-recipes through Summon's `delegate` tool: +The AI can then invoke these sub-recipes using the `subagent` tool: ``` -delegate(source: "find_files", parameters: {"search_term": "authentication"}) +subagent(subrecipe: "find_files", parameters: {"search_term": "authentication"}) ``` ### Subagents: Dynamic Task Delegation @@ -665,27 +678,28 @@ prompt: | To complete this task: 1. Spawn a subagent to analyze the frontend code: - delegate(instructions: "Analyze all React components in src/components/ and list their props and state management patterns") + subagent(instructions: "Analyze all React components in src/components/ + and list their props and state management patterns") 2. Spawn another subagent for the backend: - delegate(instructions: "Document all API endpoints in src/api/ including their request/response schemas") + subagent(instructions: "Document all API endpoints in src/api/ + including their request/response schemas") 3. Synthesize findings from both subagents into a unified report. ``` #### Parallel Subagent Execution -Use `async: true` to run delegates in parallel, then collect each result with `load(source: "")`: +Multiple subagent calls in the same message execute in parallel: ```yaml prompt: | - Run these analyses in parallel: + Run these analyses in parallel by making all subagent calls at once: - delegate(instructions: "Count lines of code by language", async: true) - delegate(instructions: "Find all TODO comments", async: true) - delegate(instructions: "List external dependencies", async: true) + subagent(instructions: "Count lines of code by language") + subagent(instructions: "Find all TODO comments") + subagent(instructions: "List external dependencies") - Use load(source: "") for each returned task id. Then combine the results into a codebase health report. ``` @@ -697,18 +711,22 @@ Customize model, provider, or behavior per subagent: prompt: | Use a faster model for simple tasks: - delegate( + subagent( instructions: "List all files modified in the last week", - model: "gpt-4o-mini", - max_turns: 3 + settings: { + model: "gpt-4o-mini", + max_turns: 3 + } ) Use the full model for complex analysis: - delegate( + subagent( instructions: "Review this code for security vulnerabilities", - model: "claude-sonnet-4-20250514", - temperature: 0.1 + settings: { + model: "claude-sonnet-4-20250514", + temperature: 0.1 + } ) ``` @@ -720,7 +738,7 @@ Limit which extensions a subagent can access: prompt: | Create a sandboxed subagent with only file reading capabilities: - delegate( + subagent( instructions: "Analyze the README files in this project", extensions: ["developer"] # Only developer extension, no network access ) @@ -801,7 +819,7 @@ prompt: | 2. **Parallelize independent tasks** - Multiple subagent calls in one message run concurrently 3. **Use `sequential_when_repeated: true`** - For tasks that shouldn't run in parallel (e.g., database migrations) 4. **Scope extensions appropriately** - Give subagents only the tools they need -5. **Use background delegation for independent work** - Pass `async: true` to `delegate`, then collect results with `load(source: "")` +5. **Use summary mode (default)** - Subagents return concise summaries; use `summary: false` only when you need full conversation history 6. **Handle failures gracefully** - Design workflows to continue even if one subagent fails ### Technical Details diff --git a/Cargo.lock b/Cargo.lock index f138041ecf..2324cf0e8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16302d16c7531355db16593d99c38c8297db0c4653aa7dd80c3556bb17f4cd8c" +checksum = "e981a7d0207a0e9eb67d0faafc04e8c6601dea572d67a3163798897c0024521d" dependencies = [ "agent-client-protocol-derive", "agent-client-protocol-schema", @@ -50,7 +50,7 @@ dependencies = [ "blocking", "futures", "futures-concurrency", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "schemars 1.2.1", "serde", "serde_json", @@ -62,19 +62,19 @@ dependencies = [ [[package]] name = "agent-client-protocol-derive" -version = "1.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5ca63f112bd2459bcaf9eda0683b9ba95fc3b5e5fdd9036ca941c6a09345b1" +checksum = "0d589c28cbe2978c76f8714cc304bc08355ee2f05d120806717725d9ffa12143" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "agent-client-protocol-http" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff55efe7a6bb92a5d0226f6055c2f277b7591060d5095c7764f445289ebda3" +checksum = "171d55fdd4a4cab6dd1ca33c2deb7ae4990f91050da15a7b045e89a6db387826" dependencies = [ "agent-client-protocol", "async-stream", @@ -134,9 +134,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" @@ -232,13 +232,22 @@ dependencies = [ [[package]] name = "ar_archive_writer" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ "object", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.6.1" @@ -258,9 +267,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -273,9 +282,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ascii" @@ -285,11 +294,11 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "askama" -version = "0.16.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" dependencies = [ - "askama_macros", + "askama_derive", "itoa", "percent-encoding", "serde", @@ -298,42 +307,31 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.16.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" dependencies = [ "askama_parser", "basic-toml", - "glob", "memchr", "proc-macro2", "quote", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "serde_derive", - "syn 2.0.118", -] - -[[package]] -name = "askama_macros" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" -dependencies = [ - "askama_derive", + "syn 2.0.117", ] [[package]] name = "askama_parser" -version = "0.16.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" dependencies = [ - "rustc-hash 2.1.3", + "memchr", "serde", "serde_derive", - "unicode-ident", - "winnow 1.0.3", + "winnow 0.7.15", ] [[package]] @@ -360,7 +358,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "synstructure", ] @@ -372,7 +370,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -393,7 +391,7 @@ checksum = "2eb025ef00a6da925cf40870b9c8d008526b6004ece399cb0974209720f0b194" dependencies = [ "quote", "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -510,7 +508,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -527,7 +525,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -600,11 +598,11 @@ dependencies = [ "aws-sdk-ssooidc", "aws-sdk-sts", "aws-smithy-async", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", - "aws-smithy-schema 0.1.0", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", @@ -633,9 +631,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -644,28 +642,27 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", "dunce", "fs_extra", - "pkg-config", ] [[package]] name = "aws-runtime" -version = "1.7.5" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9b9de216a988dd54b754a82a7660cfe14cee4f6782ae4524470972fa0ccb39" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -683,9 +680,9 @@ dependencies = [ [[package]] name = "aws-sdk-bedrockruntime" -version = "1.135.0" +version = "1.133.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74b780f2f36912bae71b4f4f8ed9a0a88832b4681a1add3caf5ca25dbc8ab2d" +checksum = "76314880945928a4ee3956e92af451ef29ef04b734d008bb94b11158a60a1034" dependencies = [ "arc-swap", "aws-credential-types", @@ -693,9 +690,9 @@ dependencies = [ "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability 0.2.6", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -711,18 +708,18 @@ dependencies = [ [[package]] name = "aws-sdk-sagemakerruntime" -version = "1.105.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3b9e91619a2ffd88b195f71a0054f0a8d247ba4550306628796ba976d4611c" +checksum = "031fec5f68bdc840361ed09b98b6ef916cc9e49de0323bb4d584adad2cb87cc7" dependencies = [ "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-eventstream", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability 0.2.6", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -737,17 +734,17 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.102.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c82b3ac19f1431854f7ace3a7531674633e286bfdde21976893bfee36fd493b" +checksum = "b647baea49ff551960b904f905681e9b4765a6c4ea08631e89dc52d8bd3f5896" dependencies = [ "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability 0.2.6", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -762,17 +759,17 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.104.0" +version = "1.103.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321000d2b4c5519ee573f73167f612efd7329322d9b26969ad1979f0427f1913" +checksum = "7ae401c65ff288aa7873117fe535cd32b7b1bb0bc43751d28901a1d5f20636b9" dependencies = [ "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability 0.2.6", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -787,17 +784,17 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.107.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0d328ba962af23ecfa3c9f23b98d3d35e325fa218d7f13d17a6bf522f8a560" +checksum = "4c80de7bb7d03e9ca8c9fd7b489f20f3948d3f3be91a7953591347d238115408" dependencies = [ "arc-swap", "aws-credential-types", "aws-runtime", "aws-smithy-async", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability 0.2.6", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -819,7 +816,7 @@ checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", - "aws-smithy-http 0.63.6", + "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -836,9 +833,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.3.0" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -847,9 +844,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.21" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d8391e65fcea47c586a22e1a41f173b38615b112b2c6b7a44e80cec3e6b706" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ "aws-smithy-types", "bytes", @@ -878,27 +875,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "aws-smithy-http" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 1.4.2", - "http-body 1.0.1", - "http-body-util", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - [[package]] name = "aws-smithy-json" version = "0.62.7" @@ -906,7 +882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" dependencies = [ "aws-smithy-runtime-api", - "aws-smithy-schema 0.1.0", + "aws-smithy-schema", "aws-smithy-types", ] @@ -919,15 +895,6 @@ dependencies = [ "aws-smithy-runtime-api", ] -[[package]] -name = "aws-smithy-observability" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" -dependencies = [ - "aws-smithy-runtime-api", -] - [[package]] name = "aws-smithy-query" version = "0.60.15" @@ -940,15 +907,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.12.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea94a9ff8464016338c851e24b472d7131c388c88898a502e781815b2ee6045" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", - "aws-smithy-http 0.64.0", - "aws-smithy-observability 0.3.0", + "aws-smithy-http", + "aws-smithy-observability", "aws-smithy-runtime-api", - "aws-smithy-schema 0.2.0", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", @@ -965,9 +932,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.13.0" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ed1ebe6e0a95ea84570225f5a8208dec4b8f77e61a9b0d6f51773fcb4612f0" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api-macros", @@ -983,13 +950,13 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api-macros" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -1003,22 +970,11 @@ dependencies = [ "http 1.4.2", ] -[[package]] -name = "aws-smithy-schema" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "http 1.4.2", -] - [[package]] name = "aws-smithy-types" -version = "1.6.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" +checksum = "32b42fcf341259d85ca10fac9a2f6448a8ec691c6955a18e45bc3b71a85fab85" dependencies = [ "base64-simd", "bytes", @@ -1058,7 +1014,7 @@ dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", - "aws-smithy-schema 0.1.0", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -1128,7 +1084,7 @@ checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -1140,7 +1096,7 @@ dependencies = [ "arc-swap", "bytes", "either", - "fs-err", + "fs-err 3.3.0", "http 1.4.2", "http-body 1.0.1", "hyper", @@ -1243,7 +1199,7 @@ dependencies = [ "bincode", "bytesize", "clircle", - "console", + "console 0.16.3", "content_inspector", "encoding_rs", "flate2", @@ -1263,7 +1219,7 @@ dependencies = [ "serde_derive", "serde_with", "serde_yaml", - "syn 2.0.118", + "syn 2.0.117", "syntect", "terminal-colorsaurus", "thiserror 2.0.18", @@ -1299,11 +1255,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.72.1" +version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1312,9 +1268,29 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.3", + "rustc-hash 1.1.0", "shlex 1.3.0", - "syn 2.0.118", + "syn 2.0.117", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex 1.3.0", + "syn 2.0.117", ] [[package]] @@ -1343,7 +1319,7 @@ dependencies = [ "biome_json_parser", "biome_json_syntax", "biome_rowan", - "bitflags 2.13.0", + "bitflags 2.11.1", "indexmap 1.9.3", "serde", "serde_json", @@ -1363,7 +1339,7 @@ dependencies = [ "biome_rowan", "biome_text_edit", "biome_text_size", - "bitflags 2.13.0", + "bitflags 2.11.1", "bpaf", "serde", "termcolor", @@ -1456,7 +1432,7 @@ dependencies = [ "biome_js_unicode_table", "biome_parser", "biome_rowan", - "bitflags 2.13.0", + "bitflags 2.11.1", "cfg-if 1.0.4", "drop_bomb", "indexmap 1.9.3", @@ -1541,7 +1517,7 @@ dependencies = [ "biome_console", "biome_diagnostics", "biome_rowan", - "bitflags 2.13.0", + "bitflags 2.11.1", "drop_bomb", ] @@ -1621,41 +1597,20 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" -[[package]] -name = "bitcoin-consensus-encoding" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" -dependencies = [ - "bitcoin-internals", -] - -[[package]] -name = "bitcoin-internals" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" -dependencies = [ - "hex-conservative 0.3.2", -] - [[package]] name = "bitcoin-io" -version = "0.1.101" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" -dependencies = [ - "bitcoin-consensus-encoding", -] +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.101" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.2", + "hex-conservative", "serde", ] @@ -1667,18 +1622,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitvec" -version = "1.1.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ "funty", "radium", @@ -1717,9 +1672,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] @@ -1777,7 +1732,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -1793,7 +1748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d4f95e880cfd28c4ca5a006cf7f6af52b4bcb7b5866f573b2faa126fb7affb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -1813,14 +1768,14 @@ checksum = "2f7e98cee839b19076cb3ce1afdb62bb182e04ff5f71f70188827002fae91094" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "brotli" -version = "8.0.4" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1829,9 +1784,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1848,13 +1803,13 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", - "serde_core", + "serde", ] [[package]] @@ -1889,7 +1844,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -1906,15 +1861,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytes-str" -version = "0.2.8" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577d2bf5650f8554d5a372af5ac93535110a0fc75b3e702bb853369febf227c2" +checksum = "7c60b5ce37e0b883c37eb89f79a1e26fbe9c1081945d024eee93e8d91a7e18b3" dependencies = [ "bytes", "serde", @@ -1963,28 +1918,27 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.4" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] [[package]] name = "candle-core" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ecb245093b0f791b89d3420c3df9c6d49c60ab63ba54db896bf8a3baf486706" +checksum = "6bd9895436c1ba5dc1037a19935d084b838db066ff4e15ef7dded020b7c12a4a" dependencies = [ "byteorder", "candle-kernels", "candle-metal-kernels", "candle-ug", - "cudarc 0.19.8", + "cudarc 0.19.7", "float8", "gemm 0.19.0", "half", - "libc", "libm", "memmap2", "num-traits", @@ -1994,30 +1948,28 @@ dependencies = [ "rand 0.9.4", "rand_distr", "rayon", - "safetensors 0.8.0", + "safetensors 0.7.0", "thiserror 2.0.18", "tokenizers 0.22.2", - "yoke 0.8.3", - "zerocopy", - "zip 8.6.0", + "yoke 0.8.2", + "zip 7.2.0", ] [[package]] name = "candle-kernels" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67450168a281bbb195a14cc85cf164c7a12023a54a03409a3324f476346e0789" +checksum = "742e2ac226b777134436e9e692f44e77c278b8a7abb1554dc10e44dc911b349f" dependencies = [ "cudaforge", ] [[package]] name = "candle-metal-kernels" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e83c6acf639bb273c929d73c67a882bb4dd08a140f121096e19ba2f213d3e" +checksum = "4b6b5a4cae6b4e1ab0efcee4dc05272d11b374a3d1ba121b3a961e36be54ab60" dependencies = [ - "block2", "half", "objc2", "objc2-foundation", @@ -2029,9 +1981,9 @@ dependencies = [ [[package]] name = "candle-nn" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa10b6ccc365b33210ce404fbf45e60d3e0bdac1004463cf1052e6ee1c1739a" +checksum = "a9317a09d6530b758990ed7f625ac69ff43653bc9ee28b0464644ad1169ada87" dependencies = [ "candle-core", "candle-metal-kernels", @@ -2040,21 +1992,21 @@ dependencies = [ "num-traits", "objc2-metal", "rayon", - "safetensors 0.8.0", + "safetensors 0.7.0", "serde", "thiserror 2.0.18", ] [[package]] name = "candle-transformers" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcbbf7ff00ff6fe2af22b93600195917fe90e90ff48424a140d1a926c44b1c1" +checksum = "f59d08c89e9f4af9c464e2f3a8e16199e7cc601e6f34538c2cfbb42b623b1783" dependencies = [ "byteorder", "candle-core", "candle-nn", - "fancy-regex 0.18.0", + "fancy-regex 0.17.0", "num-traits", "rand 0.9.4", "rayon", @@ -2066,9 +2018,9 @@ dependencies = [ [[package]] name = "candle-ug" -version = "0.11.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257411c33abf7d898a31ac20d80813dfe96ff09e23a73039e22ab293aea9b871" +checksum = "ca0fc3167cbc99c8ec1be618cb620aa21dca95038f118c3579a79370e3dc5f77" dependencies = [ "ug", "ug-cuda", @@ -2094,24 +2046,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b4a6cae9efc04cc6cbb8faf338d2c497c165c83e74509cf4dbedea948bbf6e5" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "cargo-platform" -version = "0.3.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", - "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.23.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", @@ -2141,14 +2092,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex 1.3.0", ] [[package]] @@ -2202,9 +2153,9 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if 1.0.4", "cpufeatures 0.3.0", @@ -2294,9 +2245,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.7" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" dependencies = [ "clap", ] @@ -2320,7 +2271,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -2341,12 +2292,12 @@ dependencies = [ [[package]] name = "cliclack" -version = "0.5.5" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd2a475e9dff35ddebd459b0523aab732572581f7fe53133edd20b3330a0ce6" +checksum = "4529f45438fc25ca048b242d5c48e2d3ce9a521e2a5a9123d9737d8520b030dd" dependencies = [ - "console", - "indicatif", + "console 0.16.3", + "indicatif 0.18.4", "once_cell", "strsim", "textwrap", @@ -2383,9 +2334,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "cmpv2" @@ -2394,8 +2345,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "961b955a666e25ee5a1091d219128d6e6401e3dab84efb1a2bf6b4035d797b39" dependencies = [ "crmf", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2406,8 +2357,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" dependencies = [ "const-oid 0.9.6", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2467,9 +2418,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if 1.0.4", @@ -2510,10 +2461,35 @@ dependencies = [ ] [[package]] -name = "console" -version = "0.16.4" +name = "config" +version = "0.15.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8" +dependencies = [ + "pathdiff", + "serde_core", + "toml 1.1.2+spec-1.1.0", + "winnow 1.0.3", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", @@ -2717,8 +2693,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36fe21b96d5b87f5de4b5b7202ec41c00110ac817ce6728fe75fb2fe5962ed92" dependencies = [ "cms", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2735,18 +2711,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -2754,27 +2730,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -2842,7 +2818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -2900,9 +2876,9 @@ dependencies = [ [[package]] name = "cudarc" -version = "0.19.8" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42310153e06cf4cd532901f7096beb27504d681736a29ee90728ae4e2d93b2a8" +checksum = "1cea5f10a99e025c1b44ae2354c2d8326b25ddbd0baf76bde8e55cfd4018a2cc" dependencies = [ "float8", "half", @@ -2933,7 +2909,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -2983,7 +2959,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -2997,7 +2973,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3010,7 +2986,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3021,7 +2997,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3032,7 +3008,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3043,7 +3019,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3082,9 +3058,9 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "dbus" -version = "0.9.12" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ "libc", "libdbus-sys", @@ -3130,37 +3106,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.18", -] - [[package]] name = "deno_ast" version = "0.52.0" @@ -3326,7 +3271,7 @@ checksum = "1c28ede88783f14cd8aae46ca89f230c226b40e4a81ab06fa52ed72af84beb2f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3393,7 +3338,7 @@ dependencies = [ "stringcase", "strum 0.27.2", "strum_macros 0.27.2", - "syn 2.0.118", + "syn 2.0.117", "syn-match", "thiserror 2.0.18", ] @@ -3524,6 +3469,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -3546,7 +3501,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3555,9 +3510,21 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ + "powerfmt", "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -3576,7 +3543,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3586,7 +3553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3608,7 +3575,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.117", "unicode-xid", ] @@ -3636,7 +3603,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer 0.12.0", "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", @@ -3651,7 +3618,7 @@ dependencies = [ "diplomat_core", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3671,7 +3638,7 @@ dependencies = [ "serde", "smallvec", "strck", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3701,19 +3668,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "objc2", ] [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3771,7 +3738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33175ddb7a6d418589cab2966bd14a710b3b1139459d3d5ca9edf783c4833f4c" dependencies = [ "num-bigint", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -3853,12 +3820,12 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -3876,7 +3843,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "signature", ] @@ -3917,7 +3884,7 @@ dependencies = [ "group", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "subtle", @@ -3963,7 +3930,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -3983,7 +3950,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -4114,17 +4081,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "fancy-regex" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -4224,9 +4180,9 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.4.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" dependencies = [ "borrow-or-share", "ref-cast", @@ -4289,7 +4245,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -4339,14 +4295,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" dependencies = [ "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "fs-err" -version = "3.3.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", "tokio", @@ -4477,7 +4442,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -4653,7 +4618,7 @@ dependencies = [ "num-traits", "once_cell", "paste", - "pulp 0.22.3", + "pulp 0.22.2", "raw-cpuid", "rayon", "seq-macro", @@ -4806,15 +4771,17 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if 1.0.4", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasip2", + "wasip3", "wasm-bindgen", ] @@ -4861,7 +4828,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -4908,7 +4875,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.43.0" +version = "1.40.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-http", @@ -4918,12 +4885,10 @@ dependencies = [ "async-stream", "async-trait", "aws-config", - "aws-lc-rs", "aws-sdk-bedrockruntime", "aws-sdk-sagemakerruntime", "aws-smithy-types", "axum", - "axum-server", "base64 0.22.1", "blake3", "byteorder", @@ -4937,18 +4902,19 @@ dependencies = [ "dirs", "dotenvy", "dtor 1.0.5", + "encoding_rs", "env-lock", "etcetera 0.11.0", - "fs-err", + "fs-err 3.3.0", "fs2", "futures", "gethostname", "goose-acp-macros", - "goose-download-manager", "goose-mcp", "goose-providers", "goose-sdk-types", "goose-test-support", + "hf-hub 1.0.0-rc.1", "http 1.4.2", "icu_calendar", "icu_locale", @@ -4962,15 +4928,19 @@ dependencies = [ "jsonwebtoken", "keyring", "libc", + "llama-cpp-2", + "llama-cpp-sys-2", "lru 0.18.0", "minijinja", + "mlx-lm", + "mlx-lm-utils", + "mlx-rs", "mockall", "nanoid", "nostr", "nostr-sdk", "oauth2", "once_cell", - "openssl", "opentelemetry 0.32.0", "opentelemetry-appender-tracing", "opentelemetry-otlp 0.32.0", @@ -4978,12 +4948,10 @@ dependencies = [ "opentelemetry_sdk 0.32.1", "pastey", "pctx_code_mode", - "pem", "process-wrap", "pulldown-cmark", - "rand 0.10.2", + "rand 0.10.1", "rayon", - "rcgen", "regex", "reqwest 0.13.4", "rmcp", @@ -5038,28 +5006,27 @@ dependencies = [ "uuid", "v_htmlescape", "webbrowser", - "which 8.0.4", + "which 8.0.3", "winapi", "wiremock", ] [[package]] name = "goose-acp-macros" -version = "1.43.0" +version = "1.40.0" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "goose-cli" -version = "1.43.0" +version = "1.40.0" dependencies = [ "anstream", "anyhow", "async-trait", "axum", - "axum-server", "base64 0.22.1", "bat", "bzip2", @@ -5070,7 +5037,7 @@ dependencies = [ "clap_mangen", "cliclack", "comfy-table", - "console", + "console 0.16.3", "dotenvy", "env-lock", "etcetera 0.11.0", @@ -5078,9 +5045,9 @@ dependencies = [ "goose", "goose-mcp", "goose-providers", - "indicatif", + "indicatif 0.18.4", "open", - "rand 0.10.2", + "rand 0.10.1", "regex", "reqwest 0.13.4", "rmcp", @@ -5106,58 +5073,9 @@ dependencies = [ "zip 8.6.0", ] -[[package]] -name = "goose-download-manager" -version = "0.1.0-alpha.0" -dependencies = [ - "anyhow", - "once_cell", - "reqwest 0.13.4", - "serde", - "tokio", - "tracing", - "utoipa 4.2.3", -] - -[[package]] -name = "goose-local-inference" -version = "0.1.0-alpha.0" -dependencies = [ - "anyhow", - "async-stream", - "async-trait", - "base64 0.22.1", - "chrono", - "encoding_rs", - "etcetera 0.11.0", - "fs2", - "futures", - "goose-download-manager", - "goose-provider-types", - "goose-sdk-types", - "hf-hub", - "include_dir", - "llama-cpp-2", - "llama-cpp-sys-2", - "minijinja", - "regex", - "reqwest 0.13.4", - "rmcp", - "safemlx", - "safemlx-lm", - "safemlx-lm-utils", - "serde", - "serde_json", - "tempfile", - "tokio", - "tracing", - "utoipa 4.2.3", - "uuid", -] - [[package]] name = "goose-mcp" -version = "1.43.0" +version = "1.40.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -5186,8 +5104,8 @@ dependencies = [ ] [[package]] -name = "goose-provider-types" -version = "0.1.0-alpha.0" +name = "goose-providers" +version = "1.40.0" dependencies = [ "anyhow", "async-stream", @@ -5197,10 +5115,14 @@ dependencies = [ "env-lock", "futures", "once_cell", - "rand 0.10.2", + "pem", + "pkcs1", + "pkcs8 0.11.0", + "rand 0.10.1", "regex", "reqwest 0.13.4", "rmcp", + "sec1", "serde", "serde_json", "strum 0.28.0", @@ -5209,56 +5131,21 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", + "tokio-util", "tracing", "unicode-normalization", + "url", "utoipa 4.2.3", "uuid", ] -[[package]] -name = "goose-providers" -version = "0.1.0-alpha.0" -dependencies = [ - "anyhow", - "async-stream", - "async-trait", - "chrono", - "env-lock", - "futures", - "goose-local-inference", - "goose-provider-types", - "include_dir", - "pem", - "pkcs1", - "pkcs8", - "reqwest 0.13.4", - "rmcp", - "sec1", - "serde", - "serde_json", - "tempfile", - "test-case", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "url", - "urlencoding", - "utoipa 4.2.3", - "wiremock", -] - [[package]] name = "goose-sdk" -version = "0.1.0-alpha.0" +version = "1.40.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-schema", - "anyhow", - "futures", - "goose-providers", "goose-sdk-types", - "serde_json", "thiserror 2.0.18", "tokio", "tokio-util", @@ -5267,7 +5154,7 @@ dependencies = [ [[package]] name = "goose-sdk-types" -version = "0.1.0-alpha.0" +version = "1.40.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-schema", @@ -5276,9 +5163,52 @@ dependencies = [ "serde_json", ] +[[package]] +name = "goose-server" +version = "1.40.0" +dependencies = [ + "anyhow", + "aws-lc-rs", + "axum", + "axum-server", + "base64 0.22.1", + "bytes", + "chrono", + "clap", + "config", + "futures", + "goose", + "goose-mcp", + "goose-providers", + "hex", + "http 1.4.2", + "openssl", + "pem", + "rand 0.10.1", + "rcgen", + "reqwest 0.13.4", + "rmcp", + "rustls", + "serde", + "serde_json", + "serde_path_to_error", + "serde_yaml", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-http 0.7.0", + "tracing", + "tracing-subscriber", + "utoipa 4.2.3", + "uuid", + "winreg", +] + [[package]] name = "goose-test" -version = "1.43.0" +version = "1.40.0" dependencies = [ "clap", "serde_json", @@ -5286,7 +5216,7 @@ dependencies = [ [[package]] name = "goose-test-support" -version = "1.43.0" +version = "1.40.0" dependencies = [ "axum", "env-lock", @@ -5318,9 +5248,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -5352,9 +5282,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.4.2" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26569a2763497b7bd3fbd19374b774ea6038c5293678771259cd534d49740ff" +checksum = "d43ccdfe15a81ab0a8af639e90254227c9a46afd9c5f5b6ec7efaa345c4b0f00" dependencies = [ "derive_builder", "log", @@ -5455,12 +5385,22 @@ dependencies = [ ] [[package]] -name = "hex-conservative" -version = "0.3.2" +name = "hf-hub" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830e599c2904b08f0834ee6337d8fe8f0ed4a63b5d9e7a7f49c0ffa06d08d360" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" dependencies = [ - "arrayvec", + "dirs", + "http 1.4.2", + "indicatif 0.17.11", + "libc", + "log", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "ureq", + "windows-sys 0.60.2", ] [[package]] @@ -5560,14 +5500,14 @@ dependencies = [ [[package]] name = "hstr" -version = "3.0.6" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bb87e4b300d73412f6dcc7022ee7741452b51b155c2b06e5994d0770c2dbe2" +checksum = "c1b94e40256e78ddd4e30490aa931bec17e65e9413a6ad11f64ec67815da9323" dependencies = [ "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "triomphe", ] @@ -5656,24 +5596,24 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.4.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.10.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -5704,7 +5644,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.8", + "webpki-roots 1.0.7", ] [[package]] @@ -5815,7 +5755,7 @@ checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.3", + "yoke 0.8.2", "zerofrom", "zerovec", ] @@ -5906,12 +5846,18 @@ dependencies = [ "serde", "stable_deref_trait", "writeable", - "yoke 0.8.3", + "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -5947,9 +5893,9 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "ignore" -version = "0.4.27" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe112b004901c62c2faa11f4f75e9864e0cc5af8da71c9115d184a3aa888749f" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" dependencies = [ "crossbeam-deque", "globset", @@ -6063,11 +6009,24 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.6" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ - "console", + "console 0.15.11", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console 0.16.3", "portable-atomic", "unicode-width 0.2.2", "unit-prefix", @@ -6140,7 +6099,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -6191,11 +6150,10 @@ checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1" [[package]] name = "jiff" -version = "0.2.31" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" dependencies = [ - "defmt", "jiff-static", "jiff-tzdb-platform", "log", @@ -6207,20 +6165,20 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.31" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "jiff-tzdb" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6142247df1a93c2b3587402a19710be3e6e942f1581a1702e76408f2c21d6590" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -6258,7 +6216,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -6277,16 +6235,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "jobserver" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.3.4", "libc", ] @@ -6301,12 +6259,13 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if 1.0.4", "futures-util", + "once_cell", "wasm-bindgen", ] @@ -6321,40 +6280,30 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.46.10" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05" +checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" dependencies = [ "ahash", + "base64 0.22.1", "bytecount", - "data-encoding", "email_address", - "fancy-regex 0.18.0", + "fancy-regex 0.14.0", "fraction", - "getrandom 0.3.4", "idna", "itoa", - "jsonschema-regex", "num-cmp", "num-traits", + "once_cell", "percent-encoding", "referencing", "regex", + "regex-syntax", "serde", "serde_json", - "unicode-general-category", "uuid-simd", ] -[[package]] -name = "jsonschema-regex" -version = "0.46.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13" -dependencies = [ - "regex-syntax", -] - [[package]] name = "jsonwebtoken" version = "10.4.0" @@ -6434,7 +6383,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -6446,6 +6395,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -6502,14 +6457,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "libc", "plain", - "redox_syscall 0.9.0", + "redox_syscall 0.7.5", ] [[package]] @@ -6535,7 +6490,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "libc", ] @@ -6583,7 +6538,7 @@ version = "0.1.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b291e4bc2d10c43cd8dec16d49b6104cb3cb125f596ec380a753a5db1d965dd" dependencies = [ - "bindgen", + "bindgen 0.72.1", "cc", "cmake", "find_cuda_helper", @@ -6602,9 +6557,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lopdf" @@ -6613,18 +6568,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25aab26d99567469098e64a02f42679f8965c6401263eefa31d8f2dcc37a221c" dependencies = [ "aes", - "bitflags 2.13.0", + "bitflags 2.11.1", "cbc", "ecb", "encoding_rs", "flate2", - "getrandom 0.4.3", + "getrandom 0.4.2", "indexmap 2.14.0", "itoa", "log", "md-5", "nom 8.0.0", - "rand 0.10.2", + "rand 0.10.1", "rangemap", "sha2 0.10.9", "stringprep", @@ -6721,15 +6676,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", "stable_deref_trait", @@ -6756,7 +6711,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -6765,12 +6720,6 @@ dependencies = [ "paste", ] -[[package]] -name = "micromap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" - [[package]] name = "mime" version = "0.3.17" @@ -6789,9 +6738,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.21.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" +checksum = "2929e494b2280e1e18959bb2e121da03347ae896896fdfaceaab43c88a02803f" dependencies = [ "memo-map", "serde", @@ -6799,9 +6748,9 @@ dependencies = [ [[package]] name = "minijinja-contrib" -version = "2.21.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85342f6fac0be8ccd5bd00d9066be538f34f393f577b75d81b17c8398a6b43bb" +checksum = "99df5123c54391e2a228014c1dbbd85a3dab08a25e776c810526f2f47542b3de" dependencies = [ "minijinja", "serde", @@ -6825,9 +6774,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -6835,10 +6784,103 @@ dependencies = [ ] [[package]] -name = "mockall" -version = "0.15.0" +name = "mlx-internal-macros" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6ceddfe3ce334925e96bf420fdb2dcee5bed6c632a168ece622676dadeaf8a" +checksum = "7a7c4444d624bf6b93db5cc22ebff4fdfa13593fd56154fe33b1f302a557c2c6" +dependencies = [ + "darling 0.21.3", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "mlx-lm" +version = "0.0.1" +source = "git+https://github.com/jh-block/mlx-lm#4cb8572045f55f170eb6e41005a3f8a730673c42" +dependencies = [ + "anyhow", + "clap", + "idna_adapter", + "minijinja", + "mlx-lm-utils", + "mlx-macros", + "mlx-rs", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokenizers 0.22.2", +] + +[[package]] +name = "mlx-lm-utils" +version = "0.0.1" +source = "git+https://github.com/jh-block/mlx-lm#4cb8572045f55f170eb6e41005a3f8a730673c42" +dependencies = [ + "minijinja", + "minijinja-contrib", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokenizers 0.22.2", +] + +[[package]] +name = "mlx-macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819ee8b4434690572b6feb9c3ef0b6e90137e4190b340cf00150703b410aaf9" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "mlx-rs" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a0f592c5839b0237b3072530b1d3c503923579a2009ee0761df3edd1b1f27b" +dependencies = [ + "bytemuck", + "dyn-clone", + "half", + "itertools 0.14.0", + "libc", + "mach-sys", + "mlx-internal-macros", + "mlx-macros", + "mlx-sys", + "num-complex", + "num-traits", + "num_enum", + "parking_lot", + "paste", + "safetensors 0.6.2", + "smallvec", + "strum 0.27.2", + "thiserror 2.0.18", +] + +[[package]] +name = "mlx-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3bc3880111918b2d5018f845d48fd995f9901f16efc81d1fcfd2f4210b8219" +dependencies = [ + "bindgen 0.70.1", + "cc", + "cmake", +] + +[[package]] +name = "mockall" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" dependencies = [ "cfg-if 1.0.4", "downcast", @@ -6850,14 +6892,14 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.15.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cfe16fbe8a314aeec0b861ac24e60b1e123e97634bab045475b9d6a18416fd8" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" dependencies = [ "cfg-if 1.0.4", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -6885,7 +6927,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -6963,7 +7005,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -7021,9 +7063,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.4" +version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cf5d15d70d1f8f4059e5f79923ac15891eb691d2843d01191e0585fb064d70" +checksum = "08d8f0fe13526800300a36bf3b7c5f752e62e32ab81c74a8e5caa2865708625a" dependencies = [ "base64 0.22.1", "bech32", @@ -7130,9 +7172,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.8" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -7186,7 +7228,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -7200,19 +7242,20 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.46" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-modular" -version = "0.6.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" [[package]] name = "num-order" @@ -7273,9 +7316,15 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "oauth2" version = "5.0.0" @@ -7320,7 +7369,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -7332,7 +7381,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] @@ -7343,7 +7392,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -7362,7 +7411,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -7385,7 +7434,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -7396,7 +7445,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "block2", "dispatch2", "objc2", @@ -7455,7 +7504,7 @@ version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -7479,21 +7528,22 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.6" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "is-wsl", "libc", + "pathdiff", ] [[package]] name = "openssl" -version = "0.10.81" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "cfg-if 1.0.4", "foreign-types 0.3.2", "libc", @@ -7509,7 +7559,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -7520,18 +7570,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.6.1+3.6.3" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.117" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -8025,9 +8075,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -8035,9 +8085,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -8045,24 +8095,25 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", + "sha2 0.10.9", ] [[package]] @@ -8104,7 +8155,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -8142,7 +8193,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -8174,9 +8225,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -8185,8 +8236,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -8203,13 +8264,13 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plist" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml 0.41.0", + "quick-xml 0.39.4", "serde", "time", ] @@ -8233,7 +8294,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -8349,7 +8410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -8419,9 +8480,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -8429,15 +8490,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -8472,7 +8533,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -8493,9 +8554,9 @@ dependencies = [ [[package]] name = "pulp" -version = "0.22.3" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +checksum = "2e205bb30d5b916c55e584c22201771bcf2bad9aabd5d4127f38387140c38632" dependencies = [ "bytemuck", "cfg-if 1.0.4", @@ -8510,15 +8571,15 @@ dependencies = [ [[package]] name = "pulp-wasm-simd-flag" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" +checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0" [[package]] name = "pxfm" -version = "0.1.30" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "quick-error" @@ -8548,25 +8609,25 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.41.0" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "rustls", "socket2", "thiserror 2.0.18", @@ -8577,18 +8638,17 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.4.3", + "getrandom 0.3.4", "lru-slab", - "rand 0.10.2", - "rand_pcg", + "rand 0.9.4", "ring", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -8600,23 +8660,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -8672,12 +8732,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20 0.10.1", - "getrandom 0.4.3", + "chacha20 0.10.0", + "getrandom 0.4.2", "rand_core 0.10.1", ] @@ -8735,15 +8795,6 @@ dependencies = [ "rand 0.9.4", ] -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "rangemap" version = "1.7.1" @@ -8756,7 +8807,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", ] [[package]] @@ -8798,7 +8849,6 @@ checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "aws-lc-rs", "pem", - "ring", "rustls-pki-types", "time", "x509-parser", @@ -8826,16 +8876,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", ] [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", ] [[package]] @@ -8866,21 +8916,18 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "referencing" -version = "0.46.10" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d" +checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" dependencies = [ "ahash", "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "itoa", - "micromap", + "once_cell", "parking_lot", "percent-encoding", "serde_json", @@ -8961,7 +9008,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.8", + "webpki-roots 1.0.7", ] [[package]] @@ -9074,9 +9121,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", "base64 0.22.1", @@ -9092,7 +9139,7 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "rand 0.10.2", + "rand 0.10.1", "reqwest 0.13.4", "rmcp-macros", "schemars 1.2.1", @@ -9111,15 +9158,15 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", "serde_json", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9140,10 +9187,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -9172,9 +9219,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -9200,7 +9247,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9213,7 +9260,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -9222,11 +9269,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -9237,9 +9285,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -9249,9 +9297,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -9298,17 +9346,17 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" -version = "18.0.1" +version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" +checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "cfg-if 1.0.4", "clipboard-win", "home", @@ -9341,99 +9389,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" -[[package]] -name = "safemlx" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f501c4f9748dfe9ccc905e7e13ae88873d96a00735268c69258e158da07a777f" -dependencies = [ - "bytemuck", - "dyn-clone", - "half", - "itertools 0.14.0", - "libc", - "mach-sys", - "num-complex", - "num-traits", - "num_enum", - "parking_lot", - "paste", - "safemlx-internal-macros", - "safemlx-macros", - "safemlx-sys", - "safetensors 0.6.2", - "smallvec", - "strum 0.27.2", - "thiserror 2.0.18", -] - -[[package]] -name = "safemlx-internal-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9708b1312176963a4ea6412859e6655e56a83fe6ab936142715aa0e9fd662fe5" -dependencies = [ - "darling 0.21.3", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "safemlx-lm" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f450654386009003b745adfdae7af2c7f44ae749d75621a2a0b07607945e9f" -dependencies = [ - "anyhow", - "clap", - "idna_adapter", - "minijinja", - "safemlx", - "safemlx-lm-utils", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokenizers 0.22.2", -] - -[[package]] -name = "safemlx-lm-utils" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583d39124dae0b4a93ac7528d16d543f5e717c21365e6b034a8a74d8e1b4eb3a" -dependencies = [ - "minijinja", - "minijinja-contrib", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokenizers 0.22.2", -] - -[[package]] -name = "safemlx-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d549c5fdbe69b904ef84c5cf352add203de1093b3c636e8133d94c24cfc33ce0" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "safemlx-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552285b007dab687890f5c49382e52e69a3bb1359bb2c8f2c2743743509af071" -dependencies = [ - "cc", - "cmake", -] - [[package]] name = "safetensors" version = "0.4.5" @@ -9456,15 +9411,13 @@ dependencies = [ [[package]] name = "safetensors" -version = "0.8.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b079b829cb27a1c3c374341345ed2e8b2c0c839034522cee576c140bd7f846" +checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" dependencies = [ "hashbrown 0.16.1", - "libc", "serde", "serde_json", - "tempfile", ] [[package]] @@ -9542,7 +9495,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9554,7 +9507,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9586,7 +9539,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9608,9 +9561,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -9641,7 +9594,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -9654,7 +9607,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -9724,7 +9677,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9735,7 +9688,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9791,7 +9744,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9831,9 +9784,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.21.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", "bs58", @@ -9851,14 +9804,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.21.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9893,7 +9846,7 @@ checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -9999,9 +9952,9 @@ dependencies = [ [[package]] name = "sigstore-bundle" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b51b097b7e9d898efad51e1031c855ce4008b4866d0c38ac8b0fac5745a84c" +checksum = "94ba33ef96bafd16f5dfc954113c37a689dcf721816d11680667626b1ebb0496" dependencies = [ "base64 0.22.1", "hex", @@ -10016,21 +9969,21 @@ dependencies = [ [[package]] name = "sigstore-crypto" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1210ea5dd0dd07b1466449a6c5c79000f8ca10ac30b4e8f64031c3bed4c5c662" +checksum = "77ecc48bf9facc710e772b3e7ab6a3ebc81c34be208a6ebf6c872d0ccbb8b52f" dependencies = [ "aws-lc-rs", "base64 0.22.1", "const-oid 0.9.6", - "der", + "der 0.7.10", "digest 0.10.7", "pem", "rand_core 0.9.5", "sha2 0.10.9", "signature", "sigstore-types", - "spki", + "spki 0.7.3", "thiserror 2.0.18", "tracing", "x509-cert", @@ -10038,9 +9991,9 @@ dependencies = [ [[package]] name = "sigstore-merkle" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae98f8a270885497dbe84e6dae10fa29066df0388f62980923728e38d844716" +checksum = "9f6fc5c295b5b9fa929e38acc53b29c8ed9ba3ee93e5fa87e7daa44072f0b26e" dependencies = [ "base64 0.22.1", "hex", @@ -10051,9 +10004,9 @@ dependencies = [ [[package]] name = "sigstore-rekor" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1c153f4f89f7570d0f625f660cadcb1b2fba72583f378d2d2b308c752ac45f" +checksum = "ece0d614b93c94e2ddab5c2142959396ce5a5f1d315435779e8ce0f9da250f5d" dependencies = [ "base64 0.22.1", "hex", @@ -10069,9 +10022,9 @@ dependencies = [ [[package]] name = "sigstore-trust-root" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded6afc37295a10dba6569e033dd15f85f04f00980bfcf2c64009bc8ed279985" +checksum = "a91dbe81d236075c568926b5540d86dbc0e272cd0c6cfc5523e35d5200e71cf0" dependencies = [ "base64 0.22.1", "hex", @@ -10088,16 +10041,16 @@ dependencies = [ [[package]] name = "sigstore-tsa" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88505b71600a791e52bed3777ed9cfeefd4b03dee6575d38c5542ae80e2bbdc" +checksum = "6a04c841cdc52fc5da6284d6aa1fbf93f6b65d71437b2cc04751d117eac8aa33" dependencies = [ "aws-lc-rs", "base64 0.22.1", "cmpv2", "cms", "const-oid 0.9.6", - "der", + "der 0.7.10", "hex", "jiff", "rand 0.9.4", @@ -10114,9 +10067,9 @@ dependencies = [ [[package]] name = "sigstore-tuf" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc7cced094bd306ef561312cfffa3ea05ac7eea4a62cfa81680c07a10addbc0" +checksum = "556727ed8c836bcfee6a151a3201e8605af30ef0e01dd9021a2fd43c3f5269f4" dependencies = [ "globset", "hex", @@ -10136,9 +10089,9 @@ dependencies = [ [[package]] name = "sigstore-types" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f034e86c3b8d91b32608c83f4a18939eed32c6ca08d17d2f52b88be365aac6" +checksum = "a48661f5c7a4f7496de80865e5b258adac0bc514164e6c944b93b65ba5de334b" dependencies = [ "base64 0.22.1", "hex", @@ -10150,9 +10103,9 @@ dependencies = [ [[package]] name = "sigstore-verify" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4720f300d093ecac6dcd75d4498fd1b2d9133600e679837bb9d944a4229eda7" +checksum = "5c4fed99a3609ebb236284b781674d8c45beea93bbc823ef2facd2261b3801c7" dependencies = [ "base64 0.22.1", "cms", @@ -10242,9 +10195,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -10262,9 +10215,9 @@ dependencies = [ [[package]] name = "smawk" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithy-transport-reqwest" @@ -10289,6 +10242,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "sourcemap" version = "9.3.2" @@ -10300,7 +10264,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -10309,9 +10273,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.9" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] @@ -10323,7 +10287,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -10405,7 +10379,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10428,7 +10402,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.118", + "syn 2.0.117", "tokio", "url", ] @@ -10441,7 +10415,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.13.0", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -10484,7 +10458,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.13.0", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -10541,9 +10515,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f24a9b78c40b90817bbcd1821c74ddfd74916aadd29403d001532a9195532d" +checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72" dependencies = [ "bytes", "futures-util", @@ -10616,7 +10590,7 @@ checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" dependencies = [ "quote", "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10669,7 +10643,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10681,7 +10655,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10699,7 +10673,7 @@ dependencies = [ "allocator-api2", "bumpalo", "hashbrown 0.14.5", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", ] [[package]] @@ -10728,7 +10702,7 @@ dependencies = [ "new_debug_unreachable", "num-bigint", "once_cell", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "siphasher 0.3.11", "swc_atoms", @@ -10763,7 +10737,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10772,12 +10746,12 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a573a0c72850dec8d4d8085f152d5778af35a2520c3093b242d2d1d50776da7c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "is-macro", "num-bigint", "once_cell", "phf 0.11.3", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "string_enum", "swc_atoms", @@ -10798,7 +10772,7 @@ dependencies = [ "num-bigint", "once_cell", "regex", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "ryu-js", "serde", "swc_allocator", @@ -10818,7 +10792,7 @@ checksum = "e276dc62c0a2625a560397827989c82a93fd545fcf6f7faec0935a82cc4ddbb8" dependencies = [ "proc-macro2", "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10827,10 +10801,10 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e82f7747e052c6ff6e111fa4adeb14e33b46ee6e94fe5ef717601f651db48fc" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "either", "num-bigint", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "seq-macro", "serde", "smallvec", @@ -10851,7 +10825,7 @@ checksum = "fbcababb48f0d46587a0a854b2c577eb3a56fa99687de558338021e93cd2c8f5" dependencies = [ "anyhow", "pathdiff", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10864,11 +10838,11 @@ version = "27.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1a51af1a92cd4904c073b293e491bbc0918400a45d58227b34c961dd6f52d7" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "either", "num-bigint", "phf 0.11.3", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "seq-macro", "serde", "smartstring", @@ -10890,7 +10864,7 @@ dependencies = [ "once_cell", "par-core", "phf 0.11.3", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10923,7 +10897,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -10933,7 +10907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d7748d4112c87ce1885260035e4a43cebfe7661a40174b7d77a0a04760a257" dependencies = [ "either", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10954,7 +10928,7 @@ dependencies = [ "bytes-str", "indexmap 2.14.0", "once_cell", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "sha1", "string_enum", @@ -10975,7 +10949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4408800fdeb541fabf3659db622189a0aeb386f57b6103f9294ff19dfde4f7b0" dependencies = [ "bytes-str", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -10996,7 +10970,7 @@ dependencies = [ "num_cpus", "once_cell", "par-core", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "ryu-js", "swc_atoms", "swc_common", @@ -11028,7 +11002,7 @@ checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11039,7 +11013,7 @@ checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11054,7 +11028,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -11247,9 +11221,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -11264,7 +11238,7 @@ checksum = "54b8f0a9004d6aafa6a588602a1119e6cdaacec9921aa1605383e6e7d6258fd6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11284,7 +11258,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11332,7 +11306,7 @@ checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11341,7 +11315,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "byteorder", "enum-as-inner", "libc", @@ -11369,7 +11343,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -11407,7 +11381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -11505,7 +11479,7 @@ dependencies = [ "cfg-if 1.0.4", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11516,7 +11490,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "test-case-core", ] @@ -11540,6 +11514,12 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" + [[package]] name = "thiserror" version = "1.0.69" @@ -11566,7 +11546,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11577,7 +11557,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11632,16 +11612,17 @@ dependencies = [ "fancy-regex 0.17.0", "lazy_static", "regex", - "rustc-hash 2.1.3", + "rustc-hash 2.1.2", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde_core", @@ -11651,15 +11632,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.9" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -11721,7 +11702,7 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11732,11 +11713,13 @@ checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" dependencies = [ "ahash", "aho-corasick", - "compact_str 0.9.1", + "compact_str 0.9.0", "dary_heap", "derive_builder", "esaxx-rs", "getrandom 0.3.4", + "hf-hub 0.4.3", + "indicatif 0.18.4", "itertools 0.14.0", "log", "macro_rules_attribute", @@ -11764,7 +11747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" dependencies = [ "ahash", - "compact_str 0.9.1", + "compact_str 0.9.0", "daachorse", "dary_heap", "derive_builder", @@ -11831,7 +11814,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -11862,7 +11845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a129d95275ebf4c493ec53bf0f8cd95f5ac161bc4f381700809a54f595d4470" dependencies = [ "pin-project-lite", - "rand 0.10.2", + "rand 0.10.1", "tokio", ] @@ -11878,9 +11861,9 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", @@ -11963,12 +11946,10 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "toml_writer", "winnow 1.0.3", ] @@ -12083,7 +12064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.13.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -12105,7 +12086,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.11.1", "bytes", "http 1.4.2", "percent-encoding", @@ -12159,7 +12140,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -12244,9 +12225,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.10" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c343ed63e3f5c64d1acdecb5d2c13d4e169cb5fde0052106ebaa6c6f27f9e55" +checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" dependencies = [ "cc", "regex", @@ -12354,9 +12335,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.16" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" dependencies = [ "serde", "stable_deref_trait", @@ -12423,9 +12404,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.20.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typewit" @@ -12489,11 +12470,12 @@ dependencies = [ [[package]] name = "umya-spreadsheet" -version = "3.0.0" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf31e59909bf0fed6fc9df1d24bed0f135518eb2af05a330284f81ffcfb0513" +checksum = "408c7e039c96ec1d517a1111ade7fadab889f32c096dac691a1e3b8018c3e39a" dependencies = [ "aes", + "ahash", "base64 0.22.1", "byteorder", "cbc", @@ -12501,20 +12483,18 @@ dependencies = [ "chrono", "encoding_rs", "fancy-regex 0.14.0", + "getrandom 0.2.17", "hmac 0.12.1", "html_parser", "imagesize", - "jiff", + "lazy_static", "md-5", - "num-traits", - "paste", - "phf 0.11.3", "quick-xml 0.37.5", - "rand 0.8.6", - "rgb", + "regex", "sha2 0.10.9", + "thin-vec", "thousands", - "zip 8.6.0", + "zip 2.4.2", ] [[package]] @@ -12535,12 +12515,6 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-id-start" version = "1.4.0" @@ -12585,9 +12559,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.3" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -12615,9 +12589,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "uniffi" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8" +checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" dependencies = [ "anyhow", "camino", @@ -12631,15 +12605,15 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533b0312c73e3b54eb78a4b257ceae390962dd4767995778309a74644643f9ac" +checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" dependencies = [ "anyhow", "askama", "camino", "cargo_metadata", - "fs-err", + "fs-err 2.11.0", "glob", "goblin", "heck", @@ -12648,7 +12622,7 @@ dependencies = [ "serde", "tempfile", "textwrap", - "toml 1.1.2+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "uniffi_internal_macros", "uniffi_meta", "uniffi_pipeline", @@ -12657,9 +12631,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e32e261c5b0dfaba6488f536e71957dddd6b1a498ac7eb791bee56b60a086be" +checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" dependencies = [ "anyhow", "bytes", @@ -12669,39 +12643,39 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ae78069a5e6772ef694fd5bdb628532c88d2c2f0e7142bf6a384636eadb1af" +checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" dependencies = [ "anyhow", "indexmap 2.14.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "uniffi_macros" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330be6770532e86320df31f54c70bb0be67588594e8e77fa56e9083a3fed5d0d" +checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" dependencies = [ "camino", - "fs-err", + "fs-err 2.11.0", "once_cell", "proc-macro2", "quote", "serde", - "syn 2.0.118", - "toml 1.1.2+spec-1.1.0", + "syn 2.0.117", + "toml 0.9.12+spec-1.1.0", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78de021f5547e56ab16c665a49d67d4fd3d31e77422f7739a2e9359d328cd9e7" +checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" dependencies = [ "anyhow", "siphasher 1.0.3", @@ -12711,9 +12685,9 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8201bb1907ed8a42d80e11cbc25c8a033e7a31c3cff1d911f56eedb81d4948" +checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" dependencies = [ "anyhow", "heck", @@ -12724,9 +12698,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e57996bc58009cc29bf04845d627ae313c2547b87171c1c349d6c51a1656c0" +checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" dependencies = [ "anyhow", "textwrap", @@ -12768,6 +12742,25 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.8" @@ -12838,7 +12831,8 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.118", + "regex", + "syn 2.0.117", ] [[package]] @@ -12849,16 +12843,16 @@ checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] name = "uuid" -version = "1.23.4" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -12870,6 +12864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ "outref", + "uuid", "vsimd", ] @@ -12886,8 +12881,8 @@ version = "145.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f84468f393984251db025e944544590a8fb429d5088b78102bd72f09232f0da" dependencies = [ - "bindgen", - "bitflags 2.13.0", + "bindgen 0.72.1", + "bitflags 2.11.1", "fslock", "gzip-header", "home", @@ -12972,11 +12967,20 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +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 0.51.0", ] [[package]] @@ -12996,9 +13000,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -13009,9 +13013,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -13019,9 +13023,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -13029,26 +13033,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -13073,10 +13099,22 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.103" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -13110,9 +13148,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -13123,14 +13161,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.8", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -13176,9 +13214,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.4" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "c789537cf2f7f55be8e6192f92e464174ee55f91af622777f7f1ceb0dbccd03e" dependencies = [ "libc", ] @@ -13312,7 +13350,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -13323,7 +13361,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -13334,7 +13372,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -13345,7 +13383,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -13647,6 +13685,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -13657,6 +13698,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if 1.0.4", + "windows-sys 0.61.2", +] + [[package]] name = "winsafe" version = "0.0.19" @@ -13686,12 +13737,100 @@ dependencies = [ "url", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" @@ -13731,10 +13870,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der", + "der 0.7.10", "sha1", "signature", - "spki", + "spki 0.7.3", "tls_codec", ] @@ -13751,7 +13890,6 @@ dependencies = [ "lazy_static", "nom 7.1.3", "oid-registry", - "ring", "rusticata-macros", "thiserror 2.0.18", "time", @@ -13765,7 +13903,7 @@ checksum = "f5ceece934a21607055b7ac5c25adb56a2ff559804b10705dc674d1d838c15e1" dependencies = [ "cmpv2", "cms", - "der", + "der 0.7.10", ] [[package]] @@ -13785,7 +13923,7 @@ dependencies = [ "hyper", "lazy_static", "more-asserts", - "rand 0.10.2", + "rand 0.10.1", "redb", "reqwest 0.13.4", "reqwest-middleware", @@ -13822,13 +13960,13 @@ dependencies = [ "csv", "futures", "futures-util", - "getrandom 0.4.3", + "getrandom 0.4.2", "heapify", "itertools 0.14.0", "lazy_static", "lz4_flex", "more-asserts", - "rand 0.10.2", + "rand 0.10.1", "regex", "safe-transmute", "serde", @@ -13859,7 +13997,7 @@ dependencies = [ "itertools 0.14.0", "lazy_static", "more-asserts", - "rand 0.10.2", + "rand 0.10.1", "serde", "serde_json", "sha2 0.10.9", @@ -13899,7 +14037,7 @@ dependencies = [ "more-asserts", "oneshot", "pin-project", - "rand 0.10.2", + "rand 0.10.1", "reqwest 0.13.4", "serde", "serde_json", @@ -13957,9 +14095,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive 0.8.2", @@ -13974,7 +14112,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "synstructure", ] @@ -13986,28 +14124,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -14027,28 +14165,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.5.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -14058,7 +14196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke 0.8.3", + "yoke 0.8.2", "zerofrom", "zerovec", ] @@ -14070,7 +14208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", - "yoke 0.8.3", + "yoke 0.8.2", "zerofrom", "zerovec-derive", ] @@ -14083,7 +14221,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.117", ] [[package]] @@ -14098,6 +14236,35 @@ dependencies = [ "flate2", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "indexmap 2.14.0", + "memchr", + "typed-path", +] + [[package]] name = "zip" version = "8.6.0" @@ -14114,9 +14281,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 4425dbd52d..45d3e69cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.43.0" -rust-version = "1.94.1" +version = "1.40.0" +rust-version = "1.91.1" authors = ["AAIF "] license = "Apache-2.0" repository = "https://github.com/aaif-goose/goose" @@ -31,8 +31,8 @@ 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.11", default-features = false } -candle-nn = { version = "0.11", default-features = false } +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 = "6", default-features = false } diff --git a/Justfile b/Justfile index be971a398c..56f0fe3302 100644 --- a/Justfile +++ b/Justfile @@ -13,14 +13,18 @@ check-everything: cargo clippy --all-targets -- -D warnings @echo " โ†’ Checking UI code formatting..." cd ui/desktop && pnpm run lint:check + @echo " โ†’ Validating OpenAPI schema..." + ./scripts/check-openapi-schema.sh @echo "" @echo "โœ… All style checks passed!" # Default release command release-binary: @echo "Building release version..." - cargo build --release -p goose-cli --bin goose + cargo build --release @just copy-binary + @echo "Generating OpenAPI schema..." + cargo run -p goose-server --bin generate_schema # Build Windows executable on a Windows host [unix] @@ -30,7 +34,7 @@ release-windows: [windows] release-windows: - @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'rustup target add x86_64-pc-windows-msvc; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; cargo build --release --target x86_64-pc-windows-msvc -p goose-cli --bin goose; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host "Windows executable created at ./target/x86_64-pc-windows-msvc/release/goose.exe"' + @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'rustup target add x86_64-pc-windows-msvc; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; cargo build --release --target x86_64-pc-windows-msvc -p goose-server; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Write-Host "Windows executable created at ./target/x86_64-pc-windows-msvc/release/goosed.exe"' # Build for Intel Mac release-intel: @@ -39,7 +43,14 @@ release-intel: @just copy-binary-intel copy-binary BUILD_MODE="release": - @rm -f ./ui/desktop/src/bin/goosed + @if [ -f ./target/{{BUILD_MODE}}/goosed ]; then \ + echo "Copying goosed binary from target/{{BUILD_MODE}}..."; \ + rm -f ./ui/desktop/src/bin/goosed; \ + cp -p ./target/{{BUILD_MODE}}/goosed ./ui/desktop/src/bin/; \ + else \ + echo "Binary not found in target/{{BUILD_MODE}}"; \ + exit 1; \ + fi @if [ -f ./target/{{BUILD_MODE}}/goose ]; then \ echo "Copying goose CLI binary from target/{{BUILD_MODE}}..."; \ rm -f ./ui/desktop/src/bin/goose; \ @@ -51,7 +62,14 @@ copy-binary BUILD_MODE="release": # Copy binary command for Intel build copy-binary-intel: - @rm -f ./ui/desktop/src/bin/goosed + @if [ -f ./target/x86_64-apple-darwin/release/goosed ]; then \ + echo "Copying Intel goosed binary to ui/desktop/src/bin with permissions preserved..."; \ + rm -f ./ui/desktop/src/bin/goosed; \ + cp -p ./target/x86_64-apple-darwin/release/goosed ./ui/desktop/src/bin/; \ + else \ + echo "Intel release binary not found."; \ + exit 1; \ + fi @if [ -f ./target/x86_64-apple-darwin/release/goose ]; then \ echo "Copying Intel goose CLI binary to ui/desktop/src/bin..."; \ rm -f ./ui/desktop/src/bin/goose; \ @@ -69,11 +87,10 @@ copy-binary-windows: [windows] copy-binary-windows: - @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'if (Test-Path ./target/x86_64-pc-windows-msvc/release/goose.exe) { \ + @powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'if (Test-Path ./target/x86_64-pc-windows-msvc/release/goosed.exe) { \ Write-Host "Copying Windows binary to ui/desktop/src/bin..."; \ New-Item -ItemType Directory -Force "./ui/desktop/src/bin" | Out-Null; \ - Remove-Item -Path "./ui/desktop/src/bin/goosed.exe" -Force -ErrorAction SilentlyContinue; \ - Copy-Item -Path "./target/x86_64-pc-windows-msvc/release/goose.exe" -Destination "./ui/desktop/src/bin/" -Force; \ + Copy-Item -Path "./target/x86_64-pc-windows-msvc/release/goosed.exe" -Destination "./ui/desktop/src/bin/" -Force; \ } else { \ Write-Host "Windows binary not found." -ForegroundColor Red; \ exit 1; \ @@ -99,7 +116,7 @@ run-ui-only: cd ui/desktop && pnpm install && pnpm run start-gui debug-ui: - @echo "๐Ÿš€ Starting goose frontend in external ACP backend mode" + @echo "๐Ÿš€ Starting goose frontend in external backend mode" cd ui/desktop && \ export GOOSE_EXTERNAL_BACKEND=true && \ export GOOSE_SERVER__SECRET_KEY="${GOOSE_SERVER__SECRET_KEY:-test}" && \ @@ -144,8 +161,19 @@ run-docs: # Run server run-server: - @echo "Running external ACP backend..." - GOOSE_SERVER__SECRET_KEY="${GOOSE_SERVER__SECRET_KEY:-test}" cargo run -p goose-cli --bin goose -- serve --platform desktop --host 127.0.0.1 --port 3000 + @echo "Running server..." + cargo run -p goose-server --bin goosed agent + +# Check if OpenAPI schema is up-to-date +check-openapi-schema: generate-openapi + ./scripts/check-openapi-schema.sh + +# Generate OpenAPI specification without starting the UI +generate-openapi: + @echo "Generating OpenAPI schema..." + cargo run -p goose-server --bin generate_schema + @echo "Generating frontend API..." + cd ui/desktop && npx @hey-api/openapi-ts # Check if generated ACP schema and TypeScript types are up-to-date check-acp-schema: generate-acp-types @@ -290,6 +318,7 @@ bump-version version: @cd ui/desktop && npm pkg set "version={{ version }}" # update Cargo.lock after bumping versions in Cargo.toml @cargo update --workspace + @just set-openapi-version {{ version }} # rebuild canonical model registry and mapping report from models.dev build-canonical-models: @@ -304,10 +333,14 @@ prepare-release version: Cargo.lock \ ui/desktop/package.json \ ui/pnpm-lock.yaml \ - crates/goose-provider-types/src/canonical/data/canonical_models.json \ - crates/goose-provider-types/src/canonical/data/provider_metadata.json + ui/desktop/openapi.json \ + crates/goose-providers/src/canonical/data/canonical_models.json \ + crates/goose-providers/src/canonical/data/provider_metadata.json @git commit --message "chore(release): release version {{ version }}" +set-openapi-version version: + @jq '.info.version |= "{{ version }}"' ui/desktop/openapi.json > ui/desktop/openapi.json.tmp && mv ui/desktop/openapi.json.tmp ui/desktop/openapi.json + # extract version from Cargo.toml get-tag-version: @uvx --from=toml-cli toml get --toml-path=Cargo.toml "workspace.package.version" @@ -342,6 +375,7 @@ set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] ### profile = --release or "" for debug ### allparam = OR/AND/ANY/NONE --workspace --all-features --all-targets win-bld profile allparam: + cargo run {{profile}} -p goose-server --bin generate_schema cargo build {{profile}} {{allparam}} ### Build just debug @@ -370,7 +404,6 @@ win-app-deps: win-copy-win profile: copy target{{s}}{{profile}}{{s}}*.exe ui{{s}}desktop{{s}}src{{s}}bin copy target{{s}}{{profile}}{{s}}*.dll ui{{s}}desktop{{s}}src{{s}}bin - if exist ui{{s}}desktop{{s}}src{{s}}bin{{s}}goosed.exe del /f /q ui{{s}}desktop{{s}}src{{s}}bin{{s}}goosed.exe ### "Other" copy {release|debug} files to ui/desktop/src/bin ### s = os dependent file separator diff --git a/README.md b/README.md index f6f0522594..e6c4f05608 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +> **๐Ÿฆ† goose has moved!** This project has moved from `block/goose` to the [Agentic AI Foundation (AAIF)](https://aaif.io/) at the Linux Foundation. Some links and references are still being updated โ€” please bear with us during the transition. +
# goose @@ -14,12 +16,8 @@ _your native open source AI agent โ€” desktop app, CLI, and API โ€” for code, wo Packaging status

- -aaif-goose%2Fgoose | Trendshift -
- goose is a general-purpose AI agent that runs on your machine. Not just for code โ€” use it for research, writing, automation, data analysis, or anything you need to get done. A native desktop app for macOS, Linux, and Windows. A full CLI for terminal workflows. An API to embed it anywhere. Built in Rust for performance and portability. diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index cb87c076c6..d25d7c00bc 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,23 +1,134 @@ # goose Release Manual Testing Checklist -Download the release builds from this PR. Once a build is ready, the actions bot will post a comment on this PR -with instructions on how to download and sign. +## Version: {{VERSION}} + +### Identify the high risk changes in this Release -## Use the following script to create a risk assessment and testing plan: ``` ./workflow_recipes/release_risk_check/run.sh {{VERSION}} ``` -It will generate an analysis report in `/tmp/release_report_final.md` and perform testing is necessary for high risk pr changes. +It will generate an analysis report in `/tmp/release_report_final.md` and perform testing is necessary for high risk pr changes. -## Run the goose self-test recipe -goose run --recipe goose-self-test.yaml +## Regression Testing -## Have goose produce a test plan +Make a copy of this document for each version and check off as steps are verified. -Open the release candidate desktop app and have goose produce a test plan by pointing it at this PR. Use a prompt like +### Provider Testing -> Look at the notes in PR and the report at `/tmp/release_report_final.md` and investigate potential risks in this release. After familiarizing yourself with the scope of each change, produce a suggested test plan that I should follow before publishing the release. +- [ ] Run `cd ui/desktop && pnpm run test:integration:providers` locally from the release branch and verify all providers/models work +- [ ] Launch goose, click reset providers, choose databricks and a model -goose will produce a plan. Follow this plan to finish testing. +### Starting Conversations + +Test various ways to start a conversation: + +- [ ] Open home and start a new conversation with "Hello" + - [ ] Agent responds + - [ ] Token count is updated after agent finishes + - [ ] Go to history and see there is a new entry +- [ ] Go back to the main screen, start a new conversation from the hub and see that it opens a new conversation +- [ ] Open history and click the Hello conversation - verify it loads +- [ ] Add a new message to this conversation and see that it is added +- [ ] Change the working directory of an existing conversation + - [ ] Ask "what is your working directory?" + - [ ] Response should match the new directory +- [ ] Open a new window, click chat in left side for new chat +- [ ] Type "create a tamagotchi game" in the chat input to test developer extension + +### Recipes + +#### Create Recipe from Session + +- [ ] Start a simple chat conversation like "hi" +- [ ] Click "create a recipe from this session" in the bottom chat bar + - [ ] Recipe title, description and instructions should be filled in with details from the chat + - [ ] Add a few activities and params (params unused indicator should update if added to instructions/prompts or activities) + - [ ] Can launch create and run recipe - launches in a new window showing as a recipe agent chat with parameters filled in and interact with it + - [ ] Recipe should be saved in recipe library + +#### Use Existing Recipe + +- [ ] Pick trip planner from recipe hub (go/gooserecipes) + - [ ] See the warning whether to trust this recipe (only on fresh install) + - [ ] See the form pop up + - [ ] Fill in the form with "Africa" and "14 days" + - [ ] Check results are reasonable + - [ ] Ask how many days the trip is for - should say 14 + +#### Recipe Management + +- [ ] Go to recipe manager and enter a new recipe to generate a joke + - [ ] See that it works if you run it + - [ ] Edit the recipe by bottom bar and click "View/Edit Recipe" + - [ ] Make it generate a limerick instead + - [ ] Check that the updated recipe works + - [ ] Delete the recipe from the recipe manager + - [ ] Verify recipe is actually deleted + +#### Recipe from File + +- [ ] Create a file `~/.config/goose/recipes/test-recipe.yaml` with the following content: + +```yaml +recipe: + title: test recipe again + description: testing recipe again + instructions: The value of test_param is {{test_param}} + prompt: What is the value of test_param? + parameters: + - key: test_param + input_type: string + requirement: required + description: Enter value for test_param +``` + +- [ ] See that it shows up in the list of installed recipes +- [ ] Launch the recipe, see that it asks for test_param +- [ ] Enter a number, see that it pre-fills the prompt and tells you the value after you hit submit +- [ ] Go to hub and enter "what is the value of test_param" +- [ ] See a new chat that says it has no idea (recipe is no longer active) + +### Extensions + +#### Manual Extension Addition + +- [ ] Can manually add an extension using random quotes from project + - [ ] Add new custom stdio extension with the following command and save: + - [ ] `node /ABSOLUTE/PATH/TO/goose/ui/desktop/tests/e2e/basic-mcp.ts` (use your actual project path) + - [ ] Should add and can chat to ask for a random quote + +#### Playwright Extension + +- [ ] Install the playwright extension from the extensions hub + - [ ] Tell it to open a browser and search on Google for cats + - [ ] Verify that the browser opens and navigates + +#### Extension with Environment Variables + +- [ ] Install an extension from deeplink that needs env variables: + - [ ] Use: `goose://extension?cmd=npx&arg=-y&arg=%40upstash%2Fcontext7-mcp&id=context7&name=Context7&description=Use%20up-to-date%20code%20and%20docs&env=TEST_ACCESS_TOKEN` + - [ ] Extension page should load with env variables modal showing + - [ ] Allow form input and saving extension + +### Speech-to-Text (Local Model) + +- [ ] Go to Settings > Chat > Voice dictation provider and select the small model +- [ ] Run a quick test that speech-to-text is working (click the mic button, speak, verify transcription) +- [ ] Also try OpenAI using your OpenAI key + +### Settings + +- [ ] Settings page loads and all tabs load +- [ ] Can change dark mode setting + +### Follow-up Issues + +Link any GitHub issues filed during testing: + +--- + +**Tested by:** _____ +**Date:** _____ +**Notes:** _____ diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index e47f98bf55..e50ee6d8a8 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -62,9 +62,8 @@ urlencoding = { workspace = true } clap_complete = { version = "4", default-features = false } comfy-table = { version = "7", default-features = false } sha2 = { workspace = true } -sigstore-verify = { version = "0.10", default-features = false, optional = true } +sigstore-verify = { version = "0.9", default-features = false, optional = true } axum.workspace = true -axum-server = { version = "0.8", default-features = false, optional = true } clap_complete_nushell = { version = "4", default-features = false } [target.'cfg(target_os = "windows")'.dependencies] @@ -100,18 +99,14 @@ portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel", "tui"] # disables the update command disable-update = [] rustls-tls = [ - "dep:axum-server", "reqwest/rustls", - "axum-server/tls-rustls", "sigstore-verify?/rustls", "goose/rustls-tls", "goose-mcp/rustls-tls", "goose-providers/rustls-tls", ] native-tls = [ - "dep:axum-server", "reqwest/native-tls", - "axum-server/tls-openssl", "sigstore-verify?/native-tls", "goose/native-tls", "goose-mcp/native-tls", diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 427dd80351..1b0a2ffa22 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -51,22 +51,6 @@ fn generate_serve_secret_key() -> String { ) } -#[derive(clap::ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)] -enum ServePlatform { - #[default] - Cli, - Desktop, -} - -impl From for GoosePlatform { - fn from(platform: ServePlatform) -> Self { - match platform { - ServePlatform::Cli => GoosePlatform::GooseCli, - ServePlatform::Desktop => GoosePlatform::GooseDesktop, - } - } -} - #[derive(Parser)] #[command(name = "goose", author, version, display_name = "", about, long_about = None)] pub struct Cli { @@ -408,9 +392,7 @@ async fn get_or_create_session_id( let resolved_id = if resume { let Some(id) = identifier else { - let sessions = session_manager - .list_sessions_by_types(&[SessionType::User]) - .await?; + let sessions = session_manager.list_sessions().await?; let session_id = sessions .first() .map(|s| s.id.clone()) @@ -849,18 +831,6 @@ enum Command { #[arg(long, default_value = "3284")] port: u16, - #[arg(long, help = "Serve ACP over TLS")] - tls: bool, - - #[arg(long = "tls-cert-path", value_name = "PATH")] - tls_cert_path: Option, - - #[arg(long = "tls-key-path", value_name = "PATH")] - tls_key_path: Option, - - #[arg(long, value_enum, default_value_t = ServePlatform::Cli)] - platform: ServePlatform, - #[arg( long = "with-builtin", value_name = "NAME", @@ -870,20 +840,6 @@ enum Command { action = clap::ArgAction::Append )] builtins: Vec, - - #[arg( - long = "dangerously-unauthenticated", - help = "Start the ACP endpoint without requiring GOOSE_SERVER__SECRET_KEY" - )] - dangerously_unauthenticated: bool, - - #[arg( - long = "allowed-origin", - value_name = "ORIGIN", - action = clap::ArgAction::Append, - help = "Allow an exact Origin value for ACP CORS; may be specified multiple times and replaces the default loopback origins" - )] - allowed_origins: Vec, }, /// Start or resume interactive chat sessions @@ -916,15 +872,6 @@ enum Command { )] fork: bool, - /// Open the session's conversation in $EDITOR before starting - #[arg( - long, - requires = "resume", - help = "Edit the session conversation in $EDITOR before starting", - long_help = "Open the session's conversation in your editor ($VISUAL / $EDITOR / vi) for modification before resuming. When combined with --fork, creates a new session from the edited result." - )] - edit: bool, - /// Show message history when resuming #[arg( long, @@ -1377,21 +1324,7 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> { Ok(()) } -struct ServeCommandArgs { - host: String, - - port: u16, - tls: bool, - tls_cert_path: Option, - tls_key_path: Option, - platform: ServePlatform, - builtins: Vec, - dangerously_unauthenticated: bool, - allowed_origins: Vec, -} - -async fn handle_serve_command(args: ServeCommandArgs) -> Result<()> { - use axum::http::HeaderValue; +async fn handle_serve_command(host: String, port: u16, builtins: Vec) -> Result<()> { use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}; use goose::acp::transport::create_router; use goose::config::paths::Paths; @@ -1399,18 +1332,6 @@ async fn handle_serve_command(args: ServeCommandArgs) -> Result<()> { use std::sync::Arc; use tracing::{info, warn}; - let ServeCommandArgs { - host, - port, - tls, - tls_cert_path, - tls_key_path, - platform, - builtins, - dangerously_unauthenticated, - allowed_origins, - } = args; - let builtins = if builtins.is_empty() { vec!["developer".to_string()] } else { @@ -1433,93 +1354,32 @@ async fn handle_serve_command(args: ServeCommandArgs) -> Result<()> { builtins, data_dir: Paths::data_dir(), config_dir: Paths::config_dir(), - goose_platform: platform.into(), + goose_platform: GoosePlatform::GooseCli, additional_source_roots, + scheduler: None, })); let env_secret = std::env::var(GOOSE_SERVER_SECRET_KEY_ENV) .ok() .map(|secret| secret.trim().to_string()) .filter(|secret| !secret.is_empty()); let require_token = env_secret.is_some(); - if !require_token && !dangerously_unauthenticated { - anyhow::bail!( - "{GOOSE_SERVER_SECRET_KEY_ENV} must be set to start `goose serve`; pass --dangerously-unauthenticated to run without ACP authentication" - ); - } - if dangerously_unauthenticated && !require_token { + if !require_token { warn!( - "{GOOSE_SERVER_SECRET_KEY_ENV} is not set and --dangerously-unauthenticated was passed; the ACP endpoint will accept unauthenticated connections" + "{GOOSE_SERVER_SECRET_KEY_ENV} is not set; the ACP endpoint will accept unauthenticated connections" ); } - let additional_allowed_origins = allowed_origins - .into_iter() - .map(|origin| { - let origin = origin.trim(); - if origin.is_empty() || origin == "*" { - anyhow::bail!("--allowed-origin must be a non-wildcard Origin value"); - } - HeaderValue::from_str(origin).map_err(|error| { - anyhow::anyhow!("invalid --allowed-origin value `{origin}`: {error}") - }) - }) - .collect::>>()?; let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key); - let router = create_router( - server, - secret_key, - require_token, - additional_allowed_origins, - ); - - let config = Config::global(); - let tls_cert_path = - tls_cert_path.or_else(|| config.get_param::("GOOSE_TLS_CERT_PATH").ok()); - let tls_key_path = - tls_key_path.or_else(|| config.get_param::("GOOSE_TLS_KEY_PATH").ok()); - let tls = tls - || config.get_param::("GOOSE_TLS").unwrap_or(false) - || tls_cert_path.is_some() - || tls_key_path.is_some(); + let router = create_router(server, secret_key, require_token); let addr: SocketAddr = format!("{}:{}", host, port).parse()?; - if tls { - #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] - { - let tls_setup = goose::acp::transport::tls::setup_tls( - tls_cert_path.as_deref(), - tls_key_path.as_deref(), - ) - .await?; - info!("Starting ACP server on https://{}", addr); + info!("Starting ACP server on {}", addr); - #[cfg(feature = "rustls-tls")] - axum_server::bind_rustls(addr, tls_setup.config) - .serve(router.into_make_service_with_connect_info::()) - .await?; - - #[cfg(feature = "native-tls")] - axum_server::bind_openssl(addr, tls_setup.config) - .serve(router.into_make_service_with_connect_info::()) - .await?; - } - - #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] - { - let _ = (tls_cert_path, tls_key_path); - anyhow::bail!( - "TLS was requested but no TLS backend is enabled. \ - Enable the `rustls-tls` or `native-tls` feature." - ); - } - } else { - info!("Starting ACP server on http://{}", addr); - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve( - listener, - router.into_make_service_with_connect_info::(), - ) - .await?; - } + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await?; Ok(()) } @@ -1604,7 +1464,6 @@ async fn handle_interactive_session( identifier: Option, resume: bool, fork: bool, - edit: bool, history: bool, session_opts: SessionOptions, extension_opts: ExtensionOptions, @@ -1644,31 +1503,12 @@ async fn handle_interactive_session( let goose_mode = Config::global().get_goose_mode().unwrap_or_default(); let mut session_id = get_or_create_session_id(identifier, resume, false, goose_mode).await?; - if edit || fork { - if let Some(ref id) = session_id { + if fork { + if let Some(id) = session_id { let session_manager = SessionManager::instance(); - let original = session_manager.get_session(id, true).await?; - - let target_id = if fork { - let copied = session_manager - .copy_session(id, original.name.clone()) - .await?; - let copied_id = copied.id.clone(); - session_id = Some(copied.id); - copied_id - } else { - id.clone() - }; - - if edit { - let conversation = original - .conversation - .ok_or_else(|| anyhow::anyhow!("session has no messages to edit"))?; - let edited = crate::session::editor::edit_conversation(&conversation)?; - session_manager - .replace_conversation(&target_id, &edited) - .await?; - } + let original = session_manager.get_session(&id, false).await?; + let copied = session_manager.copy_session(&id, original.name).await?; + session_id = Some(copied.id); } } @@ -2039,8 +1879,6 @@ 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; - goose::providers::local_inference::configure_huggingface_auth(); - match command { LocalModelsCommand::Search { query, limit } => { println!("Searching HuggingFace for '{}'...", query); @@ -2232,27 +2070,8 @@ pub async fn cli() -> anyhow::Result<()> { Some(Command::Serve { host, port, - tls, - tls_cert_path, - tls_key_path, - platform, builtins, - dangerously_unauthenticated, - allowed_origins, - }) => { - handle_serve_command(ServeCommandArgs { - host, - port, - tls, - tls_cert_path, - tls_key_path, - platform, - builtins, - dangerously_unauthenticated, - allowed_origins, - }) - .await - } + }) => handle_serve_command(host, port, builtins).await, Some(Command::Session { command: Some(cmd), .. }) => handle_session_subcommand(cmd).await, @@ -2261,7 +2080,6 @@ pub async fn cli() -> anyhow::Result<()> { identifier, resume, fork, - edit, history, session_opts, extension_opts, @@ -2270,7 +2088,6 @@ pub async fn cli() -> anyhow::Result<()> { identifier, resume, fork, - edit, history, session_opts, extension_opts, @@ -2449,35 +2266,6 @@ mod tests { } } - #[test] - fn serve_command_accepts_dangerously_unauthenticated_flag() { - let cli = Cli::try_parse_from([ - "goose", - "serve", - "--dangerously-unauthenticated", - "--allowed-origin", - "app://localhost", - "--allowed-origin", - "https://app.example", - ]) - .expect("parse failed"); - - match cli.command { - Some(Command::Serve { - dangerously_unauthenticated, - allowed_origins, - .. - }) => { - assert!(dangerously_unauthenticated); - assert_eq!( - allowed_origins, - vec!["app://localhost", "https://app.example"] - ); - } - _ => panic!("expected serve command"), - } - } - #[test] fn review_command_accepts_options() { let cli = Cli::try_parse_from([ diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 96f1574b65..bb51673d1a 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -28,22 +28,11 @@ use goose::session::SessionType; use goose_providers::thinking::ThinkingEffort; use serde_json::Value; use std::collections::HashMap; -use std::io::{IsTerminal, Write}; +use std::io::IsTerminal; // useful for light themes where there is no discernible colour contrast between // cursor-selected and cursor-unselected items. const MULTISELECT_VISIBILITY_HINT: &str = "<"; -const SHOW_CURSOR: &[u8] = b"\x1b[?25h"; - -struct CursorRestoreGuard; - -impl Drop for CursorRestoreGuard { - fn drop(&mut self) { - let mut stdout = std::io::stdout().lock(); - let _ = stdout.write_all(SHOW_CURSOR); - let _ = stdout.flush(); - } -} pub async fn handle_configure() -> anyhow::Result<()> { if !std::io::stdin().is_terminal() { @@ -53,7 +42,6 @@ pub async fn handle_configure() -> anyhow::Result<()> { ); } - let _cursor_restore = CursorRestoreGuard; let config = Config::global(); if !config.exists() { diff --git a/crates/goose-cli/src/commands/project.rs b/crates/goose-cli/src/commands/project.rs index 43fd67d60d..c0a55dedac 100644 --- a/crates/goose-cli/src/commands/project.rs +++ b/crates/goose-cli/src/commands/project.rs @@ -36,7 +36,7 @@ pub fn handle_project_default() -> Result<()> { } // Sort projects by last_accessed (newest first) - projects.sort_by_key(|project| std::cmp::Reverse(project.last_accessed)); + projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed)); // Get the most recent project let project = &projects[0]; @@ -178,7 +178,7 @@ pub fn handle_projects_interactive() -> Result<()> { } // Sort projects by last_accessed (newest first) - projects.sort_by_key(|project| std::cmp::Reverse(project.last_accessed)); + projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed)); // Format project paths for display let project_choices: Vec<(String, String)> = projects diff --git a/crates/goose-cli/src/commands/tui.rs b/crates/goose-cli/src/commands/tui.rs index 3b49a633f0..a0ebc3311c 100644 --- a/crates/goose-cli/src/commands/tui.rs +++ b/crates/goose-cli/src/commands/tui.rs @@ -14,10 +14,6 @@ enum TuiSource { fn find_local_script() -> Option { let exe = std::env::current_exe().ok()?; - find_local_script_from(&exe) -} - -fn find_local_script_from(exe: &Path) -> Option { let exe_dir = exe.parent().unwrap_or_else(|| Path::new(".")); let mut dir = Some(exe_dir.to_path_buf()); @@ -31,6 +27,13 @@ fn find_local_script_from(exe: &Path) -> Option { } } + if let Ok(cwd) = std::env::current_dir() { + let candidate = cwd.join(TUI_REL_PATH); + if candidate.is_file() { + return Some(candidate); + } + } + None } @@ -94,34 +97,3 @@ pub fn handle_tui(args: Vec) -> Result<()> { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - - #[test] - fn find_local_script_ignores_unrelated_directories() { - let temp_dir = tempfile::tempdir().expect("create temp dir"); - let executable = temp_dir.path().join("install/bin/goose"); - let planted_script = temp_dir.path().join("checkout").join(TUI_REL_PATH); - fs::create_dir_all(planted_script.parent().unwrap()).expect("create script directory"); - fs::write(&planted_script, "process.exit(0)\n").expect("write planted script"); - - assert_eq!(find_local_script_from(&executable), None); - } - - #[test] - fn find_local_script_accepts_executable_ancestor() { - let temp_dir = tempfile::tempdir().expect("create temp dir"); - let executable = temp_dir.path().join("target/debug/goose"); - let bundled_script = temp_dir.path().join(TUI_REL_PATH); - fs::create_dir_all(bundled_script.parent().unwrap()).expect("create script directory"); - fs::write(&bundled_script, "process.exit(0)\n").expect("write bundled script"); - - assert_eq!( - find_local_script_from(&executable).as_deref(), - Some(bundled_script.as_path()) - ); - } -} diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index 7c600a92c3..04272d2059 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -1,8 +1,4 @@ use anyhow::{bail, Context, Result}; -use reqwest::{ - header::{HeaderValue, AUTHORIZATION}, - StatusCode, -}; use sha2::{Digest, Sha256}; use sigstore_verify::trust_root::{TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; use sigstore_verify::types::{Bundle, Sha256Hash}; @@ -83,32 +79,6 @@ struct AttestationEntry { const GITHUB_ACTIONS_ISSUER: &str = "https://token.actions.githubusercontent.com"; -fn sanitized_token(token: Option<&str>) -> Option<&str> { - token.map(str::trim).filter(|tok| !tok.is_empty()) -} - -fn authorization_header_value(token: &str) -> Option { - HeaderValue::from_str(&format!("Bearer {token}")).ok() -} - -fn github_token() -> Option { - env::var("GITHUB_TOKEN") - .ok() - .and_then(|tok| sanitized_token(Some(&tok)).map(str::to_owned)) - .or_else(|| { - env::var("GH_TOKEN") - .ok() - .and_then(|tok| sanitized_token(Some(&tok)).map(str::to_owned)) - }) -} - -fn should_retry_attestations_without_token(status: StatusCode, token: Option<&str>) -> bool { - sanitized_token(token) - .and_then(authorization_header_value) - .is_some() - && matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) -} - async fn fetch_attestations(digest: &str, token: Option<&str>) -> Result> { let url = format!( "https://api.github.com/repos/aaif-goose/goose/attestations/sha256:{digest}\ @@ -116,14 +86,17 @@ async fn fetch_attestations(digest: &str, token: Option<&str>) -> Result) -> Result, -) -> Result { - let mut req = client - .get(url) - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", "2022-11-28") - .header("User-Agent", "goose-cli"); - - if let Some(value) = token.and_then(authorization_header_value) { - req = req.header(AUTHORIZATION, value); - } - - req.send().await.context("Failed to fetch attestations") -} - // Verify a single attestation bundle against the artifact digest and workflow. fn verify_bundle( bundle_json: &serde_json::Value, @@ -187,8 +142,8 @@ fn verify_bundle( Ok(()) } -/// Returns `Ok(())` when the downloaded archive has verified provenance. -async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result<()> { +/// Returns `Ok(true)` verified, `Ok(false)` skipped (soft warning), `Err` hard failure. +async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result { let digest = sha256_hex(archive_data); println!("Archive SHA-256: {digest}"); @@ -197,19 +152,30 @@ async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result<()> { _ => "release.yml", }; - let token = github_token(); + let token = env::var("GITHUB_TOKEN") + .ok() + .or_else(|| env::var("GH_TOKEN").ok()); println!("Verifying SLSA provenance via Sigstore..."); - let bundles = fetch_attestations(&digest, token.as_deref()) - .await - .context( - "Sigstore provenance check could not complete; refusing to install unverifiable update", - )?; - - if bundles.is_empty() { - bail!("No Sigstore attestation found for downloaded archive; refusing to install unverifiable update"); - } + let bundles = match fetch_attestations(&digest, token.as_deref()).await { + Ok(b) if b.is_empty() => { + eprintln!( + "Warning: No Sigstore attestation found for this build. \ + This may be expected for canary or nightly builds." + ); + return Ok(false); + } + Ok(b) => b, + Err(e) => { + eprintln!( + "Warning: Sigstore provenance check could not complete: {e}\n\ + This may be expected for releases published before provenance \ + attestations were enabled." + ); + return Ok(false); + } + }; let trusted_root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT) .context("Failed to load Sigstore trusted root")?; @@ -229,7 +195,7 @@ async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result<()> { ) { Ok(()) => { println!("Sigstore provenance verification passed."); - return Ok(()); + return Ok(true); } Err(e) => last_err = Some(e), } @@ -281,7 +247,7 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { println!("Downloaded {} bytes.", bytes.len()); // --- Verify SLSA provenance via Sigstore -------------------------------- - verify_provenance(&bytes, tag).await?; + let provenance_verified = verify_provenance(&bytes, tag).await?; // --- Extract to temp dir (hardened against path traversal) -------------- let tmp_dir = tempfile::tempdir().context("Failed to create temp directory")?; @@ -308,7 +274,11 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { #[cfg(target_os = "windows")] copy_dlls(&extracted_binary, ¤t_exe)?; - println!("goose updated successfully (verified with Sigstore SLSA provenance)."); + if provenance_verified { + println!("goose updated successfully (verified with Sigstore SLSA provenance)."); + } else { + println!("goose updated successfully."); + } // --- Reconfigure if requested ------------------------------------------- if reconfigure { @@ -767,48 +737,6 @@ mod tests { ); } - #[test] - fn test_sanitized_token_trims_blank_values() { - assert_eq!(sanitized_token(None), None); - assert_eq!(sanitized_token(Some("")), None); - assert_eq!(sanitized_token(Some(" ")), None); - assert_eq!(sanitized_token(Some(" token\n")), Some("token")); - } - - #[test] - fn test_authorization_header_value_rejects_malformed_tokens() { - assert!(authorization_header_value("token").is_some()); - assert!(authorization_header_value("bad\ntoken").is_none()); - } - - #[test] - fn test_attestation_lookup_retries_auth_failures_without_token() { - assert!(should_retry_attestations_without_token( - StatusCode::UNAUTHORIZED, - Some("token") - )); - assert!(should_retry_attestations_without_token( - StatusCode::FORBIDDEN, - Some("token") - )); - assert!(!should_retry_attestations_without_token( - StatusCode::INTERNAL_SERVER_ERROR, - Some("token") - )); - assert!(!should_retry_attestations_without_token( - StatusCode::UNAUTHORIZED, - Some("") - )); - assert!(!should_retry_attestations_without_token( - StatusCode::UNAUTHORIZED, - Some("bad\ntoken") - )); - assert!(!should_retry_attestations_without_token( - StatusCode::UNAUTHORIZED, - None - )); - } - // ----------------------------------------------------------------------- // Path validation and extraction hardening tests // ----------------------------------------------------------------------- @@ -880,11 +808,13 @@ mod tests { // ----------------------------------------------------------------------- #[tokio::test] - async fn test_verify_provenance_fails_closed_when_unverifiable() { + async fn test_verify_provenance_warns_on_missing_attestation() { let result = verify_provenance(b"not a real archive", "stable").await; - assert!( - result.is_err(), - "verify_provenance must fail closed when provenance cannot be verified" + // Network failures and missing attestations are soft warnings: Ok(false), not hard errors. + assert_eq!( + result.ok(), + Some(false), + "verify_provenance should return Ok(false) when attestations cannot be fetched" ); } diff --git a/crates/goose-cli/src/lib.rs b/crates/goose-cli/src/lib.rs index 285144c468..be821a2a29 100644 --- a/crates/goose-cli/src/lib.rs +++ b/crates/goose-cli/src/lib.rs @@ -1,5 +1,3 @@ -#![recursion_limit = "256"] - #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] compile_error!("At least one of `rustls-tls` or `native-tls` features must be enabled"); diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index b5b3c19d5b..afecc21c82 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -1,5 +1,3 @@ -#![recursion_limit = "256"] - use anyhow::Result; use goose_cli::cli::cli; diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index de228d6e99..2823d374f8 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -333,10 +333,7 @@ async fn resolve_session_id( } } } else { - match session_manager - .list_sessions_by_types(&[SessionType::User]) - .await - { + match session_manager.list_sessions().await { Ok(sessions) if !sessions.is_empty() => sessions[0].id.clone(), _ => { output::render_error("Cannot resume - no previous sessions found"); @@ -698,8 +695,6 @@ fn is_provider_unavailable_error(e: &anyhow::Error) -> bool { #[cfg(test)] mod tests { use super::*; - use goose::session::SessionManager; - use tempfile::TempDir; #[test] fn test_session_builder_config_creation() { @@ -763,44 +758,6 @@ mod tests { assert!(!config.fork); } - #[tokio::test] - async fn test_implicit_resume_ignores_newer_scheduled_sessions() { - let temp_dir = TempDir::new().unwrap(); - let session_manager = SessionManager::new(temp_dir.path().to_path_buf()); - let goose_mode = GooseMode::default(); - - let user_session = session_manager - .create_session( - temp_dir.path().to_path_buf(), - "User session".to_string(), - SessionType::User, - goose_mode, - ) - .await - .unwrap(); - session_manager - .create_session( - temp_dir.path().to_path_buf(), - "Scheduled job: test".to_string(), - SessionType::Scheduled, - goose_mode, - ) - .await - .unwrap(); - - let resolved = resolve_session_id( - &SessionBuilderConfig { - resume: true, - ..SessionBuilderConfig::default() - }, - &session_manager, - goose_mode, - ) - .await; - - assert_eq!(resolved, user_session.id); - } - #[test] fn test_truncate_with_ellipsis() { assert_eq!(truncate_with_ellipsis("abc", 5), "abc"); diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index 410acd9356..a96ce75405 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -1,4 +1,3 @@ -use goose::agents::execute_commands::list_commands; use goose::config::GooseMode; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::highlight::{CmdKind, Highlighter}; @@ -37,8 +36,8 @@ impl GooseCompleter { // Create completion candidates that match the prefix let candidates: Vec = cache .prompts - .values() - .flatten() + .iter() + .flat_map(|(_, names)| names) .filter(|name| name.starts_with(prefix.trim())) .map(|name| Pair { display: name.clone(), @@ -153,25 +152,23 @@ impl GooseCompleter { /// Complete slash commands fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec)> { - let mut commands = vec![ - "/exit".to_string(), - "/quit".to_string(), - "/help".to_string(), - "/?".to_string(), - "/t".to_string(), - "/extension".to_string(), - "/builtin".to_string(), - "/mode".to_string(), - "/model".to_string(), - "/recipe".to_string(), + // Define available slash commands + let commands = [ + "/exit", + "/quit", + "/help", + "/?", + "/t", + "/extension", + "/builtin", + "/prompts", + "/prompt", + "/mode", + "/model", + "/recipe", + "/skills", + "/status", ]; - commands.extend( - list_commands() - .iter() - .map(|command| format!("/{}", command.name)), - ); - commands.sort(); - commands.dedup(); // Find commands that match the prefix let matching_commands: Vec = commands @@ -581,15 +578,6 @@ mod tests { let (pos, candidates) = completer.complete_slash_commands("/").unwrap(); assert_eq!(pos, 0); assert!(candidates.len() > 1); - for command in list_commands() { - assert!( - candidates - .iter() - .any(|candidate| candidate.display == format!("/{}", command.name)), - "slash completion should list /{}", - command.name - ); - } // Test no match let (_pos, candidates) = completer.complete_slash_commands("/nonexistent").unwrap(); diff --git a/crates/goose-cli/src/session/editor.rs b/crates/goose-cli/src/session/editor.rs index 6ed3e297cc..1e42d6069a 100644 --- a/crates/goose-cli/src/session/editor.rs +++ b/crates/goose-cli/src/session/editor.rs @@ -1,10 +1,7 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use goose::config::Config; -use goose::conversation::message::Message; -use goose::conversation::Conversation; use std::fs; use std::io::Read; -use std::io::Write; use std::path::PathBuf; use std::process::Command; use tempfile::Builder; @@ -24,6 +21,9 @@ pub fn resolve_editor_command() -> Option { ) } +/// Inner resolution logic, separated for testability. +/// Checks sources in priority order: config, VISUAL, EDITOR. +/// Skips empty strings at each level. fn resolve_editor_from_sources( config_editor: Option<&str>, visual: Option<&str>, @@ -37,57 +37,6 @@ fn resolve_editor_from_sources( None } -/// Resolve the editor command, falling back to vi (or notepad on Windows). -pub fn resolve_editor_or_default() -> String { - let config = Config::global(); - let config_editor = config.get_goose_prompt_editor().ok().flatten(); - let visual = std::env::var("VISUAL").ok(); - let editor_env = std::env::var("EDITOR").ok(); - resolve_editor_or_default_from_sources( - config_editor.as_deref(), - visual.as_deref(), - editor_env.as_deref(), - ) -} - -fn resolve_editor_default() -> String { - if cfg!(windows) { - "notepad".to_string() - } else { - "vi".to_string() - } -} - -fn resolve_editor_or_default_from_sources( - config_editor: Option<&str>, - visual: Option<&str>, - editor_env: Option<&str>, -) -> String { - resolve_editor_from_sources(config_editor, visual, editor_env) - .unwrap_or_else(resolve_editor_default) -} - -/// Open a YAML temp file with the user's editor to edit a conversation. -/// Returns the edited conversation, or an error if the editor failed or YAML was invalid. -pub fn edit_conversation(conversation: &Conversation) -> Result { - let yaml = serde_yaml::to_string(conversation.messages())?; - - let mut tmp = NamedTempFile::with_suffix(".yaml")?; - tmp.write_all(yaml.as_bytes())?; - tmp.flush()?; - - let editor = resolve_editor_or_default(); - let path = tmp.path().to_path_buf(); - - launch_editor(&editor, &path).with_context(|| format!("failed to launch editor '{editor}'"))?; - - let edited = std::fs::read_to_string(&path)?; - let messages: Vec = - serde_yaml::from_str(&edited).context("invalid YAML โ€” session unchanged")?; - - Ok(Conversation::new_unvalidated(messages)) -} - /// Build the markdown template content for the editor prompt. fn build_template(messages: &[&str], prefill: Option<&str>) -> String { let mut content = String::from("# Goose Prompt Editor\n\n"); @@ -135,36 +84,21 @@ impl SymlinkCleanup { impl Drop for SymlinkCleanup { fn drop(&mut self) { + // Always try to clean up the symlink, ignoring any errors let _ = std::fs::remove_file(&self.symlink_path); } } -/// Split an editor command into program and arguments. -/// -/// Uses shell-word splitting only when the command contains quotes, so values like -/// `"/Applications/Sublime Text.app/.../subl" -w` work. Unquoted commands are split on -/// whitespace to avoid shlex stripping backslashes from Windows paths like -/// `C:\Windows\System32\notepad.exe`. -fn split_editor_command(editor_cmd: &str) -> Result> { - if editor_cmd.contains(['"', '\'']) { - shlex::split(editor_cmd).ok_or_else(|| { - anyhow::anyhow!("Invalid editor command: unmatched quotes in '{editor_cmd}'") - }) - } else { - Ok(editor_cmd.split_whitespace().map(String::from).collect()) - } -} - /// Launch editor and wait for completion fn launch_editor(editor_cmd: &str, file_path: &PathBuf) -> Result<()> { use std::process::Stdio; - let parts = split_editor_command(editor_cmd)?; + let parts: Vec<&str> = editor_cmd.split_whitespace().collect(); if parts.is_empty() { return Err(anyhow::anyhow!("Empty editor command")); } - let mut cmd = Command::new(&parts[0]); + let mut cmd = Command::new(parts[0]); if let Ok(cwd) = std::env::current_dir() { cmd.current_dir(cwd); } @@ -480,64 +414,54 @@ with multiple lines. ); } + // --- resolve_editor_from_sources tests --- + #[test] - fn test_resolve_editor_resolution_priority() { - assert_eq!( - resolve_editor_from_sources(Some("config-val"), Some("visual-val"), Some("editor-val")), - Some("config-val".to_string()) - ); - - assert_eq!( - resolve_editor_from_sources(Some(""), Some("visual-val"), Some("editor-val")), - Some("visual-val".to_string()) - ); - - assert_eq!( - resolve_editor_from_sources(None, Some(""), Some("editor-val")), - Some("editor-val".to_string()) - ); - - assert_eq!(resolve_editor_from_sources(None, None, None), None); - assert_eq!( - resolve_editor_from_sources(Some(""), Some(""), Some("")), - None - ); - - let default_val = resolve_editor_default(); - assert_eq!( - resolve_editor_or_default_from_sources(None, None, None), - default_val - ); - assert_eq!( - resolve_editor_or_default_from_sources(Some(""), Some(""), Some("")), - default_val - ); + fn test_resolve_editor_returns_config_when_set() { + let result = resolve_editor_from_sources(Some("code"), Some("vim"), Some("nano")); + assert_eq!(result.as_deref(), Some("code")); } #[test] - fn test_split_editor_command() { - assert_eq!( - split_editor_command("code --wait").unwrap(), - vec!["code", "--wait"] - ); + fn test_resolve_editor_falls_back_to_visual() { + let result = resolve_editor_from_sources(None, Some("vim"), Some("nano")); + assert_eq!(result.as_deref(), Some("vim")); + } - assert_eq!( - split_editor_command( - r#""/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w"# - ) - .unwrap(), - vec![ - "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", - "-w" - ] - ); + #[test] + fn test_resolve_editor_falls_back_to_editor_env() { + let result = resolve_editor_from_sources(None, None, Some("nano")); + assert_eq!(result.as_deref(), Some("nano")); + } - assert_eq!( - split_editor_command(r"C:\Windows\System32\notepad.exe").unwrap(), - vec![r"C:\Windows\System32\notepad.exe"] - ); + #[test] + fn test_resolve_editor_returns_none_when_nothing_set() { + let result = resolve_editor_from_sources(None, None, None); + assert_eq!(result, None); + } - assert!(split_editor_command(r#"code --wait "unclosed"#).is_err()); + #[test] + fn test_resolve_editor_skips_empty_config() { + let result = resolve_editor_from_sources(Some(""), Some("vim"), None); + assert_eq!(result.as_deref(), Some("vim")); + } + + #[test] + fn test_resolve_editor_skips_empty_visual() { + let result = resolve_editor_from_sources(None, Some(""), Some("nano")); + assert_eq!(result.as_deref(), Some("nano")); + } + + #[test] + fn test_resolve_editor_skips_all_empty() { + let result = resolve_editor_from_sources(Some(""), Some(""), Some("")); + assert_eq!(result, None); + } + + #[test] + fn test_resolve_editor_skips_empty_config_and_visual() { + let result = resolve_editor_from_sources(Some(""), Some(""), Some("emacs")); + assert_eq!(result.as_deref(), Some("emacs")); } // --- build_template edge case tests --- @@ -545,7 +469,9 @@ with multiple lines. #[test] fn test_build_template_empty_prefill_string() { let content = build_template(&["## User: Hello"], Some("")); + // Empty prefill should not appear in content assert!(content.contains("# Your prompt:\n\n#")); + // Should go directly to conversation context assert!(content.contains("# Recent conversation for context")); } @@ -572,8 +498,11 @@ with multiple lines. assert!(prefill_pos < context_pos); } + // --- extract_user_input with prefilled content tests --- + #[test] fn test_extract_user_input_with_prefill_kept() { + // Simulates a user who opened the editor with prefill and kept it unchanged let content = build_template(&["## User: Hello"], Some("fix the login bug")); let result = extract_user_input(&content); assert_eq!(result, "fix the login bug"); @@ -581,6 +510,7 @@ with multiple lines. #[test] fn test_extract_user_input_with_prefill_edited() { + // Simulates a user who edited the prefill text let mut content = build_template(&["## User: Hello"], Some("fix the login bug")); content = content.replace( "fix the login bug", @@ -592,6 +522,7 @@ with multiple lines. #[test] fn test_extract_user_input_prefill_replaced() { + // Simulates a user who deleted the prefill and wrote something new let mut content = build_template(&["## User: Hello"], Some("fix the login bug")); content = content.replace("fix the login bug\n", "completely different prompt\n"); let result = extract_user_input(&content); @@ -600,6 +531,7 @@ with multiple lines. #[test] fn test_extract_user_input_prefill_cleared() { + // Simulates a user who deleted the prefill and left nothing let mut content = build_template(&["## User: Hello"], Some("fix the login bug")); content = content.replace("fix the login bug\n", ""); let result = extract_user_input(&content); diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index e7e14f9802..2bd8cafe2f 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -150,7 +150,7 @@ pub fn get_input( rustyline::EventHandler::Conditional(Box::new(CtrlCHandler::new(completion_cache))), ); - let input = match editor.readline("โ€บ ") { + let input = match editor.readline("> ") { Ok(text) => text, Err(e) => match e { rustyline::error::ReadlineError::Interrupted => return Ok(InputResult::Exit), @@ -401,17 +401,10 @@ fn parse_plan_command(input: String) -> Option { Some(InputResult::Plan(options)) } -fn help_text() -> String { +fn print_help() { let newline_key = get_newline_key().to_ascii_uppercase(); let modes = GooseMode::VARIANTS.join(", "); - let additional_builtin_help = additional_builtin_help(); - let additional_builtin_help = if additional_builtin_help.is_empty() { - String::new() - } else { - format!("{additional_builtin_help}\n") - }; - - format!( + println!( "Available commands: /exit or /quit - Exit the session /t - Toggle Light/Dark/Ansi theme @@ -432,7 +425,7 @@ fn help_text() -> String { /recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). If no filepath is provided, it will be saved to ./recipe.yaml. /compact - Compact the current conversation to reduce context length while preserving key information. -{additional_builtin_help}/status - Show session status: model, provider, mode, and token usage. +/status - Show session status: model, provider, mode, and token usage. /edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text. Uses $GOOSE_PROMPT_EDITOR, $VISUAL, or $EDITOR (in that order). /skills - List available skills or enable skills by name (usage: /skills [...]) @@ -443,23 +436,7 @@ Navigation: Ctrl+C - Clear current line if text is entered, otherwise exit the session Ctrl+{newline_key} - Add a newline (configurable via GOOSE_CLI_NEWLINE_KEY) Up/Down arrows - Navigate through command history" - ) -} - -fn additional_builtin_help() -> String { - const DOCUMENTED_BUILTINS: &[&str] = - &["prompts", "prompt", "compact", "clear", "skills", "status"]; - - goose::agents::execute_commands::list_commands() - .iter() - .filter(|command| !DOCUMENTED_BUILTINS.contains(&command.name)) - .map(|command| format!("/{} - {}", command.name, command.description)) - .collect::>() - .join("\n") -} - -fn print_help() { - println!("{}", help_text()); + ); } /// Extract recent messages for editor context @@ -558,19 +535,6 @@ mod tests { assert!(handle_slash_command("/unknown").is_none()); } - #[test] - fn help_lists_builtin_agent_commands() { - let help = help_text(); - - for command in goose::agents::execute_commands::list_commands() { - assert!( - help.contains(&format!("/{}", command.name)), - "help output should list /{}", - command.name - ); - } - } - #[test] fn test_prompts_command() { // Test basic prompts command diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 1c02ececb9..73dfe72723 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -1,6 +1,6 @@ mod builder; mod completion; -pub mod editor; +mod editor; mod elicitation; mod export; mod input; @@ -53,7 +53,7 @@ use std::collections::{HashMap, HashSet}; use std::io::IsTerminal; use std::path::PathBuf; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Instant; use tokio; use tokio_util::sync::CancellationToken; use tracing::warn; @@ -487,6 +487,10 @@ impl CliSession { /// Start an interactive session, optionally with an initial message pub async fn interactive(&mut self, prompt: Option) -> Result<()> { + self.agent + .emit_hook(goose::hooks::HookEvent::SessionStart, &self.session_id) + .await; + let result = self.run_interactive(prompt).await; self.agent @@ -1135,6 +1139,9 @@ impl CliSession { /// Process a single message and exit pub async fn headless(&mut self, prompt: String) -> Result<()> { + self.agent + .emit_hook(goose::hooks::HookEvent::SessionStart, &self.session_id) + .await; let message = Message::user().with_text(&prompt); let result = self .process_message(message, CancellationToken::default(), false) @@ -1326,7 +1333,6 @@ impl CliSession { Some(Ok(AgentEvent::Usage(usage))) => { last_usage = Some(usage); } - Some(Ok(AgentEvent::MessageUsage { .. })) => {} Some(Ok(AgentEvent::McpNotification((extension_id, notification)))) => { handle_mcp_notification( &extension_id, @@ -1769,54 +1775,25 @@ fn print_run_stats( usage: Option<&ProviderUsage>, ) { let elapsed = run_started.elapsed(); - let stats = usage.and_then(|usage| usage.stats.as_ref()); - let generation_elapsed = stats - .and_then(|stats| stats.elapsed_ms) - .map(Duration::from_millis); let output_tokens = usage .and_then(|usage| usage.usage.output_tokens) .and_then(|tokens| usize::try_from(tokens).ok()) - .or_else(|| stats.and_then(|stats| stats.output_tokens)); + .or_else(|| usage.and_then(|usage| usage.stats.as_ref()?.output_tokens)); let tokens_per_second = output_tokens.map(|tokens| { - let rate_elapsed = generation_elapsed.unwrap_or(elapsed); - if rate_elapsed.as_secs_f64() > 0.0 { - tokens as f64 / rate_elapsed.as_secs_f64() + if elapsed.as_secs_f64() > 0.0 { + tokens as f64 / elapsed.as_secs_f64() } else { 0.0 } }); - let model_load_ms = stats.and_then(|stats| stats.model_load_ms); - let generation_time_to_first_token_ms = stats.and_then(|stats| stats.time_to_first_token_ms); eprintln!("\nStats:"); - if let Some(ms) = model_load_ms { - eprintln!(" Model load: {:.2}s", ms as f64 / 1000.0); - } - if model_load_ms.is_some() { - match generation_time_to_first_token_ms { - Some(ms) => eprintln!( - " Generation time to first token: {:.2}s", - ms as f64 / 1000.0 - ), - None => eprintln!(" Generation time to first token: unavailable"), - } - match first_token_at { - Some(first) => eprintln!( - " End-to-end time to first token: {:.2}s", - first.duration_since(run_started).as_secs_f64() - ), - None => eprintln!(" End-to-end time to first token: unavailable"), - } - } else if let Some(ms) = generation_time_to_first_token_ms { - eprintln!(" Time to first token: {:.2}s", ms as f64 / 1000.0); - } else { - match first_token_at { - Some(first) => eprintln!( - " Time to first token: {:.2}s", - first.duration_since(run_started).as_secs_f64() - ), - None => eprintln!(" Time to first token: unavailable"), - } + match first_token_at { + Some(first) => eprintln!( + " Time to first token: {:.2}s", + first.duration_since(run_started).as_secs_f64() + ), + None => eprintln!(" Time to first token: unavailable"), } match tokens_per_second { Some(rate) => eprintln!(" Tokens/sec: {:.2}", rate), @@ -1826,7 +1803,10 @@ fn print_run_stats( eprintln!(" Output tokens: {tokens}"); } - if let Some(draft) = stats.and_then(|stats| stats.draft.as_ref()) { + if let Some(draft) = usage + .and_then(|usage| usage.stats.as_ref()) + .and_then(|stats| stats.draft.as_ref()) + { eprintln!(" Draft accept rate: {:.1}%", draft.accept_rate * 100.0); eprintln!( " Draft tokens: {} accepted: {} target verified: {} rounds: {}", diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index fc0f253bca..7d1b56ea54 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -1,6 +1,6 @@ use anstream::println; use bat::WrappingMode; -use console::{measure_text_width, style, Color, StyledObject, Term}; +use console::{measure_text_width, style, Color, Term}; use goose::config::Config; use goose::conversation::message::{ ActionRequiredData, Message, MessageContent, SystemNotificationContent, SystemNotificationType, @@ -16,7 +16,6 @@ use rmcp::model::{CallToolRequestParams, JsonObject, PromptArgument}; use serde_json::Value; use std::cell::RefCell; use std::collections::HashMap; -use std::fmt::Display; use std::io::{Error, IsTerminal, Write}; use std::path::Path; use std::time::Duration; @@ -27,22 +26,6 @@ pub const DEFAULT_MIN_PRIORITY: f32 = 0.0; pub const DEFAULT_CLI_LIGHT_THEME: &str = "GitHub"; pub const DEFAULT_CLI_DARK_THEME: &str = "zenburn"; -fn accent(value: T) -> StyledObject { - style(value).cyan() -} - -fn success(value: T) -> StyledObject { - style(value).green() -} - -fn warning(value: T) -> StyledObject { - style(value).yellow() -} - -fn danger(value: T) -> StyledObject { - style(value).red() -} - // Re-export theme for use in main #[derive(Clone, Copy)] pub enum Theme { @@ -265,14 +248,13 @@ pub fn render_message(message: &Message, debug: bool) { } MessageContent::SystemNotification(notification) => { match notification.notification_type { - SystemNotificationType::ThinkingMessage - | SystemNotificationType::ProgressMessage => { + SystemNotificationType::ThinkingMessage => { show_thinking(); set_thinking_message(¬ification.msg); } SystemNotificationType::InlineMessage => { hide_thinking(); - println!("\n{} {}", style("ยท").dim(), ¬ification.msg); + println!("\n{}", style(¬ification.msg).yellow()); } SystemNotificationType::CreditsExhausted => { render_credits_exhausted_notification(notification); @@ -348,15 +330,14 @@ pub fn render_message_streaming( } MessageContent::SystemNotification(notification) => { match notification.notification_type { - SystemNotificationType::ThinkingMessage - | SystemNotificationType::ProgressMessage => { + SystemNotificationType::ThinkingMessage => { show_thinking(); set_thinking_message(¬ification.msg); } SystemNotificationType::InlineMessage => { flush_markdown_buffer(buffer, theme); hide_thinking(); - println!("\n{} {}", style("ยท").dim(), ¬ification.msg); + println!("\n{}", style(¬ification.msg).yellow()); } SystemNotificationType::CreditsExhausted => { flush_markdown_buffer(buffer, theme); @@ -376,7 +357,7 @@ pub fn render_message_streaming( fn render_credits_exhausted_notification(notification: &SystemNotificationContent) { hide_thinking(); - println!("\n{} {}", warning("warning:").bold(), ¬ification.msg); + println!("\n{}", style(¬ification.msg).yellow()); if let Some(url) = notification .data @@ -384,7 +365,10 @@ fn render_credits_exhausted_notification(notification: &SystemNotificationConten .and_then(|d| d.get("top_up_url")) .and_then(|v| v.as_str()) { - println!("{} {}", style("top up:").dim(), accent(url)); + println!( + "{}", + style(format!("Visit this URL to top up credits: {url}")).yellow() + ); } } @@ -431,6 +415,8 @@ pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { } if let Some(color) = color { styled_text = styled_text.fg(color); + } else { + styled_text = styled_text.green(); } print!("{}", styled_text); } @@ -438,8 +424,9 @@ pub fn render_text_no_newlines(text: &str, color: Option, dim: bool) { pub fn render_enter_plan_mode() { println!( "\n{} {}\n", - accent("Entering plan mode.").bold(), + style("Entering plan mode.").green().bold(), style("You can provide instructions to create a plan and then act on it. To exit early, type /endplan") + .green() .dim() ); } @@ -447,16 +434,18 @@ pub fn render_enter_plan_mode() { pub fn render_act_on_plan() { println!( "\n{}\n", - accent("Exiting plan mode and acting on the above plan").bold(), + style("Exiting plan mode and acting on the above plan") + .green() + .bold(), ); } pub fn render_exit_plan_mode() { - println!("\n{}\n", accent("Exiting plan mode.").bold()); + println!("\n{}\n", style("Exiting plan mode.").green().bold()); } pub fn goose_mode_message(text: &str) { - println!("\n{} {}", accent("mode:"), text); + println!("\n{}", style(text).yellow(),); } fn should_show_thinking() -> bool { @@ -592,13 +581,13 @@ fn is_file_tool_name(name: &str) -> bool { } pub fn render_error(message: &str) { - println!("\n {} {}\n", danger("error:").bold(), message); + println!("\n {} {}\n", style("error:").red().bold(), message); } pub fn render_prompts(prompts: &HashMap>) { println!(); for (extension, prompts) in prompts { - println!(" {}", accent(extension)); + println!(" {}", style(extension).green()); for prompt in prompts { println!(" - {}", style(prompt).cyan()); } @@ -609,7 +598,7 @@ pub fn render_prompts(prompts: &HashMap>) { pub fn render_prompt_info(info: &PromptInfo) { println!(); if let Some(ext) = &info.extension { - println!(" {}: {}", accent("Extension"), ext); + println!(" {}: {}", style("Extension").green(), ext); } println!(" Prompt: {}", style(&info.name).cyan().bold()); if let Some(desc) = &info.description { @@ -625,14 +614,14 @@ fn render_arguments(info: &PromptInfo) { for arg in args { let required = arg.required.unwrap_or(false); let req_str = if required { - style("(required)").bold() + style("(required)").red() } else { style("(optional)").dim() }; println!( " {} {} {}", - accent(&arg.name), + style(&arg.name).yellow(), req_str, arg.description.as_deref().unwrap_or("") ); @@ -642,13 +631,21 @@ fn render_arguments(info: &PromptInfo) { pub fn render_extension_success(name: &str) { println!(); - println!(" {} extension `{}`", success("added"), accent(name),); + println!( + " {} extension `{}`", + style("added").green(), + style(name).cyan(), + ); println!(); } pub fn render_extension_error(name: &str, error: &str) { println!(); - println!(" {} to add extension {}", danger("failed"), danger(name)); + println!( + " {} to add extension {}", + style("failed").red(), + style(name).red() + ); println!(); println!("{}", style(error).dim()); println!(); @@ -658,9 +655,9 @@ pub fn render_builtin_success(names: &str) { println!(); println!( " {} builtin{}: {}", - success("added"), + style("added").green(), if names.contains(',') { "s" } else { "" }, - accent(names) + style(names).cyan() ); println!(); } @@ -669,9 +666,9 @@ pub fn render_builtin_error(names: &str, error: &str) { println!(); println!( " {} to add builtin{}: {}", - danger("failed"), + style("failed").red(), if names.contains(',') { "s" } else { "" }, - danger(names) + style(names).red() ); println!(); println!("{}", style(error).dim()); @@ -772,7 +769,7 @@ fn render_execute_code_request(call: &CallToolRequestParams, debug: bool) { .and_then(Value::as_str) .filter(|c| !c.is_empty()); if code.is_some_and(|_| debug) { - println!("{}", code.unwrap_or_default()); + println!("{}", style(code.unwrap_or_default()).green()); } println!(); diff --git a/crates/goose-cli/src/session/streaming_buffer.rs b/crates/goose-cli/src/session/streaming_buffer.rs index cb42cce61e..ac8ddbd95f 100644 --- a/crates/goose-cli/src/session/streaming_buffer.rs +++ b/crates/goose-cli/src/session/streaming_buffer.rs @@ -524,8 +524,10 @@ impl MarkdownBuffer { } } "]" => {} - ")" if state.in_link_url => { - state.in_link_url = false; + ")" => { + if state.in_link_url { + state.in_link_url = false; + } } _ => {} } diff --git a/crates/goose-download-manager/Cargo.toml b/crates/goose-download-manager/Cargo.toml deleted file mode 100644 index e0b99e8d73..0000000000 --- a/crates/goose-download-manager/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "goose-download-manager" -version = "0.1.0-alpha.0" -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description.workspace = true - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true } -once_cell = { workspace = true } -reqwest = { workspace = true, features = ["stream"] } -serde = { workspace = true } -tokio = { workspace = true, features = ["fs", "io-util", "time"] } -tracing = { workspace = true } -utoipa = { workspace = true } diff --git a/crates/goose-download-manager/src/lib.rs b/crates/goose-download-manager/src/lib.rs deleted file mode 100644 index 89942e527c..0000000000 --- a/crates/goose-download-manager/src/lib.rs +++ /dev/null @@ -1,691 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use tokio::io::AsyncWriteExt; -use tracing::info; -use utoipa::ToSchema; - -fn partial_path_for(destination: &Path) -> PathBuf { - destination.with_extension( - destination - .extension() - .map(|e| format!("{}.part", e.to_string_lossy())) - .unwrap_or_else(|| "part".to_string()), - ) -} - -/// Remove orphaned `.part` files in the given directory (and one level of subdirectories). -/// Preserves `.part` files whose final destination is in `registered_paths` so that -/// in-progress shard downloads can resume after a restart. -pub fn cleanup_partial_downloads( - dir: &Path, - registered_paths: &std::collections::HashSet, -) { - let should_keep = |part_path: &Path| -> bool { - // Derive the final path by stripping the trailing ".part" extension - let final_path = part_path.with_extension(""); - registered_paths.contains(&final_path) - }; - - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|e| e == "part") && !should_keep(&path) { - let _ = std::fs::remove_file(&path); - } - if path.is_dir() { - if let Ok(sub_entries) = std::fs::read_dir(&path) { - for sub in sub_entries.flatten() { - let sub_path = sub.path(); - if sub_path.extension().is_some_and(|e| e == "part") - && !should_keep(&sub_path) - { - let _ = std::fs::remove_file(&sub_path); - } - } - } - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct DownloadProgress { - /// Model ID being downloaded - pub model_id: String, - /// Download status - pub status: DownloadStatus, - /// Bytes downloaded so far - pub bytes_downloaded: u64, - /// Total bytes to download - pub total_bytes: u64, - /// Download progress percentage (0-100) - pub progress_percent: f32, - /// Download speed in bytes per second - pub speed_bps: Option, - /// Estimated time remaining in seconds - pub eta_seconds: Option, - /// Error message if failed - pub error: Option, - /// Whether the background download task has exited - #[serde(skip)] - pub task_exited: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum DownloadStatus { - Downloading, - Completed, - Failed, - Cancelled, -} - -type DownloadMap = Arc>>; - -pub struct DownloadManager { - downloads: DownloadMap, -} - -impl Default for DownloadManager { - fn default() -> Self { - Self::new() - } -} - -impl DownloadManager { - pub fn new() -> Self { - Self { - downloads: Arc::new(Mutex::new(HashMap::new())), - } - } - - pub fn get_progress(&self, model_id: &str) -> Option { - self.downloads.lock().ok()?.get(model_id).cloned() - } - - pub fn is_downloading(&self, model_id: &str) -> bool { - self.get_progress(model_id) - .is_some_and(|progress| progress.status == DownloadStatus::Downloading) - } - - pub fn list_progress(&self) -> Vec { - self.downloads - .lock() - .map(|downloads| downloads.values().cloned().collect()) - .unwrap_or_default() - } - - pub fn set_progress(&self, progress: DownloadProgress) { - if let Ok(mut downloads) = self.downloads.lock() { - downloads.insert(progress.model_id.clone(), progress); - } - } - - pub fn reserve_download(&self, progress: DownloadProgress) -> Result { - let mut downloads = self - .downloads - .lock() - .map_err(|_| anyhow::anyhow!("Failed to acquire lock"))?; - - if let Some(existing) = downloads.get(&progress.model_id) { - if existing.status == DownloadStatus::Downloading - || (existing.status == DownloadStatus::Cancelled && !existing.task_exited) - { - return Ok(false); - } - } - - downloads.insert(progress.model_id.clone(), progress); - Ok(true) - } - - pub fn update_progress(&self, model_id: &str, update: impl FnOnce(&mut DownloadProgress)) { - if let Ok(mut downloads) = self.downloads.lock() { - if let Some(progress) = downloads.get_mut(model_id) { - update(progress); - } - } - } - - pub fn cancel_download(&self, model_id: &str) -> Result<()> { - let mut downloads = self - .downloads - .lock() - .map_err(|_| anyhow::anyhow!("Failed to acquire lock"))?; - - if let Some(progress) = downloads.get_mut(model_id) { - progress.status = DownloadStatus::Cancelled; - Ok(()) - } else { - anyhow::bail!("Download not found") - } - } - - pub async fn download_model( - &self, - model_id: String, - url: String, - destination: PathBuf, - on_complete: Option>, - ) -> Result<()> { - self.download_model_sharded(model_id, vec![(url, destination)], 0, on_complete) - .await - } - - pub async fn download_model_with_bearer_token( - &self, - model_id: String, - url: String, - destination: PathBuf, - bearer_token: Option, - on_complete: Option>, - ) -> Result<()> { - self.download_model_sharded_with_bearer_token( - model_id, - vec![(url, destination)], - 0, - bearer_token, - on_complete, - ) - .await - } - - pub async fn download_model_sharded( - &self, - model_id: String, - files: Vec<(String, PathBuf)>, - total_size_hint: u64, - on_complete: Option>, - ) -> Result<()> { - self.download_model_sharded_with_bearer_token( - model_id, - files, - total_size_hint, - None, - on_complete, - ) - .await - } - - pub async fn download_model_sharded_with_bearer_token( - &self, - model_id: String, - files: Vec<(String, PathBuf)>, - total_size_hint: u64, - bearer_token: Option, - on_complete: Option>, - ) -> Result<()> { - info!(model_id = %model_id, file_count = files.len(), "Starting model download"); - { - let mut downloads = self - .downloads - .lock() - .map_err(|_| anyhow::anyhow!("Failed to acquire lock"))?; - - if let Some(existing) = downloads.get(&model_id) { - if existing.status == DownloadStatus::Downloading { - anyhow::bail!("Download already in progress"); - } - if existing.status == DownloadStatus::Cancelled && !existing.task_exited { - anyhow::bail!( - "Download is being cancelled; wait for it to finish before restarting" - ); - } - } - - downloads.insert( - model_id.clone(), - DownloadProgress { - model_id: model_id.clone(), - status: DownloadStatus::Downloading, - bytes_downloaded: 0, - total_bytes: total_size_hint, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: None, - task_exited: false, - }, - ); - } - - // Create parent directories for all files - for (_, dest) in &files { - if let Some(parent) = dest.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| anyhow::anyhow!("Failed to create directory: {}", e))?; - } - } - - let downloads = self.downloads.clone(); - let model_id_clone = model_id.clone(); - let files_for_cleanup: Vec = files.iter().map(|(_, d)| d.clone()).collect(); - - tokio::spawn(async move { - let result = Self::download_files_sequentially( - &files, - &downloads, - &model_id_clone, - bearer_token.as_deref(), - ) - .await; - - match result { - Ok(_) => { - info!(model_id = %model_id_clone, "Download completed successfully"); - if let Ok(mut downloads) = downloads.lock() { - if let Some(progress) = downloads.get_mut(&model_id_clone) { - progress.status = DownloadStatus::Completed; - progress.progress_percent = 100.0; - progress.task_exited = true; - } - } - - if let Some(callback) = on_complete { - callback(); - } - } - Err(e) => { - for dest in &files_for_cleanup { - let partial = partial_path_for(dest); - let _ = tokio::fs::remove_file(&partial).await; - } - - if let Ok(mut downloads) = downloads.lock() { - if let Some(progress) = downloads.get_mut(&model_id_clone) { - if progress.status != DownloadStatus::Cancelled { - progress.status = DownloadStatus::Failed; - } - progress.error = Some(e.to_string()); - progress.task_exited = true; - } - } - } - } - }); - - Ok(()) - } - - const MAX_RETRIES: u32 = 10; - const RETRY_BASE_DELAY: std::time::Duration = std::time::Duration::from_secs(2); - const RETRY_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs(60); - - async fn cancellable_sleep( - delay: std::time::Duration, - downloads: &DownloadMap, - model_id: &str, - ) -> Result<(), anyhow::Error> { - let check_interval = std::time::Duration::from_millis(500); - let start = std::time::Instant::now(); - while start.elapsed() < delay { - if Self::is_cancelled(downloads, model_id) { - anyhow::bail!("Download cancelled"); - } - let remaining = delay.saturating_sub(start.elapsed()); - tokio::time::sleep(std::cmp::min(check_interval, remaining)).await; - } - Ok(()) - } - - fn is_cancelled(downloads: &DownloadMap, model_id: &str) -> bool { - if let Ok(downloads) = downloads.lock() { - if let Some(progress) = downloads.get(model_id) { - return progress.status == DownloadStatus::Cancelled; - } - } - false - } - - #[allow(clippy::too_many_arguments)] - /// Download multiple files sequentially, tracking cumulative progress under one model_id. - async fn download_files_sequentially( - files: &[(String, PathBuf)], - downloads: &DownloadMap, - model_id: &str, - bearer_token: Option<&str>, - ) -> Result<(), anyhow::Error> { - let client = reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(30)) - .read_timeout(std::time::Duration::from_secs(120)) - .build()?; - - // HEAD each file to get accurate total size. Only replace the hint if - // every file returned a size; partial results would underestimate. - let mut total: u64 = 0; - let mut all_resolved = true; - for (url, _) in files { - let size = Self::apply_bearer_token(client.head(url), bearer_token) - .send() - .await - .ok() - .and_then(|r| r.content_length()) - .unwrap_or(0); - if size == 0 { - all_resolved = false; - } - total += size; - } - if all_resolved && total > 0 { - if let Ok(mut dl) = downloads.lock() { - if let Some(progress) = dl.get_mut(model_id) { - progress.total_bytes = total; - } - } - } - - let start_time = std::time::Instant::now(); - let mut cumulative_bytes: u64 = 0; - // Account for already-downloaded shards - for (_, dest) in files { - let partial = partial_path_for(dest); - if dest.exists() { - if let Ok(meta) = tokio::fs::metadata(dest).await { - cumulative_bytes += meta.len(); - } - } else if partial.exists() { - if let Ok(meta) = tokio::fs::metadata(&partial).await { - cumulative_bytes += meta.len(); - } - } - } - let bytes_at_start = cumulative_bytes; - - for (url, destination) in files { - if Self::is_cancelled(downloads, model_id) { - anyhow::bail!("Download cancelled"); - } - - // Skip already-completed shards - if destination.exists() { - continue; - } - - Self::download_one_file( - &client, - url, - destination, - downloads, - model_id, - &mut cumulative_bytes, - start_time, - bytes_at_start, - bearer_token, - ) - .await?; - } - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - async fn download_one_file( - client: &reqwest::Client, - url: &str, - destination: &Path, - downloads: &DownloadMap, - model_id: &str, - cumulative_bytes: &mut u64, - start_time: std::time::Instant, - bytes_at_start: u64, - bearer_token: Option<&str>, - ) -> Result<(), anyhow::Error> { - let partial_path = partial_path_for(destination); - let mut retries = 0u32; - - let mut file_bytes: u64 = if partial_path.exists() { - tokio::fs::metadata(&partial_path).await?.len() - } else { - 0 - }; - - // Get this file's total size - let mut file_total: u64 = Self::apply_bearer_token(client.head(url), bearer_token) - .send() - .await - .ok() - .and_then(|r| r.content_length()) - .unwrap_or(0); - - // If partial matches expected size exactly, promote it - if file_total > 0 && file_bytes == file_total { - tokio::fs::rename(&partial_path, destination).await?; - // cumulative_bytes already accounts for this file from the pre-scan - return Ok(()); - } - - // If partial is oversized or remote changed, discard and re-download - if file_total > 0 && file_bytes > file_total { - info!(model_id = %model_id, file_bytes, file_total, "Partial file oversized, re-downloading"); - *cumulative_bytes = cumulative_bytes.saturating_sub(file_bytes); - file_bytes = 0; - let _ = tokio::fs::remove_file(&partial_path).await; - } - - loop { - if Self::is_cancelled(downloads, model_id) { - let _ = tokio::fs::remove_file(&partial_path).await; - anyhow::bail!("Download cancelled"); - } - - let mut request = Self::apply_bearer_token(client.get(url), bearer_token); - if file_bytes > 0 { - request = request.header("Range", format!("bytes={}-", file_bytes)); - } - - let response = match request.send().await { - Ok(r) => r, - Err(e) => { - if retries >= Self::MAX_RETRIES { - anyhow::bail!("Download failed after {} retries: {}", retries, e); - } - retries += 1; - let delay = std::cmp::min( - Self::RETRY_BASE_DELAY * 2u32.saturating_pow(retries - 1), - Self::RETRY_MAX_DELAY, - ); - info!(model_id = %model_id, retry = retries, delay_secs = ?delay.as_secs(), error = %e, "Retrying download after connection error"); - Self::cancellable_sleep(delay, downloads, model_id).await?; - continue; - } - }; - - let status = response.status(); - if status == reqwest::StatusCode::RANGE_NOT_SATISFIABLE { - if file_total > 0 && file_bytes == file_total { - break; - } - *cumulative_bytes = cumulative_bytes.saturating_sub(file_bytes); - file_bytes = 0; - let _ = tokio::fs::remove_file(&partial_path).await; - continue; - } - - if !status.is_success() && status != reqwest::StatusCode::PARTIAL_CONTENT { - let is_transient = status.is_server_error() - || status == reqwest::StatusCode::REQUEST_TIMEOUT - || status == reqwest::StatusCode::TOO_MANY_REQUESTS; - - if !is_transient || retries >= Self::MAX_RETRIES { - anyhow::bail!("Failed to download: HTTP {}", status); - } - retries += 1; - let delay = std::cmp::min( - Self::RETRY_BASE_DELAY * 2u32.saturating_pow(retries - 1), - Self::RETRY_MAX_DELAY, - ); - info!(model_id = %model_id, retry = retries, http_status = %status, "Retrying download after transient HTTP error"); - Self::cancellable_sleep(delay, downloads, model_id).await?; - continue; - } - - if file_bytes > 0 && status == reqwest::StatusCode::OK { - info!(model_id = %model_id, "Server ignored Range header, restarting file from scratch"); - // Subtract already-counted partial bytes from cumulative - *cumulative_bytes = cumulative_bytes.saturating_sub(file_bytes); - file_bytes = 0; - let _ = tokio::fs::remove_file(&partial_path).await; - } - - // If HEAD didn't return this file's size, learn it from the GET response. - // This block only fires once per file (file_total stays non-zero after), - // so retries don't double-count. Since download_files_sequentially's HEAD - // pass contributed 0 for this file, we add the discovered size to the - // shared total so progress/ETA are accurate. - if file_total == 0 { - let new_file_total = if file_bytes > 0 { - response - .headers() - .get("content-range") - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.rsplit('/').next()) - .and_then(|s| s.parse::().ok()) - } else { - response.content_length() - }; - if let Some(t) = new_file_total { - file_total = t; - if let Ok(mut dl) = downloads.lock() { - if let Some(progress) = dl.get_mut(model_id) { - progress.total_bytes = progress.total_bytes.saturating_add(t); - } - } - } - } - - let mut file = tokio::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&partial_path) - .await?; - - let file_len = tokio::fs::metadata(&partial_path).await?.len(); - if file_len != file_bytes { - file.set_len(file_bytes).await?; - } - - let mut stream_error = false; - let mut resp = response; - - loop { - let chunk_result = resp.chunk().await; - match chunk_result { - Ok(Some(chunk)) => { - if Self::is_cancelled(downloads, model_id) { - let _ = tokio::fs::remove_file(&partial_path).await; - anyhow::bail!("Download cancelled"); - } - - file.write_all(&chunk).await?; - let chunk_len = chunk.len() as u64; - file_bytes += chunk_len; - *cumulative_bytes += chunk_len; - - let elapsed = start_time.elapsed().as_secs_f64(); - let bytes_this_session = cumulative_bytes.saturating_sub(bytes_at_start); - let speed_bps = if elapsed > 0.0 { - Some((bytes_this_session as f64 / elapsed) as u64) - } else { - None - }; - - let current_total = if let Ok(dl) = downloads.lock() { - dl.get(model_id).map(|p| p.total_bytes).unwrap_or(0) - } else { - 0 - }; - - let eta_seconds = if let Some(speed) = speed_bps { - if speed > 0 && current_total > 0 { - Some(current_total.saturating_sub(*cumulative_bytes) / speed) - } else { - None - } - } else { - None - }; - - if let Ok(mut dl) = downloads.lock() { - if let Some(progress) = dl.get_mut(model_id) { - progress.bytes_downloaded = *cumulative_bytes; - progress.progress_percent = if current_total > 0 { - (*cumulative_bytes as f64 / current_total as f64 * 100.0) as f32 - } else { - 0.0 - }; - progress.speed_bps = speed_bps; - progress.eta_seconds = eta_seconds; - } - } - } - Ok(None) => break, - Err(e) => { - info!(model_id = %model_id, bytes = *cumulative_bytes, error = %e, "Download stream interrupted, will retry"); - stream_error = true; - break; - } - } - } - - file.flush().await?; - drop(file); - - if stream_error { - if retries >= Self::MAX_RETRIES { - anyhow::bail!( - "Download failed after {} retries due to stream interruption", - retries - ); - } - retries += 1; - let delay = std::cmp::min( - Self::RETRY_BASE_DELAY * 2u32.saturating_pow(retries - 1), - Self::RETRY_MAX_DELAY, - ); - info!(model_id = %model_id, retry = retries, delay_secs = ?delay.as_secs(), "Retrying download with resume"); - Self::cancellable_sleep(delay, downloads, model_id).await?; - continue; - } - - break; - } - - tokio::fs::rename(&partial_path, destination).await?; - Ok(()) - } - - pub fn clear_completed(&self, model_id: &str) { - if let Ok(mut downloads) = self.downloads.lock() { - if let Some(progress) = downloads.get(model_id) { - let is_terminal = progress.status == DownloadStatus::Completed - || progress.status == DownloadStatus::Failed - || progress.status == DownloadStatus::Cancelled; - if is_terminal && progress.task_exited { - downloads.remove(model_id); - } - } - } - } - - fn apply_bearer_token( - request: reqwest::RequestBuilder, - bearer_token: Option<&str>, - ) -> reqwest::RequestBuilder { - if let Some(token) = bearer_token.filter(|token| !token.is_empty()) { - request.header("Authorization", format!("Bearer {}", token)) - } else { - request - } - } -} - -static DOWNLOAD_MANAGER: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(DownloadManager::new); - -pub fn get_download_manager() -> &'static DownloadManager { - &DOWNLOAD_MANAGER -} diff --git a/crates/goose-local-inference/Cargo.toml b/crates/goose-local-inference/Cargo.toml deleted file mode 100644 index c3ae342747..0000000000 --- a/crates/goose-local-inference/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "goose-local-inference" -version = "0.1.0-alpha.0" -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description.workspace = true - -[lints] -workspace = true - -[features] -default = [] -cuda = ["llama-cpp-2/cuda"] -vulkan = ["llama-cpp-2/vulkan"] -mlx = ["dep:safemlx", "dep:safemlx-lm", "dep:safemlx-lm-utils"] - -[dependencies] -anyhow = { workspace = true } -async-stream = { workspace = true } -async-trait = { workspace = true } -base64 = { workspace = true } -chrono = { workspace = true } -etcetera = { workspace = true } -encoding_rs = { version = "0.8.35", default-features = false } -fs2 = { workspace = true } -futures = { workspace = true } -goose-download-manager = { version = "0.1.0-alpha.0", path = "../goose-download-manager" } -goose-provider-types = { version = "0.1.0-alpha.0", path = "../goose-provider-types", default-features = false } -goose-sdk-types = { version = "0.1.0-alpha.0", path = "../goose-sdk-types", default-features = false } -hf-hub = { version = "1.0.0-rc.1", default-features = false } -include_dir = { workspace = true } -llama-cpp-2 = { workspace = true } -llama-cpp-sys-2 = { workspace = true } -minijinja = { version = "2.18", default-features = false, features = ["loader", "multi_template", "serde"] } -reqwest = { workspace = true, features = ["json", "stream"] } -regex = { workspace = true } -rmcp = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs", "rt-multi-thread", "sync", "time"] } -tracing = { workspace = true } -utoipa = { workspace = true, features = ["chrono"] } -uuid = { workspace = true, features = ["v4", "std"] } -tempfile = { workspace = true } - -safemlx = { default-features = false, features = ["accelerate", "metal", "safetensors"], optional = true, version = "0.1.2" } -safemlx-lm = { optional = true, version = "0.1.5" } -safemlx-lm-utils = { optional = true, version = "0.1.2" } - -[target.'cfg(target_os = "macos")'.dependencies] -llama-cpp-2 = { workspace = true, features = ["sampler", "metal", "mtmd"] } diff --git a/crates/goose-local-inference/src/config_resolver.rs b/crates/goose-local-inference/src/config_resolver.rs deleted file mode 100644 index 3c5a610222..0000000000 --- a/crates/goose-local-inference/src/config_resolver.rs +++ /dev/null @@ -1,30 +0,0 @@ -use anyhow::Result; -use std::sync::OnceLock; - -pub type StringParamResolver = fn(&'static str) -> Result>; -pub type BoolParamResolver = fn(&'static str) -> Result>; - -static STRING_PARAM_RESOLVER: OnceLock = OnceLock::new(); -static BOOL_PARAM_RESOLVER: OnceLock = OnceLock::new(); - -pub fn set_string_param_resolver(resolve_param: StringParamResolver) { - let _ = STRING_PARAM_RESOLVER.set(resolve_param); -} - -pub fn set_bool_param_resolver(resolve_param: BoolParamResolver) { - let _ = BOOL_PARAM_RESOLVER.set(resolve_param); -} - -pub fn string_param(key: &'static str) -> Result> { - match STRING_PARAM_RESOLVER.get() { - Some(resolve_param) => resolve_param(key), - None => Ok(None), - } -} - -pub fn bool_param(key: &'static str) -> Result> { - match BOOL_PARAM_RESOLVER.get() { - Some(resolve_param) => resolve_param(key), - None => Ok(None), - } -} diff --git a/crates/goose-local-inference/src/huggingface_auth.rs b/crates/goose-local-inference/src/huggingface_auth.rs deleted file mode 100644 index c1510e0625..0000000000 --- a/crates/goose-local-inference/src/huggingface_auth.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{config_resolver, paths::Paths}; -use anyhow::Result; -use chrono::{DateTime, Utc}; -use futures::future::BoxFuture; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; -use std::sync::OnceLock; - -pub const HUGGINGFACE_TOKEN_SECRET_KEY: &str = "HF_TOKEN"; -pub const HUGGINGFACE_OAUTH_CACHE_PATH: &str = "huggingface/oauth/tokens.json"; - -pub type TokenResolver = fn() -> BoxFuture<'static, Result>>; - -static TOKEN_RESOLVER: OnceLock = OnceLock::new(); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HuggingFaceTokenData { - pub access_token: String, - #[serde(default)] - pub refresh_token: Option, - #[serde(default)] - pub expires_at: Option>, -} - -impl HuggingFaceTokenData { - pub fn is_expired(&self) -> bool { - self.expires_at - .is_some_and(|expires_at| expires_at <= Utc::now()) - } -} - -pub fn oauth_cache_path() -> PathBuf { - Paths::in_config_dir(HUGGINGFACE_OAUTH_CACHE_PATH) -} - -fn load_oauth_token_from_path(path: &Path) -> Option { - let contents = std::fs::read_to_string(path).ok()?; - serde_json::from_str(&contents).ok() -} - -pub fn usable_oauth_token() -> Option { - let token = load_oauth_token_from_path(&oauth_cache_path())?; - (!token.is_expired()).then_some(token.access_token) -} - -pub fn hf_token_secret() -> Result> { - Ok(config_resolver::string_param(HUGGINGFACE_TOKEN_SECRET_KEY)? - .filter(|token| !token.trim().is_empty())) -} - -pub fn set_token_resolver(resolve_token: TokenResolver) { - let _ = TOKEN_RESOLVER.set(resolve_token); -} - -pub async fn resolve_token_async() -> Result> { - if let Some(resolve_token) = TOKEN_RESOLVER.get() { - return resolve_token().await; - } - - if let Some(token) = usable_oauth_token() { - return Ok(Some(token)); - } - hf_token_secret() -} diff --git a/crates/goose-local-inference/src/lib.rs b/crates/goose-local-inference/src/lib.rs deleted file mode 100644 index e36ee9bed0..0000000000 --- a/crates/goose-local-inference/src/lib.rs +++ /dev/null @@ -1,950 +0,0 @@ -pub mod config_resolver; -pub use goose_download_manager as download_manager; -pub mod huggingface_auth; -pub mod paths; -pub mod prompt_template; -pub mod provider_utils; - -mod backend; -pub mod hf_models; -mod llamacpp; -pub mod local_model_registry; -pub mod management; -mod mlx; -pub(crate) mod multimodal; -#[cfg(feature = "mlx")] -mod native_tool_parsing; -pub(crate) mod thinking_output; -#[cfg(feature = "mlx")] -mod tool_emulation; -mod tool_parsing; - -use anyhow::Result; -use async_stream::try_stream; -use async_trait::async_trait; -use backend::{BackendLoadedModel, LocalInferenceBackend}; -use goose_provider_types::base::{MessageStream, Provider, ProviderDescriptor, ProviderMetadata}; -use goose_provider_types::conversation::message::{ - Message, MessageContent, SystemNotificationType, -}; -use goose_provider_types::conversation::token_usage::{ProviderUsage, Usage}; -use goose_provider_types::errors::ProviderError; -use goose_provider_types::images::ImageFormat; -use goose_provider_types::model::ModelConfig; -use goose_provider_types::request_log::{start_log, LoggerHandleExt, RequestLogHandle}; -use llamacpp::{LlamaCppBackend, LLAMACPP_BACKEND_ID}; -use local_model_registry::ChatTemplate; -use mlx::{MlxBackend, MLX_BACKEND_ID}; -use rmcp::model::Tool; -use serde_json::{json, Value}; -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex as StdMutex}; -use tokio::sync::{Mutex, Notify}; -use uuid::Uuid; - -type ModelSlotHandle = Arc; - -struct ModelSlot { - state: Mutex, - notify: Notify, -} - -enum ModelSlotState { - Empty, - Loading, - Loaded(Box), -} - -impl ModelSlot { - fn new() -> Self { - Self { - state: Mutex::new(ModelSlotState::Empty), - notify: Notify::new(), - } - } -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -struct ModelCacheKey { - backend_id: &'static str, - model_id: String, - chat_template: ChatTemplate, -} - -impl ModelCacheKey { - fn new( - backend_id: &'static str, - model_id: impl Into, - chat_template: ChatTemplate, - ) -> Self { - Self { - backend_id, - model_id: model_id.into(), - chat_template, - } - } -} - -pub struct InferenceRuntime { - models: StdMutex>, - cold_load_lock: Mutex<()>, - backends: HashMap<&'static str, Arc>, -} - -pub fn builtin_chat_template_names() -> Vec { - llamacpp::builtin_chat_template_names() -} - -static RUNTIME: StdMutex>> = StdMutex::new(None); - -fn current_runtime() -> Option> { - RUNTIME.lock().expect("runtime lock poisoned").clone() -} - -impl InferenceRuntime { - pub fn get_or_init() -> Result> { - let mut guard = RUNTIME.lock().expect("runtime lock poisoned"); - if let Some(runtime) = guard.as_ref() { - return Ok(runtime.clone()); - } - let llamacpp_backend: Arc = Arc::new(LlamaCppBackend::new()?); - let mlx_backend: Arc = Arc::new(MlxBackend::new()); - let mut backends = HashMap::new(); - backends.insert(LLAMACPP_BACKEND_ID, llamacpp_backend); - backends.insert(MLX_BACKEND_ID, mlx_backend); - let runtime = Arc::new(Self { - models: StdMutex::new(HashMap::new()), - cold_load_lock: Mutex::new(()), - backends, - }); - *guard = Some(runtime.clone()); - Ok(runtime) - } - - fn default_backend(&self) -> &dyn LocalInferenceBackend { - self.backends - .get(LLAMACPP_BACKEND_ID) - .expect("default local inference backend registered") - .as_ref() - } - - fn backend_for_model( - &self, - resolved: &ResolvedModelPaths, - ) -> Result, ProviderError> { - let backend_id = resolved - .backend_id - .as_deref() - .unwrap_or(LLAMACPP_BACKEND_ID); - self.backends.get(backend_id).cloned().ok_or_else(|| { - ProviderError::ExecutionError(format!( - "Local inference backend '{}' unavailable", - backend_id - )) - }) - } - - fn get_or_create_model_slot(&self, key: ModelCacheKey) -> ModelSlotHandle { - let mut map = self.models.lock().expect("model cache lock poisoned"); - map.entry(key) - .or_insert_with(|| Arc::new(ModelSlot::new())) - .clone() - } - - fn model_slot(&self, key: &ModelCacheKey) -> Option { - let map = self.models.lock().expect("model cache lock poisoned"); - map.get(key).cloned() - } - - fn other_model_slots(&self, keep_key: &ModelCacheKey) -> Vec { - let map = self.models.lock().expect("model cache lock poisoned"); - map.iter() - .filter(|(key, _)| *key != keep_key) - .map(|(_, slot)| slot.clone()) - .collect() - } -} - -pub async fn is_model_loaded(model_name: &str) -> Result { - let resolved = match resolve_model_path(model_name) { - Some(resolved) => resolved, - None => return Ok(false), - }; - let runtime = InferenceRuntime::get_or_init().map_err(|error| { - ProviderError::ExecutionError(format!("Failed to initialize local inference: {error}")) - })?; - let backend = runtime.backend_for_model(&resolved)?; - let key = ModelCacheKey::new( - backend.id(), - model_name.to_string(), - resolved.settings.chat_template, - ); - let Some(slot) = runtime.model_slot(&key) else { - return Ok(false); - }; - - let state = slot.state.lock().await; - Ok(matches!(*state, ModelSlotState::Loaded(_))) -} - -pub async fn loaded_model_ids() -> Result, ProviderError> { - let Some(runtime) = current_runtime() else { - return Ok(HashSet::new()); - }; - let slots = { - let map = runtime.models.lock().expect("model cache lock poisoned"); - map.iter() - .map(|(key, slot)| (key.model_id.clone(), slot.clone())) - .collect::>() - }; - - let mut loaded = HashSet::new(); - for (model_id, slot) in slots { - if let Ok(state) = slot.state.try_lock() { - if matches!(*state, ModelSlotState::Loaded(_)) { - loaded.insert(model_id); - } - } else { - loaded.insert(model_id); - } - } - Ok(loaded) -} - -pub async fn evict_model(model_name: &str) -> Result { - let Some(runtime) = current_runtime() else { - return Ok(false); - }; - let slots = { - let map = runtime.models.lock().expect("model cache lock poisoned"); - map.iter() - .filter(|(key, _)| key.model_id == model_name) - .map(|(_, slot)| slot.clone()) - .collect::>() - }; - - let mut evicted = false; - for slot in slots { - let mut state = slot.state.lock().await; - if matches!(*state, ModelSlotState::Loaded(_)) { - *state = ModelSlotState::Empty; - evicted = true; - slot.notify.notify_waiters(); - } - } - Ok(evicted) -} - -const PROVIDER_NAME: &str = "local"; -const DEFAULT_MODEL: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M"; - -pub const LOCAL_LLM_MODEL_CONFIG_KEY: &str = "LOCAL_LLM_MODEL"; - -#[derive(Clone)] -pub(crate) struct ResolvedModelPaths { - pub model_path: PathBuf, - pub context_limit: usize, - pub settings: crate::local_model_registry::ModelSettings, - pub mmproj_path: Option, - pub backend_id: Option, - pub draft_model_path: Option, -} - -fn resolve_model_local_path(model_id: &str) -> Option { - use crate::local_model_registry::get_registry; - - get_registry() - .lock() - .ok()? - .get_model(model_id) - .map(|entry| entry.local_path.clone()) -} - -/// 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::local_model_registry::{default_settings_for_model, 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(); - let defaults = default_settings_for_model(model_id); - 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(); - let backend_id = entry - .backend_id - .clone() - .or_else(|| settings.backend_id.clone()); - let draft_model = settings - .draft_model - .clone() - .or_else(|| { - config_resolver::string_param("GOOSE_LOCAL_DRAFT_MODEL") - .ok() - .flatten() - }) - .filter(|draft_model| draft_model != model_id); - let draft_model_path = draft_model.as_deref().and_then(resolve_model_local_path); - return Some(ResolvedModelPaths { - model_path: entry.local_path.clone(), - context_limit: ctx, - settings, - mmproj_path, - backend_id, - draft_model_path, - }); - } - } - - None -} - -pub fn available_inference_memory_bytes(runtime: &InferenceRuntime) -> u64 { - runtime.default_backend().available_memory_bytes() -} - -pub fn recommend_local_model(runtime: &InferenceRuntime) -> String { - use local_model_registry::{get_registry, is_featured_model, FEATURED_MODELS}; - - let available_memory = available_inference_memory_bytes(runtime); - - if let Ok(registry) = get_registry().lock() { - let mut models: Vec<_> = registry - .list_models() - .iter() - .filter(|m| is_featured_model(&m.id) && m.size_bytes > 0) - .collect(); - models.sort_by_key(|model| std::cmp::Reverse(model.size_bytes)); - - // Return largest that fits in available memory - for model in &models { - if available_memory >= model.size_bytes { - return model.id.clone(); - } - } - - // If nothing fits, return smallest - if let Some(smallest) = models.last() { - return smallest.id.clone(); - } - } - - // Fallback to first featured model - FEATURED_MODELS[0].spec.to_string() -} - -fn build_openai_messages_json( - system: &str, - messages: &[Message], - media_marker: Option<&str>, -) -> String { - use goose_provider_types::formats::openai::format_messages; - - let mut arr: Vec = vec![json!({"role": "system", "content": system})]; - arr.extend(format_messages(messages, &ImageFormat::OpenAi)); - strip_image_parts_from_messages(&mut arr); - if let Some(marker) = media_marker { - convert_text_media_markers(&mut arr, marker); - } - serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()) -} - -fn build_openai_text_messages_json( - system: &str, - messages: &[Message], - media_marker: Option<&str>, -) -> 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})) - })); - if let Some(marker) = media_marker { - convert_text_media_markers(&mut arr, marker); - } - serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string()) -} - -fn convert_text_media_markers(messages: &mut [Value], marker: &str) { - if marker.is_empty() { - return; - } - - for msg in messages { - let Some(content) = msg.get_mut("content") else { - continue; - }; - - if let Some(text) = content.as_str() { - if let Some(parts) = split_media_marker_text(text, marker) { - *content = json!(parts); - } - continue; - } - - let Some(content_parts) = content.as_array_mut() else { - continue; - }; - let mut updated = Vec::new(); - let mut changed = false; - for part in content_parts.iter() { - if part.get("type").and_then(|v| v.as_str()) == Some("text") { - if let Some(text) = part.get("text").and_then(|v| v.as_str()) { - if let Some(parts) = split_media_marker_text(text, marker) { - updated.extend(parts); - changed = true; - continue; - } - } - } - updated.push(part.clone()); - } - if changed { - *content_parts = updated; - } - } -} - -fn split_media_marker_text(text: &str, marker: &str) -> Option> { - let mut parts = Vec::new(); - let mut rest = text; - let mut found_marker = false; - while let Some((before, after)) = rest.split_once(marker) { - found_marker = true; - let before = before.strip_suffix('\n').unwrap_or(before); - if !before.is_empty() { - parts.push(json!({"type": "text", "text": before})); - } - parts.push(json!({"type": "media_marker", "text": marker})); - rest = after; - rest = rest.strip_prefix('\n').unwrap_or(rest); - } - if !found_marker { - return None; - } - if !rest.is_empty() { - parts.push(json!({"type": "text", "text": rest})); - } - Some(parts) -} - -/// 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. -fn strip_image_parts_from_messages(messages: &mut [Value]) { - let mut stripped = false; - for msg in messages.iter_mut() { - if let Some(content) = msg.get_mut("content").and_then(|c| c.as_array_mut()) { - for part in content.iter_mut() { - if part.get("type").and_then(|t| t.as_str()) == Some("image_url") { - *part = json!({ - "type": "text", - "text": "[Image attached โ€” image input is not supported with the currently selected model]" - }); - stripped = true; - } - } - } - } - if stripped { - tracing::warn!("Stripped image content parts from messages โ€” vision encoder not available for this model"); - } -} - -/// Convert a message into plain text for the emulator path's chat history. -/// -/// This is the emulator-path counterpart of [`format_messages`] used by the native -/// path. It reconstructs the text-based tool syntax that the emulator prompt teaches -/// the model: -/// -/// - `ToolRequest` with a `"command"` argument โ†’ `$ command` -/// - `ToolRequest` with a `"code"` argument โ†’ `` ```execute_typescript\nโ€ฆ\n``` `` -/// - `ToolResponse` โ†’ `Command output:\nโ€ฆ` -/// -/// Only `developer__shell` and `code_execution__execute_typescript` style tool calls are -/// recognized (by argument shape, not tool name). Tool calls from other extensions -/// (e.g. custom MCP tools made by a native-tool-calling model earlier in the -/// conversation) are silently dropped, since the emulator path has no syntax to -/// represent them. -fn extract_text_content(msg: &Message) -> String { - let mut parts = Vec::new(); - - for content in &msg.content { - match content { - MessageContent::Text(text) => { - let text = strip_info_messages(&text.text); - if !text.trim().is_empty() { - parts.push(text); - } - } - MessageContent::ToolRequest(req) => { - if let Ok(call) = &req.tool_call { - if let Some(cmd) = call - .arguments - .as_ref() - .and_then(|a| a.get("command")) - .and_then(|v| v.as_str()) - { - parts.push(format!("$ {}", cmd)); - } else if let Some(code) = call - .arguments - .as_ref() - .and_then(|a| a.get("code")) - .and_then(|v| v.as_str()) - { - parts.push(format!("```execute_typescript\n{}\n```", code)); - } - } - } - MessageContent::ToolResponse(response) => match &response.tool_result { - Ok(result) => { - let mut output_parts = Vec::new(); - for content_item in &result.content { - if let Some(text_content) = content_item.as_text() { - output_parts.push(text_content.text.to_string()); - } - } - if !output_parts.is_empty() { - parts.push(format!("Command output:\n{}", output_parts.join("\n"))); - } - } - Err(e) => { - parts.push(format!("Command error: {}", e)); - } - }, - MessageContent::Image(_) => { - parts.push( - "[Image attached โ€” image input is not supported with the currently selected model]" - .to_string(), - ); - } - _ => {} - } - } - - parts.join("\n") -} - -fn strip_info_messages(text: &str) -> String { - let mut remaining = text; - let mut output = String::new(); - - while let Some((before, after_start)) = remaining.split_once("") { - output.push_str(before); - if let Some((_, after_end)) = after_start.split_once("") { - remaining = after_end; - } else { - remaining = ""; - break; - } - } - - output.push_str(remaining); - output.trim().to_string() -} - -/// Build a `ProviderUsage` and write the request log entry. -fn finalize_usage( - log: &mut Option>, - model_name: String, - path_label: &str, - prompt_token_count: usize, - output_token_count: i32, - extra_log_fields: Option<(&str, &str)>, -) -> ProviderUsage { - let input_tokens = prompt_token_count as i32; - let total_tokens = input_tokens + output_token_count; - let usage = Usage::new( - Some(input_tokens), - Some(output_token_count), - Some(total_tokens), - ); - let mut log_json = serde_json::json!({ - "path": path_label, - "prompt_tokens": input_tokens, - "output_tokens": output_token_count, - }); - if let Some((key, value)) = extra_log_fields { - log_json[key] = serde_json::json!(value); - } - let _ = log.write(&log_json, Some(&usage)); - ProviderUsage::new(model_name, usage) -} - -type StreamSender = - tokio::sync::mpsc::Sender, Option), ProviderError>>; - -pub struct LocalInferenceProvider { - runtime: Arc, - name: String, -} - -impl LocalInferenceProvider { - pub async fn from_env() -> Result { - let runtime = InferenceRuntime::get_or_init()?; - Ok(Self { - runtime, - name: PROVIDER_NAME.to_string(), - }) - } -} - -impl ProviderDescriptor for LocalInferenceProvider { - fn metadata() -> ProviderMetadata - where - Self: Sized, - { - use crate::local_model_registry::{get_registry, FEATURED_MODELS}; - - let mut known_models: Vec<&str> = FEATURED_MODELS.iter().map(|m| m.spec).collect(); - - // Add any registry models not already in the featured list - let mut dynamic_models = Vec::new(); - if let Ok(registry) = get_registry().lock() { - for entry in registry.list_models() { - if !known_models.contains(&entry.id.as_str()) { - dynamic_models.push(entry.id.clone()); - } - } - } - let dynamic_refs: Vec<&str> = dynamic_models.iter().map(|s| s.as_str()).collect(); - known_models.extend(dynamic_refs); - - ProviderMetadata::new( - PROVIDER_NAME, - "Local Inference", - "Local inference using quantized GGUF models (llama.cpp)", - DEFAULT_MODEL, - known_models, - "https://github.com/utilityai/llama-cpp-rs", - vec![], - ) - } -} - -#[async_trait] -impl Provider for LocalInferenceProvider { - fn get_name(&self) -> &str { - &self.name - } - - async fn fetch_supported_models(&self) -> Result, ProviderError> { - use crate::local_model_registry::get_registry; - - let mut all_models: Vec = Vec::new(); - - if let Ok(registry) = get_registry().lock() { - for entry in registry.list_models() { - all_models.push(entry.id.clone()); - } - } - - Ok(all_models) - } - - async fn stream( - &self, - model_config: &ModelConfig, - system: &str, - messages: &[Message], - tools: &[Tool], - ) -> Result { - let resolved = resolve_model_path(&model_config.model_name).ok_or_else(|| { - ProviderError::ExecutionError(format!("Model not found: {}", model_config.model_name)) - })?; - let backend = self.runtime.backend_for_model(&resolved)?; - let model_context_limit = resolved.context_limit; - - // Allow request_params to override thinking - let mut model_settings = resolved.settings.clone(); - if let Some(false) = model_config - .request_param::("enable_thinking") - .or_else(|| { - config_resolver::bool_param("GOOSE_LOCAL_ENABLE_THINKING") - .ok() - .flatten() - }) - { - model_settings.enable_thinking = false; - } - - 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()); - let runtime = self.runtime.clone(); - - let cache_key = cache_key.clone(); - let model_arc = model_slot.clone(); - let backend = backend.clone(); - let model_name = model_config.model_name.clone(); - let temperature = model_config.temperature; - let max_tokens = model_config.max_tokens; - let context_limit = model_context_limit; - let settings = model_settings; - let resolved_model = resolved.clone(); - let system = system.to_string(); - let messages = messages.to_vec(); - let tools = tools.to_vec(); - let log_payload = serde_json::json!({ - "system": &system, - "messages": messages.iter().map(|m| { - serde_json::json!({ - "role": match m.role { rmcp::model::Role::User => "user", rmcp::model::Role::Assistant => "assistant" }, - "content": extract_text_content(m), - }) - }).collect::>(), - "tools": tools.iter().map(|t| &t.name).collect::>(), - "settings": { - "tool_calling": settings.tool_calling, - "chat_template": settings.chat_template, - "context_size": settings.context_size, - "sampling": settings.sampling, - }, - }); - - let (tx, mut rx) = tokio::sync::mpsc::channel::< - Result<(Option, Option), ProviderError>, - >(32); - let mut log = start_log(model_config, &log_payload)?; - - tokio::spawn(async move { - let mut model_load_ms = None; - - // Ensure model is loaded โ€” unload any other models first to free memory. - loop { - let state = model_slot.state.lock().await; - match &*state { - ModelSlotState::Loaded(_) => break, - ModelSlotState::Loading => { - let notified = model_slot.notify.notified(); - drop(state); - notified.await; - } - ModelSlotState::Empty => { - drop(state); - - let cold_load_guard = runtime.cold_load_lock.lock().await; - let mut state = model_slot.state.lock().await; - match &*state { - ModelSlotState::Loaded(_) => break, - ModelSlotState::Loading => { - let notified = model_slot.notify.notified(); - drop(state); - drop(cold_load_guard); - notified.await; - continue; - } - ModelSlotState::Empty => {} - } - *state = ModelSlotState::Loading; - drop(state); - - let loading_message = Message::assistant().with_system_notification( - SystemNotificationType::ProgressMessage, - format!("Loading local model {model_name}..."), - ); - if tx.send(Ok((Some(loading_message), None))).await.is_err() { - let mut state = model_slot.state.lock().await; - *state = ModelSlotState::Empty; - model_slot.notify.notify_waiters(); - return; - } - - let other_model_slots = runtime.other_model_slots(&cache_key); - for slot in other_model_slots { - let mut other = slot.state.lock().await; - if matches!(*other, ModelSlotState::Loaded(_)) { - tracing::info!("Unloading previous model to free memory"); - *other = ModelSlotState::Empty; - } - } - - let model_id = model_name.clone(); - let resolved_for_load = resolved_model.clone(); - let settings_for_load = settings.clone(); - let backend_for_load = backend.clone(); - let load_started = std::time::Instant::now(); - let loaded = match tokio::task::spawn_blocking(move || { - backend_for_load.load_model( - &model_id, - &resolved_for_load, - &settings_for_load, - ) - }) - .await - { - Ok(Ok(loaded)) => loaded, - Ok(Err(err)) => { - let mut state = model_slot.state.lock().await; - *state = ModelSlotState::Empty; - model_slot.notify.notify_waiters(); - let _ = log.error(&err); - let _ = tx.send(Err(err)).await; - return; - } - Err(err) => { - let mut state = model_slot.state.lock().await; - *state = ModelSlotState::Empty; - model_slot.notify.notify_waiters(); - let err = ProviderError::ExecutionError(err.to_string()); - let _ = log.error(&err); - let _ = tx.send(Err(err)).await; - return; - } - }; - let elapsed_ms = - u64::try_from(load_started.elapsed().as_millis()).unwrap_or(u64::MAX); - model_load_ms = Some(elapsed_ms); - tracing::info!( - backend = backend.id(), - model = %model_name, - model_load_ms = elapsed_ms, - "Loaded local inference model" - ); - let _ = log.write( - &json!({ - "path": "model_load", - "backend": backend.id(), - "model": &model_name, - "model_load_ms": elapsed_ms, - }), - None, - ); - - let mut state = model_slot.state.lock().await; - *state = ModelSlotState::Loaded(loaded); - model_slot.notify.notify_waiters(); - drop(cold_load_guard); - break; - } - } - } - - tokio::task::spawn_blocking(move || { - // Macro to log errors before sending them through the channel - macro_rules! send_err { - ($err:expr) => {{ - let err = $err; - let msg = match &err { - ProviderError::ExecutionError(s) => s.as_str(), - ProviderError::ContextLengthExceeded(s) => s.as_str(), - _ => "unknown error", - }; - let _ = log.error(msg); - let _ = tx.blocking_send(Err(err)); - return; - }}; - } - - let mut model_guard = model_arc.state.blocking_lock(); - let loaded = match &mut *model_guard { - ModelSlotState::Loaded(loaded) => loaded.as_mut(), - ModelSlotState::Empty | ModelSlotState::Loading => { - send_err!(ProviderError::ExecutionError( - "Model not loaded".to_string() - )); - } - }; - - let message_id = Uuid::new_v4().to_string(); - - let request = backend::LocalGenerationRequest { - model_name, - system: &system, - messages: &messages, - tools: &tools, - settings: &settings, - temperature, - max_tokens, - context_limit, - model_load_ms, - resolved_model: &resolved_model, - draft_model_path: resolved_model.draft_model_path.clone(), - message_id: &message_id, - tx: &tx, - log: &mut log, - }; - - let result = backend.generate(loaded, request); - - if let Err(err) = result { - let msg = match &err { - ProviderError::ExecutionError(s) => s.as_str(), - ProviderError::ContextLengthExceeded(s) => s.as_str(), - _ => "unknown error", - }; - let _ = log.error(msg); - let _ = tx.blocking_send(Err(err)); - } - }); - }); - - Ok(Box::pin(try_stream! { - while let Some(result) = rx.recv().await { - let item = result?; - yield item; - } - - })) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn converts_marker_in_string_content_to_media_marker_part() { - let mut messages = vec![json!({ - "role": "user", - "content": "look\n<__media__>\nclosely", - })]; - - convert_text_media_markers(&mut messages, "<__media__>"); - - assert_eq!( - messages[0]["content"], - json!([ - {"type": "text", "text": "look"}, - {"type": "media_marker", "text": "<__media__>"}, - {"type": "text", "text": "closely"}, - ]) - ); - } - - #[test] - fn converts_marker_inside_text_content_parts() { - let mut messages = vec![json!({ - "role": "user", - "content": [ - {"type": "text", "text": "<__media__>describe"}, - {"type": "text", "text": "next"}, - {"type": "media_marker", "text": "<__media__>"}, - ], - })]; - - convert_text_media_markers(&mut messages, "<__media__>"); - - assert_eq!( - messages[0]["content"], - json!([ - {"type": "media_marker", "text": "<__media__>"}, - {"type": "text", "text": "describe"}, - {"type": "text", "text": "next"}, - {"type": "media_marker", "text": "<__media__>"}, - ]) - ); - } -} diff --git a/crates/goose-local-inference/src/management.rs b/crates/goose-local-inference/src/management.rs deleted file mode 100644 index 31d90d53ba..0000000000 --- a/crates/goose-local-inference/src/management.rs +++ /dev/null @@ -1,860 +0,0 @@ -use super::hf_models::{ - self, register_resolved_model, resolve_local_model_selection, resolve_local_model_spec, - resolve_model_spec, HfGgufFile, HfModelInfo, HfModelVariant, -}; -use super::local_model_registry::{ - default_settings_for_model, featured_mmproj_spec, get_registry, model_id_from_repo, - ChatTemplate, LocalModelEntry, LocalModelStorage, ModelDownloadStatus, ModelSettings, - SamplingConfig, ToolCallingMode, FEATURED_MODELS, -}; -use super::{ - available_inference_memory_bytes, builtin_chat_template_names, recommend_local_model, - InferenceRuntime, -}; -use crate::download_manager::{get_download_manager, DownloadProgress, DownloadStatus}; -use crate::huggingface_auth; -use crate::paths::Paths; -use anyhow::{anyhow, Result}; -use futures::future::join_all; -use goose_sdk_types::custom_requests::{ - LocalInferenceBuiltinChatTemplatesListResponse, LocalInferenceChatTemplate, - LocalInferenceDownloadProgressDto, LocalInferenceDownloadState, LocalInferenceHfGgufFileDto, - LocalInferenceHfModelInfoDto, LocalInferenceHfModelVariantDto, - LocalInferenceHuggingFaceRepoVariantsResponse, LocalInferenceHuggingFaceSearchResponse, - LocalInferenceModelDownloadRequest, LocalInferenceModelDownloadResponse, - LocalInferenceModelDownloadStatusDto, LocalInferenceModelDto, LocalInferenceModelSettingsDto, - LocalInferenceModelSettingsReadResponse, LocalInferenceModelSettingsUpdateResponse, - LocalInferenceModelsListResponse, LocalInferenceSamplingConfig, LocalInferenceToolCallingMode, -}; -use std::collections::HashSet; -use std::path::PathBuf; -use std::sync::{Arc, OnceLock}; - -static MANAGEMENT_RUNTIME: OnceLock> = OnceLock::new(); - -#[derive(Clone)] -struct LocalModelSelection { - repo_id: String, - backend_id: String, - variant_id: Option, -} - -pub async fn list_models() -> Result { - ensure_featured_models_current().await?; - - let runtime = management_runtime()?; - let recommended_id = recommend_local_model(&runtime); - - let loaded_model_ids = crate::loaded_model_ids() - .await - .map_err(|error| anyhow!(error.to_string()))?; - let registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - let mut models: Vec = registry - .list_models() - .iter() - .map(|entry| local_model_to_dto(entry, &recommended_id, &loaded_model_ids)) - .collect(); - - models.sort_by(|a, b| { - let a_downloaded = a.status.state == LocalInferenceDownloadState::Downloaded; - let b_downloaded = b.status.state == LocalInferenceDownloadState::Downloaded; - match (b_downloaded, a_downloaded) { - (true, false) => std::cmp::Ordering::Greater, - (false, true) => std::cmp::Ordering::Less, - _ => a.id.cmp(&b.id), - } - }); - - Ok(LocalInferenceModelsListResponse { models }) -} - -pub async fn search_huggingface_models( - query: String, - limit: Option, -) -> Result { - let limit = limit.unwrap_or(20).min(50); - let models = hf_models::search_local_models(&query, limit) - .await? - .into_iter() - .map(hf_model_info_to_dto) - .collect(); - Ok(LocalInferenceHuggingFaceSearchResponse { models }) -} - -pub async fn huggingface_repo_variants( - repo_id: String, -) -> Result { - let variants = hf_models::get_repo_local_variants(&repo_id).await?; - - let runtime = management_runtime()?; - let available_memory = available_inference_memory_bytes(&runtime); - let gguf_variants: Vec<_> = variants - .iter() - .filter(|variant| variant.backend_id == "llamacpp") - .map(|variant| hf_models::HfQuantVariant { - quantization: variant.variant_id.clone(), - size_bytes: variant.size_bytes, - filename: variant.filename.clone().unwrap_or_default(), - download_url: variant.download_url.clone().unwrap_or_default(), - description: "", - quality_rank: variant.quality_rank, - sharded: variant.sharded, - }) - .collect(); - let recommended_index = hf_models::recommend_variant(&gguf_variants, available_memory); - - let (downloaded_quants, downloaded_variants) = { - let registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - let models: Vec<_> = registry - .list_models() - .iter() - .filter(|m| m.repo_id == repo_id && m.is_downloaded()) - .collect(); - ( - models.iter().map(|m| m.quantization.clone()).collect(), - models.iter().map(|m| m.id.clone()).collect(), - ) - }; - - Ok(LocalInferenceHuggingFaceRepoVariantsResponse { - variants: variants.into_iter().map(hf_model_variant_to_dto).collect(), - recommended_index, - available_memory_bytes: available_memory, - downloaded_quants, - downloaded_variants, - }) -} - -pub async fn download_model( - req: LocalInferenceModelDownloadRequest, -) -> Result { - let selection = explicit_model_selection(&req)?; - let model_id = local_model_id_from_request(&req, selection.as_ref()).await?; - let download_id = format!("{}-model", model_id); - let download_reserved = get_download_manager().reserve_download(DownloadProgress { - model_id: download_id, - status: DownloadStatus::Downloading, - bytes_downloaded: 0, - total_bytes: 0, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: None, - task_exited: false, - })?; - if !download_reserved { - return Ok(LocalInferenceModelDownloadResponse { model_id }); - } - - if let Err(error) = register_pending_download_model(&model_id, &req, selection.as_ref()) { - mark_download_failed(&model_id, &error); - return Err(error.context("Failed to register download")); - } - - let spec = req.spec.clone(); - let selection_for_task = selection.clone(); - let model_id_for_task = model_id.clone(); - tokio::spawn(async move { - let resolved = if let Some(selection) = selection_for_task { - resolve_local_model_selection( - &selection.repo_id, - &selection.backend_id, - selection.variant_id.as_deref(), - ) - .await - } else { - resolve_local_model_spec(&spec).await - }; - match resolved { - Ok(resolved) => { - if !model_download_completed(&model_id_for_task) { - return; - } - if let Err(error) = register_resolved_model(resolved, &spec) { - mark_download_failed(&model_id_for_task, error); - } - } - Err(error) => mark_download_failed(&model_id_for_task, error), - } - }); - - Ok(LocalInferenceModelDownloadResponse { model_id }) -} - -pub fn download_progress(model_id: &str) -> Result> { - Ok(get_download_manager() - .get_progress(&format!("{}-model", model_id)) - .map(download_progress_to_dto)) -} - -pub fn cancel_download(model_id: &str) -> Result<()> { - let manager = get_download_manager(); - manager.cancel_download(&format!("{}-model", model_id))?; - let _ = manager.cancel_download(&format!("{}-mmproj", model_id)); - Ok(()) -} - -pub fn delete_model(model_id: &str) -> Result<()> { - let mut registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - if registry.get_model(model_id).is_none() { - anyhow::bail!("Model not found"); - } - registry.delete_model(model_id) -} - -pub fn model_exists(model_id: &str) -> Result { - let registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - Ok(registry.get_model(model_id).is_some()) -} - -pub async fn evict_model(model_id: &str) -> Result<()> { - crate::evict_model(model_id) - .await - .map(|_| ()) - .map_err(|error| anyhow!(error.to_string())) -} - -pub fn get_model_settings(model_id: &str) -> Result { - let registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - let settings = registry - .get_model_settings(model_id) - .ok_or_else(|| anyhow!("Model not found"))?; - Ok(LocalInferenceModelSettingsReadResponse { - settings: model_settings_to_dto(settings), - }) -} - -pub fn update_model_settings( - model_id: &str, - settings: LocalInferenceModelSettingsDto, -) -> Result { - let settings = model_settings_from_dto(settings); - let mut registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - registry.update_model_settings(model_id, settings.clone())?; - Ok(LocalInferenceModelSettingsUpdateResponse { - settings: model_settings_to_dto(&settings), - }) -} - -pub fn list_builtin_chat_templates() -> LocalInferenceBuiltinChatTemplatesListResponse { - LocalInferenceBuiltinChatTemplatesListResponse { - templates: builtin_chat_template_names(), - } -} - -fn management_runtime() -> Result> { - if let Some(runtime) = MANAGEMENT_RUNTIME.get() { - return Ok(runtime.clone()); - } - - let runtime = InferenceRuntime::get_or_init()?; - match MANAGEMENT_RUNTIME.set(runtime.clone()) { - Ok(()) => Ok(runtime), - Err(_) => Ok(MANAGEMENT_RUNTIME - .get() - .expect("local inference management runtime initialized by another thread") - .clone()), - } -} - -pub async fn ensure_featured_models_current() -> Result<()> { - let mut mmproj_downloads_needed: Vec<(String, String, PathBuf)> = Vec::new(); - - struct PendingResolve { - spec: &'static str, - repo_id: String, - quantization: String, - model_id: String, - } - let mut to_resolve = Vec::new(); - - for featured in FEATURED_MODELS { - let (repo_id, quantization) = match hf_models::parse_model_spec(featured.spec) { - Ok(parts) => parts, - Err(_) => continue, - }; - - let model_id = model_id_from_repo(&repo_id, &quantization); - - { - let registry = get_registry() - .lock() - .map_err(|_| anyhow!("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 !needs_backfill { - continue; - } - } - } - - to_resolve.push(PendingResolve { - spec: featured.spec, - repo_id, - quantization, - model_id, - }); - } - - let resolved: Vec<(PendingResolve, HfGgufFile)> = - join_all(to_resolve.into_iter().map(|pending| async move { - let hf_file = match resolve_model_spec(pending.spec).await { - Ok((_repo, file)) => file, - 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 - ), - } - } - }; - (pending, hf_file) - })) - .await; - - let entries_to_add: Vec = resolved - .into_iter() - .map(|(pending, hf_file)| { - let local_path = Paths::in_data_dir("models").join(&hf_file.filename); - let settings = default_settings_for_model(&pending.model_id); - LocalModelEntry { - id: pending.model_id, - repo_id: pending.repo_id, - filename: hf_file.filename, - quantization: pending.quantization, - local_path, - source_url: hf_file.download_url, - backend_id: settings.backend_id.clone(), - storage: LocalModelStorage::GooseManaged, - settings, - size_bytes: hf_file.size_bytes, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, - mmproj_checked: false, - shard_files: vec![], - } - }) - .collect(); - - { - let mut registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - - if !entries_to_add.is_empty() { - registry.sync_with_featured(entries_to_add); - } - - for model in registry.list_models_mut() { - model.enrich_with_featured_mmproj(); - if model.is_downloaded() { - if let Some(mmproj) = featured_mmproj_spec(&model.id) { - let path = mmproj.local_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)); - } - } - } - } - let _ = registry.save(); - } - - let dm = get_download_manager(); - let hf_token = huggingface_auth::resolve_token_async().await.ok().flatten(); - let mut started_paths = std::collections::HashSet::new(); - for (model_id, url, path) in mmproj_downloads_needed { - if !path.exists() && started_paths.insert(path.clone()) { - let download_id = format!("{}-mmproj", model_id); - let dominated_by_active = dm - .get_progress(&download_id) - .is_some_and(|p| p.status == DownloadStatus::Downloading); - if !dominated_by_active { - tracing::info!(model_id = %model_id, "Auto-downloading vision encoder for existing model"); - if let Err(e) = dm - .download_model_with_bearer_token( - download_id, - url, - path, - hf_token.clone(), - None, - ) - .await - { - tracing::warn!(model_id = %model_id, error = %e, "Failed to start mmproj download"); - } - } - } - } - - Ok(()) -} - -fn local_model_to_dto( - entry: &LocalModelEntry, - recommended_id: &str, - loaded_model_ids: &HashSet, -) -> LocalInferenceModelDto { - let vision_capable = entry.settings.vision_capable; - LocalInferenceModelDto { - id: entry.id.clone(), - repo_id: entry.repo_id.clone(), - filename: entry.filename.clone(), - quantization: entry.quantization.clone(), - size_bytes: entry.file_size(), - status: model_download_status_to_dto(entry.download_status()), - recommended: recommended_id == entry.id, - is_loaded: loaded_model_ids.contains(&entry.id), - settings: model_settings_to_dto(&entry.settings), - vision_capable, - mmproj_status: vision_capable - .then(|| model_download_status_to_dto(entry.mmproj_download_status())), - } -} - -fn model_download_status_to_dto( - status: ModelDownloadStatus, -) -> LocalInferenceModelDownloadStatusDto { - match status { - ModelDownloadStatus::NotDownloaded => LocalInferenceModelDownloadStatusDto { - state: LocalInferenceDownloadState::NotDownloaded, - ..Default::default() - }, - ModelDownloadStatus::Downloading { - progress_percent, - bytes_downloaded, - total_bytes, - speed_bps, - } => LocalInferenceModelDownloadStatusDto { - state: LocalInferenceDownloadState::Downloading, - progress_percent: Some(progress_percent), - bytes_downloaded: Some(bytes_downloaded), - total_bytes: Some(total_bytes), - speed_bps: Some(speed_bps), - }, - ModelDownloadStatus::Downloaded => LocalInferenceModelDownloadStatusDto { - state: LocalInferenceDownloadState::Downloaded, - ..Default::default() - }, - } -} - -fn download_progress_to_dto(progress: DownloadProgress) -> LocalInferenceDownloadProgressDto { - LocalInferenceDownloadProgressDto { - model_id: progress.model_id, - status: serde_json::to_value(progress.status) - .ok() - .and_then(|value| value.as_str().map(ToOwned::to_owned)) - .unwrap_or_else(|| "unknown".to_string()), - bytes_downloaded: progress.bytes_downloaded, - total_bytes: progress.total_bytes, - progress_percent: progress.progress_percent, - speed_bps: progress.speed_bps, - eta_seconds: progress.eta_seconds, - error: progress.error, - task_exited: progress.task_exited, - } -} - -fn hf_model_info_to_dto(model: HfModelInfo) -> LocalInferenceHfModelInfoDto { - LocalInferenceHfModelInfoDto { - repo_id: model.repo_id, - author: model.author, - model_name: model.model_name, - downloads: model.downloads, - gguf_files: model - .gguf_files - .into_iter() - .map(|file| LocalInferenceHfGgufFileDto { - filename: file.filename, - size_bytes: file.size_bytes, - quantization: file.quantization, - download_url: file.download_url, - }) - .collect(), - variants: model - .variants - .into_iter() - .map(hf_model_variant_to_dto) - .collect(), - } -} - -fn hf_model_variant_to_dto(variant: HfModelVariant) -> LocalInferenceHfModelVariantDto { - LocalInferenceHfModelVariantDto { - variant_id: variant.variant_id, - label: variant.label, - backend_id: variant.backend_id, - format: variant.format, - model_id: variant.model_id, - download_id: variant.download_id, - size_bytes: variant.size_bytes, - filename: variant.filename, - download_url: variant.download_url, - description: variant.description, - quality_rank: variant.quality_rank, - sharded: variant.sharded, - supported: variant.supported, - unsupported_reason: variant.unsupported_reason, - } -} - -pub fn model_settings_to_dto(settings: &ModelSettings) -> LocalInferenceModelSettingsDto { - LocalInferenceModelSettingsDto { - backend_id: settings.backend_id.clone(), - context_size: settings.context_size, - max_output_tokens: settings.max_output_tokens, - draft_model: settings.draft_model.clone(), - sampling: sampling_to_dto(&settings.sampling), - repeat_penalty: settings.repeat_penalty, - repeat_last_n: settings.repeat_last_n, - frequency_penalty: settings.frequency_penalty, - presence_penalty: settings.presence_penalty, - n_batch: settings.n_batch, - n_gpu_layers: settings.n_gpu_layers, - use_mlock: settings.use_mlock, - flash_attention: settings.flash_attention, - n_threads: settings.n_threads, - tool_calling: tool_calling_to_dto(settings.tool_calling), - chat_template: chat_template_to_dto(&settings.chat_template), - enable_thinking: settings.enable_thinking, - vision_capable: settings.vision_capable, - image_token_estimate: settings.image_token_estimate, - mmproj_size_bytes: settings.mmproj_size_bytes, - } -} - -pub fn model_settings_from_dto(settings: LocalInferenceModelSettingsDto) -> ModelSettings { - ModelSettings { - backend_id: settings.backend_id, - context_size: settings.context_size, - max_output_tokens: settings.max_output_tokens, - draft_model: settings.draft_model, - sampling: sampling_from_dto(settings.sampling), - repeat_penalty: settings.repeat_penalty, - repeat_last_n: settings.repeat_last_n, - frequency_penalty: settings.frequency_penalty, - presence_penalty: settings.presence_penalty, - n_batch: settings.n_batch, - n_gpu_layers: settings.n_gpu_layers, - use_mlock: settings.use_mlock, - flash_attention: settings.flash_attention, - n_threads: settings.n_threads, - tool_calling: tool_calling_from_dto(settings.tool_calling), - chat_template: chat_template_from_dto(settings.chat_template), - enable_thinking: settings.enable_thinking, - vision_capable: settings.vision_capable, - image_token_estimate: settings.image_token_estimate, - mmproj_size_bytes: settings.mmproj_size_bytes, - } -} - -fn sampling_to_dto(sampling: &SamplingConfig) -> LocalInferenceSamplingConfig { - match sampling { - SamplingConfig::Greedy => LocalInferenceSamplingConfig::Greedy, - SamplingConfig::Temperature { - temperature, - top_k, - top_p, - min_p, - seed, - } => LocalInferenceSamplingConfig::Temperature { - temperature: *temperature, - top_k: *top_k, - top_p: *top_p, - min_p: *min_p, - seed: *seed, - }, - SamplingConfig::MirostatV2 { tau, eta, seed } => LocalInferenceSamplingConfig::MirostatV2 { - tau: *tau, - eta: *eta, - seed: *seed, - }, - } -} - -fn sampling_from_dto(sampling: LocalInferenceSamplingConfig) -> SamplingConfig { - match sampling { - LocalInferenceSamplingConfig::Greedy => SamplingConfig::Greedy, - LocalInferenceSamplingConfig::Temperature { - temperature, - top_k, - top_p, - min_p, - seed, - } => SamplingConfig::Temperature { - temperature, - top_k, - top_p, - min_p, - seed, - }, - LocalInferenceSamplingConfig::MirostatV2 { tau, eta, seed } => { - SamplingConfig::MirostatV2 { tau, eta, seed } - } - } -} - -fn tool_calling_to_dto(mode: ToolCallingMode) -> LocalInferenceToolCallingMode { - match mode { - ToolCallingMode::Auto => LocalInferenceToolCallingMode::Auto, - ToolCallingMode::ForceNative => LocalInferenceToolCallingMode::ForceNative, - ToolCallingMode::ForceEmulated => LocalInferenceToolCallingMode::ForceEmulated, - } -} - -fn tool_calling_from_dto(mode: LocalInferenceToolCallingMode) -> ToolCallingMode { - match mode { - LocalInferenceToolCallingMode::Auto => ToolCallingMode::Auto, - LocalInferenceToolCallingMode::ForceNative => ToolCallingMode::ForceNative, - LocalInferenceToolCallingMode::ForceEmulated => ToolCallingMode::ForceEmulated, - } -} - -fn chat_template_to_dto(template: &ChatTemplate) -> LocalInferenceChatTemplate { - match template { - ChatTemplate::Embedded => LocalInferenceChatTemplate::Embedded, - ChatTemplate::Builtin { name } => { - LocalInferenceChatTemplate::Builtin { name: name.clone() } - } - ChatTemplate::CustomInline { template } => LocalInferenceChatTemplate::CustomInline { - template: template.clone(), - }, - } -} - -fn chat_template_from_dto(template: LocalInferenceChatTemplate) -> ChatTemplate { - match template { - LocalInferenceChatTemplate::Embedded => ChatTemplate::Embedded, - LocalInferenceChatTemplate::Builtin { name } => ChatTemplate::Builtin { name }, - LocalInferenceChatTemplate::CustomInline { template } => { - ChatTemplate::CustomInline { template } - } - } -} - -fn explicit_model_selection( - req: &LocalInferenceModelDownloadRequest, -) -> Result> { - if let Some(backend_id) = req.backend_id.as_deref() { - let (repo_id, parsed_variant_id) = hf_models::parse_model_spec(&req.spec) - .map(|(repo_id, quantization)| (repo_id, Some(quantization))) - .unwrap_or_else(|_| (req.spec.clone(), None)); - let variant_id = req.variant_id.clone().or(parsed_variant_id); - match backend_id { - "mlx" | "llamacpp" => Ok(Some(LocalModelSelection { - repo_id, - backend_id: backend_id.to_string(), - variant_id, - })), - _ => anyhow::bail!("Unknown local inference backend '{}'", backend_id), - } - } else { - Ok(None) - } -} - -async fn local_model_id_from_request( - req: &LocalInferenceModelDownloadRequest, - selection: Option<&LocalModelSelection>, -) -> Result { - if let Some(selection) = selection { - return match selection.backend_id.as_str() { - "mlx" => Ok(selection.repo_id.clone()), - "llamacpp" => { - let quantization = selection.variant_id.as_deref().ok_or_else(|| { - anyhow!( - "llama.cpp model '{}' is missing a quantization", - selection.repo_id - ) - })?; - Ok(model_id_from_repo(&selection.repo_id, quantization)) - } - _ => anyhow::bail!("Unknown local inference backend '{}'", selection.backend_id), - }; - } - - if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { - return Ok(model_id_from_repo(&repo_id, &quantization)); - } - - let variants = hf_models::get_repo_local_variants(&req.spec).await?; - let has_llamacpp = variants - .iter() - .any(|variant| variant.backend_id == "llamacpp"); - let mlx_variants: Vec<_> = variants - .iter() - .filter(|variant| variant.backend_id == "mlx") - .collect(); - if mlx_variants.len() == 1 && !has_llamacpp { - Ok(req.spec.clone()) - } else { - anyhow::bail!( - "Model spec '{}' is ambiguous; choose one of: {}", - req.spec, - variants - .iter() - .map(|variant| variant.download_id.as_str()) - .collect::>() - .join(", ") - ) - } -} - -fn mark_download_failed(model_id: &str, error: impl std::fmt::Display) { - let manager = get_download_manager(); - let download_id = format!("{}-model", model_id); - if manager.get_progress(&download_id).is_none() { - manager.set_progress(DownloadProgress { - model_id: download_id.clone(), - status: DownloadStatus::Failed, - bytes_downloaded: 0, - total_bytes: 0, - progress_percent: 0.0, - speed_bps: None, - eta_seconds: None, - error: Some(error.to_string()), - task_exited: true, - }); - return; - } - - manager.update_progress(&download_id, |progress| { - if progress.status != DownloadStatus::Cancelled { - progress.status = DownloadStatus::Failed; - progress.error = Some(error.to_string()); - } - progress.task_exited = true; - }); -} - -fn model_download_completed(model_id: &str) -> bool { - get_download_manager() - .get_progress(&format!("{}-model", model_id)) - .is_some_and(|progress| progress.status == DownloadStatus::Completed) -} - -fn register_pending_download_model( - model_id: &str, - req: &LocalInferenceModelDownloadRequest, - selection: Option<&LocalModelSelection>, -) -> Result<()> { - let (repo_id, backend_id, variant_id) = if let Some(selection) = selection { - ( - selection.repo_id.clone(), - selection.backend_id.clone(), - selection - .variant_id - .clone() - .unwrap_or_else(|| "default".to_string()), - ) - } else if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { - (repo_id, "llamacpp".to_string(), quantization) - } else { - (req.spec.clone(), "mlx".to_string(), "default".to_string()) - }; - - let mut registry = get_registry() - .lock() - .map_err(|_| anyhow!("Failed to acquire registry lock"))?; - if registry.has_model(model_id) { - return Ok(()); - } - - let mut settings = default_settings_for_model(model_id); - if backend_id != "llamacpp" { - settings.backend_id = Some(backend_id.clone()); - } - - let filename = variant_id.clone(); - registry.add_model(LocalModelEntry { - id: model_id.to_string(), - repo_id, - filename: filename.clone(), - quantization: variant_id, - local_path: Paths::in_data_dir("models").join(filename), - source_url: req.spec.clone(), - backend_id: settings.backend_id.clone(), - storage: LocalModelStorage::HuggingFaceCache, - settings, - size_bytes: 0, - mmproj_path: None, - mmproj_source_url: None, - mmproj_size_bytes: 0, - mmproj_checked: false, - shard_files: vec![], - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn settings_round_trip_preserves_defaults() { - let settings = ModelSettings::default(); - let dto = model_settings_to_dto(&settings); - let round_trip = model_settings_from_dto(dto); - assert_eq!(round_trip.repeat_penalty, settings.repeat_penalty); - assert_eq!(round_trip.repeat_last_n, settings.repeat_last_n); - assert_eq!(round_trip.enable_thinking, settings.enable_thinking); - assert_eq!( - round_trip.image_token_estimate, - settings.image_token_estimate - ); - } - - #[tokio::test] - async fn explicit_llamacpp_selection_derives_quantized_model_id() { - let req = LocalInferenceModelDownloadRequest { - spec: "test/repo".to_string(), - backend_id: Some("llamacpp".to_string()), - variant_id: Some("Q4_K_M".to_string()), - }; - let selection = explicit_model_selection(&req).unwrap(); - let model_id = local_model_id_from_request(&req, selection.as_ref()) - .await - .unwrap(); - assert_eq!(model_id, "test/repo:Q4_K_M"); - } -} diff --git a/crates/goose-local-inference/src/mlx.rs b/crates/goose-local-inference/src/mlx.rs deleted file mode 100644 index 6b6ddb0fd4..0000000000 --- a/crates/goose-local-inference/src/mlx.rs +++ /dev/null @@ -1,1044 +0,0 @@ -#[cfg(feature = "mlx")] -mod imp { - use std::any::Any; - use std::path::{Path, PathBuf}; - - use safemlx::transforms::eval; - use safemlx::{random, Array, Device, DeviceType, Stream}; - use safemlx_lm::gemma4_mtp::generate_gemma4_mtp; - use safemlx_lm::models::{gemma4_assistant::load_gemma4_assistant_model, LoadedModel, Model}; - use safemlx_lm_utils::tokenizer::{Chat, Conversation, Role, Tokenizer}; - use serde_json::json; - - use crate::backend::{BackendLoadedModel, LocalGenerationRequest, LocalInferenceBackend}; - use crate::local_model_registry::{ModelSettings, ToolCallingMode}; - use crate::native_tool_parsing::message_from_native_tool_text; - use crate::provider_utils::filter_extensions_from_system_prompt; - use crate::thinking_output::ThinkingOutputFilter; - use crate::tool_emulation::{ - build_emulator_tool_description, load_tiny_model_prompt, message_for_emulator_action, - StreamingEmulatorParser, CODE_EXECUTION_TOOL, - }; - use crate::{extract_text_content, ResolvedModelPaths}; - use goose_provider_types::conversation::message::{Message, MessageContent}; - use goose_provider_types::conversation::token_usage::{ - DraftStats, ProviderStats, ProviderUsage, Usage, - }; - use goose_provider_types::errors::ProviderError; - use goose_provider_types::formats::openai; - use goose_provider_types::images::ImageFormat; - use goose_provider_types::request_log::LoggerHandleExt; - - pub(crate) const MLX_BACKEND_ID: &str = "mlx"; - - pub(crate) struct MlxBackend; - - impl MlxBackend { - pub(crate) fn new() -> Self { - Self - } - } - - impl LocalInferenceBackend for MlxBackend { - fn id(&self) -> &'static str { - MLX_BACKEND_ID - } - - fn load_model( - &self, - model_id: &str, - resolved: &ResolvedModelPaths, - _settings: &ModelSettings, - ) -> Result, ProviderError> { - if !resolved.model_path.exists() { - return Err(ProviderError::ExecutionError(format!( - "Model not downloaded: {}. Please download it from Settings > Local Inference.", - model_id - ))); - } - - let model_dir = model_dir_from_path(&resolved.model_path)?; - let stream = Stream::new_with_device(&Device::new(DeviceType::Gpu, 0)); - let weights_stream = Stream::new_with_device(&Device::new(DeviceType::Cpu, 0)); - let model = - LoadedModel::load(&model_dir, &stream, &weights_stream).map_err(mlx_error)?; - let tokenizer = - Tokenizer::from_file(model_dir.join("tokenizer.json")).map_err(mlx_error)?; - tracing::info!( - backend = self.id(), - model_id, - model_type = model.model_type(), - "MLX model loaded successfully" - ); - let stop_token_ids = mlx_stop_token_ids(&model, &model_dir); - Ok(Box::new(MlxLoadedModel { - model, - tokenizer, - model_dir, - stop_token_ids, - })) - } - - fn generate( - &self, - loaded: &mut dyn BackendLoadedModel, - request: LocalGenerationRequest<'_>, - ) -> Result<(), ProviderError> { - let loaded = loaded - .as_any_mut() - .downcast_mut::() - .ok_or_else(|| { - ProviderError::ExecutionError("Loaded model backend mismatch".to_string()) - })?; - - let stream = Stream::new_with_device(&Device::new(DeviceType::Gpu, 0)); - let tool_mode = if request.tools.is_empty() { - ToolMode::None - } else { - match request.settings.tool_calling { - ToolCallingMode::ForceNative => ToolMode::Native, - ToolCallingMode::Auto | ToolCallingMode::ForceEmulated => ToolMode::Emulated { - code_mode_enabled: request - .tools - .iter() - .any(|t| t.name == CODE_EXECUTION_TOOL), - }, - } - }; - let prompt = build_prompt( - &mut loaded.model, - &request.model_name, - request.system, - request.messages, - request.tools, - tool_mode, - )?; - let prompt_tokens = loaded.model.encode(&prompt, false).map_err(mlx_error)?; - if prompt_tokens.len() >= request.context_limit && request.context_limit > 0 { - return Err(ProviderError::ContextLengthExceeded(format!( - "Prompt ({} tokens) exceeds context limit ({} tokens). Try reducing conversation length.", - prompt_tokens.len(), request.context_limit - ))); - } - - let prompt_array = loaded - .model - .encode_to_array(&prompt, false, &stream) - .map_err(mlx_error)?; - let max_tokens = mlx_max_tokens( - request.settings, - request.max_tokens, - request.context_limit, - prompt_tokens.len(), - ); - let (settings_temp, seed) = sampling(request.settings); - let temp = request.temperature.unwrap_or(settings_temp); - let prng_key = prng_key(temp, seed)?; - let eos_token_ids = loaded.stop_token_ids.clone(); - let generation_started = std::time::Instant::now(); - let MlxGeneration { - generated_ids, - generated_text, - draft_stats, - time_to_first_token_ms, - streamed_response, - } = if let Some(draft_model_path) = &request.draft_model_path { - if matches!(loaded.model.model_mut(), Model::Gemma4(_)) { - let weights_stream = Stream::new_with_device(&Device::new(DeviceType::Cpu, 0)); - let mut assistant = - load_gemma4_assistant_model(draft_model_path, &stream, &weights_stream) - .map_err(|error| { - mlx_error(format!("failed to load MLX draft model: {error}")) - })?; - let target = match loaded.model.model_mut() { - Model::Gemma4(target) => target, - _ => unreachable!(), - }; - let (ids, stats) = generate_gemma4_mtp( - target, - &mut assistant, - &prompt_array, - &eos_token_ids, - max_tokens, - temp, - prng_key, - &stream, - ) - .map_err(mlx_error)?; - let generated_text = loaded.tokenizer.decode(&ids, true).map_err(mlx_error)?; - MlxGeneration { - generated_ids: ids, - generated_text, - draft_stats: Some(DraftStats { - model: Some(draft_model_path.display().to_string()), - draft_tokens: stats.draft_tokens, - accepted_tokens: stats.accepted_tokens, - target_tokens: stats.target_tokens, - rounds: stats.rounds, - accept_rate: stats.accept_rate(), - }), - time_to_first_token_ms: None, - streamed_response: false, - } - } else { - generate_single_model( - &mut loaded.model, - &loaded.tokenizer, - &prompt_array, - &eos_token_ids, - max_tokens, - temp, - prng_key, - &stream, - generation_started, - MlxStreamEmitter::new( - request.message_id, - tool_mode, - request.settings.enable_thinking, - &prompt, - request.tx, - ), - )? - } - } else { - generate_single_model( - &mut loaded.model, - &loaded.tokenizer, - &prompt_array, - &eos_token_ids, - max_tokens, - temp, - prng_key, - &stream, - generation_started, - MlxStreamEmitter::new( - request.message_id, - tool_mode, - request.settings.enable_thinking, - &prompt, - request.tx, - ), - )? - }; - - if !streamed_response { - emit_generated_response( - &generated_text, - &prompt, - request.settings.enable_thinking, - request.message_id, - tool_mode, - request.tx, - )?; - } - - let output_tokens = generated_ids.len() as i32; - let input_tokens = prompt_tokens.len() as i32; - let usage = Usage::new( - Some(input_tokens), - Some(output_tokens), - Some(input_tokens + output_tokens), - ); - let log_json = serde_json::json!({ - "path": "mlx", - "model_dir": loaded.model_dir, - "prompt_tokens": input_tokens, - "output_tokens": output_tokens, - "model_load_ms": request.model_load_ms, - "time_to_first_token_ms": time_to_first_token_ms, - "elapsed_ms": generation_started.elapsed().as_millis() as u64, - "generated_text": generated_text, - "draft": draft_stats, - }); - let _ = request.log.write(&log_json, Some(&usage)); - let stats = ProviderStats { - time_to_first_token_ms, - model_load_ms: request.model_load_ms, - elapsed_ms: Some(generation_started.elapsed().as_millis() as u64), - output_tokens: Some(generated_ids.len()), - draft: draft_stats, - }; - let provider_usage = ProviderUsage::new(request.model_name, usage).with_stats(stats); - let _ = request.tx.blocking_send(Ok((None, Some(provider_usage)))); - Ok(()) - } - - fn available_memory_bytes(&self) -> u64 { - 0 - } - } - - #[derive(Clone, Copy)] - enum ToolMode { - None, - Native, - Emulated { code_mode_enabled: bool }, - } - - struct MlxGeneration { - generated_ids: Vec, - generated_text: String, - draft_stats: Option, - time_to_first_token_ms: Option, - streamed_response: bool, - } - - struct MlxLoadedModel { - model: LoadedModel, - tokenizer: Tokenizer, - model_dir: PathBuf, - stop_token_ids: Vec, - } - - impl BackendLoadedModel for MlxLoadedModel { - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - fn model_dir_from_path(path: &Path) -> Result { - if path.is_dir() { - Ok(path.to_path_buf()) - } else { - path.parent() - .map(Path::to_path_buf) - .ok_or_else(|| mlx_error("MLX model path has no parent directory")) - } - } - - fn mlx_stop_token_ids(model: &LoadedModel, model_dir: &Path) -> Vec { - let mut ids = model.eos_token_ids().to_vec(); - for id in generation_config_eos_token_ids(model_dir) { - if !ids.contains(&id) { - ids.push(id); - } - } - ids - } - - fn generation_config_eos_token_ids(model_dir: &Path) -> Vec { - let Ok(config_json) = std::fs::read_to_string(model_dir.join("generation_config.json")) - else { - return Vec::new(); - }; - let Ok(config) = serde_json::from_str::(&config_json) else { - return Vec::new(); - }; - match config.get("eos_token_id") { - Some(value) => token_id_or_ids(value), - None => Vec::new(), - } - } - - fn token_id_or_ids(value: &serde_json::Value) -> Vec { - if let Some(id) = value.as_u64().and_then(|id| u32::try_from(id).ok()) { - return vec![id]; - } - value - .as_array() - .map(|ids| { - ids.iter() - .filter_map(|id| id.as_u64().and_then(|id| u32::try_from(id).ok())) - .collect() - }) - .unwrap_or_default() - } - - fn build_prompt( - model: &mut LoadedModel, - model_name: &str, - system: &str, - messages: &[Message], - tools: &[rmcp::model::Tool], - tool_mode: ToolMode, - ) -> Result { - match tool_mode { - ToolMode::Native => { - let conversations = openai_messages(system, messages); - let tool_specs = openai::format_tools(tools) - .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; - if let Some(prompt) = model - .apply_chat_template_json([conversations], Some(&tool_specs), true) - .map_err(mlx_error)? - { - return Ok(prompt); - } - - Ok(render_prompt(system, messages)) - } - ToolMode::Emulated { code_mode_enabled } => { - let system_prompt = format!( - "{}{}", - load_tiny_model_prompt(), - build_emulator_tool_description(tools, code_mode_enabled) - ); - if is_gemma4(model) { - let conversations = gemma4_messages_with_system(&system_prompt, messages); - if let Some(prompt) = model - .apply_chat_template_json([conversations], None, true) - .map_err(mlx_error)? - { - return Ok(prompt); - } - } - - let conversations = chat_conversations(&system_prompt, messages); - if let Some(prompt) = model - .apply_chat_template([Chat::Owned(conversations)], None, true) - .map_err(mlx_error)? - { - return Ok(prompt); - } - - Ok(render_prompt(&system_prompt, messages)) - } - ToolMode::None => { - if is_gemma4(model) { - let conversations = gemma4_messages(model_name, system, messages); - if let Some(prompt) = model - .apply_chat_template_json([conversations], None, true) - .map_err(mlx_error)? - { - return Ok(prompt); - } - } - - let conversations = chat_conversations(system, messages); - if let Some(prompt) = model - .apply_chat_template([Chat::Owned(conversations)], None, true) - .map_err(mlx_error)? - { - return Ok(prompt); - } - - Ok(render_prompt(system, messages)) - } - } - } - - fn generate_single_model( - model: &mut LoadedModel, - tokenizer: &Tokenizer, - prompt_array: &Array, - eos_token_ids: &[u32], - max_tokens: usize, - temp: f32, - prng_key: Option, - stream: &Stream, - generation_started: std::time::Instant, - mut emitter: MlxStreamEmitter<'_>, - ) -> Result { - let mut cache = model.new_cache(); - let mut generated_ids = Vec::new(); - let mut streamed_text = String::new(); - let mut time_to_first_token_ms = None; - let stream_generation = emitter.can_stream(); - let mut decode_stream = tokenizer.decode_stream(true); - { - let generator = model - .generate_with_cache(&mut cache, temp, prompt_array, prng_key, stream) - .take(max_tokens); - for token in generator { - let token = token.map_err(mlx_error)?; - eval([&token]).map_err(mlx_error)?; - let token_id = token.item::(stream); - time_to_first_token_ms.get_or_insert_with(|| { - u64::try_from(generation_started.elapsed().as_millis()).unwrap_or(u64::MAX) - }); - if eos_token_ids.contains(&token_id) { - break; - } - generated_ids.push(token_id); - if stream_generation { - if let Some(piece) = decode_stream.step(token_id).map_err(mlx_error)? { - if !piece.is_empty() { - let should_continue = emitter.push_text(&piece)?; - streamed_text.push_str(&piece); - if !should_continue { - break; - } - } - } - } - } - } - let generated_text = tokenizer.decode(&generated_ids, true).map_err(mlx_error)?; - let streamed_response = if stream_generation { - match final_stream_suffix(&generated_text, &streamed_text)? { - Some(suffix) => { - if !suffix.is_empty() { - emitter.push_text(suffix)?; - } - true - } - None => false, - } - } else { - false - }; - if streamed_response { - emitter.finish()?; - } - Ok(MlxGeneration { - generated_ids, - generated_text, - draft_stats: None, - time_to_first_token_ms, - streamed_response, - }) - } - - fn final_stream_suffix<'a>( - generated_text: &'a str, - streamed_text: &str, - ) -> Result, ProviderError> { - if streamed_text.is_empty() { - return Ok(None); - } - - generated_text - .strip_prefix(streamed_text) - .map(Some) - .ok_or_else(|| mlx_error("streamed MLX decode did not match final tokenizer decode")) - } - - fn mlx_max_tokens( - settings: &ModelSettings, - request_max_tokens: Option, - context_limit: usize, - prompt_tokens: usize, - ) -> usize { - let configured_max = settings - .max_output_tokens - .or_else(|| request_max_tokens.and_then(|tokens| usize::try_from(tokens).ok())); - if context_limit == 0 { - return configured_max.unwrap_or(4096); - } - - let context_headroom = context_limit.saturating_sub(prompt_tokens); - configured_max - .map(|max| max.min(context_headroom)) - .unwrap_or(context_headroom) - } - - fn is_gemma4(model: &LoadedModel) -> bool { - matches!(model.model_type(), "gemma4" | "gemma4_text") - } - - fn gemma4_messages( - model_name: &str, - system: &str, - messages: &[Message], - ) -> Vec { - let system = gemma4_system_prompt(model_name, system); - gemma4_messages_with_optional_system(system.as_deref(), messages) - } - - fn gemma4_messages_with_system(system: &str, messages: &[Message]) -> Vec { - gemma4_messages_with_optional_system(Some(system), messages) - } - - fn gemma4_messages_with_optional_system( - system: Option<&str>, - messages: &[Message], - ) -> Vec { - let mut values = Vec::new(); - if let Some(system) = system.map(str::trim).filter(|system| !system.is_empty()) { - values.push(json!({ - "role": "system", - "content": system, - })); - } - - for message in messages.iter().filter(|message| message.is_agent_visible()) { - let text = extract_text_content(message); - if text.trim().is_empty() { - continue; - } - - match message.role { - rmcp::model::Role::User => values.push(json!({ - "role": "user", - "content": [{"type": "text", "text": text.trim(), "content": text.trim()}], - })), - rmcp::model::Role::Assistant => values.push(json!({ - "role": "assistant", - "content": text.trim(), - })), - } - } - - values - } - - fn gemma4_system_prompt(model_name: &str, system: &str) -> Option { - if should_use_tiny_system_prompt(model_name) { - return Some(load_tiny_model_prompt()); - } - - let filtered = filter_extensions_from_system_prompt(system); - let system = filtered.trim(); - if system.is_empty() { - None - } else { - Some(system.to_string()) - } - } - - fn should_use_tiny_system_prompt(model_name: &str) -> bool { - estimate_model_size_billions(model_name).is_some_and(|size| size <= 4.0) - } - - fn estimate_model_size_billions(model_name: &str) -> Option { - let normalized = model_name.to_ascii_lowercase().replace('-', "_"); - for part in normalized.split('_') { - if let Some(value) = part.strip_suffix('b') { - if let Ok(size) = value.parse::() { - return Some(size); - } - } - if let Some(value) = part - .strip_prefix('e') - .and_then(|value| value.strip_suffix('b')) - { - if let Ok(size) = value.parse::() { - return Some(size); - } - } - } - None - } - - fn openai_messages(system: &str, messages: &[Message]) -> Vec { - let mut values = vec![serde_json::json!({ - "role": "system", - "content": system, - })]; - values.extend(openai::format_messages(messages, &ImageFormat::OpenAi)); - values - } - - fn chat_conversations(system: &str, messages: &[Message]) -> Vec> { - let mut conversations = Vec::new(); - if !system.trim().is_empty() { - conversations.push(Conversation { - role: Role::System, - content: system.trim().to_string(), - }); - } - for message in messages.iter().filter(|message| message.is_agent_visible()) { - let role = match message.role { - rmcp::model::Role::User => Role::User, - rmcp::model::Role::Assistant => Role::Assistant, - }; - let text = extract_text_content(message); - if !text.trim().is_empty() { - conversations.push(Conversation { - role, - content: text.trim().to_string(), - }); - } - } - conversations - } - - fn emit_generated_response( - generated_text: &str, - generation_prompt: &str, - enable_thinking: bool, - message_id: &str, - tool_mode: ToolMode, - tx: &tokio::sync::mpsc::Sender< - Result<(Option, Option), ProviderError>, - >, - ) -> Result<(), ProviderError> { - if generated_text.is_empty() { - return Ok(()); - } - - let (content, thinking) = - split_generated_thinking(generated_text, generation_prompt, enable_thinking); - - match tool_mode { - ToolMode::None => { - emit_assistant_message(message_id, &thinking, &content, tx)?; - } - ToolMode::Native => { - if let Some(mut message) = message_from_native_tool_text(&content, message_id)? { - prepend_thinking(&mut message, &thinking); - tx.blocking_send(Ok((Some(message), None))).map_err(|_| { - ProviderError::ExecutionError("Failed to stream MLX response".to_string()) - })?; - } else { - emit_assistant_message(message_id, &thinking, &content, tx)?; - } - } - ToolMode::Emulated { code_mode_enabled } => { - emit_assistant_message(message_id, &thinking, "", tx)?; - let mut parser = StreamingEmulatorParser::new(code_mode_enabled); - let mut actions = parser.process_chunk(&content); - actions.extend(parser.flush()); - - for action in actions { - let (message, _) = message_for_emulator_action(&action, message_id); - tx.blocking_send(Ok((Some(message), None))).map_err(|_| { - ProviderError::ExecutionError("Failed to stream MLX response".to_string()) - })?; - } - } - } - Ok(()) - } - - struct MlxStreamEmitter<'a> { - message_id: &'a str, - tool_mode: ToolMode, - tx: &'a tokio::sync::mpsc::Sender< - Result<(Option, Option), ProviderError>, - >, - output_filter: ThinkingOutputFilter, - emulator_parser: Option, - stop_after_tool_call: bool, - } - - impl<'a> MlxStreamEmitter<'a> { - fn new( - message_id: &'a str, - tool_mode: ToolMode, - enable_thinking: bool, - generation_prompt: &str, - tx: &'a tokio::sync::mpsc::Sender< - Result<(Option, Option), ProviderError>, - >, - ) -> Self { - let emulator_parser = match tool_mode { - ToolMode::Emulated { code_mode_enabled } => { - Some(StreamingEmulatorParser::new(code_mode_enabled)) - } - ToolMode::None | ToolMode::Native => None, - }; - Self { - message_id, - tool_mode, - tx, - output_filter: ThinkingOutputFilter::new(enable_thinking, generation_prompt), - emulator_parser, - stop_after_tool_call: false, - } - } - - fn can_stream(&self) -> bool { - !matches!(self.tool_mode, ToolMode::Native) - } - - fn push_text(&mut self, text: &str) -> Result { - let filtered = self.output_filter.push_text(text); - if !filtered.content.is_empty() { - self.emit_content(&filtered.content)?; - } - Ok(!self.stop_after_tool_call) - } - - fn finish(&mut self) -> Result<(), ProviderError> { - self.flush_filtered_output()?; - let actions = self - .emulator_parser - .as_mut() - .map(StreamingEmulatorParser::flush) - .unwrap_or_default(); - for action in actions { - let (message, is_tool) = message_for_emulator_action(&action, self.message_id); - if is_tool { - self.flush_filtered_output()?; - } - self.send(message)?; - self.stop_after_tool_call |= is_tool; - if is_tool { - break; - } - } - Ok(()) - } - - fn flush_filtered_output(&mut self) -> Result<(), ProviderError> { - let filtered = self.output_filter.finish(); - if !filtered.thinking.is_empty() { - let mut message = Message::assistant().with_thinking(filtered.thinking, ""); - message.id = Some(self.message_id.to_string()); - self.send(message)?; - } - if !filtered.content.is_empty() { - self.emit_content(&filtered.content)?; - } - Ok(()) - } - - fn emit_content(&mut self, content: &str) -> Result<(), ProviderError> { - match self.tool_mode { - ToolMode::None => { - let mut message = Message::assistant().with_text(content); - message.id = Some(self.message_id.to_string()); - self.send(message) - } - ToolMode::Emulated { .. } => { - let actions = self - .emulator_parser - .as_mut() - .map(|parser| parser.process_chunk(content)) - .unwrap_or_default(); - for action in actions { - let (message, is_tool) = - message_for_emulator_action(&action, self.message_id); - if is_tool { - self.flush_filtered_output()?; - } - self.send(message)?; - self.stop_after_tool_call |= is_tool; - if is_tool { - break; - } - } - Ok(()) - } - ToolMode::Native => Ok(()), - } - } - - fn send(&self, message: Message) -> Result<(), ProviderError> { - self.tx - .blocking_send(Ok((Some(message), None))) - .map_err(|_| { - ProviderError::ExecutionError("Failed to stream MLX response".to_string()) - }) - } - } - - fn split_generated_thinking( - generated_text: &str, - generation_prompt: &str, - enable_thinking: bool, - ) -> (String, String) { - let mut filter = ThinkingOutputFilter::new(enable_thinking, generation_prompt); - let mut filtered = filter.push_text(generated_text); - let final_filtered = filter.finish(); - filtered.content.push_str(&final_filtered.content); - filtered.thinking.push_str(&final_filtered.thinking); - (filtered.content, filtered.thinking) - } - - fn emit_assistant_message( - message_id: &str, - thinking: &str, - content: &str, - tx: &tokio::sync::mpsc::Sender< - Result<(Option, Option), ProviderError>, - >, - ) -> Result<(), ProviderError> { - if thinking.is_empty() && content.is_empty() { - return Ok(()); - } - - let mut message = Message::assistant(); - if !thinking.is_empty() { - message = message.with_thinking(thinking, ""); - } - if !content.is_empty() { - message = message.with_text(content); - } - message.id = Some(message_id.to_string()); - tx.blocking_send(Ok((Some(message), None))) - .map_err(|_| ProviderError::ExecutionError("Failed to stream MLX response".to_string())) - } - - fn prepend_thinking(message: &mut Message, thinking: &str) { - if !thinking.is_empty() { - message - .content - .insert(0, MessageContent::thinking(thinking, "")); - } - } - - fn sampling(settings: &ModelSettings) -> (f32, Option) { - match &settings.sampling { - crate::local_model_registry::SamplingConfig::Greedy => (0.0, None), - crate::local_model_registry::SamplingConfig::Temperature { - temperature, seed, .. - } => (*temperature, *seed), - crate::local_model_registry::SamplingConfig::MirostatV2 { seed, .. } => (0.0, *seed), - } - } - - fn prng_key(temp: f32, seed: Option) -> Result, ProviderError> { - if temp == 0.0 { - return Ok(None); - } - random::key(seed.unwrap_or(0) as u64) - .map(Some) - .map_err(mlx_error) - } - - fn render_prompt(system: &str, messages: &[Message]) -> String { - let mut prompt = String::new(); - if !system.trim().is_empty() { - prompt.push_str("System: "); - prompt.push_str(system.trim()); - prompt.push('\n'); - } - for message in messages.iter().filter(|message| message.is_agent_visible()) { - let role = match message.role { - rmcp::model::Role::User => "User", - rmcp::model::Role::Assistant => "Assistant", - }; - let text = extract_text_content(message); - if !text.trim().is_empty() { - prompt.push_str(role); - prompt.push_str(": "); - prompt.push_str(text.trim()); - prompt.push('\n'); - } - } - prompt.push_str("Assistant: "); - prompt - } - - fn mlx_error(error: impl std::fmt::Display) -> ProviderError { - ProviderError::ExecutionError(format!("MLX backend error: {}", error)) - } - - #[cfg(test)] - mod tests { - use super::{ - final_stream_suffix, mlx_max_tokens, split_generated_thinking, token_id_or_ids, - }; - use crate::local_model_registry::ModelSettings; - use serde_json::json; - - #[test] - fn extracts_thinking_started_by_generation_prompt() { - let (content, thinking) = split_generated_thinking( - "hidden reasoningvisible answer", - "<|im_start|>assistant\n\n", - true, - ); - - assert_eq!(thinking.trim(), "hidden reasoning"); - assert_eq!(content, "visible answer"); - } - - #[test] - fn leaves_think_tags_as_content_when_thinking_disabled() { - let generated = "hidden reasoningvisible answer"; - let (content, thinking) = - split_generated_thinking(generated, "<|im_start|>assistant\n\n", false); - - assert!(thinking.is_empty()); - assert_eq!(content, generated); - } - - #[test] - fn parses_single_and_multiple_eos_token_ids() { - assert_eq!(token_id_or_ids(&json!(248044)), vec![248044]); - assert_eq!( - token_id_or_ids(&json!([248046, 248044])), - vec![248046, 248044] - ); - } - - #[test] - fn final_stream_suffix_flushes_append_only_suffix() { - assert_eq!( - final_stream_suffix("hello world", "hello").unwrap(), - Some(" world") - ); - } - - #[test] - fn final_stream_suffix_does_not_replay_fully_streamed_text() { - assert_eq!( - final_stream_suffix("run tool", "run tool").unwrap(), - Some("") - ); - } - - #[test] - fn final_stream_suffix_allows_unstreamed_fallback() { - assert_eq!(final_stream_suffix("hello world", "").unwrap(), None); - } - - #[test] - fn final_stream_suffix_rejects_rewritten_streamed_prefix() { - assert!(final_stream_suffix("corrected response", "stale prefix").is_err()); - } - - #[test] - fn max_tokens_defaults_to_context_headroom() { - let settings = ModelSettings::default(); - - assert_eq!(mlx_max_tokens(&settings, None, 128_000, 1_752), 126_248); - } - - #[test] - fn max_tokens_respects_configured_caps() { - let mut settings = ModelSettings::default(); - settings.max_output_tokens = Some(2048); - - assert_eq!(mlx_max_tokens(&settings, None, 128_000, 1_752), 2048); - - let settings = ModelSettings::default(); - assert_eq!(mlx_max_tokens(&settings, Some(1024), 128_000, 1_752), 1024); - } - } -} - -#[cfg(not(feature = "mlx"))] -mod imp { - use crate::backend::{BackendLoadedModel, LocalGenerationRequest, LocalInferenceBackend}; - use crate::local_model_registry::ModelSettings; - use crate::ResolvedModelPaths; - use goose_provider_types::errors::ProviderError; - - pub(crate) const MLX_BACKEND_ID: &str = "mlx"; - - pub(crate) struct MlxBackend; - - impl MlxBackend { - pub(crate) fn new() -> Self { - Self - } - } - - impl LocalInferenceBackend for MlxBackend { - fn id(&self) -> &'static str { - MLX_BACKEND_ID - } - - fn load_model( - &self, - _model_id: &str, - _resolved: &ResolvedModelPaths, - _settings: &ModelSettings, - ) -> Result, ProviderError> { - Err(ProviderError::ExecutionError( - "MLX backend support was not compiled in. Rebuild with the `mlx` feature." - .to_string(), - )) - } - - fn generate( - &self, - _loaded: &mut dyn BackendLoadedModel, - _request: LocalGenerationRequest<'_>, - ) -> Result<(), ProviderError> { - Err(ProviderError::ExecutionError( - "MLX backend support was not compiled in. Rebuild with the `mlx` feature." - .to_string(), - )) - } - - fn available_memory_bytes(&self) -> u64 { - 0 - } - } -} - -pub(crate) use imp::{MlxBackend, MLX_BACKEND_ID}; diff --git a/crates/goose-local-inference/src/paths.rs b/crates/goose-local-inference/src/paths.rs deleted file mode 100644 index 4a79cbf306..0000000000 --- a/crates/goose-local-inference/src/paths.rs +++ /dev/null @@ -1,88 +0,0 @@ -use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; -use std::path::PathBuf; - -pub struct Paths; - -impl Paths { - fn get_dir(dir_type: DirType) -> PathBuf { - if let Ok(test_root) = std::env::var("GOOSE_PATH_ROOT") { - let base = PathBuf::from(test_root); - match dir_type { - DirType::Config => base.join("config"), - DirType::Data => base.join("data"), - DirType::State => base.join("state"), - DirType::Plugins => base.join(".agents").join("plugins"), - DirType::Agents => base.join(".agents").join("agents"), - DirType::AgentsHome => base.join(".agents"), - } - } else { - // NOTE: "Block" is kept here for backwards compatibility with existing - // user config/data directories (e.g. ~/Library/Application Support/Block/goose/). - // Changing this would orphan existing installations. - let strategy = choose_app_strategy(AppStrategyArgs { - top_level_domain: "Block".to_string(), - author: "Block".to_string(), - app_name: "goose".to_string(), - }) - .expect("goose requires a home dir"); - - match dir_type { - DirType::Config => strategy.config_dir(), - DirType::Data => strategy.data_dir(), - DirType::State => strategy.state_dir().unwrap_or(strategy.data_dir()), - DirType::Plugins => strategy.home_dir().join(".agents").join("plugins"), - DirType::Agents => strategy.home_dir().join(".agents").join("agents"), - DirType::AgentsHome => strategy.home_dir().join(".agents"), - } - } - } - - pub fn config_dir() -> PathBuf { - Self::get_dir(DirType::Config) - } - - pub fn data_dir() -> PathBuf { - Self::get_dir(DirType::Data) - } - - pub fn state_dir() -> PathBuf { - Self::get_dir(DirType::State) - } - - pub fn plugins_dir() -> PathBuf { - Self::get_dir(DirType::Plugins) - } - - pub fn agents_dir() -> PathBuf { - Self::get_dir(DirType::Agents) - } - - pub fn agents_home_dir() -> PathBuf { - Self::get_dir(DirType::AgentsHome) - } - - pub fn in_agents_home_dir(subpath: &str) -> PathBuf { - Self::agents_home_dir().join(subpath) - } - - pub fn in_state_dir(subpath: &str) -> PathBuf { - Self::state_dir().join(subpath) - } - - pub fn in_config_dir(subpath: &str) -> PathBuf { - Self::config_dir().join(subpath) - } - - pub fn in_data_dir(subpath: &str) -> PathBuf { - Self::data_dir().join(subpath) - } -} - -enum DirType { - Config, - Data, - State, - Plugins, - Agents, - AgentsHome, -} diff --git a/crates/goose-local-inference/src/prompt_template.rs b/crates/goose-local-inference/src/prompt_template.rs deleted file mode 100644 index 046fc2402f..0000000000 --- a/crates/goose-local-inference/src/prompt_template.rs +++ /dev/null @@ -1,43 +0,0 @@ -use include_dir::{include_dir, Dir}; -use minijinja::{Environment, Error as MiniJinjaError, Value as MJValue}; -use serde::Serialize; - -use crate::paths::Paths; - -static CORE_PROMPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/prompts"); - -pub fn render_string( - template_str: &str, - context: &T, -) -> Result { - let mut env = Environment::new(); - env.set_trim_blocks(true); - env.set_lstrip_blocks(true); - env.add_template("template", template_str)?; - let tmpl = env.get_template("template")?; - let ctx = MJValue::from_serialize(context); - let rendered = tmpl.render(ctx)?; - Ok(rendered.trim().to_string()) -} - -pub fn render_template(name: &str, context: &T) -> Result { - let user_path = Paths::config_dir().join("prompts").join(name); - let template_str = if user_path.exists() { - std::fs::read_to_string(&user_path).map_err(|e| { - MiniJinjaError::new( - minijinja::ErrorKind::InvalidOperation, - format!("Failed to read user template: {}", e), - ) - })? - } else { - let file = CORE_PROMPTS_DIR.get_file(name).ok_or_else(|| { - MiniJinjaError::new( - minijinja::ErrorKind::TemplateNotFound, - format!("Built-in template '{}' not found", name), - ) - })?; - String::from_utf8_lossy(file.contents()).to_string() - }; - - render_string(&template_str, context) -} diff --git a/crates/goose-local-inference/src/prompts/tiny_model_system.md b/crates/goose-local-inference/src/prompts/tiny_model_system.md deleted file mode 100644 index 2a05d84160..0000000000 --- a/crates/goose-local-inference/src/prompts/tiny_model_system.md +++ /dev/null @@ -1,22 +0,0 @@ -You are goose, an autonomous AI agent created by AAIF (Agentic AI Foundation). You act on the user's -behalf โ€” you do not explain how to do things, you DO them directly. - -The OS is {{os}}, the shell is {{shell}}, and the working directory is {{working_directory}} - -When the user asks you to do something, take action immediately. Do not describe -what you would do or give instructions โ€” execute the commands yourself. - -To run a shell command, start a new line with $: - -$ ls - -Keep your responses brief. State what you are doing, then do it. For example: - -User: how many files are in /tmp? -You: Let me check. -$ ls -1 /tmp | wc -l - -After a command runs, you will see its output. Use the output to answer the user -or take the next step. Do not repeat commands you have already run. - -Do not use shell commands if you already know the answer. \ No newline at end of file diff --git a/crates/goose-local-inference/src/provider_utils.rs b/crates/goose-local-inference/src/provider_utils.rs deleted file mode 100644 index 1300c1f669..0000000000 --- a/crates/goose-local-inference/src/provider_utils.rs +++ /dev/null @@ -1,24 +0,0 @@ -pub fn filter_extensions_from_system_prompt(system: &str) -> String { - let Some(extensions_start) = system.find("# Extensions") else { - return system.to_string(); - }; - - let Some(after_extensions) = system.get(extensions_start + 1..) else { - return system.to_string(); - }; - - if let Some(next_section_pos) = after_extensions.find("\n# ") { - let Some(before) = system.get(..extensions_start) else { - return system.to_string(); - }; - let Some(after) = system.get(extensions_start + next_section_pos + 1..) else { - return system.to_string(); - }; - format!("{}{}", before.trim_end(), after) - } else { - system - .get(..extensions_start) - .map(|s| s.trim_end().to_string()) - .unwrap_or_else(|| system.to_string()) - } -} diff --git a/crates/goose-local-inference/src/thinking_output.rs b/crates/goose-local-inference/src/thinking_output.rs deleted file mode 100644 index b47d23bc9f..0000000000 --- a/crates/goose-local-inference/src/thinking_output.rs +++ /dev/null @@ -1,81 +0,0 @@ -use goose_provider_types::thinking::{FilterOut, ThinkFilter}; - -pub(crate) struct ThinkingOutputFilter { - enabled: bool, - saw_structured_reasoning: bool, - think_filter: ThinkFilter, - pending_inline_thinking: String, - accumulated_thinking: String, -} - -impl ThinkingOutputFilter { - pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) fn accumulated_thinking(&self) -> &str { - &self.accumulated_thinking - } -} diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 7aa7c2631d..07335e821e 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -38,6 +38,6 @@ once_cell = { workspace = true } lopdf = { version = "0.42", 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 = "3", default-features = false } +umya-spreadsheet = { version = "2", default-features = false } shell-words = { workspace = true } process-wrap = { version = "9", default-features = false, features = ["std"] } diff --git a/crates/goose-mcp/src/computercontroller/pdf_tool.rs b/crates/goose-mcp/src/computercontroller/pdf_tool.rs index 6d92cc96d1..defbd5fdcb 100644 --- a/crates/goose-mcp/src/computercontroller/pdf_tool.rs +++ b/crates/goose-mcp/src/computercontroller/pdf_tool.rs @@ -84,11 +84,11 @@ pub async fn pdf_tool( last_was_text = false; } } - Object::Real(offset) - if *offset < -100.0 => - { - text.push(' '); - last_was_text = false; + Object::Real(offset) => { + if *offset < -100.0 { + text.push(' '); + last_was_text = false; + } } _ => {} } diff --git a/crates/goose-mcp/src/computercontroller/xlsx_tool.rs b/crates/goose-mcp/src/computercontroller/xlsx_tool.rs index 4e7b794155..f588dee07f 100644 --- a/crates/goose-mcp/src/computercontroller/xlsx_tool.rs +++ b/crates/goose-mcp/src/computercontroller/xlsx_tool.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; -use umya_spreadsheet::{structs::Workbook, Worksheet}; +use umya_spreadsheet::{Spreadsheet, Worksheet}; #[derive(Debug, Serialize, Deserialize)] pub struct WorksheetInfo { @@ -28,7 +28,7 @@ pub struct RangeData { } pub struct XlsxTool { - workbook: Workbook, + workbook: Spreadsheet, } impl XlsxTool { @@ -40,10 +40,10 @@ impl XlsxTool { pub fn list_worksheets(&self) -> Result> { let mut worksheets = Vec::new(); - for (index, worksheet) in self.workbook.sheet_collection().iter().enumerate() { + for (index, worksheet) in self.workbook.get_sheet_collection().iter().enumerate() { let (column_count, row_count) = self.get_worksheet_dimensions(worksheet)?; worksheets.push(WorksheetInfo { - name: worksheet.name().to_string(), + name: worksheet.get_name().to_string(), index, column_count, row_count, @@ -54,13 +54,13 @@ impl XlsxTool { pub fn get_worksheet_by_name(&self, name: &str) -> Result<&Worksheet> { self.workbook - .sheet_by_name(name) + .get_sheet_by_name(name) .context("Worksheet not found") } pub fn get_worksheet_by_index(&self, index: usize) -> Result<&Worksheet> { self.workbook - .sheet_collection() + .get_sheet_collection() .get(index) .context("Worksheet index out of bounds") } @@ -71,12 +71,12 @@ impl XlsxTool { let mut max_row = 0; // Iterate through all rows - for row_num in 1..=worksheet.highest_row() { - for col_num in 1..=worksheet.highest_column() { - if let Some(cell) = worksheet.cell((col_num, row_num)) { - let coord = cell.coordinate(); - max_col = max_col.max(coord.col_num() as usize); - max_row = max_row.max(coord.row_num() as usize); + for row_num in 1..=worksheet.get_highest_row() { + for col_num in 1..=worksheet.get_highest_column() { + if let Some(cell) = worksheet.get_cell((col_num, row_num)) { + let coord = cell.get_coordinate(); + max_col = max_col.max(*coord.get_col_num() as usize); + max_row = max_row.max(*coord.get_row_num() as usize); } } } @@ -86,9 +86,9 @@ impl XlsxTool { pub fn get_column_names(&self, worksheet: &Worksheet) -> Result> { let mut names = Vec::new(); - for col_num in 1..=worksheet.highest_column() { - if let Some(cell) = worksheet.cell((col_num, 1)) { - names.push(cell.value().into_owned()); + for col_num in 1..=worksheet.get_highest_column() { + if let Some(cell) = worksheet.get_cell((col_num, 1)) { + names.push(cell.get_value().into_owned()); } else { names.push(String::new()); } @@ -104,13 +104,13 @@ impl XlsxTool { for row_idx in start_row..=end_row { let mut row_values = Vec::new(); for col_idx in start_col..=end_col { - let cell_value = if let Some(cell) = worksheet.cell((col_idx, row_idx)) { + let cell_value = if let Some(cell) = worksheet.get_cell((col_idx, row_idx)) { CellValue { - value: cell.value().into_owned(), - formula: if cell.formula().is_empty() { + value: cell.get_value().into_owned(), + formula: if cell.get_formula().is_empty() { None } else { - Some(cell.formula().to_string()) + Some(cell.get_formula().to_string()) }, } } else { @@ -142,10 +142,12 @@ impl XlsxTool { ) -> Result<()> { let worksheet = self .workbook - .sheet_by_name_mut(worksheet_name) + .get_sheet_by_name_mut(worksheet_name) .context("Worksheet not found")?; - worksheet.cell_mut((col, row)).set_value(value.to_string()); + worksheet + .get_cell_mut((col, row)) + .set_value(value.to_string()); Ok(()) } @@ -169,18 +171,18 @@ impl XlsxTool { search_text.to_string() }; - for row_num in 1..=worksheet.highest_row() { - for col_num in 1..=worksheet.highest_column() { - if let Some(cell) = worksheet.cell((col_num, row_num)) { + for row_num in 1..=worksheet.get_highest_row() { + for col_num in 1..=worksheet.get_highest_column() { + if let Some(cell) = worksheet.get_cell((col_num, row_num)) { let cell_value = if !case_sensitive { - cell.value().to_lowercase() + cell.get_value().to_lowercase() } else { - cell.value().to_string() + cell.get_value().to_string() }; if cell_value.contains(&search_text) { - let coord = cell.coordinate(); - matches.push((coord.row_num(), coord.col_num())); + let coord = cell.get_coordinate(); + matches.push((*coord.get_row_num(), *coord.get_col_num())); } } } @@ -190,14 +192,14 @@ impl XlsxTool { } pub fn get_cell_value(&self, worksheet: &Worksheet, row: u32, col: u32) -> Result { - let cell = worksheet.cell((col, row)).context("Cell not found")?; + let cell = worksheet.get_cell((col, row)).context("Cell not found")?; Ok(CellValue { - value: cell.value().into_owned(), - formula: if cell.formula().is_empty() { + value: cell.get_value().into_owned(), + formula: if cell.get_formula().is_empty() { None } else { - Some(cell.formula().to_string()) + Some(cell.get_formula().to_string()) }, }) } diff --git a/crates/goose-mcp/src/memory/mod.rs b/crates/goose-mcp/src/memory/mod.rs index b0f086874a..715a9229f5 100644 --- a/crates/goose-mcp/src/memory/mod.rs +++ b/crates/goose-mcp/src/memory/mod.rs @@ -194,7 +194,7 @@ impl MemoryServer { let category_memories = self.retrieve(&category, is_global, working_dir)?; memories.insert( category, - category_memories.into_values().flatten().collect(), + category_memories.into_iter().flat_map(|(_, v)| v).collect(), ); } } diff --git a/crates/goose-provider-types/Cargo.toml b/crates/goose-provider-types/Cargo.toml deleted file mode 100644 index 0e651492b2..0000000000 --- a/crates/goose-provider-types/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "goose-provider-types" -version = "0.1.0-alpha.0" -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description.workspace = true - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true } -async-stream = { workspace = true } -async-trait = { workspace = true } -base64 = { workspace = true } -chrono = { workspace = true } -futures = { workspace = true } -once_cell = { workspace = true } -rand = { workspace = true } -regex = { workspace = true, features = ["unicode"] } -reqwest = { workspace = true } -rmcp = { workspace = true, features = ["server", "macros"] } -serde = { workspace = true } -serde_json = { workspace = true } -strum = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["time"] } -tracing = { workspace = true } -unicode-normalization = { version = "0.1.22", default-features = false, features = ["std"] } -utoipa = { workspace = true, features = ["chrono"] } -uuid = { workspace = true, features = ["v4", "std"] } - -[dev-dependencies] -env-lock = { workspace = true } -tempfile = { workspace = true } -test-case = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread"] } -tokio-stream = { workspace = true } diff --git a/crates/goose-provider-types/src/lib.rs b/crates/goose-provider-types/src/lib.rs deleted file mode 100644 index 3ab49ef87e..0000000000 --- a/crates/goose-provider-types/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod base; -pub mod canonical; -pub mod conversation; -pub mod errors; -pub mod formats; -pub mod goose_mode; -pub mod images; -pub mod json; -pub(crate) mod mcp_utils; -pub mod model; -pub mod permission; -pub mod request_log; -pub mod retry; -pub mod thinking; -pub mod utils; diff --git a/crates/goose-provider-types/src/utils.rs b/crates/goose-provider-types/src/utils.rs deleted file mode 100644 index 1b7777ff2d..0000000000 --- a/crates/goose-provider-types/src/utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use unicode_normalization::UnicodeNormalization; - -fn is_in_unicode_tag_range(c: char) -> bool { - matches!(c, '\u{E0000}'..='\u{E007F}') -} - -pub fn sanitize_unicode_tags(text: &str) -> String { - let normalized: String = text.nfc().collect(); - - normalized - .chars() - .filter(|&c| !is_in_unicode_tag_range(c)) - .collect() -} - -/// Extract the model name from a JSON object. Common with most providers to have this top level attribute. -pub fn get_model(data: &serde_json::Value) -> String { - if let Some(model) = data.get("model") { - if let Some(model_str) = model.as_str() { - model_str.to_string() - } else { - "Unknown".to_string() - } - } else { - "Unknown".to_string() - } -} diff --git a/crates/goose-providers/Cargo.toml b/crates/goose-providers/Cargo.toml index 5dd8c3e34e..7fd0180c6f 100644 --- a/crates/goose-providers/Cargo.toml +++ b/crates/goose-providers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goose-providers" -version = "0.1.0-alpha.0" +version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -13,10 +13,6 @@ workspace = true [features] default = [] -local-inference = ["dep:goose-local-inference"] -cuda = ["local-inference", "goose-local-inference/cuda"] -vulkan = ["local-inference", "goose-local-inference/vulkan"] -mlx = ["local-inference", "goose-local-inference/mlx"] rustls-tls = [ "reqwest/rustls", "rmcp/reqwest", @@ -33,28 +29,31 @@ native-tls = [ [dependencies] anyhow = { workspace = true } async-stream = { workspace = true } +base64 = { workspace = true } chrono = { workspace = true } futures = { workspace = true } -goose-provider-types = { version = "0.1.0-alpha.0", path = "../goose-provider-types", default-features = false } -goose-local-inference = { version = "0.1.0-alpha.0", path = "../goose-local-inference", default-features = false, optional = true } +once_cell = { workspace = true } +regex = { workspace = true, features = ["unicode"] } reqwest = { workspace = true } rmcp = { workspace = true, features = ["server", "macros"] } serde = { workspace = true } serde_json = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } +unicode-normalization = { version = "0.1.22", default-features = false, features = ["std"] } utoipa = { workspace = true, features = ["chrono"] } +uuid = { workspace = true, features = ["v4", "std"] } async-trait = { workspace = true } +strum = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true, features = ["io-util"] } tokio-util = { workspace = true, features = ["compat"] } +rand = { workspace = true } url = { workspace = true } -urlencoding = { workspace = true } 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 } -# v0.10 matches the der/const-oid series used by sec1 v0.7 and pkcs1 v0.7; upgrading to v0.11 causes type mismatches across those crates. -pkcs8 = { version = "0.10", default-features = false, features = ["alloc", "std"], optional = true } +pkcs8 = { version = "0.11.0", default-features = false, features = ["alloc", "std"], optional = true } sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8", "std"], optional = true } -include_dir = { workspace = true } [dev-dependencies] test-case = { workspace = true } @@ -62,7 +61,6 @@ tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-stream = { workspace = true } env-lock = { workspace = true } -wiremock.workspace = true [[example]] name = "streaming" diff --git a/crates/goose-providers/examples/declarative.rs b/crates/goose-providers/examples/declarative.rs deleted file mode 100644 index 1c1aa189ea..0000000000 --- a/crates/goose-providers/examples/declarative.rs +++ /dev/null @@ -1,27 +0,0 @@ -use anyhow::Result; -use futures::StreamExt; -use goose_providers::{ - base::Provider, conversation::message::Message, declarative::EnvKeyResolver, model::ModelConfig, -}; - -async fn complete(provider: &dyn Provider, model: ModelConfig) -> Result<()> { - let system = "You are a knowledgable geography expert"; - let messages = [Message::user().with_text("what is the capital of France?")]; - let mut stream = provider.stream(&model, system, &messages, &[]).await?; - - while let Some((Some(msg), _)) = stream.next().await.transpose()? { - print!("{}", msg.as_concat_text()); - } - println!(); - - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<()> { - let model = ModelConfig::new("deepseek-v4-flash"); - let provider = goose_providers::deepseek::create(None, EnvKeyResolver {})?; - complete(provider.as_ref(), model).await?; - - Ok(()) -} diff --git a/crates/goose-providers/src/anthropic.rs b/crates/goose-providers/src/anthropic.rs index 33755a6862..a0c901721c 100644 --- a/crates/goose-providers/src/anthropic.rs +++ b/crates/goose-providers/src/anthropic.rs @@ -1,6 +1,4 @@ -use crate::api_client::{AuthMethod, TlsConfig}; use crate::base::ProviderDescriptor; -use crate::declarative::{DeclarativeProviderConfig, KeyResolver}; use crate::errors::ProviderError; use crate::request_log::{start_log, LoggerHandleExt}; use anyhow::Result; @@ -92,24 +90,6 @@ impl AnthropicProviderBuilder { } } - pub fn api_client(mut self, api_client: ApiClient) -> Self { - self.api_client = api_client; - self - } - - pub fn map_api_client(mut self, f: impl FnOnce(ApiClient) -> ApiClient) -> Self { - self.api_client = f(self.api_client); - self - } - - pub fn try_map_api_client( - mut self, - f: impl FnOnce(ApiClient) -> Result, - ) -> Result { - self.api_client = f(self.api_client)?; - Ok(self) - } - pub fn supports_streaming(mut self, supports_streaming: bool) -> Self { self.supports_streaming = supports_streaming; self @@ -307,96 +287,3 @@ impl Provider for AnthropicProvider { })) } } - -fn format_options_for_provider(preserves_thinking: bool) -> AnthropicFormatOptions { - AnthropicFormatOptions { - preserve_unsigned_thinking: preserves_thinking, - preserve_thinking_context: preserves_thinking, - thinking_disabled: false, - } -} - -pub fn from_declarative_config( - config: DeclarativeProviderConfig, - tls_config: Option, - key_resolver: impl KeyResolver, -) -> Result { - let custom_models = if !config.models.is_empty() { - Some( - config - .models - .iter() - .map(|m| m.name.clone()) - .collect::>(), - ) - } else { - None - }; - - if config.dynamic_models == Some(false) && custom_models.is_none() { - return Err(anyhow::anyhow!( - "Provider '{}' has dynamic_models: false but no static models listed; \ - at least one entry in `models` is required.", - config.name - )); - } - - let api_key = if config.api_key_env.is_empty() { - None - } else { - match key_resolver.resolve_key(config.api_key_env.as_str()) { - Ok(key) => Some(key), - Err(err) => { - if config.requires_auth { - anyhow::bail!("missing required key {}: {}", config.api_key_env, err); - } - None - } - } - }; - - let auth = match api_key { - Some(key) if !key.is_empty() => AuthMethod::ApiKey { - header_name: "x-api-key".to_string(), - key, - }, - _ => AuthMethod::NoAuth, - }; - - let format_options = format_options_for_provider(config.preserves_thinking); - - let mut api_client = ApiClient::new_with_tls(config.base_url, auth, tls_config)?; - - if let Some(headers) = &config.headers { - let mut header_map = reqwest::header::HeaderMap::new(); - header_map.insert( - reqwest::header::HeaderName::from_static("anthropic-version"), - reqwest::header::HeaderValue::from_static(ANTHROPIC_API_VERSION), - ); - for (key, value) in headers { - let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())?; - let header_value = reqwest::header::HeaderValue::from_str(value)?; - header_map.insert(header_name, header_value); - } - api_client = api_client.with_headers(header_map)?; - } else { - api_client = api_client.with_header("anthropic-version", ANTHROPIC_API_VERSION)?; - } - - let supports_streaming = config.supports_streaming.unwrap_or(true); - - if !supports_streaming { - return Err(anyhow::anyhow!( - "Anthropic provider does not support non-streaming mode. All Claude models support streaming. \ - Please remove 'supports_streaming: false' from your provider configuration." - )); - } - - Ok(AnthropicProviderBuilder::new(api_client) - .supports_streaming(supports_streaming) - .name(config.name.clone()) - .custom_models(custom_models) - .dynamic_models(config.dynamic_models) - .skip_canonical_filtering(config.skip_canonical_filtering) - .format_options(format_options)) -} diff --git a/crates/goose-provider-types/src/base.rs b/crates/goose-providers/src/base.rs similarity index 100% rename from crates/goose-provider-types/src/base.rs rename to crates/goose-providers/src/base.rs diff --git a/crates/goose-provider-types/src/canonical.rs b/crates/goose-providers/src/canonical.rs similarity index 97% rename from crates/goose-provider-types/src/canonical.rs rename to crates/goose-providers/src/canonical.rs index cb92b606cd..9445e1eb72 100644 --- a/crates/goose-provider-types/src/canonical.rs +++ b/crates/goose-providers/src/canonical.rs @@ -28,7 +28,7 @@ impl ModelMapping { /// /// This avoids network calls by looking up all known models for the provider, /// filtering to text-input + tool-calling models, and sorting by release date. -/// The returned names are the canonical short names (e.g. "claude-sonnet-4.5"). +/// The returned names are the canonical short names (e.g. "claude-3.5-sonnet"). /// /// TODO: This trades speed for correctness โ€” the canonical registry may not perfectly /// match what the provider API returns (new models not yet in the registry, deprecated @@ -110,8 +110,8 @@ mod tests { #[test] fn cloud_provider_retains_cost() { - let canonical = maybe_get_canonical_model("anthropic", "claude-sonnet-4-5-20250929") - .expect("claude-sonnet-4.5 should resolve"); + let canonical = maybe_get_canonical_model("anthropic", "claude-3-5-sonnet-20241022") + .expect("claude-3.5-sonnet should resolve"); assert!(canonical.cost.input.is_some()); assert!(canonical.cost.output.is_some()); } diff --git a/crates/goose-provider-types/src/canonical/README.md b/crates/goose-providers/src/canonical/README.md similarity index 74% rename from crates/goose-provider-types/src/canonical/README.md rename to crates/goose-providers/src/canonical/README.md index 4b00a8e09f..f0c682f2ce 100644 --- a/crates/goose-provider-types/src/canonical/README.md +++ b/crates/goose-providers/src/canonical/README.md @@ -1,8 +1,8 @@ # Canonical Model System Provides a unified view of model metadata (pricing, capabilities, context limits) across different LLM providers. -Normalizes provider-specific model names (e.g., `claude-sonnet-4-5-20250929`) -to canonical IDs (e.g., `anthropic/claude-sonnet-4.5`). +Normalizes provider-specific model names (e.g., `claude-3-5-sonnet-20241022`) +to canonical IDs (e.g., `anthropic/claude-3.5-sonnet`). ## Build Canonical Models Fetches latest model metadata from OpenRouter and validates provider mappings: @@ -13,11 +13,11 @@ cargo run --bin build_canonical_models --no-check # Build only, skip checker This script performs two operations by default: 1. **Builds canonical models** - Fetches from OpenRouter API and updates the registry - - Writes to: `crates/goose-provider-types/src/canonical/data/canonical_models.json` + - Writes to: `crates/goose-providers/src/canonical/data/canonical_models.json` 2. **Checks model mappings** (unless `--no-check` is passed) - Tests provider mappings and tracks changes over time - Reports unmapped models - Compares with previous runs (like a lock file) - Shows changed/added/removed mappings - - Writes to: `crates/goose-provider-types/src/canonical/data/canonical_mapping_report.json` + - Writes to: `crates/goose-providers/src/canonical/data/canonical_mapping_report.json` The script is currently built from `crates/goose/src/bin/build_canonical_models.rs` and writes into this crate's `src/canonical/data` directory. diff --git a/crates/goose-provider-types/src/canonical/catalog.rs b/crates/goose-providers/src/canonical/catalog.rs similarity index 100% rename from crates/goose-provider-types/src/canonical/catalog.rs rename to crates/goose-providers/src/canonical/catalog.rs diff --git a/crates/goose-provider-types/src/canonical/data/canonical_mapping_report.json b/crates/goose-providers/src/canonical/data/canonical_mapping_report.json similarity index 100% rename from crates/goose-provider-types/src/canonical/data/canonical_mapping_report.json rename to crates/goose-providers/src/canonical/data/canonical_mapping_report.json diff --git a/crates/goose-provider-types/src/canonical/data/canonical_models.json b/crates/goose-providers/src/canonical/data/canonical_models.json similarity index 91% rename from crates/goose-provider-types/src/canonical/data/canonical_models.json rename to crates/goose-providers/src/canonical/data/canonical_models.json index 8d0b30e18b..3f74f2cad1 100644 --- a/crates/goose-provider-types/src/canonical/data/canonical_models.json +++ b/crates/goose-providers/src/canonical/data/canonical_models.json @@ -2670,64 +2670,6 @@ "output": 65536 } }, - { - "id": "abacus/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": "abacus/MiniMaxAI/MiniMax-M3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "abacus/Qwen/QwQ-32B", "name": "QwQ 32B", @@ -2820,9 +2762,8 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-04", - "last_updated": "2025-04", + "release_date": "2025-04-29", + "last_updated": "2025-04-29", "modalities": { "input": [ "text" @@ -2837,21 +2778,20 @@ "output": 0.29 }, "limit": { - "context": 131072, + "context": 128000, "output": 8192 } }, { - "id": "abacus/Qwen/Qwen3-Coder-480B-A35B-Instruct", - "name": "Qwen3-Coder 480B-A35B Instruct", + "id": "abacus/Qwen/qwen3-coder-480b-a35b-instruct", + "name": "Qwen3 Coder 480B A35B Instruct", "family": "qwen", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-04", - "last_updated": "2025-04", + "release_date": "2025-07-22", + "last_updated": "2025-07-22", "modalities": { "input": [ "text" @@ -2867,35 +2807,7 @@ }, "limit": { "context": 262144, - "output": 16384 - } - }, - { - "id": "abacus/Qwen/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" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.32, - "output": 3.2 - }, - "limit": { - "context": 262144, - "output": 8192 + "output": 65536 } }, { @@ -2929,36 +2841,6 @@ "output": 64000 } }, - { - "id": "abacus/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 10.0, - "output": 50.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "abacus/claude-haiku-4.5", "name": "Claude Haiku 4.5", @@ -2973,7 +2855,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -3057,13 +2940,14 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-01", "last_updated": "2025-11-01", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -3089,11 +2973,12 @@ "temperature": true, "knowledge": "2025-05-31", "release_date": "2026-02-05", - "last_updated": "2026-03-13", + "last_updated": "2026-02-05", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -3105,67 +2990,7 @@ "output": 25.0 }, "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "abacus/claude-opus-4.7", - "name": "Claude Opus 4.7", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-04-16", - "last_updated": "2026-04-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "abacus/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0 - }, - "limit": { - "context": 1000000, + "context": 200000, "output": 128000 } }, @@ -3213,7 +3038,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -3239,11 +3065,12 @@ "temperature": true, "knowledge": "2025-08-31", "release_date": "2026-02-17", - "last_updated": "2026-03-13", + "last_updated": "2026-02-17", "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -3255,37 +3082,7 @@ "output": 15.0 }, "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "abacus/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0 - }, - "limit": { - "context": 1000000, + "context": 200000, "output": 64000 } }, @@ -3373,66 +3170,6 @@ "output": 8192 } }, - { - "id": "abacus/deepseek-ai/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.14, - "output": 0.28, - "cache_read": 0.03 - }, - "limit": { - "context": 1000000, - "output": 32768 - } - }, - { - "id": "abacus/deepseek-ai/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": 1.74, - "output": 3.48, - "cache_read": 0.15 - }, - "limit": { - "context": 1000000, - "output": 32768 - } - }, { "id": "abacus/deepseek/deepseek-v3.1", "name": "DeepSeek V3.1", @@ -3470,13 +3207,15 @@ "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", + "release_date": "2025-03-20", + "last_updated": "2025-06-05", "modalities": { "input": [ "text", "image", - "audio" + "audio", + "video", + "pdf" ], "output": [ "text" @@ -3485,45 +3224,13 @@ "open_weights": false, "cost": { "input": 0.3, - "output": 2.5, - "cache_read": 0.03 + "output": 2.5 }, "limit": { "context": 1048576, "output": 65536 } }, - { - "id": "abacus/gemini-2.5-flash-image", - "name": "Nano Banana", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2024-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 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, { "id": "abacus/gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -3533,13 +3240,15 @@ "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", + "release_date": "2025-03-25", + "last_updated": "2025-03-25", "modalities": { "input": [ "text", "image", - "audio" + "audio", + "video", + "pdf" ], "output": [ "text" @@ -3548,8 +3257,7 @@ "open_weights": false, "cost": { "input": 1.25, - "output": 10.0, - "cache_read": 0.125 + "output": 10.0 }, "limit": { "context": 1048576, @@ -3571,113 +3279,19 @@ "input": [ "text", "image", - "audio" + "audio", + "video", + "pdf" ], "output": [ "text" ] }, "open_weights": false, - "cost": { - "input": 0.5, - "output": 3.0, - "cache_read": 0.05 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "abacus/gemini-3-pro-image-preview", - "name": "Nano Banana Pro", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-11-20", - "last_updated": "2025-11-20", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 12.0, - "cache_read": 0.2 - }, - "limit": { - "context": 65536, - "output": 32768 - } - }, - { - "id": "abacus/gemini-3.1-flash-image-preview", - "name": "Nano Banana 2", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-02-26", - "last_updated": "2026-02-26", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, "cost": { "input": 0.5, "output": 3.0 }, - "limit": { - "context": 1048576, - "output": 32768 - } - }, - { - "id": "abacus/gemini-3.1-flash-lite", - "name": "Gemini 3.1 Flash Lite", - "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": { - "input": [ - "text", - "image", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 1.5, - "cache_read": 0.025 - }, "limit": { "context": 1048576, "output": 65536 @@ -3732,7 +3346,9 @@ "input": [ "text", "image", - "audio" + "video", + "audio", + "pdf" ], "output": [ "text" @@ -3741,75 +3357,13 @@ "open_weights": false, "cost": { "input": 2.0, - "output": 12.0, - "cache_read": 0.2 + "output": 12.0 }, "limit": { "context": 1048576, "output": 65536 } }, - { - "id": "abacus/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", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.5, - "output": 9.0, - "cache_read": 0.15 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "abacus/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", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.14, - "output": 0.4 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, { "id": "abacus/gpt-4.1", "name": "GPT-4.1", @@ -3833,8 +3387,7 @@ "open_weights": false, "cost": { "input": 2.0, - "output": 8.0, - "cache_read": 0.5 + "output": 8.0 }, "limit": { "context": 1047576, @@ -3843,8 +3396,8 @@ }, { "id": "abacus/gpt-4.1-mini", - "name": "GPT-4.1 mini", - "family": "gpt-mini", + "name": "GPT-4.1 Mini", + "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, @@ -3864,8 +3417,7 @@ "open_weights": false, "cost": { "input": 0.4, - "output": 1.6, - "cache_read": 0.1 + "output": 1.6 }, "limit": { "context": 1047576, @@ -3874,8 +3426,8 @@ }, { "id": "abacus/gpt-4.1-nano", - "name": "GPT-4.1 nano", - "family": "gpt-nano", + "name": "GPT-4.1 Nano", + "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, @@ -3895,8 +3447,7 @@ "open_weights": false, "cost": { "input": 0.1, - "output": 0.4, - "cache_read": 0.025 + "output": 0.4 }, "limit": { "context": 1047576, @@ -3905,19 +3456,20 @@ }, { "id": "abacus/gpt-4o", - "name": "GPT-4o", + "name": "GPT-4o (2024-11-20)", "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-09", - "release_date": "2024-05-13", - "last_updated": "2024-08-06", + "knowledge": "2024-10", + "release_date": "2024-11-20", + "last_updated": "2024-11-20", "modalities": { "input": [ "text", - "image" + "image", + "audio" ], "output": [ "text" @@ -3926,8 +3478,7 @@ "open_weights": false, "cost": { "input": 2.5, - "output": 10.0, - "cache_read": 1.25 + "output": 10.0 }, "limit": { "context": 128000, @@ -3936,13 +3487,13 @@ }, { "id": "abacus/gpt-4o-mini", - "name": "GPT-4o mini", - "family": "gpt-mini", + "name": "GPT-4o Mini", + "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2023-09", + "knowledge": "2024-04", "release_date": "2024-07-18", "last_updated": "2024-07-18", "modalities": { @@ -3987,8 +3538,7 @@ "open_weights": false, "cost": { "input": 1.25, - "output": 10.0, - "cache_read": 0.125 + "output": 10.0 }, "limit": { "context": 400000, @@ -3997,8 +3547,8 @@ }, { "id": "abacus/gpt-5-codex", - "name": "GPT-5-Codex", - "family": "gpt-codex", + "name": "GPT-5 Codex", + "family": "gpt", "attachment": false, "reasoning": true, "tool_call": true, @@ -4018,8 +3568,7 @@ "open_weights": false, "cost": { "input": 1.25, - "output": 10.0, - "cache_read": 0.125 + "output": 10.0 }, "limit": { "context": 400000, @@ -4049,8 +3598,7 @@ "open_weights": false, "cost": { "input": 0.25, - "output": 2.0, - "cache_read": 0.025 + "output": 2.0 }, "limit": { "context": 400000, @@ -4080,8 +3628,7 @@ "open_weights": false, "cost": { "input": 0.05, - "output": 0.4, - "cache_read": 0.005 + "output": 0.4 }, "limit": { "context": 400000, @@ -4111,8 +3658,7 @@ "open_weights": false, "cost": { "input": 1.25, - "output": 10.0, - "cache_read": 0.125 + "output": 10.0 }, "limit": { "context": 400000, @@ -4152,7 +3698,7 @@ { "id": "abacus/gpt-5.1-codex", "name": "GPT-5.1 Codex", - "family": "gpt-codex", + "family": "gpt", "attachment": true, "reasoning": true, "tool_call": true, @@ -4172,8 +3718,7 @@ "open_weights": false, "cost": { "input": 1.25, - "output": 10.0, - "cache_read": 0.125 + "output": 10.0 }, "limit": { "context": 400000, @@ -4233,8 +3778,7 @@ "open_weights": false, "cost": { "input": 1.75, - "output": 14.0, - "cache_read": 0.175 + "output": 14.0 }, "limit": { "context": 400000, @@ -4274,7 +3818,7 @@ { "id": "abacus/gpt-5.2-codex", "name": "GPT-5.2 Codex", - "family": "gpt-codex", + "family": "gpt", "attachment": true, "reasoning": true, "tool_call": true, @@ -4285,7 +3829,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -4294,8 +3839,7 @@ "open_weights": false, "cost": { "input": 1.75, - "output": 14.0, - "cache_read": 0.175 + "output": 14.0 }, "limit": { "context": 400000, @@ -4334,7 +3878,7 @@ { "id": "abacus/gpt-5.3-codex", "name": "GPT-5.3 Codex", - "family": "gpt-codex", + "family": "gpt", "attachment": true, "reasoning": true, "tool_call": true, @@ -4345,7 +3889,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -4354,8 +3899,7 @@ "open_weights": false, "cost": { "input": 1.75, - "output": 14.0, - "cache_read": 0.18 + "output": 14.0 }, "limit": { "context": 400000, @@ -4416,197 +3960,10 @@ "open_weights": false, "cost": { "input": 2.5, - "output": 15.0, - "cache_read": 0.25 + "output": 15.0 }, "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.4-mini", - "name": "GPT-5.4 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.75, - "output": 4.5, - "cache_read": 0.075 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.4-nano", - "name": "GPT-5.4 nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 1.25, - "cache_read": 0.02 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "abacus/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1000000, + "context": 1050000, "output": 128000 } }, @@ -4697,65 +4054,6 @@ "output": 16384 } }, - { - "id": "abacus/grok-4.3", - "name": "Grok 4.3", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-17", - "last_updated": "2026-04-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 2.5, - "cache_read": 0.2 - }, - "limit": { - "context": 1000000, - "output": 32768 - } - }, - { - "id": "abacus/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0 - }, - "limit": { - "context": 500000, - "output": 32768 - } - }, { "id": "abacus/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -4874,7 +4172,7 @@ }, { "id": "abacus/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - "name": "Llama 4 Maverick 17B Instruct", + "name": "Llama 4 Maverick 17B 128E Instruct FP8", "family": "llama", "attachment": true, "reasoning": false, @@ -4898,8 +4196,8 @@ "output": 0.59 }, "limit": { - "context": 1048576, - "output": 8192 + "context": 1000000, + "output": 32768 } }, { @@ -4958,128 +4256,6 @@ "output": 4096 } }, - { - "id": "abacus/meta-llama/Meta-Llama-3.3-70B-Instruct", - "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.59, - "output": 0.79 - }, - "limit": { - "context": 131072, - "output": 8192 - } - }, - { - "id": "abacus/mimo-v2-pro", - "name": "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": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 3.0, - "cache_read": 0.2 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, - { - "id": "abacus/moonshotai/Kimi-K2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "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.95, - "output": 4.0, - "cache_read": 0.19 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "abacus/muse-spark-1.1", - "name": "Muse Spark 1.1", - "family": "muse", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-08", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 4.25, - "cache_read": 0.15 - }, - "limit": { - "context": 1000000, - "output": 32000 - } - }, { "id": "abacus/o3", "name": "o3", @@ -5103,8 +4279,7 @@ "open_weights": false, "cost": { "input": 2.0, - "output": 8.0, - "cache_read": 0.5 + "output": 8.0 }, "limit": { "context": 200000, @@ -5133,8 +4308,7 @@ "open_weights": false, "cost": { "input": 1.1, - "output": 4.4, - "cache_read": 0.55 + "output": 4.4 }, "limit": { "context": 200000, @@ -5203,9 +4377,9 @@ }, { "id": "abacus/openai/gpt-oss-120b", - "name": "GPT OSS 120B", + "name": "GPT-OSS 120B", "family": "gpt-oss", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -5213,7 +4387,8 @@ "last_updated": "2025-08-05", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -5226,7 +4401,7 @@ }, "limit": { "context": 128000, - "output": 16384 + "output": 32768 } }, { @@ -5287,14 +4462,15 @@ }, { "id": "abacus/route-llm", - "name": "RouteLLM", + "name": "Route LLM", "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, + "knowledge": "2024-10", "release_date": "2024-01-01", - "last_updated": "2026-07-10", + "last_updated": "2024-01-01", "modalities": { "input": [ "text", @@ -5311,18 +4487,17 @@ }, "limit": { "context": 128000, - "output": 64000 + "output": 16384 } }, { - "id": "abacus/zai-org/GLM-4.5", + "id": "abacus/zai-org/glm-4.5", "name": "GLM-4.5", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-04", "release_date": "2025-07-28", "last_updated": "2025-07-28", "modalities": { @@ -5339,21 +4514,20 @@ "output": 2.2 }, "limit": { - "context": 131072, - "output": 96000 + "context": 128000, + "output": 8192 } }, { - "id": "abacus/zai-org/GLM-4.6", + "id": "abacus/zai-org/glm-4.6", "name": "GLM-4.6", "family": "glm", "attachment": false, - "reasoning": true, + "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-09-30", - "last_updated": "2025-09-30", + "release_date": "2025-03-01", + "last_updated": "2025-03-01", "modalities": { "input": [ "text" @@ -5368,21 +4542,20 @@ "output": 2.2 }, "limit": { - "context": 202752, - "output": 131072 + "context": 128000, + "output": 8192 } }, { - "id": "abacus/zai-org/GLM-4.7", + "id": "abacus/zai-org/glm-4.7", "name": "GLM-4.7", "family": "glm", "attachment": false, - "reasoning": true, + "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-12-22", - "last_updated": "2025-12-22", + "release_date": "2025-06-01", + "last_updated": "2025-06-01", "modalities": { "input": [ "text" @@ -5397,20 +4570,20 @@ "output": 2.2 }, "limit": { - "context": 204800, - "output": 131072 + "context": 128000, + "output": 8192 } }, { - "id": "abacus/zai-org/GLM-5", + "id": "abacus/zai-org/glm-5", "name": "GLM-5", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-02-12", - "last_updated": "2026-02-12", + "release_date": "2026-02-11", + "last_updated": "2026-02-11", "modalities": { "input": [ "text" @@ -5429,64 +4602,6 @@ "output": 131072 } }, - { - "id": "abacus/zai-org/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - { - "id": "abacus/zai-org/GLM-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, { "id": "abliteration-ai/abliterated-model", "name": "Abliterated Model", @@ -5515,169 +4630,6 @@ "output": 8192 } }, - { - "id": "ai-router/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "ai-router/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "ai-router/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "ai-router/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "ai-router/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "aihubmix/alicloud-deepseek-v4-flash", "name": "DeepSeek V4 Flash (Alibaba Cloud)", @@ -5900,70 +4852,6 @@ "output": 128000 } }, - { - "id": "aihubmix/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - { - "id": "aihubmix/claude-opus-4.8-think", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, { "id": "aihubmix/claude-sonnet-4.6", "name": "Claude Sonnet 4.6", @@ -7858,36 +6746,6 @@ "output": 128000 } }, - { - "id": "alibaba-cn/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.1, - "output": 3.851, - "cache_read": 0.275, - "cache_write": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "alibaba-cn/kimi-k2-thinking", "name": "Moonshot Kimi K2 Thinking", @@ -13346,39 +12204,6 @@ "output": 8192 } }, - { - "id": "amazon-bedrock/anthropic.claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 10.0, - "output": 50.0, - "cache_read": 1.0, - "cache_write": 12.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/anthropic.claude-haiku-4.5-20251001-v1:0", "name": "Claude Haiku 4.5", @@ -13552,7 +12377,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -13643,39 +12467,6 @@ "output": 64000 } }, - { - "id": "amazon-bedrock/anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/au.anthropic.claude-haiku-4.5-20251001-v1:0", "name": "Claude Haiku 4.5 (AU)", @@ -13750,7 +12541,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -13841,39 +12631,6 @@ "output": 128000 } }, - { - "id": "amazon-bedrock/au.anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5 (AU)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/deepseek.r1-v1:0", "name": "DeepSeek-R1", @@ -13975,8 +12732,7 @@ "modalities": { "input": [ "text", - "image", - "pdf" + "image" ], "output": [ "text" @@ -14134,7 +12890,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -14225,39 +12980,6 @@ "output": 64000 } }, - { - "id": "amazon-bedrock/eu.anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5 (EU)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.2, - "output": 11.0, - "cache_read": 0.22, - "cache_write": 2.75 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/global.anthropic.claude-fable-5", "name": "Claude Fable 5 (Global)", @@ -14272,8 +12994,7 @@ "modalities": { "input": [ "text", - "image", - "pdf" + "image" ], "output": [ "text" @@ -14431,7 +13152,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -14522,39 +13242,6 @@ "output": 64000 } }, - { - "id": "amazon-bedrock/global.anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5 (Global)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/google.gemma-3-12b-it", "name": "Google Gemma 3 12B", @@ -14644,39 +13331,6 @@ "output": 4096 } }, - { - "id": "amazon-bedrock/jp.anthropic.claude-haiku-4.5-20251001-v1:0", - "name": "Claude Haiku 4.5 (JP)", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 5.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, { "id": "amazon-bedrock/jp.anthropic.claude-opus-4.7", "name": "Claude Opus 4.7 (JP)", @@ -14718,7 +13372,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -14809,39 +13462,6 @@ "output": 64000 } }, - { - "id": "amazon-bedrock/jp.anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5 (JP)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/meta.llama3-1-70b-instruct-v1:0", "name": "Llama 3.1 70B Instruct", @@ -15565,105 +14185,6 @@ "output": 128000 } }, - { - "id": "amazon-bedrock/openai.gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 272000, - "output": 128000 - } - }, - { - "id": "amazon-bedrock/openai.gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 272000, - "output": 128000 - } - }, - { - "id": "amazon-bedrock/openai.gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 272000, - "output": 128000 - } - }, { "id": "amazon-bedrock/openai.gpt-oss-120b", "name": "gpt-oss-120b", @@ -16047,8 +14568,7 @@ "modalities": { "input": [ "text", - "image", - "pdf" + "image" ], "output": [ "text" @@ -16239,7 +14759,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -16330,39 +14849,6 @@ "output": 64000 } }, - { - "id": "amazon-bedrock/us.anthropic.claude-sonnet-5", - "name": "Claude Sonnet 5 (US)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "amazon-bedrock/us.deepseek.r1-v1:0", "name": "DeepSeek-R1 (US)", @@ -16624,36 +15110,6 @@ "output": 101376 } }, - { - "id": "ambient/ambient/large", - "name": "Ambient Large", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26, - "cache_write": 0.0 - }, - "limit": { - "context": 101376, - "output": 101376 - } - }, { "id": "ambient/moonshotai/kimi-k2.6", "name": "Kimi K2.6", @@ -16709,9 +15165,9 @@ }, "open_weights": true, "cost": { - "input": 0.719, - "output": 3.49, - "cache_read": 0.149, + "input": 0.75, + "output": 3.5, + "cache_read": 0.16, "cache_write": 0.0 }, "limit": { @@ -16719,36 +15175,6 @@ "output": 262144 } }, - { - "id": "ambient/z-ai/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26, - "cache_write": 0.0 - }, - "limit": { - "context": 101376, - "output": 101376 - } - }, { "id": "ambient/zai-org/GLM-5.1-FP8", "name": "GLM 5.1", @@ -16780,33 +15206,168 @@ } }, { - "id": "ambient/zai-org/GLM-5.2-FP8", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, + "id": "anthropic/claude-3-haiku", + "name": "Claude Haiku 3", + "family": "claude-haiku", + "attachment": true, + "reasoning": false, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "knowledge": "2023-08-31", + "release_date": "2024-03-13", + "last_updated": "2024-03-13", "modalities": { "input": [ - "text" + "text", + "image", + "pdf" ], "output": [ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 1.2, - "output": 4.2, - "cache_read": 0.26, - "cache_write": 0.0 + "input": 0.25, + "output": 1.25, + "cache_read": 0.03, + "cache_write": 0.3 }, "limit": { - "context": 202752, - "output": 202752 + "context": 200000, + "output": 4096 + } + }, + { + "id": "anthropic/claude-3-opus", + "name": "Claude Opus 3", + "family": "claude-opus", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2023-08-31", + "release_date": "2024-02-29", + "last_updated": "2024-02-29", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5, + "cache_write": 18.75 + }, + "limit": { + "context": 200000, + "output": 4096 + } + }, + { + "id": "anthropic/claude-3-sonnet", + "name": "Claude Sonnet 3", + "family": "claude-sonnet", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2023-08-31", + "release_date": "2024-03-04", + "last_updated": "2024-03-04", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 0.3 + }, + "limit": { + "context": 200000, + "output": 4096 + } + }, + { + "id": "anthropic/claude-3.5-sonnet", + "name": "Claude Sonnet 3.5 v2", + "family": "claude-sonnet", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-04-30", + "release_date": "2024-10-22", + "last_updated": "2024-10-22", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "limit": { + "context": 200000, + "output": 8192 + } + }, + { + "id": "anthropic/claude-3.7-sonnet", + "name": "Claude Sonnet 3.7", + "family": "claude-sonnet", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-10-31", + "release_date": "2025-02-19", + "last_updated": "2025-02-19", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "limit": { + "context": 200000, + "output": 64000 } }, { @@ -16818,7 +15379,7 @@ "thinking_mode": "always_on_adaptive", "tool_call": true, "temperature": false, - "release_date": "2026-06-07", + "release_date": "2026-06-09", "last_updated": "2026-06-09", "modalities": { "input": [ @@ -16875,6 +15436,72 @@ "output": 64000 } }, + { + "id": "anthropic/claude-opus-4", + "name": "Claude Opus 4", + "family": "claude-opus", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-03-31", + "release_date": "2025-05-22", + "last_updated": "2025-05-22", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5, + "cache_write": 18.75 + }, + "limit": { + "context": 200000, + "output": 32000 + } + }, + { + "id": "anthropic/claude-opus-4.0", + "name": "Claude Opus 4 (latest)", + "family": "claude-opus", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-03-31", + "release_date": "2025-05-22", + "last_updated": "2025-05-22", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5, + "cache_write": 18.75 + }, + "limit": { + "context": 200000, + "output": 32000 + } + }, { "id": "anthropic/claude-opus-4.1", "name": "Claude Opus 4.1 (latest)", @@ -16916,7 +15543,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -16951,7 +15578,7 @@ "tool_call": true, "temperature": true, "knowledge": "2025-05-31", - "release_date": "2026-02-04", + "release_date": "2026-02-05", "last_updated": "2026-03-13", "modalities": { "input": [ @@ -16985,7 +15612,7 @@ "tool_call": true, "temperature": false, "knowledge": "2026-01-31", - "release_date": "2026-04-14", + "release_date": "2026-04-16", "last_updated": "2026-04-16", "modalities": { "input": [ @@ -17018,7 +15645,6 @@ "thinking_mode": "adaptive", "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -17043,6 +15669,72 @@ "output": 128000 } }, + { + "id": "anthropic/claude-sonnet-4", + "name": "Claude Sonnet 4", + "family": "claude-sonnet", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-03-31", + "release_date": "2025-05-22", + "last_updated": "2025-05-22", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, + { + "id": "anthropic/claude-sonnet-4.0", + "name": "Claude Sonnet 4 (latest)", + "family": "claude-sonnet", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-03-31", + "release_date": "2025-05-22", + "last_updated": "2025-05-22", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, { "id": "anthropic/claude-sonnet-4.5", "name": "Claude Sonnet 4.5 (latest)", @@ -17072,7 +15764,7 @@ "cache_write": 3.75 }, "limit": { - "context": 1000000, + "context": 200000, "output": 64000 } }, @@ -17107,41 +15799,7 @@ }, "limit": { "context": 1000000, - "output": 128000 - } - }, - { - "id": "anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "thinking_mode": "adaptive", - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-29", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 + "output": 64000 } }, { @@ -20092,133 +18750,6 @@ "output": 128000 } }, - { - "id": "azure-cognitive-services/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure-cognitive-services/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure-cognitive-services/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure-cognitive-services/gpt-chat", - "name": "GPT Chat Latest", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-05-05", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, { "id": "azure-cognitive-services/grok-4-fast-reasoning", "name": "Grok 4 Fast (Reasoning)", @@ -21715,39 +20246,6 @@ "output": 64000 } }, - { - "id": "azure/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "azure/codestral", "name": "Codestral 25.01", @@ -23175,133 +21673,6 @@ "output": 128000 } }, - { - "id": "azure/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "azure/gpt-chat", - "name": "GPT Chat Latest", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-05-05", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, { "id": "azure/gpt-image-1", "name": "GPT-Image-1", @@ -25425,85 +23796,6 @@ "output": 32768 } }, - { - "id": "blueclaw/Qwen/Qwen3.6-35B-A3B-FP8", - "name": "Qwen3.6 35B A3B FP8", - "family": "qwen", - "attachment": false, - "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": {}, - "limit": { - "context": 131072, - "output": 65536 - } - }, - { - "id": "blueclaw/Qwen3.6-27B", - "name": "Qwen3.6 27B", - "family": "qwen", - "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": {}, - "limit": { - "context": 196608, - "output": 65536 - } - }, - { - "id": "cerebras/gemma-4-31b", - "name": "Gemma 4 31B IT", - "family": "gemma", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-02", - "last_updated": "2026-07-01", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.99, - "output": 1.49 - }, - "limit": { - "context": 131072, - "output": 40960 - } - }, { "id": "cerebras/gpt-oss-120b", "name": "GPT OSS 120B", @@ -25553,7 +23845,7 @@ "cost": { "input": 2.25, "output": 2.75, - "cache_read": 2.25, + "cache_read": 0.0, "cache_write": 0.0 }, "limit": { @@ -26904,7 +25196,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -27028,39 +25319,6 @@ "output": 64000 } }, - { - "id": "cloudflare-ai-gateway/anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "cloudflare-ai-gateway/openai/gpt-3.5-turbo", "name": "GPT-3.5-turbo", @@ -27433,102 +25691,6 @@ "output": 128000 } }, - { - "id": "cloudflare-ai-gateway/openai/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "cloudflare-ai-gateway/openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "cloudflare-ai-gateway/openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "cloudflare-ai-gateway/openai/o1", "name": "o1", @@ -28891,35 +27053,6 @@ "output": 131072 } }, - { - "id": "cloudflare-ai-gateway/workers-ai/@cf/zai-org/glm-5.2", - "name": "Glm 5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "cloudflare-workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it", "name": "Gemma Sea Lion V4 27B It", @@ -30136,7 +28269,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -32296,1570 +30428,6 @@ "output": 262144 } }, - { - "id": "crossmodel/anthropic/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 10.0, - "output": 50.0, - "cache_read": 1.0, - "cache_write": 12.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/anthropic/claude-haiku-4.5", - "name": "Claude Haiku 4.5 (latest)", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 5.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "crossmodel/anthropic/claude-opus-4.7", - "name": "Claude Opus 4.7", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-04-16", - "last_updated": "2026-04-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/anthropic/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/anthropic/claude-sonnet-4.6", - "name": "Claude Sonnet 4.6", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-08-31", - "release_date": "2026-02-17", - "last_updated": "2026-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0, - "cache_read": 0.3, - "cache_write": 3.75 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "crossmodel/anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/deepseek/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.16, - "output": 0.32, - "cache_read": 0.004, - "cache_write": 0.16 - }, - "limit": { - "context": 1000000, - "output": 65000 - } - }, - { - "id": "crossmodel/deepseek/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.47, - "output": 0.94, - "cache_read": 0.005, - "cache_write": 0.47 - }, - "limit": { - "context": 1000000, - "output": 65000 - } - }, - { - "id": "crossmodel/gemini/gemini-2.5-flash", - "name": "Gemini 2.5 Flash", - "family": "gemini-flash", - "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", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.03, - "cache_write": 0.3 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/gemini/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", - "release_date": "2025-06-17", - "last_updated": "2025-06-17", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.01, - "cache_write": 0.1 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/gemini/gemini-2.5-pro", - "name": "Gemini 2.5 Pro", - "family": "gemini-pro", - "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" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.125, - "cache_write": 1.25 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/gemini/gemini-3-flash-preview", - "name": "Gemini 3 Flash Preview", - "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", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.5, - "output": 3.0, - "cache_read": 0.05, - "cache_write": 0.5 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/gemini/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", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 12.0, - "cache_read": 0.2, - "cache_write": 2.0 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/gemini/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", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.5, - "output": 9.0, - "cache_read": 0.15, - "cache_write": 1.5 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "crossmodel/minimax/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.33, - "output": 1.32, - "cache_read": 0.066, - "cache_write": 0.42 - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - { - "id": "crossmodel/minimax/minimax-m3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.33, - "output": 1.32, - "cache_read": 0.066, - "cache_write": 0.33 - }, - "limit": { - "context": 1024000, - "output": 512000 - } - }, - { - "id": "crossmodel/moonshot/kimi-k2.5", - "name": "Kimi K2.5", - "family": "kimi-k2", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-01", - "last_updated": "2026-01", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.62, - "output": 3.3, - "cache_read": 0.11, - "cache_write": 0.62 - }, - "limit": { - "context": 262000, - "output": 262000 - } - }, - { - "id": "crossmodel/moonshot/kimi-k2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "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": 1.0, - "output": 4.16, - "cache_read": 0.18, - "cache_write": 1.0 - }, - "limit": { - "context": 262000, - "output": 262000 - } - }, - { - "id": "crossmodel/moonshot/kimi-k2.7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.0, - "output": 4.16, - "cache_read": 0.18, - "cache_write": 1.0 - }, - "limit": { - "context": 262000, - "output": 262000 - } - }, - { - "id": "crossmodel/openai/gpt-4o-mini", - "name": "GPT-4o mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2023-09", - "release_date": "2024-07-18", - "last_updated": "2024-07-18", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.075, - "cache_write": 0.15 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, - { - "id": "crossmodel/openai/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 2.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.4-mini", - "name": "GPT-5.4 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.75, - "output": 4.5, - "cache_read": 0.075, - "cache_write": 0.75 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.4-nano", - "name": "GPT-5.4 nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 1.25, - "cache_read": 0.02, - "cache_write": 0.2 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 5.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.5-pro", - "name": "GPT-5.5 Pro", - "family": "gpt-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 30.0, - "output": 180.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "crossmodel/qwen/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.19, - "output": 1.13, - "cache_read": 0.019, - "cache_write": 0.24 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "crossmodel/qwen/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.32, - "output": 1.88, - "cache_read": 0.032, - "cache_write": 0.4 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "crossmodel/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": 1.88, - "output": 5.63, - "cache_read": 0.375, - "cache_write": 2.35 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "crossmodel/qwen/qwen3.7-plus", - "name": "Qwen3.7 Plus", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-06-02", - "last_updated": "2026-06-02", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.32, - "output": 1.25, - "cache_read": 0.032, - "cache_write": 0.4 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "crossmodel/tencent/hy3-preview", - "name": "Hy3 preview", - "family": "Hy", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-20", - "last_updated": "2026-04-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.19, - "output": 0.63, - "cache_read": 0.063, - "cache_write": 0.19 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "crossmodel/xiaomi/mimo-v2.5", - "name": "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": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.16, - "output": 0.32, - "cache_read": 0.004, - "cache_write": 0.16 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/xiaomi/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.47, - "output": 0.94, - "cache_read": 0.005, - "cache_write": 0.47 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "crossmodel/z-ai/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.47, - "output": 2.16, - "cache_read": 0.1, - "cache_write": 0.47 - }, - "limit": { - "context": 200000, - "output": 128000 - } - }, - { - "id": "crossmodel/z-ai/glm-5", - "name": "GLM-5", - "family": "glm", - "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.6, - "output": 3.0, - "cache_read": 0.16, - "cache_write": 0.6 - }, - "limit": { - "context": 200000, - "output": 128000 - } - }, - { - "id": "crossmodel/z-ai/glm-5-turbo", - "name": "GLM-5-Turbo", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-03-16", - "last_updated": "2026-03-16", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.9, - "output": 3.7, - "cache_read": 0.18, - "cache_write": 0.9 - }, - "limit": { - "context": 200000, - "output": 128000 - } - }, - { - "id": "crossmodel/z-ai/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.0, - "output": 3.8, - "cache_read": 0.2, - "cache_write": 1.0 - }, - "limit": { - "context": 200000, - "output": 128000 - } - }, - { - "id": "crossmodel/z-ai/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.2, - "output": 4.4, - "cache_read": 0.3, - "cache_write": 1.2 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "daoxe/claude-haiku-4.5", - "name": "Claude Haiku 4.5", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 5.0, - "cache_read": 0.1, - "cache_write": 5.0 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "daoxe/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "daoxe/claude-sonnet-4.6", - "name": "Claude Sonnet 4.6", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-08-31", - "release_date": "2026-02-17", - "last_updated": "2026-03-13", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0, - "cache_read": 0.3, - "cache_write": 3.75 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "daoxe/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", - "video", - "audio", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 12.0, - "cache_read": 0.2 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "daoxe/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "daoxe/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "daoxe/grok-4.3", - "name": "Grok 4.3", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-17", - "last_updated": "2026-04-17", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 2.5, - "cache_read": 0.2 - }, - "limit": { - "context": 1000000, - "output": 30000 - } - }, - { - "id": "daoxe/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, - { - "id": "daoxe/kimi-k2.5", - "name": "Kimi K2.5", - "family": "kimi-k2", - "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.6, - "output": 3.0, - "cache_read": 0.1 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "databricks/databricks-claude-haiku-4.5", "name": "Claude Haiku 4.5 (latest)", @@ -33934,7 +30502,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -34328,35 +30896,6 @@ "output": 65536 } }, - { - "id": "databricks/databricks-glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "databricks/databricks-gpt-5", "name": "GPT-5", @@ -34638,102 +31177,6 @@ "output": 128000 } }, - { - "id": "databricks/databricks-gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "databricks/databricks-gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "databricks/databricks-gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "databricks/databricks-gpt-oss-120b", "name": "GPT OSS 120B", @@ -34790,38 +31233,6 @@ "output": 32768 } }, - { - "id": "databricks/databricks-kimi-k2-7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.19 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "deepinfra/MiniMaxAI/MiniMax-M2.5", "name": "MiniMax M2.5", @@ -34845,102 +31256,14 @@ "cost": { "input": 0.15, "output": 1.15, - "cache_read": 0.03 + "cache_read": 0.03, + "cache_write": 0.375 }, "limit": { "context": 196608, "output": 131072 } }, - { - "id": "deepinfra/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.25, - "output": 1.0, - "cache_read": 0.05 - }, - "limit": { - "context": 196608, - "output": 131072 - } - }, - { - "id": "deepinfra/MiniMaxAI/MiniMax-M3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.06 - }, - "limit": { - "context": 524288, - "output": 128000 - } - }, - { - "id": "deepinfra/Qwen/Qwen3-32B", - "name": "Qwen3 32B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-04", - "last_updated": "2025-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.08, - "output": 0.28 - }, - "limit": { - "context": 40960, - "output": 16384 - } - }, { "id": "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", "name": "Qwen3 Coder 480B A35B Instruct Turbo", @@ -34963,135 +31286,13 @@ "open_weights": true, "cost": { "input": 0.3, - "output": 1.0, - "cache_read": 0.1 + "output": 1.0 }, "limit": { "context": 262144, "output": 66536 } }, - { - "id": "deepinfra/Qwen/Qwen3-Max", - "name": "Qwen3 Max", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-09-23", - "last_updated": "2025-09-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.2, - "output": 6.0, - "cache_read": 0.24 - }, - "limit": { - "context": 256000, - "output": 65536 - } - }, - { - "id": "deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct", - "name": "Qwen3-Next 80B-A3B Instruct", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-09", - "last_updated": "2025-09", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.09, - "output": 1.1 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "deepinfra/Qwen/Qwen3.5-122B-A10B", - "name": "Qwen3.5 122B-A10B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.29, - "output": 2.4 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "deepinfra/Qwen/Qwen3.5-27B", - "name": "Qwen3.5 27B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.26, - "output": 2.6 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, { "id": "deepinfra/Qwen/Qwen3.5-35B-A3B", "name": "Qwen 3.5 35B A3B", @@ -35156,67 +31357,6 @@ "output": 81920 } }, - { - "id": "deepinfra/Qwen/Qwen3.5-9B", - "name": "Qwen3.5 9B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.15 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "deepinfra/Qwen/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.32, - "output": 3.2 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, { "id": "deepinfra/Qwen/Qwen3.6-35B-A3B", "name": "Qwen3.6 35B A3B", @@ -35247,35 +31387,6 @@ "output": 81920 } }, - { - "id": "deepinfra/Qwen/Qwen3.7-Max", - "name": "Qwen3.7 Max", - "family": "qwen", - "attachment": false, - "reasoning": false, - "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 - }, - "limit": { - "context": 256000, - "output": 65536 - } - }, { "id": "deepinfra/XiaomiMiMo/MiMo-V2.5", "name": "MiMo-V2.5", @@ -35313,7 +31424,7 @@ "id": "deepinfra/XiaomiMiMo/MiMo-V2.5-Pro", "name": "MiMo-V2.5-Pro", "family": "mimo", - "attachment": true, + "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, @@ -35322,8 +31433,7 @@ "last_updated": "2026-04-22", "modalities": { "input": [ - "text", - "audio" + "text" ], "output": [ "text" @@ -35419,9 +31529,9 @@ }, "open_weights": true, "cost": { - "input": 0.09, - "output": 0.18, - "cache_read": 0.018 + "input": 0.1, + "output": 0.2, + "cache_read": 0.02 }, "limit": { "context": 1048576, @@ -35500,8 +31610,7 @@ "modalities": { "input": [ "text", - "image", - "video" + "image" ], "output": [ "text" @@ -35548,7 +31657,7 @@ "id": "deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", "name": "Llama 4 Maverick 17B FP8", "family": "llama", - "attachment": true, + "attachment": false, "reasoning": false, "tool_call": false, "release_date": "2025-04-05", @@ -35564,8 +31673,8 @@ }, "open_weights": true, "cost": { - "input": 0.2, - "output": 0.8 + "input": 0.15, + "output": 0.6 }, "limit": { "context": 1048576, @@ -35576,7 +31685,7 @@ "id": "deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout 17B", "family": "llama", - "attachment": true, + "attachment": false, "reasoning": false, "tool_call": true, "release_date": "2025-04-05", @@ -35664,125 +31773,6 @@ "output": 16384 } }, - { - "id": "deepinfra/moonshotai/Kimi-K2.7-Code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.74, - "output": 3.5, - "cache_read": 0.15 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5", - "name": "Llama 3.3 Nemotron Super 49B v1.5", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-07-25", - "last_updated": "2025-07-25", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.4, - "output": 0.4 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, - { - "id": "deepinfra/nvidia/Nemotron-3-Nano-30B-A3B", - "name": "Nemotron 3 Nano 30B A3B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-12-15", - "last_updated": "2025-12-15", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.2 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "deepinfra/nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning", - "name": "Nemotron 3 Nano Omni 30B A3B Reasoning", - "family": "nemotron", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-28", - "last_updated": "2026-04-28", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.8 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, { "id": "deepinfra/openai/gpt-oss-120b", "name": "GPT OSS 120B", @@ -35803,8 +31793,8 @@ }, "open_weights": true, "cost": { - "input": 0.037, - "output": 0.17 + "input": 0.039, + "output": 0.19 }, "limit": { "context": 131072, @@ -35860,9 +31850,9 @@ }, "open_weights": true, "cost": { - "input": 0.5, - "output": 2.0, - "cache_read": 0.1 + "input": 0.43, + "output": 1.74, + "cache_read": 0.08 }, "limit": { "context": 202752, @@ -35921,8 +31911,7 @@ "open_weights": true, "cost": { "input": 0.06, - "output": 0.4, - "cache_read": 0.01 + "output": 0.4 }, "limit": { "context": 202752, @@ -36009,7 +31998,7 @@ }, "open_weights": true, "cost": { - "input": 0.93, + "input": 0.95, "output": 3.0, "cache_read": 0.18 }, @@ -37112,64 +33101,6 @@ "output": 128000 } }, - { - "id": "digitalocean/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.975, - "output": 4.3, - "cache_read": 0.26 - }, - "limit": { - "context": 200000, - "output": 131072 - } - }, - { - "id": "digitalocean/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.05, - "output": 4.4, - "cache_read": 0.21 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "digitalocean/gte-large-en-v1.5", "name": "GTE Large (v1.5)", @@ -38773,1172 +34704,6 @@ "output": 131072 } }, - { - "id": "ebcloud/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.143, - "output": 0.2857 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "ebcloud/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.4286, - "output": 0.8571 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "ebcloud/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.8571, - "output": 3.4286 - }, - "limit": { - "context": 200000, - "output": 131072 - } - }, - { - "id": "ebcloud/Kimi-K2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "attachment": false, - "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.9286, - "output": 3.8571 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "empiriolabs/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-06-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.14, - "output": 0.28 - }, - "limit": { - "context": 1000000, - "output": 393216 - } - }, - { - "id": "empiriolabs/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-06-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.65, - "output": 3.3 - }, - "limit": { - "context": 1000000, - "output": 393216 - } - }, - { - "id": "empiriolabs/fugu-ultra", - "name": "Fugu Ultra", - "family": "fugu", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "release_date": "2026-06-15", - "last_updated": "2026-06-15", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 7.5, - "output": 45.0, - "cache_read": 1.5 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "empiriolabs/gemma-4-26b-a4b", - "name": "Gemma 4 26B-A4B", - "family": "gemma", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-02", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.29, - "cache_read": 0.025 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "empiriolabs/glm-4.5-flash", - "name": "GLM 4.5 Flash", - "family": "glm-flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-07-28", - "last_updated": "2025-07-28", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 200000, - "output": 98304 - } - }, - { - "id": "empiriolabs/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.0, - "output": 0.0 - }, - "limit": { - "context": 200000, - "output": 131072 - } - }, - { - "id": "empiriolabs/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-06-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.825, - "output": 3.301, - "cache_read": 0.165 - }, - "limit": { - "context": 202000, - "output": 128000 - } - }, - { - "id": "empiriolabs/glm-5.2", - "name": "GLM 5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "empiriolabs/kimi-k2-6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-04-21", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.8939, - "output": 3.7131, - "cache_read": 0.1788 - }, - "limit": { - "context": 256000, - "output": 16000 - } - }, - { - "id": "empiriolabs/kimi-k2-7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0 - }, - "limit": { - "context": 256000, - "output": 131072 - } - }, - { - "id": "empiriolabs/kimi-k2-7-code-highspeed", - "name": "Kimi K2.7 Code Highspeed", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.9, - "output": 8.0 - }, - "limit": { - "context": 256000, - "output": 131072 - } - }, - { - "id": "empiriolabs/mimo-v2-5", - "name": "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": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.7, - "output": 1.4, - "cache_read": 0.014 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "empiriolabs/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": 2.175, - "output": 4.35, - "cache_read": 0.018 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "empiriolabs/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.15, - "output": 0.6, - "cache_read": 0.03 - }, - "limit": { - "context": 200000, - "output": 32768 - } - }, - { - "id": "empiriolabs/minimax-m2-7-highspeed", - "name": "MiniMax M2.7 Highspeed", - "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, - "cache_read": 0.03 - }, - "limit": { - "context": 200000, - "output": 32768 - } - }, - { - "id": "empiriolabs/minimax-m3", - "name": "MiniMax M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.225, - "output": 0.9, - "cache_read": 0.045 - }, - "limit": { - "context": 1000000, - "output": 524288 - } - }, - { - "id": "empiriolabs/mistral-medium-3", - "name": "Mistral Medium 3", - "family": "mistral-medium", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-05", - "release_date": "2025-05-07", - "last_updated": "2025-05-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 130000, - "output": 40000 - } - }, - { - "id": "empiriolabs/mistral-small-4", - "name": "Mistral Small 4", - "family": "mistral-small", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-06", - "release_date": "2026-03-16", - "last_updated": "2026-03-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.6 - }, - "limit": { - "context": 256000, - "output": 65536 - } - }, - { - "id": "empiriolabs/muse-spark-1.1", - "name": "Muse Spark 1.1", - "family": "muse", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-08", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 4.25, - "cache_read": 1.0 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "empiriolabs/qwen3-5-122b-a10b", - "name": "Qwen3.5 122B-A10B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.115, - "output": 0.917 - }, - "limit": { - "context": 256000, - "output": 64000 - } - }, - { - "id": "empiriolabs/qwen3-5-27b", - "name": "Qwen3.5 27B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.086, - "output": 0.688 - }, - "limit": { - "context": 256000, - "output": 64000 - } - }, - { - "id": "empiriolabs/qwen3-5-35b-a3b", - "name": "Qwen3.5 35B-A3B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.057, - "output": 0.459 - }, - "limit": { - "context": 256000, - "output": 64000 - } - }, - { - "id": "empiriolabs/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" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.172, - "output": 1.032 - }, - "limit": { - "context": 256000, - "output": 64000 - } - }, - { - "id": "empiriolabs/qwen3-5-4b", - "name": "Qwen3.5 4B", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-03-02", - "last_updated": "2026-03-02", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.04, - "output": 0.07, - "cache_read": 0.02 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "empiriolabs/qwen3-5-9b", - "name": "Qwen3.5 9B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.09, - "output": 0.13, - "cache_read": 0.045 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "empiriolabs/qwen3-5-plus", - "name": "Qwen3.5 Plus", - "family": "qwen", - "attachment": true, - "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.36, - "output": 2.21 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "empiriolabs/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" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.412564, - "output": 2.475384 - }, - "limit": { - "context": 256000, - "output": 64000 - } - }, - { - "id": "empiriolabs/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.25, - "output": 1.5 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "empiriolabs/qwen3-6-max-preview", - "name": "Qwen3.6 Max Preview", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-04-20", - "last_updated": "2026-04-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.31, - "output": 7.88 - }, - "limit": { - "context": 256000, - "output": 65536 - } - }, - { - "id": "empiriolabs/qwen3-6-plus", - "name": "Qwen3.6 Plus", - "family": "qwen", - "attachment": true, - "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.5, - "output": 3.0 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "empiriolabs/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-06-12", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 7.5 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "empiriolabs/qwen3-7-plus", - "name": "Qwen3.7 Plus", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-06-02", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.4, - "output": 1.6 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "empiriolabs/qwen3-max", - "name": "Qwen3 Max", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-09-23", - "last_updated": "2025-09-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.08, - "output": 5.52 - }, - "limit": { - "context": 256000, - "output": 65536 - } - }, - { - "id": "empiriolabs/step-3.5-flash", - "name": "Step 3.5 Flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01-29", - "last_updated": "2026-02-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.3, - "cache_read": 0.02 - }, - "limit": { - "context": 256000, - "output": 131072 - } - }, - { - "id": "empiriolabs/step-3.7-flash", - "name": "Step 3.7 Flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-03-01", - "release_date": "2026-05-29", - "last_updated": "2026-05-29", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 1.15, - "cache_read": 0.04 - }, - "limit": { - "context": 256000, - "output": 131072 - } - }, { "id": "evroc/KBLab/kb-whisper-large", "name": "KB Whisper", @@ -40405,7 +35170,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -41852,7 +36616,7 @@ "cost": { "input": 1.4, "output": 4.4, - "cache_read": 0.14 + "cache_read": 0.26 }, "limit": { "context": 1048575, @@ -42353,7 +37117,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -42658,6 +37421,35 @@ "output": 32768 } }, + { + "id": "friendli/zai-org/GLM-5", + "name": "GLM-5", + "family": "glm", + "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": 1.0, + "output": 3.2, + "cache_read": 0.5 + }, + "limit": { + "context": 202752, + "output": 202752 + } + }, { "id": "friendli/zai-org/GLM-5.1", "name": "GLM-5.1", @@ -43602,7 +38394,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -43701,7 +38493,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -43825,39 +38616,6 @@ "output": 32000 } }, - { - "id": "github-copilot/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "github-copilot/gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -43956,7 +38714,7 @@ "cache_read": 0.2 }, "limit": { - "context": 1000000, + "context": 200000, "output": 64000 } }, @@ -44180,7 +38938,7 @@ "cache_read": 0.25 }, "limit": { - "context": 1050000, + "context": 400000, "output": 128000 } }, @@ -44274,164 +39032,7 @@ "cache_read": 0.5 }, "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "github-copilot/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "github-copilot/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "github-copilot/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "github-copilot/kimi-k2.7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.19 - }, - "limit": { - "context": 256000, - "output": 32000 - } - }, - { - "id": "github-copilot/mai-code-1-flash-picker", - "name": "MAI-Code-1-Flash", - "family": "mai", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-12", - "release_date": "2026-06-02", - "last_updated": "2026-06-08", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.75, - "output": 4.5, - "cache_read": 0.075 - }, - "limit": { - "context": 256000, + "context": 400000, "output": 128000 } }, @@ -46022,39 +40623,6 @@ "output": 8192 } }, - { - "id": "gitlab/duo-chat-fable-5", - "name": "Agentic Chat (Claude Fable 5)", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "gitlab/duo-chat-gpt-5-codex", "name": "Agentic Chat (GPT-5 Codex)", @@ -46359,105 +40927,6 @@ "output": 128000 } }, - { - "id": "gitlab/duo-chat-gpt-5.6-luna", - "name": "Agentic Chat (GPT-5.6 Luna)", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "gitlab/duo-chat-gpt-5.6-sol", - "name": "Agentic Chat (GPT-5.6 Sol)", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "gitlab/duo-chat-gpt-5.6-terra", - "name": "Agentic Chat (GPT-5.6 Terra)", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "gitlab/duo-chat-haiku-4.5", "name": "Agentic Chat (Claude Haiku 4.5)", @@ -46689,39 +41158,6 @@ "output": 64000 } }, - { - "id": "gitlab/duo-chat-sonnet-5", - "name": "Agentic Chat (Claude Sonnet 5)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, { "id": "gmicloud/Qwen/Qwen3.7-Max", "name": "Qwen3.7 Max", @@ -46812,38 +41248,6 @@ "output": 128000 } }, - { - "id": "gmicloud/anthropic/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "gmicloud/anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6", @@ -46994,38 +41398,6 @@ "output": 262144 } }, - { - "id": "gmicloud/openai/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "gmicloud/zai-org/GLM-5-FP8", "name": "GLM-5", @@ -47253,7 +41625,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-01", "last_updated": "2025-11-01", "modalities": { @@ -47352,7 +41724,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -47476,39 +41847,6 @@ "output": 128000 } }, - { - "id": "google-vertex-anthropic/claude-sonnet-5@default", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "google-vertex/claude-3.5-haiku", "name": "Claude Haiku 3.5", @@ -47649,7 +41987,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-01", "last_updated": "2025-11-01", "modalities": { @@ -47748,7 +42086,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -47872,39 +42209,6 @@ "output": 128000 } }, - { - "id": "google-vertex/claude-sonnet-5@default", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "google-vertex/deepseek-ai/deepseek-v3.1-maas", "name": "DeepSeek V3.1", @@ -47999,37 +42303,6 @@ "output": 65536 } }, - { - "id": "google-vertex/gemini-2.5-flash-image", - "name": "Nano Banana", - "family": "gemini-flash", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-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 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, { "id": "google-vertex/gemini-2.5-flash-lite", "name": "Gemini 2.5 Flash-Lite", @@ -48190,70 +42463,6 @@ "output": 65536 } }, - { - "id": "google-vertex/gemini-3-pro-image", - "name": "Nano Banana Pro", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 120.0 - }, - "limit": { - "context": 65536, - "output": 32768 - } - }, - { - "id": "google-vertex/gemini-3.1-flash-image", - "name": "Nano Banana 2", - "family": "gemini-flash", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "video", - "pdf" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 0.5, - "output": 60.0 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, { "id": "google-vertex/gemini-3.1-flash-lite", "name": "Gemini 3.1 Flash Lite", @@ -48862,7 +43071,7 @@ "reasoning": true, "tool_call": false, "temperature": true, - "knowledge": "2024-06", + "knowledge": "2025-06", "release_date": "2025-08-26", "last_updated": "2025-08-26", "modalities": { @@ -49410,36 +43619,6 @@ "output": 65536 } }, - { - "id": "google/gemini-omni-flash-preview", - "name": "Gemini Omni Flash Preview", - "family": "gemini", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "video" - ] - }, - "open_weights": false, - "cost": { - "input": 1.5, - "output": 17.5 - }, - "limit": { - "context": 131072, - "output": 65536 - } - }, { "id": "google/gemma-4-26b-a4b-it", "name": "Gemma 4 26B A4B IT", @@ -52547,107 +46726,16 @@ "output": 4096 } }, - { - "id": "hpc-ai/anthropic/claude-opus-4.7", - "name": "Claude Opus 4.7", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-01-31", - "release_date": "2026-04-16", - "last_updated": "2026-04-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "hpc-ai/deepseek/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.14, - "output": 0.28, - "cache_read": 0.028 - }, - "limit": { - "context": 1048576, - "output": 128000 - } - }, - { - "id": "hpc-ai/deepseek/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": 1.74, - "output": 3.48, - "cache_read": 0.145 - }, - "limit": { - "context": 1002000, - "output": 128000 - } - }, { "id": "hpc-ai/minimax/minimax-m2.5", - "name": "MiniMax-M2.5", - "family": "minimax", + "name": "MiniMax M2.5", + "family": "minimax-m2.5", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "release_date": "2026-02-12", - "last_updated": "2026-02-12", + "last_updated": "2026-06-01", "modalities": { "input": [ "text" @@ -52663,25 +46751,26 @@ "cache_read": 0.03 }, "limit": { - "context": 196000, - "output": 195000 + "context": 1000000, + "output": 131072 } }, { "id": "hpc-ai/moonshotai/kimi-k2.5", "name": "Kimi K2.5", "family": "kimi-k2", - "attachment": true, + "attachment": false, "reasoning": true, "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01", - "last_updated": "2026-01", + "temperature": false, + "knowledge": "2025-01-01", + "release_date": "2026-01-01", + "last_updated": "2026-06-01", "modalities": { "input": [ "text", - "image" + "image", + "video" ], "output": [ "text" @@ -52689,75 +46778,13 @@ }, "open_weights": true, "cost": { - "input": 0.6, - "output": 3.0, - "cache_read": 0.1 + "input": 0.3, + "output": 1.5, + "cache_read": 0.05 }, "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "hpc-ai/moonshotai/kimi-k2.7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.19 - }, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "hpc-ai/openai/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 - }, - "limit": { - "context": 1050000, - "output": 128000 + "context": 262144, + "output": 262144 } }, { @@ -52789,35 +46816,6 @@ "output": 202000 } }, - { - "id": "hpc-ai/zai-org/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, { "id": "huggingface/MiniMaxAI/MiniMax-M2", "name": "MiniMax-M2", @@ -53899,34 +47897,6 @@ "output": 32768 } }, - { - "id": "huggingface/openai/gpt-oss-20b", - "name": "GPT OSS 20B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.5 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, { "id": "huggingface/stepfun-ai/Step-3.5-Flash", "name": "Step 3.5 Flash", @@ -53962,14 +47932,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-29", "last_updated": "2026-05-29", "modalities": { "input": [ "text", - "image", - "video" + "image" ], "output": [ "text" @@ -55133,186 +49102,6 @@ "output": 2048 } }, - { - "id": "inferx/google/gemma-4-31b-it-fp8", - "name": "Gemma 4 31B IT FP8", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "inferx/qwen/qwen3-coder-next-fp8", - "name": "Qwen3 Coder Next FP8", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "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 - }, - "limit": { - "context": 256144, - "output": 65536 - } - }, - { - "id": "inferx/qwen/qwen3-coder-next-fp8-1m", - "name": "Qwen3 Coder Next FP8 1M", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "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 - }, - "limit": { - "context": 1024000, - "output": 65536 - } - }, - { - "id": "inferx/qwen/qwen3.5-122b-a10b-nvfp4", - "name": "Qwen3.5 122B A10B NVFP4", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 256144, - "output": 65536 - } - }, - { - "id": "inferx/qwen/qwen3.6-27b-fp8", - "name": "Qwen3.6 27B FP8", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "inferx/qwen/qwen3.6-35b-a3b-fp8", - "name": "Qwen3.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", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 262000, - "output": 65536 - } - }, { "id": "io-net/Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar", "name": "Qwen 3 Coder 480B", @@ -57612,688 +51401,6 @@ "output": 128000 } }, - { - "id": "kenari/claude-opus-4.7", - "name": "Claude Opus 4.7", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-04-16", - "last_updated": "2026-04-16", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "kenari/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "kenari/claude-sonnet-4.6", - "name": "Claude Sonnet 4.6", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-08-31", - "release_date": "2026-02-17", - "last_updated": "2026-03-13", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "kenari/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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "kenari/deepseek-v4-flash:free", - "name": "DeepSeek V4 Flash (Free)", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "kenari/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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "kenari/deepseek-v4-pro:free", - "name": "DeepSeek V4 Pro (Free)", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "kenari/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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "kenari/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 200000, - "output": 131072 - } - }, - { - "id": "kenari/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "kenari/gpt-5.4-mini", - "name": "GPT-5.4 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "kenari/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "kenari/gpt-image-2", - "name": "GPT-Image-2", - "family": "gpt-image", - "attachment": true, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-04-21", - "last_updated": "2026-04-21", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 272000, - "output": 16384 - } - }, - { - "id": "kenari/gpt-oss-120b", - "name": "GPT OSS 120B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, - { - "id": "kenari/gpt-oss-20b", - "name": "GPT OSS 20B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, - { - "id": "kenari/grok-4.3", - "name": "Grok 4.3", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-17", - "last_updated": "2026-04-17", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 30000 - } - }, - { - "id": "kenari/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", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "kenari/kimi-k2-6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "kenari/kimi-k2-7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "kenari/mimo-v2-5", - "name": "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": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, - { - "id": "kenari/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.0, - "output": 0.0 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, - { - "id": "kenari/minimax-m3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 512000, - "output": 128000 - } - }, - { - "id": "kenari/qwen3-7-plus", - "name": "Qwen3.7 Plus", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-06-02", - "last_updated": "2026-06-02", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, { "id": "kilo/ai21/jamba-large-1.7", "name": "AI21: Jamba Large 1.7", @@ -61883,37 +54990,6 @@ "output": 131072 } }, - { - "id": "kilo/minimax/minimax-m3", - "name": "MiniMax: MiniMax M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.06 - }, - "limit": { - "context": 1048576, - "output": 512000 - } - }, { "id": "kilo/mistralai/codestral", "name": "Mistral: Codestral 2508", @@ -64958,7 +58034,6 @@ { "id": "kilo/poolside/laguna-m.1:free", "name": "Poolside: Laguna M.1 (free)", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, @@ -64973,7 +58048,7 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { "input": 0.0, "output": 0.0 @@ -64986,7 +58061,6 @@ { "id": "kilo/poolside/laguna-xs.2:free", "name": "Poolside: Laguna XS.2 (free)", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, @@ -65001,7 +58075,7 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { "input": 0.0, "output": 0.0 @@ -68120,21 +61194,18 @@ } }, { - "id": "llmgateway/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, + "id": "llmgateway/claude-3.7-sonnet", + "name": "Claude 3.7 Sonnet", + "family": "claude", + "attachment": false, "reasoning": true, "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", + "temperature": true, + "release_date": "2025-02-24", + "last_updated": "2025-02-24", "modalities": { "input": [ - "text", - "image", - "pdf" + "text" ], "output": [ "text" @@ -68142,14 +61213,14 @@ }, "open_weights": false, "cost": { - "input": 10.0, - "output": 50.0, - "cache_read": 1.0, - "cache_write": 12.5 + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 }, "limit": { - "context": 1000000, - "output": 128000 + "context": 200000, + "output": 8192 } }, { @@ -68256,7 +61327,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-01", "last_updated": "2025-11-01", "modalities": { @@ -68355,7 +61426,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -68446,38 +61516,6 @@ "output": 64000 } }, - { - "id": "llmgateway/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 1000000 - } - }, { "id": "llmgateway/codestral", "name": "Codestral", @@ -68810,6 +61848,36 @@ "output": 65536 } }, + { + "id": "llmgateway/gemini-2.5-flash-lite-preview-09", + "name": "Gemini 2.5 Flash Lite Preview (09-2025)", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-25", + "last_updated": "2025-09-25", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 1048576, + "output": 1048576 + } + }, { "id": "llmgateway/gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -69271,8 +62339,8 @@ }, "open_weights": true, "cost": { - "input": 0.55, - "output": 2.2, + "input": 0.431, + "output": 2.007, "cache_read": 0.11, "cache_write": 0.0 }, @@ -69522,7 +62590,7 @@ "cache_write": 0.0 }, "limit": { - "context": 1024000, + "context": 1000000, "output": 131072 } }, @@ -70453,105 +63521,6 @@ "output": 128000 } }, - { - "id": "llmgateway/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "llmgateway/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "llmgateway/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "llmgateway/gpt-oss-120b", "name": "GPT OSS 120B", @@ -70722,8 +63691,8 @@ }, "open_weights": false, "cost": { - "input": 1.25, - "output": 2.5, + "input": 2.0, + "output": 6.0, "cache_read": 0.2 }, "limit": { @@ -70753,8 +63722,8 @@ }, "open_weights": false, "cost": { - "input": 1.25, - "output": 2.5, + "input": 2.0, + "output": 6.0, "cache_read": 0.2 }, "limit": { @@ -70853,36 +63822,6 @@ "output": 30000 } }, - { - "id": "llmgateway/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "llmgateway/grok-build-0.1", "name": "Grok Build 0.1", @@ -70934,8 +63873,8 @@ }, "open_weights": true, "cost": { - "input": 0.57, - "output": 2.3, + "input": 0.574, + "output": 2.294, "cache_read": 0.5 }, "limit": { @@ -70964,9 +63903,9 @@ }, "open_weights": true, "cost": { - "input": 0.6, - "output": 2.5, - "cache_read": 0.06 + "input": 0.574, + "output": 2.294, + "cache_read": 0.15 }, "limit": { "context": 262144, @@ -71356,6 +64295,70 @@ "output": 2048 } }, + { + "id": "llmgateway/mimo-v2-omni", + "name": "MiMo-V2-Omni", + "family": "mimo", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-12", + "release_date": "2026-03-18", + "last_updated": "2026-03-18", + "modalities": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.4, + "output": 2.0, + "cache_read": 0.08 + }, + "limit": { + "context": 256000, + "output": 131072 + } + }, + { + "id": "llmgateway/mimo-v2-pro", + "name": "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": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 3.0, + "cache_read": 0.2 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, { "id": "llmgateway/mimo-v2.5", "name": "MiMo-V2.5", @@ -71830,38 +64833,6 @@ "output": 16384 } }, - { - "id": "llmgateway/muse-spark-1.1", - "name": "Muse Spark 1.1", - "family": "muse", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-08", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 4.25, - "cache_read": 0.15 - }, - "limit": { - "context": 1048576, - "output": 32000 - } - }, { "id": "llmgateway/nemotron-3-ultra-550b", "name": "Nemotron 3 Ultra 550B A55B", @@ -72198,6 +65169,95 @@ "output": 32768 } }, + { + "id": "llmgateway/qwen-turbo", + "name": "Qwen Turbo", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-04", + "release_date": "2024-11-01", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.05, + "output": 0.2 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/qwen-vl-max", + "name": "Qwen-VL Max", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-04", + "release_date": "2024-04-08", + "last_updated": "2025-08-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.8, + "output": 3.2 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen-vl-plus", + "name": "Qwen-VL Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-04", + "release_date": "2024-01-25", + "last_updated": "2025-08-15", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.21, + "output": 0.64 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, { "id": "llmgateway/qwen2-5-vl-32b-instruct", "name": "Qwen2.5 VL 32B Instruct", @@ -72309,7 +65369,7 @@ "output": 0.58 }, "limit": { - "context": 262144, + "context": 262000, "output": 8192 } }, @@ -72628,7 +65688,7 @@ "open_weights": true, "cost": { "input": 0.15, - "output": 1.2 + "output": 1.5 }, "limit": { "context": 131072, @@ -72714,8 +65774,8 @@ }, "open_weights": true, "cost": { - "input": 0.98, - "output": 3.95 + "input": 0.5, + "output": 2.0 }, "limit": { "context": 131072, @@ -72930,37 +65990,6 @@ "output": 65536 } }, - { - "id": "llmgateway/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.15, - "output": 0.9, - "cache_read": 0.015 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, { "id": "llmgateway/qwen3.6-max-preview", "name": "Qwen3.6 Max Preview", @@ -73118,6 +66147,35 @@ "output": 65536 } }, + { + "id": "llmgateway/qwq-plus", + "name": "QwQ Plus", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-04", + "release_date": "2025-03-05", + "last_updated": "2025-03-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.8, + "output": 2.4 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, { "id": "llmgateway/seed-1.6-250615", "name": "Seed 1.6 (250615)", @@ -73581,35 +66639,6 @@ "output": 65536 } }, - { - "id": "longcat/LongCat-2.0", - "name": "LongCat-2.0", - "family": "longcat", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.75, - "output": 2.95, - "cache_read": 0.015 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "lucidquery/lucidnova-rf1-100b", "name": "LucidNova RF1 100B", @@ -73728,34 +66757,6 @@ "output": 60000 } }, - { - "id": "lynkr/lynkr-auto", - "name": "Lynkr Auto (complexity routing)", - "family": "auto", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-12-03", - "last_updated": "2026-07-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 128000, - "output": 8192 - } - }, { "id": "meganova/MiniMaxAI/MiniMax-M2.1", "name": "MiniMax M2.1", @@ -74467,7 +67468,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-01", "last_updated": "2025-11-01", "modalities": { @@ -74566,7 +67567,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -77177,38 +70177,6 @@ "output": 4096 } }, - { - "id": "meta/muse-spark-1.1", - "name": "Muse Spark 1.1", - "family": "muse", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-08", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 4.25, - "cache_read": 0.15 - }, - "limit": { - "context": 1000000, - "output": 32000 - } - }, { "id": "minimax-cn-coding-plan/MiniMax-M2", "name": "MiniMax-M2", @@ -78820,415 +71788,6 @@ "output": 131072 } }, - { - "id": "model-oracle-ai/auto", - "name": "Auto", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-29", - "last_updated": "2026-07-07", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/claude-haiku-4.5", - "name": "Claude Haiku 4.5 (latest)", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "model-oracle-ai/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/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": {}, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "model-oracle-ai/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "model-oracle-ai/gpt-4.1", - "name": "GPT-4.1", - "family": "gpt", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-04-14", - "last_updated": "2025-04-14", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1047576, - "output": 32768 - } - }, - { - "id": "model-oracle-ai/gpt-4.1-mini", - "name": "GPT-4.1 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-04-14", - "last_updated": "2025-04-14", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1047576, - "output": 32768 - } - }, - { - "id": "model-oracle-ai/gpt-5", - "name": "GPT-5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-09-30", - "release_date": "2025-08-07", - "last_updated": "2025-08-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/gpt-5.4-mini", - "name": "GPT-5.4 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/gpt-5.4-nano", - "name": "GPT-5.4 nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "model-oracle-ai/o4-mini", - "name": "o4-mini", - "family": "o-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-05", - "release_date": "2025-04-16", - "last_updated": "2025-04-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 200000, - "output": 100000 - } - }, { "id": "modelscope/Qwen/Qwen3-235B-A22B-Instruct", "name": "Qwen3 235B A22B Instruct 2507", @@ -93040,13 +85599,11 @@ { "id": "nano-gpt/poolside/laguna-m.1", "name": "Laguna M.1", - "family": "laguna", "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-28", - "last_updated": "2026-06-13", + "reasoning": false, + "tool_call": false, + "release_date": "2026-04-29", + "last_updated": "2026-04-29", "modalities": { "input": [ "text" @@ -93055,26 +85612,24 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.2, - "output": 0.4 + "input": 0.1, + "output": 0.3 }, "limit": { - "context": 262144, + "context": 128000, "output": 32768 } }, { "id": "nano-gpt/poolside/laguna-xs.2", "name": "Laguna XS.2", - "family": "laguna", "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-28", - "last_updated": "2026-06-13", + "reasoning": false, + "tool_call": false, + "release_date": "2026-04-29", + "last_updated": "2026-04-29", "modalities": { "input": [ "text" @@ -93083,13 +85638,13 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.2, - "output": 0.4 + "input": 0.1, + "output": 0.3 }, "limit": { - "context": 262144, + "context": 128000, "output": 32768 } }, @@ -98781,7 +91336,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -98872,39 +91427,6 @@ "output": 128000 } }, - { - "id": "neon/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "neon/claude-sonnet-4", "name": "Claude Sonnet 4.5", @@ -99208,70 +91730,6 @@ "output": 65536 } }, - { - "id": "neon/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": "neon/gemma-3-12b", - "name": "Gemma 3 12B", - "family": "gemma", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08-31", - "release_date": "2025-03-13", - "last_updated": "2025-03-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.5 - }, - "limit": { - "context": 131072, - "output": 16384 - } - }, { "id": "neon/gpt-5", "name": "GPT-5", @@ -99289,8 +91747,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99321,8 +91778,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99353,8 +91809,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99385,8 +91840,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99400,70 +91854,6 @@ "output": 128000 } }, - { - "id": "neon/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-11-13", - "last_updated": "2025-11-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.125 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "neon/gpt-5.1-codex-mini", - "name": "GPT-5.1 Codex mini", - "family": "gpt-codex", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-09-30", - "release_date": "2025-11-13", - "last_updated": "2025-11-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 2.0, - "cache_read": 0.025 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, { "id": "neon/gpt-5.2", "name": "GPT-5.2", @@ -99481,74 +91871,7 @@ "image" ], "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 1.75, - "output": 14.0, - "cache_read": 0.175 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "neon/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": "2025-12-11", - "last_updated": "2025-12-11", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 1.75, - "output": 14.0, - "cache_read": 0.175 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "neon/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-05", - "last_updated": "2026-02-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99580,8 +91903,7 @@ "pdf" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99612,8 +91934,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99644,8 +91965,7 @@ "image" ], "output": [ - "text", - "image" + "text" ] }, "open_weights": false, @@ -99659,6 +91979,38 @@ "output": 128000 } }, + { + "id": "neon/gpt-5.5", + "name": "GPT-5.5", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "knowledge": "2025-12-01", + "release_date": "2026-04-23", + "last_updated": "2026-04-23", + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5.0, + "output": 30.0, + "cache_read": 0.5 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, { "id": "neon/gpt-oss-120b", "name": "GPT OSS 120B", @@ -99715,151 +92067,6 @@ "output": 32768 } }, - { - "id": "neon/llama-4-maverick", - "name": "Llama 4 Maverick 17B Instruct", - "family": "llama", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-08", - "release_date": "2025-04-05", - "last_updated": "2025-04-05", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.5, - "output": 1.5 - }, - "limit": { - "context": 1000000, - "output": 16384 - } - }, - { - "id": "neon/meta-llama-3.1-8b-instruct", - "name": "Llama 3.1 8B Instruct", - "family": "llama", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2023-12-31", - "release_date": "2024-07-23", - "last_updated": "2024-07-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.45 - }, - "limit": { - "context": 131072, - "output": 16384 - } - }, - { - "id": "neon/meta-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", - "release_date": "2024-12-06", - "last_updated": "2024-12-06", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.5, - "output": 1.5 - }, - "limit": { - "context": 128000, - "output": 4096 - } - }, - { - "id": "neon/qwen3-next-80b-a3b-instruct", - "name": "Qwen3-Next 80B-A3B Instruct", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-09", - "last_updated": "2025-09", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 1.2 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, - { - "id": "neon/qwen35-122b-a10b", - "name": "Qwen3.5 122B-A10B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.22, - "output": 2.2 - }, - "limit": { - "context": 262144, - "output": 8000 - } - }, { "id": "neuralwatt/Qwen/Qwen3.5-397B-A17B-FP8", "name": "Qwen3.5 397B A17B FP8", @@ -101126,9 +93333,9 @@ }, "open_weights": true, "cost": { - "input": 1.6, - "output": 3.2, - "cache_read": 0.135 + "input": 1.69, + "output": 3.38, + "cache_read": 0.13 }, "limit": { "context": 1048576, @@ -101988,8 +94195,8 @@ }, "open_weights": true, "cost": { - "input": 0.8, - "output": 3.4, + "input": 0.95, + "output": 4.0, "cache_read": 0.16 }, "limit": { @@ -102462,8 +94669,8 @@ }, "open_weights": true, "cost": { - "input": 0.38, - "output": 1.55 + "input": 0.3, + "output": 1.3 }, "limit": { "context": 262144, @@ -102931,7 +95138,7 @@ "cost": { "input": 1.25, "output": 3.75, - "cache_read": 0.25, + "cache_read": 0.125, "cache_write": 1.5625 }, "limit": { @@ -103129,9 +95336,9 @@ }, "open_weights": true, "cost": { - "input": 0.522, - "output": 1.044, - "cache_read": 0.0043 + "input": 2.0, + "output": 6.0, + "cache_read": 0.4 }, "limit": { "context": 1048576, @@ -103217,8 +95424,7 @@ "open_weights": true, "cost": { "input": 0.13, - "output": 0.85, - "cache_read": 0.025 + "output": 0.85 }, "limit": { "context": 131072, @@ -103426,7 +95632,7 @@ }, "open_weights": true, "cost": { - "input": 1.38, + "input": 1.4, "output": 4.4, "cache_read": 0.26 }, @@ -105809,15 +98015,15 @@ } }, { - "id": "nvidia/z-ai/glm-5.2", - "name": "GLM-5.2", + "id": "nvidia/z-ai/glm-5.1", + "name": "GLM-5.1", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "release_date": "2026-03-27", + "last_updated": "2026-03-27", "modalities": { "input": [ "text" @@ -105832,7 +98038,7 @@ "output": 0.0 }, "limit": { - "context": 1000000, + "context": 131072, "output": 131072 } }, @@ -107920,138 +100126,6 @@ "output": 128000 } }, - { - "id": "openai/gpt-5.6", - "name": "GPT-5.6", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openai/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "openai/gpt-image-1", "name": "gpt-image-1", @@ -108162,39 +100236,6 @@ "output": 0 } }, - { - "id": "openai/gpt-realtime-2.1", - "name": "GPT-Realtime-2.1", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-09-30", - "release_date": "2026-07-06", - "last_updated": "2026-07-06", - "modalities": { - "input": [ - "text", - "audio", - "image" - ], - "output": [ - "text", - "audio" - ] - }, - "open_weights": false, - "cost": { - "input": 4.0, - "output": 24.0, - "cache_read": 0.4 - }, - "limit": { - "context": 128000, - "output": 32000 - } - }, { "id": "openai/o1", "name": "o1", @@ -108902,7 +100943,7 @@ }, { "id": "opencode-go/minimax-m2.5", - "name": "MiniMax-M2.5", + "name": "MiniMax M2.5", "family": "minimax-m2.5", "attachment": false, "reasoning": true, @@ -108932,7 +100973,7 @@ }, { "id": "opencode-go/minimax-m2.7", - "name": "MiniMax-M2.7", + "name": "MiniMax M2.7", "family": "minimax-m2.7", "attachment": false, "reasoning": true, @@ -108962,7 +101003,7 @@ }, { "id": "opencode-go/minimax-m3", - "name": "MiniMax-M3", + "name": "MiniMax M3 (3x usage)", "family": "minimax-m3", "attachment": false, "reasoning": true, @@ -108983,9 +101024,9 @@ }, "open_weights": true, "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.06 + "input": 0.1, + "output": 0.4, + "cache_read": 0.02 }, "limit": { "context": 1000000, @@ -109390,7 +101431,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -109514,39 +101554,6 @@ "output": 64000 } }, - { - "id": "opencode/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "opencode/deepseek-v4-flash", "name": "DeepSeek V4 Flash", @@ -110516,135 +102523,6 @@ "output": 128000 } }, - { - "id": "opencode/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "opencode/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "opencode/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "opencode/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "opencode/grok-build-0.1", "name": "Grok Build 0.1", @@ -110705,35 +102583,6 @@ "output": 256000 } }, - { - "id": "opencode/hy3-free", - "name": "Hy3 Free", - "family": "hy3-free", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-26", - "last_updated": "2026-06-26", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0 - }, - "limit": { - "context": 190000, - "output": 64000 - } - }, { "id": "opencode/hy3-preview-free", "name": "Hy3 preview Free", @@ -110920,38 +102769,6 @@ "output": 65536 } }, - { - "id": "opencode/kimi-k2.7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.19 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "opencode/ling-2.6-flash-free", "name": "Ling 2.6 Flash Free", @@ -111109,7 +102926,7 @@ }, { "id": "opencode/minimax-m2.1", - "name": "MiniMax-M2.1", + "name": "MiniMax M2.1", "family": "minimax", "attachment": false, "reasoning": true, @@ -111139,7 +102956,7 @@ }, { "id": "opencode/minimax-m2.1-free", - "name": "MiniMax-M2.1 Free", + "name": "MiniMax M2.1 Free", "family": "minimax-free", "attachment": false, "reasoning": true, @@ -111169,7 +102986,7 @@ }, { "id": "opencode/minimax-m2.5", - "name": "MiniMax-M2.5", + "name": "MiniMax M2.5", "family": "minimax", "attachment": false, "reasoning": true, @@ -111199,7 +103016,7 @@ }, { "id": "opencode/minimax-m2.5-free", - "name": "MiniMax-M2.5 Free", + "name": "MiniMax M2.5 Free", "family": "minimax-free", "attachment": false, "reasoning": true, @@ -111229,7 +103046,7 @@ }, { "id": "opencode/minimax-m2.7", - "name": "MiniMax-M2.7", + "name": "MiniMax M2.7", "family": "minimax", "attachment": false, "reasoning": true, @@ -111257,40 +103074,9 @@ "output": 131072 } }, - { - "id": "opencode/minimax-m3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.06 - }, - "limit": { - "context": 512000, - "output": 128000 - } - }, { "id": "opencode/minimax-m3-free", - "name": "MiniMax-M3 Free", + "name": "MiniMax M3 Free", "family": "minimax-m3-free", "attachment": false, "reasoning": true, @@ -111623,12 +103409,66 @@ "output": 4096 } }, + { + "id": "openrouter/aion-labs/aion-1.0", + "name": "Aion-1.0", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-04", + "last_updated": "2025-02-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 4.0, + "output": 8.0 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "openrouter/aion-labs/aion-1.0-mini", + "name": "Aion-1.0-Mini", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-04", + "last_updated": "2025-02-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.7, + "output": 1.4 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, { "id": "openrouter/aion-labs/aion-2.0", "name": "Aion-2.0", "attachment": false, "reasoning": true, - "tool_call": true, + "tool_call": false, "temperature": true, "release_date": "2026-02-23", "last_updated": "2026-02-23", @@ -111651,62 +103491,6 @@ "output": 32768 } }, - { - "id": "openrouter/aion-labs/aion-3.0", - "name": "Aion-3.0", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-07", - "last_updated": "2026-07-07", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 6.0, - "cache_read": 0.75 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, - { - "id": "openrouter/aion-labs/aion-3.0-mini", - "name": "Aion-3.0-Mini", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-07", - "last_updated": "2026-07-07", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.7, - "output": 1.4, - "cache_read": 0.18 - }, - "limit": { - "context": 131072, - "output": 32768 - } - }, { "id": "openrouter/aion-labs/aion-rp-llama-3.1-8b", "name": "Aion-RP 1.0 (8B)", @@ -111938,7 +103722,7 @@ "output": 5.0 }, "limit": { - "context": 32768, + "context": 16384, "output": 2048 } }, @@ -111974,39 +103758,6 @@ "output": 4096 } }, - { - "id": "openrouter/anthropic/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 10.0, - "output": 50.0, - "cache_read": 1.0, - "cache_write": 12.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "openrouter/anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 (latest)", @@ -112114,7 +103865,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -112244,8 +103995,7 @@ "attachment": true, "reasoning": true, "tool_call": true, - "temperature": true, - "knowledge": "2026-01", + "temperature": false, "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -112401,39 +104151,6 @@ "output": 128000 } }, - { - "id": "openrouter/anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "openrouter/arcee-ai/coder-large", "name": "Coder Large", @@ -112491,6 +104208,34 @@ "output": 80000 } }, + { + "id": "openrouter/arcee-ai/trinity-mini", + "name": "Trinity Mini", + "family": "trinity-mini", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-01", + "last_updated": "2025-12-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.045, + "output": 0.15 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, { "id": "openrouter/arcee-ai/virtuoso-large", "name": "Virtuoso Large", @@ -112545,7 +104290,7 @@ "output": 1.25 }, "limit": { - "context": 131072, + "context": 123000, "output": 16000 } }, @@ -112699,35 +104444,6 @@ "output": 2048 } }, - { - "id": "openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition", - "name": "Uncensored", - "family": "mistral", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": true, - "knowledge": "2024-04-30", - "release_date": "2025-07-09", - "last_updated": "2025-07-09", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.9 - }, - "limit": { - "context": 128000, - "output": 8192 - } - }, { "id": "openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free", "name": "Uncensored (free)", @@ -112954,7 +104670,7 @@ "output": 0.8001 }, "limit": { - "context": 131072, + "context": 128000, "output": 16000 } }, @@ -112979,8 +104695,8 @@ }, "open_weights": true, "cost": { - "input": 0.24, - "output": 0.9, + "input": 0.2, + "output": 0.77, "cache_read": 0.135 }, "limit": { @@ -113009,8 +104725,8 @@ }, "open_weights": true, "cost": { - "input": 0.25, - "output": 0.95, + "input": 0.21, + "output": 0.79, "cache_read": 0.13 }, "limit": { @@ -113043,7 +104759,7 @@ "output": 2.5 }, "limit": { - "context": 163840, + "context": 64000, "output": 16000 } }, @@ -113072,7 +104788,7 @@ "output": 0.8 }, "limit": { - "context": 128000, + "context": 8192, "output": 8192 } }, @@ -113127,12 +104843,12 @@ }, "open_weights": true, "cost": { - "input": 0.2145, - "output": 0.32175, - "cache_read": 0.02145 + "input": 0.2288, + "output": 0.3432, + "cache_read": 0.02288 }, "limit": { - "context": 131072, + "context": 128000, "output": 64000 } }, @@ -113188,10 +104904,10 @@ "cost": { "input": 0.09, "output": 0.18, - "cache_read": 0.018 + "cache_read": 0.02 }, "limit": { - "context": 1048576, + "context": 1000000, "output": 65536 } }, @@ -113268,7 +104984,7 @@ "reasoning": false, "tool_call": false, "temperature": true, - "knowledge": "2024-06", + "knowledge": "2025-06", "release_date": "2025-08-26", "last_updated": "2025-08-26", "modalities": { @@ -113328,6 +105044,41 @@ "output": 65535 } }, + { + "id": "openrouter/google/gemini-2.5-flash-lite-preview-09", + "name": "Gemini 2.5 Flash Lite Preview 09-2025", + "family": "gemini-flash-lite", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01-31", + "release_date": "2025-09-25", + "last_updated": "2025-09-25", + "modalities": { + "input": [ + "text", + "image", + "pdf", + "audio", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.4, + "cache_read": 0.01, + "cache_write": 0.083333 + }, + "limit": { + "context": 1048576, + "output": 65535 + } + }, { "id": "openrouter/google/gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -113469,23 +105220,22 @@ }, { "id": "openrouter/google/gemini-3-pro-image", - "name": "Nano Banana Pro", - "family": "gemini-pro", + "name": "Nano Banana Pro (Gemini 3 Pro Image)", + "family": "gemini", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", + "release_date": "2026-06-18", + "last_updated": "2026-06-18", "modalities": { "input": [ - "text", - "image" + "image", + "text" ], "output": [ - "text", - "image" + "image", + "text" ] }, "open_weights": false, @@ -113535,23 +105285,22 @@ }, { "id": "openrouter/google/gemini-3.1-flash-image", - "name": "Nano Banana 2", - "family": "gemini-flash", + "name": "Nano Banana 2 (Gemini 3.1 Flash Image)", + "family": "gemini", "attachment": true, "reasoning": true, "tool_call": false, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", + "release_date": "2026-06-18", + "last_updated": "2026-06-18", "modalities": { "input": [ - "text", - "image" + "image", + "text" ], "output": [ - "text", - "image" + "image", + "text" ] }, "open_weights": false, @@ -113630,37 +105379,6 @@ "output": 65536 } }, - { - "id": "openrouter/google/gemini-3.1-flash-lite-image", - "name": "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)", - "family": "gemini", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2025-01-01", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "image", - "text" - ], - "output": [ - "image", - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 1.5 - }, - "limit": { - "context": 65536, - "output": 66000 - } - }, { "id": "openrouter/google/gemini-3.1-flash-lite-preview", "name": "Gemini 3.1 Flash Lite Preview", @@ -113762,7 +105480,7 @@ "cache_write": 0.375 }, "limit": { - "context": 1048756, + "context": 1048576, "output": 65536 } }, @@ -114005,7 +105723,7 @@ "output": 0.0 }, "limit": { - "context": 262144, + "context": 131072, "output": 32768 } }, @@ -114416,6 +106134,34 @@ "output": 80000 } }, + { + "id": "openrouter/liquid/lfm-2-24b-a2b", + "name": "LFM2-24B-A2B", + "family": "liquid", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-02-25", + "last_updated": "2026-02-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.03, + "output": 0.12 + }, + "limit": { + "context": 32768, + "output": 32768 + } + }, { "id": "openrouter/liquid/lfm-2.5-1.2b-instruct:free", "name": "LFM2.5-1.2B-Instruct (free)", @@ -114503,6 +106249,35 @@ "output": 2000 } }, + { + "id": "openrouter/meta-llama/llama-3-8b-instruct", + "name": "Llama 3 8B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2023-12-31", + "release_date": "2024-04-18", + "last_updated": "2024-04-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.14, + "output": 0.14 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, { "id": "openrouter/meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B Instruct", @@ -114616,7 +106391,7 @@ "output": 0.201 }, "limit": { - "context": 131072, + "context": 60000, "output": 60000 } }, @@ -114641,12 +106416,12 @@ }, "open_weights": true, "cost": { - "input": 0.05, - "output": 0.33 + "input": 0.0509, + "output": 0.335 }, "limit": { - "context": 131072, - "output": 131072 + "context": 80000, + "output": 80000 } }, { @@ -114732,7 +106507,7 @@ "output": 0.0 }, "limit": { - "context": 131072, + "context": 65536, "output": 131072 } }, @@ -114758,8 +106533,8 @@ }, "open_weights": true, "cost": { - "input": 0.2, - "output": 0.8 + "input": 0.15, + "output": 0.6 }, "limit": { "context": 1048576, @@ -114792,7 +106567,7 @@ "output": 0.3 }, "limit": { - "context": 10000000, + "context": 327680, "output": 16384 } }, @@ -114855,6 +106630,35 @@ "output": 16384 } }, + { + "id": "openrouter/microsoft/phi-4-mini-instruct", + "name": "Phi 4 Mini Instruct", + "family": "phi", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-10-17", + "last_updated": "2025-10-17", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.35, + "cache_read": 0.08 + }, + "limit": { + "context": 128000, + "output": 128000 + } + }, { "id": "openrouter/microsoft/wizardlm-2-8x22b", "name": "WizardLM-2 8x22B", @@ -114879,7 +106683,7 @@ "output": 0.62 }, "limit": { - "context": 65536, + "context": 65535, "output": 8000 } }, @@ -114963,11 +106767,12 @@ "open_weights": true, "cost": { "input": 0.255, - "output": 1.02 + "output": 1.0, + "cache_read": 0.03 }, "limit": { - "context": 204800, - "output": 131072 + "context": 196608, + "output": 196608 } }, { @@ -115019,13 +106824,13 @@ }, "open_weights": true, "cost": { - "input": 0.3, - "output": 1.2, + "input": 0.29, + "output": 0.95, "cache_read": 0.03 }, "limit": { - "context": 204800, - "output": 131072 + "context": 196608, + "output": 196608 } }, { @@ -115048,12 +106853,11 @@ }, "open_weights": true, "cost": { - "input": 0.15, - "output": 0.9, - "cache_read": 0.05 + "input": 0.12, + "output": 0.48 }, "limit": { - "context": 204800, + "context": 196608, "output": 196608 } }, @@ -115077,11 +106881,11 @@ }, "open_weights": true, "cost": { - "input": 0.24, - "output": 0.96 + "input": 0.18, + "output": 0.72 }, "limit": { - "context": 204800, + "context": 196608, "output": 196608 } }, @@ -115112,8 +106916,8 @@ "cache_read": 0.06 }, "limit": { - "context": 1048576, - "output": 131072 + "context": 524288, + "output": 512000 } }, { @@ -115687,11 +107491,11 @@ "cost": { "input": 0.6, "output": 2.5, - "cache_read": 0.15 + "cache_read": 0.6 }, "limit": { "context": 262144, - "output": 100352 + "output": 262144 } }, { @@ -115717,11 +107521,10 @@ "open_weights": true, "cost": { "input": 0.375, - "output": 2.025, - "cache_read": 0.203 + "output": 2.025 }, "limit": { - "context": 262144, + "context": 256000, "output": 256000 } }, @@ -115747,9 +107550,9 @@ }, "open_weights": true, "cost": { - "input": 0.66, - "output": 3.41, - "cache_read": 0.15 + "input": 0.55, + "output": 3.2, + "cache_read": 0.11 }, "limit": { "context": 262144, @@ -115778,13 +107581,13 @@ }, "open_weights": true, "cost": { - "input": 0.719, - "output": 3.49, - "cache_read": 0.149 + "input": 0.74, + "output": 3.5, + "cache_read": 0.15 }, "limit": { "context": 262144, - "output": 262144 + "output": 16384 } }, { @@ -115843,43 +107646,13 @@ "output": 131072 } }, - { - "id": "openrouter/nex-agi/nex-n2-mini", - "name": "Nex-N2-Mini", - "family": "agi", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-24", - "last_updated": "2026-06-24", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.025, - "output": 0.1, - "cache_read": 0.0025 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "openrouter/nex-agi/nex-n2-pro", "name": "Nex-N2-Pro", "family": "agi", "attachment": true, "reasoning": true, - "tool_call": true, + "tool_call": false, "temperature": true, "release_date": "2026-06-08", "last_updated": "2026-06-08", @@ -116183,11 +107956,11 @@ }, "open_weights": true, "cost": { - "input": 0.08, - "output": 0.45 + "input": 0.085, + "output": 0.4 }, "limit": { - "context": 1000000, + "context": 262144, "output": 16384 } }, @@ -116215,7 +107988,7 @@ "output": 0.0 }, "limit": { - "context": 1000000, + "context": 262144, "output": 262144 } }, @@ -116244,7 +108017,7 @@ "cache_read": 0.1 }, "limit": { - "context": 1000000, + "context": 262144, "output": 16384 } }, @@ -117578,204 +109351,6 @@ "output": 128000 } }, - { - "id": "openrouter/openai/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openrouter/openai/gpt-5.6-luna-pro", - "name": "GPT-5.6 Luna Pro", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openrouter/openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openrouter/openai/gpt-5.6-sol-pro", - "name": "GPT-5.6 Sol Pro", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openrouter/openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "openrouter/openai/gpt-5.6-terra-pro", - "name": "GPT-5.6 Terra Pro", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "openrouter/openai/gpt-audio", "name": "GPT Audio", @@ -117887,8 +109462,8 @@ }, "open_weights": true, "cost": { - "input": 0.036, - "output": 0.18 + "input": 0.03, + "output": 0.15 }, "limit": { "context": 131072, @@ -117925,12 +109500,13 @@ }, { "id": "openrouter/openai/gpt-oss-20b", - "name": "GPT OSS 20B", + "name": "gpt-oss-20b", "family": "gpt-oss", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-06-30", "release_date": "2025-08-05", "last_updated": "2025-08-05", "modalities": { @@ -117959,6 +109535,7 @@ "reasoning": true, "tool_call": true, "temperature": true, + "knowledge": "2024-06-30", "release_date": "2025-08-05", "last_updated": "2025-08-05", "modalities": { @@ -118430,6 +110007,34 @@ "output": 128000 } }, + { + "id": "openrouter/openrouter/owl-alpha", + "name": "Owl Alpha", + "family": "alpha", + "attachment": false, + "reasoning": false, + "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 + }, + "limit": { + "context": 1048756, + "output": 262144 + } + }, { "id": "openrouter/openrouter/pareto-code", "name": "Pareto Code Router", @@ -118630,13 +110235,12 @@ { "id": "openrouter/poolside/laguna-m.1", "name": "Laguna M.1", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "release_date": "2026-04-28", - "last_updated": "2026-06-13", + "last_updated": "2026-04-28", "modalities": { "input": [ "text" @@ -118659,13 +110263,12 @@ { "id": "openrouter/poolside/laguna-m.1:free", "name": "Laguna M.1 (free)", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "release_date": "2026-04-28", - "last_updated": "2026-06-13", + "last_updated": "2026-04-28", "modalities": { "input": [ "text" @@ -118685,15 +110288,14 @@ } }, { - "id": "openrouter/poolside/laguna-xs-2.1", - "name": "Laguna XS 2.1", - "family": "laguna", + "id": "openrouter/poolside/laguna-xs.2", + "name": "Laguna XS.2", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-07-02", - "last_updated": "2026-07-02", + "release_date": "2026-04-28", + "last_updated": "2026-04-28", "modalities": { "input": [ "text" @@ -118704,9 +110306,9 @@ }, "open_weights": true, "cost": { - "input": 0.06, - "output": 0.12, - "cache_read": 0.03 + "input": 0.1, + "output": 0.2, + "cache_read": 0.05 }, "limit": { "context": 262144, @@ -118714,15 +110316,14 @@ } }, { - "id": "openrouter/poolside/laguna-xs-2.1:free", - "name": "Laguna XS 2.1 (free)", - "family": "laguna", + "id": "openrouter/poolside/laguna-xs.2:free", + "name": "Laguna XS.2 (free)", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-07-02", - "last_updated": "2026-07-02", + "release_date": "2026-04-28", + "last_updated": "2026-04-28", "modalities": { "input": [ "text" @@ -118766,7 +110367,7 @@ "output": 0.4 }, "limit": { - "context": 131072, + "context": 32768, "output": 16384 } }, @@ -118795,7 +110396,7 @@ "output": 0.1 }, "limit": { - "context": 131072, + "context": 32768, "output": 32768 } }, @@ -118824,7 +110425,7 @@ "output": 1.0 }, "limit": { - "context": 128000, + "context": 32768, "output": 32768 } }, @@ -118911,11 +110512,12 @@ }, "open_weights": true, "cost": { - "input": 0.25, - "output": 0.75 + "input": 0.8, + "output": 1.0, + "cache_read": 0.4 }, "limit": { - "context": 131072, + "context": 128000, "output": 128000 } }, @@ -118944,7 +110546,7 @@ "output": 0.24 }, "limit": { - "context": 131702, + "context": 40960, "output": 40960 } }, @@ -118998,8 +110600,9 @@ }, "open_weights": true, "cost": { - "input": 0.1495, - "output": 1.495 + "input": 0.1, + "output": 0.1, + "cache_read": 0.1 }, "limit": { "context": 262144, @@ -119031,7 +110634,7 @@ "output": 0.5 }, "limit": { - "context": 131072, + "context": 40960, "output": 16384 } }, @@ -119060,7 +110663,7 @@ "output": 0.19305 }, "limit": { - "context": 131072, + "context": 128000, "output": 32000 } }, @@ -119085,12 +110688,13 @@ }, "open_weights": true, "cost": { - "input": 0.13, - "output": 1.56 + "input": 0.08, + "output": 0.4, + "cache_read": 0.08 }, "limit": { "context": 131072, - "output": 32768 + "output": 131072 } }, { @@ -119118,7 +110722,7 @@ "output": 0.28 }, "limit": { - "context": 131072, + "context": 40960, "output": 16384 } }, @@ -119143,11 +110747,12 @@ }, "open_weights": true, "cost": { - "input": 0.117, - "output": 0.455 + "input": 0.05, + "output": 0.4, + "cache_read": 0.05 }, "limit": { - "context": 131072, + "context": 40960, "output": 8192 } }, @@ -119176,7 +110781,7 @@ "output": 1.8 }, "limit": { - "context": 1048576, + "context": 262144, "output": 65536 } }, @@ -119325,7 +110930,7 @@ "output": 0.0 }, "limit": { - "context": 1048576, + "context": 262000, "output": 262000 } }, @@ -119471,7 +111076,7 @@ "output": 0.78 }, "limit": { - "context": 262144, + "context": 131072, "output": 32768 } }, @@ -119562,7 +111167,7 @@ "output": 0.52 }, "limit": { - "context": 262144, + "context": 131072, "output": 32768 } }, @@ -119621,7 +111226,7 @@ "output": 0.416 }, "limit": { - "context": 262144, + "context": 131072, "output": 32768 } }, @@ -119646,11 +111251,11 @@ }, "open_weights": true, "cost": { - "input": 0.117, - "output": 0.455 + "input": 0.08, + "output": 0.5 }, "limit": { - "context": 256000, + "context": 131072, "output": 32768 } }, @@ -119679,7 +111284,7 @@ "output": 1.365 }, "limit": { - "context": 256000, + "context": 131072, "output": 32768 } }, @@ -119797,11 +111402,10 @@ "open_weights": true, "cost": { "input": 0.385, - "output": 2.45, - "cache_read": 0.111 + "output": 2.45 }, "limit": { - "context": 256000, + "context": 131072, "output": 64000 } }, @@ -119949,12 +111553,11 @@ }, "open_weights": true, "cost": { - "input": 0.285, - "output": 2.4, - "cache_read": 0.15 + "input": 0.2596, + "output": 2.385 }, "limit": { - "context": 262144, + "context": 262140, "output": 262140 } }, @@ -120259,7 +111862,6 @@ { "id": "openrouter/sakana/fugu-ultra", "name": "Fugu Ultra", - "family": "fugu", "attachment": true, "reasoning": true, "tool_call": true, @@ -120437,7 +112039,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-29", "last_updated": "2026-05-29", "modalities": { @@ -120461,6 +112063,33 @@ "output": 256000 } }, + { + "id": "openrouter/switchpoint/router", + "name": "Switchpoint Router", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-07-11", + "last_updated": "2025-07-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.85, + "output": 3.4 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, { "id": "openrouter/tencent/hunyuan-a13b-instruct", "name": "Hunyuan A13B Instruct", @@ -120490,35 +112119,6 @@ "output": 131072 } }, - { - "id": "openrouter/tencent/hy3", - "name": "Hy3", - "family": "hy3", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-06", - "last_updated": "2026-07-06", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.14, - "output": 0.58, - "cache_read": 0.035 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, { "id": "openrouter/tencent/hy3-preview", "name": "Hy3 preview", @@ -120548,34 +112148,6 @@ "output": 262144 } }, - { - "id": "openrouter/tencent/hy3:free", - "name": "Hy3 (free)", - "family": "hy3", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-06", - "last_updated": "2026-07-06", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, { "id": "openrouter/thedrummer/cydonia-24b-v4.1", "name": "Cydonia 24B V4.1", @@ -120629,8 +112201,8 @@ "output": 0.5 }, "limit": { - "context": 65536, - "output": 65536 + "context": 32768, + "output": 32768 } }, { @@ -120870,37 +112442,6 @@ "output": 1000000 } }, - { - "id": "openrouter/x-ai/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "openrouter/x-ai/grok-build-0.1", "name": "Grok Build 0.1", @@ -120914,8 +112455,7 @@ "modalities": { "input": [ "text", - "image", - "pdf" + "image" ], "output": [ "text" @@ -120957,11 +112497,10 @@ "open_weights": true, "cost": { "input": 0.105, - "output": 0.28, - "cache_read": 0.028 + "output": 0.28 }, "limit": { - "context": 1048576, + "context": 32000, "output": 131072 } }, @@ -121108,12 +112647,12 @@ "open_weights": true, "cost": { "input": 0.43, - "output": 1.75, + "output": 1.74, "cache_read": 0.08 }, "limit": { - "context": 200000, - "output": 16384 + "context": 202752, + "output": 131072 } }, { @@ -121286,9 +112825,9 @@ }, "open_weights": true, "cost": { - "input": 0.966, - "output": 3.036, - "cache_read": 0.1794 + "input": 0.98, + "output": 3.08, + "cache_read": 0.182 }, "limit": { "context": 202752, @@ -121315,7 +112854,7 @@ }, "open_weights": true, "cost": { - "input": 0.93, + "input": 0.94, "output": 3.0, "cache_read": 0.18 }, @@ -121426,7 +112965,7 @@ "attachment": true, "reasoning": true, "tool_call": true, - "temperature": true, + "temperature": false, "release_date": "2026-04-21", "last_updated": "2026-04-21", "modalities": { @@ -121458,7 +112997,7 @@ "attachment": true, "reasoning": true, "tool_call": true, - "temperature": false, + "temperature": true, "release_date": "2026-04-27", "last_updated": "2026-04-27", "modalities": { @@ -121473,10 +113012,10 @@ }, "open_weights": false, "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 + "input": 3.0, + "output": 15.0, + "cache_read": 0.3, + "cache_write": 3.75 }, "limit": { "context": 1000000, @@ -121573,9 +113112,9 @@ }, "open_weights": false, "cost": { - "input": 0.66, - "output": 3.41, - "cache_read": 0.15 + "input": 0.55, + "output": 3.2, + "cache_read": 0.11 }, "limit": { "context": 262144, @@ -121590,7 +113129,7 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-02-16", + "knowledge": "2025-12-01", "release_date": "2026-04-27", "last_updated": "2026-04-27", "modalities": { @@ -121607,8 +113146,7 @@ "cost": { "input": 5.0, "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 + "cache_read": 0.5 }, "limit": { "context": 1050000, @@ -121647,37 +113185,6 @@ "output": 128000 } }, - { - "id": "openrouter/~x-ai/grok", - "name": "Grok Latest", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 1000000 - } - }, { "id": "orcarouter/anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 (latest)", @@ -121785,7 +113292,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -125197,2386 +116704,6 @@ "output": 4096 } }, - { - "id": "pioneer/HuggingFaceTB/SmolLM3-3B-Base", - "name": "SmolLM3 3B Base", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-06-30", - "last_updated": "2025-06-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/LiquidAI/LFM2-24B-A2B", - "name": "LFM2 24B A2B", - "family": "liquid", - "attachment": false, - "reasoning": true, - "tool_call": false, - "temperature": true, - "release_date": "2026-01-31", - "last_updated": "2026-02-25", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.03, - "output": 0.12, - "cache_read": 0.03, - "cache_write": 0.03 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/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.279, - "output": 1.2, - "cache_read": 0.279, - "cache_write": 0.279 - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - { - "id": "pioneer/MiniMaxAI/MiniMax-M3", - "name": "MiniMax-M3", - "family": "minimax", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 1.2, - "cache_read": 0.06, - "cache_write": 0.3 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "pioneer/Qwen/Qwen3-1.7B-Base", - "name": "Qwen3 1.7B Base", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-03-31", - "last_updated": "2025-03-31", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.1, - "cache_read": 0.1, - "cache_write": 0.1 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/Qwen3-32B", - "name": "Qwen3 32B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-04", - "last_updated": "2025-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.9, - "output": 0.9, - "cache_read": 0.9, - "cache_write": 0.9 - }, - "limit": { - "context": 131072, - "output": 16384 - } - }, - { - "id": "pioneer/Qwen/Qwen3-4B-Base", - "name": "Qwen3 4B Base", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-03-31", - "last_updated": "2025-03-31", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/Qwen3-4B-Instruct", - "name": "Qwen3 4B Instruct", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-07-31", - "last_updated": "2025-07-31", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 0.2, - "cache_read": 0.2, - "cache_write": 0.2 - }, - "limit": { - "context": 262144, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/Qwen3-8B", - "name": "Qwen3 8B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-03-31", - "release_date": "2025-03-31", - "last_updated": "2025-04-28", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.2, - "cache_read": 0.2, - "cache_write": 0.2 - }, - "limit": { - "context": 131072, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/Qwen3.5-9B", - "name": "Qwen3.5 9B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 0.3, - "cache_read": 0.3, - "cache_write": 0.3 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/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.6, - "output": 0.6, - "cache_read": 0.6, - "cache_write": 0.6 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/Qwen/Qwen3.6-35B-A3B", - "name": "Qwen3.6 35B-A3B", - "family": "qwen", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-17", - "last_updated": "2026-04-17", - "modalities": { - "input": [ - "text", - "image", - "video", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.14, - "output": 1.0, - "cache_read": 0.028, - "cache_write": 0.175 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, - { - "id": "pioneer/XiaomiMiMo/MiMo-V2.5", - "name": "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": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.14, - "output": 0.28, - "cache_read": 0.0028, - "cache_write": 0.14 - }, - "limit": { - "context": 1050000, - "output": 131072 - } - }, - { - "id": "pioneer/XiaomiMiMo/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.435, - "output": 0.87, - "cache_read": 0.0036, - "cache_write": 0.435 - }, - "limit": { - "context": 1050000, - "output": 131072 - } - }, - { - "id": "pioneer/claude-haiku-4.5", - "name": "Claude Haiku 4.5 (latest)", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 5.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "pioneer/claude-opus-4.1", - "name": "Claude Opus 4.1 (latest)", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-03-31", - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 15.0, - "output": 75.0, - "cache_read": 1.5, - "cache_write": 18.75 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - { - "id": "pioneer/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", - "release_date": "2025-11-24", - "last_updated": "2025-11-24", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "pioneer/claude-opus-4.6", - "name": "Claude Opus 4.6", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-05-31", - "release_date": "2026-02-05", - "last_updated": "2026-03-13", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "pioneer/claude-opus-4.7", - "name": "Claude Opus 4.7", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-04-16", - "last_updated": "2026-04-16", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "pioneer/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "pioneer/claude-sonnet-4.5", - "name": "Claude Sonnet 4.5 (latest)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-07-31", - "release_date": "2025-09-29", - "last_updated": "2025-09-29", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0, - "cache_read": 0.3, - "cache_write": 3.75 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, - { - "id": "pioneer/claude-sonnet-4.6", - "name": "Claude Sonnet 4.6", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-08-31", - "release_date": "2026-02-17", - "last_updated": "2026-03-13", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0, - "cache_read": 0.3, - "cache_write": 3.75 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "pioneer/deepseek-ai/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.1, - "output": 0.2, - "cache_read": 0.0197, - "cache_write": 0.1 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "pioneer/deepseek-ai/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.435, - "output": 0.87, - "cache_read": 0.003625, - "cache_write": 0.435 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "pioneer/fastino/gliguard-LLMGuardrails-300M", - "name": "GLiGuard LLM Guardrails 300M", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-30", - "last_updated": "2026-04-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/fastino/gliner2-base-v1", - "name": "GLiNER2 Base", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-06-30", - "last_updated": "2025-06-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/fastino/gliner2-large-v1", - "name": "GLiNER2 Large", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-06-30", - "last_updated": "2025-06-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/fastino/gliner2-multi-large-v1", - "name": "GLiNER2 Multi Large", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-11-30", - "last_updated": "2025-11-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/fastino/gliner2-multi-v1", - "name": "GLiNER2 Multi", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2025-11-30", - "last_updated": "2025-11-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/fastino/gliner2-privacy-filter-PII-multi", - "name": "GLiNER2 Privacy Filter PII (Multi)", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-30", - "last_updated": "2026-04-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 8192, - "output": 4096 - } - }, - { - "id": "pioneer/gemini-3-flash", - "name": "Gemini 3 Flash Preview", - "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", - "video", - "audio", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.5, - "output": 3.0, - "cache_read": 0.05, - "cache_write": 0.083333 - }, - "limit": { - "context": 1048576, - "output": 65535 - } - }, - { - "id": "pioneer/gemini-3.1-pro", - "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", - "video", - "audio", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 12.0, - "cache_read": 0.2, - "cache_write": 0.375 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "pioneer/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, - "cache_write": 0.083333 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "pioneer/google/diffusiongemma-26B-A4B-it", - "name": "DiffusionGemma 26B-A4B IT", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-05-31", - "last_updated": "2026-05-31", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.5, - "output": 0.5, - "cache_read": 0.5, - "cache_write": 0.5 - }, - "limit": { - "context": 262144, - "output": 4096 - } - }, - { - "id": "pioneer/google/gemma-3-4b-pt", - "name": "Gemma 3 4B (Pretrained)", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-02-28", - "last_updated": "2025-02-28", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.15, - "cache_read": 0.15, - "cache_write": 0.15 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/google/gemma-4-12B-it", - "name": "Gemma 4 12B IT", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-05-31", - "last_updated": "2026-05-31", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 0.25, - "cache_read": 0.25, - "cache_write": 0.25 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/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", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.5, - "output": 0.5, - "cache_read": 0.5, - "cache_write": 0.5 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/google/gemma-4-E2B-it", - "name": "Gemma 4 E2B 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", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.1, - "cache_read": 0.1, - "cache_write": 0.1 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/google/gemma-4-E4B-it", - "name": "Gemma 4 E4B 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", - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.2, - "cache_read": 0.2, - "cache_write": 0.2 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/gpt-4.1", - "name": "GPT-4.1", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-04-14", - "last_updated": "2025-04-14", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 8.0, - "cache_read": 1.0, - "cache_write": 2.0 - }, - "limit": { - "context": 1047576, - "output": 32768 - } - }, - { - "id": "pioneer/gpt-4.1-mini", - "name": "GPT-4.1 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-04-14", - "last_updated": "2025-04-14", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.4, - "output": 1.6, - "cache_read": 0.2, - "cache_write": 0.4 - }, - "limit": { - "context": 1047576, - "output": 32768 - } - }, - { - "id": "pioneer/gpt-4.1-nano", - "name": "GPT-4.1 nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-04-14", - "last_updated": "2025-04-14", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.05, - "cache_write": 0.1 - }, - "limit": { - "context": 1047576, - "output": 32768 - } - }, - { - "id": "pioneer/gpt-4o", - "name": "GPT-4o", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2023-09", - "release_date": "2024-05-13", - "last_updated": "2024-08-06", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 10.0, - "cache_read": 1.25, - "cache_write": 2.5 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, - { - "id": "pioneer/gpt-4o-mini", - "name": "GPT-4o mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2023-09", - "release_date": "2024-07-18", - "last_updated": "2024-07-18", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.075, - "cache_write": 0.15 - }, - "limit": { - "context": 128000, - "output": 16384 - } - }, - { - "id": "pioneer/gpt-5-mini", - "name": "GPT-5 Mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-05-30", - "release_date": "2025-08-07", - "last_updated": "2025-08-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 2.0, - "cache_read": 0.025, - "cache_write": 0.25 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5-nano", - "name": "GPT-5 Nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-05-30", - "release_date": "2025-08-07", - "last_updated": "2025-08-07", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.05, - "output": 0.4, - "cache_read": 0.005, - "cache_write": 0.05 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5.1", - "name": "GPT-5.1", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2024-09-30", - "release_date": "2025-11-13", - "last_updated": "2025-11-13", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 10.0, - "cache_read": 0.125, - "cache_write": 1.25 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "pioneer/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-05", - "last_updated": "2026-02-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.75, - "output": 14.0, - "cache_read": 0.175, - "cache_write": 1.75 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 2.5 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5.4-mini", - "name": "GPT-5.4 mini", - "family": "gpt-mini", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.75, - "output": 4.5, - "cache_read": 0.075, - "cache_write": 0.75 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5.4-nano", - "name": "GPT-5.4 nano", - "family": "gpt-nano", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-17", - "last_updated": "2026-03-17", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 1.25, - "cache_read": 0.02, - "cache_write": 0.2 - }, - "limit": { - "context": 1047576, - "output": 128000 - } - }, - { - "id": "pioneer/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 5.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "pioneer/meta-llama/Llama-3.1-8B-Instruct", - "name": "Llama 3.1 8B Instruct", - "family": "llama", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2023-12-31", - "release_date": "2024-06-30", - "last_updated": "2024-07-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.2, - "cache_read": 0.2, - "cache_write": 0.2 - }, - "limit": { - "context": 131072, - "output": 16384 - } - }, - { - "id": "pioneer/meta-llama/Llama-3.2-1B-Instruct", - "name": "Llama 3.2 1B Instruct", - "family": "llama", - "attachment": false, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2023-12-31", - "release_date": "2024-08-31", - "last_updated": "2024-09-25", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.201, - "cache_read": 0.1, - "cache_write": 0.1 - }, - "limit": { - "context": 131072, - "output": 60000 - } - }, - { - "id": "pioneer/meta-llama/Llama-3.2-3B-Instruct", - "name": "Llama 3.2 3B Instruct", - "family": "llama", - "attachment": false, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2023-12-31", - "release_date": "2024-08-31", - "last_updated": "2024-09-25", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.335, - "cache_read": 0.1, - "cache_write": 0.1 - }, - "limit": { - "context": 131072, - "output": 80000 - } - }, - { - "id": "pioneer/meta-llama/Llama-3.3-70B-Instruct", - "name": "Llama-3.3-70B-Instruct", - "family": "llama", - "attachment": true, - "reasoning": true, - "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.9, - "output": 0.9, - "cache_read": 0.9, - "cache_write": 0.9 - }, - "limit": { - "context": 131072, - "output": 4096 - } - }, - { - "id": "pioneer/mistral-medium-3.5", - "name": "Mistral Medium 3.5", - "family": "mistral-medium", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-29", - "last_updated": "2026-04-29", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.5, - "output": 7.5, - "cache_read": 1.5, - "cache_write": 1.5 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, - { - "id": "pioneer/mistralai/Mistral-7B-Instruct-v0.3", - "name": "Mistral 7B Instruct v0.3", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2023-04-30", - "last_updated": "2023-04-30", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.2, - "output": 0.2, - "cache_read": 0.2, - "cache_write": 0.2 - }, - "limit": { - "context": 32768, - "output": 4096 - } - }, - { - "id": "pioneer/mistralai/Mistral-Nemo-Instruct", - "name": "Mistral Nemo", - "family": "mistral-nemo", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-07", - "release_date": "2024-07-01", - "last_updated": "2024-07-01", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.02, - "output": 0.03, - "cache_read": 0.02, - "cache_write": 0.02 - }, - "limit": { - "context": 131072, - "output": 128000 - } - }, - { - "id": "pioneer/mistralai/Mistral-Small-4-119B", - "name": "Mistral Small 4", - "family": "mistral-small", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-06", - "release_date": "2026-03-16", - "last_updated": "2026-03-16", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.015, - "cache_write": 0.15 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "pioneer/moonshotai/Kimi-K2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "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.95, - "output": 4.0, - "cache_read": 0.34, - "cache_write": 0.95 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, - { - "id": "pioneer/moonshotai/Kimi-K2.7-Code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.19, - "cache_write": 0.95 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "pioneer/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", - "name": "Nemotron 3 Nano 30B A3B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-12-15", - "last_updated": "2025-12-15", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.2, - "cache_read": 0.05, - "cache_write": 0.05 - }, - "limit": { - "context": 262144, - "output": 131072 - } - }, - { - "id": "pioneer/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", - "name": "Nemotron 3 Super 120B A12B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-03-11", - "last_updated": "2026-03-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.09, - "output": 0.45, - "cache_read": 0.09, - "cache_write": 0.09 - }, - "limit": { - "context": 1000000, - "output": 32000 - } - }, - { - "id": "pioneer/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", - "name": "Nemotron 3 Ultra 550B A55B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-04", - "last_updated": "2026-06-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.5, - "output": 2.5, - "cache_read": 0.15, - "cache_write": 0.5 - }, - "limit": { - "context": 1000000, - "output": 65000 - } - }, - { - "id": "pioneer/openai/gpt-oss-120b", - "name": "GPT OSS 120B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.015, - "cache_write": 0.15 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, - { - "id": "pioneer/openai/gpt-oss-20b", - "name": "GPT OSS 20B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.07, - "output": 0.3, - "cache_read": 0.035, - "cache_write": 0.07 - }, - "limit": { - "context": 131072, - "output": 8192 - } - }, - { - "id": "pioneer/pioneer/auto", - "name": "Pioneer Auto", - "family": "auto", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2024-01-01", - "last_updated": "2025-06-29", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1048576, - "output": 4096 - } - }, - { - "id": "pioneer/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_read": 0.0375, - "cache_write": 0.234375 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "pioneer/qwen3.6-max-preview", - "name": "Qwen3.6 Max Preview", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-04-20", - "last_updated": "2026-04-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.04, - "output": 6.24, - "cache_read": 0.208, - "cache_write": 1.3 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "pioneer/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.325, - "output": 1.95, - "cache_read": 0.065, - "cache_write": 0.40625 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "pioneer/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": 1.25, - "output": 3.75, - "cache_read": 0.25, - "cache_write": 1.5625 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "pioneer/qwen3.7-plus", - "name": "Qwen3.7 Plus", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2026-06-02", - "last_updated": "2026-06-02", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.32, - "output": 1.28, - "cache_read": 0.064, - "cache_write": 0.4 - }, - "limit": { - "context": 1000000, - "output": 65536 - } - }, - { - "id": "pioneer/sakana/fugu-ultra", - "name": "Fugu Ultra", - "family": "fugu", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "release_date": "2026-06-15", - "last_updated": "2026-06-15", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 5.0 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "pioneer/zai-org/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", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.98, - "output": 3.08, - "cache_read": 0.182, - "cache_write": 0.98 - }, - "limit": { - "context": 202752, - "output": 131072 - } - }, - { - "id": "pioneer/zai-org/GLM-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26, - "cache_write": 1.4 - }, - "limit": { - "context": 1048576, - "output": 128000 - } - }, { "id": "poe/anthropic/claude-haiku-3", "name": "Claude-Haiku-3", @@ -127839,7 +116966,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -131427,7 +120553,6 @@ { "id": "poolside/poolside/laguna-m.1", "name": "Laguna M.1", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, @@ -131442,37 +120567,7 @@ "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": "poolside/poolside/laguna-xs-2.1", - "name": "Laguna XS 2.1", - "family": "laguna", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-02", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, + "open_weights": false, "cost": { "input": 0.0, "output": 0.0, @@ -131487,7 +120582,6 @@ { "id": "poolside/poolside/laguna-xs.2", "name": "Laguna XS.2", - "family": "laguna", "attachment": false, "reasoning": true, "tool_call": true, @@ -135758,69 +124852,38 @@ } }, { - "id": "routing-run/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0 - }, - "limit": { - "context": 1000000, - "output": 32000 - } - }, - { - "id": "routing-run/claude-sonnet-4.6", - "name": "Claude Sonnet 4.6", - "family": "claude-sonnet", + "id": "routing-run/route/deepseek-v3.2", + "name": "DeepSeek V3.2", + "family": "deepseek", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-08-31", - "release_date": "2026-02-17", - "last_updated": "2026-03-13", + "knowledge": "2024-07", + "release_date": "2025-12-01", + "last_updated": "2025-12-01", "modalities": { "input": [ "text", "image", - "pdf" + "video" ], "output": [ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { - "input": 3.0, - "output": 15.0 + "input": 0.4928, + "output": 0.7392 }, "limit": { - "context": 1000000, - "output": 64000 + "context": 163840, + "output": 163840 } }, { - "id": "routing-run/deepseek-v4-flash", + "id": "routing-run/route/deepseek-v4-flash", "name": "DeepSeek V4 Flash", "family": "deepseek-flash", "attachment": false, @@ -135840,16 +124903,47 @@ }, "open_weights": true, "cost": { - "input": 0.112, - "output": 0.224 + "input": 0.4928, + "output": 0.7392, + "cache_read": 0.0028 }, "limit": { "context": 1000000, - "output": 64000 + "output": 131072 } }, { - "id": "routing-run/deepseek-v4-pro", + "id": "routing-run/route/deepseek-v4-flash-6bit", + "name": "DeepSeek V4 Flash 6bit", + "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.4928, + "output": 0.7392, + "cache_read": 0.0028 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "routing-run/route/deepseek-v4-pro", "name": "DeepSeek V4 Pro", "family": "deepseek-thinking", "attachment": false, @@ -135869,24 +124963,26 @@ }, "open_weights": true, "cost": { - "input": 0.348, - "output": 0.696 + "input": 0.4928, + "output": 0.7392, + "cache_read": 0.003625 }, "limit": { "context": 1000000, - "output": 64000 + "output": 131072 } }, { - "id": "routing-run/glm-5.2", - "name": "GLM-5.2", - "family": "glm", + "id": "routing-run/route/deepseek-v4-pro-6bit", + "name": "DeepSeek V4 Pro 6bit", + "family": "deepseek-thinking", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-04-24", "modalities": { "input": [ "text" @@ -135897,24 +124993,55 @@ }, "open_weights": true, "cost": { - "input": 0.8, - "output": 2.4 + "input": 0.4928, + "output": 0.7392, + "cache_read": 0.003625 }, "limit": { - "context": 200000, - "output": 32000 + "context": 1000000, + "output": 131072 } }, { - "id": "routing-run/glm-5.2-nitro", - "name": "GLM 5.2 Nitro", + "id": "routing-run/route/gemma-4-31b-it", + "name": "Gemma 4 31B IT", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-04-02", + "last_updated": "2026-04-02", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 131072, + "output": 65536 + } + }, + { + "id": "routing-run/route/glm-5.1", + "name": "GLM 5.1", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "release_date": "2026-04-07", + "last_updated": "2026-04-07", "modalities": { "input": [ "text" @@ -135925,109 +125052,80 @@ }, "open_weights": true, "cost": { - "input": 0.8, - "output": 2.4 + "input": 1.0, + "output": 3.0, + "cache_read": 0.26, + "cache_write": 0.0 }, "limit": { - "context": 200000, - "output": 32000 + "context": 202752, + "output": 65536 } }, { - "id": "routing-run/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, + "id": "routing-run/route/glm-5.1-6bit", + "name": "GLM 5.1 6bit", + "family": "glm", + "attachment": false, "reasoning": true, "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", + "temperature": true, + "release_date": "2026-04-07", + "last_updated": "2026-04-07", "modalities": { "input": [ - "text", - "image", - "pdf" + "text" ], "output": [ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { - "input": 0.7, - "output": 4.2 + "input": 1.0, + "output": 3.0, + "cache_read": 0.26, + "cache_write": 0.0 }, "limit": { - "context": 1000000, - "output": 128000 + "context": 202752, + "output": 65536 } }, { - "id": "routing-run/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", + "id": "routing-run/route/kimi-k2.5", + "name": "Kimi K2.5", + "family": "kimi-k2", "attachment": true, - "reasoning": true, + "reasoning": false, "tool_call": true, "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", + "knowledge": "2025-01", + "release_date": "2026-01", + "last_updated": "2026-01", "modalities": { "input": [ "text", "image", - "pdf" + "video" ], "output": [ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { - "input": 2.5, - "output": 15.0 + "input": 0.462, + "output": 2.42, + "cache_read": 0.1 }, "limit": { - "context": 1000000, - "output": 128000 + "context": 131072, + "output": 32768 } }, { - "id": "routing-run/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.5, - "output": 9.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "routing-run/kimi-k2.6", + "id": "routing-run/route/kimi-k2.6", "name": "Kimi K2.6", "family": "kimi-k2", "attachment": true, @@ -136049,17 +125147,18 @@ }, "open_weights": true, "cost": { - "input": 0.275, - "output": 1.1 + "input": 0.462, + "output": 2.42, + "cache_read": 0.16 }, "limit": { - "context": 200000, - "output": 32000 + "context": 262144, + "output": 262144 } }, { - "id": "routing-run/kimi-k2.6-nitro", - "name": "Kimi K2.6 Nitro", + "id": "routing-run/route/kimi-k2.6-6bit", + "name": "Kimi K2.6 6bit", "family": "kimi-k2", "attachment": true, "reasoning": true, @@ -136080,172 +125179,276 @@ }, "open_weights": true, "cost": { - "input": 0.275, - "output": 1.1 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - { - "id": "routing-run/kimi-k2.7-code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.275, - "output": 1.1 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - { - "id": "routing-run/kimi-k2.7-code-nitro", - "name": "Kimi K2.7 Code Nitro", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.275, - "output": 1.1 - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - { - "id": "routing-run/nemotron-3-ultra", - "name": "Nemotron 3 Ultra 550B A55B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-04", - "last_updated": "2026-06-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.1, - "output": 0.1 - }, - "limit": { - "context": 131072, - "output": 32000 - } - }, - { - "id": "routing-run/qwen3.5-9b", - "name": "Qwen3.5 9B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-23", - "last_updated": "2026-02-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.16, - "output": 0.48 + "input": 0.462, + "output": 2.42, + "cache_read": 0.16 }, "limit": { "context": 262144, - "output": 32000 + "output": 262144 } }, { - "id": "sakana/fugu", - "name": "Fugu", - "family": "fugu", + "id": "routing-run/route/mimo-v2.5", + "name": "MiMo V2.5", + "family": "mimo", "attachment": true, "reasoning": true, "tool_call": true, - "temperature": false, - "release_date": "2026-06-15", - "last_updated": "2026-06-15", + "temperature": true, + "knowledge": "2024-12", + "release_date": "2026-04-22", + "last_updated": "2026-04-22", "modalities": { "input": [ "text", - "image" + "image", + "video" ], "output": [ "text" ] }, - "open_weights": false, - "cost": {}, + "open_weights": true, + "cost": { + "input": 0.45, + "output": 1.35, + "cache_read": 0.2 + }, "limit": { "context": 1000000, - "output": 1000000 + "output": 262144 } }, { - "id": "sakana/fugu-ultra", - "name": "Fugu Ultra", - "family": "fugu", + "id": "routing-run/route/mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "family": "mimo", "attachment": true, "reasoning": true, "tool_call": true, - "temperature": false, - "release_date": "2026-06-15", - "last_updated": "2026-06-15", + "temperature": true, + "knowledge": "2024-12", + "release_date": "2026-04-22", + "last_updated": "2026-04-22", "modalities": { "input": [ "text", - "image" + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.45, + "output": 1.35, + "cache_read": 0.2 + }, + "limit": { + "context": 1000000, + "output": 262144 + } + }, + { + "id": "routing-run/route/mimo-v2.5-pro-6bit", + "name": "MiMo V2.5 Pro 6bit", + "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": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.45, + "output": 1.35, + "cache_read": 0.2 + }, + "limit": { + "context": 1000000, + "output": 262144 + } + }, + { + "id": "routing-run/route/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.193, + "output": 1.238, + "cache_read": 0.03, + "cache_write": 0.375 + }, + "limit": { + "context": 100000, + "output": 131072 + } + }, + { + "id": "routing-run/route/minimax-m2.5-highspeed", + "name": "MiniMax M2.5 Highspeed", + "family": "minimax", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-13", + "last_updated": "2026-02-13", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.193, + "output": 1.238, + "cache_read": 0.06, + "cache_write": 0.375 + }, + "limit": { + "context": 100000, + "output": 131072 + } + }, + { + "id": "routing-run/route/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.33, + "output": 1.32, + "cache_read": 0.06, + "cache_write": 0.375 + }, + "limit": { + "context": 100000, + "output": 131072 + } + }, + { + "id": "routing-run/route/minimax-m2.7-highspeed", + "name": "MiniMax M2.7 Highspeed", + "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.33, + "output": 1.32, + "cache_read": 0.06, + "cache_write": 0.375 + }, + "limit": { + "context": 100000, + "output": 131072 + } + }, + { + "id": "routing-run/route/mistral-large-3", + "name": "Mistral Large 3", + "family": "mistral-large", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-11", + "release_date": "2024-11-01", + "last_updated": "2025-12-02", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.5 + }, + "limit": { + "context": 128000, + "output": 32768 + } + }, + { + "id": "routing-run/route/mistral-medium", + "name": "Mistral Medium 2505", + "family": "mistral-medium", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2025-05-07", + "last_updated": "2025-05-07", + "modalities": { + "input": [ + "text", + "image", + "video" ], "output": [ "text" @@ -136253,13 +125456,157 @@ }, "open_weights": false, "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5 + "input": 0.4, + "output": 2.0 }, "limit": { - "context": 1000000, - "output": 1000000 + "context": 128000, + "output": 32768 + } + }, + { + "id": "routing-run/route/mistral-small", + "name": "Mistral Small 2503", + "family": "mistral-small", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-06", + "release_date": "2026-03-16", + "last_updated": "2026-03-16", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.15, + "output": 0.6 + }, + "limit": { + "context": 128000, + "output": 32768 + } + }, + { + "id": "routing-run/route/qwen3.6-27b", + "name": "Qwen3.6 27B", + "family": "qwen", + "attachment": false, + "reasoning": false, + "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": 1.1, + "output": 3.3 + }, + "limit": { + "context": 202000, + "output": 32768 + } + }, + { + "id": "routing-run/route/qwen3.6-27b-202k", + "name": "Qwen3.6 27B 202K", + "family": "qwen", + "attachment": false, + "reasoning": false, + "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": 1.1, + "output": 3.3 + }, + "limit": { + "context": 202000, + "output": 32768 + } + }, + { + "id": "routing-run/route/step-3.5-flash", + "name": "Step 3.5 Flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-01-29", + "last_updated": "2026-02-13", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.096, + "output": 0.288, + "cache_read": 0.019 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, + { + "id": "routing-run/route/stepfun-3.5-flash", + "name": "StepFun 3.5 Flash", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-01", + "release_date": "2026-01-29", + "last_updated": "2026-02-13", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.096, + "output": 0.288, + "cache_read": 0.019 + }, + "limit": { + "context": 262144, + "output": 65536 } }, { @@ -136691,39 +126038,6 @@ "output": 128000 } }, - { - "id": "sap-ai-core/anthropic--claude-4.8-opus", - "name": "anthropic--claude-4.8-opus", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 25.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "sap-ai-core/gemini-2.5-flash", "name": "gemini-2.5-flash", @@ -136826,74 +126140,6 @@ "output": 65536 } }, - { - "id": "sap-ai-core/gemini-3.1-flash-lite", - "name": "gemini-3.1-flash-lite", - "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": { - "input": [ - "text", - "image", - "video", - "audio", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 1.5, - "cache_read": 0.025 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "sap-ai-core/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": "sap-ai-core/gpt-4.1", "name": "gpt-4.1", @@ -140584,7 +129830,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -140962,90 +130207,6 @@ "output": 128000 } }, - { - "id": "snowflake-cortex/openai-gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "snowflake-cortex/openai-gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "snowflake-cortex/openai-gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "snowflake-cortex/snowflake-llama3.3-70b", "name": "Llama-3.3-70B-Instruct", @@ -141093,12 +130254,12 @@ }, "open_weights": true, "cost": { - "input": 1.76, - "output": 2.05 + "input": 1.64, + "output": 1.91 }, "limit": { "context": 218000, - "output": 16384 + "output": 8192 } }, { @@ -141130,34 +130291,6 @@ "output": 4096 } }, - { - "id": "stackit/Qwen/Qwen3.6-27B", - "name": "Qwen3.6 27B", - "family": "qwen", - "attachment": false, - "reasoning": false, - "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.53, - "output": 0.76 - }, - "limit": { - "context": 262144, - "output": 16384 - } - }, { "id": "stackit/cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic", "name": "Llama 3.3 70B", @@ -141178,12 +130311,12 @@ }, "open_weights": true, "cost": { - "input": 0.53, - "output": 0.76 + "input": 0.49, + "output": 0.71 }, "limit": { "context": 128000, - "output": 4096 + "output": 8192 } }, { @@ -141207,12 +130340,12 @@ }, "open_weights": true, "cost": { - "input": 0.53, - "output": 0.76 + "input": 0.49, + "output": 0.71 }, "limit": { "context": 37000, - "output": 4096 + "output": 8192 } }, { @@ -141243,10 +130376,66 @@ "output": 4096 } }, + { + "id": "stackit/neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8", + "name": "Llama 3.1 8B", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-07-23", + "last_updated": "2024-07-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.16, + "output": 0.27 + }, + "limit": { + "context": 128000, + "output": 8192 + } + }, + { + "id": "stackit/neuralmagic/Mistral-Nemo-Instruct-2407-FP8", + "name": "Mistral Nemo", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-07-01", + "last_updated": "2024-07-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.49, + "output": 0.71 + }, + "limit": { + "context": 128000, + "output": 8192 + } + }, { "id": "stackit/openai/gpt-oss-120b", - "name": "GPT OSS 120B", - "family": "gpt-oss", + "name": "GPT-OSS 120B", + "family": "gpt", "attachment": false, "reasoning": true, "tool_call": true, @@ -141263,152 +130452,14 @@ }, "open_weights": true, "cost": { - "input": 0.53, - "output": 0.76 + "input": 0.49, + "output": 0.71 }, "limit": { "context": 131000, "output": 8192 } }, - { - "id": "stackit/openai/gpt-oss-20b", - "name": "GPT OSS 20B", - "family": "gpt-oss", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.18, - "output": 0.29 - }, - "limit": { - "context": 131072, - "output": 8192 - } - }, - { - "id": "stepfun-ai-step-plan/step-3.5-flash", - "name": "Step 3.5 Flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01-29", - "last_updated": "2026-02-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": {}, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "stepfun-ai-step-plan/step-3.7-flash", - "name": "Step 3.7 Flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-03-01", - "release_date": "2026-05-29", - "last_updated": "2026-05-29", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": {}, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "stepfun-ai/step-1-32k", - "name": "Step 1 (32K)", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-06", - "release_date": "2025-01-01", - "last_updated": "2026-02-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.05, - "output": 9.59, - "cache_read": 0.41 - }, - "limit": { - "context": 32768, - "output": 32768 - } - }, - { - "id": "stepfun-ai/step-2-16k", - "name": "Step 2 (16K)", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-06", - "release_date": "2025-01-01", - "last_updated": "2026-02-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.21, - "output": 16.44, - "cache_read": 1.04 - }, - "limit": { - "context": 16384, - "output": 8192 - } - }, { "id": "stepfun-ai/step-3.5-flash", "name": "Step 3.5 Flash", @@ -141438,194 +130489,11 @@ "output": 256000 } }, - { - "id": "stepfun-ai/step-3.7-flash", - "name": "Step 3.7 Flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-03-01", - "release_date": "2026-05-29", - "last_updated": "2026-06-29", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.185, - "output": 1.11, - "cache_read": 0.037 - }, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "stepfun-ai/step-tts-2", - "name": "Step TTS 2", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-03-01", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "stepfun-ai/stepaudio-2.5-asr", - "name": "StepAudio 2.5 ASR", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-04-24", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "stepfun-ai/stepaudio-2.5-tts", - "name": "StepAudio 2.5 TTS", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-04-16", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "stepfun-step-plan/step-3.5-flash", - "name": "Step 3.5 Flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-01-29", - "last_updated": "2026-02-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": {}, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "stepfun-step-plan/step-3.7-flash", - "name": "Step 3.7 Flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-03-01", - "release_date": "2026-05-29", - "last_updated": "2026-05-29", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": {}, - "limit": { - "context": 256000, - "output": 256000 - } - }, - { - "id": "stepfun-step-plan/step-router-v1", - "name": "Step Router v1", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-05-29", - "last_updated": "2026-05-29", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 256000, - "output": 256000 - } - }, { "id": "stepfun/step-1-32k", "name": "Step 1 (32K)", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2024-06", @@ -141654,7 +130522,7 @@ "id": "stepfun/step-2-16k", "name": "Step 2 (16K)", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2024-06", @@ -141715,14 +130583,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-29", "last_updated": "2026-06-29", "modalities": { "input": [ "text", - "image", - "video" + "image" ], "output": [ "text" @@ -141730,119 +130597,15 @@ }, "open_weights": true, "cost": { - "input": 0.185, - "output": 1.11, - "cache_read": 0.037 + "input": 0.2, + "output": 1.15, + "cache_read": 0.04 }, "limit": { "context": 256000, "output": 256000 } }, - { - "id": "stepfun/step-tts-2", - "name": "Step TTS 2", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-03-01", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "stepfun/stepaudio-2.5-asr", - "name": "StepAudio 2.5 ASR", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-04-24", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "audio" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "stepfun/stepaudio-2.5-tts", - "name": "StepAudio 2.5 TTS", - "family": "step", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": false, - "release_date": "2026-04-16", - "last_updated": "2026-07-02", - "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "subconscious/subconscious/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "subconscious/subconscious/tim-qwen3.6-27b", "name": "TIM-Qwen3.6 27B", @@ -141862,9 +130625,8 @@ }, "open_weights": false, "cost": { - "input": 0.3, - "output": 3.0, - "cache_read": 0.15 + "input": 0.0, + "output": 0.0 }, "limit": { "context": 8192, @@ -142153,6 +130915,36 @@ "output": 65536 } }, + { + "id": "synthetic/hf:Qwen/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" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 3.6, + "cache_read": 0.6 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, { "id": "synthetic/hf:Qwen/Qwen3.6-27B", "name": "Qwen3.6 27B", @@ -142184,16 +130976,16 @@ } }, { - "id": "synthetic/hf:moonshotai/Kimi-K2.7-Code", - "name": "Kimi K2.7 Code", + "id": "synthetic/hf:moonshotai/Kimi-K2.6", + "name": "Kimi K2.6", "family": "kimi-k2", "attachment": true, "reasoning": true, "tool_call": true, - "temperature": false, + "temperature": true, "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", + "release_date": "2026-04-21", + "last_updated": "2026-04-21", "modalities": { "input": [ "text", @@ -142272,6 +131064,36 @@ "output": 32768 } }, + { + "id": "synthetic/hf:zai-org/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.45, + "output": 2.19, + "cache_read": 0.45 + }, + "limit": { + "context": 202752, + "output": 65536 + } + }, { "id": "synthetic/hf:zai-org/GLM-4.7-Flash", "name": "GLM-4.7-Flash", @@ -142302,6 +131124,35 @@ "output": 65536 } }, + { + "id": "synthetic/hf:zai-org/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", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.0, + "output": 3.0, + "cache_read": 1.0 + }, + "limit": { + "context": 196608, + "output": 65536 + } + }, { "id": "synthetic/hf:zai-org/GLM-5.2", "name": "GLM-5.2", @@ -142574,66 +131425,6 @@ "output": 16384 } }, - { - "id": "tencent-token-plan/hy3", - "name": "Hy3", - "family": "Hy", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-06", - "last_updated": "2026-07-06", - "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": 256000, - "output": 64000 - } - }, - { - "id": "tencent-tokenhub/hy3", - "name": "Hy3", - "family": "Hy", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-06", - "last_updated": "2026-07-06", - "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": 256000, - "output": 64000 - } - }, { "id": "tencent-tokenhub/hy3-preview", "name": "Hy3 preview", @@ -142672,7 +131463,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text", @@ -142697,7 +131488,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -142709,8 +131500,8 @@ "open_weights": false, "cost": {}, "limit": { - "context": 196608, - "output": 30000 + "context": 128000, + "output": 64000 } }, { @@ -142721,7 +131512,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -142745,7 +131536,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text", @@ -142770,7 +131561,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -142782,8 +131573,8 @@ "open_weights": false, "cost": {}, "limit": { - "context": 196608, - "output": 30000 + "context": 128000, + "output": 64000 } }, { @@ -142794,7 +131585,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-04", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -142818,7 +131609,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-03-24", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text", @@ -142843,7 +131634,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-02-26", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -142855,7 +131646,7 @@ "open_weights": false, "cost": {}, "limit": { - "context": 196608, + "context": 128000, "output": 30000 } }, @@ -142867,7 +131658,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-02-26", - "last_updated": "2026-07-06", + "last_updated": "2026-05-19", "modalities": { "input": [ "text" @@ -143411,7 +132202,7 @@ "tool_call": true, "temperature": true, "release_date": "2026-05-21", - "last_updated": "2026-07-02", + "last_updated": "2026-06-15", "modalities": { "input": [ "text" @@ -143671,7 +132462,7 @@ "temperature": true, "knowledge": "2023-12", "release_date": "2024-12-06", - "last_updated": "2026-07-02", + "last_updated": "2024-12-06", "modalities": { "input": [ "text" @@ -143682,8 +132473,8 @@ }, "open_weights": true, "cost": { - "input": 1.04, - "output": 1.04 + "input": 0.88, + "output": 0.88 }, "limit": { "context": 131072, @@ -143961,7 +132752,7 @@ "temperature": true, "knowledge": "2025-11", "release_date": "2026-04-07", - "last_updated": "2026-07-02", + "last_updated": "2026-04-07", "modalities": { "input": [ "text" @@ -144009,188 +132800,6 @@ "output": 164000 } }, - { - "id": "trustedrouter/auto", - "name": "Auto", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/cheap", - "name": "Cheap", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-05-01", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/e2e", - "name": "End-to-End Encrypted", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/fast", - "name": "Fast", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/synth", - "name": "Synth", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-20", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/synth-code", - "name": "Synth Code", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-20", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "trustedrouter/zdr", - "name": "Zero Data Retention", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-27", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 1000000, - "output": 131072 - } - }, { "id": "umans-ai-coding-plan/umans-coder", "name": "Umans Coder", @@ -144533,692 +133142,6 @@ "output": 32768 } }, - { - "id": "unorouter/claude-haiku-4.5", - "name": "Claude Haiku 4.5", - "family": "claude-haiku", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-02-28", - "release_date": "2025-10-15", - "last_updated": "2025-10-15", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.2, - "output": 6.0 - }, - "limit": { - "context": 200000, - "output": 64000 - } - }, - { - "id": "unorouter/claude-opus-4.8", - "name": "Claude Opus 4.8", - "family": "claude-opus", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01", - "release_date": "2026-05-28", - "last_updated": "2026-05-28", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.425, - "output": 2.125 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "unorouter/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.44, - "output": 7.2 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "unorouter/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.0625, - "output": 0.125 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "unorouter/deepseek-v4-flash:free", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "unorouter/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.8999, - "output": 1.7999 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "unorouter/deepseek-v4-pro:free", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, - { - "id": "unorouter/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": 0.1857, - "output": 1.1142 - }, - "limit": { - "context": 1048576, - "output": 65536 - } - }, - { - "id": "unorouter/gemma-4-31b-it:free", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 32768 - } - }, - { - "id": "unorouter/glm-4.5-flash:free", - "name": "GLM-4.5-Flash", - "family": "glm-flash", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-04", - "release_date": "2025-07-28", - "last_updated": "2025-07-28", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 131072, - "output": 98304 - } - }, - { - "id": "unorouter/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.6001, - "output": 5.0288 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "unorouter/glm-5.2:free", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "unorouter/gpt-5.2", - "name": "GPT-5.2", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2025-12-11", - "last_updated": "2025-12-11", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.05, - "output": 8.4 - }, - "limit": { - "context": 400000, - "output": 128000 - } - }, - { - "id": "unorouter/gpt-5.4", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.8, - "output": 10.8 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "unorouter/gpt-5.4:free", - "name": "GPT-5.4", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-08-31", - "release_date": "2026-03-05", - "last_updated": "2026-03-05", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "unorouter/gpt-5.5", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.1875, - "output": 1.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "unorouter/gpt-5.5:free", - "name": "GPT-5.5", - "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2025-12-01", - "release_date": "2026-04-23", - "last_updated": "2026-04-23", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "unorouter/kimi-k2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "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": 1.2675, - "output": 5.3368 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "unorouter/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.819, - "output": 3.276 - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - { - "id": "unorouter/minimax-m2.7:free", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - { - "id": "unorouter/nemotron-3-ultra-550b-a55b:free", - "name": "Nemotron 3 Ultra 550B A55B", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-04", - "last_updated": "2026-06-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "unorouter/qwen3.5-397b-a17b:free", - "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.0, - "output": 0.0 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "unorouter/step-3.7-flash:free", - "name": "Step 3.7 Flash", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-03-01", - "release_date": "2026-05-29", - "last_updated": "2026-05-29", - "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.0, - "output": 0.0 - }, - "limit": { - "context": 256000, - "output": 256000 - } - }, { "id": "upstage/solar-mini", "name": "solar-mini", @@ -145421,13 +133344,14 @@ } }, { - "id": "venice/aion-labs-aion-3.0", - "name": "Aion 3.0", + "id": "venice/arcee-trinity-large-thinking", + "name": "Trinity Large Thinking", + "family": "trinity", "attachment": false, "reasoning": true, "tool_call": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", + "release_date": "2026-04-02", + "last_updated": "2026-06-11", "modalities": { "input": [ "text" @@ -145436,42 +133360,15 @@ "text" ] }, - "open_weights": false, + "open_weights": true, "cost": { - "input": 3.75, - "output": 7.5, - "cache_read": 0.9375 + "input": 0.3125, + "output": 1.125, + "cache_read": 0.075 }, "limit": { - "context": 128000, - "output": 32768 - } - }, - { - "id": "venice/aion-labs-aion-3.0-mini", - "name": "Aion 3.0 Mini", - "attachment": false, - "reasoning": true, - "tool_call": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.875, - "output": 1.75, - "cache_read": 0.225 - }, - "limit": { - "context": 128000, - "output": 32768 + "context": 256000, + "output": 65536 } }, { @@ -145514,7 +133411,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-12-06", "last_updated": "2026-06-11", "modalities": { @@ -145642,7 +133539,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-06-11", "modalities": { @@ -145674,7 +133570,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-06-11", "modalities": { @@ -145762,38 +133657,6 @@ "output": 64000 } }, - { - "id": "venice/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-29", - "last_updated": "2026-07-01", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 15.0, - "cache_read": 0.3, - "cache_write": 3.75 - }, - "limit": { - "context": 1000000, - "output": 64000 - } - }, { "id": "venice/deepseek-v3.2", "name": "DeepSeek V3.2", @@ -146062,9 +133925,8 @@ }, "open_weights": true, "cost": { - "input": 0.13, - "output": 0.4, - "cache_read": 0.05 + "input": 0.1625, + "output": 0.5 }, "limit": { "context": 256000, @@ -146190,36 +134052,6 @@ "output": 32000 } }, - { - "id": "venice/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-07", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.27, - "output": 6.8, - "cache_read": 0.57 - }, - "limit": { - "context": 500000, - "output": 32000 - } - }, { "id": "venice/grok-build-0.1", "name": "Grok Build 0.1", @@ -146361,9 +134193,9 @@ }, "open_weights": true, "cost": { - "input": 0.75, - "output": 3.5, - "cache_read": 0.16 + "input": 0.9, + "output": 4.3, + "cache_read": 0.2 }, "limit": { "context": 256000, @@ -146472,9 +134304,9 @@ }, "open_weights": true, "cost": { - "input": 0.27, - "output": 0.95, - "cache_read": 0.03 + "input": 0.34, + "output": 1.19, + "cache_read": 0.04 }, "limit": { "context": 198000, @@ -146514,16 +134346,14 @@ "id": "venice/minimax-m3-preview", "name": "MiniMax M3 Preview", "family": "minimax-m3", - "attachment": true, + "attachment": false, "reasoning": true, "tool_call": true, "release_date": "2026-06-12", "last_updated": "2026-06-13", "modalities": { "input": [ - "text", - "image", - "video" + "text" ], "output": [ "text" @@ -146701,9 +134531,8 @@ }, "open_weights": true, "cost": { - "input": 0.07, - "output": 0.4, - "cache_read": 0.035 + "input": 0.14, + "output": 0.8 }, "limit": { "context": 200000, @@ -147016,198 +134845,6 @@ "output": 128000 } }, - { - "id": "venice/openai-gpt-56-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 7.5, - "cache_read": 0.125, - "cache_write": 1.5625 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "venice/openai-gpt-56-luna-pro", - "name": "GPT-5.6 Luna Pro", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 7.5, - "cache_read": 0.125, - "cache_write": 1.5625 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "venice/openai-gpt-56-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 6.25, - "output": 37.5, - "cache_read": 0.625, - "cache_write": 7.8125 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "venice/openai-gpt-56-sol-pro", - "name": "GPT-5.6 Sol Pro", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 6.25, - "output": 37.5, - "cache_read": 0.625, - "cache_write": 7.8125 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "venice/openai-gpt-56-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.125, - "output": 18.75, - "cache_read": 0.3125, - "cache_write": 3.90625 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "venice/openai-gpt-56-terra-pro", - "name": "GPT-5.6 Terra Pro", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.125, - "output": 18.75, - "cache_read": 0.3125, - "cache_write": 3.90625 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "venice/openai-gpt-oss-120b", "name": "OpenAI GPT OSS 120B", @@ -147582,9 +135219,8 @@ }, "open_weights": true, "cost": { - "input": 0.21, - "output": 1.9, - "cache_read": 0.1 + "input": 0.25, + "output": 1.5 }, "limit": { "context": 128000, @@ -147671,9 +135307,9 @@ }, "open_weights": true, "cost": { - "input": 0.14, - "output": 0.28, - "cache_read": 0.05 + "input": 0.175, + "output": 0.35, + "cache_read": 0.0625 }, "limit": { "context": 1000000, @@ -147877,13 +135513,13 @@ }, "open_weights": true, "cost": { - "input": 1.54, - "output": 4.84, - "cache_read": 0.286 + "input": 1.75, + "output": 5.5, + "cache_read": 0.325 }, "limit": { "context": 200000, - "output": 80000 + "output": 24000 } }, { @@ -148863,56 +136499,6 @@ "output": 0 } }, - { - "id": "vercel/alibaba/wan-v2.7-r2v", - "name": "Wan v2.7 Reference-to-Video", - "family": "o", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": true, - "release_date": "2026-04-07", - "last_updated": "2026-04-07", - "modalities": { - "input": [ - "text" - ], - "output": [ - "video" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, - { - "id": "vercel/alibaba/wan-v2.7-t2v", - "name": "Wan v2.7 Text-to-Video", - "family": "o", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": true, - "release_date": "2026-04-07", - "last_updated": "2026-04-07", - "modalities": { - "input": [ - "text" - ], - "output": [ - "video" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, { "id": "vercel/amazon/nova-2-lite", "name": "Nova 2 Lite", @@ -149078,7 +136664,8 @@ "modalities": { "input": [ "text", - "image" + "image", + "pdf" ], "output": [ "text" @@ -149129,39 +136716,6 @@ "output": 8192 } }, - { - "id": "vercel/anthropic/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-01-31", - "release_date": "2026-07-01", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 10.0, - "output": 50.0, - "cache_read": 1.0, - "cache_write": 12.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "vercel/anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5", @@ -149225,7 +136779,7 @@ }, "limit": { "context": 200000, - "output": 8192 + "output": 32000 } }, { @@ -149269,7 +136823,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-05", + "knowledge": "2025-03-31", "release_date": "2025-11-24", "last_updated": "2025-11-24", "modalities": { @@ -149368,7 +136922,6 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -149422,8 +136975,8 @@ "cache_write": 3.75 }, "limit": { - "context": 1000000, - "output": 8192 + "context": 200000, + "output": 64000 } }, { @@ -149455,7 +137008,7 @@ "cache_write": 3.75 }, "limit": { - "context": 1000000, + "context": 200000, "output": 64000 } }, @@ -149492,39 +137045,6 @@ "output": 128000 } }, - { - "id": "vercel/anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-01-31", - "release_date": "2026-06-29", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 2.5 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "vercel/arcee-ai/trinity-large-preview", "name": "Trinity Large Preview", @@ -150125,31 +137645,6 @@ "output": 0 } }, - { - "id": "vercel/bytedance/seedream-5.0-pro", - "name": "Seedream 5.0 Pro", - "family": "seed", - "attachment": false, - "reasoning": false, - "tool_call": false, - "temperature": true, - "release_date": "2026-07-11", - "last_updated": "2026-07-11", - "modalities": { - "input": [ - "text" - ], - "output": [ - "image" - ] - }, - "open_weights": false, - "cost": {}, - "limit": { - "context": 0, - "output": 0 - } - }, { "id": "vercel/cohere/command-a", "name": "Command A", @@ -150359,12 +137854,11 @@ }, "open_weights": false, "cost": { - "input": 0.21, - "output": 0.79, - "cache_read": 0.13 + "input": 0.6, + "output": 1.7 }, "limit": { - "context": 163840, + "context": 128000, "output": 128000 } }, @@ -150480,7 +137974,7 @@ "cost": { "input": 0.14, "output": 0.28, - "cache_read": 0.028 + "cache_read": 0.0028 }, "limit": { "context": 1000000, @@ -150682,15 +138176,15 @@ }, { "id": "vercel/google/gemini-3-pro-image", - "name": "Nano Banana Pro", + "name": "Nano Banana Pro (Gemini 3 Pro Image)", "family": "gemini-pro", "attachment": false, "reasoning": false, "tool_call": false, "temperature": true, - "knowledge": "2025-01", + "knowledge": "2025-03", "release_date": "2025-09-01", - "last_updated": "2026-05-28", + "last_updated": "2025-09", "modalities": { "input": [ "text" @@ -150745,13 +138239,12 @@ }, { "id": "vercel/google/gemini-3.1-flash-image", - "name": "Nano Banana 2", - "family": "gemini-flash", + "name": "Gemini 3.1 Flash Image (Nano Banana 2)", + "family": "gemini", "attachment": true, "reasoning": true, "tool_call": false, "temperature": true, - "knowledge": "2025-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -150839,37 +138332,6 @@ "output": 65000 } }, - { - "id": "vercel/google/gemini-3.1-flash-lite-image", - "name": "Gemini 3.1 Flash Lite Image (Nano Banana 2 Lite)", - "family": "gemini", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text", - "image" - ] - }, - "open_weights": false, - "cost": { - "input": 0.25, - "output": 1.5, - "cache_read": 0.03 - }, - "limit": { - "context": 65536, - "output": 4096 - } - }, { "id": "vercel/google/gemini-3.1-flash-lite-preview", "name": "Gemini 3.1 Flash Lite Preview", @@ -151017,36 +138479,6 @@ "output": 0 } }, - { - "id": "vercel/google/gemini-omni-flash-preview", - "name": "Gemini Omni Flash Preview", - "family": "gemini", - "attachment": true, - "reasoning": true, - "tool_call": false, - "temperature": true, - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.5, - "output": 9.0 - }, - "limit": { - "context": 1000000, - "output": 57920 - } - }, { "id": "vercel/google/gemma-4-26b-a4b-it", "name": "Gemma 4 26B A4B IT", @@ -151619,35 +139051,6 @@ "output": 0 } }, - { - "id": "vercel/kwaipilot/kat-coder-air-v2.5", - "name": "Kat Coder Air V2.5", - "family": "kat-coder", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-10", - "last_updated": "2026-07-10", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.03 - }, - "limit": { - "context": 256000, - "output": 80000 - } - }, { "id": "vercel/kwaipilot/kat-coder-pro-v1", "name": "KAT-Coder-Pro V1", @@ -151707,35 +139110,6 @@ "output": 256000 } }, - { - "id": "vercel/kwaipilot/kat-coder-pro-v2.5", - "name": "Kat Coder Pro V2.5", - "family": "kat-coder", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-10", - "last_updated": "2026-07-10", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.74, - "output": 2.96, - "cache_read": 0.15 - }, - "limit": { - "context": 256000, - "output": 80000 - } - }, { "id": "vercel/meituan/longcat-flash-chat", "name": "LongCat Flash Chat", @@ -152052,37 +139426,6 @@ "output": 4096 } }, - { - "id": "vercel/meta/muse-spark-1.1", - "name": "Muse Spark 1.1", - "family": "muse", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.25, - "output": 4.25, - "cache_read": 0.15 - }, - "limit": { - "context": 1048576, - "output": 1048576 - } - }, { "id": "vercel/minimax/minimax-m2", "name": "MiniMax M2", @@ -154295,105 +141638,6 @@ "output": 128000 } }, - { - "id": "vercel/openai/gpt-5.6-luna", - "name": "GPT 5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "vercel/openai/gpt-5.6-sol", - "name": "GPT 5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "vercel/openai/gpt-5.6-terra", - "name": "GPT 5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "vercel/openai/gpt-image-1", "name": "GPT Image 1", @@ -154660,38 +141904,6 @@ "output": 0 } }, - { - "id": "vercel/openai/gpt-realtime-2.1", - "name": "gpt-realtime-2.1", - "family": "gpt", - "attachment": false, - "reasoning": true, - "tool_call": false, - "temperature": true, - "knowledge": "2024-09-30", - "release_date": "2026-07-09", - "last_updated": "2026-07-06", - "modalities": { - "input": [ - "text", - "audio" - ], - "output": [ - "text", - "audio" - ] - }, - "open_weights": false, - "cost": { - "input": 4.0, - "output": 24.0, - "cache_read": 0.4 - }, - "limit": { - "context": 128000, - "output": 32000 - } - }, { "id": "vercel/openai/gpt-realtime-mini", "name": "GPT-Realtime mini", @@ -155459,7 +142671,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-28", "last_updated": "2026-05-29", "modalities": { @@ -156063,37 +143275,6 @@ "output": 1000000 } }, - { - "id": "vercel/xai/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "vercel/xai/grok-build-0.1", "name": "Grok Build 0.1", @@ -156802,12 +143983,12 @@ }, "open_weights": true, "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26 + "input": 1.5, + "output": 4.5, + "cache_read": 0.3 }, "limit": { - "context": 1040000, + "context": 1000000, "output": 128000 } }, @@ -156819,7 +144000,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-23", + "release_date": "2026-06-16", "last_updated": "2026-06-16", "modalities": { "input": [ @@ -156831,9 +144012,9 @@ }, "open_weights": false, "cost": { - "input": 2.1, - "output": 6.6, - "cache_read": 0.21 + "input": 3.0, + "output": 10.25, + "cache_read": 0.5 }, "limit": { "context": 1000000, @@ -156999,35 +144180,6 @@ "output": 65536 } }, - { - "id": "vivgrid/glm-5.2", - "name": "GLM-5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.2, - "output": 4.2, - "cache_read": 0.3 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "vivgrid/gpt-5-mini", "name": "GPT-5 Mini", @@ -157311,105 +144463,6 @@ "output": 128000 } }, - { - "id": "vivgrid/gpt-5.6-luna", - "name": "GPT 5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "vivgrid/gpt-5.6-sol", - "name": "GPT 5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "vivgrid/gpt-5.6-terra", - "name": "GPT 5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "vultr/MiniMaxAI/MiniMax-M2.7", "name": "MiniMax-M2.7", @@ -157438,126 +144491,6 @@ "output": 131072 } }, - { - "id": "vultr/Qwen/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.3, - "output": 2.0 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "vultr/Qwen/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.3, - "output": 2.0 - }, - "limit": { - "context": 262144, - "output": 65536 - } - }, - { - "id": "vultr/XiaomiMiMo/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.55, - "output": 1.65 - }, - "limit": { - "context": 1048576, - "output": 131072 - } - }, - { - "id": "vultr/deepseek-ai/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.3, - "output": 1.0 - }, - "limit": { - "context": 1000000, - "output": 384000 - } - }, { "id": "vultr/moonshotai/Kimi-K2.6", "name": "Kimi K2.6", @@ -157579,8 +144512,8 @@ }, "open_weights": true, "cost": { - "input": 0.3, - "output": 1.2 + "input": 0.15, + "output": 0.6 }, "limit": { "context": 262144, @@ -157616,6 +144549,35 @@ "output": 131072 } }, + { + "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": false, + "temperature": true, + "knowledge": "2023-12", + "release_date": "2025-10-28", + "last_updated": "2025-10-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.01, + "output": 0.01 + }, + "limit": { + "context": 8192, + "output": 4096 + } + }, { "id": "vultr/nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16", "name": "NVIDIA Nemotron 3 Nano Omni", @@ -157675,15 +144637,15 @@ } }, { - "id": "vultr/zai-org/GLM-5.2-FP8", - "name": "GLM-5.2", + "id": "vultr/zai-org/GLM-5.1-FP8", + "name": "GLM-5.1", "family": "glm", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "release_date": "2026-04-07", + "last_updated": "2026-04-07", "modalities": { "input": [ "text" @@ -157698,7 +144660,7 @@ "output": 3.1 }, "limit": { - "context": 393216, + "context": 200000, "output": 131072 } }, @@ -157765,15 +144727,15 @@ }, { "id": "wafer.ai/Kimi-K2.6", - "name": "Kimi K2.6", + "name": "Kimi-K2.6", "family": "kimi-k2", "attachment": true, "reasoning": true, "tool_call": true, - "temperature": false, + "temperature": true, "knowledge": "2025-01", - "release_date": "2026-04-21", - "last_updated": "2026-04-21", + "release_date": "2026-05-13", + "last_updated": "2026-06-01", "modalities": { "input": [ "text", @@ -157786,9 +144748,9 @@ }, "open_weights": true, "cost": { - "input": 1.14, - "output": 4.8, - "cache_read": 0.19, + "input": 0.68, + "output": 3.15, + "cache_read": 0.07, "cache_write": 0.0 }, "limit": { @@ -157797,19 +144759,21 @@ } }, { - "id": "wafer.ai/MiniMax-M3", - "name": "MiniMax-M3", - "family": "minimax", + "id": "wafer.ai/Qwen3.5-397B-A17B", + "name": "Qwen3.5-397B-A17B", + "family": "qwen", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-01", + "knowledge": "2025-04", + "release_date": "2026-02-16", "last_updated": "2026-06-01", "modalities": { "input": [ "text", - "image" + "image", + "video" ], "output": [ "text" @@ -157817,26 +144781,60 @@ }, "open_weights": true, "cost": { - "input": 0.33, - "output": 1.32, - "cache_read": 0.07, + "input": 0.43, + "output": 2.6, + "cache_read": 0.04, "cache_write": 0.0 }, "limit": { - "context": 1048576, - "output": 128000 + "context": 262144, + "output": 65536 } }, { - "id": "wafer.ai/glm5.2-fast", - "name": "GLM5.2-Fast", - "family": "glm", + "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-30", + "modalities": { + "input": [ + "text", + "image", + "video" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.15, + "output": 1.0, + "cache_read": 0.02, + "cache_write": 0.0 + }, + "limit": { + "context": 256000, + "output": 65536 + } + }, + { + "id": "wafer.ai/deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "family": "deepseek-flash", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-06-13", - "last_updated": "2026-06-13", + "knowledge": "2025-05", + "release_date": "2026-04-24", + "last_updated": "2026-05-30", "modalities": { "input": [ "text" @@ -157847,54 +144845,87 @@ }, "open_weights": true, "cost": { - "input": 3.0, - "output": 10.25, + "input": 0.14, + "output": 0.28, + "cache_read": 0.01, + "cache_write": 0.0 + }, + "limit": { + "context": 1000000, + "output": 384000 + } + }, + { + "id": "wafer.ai/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-05-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.74, + "output": 3.48, + "cache_read": 0.02, + "cache_write": 0.0 + }, + "limit": { + "context": 1000000, + "output": 384000 + } + }, + { + "id": "wafer.ai/qwen3.7-max", + "name": "Qwen3.7-Max", + "family": "qwen3.7-max", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-05-21", + "last_updated": "2026-05-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5.0, + "output": 15.0, "cache_read": 0.5, "cache_write": 0.0 }, "limit": { - "context": 1048576, - "output": 131072 - } - }, - { - "id": "wandb/JetBrains/Mellum2-12B-A2.5B-Instruct", - "name": "Mellum2 12B A2.5B", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-01", - "last_updated": "2026-06-01", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.1, - "cache_read": 0.05 - }, - "limit": { - "context": 131072, - "output": 131072 + "context": 256000, + "output": 65536 } }, { "id": "wandb/MiniMaxAI/MiniMax-M2.5", "name": "MiniMax M2.5", - "family": "minimax-m2.5", + "family": "minimax", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "release_date": "2026-02-12", - "last_updated": "2026-02-12", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -157906,8 +144937,7 @@ "open_weights": true, "cost": { "input": 0.3, - "output": 1.2, - "cache_read": 0.3 + "output": 1.2 }, "limit": { "context": 196608, @@ -157916,14 +144946,14 @@ }, { "id": "wandb/OpenPipe/Qwen3-14B-Instruct", - "name": "Qwen3 14B Instruct", + "name": "OpenPipe Qwen3 14B Instruct", "family": "qwen", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "release_date": "2025-04-29", - "last_updated": "2025-04-29", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -157935,8 +144965,7 @@ "open_weights": true, "cost": { "input": 0.05, - "output": 0.22, - "cache_read": 0.05 + "output": 0.22 }, "limit": { "context": 32768, @@ -157945,15 +144974,15 @@ }, { "id": "wandb/Qwen/Qwen3-235B-A22B-Instruct", - "name": "Qwen3 235B A22B-2507", + "name": "Qwen3 235B A22B Instruct 2507", "family": "qwen", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2025-04", - "release_date": "2025-07-22", - "last_updated": "2025-07-22", + "release_date": "2025-04-28", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -157965,8 +144994,7 @@ "open_weights": true, "cost": { "input": 0.1, - "output": 0.1, - "cache_read": 0.1 + "output": 0.1 }, "limit": { "context": 262144, @@ -157975,7 +145003,7 @@ }, { "id": "wandb/Qwen/Qwen3-235B-A22B-Thinking", - "name": "Qwen3 235B A22B Thinking-2507", + "name": "Qwen3-235B-A22B-Thinking-2507", "family": "qwen", "attachment": false, "reasoning": true, @@ -157983,7 +145011,7 @@ "temperature": true, "knowledge": "2025-04", "release_date": "2025-07-25", - "last_updated": "2025-07-25", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -157995,8 +145023,7 @@ "open_weights": true, "cost": { "input": 0.1, - "output": 0.1, - "cache_read": 0.1 + "output": 0.1 }, "limit": { "context": 262144, @@ -158012,7 +145039,7 @@ "tool_call": true, "temperature": true, "release_date": "2025-07-29", - "last_updated": "2025-07-29", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158024,8 +145051,7 @@ "open_weights": true, "cost": { "input": 0.1, - "output": 0.3, - "cache_read": 0.1 + "output": 0.3 }, "limit": { "context": 262144, @@ -158034,15 +145060,15 @@ }, { "id": "wandb/Qwen/Qwen3-Coder-480B-A35B-Instruct", - "name": "Qwen3 Coder 480B A35B", + "name": "Qwen3-Coder-480B-A35B-Instruct", "family": "qwen", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2025-04", - "release_date": "2025-07-22", - "last_updated": "2025-07-22", + "release_date": "2025-07-23", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158054,128 +145080,7 @@ "open_weights": true, "cost": { "input": 1.0, - "output": 1.5, - "cache_read": 1.0 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/Qwen/Qwen3.5-27B", - "name": "Qwen3.5-27B", - "family": "qwen3.5", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-24", - "last_updated": "2026-02-24", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.39, - "output": 3.12, - "cache_read": 0.08 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/Qwen/Qwen3.5-35B-A3B", - "name": "Qwen3.5-35B-A3B", - "family": "qwen3.5", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-02-24", - "last_updated": "2026-02-24", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.25, - "output": 1.25, - "cache_read": 0.25 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/Qwen/Qwen3.6-27B", - "name": "Qwen3.6 27B", - "family": "qwen3.6", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-22", - "last_updated": "2026-04-22", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.6, - "output": 3.6, - "cache_read": 0.12 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/Qwen/Qwen3.6-35B-A3B", - "name": "Qwen3.6 35B A3B", - "family": "qwen3.6", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-15", - "last_updated": "2026-04-15", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.25, - "output": 1.25, - "cache_read": 0.25 + "output": 1.5 }, "limit": { "context": 262144, @@ -158191,7 +145096,7 @@ "tool_call": true, "temperature": true, "release_date": "2025-08-21", - "last_updated": "2025-08-21", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158203,133 +145108,13 @@ "open_weights": true, "cost": { "input": 0.55, - "output": 1.65, - "cache_read": 0.55 + "output": 1.65 }, "limit": { "context": 161000, "output": 161000 } }, - { - "id": "wandb/deepseek-ai/DeepSeek-V4-Flash", - "name": "DeepSeek V4 Flash", - "family": "deepseek", - "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.14, - "output": 0.28, - "cache_read": 0.07 - }, - "limit": { - "context": 1048576, - "output": 1048576 - } - }, - { - "id": "wandb/deepseek-ai/DeepSeek-V4-Pro", - "name": "DeepSeek V4 Pro", - "family": "deepseek", - "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": 1.74, - "output": 3.48, - "cache_read": 0.14 - }, - "limit": { - "context": 1048576, - "output": 1048576 - } - }, - { - "id": "wandb/google/gemma-4-31B-it", - "name": "Gemma 4 31B", - "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.12, - "output": 0.35, - "cache_read": 0.09 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/ibm-granite/granite-4.1-8b", - "name": "Granite 4.1 8B", - "family": "granite", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "release_date": "2026-04-29", - "last_updated": "2026-04-29", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.05, - "output": 0.1, - "cache_read": 0.05 - }, - "limit": { - "context": 131072, - "output": 131072 - } - }, { "id": "wandb/meta-llama/Llama-3.1-70B-Instruct", "name": "Llama 3.1 70B", @@ -158339,7 +145124,7 @@ "tool_call": true, "temperature": true, "release_date": "2024-07-23", - "last_updated": "2024-07-23", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158351,8 +145136,7 @@ "open_weights": true, "cost": { "input": 0.8, - "output": 0.8, - "cache_read": 0.8 + "output": 0.8 }, "limit": { "context": 128000, @@ -158361,7 +145145,7 @@ }, { "id": "wandb/meta-llama/Llama-3.1-8B-Instruct", - "name": "Llama 3.1 8B", + "name": "Meta-Llama-3.1-8B-Instruct", "family": "llama", "attachment": false, "reasoning": false, @@ -158369,7 +145153,7 @@ "temperature": true, "knowledge": "2023-12", "release_date": "2024-07-23", - "last_updated": "2024-07-23", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158381,8 +145165,7 @@ "open_weights": true, "cost": { "input": 0.22, - "output": 0.22, - "cache_read": 0.22 + "output": 0.22 }, "limit": { "context": 128000, @@ -158391,15 +145174,15 @@ }, { "id": "wandb/meta-llama/Llama-3.3-70B-Instruct", - "name": "Llama 3.3 70B", + "name": "Llama-3.3-70B-Instruct", "family": "llama", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2023-12", - "release_date": "2024-12-01", - "last_updated": "2024-12-01", + "release_date": "2024-12-06", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158411,25 +145194,54 @@ "open_weights": true, "cost": { "input": 0.71, - "output": 0.71, - "cache_read": 0.71 + "output": 0.71 }, "limit": { "context": 128000, "output": 128000 } }, + { + "id": "wandb/meta-llama/Llama-4-Scout-17B-16E-Instruct", + "name": "Llama 4 Scout 17B 16E Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-12", + "release_date": "2025-01-31", + "last_updated": "2026-03-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.17, + "output": 0.66 + }, + "limit": { + "context": 64000, + "output": 64000 + } + }, { "id": "wandb/microsoft/Phi-4-mini-instruct", - "name": "Phi 4 Mini 3.8B", + "name": "Phi-4-mini-instruct", "family": "phi", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2023-10", - "release_date": "2025-02-01", - "last_updated": "2025-02-01", + "release_date": "2024-12-11", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158441,8 +145253,7 @@ "open_weights": true, "cost": { "input": 0.08, - "output": 0.35, - "cache_read": 0.08 + "output": 0.35 }, "limit": { "context": 128000, @@ -158457,9 +145268,8 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-02-02", - "last_updated": "2026-02-02", + "release_date": "2026-01-27", + "last_updated": "2026-03-12", "modalities": { "input": [ "text", @@ -158471,71 +145281,8 @@ }, "open_weights": true, "cost": { - "input": 0.6, - "output": 3.0, - "cache_read": 0.1 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/moonshotai/Kimi-K2.6", - "name": "Kimi K2.6", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-04-20", - "last_updated": "2026-04-20", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.95, - "output": 4.0, - "cache_read": 0.16 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/moonshotai/Kimi-K2.7-Code", - "name": "Kimi K2.7 Code", - "family": "kimi-k2", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01", - "release_date": "2026-06-12", - "last_updated": "2026-06-12", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.94, - "output": 4.0, - "cache_read": 0.19 + "input": 0.5, + "output": 2.85 }, "limit": { "context": 262144, @@ -158544,14 +145291,14 @@ }, { "id": "wandb/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", - "name": "Nemotron 3 Super", + "name": "NVIDIA Nemotron 3 Super 120B", "family": "nemotron", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "release_date": "2026-03-11", - "last_updated": "2026-03-11", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158563,37 +145310,7 @@ "open_weights": true, "cost": { "input": 0.2, - "output": 0.8, - "cache_read": 0.2 - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - { - "id": "wandb/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B", - "name": "Nemotron 3 Ultra", - "family": "nemotron", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-04", - "last_updated": "2026-06-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.75, - "output": 2.75, - "cache_read": 0.15 + "output": 0.8 }, "limit": { "context": 262144, @@ -158609,7 +145326,7 @@ "tool_call": true, "temperature": true, "release_date": "2025-08-05", - "last_updated": "2025-08-05", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158618,11 +145335,10 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 0.04, - "output": 0.14, - "cache_read": 0.04 + "input": 0.15, + "output": 0.6 }, "limit": { "context": 131072, @@ -158638,7 +145354,35 @@ "tool_call": true, "temperature": true, "release_date": "2025-08-05", - "last_updated": "2025-08-05", + "last_updated": "2026-03-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.05, + "output": 0.2 + }, + "limit": { + "context": 131072, + "output": 131072 + } + }, + { + "id": "wandb/zai-org/GLM-5-FP8", + "name": "GLM 5", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-11", + "last_updated": "2026-03-12", "modalities": { "input": [ "text" @@ -158649,18 +145393,17 @@ }, "open_weights": true, "cost": { - "input": 0.03, - "output": 0.13, - "cache_read": 0.03 + "input": 1.0, + "output": 3.2 }, "limit": { - "context": 131072, - "output": 131072 + "context": 200000, + "output": 200000 } }, { "id": "wandb/zai-org/GLM-5.1", - "name": "GLM 5.1", + "name": "GLM-5.1", "family": "glm", "attachment": false, "reasoning": true, @@ -158680,40 +145423,12 @@ "cost": { "input": 1.4, "output": 4.4, - "cache_read": 0.26 + "cache_read": 0.26, + "cache_write": 0.0 }, "limit": { - "context": 202752, - "output": 202752 - } - }, - { - "id": "wandb/zai-org/GLM-5.2", - "name": "GLM 5.2", - "family": "glm", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-06-16", - "last_updated": "2026-06-16", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 1.39, - "output": 4.4, - "cache_read": 0.26 - }, - "limit": { - "context": 262144, - "output": 262144 + "context": 200000, + "output": 131072 } }, { @@ -158840,37 +145555,6 @@ "output": 30000 } }, - { - "id": "x-ai/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "x-ai/grok-build-0.1", "name": "Grok Build 0.1", @@ -158986,6 +145670,40 @@ "output": 0 } }, + { + "id": "xiaomi-token-plan-ams/mimo-v2-omni", + "name": "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": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0 + }, + "limit": { + "context": 262144, + "output": 131072 + } + }, { "id": "xiaomi-token-plan-ams/mimo-v2-pro", "name": "MiMo-V2-Pro", @@ -159187,6 +145905,40 @@ "output": 8192 } }, + { + "id": "xiaomi-token-plan-cn/mimo-v2-omni", + "name": "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": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0 + }, + "limit": { + "context": 262144, + "output": 131072 + } + }, { "id": "xiaomi-token-plan-cn/mimo-v2-pro", "name": "MiMo-V2-Pro", @@ -159388,6 +146140,40 @@ "output": 8192 } }, + { + "id": "xiaomi-token-plan-sgp/mimo-v2-omni", + "name": "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": { + "input": [ + "text", + "image", + "audio", + "video", + "pdf" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0 + }, + "limit": { + "context": 262144, + "output": 131072 + } + }, { "id": "xiaomi-token-plan-sgp/mimo-v2-pro", "name": "MiMo-V2-Pro", @@ -159776,37 +146562,6 @@ "output": 131072 } }, - { - "id": "xpersona/claude-fable-5", - "name": "Claude Fable 5", - "family": "claude-fable", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-09", - "last_updated": "2026-06-09", - "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 3.0, - "output": 18.0, - "cache_read": 0.3 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "xpersona/xpersona-frieren-coder", "name": "Xpersona Frieren 1", @@ -160488,7 +147243,7 @@ { "id": "zeldoc/z-code", "name": "Z-Code", - "attachment": false, + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, @@ -160497,7 +147252,9 @@ "last_updated": "2026-04-15", "modalities": { "input": [ - "text" + "text", + "image", + "video" ], "output": [ "text" @@ -160508,37 +147265,9 @@ "input": 0.0, "output": 0.0 }, - "limit": { - "context": 1000000, - "output": 131072 - } - }, - { - "id": "zenifra/alibaba/qwen3.6-35b-a3b", - "name": "Qwen3.6 35B-A3B", - "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.19, - "output": 0.48 - }, "limit": { "context": 262144, - "output": 65536 + "output": 262144 } }, { @@ -160835,7 +147564,6 @@ "reasoning": true, "tool_call": true, "temperature": false, - "knowledge": "2026-01", "release_date": "2026-05-28", "last_updated": "2026-05-28", "modalities": { @@ -160955,72 +147683,6 @@ "output": 64000 } }, - { - "id": "zenmux/anthropic/claude-sonnet-5", - "name": "Claude Sonnet 5", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 10.0, - "cache_read": 0.2, - "cache_write": 4.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, - { - "id": "zenmux/anthropic/claude-sonnet-5-free", - "name": "Claude Sonnet 5 (Free)", - "family": "claude-sonnet", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-01-31", - "release_date": "2026-06-30", - "last_updated": "2026-06-30", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 0.0, - "output": 0.0, - "cache_read": 0.0, - "cache_write": 0.0 - }, - "limit": { - "context": 1000000, - "output": 128000 - } - }, { "id": "zenmux/baidu/ernie-5.0-thinking-preview", "name": "ERNIE 5.0", @@ -162534,105 +149196,6 @@ "output": 128000 } }, - { - "id": "zenmux/openai/gpt-5.6-luna", - "name": "GPT-5.6 Luna", - "family": "gpt-luna", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 6.0, - "cache_read": 0.1, - "cache_write": 1.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "zenmux/openai/gpt-5.6-sol", - "name": "GPT-5.6 Sol", - "family": "gpt-sol", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 5.0, - "output": 30.0, - "cache_read": 0.5, - "cache_write": 6.25 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, - { - "id": "zenmux/openai/gpt-5.6-terra", - "name": "GPT-5.6 Terra", - "family": "gpt-terra", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": false, - "knowledge": "2026-02-16", - "release_date": "2026-07-09", - "last_updated": "2026-07-09", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.25, - "cache_write": 3.125 - }, - "limit": { - "context": 1050000, - "output": 128000 - } - }, { "id": "zenmux/qwen/qwen3-coder-plus", "name": "Qwen3-Coder-Plus", @@ -162959,14 +149522,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-29", "last_updated": "2026-05-29", "modalities": { "input": [ "text", - "image", - "video" + "image" ], "output": [ "text" @@ -162989,14 +149551,13 @@ "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2026-03-01", + "knowledge": "2026-01-01", "release_date": "2026-05-29", "last_updated": "2026-05-29", "modalities": { "input": [ "text", - "image", - "video" + "image" ], "output": [ "text" @@ -163440,37 +150001,6 @@ "output": 1000000 } }, - { - "id": "zenmux/x-ai/grok-4.5", - "name": "Grok 4.5", - "family": "grok", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "release_date": "2026-07-08", - "last_updated": "2026-07-08", - "modalities": { - "input": [ - "text", - "image", - "pdf" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 6.0, - "cache_read": 0.5 - }, - "limit": { - "context": 500000, - "output": 500000 - } - }, { "id": "zenmux/x-ai/grok-build-0.1", "name": "Grok Build 0.1", @@ -164672,9 +151202,9 @@ }, "open_weights": false, "cost": { - "input": 1.4, - "output": 4.4, - "cache_read": 0.26, + "input": 6.0, + "output": 24.0, + "cache_read": 1.3, "cache_write": 0.0 }, "limit": { diff --git a/crates/goose-provider-types/src/canonical/data/provider_metadata.json b/crates/goose-providers/src/canonical/data/provider_metadata.json similarity index 84% rename from crates/goose-provider-types/src/canonical/data/provider_metadata.json rename to crates/goose-providers/src/canonical/data/provider_metadata.json index b120b3dc2f..6c150c3649 100644 --- a/crates/goose-provider-types/src/canonical/data/provider_metadata.json +++ b/crates/goose-providers/src/canonical/data/provider_metadata.json @@ -19,7 +19,7 @@ "env": [ "ABACUS_API_KEY" ], - "model_count": 95 + "model_count": 65 }, { "id": "abliteration-ai", @@ -32,17 +32,6 @@ ], "model_count": 1 }, - { - "id": "ai-router", - "display_name": "AI-ROUTER", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.ai-router.dev/v1", - "doc": "https://ai-router.dev/openai-compatible-api-gateway/", - "env": [ - "AI_ROUTER_API_KEY" - ], - "model_count": 5 - }, { "id": "alibaba", "display_name": "Alibaba", @@ -63,7 +52,7 @@ "env": [ "DASHSCOPE_API_KEY" ], - "model_count": 84 + "model_count": 83 }, { "id": "alibaba-coding-plan", @@ -118,7 +107,7 @@ "env": [ "AMBIENT_API_KEY" ], - "model_count": 6 + "model_count": 3 }, { "id": "anyapi", @@ -186,17 +175,6 @@ ], "model_count": 8 }, - { - "id": "blueclaw", - "display_name": "Blue Claw", - "npm": "@ai-sdk/openai-compatible", - "api": "https://openai.blueclaw.network/v1", - "doc": "https://blueclaw.network", - "env": [ - "BLUECLAW_API_KEY" - ], - "model_count": 2 - }, { "id": "chutes", "display_name": "Chutes", @@ -275,28 +253,6 @@ ], "model_count": 23 }, - { - "id": "crossmodel", - "display_name": "CrossModel", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.crossmodel.ai/v1", - "doc": "https://www.crossmodel.ai/docs", - "env": [ - "CROSSMODEL_API_KEY" - ], - "model_count": 40 - }, - { - "id": "daoxe", - "display_name": "DaoXE", - "npm": "@ai-sdk/openai-compatible", - "api": "https://daoxe.com/v1", - "doc": "https://daoxe.com/pricing", - "env": [ - "DAOXE_API_KEY" - ], - "model_count": 9 - }, { "id": "databricks", "display_name": "Databricks", @@ -307,7 +263,7 @@ "DATABRICKS_HOST", "DATABRICKS_TOKEN" ], - "model_count": 30 + "model_count": 25 }, { "id": "deepseek", @@ -329,7 +285,7 @@ "env": [ "DIGITALOCEAN_ACCESS_TOKEN" ], - "model_count": 82 + "model_count": 80 }, { "id": "dinference", @@ -353,28 +309,6 @@ ], "model_count": 3 }, - { - "id": "ebcloud", - "display_name": "EBCloud", - "npm": "@ai-sdk/openai-compatible", - "api": "https://maas-api.ebcloud.com/v1", - "doc": "https://docs.ebtech.com/ai/model-api.html", - "env": [ - "EBCLOUD_API_KEY" - ], - "model_count": 4 - }, - { - "id": "empiriolabs", - "display_name": "EmpirioLabs AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.empiriolabs.ai/v1", - "doc": "https://docs.empiriolabs.ai", - "env": [ - "EMPIRIOLABS_API_KEY" - ], - "model_count": 36 - }, { "id": "evroc", "display_name": "evroc", @@ -428,7 +362,7 @@ "env": [ "FRIENDLI_TOKEN" ], - "model_count": 6 + "model_count": 7 }, { "id": "frogbot", @@ -450,7 +384,7 @@ "env": [ "GITHUB_TOKEN" ], - "model_count": 28 + "model_count": 22 }, { "id": "github-models", @@ -472,7 +406,7 @@ "env": [ "GMICLOUD_API_KEY" ], - "model_count": 13 + "model_count": 11 }, { "id": "helicone", @@ -494,7 +428,7 @@ "env": [ "HPC_AI_API_KEY" ], - "model_count": 9 + "model_count": 3 }, { "id": "huggingface", @@ -505,7 +439,7 @@ "env": [ "HF_TOKEN" ], - "model_count": 51 + "model_count": 50 }, { "id": "iflowcn", @@ -551,17 +485,6 @@ ], "model_count": 9 }, - { - "id": "inferx", - "display_name": "InferX", - "npm": "@ai-sdk/openai-compatible", - "api": "https://model.inferx.net/v1", - "doc": "https://model.inferx.net/endpoints", - "env": [ - "INFERX_API_KEY" - ], - "model_count": 6 - }, { "id": "io-net", "display_name": "IO.NET", @@ -584,17 +507,6 @@ ], "model_count": 61 }, - { - "id": "kenari", - "display_name": "Kenari", - "npm": "@ai-sdk/openai-compatible", - "api": "https://kenari.id/v1", - "doc": "https://kenari.id/docs", - "env": [ - "KENARI_API_KEY" - ], - "model_count": 23 - }, { "id": "kilo", "display_name": "Kilo Gateway", @@ -604,7 +516,7 @@ "env": [ "KILO_API_KEY" ], - "model_count": 346 + "model_count": 345 }, { "id": "kimi-for-coding", @@ -648,7 +560,7 @@ "env": [ "LLMGATEWAY_API_KEY" ], - "model_count": 178 + "model_count": 180 }, { "id": "llmtr", @@ -672,17 +584,6 @@ ], "model_count": 3 }, - { - "id": "longcat", - "display_name": "LongCat", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.longcat.chat/openai", - "doc": "https://longcat.chat/platform/docs/", - "env": [ - "LONGCAT_API_KEY" - ], - "model_count": 1 - }, { "id": "lucidquery", "display_name": "LucidQuery", @@ -694,17 +595,6 @@ ], "model_count": 4 }, - { - "id": "lynkr", - "display_name": "Lynkr", - "npm": "@ai-sdk/openai-compatible", - "api": "http://127.0.0.1:8081/v1", - "doc": "https://github.com/Fast-Editor/Lynkr", - "env": [ - "LYNKR_API_KEY" - ], - "model_count": 1 - }, { "id": "meganova", "display_name": "Meganova", @@ -716,17 +606,6 @@ ], "model_count": 19 }, - { - "id": "meta", - "display_name": "Meta", - "npm": "@ai-sdk/openai", - "api": "https://api.meta.ai/v1", - "doc": "https://dev.meta.ai/docs", - "env": [ - "META_MODEL_API_KEY" - ], - "model_count": 1 - }, { "id": "meta-llama", "display_name": "Llama", @@ -804,17 +683,6 @@ ], "model_count": 2 }, - { - "id": "model-oracle-ai", - "display_name": "Model Oracle AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.modeloracle.com/api/v1", - "doc": "https://modeloracle.com/setup/", - "env": [ - "MODEL_ORACLE_API_KEY" - ], - "model_count": 15 - }, { "id": "modelscope", "display_name": "ModelScope", @@ -896,13 +764,13 @@ "id": "neon", "display_name": "Neon", "npm": "@ai-sdk/openai-compatible", - "api": "${NEON_AI_GATEWAY_BASE_URL}/v1", + "api": "${NEON_AI_GATEWAY_BASE_URL}/ai-gateway/mlflow/v1", "doc": "https://neon.com/docs", "env": [ "NEON_AI_GATEWAY_BASE_URL", "NEON_AI_GATEWAY_TOKEN" ], - "model_count": 36 + "model_count": 25 }, { "id": "neuralwatt", @@ -968,7 +836,7 @@ "env": [ "OPENCODE_API_KEY" ], - "model_count": 79 + "model_count": 71 }, { "id": "opencode-go", @@ -1014,17 +882,6 @@ ], "model_count": 18 }, - { - "id": "pioneer", - "display_name": "Pioneer", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.pioneer.ai/v1", - "doc": "https://agent.pioneer.ai/llms.txt", - "env": [ - "PIONEER_API_KEY" - ], - "model_count": 76 - }, { "id": "poe", "display_name": "Poe", @@ -1045,7 +902,7 @@ "env": [ "POOLSIDE_API_KEY" ], - "model_count": 3 + "model_count": 2 }, { "id": "privatemode-ai", @@ -1107,23 +964,12 @@ "id": "routing-run", "display_name": "routing.run", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.routing.run/v1", + "api": "https://ai.routing.sh/v1", "doc": "https://docs.routing.run/api-reference/models", "env": [ "ROUTING_RUN_API_KEY" ], - "model_count": 15 - }, - { - "id": "sakana", - "display_name": "Sakana AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.sakana.ai/v1", - "doc": "https://console.sakana.ai/models", - "env": [ - "SAKANA_API_KEY" - ], - "model_count": 3 + "model_count": 26 }, { "id": "sarvam", @@ -1179,7 +1025,7 @@ "SNOWFLAKE_ACCOUNT", "SNOWFLAKE_CORTEX_PAT" ], - "model_count": 21 + "model_count": 18 }, { "id": "stackit", @@ -1194,58 +1040,36 @@ }, { "id": "stepfun", - "display_name": "StepFun (China)", + "display_name": "StepFun", "npm": "@ai-sdk/openai-compatible", "api": "https://api.stepfun.com/v1", "doc": "https://platform.stepfun.com/docs/zh/overview/concept", "env": [ "STEPFUN_API_KEY" ], - "model_count": 8 + "model_count": 5 }, { "id": "stepfun-ai", - "display_name": "StepFun (Global)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.stepfun.ai/v1", - "doc": "https://platform.stepfun.ai/docs/en/overview/concept", - "env": [ - "STEPFUN_API_KEY" - ], - "model_count": 8 - }, - { - "id": "stepfun-ai-step-plan", - "display_name": "StepFun Step Plan (Global)", + "display_name": "StepFun AI", "npm": "@ai-sdk/openai-compatible", "api": "https://api.stepfun.ai/step_plan/v1", - "doc": "https://platform.stepfun.ai/docs/en/step-plan/integrations/reasoning-api", + "doc": "https://platform.stepfun.ai/docs/en/step-plan/integrations/open-code", "env": [ "STEPFUN_API_KEY" ], - "model_count": 3 - }, - { - "id": "stepfun-step-plan", - "display_name": "StepFun Step Plan (China)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.stepfun.com/step_plan/v1", - "doc": "https://platform.stepfun.com/docs/zh/step-plan/integrations/reasoning-api", - "env": [ - "STEPFUN_API_KEY" - ], - "model_count": 4 + "model_count": 2 }, { "id": "subconscious", "display_name": "Subconscious", - "npm": "@ai-sdk/anthropic", + "npm": "@ai-sdk/openai-compatible", "api": "https://api.subconscious.dev/v1", "doc": "https://docs.subconscious.dev", "env": [ "SUBCONSCIOUS_API_KEY" ], - "model_count": 2 + "model_count": 1 }, { "id": "submodel", @@ -1267,7 +1091,7 @@ "env": [ "SYNTHETIC_API_KEY" ], - "model_count": 7 + "model_count": 10 }, { "id": "tencent-coding-plan", @@ -1280,17 +1104,6 @@ ], "model_count": 8 }, - { - "id": "tencent-token-plan", - "display_name": "Tencent Token Plan", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.lkeap.cloud.tencent.com/plan/v3", - "doc": "https://cloud.tencent.com/document/product/1823/130060", - "env": [ - "TENCENT_TOKEN_PLAN_API_KEY" - ], - "model_count": 1 - }, { "id": "tencent-tokenhub", "display_name": "Tencent TokenHub", @@ -1300,7 +1113,7 @@ "env": [ "TENCENT_TOKENHUB_API_KEY" ], - "model_count": 2 + "model_count": 1 }, { "id": "the-grid-ai", @@ -1324,17 +1137,6 @@ ], "model_count": 7 }, - { - "id": "trustedrouter", - "display_name": "TrustedRouter", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.trustedrouter.com/v1", - "doc": "https://trustedrouter.com/docs", - "env": [ - "TRUSTEDROUTER_API_KEY" - ], - "model_count": 7 - }, { "id": "umans-ai", "display_name": "Umans AI", @@ -1357,17 +1159,6 @@ ], "model_count": 6 }, - { - "id": "unorouter", - "display_name": "UnoRouter", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.unorouter.com/v1", - "doc": "https://unorouter.com/models", - "env": [ - "UNOROUTER_API_KEY" - ], - "model_count": 23 - }, { "id": "upstage", "display_name": "Upstage", @@ -1388,7 +1179,7 @@ "env": [ "VIVGRID_API_KEY" ], - "model_count": 17 + "model_count": 13 }, { "id": "vultr", @@ -1399,7 +1190,7 @@ "env": [ "VULTR_API_KEY" ], - "model_count": 10 + "model_count": 7 }, { "id": "wafer.ai", @@ -1410,7 +1201,7 @@ "env": [ "WAFER_API_KEY" ], - "model_count": 5 + "model_count": 8 }, { "id": "wandb", @@ -1421,7 +1212,7 @@ "env": [ "WANDB_API_KEY" ], - "model_count": 29 + "model_count": 18 }, { "id": "xiaomi", @@ -1443,7 +1234,7 @@ "env": [ "XIAOMI_API_KEY" ], - "model_count": 7 + "model_count": 8 }, { "id": "xiaomi-token-plan-cn", @@ -1454,7 +1245,7 @@ "env": [ "XIAOMI_API_KEY" ], - "model_count": 7 + "model_count": 8 }, { "id": "xiaomi-token-plan-sgp", @@ -1465,7 +1256,7 @@ "env": [ "XIAOMI_API_KEY" ], - "model_count": 7 + "model_count": 8 }, { "id": "xpersona", @@ -1476,7 +1267,7 @@ "env": [ "XPERSONA_API_KEY" ], - "model_count": 3 + "model_count": 2 }, { "id": "zai", @@ -1511,17 +1302,6 @@ ], "model_count": 1 }, - { - "id": "zenifra", - "display_name": "Zenifra", - "npm": "@ai-sdk/openai-compatible", - "api": "https://ai.zenifra.com/v1", - "doc": "https://docs.zenifra.com", - "env": [ - "ZENIFRA_AI_KEY" - ], - "model_count": 1 - }, { "id": "zenmux", "display_name": "ZenMux", @@ -1531,7 +1311,7 @@ "env": [ "ZENMUX_API_KEY" ], - "model_count": 118 + "model_count": 112 }, { "id": "zhipuai", diff --git a/crates/goose-provider-types/src/canonical/model.rs b/crates/goose-providers/src/canonical/model.rs similarity index 100% rename from crates/goose-provider-types/src/canonical/model.rs rename to crates/goose-providers/src/canonical/model.rs diff --git a/crates/goose-provider-types/src/canonical/name_builder.rs b/crates/goose-providers/src/canonical/name_builder.rs similarity index 92% rename from crates/goose-provider-types/src/canonical/name_builder.rs rename to crates/goose-providers/src/canonical/name_builder.rs index 7dadd1e847..ce2f2042d8 100644 --- a/crates/goose-provider-types/src/canonical/name_builder.rs +++ b/crates/goose-providers/src/canonical/name_builder.rs @@ -329,8 +329,8 @@ mod tests { // === Direct provider (non-hosting) === assert_eq!( - map_to_canonical_model("anthropic", "claude-sonnet-4-5-20250929", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("anthropic", "claude-3-5-sonnet-20241022", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); assert_eq!( map_to_canonical_model("openai", "gpt-4o-latest", r), @@ -353,22 +353,22 @@ mod tests { // === Anthropic Claude - basic === assert_eq!( - map_to_canonical_model("databricks", "claude-sonnet-4-5", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "claude-3-5-sonnet", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); assert_eq!( - map_to_canonical_model("databricks", "claude-sonnet-4-5-20250929", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "claude-3-5-sonnet-20241022", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); assert_eq!( - map_to_canonical_model("databricks", "claude-sonnet-4-5-latest", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "claude-3-5-sonnet-latest", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); // 4.x: {version}-{model} โ†’ {model}-{version} assert_eq!( - map_to_canonical_model("databricks", "claude-sonnet-4-5", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "claude-4-sonnet", r), + Some("anthropic/claude-sonnet-4".to_string()) ); // 4.x with minor version + prefix stripping @@ -379,24 +379,16 @@ mod tests { // === Claude with platform suffixes === assert_eq!( - map_to_canonical_model("databricks", "claude-sonnet-4-5-bedrock", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "claude-4-sonnet-bedrock", r), + Some("anthropic/claude-sonnet-4".to_string()) ); assert_eq!( - map_to_canonical_model("databricks", "goose-claude-sonnet-4-5-bedrock", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "goose-claude-4-sonnet-bedrock", r), + Some("anthropic/claude-sonnet-4".to_string()) ); assert_eq!( - map_to_canonical_model("bedrock", "claude-sonnet-4-5", r), - Some("anthropic/claude-sonnet-4.5".to_string()) - ); - assert_eq!( - map_to_canonical_model("aws_bedrock", "global.anthropic.claude-sonnet-5", r), - Some("amazon-bedrock/global.anthropic.claude-sonnet-5".to_string()) - ); - assert_eq!( - map_to_canonical_model("anthropic", "claude-sonnet-5", r), - Some("anthropic/claude-sonnet-5".to_string()) + map_to_canonical_model("bedrock", "claude-3-5-sonnet", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); // === OpenAI GPT === @@ -494,8 +486,8 @@ mod tests { // === Provider-prefixed extraction === assert_eq!( - map_to_canonical_model("databricks", "anthropic-claude-sonnet-4-5", r), - Some("anthropic/claude-sonnet-4.5".to_string()) + map_to_canonical_model("databricks", "anthropic-claude-3-5-sonnet", r), + Some("anthropic/claude-3.5-sonnet".to_string()) ); assert_eq!( map_to_canonical_model("databricks", "openai-gpt-4o", r), diff --git a/crates/goose-provider-types/src/canonical/registry.rs b/crates/goose-providers/src/canonical/registry.rs similarity index 100% rename from crates/goose-provider-types/src/canonical/registry.rs rename to crates/goose-providers/src/canonical/registry.rs diff --git a/crates/goose-provider-types/src/conversation.rs b/crates/goose-providers/src/conversation.rs similarity index 81% rename from crates/goose-provider-types/src/conversation.rs rename to crates/goose-providers/src/conversation.rs index 7f8276579d..457a6b3291 100644 --- a/crates/goose-provider-types/src/conversation.rs +++ b/crates/goose-providers/src/conversation.rs @@ -43,10 +43,6 @@ impl Conversation { &self.0 } - pub fn messages_mut(&mut self) -> &mut Vec { - &mut self.0 - } - pub fn push(&mut self, message: Message) { if message.content.is_empty() && message.metadata.inference.is_some() { if let Some(existing) = self @@ -135,10 +131,6 @@ impl Conversation { self.0.pop() } - pub fn remove(&mut self, index: usize) -> Message { - self.0.remove(index) - } - pub fn truncate(&mut self, len: usize) { self.0.truncate(len); } @@ -251,7 +243,6 @@ fn fix_messages(messages: Vec) -> (Vec, Vec) { fix_empty_tool_results, fix_tool_calling, merge_consecutive_messages, - dedupe_signed_thinking, fix_lead_trail, populate_if_empty, ] @@ -507,71 +498,6 @@ pub fn merge_consecutive_messages(messages: Vec) -> (Vec, Vec< (merged_messages, issues) } -/// Signed thinking carries a signature; redacted thinking is always signed. -/// Signed blocks must be replayed exactly; unsigned reasoning summaries need not. -fn is_signed_thinking(content: &MessageContent) -> bool { - match content { - MessageContent::Thinking(t) => !t.signature.is_empty(), - MessageContent::RedactedThinking(_) => true, - _ => false, - } -} - -/// Drops duplicate signed thinking blocks, keeping the first occurrence. Some -/// signed-replay APIs (like Anthropic) reject a request that repeats the same -/// signed block more than once. -/// -/// Duplicates arise two ways, both handled here: -/// - Within one assistant message, when a standalone thinking message is -/// merged with a tool-call message that re-embedded the same thinking. -/// - Across assistant messages, when the agent splits one provider turn into -/// several tool-call messages (interleaved with tool results) that each -/// carry a copy of the turn's signed thinking. -/// -/// The `seen` set spans the whole conversation. A signed block carries a -/// cryptographic signature unique to its generation, so an exact (text + -/// signature) match can only be the same turn's thinking copied onto split -/// messages โ€” never two genuinely distinct thoughts. Unsigned reasoning -/// summaries are left untouched, since providers like Kimi/DeepSeek require -/// them echoed on every tool-call message. -/// -/// This runs before any provider formatter, so it covers every Claude transport -/// (direct Anthropic, Bedrock, Databricks, Vertex) in one place. -fn dedupe_signed_thinking(messages: Vec) -> (Vec, Vec) { - let mut issues = Vec::new(); - let mut seen: Vec = Vec::new(); - - let fixed_messages = messages - .into_iter() - .map(|mut message| { - if message.role != Role::Assistant { - return message; - } - - let original_len = message.content.len(); - let mut deduped: Vec = Vec::with_capacity(original_len); - for content in &message.content { - let is_signed = is_signed_thinking(content); - if is_signed && seen.contains(content) { - continue; - } - if is_signed { - seen.push(content.clone()); - } - deduped.push(content.clone()); - } - - if deduped.len() != original_len { - issues.push("Removed duplicate signed thinking block".to_string()); - message.content = deduped; - } - message - }) - .collect(); - - (fixed_messages, issues) -} - fn has_tool_response(message: &Message) -> bool { message .content @@ -1374,237 +1300,6 @@ mod tests { assert!(!fixed_messages[5].metadata.agent_visible); } - #[test] - fn test_dedupes_duplicate_signed_thinking_around_tool_call() { - use crate::conversation::message::MessageContent; - use rmcp::model::Content; - - // Reproduces the Anthropic 400 scenario: a standalone signed thinking - // message immediately followed by an assistant message that repeats the - // same signed thinking plus a tool_use. After merging consecutive - // assistant messages these become two adjacent identical thinking blocks. - let messages = vec![ - Message::user().with_text("Do the thing"), - Message::assistant().with_thinking("Let me think about this", "sig-1"), - Message::assistant() - .with_thinking("Let me think about this", "sig-1") - .with_tool_request( - "tool_1", - Ok(CallToolRequestParams::new("do_thing").with_arguments(object!({"x": 1}))), - ), - Message::user().with_tool_response( - "tool_1", - Ok(rmcp::model::CallToolResult::success(vec![Content::text( - "done", - )])), - ), - Message::user().with_text("Now continue"), - ]; - - let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); - - assert!( - issues - .iter() - .any(|i| i == "Removed duplicate signed thinking block"), - "expected dedupe issue, got: {:?}", - issues - ); - - let fixed_messages = fixed.messages(); - let assistant = fixed_messages - .iter() - .find(|m| { - m.role == Role::Assistant - && m.content - .iter() - .any(|c| matches!(c, MessageContent::ToolRequest(_))) - }) - .expect("assistant tool-call message should exist"); - - let thinking_count = assistant - .content - .iter() - .filter(|c| { - matches!( - c, - MessageContent::Thinking(_) | MessageContent::RedactedThinking(_) - ) - }) - .count(); - assert_eq!( - thinking_count, 1, - "duplicate signed thinking should be collapsed to one block" - ); - } - - #[test] - fn test_keeps_distinct_signed_thinking_blocks_in_assistant_message() { - use crate::conversation::message::MessageContent; - use rmcp::model::Content; - - // Distinct signed thinking blocks (different signatures) must be preserved. - let messages = vec![ - Message::user().with_text("Do the thing"), - Message::assistant() - .with_thinking("First thought", "sig-A") - .with_thinking("Second thought", "sig-B") - .with_tool_request( - "tool_1", - Ok(CallToolRequestParams::new("do_thing").with_arguments(object!({"x": 1}))), - ), - Message::user().with_tool_response( - "tool_1", - Ok(rmcp::model::CallToolResult::success(vec![Content::text( - "done", - )])), - ), - Message::user().with_text("Now continue"), - ]; - - let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); - - assert!( - !issues - .iter() - .any(|i| i == "Removed duplicate signed thinking block"), - "should not dedupe distinct thinking blocks, got: {:?}", - issues - ); - - let fixed_messages = fixed.messages(); - let thinking_count = fixed_messages[1] - .content - .iter() - .filter(|c| { - matches!( - c, - MessageContent::Thinking(_) | MessageContent::RedactedThinking(_) - ) - }) - .count(); - assert_eq!(thinking_count, 2, "distinct thinking blocks must be kept"); - } - - #[test] - fn test_keeps_duplicate_unsigned_thinking_blocks() { - use crate::conversation::message::MessageContent; - - // Unsigned thinking (reasoning summaries from non-Anthropic providers) - // can legitimately repeat and must not be dropped, since only signed - // blocks trigger the Anthropic exact-replay 400. - let messages = vec![ - Message::user().with_text("Do the thing"), - Message::assistant() - .with_thinking("same reasoning", "") - .with_thinking("same reasoning", ""), - Message::user().with_text("Now continue"), - ]; - - let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); - - assert!( - !issues - .iter() - .any(|i| i == "Removed duplicate signed thinking block"), - "unsigned thinking must not be deduped, got: {:?}", - issues - ); - - let fixed_messages = fixed.messages(); - let thinking_count = fixed_messages - .iter() - .flat_map(|m| m.content.iter()) - .filter(|c| matches!(c, MessageContent::Thinking(_))) - .count(); - assert_eq!( - thinking_count, 2, - "duplicate unsigned thinking blocks must be kept" - ); - } - - #[test] - fn test_dedupes_signed_thinking_across_split_tool_messages() { - use crate::conversation::message::MessageContent; - use rmcp::model::Content; - - // The agent splits one provider turn with multiple tool calls into one - // assistant message per call (interleaved with tool results), each - // carrying the same signed thinking. merge_consecutive_messages cannot - // merge them because tool results sit between, so the dedupe must span - // messages and keep the signed block only on the first. - let messages = vec![ - Message::user().with_text("Use both tools"), - Message::assistant() - .with_thinking("multi-tool reasoning", "sig-1") - .with_tool_request( - "call_1", - Ok(CallToolRequestParams::new("tool_a").with_arguments(object!({"p": 1}))), - ), - Message::user().with_tool_response( - "call_1", - Ok(rmcp::model::CallToolResult::success(vec![Content::text( - "ok", - )])), - ), - Message::assistant() - .with_thinking("multi-tool reasoning", "sig-1") - .with_tool_request( - "call_2", - Ok(CallToolRequestParams::new("tool_b").with_arguments(object!({"p": 2}))), - ), - Message::user().with_tool_response( - "call_2", - Ok(rmcp::model::CallToolResult::success(vec![Content::text( - "ok", - )])), - ), - Message::user().with_text("Now continue"), - ]; - - let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); - - assert!( - issues - .iter() - .any(|i| i == "Removed duplicate signed thinking block"), - "expected cross-message dedupe issue, got: {:?}", - issues - ); - - let fixed_messages = fixed.messages(); - let total_thinking = fixed_messages - .iter() - .flat_map(|m| m.content.iter()) - .filter(|c| { - matches!( - c, - MessageContent::Thinking(_) | MessageContent::RedactedThinking(_) - ) - }) - .count(); - assert_eq!( - total_thinking, 1, - "the repeated signed block must survive only once across the turn" - ); - - let total_tool_requests = fixed_messages - .iter() - .flat_map(|m| m.content.iter()) - .filter(|c| matches!(c, MessageContent::ToolRequest(_))) - .count(); - assert_eq!(total_tool_requests, 2, "both tool calls must be preserved"); - - // The first split message keeps the thinking; the second loses it. - assert!( - fixed_messages[1] - .content - .iter() - .any(|c| matches!(c, MessageContent::Thinking(_))), - "first split message keeps signed thinking" - ); - } - #[test] fn test_push_coalesces_thinking_deltas() { use crate::conversation::message::MessageContent; diff --git a/crates/goose-provider-types/src/conversation/message.rs b/crates/goose-providers/src/conversation/message.rs similarity index 96% rename from crates/goose-provider-types/src/conversation/message.rs rename to crates/goose-providers/src/conversation/message.rs index 1c095089c7..38c3a50069 100644 --- a/crates/goose-provider-types/src/conversation/message.rs +++ b/crates/goose-providers/src/conversation/message.rs @@ -1,4 +1,3 @@ -use crate::conversation::token_usage::{CostSource, ProviderUsage}; use crate::conversation::tool_result_serde; use crate::mcp_utils::extract_text_from_resource; use crate::utils::sanitize_unicode_tags; @@ -253,7 +252,6 @@ pub struct FrontendToolRequest { #[serde(rename_all = "camelCase")] pub enum SystemNotificationType { ThinkingMessage, - ProgressMessage, InlineMessage, CreditsExhausted, } @@ -668,49 +666,6 @@ pub struct InferenceMetadata { pub resolved_model: Option, } -#[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct MessageUsage { - #[serde(skip_serializing_if = "Option::is_none")] - pub input_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_read_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_write_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost_source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub elapsed_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub time_to_first_token_ms: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub is_compaction: bool, -} - -impl MessageUsage { - pub fn from_provider_usage(usage: &ProviderUsage, is_compaction: bool) -> Self { - let stats = usage.stats.as_ref(); - MessageUsage { - input_tokens: usage.usage.input_tokens, - output_tokens: usage.usage.output_tokens, - total_tokens: usage.usage.total_tokens, - cache_read_tokens: usage.usage.cache_read_input_tokens, - cache_write_tokens: usage.usage.cache_write_input_tokens, - cost: usage.cost, - cost_source: usage.cost_source, - elapsed_ms: stats.and_then(|s| s.elapsed_ms), - time_to_first_token_ms: stats.and_then(|s| s.time_to_first_token_ms), - is_compaction, - } - } -} - #[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize, Debug)] /// Metadata for message visibility and model inference details #[serde(rename_all = "camelCase")] @@ -726,8 +681,6 @@ pub struct MessageMetadata { /// without matching user-visible text. Never sent to providers. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub steer: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub usage: Option>, } impl Default for MessageMetadata { @@ -737,7 +690,6 @@ impl Default for MessageMetadata { agent_visible: true, inference: None, steer: false, - usage: None, } } } diff --git a/crates/goose-provider-types/src/conversation/token_usage.rs b/crates/goose-providers/src/conversation/token_usage.rs similarity index 91% rename from crates/goose-provider-types/src/conversation/token_usage.rs rename to crates/goose-providers/src/conversation/token_usage.rs index 4fa2db5c52..d62a4d3701 100644 --- a/crates/goose-provider-types/src/conversation/token_usage.rs +++ b/crates/goose-providers/src/conversation/token_usage.rs @@ -9,23 +9,11 @@ pub struct ProviderUsage { pub usage: Usage, #[serde(default, skip_serializing_if = "Option::is_none")] pub stats: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cost: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cost_source: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum CostSource { - ProviderReported, - Estimated, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderStats { pub time_to_first_token_ms: Option, - pub model_load_ms: Option, pub elapsed_ms: Option, pub output_tokens: Option, pub draft: Option, @@ -47,8 +35,6 @@ impl ProviderUsage { model, usage, stats: None, - cost: None, - cost_source: None, } } @@ -57,10 +43,14 @@ impl ProviderUsage { self } - pub fn with_cost(mut self, cost: f64, source: CostSource) -> Self { - self.cost = Some(cost); - self.cost_source = Some(source); - self + /// Combine this ProviderUsage with another, adding their token counts + /// Uses the model from this ProviderUsage + pub fn combine_with(&self, other: &ProviderUsage) -> ProviderUsage { + ProviderUsage { + model: self.model.clone(), + usage: self.usage + other.usage, + stats: self.stats.clone().or_else(|| other.stats.clone()), + } } } diff --git a/crates/goose-provider-types/src/conversation/tool_result_serde.rs b/crates/goose-providers/src/conversation/tool_result_serde.rs similarity index 100% rename from crates/goose-provider-types/src/conversation/tool_result_serde.rs rename to crates/goose-providers/src/conversation/tool_result_serde.rs diff --git a/crates/goose-providers/src/declarative.rs b/crates/goose-providers/src/declarative.rs deleted file mode 100644 index bde7823897..0000000000 --- a/crates/goose-providers/src/declarative.rs +++ /dev/null @@ -1,592 +0,0 @@ -#[macro_use] -mod macros; - -use std::{collections::HashMap, path::Path, str::FromStr}; - -use anyhow::Result; -use include_dir::{include_dir, Dir}; -use serde::{Deserialize, Deserializer, Serialize}; -use utoipa::ToSchema; - -pub static FIXED_PROVIDERS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/declarative/definitions"); - -pub(crate) mod declarative_providers { - use super::*; - - expose_declarative_providers!( - alibaba, - atomic_chat, - cerebras, - deepseek, - empiriolabs, - fireworks, - futurmix, - groq, - iflytek, - iflytek_astron, - inception, - llama_swap, - lmstudio, - meta, - minimax, - mistral, - moonshot, - nearai, - novita, - nvidia, - ollama_cloud, - omlx, - opencode_go, - orcarouter, - ovhcloud, - perplexity, - routstr, - saladcloud, - scaleway, - tanzu, - tensorix, - together, - venice, - vercel_ai_gateway, - zai, - zhipu, - ); -} - -use crate::{ - anthropic, - api_client::TlsConfig, - base::{ModelInfo, Provider}, - ollama, openai, -}; - -pub fn fixed_provider_configs() -> anyhow::Result> { - declarative_providers::fixed_provider_configs() -} - -pub fn fixed_provider_config_entries() -> Vec<(&'static str, &'static str)> { - declarative_providers::fixed_provider_config_entries() -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct EnvVarConfig { - pub name: String, - #[serde(default)] - pub required: bool, - #[serde(default)] - pub secret: bool, - /// Defaults to the value of `required` if not specified. - /// UIs may use this to feature this config value more prominently. - pub primary: Option, - pub description: Option, - pub default: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum ProviderEngine { - #[serde(alias = "openai_compatible")] - OpenAI, - #[serde(alias = "ollama_compatible")] - Ollama, - #[serde(alias = "anthropic_compatible")] - Anthropic, -} - -impl FromStr for ProviderEngine { - type Err = anyhow::Error; - - fn from_str(engine: &str) -> Result { - match engine.trim().to_lowercase().as_str() { - "openai" | "openai_compatible" => Ok(Self::OpenAI), - "anthropic" | "anthropic_compatible" => Ok(Self::Anthropic), - "ollama" | "ollama_compatible" => Ok(Self::Ollama), - _ => Err(anyhow::anyhow!("Invalid provider type: {}", engine)), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -pub struct DeclarativeProviderConfig { - pub name: String, - pub engine: ProviderEngine, - pub display_name: String, - pub description: Option, - #[serde(default)] - pub api_key_env: String, - pub base_url: String, - pub models: Vec, - pub headers: Option>, - pub timeout_seconds: Option, - pub supports_streaming: Option, - #[serde(default = "default_requires_auth")] - pub requires_auth: bool, - #[serde(default)] - pub catalog_provider_id: Option, - #[serde(default)] - pub base_path: Option, - #[serde(default)] - pub env_vars: Option>, - /// Controls whether `fetch_supported_models` calls the provider's `/v1/models` - /// endpoint or returns the static `models` list directly. - /// - /// - `Some(false)` + non-empty `models`: return the static list; no API call. - /// Construction fails if `models` is empty. - /// - `Some(true)` or `None`: try the API; fall back to `models` on 404. - #[serde(default)] - pub dynamic_models: Option, - #[serde(default)] - pub skip_canonical_filtering: bool, - #[serde(default, deserialize_with = "deserialize_non_empty_string")] - pub model_doc_link: Option, - #[serde(default)] - pub setup_steps: Vec, - #[serde(default, deserialize_with = "deserialize_non_empty_string")] - pub fast_model: Option, - #[serde(default)] - pub preserves_thinking: bool, -} - -fn default_requires_auth() -> bool { - true -} - -pub fn should_preserve_thinking_by_default(engine: &ProviderEngine) -> bool { - matches!(engine, ProviderEngine::OpenAI) -} - -/// Deserialize an optional string, treating empty/whitespace-only values as None. -fn deserialize_non_empty_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let opt: Option = Option::deserialize(deserializer)?; - Ok(opt.filter(|s| !s.trim().is_empty())) -} - -impl DeclarativeProviderConfig { - pub fn id(&self) -> &str { - &self.name - } - - pub fn display_name(&self) -> &str { - &self.display_name - } - - pub fn models(&self) -> &[ModelInfo] { - &self.models - } -} - -pub trait KeyResolver { - type Error: std::error::Error + Send + Sync + 'static; - - fn resolve_key(&self, key: &str) -> std::result::Result; -} - -pub struct EnvKeyResolver; - -impl EnvKeyResolver { - pub fn new() -> Self { - EnvKeyResolver {} - } -} - -impl Default for EnvKeyResolver { - fn default() -> Self { - Self::new() - } -} - -impl KeyResolver for EnvKeyResolver { - type Error = std::env::VarError; - - fn resolve_key(&self, key: &str) -> std::result::Result { - std::env::var(key) - } -} - -fn expand_env_vars(template: &str, env_vars: &[EnvVarConfig]) -> Result { - let mut result = template.to_string(); - - for var in env_vars { - let placeholder = format!("${{{}}}", var.name); - if !result.contains(&placeholder) { - continue; - } - - let value = match std::env::var(&var.name) { - Ok(value) => value, - Err(_) => match &var.default { - Some(default) => default.clone(), - None if var.required => { - anyhow::bail!("Required environment variable {} is not set", var.name) - } - None => continue, - }, - }; - - result = result.replace(&placeholder, &value); - } - - Ok(result) -} - -fn resolve_config(config: &mut DeclarativeProviderConfig) -> Result<()> { - if let Some(env_vars) = &config.env_vars { - config.base_url = expand_env_vars(&config.base_url, env_vars)?; - - for var in env_vars { - if var.name.ends_with("_STREAMING") { - let value = std::env::var(&var.name) - .ok() - .or_else(|| var.default.clone()) - .map(|value| value.eq_ignore_ascii_case("true")); - if let Some(value) = value { - config.supports_streaming = Some(value); - } - } - } - } - - Ok(()) -} - -pub fn deserialize_provider_config(json: &str) -> Result { - let raw: serde_json::Value = serde_json::from_str(json)?; - let preserves_thinking_was_set = raw.get("preserves_thinking").is_some(); - let mut config: DeclarativeProviderConfig = serde_json::from_value(raw)?; - - if !preserves_thinking_was_set { - config.preserves_thinking = should_preserve_thinking_by_default(&config.engine); - } - - Ok(config) -} - -fn config_from_json(json: &str) -> Result { - let mut config = deserialize_provider_config(json)?; - resolve_config(&mut config)?; - Ok(config) -} - -pub fn load_custom_providers(dir: &Path) -> Result> { - if !dir.exists() { - return Ok(Vec::new()); - } - - std::fs::read_dir(dir)? - .filter_map(|entry| { - let path = entry.ok()?.path(); - (path.extension()? == "json").then_some(path) - }) - .map(|path| { - let content = std::fs::read_to_string(&path)?; - deserialize_provider_config(&content) - .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", path.display(), e)) - }) - .collect() -} - -pub fn from_json( - json: &str, - tls_config: Option, - key_resolver: impl KeyResolver, -) -> Result> { - let config = config_from_json(json)?; - - match config.engine { - ProviderEngine::OpenAI => openai::from_declarative_config(config, tls_config, key_resolver) - .map(|provider| Box::new(provider.build()) as Box), - ProviderEngine::Ollama => ollama::from_declarative_config(config, tls_config, key_resolver) - .map(|provider| Box::new(provider.build()) as Box), - ProviderEngine::Anthropic => { - anthropic::from_declarative_config(config, tls_config, key_resolver) - .map(|provider| Box::new(provider.build()) as Box) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use std::collections::HashSet; - - fn model_json() -> serde_json::Value { - json!({ - "name": "test-model", - "context_limit": 4096, - "input_token_cost": null, - "output_token_cost": null, - "currency": null, - "supports_cache_control": null, - "reasoning": false - }) - } - - #[test] - fn provider_engine_deserializes_compatible_aliases() { - let openai: DeclarativeProviderConfig = serde_json::from_value(json!({ - "name": "test-openai", - "engine": "openai_compatible", - "display_name": "Test OpenAI", - "base_url": "http://localhost:1234", - "models": [model_json()] - })) - .unwrap(); - assert_eq!(openai.engine, ProviderEngine::OpenAI); - - let anthropic: DeclarativeProviderConfig = serde_json::from_value(json!({ - "name": "test-anthropic", - "engine": "anthropic_compatible", - "display_name": "Test Anthropic", - "base_url": "http://localhost:1234", - "models": [model_json()] - })) - .unwrap(); - assert_eq!(anthropic.engine, ProviderEngine::Anthropic); - - let ollama: DeclarativeProviderConfig = serde_json::from_value(json!({ - "name": "test-ollama", - "engine": "ollama_compatible", - "display_name": "Test Ollama", - "base_url": "http://localhost:11434", - "models": [model_json()] - })) - .unwrap(); - assert_eq!(ollama.engine, ProviderEngine::Ollama); - } - - #[test] - fn groq_json_disables_thinking_preservation() { - let config = - deserialize_provider_config(crate::groq::JSON).expect("groq.json should parse"); - - assert!(!config.preserves_thinking); - } - - fn placeholder_var_names(template: &str) -> Vec { - template - .split("${") - .skip(1) - .filter_map(|chunk| chunk.split_once('}')) - .map(|(name, _)| name.to_string()) - .collect() - } - - fn validate_provider_id(id: &str) -> Result<()> { - let mut chars = id.chars(); - let Some(first) = chars.next() else { - anyhow::bail!("Invalid provider id: provider id cannot be empty"); - }; - - if !(first.is_ascii_lowercase() || first.is_ascii_digit() || first == '_') { - anyhow::bail!("Invalid provider id: {id}"); - } - - if chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-') - { - Ok(()) - } else { - anyhow::bail!("Invalid provider id: {id}") - } - } - - #[test] - fn expose_declarative_providers_enumerates_all_bundled_json_files() { - let enumerated: HashSet<_> = fixed_provider_config_entries() - .into_iter() - .map(|(path, _)| path.to_string()) - .collect(); - let bundled: HashSet<_> = FIXED_PROVIDERS - .files() - .filter(|file| file.path().extension().and_then(|s| s.to_str()) == Some("json")) - .map(|file| { - file.path() - .file_name() - .unwrap() - .to_string_lossy() - .into_owned() - }) - .collect(); - - assert_eq!(enumerated, bundled); - } - - #[test] - fn all_bundled_providers_are_valid() { - let mut seen_ids = HashSet::new(); - - for (path, json) in fixed_provider_config_entries() { - let config = deserialize_provider_config(json) - .unwrap_or_else(|e| panic!("{path} failed to parse: {e}")); - - validate_provider_id(config.id()) - .unwrap_or_else(|e| panic!("{path} has an invalid provider id: {e}")); - assert!( - seen_ids.insert(config.id().to_string()), - "{path} has a duplicate provider id: {}", - config.id() - ); - assert!(!config.base_url.is_empty(), "{path} has an empty base_url"); - - if config.dynamic_models == Some(false) { - assert!( - !config.models.is_empty(), - "{path} disables dynamic_models but lists no static models" - ); - } - - let declared: HashSet<&str> = config - .env_vars - .iter() - .flatten() - .map(|v| v.name.as_str()) - .collect(); - let templates = std::iter::once(config.base_url.as_str()) - .chain(config.base_path.as_deref()) - .chain( - config - .headers - .iter() - .flat_map(|h| h.values()) - .map(String::as_str), - ); - for template in templates { - for var in placeholder_var_names(template) { - assert!( - declared.contains(var.as_str()), - "{path} references ${{{var}}} but declares no matching env_var" - ); - } - } - } - - assert!(!seen_ids.is_empty(), "no bundled providers were found"); - } - - #[test] - fn fixed_provider_configs_are_unresolved() { - let configs = fixed_provider_configs().expect("bundled providers should load"); - let config = configs - .iter() - .find(|config| config.env_vars.is_some()) - .expect("at least one bundled provider should declare env_vars"); - - assert!( - config.base_url.contains("${"), - "{} should keep base_url placeholders unresolved", - config.id() - ); - } - - #[test] - fn from_json_defaults_openai_preserves_thinking_to_true() { - let json = json!({ - "name": "test-provider", - "engine": "openai", - "display_name": "Test Provider", - "base_url": "http://localhost:1234/v1/chat/completions", - "models": [model_json()], - "requires_auth": false, - "dynamic_models": false - }) - .to_string(); - - let config = config_from_json(&json).unwrap(); - - assert!(config.preserves_thinking); - } - - #[test] - fn from_json_preserves_explicit_openai_preserves_thinking_false() { - let json = json!({ - "name": "test-provider", - "engine": "openai", - "display_name": "Test Provider", - "base_url": "http://localhost:1234/v1/chat/completions", - "models": [model_json()], - "requires_auth": false, - "dynamic_models": false, - "preserves_thinking": false - }) - .to_string(); - - let config = config_from_json(&json).unwrap(); - - assert!(!config.preserves_thinking); - } - - #[test] - fn from_json_expands_base_url_from_env_var_default() { - let _guard = env_lock::lock_env([("TEST_PROVIDER_HOST", None::<&str>)]); - let json = json!({ - "name": "test-provider", - "engine": "openai", - "display_name": "Test Provider", - "base_url": "${TEST_PROVIDER_HOST}/v1/chat/completions", - "models": [model_json()], - "requires_auth": false, - "dynamic_models": false, - "env_vars": [{ - "name": "TEST_PROVIDER_HOST", - "default": "http://localhost:1234" - }] - }) - .to_string(); - - let provider = from_json(&json, None, EnvKeyResolver).unwrap(); - - assert_eq!(provider.get_name(), "test-provider"); - } - - #[tokio::test] - async fn from_json_ollama_returns_static_models_when_dynamic_models_false() { - let json = json!({ - "name": "test-ollama", - "engine": "ollama", - "display_name": "Test Ollama", - "base_url": "http://localhost:11434", - "models": [model_json()], - "requires_auth": false, - "dynamic_models": false - }) - .to_string(); - - let provider = from_json(&json, None, EnvKeyResolver).unwrap(); - - assert_eq!( - provider.fetch_supported_models().await.unwrap(), - vec!["test-model".to_string()] - ); - } - - #[test] - fn from_json_errors_when_required_env_var_is_missing() { - let _guard = env_lock::lock_env([("TEST_PROVIDER_REQUIRED_HOST", None::<&str>)]); - let json = json!({ - "name": "test-provider", - "engine": "openai", - "display_name": "Test Provider", - "base_url": "${TEST_PROVIDER_REQUIRED_HOST}/v1/chat/completions", - "models": [model_json()], - "requires_auth": false, - "dynamic_models": false, - "env_vars": [{ - "name": "TEST_PROVIDER_REQUIRED_HOST", - "required": true - }] - }) - .to_string(); - - let err = match from_json(&json, None, EnvKeyResolver) { - Ok(_) => panic!("expected missing required env var error"), - Err(err) => err, - }; - - assert!(err - .to_string() - .contains("Required environment variable TEST_PROVIDER_REQUIRED_HOST is not set")); - } -} diff --git a/crates/goose-providers/src/declarative/definitions/fireworks.json b/crates/goose-providers/src/declarative/definitions/fireworks.json deleted file mode 100644 index 85b56bb674..0000000000 --- a/crates/goose-providers/src/declarative/definitions/fireworks.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "fireworks-ai", - "engine": "openai", - "display_name": "Fireworks AI", - "description": "Fast serverless inference for open models with an OpenAI-compatible API", - "api_key_env": "FIREWORKS_API_KEY", - "base_url": "https://api.fireworks.ai/inference/v1/chat/completions", - "catalog_provider_id": "fireworks-ai", - "dynamic_models": false, - "models": [ - { - "name": "accounts/fireworks/models/kimi-k2p7-code", - "context_limit": 262000 - }, - { - "name": "accounts/fireworks/models/kimi-k2p6", - "context_limit": 262000 - }, - { - "name": "accounts/fireworks/models/glm-5p2", - "context_limit": 1048576 - }, - { - "name": "accounts/fireworks/models/deepseek-v4-pro", - "context_limit": 1000000 - }, - { - "name": "accounts/fireworks/models/deepseek-v4-flash", - "context_limit": 1000000 - }, - { - "name": "accounts/fireworks/models/minimax-m3", - "context_limit": 512000 - }, - { - "name": "accounts/fireworks/models/qwen3p7-plus", - "context_limit": 262144 - }, - { - "name": "accounts/fireworks/models/glm-5p1", - "context_limit": 202800 - }, - { - "name": "accounts/fireworks/models/gpt-oss-120b", - "context_limit": 131072 - }, - { - "name": "accounts/fireworks/models/gpt-oss-20b", - "context_limit": 131072 - } - ], - "preserves_thinking": true, - "supports_streaming": true, - "model_doc_link": "https://fireworks.ai/models" -} diff --git a/crates/goose-providers/src/declarative/definitions/iflytek.json b/crates/goose-providers/src/declarative/definitions/iflytek.json deleted file mode 100644 index a0c2da0387..0000000000 --- a/crates/goose-providers/src/declarative/definitions/iflytek.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "iflytek", - "engine": "openai", - "display_name": "iFlytek Spark", - "description": "iFlytek Spark (่ฎฏ้ฃžๆ˜Ÿ็ซ) models via the OpenAI-compatible HTTP API. Authenticate with your Spark HTTP API password (APIPassword). Lists the 4.0Ultra and Max families, which per the Spark HTTP docs accept system messages (always sent by goose). Best used for chat: Spark only returns OpenAI-style tool_calls when the request body sets tool_calls_switch=true, which this declarative config cannot inject, so tool-using extensions may not work as-is.", - "api_key_env": "SPARK_API_PASSWORD", - "base_url": "https://spark-api-open.xf-yun.com/v1", - "dynamic_models": false, - "skip_canonical_filtering": true, - "models": [ - {"name": "4.0Ultra", "context_limit": 8192}, - {"name": "generalv3.5", "context_limit": 8192}, - {"name": "max-32k", "context_limit": 32768} - ], - "supports_streaming": true, - "model_doc_link": "https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html", - "setup_steps": [ - "Sign in to https://xinghuo.xfyun.cn/sparkapi and create or select a Spark model plan", - "Open the model's 'HTTP ๆœๅŠกๆŽฅๅฃ่ฎค่ฏไฟกๆฏ' page and copy the APIPassword (each model version has its own password)", - "Paste the APIPassword above as SPARK_API_PASSWORD" - ] -} diff --git a/crates/goose-providers/src/declarative/definitions/iflytek_astron.json b/crates/goose-providers/src/declarative/definitions/iflytek_astron.json deleted file mode 100644 index 6cffd3ba6a..0000000000 --- a/crates/goose-providers/src/declarative/definitions/iflytek_astron.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "iflytek_astron", - "engine": "openai", - "display_name": "iFlytek Astron MaaS", - "description": "iFlytek Astron MaaS (่ฎฏ้ฃžๆ˜Ÿ่พฐ) models via the OpenAI-compatible API. Defaults to the Token Plan endpoint; set ASTRON_BASE_URL to https://maas-coding-api.cn-huabei-1.xf-yun.com/v2 to use the Coding Plan (e.g. astron-code-latest). Each plan has its own API key.", - "api_key_env": "ASTRON_API_KEY", - "base_url": "${ASTRON_BASE_URL}", - "env_vars": [ - { - "name": "ASTRON_BASE_URL", - "required": false, - "secret": false, - "default": "https://maas-token-api.cn-huabei-1.xf-yun.com/v2", - "description": "Astron MaaS API base URL. Use https://maas-coding-api.cn-huabei-1.xf-yun.com/v2 for Coding Plan models like astron-code-latest." - } - ], - "dynamic_models": false, - "skip_canonical_filtering": true, - "models": [ - {"name": "xsparkx2", "context_limit": 131072}, - {"name": "xsparkx2flash", "context_limit": 131072}, - {"name": "xopglm51", "context_limit": 204800}, - {"name": "xopglm5", "context_limit": 204800}, - {"name": "xopdeepseekv4pro", "context_limit": 131072}, - {"name": "xopdeepseekv4flash", "context_limit": 131072}, - {"name": "xopdeepseekv32", "context_limit": 131072}, - {"name": "xopkimik26", "context_limit": 262144}, - {"name": "xminimaxm25", "context_limit": 131072}, - {"name": "xopqwen35397b", "context_limit": 262144}, - {"name": "astron-code-latest", "context_limit": 131072} - ], - "supports_streaming": true, - "preserves_thinking": true, - "model_doc_link": "https://www.xfyun.cn/doc/spark/TokenPlan.html", - "setup_steps": [ - "Sign in to https://maas.xfyun.cn and subscribe to a Token Plan (general models) or Coding Plan (astron-code-latest)", - "Copy the dedicated API Key for your plan from the subscription page", - "Paste the API Key above as ASTRON_API_KEY", - "For Coding Plan, set ASTRON_BASE_URL to https://maas-coding-api.cn-huabei-1.xf-yun.com/v2" - ] -} diff --git a/crates/goose-providers/src/declarative/definitions/meta.json b/crates/goose-providers/src/declarative/definitions/meta.json deleted file mode 100644 index 6b0c0f1237..0000000000 --- a/crates/goose-providers/src/declarative/definitions/meta.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "meta", - "engine": "openai", - "display_name": "Meta", - "description": "Meta's Model API, home of the Muse Spark models", - "api_key_env": "META_MODEL_API_KEY", - "base_url": "https://api.meta.ai/v1", - "catalog_provider_id": "meta", - "dynamic_models": true, - "models": [ - { - "name": "muse-spark-1.1", - "context_limit": 1000000, - "max_tokens": 32000, - "input_token_cost": 0.00000125, - "output_token_cost": 0.00000425, - "currency": "USD", - "reasoning": true - } - ], - "preserves_thinking": true, - "supports_streaming": true, - "model_doc_link": "https://dev.meta.ai/docs", - "setup_steps": [ - "Sign in to https://dev.meta.ai", - "Navigate to API Keys in your account settings", - "Create a new API key", - "Copy the key and paste it above" - ] -} diff --git a/crates/goose-providers/src/declarative/macros.rs b/crates/goose-providers/src/declarative/macros.rs deleted file mode 100644 index a2fe7ef1f8..0000000000 --- a/crates/goose-providers/src/declarative/macros.rs +++ /dev/null @@ -1,44 +0,0 @@ -macro_rules! expose_declarative_provider { - ($module:ident, $definition:expr) => { - pub mod $module { - use anyhow::Result; - - use crate::{ - api_client::TlsConfig, - base::Provider, - declarative::{from_json, KeyResolver}, - }; - - pub const JSON: &str = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/declarative/definitions/", - $definition, - ".json" - )); - - pub fn create( - tls_config: Option, - key_resolver: impl KeyResolver, - ) -> Result> { - from_json(JSON, tls_config, key_resolver) - } - } - }; -} - -macro_rules! expose_declarative_providers { - ($($module:ident),+ $(,)?) => { - $(expose_declarative_provider!($module, stringify!($module));)+ - - pub(crate) fn fixed_provider_configs() -> anyhow::Result> { - fixed_provider_config_entries() - .into_iter() - .map(|(_, json)| deserialize_provider_config(json)) - .collect() - } - - pub(crate) fn fixed_provider_config_entries() -> Vec<(&'static str, &'static str)> { - vec![$((concat!(stringify!($module), ".json"), $module::JSON)),+] - } - }; -} diff --git a/crates/goose-provider-types/src/errors.rs b/crates/goose-providers/src/errors.rs similarity index 100% rename from crates/goose-provider-types/src/errors.rs rename to crates/goose-providers/src/errors.rs diff --git a/crates/goose-provider-types/src/formats.rs b/crates/goose-providers/src/formats.rs similarity index 57% rename from crates/goose-provider-types/src/formats.rs rename to crates/goose-providers/src/formats.rs index 03f7590961..5abaf4c3e9 100644 --- a/crates/goose-provider-types/src/formats.rs +++ b/crates/goose-providers/src/formats.rs @@ -1,8 +1,4 @@ pub mod anthropic; -pub mod databricks; -pub mod google; pub mod ollama; pub mod openai; pub mod openai_responses; - -pub mod snowflake; diff --git a/crates/goose-provider-types/src/formats/anthropic.rs b/crates/goose-providers/src/formats/anthropic.rs similarity index 97% rename from crates/goose-provider-types/src/formats/anthropic.rs rename to crates/goose-providers/src/formats/anthropic.rs index 5ceb5fe57c..acdce55cff 100644 --- a/crates/goose-provider-types/src/formats/anthropic.rs +++ b/crates/goose-providers/src/formats/anthropic.rs @@ -1,7 +1,7 @@ use crate::canonical::maybe_get_canonical_model; use crate::canonical::ThinkingMode; use crate::conversation::message::{Message, MessageContent}; -use crate::conversation::token_usage::{CostSource, ProviderUsage, Usage}; +use crate::conversation::token_usage::{ProviderUsage, Usage}; use crate::errors::ProviderError; use crate::images::{convert_image, ImageFormat}; use crate::mcp_utils::extract_text_from_resource; @@ -580,19 +580,6 @@ pub fn get_usage(data: &Value) -> Result { } } -fn provider_usage_with_cost( - model: String, - usage: Usage, - data: &Value, - fallback_cost: Option, -) -> ProviderUsage { - let provider_usage = ProviderUsage::new(model, usage); - match super::openai::get_cost(data).or(fallback_cost) { - Some(cost) => provider_usage.with_cost(cost, CostSource::ProviderReported), - None => provider_usage, - } -} - pub fn thinking_effort(model_config: &ModelConfig) -> ThinkingEffort { model_config .thinking_effort() @@ -631,8 +618,7 @@ pub fn thinking_budget_tokens(model_config: &ModelConfig) -> i32 { // Anthropic counts thinking tokens against max_tokens, so the budget must leave // room for a response. Clamp it to preserve at least this many answer tokens, and // drop thinking only when even a minimal budget wouldn't fit under the cap. -// Shared with the Bedrock formatter, which applies the same clamp. -pub const MIN_ANSWER_TOKENS: i32 = 1024; +const MIN_ANSWER_TOKENS: i32 = 1024; fn apply_thinking_config( payload: &mut Value, @@ -823,7 +809,7 @@ where .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); - final_usage = Some(provider_usage_with_cost(model, usage, usage_data, None)); + final_usage = Some(ProviderUsage::new(model, usage)); } } continue; @@ -957,18 +943,13 @@ where if let Some(existing_usage) = &final_usage { let merged_usage = merge_delta_usage(&existing_usage.usage, &delta_usage, usage_data); - final_usage = Some(provider_usage_with_cost( - existing_usage.model.clone(), - merged_usage, - usage_data, - existing_usage.cost, - )); + final_usage = Some(ProviderUsage::new(existing_usage.model.clone(), merged_usage)); } else { let model = event.data.get("model") .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); - final_usage = Some(provider_usage_with_cost(model, delta_usage, usage_data, None)); + final_usage = Some(ProviderUsage::new(model, delta_usage)); } } if let Some(delta) = event.data.get("delta") { @@ -1012,8 +993,7 @@ where .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(); - let fallback_cost = final_usage.as_ref().and_then(|u| u.cost); - final_usage = Some(provider_usage_with_cost(model, usage, usage_data, fallback_cost)); + final_usage = Some(ProviderUsage::new(model, usage)); } break; } @@ -1395,7 +1375,7 @@ mod tests { ("ANTHROPIC_PRESERVE_THINKING_CONTEXT", None::<&str>), ]); - let mut config = cfg_with_effort("claude-sonnet-4-5-20250929", "high"); + let mut config = cfg_with_effort("claude-3-7-sonnet-20250219", "high"); config.max_tokens = Some(64000); let messages = vec![Message::user().with_text("Hello")]; @@ -1417,7 +1397,7 @@ mod tests { ("ANTHROPIC_PRESERVE_THINKING_CONTEXT", None::<&str>), ]); - let mut config = cfg_with_effort("claude-sonnet-4-5-20250929", "high"); + let mut config = cfg_with_effort("claude-3-7-sonnet-20250219", "high"); let messages = vec![Message::user().with_text("Hello")]; // Budget larger than max_tokens is clamped to leave room for a response. @@ -1769,12 +1749,12 @@ mod tests { ); // Non-adaptive Claude with effort โ†’ enabled assert_eq!( - thinking_type(&cfg_with_effort("claude-sonnet-4-5-20250929", "high")), + thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "high")), ThinkingType::Enabled ); // Non-adaptive Claude with off โ†’ disabled assert_eq!( - thinking_type(&cfg_with_effort("claude-sonnet-4-5-20250929", "off")), + thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "off")), ThinkingType::Disabled ); } @@ -1833,7 +1813,7 @@ mod tests { ThinkingType::Disabled ); assert_eq!( - thinking_type(&cfg_with_effort("claude-sonnet-4-5-20250929", "off")), + thinking_type(&cfg_with_effort("claude-3-7-sonnet-20250219", "off")), ThinkingType::Disabled ); } @@ -2040,35 +2020,6 @@ mod tests { assert_eq!(usage.usage.cache_write_input_tokens, Some(10000)); } - #[tokio::test] - async fn test_streaming_preserves_provider_cost_from_delta() { - let events = concat!( - r#"data: {"type":"message_start","message":{"id":"m1","role":"assistant","content":[],"model":"glm-4.7","usage":{"input_tokens":100,"output_tokens":0}}}"#, - "\n", - r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#, - "\n", - r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}"#, - "\n", - r#"data: {"type":"content_block_stop","index":0}"#, - "\n", - r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":50,"cost":0.0123}}"#, - "\n", - r#"data: {"type":"message_stop"}"#, - ); - - let usage = collect_stream_results(events) - .await - .into_iter() - .filter_map(|r| r.ok().and_then(|(_, usage)| usage)) - .next_back() - .expect("stream should yield usage"); - - assert_eq!(usage.cost, Some(0.0123)); - assert_eq!(usage.cost_source, Some(CostSource::ProviderReported)); - assert_eq!(usage.usage.input_tokens, Some(100)); - assert_eq!(usage.usage.output_tokens, Some(50)); - } - #[tokio::test] async fn test_streaming_delta_usage_is_cumulative_and_wins() { // Server tool use grows input during the turn: the final diff --git a/crates/goose-provider-types/src/formats/ollama.rs b/crates/goose-providers/src/formats/ollama.rs similarity index 100% rename from crates/goose-provider-types/src/formats/ollama.rs rename to crates/goose-providers/src/formats/ollama.rs diff --git a/crates/goose-provider-types/src/formats/openai.rs b/crates/goose-providers/src/formats/openai.rs similarity index 93% rename from crates/goose-provider-types/src/formats/openai.rs rename to crates/goose-providers/src/formats/openai.rs index 5d46576e14..0dd54b7077 100644 --- a/crates/goose-provider-types/src/formats/openai.rs +++ b/crates/goose-providers/src/formats/openai.rs @@ -1,5 +1,5 @@ use crate::conversation::message::{Message, MessageContent, ProviderMetadata}; -use crate::conversation::token_usage::{CostSource, ProviderUsage, Usage}; +use crate::conversation::token_usage::{ProviderUsage, Usage}; use crate::errors::ProviderError; use crate::images::{convert_image, detect_image_path, load_image_file, ImageFormat}; use crate::json::{parse_tool_arguments, truncation_error_message}; @@ -138,18 +138,9 @@ struct StreamingChoice { #[serde(default)] delta: Delta, index: Option, - #[serde(default, deserialize_with = "empty_finish_reason_as_none")] finish_reason: Option, } -fn empty_finish_reason_as_none<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Option::::deserialize(deserializer)?; - Ok(value.filter(|reason| !reason.is_empty())) -} - #[derive(Serialize, Deserialize, Debug)] struct StreamingChunk { choices: Vec, @@ -248,7 +239,7 @@ pub fn format_messages_with_options( if !text.text.is_empty() { if message.role == Role::User { if let Some(image_path) = detect_image_path(&text.text) { - if let Ok(image) = load_image_file(image_path.as_ref()) { + if let Ok(image) = load_image_file(image_path) { has_non_text_content = true; content_array.push(json!({"type": "text", "text": text.text})); content_array.push(convert_image(&image, image_format)); @@ -718,13 +709,13 @@ pub fn response_to_message(response: &Value) -> anyhow::Result { }) .filter(|m: &serde_json::Map| !m.is_empty()); - if function_name.is_empty() { + if !is_valid_function_name(&function_name) { let error = ErrorData { code: ErrorCode::INVALID_REQUEST, - message: Cow::from( - "The provided function name was empty; a tool call must name a tool" - .to_string(), - ), + message: Cow::from(format!( + "The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+", + function_name + )), data: None, }; content.push(MessageContent::tool_request_with_metadata( @@ -732,50 +723,54 @@ pub fn response_to_message(response: &Value) -> anyhow::Result { Err(error), metadata.as_ref(), )); - continue; - } - match parse_tool_arguments(&arguments_str) { - Some(params) if params.is_object() => { - content.push(MessageContent::tool_request_with_metadata( - id, - Ok(CallToolRequestParams::new(function_name) - .with_arguments(object(params))), - metadata.as_ref(), - )); - } - Some(other) => { - let error = ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "Tool arguments for {} (id {}) must be a JSON object, got {}. Raw arguments: '{}'", - function_name, + } else { + match parse_tool_arguments(&arguments_str) { + Some(params) if params.is_object() => { + content.push(MessageContent::tool_request_with_metadata( id, - describe_json_value(&other), - arguments_str - )), - data: None, - }; - content.push(MessageContent::tool_request_with_metadata( - id, - Err(error), - metadata.as_ref(), - )); - } - None => { - let message_text = - truncation_error_message(&arguments_str).unwrap_or_else(|| { - format!("Could not interpret tool use parameters for id {id}") - }); - let error = ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(message_text), - data: None, - }; - content.push(MessageContent::tool_request_with_metadata( - id, - Err(error), - metadata.as_ref(), - )); + Ok(CallToolRequestParams::new(function_name) + .with_arguments(object(params))), + metadata.as_ref(), + )); + } + // Valid JSON but NOT an object (a bare array/string/number). + // Weaker models emit this; surface a tool error so the model + // retries with a proper object instead of crashing the run + // (rmcp's `object()` debug-asserts on non-objects). + Some(other) => { + let error = ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: Cow::from(format!( + "Tool arguments for {} (id {}) must be a JSON object, got {}. Raw arguments: '{}'", + function_name, + id, + describe_json_value(&other), + arguments_str + )), + data: None, + }; + content.push(MessageContent::tool_request_with_metadata( + id, + Err(error), + metadata.as_ref(), + )); + } + None => { + let message_text = truncation_error_message(&arguments_str) + .unwrap_or_else(|| { + format!("Could not interpret tool use parameters for id {id}") + }); + let error = ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: Cow::from(message_text), + data: None, + }; + content.push(MessageContent::tool_request_with_metadata( + id, + Err(error), + metadata.as_ref(), + )); + } } } } @@ -841,13 +836,6 @@ pub fn get_usage(usage: &Value) -> Usage { .with_cache_tokens(cache_read_input_tokens, cache_write_input_tokens) } -pub fn get_cost(usage: &Value) -> Option { - usage - .get("cost") - .and_then(|v| v.as_f64()) - .filter(|c| c.is_finite() && *c >= 0.0) -} - fn extract_usage_with_output_tokens( chunk: &StreamingChunk, fallback_model: Option<&str>, @@ -856,13 +844,11 @@ fn extract_usage_with_output_tokens( .usage .as_ref() .and_then(|u| { - chunk.model.as_deref().or(fallback_model).map(|model| { - let usage = ProviderUsage::new(model.to_string(), get_usage(u)); - match get_cost(u) { - Some(cost) => usage.with_cost(cost, CostSource::ProviderReported), - None => usage, - } - }) + chunk + .model + .as_deref() + .or(fallback_model) + .map(|model| ProviderUsage::new(model.to_string(), get_usage(u))) }) .filter(|u| u.usage.output_tokens.is_some()) } @@ -1515,10 +1501,13 @@ pub fn openai_reasoning_effort_for_thinking( model_name: &str, effort: ThinkingEffort, ) -> Option { - let supported = openai_reasoning_efforts_for_model(model_name); + if effort == ThinkingEffort::Off { + return Some("none".to_string()); + } + let supported = openai_reasoning_efforts_for_model(model_name); let preferred: &[&str] = match effort { - ThinkingEffort::Off => &["none", "low"], + ThinkingEffort::Off => unreachable!(), ThinkingEffort::Low => &["low", "medium", "high", "xhigh"], ThinkingEffort::Medium => &["medium", "high", "low", "xhigh"], ThinkingEffort::High => &["high", "medium", "xhigh", "low"], @@ -1531,7 +1520,7 @@ pub fn openai_reasoning_effort_for_thinking( .map(|level| (*level).to_string()) } -pub(crate) fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [&'static str] { +fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [&'static str] { let normalized = model_name.to_ascii_lowercase(); if normalized.contains("gpt-5") { @@ -1541,10 +1530,8 @@ pub(crate) fn openai_reasoning_efforts_for_model(model_name: &str) -> &'static [ || normalized.contains("gpt-5-4") || normalized.contains("gpt-5.5") || normalized.contains("gpt-5-5") - || normalized.contains("gpt-5.6") - || normalized.contains("gpt-5-6") { - &["none", "low", "medium", "high", "xhigh"] + &["low", "medium", "high", "xhigh"] } else { &["low", "medium", "high"] } @@ -2016,9 +2003,10 @@ mod tests { } #[test] - fn test_response_to_message_empty_func_name() -> anyhow::Result<()> { + fn test_response_to_message_invalid_func_name() -> anyhow::Result<()> { let mut response: Value = serde_json::from_str(OPENAI_TOOL_USE_RESPONSE)?; - response["choices"][0]["message"]["tool_calls"][0]["function"]["name"] = json!(""); + response["choices"][0]["message"]["tool_calls"][0]["function"]["name"] = + json!("invalid fn"); let message = response_to_message(&response)?; @@ -2031,7 +2019,7 @@ mod tests { }) => { assert!(msg.starts_with("The provided function name")); } - _ => panic!("Expected invalid-request error for empty name"), + _ => panic!("Expected ToolNotFound error"), } } else { panic!("Expected ToolRequest content"); @@ -2040,48 +2028,6 @@ mod tests { Ok(()) } - #[test] - fn test_response_to_message_passes_names_through_to_dispatch() -> anyhow::Result<()> { - for name in [ - "developer.shell", - "functions.example_fn", - "example fn", - "???!", - ] { - let mut response: Value = serde_json::from_str(OPENAI_TOOL_USE_RESPONSE)?; - response["choices"][0]["message"]["tool_calls"][0]["function"]["name"] = json!(name); - - let message = response_to_message(&response)?; - - if let MessageContent::ToolRequest(request) = &message.content[0] { - let tool_call = request.tool_call.as_ref().expect("tool call should parse"); - assert_eq!(tool_call.name, name, "name must pass through verbatim"); - } else { - panic!("Expected ToolRequest content"); - } - } - - Ok(()) - } - - #[test] - fn test_response_to_message_fenced_arguments() -> anyhow::Result<()> { - let mut response: Value = serde_json::from_str(OPENAI_TOOL_USE_RESPONSE)?; - response["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"] = - json!("```json\n{\"param\": \"value\"}\n```"); - - let message = response_to_message(&response)?; - - if let MessageContent::ToolRequest(request) = &message.content[0] { - let tool_call = request.tool_call.as_ref().expect("tool call should parse"); - assert_eq!(tool_call.arguments, Some(object!({"param": "value"}))); - } else { - panic!("Expected ToolRequest content"); - } - - Ok(()) - } - #[test] fn test_response_to_message_json_decode_error() -> anyhow::Result<()> { let mut response: Value = serde_json::from_str(OPENAI_TOOL_USE_RESPONSE)?; @@ -2500,10 +2446,10 @@ mod tests { } #[test] - fn test_create_request_gpt56_max_effort_uses_xhigh() -> anyhow::Result<()> { - let model_config = test_model_config("gpt-5.6-luna") + fn test_create_request_o3_off_effort_preserves_none() -> anyhow::Result<()> { + let model_config = test_model_config("o3") .with_max_tokens(Some(1024)) - .with_thinking_effort(ThinkingEffort::Max); + .with_thinking_effort(ThinkingEffort::Off); let request = create_request( &model_config, "system", @@ -2514,7 +2460,7 @@ mod tests { )?; let obj = request.as_object().unwrap(); - assert_eq!(obj.get("reasoning_effort"), Some(&json!("xhigh"))); + assert_eq!(obj.get("reasoning_effort"), Some(&json!("none"))); assert!(obj.get("thinking_effort").is_none()); Ok(()) @@ -2737,30 +2683,6 @@ data: [DONE] Ok(()) } - #[tokio::test] - async fn test_streaming_empty_finish_reason_is_not_terminal() -> anyhow::Result<()> { - let response_lines = r#" -data: {"model":"m","choices":[{"delta":{"role":"assistant","content":"Checking."},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340} -data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"developer__shell","arguments":""}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340} -data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"command\""}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340} -data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"function":{"arguments":": \"ls\"}"}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340} -data: {"model":"m","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":100,"completion_tokens":20,"total_tokens":120},"object":"chat.completion.chunk","id":"1","created":1753288340} -data: [DONE] -"#; - - let result = run_streaming_test(response_lines).await?; - - assert!(result.has_text_content, "Expected text content in response"); - assert_eq!( - result.tool_calls, - vec!["developer__shell"], - "tool call must survive intermediate empty-string finish_reason" - ); - assert_usage_yielded_once(&result, 100, 20, 120); - - Ok(()) - } - #[tokio::test] async fn test_openrouter_streaming_usage_yielded_once() -> anyhow::Result<()> { let response_lines = r#" @@ -3894,68 +3816,6 @@ data: [DONE]"#; Ok(()) } - #[tokio::test] - async fn test_streaming_tool_call_dotted_name_passes_through() -> anyhow::Result<()> { - let response_lines = concat!( - "data: {\"id\":\"x\",\"object\":\"chat.completion.chunk\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"tc1\",\"type\":\"function\",\"function\":{\"name\":\"ext__db.query\",\"arguments\":\"{\\\"command\\\": \\\"ls\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n", - "data: [DONE]" - ); - let lines: Vec = response_lines.lines().map(|s| s.to_string()).collect(); - let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); - let mut messages = std::pin::pin!(response_to_streaming_message(response_stream)); - - let mut tool_calls = Vec::new(); - while let Some(result) = messages.next().await { - let (message, _usage) = result?; - if let Some(msg) = message { - for content in &msg.content { - if let MessageContent::ToolRequest(request) = content { - let tool_call = request.tool_call.as_ref().expect("tool call should parse"); - tool_calls.push((tool_call.name.to_string(), tool_call.arguments.clone())); - } - } - } - } - - assert_eq!(tool_calls.len(), 1); - assert_eq!(tool_calls[0].0, "ext__db.query"); - assert_eq!(tool_calls[0].1, Some(object!({"command": "ls"}))); - Ok(()) - } - - #[tokio::test] - async fn test_streaming_tool_call_degenerate_name_passes_through() -> anyhow::Result<()> { - let response_lines = concat!( - "data: {\"id\":\"x\",\"object\":\"chat.completion.chunk\",\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"tc1\",\"type\":\"function\",\"function\":{\"name\":\"???\",\"arguments\":\"{}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n", - "data: [DONE]" - ); - let lines: Vec = response_lines.lines().map(|s| s.to_string()).collect(); - let response_stream = tokio_stream::iter(lines.into_iter().map(Ok)); - let mut messages = std::pin::pin!(response_to_streaming_message(response_stream)); - - let mut names = Vec::new(); - while let Some(result) = messages.next().await { - let (message, _usage) = result?; - if let Some(msg) = message { - for content in &msg.content { - if let MessageContent::ToolRequest(request) = content { - names.push( - request - .tool_call - .as_ref() - .expect("passes through") - .name - .to_string(), - ); - } - } - } - } - - assert_eq!(names, vec!["???".to_string()]); - Ok(()) - } - #[tokio::test] async fn test_streaming_multiple_tool_calls_without_tool_call_index() -> anyhow::Result<()> { let response_lines = concat!( diff --git a/crates/goose-provider-types/src/formats/openai_responses.rs b/crates/goose-providers/src/formats/openai_responses.rs similarity index 84% rename from crates/goose-provider-types/src/formats/openai_responses.rs rename to crates/goose-providers/src/formats/openai_responses.rs index f8e0aadd03..54cbc0b09c 100644 --- a/crates/goose-provider-types/src/formats/openai_responses.rs +++ b/crates/goose-providers/src/formats/openai_responses.rs @@ -3,11 +3,10 @@ use crate::conversation::token_usage::{ProviderUsage, Usage}; use crate::errors::ProviderError; use crate::formats::openai::{ extract_reasoning_effort, is_openai_responses_model, openai_reasoning_effort_for_thinking, - sanitize_function_name, }; use crate::mcp_utils::extract_text_from_resource; use crate::model::ModelConfig; -use anyhow::{anyhow, Error}; +use anyhow::Error; use async_stream::try_stream; use chrono; use futures::Stream; @@ -318,24 +317,19 @@ pub struct ResponseMetadata { #[serde(rename_all = "snake_case")] pub enum ResponseOutputItemInfo { Reasoning { - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, + id: String, #[serde(default)] summary: Vec, }, Message { - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - status: Option, + id: String, + status: String, role: String, content: Vec, }, FunctionCall { - #[serde(default, skip_serializing_if = "Option::is_none")] - id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - status: Option, + id: String, + status: String, #[serde(skip_serializing_if = "Option::is_none")] call_id: Option, name: String, @@ -397,7 +391,6 @@ fn add_message_items(input_items: &mut Vec, messages: &[Message]) { match &request.tool_call { Ok(tool_call) => { - let sanitized_name = sanitize_function_name(&tool_call.name); let arguments_str = tool_call .arguments .as_ref() @@ -414,7 +407,7 @@ fn add_message_items(input_items: &mut Vec, messages: &[Message]) { input_items.push(json!({ "type": "function_call", "call_id": request.id, - "name": sanitized_name, + "name": tool_call.name, "arguments": arguments_str })); } @@ -525,7 +518,6 @@ fn add_message_items(input_items: &mut Vec, messages: &[Message]) { match &request.tool_call { Ok(tool_call) => { - let sanitized_name = sanitize_function_name(&tool_call.name); let arguments_str = tool_call .arguments .as_ref() @@ -537,7 +529,7 @@ fn add_message_items(input_items: &mut Vec, messages: &[Message]) { input_items.push(json!({ "type": "function_call", "call_id": request.id, - "name": sanitized_name, + "name": tool_call.name, "arguments": arguments_str })); } @@ -563,14 +555,6 @@ fn add_message_items(input_items: &mut Vec, messages: &[Message]) { } } -fn is_gpt_5_6_model(model_name: &str) -> bool { - let normalized = model_name.to_ascii_lowercase(); - normalized == "gpt-5.6" - || normalized.starts_with("gpt-5.6-") - || normalized == "gpt-5-6" - || normalized.starts_with("gpt-5-6-") -} - pub fn create_responses_request( model_config: &ModelConfig, system: &str, @@ -598,15 +582,11 @@ pub fn create_responses_request( let is_reasoning_model = is_openai_responses_model(&model_name); let reasoning_effort = if is_reasoning_model { if let Some(effort) = legacy_reasoning_effort.as_deref() { - if effort.eq_ignore_ascii_case("none") { - legacy_reasoning_effort - } else { - effort - .parse() - .ok() - .and_then(|effort| openai_reasoning_effort_for_thinking(&model_name, effort)) - .or(legacy_reasoning_effort) - } + effort + .parse() + .ok() + .and_then(|effort| openai_reasoning_effort_for_thinking(&model_name, effort)) + .or(legacy_reasoning_effort) } else { model_config .thinking_effort() @@ -616,44 +596,20 @@ pub fn create_responses_request( None }; - let store = model_config.request_param::("store").unwrap_or(false); - let reasoning_mode = model_config - .request_param::("reasoning_mode") - .map(|mode| { - let normalized = mode.to_ascii_lowercase(); - match normalized.as_str() { - "standard" | "pro" => Ok(normalized), - _ => Err(anyhow!( - "Invalid reasoning_mode '{}'. Supported values are: standard, pro", - mode - )), - } - }) - .transpose()?; - if reasoning_mode.is_some() && !is_gpt_5_6_model(&model_name) { - return Err(anyhow!( - "reasoning_mode is only supported for GPT-5.6 models" - )); - } let mut payload = json!({ "model": model_name, "input": input_items, - "store": store, + "store": false, }); - if reasoning_effort.is_some() || reasoning_mode.is_some() { - let mut reasoning = serde_json::Map::new(); - if let Some(effort) = reasoning_effort { - reasoning.insert("effort".to_string(), json!(effort)); - reasoning.insert("summary".to_string(), json!("auto")); - } - if let Some(mode) = reasoning_mode { - reasoning.insert("mode".to_string(), json!(mode)); - } - payload - .as_object_mut() - .unwrap() - .insert("reasoning".to_string(), Value::Object(reasoning)); + if let Some(effort) = reasoning_effort { + payload.as_object_mut().unwrap().insert( + "reasoning".to_string(), + json!({ + "effort": effort, + "summary": "auto", + }), + ); } if !tools.is_empty() { @@ -685,12 +641,10 @@ pub fn create_responses_request( } } - if let Some(max_tokens) = model_config.max_tokens { - payload - .as_object_mut() - .unwrap() - .insert("max_output_tokens".to_string(), json!(max_tokens)); - } + payload.as_object_mut().unwrap().insert( + "max_output_tokens".to_string(), + json!(model_config.max_output_tokens()), + ); Ok(payload) } @@ -736,9 +690,7 @@ pub fn responses_api_to_message(response: &ResponsesApiResponse) -> anyhow::Resu arguments, .. } => { - let request_id = call_id.clone().or_else(|| id.clone()).ok_or_else(|| { - anyhow!("Responses function_call output missing call_id and id") - })?; + let request_id = call_id.clone().or_else(|| id.clone()).unwrap_or_default(); let parsed_args = if arguments.is_empty() { json!({}) } else { @@ -771,7 +723,7 @@ pub fn get_responses_usage(response: &ResponsesApiResponse) -> Usage { fn process_streaming_output_items( output_items: Vec, is_text_response: bool, -) -> anyhow::Result> { +) -> Vec { let mut content = Vec::new(); for item in output_items { @@ -819,9 +771,7 @@ fn process_streaming_output_items( arguments, .. } => { - let request_id = call_id.or(id).ok_or_else(|| { - anyhow!("Responses function_call output missing call_id and id") - })?; + let request_id = call_id.unwrap_or(id); let parsed_args = if arguments.is_empty() { json!({}) } else { @@ -836,7 +786,7 @@ fn process_streaming_output_items( } } - Ok(content) + content } pub fn responses_api_to_streaming_message( @@ -994,7 +944,7 @@ where } // Process final output items and yield usage data - let content = process_streaming_output_items(output_items, is_text_response)?; + let content = process_streaming_output_items(output_items, is_text_response); if !content.is_empty() { let mut message = Message::new(Role::Assistant, chrono::Utc::now().timestamp(), content); @@ -1102,84 +1052,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_responses_stream_allows_message_output_without_id_status() -> 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","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello world"}]}],"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(()) - } - - #[tokio::test] - async fn test_responses_stream_allows_function_call_without_id_status() -> 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.completed","sequence_number":2,"response":{"id":"resp_1","object":"response","created_at":1737368310,"status":"completed","model":"gpt-5.2-pro","output":[{"type":"reasoning","summary":[]},{"type":"function_call","call_id":"call_abc","name":"shell","arguments":"{\"command\":\"pwd\"}"}],"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 tool_request_id = None; - 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::ToolRequest(request) = content { - tool_request_id = Some(request.id); - } - } - } - if let Some(final_usage) = maybe_usage { - usage = Some(final_usage); - } - } - - assert_eq!(tool_request_id.as_deref(), Some("call_abc")); - let usage = usage.expect("usage should be present at completion"); - assert_eq!(usage.model, "gpt-5.2-pro"); - 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!({ @@ -1492,46 +1364,6 @@ mod tests { assert_eq!(result["reasoning"]["summary"], "auto"); } - #[test] - fn test_responses_request_supports_gpt_5_6_xhigh_effort() { - let model_config = ModelConfig::new("gpt-5.6-sol-xhigh"); - - let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap(); - - assert_eq!(result["model"], "gpt-5.6-sol"); - assert_eq!(result["reasoning"]["effort"], "xhigh"); - assert_eq!(result["reasoning"]["summary"], "auto"); - } - - #[test] - fn test_responses_request_supports_gpt_5_6_reasoning_mode() { - let model_config = ModelConfig::new("gpt-5.6-sol").with_merged_request_params( - std::collections::HashMap::from([("reasoning_mode".to_string(), json!("pro"))]), - ); - - let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap(); - - assert_eq!(result["reasoning"]["mode"], "pro"); - assert!(result["reasoning"].get("effort").is_none()); - assert!(result["reasoning"].get("summary").is_none()); - } - - #[test] - fn test_responses_request_rejects_reasoning_mode_for_non_gpt_5_6_model() { - for model_name in ["gpt-5.5", "gpt-5.60"] { - let model_config = ModelConfig::new(model_name).with_merged_request_params( - std::collections::HashMap::from([("reasoning_mode".to_string(), json!("pro"))]), - ); - - let error = create_responses_request(&model_config, "You are helpful.", &[], &[]) - .expect_err("reasoning mode should be gated to GPT-5.6 models"); - - assert!(error - .to_string() - .contains("reasoning_mode is only supported for GPT-5.6 models")); - } - } - #[test] fn test_responses_request_without_effort_suffix_omits_reasoning() { for model_name in ["gpt-5.4", "o3", "gpt-5-nano"] { @@ -1557,31 +1389,6 @@ mod tests { } } - #[test] - fn test_responses_request_omits_default_max_output_tokens_for_unknown_model() { - let model_config = ModelConfig::new("gpt-5.6-sol"); - - let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap(); - - assert_eq!(result["model"], "gpt-5.6-sol"); - assert!( - result.get("max_output_tokens").is_none(), - "unknown/new models should not receive Goose's fallback max_output_tokens" - ); - } - - #[test] - fn test_responses_request_includes_canonical_max_output_tokens() { - let model_config = ModelConfig::new("gpt-5.4").with_canonical_limits("openai"); - - let result = create_responses_request(&model_config, "You are helpful.", &[], &[]).unwrap(); - - assert_eq!( - result["max_output_tokens"], - model_config.max_tokens.unwrap() - ); - } - #[test] fn test_responses_request_non_reasoning_model_ignores_global_thinking_effort() { let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", Some("high"))]); @@ -1605,27 +1412,6 @@ mod tests { ); } - #[test] - fn test_request_params_override_store() { - let model_config = ModelConfig { - model_name: "o3".to_string(), - context_limit: None, - temperature: None, - max_tokens: None, - toolshim: false, - toolshim_model: None, - request_params: Some(std::collections::HashMap::from([( - "store".to_string(), - serde_json::json!(true), - )])), - reasoning: None, - }; - - let result = create_responses_request(&model_config, "", &[], &[]).unwrap(); - - assert_eq!(result["store"], true); - } - #[test] fn test_user_image_serialized_in_responses_request() { use crate::conversation::message::Message; @@ -1981,7 +1767,7 @@ mod tests { } #[test] - fn test_refusal_content_part_deserializes_in_streaming_output() -> anyhow::Result<()> { + fn test_refusal_content_part_deserializes_in_streaming_output() { let json = r#"{ "type": "message", "id": "msg_1", @@ -1991,15 +1777,13 @@ mod tests { }"#; let item: ResponseOutputItemInfo = serde_json::from_str(json).unwrap(); - let content = process_streaming_output_items(vec![item], false)?; + let content = process_streaming_output_items(vec![item], false); assert_eq!(content.len(), 1); if let MessageContent::Text(t) = &content[0] { assert_eq!(t.text, "I'm unable to assist."); } else { panic!("expected text content from refusal"); } - - Ok(()) } #[test] @@ -2016,47 +1800,28 @@ mod tests { } #[test] - fn test_streamed_refusal_not_duplicated_in_output_items() -> anyhow::Result<()> { + fn test_streamed_refusal_not_duplicated_in_output_items() { let output_items = vec![ResponseOutputItemInfo::Message { - id: Some("msg_1".to_string()), - status: Some("completed".to_string()), + id: "msg_1".to_string(), + status: "completed".to_string(), role: "assistant".to_string(), content: vec![ContentPart::Refusal { refusal: "I cannot help with that.".to_string(), }], }]; - let content = process_streaming_output_items(output_items.clone(), true)?; + let content = process_streaming_output_items(output_items.clone(), true); assert!( content.is_empty(), "refusal should be suppressed when already streamed" ); - let content = process_streaming_output_items(output_items, false)?; + let content = process_streaming_output_items(output_items, false); assert_eq!( content.len(), 1, "refusal should appear in non-streaming path" ); - - Ok(()) - } - - #[test] - fn test_function_call_output_requires_call_id_or_id() { - let output_items = vec![ResponseOutputItemInfo::FunctionCall { - id: None, - status: None, - call_id: None, - name: "shell".to_string(), - arguments: "{}".to_string(), - }]; - - let error = process_streaming_output_items(output_items, false).unwrap_err(); - assert!( - error.to_string().contains("missing call_id and id"), - "unexpected error: {error}" - ); } #[test] @@ -2099,46 +1864,6 @@ mod tests { assert_eq!(input[1]["output"], "clicked"); } - #[test] - fn test_responses_request_sanitizes_replayed_function_call_names() { - use crate::conversation::message::Message; - - let messages = vec![ - Message::assistant().with_tool_request( - "call_agent", - Ok(CallToolRequestParams::new("Crack Catcher") - .with_arguments(object!({"prompt": "verify the work"}))), - ), - Message::assistant().with_frontend_tool_request( - "call_frontend_agent", - Ok(CallToolRequestParams::new("@Review Agent") - .with_arguments(object!({"prompt": "check it"}))), - ), - ]; - - let model_config = ModelConfig { - model_name: "gpt-5.5".to_string(), - context_limit: None, - temperature: None, - max_tokens: None, - toolshim: false, - toolshim_model: None, - request_params: None, - reasoning: None, - }; - - let result = create_responses_request(&model_config, "", &messages, &[]).unwrap(); - let input = result["input"].as_array().unwrap(); - - assert_eq!(input[0]["type"], "function_call"); - assert_eq!(input[0]["call_id"], "call_agent"); - assert_eq!(input[0]["name"], "Crack_Catcher"); - - assert_eq!(input[1]["type"], "function_call"); - assert_eq!(input[1]["call_id"], "call_frontend_agent"); - assert_eq!(input[1]["name"], "_Review_Agent"); - } - #[test] fn test_tool_request_error_emits_function_call_output() { use crate::conversation::message::Message; diff --git a/crates/goose-provider-types/src/goose_mode.rs b/crates/goose-providers/src/goose_mode.rs similarity index 100% rename from crates/goose-provider-types/src/goose_mode.rs rename to crates/goose-providers/src/goose_mode.rs diff --git a/crates/goose-provider-types/src/images.rs b/crates/goose-providers/src/images.rs similarity index 63% rename from crates/goose-provider-types/src/images.rs rename to crates/goose-providers/src/images.rs index 5c27bd710b..1f194862fc 100644 --- a/crates/goose-provider-types/src/images.rs +++ b/crates/goose-providers/src/images.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, io::Read as _, path::Path}; +use std::{io::Read as _, path::Path}; use base64::Engine as _; use rmcp::model::{AnnotateAble as _, ImageContent, RawImageContent}; @@ -33,11 +33,11 @@ pub fn convert_image(image: &ImageContent, image_format: &ImageFormat) -> Value } } -pub fn detect_image_path(text: &str) -> Option> { +pub fn detect_image_path(text: &str) -> Option<&str> { const EXTENSIONS: [&str; 3] = [".png", ".jpg", ".jpeg"]; const MAX_PATH_LEN: usize = 4096; - let mut best: Option<(usize, Cow<'_, str>)> = None; + let mut best: Option<(usize, &str)> = None; let mut from = 0; while from < text.len() { let Some(end) = EXTENSIONS @@ -49,7 +49,8 @@ pub fn detect_image_path(text: &str) -> Option> { }; let terminator = text.get(end..).and_then(|rest| rest.chars().next()); - let terminated = terminator.is_none_or(is_path_terminator); + let terminated = + terminator.is_none_or(|c| c == '/' || c.is_whitespace() || c == '"' || c == '\''); if terminated { let mut floor = end.saturating_sub(MAX_PATH_LEN); @@ -62,23 +63,24 @@ pub fn detect_image_path(text: &str) -> Option> { let preceded_by_boundary = text .get(..start) .and_then(|prefix| prefix.chars().next_back()) - .is_none_or(is_path_leading_boundary); + .is_none_or(|c| c.is_whitespace() || c == '"' || c == '\''); if !preceded_by_boundary { continue; } let Some(candidate) = text.get(start..end) else { continue; }; - if let Some(candidate_path) = image_path_candidate(candidate) { + let path = Path::new(candidate); + if path.is_absolute() && path.is_file() && is_image_file(path) { // Keep the first referenced path, but allow a longer // match anchored at the same start to extend it (a // whitespace-terminated extension may be a prefix of a // spaced filename ending in a later extension). match best { Some((best_start, _)) if start == best_start => { - best = Some((start, candidate_path)); + best = Some((start, candidate)); } - None => best = Some((start, candidate_path)), + None => best = Some((start, candidate)), Some(_) => {} } break; @@ -91,78 +93,6 @@ pub fn detect_image_path(text: &str) -> Option> { best.map(|(_, candidate)| candidate) } -fn clean_path(path: &str) -> Cow<'_, str> { - if !path.contains('\\') { - return Cow::Borrowed(path); - } - - let mut cleaned = String::with_capacity(path.len()); - let mut chars = path.chars().peekable(); - let mut changed = false; - - while let Some(c) = chars.next() { - if c == '\\' { - if let Some(&next) = chars.peek() { - if !next.is_alphanumeric() { - cleaned.push(next); - chars.next(); - changed = true; - continue; - } - } - } - cleaned.push(c); - } - - if changed { - Cow::Owned(cleaned) - } else { - Cow::Borrowed(path) - } -} - -fn image_path_candidate(candidate: &str) -> Option> { - if is_existing_image_path(candidate) { - return Some(Cow::Borrowed(candidate)); - } - - let cleaned = clean_path(candidate); - if cleaned.as_ref() != candidate && is_existing_image_path(cleaned.as_ref()) { - return Some(cleaned); - } - - None -} - -fn is_existing_image_path(candidate: &str) -> bool { - let path = Path::new(candidate); - path.is_absolute() && path.is_file() && is_image_file(path) -} - -fn is_path_leading_boundary(c: char) -> bool { - c.is_whitespace() - || matches!( - c, - '"' | '\'' | '\u{00AB}' | '\u{00BB}' | '\u{2018}' - ..='\u{201F}' | '\u{2039}' | '\u{203A}' - ) -} - -fn is_path_terminator(c: char) -> bool { - c == '/' - || c.is_whitespace() - || matches!( - c, - '"' | '\'' | '\u{00AB}' | '\u{00BB}' | '\u{2013}' - ..='\u{201F}' | '\u{2026}' | '\u{2039}' | '\u{203A}' - ) - || ('\u{2300}'..='\u{23FF}').contains(&c) - || ('\u{2600}'..='\u{27BF}').contains(&c) - || ('\u{2B00}'..='\u{2BFF}').contains(&c) - || ('\u{1F1E6}'..='\u{1F1FF}').contains(&c) - || ('\u{1F300}'..='\u{1FAFF}').contains(&c) -} - /// Case-insensitive ASCII substring search returning a byte index into /// `haystack` (no allocation, so the index stays valid for slicing). fn find_ascii_ci(haystack: &str, needle: &str, from: usize) -> Option { @@ -266,23 +196,23 @@ mod tests { // Test with valid PNG file using absolute path let text = format!("Here is an image {}", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + assert_eq!(detect_image_path(&text), Some(png_path_str)); // Test with non-image file that has .png extension let text = format!("Here is a fake image {}", fake_png_path.to_str().unwrap()); - assert_eq!(detect_image_path(&text).as_deref(), None); + assert_eq!(detect_image_path(&text), None); // Test with nonexistent file let text = "Here is a fake.png that doesn't exist"; - assert_eq!(detect_image_path(text).as_deref(), None); + assert_eq!(detect_image_path(text), None); // Test with non-image file let text = "Here is a file.txt"; - assert_eq!(detect_image_path(text).as_deref(), None); + assert_eq!(detect_image_path(text), None); // Test with relative path (should not match) let text = "Here is a relative/path/image.png"; - assert_eq!(detect_image_path(text).as_deref(), None); + assert_eq!(detect_image_path(text), None); } #[test] @@ -295,29 +225,25 @@ mod tests { let png_path_str = png_path.to_str().unwrap(); let text = format!("please describe {} for me", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + assert_eq!(detect_image_path(&text), Some(png_path_str)); // Case-insensitive extension also matches. let upper = temp_dir.path().join("Another Shot.PNG"); std::fs::write(&upper, png_data).unwrap(); let upper_str = upper.to_str().unwrap(); let text = format!("see {}", upper_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(upper_str)); + assert_eq!(detect_image_path(&text), Some(upper_str)); // Quoted path with spaces: the closing quote terminates the candidate. let text = format!("describe \"{}\" please", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + assert_eq!(detect_image_path(&text), Some(png_path_str)); let text = format!("describe '{}'", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); - let text = format!("describe โ€œ{}โ€ please", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); - let text = format!("describe ยซ{}ยป please", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + assert_eq!(detect_image_path(&text), Some(png_path_str)); // A stray closing quote in prose must not act as a terminator for an // unquoted path. let text = format!("here {}\" trailing", png_path_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); + assert_eq!(detect_image_path(&text), Some(png_path_str)); // When a spaced filename contains an earlier image extension, prefer // the longer existing candidate over the embedded prefix. @@ -327,7 +253,7 @@ mod tests { let prefix = temp_dir.path().join("Screen Shot.png"); std::fs::write(&prefix, png_data).unwrap(); let text = format!("look at {}", edited_str); - assert_eq!(detect_image_path(&text).as_deref(), Some(edited_str)); + assert_eq!(detect_image_path(&text), Some(edited_str)); // With multiple distinct images, the first referenced one wins even if // a later one has a longer path. @@ -340,93 +266,7 @@ mod tests { a.to_str().unwrap(), longer.to_str().unwrap() ); - assert_eq!( - detect_image_path(&text).as_deref(), - Some(a.to_str().unwrap()) - ); - } - - #[test] - fn test_detect_image_path_with_shell_escaped_metacharacters() { - let temp_dir = tempfile::tempdir().unwrap(); - let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; - let png_path = temp_dir - .path() - .join("Bob's Project (v2) & $draft [final].png"); - std::fs::write(&png_path, png_data).unwrap(); - let png_path_str = png_path.to_str().unwrap(); - - let escaped_path = png_path_str - .replace(' ', "\\ ") - .replace('(', "\\(") - .replace(')', "\\)") - .replace('&', "\\&") - .replace('$', "\\$") - .replace('\'', "\\'") - .replace('[', "\\[") - .replace(']', "\\]"); - let text = format!("please describe {}", escaped_path); - - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); - } - - #[test] - fn test_detect_image_path_prefers_existing_literal_backslash_path() { - let temp_dir = tempfile::tempdir().unwrap(); - let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; - let png_path = temp_dir.path().join("literal\\&name.png"); - std::fs::write(&png_path, png_data).unwrap(); - let png_path_str = png_path.to_str().unwrap(); - let text = format!("please describe {}", png_path_str); - - assert_eq!(detect_image_path(&text).as_deref(), Some(png_path_str)); - } - - #[test] - fn test_detect_image_path_with_unicode_separators() { - let temp_dir = tempfile::tempdir().unwrap(); - let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; - let png_path = temp_dir.path().join("photo.png"); - std::fs::write(&png_path, png_data).unwrap(); - let png_path_str = png_path.to_str().unwrap(); - - assert_eq!( - detect_image_path(&format!("{png_path_str}๐Ÿ™‚")).as_deref(), - Some(png_path_str) - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}๐Ÿ‡บ๐Ÿ‡ธ")).as_deref(), - Some(png_path_str) - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}โŒš")).as_deref(), - Some(png_path_str) - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}โญ")).as_deref(), - Some(png_path_str) - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}โ€ฆ more text")).as_deref(), - Some(png_path_str) - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}\u{2014}more text")).as_deref(), - Some(png_path_str) - ); - - assert_eq!( - detect_image_path(&format!("{png_path_str}\u{200B}.backup")).as_deref(), - None - ); - assert_eq!( - detect_image_path(&format!("{png_path_str}\u{0301}")).as_deref(), - None - ); - assert_eq!( - detect_image_path(&format!("file:{png_path_str}")).as_deref(), - None - ); + assert_eq!(detect_image_path(&text), Some(a.to_str().unwrap())); } #[test] @@ -440,14 +280,14 @@ mod tests { let png_path = temp_dir.path().join("photo.png"); std::fs::write(&png_path, png_data).unwrap(); let url = format!("https:/{}/photo.png", dir); - assert_eq!(detect_image_path(&url).as_deref(), None); + assert_eq!(detect_image_path(&url), None); // A backup file sharing the image extension prefix must not be // truncated to the bare image path. let real = temp_dir.path().join("shot.png"); std::fs::write(&real, png_data).unwrap(); let backup = format!("{}.backup", real.to_str().unwrap()); - assert_eq!(detect_image_path(&backup).as_deref(), None); + assert_eq!(detect_image_path(&backup), None); } #[test] @@ -455,7 +295,7 @@ mod tests { // Many extension-like tokens but no real absolute path: must scan // cheaply (bounded) and find nothing. let text = "see foo.png and bar.jpg and baz.jpeg ".repeat(500); - assert_eq!(detect_image_path(&text).as_deref(), None); + assert_eq!(detect_image_path(&text), None); } #[test] diff --git a/crates/goose-provider-types/src/json.rs b/crates/goose-providers/src/json.rs similarity index 74% rename from crates/goose-provider-types/src/json.rs rename to crates/goose-providers/src/json.rs index 93ba4cc31d..c1a4baabd9 100644 --- a/crates/goose-provider-types/src/json.rs +++ b/crates/goose-providers/src/json.rs @@ -53,8 +53,10 @@ fn repair_truncated_json(s: &str) -> String { '"' => in_string = true, '{' => closers.push('}'), '[' => closers.push(']'), - '}' | ']' if closers.last() == Some(&c) => { - closers.pop(); + '}' | ']' => { + if closers.last() == Some(&c) { + closers.pop(); + } } _ => {} } @@ -213,56 +215,6 @@ pub fn truncation_error_message(args: &str) -> Option { )) } -fn strip_json_wrapper(args: &str) -> Option<&str> { - let trimmed = args.trim(); - - if let Some(rest) = trimmed.strip_prefix("```") { - let body = rest.strip_suffix("```")?.trim_start(); - let body = body - .strip_prefix("json") - .or_else(|| body.strip_prefix("JSON")) - .unwrap_or(body); - return Some(body.trim()); - } - - if trimmed.starts_with('<') && !trimmed.starts_with("')?; - let tag_name = trimmed.get(1..open_end)?.split_whitespace().next()?; - if tag_name.is_empty() - || !tag_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') - { - return None; - } - let closing = format!(""); - let body = trimmed - .get(open_end + 1..)? - .trim_end() - .strip_suffix(&closing)?; - return Some(body.trim()); - } - - None -} - -fn unwrap_double_encoded_object(value: serde_json::Value) -> serde_json::Value { - let serde_json::Value::String(s) = &value else { - return value; - }; - - let mut current = s.trim().to_string(); - for _ in 0..3 { - match serde_json::from_str::(¤t) { - Ok(inner @ serde_json::Value::Object(_)) => return inner, - Ok(serde_json::Value::String(inner)) => current = inner.trim().to_string(), - _ => break, - } - } - - value -} - /// Parse tool-call arguments, returning `None` when the input looks truncated /// so callers can surface an actionable error rather than invoking a tool with /// incomplete arguments. Non-truncated malformation (e.g. unescaped control @@ -273,23 +225,12 @@ pub fn parse_tool_arguments(args: &str) -> Option { } if let Ok(value) = serde_json::from_str::(args) { - return Some(unwrap_double_encoded_object(value)); - } - - if let Some(inner) = strip_json_wrapper(args) { - if let Ok(value) = serde_json::from_str::(inner) { - return Some(unwrap_double_encoded_object(value)); - } - if !looks_truncated(inner) { - if let Ok(value) = safely_parse_json(inner) { - return Some(unwrap_double_encoded_object(value)); - } - } + return Some(value); } if !looks_truncated(args) { if let Ok(value) = safely_parse_json(args) { - return Some(unwrap_double_encoded_object(value)); + return Some(value); } } @@ -475,78 +416,6 @@ mod tests { assert!(msg.contains("cut off at:"), "msg: {msg}"); } - #[test] - fn test_parse_tool_arguments_markdown_fenced() { - let fenced = "```json\n{\"command\": \"ls\"}\n```"; - let parsed = parse_tool_arguments(fenced).expect("fenced JSON should parse"); - assert_eq!(parsed["command"], "ls"); - - let fenced_no_lang = "```\n{\"command\": \"ls\"}\n```"; - let parsed = parse_tool_arguments(fenced_no_lang).expect("fenced JSON should parse"); - assert_eq!(parsed["command"], "ls"); - - let fenced_inline = "```json{\"command\": \"ls\"}```"; - let parsed = parse_tool_arguments(fenced_inline).expect("fenced JSON should parse"); - assert_eq!(parsed["command"], "ls"); - } - - #[test] - fn test_parse_tool_arguments_double_encoded() { - let double_encoded = r#""{\"command\": \"ls\"}""#; - let parsed = parse_tool_arguments(double_encoded).expect("double-encoded should parse"); - assert!(parsed.is_object(), "expected object, got {parsed:?}"); - assert_eq!(parsed["command"], "ls"); - - let twice = serde_json::to_string(&serde_json::json!(r#"{"command": "ls"}"#)).unwrap(); - let twice = serde_json::to_string(&serde_json::json!(twice)).unwrap(); - let parsed = parse_tool_arguments(&twice).expect("twice-encoded should parse"); - assert!(parsed.is_object(), "expected object, got {parsed:?}"); - assert_eq!(parsed["command"], "ls"); - } - - #[test] - fn test_parse_tool_arguments_xml_wrapped() { - let wrapped = r#"{"command": "ls"}"#; - let parsed = parse_tool_arguments(wrapped).expect("xml-wrapped JSON should parse"); - assert_eq!(parsed["command"], "ls"); - - let with_attrs = r#"{"command": "ls"}"#; - let parsed = parse_tool_arguments(with_attrs).expect("xml-wrapped JSON should parse"); - assert_eq!(parsed["command"], "ls"); - } - - #[test] - fn test_parse_tool_arguments_unrecoverable_wrappers_still_fail() { - assert!(parse_tool_arguments(r#"{"x": 1}"#).is_none()); - assert!(parse_tool_arguments("```json\n{\"x\": 1}\n``` extra").is_none()); - assert!(parse_tool_arguments(r#"{"x": 1}"#).is_none()); - assert!(parse_tool_arguments(r#""#).is_none()); - assert!(parse_tool_arguments("```").is_none()); - } - - #[test] - fn test_parse_tool_arguments_non_object_json_unchanged() { - assert_eq!(parse_tool_arguments("null"), Some(serde_json::Value::Null)); - assert_eq!(parse_tool_arguments("42"), Some(serde_json::json!(42))); - assert_eq!( - parse_tool_arguments("[1, 2]"), - Some(serde_json::json!([1, 2])) - ); - } - - #[test] - fn test_parse_tool_arguments_plain_string_preserved() { - let plain = r#""hello""#; - let parsed = parse_tool_arguments(plain).expect("valid JSON string should parse"); - assert_eq!(parsed, serde_json::json!("hello")); - } - - #[test] - fn test_parse_tool_arguments_fenced_truncated_still_fails() { - let truncated = "```json\n{\"path\": \"/report.md\", \"content\": \"# cut"; - assert!(parse_tool_arguments(truncated).is_none()); - } - #[test] fn test_truncation_error_message_malformed() { // Malformed JSON that ends with } (not truncated, just broken). diff --git a/crates/goose-providers/src/lib.rs b/crates/goose-providers/src/lib.rs index 77e011b176..99944f92d9 100644 --- a/crates/goose-providers/src/lib.rs +++ b/crates/goose-providers/src/lib.rs @@ -1,21 +1,21 @@ pub mod anthropic; pub mod api_client; -pub mod databricks; -pub mod databricks_auth; -pub mod databricks_v2; -pub mod google; -pub use goose_provider_types::{ - base, canonical, conversation, errors, formats, goose_mode, images, json, model, permission, - request_log, retry, thinking, utils, -}; -pub mod declarative; +pub mod base; +pub mod canonical; +pub mod conversation; +pub mod errors; +pub mod formats; +pub mod goose_mode; pub mod http_status; -#[cfg(feature = "local-inference")] -pub mod local_inference; +pub mod images; +pub mod json; +pub(crate) mod mcp_utils; +pub mod model; pub mod ollama; pub mod openai; pub mod openai_compatible; - -pub use declarative::declarative_providers::*; - -pub mod snowflake; +pub mod permission; +pub mod request_log; +pub mod retry; +pub mod thinking; +pub mod utils; diff --git a/crates/goose-providers/src/local_inference.rs b/crates/goose-providers/src/local_inference.rs deleted file mode 100644 index 954840b856..0000000000 --- a/crates/goose-providers/src/local_inference.rs +++ /dev/null @@ -1 +0,0 @@ -pub use goose_local_inference::*; diff --git a/crates/goose-provider-types/src/mcp_utils.rs b/crates/goose-providers/src/mcp_utils.rs similarity index 100% rename from crates/goose-provider-types/src/mcp_utils.rs rename to crates/goose-providers/src/mcp_utils.rs diff --git a/crates/goose-provider-types/src/model.rs b/crates/goose-providers/src/model.rs similarity index 88% rename from crates/goose-provider-types/src/model.rs rename to crates/goose-providers/src/model.rs index fc448db656..cb5ed56e52 100644 --- a/crates/goose-provider-types/src/model.rs +++ b/crates/goose-providers/src/model.rs @@ -8,19 +8,6 @@ use utoipa::ToSchema; pub const DEFAULT_CONTEXT_LIMIT: usize = 128_000; -/// Request param keys that describe model-family-agnostic reasoning behavior and -/// are therefore safe to carry across a model switch or subagent delegation. -/// Provider-specific keys (e.g. `anthropic_beta`) are deliberately excluded so -/// they can't bleed into a request targeting a different model family. -const INHERITED_SESSION_PARAM_KEYS: &[&str] = &[ - "thinking_effort", - "thinking_budget", - "budget_tokens", - "enable_thinking", - "preserve_thinking_context", - "preserve_unsigned_thinking", -]; - #[derive(Debug, Clone, Serialize, ToSchema)] pub struct ModelConfig { pub model_name: String, @@ -200,13 +187,22 @@ impl ModelConfig { previous: Option<&ModelConfig>, request_params: Option>, ) -> Self { - if let Some(previous_params) = previous.and_then(|p| p.request_params.as_ref()) { - for key in INHERITED_SESSION_PARAM_KEYS { - if let Some(value) = previous_params.get(*key) { - self.request_params - .get_or_insert_with(HashMap::new) - .entry(key.to_string()) - .or_insert_with(|| value.clone()); + if let Some(previous) = previous { + let has_thinking_effort = self + .request_params + .as_ref() + .and_then(|params| params.get("thinking_effort")) + .is_some(); + + if !has_thinking_effort { + if let Some(thinking_effort) = previous + .request_params + .as_ref() + .and_then(|params| params.get("thinking_effort")) + .cloned() + { + let params = self.request_params.get_or_insert_with(HashMap::new); + params.insert("thinking_effort".to_string(), thinking_effort); } } } @@ -369,28 +365,15 @@ mod tests { } #[test] - fn inherits_reasoning_controls_but_not_provider_specific_params() { + fn does_not_preserve_unrelated_request_params() { let previous = config_with_params( "previous", - HashMap::from([ - ("budget_tokens".to_string(), serde_json::json!(8192)), - ( - "preserve_thinking_context".to_string(), - serde_json::json!(true), - ), - ("anthropic_beta".to_string(), serde_json::json!("beta")), - ]), + HashMap::from([("provider_specific".to_string(), serde_json::json!("old"))]), ); let config = ModelConfig::new("next") .with_inherited_session_settings_from(Some(&previous), None); - let params = config.request_params.expect("reasoning controls inherited"); - assert_eq!(params.get("budget_tokens"), Some(&serde_json::json!(8192))); - assert_eq!( - params.get("preserve_thinking_context"), - Some(&serde_json::json!(true)) - ); - assert_eq!(params.get("anthropic_beta"), None); + assert!(config.request_params.is_none()); } #[test] @@ -581,20 +564,6 @@ mod tests { assert_eq!(config.max_output_tokens(), 4_096); } - #[test] - fn resolves_claude_sonnet_5_on_aws_bedrock() { - let _guard = env_lock::lock_env([ - ("GOOSE_MAX_TOKENS", None::<&str>), - ("GOOSE_CONTEXT_LIMIT", None::<&str>), - ]); - let config = ModelConfig::new("global.anthropic.claude-sonnet-5") - .with_canonical_limits("aws_bedrock"); - - assert_eq!(config.context_limit, Some(1_000_000)); - assert_eq!(config.max_tokens, Some(128_000)); - assert_eq!(config.reasoning, Some(true)); - } - #[test] fn unknown_model_leaves_fields_none() { let _guard = env_lock::lock_env([ @@ -624,20 +593,6 @@ mod tests { let config = ModelConfig::new("gpt-5.4-xhigh").with_canonical_limits("openai"); assert_eq!(config.context_limit, Some(1_050_000)); - // "gpt-5.6-sol-xhigh" should resolve via "gpt-5.6-sol" - let config = ModelConfig::new("gpt-5.6-sol-xhigh").with_canonical_limits("openai"); - assert_eq!(config.context_limit, Some(1_050_000)); - assert_eq!(config.max_tokens, Some(128_000)); - assert_eq!(config.reasoning, Some(true)); - let canonical = crate::canonical::maybe_get_canonical_model("openai", "gpt-5.6-sol") - .expect("gpt-5.6-sol should have canonical metadata"); - assert_eq!(canonical.temperature, Some(false)); - - let config = ModelConfig::new("gpt-5.6-sol").with_canonical_limits("chatgpt_codex"); - assert_eq!(config.context_limit, Some(1_050_000)); - assert_eq!(config.max_tokens, Some(128_000)); - assert_eq!(config.reasoning, Some(true)); - // "gpt-5.4-nano-low" should resolve via "gpt-5.4-nano" let config = ModelConfig::new("gpt-5.4-nano-low").with_canonical_limits("openai"); assert_eq!(config.context_limit, Some(400_000)); diff --git a/crates/goose-providers/src/ollama.rs b/crates/goose-providers/src/ollama.rs index f6ac65fcc0..7db1fb3742 100644 --- a/crates/goose-providers/src/ollama.rs +++ b/crates/goose-providers/src/ollama.rs @@ -2,10 +2,8 @@ use super::api_client::ApiClient; use super::base::{ConfigKey, MessageStream, Provider, ProviderMetadata}; use super::openai_compatible::handle_status; use super::retry::{ProviderRetry, RetryConfig}; -use crate::api_client::{AuthMethod, TlsConfig}; use crate::base::ProviderDescriptor; use crate::conversation::message::Message; -use crate::declarative::{DeclarativeProviderConfig, KeyResolver}; use crate::errors::ProviderError; use crate::formats::ollama::{create_request, response_to_streaming_message_ollama}; use crate::images::ImageFormat; @@ -15,7 +13,7 @@ use anyhow::{Error, Result}; use async_stream::try_stream; use async_trait::async_trait; use futures::TryStreamExt; -use reqwest::{Response, StatusCode}; +use reqwest::Response; use rmcp::model::Tool; use serde_json::{json, Value}; use std::time::Duration; @@ -23,7 +21,6 @@ use tokio::pin; use tokio_stream::StreamExt; use tokio_util::codec::{FramedRead, LinesCodec}; use tokio_util::io::StreamReader; -use url::Url; pub const OLLAMA_PROVIDER_NAME: &str = "ollama"; pub const OLLAMA_HOST: &str = "localhost"; @@ -82,88 +79,10 @@ pub struct OllamaProvider { #[serde(skip)] api_client: ApiClient, name: String, - custom_models: Option>, - dynamic_models: Option, skip_canonical_filtering: bool, options: OllamaOptions, } -pub struct OllamaProviderBuilder { - api_client: ApiClient, - name: String, - custom_models: Option>, - dynamic_models: Option, - skip_canonical_filtering: bool, - options: OllamaOptions, -} - -impl OllamaProviderBuilder { - pub fn new(api_client: ApiClient) -> Self { - Self { - api_client, - name: OLLAMA_PROVIDER_NAME.to_string(), - custom_models: None, - dynamic_models: None, - skip_canonical_filtering: false, - options: OllamaOptions::default(), - } - } - - pub fn api_client(mut self, api_client: ApiClient) -> Self { - self.api_client = api_client; - self - } - - pub fn map_api_client(mut self, f: impl FnOnce(ApiClient) -> ApiClient) -> Self { - self.api_client = f(self.api_client); - self - } - - pub fn try_map_api_client( - mut self, - f: impl FnOnce(ApiClient) -> Result, - ) -> Result { - self.api_client = f(self.api_client)?; - Ok(self) - } - - pub fn name(mut self, name: impl Into) -> Self { - self.name = name.into(); - self - } - - pub fn custom_models(mut self, custom_models: Option>) -> Self { - self.custom_models = custom_models; - self - } - - pub fn dynamic_models(mut self, dynamic_models: Option) -> Self { - self.dynamic_models = dynamic_models; - self - } - - pub fn skip_canonical_filtering(mut self, skip_canonical_filtering: bool) -> Self { - self.skip_canonical_filtering = skip_canonical_filtering; - self - } - - pub fn options(mut self, options: OllamaOptions) -> Self { - self.options = options; - self - } - - pub fn build(self) -> OllamaProvider { - OllamaProvider { - api_client: self.api_client, - name: self.name, - custom_models: self.custom_models, - dynamic_models: self.dynamic_models, - skip_canonical_filtering: self.skip_canonical_filtering, - options: self.options, - } - } -} - impl OllamaProvider { pub fn new( api_client: ApiClient, @@ -171,63 +90,13 @@ impl OllamaProvider { skip_canonical_filtering: bool, options: OllamaOptions, ) -> Self { - OllamaProviderBuilder::new(api_client) - .name(name) - .skip_canonical_filtering(skip_canonical_filtering) - .options(options) - .build() + Self { + api_client, + name, + skip_canonical_filtering, + options, + } } - - pub fn with_options(mut self, options: OllamaOptions) -> Self { - self.options = options; - self - } - - async fn fetch_models_from_api(&self) -> Result, ProviderError> { - fetch_ollama_model_names(&self.api_client) - .await? - .ok_or_else(|| ProviderError::RequestFailed("No models array in response".to_string())) - } -} - -pub async fn fetch_ollama_model_names( - client: &ApiClient, -) -> Result>, ProviderError> { - let response = client - .request("api/tags") - .response_get() - .await - .map_err(|e| ProviderError::RequestFailed(format!("Failed to fetch models: {}", e)))?; - - if response.status() == StatusCode::NOT_FOUND { - return Err(ProviderError::EndpointNotFound( - "Ollama models endpoint not found".to_string(), - )); - } - - if !response.status().is_success() { - return Err(ProviderError::RequestFailed(format!( - "Failed to fetch models: HTTP {}", - response.status() - ))); - } - - let json: Value = response - .json() - .await - .map_err(|e| ProviderError::RequestFailed(format!("Failed to parse response: {}", e)))?; - - let Some(models) = json.get("models").and_then(|m| m.as_array()) else { - return Ok(None); - }; - - let mut names: Vec = models - .iter() - .filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(String::from)) - .collect(); - - names.sort(); - Ok(Some(names)) } fn resolve_ollama_num_ctx(options: &OllamaOptions, model_config: &ModelConfig) -> Option { @@ -267,100 +136,6 @@ fn apply_ollama_options(payload: &mut Value, options: &OllamaOptions, model_conf } } -pub fn from_declarative_config( - config: DeclarativeProviderConfig, - tls_config: Option, - key_resolver: impl KeyResolver, -) -> Result { - let custom_models = if !config.models.is_empty() { - Some( - config - .models - .iter() - .map(|m| m.name.clone()) - .collect::>(), - ) - } else { - None - }; - - if config.dynamic_models == Some(false) && custom_models.is_none() { - return Err(anyhow::anyhow!( - "Provider '{}' has dynamic_models: false but no static models listed; \ - at least one entry in `models` is required.", - config.name - )); - } - - let timeout = Duration::from_secs(config.timeout_seconds.unwrap_or(OLLAMA_TIMEOUT)); - - let base_has_scheme = - config.base_url.starts_with("http://") || config.base_url.starts_with("https://"); - let base = if base_has_scheme { - config.base_url.clone() - } else { - format!("http://{}", config.base_url) - }; - - let mut base_url = Url::parse(&base) - .map_err(|e| anyhow::anyhow!("Invalid base URL '{}': {}", config.base_url, e))?; - - let is_localhost = matches!(base_url.host_str(), Some("localhost" | "127.0.0.1" | "::1")); - - if base_url.port().is_none() && !base_has_scheme && is_localhost { - base_url - .set_port(Some(OLLAMA_DEFAULT_PORT)) - .map_err(|_| anyhow::anyhow!("Failed to set default port"))?; - } - - let api_key = if config.api_key_env.is_empty() { - None - } else { - match key_resolver.resolve_key(config.api_key_env.as_str()) { - Ok(key) => Some(key), - Err(err) => { - if config.requires_auth { - anyhow::bail!("missing required key {}: {}", config.api_key_env, err); - } - None - } - } - }; - - let auth = match api_key { - Some(key) if !key.is_empty() => AuthMethod::BearerToken(key), - _ => AuthMethod::NoAuth, - }; - - let mut api_client = - ApiClient::with_timeout_and_tls(base_url.to_string(), auth, timeout, tls_config)?; - - if let Some(headers) = &config.headers { - let mut header_map = reqwest::header::HeaderMap::new(); - for (key, value) in headers { - let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())?; - let header_value = reqwest::header::HeaderValue::from_str(value)?; - header_map.insert(header_name, header_value); - } - api_client = api_client.with_headers(header_map)?; - } - - let supports_streaming = config.supports_streaming.unwrap_or(true); - - if !supports_streaming { - return Err(anyhow::anyhow!( - "Ollama provider does not support non-streaming mode. All Ollama models support streaming. \ - Please remove 'supports_streaming: false' from your provider configuration." - )); - } - - Ok(OllamaProviderBuilder::new(api_client) - .name(config.name.clone()) - .custom_models(custom_models) - .dynamic_models(config.dynamic_models) - .skip_canonical_filtering(config.skip_canonical_filtering)) -} - impl ProviderDescriptor for OllamaProvider { fn metadata() -> ProviderMetadata { ProviderMetadata::new( @@ -438,26 +213,39 @@ impl Provider for OllamaProvider { } async fn fetch_supported_models(&self) -> Result, ProviderError> { - if let Some(custom_models) = &self.custom_models { - if self.dynamic_models == Some(false) { - return Ok(custom_models.clone()); - } + let response = self + .api_client + .request("api/tags") + .response_get() + .await + .map_err(|e| ProviderError::RequestFailed(format!("Failed to fetch models: {}", e)))?; - match self.fetch_models_from_api().await { - Ok(models) => return Ok(models), - Err(e) if e.is_endpoint_not_found() => { - tracing::debug!( - "Models endpoint not implemented for provider '{}' ({}), using predefined list", - self.name, - e - ); - return Ok(custom_models.clone()); - } - Err(e) => return Err(e), - } + if !response.status().is_success() { + return Err(ProviderError::RequestFailed(format!( + "Failed to fetch models: HTTP {}", + response.status() + ))); } - self.fetch_models_from_api().await + let json_response = response.json::().await.map_err(|e| { + ProviderError::RequestFailed(format!("Failed to parse response: {}", e)) + })?; + + let models = json_response + .get("models") + .and_then(|m| m.as_array()) + .ok_or_else(|| { + ProviderError::RequestFailed("No models array in response".to_string()) + })?; + + let mut model_names: Vec = models + .iter() + .filter_map(|model| model.get("name").and_then(|n| n.as_str()).map(String::from)) + .collect(); + + model_names.sort(); + + Ok(model_names) } } @@ -535,105 +323,6 @@ fn stream_ollama( #[cfg(test)] mod tests { use super::*; - use crate::base::ModelInfo; - - fn ollama_config( - dynamic_models: Option, - models: Vec, - ) -> DeclarativeProviderConfig { - ollama_config_with_base_url(dynamic_models, models, "http://localhost:11434") - } - - fn ollama_config_with_base_url( - dynamic_models: Option, - models: Vec, - base_url: &str, - ) -> DeclarativeProviderConfig { - DeclarativeProviderConfig { - name: "test-ollama".to_string(), - engine: crate::declarative::ProviderEngine::Ollama, - display_name: "Test Ollama".to_string(), - description: None, - api_key_env: String::new(), - base_url: base_url.to_string(), - models, - headers: None, - timeout_seconds: None, - supports_streaming: None, - requires_auth: false, - catalog_provider_id: None, - base_path: None, - env_vars: None, - dynamic_models, - skip_canonical_filtering: false, - model_doc_link: None, - setup_steps: vec![], - fast_model: None, - preserves_thinking: false, - } - } - - #[tokio::test] - async fn fetch_supported_models_uses_static_models_when_dynamic_models_false() { - let provider = from_declarative_config( - ollama_config(Some(false), vec![ModelInfo::new("static-model", 4096)]), - None, - crate::declarative::EnvKeyResolver, - ) - .unwrap() - .build(); - - assert_eq!( - provider.fetch_supported_models().await.unwrap(), - vec!["static-model".to_string()] - ); - } - - #[test] - fn from_custom_config_requires_static_models_when_dynamic_models_false() { - let err = from_declarative_config( - ollama_config(Some(false), vec![]), - None, - crate::declarative::EnvKeyResolver, - ) - .err() - .expect("expected static models validation error"); - - assert!(err - .to_string() - .contains("dynamic_models: false but no static models listed")); - } - - #[tokio::test] - async fn fetch_supported_models_falls_back_to_static_models_on_404() { - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - let server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/api/tags")) - .respond_with(ResponseTemplate::new(404)) - .expect(1) - .mount(&server) - .await; - - let provider = from_declarative_config( - ollama_config_with_base_url( - None, - vec![ModelInfo::new("static-model", 4096)], - &server.uri(), - ), - None, - crate::declarative::EnvKeyResolver, - ) - .unwrap() - .build(); - - assert_eq!( - provider.fetch_supported_models().await.unwrap(), - vec!["static-model".to_string()] - ); - } #[test] fn test_apply_ollama_options_uses_input_limit() { diff --git a/crates/goose-providers/src/openai.rs b/crates/goose-providers/src/openai.rs index ac6dd4f0f8..e44acbb60a 100644 --- a/crates/goose-providers/src/openai.rs +++ b/crates/goose-providers/src/openai.rs @@ -1,14 +1,12 @@ use super::api_client::ApiClient; use super::base::{ConfigKey, ModelInfo, Provider, ProviderMetadata}; use super::retry::ProviderRetry; -use crate::api_client::{AuthMethod, TlsConfig}; use crate::conversation::message::Message; -use crate::conversation::token_usage::{CostSource, ProviderUsage}; -use crate::declarative::{DeclarativeProviderConfig, KeyResolver}; +use crate::conversation::token_usage::ProviderUsage; use crate::errors::ProviderError; use crate::formats::openai::is_openai_responses_model; use crate::formats::openai::{ - create_request_with_options, get_cost, get_usage, response_to_message, OpenAiFormatOptions, + create_request_with_options, get_usage, response_to_message, OpenAiFormatOptions, }; use crate::formats::openai_responses::{ create_responses_request, get_responses_usage, responses_api_to_message, ResponsesApiResponse, @@ -18,11 +16,9 @@ use crate::openai_compatible::{ handle_response_openai_compat, handle_status, stream_openai_compat, stream_responses_compat, }; use crate::request_log::{start_log, LoggerHandleExt}; -use crate::thinking::ThinkingEffort; use anyhow::Result; use async_trait::async_trait; use reqwest::StatusCode; -use serde_json::json; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -62,16 +58,9 @@ pub const OPEN_AI_KNOWN_MODELS: &[(&str, usize)] = &[ ("gpt-5.4-mini", 400_000), ("gpt-5.4-nano", 400_000), ("gpt-5.4-pro", 1_050_000), - ("gpt-5.5", 1_050_000), - ("gpt-5.5-pro", 1_050_000), - ("gpt-5.6", 1_050_000), - ("gpt-5.6-sol", 1_050_000), - ("gpt-5.6-terra", 1_050_000), - ("gpt-5.6-luna", 1_050_000), ]; pub const OPEN_AI_DOC_URL: &str = "https://platform.openai.com/docs/models"; -const DEFAULT_TIMEOUT_SECONDS: u64 = 600; type OpenAiBaseUrlParts = (String, Vec<(String, String)>, bool); @@ -189,19 +178,6 @@ impl OpenAiProviderBuilder { self } - pub fn map_api_client(mut self, f: impl FnOnce(ApiClient) -> ApiClient) -> Self { - self.api_client = f(self.api_client); - self - } - - pub fn try_map_api_client( - mut self, - f: impl FnOnce(ApiClient) -> Result, - ) -> Result { - self.api_client = f(self.api_client)?; - Ok(self) - } - pub fn base_path(mut self, base_path: impl Into) -> Self { self.base_path = base_path.into(); self @@ -349,33 +325,7 @@ impl OpenAiProvider { const PROVIDERS_NEEDING_STANDARD_CHAT_PARAMS: &[&str] = &["nearai"]; - /// Providers whose reasoning models accept an OpenAI-style - /// `reasoning_effort` field on chat-completions requests but aren't - /// matched by [`is_openai_responses_model`] (which only recognises - /// OpenAI's own `o*`/`gpt-5*` model names). These need the unified - /// [`ThinkingEffort`] mapped onto the request explicitly. - const PROVIDERS_NEEDING_REASONING_EFFORT_MAPPING: &[&str] = &["meta"]; - - /// Maps the unified thinking effort onto Meta's Muse Spark - /// `reasoning_effort` levels: `low`, `medium`, `high`, `xhigh`. - /// - /// Muse Spark always reasons and has no supported "disable reasoning" - /// level, so `Off` is clamped to `low` (the lightest level Meta - /// supports) rather than sent as-is or omitted. - fn meta_reasoning_effort(effort: ThinkingEffort) -> &'static str { - match effort { - ThinkingEffort::Off | ThinkingEffort::Low => "low", - ThinkingEffort::Medium => "medium", - ThinkingEffort::High => "high", - ThinkingEffort::Max => "xhigh", - } - } - - fn sanitize_request_for_compat( - &self, - mut payload: serde_json::Value, - model_config: &ModelConfig, - ) -> serde_json::Value { + fn sanitize_request_for_compat(&self, mut payload: serde_json::Value) -> serde_json::Value { if let Some(obj) = payload.as_object_mut() { if Self::PROVIDERS_NEEDING_MAX_TOKENS_REMAP.contains(&self.name.as_str()) { if let Some(value) = obj.remove("max_completion_tokens") { @@ -401,20 +351,6 @@ impl OpenAiProvider { } } } - - if Self::PROVIDERS_NEEDING_REASONING_EFFORT_MAPPING.contains(&self.name.as_str()) { - match model_config.thinking_effort() { - Some(effort) => { - obj.insert( - "reasoning_effort".to_string(), - json!(Self::meta_reasoning_effort(effort)), - ); - } - None => { - obj.remove("reasoning_effort"); - } - } - } } payload @@ -689,11 +625,7 @@ impl Provider for OpenAiProvider { let message = responses_api_to_message(&responses_api_response)?; let usage_data = get_responses_usage(&responses_api_response); - let usage_json = json.get("usage").unwrap_or(&serde_json::Value::Null); - let mut usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); - if let Some(cost) = get_cost(usage_json) { - usage = usage.with_cost(cost, CostSource::ProviderReported); - } + let usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); log.write( &serde_json::to_value(&message).unwrap_or_default(), @@ -714,7 +646,7 @@ impl Provider for OpenAiProvider { preserve_thinking_context: self.preserve_thinking_context, }, )?; - let payload = self.sanitize_request_for_compat(payload, model_config); + let payload = self.sanitize_request_for_compat(payload); let mut log = start_log(model_config, &payload)?; let response = self @@ -741,12 +673,8 @@ impl Provider for OpenAiProvider { ProviderError::RequestFailed(format!("Failed to parse message: {}", e)) })?; - let usage_json = json.get("usage").unwrap_or(&serde_json::Value::Null); - let usage_data = get_usage(usage_json); - let mut usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); - if let Some(cost) = get_cost(usage_json) { - usage = usage.with_cost(cost, CostSource::ProviderReported); - } + let usage_data = get_usage(json.get("usage").unwrap_or(&serde_json::Value::Null)); + let usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); log.write( &serde_json::to_value(&message).unwrap_or_default(), @@ -759,97 +687,6 @@ impl Provider for OpenAiProvider { } } -pub fn from_declarative_config( - config: DeclarativeProviderConfig, - tls_config: Option, - key_resolver: impl KeyResolver, -) -> Result { - let custom_models = if !config.models.is_empty() { - Some( - config - .models - .iter() - .map(|m| m.name.clone()) - .collect::>(), - ) - } else { - None - }; - - if config.dynamic_models == Some(false) && custom_models.is_none() { - return Err(anyhow::anyhow!( - "Provider '{}' has dynamic_models: false but no static models listed; \ - at least one entry in `models` is required.", - config.name - )); - } - - let api_key = if config.api_key_env.is_empty() { - None - } else { - match key_resolver.resolve_key(config.api_key_env.as_str()) { - Ok(key) => Some(key), - Err(err) => { - if config.requires_auth { - anyhow::bail!("missing required key {}: {}", config.api_key_env, err); - } - None - } - } - }; - - let normalized_base_url = ensure_url_scheme(&config.base_url); - let url = url::Url::parse(&normalized_base_url) - .map_err(|e| anyhow::anyhow!("Invalid base URL '{}': {}", config.base_url, e))?; - - let host = url[..url::Position::BeforePath].to_string(); - let base_path = if let Some(ref explicit_path) = config.base_path { - explicit_path.trim_start_matches('/').to_string() - } else { - derive_base_path(url.path()) - }; - - let timeout_secs = config.timeout_seconds.unwrap_or(DEFAULT_TIMEOUT_SECONDS); - - let auth = match api_key { - Some(key) if !key.is_empty() => AuthMethod::BearerToken(key), - _ => AuthMethod::NoAuth, - }; - let mut api_client = ApiClient::with_timeout_and_tls( - host, - auth, - std::time::Duration::from_secs(timeout_secs), - tls_config, - )?; - - if let Some(query) = url.query() { - let query_params = url::form_urlencoded::parse(query.as_bytes()) - .map(|(key, value)| (key.into_owned(), value.into_owned())) - .collect(); - api_client = api_client.with_query(query_params); - } - - if let Some(headers) = &config.headers { - let mut header_map = reqwest::header::HeaderMap::new(); - for (key, value) in headers { - let header_name = reqwest::header::HeaderName::from_bytes(key.as_bytes())?; - let header_value = reqwest::header::HeaderValue::from_str(value)?; - header_map.insert(header_name, header_value); - } - api_client = api_client.with_headers(header_map)?; - } - - Ok(OpenAiProviderBuilder::new(api_client) - .base_path(base_path) - .custom_headers(config.headers) - .supports_streaming(config.supports_streaming.unwrap_or(true)) - .name(config.name.clone()) - .custom_models(custom_models) - .dynamic_models(config.dynamic_models) - .skip_canonical_filtering(config.skip_canonical_filtering) - .preserve_thinking_context(config.preserves_thinking)) -} - pub fn parse_custom_headers(s: String) -> HashMap { s.split(',') .filter_map(|header| { @@ -861,26 +698,6 @@ pub fn parse_custom_headers(s: String) -> HashMap { .collect() } -pub fn derive_base_path(url_path: &str) -> String { - let stripped = url_path.trim_start_matches('/'); - let normalized = stripped.trim_end_matches('/'); - if normalized.is_empty() { - "v1/chat/completions".to_string() - } else if normalized.ends_with("chat/completions") { - stripped.to_string() - } else if ends_with_version_segment(normalized) { - format!("{}/chat/completions", normalized) - } else { - format!("{}/v1/chat/completions", normalized) - } -} - -fn ends_with_version_segment(path: &str) -> bool { - let last = path.rsplit('/').next().unwrap_or(path); - last.strip_prefix('v') - .is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())) -} - #[cfg(test)] mod tests { use super::*; @@ -918,8 +735,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = provider - .sanitize_request_for_compat(payload, &ModelConfig::new("mistral-medium-latest")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert!(!obj.contains_key("max_completion_tokens")); @@ -936,8 +752,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = provider - .sanitize_request_for_compat(payload, &ModelConfig::new("mistral-medium-latest")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert!(!obj.contains_key("max_completion_tokens")); @@ -953,7 +768,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = provider.sanitize_request_for_compat(payload, &ModelConfig::new("o3")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert!(obj.contains_key("max_completion_tokens")); @@ -969,8 +784,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = - provider.sanitize_request_for_compat(payload, &ModelConfig::new("future-model")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert!(obj.contains_key("max_completion_tokens")); @@ -985,10 +799,7 @@ mod tests { "messages": [] }); - let result = provider.sanitize_request_for_compat( - payload.clone(), - &ModelConfig::new("llama-3.3-70b-versatile"), - ); + let result = provider.sanitize_request_for_compat(payload.clone()); assert_eq!(result, payload); } @@ -1011,8 +822,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = provider - .sanitize_request_for_compat(payload, &ModelConfig::new("Qwen/Qwen3.6-35B-A3B-FP8")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert!(!obj.contains_key("reasoning_effort")); @@ -1032,8 +842,7 @@ mod tests { "max_completion_tokens": 16384 }); - let result = - provider.sanitize_request_for_compat(payload, &ModelConfig::new("openai/gpt-5")); + let result = provider.sanitize_request_for_compat(payload); let obj = result.as_object().unwrap(); assert_eq!(obj.get("reasoning_effort"), Some(&json!("medium"))); @@ -1041,73 +850,6 @@ mod tests { assert_eq!(obj.get("max_tokens").unwrap(), &json!(16384)); } - #[test] - fn sanitize_meta_applies_reasoning_effort_from_thinking_effort() { - let provider = make_provider("meta"); - let payload = json!({ - "model": "muse-spark-1.1", - "messages": [] - }); - let model_config = - ModelConfig::new("muse-spark-1.1").with_thinking_effort(ThinkingEffort::High); - - let result = provider.sanitize_request_for_compat(payload, &model_config); - let obj = result.as_object().unwrap(); - - assert_eq!(obj.get("reasoning_effort"), Some(&json!("high"))); - } - - #[test] - fn sanitize_meta_maps_max_thinking_effort_to_xhigh() { - let provider = make_provider("meta"); - let payload = json!({ - "model": "muse-spark-1.1", - "messages": [] - }); - let model_config = - ModelConfig::new("muse-spark-1.1").with_thinking_effort(ThinkingEffort::Max); - - let result = provider.sanitize_request_for_compat(payload, &model_config); - let obj = result.as_object().unwrap(); - - assert_eq!(obj.get("reasoning_effort"), Some(&json!("xhigh"))); - } - - #[test] - fn sanitize_meta_clamps_off_thinking_effort_to_low() { - // Muse Spark always reasons and has no "disable reasoning" level, - // so an explicit `Off` must be clamped to the lightest supported - // level rather than omitted or sent as-is. - let provider = make_provider("meta"); - let payload = json!({ - "model": "muse-spark-1.1", - "messages": [], - "reasoning_effort": "high" - }); - let model_config = - ModelConfig::new("muse-spark-1.1").with_thinking_effort(ThinkingEffort::Off); - - let result = provider.sanitize_request_for_compat(payload, &model_config); - let obj = result.as_object().unwrap(); - - assert_eq!(obj.get("reasoning_effort"), Some(&json!("low"))); - } - - #[test] - fn sanitize_meta_omits_reasoning_effort_when_unset() { - let provider = make_provider("meta"); - let payload = json!({ - "model": "muse-spark-1.1", - "messages": [] - }); - let model_config = ModelConfig::new("muse-spark-1.1"); - - let result = provider.sanitize_request_for_compat(payload, &model_config); - let obj = result.as_object().unwrap(); - - assert!(!obj.contains_key("reasoning_effort")); - } - #[test] fn nearai_uses_chat_completions_for_openai_reasoning_models() { let provider = make_provider("nearai"); @@ -1121,8 +863,6 @@ mod tests { for (model_name, base_path, expected) in [ ("gpt-5.4", "v1/chat/completions", true), ("gpt-5.4-xhigh", "v1/chat/completions", true), - ("gpt-5.6-sol", "v1/chat/completions", true), - ("gpt-5.6-terra-xhigh", "v1/chat/completions", true), ("gpt-5.2-pro-2025-12-11", "v1/chat/completions", true), ("gpt-4o", "v1/chat/completions", false), ("gpt-5.2-codex", "openai/v1/chat/completions", false), @@ -1210,60 +950,6 @@ mod tests { ); } - fn custom_config(base_url: &str) -> DeclarativeProviderConfig { - DeclarativeProviderConfig { - name: "test-openai".to_string(), - engine: crate::declarative::ProviderEngine::OpenAI, - display_name: "Test OpenAI".to_string(), - description: None, - api_key_env: String::new(), - base_url: base_url.to_string(), - models: vec![crate::base::ModelInfo::new("test-model", 4096)], - headers: None, - timeout_seconds: None, - supports_streaming: None, - requires_auth: false, - catalog_provider_id: None, - base_path: None, - env_vars: None, - dynamic_models: Some(false), - skip_canonical_filtering: false, - model_doc_link: None, - setup_steps: vec![], - fast_model: None, - preserves_thinking: false, - } - } - - #[test] - fn from_custom_config_preserves_ipv6_authority() { - let provider = from_declarative_config( - custom_config("http://[::1]:1234/v1"), - None, - crate::declarative::EnvKeyResolver, - ) - .unwrap() - .build(); - - assert_eq!(provider.api_client.host(), "http://[::1]:1234"); - } - - #[test] - fn from_custom_config_preserves_userinfo_authority() { - let provider = from_declarative_config( - custom_config("https://user:pass@gateway.example/v1"), - None, - crate::declarative::EnvKeyResolver, - ) - .unwrap() - .build(); - - assert_eq!( - provider.api_client.host(), - "https://user:pass@gateway.example" - ); - } - #[test] fn parse_n_ctx_falls_back_to_sole_entry_when_id_differs() { let body = json!({ @@ -1284,37 +970,4 @@ mod tests { }); assert_eq!(parse_n_ctx_from_models(&body, "model-c"), None); } - - #[test] - fn derive_base_path_not_removing_api_path() { - let r = derive_base_path("https://opencode.ai/zen/go"); - assert_eq!(r, "https://opencode.ai/zen/go/v1/chat/completions"); - } - - #[test] - fn derive_base_path_should_support_v1() { - let r = derive_base_path("https://opencode.ai/zen/go/v1"); - assert_eq!(r, "https://opencode.ai/zen/go/v1/chat/completions"); - } - - #[test] - fn derive_base_path_should_support_no_base_path() { - let r = derive_base_path("https://opencode.ai/"); - assert_eq!(r, "https://opencode.ai/v1/chat/completions"); - } - - #[test] - fn derive_base_path_preserves_non_v1_version_prefix() { - // Zhipu's default base_url is https://open.bigmodel.cn/api/paas/v4 and - // from_custom_config passes url.path() ("/api/paas/v4") here. The - // existing /api/paas/v4 version must not gain an extra /v1 segment. - let r = derive_base_path("/api/paas/v4"); - assert_eq!(r, "api/paas/v4/chat/completions"); - } - - #[test] - fn derive_base_path_does_not_treat_v_word_as_version() { - let r = derive_base_path("/api/voice"); - assert_eq!(r, "api/voice/v1/chat/completions"); - } } diff --git a/crates/goose-providers/src/openai_compatible.rs b/crates/goose-providers/src/openai_compatible.rs index 78f96aed92..5ce39c8173 100644 --- a/crates/goose-providers/src/openai_compatible.rs +++ b/crates/goose-providers/src/openai_compatible.rs @@ -1,4 +1,4 @@ -use crate::conversation::token_usage::{CostSource, ProviderUsage}; +use crate::conversation::token_usage::ProviderUsage; use crate::images::ImageFormat; use anyhow::Error; use async_stream::try_stream; @@ -18,7 +18,7 @@ use super::retry::ProviderRetry; use crate::conversation::message::Message; use crate::errors::ProviderError; use crate::formats::openai::{ - create_request, get_cost, get_usage, response_to_message, response_to_streaming_message, + create_request, get_usage, response_to_message, response_to_streaming_message, }; use crate::formats::openai_responses::responses_api_to_streaming_message; use crate::model::ModelConfig; @@ -143,12 +143,8 @@ impl Provider for OpenAiCompatibleProvider { ProviderError::RequestFailed(format!("Failed to parse message: {}", e)) })?; - let usage_json = json.get("usage").unwrap_or(&serde_json::Value::Null); - let usage_data = get_usage(usage_json); - let mut usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); - if let Some(cost) = get_cost(usage_json) { - usage = usage.with_cost(cost, CostSource::ProviderReported); - } + let usage_data = get_usage(json.get("usage").unwrap_or(&serde_json::Value::Null)); + let usage = ProviderUsage::new(model_config.model_name.clone(), usage_data); log.write( &serde_json::to_value(&message).unwrap_or_default(), diff --git a/crates/goose-provider-types/src/permission.rs b/crates/goose-providers/src/permission.rs similarity index 100% rename from crates/goose-provider-types/src/permission.rs rename to crates/goose-providers/src/permission.rs diff --git a/crates/goose-provider-types/src/request_log.rs b/crates/goose-providers/src/request_log.rs similarity index 100% rename from crates/goose-provider-types/src/request_log.rs rename to crates/goose-providers/src/request_log.rs diff --git a/crates/goose-provider-types/src/retry.rs b/crates/goose-providers/src/retry.rs similarity index 84% rename from crates/goose-provider-types/src/retry.rs rename to crates/goose-providers/src/retry.rs index 49afd9bfe8..8660a9f1cf 100644 --- a/crates/goose-provider-types/src/retry.rs +++ b/crates/goose-providers/src/retry.rs @@ -81,27 +81,11 @@ impl RetryConfig { } } -/// Substrings marking a `RequestFailed` (4xx) as deterministically permanent: -/// Anthropic rejects signed `thinking`/`redacted_thinking` blocks as immutable -/// once a thinking model's config changes mid-conversation, and the identical -/// payload is rebuilt on every retry โ€” so retrying can never succeed. -const PERMANENT_REQUEST_FAILURE_MARKERS: &[&str] = &[ - "blocks in the latest assistant message cannot be modified", - "must remain as they were in the original response", -]; - -fn is_permanent_request_failure(message: &str) -> bool { - PERMANENT_REQUEST_FAILURE_MARKERS - .iter() - .any(|marker| message.contains(marker)) -} - pub fn should_retry(error: &ProviderError, config: &RetryConfig) -> bool { match error { ProviderError::RateLimitExceeded { .. } | ProviderError::ServerError(_) | ProviderError::NetworkError(_) => true, - ProviderError::RequestFailed(message) if is_permanent_request_failure(message) => false, ProviderError::RequestFailed(_) => !config.transient_only, _ => false, } @@ -270,33 +254,6 @@ mod tests { assert!(should_retry(&error, &config)); } - #[test] - fn never_retries_permanent_thinking_block_400() { - let config = RetryConfig::default(); - let error = ProviderError::RequestFailed( - "Bad request (400): {\"message\":\"messages.3.content.1: `thinking` or \ - `redacted_thinking` blocks in the latest assistant message cannot be \ - modified. These blocks must remain as they were in the original \ - response.\"}" - .into(), - ); - assert!(!should_retry(&error, &config)); - } - - #[test] - fn permanent_request_failure_marker_detection() { - assert!(is_permanent_request_failure( - "messages.3.content.1: `thinking` blocks in the latest assistant message \ - cannot be modified" - )); - assert!(is_permanent_request_failure( - "These blocks must remain as they were in the original response." - )); - assert!(!is_permanent_request_failure( - "Bad request (400): model not found" - )); - } - #[test] fn transient_only_skips_request_failed() { let config = RetryConfig::default().transient_only(); diff --git a/crates/goose-provider-types/src/thinking.rs b/crates/goose-providers/src/thinking.rs similarity index 100% rename from crates/goose-provider-types/src/thinking.rs rename to crates/goose-providers/src/thinking.rs diff --git a/crates/goose-providers/src/utils.rs b/crates/goose-providers/src/utils.rs new file mode 100644 index 0000000000..3768ad6a30 --- /dev/null +++ b/crates/goose-providers/src/utils.rs @@ -0,0 +1,14 @@ +use unicode_normalization::UnicodeNormalization; + +fn is_in_unicode_tag_range(c: char) -> bool { + matches!(c, '\u{E0000}'..='\u{E007F}') +} + +pub fn sanitize_unicode_tags(text: &str) -> String { + let normalized: String = text.nfc().collect(); + + normalized + .chars() + .filter(|&c| !is_in_unicode_tag_range(c)) + .collect() +} diff --git a/crates/goose-sdk-types/Cargo.toml b/crates/goose-sdk-types/Cargo.toml index afda65dd86..0414365d04 100644 --- a/crates/goose-sdk-types/Cargo.toml +++ b/crates/goose-sdk-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goose-sdk-types" -version = "0.1.0-alpha.0" +version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/crates/goose-sdk-types/src/custom_notifications.rs b/crates/goose-sdk-types/src/custom_notifications.rs index 868e3185de..3170e93ccb 100644 --- a/crates/goose-sdk-types/src/custom_notifications.rs +++ b/crates/goose-sdk-types/src/custom_notifications.rs @@ -25,14 +25,12 @@ pub struct GooseSessionNotification { "propertyName": "sessionUpdate", "mapping": { "usage_update": "#/$defs/SessionUsageUpdate", - "status_message": "#/$defs/StatusMessageUpdate", - "message_usage": "#/$defs/MessageUsageUpdate" + "status_message": "#/$defs/StatusMessageUpdate" } }))] pub enum GooseSessionUpdate { UsageUpdate(SessionUsageUpdate), StatusMessage(StatusMessageUpdate), - MessageUsage(MessageUsageUpdate), } impl Default for GooseSessionUpdate { @@ -53,55 +51,6 @@ pub struct SessionUsageUpdate { pub accumulated_cost: Option, } -/// Per-message token usage/cost/timing, keyed by the message id used for -/// chunk matching. Sent live after a turn's messages and on replay. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct MessageUsageUpdate { - #[serde(skip_serializing_if = "Option::is_none")] - pub message_id: Option, - pub usage: MessageUsageData, -} - -/// Wire mirror of the conversation `MessageUsage` (this crate cannot depend on -/// goose-provider-types); field names and serde casing MUST stay in parity. -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct MessageUsageData { - #[serde(skip_serializing_if = "Option::is_none")] - pub input_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub total_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_read_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_write_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cost_source: Option, - /// Wall-clock generation time, used by the client for a tokens/sec readout. - #[serde(skip_serializing_if = "Option::is_none")] - pub elapsed_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub time_to_first_token_ms: Option, - /// Usage from a compaction/summarization call rather than a normal turn. - #[serde(default)] - pub is_compaction: bool, -} - -/// Wire mirror of the conversation `CostSource`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum CostSourceData { - /// Cost returned directly by the provider (e.g. OpenRouter `usage.cost`). - ProviderReported, - /// Cost computed from the canonical pricing table. - Estimated, -} - /// Live UI/session status. This is not conversation transcript content, and /// should not be persisted or replayed as history. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -177,50 +126,4 @@ mod tests { }) ); } - - #[test] - fn message_usage_serializes_to_expected_wire_shape() { - let notification = GooseSessionNotification { - session_id: "s1".to_string(), - update: GooseSessionUpdate::MessageUsage(MessageUsageUpdate { - message_id: Some("m1".to_string()), - usage: MessageUsageData { - input_tokens: Some(1200), - output_tokens: Some(340), - total_tokens: Some(1540), - cache_read_tokens: Some(1000), - cache_write_tokens: None, - cost: Some(0.0123), - cost_source: Some(CostSourceData::Estimated), - elapsed_ms: Some(4200), - time_to_first_token_ms: Some(840), - is_compaction: false, - }, - }), - }; - - let value = serde_json::to_value(notification).unwrap(); - - assert_eq!( - value, - json!({ - "sessionId": "s1", - "update": { - "sessionUpdate": "message_usage", - "messageId": "m1", - "usage": { - "inputTokens": 1200, - "outputTokens": 340, - "totalTokens": 1540, - "cacheReadTokens": 1000, - "cost": 0.0123, - "costSource": "estimated", - "elapsedMs": 4200, - "timeToFirstTokenMs": 840, - "isCompaction": false - } - } - }) - ); - } } diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index a4502ca255..8bd0ad4683 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -157,19 +157,6 @@ pub struct AppsImportResponse { pub message: String, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request(method = "_goose/unstable/apps/delete", response = AppsDeleteResponse)] -#[serde(rename_all = "camelCase")] -pub struct AppsDeleteRequest { - pub name: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -pub struct AppsDeleteResponse { - pub name: String, - pub message: String, -} - /// Update the working directory for a session. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "_goose/unstable/session/working-dir/update", response = EmptyResponse)] @@ -1784,363 +1771,6 @@ pub struct ProviderInventoryEntryDto { pub model_selection_hint: Option, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum LocalInferenceToolCallingMode { - #[default] - Auto, - ForceNative, - ForceEmulated, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum LocalInferenceChatTemplate { - #[default] - Embedded, - Builtin { - name: String, - }, - CustomInline { - template: String, - }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type", rename_all_fields = "camelCase")] -pub enum LocalInferenceSamplingConfig { - Greedy, - Temperature { - temperature: f32, - top_k: i32, - top_p: f32, - min_p: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - seed: Option, - }, - MirostatV2 { - tau: f32, - eta: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - seed: Option, - }, -} - -impl Default for LocalInferenceSamplingConfig { - fn default() -> Self { - Self::Temperature { - temperature: 0.8, - top_k: 40, - top_p: 0.95, - min_p: 0.05, - seed: None, - } - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelSettingsDto { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub backend_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub context_size: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_output_tokens: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub draft_model: Option, - #[serde(default)] - pub sampling: LocalInferenceSamplingConfig, - pub repeat_penalty: f32, - pub repeat_last_n: i32, - pub frequency_penalty: f32, - pub presence_penalty: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub n_batch: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub n_gpu_layers: Option, - pub use_mlock: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub flash_attention: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub n_threads: Option, - #[serde(default)] - pub tool_calling: LocalInferenceToolCallingMode, - #[serde(default)] - pub chat_template: LocalInferenceChatTemplate, - pub enable_thinking: bool, - pub vision_capable: bool, - pub image_token_estimate: usize, - pub mmproj_size_bytes: u64, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum LocalInferenceDownloadState { - #[default] - NotDownloaded, - Downloading, - Downloaded, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadStatusDto { - pub state: LocalInferenceDownloadState, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub progress_percent: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bytes_downloaded: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub total_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed_bps: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceDownloadProgressDto { - pub model_id: String, - pub status: String, - pub bytes_downloaded: u64, - pub total_bytes: u64, - pub progress_percent: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub speed_bps: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub eta_seconds: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, - pub task_exited: bool, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDto { - pub id: String, - pub repo_id: String, - pub filename: String, - pub quantization: String, - pub size_bytes: u64, - pub status: LocalInferenceModelDownloadStatusDto, - pub recommended: bool, - pub is_loaded: bool, - pub settings: LocalInferenceModelSettingsDto, - pub vision_capable: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub mmproj_status: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHfModelVariantDto { - pub variant_id: String, - pub label: String, - pub backend_id: String, - pub format: String, - pub model_id: String, - pub download_id: String, - pub size_bytes: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub filename: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub download_url: Option, - pub description: String, - pub quality_rank: u8, - pub sharded: bool, - pub supported: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub unsupported_reason: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHfGgufFileDto { - pub filename: String, - pub size_bytes: u64, - pub quantization: String, - pub download_url: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHfModelInfoDto { - pub repo_id: String, - pub author: String, - pub model_name: String, - pub downloads: u64, - #[serde(default)] - pub gguf_files: Vec, - #[serde(default)] - pub variants: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/list", - response = LocalInferenceModelsListResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelsListRequest {} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelsListResponse { - pub models: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/download", - response = LocalInferenceModelDownloadResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadRequest { - pub spec: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub backend_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub variant_id: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadResponse { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/download/progress", - response = LocalInferenceModelDownloadProgressResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadProgressRequest { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadProgressResponse { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub progress: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/download/cancel", - response = EmptyResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDownloadCancelRequest { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/delete", - response = EmptyResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelDeleteRequest { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/evict", - response = EmptyResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelEvictRequest { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/settings/read", - response = LocalInferenceModelSettingsReadResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelSettingsReadRequest { - pub model_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelSettingsReadResponse { - pub settings: LocalInferenceModelSettingsDto, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/models/settings/update", - response = LocalInferenceModelSettingsUpdateResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelSettingsUpdateRequest { - pub model_id: String, - pub settings: LocalInferenceModelSettingsDto, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceModelSettingsUpdateResponse { - pub settings: LocalInferenceModelSettingsDto, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/huggingface/search", - response = LocalInferenceHuggingFaceSearchResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHuggingFaceSearchRequest { - pub query: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub limit: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHuggingFaceSearchResponse { - pub models: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/huggingface/repo/variants", - response = LocalInferenceHuggingFaceRepoVariantsResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHuggingFaceRepoVariantsRequest { - pub repo_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceHuggingFaceRepoVariantsResponse { - pub variants: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub recommended_index: Option, - pub available_memory_bytes: u64, - pub downloaded_quants: Vec, - pub downloaded_variants: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request( - method = "_goose/unstable/local-inference/chat-templates/builtin/list", - response = LocalInferenceBuiltinChatTemplatesListResponse -)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceBuiltinChatTemplatesListRequest {} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -#[serde(rename_all = "camelCase")] -pub struct LocalInferenceBuiltinChatTemplatesListResponse { - pub templates: Vec, -} - /// Empty success response for operations that return no data. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct EmptyResponse {} @@ -2169,7 +1799,6 @@ pub struct DictationLocalModelStatus { pub size_mb: u32, pub downloaded: bool, pub download_in_progress: bool, - pub recommended: bool, } /// Kick off a background download of a local Whisper model. diff --git a/crates/goose-sdk-types/src/lib.rs b/crates/goose-sdk-types/src/lib.rs index 0358354605..5b50213069 100644 --- a/crates/goose-sdk-types/src/lib.rs +++ b/crates/goose-sdk-types/src/lib.rs @@ -1,7 +1,8 @@ -//! Shared Goose ACP wire types. +//! Shared types for the Goose SDK. //! -//! These wire types keep a single source of truth for Goose's custom -//! `_goose/*` JSON-RPC methods. +//! These wire types are used by both the ACP client/server path and the +//! in-process uniffi bindings, keeping a single source of truth for Goose's +//! custom `_goose/*` JSON-RPC methods. pub mod custom_notifications; pub mod custom_requests; diff --git a/crates/goose-sdk/.gitignore b/crates/goose-sdk/.gitignore index 0916a2813c..4d0904c04a 100644 --- a/crates/goose-sdk/.gitignore +++ b/crates/goose-sdk/.gitignore @@ -1,6 +1,2 @@ generated examples/uniffi/*.jar -python/build/ -python/dist/ -python/src/*.egg-info/ -python/src/goose/ diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml index 48cdfb7cab..6bd1e5a34b 100644 --- a/crates/goose-sdk/Cargo.toml +++ b/crates/goose-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "goose-sdk" -version = "0.1.0-alpha.0" +version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -19,28 +19,15 @@ required-features = ["uniffi"] [features] default = [] -uniffi = [ - "dep:uniffi", - "dep:thiserror", - "dep:anyhow", - "dep:goose-providers", - "dep:futures", - "dep:serde_json", - "dep:tokio", -] +uniffi = ["dep:uniffi", "dep:thiserror"] [dependencies] -goose-sdk-types = { version = "0.1.0-alpha.0", path = "../goose-sdk-types" } +goose-sdk-types = { path = "../goose-sdk-types" } agent-client-protocol = { workspace = true, features = ["unstable"] } agent-client-protocol-schema = { workspace = true } -uniffi = { version = "0.32", features = ["cli"], optional = true } +uniffi = { version = "0.31", features = ["cli"], optional = true } thiserror = { version = "2", optional = true } -goose-providers = { version = "0.1.0-alpha.0", path = "../goose-providers", features = ["rustls-tls"], optional = true } -futures = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } -tokio = { workspace = true, features = ["rt-multi-thread", "sync"], optional = true } -anyhow = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process", "io-std", "io-util"] } diff --git a/crates/goose-sdk/README.md b/crates/goose-sdk/README.md index 1649f8eb51..3dfb839e37 100644 --- a/crates/goose-sdk/README.md +++ b/crates/goose-sdk/README.md @@ -4,36 +4,13 @@ The bindings layer for Goose. It houses the shared types used for both ACP and SDK access, and exposes a cross-language version of the Goose API. With `--features uniffi` the crate compiles to native bindings for Python and -Kotlin (namespace `goose` / `io.github.aaif_goose`). The UniFFI surface currently lets -callers construct declarative providers from JSON and stream provider -completions. +Kotlin (namespace `aaif_goose` / `aaif.goose`). The published surface is +currently a `ping` -> `pong` stub in `src/bindings.rs` โ€” the scaffold for the +real implementation. ```bash -just python # build bindings + run examples/uniffi/provider.py -just kotlin # build the Maven artifact + run examples/uniffi/kotlin +just python # build bindings + run examples/uniffi/ping.py +just kotlin # build bindings + run examples/uniffi/Ping.kt ``` -## Python package - -The PyPI package is published as `goose-sdk` and imports as `goose`. -Build a local wheel from the repository root with: - -```bash -just --justfile crates/goose-sdk/justfile python-wheel -``` - -This regenerates the UniFFI Python bindings, copies the release native library -into the package, and writes the wheel to `crates/goose-sdk/python/dist/`. - -## Maven package - -The Maven Central artifact is published as `io.github.aaif-goose:gdk` and uses -the Rust crate version from `crates/goose-sdk/Cargo.toml`. - -```bash -just --justfile crates/goose-sdk/justfile maven-package -``` - -This regenerates the UniFFI Kotlin bindings and packages them with the native -library in a JVM jar. CI builds the native libraries for supported platforms and -can optionally publish the combined artifact to Maven Central. +Both print `pong: aaif.io`. diff --git a/crates/goose-sdk/examples/deepseek.json b/crates/goose-sdk/examples/deepseek.json deleted file mode 100644 index 1d22074437..0000000000 --- a/crates/goose-sdk/examples/deepseek.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "deepseek", - "engine": "openai", - "display_name": "DeepSeek", - "description": "Custom DeepSeek provider", - "api_key_env": "DEEPSEEK_API_KEY", - "base_url": "https://api.deepseek.com", - "models": [ - { - "name": "deepseek-chat", - "context_limit": 128000, - "input_token_cost": null, - "output_token_cost": null, - "currency": null, - "supports_cache_control": null - }, - { - "name": "deepseek-reasoner", - "context_limit": 128000, - "input_token_cost": null, - "output_token_cost": null, - "currency": null, - "supports_cache_control": null - } - ], - "headers": null, - "timeout_seconds": null, - "preserves_thinking": true, - "supports_streaming": true -} diff --git a/crates/goose-sdk/examples/uniffi/Ping.kt b/crates/goose-sdk/examples/uniffi/Ping.kt new file mode 100644 index 0000000000..0f043ee003 --- /dev/null +++ b/crates/goose-sdk/examples/uniffi/Ping.kt @@ -0,0 +1,9 @@ +package aaif.example + +import aaif.goose.Client + +fun main() { + val client = Client() + val pong = client.ping("aaif.io") + println(pong.message) +} diff --git a/crates/goose-sdk/examples/uniffi/README.md b/crates/goose-sdk/examples/uniffi/README.md deleted file mode 100644 index 95c3fee7ba..0000000000 --- a/crates/goose-sdk/examples/uniffi/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# UniFFI examples - -These examples exercise the in-process Goose SDK UniFFI bindings from Python and Kotlin. - -## Prerequisites - -```bash -source bin/activate-hermit -``` - -The Python example uses the declarative DeepSeek provider: - -```bash -export DEEPSEEK_API_KEY=... -``` - -The Kotlin/JVM Maven smoke test uses the native OpenAI provider: - -```bash -export OPENAI_API_KEY=... -``` - -## Generate bindings - -Regenerate the Python bindings before running the Python example: - -```bash -just --justfile crates/goose-sdk/justfile _generate python -``` - -This writes generated bindings and the debug native library under `crates/goose-sdk/generated/`. - -## Python provider example - -```bash -DYLD_LIBRARY_PATH=target/debug LD_LIBRARY_PATH=target/debug \ - uv run --script crates/goose-sdk/examples/uniffi/provider.py -``` - -## Kotlin/JVM Maven smoke test - -Build the local Maven artifact and run the downstream smoke test app: - -```bash -just --justfile crates/goose-sdk/justfile maven-package -cd crates/goose-sdk/examples/uniffi/kotlin -gradle --no-daemon run -``` - -Or run the same flow through the goose-sdk justfile: - -```bash -just --justfile crates/goose-sdk/justfile kotlin -``` - -The Kotlin example consumes the local Maven artifact `io.github.aaif-goose:gdk` from `mavenLocal()` and imports the generated package namespace `io.github.aaif_goose`. - -On newer JDKs, the example enables native access with `--enable-native-access=ALL-UNNAMED` because the SDK uses JNA to load the bundled native library. diff --git a/crates/goose-sdk/examples/uniffi/kotlin/README.md b/crates/goose-sdk/examples/uniffi/kotlin/README.md deleted file mode 100644 index 7c0cca2399..0000000000 --- a/crates/goose-sdk/examples/uniffi/kotlin/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Kotlin/JVM GDK smoke test - -This is a small downstream Kotlin/JVM app that consumes the Maven artifact -`io.github.aaif-goose:gdk` from `mavenLocal()`. - -From the repository root, first build and publish the Maven artifact locally: - -```bash -source bin/activate-hermit -just --justfile crates/goose-sdk/justfile maven-package -``` - -Then run the smoke test: - -```bash -cd crates/goose-sdk/examples/uniffi/kotlin -gradle --no-daemon run -``` - -Set `DATABRICKS_HOST` and `DATABRICKS_TOKEN` before running the example. -`DATABRICKS_HOST` should be the Databricks workspace URL, for example -`https://dbc-xxxxxxxx-xxxx.cloud.databricks.com`. The example uses the native -GDK `DatabricksProvider`, not the declarative JSON provider. The expected output -is a streamed completion from Databricks followed by optional usage metadata. -The important failure to watch for is `UnsatisfiedLinkError` or a missing native -library resource, which would mean the bundled native library was not loaded -correctly. - -The example sets `--enable-native-access=ALL-UNNAMED` because JNA loads the -bundled Goose native library. Newer JDKs warn when native access is not enabled -explicitly, and future JDKs may require it. diff --git a/crates/goose-sdk/examples/uniffi/kotlin/build.gradle.kts b/crates/goose-sdk/examples/uniffi/kotlin/build.gradle.kts deleted file mode 100644 index 0a86abcabf..0000000000 --- a/crates/goose-sdk/examples/uniffi/kotlin/build.gradle.kts +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - kotlin("jvm") version "2.2.21" - application -} - -fun gooseSdkVersion(): String { - val cargoToml = file("../../../Cargo.toml").readText() - return Regex("(?m)^version\\s*=\\s*\"([^\"]+)\"") - .find(cargoToml) - ?.groupValues - ?.get(1) - ?: error("Could not find goose-sdk version in ../../../Cargo.toml") -} - -dependencies { - implementation("io.github.aaif-goose:gdk:${gooseSdkVersion()}") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") -} - -kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) - } -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -application { - mainClass.set("MainKt") - applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") -} diff --git a/crates/goose-sdk/examples/uniffi/kotlin/settings.gradle.kts b/crates/goose-sdk/examples/uniffi/kotlin/settings.gradle.kts deleted file mode 100644 index b20e2e1851..0000000000 --- a/crates/goose-sdk/examples/uniffi/kotlin/settings.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenLocal() - mavenCentral() - } -} - -rootProject.name = "goose-sdk-kotlin-smoke-test" diff --git a/crates/goose-sdk/examples/uniffi/kotlin/src/main/kotlin/Main.kt b/crates/goose-sdk/examples/uniffi/kotlin/src/main/kotlin/Main.kt deleted file mode 100644 index 7b2401d713..0000000000 --- a/crates/goose-sdk/examples/uniffi/kotlin/src/main/kotlin/Main.kt +++ /dev/null @@ -1,35 +0,0 @@ -import io.github.aaif_goose.MessageRole -import io.github.aaif_goose.ProviderMessage -import io.github.aaif_goose.ProviderModelConfig -import io.github.aaif_goose.streamFlow -import io.github.aaif_goose.providers.openai.defaultModel -import io.github.aaif_goose.providers.openai.provider as openAiProvider -import kotlinx.coroutines.runBlocking - -fun main() = runBlocking { - val apiKey = System.getenv("OPENAI_API_KEY") - require(!apiKey.isNullOrBlank()) { - "Set OPENAI_API_KEY before running this example." - } - - val provider = openAiProvider(apiKey) - val model = ProviderModelConfig(modelName = defaultModel()) - val messages = listOf( - ProviderMessage( - role = MessageRole.USER, - text = "What is the capital of France? Answer in one sentence.", - ), - ) - - provider - .streamFlow( - model, - "You are a knowledgeable geography expert.", - messages, - ) - .collect { chunk -> - chunk.text?.let { print(it) } - chunk.usageJson?.let { println("\nusage: $it") } - } - println() -} diff --git a/crates/goose-sdk/examples/uniffi/ping.py b/crates/goose-sdk/examples/uniffi/ping.py new file mode 100644 index 0000000000..9503b04808 --- /dev/null +++ b/crates/goose-sdk/examples/uniffi/ping.py @@ -0,0 +1,21 @@ +"""Minimal Goose SDK demo: ping the SDK and print the pong.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent.parent / "generated")) + +from aaif_goose import Client # noqa: E402 + + +def main() -> None: + client = Client() + pong = client.ping("aaif.io") + print(pong.message) + + +if __name__ == "__main__": + main() diff --git a/crates/goose-sdk/examples/uniffi/provider.py b/crates/goose-sdk/examples/uniffi/provider.py deleted file mode 100755 index 87f9886b9a..0000000000 --- a/crates/goose-sdk/examples/uniffi/provider.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env -S uv run --script -"""Goose SDK demo: build a declarative provider and stream a completion.""" -import asyncio -import json -import sys -from pathlib import Path - -HERE = Path(__file__).resolve().parent -sys.path.insert(0, str(HERE.parent.parent / "generated")) - -from goose import ( # noqa: E402 - MessageRole, - ProviderMessage, - ProviderModelConfig, - declarative_provider_from_json, -) - - -async def main() -> None: - provider = declarative_provider_from_json((HERE.parent / "deepseek.json").read_text()) - model = ProviderModelConfig(model_name="deepseek-v4-flash") - messages = [ProviderMessage(role=MessageRole.USER, text="what is the capital of France?")] - stream = await provider.stream( - model, - "You are a knowledgable geography expert", - messages, - ) - - while chunk := await stream.next(): - if chunk.text: - print(chunk.text, end="") - if chunk.usage_json: - usage = json.loads(chunk.usage_json) - print(f"\nusage: {usage}") - print() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/crates/goose-sdk/justfile b/crates/goose-sdk/justfile index fe49ac8d3e..4d7acf0151 100644 --- a/crates/goose-sdk/justfile +++ b/crates/goose-sdk/justfile @@ -2,164 +2,37 @@ set shell := ["bash", "-cu"] set working-directory := '../..' lib_ext := if os() == "macos" { "dylib" } else if os() == "windows" { "dll" } else { "so" } -lib_prefix := if os() == "windows" { "" } else { "lib" } -lib_name := lib_prefix + "goose_sdk." + lib_ext -debug_lib_dir := "./target/debug" -release_lib_dir := "./target/release" -debug_lib_path := debug_lib_dir / lib_name -release_lib_path := release_lib_dir / lib_name -debug_bindgen := "./target/debug/goose-uniffi-bindgen" -release_bindgen := "./target/release/goose-uniffi-bindgen" +lib_dir := "./target/debug" +lib_path := lib_dir / "libgoose_sdk." + lib_ext +bindgen := "./target/debug/goose-uniffi-bindgen" config := "./crates/goose-sdk/uniffi.toml" gen_dir := "./crates/goose-sdk/generated" examples_dir := "./crates/goose-sdk/examples/uniffi" -python_dir := "./crates/goose-sdk/python" -python_package_dir := python_dir / "src/goose" -maven_dir := "./crates/goose-sdk/maven" -maven_resources_dir := maven_dir / "src/main/resources" -maven_kotlin_dir := maven_dir / "src/main/kotlin" -_default: +default: @just --list --justfile {{justfile()}} _build: cargo build -p goose-sdk --features uniffi -q -_build-release: - cargo build -p goose-sdk --features uniffi --release -q - _generate lang: _build - {{debug_bindgen}} generate --library {{debug_lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null - cp {{debug_lib_path}} {{gen_dir}}/ + {{bindgen}} generate --library {{lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null + cp {{lib_path}} {{gen_dir}}/ touch {{gen_dir}}/__init__.py python: (_generate "python") - DYLD_LIBRARY_PATH={{debug_lib_dir}} LD_LIBRARY_PATH={{debug_lib_dir}} \ - python3 {{examples_dir}}/provider.py + DYLD_LIBRARY_PATH={{lib_dir}} LD_LIBRARY_PATH={{lib_dir}} \ + python3 {{examples_dir}}/ping.py -kotlin: maven-package - cd {{examples_dir}}/kotlin && gradle --no-daemon run - -python-bindings profile="debug": - @case "{{profile}}" in \ - debug) cargo build -p goose-sdk --features uniffi -q; bindgen={{debug_bindgen}}; lib_path={{debug_lib_path}} ;; \ - release) cargo build -p goose-sdk --features uniffi --release -q; bindgen={{release_bindgen}}; lib_path={{release_lib_path}} ;; \ - *) echo 'profile must be debug or release' >&2; exit 1 ;; \ - esac; \ - rm -rf {{python_package_dir}}; \ - mkdir -p {{python_package_dir}}; \ - "$bindgen" generate --library "$lib_path" --config {{config}} --language python --no-format --out-dir {{python_package_dir}} 2>/dev/null; \ - mv {{python_package_dir}}/goose.py {{python_package_dir}}/__init__.py; \ - cp "$lib_path" {{python_package_dir}}/; \ - touch {{python_package_dir}}/py.typed - -python-wheel: (python-bindings "release") - rm -rf {{python_dir}}/build {{python_dir}}/dist {{python_dir}}/*.egg-info {{python_dir}}/src/*.egg-info - cd {{python_dir}} && UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple --from build pyproject-build --wheel - -python-check: python-wheel - python3 -m pip install --force-reinstall {{python_dir}}/dist/*.whl - python3 -c 'import goose; print(goose.__name__)' - -python-publish repository="pypi": python-wheel - UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine check {{python_dir}}/dist/*.whl - @if [ "{{repository}}" = "pypi" ]; then \ - UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine upload {{python_dir}}/dist/*.whl; \ - else \ - UV_NO_CONFIG=1 PIP_CONFIG_FILE=/dev/null PIP_INDEX_URL=https://pypi.org/simple uvx --default-index https://pypi.org/simple twine upload --repository {{repository}} {{python_dir}}/dist/*.whl; \ +kotlin: (_generate "kotlin") + @if [ ! -f {{examples_dir}}/jna.jar ]; then \ + curl -sSL -o {{examples_dir}}/jna.jar \ + https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \ fi - -maven-bindings profile="release": - @case "{{profile}}" in \ - debug) cargo build -p goose-sdk --features uniffi -q; lib_path={{debug_lib_path}} ;; \ - release) cargo build -p goose-sdk --features uniffi --release -q; lib_path={{release_lib_path}} ;; \ - *) echo 'profile must be debug or release' >&2; exit 1 ;; \ - esac; \ - crates/goose-sdk/scripts/prepare-maven-package.sh "$lib_path" - -maven-package: (maven-bindings "release") - cd {{maven_dir}} && ./gradlew --no-daemon publishToMavenLocal - -maven-publish: (maven-bindings "release") - cd {{maven_dir}} && ./gradlew --no-daemon publishAndReleaseToMavenCentral - -crates-publish dry_run="true": - @set -euo pipefail; \ - dry_run_flag=""; \ - if [ "{{dry_run}}" = "true" ]; then \ - dry_run_flag="--dry-run"; \ - elif [ "{{dry_run}}" != "false" ]; then \ - echo 'dry_run must be true or false' >&2; \ - exit 1; \ - fi; \ - for crate in \ - goose-provider-types \ - goose-sdk-types \ - goose-download-manager \ - goose-local-inference \ - goose-providers \ - goose-sdk; do \ - cargo publish -p "$crate" $dry_run_flag --allow-dirty; \ - done - -bump-version rust_version: - #!/usr/bin/env bash - set -euo pipefail - python3 - "{{rust_version}}" <<'PY' - import re - import sys - from pathlib import Path - - rust_version = sys.argv[1] - match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-alpha\.(\d+))?", rust_version) - if not match: - raise SystemExit("rust_version must look like 0.1.0 or 0.1.0-alpha.0") - - major, minor, patch, alpha = match.groups() - python_version = f"{major}.{minor}.{patch}" if alpha is None else f"{major}.{minor}.{patch}a{alpha}" - - crate_names = [ - "goose-provider-types", - "goose-sdk-types", - "goose-download-manager", - "goose-local-inference", - "goose-providers", - "goose-sdk", - ] - crate_paths = {name: Path("crates") / name / "Cargo.toml" for name in crate_names} - - def replace_unique(path: Path, pattern: str, replacement: str) -> None: - text = path.read_text() - new_text, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE) - if count != 1: - raise SystemExit(f"expected one match for {pattern!r} in {path}") - path.write_text(new_text) - - for path in crate_paths.values(): - replace_unique(path, r'^version\s*=\s*"[^"]+"', f'version = "{rust_version}"') - - dependency_names = [ - "goose-provider-types", - "goose-sdk-types", - "goose-download-manager", - "goose-local-inference", - "goose-providers", - ] - for path in crate_paths.values(): - text = path.read_text() - for dependency in dependency_names: - text = re.sub( - rf'({re.escape(dependency)}\s*=\s*[^\n]*version\s*=\s*)"[^"]+"', - rf'\1"{rust_version}"', - text, - ) - path.write_text(text) - - pyproject = Path("crates/goose-sdk/python/pyproject.toml") - replace_unique(pyproject, r'^version\s*=\s*"[^"]+"', f'version = "{python_version}"') - - print(f"Rust crates: {rust_version}") - print(f"Python package: {python_version}") - print(f"Maven artifact: {rust_version}") - PY - cargo fmt --all + kotlinc -cp {{examples_dir}}/jna.jar -nowarn \ + {{gen_dir}}/aaif/goose/aaif_goose.kt \ + {{examples_dir}}/Ping.kt \ + -include-runtime -d {{examples_dir}}/ping.jar 2>/dev/null + java -Djna.library.path={{lib_dir}} \ + --enable-native-access=ALL-UNNAMED \ + -cp {{examples_dir}}/ping.jar:{{examples_dir}}/jna.jar aaif.example.PingKt diff --git a/crates/goose-sdk/maven/README.md b/crates/goose-sdk/maven/README.md deleted file mode 100644 index 0f3a913122..0000000000 --- a/crates/goose-sdk/maven/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Goose SDK Maven package - -This project packages the UniFFI-generated Kotlin/JVM bindings for `goose-sdk` -as the Maven artifact `io.github.aaif-goose:gdk`. - -The artifact version is read from `crates/goose-sdk/Cargo.toml`, so it stays in -lockstep with the Rust crate version. The jar includes the generated Kotlin API -and native libraries under JNA platform resource directories. - -Build locally from the repository root: - -```bash -just --justfile crates/goose-sdk/justfile maven-package -``` - -Publish to Maven Central from the repository root: - -```bash -just --justfile crates/goose-sdk/justfile maven-publish -``` - -Publishing requires the standard Gradle properties used by -`com.vanniktech.maven.publish` for Maven Central credentials and in-memory PGP -signing, for example via environment variables: - -- `ORG_GRADLE_PROJECT_mavenCentralUsername` -- `ORG_GRADLE_PROJECT_mavenCentralPassword` -- `ORG_GRADLE_PROJECT_signingInMemoryKey` -- `ORG_GRADLE_PROJECT_signingInMemoryKeyPassword` diff --git a/crates/goose-sdk/maven/build.gradle.kts b/crates/goose-sdk/maven/build.gradle.kts deleted file mode 100644 index 97448236fc..0000000000 --- a/crates/goose-sdk/maven/build.gradle.kts +++ /dev/null @@ -1,86 +0,0 @@ -plugins { - kotlin("jvm") version "2.2.21" - `java-library` - id("com.vanniktech.maven.publish") version "0.34.0" -} - -group = "io.github.aaif-goose" -version = gooseSdkVersion() - -fun gooseSdkVersion(): String { - val cargoToml = file("../Cargo.toml").readText() - return Regex("(?m)^version\\s*=\\s*\"([^\"]+)\"") - .find(cargoToml) - ?.groupValues - ?.get(1) - ?: error("Could not find goose-sdk version in ../Cargo.toml") -} - -kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) - } -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - withSourcesJar() -} - -dependencies { - api("net.java.dev.jna:jna:5.14.0") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") -} - -tasks.jar { - manifest { - attributes( - "Implementation-Title" to "Goose SDK", - "Implementation-Version" to project.version, - ) - } -} - -tasks.withType().configureEach { - dependsOn(tasks.named("plainJavadocJar")) -} - -mavenPublishing { - publishToMavenCentral(automaticRelease = true) - if (providers.gradleProperty("signingInMemoryKey").isPresent) { - signAllPublications() - } - - coordinates( - groupId = "io.github.aaif-goose", - artifactId = "gdk", - version = project.version.toString(), - ) - - pom { - name.set("Goose GDK") - description.set("Kotlin/JVM bindings for the Goose SDK") - inceptionYear.set("2026") - url.set("https://github.com/aaif-goose/goose") - licenses { - license { - name.set("Apache License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - distribution.set("repo") - } - } - developers { - developer { - id.set("aaif") - name.set("Agentic AI Foundation") - email.set("ai-oss-tools@block.xyz") - } - } - scm { - connection.set("scm:git:https://github.com/aaif-goose/goose.git") - developerConnection.set("scm:git:ssh://git@github.com/aaif-goose/goose.git") - url.set("https://github.com/aaif-goose/goose") - } - } -} diff --git a/crates/goose-sdk/maven/gradlew b/crates/goose-sdk/maven/gradlew deleted file mode 100755 index c42a357e4e..0000000000 --- a/crates/goose-sdk/maven/gradlew +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if command -v gradle >/dev/null 2>&1; then - exec gradle "$@" -fi - -echo "gradle is required to build the goose-sdk Maven package" >&2 -echo "Install gradle or run inside the Hermit environment." >&2 -exit 1 diff --git a/crates/goose-sdk/maven/settings.gradle.kts b/crates/goose-sdk/maven/settings.gradle.kts deleted file mode 100644 index e1289af9bd..0000000000 --- a/crates/goose-sdk/maven/settings.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - } -} - -rootProject.name = "goose-sdk-maven" diff --git a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/NativeLibraryLoader.kt b/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/NativeLibraryLoader.kt deleted file mode 100644 index a4958f6551..0000000000 --- a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/NativeLibraryLoader.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.aaif_goose - -import com.sun.jna.Platform -import java.nio.file.Files - -internal object NativeLibraryLoader { - init { - val componentName = "goose" - if (System.getProperty("uniffi.component.$componentName.libraryOverride") == null) { - val resource = nativeResourcePath() - val stream = NativeLibraryLoader::class.java.classLoader.getResourceAsStream(resource) - ?: error("Goose SDK native library resource not found: $resource") - val library = Files.createTempFile("goose-sdk-", nativeLibraryFileName()).toFile() - library.deleteOnExit() - stream.use { input -> library.outputStream().use { output -> input.copyTo(output) } } - System.setProperty("uniffi.component.$componentName.libraryOverride", library.absolutePath) - } - } - - fun ensureLoaded() = Unit - - private fun nativeResourcePath(): String = "${jnaResourcePrefix()}/${nativeLibraryFileName()}" - - private fun nativeLibraryFileName(): String = when (osName()) { - "darwin" -> "libgoose_sdk.dylib" - "linux" -> "libgoose_sdk.so" - "win32" -> "goose_sdk.dll" - else -> error("Unsupported OS: ${System.getProperty("os.name")}") - } - - private fun jnaResourcePrefix(): String = "${osName()}-${archName()}" - - private fun osName(): String = when { - System.getProperty("os.name").startsWith("Mac OS X") -> "darwin" - System.getProperty("os.name").startsWith("Linux") -> "linux" - System.getProperty("os.name").startsWith("Windows") -> "win32" - else -> System.getProperty("os.name").lowercase().replace(Regex("\\s+"), "-") - } - - private fun archName(): String = when { - Platform.isARM() && Platform.is64Bit() -> "aarch64" - Platform.isIntel() && Platform.is64Bit() -> "x86-64" - else -> System.getProperty("os.arch") - } -} diff --git a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/ProviderFlow.kt b/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/ProviderFlow.kt deleted file mode 100644 index d52b61929f..0000000000 --- a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/ProviderFlow.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.aaif_goose - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -public fun Provider.streamFlow( - model: ProviderModelConfig, - system: String, - messages: List, -): Flow = flow { - val stream = stream(model, system, messages) - while (true) { - emit(stream.next() ?: break) - } -} diff --git a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/databricks/Databricks.kt b/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/databricks/Databricks.kt deleted file mode 100644 index 1fe49dc4e4..0000000000 --- a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/databricks/Databricks.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.aaif_goose.providers.databricks - -public fun provider(host: String, token: String): io.github.aaif_goose.Provider = - io.github.aaif_goose.databricksProvider(host, token) - -public fun defaultModel(): String = io.github.aaif_goose.databricksDefaultModel() diff --git a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/openai/OpenAi.kt b/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/openai/OpenAi.kt deleted file mode 100644 index 9a52638fb3..0000000000 --- a/crates/goose-sdk/maven/src/support/kotlin/io/github/aaif_goose/providers/openai/OpenAi.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.aaif_goose.providers.openai - -public fun provider(apiKey: String): io.github.aaif_goose.Provider = io.github.aaif_goose.openaiProvider(apiKey) - -public fun defaultModel(): String = io.github.aaif_goose.openaiDefaultModel() diff --git a/crates/goose-sdk/python/README.md b/crates/goose-sdk/python/README.md deleted file mode 100644 index 492d4d4fcf..0000000000 --- a/crates/goose-sdk/python/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# goose-sdk - -Python bindings for the Goose SDK. - -This package is generated from the Rust `goose-sdk` crate using UniFFI. - -## Build a local wheel - -From the repository root: - -```bash -just --justfile crates/goose-sdk/justfile python-wheel -``` - -The wheel is written to `crates/goose-sdk/python/dist/`. diff --git a/crates/goose-sdk/python/pyproject.toml b/crates/goose-sdk/python/pyproject.toml deleted file mode 100644 index c12f5b9b30..0000000000 --- a/crates/goose-sdk/python/pyproject.toml +++ /dev/null @@ -1,34 +0,0 @@ -[build-system] -requires = ["setuptools>=69", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "goose-sdk" -version = "0.1.0a0" -description = "Python bindings for the Goose SDK" -readme = "README.md" -requires-python = ">=3.9" -license = "Apache-2.0" -authors = [{ name = "AAIF", email = "ai-oss-tools@block.xyz" }] -keywords = ["goose", "sdk", "ai", "agent", "uniffi"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Rust", -] - -[project.urls] -Homepage = "https://github.com/aaif-goose/goose" -Repository = "https://github.com/aaif-goose/goose" -Issues = "https://github.com/aaif-goose/goose/issues" - -[tool.setuptools] -package-dir = { "" = "src" } - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools.package-data] -goose = ["*.so", "*.dylib", "*.dll", "py.typed"] diff --git a/crates/goose-sdk/python/setup.py b/crates/goose-sdk/python/setup.py deleted file mode 100644 index 5166d0ee97..0000000000 --- a/crates/goose-sdk/python/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from setuptools import setup -from wheel.bdist_wheel import bdist_wheel - - -class BinaryWheel(bdist_wheel): - def finalize_options(self): - super().finalize_options() - self.root_is_pure = False - - def get_tag(self): - _, _, platform_tag = super().get_tag() - return "py3", "none", platform_tag - - -setup(cmdclass={"bdist_wheel": BinaryWheel}) diff --git a/crates/goose-sdk/scripts/maven-resource-prefix.sh b/crates/goose-sdk/scripts/maven-resource-prefix.sh deleted file mode 100755 index fa8ece254f..0000000000 --- a/crates/goose-sdk/scripts/maven-resource-prefix.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -case "$(uname -s)-$(uname -m)" in - Darwin-arm64) echo "darwin-aarch64" ;; - Darwin-x86_64) echo "darwin-x86-64" ;; - Linux-x86_64) echo "linux-x86-64" ;; - MINGW64_NT-*-x86_64|MSYS_NT-*-x86_64|CYGWIN_NT-*-x86_64) echo "win32-x86-64" ;; - *) echo "unsupported platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;; -esac diff --git a/crates/goose-sdk/scripts/prepare-maven-package.sh b/crates/goose-sdk/scripts/prepare-maven-package.sh deleted file mode 100755 index f99f316a2f..0000000000 --- a/crates/goose-sdk/scripts/prepare-maven-package.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ "$#" -lt 1 ]; then - echo "usage: $0 [jna-resource-prefix]" >&2 - exit 1 -fi - -native_lib="$1" -resource_prefix="${2:-$(crates/goose-sdk/scripts/maven-resource-prefix.sh)}" - -case "$resource_prefix" in - darwin-aarch64|linux-x86-64|win32-x86-64) ;; - *) echo "unsupported JNA resource prefix: $resource_prefix" >&2; exit 1 ;; -esac - -if [ ! -f "$native_lib" ]; then - echo "native library not found: $native_lib" >&2 - exit 1 -fi - -bindgen="target/release/goose-uniffi-bindgen" -if [ ! -x "$bindgen" ]; then - cargo build -p goose-sdk --features uniffi --release -q -fi - -maven_dir="crates/goose-sdk/maven" -kotlin_dir="$maven_dir/src/main/kotlin" -support_kotlin_dir="$maven_dir/src/support/kotlin" -resources_dir="$maven_dir/src/main/resources" - -rm -rf "$kotlin_dir/io/github/aaif_goose" "$resources_dir/$resource_prefix" -mkdir -p "$kotlin_dir" "$resources_dir/$resource_prefix" "$resources_dir/META-INF" -cp LICENSE "$resources_dir/META-INF/LICENSE" - -"$bindgen" generate \ - --library "$native_lib" \ - --config crates/goose-sdk/uniffi.toml \ - --language kotlin \ - --no-format \ - --out-dir "$kotlin_dir" 2>/dev/null - -python3 - "$kotlin_dir/io/github/aaif_goose/goose.kt" <<'PY' -import sys -from pathlib import Path - -path = Path(sys.argv[1]) -text = path.read_text() -needle = 'private fun findLibraryName(componentName: String): String {\n' -replacement = needle + ' NativeLibraryLoader.ensureLoaded()\n' -if needle not in text: - raise SystemExit('could not find findLibraryName in generated Kotlin bindings') -path.write_text(text.replace(needle, replacement, 1)) -PY - -cp -R "$support_kotlin_dir"/. "$kotlin_dir"/ -cp "$native_lib" "$resources_dir/$resource_prefix/" diff --git a/crates/goose-sdk/src/bindings.rs b/crates/goose-sdk/src/bindings.rs index f6728344d7..a601a744c0 100644 --- a/crates/goose-sdk/src/bindings.rs +++ b/crates/goose-sdk/src/bindings.rs @@ -1,22 +1,14 @@ //! In-process uniffi bindings for the Goose SDK. //! -//! This is the API surface exposed to Python and Kotlin. It currently focuses -//! on declarative providers: consumers can construct a provider from JSON and -//! stream completions from it. +//! This is the published API surface exposed to Python and Kotlin. Right now it +//! is a minimal `ping` -> `pong` round-trip that proves the uniffi +//! infrastructure end to end without depending on the `goose` core crate. +//! +//! To build the real SDK, add `goose` (and whatever else you need) as +//! dependencies and replace the [`Client`] methods below with the actual +//! agent surface. -use std::{future::Future, sync::Arc, sync::OnceLock}; - -use futures::StreamExt; -use goose_providers::{ - api_client::{ApiClient, AuthMethod}, - base::{MessageStream, Provider as GooseProvider}, - conversation::message::Message, - databricks::DatabricksProvider as GooseDatabricksProvider, - databricks_auth::DatabricksAuth, - declarative::EnvKeyResolver, - model::ModelConfig, - openai::OpenAiProviderBuilder, -}; +use std::sync::Arc; /// Errors surfaced across the uniffi boundary. #[derive(Debug, thiserror::Error, uniffi::Error)] @@ -25,304 +17,53 @@ pub enum GooseError { Generic(String), } -impl From for GooseError { - fn from(error: anyhow::Error) -> Self { - Self::Generic(error.to_string()) - } -} - -impl From for GooseError { - fn from(error: goose_providers::errors::ProviderError) -> Self { - Self::Generic(error.to_string()) - } -} - -impl From for GooseError { - fn from(error: serde_json::Error) -> Self { - Self::Generic(error.to_string()) - } -} - -/// A text message passed to a provider. +/// A reply to a [`Client::ping`] call. #[derive(Debug, Clone, uniffi::Record)] -pub struct ProviderMessage { - pub role: MessageRole, - pub text: String, +pub struct Pong { + /// Echo of the message that was pinged. + pub message: String, } -/// Supported message roles for provider requests and streamed responses. -#[derive(Debug, Clone, uniffi::Enum)] -pub enum MessageRole { - User, - Assistant, -} - -impl ProviderMessage { - fn to_goose_message(&self) -> Message { - match self.role { - MessageRole::User => Message::user().with_text(&self.text), - MessageRole::Assistant => Message::assistant().with_text(&self.text), - } - } -} - -/// Model selection and optional generation settings for a provider request. -#[derive(Debug, Clone, uniffi::Record)] -pub struct ProviderModelConfig { - pub model_name: String, - #[uniffi(default = None)] - pub context_limit: Option, - #[uniffi(default = None)] - pub temperature: Option, - #[uniffi(default = None)] - pub max_tokens: Option, - #[uniffi(default = false)] - pub toolshim: bool, - #[uniffi(default = None)] - pub toolshim_model: Option, - /// Provider-specific request parameters as a JSON object string. - #[uniffi(default = None)] - pub request_params_json: Option, - #[uniffi(default = None)] - pub reasoning: Option, -} - -impl ProviderModelConfig { - fn to_goose_model_config(&self) -> Result { - let mut config = ModelConfig::new(&self.model_name) - .with_context_limit(self.context_limit.map(|limit| limit as usize)) - .with_temperature(self.temperature) - .with_max_tokens(self.max_tokens) - .with_toolshim(self.toolshim) - .with_toolshim_model(self.toolshim_model.clone()); - - if let Some(request_params_json) = &self.request_params_json { - let request_params = serde_json::from_str(request_params_json)?; - config = config.with_merged_request_params(request_params); - } - - config.reasoning = self.reasoning; - Ok(config) - } -} - -/// One item yielded by a provider stream. -#[derive(Debug, Clone, uniffi::Record)] -pub struct ProviderStreamChunk { - /// The concatenated text content in this message chunk, if one was emitted. - pub text: Option, - /// Full Goose message JSON for callers that need non-text content such as tool requests. - pub message_json: Option, - /// Provider usage JSON when the provider emits usage metadata. - pub usage_json: Option, -} - -static RUNTIME: OnceLock = OnceLock::new(); - -fn runtime() -> Result<&'static tokio::runtime::Runtime, GooseError> { - if let Some(runtime) = RUNTIME.get() { - return Ok(runtime); - } - - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .map_err(|error| GooseError::Generic(error.to_string()))?; - - let _ = RUNTIME.set(runtime); - Ok(RUNTIME.get().expect("runtime was initialized")) -} - -async fn run_on_runtime( - future: impl Future + Send + 'static, -) -> Result -where - T: Send + 'static, -{ - let (sender, receiver) = tokio::sync::oneshot::channel(); - runtime()?.spawn(async move { - let _ = sender.send(future.await); - }); - - receiver - .await - .map_err(|_| GooseError::Generic("runtime task was cancelled".to_string())) -} - -struct ProviderHandle { - provider: Arc, -} - -impl ProviderHandle { - fn new(provider: Box) -> Self { - Self { - provider: Arc::from(provider), - } - } - - fn name(&self) -> String { - self.provider.get_name().to_string() - } - - async fn stream( - &self, - model: ProviderModelConfig, - system: String, - messages: Vec, - ) -> Result, GooseError> { - let model = model.to_goose_model_config()?; - let messages = messages - .iter() - .map(ProviderMessage::to_goose_message) - .collect::>(); - let provider = Arc::clone(&self.provider); - let stream = - run_on_runtime(async move { provider.stream(&model, &system, &messages, &[]).await }) - .await??; - - Ok(Arc::new(ProviderStream { - stream: Arc::new(tokio::sync::Mutex::new(stream)), - })) - } -} - -/// A Goose provider backed by one of Goose's native provider implementations. +/// The top-level entry point for the Goose SDK. +/// +/// This is the object that consuming languages instantiate. Today it only knows +/// how to answer a ping; extend it with the real agent API. #[derive(uniffi::Object)] -pub struct Provider { - handle: ProviderHandle, -} +pub struct Client {} -impl Provider { - fn new(provider: Box) -> Arc { - Arc::new(Self { - handle: ProviderHandle::new(provider), +#[uniffi::export] +impl Client { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self {}) + } + + /// Round-trip a message through the SDK. Returns a [`Pong`] echoing the + /// supplied `message`, prefixed with `pong: `. + pub fn ping(&self, message: String) -> Result { + if message.is_empty() { + return Err(GooseError::Generic("message must not be empty".into())); + } + Ok(Pong { + message: format!("pong: {message}"), }) } } -#[uniffi::export] -impl Provider { - pub fn name(&self) -> String { - self.handle.name() - } - - /// Start a streaming completion request. Tools are not yet exposed over the - /// uniffi boundary, so this calls providers with an empty tool list. - pub async fn stream( - &self, - model: ProviderModelConfig, - system: String, - messages: Vec, - ) -> Result, GooseError> { - self.handle.stream(model, system, messages).await - } -} - -#[uniffi::export] -pub fn declarative_provider_from_json(json: String) -> Result, GooseError> { - let provider = goose_providers::declarative::from_json(&json, None, EnvKeyResolver {})?; - Ok(Provider::new(provider)) -} - -#[uniffi::export] -pub fn openai_default_model() -> String { - goose_providers::openai::OPEN_AI_DEFAULT_MODEL.to_string() -} - -#[uniffi::export] -pub fn openai_provider(api_key: String) -> Result, GooseError> { - let api_client = ApiClient::new_with_tls( - "https://api.openai.com".to_string(), - AuthMethod::BearerToken(api_key), - None, - )?; - let provider = OpenAiProviderBuilder::new(api_client).build(); - Ok(Provider::new(Box::new(provider))) -} - -#[uniffi::export] -pub fn databricks_default_model() -> String { - goose_providers::databricks::DATABRICKS_DEFAULT_MODEL.to_string() -} - -#[uniffi::export] -pub fn databricks_provider(host: String, token: String) -> Result, GooseError> { - let retry_config = GooseDatabricksProvider::load_retry_config(|key| std::env::var(key).ok()); - let provider = GooseDatabricksProvider::new( - host, - DatabricksAuth::token(token), - retry_config, - None, - None, - None, - None, - None, - None, - None, - )?; - - Ok(Provider::new(Box::new(provider))) -} - -/// An async iterator over provider stream chunks. -#[derive(uniffi::Object)] -pub struct ProviderStream { - stream: Arc>, -} - -#[uniffi::export] -impl ProviderStream { - /// Return the next stream chunk, or `None` when the stream is exhausted. - pub async fn next(&self) -> Result, GooseError> { - let stream = Arc::clone(&self.stream); - run_on_runtime(async move { - let mut stream = stream.lock().await; - let Some((message, usage)) = stream.next().await.transpose()? else { - return Ok(None); - }; - - let text = message.as_ref().map(Message::as_concat_text); - let message_json = message.as_ref().map(serde_json::to_string).transpose()?; - let usage_json = usage.as_ref().map(serde_json::to_string).transpose()?; - - Ok(Some(ProviderStreamChunk { - text, - message_json, - usage_json, - })) - }) - .await? - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn model_config_rejects_invalid_request_params_json() { - let config = ProviderModelConfig { - model_name: "test".to_string(), - context_limit: None, - temperature: None, - max_tokens: None, - toolshim: false, - toolshim_model: None, - request_params_json: Some("not json".to_string()), - reasoning: None, - }; - - assert!(config.to_goose_model_config().is_err()); + fn ping_returns_pong() { + let client = Client::new(); + let pong = client.ping("aaif.io".into()).expect("ping should succeed"); + assert_eq!(pong.message, "pong: aaif.io"); } #[test] - fn provider_message_converts_user_text() { - let message = ProviderMessage { - role: MessageRole::User, - text: "what is the capital of France?".to_string(), - } - .to_goose_message(); - - assert_eq!(message.as_concat_text(), "what is the capital of France?"); + fn empty_ping_errors() { + let client = Client::new(); + assert!(client.ping(String::new()).is_err()); } } diff --git a/crates/goose-sdk/src/lib.rs b/crates/goose-sdk/src/lib.rs index 4742ea2de8..bab0fa2a52 100644 --- a/crates/goose-sdk/src/lib.rs +++ b/crates/goose-sdk/src/lib.rs @@ -5,15 +5,17 @@ //! that talks to `goose acp` over stdio. //! //! With `--features uniffi` the crate additionally compiles as a -//! `cdylib`/`staticlib` and exposes an in-process API to Python and Kotlin via -//! [uniffi-rs](https://github.com/mozilla/uniffi-rs). The current uniffi surface -//! lets callers construct declarative providers from JSON and stream provider -//! completions. +//! `cdylib`/`staticlib` and exposes a small in-process API to Python and Kotlin +//! via [uniffi-rs](https://github.com/mozilla/uniffi-rs). +//! +//! The published uniffi surface is intentionally a single `ping` -> `pong` +//! round-trip. It exists as a working scaffold for adding the real Goose SDK +//! API: replace [`bindings`] with the actual implementation. pub use goose_sdk_types::{custom_notifications, custom_requests}; #[cfg(feature = "uniffi")] -uniffi::setup_scaffolding!("goose"); +uniffi::setup_scaffolding!("aaif_goose"); #[cfg(feature = "uniffi")] pub mod bindings; diff --git a/crates/goose-sdk/uniffi.toml b/crates/goose-sdk/uniffi.toml index ceb625960a..7c78ef7121 100644 --- a/crates/goose-sdk/uniffi.toml +++ b/crates/goose-sdk/uniffi.toml @@ -1,2 +1,2 @@ [bindings.kotlin] -package_name = "io.github.aaif_goose" +package_name = "aaif.goose" diff --git a/crates/goose-server/ALLOWLIST.md b/crates/goose-server/ALLOWLIST.md new file mode 100644 index 0000000000..5634adfdb6 --- /dev/null +++ b/crates/goose-server/ALLOWLIST.md @@ -0,0 +1,60 @@ +IMPORTANT: currently GOOSE_ALLOWLIST is used in main.ts in ui/desktop, and not in goose-server. The following is for reference when it is used on the server side for launch time enforcement. + +# goose Extension Allowlist + +The allowlist feature provides a security mechanism for controlling which MCP commands can be used by goose. +By default, goose will let you run any MCP via any command, which isn't always desired. + +## How It Works + +1. When enabled, goose will only allow execution of commands that match entries in the allowlist +2. Commands not in the allowlist will be rejected with an error message +3. The allowlist is fetched from a URL specified by the `GOOSE_ALLOWLIST` environment variable and cached while running. + +## Setup + +Set the `GOOSE_ALLOWLIST` environment variable to the URL of your allowlist YAML file: + +```bash +export GOOSE_ALLOWLIST=https://example.com/goose-allowlist.yaml +``` + +If this environment variable is not set, no allowlist restrictions will be applied (all commands will be allowed). + +## Bypassing the Allowlist + +In certain development or testing scenarios, you may need to bypass the allowlist restrictions. You can do this by setting the `GOOSE_ALLOWLIST_BYPASS` environment variable to `true`: + +```bash +# For the GUI, you can have it show a warning instead of blocking (but it will always show a warning): +export GOOSE_ALLOWLIST_WARNING=true +``` + + +When this environment variable is set to `true` (case-insensitive), the allowlist check will be bypassed and all commands will be allowed, even if the `GOOSE_ALLOWLIST` environment variable is set. + +## Allowlist File Format + +The allowlist file should be a YAML file with the following structure: + +```yaml +extensions: + - id: extension-id-1 + command: command-name-1 + - id: extension-id-2 + command: command-name-2 +``` + +Example: + +```yaml +extensions: + - id: slack + command: uvx mcp_slack + - id: github + command: uvx mcp_github + - id: jira + command: uvx mcp_jira +``` + +Note that the command should be the full command to launch the MCP (environment variables are provided for context by goose). Additional arguments will be rejected (to avoid injection attacks) \ No newline at end of file diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml new file mode 100644 index 0000000000..a05618a332 --- /dev/null +++ b/crates/goose-server/Cargo.toml @@ -0,0 +1,109 @@ +[package] +name = "goose-server" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description.workspace = true + +[lints] +workspace = true + +[features] +default = [ + "code-mode", + "local-inference", + "aws-providers", + "telemetry", + "nostr", + "otel", + "rustls-tls", + "system-keyring", +] +code-mode = ["goose/code-mode"] +local-inference = ["goose/local-inference"] +aws-providers = ["goose/aws-providers"] +cuda = ["goose/cuda", "local-inference"] +vulkan = ["goose/vulkan", "local-inference"] +mlx = ["goose/mlx", "local-inference"] +telemetry = ["goose/telemetry"] +nostr = ["goose/nostr"] +otel = ["goose/otel"] +system-keyring = ["goose/system-keyring"] +portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] +rustls-tls = [ + "reqwest/rustls", + "axum-server/tls-rustls", + "dep:rustls", + "dep:aws-lc-rs", + "goose/rustls-tls", + "goose-mcp/rustls-tls", + "goose-providers/rustls-tls", +] +native-tls = [ + "reqwest/native-tls", + "axum-server/tls-openssl", + "dep:openssl", + "goose/native-tls", + "goose-mcp/native-tls", + "goose-providers/native-tls", +] + +[dependencies] +goose = { path = "../goose", default-features = false } +goose-mcp = { path = "../goose-mcp", default-features = false } +goose-providers = { path = "../goose-providers", default-features = false } +rmcp = { workspace = true } +axum = { workspace = true, features = ["ws", "macros"] } +tokio = { workspace = true } +chrono = { workspace = true } +tower-http = { workspace = true, features = ["cors"] } +serde = { workspace = true } +serde_json = { workspace = true, features = ["preserve_order"] } +futures = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "time"] } +tokio-stream = { workspace = true } +anyhow = { workspace = true } +bytes = { workspace = true } +http = { workspace = true } +base64 = { workspace = true } +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"] } +tokio-util = { workspace = true } +serde_path_to_error = { version = "0.1.8", default-features = false } +rand = { workspace = true } +hex = { version = "0.4.3", default-features = false, features = ["std"] } +rustls = { workspace = true, optional = true } +uuid = { workspace = true } +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", default-features = false } + +[[bin]] +name = "goosed" +path = "src/main.rs" + +[[bin]] +name = "generate_schema" +path = "src/bin/generate_schema.rs" + +[dev-dependencies] +tower = { version = "0.5.2", default-features = false } + +[package.metadata.cargo-machete] +ignored = [ + # Used only in windows + "winreg", +] diff --git a/crates/goose-server/build.rs b/crates/goose-server/build.rs new file mode 100644 index 0000000000..23a0fa399a --- /dev/null +++ b/crates/goose-server/build.rs @@ -0,0 +1,4 @@ +// We'll generate the schema at runtime since we need access to the complete application context +fn main() { + println!("cargo:rerun-if-changed=src/"); +} diff --git a/crates/goose-server/src/auth.rs b/crates/goose-server/src/auth.rs new file mode 100644 index 0000000000..c450b6427a --- /dev/null +++ b/crates/goose-server/src/auth.rs @@ -0,0 +1,31 @@ +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware::Next, + response::Response, +}; +pub use goose::acp::transport::auth::check_acp_token; +use goose::acp::transport::auth::token_matches; + +pub async fn check_token( + State(state): State, + request: Request, + next: Next, +) -> Result { + if request.uri().path() == "/status" + || request.uri().path() == "/mcp-app-proxy" + || request.uri().path() == "/mcp-app-guest" + { + return Ok(next.run(request).await); + } + let secret_key = request + .headers() + .get("X-Secret-Key") + .and_then(|value| value.to_str().ok()); + + if token_matches(secret_key, &state) { + Ok(next.run(request).await) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} diff --git a/crates/goose-server/src/bin/generate_schema.rs b/crates/goose-server/src/bin/generate_schema.rs new file mode 100644 index 0000000000..ec5bd33aad --- /dev/null +++ b/crates/goose-server/src/bin/generate_schema.rs @@ -0,0 +1,30 @@ +use goose_server::openapi; +use std::env; +use std::fs; +use std::path::PathBuf; + +fn main() { + let schema = openapi::generate_schema(); + + let package_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let output_path = PathBuf::from(package_dir) + .join("..") + .join("..") + .join("ui") + .join("desktop") + .join("openapi.json"); + + // Ensure parent directory exists + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + + fs::write(&output_path, format!("{schema}\n")).unwrap(); + eprintln!( + "Successfully generated OpenAPI schema at {}", + output_path.canonicalize().unwrap().display() + ); + + // Output the schema to stdout for piping + println!("{}", schema); +} diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs new file mode 100644 index 0000000000..1a4d6f0fa4 --- /dev/null +++ b/crates/goose-server/src/commands/agent.rs @@ -0,0 +1,148 @@ +use crate::configuration; +use crate::state; +use anyhow::Result; +use axum::middleware; +use axum_server::Handle; +use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig}; +use goose::acp::transport::create_acp_router; +use goose::agents::GoosePlatform; +use goose::config::paths::Paths; +use goose_server::auth::{check_acp_token, check_token}; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] +use goose_server::tls::setup_tls; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use tracing::info; + +fn boot_marker(message: &str) { + eprintln!("GOOSED_BOOT: {message}"); +} + +#[cfg(unix)] +async fn shutdown_signal() { + use tokio::signal::unix::{signal, SignalKind}; + + let mut sigint = signal(SignalKind::interrupt()).expect("failed to install SIGINT handler"); + let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler"); + + tokio::select! { + _ = sigint.recv() => {}, + _ = sigterm.recv() => {}, + } +} + +#[cfg(not(unix))] +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; +} + +pub async fn run() -> Result<()> { + // Install the rustls crypto provider early, before any spawned tasks (tunnel, etc.) + // try to open TLS connections. Both `ring` and `aws-lc-rs` + // features are enabled on rustls (via different transitive deps), so rustls + // cannot auto-detect a provider โ€” we must pick one explicitly. + #[cfg(feature = "rustls-tls")] + let _ = rustls::crypto::ring::default_provider().install_default(); + + boot_marker("main entered"); + crate::logging::setup_logging(Some("goosed"))?; + + let settings = configuration::Settings::new()?; + + let secret_key = std::env::var("GOOSE_SERVER__SECRET_KEY") + .unwrap_or_else(|_| hex::encode(rand::random::<[u8; 32]>())); + + boot_marker("appstate init start"); + let app_state = state::AppState::new(settings.tls).await?; + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + // TODO(acp-migration): When ui/desktop launches `goose serve` directly, + // move any goosed-only ACP setup into the goose serve path before deleting + // this bridge. In particular, verify everything ACP currently gets from + // goosed startup/AppState initialization, including builtin extension + // registration and the desktop platform identity. + let acp_server = Arc::new(AcpServer::new(AcpServerFactoryConfig { + builtins: vec!["developer".to_string()], + data_dir: Paths::data_dir(), + config_dir: Paths::config_dir(), + goose_platform: GoosePlatform::GooseDesktop, + additional_source_roots: Vec::new(), + scheduler: Some(app_state.scheduler()), + })); + + let rest_router = crate::routes::configure(app_state.clone(), secret_key.clone()).layer( + middleware::from_fn_with_state(secret_key.clone(), check_token), + ); + let acp_router = create_acp_router(acp_server).layer(middleware::from_fn_with_state( + secret_key.clone(), + check_acp_token, + )); + + let app = rest_router.merge(acp_router).layer(cors); + + let addr = settings.socket_addr(); + + if settings.tls { + #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] + { + boot_marker("tls setup start"); + let tls_setup = setup_tls( + settings.tls_cert_path.as_deref(), + settings.tls_key_path.as_deref(), + ) + .await?; + + let handle = Handle::new(); + let shutdown_handle = handle.clone(); + tokio::spawn(async move { + shutdown_signal().await; + shutdown_handle.graceful_shutdown(None); + }); + + info!("listening on https://{}", addr); + boot_marker("listening"); + + #[cfg(feature = "rustls-tls")] + axum_server::bind_rustls(addr, tls_setup.config) + .handle(handle) + .serve(app.into_make_service()) + .await?; + + #[cfg(feature = "native-tls")] + axum_server::bind_openssl(addr, tls_setup.config) + .handle(handle) + .serve(app.into_make_service()) + .await?; + } + + #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))] + { + anyhow::bail!( + "TLS was requested but no TLS backend is enabled. \ + Enable the `rustls-tls` or `native-tls` feature." + ); + } + } else { + boot_marker("tcp bind start"); + let listener = tokio::net::TcpListener::bind(addr).await?; + + info!("listening on http://{}", addr); + boot_marker("listening"); + + axum::serve(listener, app) + .with_graceful_shutdown(async { shutdown_signal().await }) + .await?; + } + + #[cfg(feature = "otel")] + if goose::otel::otlp::is_otlp_initialized() { + goose::otel::otlp::shutdown_otlp(); + } + + info!("server shutdown complete"); + Ok(()) +} diff --git a/crates/goose-server/src/commands/mod.rs b/crates/goose-server/src/commands/mod.rs new file mode 100644 index 0000000000..f17bc55db8 --- /dev/null +++ b/crates/goose-server/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod agent; diff --git a/crates/goose-server/src/configuration.rs b/crates/goose-server/src/configuration.rs new file mode 100644 index 0000000000..c6f4583671 --- /dev/null +++ b/crates/goose-server/src/configuration.rs @@ -0,0 +1,102 @@ +use crate::error::{to_env_var, ConfigError}; +use config::{Config, Environment}; +use serde::Deserialize; +use std::net::SocketAddr; + +#[derive(Debug, Default, Deserialize)] +pub struct Settings { + #[serde(default = "default_host")] + pub host: String, + #[serde(default = "default_port")] + pub port: u16, + #[serde(default = "default_tls")] + pub tls: bool, + pub tls_cert_path: Option, + pub tls_key_path: Option, +} + +impl Settings { + pub fn socket_addr(&self) -> SocketAddr { + format!("{}:{}", self.host, self.port) + .parse() + .expect("Failed to parse socket address") + } + + pub fn new() -> Result { + Self::load_and_validate() + } + + fn load_and_validate() -> Result { + // Start with default configuration + let config = Config::builder() + // Server defaults + .set_default("host", default_host())? + .set_default("port", default_port())? + .set_default("tls", default_tls())? + // Layer on the environment variables + .add_source( + Environment::with_prefix("GOOSE") + .prefix_separator("_") + .separator("__") + .try_parsing(true), + ) + .build()?; + + // Try to deserialize the configuration + let result: Result = config.try_deserialize(); + + // Handle missing field errors specially + match result { + Ok(settings) => Ok(settings), + Err(err) => { + tracing::debug!("Configuration error: {:?}", &err); + + // Handle both NotFound and missing field message variants + let error_str = err.to_string(); + if error_str.starts_with("missing field") { + // Extract field name from error message "missing field `type`" + let field = error_str + .trim_start_matches("missing field `") + .trim_end_matches("`"); + let env_var = to_env_var(field); + Err(ConfigError::MissingEnvVar { env_var }) + } else if let config::ConfigError::NotFound(field) = &err { + let env_var = to_env_var(field); + Err(ConfigError::MissingEnvVar { env_var }) + } else { + Err(ConfigError::Other(err)) + } + } + } + } +} + +fn default_host() -> String { + "127.0.0.1".to_string() +} + +fn default_port() -> u16 { + 3000 +} + +fn default_tls() -> bool { + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_socket_addr_conversion() { + let server_settings = Settings { + host: "127.0.0.1".to_string(), + port: 3000, + tls: true, + tls_cert_path: None, + tls_key_path: None, + }; + let addr = server_settings.socket_addr(); + assert_eq!(addr.to_string(), "127.0.0.1:3000"); + } +} diff --git a/crates/goose-server/src/error.rs b/crates/goose-server/src/error.rs new file mode 100644 index 0000000000..5f38f85f1c --- /dev/null +++ b/crates/goose-server/src/error.rs @@ -0,0 +1,40 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConfigError { + #[error("Missing required environment variable: {env_var}")] + MissingEnvVar { env_var: String }, + #[error("Configuration error: {0}")] + Other(#[from] config::ConfigError), +} + +// Helper function to format environment variable names +pub(crate) fn to_env_var(field_path: &str) -> String { + // Handle nested fields by converting dots to double underscores + // If the field is in the provider object, we need to prefix it appropriately + let normalized_path = if field_path == "type" { + "provider.type".to_string() + } else if field_path.starts_with("provider.") { + field_path.to_string() + } else { + format!("provider.{}", field_path) + }; + + format!( + "GOOSE_{}", + normalized_path.replace('.', "__").to_uppercase() + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_env_var_conversion() { + assert_eq!(to_env_var("type"), "GOOSE_PROVIDER__TYPE"); + assert_eq!(to_env_var("api_key"), "GOOSE_PROVIDER__API_KEY"); + assert_eq!(to_env_var("provider.host"), "GOOSE_PROVIDER__HOST"); + assert_eq!(to_env_var("provider.api_key"), "GOOSE_PROVIDER__API_KEY"); + } +} diff --git a/crates/goose-server/src/lib.rs b/crates/goose-server/src/lib.rs new file mode 100644 index 0000000000..551a2fdbd7 --- /dev/null +++ b/crates/goose-server/src/lib.rs @@ -0,0 +1,18 @@ +#[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"); + +pub mod auth; +pub mod configuration; +pub mod error; +pub mod openapi; +pub mod routes; +pub mod session_event_bus; +pub mod state; +#[cfg(any(feature = "rustls-tls", feature = "native-tls"))] +pub mod tls; +// Re-export commonly used items +pub use openapi::*; +pub use state::*; diff --git a/crates/goose-server/src/logging.rs b/crates/goose-server/src/logging.rs new file mode 100644 index 0000000000..499cd11845 --- /dev/null +++ b/crates/goose-server/src/logging.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use goose::providers::utils::init_goose_request_log; +use tracing_subscriber::util::SubscriberInitExt; + +/// Sets up the logging infrastructure for the server. +/// Logs go to a JSON file and a pretty console layer on stderr. +pub fn setup_logging(name: Option<&str>) -> Result<()> { + init_goose_request_log()?; + let config = goose::logging::LoggingConfig { + component: "server", + name, + extra_directives: &["goose_server=info", "tower_http=info"], + console: true, + json: false, + }; + let subscriber = goose::logging::build_logging_subscriber(&config)?; + subscriber.try_init()?; + Ok(()) +} diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs new file mode 100644 index 0000000000..cfde78440a --- /dev/null +++ b/crates/goose-server/src/main.rs @@ -0,0 +1,106 @@ +mod commands; +mod configuration; +mod error; +mod logging; +mod openapi; +mod routes; +mod session_event_bus; +mod state; +use std::path::PathBuf; +use std::{backtrace::Backtrace, panic::PanicHookInfo}; + +use clap::{Parser, Subcommand}; +use goose::agents::validate_extensions; +use goose_mcp::{ + mcp_server_runner::{serve, McpCommand}, + AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer, +}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Run the agent server + Agent, + /// Run the MCP server + Mcp { + #[arg(value_parser = clap::value_parser!(McpCommand))] + server: McpCommand, + }, + /// Validate a bundled-extensions JSON file + #[command(name = "validate-extensions")] + ValidateExtensions { + /// Path to the bundled-extensions JSON file + path: PathBuf, + }, +} + +fn boot_marker(message: &str) { + eprintln!("GOOSED_BOOT: {message}"); +} + +fn install_panic_hook() { + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info: &PanicHookInfo<'_>| { + let location = panic_info + .location() + .map(|location| format!("{}:{}", location.file(), location.line())) + .unwrap_or_else(|| "unknown".to_string()); + + let payload = panic_info + .payload() + .downcast_ref::<&str>() + .map(|msg| (*msg).to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap_or_else(|| "unknown panic payload".to_string()); + + eprintln!("GOOSED_BOOT: panic at {location}: {payload}"); + eprintln!("GOOSED_BOOT: backtrace:\n{}", Backtrace::force_capture()); + + default_hook(panic_info); + })); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + install_panic_hook(); + boot_marker("main entered"); + + let cli = Cli::parse(); + boot_marker(&format!( + "command parsed: {:?}", + std::mem::discriminant(&cli.command) + )); + + match cli.command { + Commands::Agent => { + commands::agent::run().await?; + } + Commands::Mcp { server } => { + logging::setup_logging(Some(&format!("mcp-{}", server.name())))?; + match server { + McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?, + McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?, + McpCommand::Memory => serve(MemoryServer::new()).await?, + McpCommand::Tutorial => serve(TutorialServer::new()).await?, + } + } + Commands::ValidateExtensions { path } => { + match validate_extensions::validate_bundled_extensions(&path) { + Ok(msg) => println!("{msg}"), + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + } + } + } + + Ok(()) +} diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs new file mode 100644 index 0000000000..5f0d34c76f --- /dev/null +++ b/crates/goose-server/src/openapi.rs @@ -0,0 +1,698 @@ +use goose::agents::extension::Envs; +use goose::agents::extension::ToolInfo; +use goose::agents::ExtensionConfig; +use goose::config::permission::PermissionLevel; +use goose::config::ExtensionEntry; +use goose::conversation::token_usage::Usage; +use goose::conversation::Conversation; +use goose::download_manager::{DownloadProgress, DownloadStatus}; +use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType}; +use goose::session::{ + DiagnosticsConfig, DiagnosticsError, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, + DiagnosticsPrompt, DiagnosticsReport, DiagnosticsScheduledRecipe, DiagnosticsTextFile, Session, + SessionType, SystemInfo, +}; +use goose_providers::model::ModelConfig; +use goose_providers::permission::Permission; +use goose_providers::permission::PrincipalType; +use goose_providers::thinking::ThinkingEffort; +use rmcp::model::{ + Annotations, Content, EmbeddedResource, Icon, IconTheme, ImageContent, JsonObject, + RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, + ResourceContents, Role, TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution, +}; +use utoipa::{OpenApi, ToSchema}; + +use goose::config::declarative_providers::{ + DeclarativeProviderConfig, EnvVarConfig, LoadedProvider, ProviderEngine, +}; +use goose::conversation::message::{ + ActionRequired, ActionRequiredData, FrontendToolRequest, InferenceMetadata, Message, + MessageContent, MessageMetadata, RedactedThinkingContent, SystemNotificationContent, + SystemNotificationType, ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest, + ToolResponse, +}; + +use crate::routes::recipe_utils::RecipeManifest; +use crate::routes::reply::MessageEvent; +use utoipa::openapi::schema::{ + AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema, + SchemaFormat, SchemaType, +}; +use utoipa::openapi::{AllOfBuilder, Ref, RefOr}; + +macro_rules! derive_utoipa { + ($inner_type:ident as $schema_name:ident) => { + pub struct $schema_name {} + + impl<'__s> ToSchema<'__s> for $schema_name { + fn schema() -> (&'__s str, utoipa::openapi::RefOr) { + let settings = rmcp::schemars::generate::SchemaSettings::openapi3(); + let generator = settings.into_generator(); + let schema = generator.into_root_schema_for::<$inner_type>(); + let schema = convert_schemars_to_utoipa(schema); + (stringify!($inner_type), schema) + } + + fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> { + Vec::new() + } + } + }; + ($inner_type:ident as $schema_name:ident => $output_name:expr) => { + pub struct $schema_name {} + + impl<'__s> ToSchema<'__s> for $schema_name { + fn schema() -> (&'__s str, utoipa::openapi::RefOr) { + let settings = rmcp::schemars::generate::SchemaSettings::openapi3(); + let generator = settings.into_generator(); + let schema = generator.into_root_schema_for::<$inner_type>(); + let schema = convert_schemars_to_utoipa(schema); + ($output_name, schema) + } + + fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> { + Vec::new() + } + } + }; +} + +fn convert_schemars_to_utoipa(schema: rmcp::schemars::Schema) -> RefOr { + if let Some(true) = schema.as_bool() { + return RefOr::T(Schema::Object(ObjectBuilder::new().build())); + } + + if let Some(false) = schema.as_bool() { + return RefOr::T(Schema::Object(ObjectBuilder::new().build())); + } + + if let Some(obj) = schema.as_object() { + return convert_json_object_to_utoipa(obj); + } + + RefOr::T(Schema::Object(ObjectBuilder::new().build())) +} + +fn convert_json_object_to_utoipa( + obj: &serde_json::Map, +) -> RefOr { + use serde_json::Value; + + if let Some(Value::String(reference)) = obj.get("$ref") { + return RefOr::Ref(Ref::new(reference.clone())); + } + + if let Some(Value::Array(one_of)) = obj.get("oneOf") { + let mut builder = OneOfBuilder::new(); + for item in one_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); + } + } + return RefOr::T(Schema::OneOf(builder.build())); + } + + // Handle the discriminated union pattern from schemars: an object with + // `type`, `properties`, `required` AND `allOf` (e.g. each variant of a + // `#[serde(tag = "type")]` enum). We merge the inline object (which carries + // the discriminator property) with the `allOf` refs into a single `allOf`. + if let Some(Value::Array(all_of)) = obj.get("allOf") { + let has_inline_properties = obj.contains_key("properties") || obj.contains_key("type"); + if has_inline_properties { + let mut builder = AllOfBuilder::new(); + // Build an object schema from the inline properties/required + let mut obj_without_allof = obj.clone(); + obj_without_allof.remove("allOf"); + builder = builder.item(convert_json_object_to_utoipa(&obj_without_allof)); + for item in all_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); + } + } + return RefOr::T(Schema::AllOf(builder.build())); + } + + let mut builder = AllOfBuilder::new(); + for item in all_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); + } + } + return RefOr::T(Schema::AllOf(builder.build())); + } + + if let Some(Value::Array(any_of)) = obj.get("anyOf") { + let mut builder = AnyOfBuilder::new(); + for item in any_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); + } + } + return RefOr::T(Schema::AnyOf(builder.build())); + } + + match obj.get("type") { + Some(Value::String(type_str)) => convert_typed_schema(type_str, obj), + Some(Value::Array(types)) => { + let mut builder = AnyOfBuilder::new(); + for type_val in types { + if let Value::String(type_str) = type_val { + builder = builder.item(convert_typed_schema(type_str, obj)); + } + } + RefOr::T(Schema::AnyOf(builder.build())) + } + None => RefOr::T(Schema::Object(ObjectBuilder::new().build())), + _ => RefOr::T(Schema::Object(ObjectBuilder::new().build())), + } +} + +fn convert_typed_schema( + type_str: &str, + obj: &serde_json::Map, +) -> RefOr { + use serde_json::Value; + + match type_str { + "object" => { + let mut object_builder = ObjectBuilder::new(); + + if let Some(Value::Object(properties)) = obj.get("properties") { + for (name, prop_value) in properties { + if let Ok(prop_schema) = rmcp::schemars::Schema::try_from(prop_value.clone()) { + let prop = convert_schemars_to_utoipa(prop_schema); + object_builder = object_builder.property(name, prop); + } + } + } + + if let Some(Value::Array(required)) = obj.get("required") { + for req in required { + if let Value::String(field_name) = req { + object_builder = object_builder.required(field_name); + } + } + } + + if let Some(additional) = obj.get("additionalProperties") { + match additional { + Value::Bool(false) => { + object_builder = object_builder + .additional_properties(Some(AdditionalProperties::FreeForm(false))); + } + Value::Bool(true) => { + object_builder = object_builder + .additional_properties(Some(AdditionalProperties::FreeForm(true))); + } + _ => { + if let Ok(schema) = rmcp::schemars::Schema::try_from(additional.clone()) { + let schema = convert_schemars_to_utoipa(schema); + object_builder = object_builder + .additional_properties(Some(AdditionalProperties::RefOr(schema))); + } + } + } + } + + RefOr::T(Schema::Object(object_builder.build())) + } + "array" => { + let mut array_builder = ArrayBuilder::new(); + + if let Some(items) = obj.get("items") { + match items { + Value::Object(_) | Value::Bool(_) => { + if let Ok(item_schema) = rmcp::schemars::Schema::try_from(items.clone()) { + let item_schema = convert_schemars_to_utoipa(item_schema); + array_builder = array_builder.items(item_schema); + } + } + Value::Array(item_schemas) => { + let mut any_of = AnyOfBuilder::new(); + for item in item_schemas { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + any_of = any_of.item(convert_schemars_to_utoipa(schema)); + } + } + let any_of_schema = RefOr::T(Schema::AnyOf(any_of.build())); + array_builder = array_builder.items(any_of_schema); + } + _ => {} + } + } + + if let Some(Value::Number(min_items)) = obj.get("minItems") { + if let Some(min) = min_items.as_u64() { + array_builder = array_builder.min_items(Some(min as usize)); + } + } + if let Some(Value::Number(max_items)) = obj.get("maxItems") { + if let Some(max) = max_items.as_u64() { + array_builder = array_builder.max_items(Some(max as usize)); + } + } + + RefOr::T(Schema::Array(array_builder.build())) + } + "string" => { + let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String); + + if let Some(Value::Array(enum_values)) = obj.get("enum") { + let values: Vec = enum_values + .iter() + .filter_map(|v| { + if let Value::String(s) = v { + Some(Value::String(s.clone())) + } else { + None + } + }) + .collect(); + if !values.is_empty() { + object_builder = object_builder.enum_values(Some(values)); + } + } + + if let Some(Value::Number(min_length)) = obj.get("minLength") { + if let Some(min) = min_length.as_u64() { + object_builder = object_builder.min_length(Some(min as usize)); + } + } + if let Some(Value::Number(max_length)) = obj.get("maxLength") { + if let Some(max) = max_length.as_u64() { + object_builder = object_builder.max_length(Some(max as usize)); + } + } + if let Some(Value::String(pattern)) = obj.get("pattern") { + object_builder = object_builder.pattern(Some(pattern.clone())); + } + if let Some(Value::String(format)) = obj.get("format") { + object_builder = object_builder.format(Some(SchemaFormat::Custom(format.clone()))); + } + + RefOr::T(Schema::Object(object_builder.build())) + } + "number" => { + let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Number); + + if let Some(Value::Number(minimum)) = obj.get("minimum") { + if let Some(min) = minimum.as_f64() { + object_builder = object_builder.minimum(Some(min)); + } + } + if let Some(Value::Number(maximum)) = obj.get("maximum") { + if let Some(max) = maximum.as_f64() { + object_builder = object_builder.maximum(Some(max)); + } + } + if let Some(Value::Number(exclusive_minimum)) = obj.get("exclusiveMinimum") { + if let Some(min) = exclusive_minimum.as_f64() { + object_builder = object_builder.exclusive_minimum(Some(min)); + } + } + if let Some(Value::Number(exclusive_maximum)) = obj.get("exclusiveMaximum") { + if let Some(max) = exclusive_maximum.as_f64() { + object_builder = object_builder.exclusive_maximum(Some(max)); + } + } + if let Some(Value::Number(multiple_of)) = obj.get("multipleOf") { + if let Some(mult) = multiple_of.as_f64() { + object_builder = object_builder.multiple_of(Some(mult)); + } + } + + RefOr::T(Schema::Object(object_builder.build())) + } + "integer" => { + let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Integer); + + if let Some(Value::Number(minimum)) = obj.get("minimum") { + if let Some(min) = minimum.as_f64() { + object_builder = object_builder.minimum(Some(min)); + } + } + if let Some(Value::Number(maximum)) = obj.get("maximum") { + if let Some(max) = maximum.as_f64() { + object_builder = object_builder.maximum(Some(max)); + } + } + if let Some(Value::Number(exclusive_minimum)) = obj.get("exclusiveMinimum") { + if let Some(min) = exclusive_minimum.as_f64() { + object_builder = object_builder.exclusive_minimum(Some(min)); + } + } + if let Some(Value::Number(exclusive_maximum)) = obj.get("exclusiveMaximum") { + if let Some(max) = exclusive_maximum.as_f64() { + object_builder = object_builder.exclusive_maximum(Some(max)); + } + } + if let Some(Value::Number(multiple_of)) = obj.get("multipleOf") { + if let Some(mult) = multiple_of.as_f64() { + object_builder = object_builder.multiple_of(Some(mult)); + } + } + + RefOr::T(Schema::Object(object_builder.build())) + } + "boolean" => RefOr::T(Schema::Object( + ObjectBuilder::new() + .schema_type(SchemaType::Boolean) + .build(), + )), + "null" => RefOr::T(Schema::Object( + ObjectBuilder::new().schema_type(SchemaType::String).build(), + )), + _ => RefOr::T(Schema::Object(ObjectBuilder::new().build())), + } +} + +derive_utoipa!(Role as RoleSchema); +derive_utoipa!(Content as ContentSchema); +derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock"); +derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema); +derive_utoipa!(ImageContent as ImageContentSchema); +derive_utoipa!(TextContent as TextContentSchema); +derive_utoipa!(RawTextContent as RawTextContentSchema); +derive_utoipa!(RawImageContent as RawImageContentSchema); +derive_utoipa!(RawAudioContent as RawAudioContentSchema); +derive_utoipa!(RawEmbeddedResource as RawEmbeddedResourceSchema); +derive_utoipa!(RawResource as RawResourceSchema); +derive_utoipa!(Tool as ToolSchema); +derive_utoipa!(ToolAnnotations as ToolAnnotationsSchema); +derive_utoipa!(ToolExecution as ToolExecutionSchema); +derive_utoipa!(TaskSupport as TaskSupportSchema); +derive_utoipa!(Annotations as AnnotationsSchema); +derive_utoipa!(ResourceContents as ResourceContentsSchema); +derive_utoipa!(JsonObject as JsonObjectSchema); +derive_utoipa!(Icon as IconSchema); +derive_utoipa!(IconTheme as IconThemeSchema); + +#[derive(OpenApi)] +#[openapi( + paths( + super::routes::status::status, + super::routes::status::system_info, + super::routes::status::diagnostics, + super::routes::config_management::validate_config, + 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::list_provider_secrets, + super::routes::config_management::delete_provider_secret, + super::routes::config_management::providers, + super::routes::config_management::get_provider_models, + super::routes::config_management::get_provider_model_info, + super::routes::config_management::get_slash_commands, + super::routes::config_management::create_custom_provider, + super::routes::config_management::get_custom_provider, + super::routes::config_management::update_custom_provider, + super::routes::config_management::remove_custom_provider, + super::routes::config_management::get_provider_catalog, + super::routes::config_management::get_provider_catalog_template, + super::routes::config_management::cleanup_provider_cache, + super::routes::config_management::check_provider, + super::routes::config_management::set_config_provider, + super::routes::config_management::get_canonical_model_info, + super::routes::prompts::get_prompts, + super::routes::prompts::get_prompt, + super::routes::prompts::save_prompt, + super::routes::prompts::reset_prompt, + super::routes::agent::start_agent, + super::routes::agent::resume_agent, + super::routes::agent::stop_agent, + super::routes::agent::restart_agent, + super::routes::agent::update_working_dir, + super::routes::agent::get_tools, + 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, + super::routes::reply::reply, + super::routes::session_events::session_events, + super::routes::session_events::session_reply, + super::routes::session_events::session_cancel, + super::routes::session::get_session, + super::routes::session::update_session_name, + 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, + super::routes::schedule::update_schedule, + super::routes::schedule::run_now_handler, + super::routes::schedule::pause_schedule, + super::routes::schedule::unpause_schedule, + super::routes::schedule::kill_running_job, + super::routes::schedule::inspect_running_job, + super::routes::schedule::sessions_handler, + super::routes::recipe::encode_recipe, + super::routes::recipe::decode_recipe, + super::routes::recipe::scan_recipe, + super::routes::recipe::list_recipes, + super::routes::recipe::delete_recipe, + super::routes::recipe::schedule_recipe, + super::routes::recipe::set_recipe_slash_command, + super::routes::recipe::save_recipe, + super::routes::recipe::parse_recipe, + super::routes::recipe::recipe_to_yaml, + super::routes::telemetry::send_telemetry_event, + super::routes::dictation::transcribe_dictation, + super::routes::dictation::get_dictation_config, + ), + components(schemas( + super::routes::config_management::UpsertConfigQuery, + super::routes::config_management::ConfigKeyQuery, + super::routes::config_management::ConfigResponse, + super::routes::config_management::ProvidersResponse, + super::routes::config_management::ProviderDetails, + super::routes::config_management::ProviderSecretsResponse, + super::routes::config_management::ProviderSecret, + super::routes::config_management::ProviderSecretStorage, + super::routes::config_management::ProviderSecretStatus, + 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::UpdateCustomProviderRequest, + goose::providers::catalog::ProviderCatalogEntry, + goose::providers::catalog::ProviderTemplate, + goose::providers::catalog::ModelTemplate, + goose::providers::catalog::ModelCapabilities, + super::routes::config_management::CreateCustomProviderResponse, + super::routes::config_management::CheckProviderRequest, + super::routes::config_management::SetProviderRequest, + super::routes::config_management::ModelInfoQuery, + super::routes::config_management::ModelInfoResponse, + super::routes::config_management::ModelInfoData, + super::routes::prompts::PromptsListResponse, + super::routes::prompts::PromptContentResponse, + super::routes::prompts::SavePromptRequest, + goose::prompt_template::Template, + super::routes::action_required::ConfirmToolActionRequest, + super::routes::reply::ChatRequest, + super::routes::session_events::SessionReplyRequest, + super::routes::session_events::SessionReplyResponse, + super::routes::session_events::CancelRequest, + super::routes::session::UpdateSessionNameRequest, + super::routes::session::UpdateSessionUserRecipeValuesRequest, + super::routes::session::UpdateSessionUserRecipeValuesResponse, + super::routes::session::ForkRequest, + super::routes::session::ForkResponse, + super::routes::session::SessionExtensionsResponse, + Message, + MessageContent, + MessageMetadata, + InferenceMetadata, + TokenState, + Usage, + ContentSchema, + EmbeddedResourceSchema, + ImageContentSchema, + AnnotationsSchema, + TextContentSchema, + RawTextContentSchema, + RawImageContentSchema, + RawAudioContentSchema, + RawEmbeddedResourceSchema, + RawResourceSchema, + ToolResponse, + ToolRequest, + ToolConfirmationRequest, + ActionRequired, + ActionRequiredData, + ThinkingContent, + RedactedThinkingContent, + FrontendToolRequest, + ResourceContentsSchema, + SystemNotificationType, + SystemNotificationContent, + MessageEvent, + JsonObjectSchema, + RoleSchema, + ProviderMetadata, + ProviderType, + LoadedProvider, + ProviderEngine, + DeclarativeProviderConfig, + EnvVarConfig, + ExtensionEntry, + ExtensionConfig, + ConfigKey, + Envs, + RecipeManifest, + ToolSchema, + ToolAnnotationsSchema, + ToolExecutionSchema, + TaskSupportSchema, + ToolInfo, + PermissionLevel, + Permission, + PrincipalType, + ModelInfo, + ModelConfig, + ThinkingEffort, + super::routes::config_management::ProviderModelInfoQuery, + Session, + goose_providers::goose_mode::GooseMode, + SessionType, + SystemInfo, + DiagnosticsConfig, + DiagnosticsError, + DiagnosticsExtensions, + DiagnosticsLevel, + DiagnosticsLogs, + DiagnosticsPrompt, + DiagnosticsReport, + DiagnosticsScheduledRecipe, + DiagnosticsTextFile, + Conversation, + IconSchema, + IconThemeSchema, + goose::session::extension_data::ExtensionData, + super::routes::schedule::CreateScheduleRequest, + super::routes::schedule::UpdateScheduleRequest, + super::routes::schedule::KillJobResponse, + super::routes::schedule::InspectJobResponse, + goose::scheduler::ScheduledJob, + super::routes::schedule::RunNowResponse, + super::routes::schedule::ListSchedulesResponse, + super::routes::schedule::SessionsQuery, + super::routes::schedule::SessionDisplayInfo, + super::routes::recipe::EncodeRecipeRequest, + super::routes::recipe::EncodeRecipeResponse, + super::routes::recipe::DecodeRecipeRequest, + super::routes::recipe::DecodeRecipeResponse, + super::routes::recipe::ScanRecipeRequest, + super::routes::recipe::ScanRecipeResponse, + super::routes::recipe::ListRecipeResponse, + super::routes::recipe::ScheduleRecipeRequest, + super::routes::recipe::SetSlashCommandRequest, + super::routes::recipe::DeleteRecipeRequest, + super::routes::recipe::SaveRecipeRequest, + super::routes::recipe::SaveRecipeResponse, + super::routes::errors::ErrorResponse, + super::routes::recipe::ParseRecipeRequest, + super::routes::recipe::ParseRecipeResponse, + super::routes::recipe::RecipeToYamlRequest, + super::routes::recipe::RecipeToYamlResponse, + goose::recipe::Recipe, + goose::recipe::Author, + goose::recipe::Settings, + goose::recipe::RecipeParameter, + goose::recipe::RecipeParameterInputType, + goose::recipe::RecipeParameterRequirement, + goose::recipe::Response, + goose::recipe::SubRecipe, + goose::agents::types::RetryConfig, + goose::agents::types::SuccessCheck, + super::routes::agent::UpdateProviderRequest, + super::routes::agent::UpdateSessionRequest, + super::routes::agent::GetToolsQuery, + ContentBlockSchema, + super::routes::agent::StartAgentRequest, + super::routes::agent::ResumeAgentRequest, + super::routes::agent::StopAgentRequest, + 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, + super::routes::telemetry::TelemetryEventRequest, + goose::goose_apps::GooseApp, + goose::goose_apps::WindowProps, + goose::goose_apps::McpAppResource, + goose::goose_apps::CspMetadata, + goose::goose_apps::PermissionsMetadata, + goose::goose_apps::UiMetadata, + goose::goose_apps::ResourceMetadata, + super::routes::dictation::TranscribeRequest, + super::routes::dictation::TranscribeResponse, + goose::dictation::providers::DictationProvider, + super::routes::dictation::DictationProviderStatus, + DownloadProgress, + DownloadStatus, + )) +)] +pub struct ApiDoc; + +#[cfg(feature = "local-inference")] +#[derive(OpenApi)] +#[openapi( + paths( + super::routes::dictation::list_models, + super::routes::dictation::download_model, + super::routes::dictation::get_download_progress, + super::routes::dictation::cancel_download, + super::routes::dictation::delete_model, + 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, + super::routes::local_inference::cancel_local_model_download, + super::routes::local_inference::delete_local_model, + super::routes::local_inference::get_model_settings, + super::routes::local_inference::update_model_settings, + ), + components(schemas( + super::routes::dictation::WhisperModelResponse, + super::routes::local_inference::LocalModelResponse, + super::routes::local_inference::ModelDownloadStatus, + super::routes::local_inference::DownloadModelRequest, + goose::providers::local_inference::hf_models::HfModelInfo, + goose::providers::local_inference::hf_models::HfModelVariant, + goose::providers::local_inference::hf_models::HfGgufFile, + 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; + +#[allow(dead_code)] // Used by generate_schema binary +pub fn generate_schema() -> String { + #[allow(unused_mut)] + let mut api_doc = ApiDoc::openapi(); + + #[cfg(feature = "local-inference")] + api_doc.merge(LocalInferenceApiDoc::openapi()); + + serde_json::to_string_pretty(&api_doc).unwrap() +} diff --git a/crates/goose-server/src/routes/action_required.rs b/crates/goose-server/src/routes/action_required.rs new file mode 100644 index 0000000000..1301f41c40 --- /dev/null +++ b/crates/goose-server/src/routes/action_required.rs @@ -0,0 +1,100 @@ +use crate::routes::errors::ErrorResponse; +use crate::state::AppState; +use axum::{extract::State, routing::post, Json, Router}; +use goose::permission::permission_confirmation::PrincipalType; +use goose::permission::{Permission, PermissionConfirmation}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::sync::Arc; +use utoipa::ToSchema; + +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmToolActionRequest { + id: String, + #[serde(default = "default_principal_type")] + principal_type: PrincipalType, + action: Permission, + session_id: String, +} + +fn default_principal_type() -> PrincipalType { + PrincipalType::Tool +} + +#[utoipa::path( + post, + path = "/action-required/tool-confirmation", + request_body = ConfirmToolActionRequest, + responses( + (status = 200, description = "Tool confirmation action is confirmed", body = Value), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn confirm_tool_action( + State(state): State>, + Json(request): Json, +) -> Result, ErrorResponse> { + let agent = state.get_agent_for_route(request.session_id).await?; + + agent + .handle_confirmation( + request.id.clone(), + PermissionConfirmation { + principal_type: request.principal_type, + permission: request.action, + }, + ) + .await; + + Ok(Json(Value::Object(serde_json::Map::new()))) +} + +pub fn routes(state: Arc) -> Router { + Router::new() + .route( + "/action-required/tool-confirmation", + post(confirm_tool_action), + ) + .with_state(state) +} + +#[cfg(test)] +mod tests { + use super::*; + + mod integration_tests { + use super::*; + use axum::{body::Body, http::Request}; + use http::StatusCode; + use tower::ServiceExt; + + #[tokio::test(flavor = "multi_thread")] + async fn test_tool_confirmation_endpoint() { + let state = AppState::new(true).await.unwrap(); + + let app = routes(state); + + let request = Request::builder() + .uri("/action-required/tool-confirmation") + .method("POST") + .header("content-type", "application/json") + .header("x-secret-key", "test-secret") + .body(Body::from( + serde_json::to_string(&ConfirmToolActionRequest { + id: "test-id".to_string(), + principal_type: PrincipalType::Tool, + action: Permission::AllowOnce, + session_id: "test-session".to_string(), + }) + .unwrap(), + )) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + } +} diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs new file mode 100644 index 0000000000..20c91d0db6 --- /dev/null +++ b/crates/goose-server/src/routes/agent.rs @@ -0,0 +1,1019 @@ +use crate::routes::config_management::resolve_provider_model_info; +use crate::routes::errors::ErrorResponse; +use crate::routes::recipe_utils::{ + apply_recipe_to_agent, build_recipe_with_parameter_values, load_recipe_by_id, validate_recipe, +}; +use crate::state::AppState; +use axum::response::IntoResponse; +use axum::{ + extract::{Query, State}, + http::StatusCode, + routing::{get, post}, + Json, Router, +}; +use goose::agents::{Container, ExtensionLoadResult}; + +use goose::agents::ExtensionConfig; +use goose::config::resolve_extensions_for_new_session; +use goose::config::{Config, GooseMode}; +use goose::providers::create; +use goose::recipe::Recipe; +use goose::recipe_deeplink; +use goose::session::session_manager::SessionType; +use goose::session::{EnabledExtensionsState, ExtensionState, Session}; +use goose::{ + agents::{extension::ToolInfo, extension_manager::get_parameter_names}, + config::permission::PermissionLevel, +}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::error; + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct UpdateFromSessionRequest { + session_id: String, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct UpdateProviderRequest { + provider: String, + model: Option, + session_id: String, + context_limit: Option, + request_params: Option>, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct UpdateSessionRequest { + session_id: String, + goose_mode: Option, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct GetToolsQuery { + extension_name: Option, + session_id: String, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct StartAgentRequest { + working_dir: String, + #[serde(default)] + recipe: Option, + #[serde(default)] + recipe_id: Option, + #[serde(default)] + recipe_deeplink: Option, + #[serde(default)] + extension_overrides: Option>, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct StopAgentRequest { + session_id: String, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct RestartAgentRequest { + session_id: String, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct UpdateWorkingDirRequest { + session_id: String, + working_dir: String, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub struct ResumeAgentRequest { + session_id: String, + 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, + container_id: Option, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct ResumeAgentResponse { + pub session: Session, + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_results: Option>, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct RestartAgentResponse { + pub extension_results: Vec, +} + +#[utoipa::path( + post, + path = "/agent/start", + request_body = StartAgentRequest, + responses( + (status = 200, description = "Agent started successfully", body = Session), + (status = 400, description = "Bad request", body = ErrorResponse), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 500, description = "Internal server error", body = ErrorResponse) + ) +)] +#[allow(clippy::too_many_lines)] +async fn start_agent( + State(state): State>, + Json(payload): Json, +) -> Result, ErrorResponse> { + #[cfg(feature = "telemetry")] + goose::posthog::set_session_context("desktop", false); + + let StartAgentRequest { + working_dir, + recipe, + recipe_id, + recipe_deeplink, + extension_overrides, + } = payload; + + let original_recipe = if let Some(deeplink) = recipe_deeplink { + match recipe_deeplink::decode(&deeplink) { + Ok(recipe) => Some(recipe), + Err(err) => { + error!("Failed to decode recipe deeplink: {}", err); + #[cfg(feature = "telemetry")] + goose::posthog::emit_error("recipe_deeplink_decode_failed", &err.to_string()); + return Err(ErrorResponse { + message: err.to_string(), + status: StatusCode::BAD_REQUEST, + }); + } + } + } else if let Some(id) = recipe_id { + match load_recipe_by_id(state.as_ref(), &id).await { + Ok(recipe) => Some(recipe), + Err(err) => return Err(err), + } + } else { + recipe + }; + + if let Some(ref recipe) = original_recipe { + if let Err(err) = validate_recipe(recipe) { + return Err(ErrorResponse { + message: err.message, + status: err.status, + }); + } + } + + let name = "New Chat".to_string(); + + let manager = state.session_manager(); + let config = Config::global(); + let current_mode = config.get_goose_mode().unwrap_or_default(); + + let mut session = manager + .create_session( + PathBuf::from(&working_dir), + name, + SessionType::User, + current_mode, + ) + .await + .map_err(|err| { + error!("Failed to create session: {}", err); + #[cfg(feature = "telemetry")] + goose::posthog::emit_error("session_create_failed", &err.to_string()); + ErrorResponse { + message: format!("Failed to create session: {}", err), + status: StatusCode::BAD_REQUEST, + } + })?; + + let recipe_extensions = original_recipe + .as_ref() + .and_then(|r| r.extensions.as_deref()); + let has_extension_overrides = extension_overrides.is_some(); + let mut extensions_to_use = + resolve_extensions_for_new_session(recipe_extensions, extension_overrides); + if recipe_extensions.is_none() && !has_extension_overrides { + extensions_to_use.extend(goose::plugins::mcp_servers::enabled_plugin_mcp_servers( + Some(&PathBuf::from(&working_dir)), + )); + } + + let mut extension_data = session.extension_data.clone(); + let extensions_state = EnabledExtensionsState::new(extensions_to_use); + if let Err(e) = extensions_state.to_extension_data(&mut extension_data) { + tracing::warn!("Failed to initialize session with extensions: {}", e); + } else { + manager + .update(&session.id) + .extension_data(extension_data.clone()) + .apply() + .await + .map_err(|err| { + error!("Failed to save initial extension state: {}", err); + ErrorResponse { + message: format!("Failed to save initial extension state: {}", err), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + })?; + } + + if let Some(recipe) = original_recipe { + let mut update = manager.update(&session.id).recipe(Some(recipe.clone())); + + if let Some(ref settings) = recipe.settings { + if let Some(ref provider) = settings.goose_provider { + update = update.provider_name(provider); + + if let Some(ref model) = settings.goose_model { + if let Ok(model_config) = + goose::model_config::model_config_from_user_config(provider, model) + { + update = update.model_config(model_config); + } + } + } + } + + update.apply().await.map_err(|err| { + error!("Failed to update session with recipe: {}", err); + ErrorResponse { + message: format!("Failed to update session with recipe: {}", err), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + })?; + } + + // Refetch session to get all updates + session = manager + .get_session(&session.id, false) + .await + .map_err(|err| { + error!("Failed to get updated session: {}", err); + ErrorResponse { + message: format!("Failed to get updated session: {}", err), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + })?; + + // Eagerly start loading extensions in the background + let session_for_spawn = session.clone(); + let state_for_spawn = state.clone(); + let session_id_for_task = session.id.clone(); + let task = tokio::spawn(async move { + match state_for_spawn + .get_agent(session_for_spawn.id.clone()) + .await + { + Ok(agent) => { + let results = agent.load_extensions_from_session(&session_for_spawn).await; + tracing::debug!( + "Background extension loading completed for session {}", + session_for_spawn.id + ); + results + } + Err(e) => { + tracing::warn!( + "Failed to create agent for background extension loading: {}", + e + ); + vec![] + } + } + }); + + state + .set_extension_loading_task(session_id_for_task, task) + .await; + + Ok(Json(session)) +} + +#[utoipa::path( + post, + path = "/agent/resume", + request_body = ResumeAgentRequest, + responses( + (status = 200, description = "Agent started successfully", body = ResumeAgentResponse), + (status = 400, description = "Bad request - invalid working directory"), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 500, description = "Internal server error") + ) +)] +async fn resume_agent( + State(state): State>, + Json(payload): Json, +) -> Result, ErrorResponse> { + #[cfg(feature = "telemetry")] + goose::posthog::set_session_context("desktop", true); + + let session = state + .session_manager() + .get_session(&payload.session_id, true) + .await + .map_err(|err| { + error!("Failed to resume session {}: {}", payload.session_id, err); + #[cfg(feature = "telemetry")] + goose::posthog::emit_error("session_resume_failed", &err.to_string()); + ErrorResponse { + message: format!("Failed to resume session: {}", err), + status: StatusCode::NOT_FOUND, + } + })?; + + let (extension_results, session) = if payload.load_model_and_extensions { + let agent = state + .get_agent_for_route(payload.session_id.clone()) + .await + .map_err(|code| ErrorResponse { + message: "Failed to get agent for route".into(), + status: code, + })?; + + if !state.has_extension_loading_task(&payload.session_id).await { + let session_for_task = session.clone(); + let agent_for_task = agent.clone(); + let session_id_for_task = payload.session_id.clone(); + let task = tokio::spawn(async move { + agent_for_task + .load_extensions_from_session(&session_for_task) + .await + }); + state + .set_extension_loading_task(session_id_for_task, task) + .await; + } + + let provider_changed = agent + .restore_provider_from_session(&session) + .await + .map_err(|e| ErrorResponse { + message: e.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let session = if provider_changed { + state + .session_manager() + .get_session(&payload.session_id, true) + .await + .map_err(|err| ErrorResponse { + message: format!("Failed to re-fetch session: {}", err), + status: StatusCode::INTERNAL_SERVER_ERROR, + })? + } else { + session + }; + + let extension_results = match state.take_extension_loading_task(&payload.session_id).await { + Ok(Some(results)) => { + tracing::debug!( + "Using background extension loading results for session {}", + payload.session_id + ); + state + .remove_extension_loading_task(&payload.session_id) + .await; + results + } + Ok(None) => { + tracing::debug!( + "Extension loading task for session {} was already consumed", + payload.session_id + ); + vec![] + } + Err(e) => { + state + .remove_extension_loading_task(&payload.session_id) + .await; + tracing::warn!( + "Background extension loading failed for session {}, retrying synchronously: {}", + payload.session_id, + e + ); + agent.load_extensions_from_session(&session).await + } + }; + + (Some(extension_results), session) + } else { + (None, session) + }; + + Ok(Json(ResumeAgentResponse { + session, + extension_results, + })) +} + +#[utoipa::path( + post, + path = "/agent/update_from_session", + request_body = UpdateFromSessionRequest, + responses( + (status = 200, description = "Update agent from session data successfully"), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + ), +)] +async fn update_from_session( + State(state): State>, + Json(payload): Json, +) -> Result { + let agent = state + .get_agent_for_route(payload.session_id.clone()) + .await + .map_err(|status| ErrorResponse { + message: format!("Failed to get agent: {}", status), + status, + })?; + let session = state + .session_manager() + .get_session(&payload.session_id, false) + .await + .map_err(|err| ErrorResponse { + message: format!("Failed to get session: {}", err), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + if let Some(recipe) = session.recipe { + if session.session_type == SessionType::Scheduled { + if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await { + agent + .extend_system_prompt("recipe".to_string(), prompt) + .await; + } + } else { + match build_recipe_with_parameter_values( + &recipe, + session.user_recipe_values.unwrap_or_default(), + ) + .await + { + Ok(Some(recipe)) => { + if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await { + agent + .extend_system_prompt("recipe".to_string(), prompt) + .await; + } + } + Ok(None) => { + // Recipe has missing parameters + } + Err(e) => { + return Err(ErrorResponse { + message: e.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + }); + } + } + } + } + + Ok(StatusCode::OK) +} + +#[utoipa::path( + get, + path = "/agent/tools", + params( + ("extension_name" = Option, Query, description = "Optional extension name to filter tools"), + ("session_id" = String, Query, description = "Required session ID to scope tools to a specific session") + ), + responses( + (status = 200, description = "Tools retrieved successfully", body = Vec), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn get_tools( + State(state): State>, + Query(query): Query, +) -> Result>, StatusCode> { + let session_id = query.session_id; + let agent = state.get_agent_for_route(session_id.clone()).await?; + let goose_mode = agent.goose_mode().await; + let permission_manager = agent.config.permission_manager.clone(); + + let mut tools: Vec = agent + .list_tools(&session_id, query.extension_name) + .await + .into_iter() + .map(|tool| { + let permission = permission_manager + .get_user_permission(&tool.name) + .or_else(|| { + if goose_mode == GooseMode::SmartApprove { + permission_manager.get_smart_approve_permission(&tool.name) + } else if goose_mode == GooseMode::Approve { + Some(PermissionLevel::AskBefore) + } else { + None + } + }); + + ToolInfo::new( + &tool.name, + tool.description + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or_default(), + get_parameter_names(&tool), + permission, + ) + .with_input_schema(serde_json::Value::Object( + tool.input_schema.as_ref().clone(), + )) + }) + .collect::>(); + tools.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(Json(tools)) +} + +#[utoipa::path( + post, + path = "/agent/update_provider", + request_body = UpdateProviderRequest, + responses( + (status = 200, description = "Provider updated successfully"), + (status = 400, description = "Bad request - missing or invalid parameters"), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn update_agent_provider( + State(state): State>, + Json(payload): Json, +) -> Result<(), impl IntoResponse> { + let agent = state + .get_agent_for_route(payload.session_id.clone()) + .await + .map_err(|e| (e, "No agent for session id".to_owned()))?; + + let config = Config::global(); + let model = match payload.model.or_else(|| config.get_goose_model().ok()) { + Some(m) => m, + None => { + return Err((StatusCode::BAD_REQUEST, "No model specified".to_owned())); + } + }; + + let mut model_config = + goose::model_config::model_config_from_user_config(&payload.provider, &model) + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("Invalid model config: {}", e), + ) + })? + .with_context_limit(payload.context_limit); + + if let Some(request_params) = payload.request_params { + model_config = model_config.with_merged_request_params(request_params); + } + let model_info = resolve_provider_model_info(&payload.provider, &model) + .await + .map_err(|e| (e.status, e.message))?; + model_config.reasoning = Some(model_info.reasoning); + + let extensions = + EnabledExtensionsState::for_session(state.session_manager(), &payload.session_id, config) + .await; + + let new_provider = create(&payload.provider, extensions).await.map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("Failed to create {} provider: {}", &payload.provider, e), + ) + })?; + + agent + .update_provider(new_provider, model_config, &payload.session_id) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to update provider: {}", e), + ) + })?; + + // Propagate session mode to the new provider + let mode = agent.goose_mode().await; + agent + .update_goose_mode(mode, &payload.session_id) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to propagate mode to provider: {}", e), + ) + })?; + + Ok(()) +} + +#[utoipa::path( + post, + path = "/agent/update_session", + request_body = UpdateSessionRequest, + responses( + (status = 200, description = "Session updated"), + (status = 400, description = "Invalid request"), + (status = 500, description = "Internal error") + ) +)] +async fn update_session( + State(state): State>, + Json(payload): Json, +) -> Result<(), (StatusCode, String)> { + let agent = state + .get_agent_for_route(payload.session_id.clone()) + .await + .map_err(|e| (e, "No agent for session id".to_owned()))?; + + if let Some(mode_str) = payload.goose_mode { + let mode: GooseMode = mode_str.parse().map_err(|_| { + ( + StatusCode::BAD_REQUEST, + format!("Invalid mode: {}", mode_str), + ) + })?; + + agent + .update_goose_mode(mode, &payload.session_id) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to update mode: {}", e), + ) + })?; + } + + 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 { + #[cfg(feature = "telemetry")] + let extension_name = request.config.name(); + + ensure_extensions_loaded(&state, &request.session_id).await?; + + 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 { + ensure_extensions_loaded(&state, &request.session_id).await?; + + 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", + request_body = SetContainerRequest, + responses( + (status = 200, description = "Container set successfully"), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 424, description = "Agent not initialized"), + (status = 500, description = "Internal server error") + ) +)] +async fn set_container( + State(state): State>, + Json(request): Json, +) -> Result { + let agent = state.get_agent(request.session_id.clone()).await?; + + let container = request.container_id.map(Container::new); + agent.set_container(container).await; + + Ok(StatusCode::OK) +} + +#[utoipa::path( + post, + path = "/agent/stop", + request_body = StopAgentRequest, + responses( + (status = 200, description = "Agent stopped successfully", body = String), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ) +)] +async fn stop_agent( + State(state): State>, + Json(payload): Json, +) -> Result { + let session_id = payload.session_id; + state + .agent_manager + .remove_session(&session_id) + .await + .map_err(|e| ErrorResponse { + message: format!("Failed to stop agent for session {}: {}", session_id, e), + status: StatusCode::NOT_FOUND, + })?; + + Ok(StatusCode::OK) +} + +async fn restart_agent_internal( + state: &Arc, + session_id: &str, + session: &Session, +) -> Result, ErrorResponse> { + // Remove existing agent (ignore error if not found) + let _ = state.agent_manager.remove_session(session_id).await; + + let agent = state + .get_agent_for_route(session_id.to_string()) + .await + .map_err(|code| ErrorResponse { + message: "Failed to create new agent during restart".into(), + status: code, + })?; + + let provider_future = agent.restore_provider_from_session(session); + let extensions_future = agent.load_extensions_from_session(session); + + let (provider_result, extension_results) = tokio::join!(provider_future, extensions_future); + provider_result.map_err(|e| ErrorResponse { + message: e.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + if let Some(ref recipe) = session.recipe { + if session.session_type == SessionType::Scheduled { + if let Some(prompt) = apply_recipe_to_agent(&agent, recipe, true).await { + agent + .extend_system_prompt("recipe".to_string(), prompt) + .await; + } + } else { + match build_recipe_with_parameter_values( + recipe, + session.user_recipe_values.clone().unwrap_or_default(), + ) + .await + { + Ok(Some(recipe)) => { + if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await { + agent + .extend_system_prompt("recipe".to_string(), prompt) + .await; + } + } + Ok(None) => { + // Recipe has missing parameters + } + Err(e) => { + return Err(ErrorResponse { + message: e.to_string(), + status: StatusCode::INTERNAL_SERVER_ERROR, + }); + } + } + } + } + + Ok(extension_results) +} + +#[utoipa::path( + post, + path = "/agent/restart", + request_body = RestartAgentRequest, + responses( + (status = 200, description = "Agent restarted successfully", body = RestartAgentResponse), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ) +)] +async fn restart_agent( + State(state): State>, + Json(payload): Json, +) -> Result, ErrorResponse> { + let session_id = payload.session_id.clone(); + + let session = state + .session_manager() + .get_session(&session_id, false) + .await + .map_err(|err| { + error!("Failed to get session during restart: {}", err); + ErrorResponse { + message: format!("Failed to get session: {}", err), + status: StatusCode::NOT_FOUND, + } + })?; + + let extension_results = restart_agent_internal(&state, &session_id, &session).await?; + + Ok(Json(RestartAgentResponse { extension_results })) +} + +#[utoipa::path( + post, + path = "/agent/update_working_dir", + request_body = UpdateWorkingDirRequest, + responses( + (status = 200, description = "Working directory updated and agent restarted successfully"), + (status = 400, description = "Bad request - invalid directory path"), + (status = 401, description = "Unauthorized - invalid secret key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ) +)] +async fn update_working_dir( + State(state): State>, + Json(payload): Json, +) -> Result { + let session_id = payload.session_id.clone(); + let working_dir = payload.working_dir.trim(); + + if working_dir.is_empty() { + return Err(ErrorResponse { + message: "Working directory cannot be empty".into(), + status: StatusCode::BAD_REQUEST, + }); + } + + let path = PathBuf::from(working_dir); + if !path.exists() || !path.is_dir() { + return Err(ErrorResponse { + message: "Invalid directory path".into(), + status: StatusCode::BAD_REQUEST, + }); + } + + // Update the session's working directory + state + .session_manager() + .update(&session_id) + .working_dir(path) + .apply() + .await + .map_err(|e| { + error!("Failed to update session working directory: {}", e); + ErrorResponse { + message: format!("Failed to update working directory: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + })?; + + // Get the updated session and restart the agent + let session = state + .session_manager() + .get_session(&session_id, false) + .await + .map_err(|err| { + error!("Failed to get session after working dir update: {}", err); + ErrorResponse { + message: format!("Failed to get session: {}", err), + status: StatusCode::NOT_FOUND, + } + })?; + + restart_agent_internal(&state, &session_id, &session).await?; + + Ok(StatusCode::OK) +} + +async fn ensure_extensions_loaded(state: &AppState, session_id: &str) -> Result<(), ErrorResponse> { + match state.take_extension_loading_task(session_id).await { + Ok(Some(_)) => { + tracing::debug!( + "Awaited background extension loading for session {} before serving request", + session_id + ); + state.remove_extension_loading_task(session_id).await; + Ok(()) + } + Ok(None) => Ok(()), + Err(e) => { + state.remove_extension_loading_task(session_id).await; + tracing::warn!( + "Background extension loading failed for session {}, retrying synchronously: {}", + session_id, + e + ); + let session = state + .session_manager() + .get_session(session_id, false) + .await + .map_err(|err| ErrorResponse { + message: format!( + "Failed to get session after extension loading failed: {}", + err + ), + status: StatusCode::NOT_FOUND, + })?; + let agent = state + .get_agent(session_id.to_string()) + .await + .map_err(|err| { + ErrorResponse::internal(format!( + "Failed to get agent after extension loading failed: {}", + err + )) + })?; + agent.load_extensions_from_session(&session).await; + Ok(()) + } + } +} + +pub fn routes(state: Arc) -> Router { + Router::new() + .route("/agent/start", post(start_agent)) + .route("/agent/resume", post(resume_agent)) + .route("/agent/restart", post(restart_agent)) + .route("/agent/update_working_dir", post(update_working_dir)) + .route("/agent/tools", get(get_tools)) + .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) +} diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs new file mode 100644 index 0000000000..0f65ef7aa8 --- /dev/null +++ b/crates/goose-server/src/routes/config_management.rs @@ -0,0 +1,957 @@ +use crate::routes::errors::ErrorResponse; +use crate::routes::utils::check_provider_configured; +use crate::state::AppState; +use axum::routing::put; +use axum::{ + extract::Path, + routing::{delete, get, post}, + Json, Router, +}; +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::providers::base::{ModelInfo, ProviderMetadata, ProviderType}; +use goose::providers::canonical::maybe_get_canonical_model; +use goose::providers::catalog::{ + get_provider_template, get_providers_by_format, ProviderCatalogEntry, ProviderFormat, + ProviderTemplate, +}; +use goose::providers::create_with_default_model; +use goose::providers::providers as get_providers; +use goose::{ + agents::execute_commands, agents::ExtensionConfig, slash_commands::recipe_slash_command, +}; +use goose_providers::model::ModelConfig; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +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, + pub value: Value, + pub is_secret: bool, +} + +#[derive(Deserialize, Serialize, ToSchema)] +pub struct ConfigKeyQuery { + pub key: String, + pub is_secret: bool, +} + +#[derive(Serialize, ToSchema)] +pub struct ConfigResponse { + pub config: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ProviderDetails { + pub name: String, + pub metadata: ProviderMetadata, + pub is_configured: bool, + pub provider_type: ProviderType, + #[serde(skip_serializing_if = "Option::is_none")] + pub saved_model: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct ProvidersResponse { + pub providers: Vec, +} + +#[derive(Deserialize, ToSchema)] +pub struct UpdateCustomProviderRequest { + pub engine: String, + pub display_name: String, + pub api_url: String, + pub api_key: String, + pub models: Vec, + pub supports_streaming: Option, + pub headers: Option>, + #[serde(default = "default_requires_auth")] + pub requires_auth: bool, + #[serde(default)] + pub catalog_provider_id: Option, + #[serde(default)] + pub base_path: Option, + #[serde(default)] + pub preserves_thinking: Option, +} + +fn default_requires_auth() -> bool { + true +} + +fn normalize_custom_provider_api_key(api_key: String) -> Option { + let api_key = api_key.trim().to_string(); + (!api_key.is_empty()).then_some(api_key) +} + +#[derive(Deserialize, ToSchema)] +pub struct CheckProviderRequest { + pub provider: String, +} + +#[derive(Deserialize, ToSchema)] +pub struct SetProviderRequest { + pub provider: String, + pub model: String, +} + +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MaskedSecret { + pub masked_value: String, +} + +#[derive(Serialize, ToSchema)] +#[serde(untagged)] +pub enum ConfigValueResponse { + Value(Value), + MaskedValue(MaskedSecret), +} + +pub use goose::providers::provider_secrets::{ + ProviderSecret, ProviderSecretStatus, ProviderSecretStorage, +}; + +#[derive(Debug, Serialize, ToSchema)] +pub struct ProviderSecretsResponse { + pub secrets: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub enum CommandType { + Builtin, + Recipe, + Skill, + Agent, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SlashCommand { + pub command: String, + pub help: String, + pub command_type: CommandType, +} +#[derive(Serialize, ToSchema)] +pub struct SlashCommandsResponse { + pub commands: Vec, +} + +#[utoipa::path( + post, + path = "/config/upsert", + request_body = UpsertConfigQuery, + responses( + (status = 200, description = "Configuration value upserted successfully", body = String), + (status = 500, description = "Internal server error") + ) +)] +pub async fn upsert_config( + Json(query): Json, +) -> Result, ErrorResponse> { + let config = Config::global(); + + if query.key == "GOOSE_PROVIDER" { + if let Some(name) = query.value.as_str() { + let model = goose::config::get_provider_entry(config, name) + .map(|e| e.model) + .or_else(|| config.get_goose_model().ok()) + .unwrap_or_default(); + goose::config::set_active_provider(config, name, &model)?; + return Ok(Json(Value::String(format!("Upserted key {}", query.key)))); + } + } + if query.key == "GOOSE_MODEL" { + if let Some(model) = query.value.as_str() { + if let Ok(provider) = config.get_goose_provider() { + goose::config::set_active_provider(config, &provider, model)?; + return Ok(Json(Value::String(format!("Upserted key {}", query.key)))); + } + } + } + + config.set(&query.key, &query.value, query.is_secret)?; + Ok(Json(Value::String(format!("Upserted key {}", query.key)))) +} + +#[utoipa::path( + post, + path = "/config/remove", + request_body = ConfigKeyQuery, + responses( + (status = 200, description = "Configuration value removed successfully", body = String), + (status = 404, description = "Configuration key not found"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn remove_config( + Json(query): Json, +) -> Result, ErrorResponse> { + let config = Config::global(); + + if query.is_secret { + config.delete_secret(&query.key)?; + } else if query.key == "GOOSE_PROVIDER" || query.key == "active_provider" { + config.delete("active_provider")?; + config.delete("GOOSE_PROVIDER")?; + } else if query.key == "GOOSE_MODEL" { + if let Ok(provider) = config.get_goose_provider() { + goose::config::set_active_provider(config, &provider, "")?; + } + config.delete("GOOSE_MODEL")?; + } else { + config.delete(&query.key)?; + } + + Ok(Json(format!("Removed key {}", query.key))) +} + +const SECRET_MASK_SHOW_LEN: usize = 8; + +fn mask_secret(secret: Value) -> String { + let as_string = match secret { + Value::String(s) => s, + _ => serde_json::to_string(&secret).unwrap_or_else(|_| secret.to_string()), + }; + + let chars: Vec<_> = as_string.chars().collect(); + let show_len = std::cmp::min(chars.len() / 2, SECRET_MASK_SHOW_LEN); + let visible: String = chars.iter().take(show_len).collect(); + let mask = "*".repeat(chars.len() - show_len); + + format!("{}{}", visible, mask) +} + +#[utoipa::path( + get, + path = "/config/provider-secrets", + responses( + (status = 200, description = "Provider secrets retrieved successfully", body = ProviderSecretsResponse), + (status = 500, description = "Internal server error") + ) +)] +pub async fn list_provider_secrets() -> Result, ErrorResponse> { + let secrets = goose::providers::provider_secrets::list_provider_secrets().await?; + Ok(Json(ProviderSecretsResponse { secrets })) +} + +#[utoipa::path( + delete, + path = "/config/provider-secrets/{id}", + params( + ("id" = String, Path, description = "Provider secret identifier") + ), + responses( + (status = 200, description = "Provider secret deleted successfully", body = String), + (status = 400, description = "Invalid provider secret identifier"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn delete_provider_secret(Path(id): Path) -> Result, ErrorResponse> { + use goose::providers::provider_secrets::DeleteProviderSecretError; + + match goose::providers::provider_secrets::delete_provider_secret(&id).await { + Ok(()) => Ok(Json(format!("Deleted provider secret {}", id))), + Err(DeleteProviderSecretError::InvalidId(id)) => Err(ErrorResponse::bad_request(format!( + "Invalid provider secret id: '{}'", + id + ))), + Err(DeleteProviderSecretError::Config(e)) => Err(e.into()), + Err(DeleteProviderSecretError::Other(e)) => Err(ErrorResponse::internal(e.to_string())), + } +} + +#[utoipa::path( + post, + path = "/config/read", + request_body = ConfigKeyQuery, + responses( + (status = 200, description = "Configuration value retrieved successfully", body = Value), + (status = 500, description = "Unable to get the configuration value"), + ) +)] +pub async fn read_config( + Json(query): Json, +) -> Result, ErrorResponse> { + let config = Config::global(); + + if query.key == "GOOSE_PROVIDER" || query.key == "active_provider" { + if let Ok(val) = config.get_goose_provider() { + return Ok(Json(ConfigValueResponse::Value(Value::String(val)))); + } + return Ok(Json(ConfigValueResponse::Value(Value::Null))); + } + if query.key == "GOOSE_MODEL" { + if let Ok(val) = config.get_goose_model() { + return Ok(Json(ConfigValueResponse::Value(Value::String(val)))); + } + return Ok(Json(ConfigValueResponse::Value(Value::Null))); + } + + let response_value = match config.get(&query.key, query.is_secret) { + Ok(value) => { + if query.is_secret { + ConfigValueResponse::MaskedValue(MaskedSecret { + masked_value: mask_secret(value), + }) + } else { + ConfigValueResponse::Value(value) + } + } + Err(ConfigError::NotFound(_)) => ConfigValueResponse::Value(Value::Null), + Err(e) => return Err(e.into()), + }; + 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", + responses( + (status = 200, description = "All configuration values retrieved successfully", body = ConfigResponse) + ) +)] +pub async fn read_all_config() -> Result, ErrorResponse> { + let config = Config::global(); + let values = config + .all_values() + .map_err(|e| ErrorResponse::unprocessable(e.to_string()))?; + Ok(Json(ConfigResponse { config: values })) +} + +#[utoipa::path( + get, + path = "/config/providers", + responses( + (status = 200, description = "All configuration values retrieved successfully", body = [ProviderDetails]) + ) +)] +pub async fn providers() -> Result>, ErrorResponse> { + let config = Config::global(); + let providers = get_providers().await; + let providers_response: Vec = providers + .into_iter() + .map(|(metadata, provider_type)| { + let is_configured = check_provider_configured(&metadata, provider_type); + let saved_model = goose::config::get_provider_entry(config, &metadata.name) + .map(|e| e.model) + .filter(|m| !m.is_empty()); + + ProviderDetails { + name: metadata.name.clone(), + metadata, + is_configured, + provider_type, + saved_model, + } + }) + .collect(); + + Ok(Json(providers_response)) +} + +#[utoipa::path( + get, + path = "/config/providers/{name}/models", + params( + ("name" = String, Path, description = "Provider name (e.g., openai)") + ), + responses( + (status = 200, description = "Models fetched successfully", body = [ModelInfo]), + (status = 400, description = "Unknown provider, provider not configured, or authentication error"), + (status = 429, description = "Rate limit exceeded"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn get_provider_models( + Path(name): Path, +) -> Result>, ErrorResponse> { + let all = get_providers().await.into_iter().collect::>(); + let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else { + return Err(ErrorResponse::bad_request(format!( + "Unknown provider: {}", + name + ))); + }; + if !check_provider_configured(&metadata, provider_type) { + return Err(ErrorResponse::bad_request(format!( + "Provider '{}' is not configured", + name + ))); + } + + let provider = goose::providers::create(&name, Vec::new()).await?; + + let models_result = provider + .fetch_recommended_model_info(goose::model_config::global_toolshim()) + .await; + + match models_result { + Ok(models) => Ok(Json(models)), + Err(provider_error) => Err(provider_error.into()), + } +} + +#[derive(Deserialize, ToSchema)] +pub struct ProviderModelInfoQuery { + pub model: String, +} + +pub async fn resolve_provider_model_info( + name: &str, + model: &str, +) -> Result { + let all = get_providers().await.into_iter().collect::>(); + let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else { + return Err(ErrorResponse::bad_request(format!( + "Unknown provider: {}", + name + ))); + }; + if !check_provider_configured(&metadata, provider_type) { + return Err(ErrorResponse::bad_request(format!( + "Provider '{}' is not configured", + name + ))); + } + + let model_config = goose::model_config::model_config_from_user_config(name, model)?; + let provider = goose::providers::create(name, Vec::new()).await?; + match provider.fetch_model_info(model).await { + Ok(info) => Ok(info), + Err(error) => { + let mut info = ModelInfo::new(model, model_config.context_limit()); + info.reasoning = model_config.is_reasoning_model(); + tracing::debug!( + provider = name, + model, + error = %error, + "Falling back to local model metadata" + ); + Ok(info) + } + } +} + +#[utoipa::path( + post, + path = "/config/providers/{name}/model-info", + params( + ("name" = String, Path, description = "Provider name (e.g., openai)") + ), + request_body = ProviderModelInfoQuery, + responses( + (status = 200, description = "Model metadata fetched successfully", body = ModelInfo), + (status = 400, description = "Unknown provider, provider not configured, or authentication error"), + (status = 429, description = "Rate limit exceeded"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn get_provider_model_info( + Path(name): Path, + Json(query): Json, +) -> Result, ErrorResponse> { + resolve_provider_model_info(&name, &query.model) + .await + .map(Json) +} + +#[derive(Deserialize, utoipa::IntoParams)] +pub struct SlashCommandsQuery { + /// Optional working directory to discover local skills from + pub working_dir: Option, +} + +#[utoipa::path( + get, + path = "/config/slash_commands", + params(SlashCommandsQuery), + responses( + (status = 200, description = "Slash commands retrieved successfully", body = SlashCommandsResponse) + ) +)] +pub async fn get_slash_commands( + axum::extract::Query(query): axum::extract::Query, +) -> Result, ErrorResponse> { + let mut commands: Vec<_> = recipe_slash_command::list_commands() + .iter() + .map(|command| SlashCommand { + command: command.command.clone(), + help: command.recipe_path.clone(), + command_type: CommandType::Recipe, + }) + .collect(); + + for cmd_def in execute_commands::list_commands() { + commands.push(SlashCommand { + command: cmd_def.name.to_string(), + help: cmd_def.description.to_string(), + command_type: CommandType::Builtin, + }); + } + + let working_dir = query.working_dir.map(std::path::PathBuf::from); + for source in goose::skills::list_installed_skills(working_dir.as_deref()) { + commands.push(SlashCommand { + command: source.name, + help: source.description, + command_type: CommandType::Skill, + }); + } + + let discover_dir = working_dir + .as_deref() + .unwrap_or_else(|| std::path::Path::new(".")); + for source in + goose::agents::platform_extensions::summon::discover_filesystem_sources(discover_dir) + { + if matches!( + source.source_type, + SourceType::Agent | SourceType::Recipe | SourceType::Subrecipe + ) && !source.content.is_empty() + { + commands.push(SlashCommand { + command: source.name, + help: source.description, + command_type: CommandType::Agent, + }); + } + } + + Ok(Json(SlashCommandsResponse { commands })) +} + +#[derive(Serialize, ToSchema)] +pub struct ModelInfoData { + pub provider: String, + pub model: String, + pub context_limit: usize, + pub max_output_tokens: Option, + pub reasoning: bool, + pub input_token_cost: Option, + pub output_token_cost: Option, + pub cache_read_token_cost: Option, + pub cache_write_token_cost: Option, + pub currency: String, +} + +#[derive(Serialize, ToSchema)] +pub struct ModelInfoResponse { + pub model_info: Option, + pub source: String, +} + +#[derive(Deserialize, ToSchema)] +pub struct ModelInfoQuery { + pub provider: String, + pub model: String, +} + +#[utoipa::path( + post, + path = "/config/canonical-model-info", + request_body = ModelInfoQuery, + responses( + (status = 200, description = "Model information retrieved successfully", body = ModelInfoResponse) + ) +)] +pub async fn get_canonical_model_info( + Json(query): Json, +) -> Json { + let canonical_model = maybe_get_canonical_model(&query.provider, &query.model); + + let model_info = canonical_model.map(|canonical_model| ModelInfoData { + provider: query.provider.clone(), + model: query.model.clone(), + context_limit: canonical_model.limit.context, + max_output_tokens: canonical_model.limit.output, + reasoning: canonical_model + .reasoning + .unwrap_or_else(|| ModelConfig::new(&query.model).is_reasoning_model()), + // Costs are per million tokens - client handles division for display + input_token_cost: canonical_model.cost.input, + output_token_cost: canonical_model.cost.output, + cache_read_token_cost: canonical_model.cost.cache_read, + cache_write_token_cost: canonical_model.cost.cache_write, + currency: "$".to_string(), + }); + + Json(ModelInfoResponse { + model_info, + source: "canonical".to_string(), + }) +} + +#[utoipa::path( + get, + path = "/config/validate", + responses( + (status = 200, description = "Config validation result", body = String), + (status = 422, description = "Config file is corrupted") + ) +)] +pub async fn validate_config() -> Result, ErrorResponse> { + let config_path = Paths::config_dir().join("config.yaml"); + + if !config_path.exists() { + return Ok(Json("Config file does not exist".to_string())); + } + + let content = std::fs::read_to_string(&config_path)?; + serde_yaml::from_str::(&content) + .map_err(|e| ErrorResponse::unprocessable(format!("Config file is corrupted: {}", e)))?; + + Ok(Json("Config file is valid".to_string())) +} +#[derive(Serialize, ToSchema)] +pub struct CreateCustomProviderResponse { + pub provider_name: String, +} + +#[utoipa::path( + post, + path = "/config/custom-providers", + request_body = UpdateCustomProviderRequest, + responses( + (status = 200, description = "Custom provider created successfully", body = CreateCustomProviderResponse), + (status = 400, description = "Invalid request"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn create_custom_provider( + Json(request): Json, +) -> Result, ErrorResponse> { + let config = goose::config::declarative_providers::create_custom_provider( + goose::config::declarative_providers::CreateCustomProviderParams { + engine: request.engine, + display_name: request.display_name, + api_url: request.api_url, + api_key: normalize_custom_provider_api_key(request.api_key), + models: request.models, + supports_streaming: request.supports_streaming, + headers: request.headers, + requires_auth: request.requires_auth, + catalog_provider_id: request.catalog_provider_id, + base_path: request.base_path, + preserves_thinking: request.preserves_thinking, + }, + )?; + + goose::providers::refresh_custom_providers().await?; + + Ok(Json(CreateCustomProviderResponse { + provider_name: config.id().to_string(), + })) +} + +#[utoipa::path( + get, + path = "/config/custom-providers/{id}", + responses( + (status = 200, description = "Custom provider retrieved successfully", body = LoadedProvider), + (status = 404, description = "Provider not found"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn get_custom_provider( + Path(id): Path, +) -> Result, ErrorResponse> { + let loaded_provider = goose::config::declarative_providers::load_provider(id.as_str()) + .map_err(|e| { + ErrorResponse::not_found(format!("Custom provider '{}' not found: {}", id, e)) + })?; + + Ok(Json(loaded_provider)) +} + +#[utoipa::path( + delete, + path = "/config/custom-providers/{id}", + responses( + (status = 200, description = "Custom provider removed successfully", body = String), + (status = 404, description = "Provider not found"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn remove_custom_provider(Path(id): Path) -> Result, ErrorResponse> { + goose::config::declarative_providers::remove_custom_provider(&id)?; + + goose::providers::refresh_custom_providers().await?; + + Ok(Json(format!("Removed custom provider: {}", id))) +} + +#[utoipa::path( + post, + path = "/config/providers/{name}/cleanup", + params( + ("name" = String, Path, description = "Provider name (e.g., githubcopilot)") + ), + responses( + (status = 200, description = "Provider cache cleaned up successfully", body = String), + (status = 500, description = "Internal server error") + ) +)] +pub async fn cleanup_provider_cache( + Path(name): Path, +) -> Result, ErrorResponse> { + goose::providers::cleanup_provider(&name).await?; + Ok(Json(format!("Cleaned up provider cache: {}", name))) +} + +#[utoipa::path( + put, + path = "/config/custom-providers/{id}", + request_body = UpdateCustomProviderRequest, + responses( + (status = 200, description = "Custom provider updated successfully", body = String), + (status = 404, description = "Provider not found"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn update_custom_provider( + Path(id): Path, + Json(request): Json, +) -> Result, ErrorResponse> { + goose::config::declarative_providers::update_custom_provider( + goose::config::declarative_providers::UpdateCustomProviderParams { + id: id.clone(), + engine: request.engine, + display_name: request.display_name, + api_url: request.api_url, + api_key: normalize_custom_provider_api_key(request.api_key), + models: request.models, + supports_streaming: request.supports_streaming, + headers: request.headers, + requires_auth: request.requires_auth, + catalog_provider_id: request.catalog_provider_id, + base_path: request.base_path, + preserves_thinking: request.preserves_thinking, + }, + )?; + + goose::providers::refresh_custom_providers().await?; + + Ok(Json(format!("Updated custom provider: {}", id))) +} + +#[utoipa::path( + post, + path = "/config/check_provider", + request_body = CheckProviderRequest, +)] +pub async fn check_provider( + Json(CheckProviderRequest { provider }): Json, +) -> Result<(), ErrorResponse> { + create_with_default_model(&provider, Vec::new()) + .await + .map_err(|err| { + ErrorResponse::bad_request(format!("Provider '{}' check failed: {}", provider, err)) + })?; + Ok(()) +} + +#[utoipa::path( + post, + path = "/config/set_provider", + request_body = SetProviderRequest, +)] +pub async fn set_config_provider( + Json(SetProviderRequest { provider, model }): Json, +) -> Result<(), ErrorResponse> { + create_with_default_model(&provider, Vec::new()) + .await + .and_then(|_| { + let config = Config::global(); + goose::config::set_active_provider(config, &provider, &model) + .map_err(|e| anyhow::anyhow!(e)) + }) + .map_err(|err| { + ErrorResponse::bad_request(format!( + "Failed to set provider to '{}' with model '{}': {}", + provider, model, err + )) + })?; + Ok(()) +} + +#[utoipa::path( + get, + path = "/config/provider-catalog", + params( + ("format" = Option, Query, description = "Filter by provider format (openai, anthropic, ollama)") + ), + responses( + (status = 200, description = "Provider catalog retrieved successfully", body = [ProviderCatalogEntry]), + (status = 400, description = "Invalid format parameter") + ) +)] +pub async fn get_provider_catalog( + axum::extract::Query(params): axum::extract::Query>, +) -> Result>, ErrorResponse> { + let format_str = params.get("format").map(|s| s.as_str()).unwrap_or("openai"); + + let format = format_str.parse::().map_err(|_| { + ErrorResponse::bad_request(format!( + "Invalid format '{}'. Must be one of: openai, anthropic, ollama", + format_str + )) + })?; + + let providers = get_providers_by_format(format).await; + Ok(Json(providers)) +} + +#[utoipa::path( + get, + path = "/config/provider-catalog/{id}", + params( + ("id" = String, Path, description = "Provider ID from models.dev") + ), + responses( + (status = 200, description = "Provider template retrieved successfully", body = ProviderTemplate), + (status = 404, description = "Provider not found in catalog") + ) +)] +pub async fn get_provider_catalog_template( + Path(id): Path, +) -> Result, ErrorResponse> { + let template = get_provider_template(&id).ok_or_else(|| { + ErrorResponse::not_found(format!("Provider '{}' not found in catalog", id)) + })?; + + Ok(Json(template)) +} + +pub fn routes(state: Arc) -> Router { + Router::new() + .route("/config", get(read_all_config)) + .route("/config/upsert", post(upsert_config)) + .route("/config/remove", post(remove_config)) + .route("/config/read", post(read_config)) + .route("/config/provider-secrets", get(list_provider_secrets)) + .route( + "/config/provider-secrets/{id}", + delete(delete_provider_secret), + ) + .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( + "/config/providers/{name}/model-info", + post(get_provider_model_info), + ) + .route("/config/provider-catalog", get(get_provider_catalog)) + .route( + "/config/provider-catalog/{id}", + get(get_provider_catalog_template), + ) + .route( + "/config/providers/{name}/cleanup", + post(cleanup_provider_cache), + ) + .route("/config/slash_commands", get(get_slash_commands)) + .route( + "/config/canonical-model-info", + post(get_canonical_model_info), + ) + .route("/config/validate", get(validate_config)) + .route("/config/custom-providers", post(create_custom_provider)) + .route( + "/config/custom-providers/{id}", + delete(remove_custom_provider), + ) + .route("/config/custom-providers/{id}", put(update_custom_provider)) + .route("/config/custom-providers/{id}", get(get_custom_provider)) + .route("/config/check_provider", post(check_provider)) + .route("/config/set_provider", post(set_config_provider)) + .with_state(state) +} diff --git a/crates/goose-server/src/routes/dictation.rs b/crates/goose-server/src/routes/dictation.rs new file mode 100644 index 0000000000..78602223e4 --- /dev/null +++ b/crates/goose-server/src/routes/dictation.rs @@ -0,0 +1,374 @@ +use crate::routes::errors::ErrorResponse; +use crate::state::AppState; +use axum::{ + extract::DefaultBodyLimit, + http::StatusCode, + routing::{get, post}, + Json, Router, +}; +#[cfg(feature = "local-inference")] +use axum::{extract::Path, routing::delete}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +#[cfg(feature = "local-inference")] +use goose::dictation::providers::transcribe_local; +use goose::dictation::providers::{ + all_providers, is_configured, transcribe_with_provider, DictationProvider, +}; +#[cfg(feature = "local-inference")] +use goose::dictation::whisper; +#[cfg(feature = "local-inference")] +use goose::download_manager::{get_download_manager, DownloadProgress}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use utoipa::ToSchema; + +const MAX_AUDIO_SIZE_BYTES: usize = 50 * 1024 * 1024; + +#[cfg(feature = "local-inference")] +#[derive(Debug, Serialize, ToSchema)] +pub struct WhisperModelResponse { + #[serde(flatten)] + #[schema(inline)] + model: &'static whisper::WhisperModel, + downloaded: bool, + recommended: bool, +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct TranscribeRequest { + /// Base64 encoded audio data + pub audio: String, + /// MIME type of the audio (e.g., "audio/webm", "audio/wav") + pub mime_type: String, + /// Transcription provider to use + pub provider: DictationProvider, +} + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct TranscribeResponse { + /// Transcribed text from the audio + pub text: String, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct DictationProviderStatus { + /// Whether the provider is fully configured and ready to use + pub configured: bool, + /// Custom host URL if configured (only for providers that support it) + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + /// Description of what this provider does + pub description: String, + /// Whether this provider uses the main provider config (true) or has its own key (false) + pub uses_provider_config: bool, + /// Path to settings if uses_provider_config is true + #[serde(skip_serializing_if = "Option::is_none")] + pub settings_path: Option, + /// Config key name if uses_provider_config is false + #[serde(skip_serializing_if = "Option::is_none")] + pub config_key: Option, +} + +fn validate_audio(audio: &str, mime_type: &str) -> Result<(Vec, &'static str), ErrorResponse> { + let audio_bytes = BASE64 + .decode(audio) + .map_err(|_| ErrorResponse::bad_request("Invalid base64 audio data"))?; + + let extension = match mime_type { + "audio/webm" | "audio/webm;codecs=opus" => "webm", + "audio/mp4" => "mp4", + "audio/mpeg" | "audio/mpga" => "mp3", + "audio/m4a" => "m4a", + "audio/wav" | "audio/x-wav" => "wav", + _ => { + return Err(ErrorResponse { + message: format!("Unsupported audio format: {}", mime_type), + status: StatusCode::UNSUPPORTED_MEDIA_TYPE, + }); + } + }; + + Ok((audio_bytes, extension)) +} + +fn convert_error(e: anyhow::Error) -> ErrorResponse { + let error_msg = e.to_string(); + + if error_msg.contains("Invalid API key") { + ErrorResponse { + message: error_msg, + status: StatusCode::UNAUTHORIZED, + } + } else if error_msg.contains("Rate limit exceeded") || error_msg.contains("quota") { + ErrorResponse { + message: error_msg, + status: StatusCode::TOO_MANY_REQUESTS, + } + } else if error_msg.contains("not configured") { + ErrorResponse { + message: error_msg, + status: StatusCode::PRECONDITION_FAILED, + } + } else if error_msg.contains("timeout") { + ErrorResponse { + message: error_msg, + status: StatusCode::GATEWAY_TIMEOUT, + } + } else if error_msg.contains("API error") { + ErrorResponse { + message: error_msg, + status: StatusCode::BAD_GATEWAY, + } + } else { + ErrorResponse::internal(error_msg) + } +} + +#[utoipa::path( + post, + path = "/dictation/transcribe", + request_body = TranscribeRequest, + responses( + (status = 200, description = "Audio transcribed successfully", body = TranscribeResponse), + (status = 400, description = "Invalid request (bad base64 or unsupported format)"), + (status = 401, description = "Invalid API key"), + (status = 412, description = "Provider not configured"), + (status = 413, description = "Audio file too large (max 50MB)"), + (status = 429, description = "Rate limit exceeded"), + (status = 500, description = "Internal server error"), + (status = 502, description = "Provider API error"), + (status = 503, description = "Service unavailable"), + (status = 504, description = "Request timeout") + ) +)] +pub async fn transcribe_dictation( + Json(request): Json, +) -> Result, ErrorResponse> { + let (audio_bytes, extension) = validate_audio(&request.audio, &request.mime_type)?; + + let text = match request.provider { + DictationProvider::OpenAI => transcribe_with_provider( + DictationProvider::OpenAI, + "model".to_string(), + "whisper-1".to_string(), + audio_bytes, + extension, + &request.mime_type, + ) + .await + .map_err(convert_error)?, + DictationProvider::Groq => transcribe_with_provider( + DictationProvider::Groq, + "model".to_string(), + "whisper-large-v3-turbo".to_string(), + audio_bytes, + extension, + &request.mime_type, + ) + .await + .map_err(convert_error)?, + DictationProvider::ElevenLabs => transcribe_with_provider( + DictationProvider::ElevenLabs, + "model_id".to_string(), + "scribe_v1".to_string(), + audio_bytes, + extension, + &request.mime_type, + ) + .await + .map_err(convert_error)?, + #[cfg(feature = "local-inference")] + DictationProvider::Local => transcribe_local(audio_bytes).await.map_err(convert_error)?, + }; + + Ok(Json(TranscribeResponse { text })) +} + +#[utoipa::path( + get, + path = "/dictation/config", + responses( + (status = 200, description = "Audio transcription provider configurations", body = HashMap) + ) +)] +pub async fn get_dictation_config( +) -> Result>, ErrorResponse> { + let config = goose::config::Config::global(); + let mut providers = HashMap::new(); + + for def in all_providers() { + let provider = def.provider; + let configured = is_configured(provider); + + let host = if let Some(host_key) = def.host_key { + config + .get(host_key, false) + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + } else { + None + }; + + providers.insert( + provider, + DictationProviderStatus { + configured, + host, + description: def.description.to_string(), + uses_provider_config: def.uses_provider_config, + settings_path: def.settings_path.map(|s| s.to_string()), + config_key: if !def.uses_provider_config { + Some(def.config_key.to_string()) + } else { + None + }, + }, + ); + } + + Ok(Json(providers)) +} + +#[cfg(feature = "local-inference")] +#[utoipa::path( + get, + path = "/dictation/models", + responses( + (status = 200, description = "List of available Whisper models", body = Vec) + ) +)] +pub async fn list_models() -> Result>, ErrorResponse> { + let recommended_id = whisper::recommend_model(); + let models = whisper::available_models() + .iter() + .map(|m| WhisperModelResponse { + model: m, + downloaded: m.is_downloaded(), + recommended: m.id == recommended_id, + }) + .collect(); + + Ok(Json(models)) +} + +#[cfg(feature = "local-inference")] +#[utoipa::path( + post, + path = "/dictation/models/{model_id}/download", + responses( + (status = 202, description = "Download started"), + (status = 400, description = "Download already in progress"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn download_model(Path(model_id): Path) -> Result { + let model = whisper::get_model(&model_id) + .ok_or_else(|| ErrorResponse::bad_request("Model not found"))?; + + let manager = get_download_manager(); + let model_id_for_config = model.id.to_string(); + manager + .download_model( + model.id.to_string(), + model.url.to_string(), + model.local_path(), + Some(Box::new(move || { + let _ = goose::config::Config::global() + .set_param(whisper::LOCAL_WHISPER_MODEL_CONFIG_KEY, model_id_for_config); + })), + ) + .await + .map_err(convert_error)?; + + Ok(StatusCode::ACCEPTED) +} + +#[cfg(feature = "local-inference")] +#[utoipa::path( + get, + path = "/dictation/models/{model_id}/download", + responses( + (status = 200, description = "Download progress", body = DownloadProgress), + (status = 404, description = "Download not found") + ) +)] +pub async fn get_download_progress( + Path(model_id): Path, +) -> Result, ErrorResponse> { + let manager = get_download_manager(); + let progress = manager + .get_progress(&model_id) + .ok_or_else(|| ErrorResponse::bad_request("Download not found"))?; + + Ok(Json(progress)) +} + +#[cfg(feature = "local-inference")] +#[utoipa::path( + delete, + path = "/dictation/models/{model_id}/download", + responses( + (status = 200, description = "Download cancelled"), + (status = 404, description = "Download not found") + ) +)] +pub async fn cancel_download(Path(model_id): Path) -> Result { + let manager = get_download_manager(); + manager.cancel_download(&model_id).map_err(convert_error)?; + Ok(StatusCode::OK) +} + +#[cfg(feature = "local-inference")] +#[utoipa::path( + delete, + path = "/dictation/models/{model_id}", + responses( + (status = 200, description = "Model deleted"), + (status = 404, description = "Model not found or not downloaded"), + (status = 500, description = "Failed to delete model") + ) +)] +pub async fn delete_model(Path(model_id): Path) -> Result { + let model = whisper::get_model(&model_id) + .ok_or_else(|| ErrorResponse::bad_request("Model not found"))?; + + let path = model.local_path(); + + if !path.exists() { + return Err(ErrorResponse::bad_request("Model not downloaded")); + } + + tokio::fs::remove_file(&path) + .await + .map_err(|e| ErrorResponse::internal(format!("Failed to delete model: {}", e)))?; + + Ok(StatusCode::OK) +} + +pub fn routes(state: Arc) -> Router { + let router = Router::new() + .route("/dictation/transcribe", post(transcribe_dictation)) + .route("/dictation/config", get(get_dictation_config)); + + #[cfg(feature = "local-inference")] + let router = router + .route("/dictation/models", get(list_models)) + .route( + "/dictation/models/{model_id}/download", + post(download_model), + ) + .route( + "/dictation/models/{model_id}/download", + get(get_download_progress), + ) + .route( + "/dictation/models/{model_id}/download", + delete(cancel_download), + ) + .route("/dictation/models/{model_id}", delete(delete_model)); + + router + .layer(DefaultBodyLimit::max(MAX_AUDIO_SIZE_BYTES)) + .with_state(state) +} diff --git a/crates/goose-server/src/routes/errors.rs b/crates/goose-server/src/routes/errors.rs new file mode 100644 index 0000000000..ab78df291a --- /dev/null +++ b/crates/goose-server/src/routes/errors.rs @@ -0,0 +1,129 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use goose::config::ConfigError; +use goose_providers::errors::ProviderError; +use serde::Serialize; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, ToSchema)] +pub struct ErrorResponse { + pub message: String, + #[serde(skip)] + pub status: StatusCode, +} + +impl ErrorResponse { + pub(crate) fn internal(message: impl Into) -> Self { + Self { + message: message.into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn bad_request(message: impl Into) -> Self { + Self { + message: message.into(), + status: StatusCode::BAD_REQUEST, + } + } + + pub(crate) fn not_found(message: impl Into) -> Self { + Self { + message: message.into(), + status: StatusCode::NOT_FOUND, + } + } + + pub(crate) fn unprocessable(message: impl Into) -> Self { + Self { + message: message.into(), + status: StatusCode::UNPROCESSABLE_ENTITY, + } + } +} + +impl IntoResponse for ErrorResponse { + fn into_response(self) -> Response { + if self.status.is_server_error() { + tracing::error!(status = %self.status, message = %self.message, "server error response"); + } else if self.status.is_client_error() { + tracing::warn!(status = %self.status, message = %self.message, "client error response"); + } + + let body = Json(serde_json::json!({ + "message": self.message, + })); + + (self.status, body).into_response() + } +} + +impl From for ErrorResponse { + fn from(err: anyhow::Error) -> Self { + Self::internal(err.to_string()) + } +} + +impl From for ErrorResponse { + fn from(err: ConfigError) -> Self { + match err { + ConfigError::NotFound(key) => Self::not_found(format!("Config key not found: {}", key)), + _ => Self::internal(err.to_string()), + } + } +} + +impl From for ErrorResponse { + fn from(status: StatusCode) -> Self { + let message = status.canonical_reason().unwrap_or("Unknown error"); + Self { + message: message.to_string(), + status, + } + } +} + +impl From for ErrorResponse { + fn from(err: std::io::Error) -> Self { + Self::internal(format!("IO error: {}", err)) + } +} + +impl From for ErrorResponse { + fn from(err: serde_json::Error) -> Self { + Self::internal(format!("JSON serialization error: {}", err)) + } +} + +impl From for ErrorResponse { + fn from(err: serde_yaml::Error) -> Self { + Self::unprocessable(format!("YAML parsing error: {}", err)) + } +} + +impl From for ErrorResponse { + fn from(err: ProviderError) -> Self { + let (status, message) = match err { + ProviderError::Authentication(_) => ( + StatusCode::BAD_REQUEST, + format!("Authentication failed: {}", err), + ), + ProviderError::UsageError(_) => { + (StatusCode::BAD_REQUEST, format!("Usage error: {}", err)) + } + ProviderError::RateLimitExceeded { .. } => ( + StatusCode::TOO_MANY_REQUESTS, + format!("Rate limit exceeded: {}", err), + ), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Provider error: {}", err), + ), + }; + + Self { message, status } + } +} diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs new file mode 100644 index 0000000000..39e84bb463 --- /dev/null +++ b/crates/goose-server/src/routes/local_inference.rs @@ -0,0 +1,885 @@ +use std::path::PathBuf; + +use crate::routes::errors::ErrorResponse; +use crate::state::AppState; +use axum::{ + extract::{Path, Query}, + http::StatusCode, + routing::{delete, get, post}, + Json, Router, +}; +use futures::future::join_all; +use goose::config::paths::Paths; +use goose::download_manager::{get_download_manager, DownloadProgress, DownloadStatus}; +use goose::providers::huggingface_auth; +use goose::providers::local_inference::hf_models::{self, HfModelInfo, HfModelVariant}; +use goose::providers::local_inference::{ + available_inference_memory_bytes, builtin_chat_template_names, + hf_models::{ + register_resolved_model, resolve_local_model_selection, resolve_local_model_spec, + resolve_model_spec, HfGgufFile, + }, + local_model_registry::{ + default_settings_for_model, featured_mmproj_spec, get_registry, model_id_from_repo, + LocalModelEntry, LocalModelStorage, ModelDownloadStatus as RegistryDownloadStatus, + ModelSettings, FEATURED_MODELS, + }, + recommend_local_model, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::debug; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(tag = "state")] +pub enum ModelDownloadStatus { + NotDownloaded, + Downloading { + progress_percent: f32, + bytes_downloaded: u64, + total_bytes: u64, + speed_bps: Option, + }, + Downloaded, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct LocalModelResponse { + pub id: String, + pub repo_id: String, + pub filename: String, + pub quantization: String, + pub size_bytes: u64, + pub status: ModelDownloadStatus, + pub recommended: bool, + pub settings: ModelSettings, + pub vision_capable: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub mmproj_status: Option, +} + +async fn ensure_featured_models_in_registry() -> Result<(), ErrorResponse> { + let mut mmproj_downloads_needed: Vec<(String, String, PathBuf)> = Vec::new(); + + struct PendingResolve { + spec: &'static str, + repo_id: String, + quantization: String, + model_id: String, + } + let mut to_resolve = Vec::new(); + + for featured in FEATURED_MODELS { + let (repo_id, quantization) = match hf_models::parse_model_spec(featured.spec) { + Ok(parts) => parts, + Err(_) => continue, + }; + + let model_id = model_id_from_repo(&repo_id, &quantization); + + { + let registry = get_registry() + .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 !needs_backfill { + continue; + } + // Fall through to resolve for backfill + } + } + + to_resolve.push(PendingResolve { + spec: featured.spec, + repo_id, + quantization, + model_id, + }); + } + + let resolved: Vec<(PendingResolve, HfGgufFile)> = + join_all(to_resolve.into_iter().map(|pending| async move { + let hf_file = match resolve_model_spec(pending.spec).await { + Ok((_repo, file)) => file, + 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 + ), + } + } + }; + (pending, hf_file) + })) + .await; + + let entries_to_add: Vec = resolved + .into_iter() + .map(|(pending, hf_file)| { + let local_path = Paths::in_data_dir("models").join(&hf_file.filename); + let settings = default_settings_for_model(&pending.model_id); + LocalModelEntry { + id: pending.model_id, + repo_id: pending.repo_id, + filename: hf_file.filename, + quantization: pending.quantization, + local_path, + source_url: hf_file.download_url, + backend_id: settings.backend_id.clone(), + storage: LocalModelStorage::GooseManaged, + settings, + size_bytes: hf_file.size_bytes, + mmproj_path: None, + mmproj_source_url: None, + mmproj_size_bytes: 0, + mmproj_checked: false, + shard_files: vec![], + } + }) + .collect(); + + { + let mut registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + if !entries_to_add.is_empty() { + registry.sync_with_featured(entries_to_add); + } + + // 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(); + if model.is_downloaded() { + if let Some(mmproj) = featured_mmproj_spec(&model.id) { + let path = mmproj.local_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)); + } + } + } + } + let _ = registry.save(); + } + + // Auto-download mmproj files for models that are already downloaded. + // Deduplicate by path since multiple quants share one mmproj file. + let dm = get_download_manager(); + let hf_token = huggingface_auth::resolve_token_async().await.ok().flatten(); + let mut started_paths = std::collections::HashSet::new(); + for (model_id, url, path) in mmproj_downloads_needed { + if !path.exists() && started_paths.insert(path.clone()) { + let download_id = format!("{}-mmproj", model_id); + let dominated_by_active = dm + .get_progress(&download_id) + .is_some_and(|p| p.status == goose::download_manager::DownloadStatus::Downloading); + if !dominated_by_active { + tracing::info!(model_id = %model_id, "Auto-downloading vision encoder for existing model"); + if let Err(e) = dm + .download_model_with_bearer_token( + download_id, + url, + path, + hf_token.clone(), + None, + ) + .await + { + tracing::warn!(model_id = %model_id, error = %e, "Failed to start mmproj download"); + } + } + } + } + + Ok(()) +} + +#[utoipa::path( + post, + path = "/local-inference/sync-featured", + responses( + (status = 200, description = "Featured models synced to registry") + ) +)] +pub async fn sync_featured_models() -> Result { + ensure_featured_models_in_registry().await?; + Ok(StatusCode::OK) +} + +#[utoipa::path( + get, + path = "/local-inference/models", + responses( + (status = 200, description = "List of available local LLM models", body = Vec) + ) +)] +pub async fn list_local_models( + axum::extract::State(state): axum::extract::State>, +) -> Result>, ErrorResponse> { + let runtime = state.get_inference_runtime()?; + let recommended_id = recommend_local_model(&runtime); + + let registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + let mut models: Vec = Vec::new(); + + for entry in registry.list_models() { + let goose_status = entry.download_status(); + + let status = match goose_status { + RegistryDownloadStatus::NotDownloaded => ModelDownloadStatus::NotDownloaded, + RegistryDownloadStatus::Downloading { + progress_percent, + bytes_downloaded, + total_bytes, + speed_bps, + } => ModelDownloadStatus::Downloading { + progress_percent, + bytes_downloaded, + total_bytes, + speed_bps: Some(speed_bps), + }, + RegistryDownloadStatus::Downloaded => ModelDownloadStatus::Downloaded, + }; + + let size_bytes = entry.file_size(); + + let vision_capable = entry.settings.vision_capable; + let mmproj_status = if vision_capable { + let ms = entry.mmproj_download_status(); + Some(match ms { + RegistryDownloadStatus::NotDownloaded => ModelDownloadStatus::NotDownloaded, + RegistryDownloadStatus::Downloading { + progress_percent, + bytes_downloaded, + total_bytes, + speed_bps, + } => ModelDownloadStatus::Downloading { + progress_percent, + bytes_downloaded, + total_bytes, + speed_bps: Some(speed_bps), + }, + RegistryDownloadStatus::Downloaded => ModelDownloadStatus::Downloaded, + }) + } else { + None + }; + + models.push(LocalModelResponse { + id: entry.id.clone(), + repo_id: entry.repo_id.clone(), + filename: entry.filename.clone(), + quantization: entry.quantization.clone(), + size_bytes, + status, + recommended: recommended_id == entry.id, + settings: entry.settings.clone(), + vision_capable, + mmproj_status, + }); + } + + models.sort_by(|a, b| { + let a_downloaded = matches!(a.status, ModelDownloadStatus::Downloaded); + let b_downloaded = matches!(b.status, ModelDownloadStatus::Downloaded); + match (b_downloaded, a_downloaded) { + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + _ => a.id.cmp(&b.id), + } + }); + + Ok(Json(models)) +} + +#[derive(Debug, Deserialize)] +pub struct SearchQuery { + pub q: String, + pub limit: Option, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct RepoVariantsResponse { + pub variants: Vec, + pub recommended_index: Option, + pub available_memory_bytes: u64, + pub downloaded_quants: Vec, + pub downloaded_variants: Vec, +} + +#[utoipa::path( + get, + path = "/local-inference/search", + params( + ("q" = String, Query, description = "Search query"), + ("limit" = Option, Query, description = "Max results") + ), + responses( + (status = 200, description = "Search results", body = Vec), + (status = 500, description = "Search failed") + ) +)] +pub async fn search_hf_models( + Query(params): Query, +) -> Result>, ErrorResponse> { + let limit = params.limit.unwrap_or(20).min(50); + let results = hf_models::search_local_models(¶ms.q, limit) + .await + .map_err(|e| ErrorResponse::internal(format!("Search failed: {}", e)))?; + Ok(Json(results)) +} + +#[utoipa::path( + get, + path = "/local-inference/repo/{author}/{repo}/files", + responses( + (status = 200, description = "GGUF files in the repo", body = RepoVariantsResponse) + ) +)] +pub async fn get_repo_files( + axum::extract::State(state): axum::extract::State>, + Path((author, repo)): Path<(String, String)>, +) -> Result, ErrorResponse> { + let repo_id = format!("{}/{}", author, repo); + let variants = hf_models::get_repo_local_variants(&repo_id) + .await + .map_err(|e| ErrorResponse::internal(format!("Failed to fetch repo files: {}", e)))?; + + let runtime = state.get_inference_runtime()?; + let available_memory = available_inference_memory_bytes(&runtime); + let gguf_variants: Vec<_> = variants + .iter() + .filter(|variant| variant.backend_id == "llamacpp") + .map( + |variant| goose::providers::local_inference::hf_models::HfQuantVariant { + quantization: variant.variant_id.clone(), + size_bytes: variant.size_bytes, + filename: variant.filename.clone().unwrap_or_default(), + download_url: variant.download_url.clone().unwrap_or_default(), + description: "", + quality_rank: variant.quality_rank, + sharded: variant.sharded, + }, + ) + .collect(); + let recommended_index = hf_models::recommend_variant(&gguf_variants, available_memory); + + let (downloaded_quants, downloaded_variants) = { + let registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + let models: Vec<_> = registry + .list_models() + .iter() + .filter(|m| m.repo_id == repo_id && m.is_downloaded()) + .collect(); + ( + models.iter().map(|m| m.quantization.clone()).collect(), + models.iter().map(|m| m.id.clone()).collect(), + ) + }; + + Ok(Json(RepoVariantsResponse { + variants, + recommended_index, + available_memory_bytes: available_memory, + downloaded_quants, + downloaded_variants, + })) +} + +#[derive(Debug, Deserialize, ToSchema)] +pub struct DownloadModelRequest { + /// Model spec/download id like "bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M" or "google/gemma-4-31B-it" + pub spec: String, + /// Optional backend id for callers selecting a concrete variant row. + pub backend_id: Option, + /// Optional backend-specific variant id, such as a GGUF quantization or MLX dtype. + pub variant_id: Option, +} + +#[derive(Clone)] +struct LocalModelSelection { + repo_id: String, + backend_id: String, + variant_id: Option, +} + +fn explicit_model_selection( + req: &DownloadModelRequest, +) -> anyhow::Result> { + if let Some(backend_id) = req.backend_id.as_deref() { + let (repo_id, parsed_variant_id) = hf_models::parse_model_spec(&req.spec) + .map(|(repo_id, quantization)| (repo_id, Some(quantization))) + .unwrap_or_else(|_| (req.spec.clone(), None)); + let variant_id = req.variant_id.clone().or(parsed_variant_id); + match backend_id { + "mlx" | "llamacpp" => Ok(Some(LocalModelSelection { + repo_id, + backend_id: backend_id.to_string(), + variant_id, + })), + _ => anyhow::bail!("Unknown local inference backend '{}'", backend_id), + } + } else { + Ok(None) + } +} + +async fn local_model_id_from_request( + req: &DownloadModelRequest, + selection: Option<&LocalModelSelection>, +) -> anyhow::Result { + if let Some(selection) = selection { + return match selection.backend_id.as_str() { + "mlx" => Ok(selection.repo_id.clone()), + "llamacpp" => { + let quantization = selection.variant_id.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "llama.cpp model '{}' is missing a quantization", + selection.repo_id + ) + })?; + Ok(model_id_from_repo(&selection.repo_id, quantization)) + } + _ => anyhow::bail!("Unknown local inference backend '{}'", selection.backend_id), + }; + } + + if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { + return Ok(model_id_from_repo(&repo_id, &quantization)); + } + + let variants = hf_models::get_repo_local_variants(&req.spec).await?; + let has_llamacpp = variants + .iter() + .any(|variant| variant.backend_id == "llamacpp"); + let mlx_variants: Vec<_> = variants + .iter() + .filter(|variant| variant.backend_id == "mlx") + .collect(); + if mlx_variants.len() == 1 && !has_llamacpp { + Ok(req.spec.clone()) + } else { + anyhow::bail!( + "Model spec '{}' is ambiguous; choose one of: {}", + req.spec, + variants + .iter() + .map(|variant| variant.download_id.as_str()) + .collect::>() + .join(", ") + ) + } +} + +fn mark_download_failed(model_id: &str, error: impl std::fmt::Display) { + let manager = get_download_manager(); + let download_id = format!("{}-model", model_id); + if manager.get_progress(&download_id).is_none() { + manager.set_progress(DownloadProgress { + model_id: download_id.clone(), + status: DownloadStatus::Failed, + bytes_downloaded: 0, + total_bytes: 0, + progress_percent: 0.0, + speed_bps: None, + eta_seconds: None, + error: Some(error.to_string()), + task_exited: true, + }); + return; + } + + manager.update_progress(&download_id, |progress| { + if progress.status != DownloadStatus::Cancelled { + progress.status = DownloadStatus::Failed; + progress.error = Some(error.to_string()); + } + progress.task_exited = true; + }); +} + +fn model_download_completed(model_id: &str) -> bool { + get_download_manager() + .get_progress(&format!("{}-model", model_id)) + .is_some_and(|progress| progress.status == DownloadStatus::Completed) +} + +fn register_pending_download_model( + model_id: &str, + req: &DownloadModelRequest, + selection: Option<&LocalModelSelection>, +) -> anyhow::Result<()> { + let (repo_id, backend_id, variant_id) = if let Some(selection) = selection { + ( + selection.repo_id.clone(), + selection.backend_id.clone(), + selection + .variant_id + .clone() + .unwrap_or_else(|| "default".to_string()), + ) + } else if let Ok((repo_id, quantization)) = hf_models::parse_model_spec(&req.spec) { + (repo_id, "llamacpp".to_string(), quantization) + } else { + (req.spec.clone(), "mlx".to_string(), "default".to_string()) + }; + + let mut registry = get_registry() + .lock() + .map_err(|_| anyhow::anyhow!("Failed to acquire registry lock"))?; + if registry.has_model(model_id) { + return Ok(()); + } + + let mut settings = default_settings_for_model(model_id); + if backend_id != "llamacpp" { + settings.backend_id = Some(backend_id.clone()); + } + + let filename = variant_id.clone(); + registry.add_model(LocalModelEntry { + id: model_id.to_string(), + repo_id, + filename: filename.clone(), + quantization: variant_id, + local_path: Paths::in_data_dir("models").join(filename), + source_url: req.spec.clone(), + backend_id: settings.backend_id.clone(), + storage: LocalModelStorage::HuggingFaceCache, + settings, + size_bytes: 0, + mmproj_path: None, + mmproj_source_url: None, + mmproj_size_bytes: 0, + mmproj_checked: false, + shard_files: vec![], + }) +} + +#[utoipa::path( + post, + path = "/local-inference/download", + request_body = DownloadModelRequest, + responses( + (status = 202, description = "Download started", body = String), + (status = 400, description = "Invalid request") + ) +)] +pub async fn download_hf_model( + Json(req): Json, +) -> Result<(StatusCode, Json), ErrorResponse> { + let selection = explicit_model_selection(&req) + .map_err(|e| ErrorResponse::bad_request(format!("Invalid spec: {}", e)))?; + let model_id = local_model_id_from_request(&req, selection.as_ref()) + .await + .map_err(|e| ErrorResponse::bad_request(format!("Invalid spec: {}", e)))?; + let download_id = format!("{}-model", model_id); + let download_reserved = get_download_manager() + .reserve_download(DownloadProgress { + model_id: download_id, + status: DownloadStatus::Downloading, + bytes_downloaded: 0, + total_bytes: 0, + progress_percent: 0.0, + speed_bps: None, + eta_seconds: None, + error: None, + task_exited: false, + }) + .map_err(|e| ErrorResponse::internal(format!("Download failed: {}", e)))?; + if !download_reserved { + return Ok((StatusCode::ACCEPTED, Json(model_id))); + } + + if let Err(error) = register_pending_download_model(&model_id, &req, selection.as_ref()) { + mark_download_failed(&model_id, &error); + return Err(ErrorResponse::internal(format!( + "Failed to register download: {}", + error + ))); + } + + let spec = req.spec.clone(); + let selection_for_task = selection.clone(); + let model_id_for_task = model_id.clone(); + tokio::spawn(async move { + let resolved = if let Some(selection) = selection_for_task { + resolve_local_model_selection( + &selection.repo_id, + &selection.backend_id, + selection.variant_id.as_deref(), + ) + .await + } else { + resolve_local_model_spec(&spec).await + }; + match resolved { + Ok(resolved) => { + if !model_download_completed(&model_id_for_task) { + return; + } + if let Err(error) = register_resolved_model(resolved, &spec) { + mark_download_failed(&model_id_for_task, error); + } + } + Err(error) => mark_download_failed(&model_id_for_task, error), + } + }); + + Ok((StatusCode::ACCEPTED, Json(model_id))) +} + +#[utoipa::path( + get, + path = "/local-inference/models/{model_id}/download", + responses( + (status = 200, description = "Download progress", body = DownloadProgress), + (status = 404, description = "No active download") + ) +)] +pub async fn get_local_model_download_progress( + Path(model_id): Path, +) -> Result, ErrorResponse> { + let download_id = format!("{}-model", model_id); + debug!(model_id = %model_id, download_id = %download_id, "Getting download progress"); + + let manager = get_download_manager(); + + let model_progress = manager + .get_progress(&download_id) + .ok_or_else(|| ErrorResponse::not_found("No active download"))?; + + Ok(Json(model_progress)) +} + +#[utoipa::path( + delete, + path = "/local-inference/models/{model_id}/download", + responses( + (status = 200, description = "Download cancelled"), + (status = 404, description = "No active download") + ) +)] +pub async fn cancel_local_model_download( + Path(model_id): Path, +) -> Result { + let manager = get_download_manager(); + manager + .cancel_download(&format!("{}-model", model_id)) + .map_err(|e| ErrorResponse::internal(format!("{}", e)))?; + let _ = manager.cancel_download(&format!("{}-mmproj", model_id)); + + Ok(StatusCode::OK) +} + +#[utoipa::path( + delete, + path = "/local-inference/models/{model_id}", + responses( + (status = 200, description = "Model deleted"), + (status = 404, description = "Model not found") + ) +)] +pub async fn delete_local_model(Path(model_id): Path) -> Result { + let mut registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + if registry.get_model(&model_id).is_none() { + return Err(ErrorResponse::not_found("Model not found")); + } + registry + .delete_model(&model_id) + .map_err(|e| ErrorResponse::internal(format!("{}", e)))?; + + Ok(StatusCode::OK) +} + +#[utoipa::path( + get, + path = "/local-inference/models/{model_id}/settings", + responses( + (status = 200, description = "Model settings", body = ModelSettings), + (status = 404, description = "Model not found") + ) +)] +pub async fn get_model_settings( + Path(model_id): Path, +) -> Result, ErrorResponse> { + let registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + if let Some(settings) = registry.get_model_settings(&model_id) { + return Ok(Json(settings.clone())); + } + + Err(ErrorResponse::not_found("Model not found")) +} + +#[utoipa::path( + put, + path = "/local-inference/models/{model_id}/settings", + request_body = ModelSettings, + responses( + (status = 200, description = "Settings updated", body = ModelSettings), + (status = 404, description = "Model not found"), + (status = 500, description = "Failed to save settings") + ) +)] +pub async fn update_model_settings( + Path(model_id): Path, + Json(settings): Json, +) -> Result, ErrorResponse> { + let mut registry = get_registry() + .lock() + .map_err(|_| ErrorResponse::internal("Failed to acquire registry lock"))?; + + registry + .update_model_settings(&model_id, settings.clone()) + .map_err(|e| ErrorResponse::not_found(format!("{}", e)))?; + + 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() + .map(|reg| { + reg.list_models() + .iter() + .flat_map(|m| { + m.all_local_paths() + .map(|p| p.to_path_buf()) + .chain(m.mmproj_path.as_deref().map(|p| p.to_path_buf())) + }) + .collect() + }) + .unwrap_or_default(); + goose::download_manager::cleanup_partial_downloads( + &Paths::in_data_dir("models"), + ®istered_paths, + ); + + Router::new() + .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), + ) + .route("/local-inference/download", post(download_hf_model)) + .route( + "/local-inference/models/{model_id}/download", + get(get_local_model_download_progress), + ) + .route( + "/local-inference/models/{model_id}/download", + delete(cancel_local_model_download), + ) + .route( + "/local-inference/models/{model_id}", + delete(delete_local_model), + ) + .route( + "/local-inference/models/{model_id}/settings", + get(get_model_settings), + ) + .route( + "/local-inference/models/{model_id}/settings", + axum::routing::put(update_model_settings), + ) + .with_state(state) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn progress_for(model_id: &str, status: DownloadStatus) -> DownloadProgress { + DownloadProgress { + model_id: format!("{}-model", model_id), + status, + bytes_downloaded: 0, + total_bytes: 0, + progress_percent: 0.0, + speed_bps: None, + eta_seconds: None, + error: None, + task_exited: true, + } + } + + #[test] + fn model_download_completed_requires_completed_progress() { + let model_id = "test-completed-registration-gate"; + let manager = get_download_manager(); + manager.set_progress(progress_for(model_id, DownloadStatus::Completed)); + + assert!(model_download_completed(model_id)); + + manager.clear_completed(&format!("{}-model", model_id)); + } + + #[test] + fn model_download_completed_rejects_cancelled_progress() { + let model_id = "test-cancelled-registration-gate"; + let manager = get_download_manager(); + manager.set_progress(progress_for(model_id, DownloadStatus::Cancelled)); + + assert!(!model_download_completed(model_id)); + + manager.clear_completed(&format!("{}-model", model_id)); + } +} diff --git a/crates/goose-server/src/routes/mcp_app_proxy.rs b/crates/goose-server/src/routes/mcp_app_proxy.rs new file mode 100644 index 0000000000..2a4a7502cf --- /dev/null +++ b/crates/goose-server/src/routes/mcp_app_proxy.rs @@ -0,0 +1,295 @@ +use axum::{ + extract::Query, + http::{header, StatusCode}, + response::{Html, IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use serde::Deserialize; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; +use tokio::sync::RwLock; +use uuid::Uuid; + +const GUEST_HTML_TTL_SECS: u64 = 300; // 5 minutes +const GUEST_HTML_MAX_ENTRIES: usize = 64; + +/// In-memory store for guest HTML content. +/// Maps nonce -> (html_content, csp_string, created_at) +/// Entries are consumed on first read and evicted after TTL. +type GuestHtmlStore = Arc>>; + +#[derive(Deserialize)] +struct ProxyQuery { + secret: String, + /// Comma-separated list of domains for connect-src (fetch, XHR, WebSocket) + connect_domains: Option, + /// Comma-separated list of domains for resource loading (scripts, styles, images, fonts, media) + resource_domains: Option, + /// Comma-separated list of origins for nested iframes (frame-src) + frame_domains: Option, + /// Comma-separated list of allowed base URIs (base-uri) + base_uri_domains: Option, + /// Comma-separated list of domains for script-src (external scripts like SDKs) + script_domains: Option, +} + +#[derive(Deserialize)] +struct GuestQuery { + secret: String, + nonce: String, +} + +#[derive(Deserialize)] +struct StoreGuestBody { + secret: String, + html: String, + /// CSP string to apply to the guest page + csp: Option, +} + +const MCP_APP_PROXY_HTML: &str = include_str!("templates/mcp_app_proxy.html"); + +/// Build the outer sandbox CSP based on declared domains. +/// +/// This CSP acts as a ceiling - the inner guest UI iframe cannot exceed these +/// permissions, even if it tried. This is the single source of truth for +/// security policy enforcement. +/// +/// Based on the MCP Apps specification (ext-apps SEP): +/// +fn build_outer_csp( + connect_domains: &[String], + resource_domains: &[String], + frame_domains: &[String], + base_uri_domains: &[String], + script_domains: &[String], +) -> String { + let resources = if resource_domains.is_empty() { + String::new() + } else { + format!(" {}", resource_domains.join(" ")) + }; + + let scripts = if script_domains.is_empty() { + String::new() + } else { + format!(" {}", script_domains.join(" ")) + }; + + let connections = if connect_domains.is_empty() { + String::new() + } else { + format!(" {}", connect_domains.join(" ")) + }; + + // frame-src needs 'self' so the proxy can load the guest iframe from /mcp-app-guest + let frame_src = if frame_domains.is_empty() { + "frame-src 'self'".to_string() + } else { + format!("frame-src 'self' {}", frame_domains.join(" ")) + }; + + let base_uris = if base_uri_domains.is_empty() { + String::new() + } else { + format!(" {}", base_uri_domains.join(" ")) + }; + + format!( + "default-src 'none'; \ + script-src 'self' 'unsafe-inline'{resources}{scripts}; \ + script-src-elem 'self' 'unsafe-inline'{resources}{scripts}; \ + style-src 'self' 'unsafe-inline'{resources}; \ + style-src-elem 'self' 'unsafe-inline'{resources}; \ + connect-src 'self'{connections}; \ + img-src 'self' data: blob:{resources}; \ + font-src 'self'{resources}; \ + media-src 'self' data: blob:{resources}; \ + {frame_src}; \ + object-src 'none'; \ + base-uri 'self'{base_uris}" + ) +} + +/// Parse comma-separated domains, filtering out empty strings +fn parse_domains(domains: Option<&String>) -> Vec { + domains + .map(|d| { + d.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }) + .unwrap_or_default() +} + +#[derive(Clone)] +struct AppState { + secret_key: String, + guest_store: GuestHtmlStore, +} + +#[utoipa::path( + get, + path = "/mcp-app-proxy", + params( + ("secret" = String, Query, description = "Secret key for authentication"), + ("connect_domains" = Option, Query, description = "Comma-separated domains for connect-src"), + ("resource_domains" = Option, Query, description = "Comma-separated domains for resource loading"), + ("frame_domains" = Option, Query, description = "Comma-separated origins for nested iframes (frame-src)"), + ("base_uri_domains" = Option, Query, description = "Comma-separated allowed base URIs (base-uri)"), + ("script_domains" = Option, Query, description = "Comma-separated domains for script-src") + ), + responses( + (status = 200, description = "MCP App proxy HTML page", content_type = "text/html"), + (status = 401, description = "Unauthorized - invalid or missing secret"), + ) +)] +async fn mcp_app_proxy( + axum::extract::State(state): axum::extract::State, + Query(params): Query, +) -> Response { + if params.secret != state.secret_key { + return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); + } + + // Parse domains from query params + let connect_domains = parse_domains(params.connect_domains.as_ref()); + let resource_domains = parse_domains(params.resource_domains.as_ref()); + let frame_domains = parse_domains(params.frame_domains.as_ref()); + let base_uri_domains = parse_domains(params.base_uri_domains.as_ref()); + let script_domains = parse_domains(params.script_domains.as_ref()); + + // Build the outer CSP based on declared domains + let csp = build_outer_csp( + &connect_domains, + &resource_domains, + &frame_domains, + &base_uri_domains, + &script_domains, + ); + + // Replace the CSP placeholder in the HTML template + let html = MCP_APP_PROXY_HTML.replace("{{OUTER_CSP}}", &csp); + + ( + [ + (header::CONTENT_TYPE, "text/html; charset=utf-8"), + ( + header::HeaderName::from_static("referrer-policy"), + "no-referrer", + ), + ], + Html(html), + ) + .into_response() +} + +/// Store guest HTML and return a nonce for retrieval. +/// The proxy page calls this via fetch, then sets the guest iframe src to /mcp-app-guest?nonce=... +async fn store_guest_html( + axum::extract::State(state): axum::extract::State, + Json(body): Json, +) -> Response { + if body.secret != state.secret_key { + return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); + } + + let nonce = Uuid::new_v4().to_string(); + let csp = body.csp.unwrap_or_default(); + + { + let mut store = state.guest_store.write().await; + + // Evict expired entries + let cutoff = Instant::now() - std::time::Duration::from_secs(GUEST_HTML_TTL_SECS); + store.retain(|_, (_, _, created)| *created > cutoff); + + // If still at capacity, drop the oldest entry + if store.len() >= GUEST_HTML_MAX_ENTRIES { + if let Some(oldest_key) = store + .iter() + .min_by_key(|(_, (_, _, created))| *created) + .map(|(k, _)| k.clone()) + { + store.remove(&oldest_key); + } + } + + store.insert(nonce.clone(), (body.html, csp, Instant::now())); + } + + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/json")], + format!(r#"{{"nonce":"{}"}}"#, nonce), + ) + .into_response() +} + +/// Serve stored guest HTML with a real HTTPS URL. +/// This gives the guest iframe `window.location.protocol === "https:"`, +/// which is required by SDKs like Square Web Payments that check for secure context. +async fn serve_guest_html( + axum::extract::State(state): axum::extract::State, + Query(params): Query, +) -> Response { + if params.secret != state.secret_key { + return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); + } + + // Consume the entry (one-time use) + let entry = { + let mut store = state.guest_store.write().await; + store.remove(¶ms.nonce) + }; + + match entry { + Some((html, csp, _created)) => { + let mut response = Html(html).into_response(); + let headers = response.headers_mut(); + // Use strict-origin so third-party SDKs (e.g. Square Web Payments) + // receive the origin in their requests, which they need for auth. + // no-referrer would cause 401s from SDK servers. + headers.insert( + header::HeaderName::from_static("referrer-policy"), + header::HeaderValue::from_static("strict-origin"), + ); + if !csp.is_empty() { + match csp.parse::() { + Ok(csp_value) => { + headers.insert(header::CONTENT_SECURITY_POLICY, csp_value); + } + Err(_) => { + return ( + StatusCode::BAD_REQUEST, + "Invalid characters in Content-Security-Policy value", + ) + .into_response(); + } + } + } + response + } + None => ( + StatusCode::NOT_FOUND, + "Guest content not found or already consumed", + ) + .into_response(), + } +} + +pub fn routes(secret_key: String) -> Router { + let state = AppState { + secret_key, + guest_store: Arc::new(RwLock::new(HashMap::new())), + }; + + Router::new() + .route("/mcp-app-proxy", get(mcp_app_proxy)) + .route("/mcp-app-guest", get(serve_guest_html)) + .route("/mcp-app-guest", post(store_guest_html)) + .with_state(state) +} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs new file mode 100644 index 0000000000..45b0da38aa --- /dev/null +++ b/crates/goose-server/src/routes/mod.rs @@ -0,0 +1,47 @@ +pub mod action_required; +pub mod agent; +pub mod config_management; +pub mod dictation; +pub mod errors; +#[cfg(feature = "local-inference")] +pub mod local_inference; +pub mod mcp_app_proxy; +pub mod prompts; +pub mod recipe; +pub mod recipe_utils; +pub mod reply; +pub mod sampling; +pub mod schedule; +pub mod session; +pub mod session_events; +pub mod status; +pub mod telemetry; +pub mod utils; + +use std::sync::Arc; + +use axum::Router; + +// Function to configure all routes +pub fn configure(state: Arc, secret_key: String) -> Router { + let router = Router::new() + .merge(status::routes(state.clone())) + .merge(reply::routes(state.clone())) + .merge(action_required::routes(state.clone())) + .merge(agent::routes(state.clone())) + .merge(config_management::routes(state.clone())) + .merge(prompts::routes()) + .merge(recipe::routes(state.clone())) + .merge(session::routes(state.clone())) + .merge(schedule::routes(state.clone())) + .merge(telemetry::routes(state.clone())) + .merge(mcp_app_proxy::routes(secret_key)) + .merge(session_events::routes(state.clone())) + .merge(sampling::routes(state.clone())) + .merge(dictation::routes(state.clone())); + + #[cfg(feature = "local-inference")] + let router = router.merge(local_inference::routes(state)); + + router +} diff --git a/crates/goose-server/src/routes/prompts.rs b/crates/goose-server/src/routes/prompts.rs new file mode 100644 index 0000000000..b807dc5910 --- /dev/null +++ b/crates/goose-server/src/routes/prompts.rs @@ -0,0 +1,132 @@ +use crate::routes::errors::ErrorResponse; +use axum::{ + extract::Path, + routing::{delete, get, put}, + Json, Router, +}; +use goose::prompt_template::{ + get_template, list_templates, reset_template, save_template, Template, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, ToSchema)] +pub struct PromptsListResponse { + pub prompts: Vec