diff --git a/.cargo/config.toml b/.cargo/config.toml index 8bf45fb405..91bdb04b9a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,5 @@ [target.x86_64-pc-windows-msvc] -rustflags = ["-C", "link-args=/FORCE:MULTIPLE"] +rustflags = ["-C", "link-args=/FORCE:MULTIPLE /STACK:8388608"] [target.aarch64-pc-windows-msvc] -rustflags = ["-C", "link-args=/FORCE:MULTIPLE"] +rustflags = ["-C", "link-args=/FORCE:MULTIPLE /STACK:8388608"] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 29afce9c4a..d95ef3c462 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -# CODEOWNERS file for block/goose repository +# CODEOWNERS file for aaif-goose/goose repository # See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Documentation owned by DevRel diff --git a/.github/DISCUSSION_TEMPLATE/qa.yml b/.github/DISCUSSION_TEMPLATE/qa.yml index 9bbd676aff..c7c314e124 100644 --- a/.github/DISCUSSION_TEMPLATE/qa.yml +++ b/.github/DISCUSSION_TEMPLATE/qa.yml @@ -6,7 +6,7 @@ body: attributes: value: | πŸ’‘ Before posting, please attach your **diagnostics zip** β€” it helps the Goose team debug faster and saves everyone time. - [How to capture and share diagnostics](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/) + [How to capture and share diagnostics](https://goose-docs.ai/docs/troubleshooting/diagnostics-and-reporting/) - type: textarea id: problem attributes: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e2d4d93e0e..6f464634ad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,10 +9,10 @@ assignees: '' **Describe the bug** πŸ’‘ Before filing, please check common issues: -https://block.github.io/goose/docs/troubleshooting +https://goose-docs.ai/docs/troubleshooting πŸ“¦ To help us debug faster, attach your **diagnostics zip** if possible. -πŸ‘‰ How to capture it: https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting/ +πŸ‘‰ How to capture it: https://goose-docs.ai/docs/troubleshooting/diagnostics-and-reporting/ A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4b6341cea9..5c2d2a4572 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://discord.gg/goose-oss about: Please ask and answer questions here. - name: Report a security vulnerability - url: https://github.com/block/goose/security/policy + url: https://github.com/aaif-goose/goose/security/policy about: Please report security vulnerabilities here. diff --git a/.github/ISSUE_TEMPLATE/submit-recipe.yml b/.github/ISSUE_TEMPLATE/submit-recipe.yml index b2af2ae9e5..4c632582d0 100644 --- a/.github/ISSUE_TEMPLATE/submit-recipe.yml +++ b/.github/ISSUE_TEMPLATE/submit-recipe.yml @@ -15,7 +15,7 @@ body: - 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://block.github.io/goose/recipes) + - 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. diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index b7b4a7fd4d..c2215b0887 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -18,7 +18,7 @@ on: jobs: deny: runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' permissions: contents: read steps: diff --git a/.github/workflows/cargo-machete.yml b/.github/workflows/cargo-machete.yml index 17601cc5b5..e5e7e043d8 100644 --- a/.github/workflows/cargo-machete.yml +++ b/.github/workflows/cargo-machete.yml @@ -16,7 +16,7 @@ on: jobs: machete: runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' permissions: contents: read steps: diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 9154827ce9..66e2d71c51 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -77,7 +77,7 @@ jobs: timeout-minutes: 15 container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest options: --user root env: HOME: /tmp/goose-home diff --git a/.github/workflows/docs-update-cli-ref.yml b/.github/workflows/docs-update-cli-ref.yml index 23ca92b92f..37600bfe31 100644 --- a/.github/workflows/docs-update-cli-ref.yml +++ b/.github/workflows/docs-update-cli-ref.yml @@ -49,10 +49,10 @@ jobs: fetch-tags: true # Fetch all tags so we can checkout version tags - name: Fetch upstream tags (for forks) - if: github.repository != 'block/goose' + 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/block/goose.git || git remote set-url upstream https://github.com/block/goose.git + 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)" @@ -80,7 +80,7 @@ jobs: - name: Install goose CLI run: | mkdir -p /home/runner/.local/bin - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ + 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 diff --git a/.github/workflows/docs-update-recipe-ref.yml b/.github/workflows/docs-update-recipe-ref.yml index bd671ceb42..5f5926a8a3 100644 --- a/.github/workflows/docs-update-recipe-ref.yml +++ b/.github/workflows/docs-update-recipe-ref.yml @@ -47,10 +47,10 @@ jobs: fetch-tags: true # Fetch all tags so we can checkout version tags - name: Fetch upstream tags (for forks) - if: github.repository != 'block/goose' + 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/block/goose.git || git remote set-url upstream https://github.com/block/goose.git + 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)" @@ -68,7 +68,7 @@ jobs: - name: Install goose CLI run: | mkdir -p /home/runner/.local/bin - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ + 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 diff --git a/.github/workflows/goose-issue-solver.yml b/.github/workflows/goose-issue-solver.yml index 8c36fdeeaf..d3fc40dc8c 100644 --- a/.github/workflows/goose-issue-solver.yml +++ b/.github/workflows/goose-issue-solver.yml @@ -131,7 +131,7 @@ jobs: timeout-minutes: 30 container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest options: --user root env: GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} diff --git a/.github/workflows/goose-pr-reviewer.yml b/.github/workflows/goose-pr-reviewer.yml index fc5b7aba3e..a6fcb7995f 100644 --- a/.github/workflows/goose-pr-reviewer.yml +++ b/.github/workflows/goose-pr-reviewer.yml @@ -223,7 +223,7 @@ jobs: timeout-minutes: 15 container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest options: --user root env: GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} diff --git a/.github/workflows/goose-release-notes.yml b/.github/workflows/goose-release-notes.yml index 4efc16daf3..4a2d70b001 100644 --- a/.github/workflows/goose-release-notes.yml +++ b/.github/workflows/goose-release-notes.yml @@ -51,7 +51,7 @@ env: - πŸ“š **Documentation** - Documentation updates Format each item as: - - Concise description [#XXXX](https://github.com/block/goose/pull/XXXX) + - Concise description [#XXXX](https://github.com/aaif-goose/goose/pull/XXXX) Rules: - Extract PR numbers from commit messages (look for (#XXXX) pattern) @@ -85,7 +85,7 @@ jobs: github.event.workflow_run.head_branch != 'stable') container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest options: --user root env: GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'anthropic' }} @@ -190,7 +190,7 @@ jobs: DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_RELEASE_BOT_TOKEN }} DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_RELEASE_BOT_CHANNEL_ID }} RELEASE_TAG: ${{ needs.generate-release-notes.outputs.release_tag }} - RELEASE_URL: https://github.com/block/goose/releases/tag/${{ needs.generate-release-notes.outputs.release_tag }} + RELEASE_URL: https://github.com/aaif-goose/goose/releases/tag/${{ needs.generate-release-notes.outputs.release_tag }} RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }} NOTES_LENGTH: ${{ needs.generate-release-notes.outputs.notes_length }} shell: bash diff --git a/.github/workflows/minor-release.yaml b/.github/workflows/minor-release.yaml index 106b41c22e..539dc33543 100644 --- a/.github/workflows/minor-release.yaml +++ b/.github/workflows/minor-release.yaml @@ -11,7 +11,7 @@ on: jobs: release: - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' uses: ./.github/workflows/create-version-bump-pr.yaml secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/pr-website-preview.yml b/.github/workflows/pr-website-preview.yml index 7e81a7aa63..41cc51164b 100644 --- a/.github/workflows/pr-website-preview.yml +++ b/.github/workflows/pr-website-preview.yml @@ -17,6 +17,9 @@ concurrency: jobs: deploy: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - name: Checkout the branch uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -34,14 +37,14 @@ jobs: INKEEP_API_KEY: ${{ secrets.INKEEP_API_KEY }} INKEEP_INTEGRATION_ID: ${{ secrets.INKEEP_INTEGRATION_ID }} INKEEP_ORG_ID: ${{ secrets.INKEEP_ORG_ID }} - TARGET_PATH: "/goose/pr-preview/pr-${{ github.event.number }}/" + TARGET_PATH: "/pr-preview/pr-${{ github.event.number }}/" run: | npm install npm run build - name: Deploy preview uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1.8.1 - if: ${{ github.event.pull_request.head.repo.full_name == 'block/goose' }} + if: ${{ github.event.pull_request.head.repo.full_name == 'aaif-goose/goose' }} with: source-dir: documentation/build diff --git a/.github/workflows/rebuild-skills-marketplace.yml b/.github/workflows/rebuild-skills-marketplace.yml index 1985a4a821..846196c67d 100644 --- a/.github/workflows/rebuild-skills-marketplace.yml +++ b/.github/workflows/rebuild-skills-marketplace.yml @@ -23,7 +23,7 @@ concurrency: jobs: rebuild-docs: runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' permissions: contents: write diff --git a/.github/workflows/recipe-security-scanner.yml b/.github/workflows/recipe-security-scanner.yml index e6f2207c57..f8948ba547 100644 --- a/.github/workflows/recipe-security-scanner.yml +++ b/.github/workflows/recipe-security-scanner.yml @@ -146,7 +146,7 @@ jobs: - name: Scan all recipe files if: steps.find_recipes.outputs.has_recipes == 'true' && steps.recipe_changes.outputs.recipe_files_changed == 'true' env: - SECURITY_SCANNER_ANTHROPIC_API_KEY: ${{ secrets.SECURITY_SCANNER_ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} TRAINING_DATA_LOW: ${{ secrets.TRAINING_DATA_LOW }} TRAINING_DATA_MEDIUM: ${{ secrets.TRAINING_DATA_MEDIUM }} TRAINING_DATA_EXTREME: ${{ secrets.TRAINING_DATA_EXTREME }} @@ -159,7 +159,7 @@ jobs: sudo chmod -R 777 "$OUT" || true # Verify secrets are available (without logging details) - if [ -z "$SECURITY_SCANNER_ANTHROPIC_API_KEY" ] || [ -z "$TRAINING_DATA_LOW" ] || [ -z "$TRAINING_DATA_MEDIUM" ] || [ -z "$TRAINING_DATA_EXTREME" ]; then + if [ -z "$OPENAI_API_KEY" ] || [ -z "$TRAINING_DATA_LOW" ] || [ -z "$TRAINING_DATA_MEDIUM" ] || [ -z "$TRAINING_DATA_EXTREME" ]; then echo "❌ One or more required secrets are missing or inaccessible" exit 1 fi @@ -183,7 +183,7 @@ jobs: # Run scanner on this recipe with training data if docker run --rm \ - -e SECURITY_SCANNER_ANTHROPIC_API_KEY="$SECURITY_SCANNER_ANTHROPIC_API_KEY" \ + -e OPENAI_API_KEY="$OPENAI_API_KEY" \ -e TRAINING_DATA_LOW="$TRAINING_DATA_LOW" \ -e TRAINING_DATA_MEDIUM="$TRAINING_DATA_MEDIUM" \ -e TRAINING_DATA_EXTREME="$TRAINING_DATA_EXTREME" \ diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4d5bd6c7d7..1238972d64 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -22,7 +22,7 @@ jobs: name: Scorecard analysis runs-on: ubuntu-latest # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. - if: github.repository == 'block/goose' && (github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request') + if: github.repository == 'aaif-goose/goose' && (github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request') permissions: # Needed to upload the results to code-scanning dashboard. security-events: write diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 74a2f1d80f..8a689ff1de 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,7 +24,7 @@ jobs: stale: name: 'Mark and Close Stale PRs' runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' steps: # Use the official stale action from GitHub diff --git a/.github/workflows/test-finder.yml b/.github/workflows/test-finder.yml index 356ce504c4..7a4d5d71ef 100644 --- a/.github/workflows/test-finder.yml +++ b/.github/workflows/test-finder.yml @@ -20,7 +20,7 @@ jobs: find-untested-code: runs-on: ubuntu-latest container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest options: --user root env: GOOSE_PROVIDER: ${{ vars.GOOSE_PROVIDER || 'openai' }} diff --git a/.github/workflows/update-hacktoberfest-leaderboard.yml b/.github/workflows/update-hacktoberfest-leaderboard.yml index f984e9f851..3816f25867 100644 --- a/.github/workflows/update-hacktoberfest-leaderboard.yml +++ b/.github/workflows/update-hacktoberfest-leaderboard.yml @@ -9,7 +9,7 @@ on: jobs: update-leaderboard: runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' permissions: contents: read pull-requests: read @@ -29,7 +29,7 @@ jobs: // Track the Goose repo only const REPOS = [ - 'block/goose' + 'aaif-goose/goose' ]; const POINT_VALUES = { small: 5, medium: 10, large: 15 }; diff --git a/.github/workflows/update-health-dashboard.yml b/.github/workflows/update-health-dashboard.yml index 1504fe15e5..62de623749 100644 --- a/.github/workflows/update-health-dashboard.yml +++ b/.github/workflows/update-health-dashboard.yml @@ -23,7 +23,7 @@ jobs: update-dashboard: name: 'Update Health Dashboard' runs-on: ubuntu-latest - if: github.repository == 'block/goose' + if: github.repository == 'aaif-goose/goose' steps: - name: 'Download previous metrics' diff --git a/BUILDING_DOCKER.md b/BUILDING_DOCKER.md index ed61e4b5e0..a86bd5661c 100644 --- a/BUILDING_DOCKER.md +++ b/BUILDING_DOCKER.md @@ -10,17 +10,17 @@ The easiest way to use goose with Docker is to pull the pre-built image from Git ```bash # Pull the latest image -docker pull ghcr.io/block/goose:latest +docker pull ghcr.io/aaif-goose/goose:latest # Run goose CLI -docker run --rm ghcr.io/block/goose:latest --version +docker run --rm ghcr.io/aaif-goose/goose:latest --version # Run with LLM configuration docker run --rm \ -e GOOSE_PROVIDER=openai \ -e GOOSE_MODEL=gpt-4o \ -e OPENAI_API_KEY=$OPENAI_API_KEY \ - ghcr.io/block/goose:latest run -t "Hello, world!" + ghcr.io/aaif-goose/goose:latest run -t "Hello, world!" ``` ## Building from Source @@ -35,7 +35,7 @@ docker run --rm \ 1. Clone the repository: ```bash -git clone https://github.com/block/goose.git +git clone https://github.com/aaif-goose/goose.git cd goose ``` @@ -110,7 +110,7 @@ version: '3.8' services: goose: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest environment: - GOOSE_PROVIDER=${GOOSE_PROVIDER:-openai} - GOOSE_MODEL=${GOOSE_MODEL:-gpt-4o} @@ -163,7 +163,7 @@ docker run --rm \ -c "apt-get update && apt-get install -y vim && goose --version" # Or create a custom Dockerfile -FROM ghcr.io/block/goose:latest +FROM ghcr.io/aaif-goose/goose:latest USER root RUN apt-get update && apt-get install -y \ vim \ @@ -181,7 +181,7 @@ jobs: analyze: runs-on: ubuntu-latest container: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest env: GOOSE_PROVIDER: openai GOOSE_MODEL: gpt-4o @@ -197,7 +197,7 @@ jobs: ```yaml analyze: - image: ghcr.io/block/goose:latest + image: ghcr.io/aaif-goose/goose:latest variables: GOOSE_PROVIDER: openai GOOSE_MODEL: gpt-4o @@ -298,7 +298,7 @@ For production deployments: Example production Dockerfile: ```dockerfile -FROM ghcr.io/block/goose:v1.6.0 +FROM ghcr.io/aaif-goose/goose:v1.6.0 # Add any additional tools needed for your use case USER root RUN apt-get update && apt-get install -y your-tools && rm -rf /var/lib/apt/lists/* @@ -318,5 +318,5 @@ When contributing Docker-related changes: ## Related Documentation - [goose in Docker Tutorial](documentation/docs/tutorials/goose-in-docker.md) - Step-by-step tutorial -- [Installation Guide](https://block.github.io/goose/docs/getting-started/installation) - All installation methods -- [Configuration Guide](https://block.github.io/goose/docs/guides/config-files) - Detailed configuration options +- [Installation Guide](https://goose-docs.ai/docs/getting-started/installation) - All installation methods +- [Configuration Guide](https://goose-docs.ai/docs/guides/config-files) - Detailed configuration options diff --git a/BUILDING_LINUX.md b/BUILDING_LINUX.md index f035ecb6cc..4efa33040c 100644 --- a/BUILDING_LINUX.md +++ b/BUILDING_LINUX.md @@ -30,9 +30,9 @@ sudo zypper install dpkg fakeroot gcc gcc-c++ make **android / termux:** goose is not officially support termux build yet, you need some minor patch to fix build issues. -We will publish goose (block-goose) into termux-packages. +We will publish goose (block-goose) into termux-packages. If you want to try there is a non-official build, https://github.com/shawn111/goose/releases/download/termux/goose-termux-aarch64.tar.bz2 -For more details, see: https://github.com/block/goose/pull/3890 +For more details, see: https://github.com/aaif-goose/goose/pull/3890 ```bash pkg install rust @@ -49,7 +49,7 @@ pkg install cmake protobuf clang build-essential ### 1. Clone and Setup ```bash -git clone https://github.com/block/goose.git +git clone https://github.com/aaif-goose/goose.git cd goose ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50da025478..5ffd43ca2f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -213,7 +213,7 @@ and stepping through the server code while interacting with the UI. To fork the repository: -1. Go to https://github.com/block/goose and click β€œFork” (top-right corner). +1. Go to https://github.com/aaif-goose/goose and click β€œFork” (top-right corner). 2. This creates https://github.com//goose under your GitHub account. 3. Clone your fork (not the main repo): @@ -225,7 +225,7 @@ cd goose 4. Add the main repository as upstream: ``` -git remote add upstream https://github.com/block/goose.git +git remote add upstream https://github.com/aaif-goose/goose.git ``` 5. Create a branch in your fork for your changes: @@ -250,7 +250,7 @@ git merge upstream/main git push origin my-feature-branch ``` -8. Open a Pull Request from your branch on your fork to block/goose’s main branch. +8. Open a Pull Request from your branch on your fork to aaif-goose/goose’s main branch. ## Keeping Your Fork Up-to-Date @@ -262,7 +262,7 @@ repository. This helps avoid conflicts and allows us to merge your pull requests 1. **Add the Main Repository as a Remote** (Skip if you have already set this up): ```bash - git remote add upstream https://github.com/block/goose.git + git remote add upstream https://github.com/aaif-goose/goose.git ``` 2. **Fetch the Latest Changes from the Main Repository**: @@ -346,7 +346,7 @@ Then you can view your traces at http://localhost:3000 This project follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for PR titles. Conventional Commits make it easier to understand the history of a project and facilitate automation around versioning and changelog generation. -[issues]: https://github.com/block/goose/issues +[issues]: https://github.com/aaif-goose/goose/issues [hermit]: https://cashapp.github.io/hermit/ [just]: https://github.com/casey/just?tab=readme-ov-file#installation @@ -366,7 +366,7 @@ There are numerous ways to be an open source contributor and contribute to goose - **Stars on GitHub:** If you resonate with our project and find it valuable, consider starring our goose on GitHub! 🌟 - **Ask Questions:** Your questions not only help us improve but also benefit the community. If you have a question, don't hesitate to ask it on [Discord](https://discord.gg/goose-oss). -- **Give Feedback:** Have a feature you want to see or encounter an issue with goose, [click here to open an issue](https://github.com/block/goose/issues/new/choose), [start a discussion](https://github.com/block/goose/discussions) or tell us on Discord. +- **Give Feedback:** Have a feature you want to see or encounter an issue with goose, [click here to open an issue](https://github.com/aaif-goose/goose/issues/new/choose), [start a discussion](https://github.com/aaif-goose/goose/discussions) or tell us on Discord. - **Participate in Community Events:** We host a variety of community events and livestreams on Discord every month, ranging from workshops to brainstorming sessions. You can subscribe to our [events calendar](https://calget.com/c/t7jszrie) or follow us on [social media](https://linktr.ee/goose_oss) to stay in touch. - **Improve Documentation:** Good documentation is key to the success of any project. You can help improve the quality of our existing docs or add new pages. - **Help Other Members:** See another community member stuck? Or a contributor blocked by a question you know the answer to? Reply to community threads or do a code review for others to help. diff --git a/CONTRIBUTING_RECIPES.md b/CONTRIBUTING_RECIPES.md index 89a1026ac1..55bf973b15 100644 --- a/CONTRIBUTING_RECIPES.md +++ b/CONTRIBUTING_RECIPES.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing to the goose Recipe Cookbook! This g ## πŸš€ Quick Start -1. [Fork this repository](https://github.com/block/goose/fork) +1. [Fork this repository](https://github.com/aaif-goose/goose/fork) 2. Add your recipe file here: `documentation/src/pages/recipes/data/recipes/` 3. Create a pull request @@ -62,7 +62,7 @@ parameters: default: "default_value" ``` -πŸ“š **Need help with the format?** Check out the [Recipe Reference Guide](https://block.github.io/goose/docs/guides/recipes/recipe-reference) or [existing recipes](documentation/src/pages/recipes/data/recipes/) for examples. +πŸ“š **Need help with the format?** Check out the [Recipe Reference Guide](https://goose-docs.ai/docs/guides/recipes/recipe-reference) or [existing recipes](documentation/src/pages/recipes/data/recipes/) for examples. ### Step 4: Create a Pull Request @@ -84,7 +84,7 @@ Your recipe should: - [ ] **Work correctly** - Test it before submitting - [ ] **Be useful** - Solve a real problem or demonstrate a valuable workflow -- [ ] **Follow the format** - Refer to the [Recipe Reference Guide](https://block.github.io/goose/docs/guides/recipes/recipe-reference) +- [ ] **Follow the format** - Refer to the [Recipe Reference Guide](https://goose-docs.ai/docs/guides/recipes/recipe-reference) - [ ] **Have a unique filename** - No conflicts with existing recipe files ### πŸ“ **Naming Guidelines:** @@ -119,7 +119,7 @@ Need inspiration? Consider recipes for: - πŸ“– **Browse existing recipes** for examples - πŸ’¬ **Ask questions** in your PR - πŸ› **Report issues** if something isn't working -- πŸ“š **Check the docs** at [block.github.io/goose](https://block.github.io/goose/docs/guides/recipes/) +- πŸ“š **Check the docs** at [goose-docs.ai](https://goose-docs.ai/docs/guides/recipes/) ## 🀝 Community Guidelines @@ -128,6 +128,6 @@ Need inspiration? Consider recipes for: --- -**Ready to contribute?** [Fork the repo](https://github.com/block/goose/fork) and start creating! +**Ready to contribute?** [Fork the repo](https://github.com/aaif-goose/goose/fork) and start creating! *Questions? Ask in your PR or hop into [discord](https://discord.gg/goose-oss) - we're here to help!* πŸ’™ diff --git a/Cargo.lock b/Cargo.lock index 1f12205c4c..1346958d90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4326,7 +4326,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.29.0" +version = "1.30.0" dependencies = [ "agent-client-protocol-schema", "ahash", @@ -4369,6 +4369,7 @@ dependencies = [ "jsonwebtoken", "keyring", "lazy_static", + "libc", "llama-cpp-2", "lru", "minijinja", @@ -4446,7 +4447,7 @@ dependencies = [ [[package]] name = "goose-acp" -version = "1.29.0" +version = "1.30.0" dependencies = [ "agent-client-protocol-schema", "anyhow", @@ -4458,6 +4459,7 @@ dependencies = [ "goose", "goose-acp-macros", "goose-mcp", + "goose-sdk", "goose-test-support", "http-body-util", "regex", @@ -4481,7 +4483,7 @@ dependencies = [ [[package]] name = "goose-acp-macros" -version = "1.29.0" +version = "1.30.0" dependencies = [ "quote", "syn 2.0.117", @@ -4489,7 +4491,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.29.0" +version = "1.30.0" dependencies = [ "anstream 0.6.21", "anyhow", @@ -4542,7 +4544,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.29.0" +version = "1.30.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -4569,9 +4571,22 @@ dependencies = [ "url", ] +[[package]] +name = "goose-sdk" +version = "1.30.0" +dependencies = [ + "agent-client-protocol-schema", + "sacp", + "schemars 1.2.1", + "serde", + "serde_json", + "tokio", + "tokio-util", +] + [[package]] name = "goose-server" -version = "1.29.0" +version = "1.30.0" dependencies = [ "anyhow", "aws-lc-rs", @@ -4618,7 +4633,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.29.0" +version = "1.30.0" dependencies = [ "clap", "serde_json", @@ -4626,7 +4641,7 @@ dependencies = [ [[package]] name = "goose-test-support" -version = "1.29.0" +version = "1.30.0" dependencies = [ "axum", "env-lock", @@ -5746,9 +5761,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libdbus-sys" diff --git a/Cargo.toml b/Cargo.toml index 697cd51495..4cd6bb8a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "crates/*", + "crates/*", # Mainly for cargo-machete to not error out during inspection. "vendor/v8" ] @@ -8,10 +8,10 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.29.0" -authors = ["Block "] +version = "1.30.0" +authors = ["AAIF "] license = "Apache-2.0" -repository = "https://github.com/block/goose" +repository = "https://github.com/aaif-goose/goose" description = "An AI agent" [workspace.lints.clippy] @@ -105,4 +105,4 @@ opentelemetry = { git = "https://github.com/open-telemetry/opentelemetry-rust", opentelemetry_sdk = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } opentelemetry-appender-tracing = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } opentelemetry-otlp = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } -opentelemetry-stdout = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } \ No newline at end of file +opentelemetry-stdout = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } diff --git a/Dockerfile b/Dockerfile index c0f78860cc..05429deeea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,5 +72,5 @@ CMD ["--help"] # Labels for metadata LABEL org.opencontainers.image.title="goose" LABEL org.opencontainers.image.description="goose CLI" -LABEL org.opencontainers.image.vendor="Block" -LABEL org.opencontainers.image.source="https://github.com/block/goose" +LABEL org.opencontainers.image.vendor="AAIF" +LABEL org.opencontainers.image.source="https://github.com/aaif-goose/goose" diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ee6a912c49..ff574e9f4e 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -175,7 +175,7 @@ This governance model may evolve as goose grows. Any proposed modification to th ## Current Membership -Core Maintainers and Maintainers are listed in the main goose repository's [MAINTAINERS.md](https://github.com/block/goose/blob/main/MAINTAINERS.md) file with their areas of expertise where applicable. +Core Maintainers and Maintainers are listed in the main goose repository's [MAINTAINERS.md](https://github.com/aaif-goose/goose/blob/main/MAINTAINERS.md) file with their areas of expertise where applicable. ## Summary @@ -190,7 +190,7 @@ We believe this balance enables goose to remain innovative while building a stro # General Project Policies -Founded by Block, goose has been established as a Series of LF Projects, LLC. Policies applicable to goose and participants in the goose project, including guidelines on the usage of trademarks, are located at [https://lfprojects.org/policies/](https://lfprojects.org/policies/). Governance changes approved as per the provisions of this governance document must also be approved by LF Projects, LLC. +Founded by Block and now stewarded by AAIF (Agentic AI Foundation), goose has been established as a Series of LF Projects, LLC. Policies applicable to goose and participants in the goose project, including guidelines on the usage of trademarks, are located at [https://lfprojects.org/policies/](https://lfprojects.org/policies/). Governance changes approved as per the provisions of this governance document must also be approved by LF Projects, LLC. goose participants acknowledge that the copyright in all new contributions will be retained by the copyright holder as independent works of authorship and that no contributor or copyright holder will be required to assign copyrights to the project. Except as described below, all code and specification contributions to the project must be made using the Apache License, Version 2.0 available at (the β€œProject License”). diff --git a/README.md b/README.md index cd2dc4e5c4..fed529b2da 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,50 @@ +> **πŸ¦† 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 -_a local, extensible, open source AI agent that automates engineering tasks_ +_your native open source AI agent β€” desktop app, CLI, and API β€” for code, workflows, and everything in between_

Discord - CI + CI

-goose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_. +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. -Whether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision. +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. -Designed for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation. +goose works with 15+ providers β€” Anthropic, OpenAI, Google, Ollama, OpenRouter, Azure, Bedrock, and more. Use API keys or your existing Claude, ChatGPT, or Gemini subscriptions via [ACP](https://goose-docs.ai/docs/guides/acp-providers). Connect to 70+ extensions via the [Model Context Protocol](https://modelcontextprotocol.io/) open standard. -[![Watch the video](https://github.com/user-attachments/assets/ddc71240-3928-41b5-8210-626dfb28af7a)](https://youtu.be/D-DpDunrbpo) +goose is part of the [Agentic AI Foundation (AAIF)](https://aaif.io/) at the Linux Foundation. -# Quick Links -- [Quickstart](https://block.github.io/goose/docs/quickstart) -- [Installation](https://block.github.io/goose/docs/getting-started/installation) -- [Tutorials](https://block.github.io/goose/docs/category/tutorials) -- [Documentation](https://block.github.io/goose/docs/category/getting-started) -- [Governance](https://github.com/block/goose/blob/main/GOVERNANCE.md) -- [Custom Distributions](https://github.com/block/goose/blob/main/CUSTOM_DISTROS.md) - build your own goose distro with preconfigured providers, extensions, and branding +# Get started -## Need Help? -- [Diagnostics & Reporting](https://block.github.io/goose/docs/troubleshooting/diagnostics-and-reporting) -- [Known Issues](https://block.github.io/goose/docs/troubleshooting/known-issues) +**[Download the desktop app](https://goose-docs.ai/docs/getting-started/installation)** for macOS, Linux, and Windows. + +Or install the CLI: + +```bash +curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash +``` + +# Quick links +- [Quickstart](https://goose-docs.ai/docs/quickstart) +- [Installation](https://goose-docs.ai/docs/getting-started/installation) +- [Tutorials](https://goose-docs.ai/docs/category/tutorials) +- [Documentation](https://goose-docs.ai/docs/category/getting-started) +- [Governance](https://github.com/aaif-goose/goose/blob/main/GOVERNANCE.md) +- [Custom Distributions](https://github.com/aaif-goose/goose/blob/main/CUSTOM_DISTROS.md) β€” build your own goose distro with preconfigured providers, extensions, and branding + +## Need help? +- [Diagnostics & Reporting](https://goose-docs.ai/docs/troubleshooting/diagnostics-and-reporting) +- [Known Issues](https://goose-docs.ai/docs/troubleshooting/known-issues) # a little goose humor πŸͺΏ @@ -40,10 +52,8 @@ Designed for maximum flexibility, goose works with any LLM and supports multi-mo > > Because it always helps them "migrate" their code to production! πŸš€ -# goose around with us +# goose around with us - [Discord](https://discord.gg/goose-oss) - [YouTube](https://www.youtube.com/@goose-oss) - [LinkedIn](https://www.linkedin.com/company/goose-oss) - [Twitter/X](https://x.com/goose_oss) -- [Bluesky](https://bsky.app/profile/opensource.block.xyz) -- [Nostr](https://njump.me/opensource@block.xyz) diff --git a/RELEASE.md b/RELEASE.md index 2f78f5ae33..a5b54fd682 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,7 @@ goose uses GitHub actions to automate the release process. The actual releases a These are typically done once per week. The process has two automated phases: -1. **Version bump PR** β€” An [action](https://github.com/block/goose/actions/workflows/minor-release.yaml) runs every Tuesday (or can be triggered manually) that creates a PR to bump the version on `main`. Review and merge this PR. +1. **Version bump PR** β€” An [action](https://github.com/aaif-goose/goose/actions/workflows/minor-release.yaml) runs every Tuesday (or can be triggered manually) that creates a PR to bump the version on `main`. Review and merge this PR. 2. **Release branch + PR** β€” When the version bump PR merges, automation creates a `release/` branch from `main` and opens a release PR with a QA checklist. @@ -18,13 +18,13 @@ From there: - Download and test the .zip from the release PR - When ready, follow the instructions on the release PR to tag and release -To trigger the release, find [the corresponding PR](https://github.com/block/goose/pulls?q=is%3Apr+%22chore%28release%29%22+author%3Aapp%2Fgithub-actions+) and follow the instructions in the PR description. +To trigger the release, find [the corresponding PR](https://github.com/aaif-goose/goose/pulls?q=is%3Apr+%22chore%28release%29%22+author%3Aapp%2Fgithub-actions+) and follow the instructions in the PR description. ## Patch version releases When a minor release is tagged, automation immediately creates the next patch release branch (e.g. `release/1.25.1` from `release/1.25.0`) with the version already bumped and a release PR open. Cherry-pick fixes into this branch, then tag when ready. -To trigger the release, find [the corresponding PR](https://github.com/block/goose/pulls?q=is%3Apr+%22chore%28release%29%22+%22%28patch%29%22+author%3Aapp%2Fgithub-actions+) and follow the instructions in the PR description. +To trigger the release, find [the corresponding PR](https://github.com/aaif-goose/goose/pulls?q=is%3Apr+%22chore%28release%29%22+%22%28patch%29%22+author%3Aapp%2Fgithub-actions+) and follow the instructions in the PR description. ## High level release flow: diff --git a/SECURITY.md b/SECURITY.md index 96cd246e62..42eca542be 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,6 +10,6 @@ > > In some circumstances, goose may follow commands found embedded in content even if those commands conflict with the task given to goose. We suggest taking the precautions above to limit risks from prompt injection. By taking these steps, you can reduce the potential security risks associated with developer agents and better protect your systems and users. > -> Block recognizes the important contributions our open source community makes. Part of keeping Block and its customers safe is by making sure that we find and fix any security issues found in our open source projects. If you find a security vulnerability, we encourage you to privately report it in the repository’s Security tab -> Report a vulnerability. +> AAIF (Agentic AI Foundation) recognizes the important contributions our open source community makes. Part of keeping goose and its users safe is by making sure that we find and fix any security issues found in our open source projects. If you find a security vulnerability, we encourage you to privately report it in the repository’s Security tab -> Report a vulnerability. > -> Please see [privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) for more information. For assistance or escalation, please contact the [Block Open Source Governance Committee](mailto:open-source-governance@block.xyz) +> Please see [privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) for more information. For assistance or escalation, please contact the [AAIF Open Source Governance Committee](mailto:open-source-governance@block.xyz) diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs index a15213f653..532e1aa89c 100644 --- a/crates/goose-acp-macros/src/lib.rs +++ b/crates/goose-acp-macros/src/lib.rs @@ -1,16 +1,20 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ - parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Lit, Pat, PathArguments, - ReturnType, Type, + parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Pat, PathArguments, ReturnType, + Type, }; -/// Marks an impl block as containing `#[custom_method("...")]`-annotated handlers. +/// Marks an impl block as containing `#[custom_method(RequestType)]`-annotated handlers. +/// +/// The request type must derive `sacp::JsonRpcRequest` with a `#[request(method = "...")]` +/// attribute β€” the method name is extracted from that type at compile time, eliminating +/// duplication between the request struct and the handler. /// /// Generates two methods on the impl: /// /// 1. `handle_custom_request` β€” a dispatcher that: -/// - Uses each annotation string as the method name (include `_goose/` for goose-only methods) +/// - Uses `::matches_method` to match incoming methods /// - Parses JSON params into the handler's typed parameter (if any) /// - Serializes the handler's return value to JSON /// @@ -25,11 +29,11 @@ use syn::{ /// /// ```ignore /// // No params β€” called for requests with no/empty params -/// #[custom_method("session/list")] -/// async fn on_list_sessions(&self) -> Result { .. } +/// #[custom_method(GetExtensionsRequest)] +/// async fn on_get_extensions(&self) -> Result { .. } /// /// // Typed params β€” JSON params auto-deserialized -/// #[custom_method("session/get")] +/// #[custom_method(GetSessionRequest)] /// async fn on_get_session(&self, req: GetSessionRequest) -> Result { .. } /// ``` /// @@ -40,15 +44,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut routes: Vec = Vec::new(); - // Collect all #[custom_method("...")] annotations and strip them. + // Collect all #[custom_method(RequestType)] annotations and strip them. for item in &mut impl_block.items { if let ImplItem::Fn(method) = item { - let mut route_name = None; + let mut request_type = None; method.attrs.retain(|attr| { if attr.path().is_ident("custom_method") { if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Lit::Str(s)) = meta_list.parse_args::() { - route_name = Some(s.value()); + if let Ok(ty) = meta_list.parse_args::() { + request_type = Some(ty); } } false // strip the attribute @@ -57,7 +61,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } }); - if let Some(name) = route_name { + if let Some(req_type) = request_type { let fn_ident = method.sig.ident.clone(); let param_type = extract_param_type(&method.sig); @@ -65,7 +69,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let ok_type = extract_result_ok_type(&method.sig); routes.push(Route { - method_name: name, + request_type: req_type, fn_ident, param_type, return_type, @@ -75,31 +79,31 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } } - // Generate the dispatch arms. + // Generate the dispatch arms using matches_method for routing. let arms: Vec<_> = routes .iter() .map(|route| { - let method = &route.method_name; + let req_type = &route.request_type; let fn_ident = &route.fn_ident; match &route.param_type { Some(_) => { quote! { - #method => { + if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { let req = serde_json::from_value(params) .map_err(|e| sacp::Error::invalid_params().data(e.to_string()))?; let result = self.#fn_ident(req).await?; - serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + return serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())); } } } None => { quote! { - #method => { + if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { let result = self.#fn_ident().await?; - serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + return serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())); } } } @@ -111,7 +115,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let schema_entries: Vec<_> = routes .iter() .map(|route| { - let method = &route.method_name; + let req_type = &route.request_type; let params_expr = if let Some(pt) = &route.param_type { if is_json_value(pt) { @@ -120,7 +124,12 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { Some(generator.subschema_for::<#pt>()) } } } else { - quote! { None } + // Even with no handler param, generate schema from the request type + if is_json_value(req_type) { + quote! { None } + } else { + quote! { Some(generator.subschema_for::<#req_type>()) } + } }; let response_expr = if let Some(ok_ty) = &route.ok_type { @@ -141,7 +150,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { Some(#name.to_string()) } } } else { - quote! { None } + let name = type_name(req_type); + quote! { Some(#name.to_string()) } }; let response_name_expr = if let Some(ok_ty) = &route.ok_type { @@ -156,12 +166,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { }; quote! { - crate::custom_requests::CustomMethodSchema { - method: #method.to_string(), - params_schema: #params_expr, - params_type_name: #params_name_expr, - response_schema: #response_expr, - response_type_name: #response_name_expr, + { + let dummy = <#req_type as Default>::default(); + crate::custom_requests::CustomMethodSchema { + method: sacp::JsonRpcMessage::method(&dummy).to_string(), + params_schema: #params_expr, + params_type_name: #params_name_expr, + response_schema: #response_expr, + response_type_name: #response_name_expr, + } } } }) @@ -174,10 +187,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { method: &str, params: serde_json::Value, ) -> Result { - match method { - #(#arms)* - _ => Err(sacp::Error::method_not_found()), - } + #(#arms)* + Err(sacp::Error::method_not_found()) } }; @@ -202,7 +213,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } struct Route { - method_name: String, + request_type: Type, fn_ident: syn::Ident, param_type: Option, #[allow(dead_code)] diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index a271d74192..8bc2b1e7ee 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -47,6 +47,7 @@ http-body-util = "0.1.3" uuid = { workspace = true, features = ["v7"] } schemars = { workspace = true, features = ["derive"] } goose-acp-macros = { path = "../goose-acp-macros" } +goose-sdk = { path = "../goose-sdk" } [dev-dependencies] async-trait = { workspace = true } diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json index 5050c86e62..f5df690d39 100644 --- a/crates/goose-acp/acp-meta.json +++ b/crates/goose-acp/acp-meta.json @@ -47,8 +47,48 @@ }, { "method": "_goose/config/extensions", - "requestType": null, + "requestType": "GetExtensionsRequest", "responseType": "GetExtensionsResponse" + }, + { + "method": "_goose/session/provider/update", + "requestType": "UpdateProviderRequest", + "responseType": "UpdateProviderResponse" + }, + { + "method": "_goose/providers/list", + "requestType": "ListProvidersRequest", + "responseType": "ListProvidersResponse" + }, + { + "method": "_goose/config/read", + "requestType": "ReadConfigRequest", + "responseType": "ReadConfigResponse" + }, + { + "method": "_goose/config/upsert", + "requestType": "UpsertConfigRequest", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/config/remove", + "requestType": "RemoveConfigRequest", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/secret/check", + "requestType": "CheckSecretRequest", + "responseType": "CheckSecretResponse" + }, + { + "method": "_goose/secret/upsert", + "requestType": "UpsertSecretRequest", + "responseType": "EmptyResponse" + }, + { + "method": "_goose/secret/remove", + "requestType": "RemoveSecretRequest", + "responseType": "EmptyResponse" } ] } diff --git a/crates/goose-acp/acp-schema.json b/crates/goose-acp/acp-schema.json index f6db67e3fa..b0e2d6ba27 100644 --- a/crates/goose-acp/acp-schema.json +++ b/crates/goose-acp/acp-schema.json @@ -9,12 +9,12 @@ "type": "string" }, "config": { - "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform)." + "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).", + "default": null } }, "required": [ - "sessionId", - "config" + "sessionId" ], "description": "Add an extension to an active session.", "x-side": "agent", @@ -62,13 +62,14 @@ "properties": { "tools": { "type": "array", - "items": true, + "items": {}, "description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`." } }, "required": [ "tools" ], + "description": "Tools response.", "x-side": "agent", "x-method": "_goose/tools" }, @@ -98,12 +99,11 @@ "type": "object", "properties": { "result": { - "description": "The resource result from the extension (MCP ReadResourceResult)." + "description": "The resource result from the extension (MCP ReadResourceResult).", + "default": null } }, - "required": [ - "result" - ], + "description": "Resource read response.", "x-side": "agent", "x-method": "_goose/resource/read" }, @@ -147,12 +147,10 @@ "type": "object", "properties": { "session": { - "description": "The session object with id, name, working_dir, timestamps, tokens, etc." + "description": "The session object with id, name, working_dir, timestamps, tokens, etc.", + "default": null } }, - "required": [ - "session" - ], "description": "Get a session response.", "x-side": "agent", "x-method": "session/get" @@ -195,6 +193,7 @@ "required": [ "data" ], + "description": "Export session response.", "x-side": "agent", "x-method": "_goose/session/export" }, @@ -216,21 +215,26 @@ "type": "object", "properties": { "session": { - "description": "The imported session object." + "description": "The imported session object.", + "default": null } }, - "required": [ - "session" - ], + "description": "Import session response.", "x-side": "agent", "x-method": "_goose/session/import" }, + "GetExtensionsRequest": { + "type": "object", + "description": "List configured extensions and any warnings.", + "x-side": "agent", + "x-method": "_goose/config/extensions" + }, "GetExtensionsResponse": { "type": "object", "properties": { "extensions": { "type": "array", - "items": true, + "items": {}, "description": "Array of ExtensionEntry objects with `enabled` flag and config details." }, "warnings": { @@ -248,6 +252,212 @@ "x-side": "agent", "x-method": "_goose/config/extensions" }, + "UpdateProviderRequest": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "contextLimit": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + }, + "requestParams": { + "type": [ + "object", + "null" + ], + "additionalProperties": {} + } + }, + "required": [ + "sessionId", + "provider" + ], + "description": "Atomically update the provider for a live session.", + "x-side": "agent", + "x-method": "_goose/session/provider/update" + }, + "UpdateProviderResponse": { + "type": "object", + "properties": { + "configOptions": { + "type": "array", + "items": {}, + "description": "Refreshed session config options after the provider/model change." + } + }, + "required": [ + "configOptions" + ], + "description": "Provider update response.", + "x-side": "agent", + "x-method": "_goose/session/provider/update" + }, + "ListProvidersRequest": { + "type": "object", + "description": "List providers available through goose, including the config-default sentinel.", + "x-side": "agent", + "x-method": "_goose/providers/list" + }, + "ListProvidersResponse": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/$defs/ProviderListEntry" + } + } + }, + "required": [ + "providers" + ], + "description": "Provider list response.", + "x-side": "agent", + "x-method": "_goose/providers/list" + }, + "ProviderListEntry": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "id", + "label" + ] + }, + "ReadConfigRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "required": [ + "key" + ], + "description": "Read a single non-secret config value.", + "x-side": "agent", + "x-method": "_goose/config/read" + }, + "ReadConfigResponse": { + "type": "object", + "properties": { + "value": { + "default": null + } + }, + "description": "Config read response.", + "x-side": "agent", + "x-method": "_goose/config/read" + }, + "UpsertConfigRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "description": "Upsert a single non-secret config value.", + "x-side": "agent", + "x-method": "_goose/config/upsert" + }, + "RemoveConfigRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "required": [ + "key" + ], + "description": "Remove a single non-secret config value.", + "x-side": "agent", + "x-method": "_goose/config/remove" + }, + "CheckSecretRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "required": [ + "key" + ], + "description": "Check whether a secret exists. Never returns the actual value.", + "x-side": "agent", + "x-method": "_goose/secret/check" + }, + "CheckSecretResponse": { + "type": "object", + "properties": { + "exists": { + "type": "boolean" + } + }, + "required": [ + "exists" + ], + "description": "Secret check response.", + "x-side": "agent", + "x-method": "_goose/secret/check" + }, + "UpsertSecretRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": {} + }, + "required": [ + "key", + "value" + ], + "description": "Set a secret value (write-only).", + "x-side": "agent", + "x-method": "_goose/secret/upsert" + }, + "RemoveSecretRequest": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "required": [ + "key" + ], + "description": "Remove a secret.", + "x-side": "agent", + "x-method": "_goose/secret/remove" + }, "ExtRequest": { "properties": { "id": { @@ -340,6 +550,87 @@ ], "description": "Params for _goose/session/import", "title": "ImportSessionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetExtensionsRequest" + } + ], + "description": "Params for _goose/config/extensions", + "title": "GetExtensionsRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/UpdateProviderRequest" + } + ], + "description": "Params for _goose/session/provider/update", + "title": "UpdateProviderRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListProvidersRequest" + } + ], + "description": "Params for _goose/providers/list", + "title": "ListProvidersRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ReadConfigRequest" + } + ], + "description": "Params for _goose/config/read", + "title": "ReadConfigRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/UpsertConfigRequest" + } + ], + "description": "Params for _goose/config/upsert", + "title": "UpsertConfigRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/RemoveConfigRequest" + } + ], + "description": "Params for _goose/config/remove", + "title": "RemoveConfigRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/CheckSecretRequest" + } + ], + "description": "Params for _goose/secret/check", + "title": "CheckSecretRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/UpsertSecretRequest" + } + ], + "description": "Params for _goose/secret/upsert", + "title": "UpsertSecretRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/RemoveSecretRequest" + } + ], + "description": "Params for _goose/secret/remove", + "title": "RemoveSecretRequest" } ] }, @@ -426,6 +717,38 @@ } ], "title": "GetExtensionsResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/UpdateProviderResponse" + } + ], + "title": "UpdateProviderResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ListProvidersResponse" + } + ], + "title": "ListProvidersResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ReadConfigResponse" + } + ], + "title": "ReadConfigResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/CheckSecretResponse" + } + ], + "title": "CheckSecretResponse" } ] }, diff --git a/crates/goose-acp/src/bin/generate_acp_schema.rs b/crates/goose-acp/src/bin/generate_acp_schema.rs index f4e8ac9eb4..0e9bf87989 100644 --- a/crates/goose-acp/src/bin/generate_acp_schema.rs +++ b/crates/goose-acp/src/bin/generate_acp_schema.rs @@ -35,6 +35,13 @@ fn main() { } } + // Replace `true` with `{}` throughout $defs. Both mean "accept any value" in + // JSON Schema, but many TS codegen tools (e.g. @hey-api/openapi-ts Zod plugin) + // silently drop properties whose schema is the bare `true` literal. + for def in defs.values_mut() { + replace_true_schemas(def); + } + // Annotate $defs entries with x-method/x-side. Only set x-method for types // used by exactly one method (shared types like EmptyResponse skip x-method). for (name, methods_list) in &type_methods { @@ -181,3 +188,31 @@ fn main() { println!("{json_str}"); } + +/// Recursively replace `true` with `{}` in a JSON value. +/// +/// In JSON Schema, `true` and `{}` both mean "accept any value", but many +/// TypeScript codegen tools only handle the object form. +fn replace_true_schemas(value: &mut Value) { + match value { + Value::Object(map) => { + for v in map.values_mut() { + if *v == Value::Bool(true) { + *v = json!({}); + } else { + replace_true_schemas(v); + } + } + } + Value::Array(arr) => { + for v in arr.iter_mut() { + if *v == Value::Bool(true) { + *v = json!({}); + } else { + replace_true_schemas(v); + } + } + } + _ => {} + } +} diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-acp/src/custom_requests.rs deleted file mode 100644 index 2782e25d78..0000000000 --- a/crates/goose-acp/src/custom_requests.rs +++ /dev/null @@ -1,132 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// Schema descriptor for a single custom method, produced by the -/// `#[custom_methods]` macro's generated `custom_method_schemas()` function. -/// -/// `params_schema` / `response_schema` hold `$ref` pointers or inline schemas -/// produced by `SchemaGenerator::subschema_for`. All referenced types are -/// collected in the generator's `$defs` map. -/// -/// `params_type_name` / `response_type_name` carry the Rust struct name so the -/// binary can key `$defs` entries and annotate them with `x-method` / `x-side`. -#[derive(Debug, Serialize)] -pub struct CustomMethodSchema { - pub method: String, - pub params_schema: Option, - pub params_type_name: Option, - pub response_schema: Option, - pub response_type_name: Option, -} - -/// Add an extension to an active session. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct AddExtensionRequest { - pub session_id: String, - /// Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). - pub config: serde_json::Value, -} - -/// Remove an extension from an active session. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct RemoveExtensionRequest { - pub session_id: String, - pub name: String, -} - -/// List all tools available in a session. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct GetToolsRequest { - pub session_id: String, -} - -#[derive(Debug, Serialize, JsonSchema)] -pub struct GetToolsResponse { - /// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. - pub tools: Vec, -} - -/// Read a resource from an extension. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct ReadResourceRequest { - pub session_id: String, - pub uri: String, - pub extension_name: String, -} - -#[derive(Debug, Serialize, JsonSchema)] -pub struct ReadResourceResponse { - /// The resource result from the extension (MCP ReadResourceResult). - pub result: serde_json::Value, -} - -/// Update the working directory for a session. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateWorkingDirRequest { - pub session_id: String, - pub working_dir: String, -} - -/// Get a session by ID. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct GetSessionRequest { - pub session_id: String, - #[serde(default)] - pub include_messages: bool, -} - -/// Get a session response. -#[derive(Debug, Serialize, JsonSchema)] -pub struct GetSessionResponse { - /// The session object with id, name, working_dir, timestamps, tokens, etc. - pub session: serde_json::Value, -} - -/// Delete a session. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct DeleteSessionRequest { - pub session_id: String, -} - -/// Export a session as a JSON string. -#[derive(Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct ExportSessionRequest { - pub session_id: String, -} - -#[derive(Debug, Serialize, JsonSchema)] -pub struct ExportSessionResponse { - pub data: String, -} - -/// Import a session from a JSON string. -#[derive(Debug, Deserialize, JsonSchema)] -pub struct ImportSessionRequest { - pub data: String, -} - -#[derive(Debug, Serialize, JsonSchema)] -pub struct ImportSessionResponse { - /// The imported session object. - pub session: serde_json::Value, -} - -/// List configured extensions and any warnings. -#[derive(Debug, Serialize, JsonSchema)] -pub struct GetExtensionsResponse { - /// Array of ExtensionEntry objects with `enabled` flag and config details. - pub extensions: Vec, - pub warnings: Vec, -} - -/// Empty success response for operations that return no data. -#[derive(Debug, Serialize, JsonSchema)] -pub struct EmptyResponse {} diff --git a/crates/goose-acp/src/lib.rs b/crates/goose-acp/src/lib.rs index 97c405e2d7..d1bddef8c5 100644 --- a/crates/goose-acp/src/lib.rs +++ b/crates/goose-acp/src/lib.rs @@ -1,7 +1,7 @@ #![recursion_limit = "256"] mod adapters; -pub mod custom_requests; +pub use goose_sdk::custom_requests; mod fs; pub mod server; pub mod server_factory; diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 281e678fc6..4a12d2043a 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -3,7 +3,8 @@ use crate::fs::AcpTools; use crate::tools::AcpAwareToolMeta; use anyhow::Result; use fs_err as fs; -use goose::acp::PermissionDecision; +use futures::future::BoxFuture; +use goose::acp::{PermissionDecision, ACP_CURRENT_MODEL}; use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS}; use goose::agents::mcp_client::McpClientTrait; use goose::agents::platform_extensions::developer::DeveloperClient; @@ -20,9 +21,8 @@ use goose::mcp_utils::ToolResult; use goose::permission::permission_confirmation::PrincipalType; use goose::permission::{Permission, PermissionConfirmation}; use goose::providers::base::Provider; -use goose::providers::provider_registry::ProviderConstructor; use goose::session::session_manager::SessionType; -use goose::session::{Session, SessionManager}; +use goose::session::{EnabledExtensionsState, Session, SessionManager}; use goose_acp_macros::custom_methods; use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role}; use sacp::schema::{ @@ -31,7 +31,7 @@ use sacp::schema::{ ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource, EmbeddedResourceResource, FileSystemCapabilities, ImageContent, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, - LoadSessionResponse, McpCapabilities, McpServer, ModelId, ModelInfo, NewSessionRequest, + LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest, NewSessionResponse, PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse, RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionCapabilities, SessionCloseCapabilities, SessionConfigOption, @@ -57,6 +57,19 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; use url::Url; +pub type AcpProviderFactory = Arc< + dyn Fn( + String, + goose::model::ModelConfig, + Vec, + ) -> BoxFuture<'static, Result>> + + Send + + Sync, +>; + +const DEFAULT_PROVIDER_ID: &str = "goose"; +const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)"; + struct GooseAcpSession { agent: Arc, messages: Conversation, @@ -66,7 +79,7 @@ struct GooseAcpSession { pub struct GooseAcpAgent { sessions: Arc>>, - provider_factory: ProviderConstructor, + provider_factory: AcpProviderFactory, builtins: Vec, client_fs_capabilities: OnceCell, client_terminal: OnceCell, @@ -77,34 +90,46 @@ pub struct GooseAcpAgent { disable_session_naming: bool, } +fn extract_timeout_from_meta(meta: &Option) -> Option { + meta.as_ref() + .and_then(|m| m.get("timeout")) + .and_then(|v| v.as_u64()) +} + fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result { match mcp_server { - McpServer::Stdio(stdio) => Ok(ExtensionConfig::Stdio { - name: stdio.name, - description: String::new(), - cmd: stdio.command.to_string_lossy().to_string(), - args: stdio.args, - envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()), - env_keys: vec![], - timeout: None, - bundled: Some(false), - available_tools: vec![], - }), - McpServer::Http(http) => Ok(ExtensionConfig::StreamableHttp { - name: http.name, - description: String::new(), - uri: http.url, - envs: Envs::default(), - env_keys: vec![], - headers: http - .headers - .into_iter() - .map(|h| (h.name, h.value)) - .collect(), - timeout: None, - bundled: Some(false), - available_tools: vec![], - }), + McpServer::Stdio(stdio) => { + let timeout = extract_timeout_from_meta(&stdio.meta); + Ok(ExtensionConfig::Stdio { + name: stdio.name, + description: String::new(), + cmd: stdio.command.to_string_lossy().to_string(), + args: stdio.args, + envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()), + env_keys: vec![], + timeout, + bundled: Some(false), + available_tools: vec![], + }) + } + McpServer::Http(http) => { + let timeout = extract_timeout_from_meta(&http.meta); + Ok(ExtensionConfig::StreamableHttp { + name: http.name, + description: String::new(), + uri: http.url, + envs: Envs::default(), + env_keys: vec![], + headers: http + .headers + .into_iter() + .map(|h| (h.name, h.value)) + .collect(), + timeout, + bundled: Some(false), + available_tools: vec![], + }) + } McpServer::Sse(_) => Err("SSE is unsupported, migrate to streamable_http".to_string()), _ => Err("Unknown MCP server type".to_string()), } @@ -328,6 +353,56 @@ async fn build_model_state(provider: &dyn Provider) -> Result) -> Vec { + let mut providers = goose::providers::providers() + .await + .into_iter() + .map(|(metadata, _)| ProviderListEntry { + id: metadata.name, + label: metadata.display_name, + }) + .collect::>(); + providers.sort_by(|left, right| left.id.cmp(&right.id)); + providers.dedup_by(|left, right| left.id == right.id); + + if let Some(current_provider) = current_provider { + if current_provider != DEFAULT_PROVIDER_ID + && !providers + .iter() + .any(|provider| provider.id == current_provider) + { + providers.push(ProviderListEntry { + id: current_provider.to_string(), + label: current_provider.to_string(), + }); + providers.sort_by(|left, right| left.id.cmp(&right.id)); + } + } + + let mut entries = Vec::with_capacity(providers.len() + 1); + entries.push(ProviderListEntry { + id: DEFAULT_PROVIDER_ID.to_string(), + label: DEFAULT_PROVIDER_LABEL.to_string(), + }); + entries.extend(providers); + entries +} + +async fn build_provider_options(current_provider: Option<&str>) -> Vec { + list_provider_entries(current_provider) + .await + .into_iter() + .map(|provider| SessionConfigSelectOption::new(provider.id, provider.label)) + .collect() +} + +fn session_provider_selection(session: &Session) -> &str { + session + .provider_name + .as_deref() + .unwrap_or(DEFAULT_PROVIDER_ID) +} + fn build_mode_state(current_mode: GooseMode) -> Result { let mut available = Vec::with_capacity(GooseMode::VARIANTS.len()); for &name in GooseMode::VARIANTS { @@ -348,6 +423,8 @@ fn build_mode_state(current_mode: GooseMode) -> Result, ) -> Vec { let mode_options: Vec = mode_state .available_modes @@ -363,6 +440,12 @@ fn build_config_options( .map(|m| SessionConfigSelectOption::new(m.model_id.0.clone(), m.name.clone())) .collect(); vec![ + SessionConfigOption::select( + "provider", + "Provider", + provider_selection.to_string(), + provider_options, + ), SessionConfigOption::select( "mode", "Mode", @@ -387,7 +470,7 @@ impl GooseAcpAgent { // TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir. pub async fn new( - provider_factory: ProviderConstructor, + provider_factory: AcpProviderFactory, builtins: Vec, data_dir: std::path::PathBuf, config_dir: std::path::PathBuf, @@ -411,6 +494,19 @@ impl GooseAcpAgent { }) } + fn load_config(&self) -> Result { + Config::new(self.config_dir.join(CONFIG_YAML_NAME), "goose").map_err(Into::into) + } + + async fn create_provider( + &self, + provider_name: &str, + model_config: goose::model::ModelConfig, + extensions: Vec, + ) -> Result> { + (self.provider_factory)(provider_name.to_string(), model_config, extensions).await + } + async fn create_agent_for_session( &self, cx: Option<&ConnectionTo>, @@ -479,21 +575,30 @@ impl GooseAcpAgent { let skip_developer = acp_developer.is_some(); let sid_str = session_id.map(|s| s.0.to_string()); - for ext in extensions { - if skip_developer && ext.name() == "developer" { - continue; - } - let name = ext.name().to_string(); - match agent - .extension_manager - .add_extension(ext, None, None, sid_str.as_deref()) - .await - { - Ok(_) => info!(extension = %name, "extension loaded"), - Err(e) => warn!(extension = %name, error = %e, "extension load failed"), - } + if skip_developer { + extensions.retain(|ext| ext.name() != "developer"); } + let ext_manager = &agent.extension_manager; + let extension_futures = extensions + .into_iter() + .map(|ext| { + let ext_manager = Arc::clone(ext_manager); + let sid = sid_str.clone(); + async move { + let name = ext.name().to_string(); + match ext_manager + .add_extension(ext, None, None, sid.as_deref()) + .await + { + Ok(_) => info!(extension = %name, "extension loaded"), + Err(e) => warn!(extension = %name, error = %e, "extension load failed"), + } + } + }) + .collect::>(); + futures::future::join_all(extension_futures).await; + if let Some((client, config)) = acp_developer { let info = client.get_info().cloned(); agent @@ -843,6 +948,16 @@ impl GooseAcpAgent { ) -> Result { debug!(?args, "new session request"); + // Allow the client to request a specific provider via _meta.provider, + // avoiding the double-create when the client would otherwise call + // _goose/session/provider/update immediately after. + let requested_provider = args + .meta + .as_ref() + .and_then(|m| m.get("provider")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let goose_session = self .session_manager .create_session( @@ -856,6 +971,30 @@ impl GooseAcpAgent { sacp::Error::internal_error().data(format!("Failed to create session: {}", e)) })?; + if let Some(ref provider_name) = requested_provider { + self.session_manager + .update(&goose_session.id) + .provider_name(provider_name) + .apply() + .await + .map_err(|e| { + sacp::Error::internal_error() + .data(format!("Failed to set provider on session: {}", e)) + })?; + } + + // Reload the session so init_provider sees the updated provider_name. + let goose_session = if requested_provider.is_some() { + self.session_manager + .get_session(&goose_session.id, false) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to reload session: {}", e)) + })? + } else { + goose_session + }; + let session_id = SessionId::new(goose_session.id.clone()); let agent = self @@ -870,7 +1009,6 @@ impl GooseAcpAgent { .map_err(|e| { sacp::Error::internal_error().data(format!("Failed to set provider: {}", e)) })?; - Self::add_mcp_extensions(&agent, args.mcp_servers, &goose_session.id).await?; let session = GooseAcpSession { @@ -892,25 +1030,55 @@ impl GooseAcpAgent { let model_state = build_model_state(&*provider).await?; let mode_state = build_mode_state(self.goose_mode)?; + let provider_selection = session_provider_selection(&goose_session).to_string(); + let session_id_for_response = SessionId::new(goose_session.id); + let provider_options = build_provider_options(Some(provider.get_name())).await; - Ok(NewSessionResponse::new(SessionId::new(goose_session.id)) + Ok(NewSessionResponse::new(session_id_for_response) .models(model_state.clone()) .modes(mode_state.clone()) - .config_options(build_config_options(&mode_state, &model_state))) + .config_options(build_config_options( + &mode_state, + &model_state, + &provider_selection, + provider_options, + ))) } async fn init_provider(&self, agent: &Agent, session: &Session) -> Result> { + let config = self.load_config()?; + let global_provider = config.get_goose_provider().ok(); + let provider_override = session + .provider_name + .as_deref() + .filter(|provider| *provider != DEFAULT_PROVIDER_ID); + let provider_name = provider_override + .map(ToOwned::to_owned) + .or_else(|| global_provider.clone()) + .ok_or_else(|| anyhow::anyhow!("Could not configure agent: missing provider"))?; + let explicitly_switched = + provider_override.is_some() && provider_override != global_provider.as_deref(); let model_config = match &session.model_config { - Some(config) => config.clone(), + Some(model_config) => model_config.clone(), + None if explicitly_switched => { + // The provider was set via _meta.provider (or similar) without an + // explicit model. Use the provider's own default model from the + // registry so we don't leak the global config model (which belongs + // to a different provider) into this one. + let entry = goose::providers::get_from_registry(&provider_name).await?; + let default_model = &entry.metadata().default_model; + goose::model::ModelConfig::new(default_model)?.with_canonical_limits(&provider_name) + } None => { - let config_path = self.config_dir.join(CONFIG_YAML_NAME); - let config = Config::new(&config_path, "goose")?; let model_id = config.get_goose_model()?; - let provider_name = config.get_goose_provider()?; goose::model::ModelConfig::new(&model_id)?.with_canonical_limits(&provider_name) } }; - let provider = (self.provider_factory)(model_config, Vec::new()).await?; + let extensions = + EnabledExtensionsState::extensions_or_default(Some(&session.extension_data), &config); + let provider = self + .create_provider(&provider_name, model_config, extensions) + .await?; agent.update_provider(provider.clone(), &session.id).await?; Ok(provider) } @@ -932,10 +1100,11 @@ impl GooseAcpAgent { } async fn add_mcp_extensions( - agent: &Agent, + agent: &Arc, mcp_servers: Vec, session_id: &str, ) -> Result<(), sacp::Error> { + let mut configs = Vec::with_capacity(mcp_servers.len()); for mcp_server in mcp_servers { let config = match mcp_server_to_extension_config(mcp_server) { Ok(c) => c, @@ -943,10 +1112,24 @@ impl GooseAcpAgent { return Err(sacp::Error::invalid_params().data(msg)); } }; - let name = config.name().to_string(); - if let Err(e) = agent.add_extension(config, session_id).await { - return Err(sacp::Error::internal_error() - .data(format!("Failed to add MCP server '{}': {}", name, e))); + configs.push(config); + } + + if configs.is_empty() { + return Ok(()); + } + + let results = agent + .add_extensions_bulk(configs, session_id) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + for result in &results { + if !result.success { + let error_msg = result.error.as_deref().unwrap_or("unknown error"); + return Err(sacp::Error::internal_error().data(format!( + "Failed to add MCP server '{}': {}", + result.name, error_msg + ))); } } Ok(()) @@ -969,7 +1152,6 @@ impl GooseAcpAgent { sacp::Error::resource_not_found(Some(session_id.clone())) .data(format!("Session not found: {}", session_id)) })?; - let loaded_mode = goose_session.goose_mode; let acp_session_id = SessionId::new(session_id.clone()); @@ -979,7 +1161,6 @@ impl GooseAcpAgent { .map_err(|e| { sacp::Error::internal_error().data(format!("Failed to create agent: {}", e)) })?; - let provider = self .init_provider(&agent, &goose_session) .await @@ -987,8 +1168,15 @@ impl GooseAcpAgent { sacp::Error::internal_error().data(format!("Failed to set provider: {}", e)) })?; + agent + .update_goose_mode(loaded_mode, &session_id) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to restore mode: {}", e)) + })?; Self::add_mcp_extensions(&agent, args.mcp_servers, &session_id).await?; + let provider_selection = session_provider_selection(&goose_session).to_string(); let conversation = goose_session.conversation.ok_or_else(|| { sacp::Error::internal_error() .data(format!("Session {} has no conversation data", session_id)) @@ -1071,11 +1259,17 @@ impl GooseAcpAgent { let model_state = build_model_state(&*provider).await?; let mode_state = build_mode_state(goose_mode)?; + let provider_options = build_provider_options(Some(provider.get_name())).await; Ok(LoadSessionResponse::new() .models(model_state.clone()) .modes(mode_state.clone()) - .config_options(build_config_options(&mode_state, &model_state))) + .config_options(build_config_options( + &mode_state, + &model_state, + &provider_selection, + provider_options, + ))) } async fn on_prompt( @@ -1143,7 +1337,6 @@ impl GooseAcpAgent { if let Some(session) = sessions.get_mut(&session_id) { session.cancel_token = None; } - Ok(PromptResponse::new(if was_cancelled { StopReason::Cancelled } else { @@ -1174,25 +1367,28 @@ impl GooseAcpAgent { session_id: &str, model_id: &str, ) -> Result { - let config_path = self.config_dir.join(CONFIG_YAML_NAME); - let config = Config::new(&config_path, "goose").map_err(|e| { + let config = self.load_config().map_err(|e| { sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) })?; - let provider_name = config.get_goose_provider().map_err(|_| { - sacp::Error::internal_error().data("No provider configured".to_string()) + let agent = self.get_session_agent(session_id, None).await?; + let current_provider = agent.provider().await.map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to get provider: {}", e)) })?; + let provider_name = current_provider.get_name().to_string(); + let extensions = + EnabledExtensionsState::for_session(&self.session_manager, session_id, &config).await; let model_config = goose::model::ModelConfig::new(model_id) .map_err(|e| { sacp::Error::invalid_params().data(format!("Invalid model config: {}", e)) })? .with_canonical_limits(&provider_name); - let provider = (self.provider_factory)(model_config, Vec::new()) + let provider = self + .create_provider(&provider_name, model_config, extensions) .await .map_err(|e| { sacp::Error::internal_error().data(format!("Failed to create provider: {}", e)) })?; - let agent = self.get_session_agent(session_id, None).await?; agent .update_provider(provider, session_id) .await @@ -1200,6 +1396,14 @@ impl GooseAcpAgent { sacp::Error::internal_error().data(format!("Failed to update provider: {}", e)) })?; + let mode = agent.goose_mode().await; + agent + .update_goose_mode(mode, session_id) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to propagate mode: {}", e)) + })?; + info!(session_id = %session_id, model_id = %model_id, "Model switched"); Ok(SetSessionModelResponse::new()) } @@ -1208,6 +1412,11 @@ impl GooseAcpAgent { &self, session_id: &SessionId, ) -> Result<(SessionNotification, Vec), sacp::Error> { + let session = self + .session_manager + .get_session(&session_id.0, false) + .await + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; let agent = self.get_session_agent(&session_id.0, None).await?; let provider = agent.provider().await.map_err(|e| { sacp::Error::internal_error().data(format!("Failed to get provider: {}", e)) @@ -1215,7 +1424,13 @@ impl GooseAcpAgent { let goose_mode = agent.goose_mode().await; let model_state = build_model_state(&*provider).await?; let mode_state = build_mode_state(goose_mode)?; - let config_options = build_config_options(&mode_state, &model_state); + let provider_options = build_provider_options(Some(provider.get_name())).await; + let config_options = build_config_options( + &mode_state, + &model_state, + session_provider_selection(&session), + provider_options, + ); let notification = SessionNotification::new( session_id.clone(), SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options.clone())), @@ -1243,6 +1458,120 @@ impl GooseAcpAgent { Ok(SetSessionModeResponse::new()) } + async fn update_provider( + &self, + session_id: &str, + provider_name: &str, + model_name: Option<&str>, + context_limit: Option, + request_params: Option>, + ) -> Result, sacp::Error> { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + let agent = self.get_session_agent(session_id, None).await?; + let current_provider = agent.provider().await.map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to get provider: {}", e)) + })?; + let current_provider_name = current_provider.get_name(); + let current_model = current_provider.get_model_config().model_name; + let has_default_overrides = + model_name.is_some() || context_limit.is_some() || request_params.is_some(); + let use_default_provider = provider_name == DEFAULT_PROVIDER_ID; + let resolved_provider_name = if use_default_provider { + config.get_goose_provider().map_err(|e| { + sacp::Error::internal_error().data(format!( + "Failed to resolve default provider from config: {}", + e + )) + })? + } else { + provider_name.to_string() + }; + let is_changing_provider = resolved_provider_name != current_provider_name; + let default_model = if let Some(model_name) = model_name { + model_name.to_string() + } else if use_default_provider { + config.get_goose_model().map_err(|e| { + sacp::Error::internal_error().data(format!( + "Failed to resolve default model from config: {}", + e + )) + })? + } else if is_changing_provider { + ACP_CURRENT_MODEL.to_string() + } else { + current_model + }; + let model = model_name.unwrap_or(&default_model); + let model_config = goose::model::ModelConfig::new(model) + .map_err(|e| { + sacp::Error::invalid_params().data(format!("Invalid model config: {}", e)) + })? + .with_canonical_limits(&resolved_provider_name) + .with_context_limit(context_limit) + .with_request_params(request_params); + let extensions = + EnabledExtensionsState::for_session(&self.session_manager, session_id, &config).await; + let new_provider = self + .create_provider(&resolved_provider_name, model_config, extensions) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to create provider: {}", e)) + })?; + + agent + .update_provider(new_provider, session_id) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to update provider: {}", e)) + })?; + + let mode = agent.goose_mode().await; + agent + .update_goose_mode(mode, session_id) + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to propagate mode: {}", e)) + })?; + + let provider = agent.provider().await.map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to get provider: {}", e)) + })?; + + if use_default_provider { + let update = self + .session_manager + .update(session_id) + .provider_name(DEFAULT_PROVIDER_ID); + if has_default_overrides { + let provider_model_config = provider.get_model_config(); + update + .model_config(provider_model_config) + .apply() + .await + .map_err(|e| { + sacp::Error::internal_error().data(format!( + "Failed to persist default provider selection overrides: {}", + e + )) + })?; + } else { + update.clear_model_config().apply().await.map_err(|e| { + sacp::Error::internal_error().data(format!( + "Failed to persist default provider selection: {}", + e + )) + })?; + } + } + + let (_, config_options) = self + .build_config_update(&SessionId::new(session_id.to_string())) + .await?; + Ok(config_options) + } + async fn on_list_sessions(&self) -> Result { let sessions = self .session_manager @@ -1279,7 +1608,7 @@ impl GooseAcpAgent { #[custom_methods] impl GooseAcpAgent { - #[custom_method("_goose/extensions/add")] + #[custom_method(AddExtensionRequest)] async fn on_add_extension( &self, req: AddExtensionRequest, @@ -1294,7 +1623,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/extensions/remove")] + #[custom_method(RemoveExtensionRequest)] async fn on_remove_extension( &self, req: RemoveExtensionRequest, @@ -1307,7 +1636,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/tools")] + #[custom_method(GetToolsRequest)] async fn on_get_tools(&self, req: GetToolsRequest) -> Result { let agent = self.get_session_agent(&req.session_id, None).await?; let tools = agent.list_tools(&req.session_id, None).await; @@ -1319,7 +1648,7 @@ impl GooseAcpAgent { Ok(GetToolsResponse { tools: tools_json }) } - #[custom_method("_goose/resource/read")] + #[custom_method(ReadResourceRequest)] async fn on_read_resource( &self, req: ReadResourceRequest, @@ -1338,7 +1667,7 @@ impl GooseAcpAgent { }) } - #[custom_method("_goose/working_dir/update")] + #[custom_method(UpdateWorkingDirRequest)] async fn on_update_working_dir( &self, req: UpdateWorkingDirRequest, @@ -1370,8 +1699,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - // TODO: use typed GetSessionRequest when agent-client-protocol-schema adds it (Discussion #60) - #[custom_method("session/get")] + #[custom_method(GetSessionRequest)] async fn on_get_session( &self, req: GetSessionRequest, @@ -1388,8 +1716,7 @@ impl GooseAcpAgent { }) } - // TODO: use typed DeleteSessionRequest when agent-client-protocol-schema adds it (RFD #395) - #[custom_method("session/delete")] + #[custom_method(DeleteSessionRequest)] async fn on_delete_session( &self, req: DeleteSessionRequest, @@ -1402,7 +1729,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/session/export")] + #[custom_method(ExportSessionRequest)] async fn on_export_session( &self, req: ExportSessionRequest, @@ -1415,7 +1742,7 @@ impl GooseAcpAgent { Ok(ExportSessionResponse { data }) } - #[custom_method("_goose/session/import")] + #[custom_method(ImportSessionRequest)] async fn on_import_session( &self, req: ImportSessionRequest, @@ -1432,7 +1759,7 @@ impl GooseAcpAgent { }) } - #[custom_method("_goose/config/extensions")] + #[custom_method(GetExtensionsRequest)] async fn on_get_extensions(&self) -> Result { let extensions = goose::config::extensions::get_all_extensions(); let warnings = goose::config::extensions::get_warnings(); @@ -1446,6 +1773,124 @@ impl GooseAcpAgent { warnings, }) } + + #[custom_method(UpdateProviderRequest)] + async fn on_update_provider( + &self, + req: UpdateProviderRequest, + ) -> Result { + let config_options = self + .update_provider( + &req.session_id, + &req.provider, + req.model.as_deref(), + req.context_limit, + req.request_params, + ) + .await?; + let config_options = config_options + .into_iter() + .map(|option| serde_json::to_value(&option)) + .collect::, _>>() + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(UpdateProviderResponse { config_options }) + } + + #[custom_method(ListProvidersRequest)] + async fn on_list_providers( + &self, + _req: ListProvidersRequest, + ) -> Result { + Ok(ListProvidersResponse { + providers: list_provider_entries(None).await, + }) + } + + #[custom_method(ReadConfigRequest)] + async fn on_read_config( + &self, + req: ReadConfigRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + let response = match config.get_param::(&req.key) { + Ok(value) => ReadConfigResponse { value }, + Err(goose::config::ConfigError::NotFound(_)) => ReadConfigResponse { + value: serde_json::Value::Null, + }, + Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())), + }; + Ok(response) + } + + #[custom_method(UpsertConfigRequest)] + async fn on_upsert_config( + &self, + req: UpsertConfigRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + config + .set_param(&req.key, &req.value) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(EmptyResponse {}) + } + + #[custom_method(RemoveConfigRequest)] + async fn on_remove_config( + &self, + req: RemoveConfigRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + config + .delete(&req.key) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(EmptyResponse {}) + } + + #[custom_method(CheckSecretRequest)] + async fn on_check_secret( + &self, + req: CheckSecretRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + let exists = config.get_secret::(&req.key).is_ok(); + Ok(CheckSecretResponse { exists }) + } + + #[custom_method(UpsertSecretRequest)] + async fn on_upsert_secret( + &self, + req: UpsertSecretRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + config + .set_secret(&req.key, &req.value) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(EmptyResponse {}) + } + + #[custom_method(RemoveSecretRequest)] + async fn on_remove_secret( + &self, + req: RemoveSecretRequest, + ) -> Result { + let config = self.load_config().map_err(|e| { + sacp::Error::internal_error().data(format!("Failed to read config: {}", e)) + })?; + config + .delete_secret(&req.key) + .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + Ok(EmptyResponse {}) + } } pub struct GooseAcpHandler { @@ -1523,6 +1968,12 @@ impl HandleDispatchFrom for GooseAcpHandler { .clone(); let session_id = req.session_id.clone(); match req.config_id.0.as_ref() { + "provider" => { + match agent.update_provider(&session_id.0, &value_id.0, None, None, None).await { + Ok(_) => {} + Err(e) => { responder.respond_with_error(e)?; return Ok(()); } + } + } "mode" => { match agent.on_set_mode(&session_id.0, &value_id.0).await { Ok(_) => {} @@ -2067,11 +2518,23 @@ print(\"hello, world\") #[test_case( build_mode_state(GooseMode::Auto).unwrap(), + "openai", + vec![ + SessionConfigSelectOption::new("anthropic", "anthropic"), + SessionConfigSelectOption::new("openai", "openai"), + ], SessionModelState::new( ModelId::new("gpt-4"), vec![ModelInfo::new(ModelId::new("gpt-4"), "gpt-4"), ModelInfo::new(ModelId::new("gpt-3.5"), "gpt-3.5")], ) => vec![ + SessionConfigOption::select( + "provider", "Provider", "openai", + vec![ + SessionConfigSelectOption::new("anthropic", "anthropic"), + SessionConfigSelectOption::new("openai", "openai"), + ], + ), SessionConfigOption::select( "mode", "Mode", "auto", vec![ @@ -2093,8 +2556,14 @@ print(\"hello, world\") )] #[test_case( build_mode_state(GooseMode::Approve).unwrap(), + "openai", + vec![SessionConfigSelectOption::new("openai", "openai")], SessionModelState::new(ModelId::new("only-model"), vec![ModelInfo::new(ModelId::new("only-model"), "only-model")]) => vec![ + SessionConfigOption::select( + "provider", "Provider", "openai", + vec![SessionConfigSelectOption::new("openai", "openai")], + ), SessionConfigOption::select( "mode", "Mode", "approve", vec![ @@ -2113,8 +2582,10 @@ print(\"hello, world\") )] fn test_build_config_options( mode_state: SessionModeState, + provider_name: &'static str, + provider_options: Vec, model_state: SessionModelState, ) -> Vec { - build_config_options(&mode_state, &model_state) + build_config_options(&mode_state, &model_state, provider_name, provider_options) } } diff --git a/crates/goose-acp/src/server_factory.rs b/crates/goose-acp/src/server_factory.rs index b2810d8564..5c63d0175c 100644 --- a/crates/goose-acp/src/server_factory.rs +++ b/crates/goose-acp/src/server_factory.rs @@ -1,9 +1,8 @@ use anyhow::Result; -use goose::providers::provider_registry::ProviderConstructor; use std::sync::Arc; use tracing::info; -use crate::server::GooseAcpAgent; +use crate::server::{AcpProviderFactory, GooseAcpAgent}; pub struct AcpServerFactoryConfig { pub builtins: Vec, @@ -32,16 +31,12 @@ impl AcpServer { .unwrap_or(goose::config::GooseMode::Auto); let disable_session_naming = config.get_goose_disable_session_naming().unwrap_or(false); - let config_dir = self.config.config_dir.clone(); - let provider_factory: ProviderConstructor = Arc::new(move |model_config, extensions| { - let config_dir = config_dir.clone(); - Box::pin(async move { - let config_path = config_dir.join(goose::config::base::CONFIG_YAML_NAME); - let config = goose::config::Config::new(&config_path, "goose")?; - let provider_name = config.get_goose_provider()?; - goose::providers::create(&provider_name, model_config, extensions).await - }) - }); + let provider_factory: AcpProviderFactory = + Arc::new(move |provider_name, model_config, extensions| { + Box::pin(async move { + goose::providers::create(&provider_name, model_config, extensions).await + }) + }); let agent = GooseAcpAgent::new( provider_factory, diff --git a/crates/goose-acp/tests/common_tests/mod.rs b/crates/goose-acp/tests/common_tests/mod.rs index a3076b0022..e965a9260b 100644 --- a/crates/goose-acp/tests/common_tests/mod.rs +++ b/crates/goose-acp/tests/common_tests/mod.rs @@ -11,7 +11,7 @@ use fixtures::{ use fs_err as fs; use goose::config::base::CONFIG_YAML_NAME; use goose::config::GooseMode; -use goose::providers::provider_registry::ProviderConstructor; +use goose_acp::server::AcpProviderFactory; use goose_test_support::{McpFixture, FAKE_CODE, TEST_IMAGE_B64, TEST_MODEL}; use sacp::schema::{ ListSessionsResponse, McpServer, McpServerHttp, ModelId, SessionInfo, SessionModeId, @@ -332,8 +332,8 @@ pub async fn run_fs_write_text_file_true() { } pub async fn run_initialize_doesnt_hit_provider() { - let provider_factory: ProviderConstructor = - Arc::new(|_, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) })); + let provider_factory: AcpProviderFactory = + Arc::new(|_, _, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) })); let openai = OpenAiFixture::new(vec![], C::expected_session_id()).await; let config = TestConnectionConfig { @@ -341,12 +341,7 @@ pub async fn run_initialize_doesnt_hit_provider() { ..Default::default() }; - let conn = C::new(config, openai).await; - assert!(!conn.auth_methods().is_empty()); - assert!(conn - .auth_methods() - .iter() - .any(|m| m.id().0.as_ref() == "goose-provider")); + let _conn = C::new(config, openai).await; } pub async fn run_load_mode() { @@ -740,6 +735,24 @@ pub async fn run_model_list() { assert_eq!(models.current_model_id, ModelId::new(TEST_MODEL)); } +#[allow(dead_code)] +pub async fn run_new_session_returns_initial_config() { + let expected_session_id = C::expected_session_id(); + let openai = OpenAiFixture::new(vec![], expected_session_id.clone()).await; + + let mut conn = C::new(TestConnectionConfig::default(), openai).await; + let SessionData { + session, + models, + modes, + } = conn.new_session().await.unwrap(); + expected_session_id.set(&session.session_id().0); + + assert!(modes.is_some()); + let models = models.expect("new_session should return models inline"); + assert!(!models.available_models.is_empty()); +} + pub async fn run_config_option_model_set() { run_model_set_impl::(SetModelVia::ConfigOption).await; } @@ -808,13 +821,16 @@ async fn run_model_set_impl(via: SetModelVia) { .unwrap(); assert_eq!(output.text, "2"); - // Both model paths emit ConfigOption (no CurrentModelUpdate in the schema). + // Some connections emit a ConfigOption update immediately on model change, + // while the stripped legacy provider path only updates local state before + // the next prompt. let prompt_notifs = session_b.notifications(); let mut all = set_model_notifs; all.extend(prompt_notifs); - assert_eq!( - all, - vec![Notification::ConfigOption, Notification::AgentMessage], + assert!( + all == vec![Notification::AgentMessage] + || all == vec![Notification::ConfigOption, Notification::AgentMessage], + "unexpected notifications after model change: {all:?}" ); // Prompt A: expects default TEST_MODEL (proves sessions are independent) diff --git a/crates/goose-acp/tests/custom_requests_test.rs b/crates/goose-acp/tests/custom_requests_test.rs index c54b96cc1d..29f983bec1 100644 --- a/crates/goose-acp/tests/custom_requests_test.rs +++ b/crates/goose-acp/tests/custom_requests_test.rs @@ -5,11 +5,66 @@ use common_tests::fixtures::server::AcpServerConnection; use common_tests::fixtures::{ run_test, send_custom, Connection, Session, SessionData, TestConnectionConfig, }; +use goose::model::ModelConfig; +use goose::providers::base::{MessageStream, Provider}; +use goose::providers::errors::ProviderError; +use goose_acp::server::AcpProviderFactory; use goose_test_support::EnforceSessionId; use std::sync::Arc; use common_tests::fixtures::OpenAiFixture; +struct MockProvider { + name: String, + model_config: ModelConfig, + recommended_models: Vec, +} + +#[async_trait::async_trait] +impl Provider for MockProvider { + fn get_name(&self) -> &str { + &self.name + } + + async fn stream( + &self, + _model_config: &ModelConfig, + _session_id: &str, + _system: &str, + _messages: &[goose::conversation::message::Message], + _tools: &[rmcp::model::Tool], + ) -> Result { + unimplemented!() + } + + fn get_model_config(&self) -> ModelConfig { + self.model_config.clone() + } + + async fn fetch_recommended_models(&self) -> Result, ProviderError> { + Ok(self.recommended_models.clone()) + } +} + +fn mock_provider_factory() -> AcpProviderFactory { + Arc::new(|provider_name, model_config, _extensions| { + Box::pin(async move { + let recommended_models = match provider_name.as_str() { + "anthropic" => vec![ + "claude-3-7-sonnet-latest".to_string(), + "claude-3-5-haiku-latest".to_string(), + ], + _ => vec!["gpt-4o".to_string(), "o4-mini".to_string()], + }; + Ok(Arc::new(MockProvider { + name: provider_name, + model_config, + recommended_models, + }) as Arc) + }) + }) +} + #[test] fn test_custom_session_get() { run_test(async { @@ -83,6 +138,209 @@ fn test_custom_get_extensions() { }); } +#[test] +fn test_custom_list_providers() { + run_test(async { + let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; + let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; + + let response = send_custom(conn.cx(), "_goose/providers/list", serde_json::json!({})) + .await + .expect("provider list should succeed"); + let providers = response + .get("providers") + .and_then(|value| value.as_array()) + .expect("missing providers array"); + + assert!( + providers.iter().any(|provider| { + provider.get("id") == Some(&serde_json::json!("goose")) + && provider.get("label") == Some(&serde_json::json!("Goose (Default)")) + }), + "expected Goose default provider sentinel" + ); + assert!( + providers + .iter() + .any(|provider| provider.get("id") == Some(&serde_json::json!("openai"))), + "expected at least one concrete provider from the goose registry" + ); + }); +} + +#[test] +fn test_custom_config_crud() { + run_test(async { + let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; + let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; + + send_custom( + conn.cx(), + "_goose/config/upsert", + serde_json::json!({ + "key": "GOOSE_PROVIDER", + "value": "anthropic", + }), + ) + .await + .expect("config upsert should succeed"); + + let response = send_custom( + conn.cx(), + "_goose/config/read", + serde_json::json!({ + "key": "GOOSE_PROVIDER", + }), + ) + .await + .expect("config read should succeed"); + assert_eq!(response.get("value"), Some(&serde_json::json!("anthropic"))); + + send_custom( + conn.cx(), + "_goose/config/remove", + serde_json::json!({ + "key": "GOOSE_PROVIDER", + }), + ) + .await + .expect("config remove should succeed"); + + let response = send_custom( + conn.cx(), + "_goose/config/read", + serde_json::json!({ + "key": "GOOSE_PROVIDER", + }), + ) + .await + .expect("config read after remove should succeed"); + assert_eq!(response.get("value"), Some(&serde_json::Value::Null)); + }); +} + +#[test] +fn test_provider_switching_updates_session_state() { + run_test(async { + let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; + let config = TestConnectionConfig { + provider_factory: Some(mock_provider_factory()), + current_model: "gpt-4o".to_string(), + ..Default::default() + }; + let mut conn = AcpServerConnection::new(config, openai).await; + + let SessionData { session, .. } = conn.new_session().await.unwrap(); + let session_id = session.session_id().0.clone(); + + conn.set_config_option(&session_id, "provider", "anthropic") + .await + .expect("provider config option should succeed"); + + let response = send_custom( + conn.cx(), + "session/get", + serde_json::json!({ + "sessionId": session_id, + }), + ) + .await + .expect("session/get should succeed"); + let session_value = response.get("session").expect("missing session"); + assert_eq!( + session_value.get("provider_name"), + Some(&serde_json::json!("anthropic")) + ); + assert_eq!( + session_value + .get("model_config") + .and_then(|value| value.get("model_name")), + Some(&serde_json::json!("current")) + ); + + let response = send_custom( + conn.cx(), + "_goose/session/provider/update", + serde_json::json!({ + "sessionId": session_id, + "provider": "openai", + "model": "o4-mini", + }), + ) + .await + .expect("provider update should succeed"); + let config_options = response + .get("configOptions") + .and_then(|value| value.as_array()) + .expect("missing config options"); + assert!( + !config_options.is_empty(), + "expected refreshed config options" + ); + + let response = send_custom( + conn.cx(), + "session/get", + serde_json::json!({ + "sessionId": session_id, + }), + ) + .await + .expect("session/get after provider update should succeed"); + let session_value = response.get("session").expect("missing session"); + assert_eq!( + session_value.get("provider_name"), + Some(&serde_json::json!("openai")) + ); + assert_eq!( + session_value + .get("model_config") + .and_then(|value| value.get("model_name")), + Some(&serde_json::json!("o4-mini")) + ); + + let response = send_custom( + conn.cx(), + "_goose/session/provider/update", + serde_json::json!({ + "sessionId": session_id, + "provider": "goose", + }), + ) + .await + .expect("provider reset to goose should succeed"); + let config_options = response + .get("configOptions") + .and_then(|value| value.as_array()) + .expect("missing config options after reset"); + assert!( + config_options + .iter() + .any(|option| option.get("id") == Some(&serde_json::json!("provider"))), + "missing provider config option after reset" + ); + + let response = send_custom( + conn.cx(), + "session/get", + serde_json::json!({ + "sessionId": session_id, + }), + ) + .await + .expect("session/get after provider reset should succeed"); + let session_value = response.get("session").expect("missing session"); + assert_eq!( + session_value.get("provider_name"), + Some(&serde_json::json!("goose")) + ); + assert_eq!( + session_value.get("model_config"), + Some(&serde_json::Value::Null) + ); + }); +} + #[test] fn test_custom_unknown_method() { run_test(async { diff --git a/crates/goose-acp/tests/fixtures/mod.rs b/crates/goose-acp/tests/fixtures/mod.rs index e7fb12dea8..4fc0c970a2 100644 --- a/crates/goose-acp/tests/fixtures/mod.rs +++ b/crates/goose-acp/tests/fixtures/mod.rs @@ -3,19 +3,18 @@ use async_trait::async_trait; use fs_err as fs; -pub use goose::acp::{map_permission_response, PermissionDecision, PermissionMapping}; +pub use goose::acp::{map_permission_response, PermissionDecision}; use goose::builtin_extension::register_builtin_extensions; use goose::config::paths::Paths; use goose::config::{GooseMode, PermissionManager}; use goose::providers::api_client::{ApiClient, AuthMethod as ApiAuthMethod}; use goose::providers::base::Provider; use goose::providers::openai::OpenAiProvider; -use goose::providers::provider_registry::ProviderConstructor; use goose::session_context::SESSION_ID_HEADER; -use goose_acp::server::{serve, GooseAcpAgent}; +use goose_acp::server::{serve, AcpProviderFactory, GooseAcpAgent}; use goose_test_support::{ExpectedSessionId, TEST_MODEL}; use sacp::schema::{ - AuthMethod, CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer, + CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer, ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalResponse, SessionModeState, SessionModelState, SessionUpdate, TerminalExitStatus, TerminalId, TerminalOutputResponse, ToolCallContent, ToolCallStatus, ToolKind, WaitForTerminalExitResponse, WriteTextFileRequest, @@ -155,7 +154,7 @@ pub async fn spawn_acp_server_in_process( builtins: &[String], data_root: &std::path::Path, goose_mode: GooseMode, - provider_factory: Option, + provider_factory: Option, current_model: &str, ) -> (DuplexTransport, JoinHandle<()>, Arc) { fs::create_dir_all(data_root).unwrap(); @@ -171,7 +170,7 @@ pub async fn spawn_acp_server_in_process( } let provider_factory = provider_factory.unwrap_or_else(|| { let base_url = openai_base_url.to_string(); - Arc::new(move |model_config, _extensions| { + Arc::new(move |_provider_name, model_config, _extensions| { let base_url = base_url.clone(); Box::pin(async move { let api_client = @@ -472,7 +471,7 @@ pub struct TestConnectionConfig { pub goose_mode: GooseMode, pub cwd: Option, pub data_root: PathBuf, - pub provider_factory: Option, + pub provider_factory: Option, pub read_text_file: Option, pub write_text_file: Option, pub terminal: Option>, @@ -524,7 +523,6 @@ pub trait Connection: Sized { config_id: &str, value: &str, ) -> anyhow::Result<()>; - fn auth_methods(&self) -> &[AuthMethod]; fn data_root(&self) -> std::path::PathBuf; fn reset_openai(&self); fn reset_permissions(&self); diff --git a/crates/goose-acp/tests/fixtures/provider.rs b/crates/goose-acp/tests/fixtures/provider.rs index d9a38d1e85..6f5b658aae 100644 --- a/crates/goose-acp/tests/fixtures/provider.rs +++ b/crates/goose-acp/tests/fixtures/provider.rs @@ -4,7 +4,7 @@ use super::{ }; use async_trait::async_trait; use futures::StreamExt; -use goose::acp::{AcpProvider, AcpProviderConfig, PermissionMapping}; +use goose::acp::{AcpProvider, AcpProviderConfig}; use goose::config::{GooseMode, PermissionManager}; use goose::conversation::message::{ActionRequiredData, Message, MessageContent}; use goose::model::ModelConfig; @@ -12,9 +12,12 @@ use goose::permission::permission_confirmation::PrincipalType; use goose::permission::{Permission, PermissionConfirmation}; use goose::providers::base::Provider; use goose_test_support::{ExpectedSessionId, IgnoreSessionId, TEST_MODEL}; -use sacp::schema::{AuthMethod, ListSessionsResponse, McpServer, SessionUpdate, ToolCallStatus}; +use sacp::schema::{ + ListSessionsResponse, McpServer, ModelId, ModelInfo, SessionModelState, SessionUpdate, + ToolCallStatus, +}; use sacp::{Channel, Client, ConnectTo, DynConnectTo}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; use strum::VariantNames; @@ -28,10 +31,10 @@ pub struct AcpProviderConnection { /// Option so close_session can trigger session/close via Drop. provider: Arc>>, permission_manager: Arc, - auth_methods: Vec, session_counter: usize, notification_sink: NotificationSink, session_models: SessionModels, + strip_config_options: bool, work_dir: std::path::PathBuf, data_root: std::path::PathBuf, _openai: OpenAiFixture, @@ -186,7 +189,6 @@ impl Connection for AcpProviderConnection { (mode, mode.to_string()) }) .collect(), - permission_mapping: PermissionMapping::default(), notification_callback: Some(Arc::new(move |n| { sink_clone.lock().unwrap().push(n.update.clone()); })), @@ -208,15 +210,13 @@ impl Connection for AcpProviderConnection { .await .unwrap(); - let auth_methods = provider.auth_methods().to_vec(); - Self { provider: Arc::new(Mutex::new(Some(provider))), permission_manager, - auth_methods, session_counter: 0, notification_sink, session_models, + strip_config_options: config.strip_config_options, work_dir: cwd_path, data_root, _openai: openai, @@ -226,18 +226,23 @@ impl Connection for AcpProviderConnection { } async fn new_session(&mut self) -> anyhow::Result> { - // Tests like run_model_set call new_session() multiple times on the same - // connection, so each needs a distinct key to avoid returning a cached session. self.session_counter += 1; let goose_id = format!("test-session-{}", self.session_counter); - let response = self - .provider - .lock() - .await - .as_ref() - .unwrap() - .ensure_session(Some(&goose_id)) - .await?; + + let models = if self.strip_config_options { + None + } else { + let provider = self.provider.lock().await; + let provider = provider.as_ref().unwrap(); + let available_models = provider.fetch_supported_models().await?; + Some(SessionModelState::new( + ModelId::new(provider.get_model_config().model_name.clone()), + available_models + .into_iter() + .map(|model_id| ModelInfo::new(ModelId::new(model_id.clone()), model_id)) + .collect(), + )) + }; let session = AcpProviderSession { provider: Arc::clone(&self.provider), @@ -246,10 +251,11 @@ impl Connection for AcpProviderConnection { session_models: self.session_models.clone(), work_dir: self.work_dir.clone(), }; + self.notification_sink.lock().unwrap().clear(); Ok(SessionData { session, - models: response.models, - modes: response.modes, + models, + modes: None, }) } @@ -264,13 +270,7 @@ impl Connection for AcpProviderConnection { } async fn list_sessions(&self) -> anyhow::Result { - self.provider - .lock() - .await - .as_ref() - .unwrap() - .list_sessions() - .await + Err(anyhow::anyhow!("not implemented for AcpProviderConnection")) } async fn close_session(&self, _session_id: &str) -> anyhow::Result<()> { @@ -279,89 +279,29 @@ impl Connection for AcpProviderConnection { Ok(()) } - async fn delete_session(&self, session_id: &str) -> anyhow::Result<()> { - self.provider - .lock() - .await - .as_ref() - .unwrap() - .delete_session(session_id) - .await + async fn delete_session(&self, _session_id: &str) -> anyhow::Result<()> { + Err(anyhow::anyhow!("not implemented for AcpProviderConnection")) } fn data_root(&self) -> std::path::PathBuf { self.data_root.clone() } - async fn set_mode(&self, session_id: &str, mode_id: &str) -> anyhow::Result<()> { - let mode = GooseMode::from_str(mode_id) - .map_err(|_| sacp::Error::invalid_params().data(format!("Invalid mode: {mode_id}")))?; - let guard = self.provider.lock().await; - let provider = guard.as_ref().unwrap(); - if !provider.has_session(session_id).await { - return Err( - sacp::Error::resource_not_found(Some(session_id.to_string())) - .data(format!("Session not found: {session_id}")) - .into(), - ); - } - provider - .update_mode(session_id, mode) - .await - .map_err(|e| anyhow::anyhow!("{e}")) + async fn set_mode(&self, _session_id: &str, _mode_id: &str) -> anyhow::Result<()> { + Err(anyhow::anyhow!("not implemented for AcpProviderConnection")) } - async fn set_model(&self, session_id: &str, model_id: &str) -> anyhow::Result<()> { - let config = ModelConfig::new(model_id).map_err(|e| anyhow::anyhow!("{e}"))?; - self.session_models - .lock() - .unwrap() - .insert(session_id.to_string(), config); - Ok(()) + async fn set_model(&self, _session_id: &str, _model_id: &str) -> anyhow::Result<()> { + Err(anyhow::anyhow!("not implemented for AcpProviderConnection")) } async fn set_config_option( &self, - session_id: &str, - config_id: &str, - value: &str, + _session_id: &str, + _config_id: &str, + _value: &str, ) -> anyhow::Result<()> { - // Check up front because the "model" branch doesn't go through the provider. - let guard = self.provider.lock().await; - let provider = guard.as_ref().unwrap(); - if !provider.has_session(session_id).await { - return Err( - sacp::Error::resource_not_found(Some(session_id.to_string())) - .data(format!("Session not found: {session_id}")) - .into(), - ); - } - match config_id { - "mode" => { - let mode = GooseMode::from_str(value).map_err(|_| { - sacp::Error::invalid_params().data(format!("Invalid mode: {value}")) - })?; - provider - .update_mode(session_id, mode) - .await - .map_err(|e| anyhow::anyhow!("{e}")) - } - "model" => { - let config = ModelConfig::new(value).map_err(|e| anyhow::anyhow!("{e}"))?; - self.session_models - .lock() - .unwrap() - .insert(session_id.to_string(), config); - Ok(()) - } - other => Err(sacp::Error::invalid_params() - .data(format!("Unsupported config option: {other}")) - .into()), - } - } - - fn auth_methods(&self) -> &[AuthMethod] { - &self.auth_methods + Err(anyhow::anyhow!("not implemented for AcpProviderConnection")) } fn reset_openai(&self) { @@ -425,6 +365,8 @@ fn strip_config_options(transport: DuplexTransport) -> Channel { }); tokio::spawn(async move { + let mut stripped_initial_config = HashSet::new(); + let goose_to_server = async { let mut from_goose = filter.rx; while let Some(msg) = from_goose.next().await { @@ -437,19 +379,67 @@ fn strip_config_options(transport: DuplexTransport) -> Channel { let server_to_goose = async { let mut from_server = server.rx; while let Some(msg) = from_server.next().await { - let msg = msg.map(|m| match m { - sacp::jsonrpcmsg::Message::Response(mut resp) => { - if let Some(ref mut result) = resp.result { - if let Some(obj) = result.as_object_mut() { - obj.remove("configOptions"); + let msg = match msg { + Ok(m) => match m { + sacp::jsonrpcmsg::Message::Response(mut resp) => { + if let Some(ref mut result) = resp.result { + if let Some(obj) = result.as_object_mut() { + obj.remove("configOptions"); + } + } + Ok(Some(sacp::jsonrpcmsg::Message::Response(resp))) + } + sacp::jsonrpcmsg::Message::Request(req) + if req.id.is_none() + && req.method == "session/update" + && req + .params + .as_ref() + .and_then(|params| match params { + sacp::jsonrpcmsg::Params::Object(obj) => Some(obj), + _ => None, + }) + .and_then(|obj| obj.get("update")) + .and_then(|update| update.get("sessionUpdate")) + .and_then(|session_update| session_update.as_str()) + == Some("config_option_update") => + { + let session_id = req + .params + .as_ref() + .and_then(|params| match params { + sacp::jsonrpcmsg::Params::Object(obj) => Some(obj), + _ => None, + }) + .and_then(|obj| obj.get("sessionId")) + .and_then(|session_id| session_id.as_str()) + .map(str::to_owned); + if let Some(session_id) = session_id { + if stripped_initial_config.insert(session_id) { + Ok(None) + } else { + Ok(Some(sacp::jsonrpcmsg::Message::Request(req))) + } + } else { + Ok(Some(sacp::jsonrpcmsg::Message::Request(req))) } } - sacp::jsonrpcmsg::Message::Response(resp) + other => Ok(Some(other)), + }, + Err(err) => Err(err), + }; + match msg { + Ok(Some(msg)) => { + if filter.tx.unbounded_send(Ok(msg)).is_err() { + break; + } + } + Ok(None) => continue, + Err(err) => { + if filter.tx.unbounded_send(Err(err)).is_err() { + break; + } } - other => other, - }); - if filter.tx.unbounded_send(msg).is_err() { - break; } } }; diff --git a/crates/goose-acp/tests/fixtures/server.rs b/crates/goose-acp/tests/fixtures/server.rs index 6a6ed2443b..20fb1c093b 100644 --- a/crates/goose-acp/tests/fixtures/server.rs +++ b/crates/goose-acp/tests/fixtures/server.rs @@ -1,19 +1,20 @@ use super::{ - map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision, - PermissionMapping, Session, SessionData, TestConnectionConfig, TestOutput, + map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision, Session, + SessionData, TestConnectionConfig, TestOutput, }; use async_trait::async_trait; use goose::config::PermissionManager; use goose_test_support::{EnforceSessionId, ExpectedSessionId}; use sacp::schema::{ - AuthMethod, ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest, + ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest, FileSystemCapabilities, ImageContent, InitializeRequest, KillTerminalRequest, - ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, NewSessionRequest, - PromptRequest, ProtocolVersion, ReadTextFileRequest, ReleaseTerminalRequest, - RequestPermissionRequest, SessionConfigOptionValue, SessionId, SessionModeId, - SessionNotification, SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest, - SetSessionModelRequest, StopReason, TerminalOutputRequest, TextContent, ToolCallStatus, - WaitForTerminalExitRequest, WriteTextFileRequest, + ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, ModelId, ModelInfo, + NewSessionRequest, PromptRequest, ProtocolVersion, ReadTextFileRequest, ReleaseTerminalRequest, + RequestPermissionRequest, SessionConfigKind, SessionConfigOptionCategory, + SessionConfigOptionValue, SessionId, SessionModeId, SessionModelState, SessionNotification, + SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModelRequest, + StopReason, TerminalOutputRequest, TextContent, ToolCallStatus, WaitForTerminalExitRequest, + WriteTextFileRequest, }; use sacp::{Agent, Client, ConnectionTo}; use std::sync::{Arc, Mutex}; @@ -30,7 +31,6 @@ pub struct AcpServerConnection { permission: Arc>, notify: Arc, permission_manager: Arc, - auth_methods: Vec, _openai: super::OpenAiFixture, _temp_dir: Option, } @@ -133,7 +133,7 @@ impl Connection for AcpServerConnection { fs_cap = fs_cap.write_text_file(true); } - let (cx, auth_methods) = { + let cx = { let updates_clone = updates.clone(); let notify_clone = notify.clone(); let permission_clone = permission.clone(); @@ -143,14 +143,10 @@ impl Connection for AcpServerConnection { let cx_holder: Arc>>> = Arc::new(Mutex::new(None)); let cx_holder_clone = cx_holder.clone(); - let auth_holder: Arc>> = Arc::new(Mutex::new(Vec::new())); - let auth_holder_clone = auth_holder.clone(); let (ready_tx, ready_rx) = tokio::sync::oneshot::channel(); tokio::spawn(async move { - let permission_mapping = PermissionMapping::default(); - let result = Client .builder() .on_receive_notification( @@ -170,9 +166,7 @@ impl Connection for AcpServerConnection { let permission = permission_clone.clone(); async move |req: RequestPermissionRequest, responder, _connection_cx| { let decision = *permission.lock().unwrap(); - let response = - map_permission_response(&permission_mapping, &req, decision); - responder.respond(response) + responder.respond(map_permission_response(&req, decision)) } }, sacp::on_receive_request!(), @@ -261,9 +255,8 @@ impl Connection for AcpServerConnection { ) .connect_with(transport, { let cx_holder = cx_holder_clone; - let auth_holder = auth_holder_clone; async move |cx: ConnectionTo| { - let resp = cx + let _resp = cx .send_request( InitializeRequest::new(ProtocolVersion::LATEST) .client_capabilities( @@ -276,7 +269,6 @@ impl Connection for AcpServerConnection { .await .unwrap(); - *auth_holder.lock().unwrap() = resp.auth_methods; *cx_holder.lock().unwrap() = Some(cx.clone()); let _ = ready_tx.send(()); @@ -292,8 +284,7 @@ impl Connection for AcpServerConnection { ready_rx.await.unwrap(); let cx = cx_holder.lock().unwrap().take().unwrap(); - let auth = std::mem::take(&mut *auth_holder.lock().unwrap()); - (cx, auth) + cx }; Self { @@ -305,7 +296,6 @@ impl Connection for AcpServerConnection { permission, notify, permission_manager, - auth_methods, _openai: openai, _temp_dir: temp_dir, } @@ -330,9 +320,13 @@ impl Connection for AcpServerConnection { notify: self.notify.clone(), _work_dir: work_dir, }; + let models = response.models.or_else(|| { + extract_model_state_from_config_options(response.config_options.as_deref()) + }); + self.updates.lock().unwrap().clear(); Ok(SessionData { session, - models: response.models, + models, modes: response.modes, }) } @@ -438,10 +432,6 @@ impl Connection for AcpServerConnection { .map_err(|e| e.into()) } - fn auth_methods(&self) -> &[AuthMethod] { - &self.auth_methods - } - fn data_root(&self) -> std::path::PathBuf { self.data_root.clone() } @@ -504,6 +494,46 @@ impl Session for AcpServerSession { } } +fn extract_model_state_from_config_options( + config_options: Option<&[sacp::schema::SessionConfigOption]>, +) -> Option { + let option = config_options? + .iter() + .find(|option| option.category.as_ref() == Some(&SessionConfigOptionCategory::Model))?; + let SessionConfigKind::Select(select) = &option.kind else { + return None; + }; + + let available_models = match &select.options { + sacp::schema::SessionConfigSelectOptions::Ungrouped(options) => options + .iter() + .map(|option| { + ModelInfo::new( + ModelId::new(option.value.0.to_string()), + option.name.clone(), + ) + }) + .collect(), + sacp::schema::SessionConfigSelectOptions::Grouped(groups) => groups + .iter() + .flat_map(|group| { + group.options.iter().map(|option| { + ModelInfo::new( + ModelId::new(option.value.0.to_string()), + option.name.clone(), + ) + }) + }) + .collect(), + _ => Vec::new(), + }; + + Some(SessionModelState::new( + ModelId::new(select.current_value.0.to_string()), + available_models, + )) +} + fn collect_agent_text(updates: &Arc>>) -> String { let guard = updates.lock().unwrap(); let mut text = String::new(); diff --git a/crates/goose-acp/tests/provider_test.rs b/crates/goose-acp/tests/provider_test.rs index a4fed9592c..9a1b1baf68 100644 --- a/crates/goose-acp/tests/provider_test.rs +++ b/crates/goose-acp/tests/provider_test.rs @@ -1,47 +1,28 @@ #![recursion_limit = "256"] +#[allow(dead_code)] mod common_tests; use common_tests::fixtures::provider::AcpProviderConnection; use common_tests::fixtures::run_test; use common_tests::{ - run_close_session, run_config_mcp, run_config_option_mode_set, run_config_option_model_set, - run_delete_session, run_fs_read_text_file_true, run_fs_write_text_file_false, - run_fs_write_text_file_true, run_initialize_doesnt_hit_provider, run_list_sessions, - run_load_mode, run_load_model, run_load_session_error, run_load_session_mcp, run_mode_set, - run_model_list, run_model_set, run_model_set_error_session_not_found, - run_permission_persistence, run_prompt_basic, run_prompt_codemode, run_prompt_error, - run_prompt_image, run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch, - run_prompt_skill, run_shell_terminal_false, run_shell_terminal_true, + run_close_session, run_config_mcp, run_delete_session, run_fs_read_text_file_true, + run_fs_write_text_file_false, run_fs_write_text_file_true, run_load_mode, run_load_model, + run_load_session_error, run_load_session_mcp, run_model_list, run_permission_persistence, + run_prompt_basic, run_prompt_codemode, run_prompt_error, run_prompt_image, + run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill, + run_shell_terminal_false, run_shell_terminal_true, }; -tests_config_option_set_error!(AcpProviderConnection); -tests_mode_set_error!(AcpProviderConnection); - #[test] fn test_config_mcp() { run_test(async { run_config_mcp::().await }); } -#[test] -fn test_config_option_mode_set() { - run_test(async { run_config_option_mode_set::().await }); -} - -#[test] -fn test_list_sessions() { - run_test(async { run_list_sessions::().await }); -} - #[test] fn test_close_session() { run_test(async { run_close_session::().await }); } -#[test] -fn test_config_option_model_set() { - run_test(async { run_config_option_model_set::().await }); -} - #[test] #[ignore = "delete is a server-side custom method not routed through the provider"] fn test_delete_session() { @@ -65,11 +46,6 @@ fn test_fs_write_text_file_true() { run_test(async { run_fs_write_text_file_true::().await }); } -#[test] -fn test_initialize_doesnt_hit_provider() { - run_test(async { run_initialize_doesnt_hit_provider::().await }); -} - #[test] #[ignore = "TODO: implement load_session in ACP provider"] fn test_load_mode() { @@ -94,27 +70,11 @@ fn test_load_session_mcp() { run_test(async { run_load_session_mcp::().await }); } -#[test] -fn test_mode_set() { - run_test(async { run_mode_set::().await }); -} - #[test] fn test_model_list() { run_test(async { run_model_list::().await }); } -#[test] -fn test_model_set() { - run_test(async { run_model_set::().await }); -} - -#[test] -#[ignore = "ensure_session lazy-creates sessions so deleted ones reappear"] -fn test_model_set_error_session_not_found() { - run_test(async { run_model_set_error_session_not_found::().await }); -} - #[test] fn test_permission_persistence() { run_test(async { run_permission_persistence::().await }); diff --git a/crates/goose-acp/tests/server_test.rs b/crates/goose-acp/tests/server_test.rs index e2a278b58e..a57aa40e6b 100644 --- a/crates/goose-acp/tests/server_test.rs +++ b/crates/goose-acp/tests/server_test.rs @@ -1,3 +1,4 @@ +#[allow(dead_code)] mod common_tests; use common_tests::fixtures::run_test; use common_tests::fixtures::server::AcpServerConnection; @@ -7,9 +8,10 @@ use common_tests::{ run_fs_write_text_file_true, run_initialize_doesnt_hit_provider, run_list_sessions, run_load_mode, run_load_model, run_load_session_error, run_load_session_mcp, run_mode_set, run_model_list, run_model_set, run_model_set_error_session_not_found, - run_permission_persistence, run_prompt_basic, run_prompt_codemode, run_prompt_error, - run_prompt_image, run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch, - run_prompt_skill, run_shell_terminal_false, run_shell_terminal_true, + run_new_session_returns_initial_config, run_permission_persistence, run_prompt_basic, + run_prompt_codemode, run_prompt_error, run_prompt_image, run_prompt_image_attachment, + run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill, run_shell_terminal_false, + run_shell_terminal_true, }; tests_config_option_set_error!(AcpServerConnection); @@ -95,6 +97,11 @@ fn test_model_list() { run_test(async { run_model_list::().await }); } +#[test] +fn test_new_session_returns_initial_config() { + run_test(async { run_new_session_returns_initial_config::().await }); +} + #[test] fn test_model_set() { run_test(async { run_model_set::().await }); diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 585427c9b1..5788babac2 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1478,7 +1478,11 @@ pub fn configure_keyring_dialog() -> anyhow::Result<()> { }; let _ = cliclack::log::info(format!("Current secret storage: {}", current_status)); - let _ = cliclack::log::warning("Note: Disabling the keyring stores secrets in a plain text file (~/.config/goose/secrets.yaml)"); + let secrets_path = Paths::config_dir().join("secrets.yaml"); + let _ = cliclack::log::warning(format!( + "Note: Disabling the keyring stores secrets in a plain text file ({})", + secrets_path.display() + )); let storage_option = cliclack::select("How would you like to store secrets?") .item( @@ -1504,9 +1508,10 @@ pub fn configure_keyring_dialog() -> anyhow::Result<()> { "file" => { // Set the disable flag to use file storage config.set_param("GOOSE_DISABLE_KEYRING", Value::String("true".to_string()))?; - cliclack::outro( - "Secret storage set to file (~/.config/goose/secrets.yaml). Keep this file secure!", - )?; + cliclack::outro(format!( + "Secret storage set to file ({}). Keep this file secure!", + secrets_path.display(), + ))?; let _ = cliclack::log::info("You may need to restart goose for this change to take effect"); } diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index c0fbb59589..a513ac52c9 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -69,7 +69,7 @@ const GITHUB_ACTIONS_ISSUER: &str = "https://token.actions.githubusercontent.com async fn fetch_attestations(digest: &str, token: Option<&str>) -> Result> { let url = format!( - "https://api.github.com/repos/block/goose/attestations/sha256:{digest}\ + "https://api.github.com/repos/aaif-goose/goose/attestations/sha256:{digest}\ ?per_page=30&predicate_type=https://slsa.dev/provenance/v1" ); @@ -209,7 +209,7 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { { let tag = if canary { "canary" } else { "stable" }; let asset = asset_name(); - let url = format!("https://github.com/block/goose/releases/download/{tag}/{asset}"); + let url = format!("https://github.com/aaif-goose/goose/releases/download/{tag}/{asset}"); println!("Downloading {asset} from {tag} release..."); diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index f83aeb5837..dc1447d494 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -665,7 +665,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { "Error {}.\n\ Please check your system keychain and run 'goose configure' again.\n\ If your system is unable to use the keyring, please try setting secret key(s) via environment variables.\n\ - For more info, see: https://block.github.io/goose/docs/troubleshooting/#keychainkeyring-errors", + For more info, see: https://goose-docs.ai/docs/troubleshooting/#keychainkeyring-errors", e )); process::exit(1); diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index 24d9d37ca7..1bb0b77a7a 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -3,6 +3,8 @@ use once_cell::sync::Lazy; use rmcp::{ServerHandler, ServiceExt}; use std::collections::HashMap; +// NOTE: "Block" is kept here for backwards compatibility with existing +// user config/data directories. Changing this would orphan existing installations. pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { top_level_domain: "Block".to_string(), author: "Block".to_string(), diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml new file mode 100644 index 0000000000..6eef747a3c --- /dev/null +++ b/crates/goose-sdk/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "goose-sdk" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Rust SDK for talking to Goose over the Agent Client Protocol (ACP)" + +[dependencies] +sacp = { workspace = true, features = ["unstable"] } +agent-client-protocol-schema = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +schemars = { workspace = true, features = ["derive"] } + +[dev-dependencies] +tokio = { workspace = true } +tokio-util = { workspace = true, features = ["compat", "rt"] } + +[package.metadata.cargo-machete] +# Used to provide extras imports for sacp +ignored = ["agent-client-protocol-schema"] diff --git a/crates/goose-sdk/examples/acp_client.rs b/crates/goose-sdk/examples/acp_client.rs new file mode 100644 index 0000000000..8ef5b29ca4 --- /dev/null +++ b/crates/goose-sdk/examples/acp_client.rs @@ -0,0 +1,156 @@ +//! ACP Client Example +//! +//! Spawns `goose acp` as a child process and sends it a completion request +//! using the Agent Client Protocol over stdio. +//! +//! # Prerequisites +//! +//! You must have goose built and a provider configured (`goose configure`). +//! +//! # Usage +//! +//! ```bash +//! cargo run -p goose-sdk --example acp_client -- "What is 2 + 2?" +//! ``` +//! +//! Or with a custom goose binary path: +//! +//! ```bash +//! cargo run -p goose-sdk --example acp_client -- --goose-bin ./target/debug/goose "Explain Rust's ownership model in one sentence" +//! ``` + +use goose_sdk::custom_requests::GetExtensionsRequest; +use sacp::schema::{ + ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome, + RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, + SessionNotification, SessionUpdate, +}; +use sacp::{Client, ConnectionTo}; +use std::path::PathBuf; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Parse args: [--goose-bin PATH] PROMPT + let args: Vec = std::env::args().skip(1).collect(); + let (goose_bin, prompt) = parse_args(&args)?; + + eprintln!("πŸš€ Spawning: {} acp", goose_bin.display()); + + let mut child = tokio::process::Command::new(&goose_bin) + .arg("acp") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .spawn() + .map_err(|e| format!("Failed to spawn '{}': {e}", goose_bin.display()))?; + + let child_stdin = child.stdin.take().expect("stdin should be piped"); + let child_stdout = child.stdout.take().expect("stdout should be piped"); + + let transport = sacp::ByteStreams::new(child_stdin.compat_write(), child_stdout.compat()); + + let prompt_clone = prompt.clone(); + + Client + .builder() + .name("acp-client-example") + // Print session notifications (agent text, tool calls, etc.) + .on_receive_notification( + async move |notification: SessionNotification, _cx| { + match ¬ification.update { + SessionUpdate::AgentMessageChunk(chunk) => { + if let ContentBlock::Text(text) = &chunk.content { + print!("{}", text.text); + } + } + SessionUpdate::ToolCall(tool_call) => { + eprintln!("πŸ”§ Tool call: {}", tool_call.title); + } + SessionUpdate::ToolCallUpdate(update) => { + if let Some(status) = &update.fields.status { + eprintln!(" Tool status: {:?}", status); + } + } + _ => {} + } + Ok(()) + }, + sacp::on_receive_notification!(), + ) + // Auto-approve all permission requests + .on_receive_request( + async move |request: RequestPermissionRequest, responder, _cx| { + eprintln!("βœ… Auto-approving permission request"); + let option_id = request.options.first().map(|opt| opt.option_id.clone()); + match option_id { + Some(id) => responder.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)), + )), + None => responder.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Cancelled, + )), + } + }, + sacp::on_receive_request!(), + ) + .connect_with(transport, async move |cx: ConnectionTo| { + // Step 1: Initialize + eprintln!("🀝 Initializing..."); + let init_response = cx + .send_request(InitializeRequest::new(ProtocolVersion::LATEST)) + .block_task() + .await?; + eprintln!("βœ“ Agent initialized: {:?}", init_response.agent_info); + + let response = cx + .send_request(GetExtensionsRequest {}) + .block_task() + .await?; + eprintln!("Extensions: {:?}", response.extensions); + + // Step 2: Create a session and send the prompt + eprintln!("πŸ’¬ Sending prompt: \"{}\"", prompt_clone); + cx.build_session_cwd()? + .block_task() + .run_until(async |mut session| { + session.send_prompt(&prompt_clone)?; + let response = session.read_to_string().await?; + + // read_to_string collects text; we already printed chunks above, + // so just print a newline to finish. + println!(); + eprintln!("βœ… Done ({} chars)", response.len()); + Ok(()) + }) + .await + }) + .await?; + + let _ = child.kill().await; + Ok(()) +} + +fn parse_args(args: &[String]) -> Result<(PathBuf, String), String> { + let mut goose_bin = PathBuf::from("goose"); + let mut i = 0; + + while i < args.len() { + match args[i].as_str() { + "--goose-bin" => { + i += 1; + goose_bin = PathBuf::from(args.get(i).ok_or("--goose-bin requires a value")?); + } + _ => break, + } + i += 1; + } + + let prompt = args[i..].join(" "); + + if prompt.is_empty() { + return Err("Usage: acp_client [--goose-bin PATH] PROMPT".into()); + } + + Ok((goose_bin, prompt)) +} diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs new file mode 100644 index 0000000000..afa61149ed --- /dev/null +++ b/crates/goose-sdk/src/custom_requests.rs @@ -0,0 +1,259 @@ +use sacp::{JsonRpcRequest, JsonRpcResponse}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Schema descriptor for a single custom method, produced by the +/// `#[custom_methods]` macro's generated `custom_method_schemas()` function. +/// +/// `params_schema` / `response_schema` hold `$ref` pointers or inline schemas +/// produced by `SchemaGenerator::subschema_for`. All referenced types are +/// collected in the generator's `$defs` map. +/// +/// `params_type_name` / `response_type_name` carry the Rust struct name so the +/// binary can key `$defs` entries and annotate them with `x-method` / `x-side`. +#[derive(Debug, Serialize)] +pub struct CustomMethodSchema { + pub method: String, + pub params_schema: Option, + pub params_type_name: Option, + pub response_schema: Option, + pub response_type_name: Option, +} + +/// Add an extension to an active session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/extensions/add", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct AddExtensionRequest { + pub session_id: String, + /// Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). + #[serde(default)] + pub config: serde_json::Value, +} + +/// Remove an extension from an active session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/extensions/remove", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct RemoveExtensionRequest { + pub session_id: String, + pub name: String, +} + +/// List all tools available in a session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/tools", response = GetToolsResponse)] +#[serde(rename_all = "camelCase")] +pub struct GetToolsRequest { + pub session_id: String, +} + +/// Tools response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct GetToolsResponse { + /// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. + pub tools: Vec, +} + +/// Read a resource from an extension. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/resource/read", response = ReadResourceResponse)] +#[serde(rename_all = "camelCase")] +pub struct ReadResourceRequest { + pub session_id: String, + pub uri: String, + pub extension_name: String, +} + +/// Resource read response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ReadResourceResponse { + /// The resource result from the extension (MCP ReadResourceResult). + #[serde(default)] + pub result: serde_json::Value, +} + +/// Update the working directory for a session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/working_dir/update", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct UpdateWorkingDirRequest { + pub session_id: String, + pub working_dir: String, +} + +/// Get a session by ID. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "session/get", response = GetSessionResponse)] +#[serde(rename_all = "camelCase")] +pub struct GetSessionRequest { + pub session_id: String, + #[serde(default)] + pub include_messages: bool, +} + +/// Get a session response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct GetSessionResponse { + /// The session object with id, name, working_dir, timestamps, tokens, etc. + #[serde(default)] + pub session: serde_json::Value, +} + +/// Delete a session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "session/delete", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct DeleteSessionRequest { + pub session_id: String, +} + +/// Export a session as a JSON string. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/session/export", response = ExportSessionResponse)] +#[serde(rename_all = "camelCase")] +pub struct ExportSessionRequest { + pub session_id: String, +} + +/// Export session response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ExportSessionResponse { + pub data: String, +} + +/// Import a session from a JSON string. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/session/import", response = ImportSessionResponse)] +pub struct ImportSessionRequest { + pub data: String, +} + +/// Import session response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ImportSessionResponse { + /// The imported session object. + #[serde(default)] + pub session: serde_json::Value, +} + +/// List configured extensions and any warnings. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/config/extensions", response = GetExtensionsResponse)] +pub struct GetExtensionsRequest {} + +/// List configured extensions and any warnings. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct GetExtensionsResponse { + /// Array of ExtensionEntry objects with `enabled` flag and config details. + pub extensions: Vec, + pub warnings: Vec, +} + +/// Atomically update the provider for a live session. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/session/provider/update", response = UpdateProviderResponse)] +#[serde(rename_all = "camelCase")] +pub struct UpdateProviderRequest { + pub session_id: String, + pub provider: String, + pub model: Option, + pub context_limit: Option, + pub request_params: Option>, +} + +/// Provider update response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct UpdateProviderResponse { + /// Refreshed session config options after the provider/model change. + pub config_options: Vec, +} + +/// Read a single non-secret config value. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/config/read", response = ReadConfigResponse)] +#[serde(rename_all = "camelCase")] +pub struct ReadConfigRequest { + pub key: String, +} + +/// Config read response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct ReadConfigResponse { + #[serde(default)] + pub value: serde_json::Value, +} + +/// Upsert a single non-secret config value. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/config/upsert", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct UpsertConfigRequest { + pub key: String, + pub value: serde_json::Value, +} + +/// Remove a single non-secret config value. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/config/remove", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct RemoveConfigRequest { + pub key: String, +} + +/// Check whether a secret exists. Never returns the actual value. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/secret/check", response = CheckSecretResponse)] +#[serde(rename_all = "camelCase")] +pub struct CheckSecretRequest { + pub key: String, +} + +/// Secret check response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct CheckSecretResponse { + pub exists: bool, +} + +/// Set a secret value (write-only). +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/secret/upsert", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct UpsertSecretRequest { + pub key: String, + pub value: serde_json::Value, +} + +/// Remove a secret. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/secret/remove", response = EmptyResponse)] +#[serde(rename_all = "camelCase")] +pub struct RemoveSecretRequest { + pub key: String, +} + +/// List providers available through goose, including the config-default sentinel. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/providers/list", response = ListProvidersResponse)] +pub struct ListProvidersRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ProviderListEntry { + pub id: String, + pub label: String, +} + +/// Provider list response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct ListProvidersResponse { + pub providers: Vec, +} + +/// Empty success response for operations that return no data. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +pub struct EmptyResponse {} diff --git a/crates/goose-sdk/src/lib.rs b/crates/goose-sdk/src/lib.rs new file mode 100644 index 0000000000..6c1c1bf50c --- /dev/null +++ b/crates/goose-sdk/src/lib.rs @@ -0,0 +1 @@ +pub mod custom_requests; diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 161a0c25e7..1d18e2b40f 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -252,7 +252,10 @@ pub async fn read_config( ) )] pub async fn get_extensions() -> Result, ErrorResponse> { - let extensions = goose::config::get_all_extensions(); + 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, @@ -424,19 +427,13 @@ pub async fn get_slash_commands( let working_dir = query.working_dir.map(std::path::PathBuf::from); for source in - goose::agents::platform_extensions::summon::list_installed_sources(working_dir.as_deref()) + goose::agents::platform_extensions::skills::list_installed_skills(working_dir.as_deref()) { - if matches!( - source.kind, - goose::agents::platform_extensions::summon::SourceKind::Skill - | goose::agents::platform_extensions::summon::SourceKind::BuiltinSkill - ) { - commands.push(SlashCommand { - command: source.name, - help: source.description, - command_type: CommandType::Skill, - }); - } + commands.push(SlashCommand { + command: source.name, + help: source.description, + command_type: CommandType::Skill, + }); } Ok(Json(SlashCommandsResponse { commands })) diff --git a/crates/goose-server/ui/desktop/openapi.json b/crates/goose-server/ui/desktop/openapi.json index f7471caa15..7ef2609779 100644 --- a/crates/goose-server/ui/desktop/openapi.json +++ b/crates/goose-server/ui/desktop/openapi.json @@ -4,7 +4,7 @@ "title": "goose-server", "description": "An AI agent", "contact": { - "name": "Block", + "name": "AAIF", "email": "ai-oss-tools@block.xyz" }, "license": { diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 7b69b0979a..a3a68b699e 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -188,6 +188,7 @@ pkcs1 = { version = "0.7", default-features = false, features = ["pkcs8"], optio pkcs8 = { version = "0.10", default-features = false, features = ["alloc"], optional = true } sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8"], optional = true } + [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } keyring = { version = "3.6.2", features = ["windows-native"] } @@ -201,6 +202,7 @@ keyring = { version = "3.6.2", features = ["apple-native"] } [target.'cfg(target_os = "linux")'.dependencies] keyring = { version = "3.6.2", features = ["sync-secret-service"] } +libc = "0.2.184" [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/goose/src/acp/common.rs b/crates/goose/src/acp/common.rs index 759687b23a..803dd718e3 100644 --- a/crates/goose/src/acp/common.rs +++ b/crates/goose/src/acp/common.rs @@ -3,27 +3,10 @@ use std::str::FromStr; use crate::permission::Permission; use sacp::schema::{ PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest, - RequestPermissionResponse, SelectedPermissionOutcome, ToolCallStatus, + RequestPermissionResponse, SelectedPermissionOutcome, }; use strum::{Display, EnumString}; -#[derive(Clone, Debug)] -pub struct PermissionMapping { - pub allow_option_id: Option, - pub reject_option_id: Option, - pub rejected_tool_status: ToolCallStatus, -} - -impl Default for PermissionMapping { - fn default() -> Self { - Self { - allow_option_id: None, - reject_option_id: None, - rejected_tool_status: ToolCallStatus::Failed, - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq, Display, EnumString)] #[strum(serialize_all = "snake_case")] pub enum PermissionDecision { @@ -81,60 +64,30 @@ impl From<&RequestPermissionOutcome> for PermissionDecision { } } +/// Map a permission decision to a response by matching the option kind from the +/// request. Each decision tries its preferred kind first, then falls back to the +/// closest alternative (e.g. AllowAlways falls back to AllowOnce). pub fn map_permission_response( - mapping: &PermissionMapping, request: &RequestPermissionRequest, decision: PermissionDecision, ) -> RequestPermissionResponse { let selected_id = match decision { - PermissionDecision::AllowAlways => select_option_id( - &request.options, - &mapping.allow_option_id, - PermissionOptionKind::AllowAlways, - ) - .or_else(|| { - select_option_id( - &request.options, - &mapping.allow_option_id, - PermissionOptionKind::AllowOnce, - ) - }), - PermissionDecision::AllowOnce => select_option_id( - &request.options, - &mapping.allow_option_id, - PermissionOptionKind::AllowOnce, - ) - .or_else(|| { - select_option_id( - &request.options, - &mapping.allow_option_id, - PermissionOptionKind::AllowAlways, - ) - }), - PermissionDecision::RejectAlways => select_option_id( - &request.options, - &mapping.reject_option_id, - PermissionOptionKind::RejectAlways, - ) - .or_else(|| { - select_option_id( - &request.options, - &mapping.reject_option_id, - PermissionOptionKind::RejectOnce, - ) - }), - PermissionDecision::RejectOnce => select_option_id( - &request.options, - &mapping.reject_option_id, - PermissionOptionKind::RejectOnce, - ) - .or_else(|| { - select_option_id( - &request.options, - &mapping.reject_option_id, - PermissionOptionKind::RejectAlways, - ) - }), + PermissionDecision::AllowAlways => { + find_option(&request.options, PermissionOptionKind::AllowAlways) + .or_else(|| find_option(&request.options, PermissionOptionKind::AllowOnce)) + } + PermissionDecision::AllowOnce => { + find_option(&request.options, PermissionOptionKind::AllowOnce) + .or_else(|| find_option(&request.options, PermissionOptionKind::AllowAlways)) + } + PermissionDecision::RejectAlways => { + find_option(&request.options, PermissionOptionKind::RejectAlways) + .or_else(|| find_option(&request.options, PermissionOptionKind::RejectOnce)) + } + PermissionDecision::RejectOnce => { + find_option(&request.options, PermissionOptionKind::RejectOnce) + .or_else(|| find_option(&request.options, PermissionOptionKind::RejectAlways)) + } PermissionDecision::Cancel => None, }; @@ -147,18 +100,7 @@ pub fn map_permission_response( } } -fn select_option_id( - options: &[PermissionOption], - preferred_id: &Option, - kind: PermissionOptionKind, -) -> Option { - if let Some(preferred_id) = preferred_id { - let preferred = sacp::schema::PermissionOptionId::new(preferred_id.clone()); - if options.iter().any(|opt| opt.option_id == preferred) { - return Some(preferred_id.clone()); - } - } - +fn find_option(options: &[PermissionOption], kind: PermissionOptionKind) -> Option { options .iter() .find(|opt| opt.kind == kind) @@ -186,88 +128,34 @@ mod tests { } #[test_case( - Some("allow"), - None, - PermissionDecision::AllowOnce, - "allow", - true; - "allow_uses_preferred_id" - )] - #[test_case( - None, - None, PermissionDecision::AllowAlways, - "allow_always", - false; - "allow_always_prefers_kind" + "allow_always"; + "allow_always_matches_kind" )] #[test_case( - Some("missing"), - None, PermissionDecision::AllowOnce, - "allow_once", - false; - "allow_falls_back_to_kind" + "allow_once"; + "allow_once_matches_kind" )] #[test_case( - None, - Some("reject"), PermissionDecision::RejectOnce, - "reject", - true; - "reject_uses_preferred_id" + "reject_once"; + "reject_once_matches_kind" )] #[test_case( - None, - Some("missing"), - PermissionDecision::RejectOnce, - "reject_once", - false; - "reject_falls_back_to_kind" + PermissionDecision::RejectAlways, + "reject_always"; + "reject_always_matches_kind" )] - fn test_permission_mapping( - allow_option_id: Option<&str>, - reject_option_id: Option<&str>, - decision: PermissionDecision, - expected_id: &str, - include_preferred: bool, - ) { - let mut options = vec![ + fn test_permission_response(decision: PermissionDecision, expected_id: &str) { + let options = vec![ option("allow_once", PermissionOptionKind::AllowOnce), option("allow_always", PermissionOptionKind::AllowAlways), option("reject_once", PermissionOptionKind::RejectOnce), - option("reject", PermissionOptionKind::RejectAlways), + option("reject_always", PermissionOptionKind::RejectAlways), ]; - - if include_preferred { - if let Some(preferred_allow) = allow_option_id { - if !options - .iter() - .any(|opt| opt.option_id.0.as_ref() == preferred_allow) - { - options.push(option(preferred_allow, PermissionOptionKind::AllowOnce)); - } - } - - if let Some(preferred_reject) = reject_option_id { - if !options - .iter() - .any(|opt| opt.option_id.0.as_ref() == preferred_reject) - { - options.push(option(preferred_reject, PermissionOptionKind::RejectOnce)); - } - } - } - let request = make_request(options); - - let mapping = PermissionMapping { - allow_option_id: allow_option_id.map(|s| s.to_string()), - reject_option_id: reject_option_id.map(|s| s.to_string()), - rejected_tool_status: ToolCallStatus::Failed, - }; - - let response = map_permission_response(&mapping, &request, decision); + let response = map_permission_response(&request, decision); match response.outcome { RequestPermissionOutcome::Selected(selected) => { assert_eq!(selected.option_id.0.as_ref(), expected_id); @@ -276,10 +164,22 @@ mod tests { } } + #[test] + fn test_allow_always_falls_back_to_allow_once() { + let request = make_request(vec![option("allow_once", PermissionOptionKind::AllowOnce)]); + let response = map_permission_response(&request, PermissionDecision::AllowAlways); + match response.outcome { + RequestPermissionOutcome::Selected(selected) => { + assert_eq!(selected.option_id.0.as_ref(), "allow_once"); + } + _ => panic!("expected selected outcome"), + } + } + #[test_case(PermissionDecision::Cancel; "cancelled")] fn test_permission_cancelled(decision: PermissionDecision) { let request = make_request(vec![option("allow_once", PermissionOptionKind::AllowOnce)]); - let response = map_permission_response(&PermissionMapping::default(), &request, decision); + let response = map_permission_response(&request, decision); assert!(matches!( response.outcome, RequestPermissionOutcome::Cancelled diff --git a/crates/goose/src/acp/mod.rs b/crates/goose/src/acp/mod.rs index 4692125e53..6e995948f1 100644 --- a/crates/goose/src/acp/mod.rs +++ b/crates/goose/src/acp/mod.rs @@ -1,7 +1,7 @@ mod common; mod provider; -pub use common::{map_permission_response, PermissionDecision, PermissionMapping}; +pub use common::{map_permission_response, PermissionDecision}; pub use provider::{ extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, }; diff --git a/crates/goose/src/acp/provider.rs b/crates/goose/src/acp/provider.rs index 6f9d0f3ba8..575fe631a4 100644 --- a/crates/goose/src/acp/provider.rs +++ b/crates/goose/src/acp/provider.rs @@ -4,14 +4,14 @@ use async_stream::try_stream; use futures::future::BoxFuture; use rmcp::model::{Role, Tool}; use sacp::schema::{ - AuthMethod, CloseSessionRequest, ContentBlock, ContentChunk, EnvVariable, HttpHeader, - ImageContent, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, - McpCapabilities, McpServer, McpServerHttp, McpServerStdio, NewSessionRequest, - NewSessionResponse, PromptRequest, PromptResponse, ProtocolVersion, RequestPermissionOutcome, - RequestPermissionRequest, RequestPermissionResponse, SessionConfigKind, - SessionConfigOptionCategory, SessionConfigSelectOptions, SessionId, SessionNotification, - SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModeResponse, - SetSessionModelRequest, StopReason, TextContent, ToolCallContent, + ClientCapabilities, CloseSessionRequest, ContentBlock, ContentChunk, EnvVariable, HttpHeader, + ImageContent, InitializeRequest, InitializeResponse, McpCapabilities, McpServer, McpServerHttp, + McpServerStdio, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, + ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, + SessionConfigKind, SessionConfigOption, SessionConfigOptionCategory, + SessionConfigSelectOptions, SessionId, SessionNotification, SessionUpdate, + SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModeResponse, StopReason, + TextContent, ToolCallContent, }; use sacp::{Agent, Client, ConnectionTo}; use std::collections::{HashMap, HashSet}; @@ -21,10 +21,10 @@ use std::process::Stdio; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use tokio::process::{Child, Command}; -use tokio::sync::{mpsc, oneshot, Mutex as TokioMutex, OnceCell}; -use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; +use tokio::sync::{mpsc, oneshot, Mutex as TokioMutex}; +use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; -use crate::acp::{map_permission_response, PermissionDecision, PermissionMapping}; +use crate::acp::{map_permission_response, PermissionDecision}; use crate::config::{ExtensionConfig, GooseMode}; use crate::conversation::message::{Message, MessageContent}; use crate::model::ModelConfig; @@ -34,7 +34,7 @@ use crate::providers::base::{MessageStream, PermissionRouting, Provider}; use crate::providers::errors::ProviderError; use crate::subprocess::configure_subprocess; -/// Sentinel: resolved to SessionModelState.current_model_id at connect time. +/// Sentinel: resolved to the actual model name during connect(). pub const ACP_CURRENT_MODEL: &str = "current"; pub struct AcpProviderConfig { @@ -46,7 +46,6 @@ pub struct AcpProviderConfig { pub mcp_servers: Vec, pub session_mode_id: Option, pub mode_mapping: HashMap, - pub permission_mapping: PermissionMapping, pub notification_callback: Option>, } @@ -54,19 +53,11 @@ enum ClientRequest { NewSession { response_tx: oneshot::Sender>, }, - ListSessions { - response_tx: oneshot::Sender>, - }, SetMode { session_id: SessionId, mode_id: String, response_tx: oneshot::Sender>, }, - SetModel { - session_id: SessionId, - model_id: String, - response_tx: oneshot::Sender>, - }, SetConfigOption { session_id: SessionId, config_id: String, @@ -78,16 +69,6 @@ enum ClientRequest { content: Vec, response_tx: mpsc::Sender, }, - CloseSession { - session_id: SessionId, - response_tx: oneshot::Sender>, - }, - // For ACP methods not yet in agent-client-protocol-schema (e.g. session/delete) - Untyped { - method: String, - params: serde_json::Value, - response_tx: oneshot::Sender>, - }, } // tokio I/O handles can't move between runtimes, so the child process must be @@ -119,24 +100,26 @@ enum AcpUpdate { Error(String), } +/// The single ACP session backing this provider instance. +#[derive(Clone)] +struct AcpSession { + id: SessionId, + response: NewSessionResponse, +} + pub struct AcpProvider { name: String, model: ModelConfig, goose_mode: Arc>, - tx: Option>, - loop_thread: Option>, mode_mapping: HashMap, - permission_mapping: PermissionMapping, - rejected_tool_calls: Arc>>, + + session: AcpSession, + pending_confirmations: Arc>>>, - goose_to_acp_id: Arc>>, - acp_to_goose_id: Arc>>, - /// Per-session model tracking for detecting model changes in stream(). - session_model: Arc>>, - auth_methods: Vec, - supports_close: bool, - init_session: OnceCell, + + tx: Option>, + loop_thread: Option>, } impl std::fmt::Debug for AcpProvider { @@ -148,8 +131,6 @@ impl std::fmt::Debug for AcpProvider { } } -// Dedicated runtime on an OS thread so session/close completes even during -// main runtime shutdown. See reqwest InnerClientHandle. fn spawn_client_loop(fut: impl Future + Send + 'static) -> JoinHandle<()> { std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() @@ -211,117 +192,63 @@ impl AcpProvider { let (tx, rx) = mpsc::channel(32); let (init_tx, init_rx) = oneshot::channel(); let mode_mapping = config.mode_mapping.clone(); - let permission_mapping = config.permission_mapping.clone(); - let rejected_tool_calls = Arc::new(TokioMutex::new(HashSet::new())); - let goose_mode = Arc::new(Mutex::new(goose_mode)); - let client_loop = AcpClientLoop::new(config, goose_mode.clone()); + let goose_mode_shared = Arc::new(Mutex::new(goose_mode)); + let client_loop = AcpClientLoop::new(config, goose_mode_shared.clone()); let loop_thread = spawn_client_loop(run(client_loop, rx, init_tx)); - let init_response = init_rx + let _init_response = init_rx .await .context("ACP client initialization cancelled")??; - let supports_close = init_response - .agent_capabilities - .session_capabilities - .close - .is_some(); - let mut provider = Self::new_with_runtime( - name, - model, - goose_mode, - tx, - loop_thread, - mode_mapping, - permission_mapping, - rejected_tool_calls, - init_response.auth_methods, - supports_close, - ); - if provider.model.model_name == ACP_CURRENT_MODEL { - let response = provider.get_init_session().await?; - let (current_model, _) = resolve_model_info(&provider.name, response)?; - tracing::info!(from = ACP_CURRENT_MODEL, to = %current_model, "resolved ACP model"); - provider.model.model_name = current_model; - } - Ok(provider) - } + // Create the ACP session eagerly during connect. + let (session_tx, session_rx) = oneshot::channel(); + tx.send(ClientRequest::NewSession { + response_tx: session_tx, + }) + .await + .context("ACP client is unavailable")?; + let response = session_rx + .await + .context("ACP session creation cancelled")??; - #[allow(clippy::too_many_arguments)] - fn new_with_runtime( - name: String, - model: ModelConfig, - goose_mode: Arc>, - tx: mpsc::Sender, - loop_thread: JoinHandle<()>, - mode_mapping: HashMap, - permission_mapping: PermissionMapping, - rejected_tool_calls: Arc>>, - auth_methods: Vec, - supports_close: bool, - ) -> Self { - Self { + // Resolve model from the session response. + let resolved_model = if model.model_name == ACP_CURRENT_MODEL { + if let Ok((resolved, _)) = resolve_model_info(&name, &response) { + tracing::info!(from = ACP_CURRENT_MODEL, to = %resolved, "resolved ACP model"); + ModelConfig { + model_name: resolved, + ..model + } + } else { + model + } + } else { + model + }; + + let session = AcpSession { + id: response.session_id.clone(), + response, + }; + + Ok(Self { name, - model, - goose_mode, + model: resolved_model, + goose_mode: goose_mode_shared, + mode_mapping, + session, + pending_confirmations: Arc::new(TokioMutex::new(HashMap::new())), tx: Some(tx), loop_thread: Some(loop_thread), - mode_mapping, - permission_mapping, - rejected_tool_calls, - pending_confirmations: Arc::new(TokioMutex::new(HashMap::new())), - goose_to_acp_id: Arc::new(TokioMutex::new(HashMap::new())), - acp_to_goose_id: Arc::new(TokioMutex::new(HashMap::new())), - session_model: Arc::new(TokioMutex::new(HashMap::new())), - auth_methods, - supports_close, - init_session: OnceCell::new(), - } + }) } - pub fn auth_methods(&self) -> &[AuthMethod] { - &self.auth_methods + fn acp_session_id(&self) -> SessionId { + self.session.id.clone() } - pub async fn new_session(&self) -> Result { - let (response_tx, response_rx) = oneshot::channel(); - self.tx - .as_ref() - .unwrap() - .send(ClientRequest::NewSession { response_tx }) - .await - .context("ACP client is unavailable")?; - response_rx - .await - .context(format!("ACP {} cancelled", AGENT_METHOD_NAMES.session_new))? - } - - pub async fn list_sessions(&self) -> Result { - let (response_tx, response_rx) = oneshot::channel(); - self.tx - .as_ref() - .unwrap() - .send(ClientRequest::ListSessions { response_tx }) - .await - .context("ACP client is unavailable")?; - let raw = response_rx.await.context("ACP request cancelled")??; - let acp_to_goose = self.acp_to_goose_id.lock().await; - Ok(map_sessions_to_goose_ids(raw, &acp_to_goose)) - } - - async fn resolve_acp_session_id(&self, goose_id: &str) -> Result { - let map = self.goose_to_acp_id.lock().await; - map.get(goose_id) - .map(|r| r.session_id.clone()) - .ok_or_else(|| { - sacp::Error::resource_not_found(Some(goose_id.to_string())) - .data(format!("Session not found: {goose_id}")) - .into() - }) - } - - pub(crate) async fn send_set_mode(&self, goose_id: &str, mode_id: String) -> Result<()> { - let session_id = self.resolve_acp_session_id(goose_id).await?; + pub(crate) async fn send_set_mode(&self, _goose_id: &str, mode_id: String) -> Result<()> { + let session_id = self.acp_session_id(); let (response_tx, response_rx) = oneshot::channel(); self.tx .as_ref() @@ -336,29 +263,13 @@ impl AcpProvider { response_rx.await.context("ACP request cancelled")? } - pub(crate) async fn send_set_model(&self, goose_id: &str, model_id: String) -> Result<()> { - let session_id = self.resolve_acp_session_id(goose_id).await?; - let (response_tx, response_rx) = oneshot::channel(); - self.tx - .as_ref() - .unwrap() - .send(ClientRequest::SetModel { - session_id, - model_id, - response_tx, - }) - .await - .context("ACP client is unavailable")?; - response_rx.await.context("ACP request cancelled")? - } - pub(crate) async fn send_set_config_option( &self, - goose_id: &str, + _goose_id: &str, config_id: String, value: String, ) -> Result<()> { - let session_id = self.resolve_acp_session_id(goose_id).await?; + let session_id = self.acp_session_id(); let (response_tx, response_rx) = oneshot::channel(); self.tx .as_ref() @@ -374,110 +285,6 @@ impl AcpProvider { response_rx.await.context("ACP request cancelled")? } - // Only used by tests; session/delete has no typed request in agent-client-protocol-schema yet. - #[doc(hidden)] - pub async fn delete_session(&self, goose_id: &str) -> Result<()> { - let session_id = self.resolve_acp_session_id(goose_id).await?; - self.send_untyped( - "session/delete", - serde_json::json!({ "sessionId": session_id.0 }), - ) - .await?; - - // Clean up cached mappings so ensure_session doesn't return a stale entry. - self.goose_to_acp_id.lock().await.remove(goose_id); - self.acp_to_goose_id - .lock() - .await - .remove(session_id.0.as_ref()); - self.session_model.lock().await.remove(goose_id); - Ok(()) - } - - pub async fn send_untyped( - &self, - method: &str, - params: serde_json::Value, - ) -> Result { - let (response_tx, response_rx) = oneshot::channel(); - self.tx - .as_ref() - .unwrap() - .send(ClientRequest::Untyped { - method: method.to_string(), - params, - response_tx, - }) - .await - .context("ACP client is unavailable")?; - response_rx.await.context("ACP request cancelled")? - } - - pub async fn has_session(&self, goose_id: &str) -> bool { - self.goose_to_acp_id.lock().await.contains_key(goose_id) - } - - // If false, callers fall back to legacy set_mode/set_model. - async fn session_has_config_option( - &self, - goose_id: &str, - category: SessionConfigOptionCategory, - ) -> bool { - let map = self.goose_to_acp_id.lock().await; - map.get(goose_id) - .and_then(|r| r.config_options.as_ref()) - .is_some_and(|opts| opts.iter().any(|o| o.category.as_ref() == Some(&category))) - } - - pub async fn handle_permission_confirmation( - &self, - request_id: &str, - confirmation: &PermissionConfirmation, - ) -> bool { - let mut pending = self.pending_confirmations.lock().await; - if let Some(tx) = pending.remove(request_id) { - let _ = tx.send(confirmation.clone()); - return true; - } - false - } - - pub async fn ensure_session( - &self, - session_id: Option<&str>, - ) -> Result { - if let Some(session_id) = session_id { - if let Some(response) = self.goose_to_acp_id.lock().await.get(session_id) { - return Ok(response.clone()); - } - } - - let response = self.new_session().await.map_err(|e| { - ProviderError::RequestFailed(format!("Failed to create ACP session: {e}")) - })?; - - if let Some(session_id) = session_id { - self.goose_to_acp_id - .lock() - .await - .insert(session_id.to_string(), response.clone()); - self.acp_to_goose_id - .lock() - .await - .insert(response.session_id.0.to_string(), session_id.to_string()); - - // Initialize model tracking so stream() can detect changes. - let (current_model, _) = resolve_model_info(&self.name, &response)?; - self.session_model - .lock() - .await - .entry(session_id.to_string()) - .or_insert(current_model); - } - - Ok(response) - } - async fn prompt( &self, session_id: SessionId, @@ -497,31 +304,12 @@ impl AcpProvider { Ok(response_rx) } - async fn get_init_session(&self) -> Result<&NewSessionResponse> { - self.init_session - .get_or_try_init(|| async { - let response = self.new_session().await?; - if self.supports_close { - self.close_session_by_acp_id(response.session_id.clone()) - .await?; - } - Ok(response) - }) - .await - } - - async fn close_session_by_acp_id(&self, session_id: SessionId) -> Result<()> { - let (response_tx, response_rx) = oneshot::channel(); - self.tx + fn session_has_config_option(&self, category: SessionConfigOptionCategory) -> bool { + self.session + .response + .config_options .as_ref() - .unwrap() - .send(ClientRequest::CloseSession { - session_id, - response_tx, - }) - .await - .context("ACP client is unavailable")?; - response_rx.await.context("ACP request cancelled")? + .is_some_and(|opts| opts.iter().any(|o| o.category.as_ref() == Some(&category))) } } @@ -536,37 +324,25 @@ impl Provider for AcpProvider { } async fn update_mode(&self, session_id: &str, mode: GooseMode) -> Result<(), ProviderError> { - let map = self.goose_to_acp_id.lock().await; - if map.is_empty() { - // Pre-initialization: no ACP session yet, just store the mode. - // The shared Arc> is read at session creation time. - drop(map); - } else { - drop(map); - let mode_str = self.mode_mapping[&mode].clone(); - if self - .session_has_config_option(session_id, SessionConfigOptionCategory::Mode) + let mode_str = self + .mode_mapping + .get(&mode) + .cloned() + .unwrap_or_else(|| format!("{mode:?}")); + + if self.session_has_config_option(SessionConfigOptionCategory::Mode) { + self.send_set_config_option(session_id, "mode".into(), mode_str) .await - { - self.send_set_config_option(session_id, "mode".into(), mode_str) - .await - .map_err(|e| { - ProviderError::RequestFailed(format!("Failed to set mode: {e}")) - })?; - } else { - self.send_set_mode(session_id, mode_str) - .await - .map_err(|e| { - ProviderError::RequestFailed(format!("Failed to set mode: {e}")) - })?; - } + .map_err(|e| ProviderError::RequestFailed(format!("Failed to set mode: {e}")))?; + } else { + self.send_set_mode(session_id, mode_str) + .await + .map_err(|e| ProviderError::RequestFailed(format!("Failed to set mode: {e}")))?; } - let mut current = self - .goose_mode - .lock() - .map_err(|_| ProviderError::RequestFailed("Failed to update mode".into()))?; - *current = mode; + if let Ok(mut guard) = self.goose_mode.lock() { + *guard = mode; + } Ok(()) } @@ -574,76 +350,49 @@ impl Provider for AcpProvider { PermissionRouting::ActionRequired } + fn manages_own_context(&self) -> bool { + true + } + async fn handle_permission_confirmation( &self, request_id: &str, confirmation: &PermissionConfirmation, ) -> bool { - AcpProvider::handle_permission_confirmation(self, request_id, confirmation).await + let mut pending = self.pending_confirmations.lock().await; + if let Some(tx) = pending.remove(request_id) { + let _ = tx.send(confirmation.clone()); + return true; + } + false } async fn stream( &self, - model_config: &ModelConfig, - session_id: &str, + _model_config: &ModelConfig, + _session_id: &str, _system: &str, messages: &[Message], _tools: &[Tool], ) -> Result { - let response = self.ensure_session(Some(session_id)).await?; - - // Provider trait has no update_model β€” stream() is the only place to forward model changes. - { - let new_model = &model_config.model_name; - let tracked = self.session_model.lock().await.get(session_id).cloned(); - if tracked.as_deref() != Some(new_model) { - if self - .session_has_config_option(session_id, SessionConfigOptionCategory::Model) - .await - { - self.send_set_config_option(session_id, "model".into(), new_model.clone()) - .await - .map_err(|e| { - ProviderError::RequestFailed(format!("Failed to set model: {e}")) - })?; - } else { - self.send_set_model(session_id, new_model.clone()) - .await - .map_err(|e| { - ProviderError::RequestFailed(format!("Failed to set model: {e}")) - })?; - } - self.session_model - .lock() - .await - .insert(session_id.to_string(), new_model.clone()); - } - } + let session_id = self.acp_session_id(); let prompt_blocks = messages_to_prompt(messages); let mut rx = self - .prompt(response.session_id, prompt_blocks) + .prompt(session_id, prompt_blocks) .await .map_err(|e| ProviderError::RequestFailed(format!("Failed to send ACP prompt: {e}")))?; let pending_confirmations = self.pending_confirmations.clone(); - let rejected_tool_calls = self.rejected_tool_calls.clone(); - let permission_mapping = self.permission_mapping.clone(); let goose_mode = *self .goose_mode .lock() .map_err(|_| ProviderError::RequestFailed("goose_mode lock poisoned".into()))?; let reject_all_tools = goose_mode == GooseMode::Chat; - Ok(Box::pin(try_stream! { - // ACP agents execute tools internally. Goose never dispatches tool calls; - // it only sees text, thoughts, and permission requests from the agent. - // - // In Chat mode (reject_all_tools), we suppress all text after a tool - // starts because the agent may send tool results as AcpUpdate::Text, - // bypassing the permission response. let mut suppress_text = false; + let mut rejected_tool_calls: HashSet = HashSet::new(); while let Some(update) = rx.recv().await { match update { @@ -662,12 +411,11 @@ impl Provider for AcpProvider { AcpUpdate::ToolCallStart { id, .. } => { if reject_all_tools { suppress_text = true; - rejected_tool_calls.lock().await.insert(id); + rejected_tool_calls.insert(id); } } AcpUpdate::ToolCallComplete { id, .. } => { - let is_error = rejected_tool_calls.lock().await.remove(&id); - if is_error { + if rejected_tool_calls.remove(&id) { let message = Message::assistant().with_text("Tool call was denied."); yield (Some(message), None); } @@ -675,10 +423,9 @@ impl Provider for AcpProvider { AcpUpdate::PermissionRequest { request, response_tx } => { if let Some(decision) = permission_decision_from_mode(goose_mode) { if decision.should_record_rejection() { - rejected_tool_calls.lock().await.insert(request.tool_call.tool_call_id.0.to_string()); + rejected_tool_calls.insert(request.tool_call.tool_call_id.0.to_string()); } - let response = map_permission_response(&permission_mapping, &request, decision); - let _ = response_tx.send(response); + let _ = response_tx.send(map_permission_response(&request, decision)); continue; } @@ -703,10 +450,9 @@ impl Provider for AcpProvider { let decision = PermissionDecision::from(confirmation.permission); if decision.should_record_rejection() { - rejected_tool_calls.lock().await.insert(request.tool_call.tool_call_id.0.to_string()); + rejected_tool_calls.insert(request.tool_call.tool_call_id.0.to_string()); } - let response = map_permission_response(&permission_mapping, &request, decision); - let _ = response_tx.send(response); + let _ = response_tx.send(map_permission_response(&request, decision)); } AcpUpdate::Complete(_reason) => { break; @@ -720,17 +466,13 @@ impl Provider for AcpProvider { } async fn fetch_supported_models(&self) -> Result, ProviderError> { - let response = self.get_init_session().await.map_err(|e| { - ProviderError::RequestFailed(format!("Failed to create ACP session: {e}")) - })?; - let (_, available) = resolve_model_info(&self.name, response)?; + let (_, available) = resolve_model_info(&self.name, &self.session.response)?; Ok(available) } } impl Drop for AcpProvider { fn drop(&mut self) { - // Join OS thread so session/close completes before runtime exits (reqwest InnerClientHandle pattern). self.tx.take(); if let Some(h) = self.loop_thread.take() { if let Err(e) = h.join() { @@ -807,12 +549,11 @@ impl AcpClientLoop { { let prompt_response_tx = prompt_response_tx.clone(); let reverse_modes = reverse_modes.clone(); + let goose_mode = goose_mode.clone(); async move |notification: SessionNotification, _cx| { if let Some(ref cb) = notification_callback { cb(notification.clone()); } - // stream() reads goose_mode at call time, so it must - // reflect any prior set_mode before the next prompt. match ¬ification.update { SessionUpdate::CurrentModeUpdate(update) => { if let Some(mode) = resolve_mode( @@ -915,7 +656,7 @@ impl AcpClientLoop { sacp::on_receive_request!(), ) .connect_with(transport, async move |cx: ConnectionTo| { - handle_requests(config, cx, rx, prompt_response_tx, init_tx).await + handle_requests(config, goose_mode, cx, rx, prompt_response_tx, init_tx).await }) .await?; @@ -943,7 +684,6 @@ async fn spawn_acp_process(config: &AcpProviderConfig) -> Result { cmd.spawn().context("failed to spawn ACP process") } -// sacp panics on Err from connect_with handlers, so log send failures instead of ?. fn log_undelivered(result: Result<(), E>, method: &str) { if let Err(e) = result { tracing::debug!(method, error = ?e, "response not delivered"); @@ -952,6 +692,7 @@ fn log_undelivered(result: Result<(), E>, method: &str) { async fn handle_requests( config: AcpProviderConfig, + goose_mode: Arc>, cx: ConnectionTo, rx: &mut mpsc::Receiver, prompt_response_tx: Arc>>>, @@ -959,13 +700,16 @@ async fn handle_requests( ) -> Result<(), sacp::Error> { let mut init_tx = Some(init_tx); + let client_capabilities = ClientCapabilities::new(); let init_response: InitializeResponse = cx - .send_request(InitializeRequest::new(ProtocolVersion::LATEST)) + .send_request( + InitializeRequest::new(ProtocolVersion::LATEST) + .client_capabilities(client_capabilities), + ) .block_task() .await .map_err(|err| { let message = format!("ACP {} failed: {err}", AGENT_METHOD_NAMES.initialize); - // Attempt to send a specific error to the ctor waiting on init_rx; if let Some(tx) = init_tx.take() { let _ = tx.send(Err(anyhow::anyhow!(message.clone()))); } @@ -997,7 +741,7 @@ async fn handle_requests( let result = match session { Ok(session) => { session_ids.push(session.session_id.clone()); - apply_session_mode(&config, &cx, session).await + apply_session_mode(&config, &goose_mode, &cx, session).await } Err(err) => Err(anyhow::anyhow!( "ACP {} failed: {err}", @@ -1006,14 +750,6 @@ async fn handle_requests( }; log_undelivered(response_tx.send(result), AGENT_METHOD_NAMES.session_new); } - ClientRequest::ListSessions { response_tx } => { - let result: Result = cx - .send_request(ListSessionsRequest::new()) - .block_task() - .await - .map_err(anyhow::Error::from); - log_undelivered(response_tx.send(result), AGENT_METHOD_NAMES.session_list); - } ClientRequest::SetMode { session_id, mode_id, @@ -1030,22 +766,6 @@ async fn handle_requests( AGENT_METHOD_NAMES.session_set_mode, ); } - ClientRequest::SetModel { - session_id, - model_id, - response_tx, - } => { - let result: Result<()> = cx - .send_request(SetSessionModelRequest::new(session_id, model_id)) - .block_task() - .await - .map(|_| ()) - .map_err(anyhow::Error::from); - log_undelivered( - response_tx.send(result), - AGENT_METHOD_NAMES.session_set_model, - ); - } ClientRequest::SetConfigOption { session_id, config_id, @@ -1065,35 +785,6 @@ async fn handle_requests( AGENT_METHOD_NAMES.session_set_config_option, ); } - ClientRequest::CloseSession { - session_id, - response_tx, - } => { - let result: Result<()> = cx - .send_request(CloseSessionRequest::new(session_id.clone())) - .block_task() - .await - .map(|_| ()) - .map_err(anyhow::Error::from); - session_ids.retain(|s| s != &session_id); - log_undelivered(response_tx.send(result), AGENT_METHOD_NAMES.session_close); - } - ClientRequest::Untyped { - method, - params, - response_tx, - } => { - let result: Result = - match sacp::UntypedMessage::new(&method, params) { - Ok(msg) => cx - .send_request(msg) - .block_task() - .await - .map_err(anyhow::Error::from), - Err(e) => Err(anyhow::Error::from(e)), - }; - log_undelivered(response_tx.send(result), &method); - } ClientRequest::Prompt { session_id, content, @@ -1126,7 +817,6 @@ async fn handle_requests( } } - // After loop exits (channel closed by Drop): if supports_close { for session_id in session_ids { if let Err(e) = cx @@ -1144,10 +834,16 @@ async fn handle_requests( async fn apply_session_mode( config: &AcpProviderConfig, + goose_mode: &Arc>, cx: &ConnectionTo, session: NewSessionResponse, ) -> Result { - if let (Some(mode_id), Some(modes)) = (config.session_mode_id.clone(), session.modes.as_ref()) { + let current_mode = goose_mode.lock().ok().map(|mode| *mode); + let requested_mode_id = current_mode + .and_then(|mode| config.mode_mapping.get(&mode).cloned()) + .or_else(|| config.session_mode_id.clone()); + + if let (Some(mode_id), Some(modes)) = (requested_mode_id, session.modes.as_ref()) { if modes.current_mode_id.0.as_ref() != mode_id.as_str() { let available: Vec = modes .available_modes @@ -1325,32 +1021,45 @@ fn build_action_required_message(request: &RequestPermissionRequest) -> Option Option<(String, Vec)> { + let select = config_options.iter().find_map(|opt| { + if opt.category.as_ref() != Some(&SessionConfigOptionCategory::Model) { + return None; + } + match &opt.kind { + SessionConfigKind::Select(select) => Some(select), + _ => None, + } + })?; + + let current = select.current_value.0.to_string(); + let available = match &select.options { + SessionConfigSelectOptions::Ungrouped(options) => options + .iter() + .map(|option| option.value.0.to_string()) + .collect(), + SessionConfigSelectOptions::Grouped(groups) => groups + .iter() + .flat_map(|group| { + group + .options + .iter() + .map(|option| option.value.0.to_string()) + }) + .collect(), + _ => Vec::new(), + }; + Some((current, available)) +} + fn resolve_model_info( provider_name: &str, response: &NewSessionResponse, ) -> Result<(String, Vec), ProviderError> { if let Some(opts) = &response.config_options { - if let Some(sel) = opts.iter().find_map(|opt| { - if opt.category.as_ref() != Some(&SessionConfigOptionCategory::Model) { - return None; - } - match &opt.kind { - SessionConfigKind::Select(s) => Some(s), - _ => None, - } - }) { - let current = sel.current_value.0.to_string(); - let available = match &sel.options { - SessionConfigSelectOptions::Ungrouped(opts) => { - opts.iter().map(|o| o.value.0.to_string()).collect() - } - SessionConfigSelectOptions::Grouped(groups) => groups - .iter() - .flat_map(|g| g.options.iter().map(|o| o.value.0.to_string())) - .collect(), - _ => vec![], - }; + if let Some((current, available)) = extract_model_info_from_config_options(opts) { return Ok((current, available)); } } @@ -1379,8 +1088,6 @@ fn reverse_mode_mapping( reverse } -// When multiple GooseModes map to the same provider ID (e.g. codex "read-only"), -// prefer the current mode if it's among candidates. fn resolve_mode( reverse_modes: &HashMap>, mode_id: &str, @@ -1406,29 +1113,11 @@ fn permission_decision_from_mode(goose_mode: GooseMode) -> Option, -) -> ListSessionsResponse { - let sessions = response - .sessions - .into_iter() - .filter_map(|mut info| { - let goose_id = acp_to_goose.get(info.session_id.0.as_ref())?; - info.session_id = SessionId::new(goose_id.clone()); - Some(info) - }) - .collect(); - ListSessionsResponse::new(sessions) -} - #[cfg(test)] mod tests { use super::*; use crate::agents::extension::Envs; - use sacp::schema::{SessionConfigOption, SessionConfigSelectOption, SessionInfo}; + use sacp::schema::SessionConfigSelectOption; use test_case::test_case; #[test_case( @@ -1523,64 +1212,6 @@ mod tests { assert!(filtered.is_empty()); } - #[test_case( - ListSessionsResponse::new(vec![ - SessionInfo::new(SessionId::new("20260318_1"), "/Users/codefromthecrypt/oss/goose-2") - .title("Fix login bug".to_string()) - .updated_at("2026-03-18T07:02:42.549655Z".to_string()), - SessionInfo::new(SessionId::new("20260318_2"), "/tmp/test-acpx") - .title("Add caching layer".to_string()) - .updated_at("2026-03-18T07:05:01.123Z".to_string()), - ]), - HashMap::from([ - ("20260318_1".to_string(), "goose-session-1".to_string()), - ("20260318_2".to_string(), "goose-session-2".to_string()), - ]), - ListSessionsResponse::new(vec![ - SessionInfo::new(SessionId::new("goose-session-1"), "/Users/codefromthecrypt/oss/goose-2") - .title("Fix login bug".to_string()) - .updated_at("2026-03-18T07:02:42.549655Z".to_string()), - SessionInfo::new(SessionId::new("goose-session-2"), "/tmp/test-acpx") - .title("Add caching layer".to_string()) - .updated_at("2026-03-18T07:05:01.123Z".to_string()), - ]) - ; "all sessions mapped with all fields preserved" - )] - #[test_case( - ListSessionsResponse::new(vec![ - SessionInfo::new(SessionId::new("20260318_1"), "/Users/codefromthecrypt/oss/goose-2") - .title("Fix login bug".to_string()), - SessionInfo::new(SessionId::new("other-agent-session"), "/tmp/other") - .title("Not our session".to_string()), - ]), - HashMap::from([ - ("20260318_1".to_string(), "goose-session-1".to_string()), - ]), - ListSessionsResponse::new(vec![ - SessionInfo::new(SessionId::new("goose-session-1"), "/Users/codefromthecrypt/oss/goose-2") - .title("Fix login bug".to_string()), - ]) - ; "unmapped sessions filtered out" - )] - #[test_case( - ListSessionsResponse::new(vec![ - SessionInfo::new(SessionId::new("20260318_1"), "/Users/codefromthecrypt/oss/goose-2") - .title("ACP Session".to_string()) - .updated_at("2026-03-18T01:29:02.141700Z".to_string()), - ]), - HashMap::new(), - ListSessionsResponse::new(vec![]) - ; "empty map returns empty list" - )] - fn test_map_sessions_to_goose_ids( - response: ListSessionsResponse, - acp_to_goose: HashMap, - expected: ListSessionsResponse, - ) { - let result = map_sessions_to_goose_ids(response, &acp_to_goose); - assert_eq!(result, expected); - } - #[test_case(GooseMode::Auto => Some(PermissionDecision::AllowOnce) ; "auto allows")] #[test_case(GooseMode::Chat => Some(PermissionDecision::RejectOnce) ; "chat rejects")] #[test_case(GooseMode::Approve => None ; "approve defers")] @@ -1699,7 +1330,6 @@ mod tests { resolve_model_info("test", &response) } - // Codex mapping: read-only maps to both Approve and Chat. fn codex_reverse_modes() -> HashMap> { HashMap::from([ ("full-access".to_string(), vec![GooseMode::Auto]), @@ -1735,9 +1365,7 @@ mod tests { let reverse_modes = codex_reverse_modes(); let current = Arc::new(Mutex::new(current)); let result = resolve_mode(&reverse_modes, mode_id, ¤t); - // For the fallback case, just check we got *some* candidate (order is nondeterministic). if mode_id == "read-only" && expected == Some(GooseMode::Approve) { - // Current (Auto) not in candidates β€” any candidate is valid. assert!(result == Some(GooseMode::Approve) || result == Some(GooseMode::Chat)); } else { assert_eq!(result, expected); diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 06b1d00f21..57446f07d1 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -395,10 +395,12 @@ impl Agent { &self, response: &Message, tools: &[rmcp::model::Tool], + suppress_replayed_thinking: bool, ) -> ToolCategorizeResult { // Categorize tool requests - let (frontend_requests, remaining_requests, filtered_response) = - self.categorize_tool_requests(response, tools).await; + let (frontend_requests, remaining_requests, filtered_response) = self + .categorize_tool_requests(response, tools, suppress_replayed_thinking) + .await; ToolCategorizeResult { frontend_requests, @@ -759,6 +761,71 @@ impl Agent { Ok(()) } + /// Load multiple extensions in parallel, persisting state once at the end. + /// + /// Unlike `add_extension`, this avoids per-extension persistence and acquires + /// the container lock once upfront to prevent serialisation of the parallel futures. + pub async fn add_extensions_bulk( + self: &Arc, + extensions: Vec, + session_id: &str, + ) -> anyhow::Result> { + let working_dir = match self + .config + .session_manager + .get_session(session_id, false) + .await + { + Ok(session) => Some(session.working_dir), + Err(e) => { + warn!("Failed to get session for bulk load: {}", e); + None + } + }; + let container = self.container.lock().await.clone(); + + let extension_futures = extensions + .into_iter() + .map(|config| { + let ext_manager = Arc::clone(&self.extension_manager); + let working_dir = working_dir.clone(); + let container = container.clone(); + let sid = session_id.to_string(); + + async move { + let name = config.name().to_string(); + match ext_manager + .add_extension(config, working_dir, container.as_ref(), Some(&sid)) + .await + { + Ok(_) => ExtensionLoadResult { + name, + success: true, + error: None, + }, + Err(e) => { + let error_msg = e.to_string(); + warn!("Failed to load extension {}: {}", name, error_msg); + ExtensionLoadResult { + name, + success: false, + error: Some(error_msg), + } + } + } + } + }) + .collect::>(); + + let results = futures::future::join_all(extension_futures).await; + + if results.iter().any(|r| r.success) { + self.persist_extension_state(session_id).await?; + } + + Ok(results) + } + async fn add_extension_inner( &self, extension: ExtensionConfig, @@ -1224,6 +1291,11 @@ impl Agent { let mut did_recovery_compact_this_iteration = false; let mut exit_chat = false; + // Track whether this provider turn has already emitted visible + // thinking so a later tool-call chunk can suppress replayed + // reasoning without hiding final-only non-streaming thoughts. + let mut surfaced_thinking_in_turn = false; + while let Some(next) = stream.next().await { if is_token_cancelled(&cancel_token) || exit_chat { break; @@ -1242,7 +1314,23 @@ impl Agent { frontend_requests, remaining_requests, filtered_response, - } = self.categorize_tools(&response, &tools).await; + } = self + .categorize_tools( + &response, + &tools, + surfaced_thinking_in_turn, + ) + .await; + + surfaced_thinking_in_turn |= filtered_response.content.iter().any( + |content| { + matches!( + content, + MessageContent::Thinking(_) + | MessageContent::RedactedThinking(_) + ) + }, + ); yield AgentEvent::Message(filtered_response.clone()); tokio::task::yield_now().await; @@ -1490,6 +1578,7 @@ impl Agent { no_tools_called = false; } } + #[allow(unused_variables)] Err(ref provider_err @ ProviderError::ContextLengthExceeded(_)) => { #[cfg(feature = "telemetry")] crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); diff --git a/crates/goose/src/agents/builtin_skills/skills/goose_doc_guide.md b/crates/goose/src/agents/builtin_skills/skills/goose_doc_guide.md index 268d23cb51..62c8257b05 100644 --- a/crates/goose/src/agents/builtin_skills/skills/goose_doc_guide.md +++ b/crates/goose/src/agents/builtin_skills/skills/goose_doc_guide.md @@ -15,11 +15,11 @@ Do NOT use this skill for: ## Steps (COMPLETE ALL BEFORE RESPONDING) 1. **Fetch official docs** - - Fetch the doc map from `https://block.github.io/goose/goose-docs-map.md` + - Fetch the doc map from `https://goose-docs.ai/goose-docs-map.md` - Search the doc map for pages relevant to the user's topic and get the paths for these pages - Use the EXACT paths from the doc map. For example: - If doc map shows: `docs/guides/sessions/session-management.md` - - Fetch: `https://block.github.io/goose/docs/guides/sessions/session-management.md` + - Fetch: `https://goose-docs.ai/docs/guides/sessions/session-management.md` - Do NOT modify or guess paths. - **ONLY fetch paths that are explicitly listed in the doc map - do not guess or infer URLs** - Make multiple fetch calls in parallel and save to temp files @@ -53,4 +53,4 @@ Do NOT use this skill for: 5. **List documentation links** - Only include docs actually used - Remove `.md` suffix from URLs - - Example: If you fetched `https://block.github.io/goose/docs/guides/sessions/session-management.md`, list it as `https://block.github.io/goose/docs/guides/sessions/session-management` + - Example: If you fetched `https://goose-docs.ai/docs/guides/sessions/session-management.md`, list it as `https://goose-docs.ai/docs/guides/sessions/session-management` diff --git a/crates/goose/src/agents/execute_commands.rs b/crates/goose/src/agents/execute_commands.rs index 8efdeca366..96b55a0d94 100644 --- a/crates/goose/src/agents/execute_commands.rs +++ b/crates/goose/src/agents/execute_commands.rs @@ -135,7 +135,8 @@ impl Agent { } async fn handle_skills_command(&self, session_id: &str) -> Result> { - use super::platform_extensions::summon::{list_installed_sources, SourceKind}; + use super::platform_extensions::skills::list_installed_skills; + use super::platform_extensions::SourceKind; let working_dir = self .config @@ -144,7 +145,7 @@ impl Agent { .await .ok() .map(|s| s.working_dir); - let sources = list_installed_sources(working_dir.as_deref()); + let sources = list_installed_skills(working_dir.as_deref()); let skills: Vec<_> = sources .iter() .filter(|s| matches!(s.kind, SourceKind::Skill | SourceKind::BuiltinSkill)) diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 6dd055de52..6f80e976d1 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -6,7 +6,7 @@ use futures::{future, FutureExt}; use once_cell::sync::Lazy; use rmcp::service::{ClientInitializeError, ServiceError}; use rmcp::transport::streamable_http_client::{ - AuthRequiredError, StreamableHttpClientTransportConfig, StreamableHttpError, + StreamableHttpClientTransportConfig, StreamableHttpError, }; use rmcp::transport::{ ConfigureCommandExt, DynamicTransportError, StreamableHttpClientTransport, TokioChildProcess, @@ -58,6 +58,14 @@ static RE_ENV_BRACES: Lazy = static RE_ENV_SIMPLE: Lazy = Lazy::new(|| regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex")); +fn resolve_timeout(timeout: Option) -> u64 { + timeout.unwrap_or_else(|| { + Config::global() + .get_goose_default_extension_timeout() + .unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT) + }) +} + struct Extension { pub config: ExtensionConfig, /// Resolved config snapshot (with secrets from keyring substituted) @@ -275,7 +283,7 @@ async fn child_process_client( let client_result = McpClient::connect_with_container( transport, - Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)), + Duration::from_secs(resolve_timeout(*timeout)), provider, docker_container, client_name, @@ -296,25 +304,26 @@ async fn child_process_client( } } -fn extract_auth_error( - res: &Result, -) -> Option<&AuthRequiredError> { - match res { - Ok(_) => None, - Err(err) => match err { - ClientInitializeError::TransportError { - error: DynamicTransportError { error, .. }, - .. - } => error - .downcast_ref::>() - .and_then(|auth_error| match auth_error { - StreamableHttpError::AuthRequired(auth_required_error) => { - Some(auth_required_error) - } - _ => None, - }), - _ => None, - }, +/// Retry with OAuth for typed auth challenges and wrapped bare HTTP 401 responses. +fn should_attempt_oauth_fallback(res: &Result) -> bool { + let Err(ClientInitializeError::TransportError { + error: DynamicTransportError { error, .. }, + .. + }) = res + else { + return false; + }; + + if let Some(http_err) = error.downcast_ref::>() { + match http_err { + StreamableHttpError::AuthRequired(_) => true, + StreamableHttpError::UnexpectedServerResponse(body) => body.starts_with("HTTP 401"), + _ => false, + } + } else { + error + .to_string() + .contains("unexpected server response: HTTP 401") } } @@ -440,8 +449,7 @@ async fn create_streamable_http_client( }, ); - let timeout_duration = - Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)); + let timeout_duration = Duration::from_secs(resolve_timeout(timeout)); let client_res = McpClient::connect( transport, @@ -453,37 +461,39 @@ async fn create_streamable_http_client( ) .await; - if extract_auth_error(&client_res).is_some() { - let auth_manager = oauth_flow(&uri.to_string(), &name.to_string()) - .await - .map_err(|_| ExtensionError::SetupError("auth error".to_string()))?; - let mut auth_headers = HeaderMap::new(); - auth_headers.insert(reqwest::header::USER_AGENT, GOOSE_USER_AGENT); - let auth_http_client = reqwest::Client::builder() - .default_headers(auth_headers) - .build() - .map_err(|_| { - ExtensionError::ConfigError("could not construct http client".to_string()) - })?; - let auth_client = AuthClient::new(auth_http_client, auth_manager); - let transport = StreamableHttpClientTransport::with_client( - auth_client, - StreamableHttpClientTransportConfig { - uri: uri.into(), - ..Default::default() - }, - ); - Ok(Box::new( - McpClient::connect( - transport, - timeout_duration, - provider, - client_name, - capabilities, - roots_dir.to_path_buf(), - ) - .await?, - )) + if should_attempt_oauth_fallback(&client_res) { + match oauth_flow(&uri.to_string(), &name.to_string()).await { + Ok(auth_manager) => { + let mut auth_headers = HeaderMap::new(); + auth_headers.insert(reqwest::header::USER_AGENT, GOOSE_USER_AGENT); + let auth_http_client = reqwest::Client::builder() + .default_headers(auth_headers) + .build() + .map_err(|_| { + ExtensionError::ConfigError("could not construct http client".to_string()) + })?; + let auth_client = AuthClient::new(auth_http_client, auth_manager); + let transport = StreamableHttpClientTransport::with_client( + auth_client, + StreamableHttpClientTransportConfig { + uri: uri.into(), + ..Default::default() + }, + ); + Ok(Box::new( + McpClient::connect( + transport, + timeout_duration, + provider, + client_name, + capabilities, + roots_dir.to_path_buf(), + ) + .await?, + )) + } + Err(_) => Ok(Box::new(client_res?)), + } } else { Ok(Box::new(client_res?)) } @@ -633,7 +643,7 @@ impl ExtensionManager { (def.client_factory)(context) } else { // Builtin MCP server extension - let timeout_secs = timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT); + let timeout_secs = resolve_timeout(timeout); let extension_fn = get_builtin_extension(normalized_name.as_str()).ok_or_else(|| { ExtensionError::ConfigError(format!("Unknown extension: {}", name)) @@ -2305,4 +2315,43 @@ mod tests { "old extension must be preserved when replacement client creation fails" ); } + + fn transport_err(error: Box) -> ClientInitializeError { + ClientInitializeError::TransportError { + error: rmcp::transport::DynamicTransportError { + transport_name: "test".into(), + transport_type_id: std::any::TypeId::of::<()>(), + error, + }, + context: "test context".into(), + } + } + + fn streamable_err( + e: rmcp::transport::streamable_http_client::StreamableHttpError, + ) -> ClientInitializeError { + transport_err(Box::new(e)) + } + + #[test] + fn test_oauth_fallback_on_typed_auth_required() { + let err = streamable_err( + rmcp::transport::streamable_http_client::StreamableHttpError::AuthRequired( + rmcp::transport::streamable_http_client::AuthRequiredError { + www_authenticate_header: "Bearer realm=\"test\"".to_string(), + }, + ), + ); + assert!(should_attempt_oauth_fallback(&Err(err))); + } + + #[test] + fn test_oauth_fallback_on_unexpected_response_http_401_prefix() { + let err = streamable_err( + rmcp::transport::streamable_http_client::StreamableHttpError::UnexpectedServerResponse( + std::borrow::Cow::Borrowed("HTTP 401 Unauthorized"), + ), + ); + assert!(should_attempt_oauth_fallback(&Err(err))); + } } diff --git a/crates/goose/src/agents/platform_extensions/developer/shell.rs b/crates/goose/src/agents/platform_extensions/developer/shell.rs index a53d7d023e..8ac856eb45 100644 --- a/crates/goose/src/agents/platform_extensions/developer/shell.rs +++ b/crates/goose/src/agents/platform_extensions/developer/shell.rs @@ -14,6 +14,79 @@ use tokio_stream::{wrappers::SplitStream, StreamExt}; use crate::subprocess::SubprocessExt; +/// Check if the current process is running inside a Flatpak sandbox. +/// +/// When inside Flatpak, shell commands must be wrapped with `flatpak-spawn --host` +/// to execute on the host system rather than inside the sandbox. +#[cfg(not(windows))] +fn is_flatpak() -> bool { + std::path::Path::new("/.flatpak-info").exists() +} + +#[cfg(not(windows))] +const FLATPAK_HOST_ARGS: [&str; 2] = ["--host", "--watch-bus"]; + +#[cfg(not(windows))] +fn flatpak_spawn_command() -> tokio::process::Command { + let mut command = tokio::process::Command::new("flatpak-spawn"); + command.args(FLATPAK_HOST_ARGS); + command +} + +#[cfg(not(windows))] +fn flatpak_spawn_process() -> std::process::Command { + let mut command = std::process::Command::new("flatpak-spawn"); + command.args(FLATPAK_HOST_ARGS); + command +} + +/// Resolve the preferred Unix shell, respecting GOOSE_SHELL. +/// +/// Returns `(shell_path, is_user_configured)` β€” the boolean is true when +/// `GOOSE_SHELL` was explicitly set, which matters in Flatpak mode: an +/// explicit path is passed through as-is (the user likely intends a host +/// path), whereas auto-detected defaults are reduced to their basename so +/// the host's PATH resolves the correct binary. +/// +/// In Flatpak mode without an explicit GOOSE_SHELL, we skip sandbox +/// filesystem checks (e.g. `/bin/bash` existing inside the sandbox tells +/// us nothing about the host) and default to `"bash"` directly. +#[cfg(not(windows))] +fn unix_shell() -> (String, bool) { + match std::env::var("GOOSE_SHELL") { + Ok(shell) => (shell, true), + Err(_) => { + let shell = if is_flatpak() { + // Don't inspect sandbox paths β€” they don't reflect the host. + // Default to "bash" (ubiquitous on Flatpak-capable Linux hosts). + "bash".to_string() + } else if PathBuf::from("/bin/bash").is_file() { + "/bin/bash".to_string() + } else { + std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string()) + }; + (shell, false) + } + } +} + +/// Return the shell reference to pass to `flatpak-spawn --host`. +/// +/// If the user explicitly configured GOOSE_SHELL, honour the full path +/// (it likely refers to a host binary, e.g. a Nix-profile shell). +/// Otherwise strip to basename so the host's default PATH resolves it. +#[cfg(not(windows))] +fn flatpak_shell_arg(shell: &str, is_user_configured: bool) -> &str { + if is_user_configured { + shell + } else { + std::path::Path::new(shell) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("bash") + } +} + const OUTPUT_LIMIT_LINES: usize = 2000; pub const OUTPUT_LIMIT_BYTES: usize = 50_000; const OUTPUT_PREVIEW_LINES: usize = 50; @@ -69,21 +142,31 @@ pub struct ShellOutput { /// source the user's profile and recover the full PATH. #[cfg(not(windows))] fn resolve_login_shell_path() -> Option { - let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| { - if PathBuf::from("/bin/bash").is_file() { - "/bin/bash".to_string() - } else { - std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string()) - } - }); + let (shell, is_user_configured) = unix_shell(); - let mut child = std::process::Command::new(&shell) - .args(["-l", "-i", "-c", "echo $PATH"]) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .ok()?; + let mut child = if is_flatpak() { + flatpak_spawn_process() + .args([ + flatpak_shell_arg(&shell, is_user_configured), + "-l", + "-i", + "-c", + "echo $PATH", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .ok()? + } else { + std::process::Command::new(&shell) + .args(["-l", "-i", "-c", "echo $PATH"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .ok()? + }; let mut stdout = child.stdout.take()?; let (tx, rx) = std::sync::mpsc::channel(); @@ -309,14 +392,7 @@ async fn run_command( working_dir: Option<&std::path::Path>, login_path: Option<&str>, ) -> Result { - let mut command = build_shell_command(command_line); - if let Some(path) = working_dir { - command.current_dir(path); - } - - if let Some(path) = login_path { - command.env("PATH", path); - } + let mut command = build_shell_command(command_line, working_dir, login_path); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); @@ -401,7 +477,11 @@ async fn run_command( }) } -fn build_shell_command(command_line: &str) -> tokio::process::Command { +fn build_shell_command( + command_line: &str, + working_dir: Option<&std::path::Path>, + login_path: Option<&str>, +) -> tokio::process::Command { #[cfg(windows)] let mut command = { let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| "cmd".to_string()); @@ -423,21 +503,45 @@ fn build_shell_command(command_line: &str) -> tokio::process::Command { command.args(["-c", command_line]); } } + if let Some(path) = working_dir { + command.current_dir(path); + } + if let Some(path) = login_path { + command.env("PATH", path); + } command }; #[cfg(not(windows))] let mut command = { - let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| { - if PathBuf::from("/bin/bash").is_file() { - "/bin/bash".to_string() - } else { - std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string()) + let (shell, is_user_configured) = unix_shell(); + + if is_flatpak() { + let mut command = flatpak_spawn_command(); + if let Some(dir) = working_dir { + command.arg(format!("--directory={}", dir.display())); } - }); - let mut command = tokio::process::Command::new(shell); - command.arg("-c").arg(command_line); - command + if let Some(path) = login_path { + command.arg(format!("--env=PATH={}", path)); + } + // If GOOSE_SHELL was explicitly set, honour the full path (likely a host + // binary). Otherwise use basename so the host's PATH resolves it. + command + .arg(flatpak_shell_arg(&shell, is_user_configured)) + .arg("-c") + .arg(command_line); + command + } else { + let mut command = tokio::process::Command::new(shell); + command.arg("-c").arg(command_line); + if let Some(path) = working_dir { + command.current_dir(path); + } + if let Some(path) = login_path { + command.env("PATH", path); + } + command + } }; command.set_no_window(); diff --git a/crates/goose/src/agents/platform_extensions/mod.rs b/crates/goose/src/agents/platform_extensions/mod.rs index 5a337b4e3f..743552f421 100644 --- a/crates/goose/src/agents/platform_extensions/mod.rs +++ b/crates/goose/src/agents/platform_extensions/mod.rs @@ -6,16 +6,79 @@ pub mod code_execution; pub mod developer; pub mod ext_manager; pub mod orchestrator; +pub mod skills; pub mod summarize; pub mod summon; pub mod todo; pub mod tom; use std::collections::HashMap; +use std::path::PathBuf; use crate::agents::mcp_client::McpClientTrait; use crate::session::Session; use once_cell::sync::Lazy; +use serde::Deserialize; +use tracing::warn; + +#[derive(Debug, Clone)] +pub struct Source { + pub name: String, + pub kind: SourceKind, + pub description: String, + pub path: PathBuf, + pub content: String, + pub supporting_files: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SourceKind { + Subrecipe, + Recipe, + Skill, + Agent, + BuiltinSkill, +} + +impl std::fmt::Display for SourceKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SourceKind::Subrecipe => write!(f, "subrecipe"), + SourceKind::Recipe => write!(f, "recipe"), + SourceKind::Skill => write!(f, "skill"), + SourceKind::Agent => write!(f, "agent"), + SourceKind::BuiltinSkill => write!(f, "builtin skill"), + } + } +} + +impl Source { + pub fn to_load_text(&self) -> String { + format!( + "## {} ({})\n\n{}\n\n### Content\n\n{}", + self.name, self.kind, self.description, self.content + ) + } +} + +pub fn parse_frontmatter Deserialize<'de>>(content: &str) -> Option<(T, String)> { + let parts: Vec<&str> = content.split("---").collect(); + if parts.len() < 3 { + return None; + } + + let yaml_content = parts[1].trim(); + let metadata: T = match serde_yaml::from_str(yaml_content) { + Ok(m) => m, + Err(e) => { + warn!("Failed to parse frontmatter: {}", e); + return None; + } + }; + + let body = parts[2..].join("---").trim().to_string(); + Some((metadata, body)) +} pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; @@ -189,6 +252,19 @@ pub static PLATFORM_EXTENSIONS: Lazy }, ); + map.insert( + skills::EXTENSION_NAME, + PlatformExtensionDef { + name: skills::EXTENSION_NAME, + display_name: "Skills", + description: "Discover and provide skill instructions from filesystem and builtins", + default_enabled: true, + unprefixed_tools: false, + hidden: true, + client_factory: |ctx| Box::new(skills::SkillsClient::new(ctx).unwrap()), + }, + ); + map }, ); diff --git a/crates/goose/src/agents/platform_extensions/orchestrator.rs b/crates/goose/src/agents/platform_extensions/orchestrator.rs index b01b0cb614..4580034358 100644 --- a/crates/goose/src/agents/platform_extensions/orchestrator.rs +++ b/crates/goose/src/agents/platform_extensions/orchestrator.rs @@ -2,11 +2,13 @@ use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; use crate::agents::tool_execution::ToolCallContext; use crate::agents::{AgentEvent, SessionConfig}; -use crate::config::GooseMode; +use crate::config::{Config, ExtensionConfig, GooseMode}; use crate::context_mgmt::format_message_for_compacting; use crate::conversation::message::Message; use crate::execution::manager::AgentManager; +use crate::providers; use crate::providers::base::Provider; +use crate::session::extension_data::EnabledExtensionsState; use crate::session::session_manager::SessionType; use anyhow::Result; use async_trait::async_trait; @@ -139,6 +141,11 @@ impl OrchestratorClient { .ok_or_else(|| "Provider not available".to_string()) } + fn parent_extensions(&self) -> Vec { + let extension_data = self.context.session.as_ref().map(|s| &s.extension_data); + EnabledExtensionsState::extensions_or_default(extension_data, Config::global()) + } + async fn handle_list_sessions( &self, arguments: Option, @@ -397,8 +404,15 @@ impl OrchestratorClient { .await .map_err(|e| format!("Failed to create agent: {}", e))?; - // Inherit the orchestrator's provider and model - let provider = self.get_provider().await?; + let parent_provider = self.get_provider().await?; + let extensions = self.parent_extensions(); + let provider = providers::create( + parent_provider.get_name(), + parent_provider.get_model_config(), + extensions, + ) + .await + .map_err(|e| format!("Failed to create provider for new agent: {}", e))?; agent .update_provider(provider, &session.id) .await @@ -432,11 +446,20 @@ impl OrchestratorClient { .map_err(|e| format!("Failed to get agent for session '{}': {}", session_id, e))?; if agent.provider().await.is_err() { - if let Ok(provider) = self.get_provider().await { - agent - .update_provider(provider, &session_id) - .await - .map_err(|e| format!("Failed to set provider: {}", e))?; + if let Ok(parent_provider) = self.get_provider().await { + let extensions = self.parent_extensions(); + if let Ok(provider) = providers::create( + parent_provider.get_name(), + parent_provider.get_model_config(), + extensions, + ) + .await + { + agent + .update_provider(provider, &session_id) + .await + .map_err(|e| format!("Failed to set provider: {}", e))?; + } } } diff --git a/crates/goose/src/agents/platform_extensions/skills.rs b/crates/goose/src/agents/platform_extensions/skills.rs new file mode 100644 index 0000000000..650d716a56 --- /dev/null +++ b/crates/goose/src/agents/platform_extensions/skills.rs @@ -0,0 +1,410 @@ +use super::{parse_frontmatter, Source, SourceKind}; +use crate::agents::builtin_skills; +use crate::agents::extension::PlatformExtensionContext; +use crate::agents::mcp_client::{Error, McpClientTrait}; +use crate::agents::tool_execution::ToolCallContext; +use crate::config::paths::Paths; +use async_trait::async_trait; +use rmcp::model::{ + CallToolResult, Implementation, InitializeResult, JsonObject, ListToolsResult, + ServerCapabilities, ServerNotification, +}; +use serde::Deserialize; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::warn; + +pub static EXTENSION_NAME: &str = "skills"; + +#[derive(Debug, Deserialize)] +struct SkillMetadata { + name: String, + description: String, +} + +pub fn parse_skill_content(content: &str, path: PathBuf) -> Option { + let (metadata, body): (SkillMetadata, String) = parse_frontmatter(content)?; + + if metadata.name.contains('/') { + warn!( + "Skill name '{}' contains '/' which is not allowed, skipping", + metadata.name + ); + return None; + } + + Some(Source { + name: metadata.name, + kind: SourceKind::Skill, + description: metadata.description, + path, + content: body, + supporting_files: Vec::new(), + }) +} + +pub fn scan_skills_from_dir(dir: &Path, seen: &mut HashSet) -> Vec { + let mut sources = Vec::new(); + let mut visited_dirs = HashSet::new(); + for skill_file in collect_skill_files(dir, &mut visited_dirs) { + let Some(skill_dir) = skill_file.parent() else { + continue; + }; + let content = match std::fs::read_to_string(&skill_file) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read skill file {}: {}", skill_file.display(), e); + continue; + } + }; + + if let Some(mut source) = parse_skill_content(&content, skill_dir.to_path_buf()) { + if !seen.contains(&source.name) { + let mut visited_support_dirs = HashSet::new(); + source.supporting_files = + find_supporting_files(skill_dir, &mut visited_support_dirs); + seen.insert(source.name.clone()); + sources.push(source); + } + } + } + sources +} + +fn collect_skill_files(dir: &Path, visited_dirs: &mut HashSet) -> Vec { + let mut skill_files = Vec::new(); + + walk_files_recursively( + dir, + visited_dirs, + &mut |path| !should_skip_dir(path), + &mut |path| { + if path.file_name().and_then(|name| name.to_str()) == Some("SKILL.md") { + skill_files.push(path.to_path_buf()); + } + }, + ); + + skill_files +} + +fn should_skip_dir(path: &Path) -> bool { + matches!( + path.file_name().and_then(|name| name.to_str()), + Some(".git") | Some(".hg") | Some(".svn") + ) +} + +fn walk_files_recursively( + dir: &Path, + visited_dirs: &mut HashSet, + should_descend: &mut G, + visit_file: &mut F, +) where + F: FnMut(&Path), + G: FnMut(&Path) -> bool, +{ + let canonical_dir = match std::fs::canonicalize(dir) { + Ok(path) => path, + Err(_) => return, + }; + + if !visited_dirs.insert(canonical_dir) { + return; + } + + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + if should_descend(&path) { + walk_files_recursively(&path, visited_dirs, should_descend, visit_file); + } + } else if path.is_file() { + visit_file(&path); + } + } +} + +pub fn find_supporting_files( + directory: &Path, + visited_dirs: &mut HashSet, +) -> Vec { + let mut files = Vec::new(); + + walk_files_recursively( + directory, + visited_dirs, + &mut |path| !should_skip_dir(path) && !path.join("SKILL.md").is_file(), + &mut |path| { + let is_skill_md = path + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n == "SKILL.md") + .unwrap_or(false); + if !is_skill_md { + files.push(path.to_path_buf()); + } + }, + ); + + files +} + +fn skill_dirs(working_dir: &Path) -> (Vec, Vec) { + let home = dirs::home_dir(); + let config = Paths::config_dir(); + + let local = vec![ + working_dir.join(".goose/skills"), + working_dir.join(".claude/skills"), + working_dir.join(".agents/skills"), + ]; + + let global = [ + home.as_ref().map(|h| h.join(".agents/skills")), + Some(config.join("skills")), + home.as_ref().map(|h| h.join(".claude/skills")), + home.as_ref().map(|h| h.join(".config/agents/skills")), + ] + .into_iter() + .flatten() + .collect(); + + (local, global) +} + +pub fn discover_skills(working_dir: &Path) -> Vec { + let mut sources = Vec::new(); + let mut seen = HashSet::new(); + + let (local_dirs, global_dirs) = skill_dirs(working_dir); + + for dir in local_dirs { + sources.extend(scan_skills_from_dir(&dir, &mut seen)); + } + + for dir in global_dirs { + sources.extend(scan_skills_from_dir(&dir, &mut seen)); + } + + for content in builtin_skills::get_all() { + if let Some(source) = parse_skill_content(content, PathBuf::new()) { + if !seen.contains(&source.name) { + seen.insert(source.name.clone()); + sources.push(Source { + kind: SourceKind::BuiltinSkill, + ..source + }); + } + } + } + + sources +} + +pub fn list_installed_skills(working_dir: Option<&Path>) -> Vec { + let dir = working_dir + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); + discover_skills(&dir) +} + +fn build_skill_instructions(skills: &[&Source]) -> String { + let mut instructions = String::new(); + if !skills.is_empty() { + instructions.push_str( + "\n\nYou have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them:", + ); + for skill in skills { + instructions.push_str(&format!("\nβ€’ {} - {}", skill.name, skill.description)); + } + } + instructions +} + +pub struct SkillsClient { + info: InitializeResult, +} + +impl SkillsClient { + pub fn new(context: PlatformExtensionContext) -> anyhow::Result { + let instructions = if let Some(session) = &context.session { + let sources = discover_skills(&session.working_dir); + let mut skills: Vec<&Source> = sources + .iter() + .filter(|s| s.kind == SourceKind::Skill || s.kind == SourceKind::BuiltinSkill) + .collect(); + skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path))); + build_skill_instructions(&skills) + } else { + String::new() + }; + + let info = InitializeResult::new(ServerCapabilities::builder().build()) + .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Skills")) + .with_instructions(instructions); + + Ok(Self { info }) + } +} + +#[async_trait] +impl McpClientTrait for SkillsClient { + async fn list_tools( + &self, + _session_id: &str, + _next_cursor: Option, + _cancellation_token: CancellationToken, + ) -> Result { + Ok(ListToolsResult { + tools: vec![], + next_cursor: None, + meta: None, + }) + } + + async fn call_tool( + &self, + _ctx: &ToolCallContext, + name: &str, + _arguments: Option, + _cancellation_token: CancellationToken, + ) -> Result { + Ok(CallToolResult::error(vec![rmcp::model::Content::text( + format!("Error: Unknown tool: {}", name), + )])) + } + + fn get_info(&self) -> Option<&InitializeResult> { + Some(&self.info) + } + + async fn subscribe(&self) -> mpsc::Receiver { + let (_tx, rx) = mpsc::channel(1); + rx + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::sync::Arc; + use tempfile::TempDir; + + #[test] + fn test_parse_skill_content() { + let skill = "---\nname: test-skill\ndescription: A test skill\n---\nSkill body here."; + let source = parse_skill_content(skill, PathBuf::new()).unwrap(); + assert_eq!(source.name, "test-skill"); + assert_eq!(source.kind, SourceKind::Skill); + assert!(source.content.contains("Skill body")); + } + + #[test] + fn test_parse_skill_rejects_slash_in_name() { + let skill = "---\nname: bad/skill\ndescription: A skill\n---\nContent."; + assert!(parse_skill_content(skill, PathBuf::new()).is_none()); + } + + #[test] + fn test_parse_skill_rejects_invalid_frontmatter() { + assert!(parse_skill_content("no frontmatter", PathBuf::new()).is_none()); + assert!(parse_skill_content("---\nunclosed", PathBuf::new()).is_none()); + } + + #[test] + fn test_discover_skills_from_filesystem() { + let temp_dir = TempDir::new().unwrap(); + + let goose_skill = temp_dir.path().join(".goose/skills/my-skill"); + fs::create_dir_all(&goose_skill).unwrap(); + fs::write( + goose_skill.join("SKILL.md"), + "---\nname: my-skill\ndescription: goose version\n---\nContent", + ) + .unwrap(); + + let claude_skill = temp_dir.path().join(".claude/skills/my-skill"); + fs::create_dir_all(&claude_skill).unwrap(); + fs::write( + claude_skill.join("SKILL.md"), + "---\nname: my-skill\ndescription: claude version\n---\nContent", + ) + .unwrap(); + + let sources = discover_skills(temp_dir.path()); + let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); + assert_eq!(skill.description, "goose version"); + } + + #[test] + fn test_discover_skills_includes_builtins() { + let temp_dir = TempDir::new().unwrap(); + let sources = discover_skills(temp_dir.path()); + assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill)); + } + + #[test] + fn test_skill_supporting_files() { + let temp_dir = TempDir::new().unwrap(); + + let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); + fs::create_dir_all(skill_dir.join("templates/nested")).unwrap(); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: my-skill\ndescription: A skill\n---\nContent", + ) + .unwrap(); + fs::write(skill_dir.join("myscript.sh"), "#!/bin/bash\necho ok").unwrap(); + fs::write(skill_dir.join("templates/report.txt"), "template").unwrap(); + fs::write(skill_dir.join("templates/nested/checklist.txt"), "nested").unwrap(); + + let sources = discover_skills(temp_dir.path()); + let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); + assert_eq!(skill.supporting_files.len(), 3); + } + + #[test] + fn test_build_skill_instructions_empty() { + let empty: &[&Source] = &[]; + assert_eq!(build_skill_instructions(empty), ""); + } + + #[test] + fn test_build_skill_instructions_with_skills() { + let skill = Source { + name: "test".to_string(), + kind: SourceKind::Skill, + description: "A test skill".to_string(), + path: PathBuf::new(), + content: String::new(), + supporting_files: vec![], + }; + let instructions = build_skill_instructions(&[&skill]); + assert!(instructions.contains("test - A test skill")); + } + + #[tokio::test] + async fn test_skills_client_no_tools() { + let context = PlatformExtensionContext { + extension_manager: None, + session_manager: Arc::new(crate::session::SessionManager::instance()), + session: None, + }; + let client = SkillsClient::new(context).unwrap(); + let result = client + .list_tools("test", None, CancellationToken::new()) + .await + .unwrap(); + assert!(result.tools.is_empty()); + } +} diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index b32611b50d..3f642daee5 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1,10 +1,4 @@ -//! Summon Extension - Unified tooling for recipes, skills, and subagents -//! -//! Provides two tools: -//! - `load`: Inject knowledge into current context or discover available sources -//! - `delegate`: Run tasks in isolated subagents (sync or async) - -use crate::agents::builtin_skills; +use super::{parse_frontmatter, Source, SourceKind}; use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; use crate::agents::subagent_handler::{run_subagent_task, OnMessageCallback, SubagentRunParams}; @@ -26,7 +20,7 @@ use rmcp::model::{ ServerCapabilities, ServerNotification, Tool, }; use serde::Deserialize; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; @@ -39,54 +33,12 @@ use tracing::{info, warn}; pub static EXTENSION_NAME: &str = "summon"; -#[derive(Debug, Clone)] -pub struct Source { - pub name: String, - pub kind: SourceKind, - pub description: String, - pub path: PathBuf, - pub content: String, - pub supporting_files: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum SourceKind { - Subrecipe, - Recipe, - Skill, - Agent, - BuiltinSkill, -} - -impl std::fmt::Display for SourceKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SourceKind::Subrecipe => write!(f, "subrecipe"), - SourceKind::Recipe => write!(f, "recipe"), - SourceKind::Skill => write!(f, "skill"), - SourceKind::Agent => write!(f, "agent"), - SourceKind::BuiltinSkill => write!(f, "builtin skill"), - } - } -} - -impl Source { - /// Format the source content for loading into context - pub fn to_load_text(&self) -> String { - format!( - "## {} ({})\n\n{}\n\n### Content\n\n{}", - self.name, self.kind, self.description, self.content - ) - } -} - fn kind_plural(kind: SourceKind) -> &'static str { match kind { SourceKind::Subrecipe => "Subrecipes", SourceKind::Recipe => "Recipes", - SourceKind::Skill => "Skills", SourceKind::Agent => "Agents", - SourceKind::BuiltinSkill => "Builtin Skills", + _ => "Other", } } @@ -134,12 +86,6 @@ pub struct CompletedTask { pub duration: Duration, } -#[derive(Debug, Deserialize)] -struct SkillMetadata { - name: String, - description: String, -} - #[derive(Debug, Deserialize)] struct AgentMetadata { name: String, @@ -149,46 +95,6 @@ struct AgentMetadata { model: Option, } -fn parse_frontmatter Deserialize<'de>>(content: &str) -> Option<(T, String)> { - let parts: Vec<&str> = content.split("---").collect(); - if parts.len() < 3 { - return None; - } - - let yaml_content = parts[1].trim(); - let metadata: T = match serde_yaml::from_str(yaml_content) { - Ok(m) => m, - Err(e) => { - warn!("Failed to parse frontmatter: {}", e); - return None; - } - }; - - let body = parts[2..].join("---").trim().to_string(); - Some((metadata, body)) -} - -fn parse_skill_content(content: &str, path: PathBuf) -> Option { - let (metadata, body): (SkillMetadata, String) = parse_frontmatter(content)?; - - if metadata.name.contains('/') { - warn!( - "Skill name '{}' contains '/' which is not allowed, skipping", - metadata.name - ); - return None; - } - - Some(Source { - name: metadata.name, - kind: SourceKind::Skill, - description: metadata.description, - path, - content: body, - supporting_files: Vec::new(), - }) -} - fn parse_agent_content(content: &str, path: PathBuf) -> Option { let (metadata, body): (AgentMetadata, String) = parse_frontmatter(content)?; @@ -211,96 +117,6 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option { }) } -/// Scan a directory for skill subdirectories containing SKILL.md files. -/// Returns discovered skills, skipping any whose names are already in `seen`. -fn scan_skills_from_dir(dir: &Path, seen: &mut std::collections::HashSet) -> Vec { - let mut sources = Vec::new(); - let mut visited_dirs = HashSet::new(); - for skill_file in collect_skill_files(dir, &mut visited_dirs) { - let Some(skill_dir) = skill_file.parent() else { - continue; - }; - let content = match std::fs::read_to_string(&skill_file) { - Ok(c) => c, - Err(e) => { - warn!("Failed to read skill file {}: {}", skill_file.display(), e); - continue; - } - }; - - if let Some(mut source) = parse_skill_content(&content, skill_dir.to_path_buf()) { - if !seen.contains(&source.name) { - let mut visited_support_dirs = HashSet::new(); - source.supporting_files = - find_supporting_files(skill_dir, &mut visited_support_dirs); - seen.insert(source.name.clone()); - sources.push(source); - } - } - } - sources -} - -fn collect_skill_files(dir: &Path, visited_dirs: &mut HashSet) -> Vec { - let mut skill_files = Vec::new(); - - walk_files_recursively( - dir, - visited_dirs, - &mut |path| !should_skip_skill_walk_dir(path), - &mut |path| { - if path.file_name().and_then(|name| name.to_str()) == Some("SKILL.md") { - skill_files.push(path.to_path_buf()); - } - }, - ); - - skill_files -} - -fn should_skip_skill_walk_dir(path: &Path) -> bool { - matches!( - path.file_name().and_then(|name| name.to_str()), - Some(".git") | Some(".hg") | Some(".svn") - ) -} - -fn walk_files_recursively( - dir: &Path, - visited_dirs: &mut HashSet, - should_descend: &mut G, - visit_file: &mut F, -) where - F: FnMut(&Path), - G: FnMut(&Path) -> bool, -{ - let canonical_dir = match std::fs::canonicalize(dir) { - Ok(path) => path, - Err(_) => return, - }; - - if !visited_dirs.insert(canonical_dir) { - return; - } - - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let path = entry.path(); - - if path.is_dir() { - if should_descend(&path) { - walk_files_recursively(&path, visited_dirs, should_descend, visit_file); - } - } else if path.is_file() { - visit_file(&path); - } - } -} - fn scan_recipes_from_dir( dir: &Path, kind: SourceKind, @@ -390,16 +206,6 @@ fn scan_agents_from_dir( } } -/// Returns all discovered sources (skills, recipes, agents) from the given working directory. -/// If no directory is provided, falls back to `std::env::current_dir()`. -/// This is useful for listing what's available without needing a full SummonClient instance. -pub fn list_installed_sources(working_dir: Option<&Path>) -> Vec { - let dir = working_dir - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); - discover_filesystem_sources(&dir) -} - fn discover_filesystem_sources(working_dir: &Path) -> Vec { let mut sources: Vec = Vec::new(); let mut seen: std::collections::HashSet = std::collections::HashSet::new(); @@ -430,22 +236,6 @@ fn discover_filesystem_sources(working_dir: &Path) -> Vec { ) .collect(); - let local_skill_dirs: Vec = vec![ - working_dir.join(".goose/skills"), - working_dir.join(".claude/skills"), - working_dir.join(".agents/skills"), - ]; - - let global_skill_dirs: Vec = [ - home.as_ref().map(|h| h.join(".agents/skills")), - Some(config.join("skills")), - home.as_ref().map(|h| h.join(".claude/skills")), - home.as_ref().map(|h| h.join(".config/agents/skills")), - ] - .into_iter() - .flatten() - .collect(); - let local_agent_dirs: Vec = vec![ working_dir.join(".goose/agents"), working_dir.join(".claude/agents"), @@ -465,10 +255,6 @@ fn discover_filesystem_sources(working_dir: &Path) -> Vec { scan_recipes_from_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); } - for dir in local_skill_dirs { - sources.extend(scan_skills_from_dir(&dir, &mut seen)); - } - for dir in local_agent_dirs { scan_agents_from_dir(&dir, &mut sources, &mut seen); } @@ -477,52 +263,13 @@ fn discover_filesystem_sources(working_dir: &Path) -> Vec { scan_recipes_from_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); } - for dir in global_skill_dirs { - sources.extend(scan_skills_from_dir(&dir, &mut seen)); - } - for dir in global_agent_dirs { scan_agents_from_dir(&dir, &mut sources, &mut seen); } - for content in builtin_skills::get_all() { - if let Some(source) = parse_skill_content(content, PathBuf::new()) { - if !seen.contains(&source.name) { - seen.insert(source.name.clone()); - sources.push(Source { - kind: SourceKind::BuiltinSkill, - ..source - }); - } - } - } - sources } -/// Collect all files in a skill directory recursively, excluding SKILL.md itself. -fn find_supporting_files(directory: &Path, visited_dirs: &mut HashSet) -> Vec { - let mut files = Vec::new(); - - walk_files_recursively( - directory, - visited_dirs, - &mut |path| !should_skip_skill_walk_dir(path) && !path.join("SKILL.md").is_file(), - &mut |path| { - let is_skill_md = path - .file_name() - .and_then(|n| n.to_str()) - .map(|n| n == "SKILL.md") - .unwrap_or(false); - if !is_skill_md { - files.push(path.to_path_buf()); - } - }, - ); - - files -} - fn round_duration(d: Duration) -> String { let secs = d.as_secs(); if secs < 60 { @@ -573,31 +320,8 @@ impl Drop for SummonClient { impl SummonClient { pub fn new(context: PlatformExtensionContext) -> Result { - let instructions = if let Some(session) = &context.session { - let mut instructions = "".to_string(); - let sources = discover_filesystem_sources(&session.working_dir); - - let mut skills: Vec<&Source> = sources - .iter() - .filter(|s| s.kind == SourceKind::Skill || s.kind == SourceKind::BuiltinSkill) - .collect(); - - skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path))); - - if !skills.is_empty() { - instructions.push_str("\n\nYou have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them:"); - for skill in &skills { - instructions.push_str(&format!("\nβ€’ {} - {}", skill.name, skill.description)); - } - } - Some(instructions) - } else { - None - }; - let info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) - .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Summon")) - .with_instructions(instructions.unwrap_or_default()); + .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Summon")); Ok(Self { info, @@ -650,13 +374,13 @@ impl SummonClient { Tool::new( "load", "Load knowledge into your current context or discover available sources.\n\n\ - Call with no arguments to list all available sources (subrecipes, recipes, skills, agents).\n\ + Call with no arguments to list all available sources (subrecipes, recipes, agents).\n\ Call with a source name to load its content into your context.\n\ For background tasks: load(source: \"task_id\") waits for the task and returns the result.\n\ To cancel a running task: load(source: \"task_id\", cancel: true) stops and returns output.\n\n\ Examples:\n\ - load() β†’ Lists available sources\n\ - - load(source: \"rust-patterns\") β†’ Loads the rust-patterns skill\n\ + - load(source: \"deploy\") β†’ Loads the deploy recipe\n\ - load(source: \"20260219_1\") β†’ Waits for background task, then returns result" .to_string(), schema.as_object().unwrap().clone(), @@ -673,7 +397,7 @@ impl SummonClient { }, "source": { "type": "string", - "description": "Name of a recipe, skill, or agent to run." + "description": "Name of a recipe or agent to run." }, "parameters": { "type": "object", @@ -715,8 +439,8 @@ impl SummonClient { "Delegate a task to a subagent that runs independently with its own context.\n\n\ Modes:\n\ 1. Ad-hoc: Provide `instructions` for a custom task\n\ - 2. Source-based: Provide `source` name to run a subrecipe, recipe, skill, or agent\n\ - 3. Combined: Pair a source with a task (e.g., source: \"rust-patterns\", instructions: \"review auth.rs\")\n\n\ + 2. Source-based: Provide `source` name to run a subrecipe, recipe, or agent\n\ + 3. Combined: Pair a source with a task (e.g., source: \"deploy\", instructions: \"deploy to staging\")\n\n\ Effective Delegation:\n\ - Delegates know only instructions + source content\n\ - Delegates cannot coordinate. Same-file work = conflicts.\n\ @@ -786,47 +510,6 @@ impl SummonClient { return Ok(Some(source)); } - if let Some((skill_name, raw_relative_path)) = name.split_once('/') { - let relative_path = raw_relative_path.replace('\\', "/"); - if let Some(skill) = sources.iter().find(|s| { - s.name == skill_name - && matches!(s.kind, SourceKind::Skill | SourceKind::BuiltinSkill) - }) { - let canonical_skill_dir = skill - .path - .canonicalize() - .unwrap_or_else(|_| skill.path.clone()); - - for file_path in &skill.supporting_files { - if let Ok(rel) = file_path.strip_prefix(&skill.path) { - let rel_normalized = rel.to_string_lossy().replace('\\', "/"); - if rel_normalized == relative_path { - let canonical_file = file_path - .canonicalize() - .map_err(|e| format!("Failed to resolve '{}': {}", name, e))?; - if !canonical_file.starts_with(&canonical_skill_dir) { - return Err(format!( - "Refusing to load '{}': file resolves outside the skill directory", - name - )); - } - return match std::fs::read_to_string(&canonical_file) { - Ok(content) => Ok(Some(Source { - name: name.to_string(), - kind: SourceKind::Skill, - description: format!("Supporting file for {}", skill_name), - path: file_path.clone(), - content, - supporting_files: vec![], - })), - Err(e) => Err(format!("Failed to read '{}': {}", name, e)), - }; - } - } - } - } - } - Ok(None) } @@ -1119,10 +802,9 @@ impl SummonClient { "No sources available for load/delegate.\n\n\ Sources are discovered from:\n\ β€’ Current recipe's sub_recipes\n\ - β€’ .agents/skills/, .agents/recipes/, .agents/agents/ (project-level)\n\ - β€’ ~/.agents/skills/, ~/.agents/agents/ (global)\n\ - β€’ GOOSE_RECIPE_PATH directories\n\ - β€’ Builtin skills", + β€’ .agents/recipes/, .agents/agents/ (project-level)\n\ + β€’ ~/.agents/agents/ (global)\n\ + β€’ GOOSE_RECIPE_PATH directories", )]); } @@ -1145,13 +827,7 @@ impl SummonClient { } } - for kind in [ - SourceKind::Subrecipe, - SourceKind::Recipe, - SourceKind::Skill, - SourceKind::Agent, - SourceKind::BuiltinSkill, - ] { + for kind in [SourceKind::Subrecipe, SourceKind::Recipe, SourceKind::Agent] { let kind_sources: Vec<_> = sources.iter().filter(|s| s.kind == kind).collect(); if !kind_sources.is_empty() { output.push_str(&format!("\n{}:\n", kind_plural(kind))); @@ -1183,72 +859,16 @@ impl SummonClient { Some(source) => { let content = source.to_load_text(); - let mut output = format!( - "# Loaded: {} ({})\n\n{}\n", + let output = format!( + "# Loaded: {} ({})\n\n{}\n\n---\nThis knowledge is now available in your context.", source.name, source.kind, content ); - if !source.supporting_files.is_empty() { - output.push_str(&format!( - "\n## Supporting Files\n\nSkill directory: {}\n\nThe following supporting files are available:\n", - source.path.display() - )); - for file in &source.supporting_files { - if let Ok(relative) = file.strip_prefix(&source.path) { - let rel_str = relative.to_string_lossy().replace('\\', "/"); - output.push_str(&format!( - "- {} β†’ load(source: \"{}/{}\")\n", - rel_str, source.name, rel_str - )); - } - } - output.push_str( - "\nUse load(source: \"/\") to load individual files into context, or use file tools to read/run them directly.\n", - ); - } - - output.push_str("\n---\nThis knowledge is now available in your context."); - Ok(vec![Content::text(output)]) } None => { let sources = self.get_sources(session_id, working_dir).await; - if let Some((skill_name, _)) = name.split_once('/') { - if let Some(skill) = sources.iter().find(|s| { - s.name == skill_name - && matches!(s.kind, SourceKind::Skill | SourceKind::BuiltinSkill) - }) { - let available: Vec = skill - .supporting_files - .iter() - .filter_map(|f| { - f.strip_prefix(&skill.path) - .ok() - .map(|r| r.to_string_lossy().replace('\\', "/")) - }) - .collect(); - if !available.is_empty() { - let total = available.len(); - let display: Vec<_> = available.into_iter().take(10).collect(); - let suffix = if total > 10 { - format!(" (and {} more)", total - 10) - } else { - String::new() - }; - return Err(format!( - "Source '{}' not found. Available files for {}: {}{}", - name, - skill_name, - display.join(", "), - suffix - )); - } else { - return Err(format!("Skill '{}' has no supporting files.", skill_name)); - } - } - } - let suggestions: Vec<&str> = sources .iter() .filter(|s| { @@ -1446,24 +1066,18 @@ impl SummonClient { .await? .ok_or_else(|| format!("Source '{}' not found", source_name))?; - if source_name.contains('/') - && matches!(source.kind, SourceKind::Skill | SourceKind::BuiltinSkill) - { - return Err(format!( - "Cannot delegate to supporting file '{}'. Use load() to read it instead.", - source_name - )); - } - let mut recipe = match source.kind { SourceKind::Recipe | SourceKind::Subrecipe => { self.build_recipe_from_source(&source, params, session_id) .await? } - SourceKind::Skill | SourceKind::BuiltinSkill => { - self.build_recipe_from_skill(&source, params)? - } SourceKind::Agent => self.build_recipe_from_agent(&source, params)?, + _ => { + return Err(format!( + "Source '{}' has kind '{}' which cannot be delegated from summon", + source_name, source.kind + )) + } }; if let Some(extra_instructions) = ¶ms.instructions { @@ -1556,26 +1170,6 @@ impl SummonClient { .map_err(|e| format!("Failed to build recipe: {}", e)) } - fn build_recipe_from_skill( - &self, - source: &Source, - params: &DelegateParams, - ) -> Result { - let mut builder = Recipe::builder() - .version("1.0.0") - .title(format!("Skill: {}", source.name)) - .description(source.description.clone()) - .instructions(&source.content); - - if params.instructions.is_none() { - builder = builder.prompt("Apply the skill knowledge to produce a useful result."); - } - - builder - .build() - .map_err(|e| format!("Failed to build recipe from skill: {}", e)) - } - fn build_recipe_from_agent( &self, source: &Source, @@ -2066,17 +1660,7 @@ mod tests { } #[test] - fn test_frontmatter_parsing() { - let skill = r#"--- -name: test-skill -description: A test skill ---- -Skill body here."#; - let source = parse_skill_content(skill, PathBuf::new()).unwrap(); - assert_eq!(source.name, "test-skill"); - assert_eq!(source.kind, SourceKind::Skill); - assert!(source.content.contains("Skill body")); - + fn test_agent_frontmatter_parsing() { let agent = r#"--- name: reviewer model: sonnet @@ -2085,465 +1669,155 @@ You review code."#; let source = parse_agent_content(agent, PathBuf::new()).unwrap(); assert_eq!(source.name, "reviewer"); assert!(source.description.contains("sonnet")); - - assert!(parse_skill_content("no frontmatter", PathBuf::new()).is_none()); - assert!(parse_skill_content("---\nunclosed", PathBuf::new()).is_none()); } #[tokio::test] - async fn test_source_discovery_and_priority() { + async fn test_discover_recipes_and_agents() { let temp_dir = TempDir::new().unwrap(); - let goose_skill = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(&goose_skill).unwrap(); - fs::write( - goose_skill.join("SKILL.md"), - "---\nname: my-skill\ndescription: goose version\n---\nContent", - ) - .unwrap(); - - let claude_skill = temp_dir.path().join(".claude/skills/my-skill"); - fs::create_dir_all(&claude_skill).unwrap(); - fs::write( - claude_skill.join("SKILL.md"), - "---\nname: my-skill\ndescription: claude version\n---\nContent", - ) - .unwrap(); - let recipes = temp_dir.path().join(".goose/recipes"); fs::create_dir_all(&recipes).unwrap(); fs::write( - recipes.join("test.yaml"), - "title: Test\ndescription: A recipe\ninstructions: Do it", + recipes.join("deploy.yaml"), + "title: Deploy\ndescription: Deploy to production\ninstructions: Run deploy steps", + ) + .unwrap(); + + let agents = temp_dir.path().join(".goose/agents"); + fs::create_dir_all(&agents).unwrap(); + fs::write( + agents.join("reviewer.md"), + "---\nname: reviewer\nmodel: sonnet\ndescription: Code reviewer\n---\nYou review code.", ) .unwrap(); let client = SummonClient::new(create_test_context()).unwrap(); let sources = client.discover_filesystem_sources(temp_dir.path()); - let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); - assert_eq!(skill.description, "goose version"); - - assert!(sources + let recipe = sources .iter() - .any(|s| s.name == "test" && s.kind == SourceKind::Recipe)); + .find(|s| s.name == "deploy" && s.kind == SourceKind::Recipe) + .unwrap(); + assert_eq!(recipe.description, "Deploy to production"); + assert_eq!(recipe.content, "Run deploy steps"); - assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill)); - } - - #[tokio::test] - async fn test_skill_supporting_files_discovered() { - let temp_dir = TempDir::new().unwrap(); - - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(skill_dir.join("templates/nested")).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill with scripts\n---\nRun check_all.sh", - ) - .unwrap(); - fs::write(skill_dir.join("myscript.sh"), "#!/bin/bash\necho ok").unwrap(); - fs::write(skill_dir.join("templates/report.txt"), "template content").unwrap(); - fs::write( - skill_dir.join("templates/nested/checklist.txt"), - "nested template content", - ) - .unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.discover_filesystem_sources(temp_dir.path()); - - let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); - assert_eq!(skill.path, skill_dir); - assert_eq!(skill.supporting_files.len(), 3); - - let file_names: Vec = skill - .supporting_files + let agent = sources .iter() - .filter_map(|f| f.file_name().map(|n| n.to_string_lossy().to_string())) - .collect(); - assert!(file_names.contains(&"myscript.sh".to_string())); - assert!(file_names.contains(&"report.txt".to_string())); - assert!(file_names.contains(&"checklist.txt".to_string())); + .find(|s| s.name == "reviewer" && s.kind == SourceKind::Agent) + .unwrap(); + assert_eq!(agent.description, "Code reviewer"); + assert!(agent.content.contains("You review code")); } #[tokio::test] - async fn test_nested_claude_catalog_skills_discovered() { + async fn test_recipe_deduplication_local_wins() { let temp_dir = TempDir::new().unwrap(); - let root_skill_file = temp_dir.path().join(".claude/skills/SKILL.md"); - fs::create_dir_all(root_skill_file.parent().unwrap()).unwrap(); + let local = temp_dir.path().join(".goose/recipes"); + fs::create_dir_all(&local).unwrap(); fs::write( - &root_skill_file, - "---\nname: root-skill\ndescription: Root level skill\n---\nRoot content", + local.join("deploy.yaml"), + "title: Deploy\ndescription: Local deploy\ninstructions: local steps", ) .unwrap(); - let nested_skill_dir = temp_dir.path().join(".claude/skills/catalog/internal/ai"); - fs::create_dir_all(&nested_skill_dir).unwrap(); + let also_local = temp_dir.path().join(".agents/recipes"); + fs::create_dir_all(&also_local).unwrap(); fs::write( - nested_skill_dir.join("SKILL.md"), - "---\nname: nested-skill\ndescription: Nested catalog skill\n---\nNested content", + also_local.join("deploy.yaml"), + "title: Deploy\ndescription: Agents deploy\ninstructions: agents steps", ) .unwrap(); let client = SummonClient::new(create_test_context()).unwrap(); let sources = client.discover_filesystem_sources(temp_dir.path()); - let root_skill = sources.iter().find(|s| s.name == "root-skill").unwrap(); - assert_eq!(root_skill.path, temp_dir.path().join(".claude/skills")); - - let nested_skill = sources.iter().find(|s| s.name == "nested-skill").unwrap(); - assert_eq!(nested_skill.path, nested_skill_dir); + let deploys: Vec<_> = sources.iter().filter(|s| s.name == "deploy").collect(); + assert_eq!(deploys.len(), 1); } #[tokio::test] - async fn test_root_skill_supporting_files_exclude_nested_skill_subtrees() { + async fn test_load_recipe_source() { let temp_dir = TempDir::new().unwrap(); - let root_skill_dir = temp_dir.path().join(".claude/skills"); - fs::create_dir_all(&root_skill_dir).unwrap(); + let recipes = temp_dir.path().join(".goose/recipes"); + fs::create_dir_all(&recipes).unwrap(); fs::write( - root_skill_dir.join("SKILL.md"), - "---\nname: root-skill\ndescription: Root level skill\n---\nRoot content", + recipes.join("deploy.yaml"), + "title: Deploy\ndescription: Deploy to production\ninstructions: Run deploy steps", ) .unwrap(); - fs::write(root_skill_dir.join("README.md"), "root readme").unwrap(); - - let nested_skill_dir = root_skill_dir.join("catalog/internal/ai"); - fs::create_dir_all(&nested_skill_dir).unwrap(); - fs::write( - nested_skill_dir.join("SKILL.md"), - "---\nname: nested-skill\ndescription: Nested catalog skill\n---\nNested content", - ) - .unwrap(); - fs::write(nested_skill_dir.join("notes.md"), "nested notes").unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.discover_filesystem_sources(temp_dir.path()); - - let root_skill = sources.iter().find(|s| s.name == "root-skill").unwrap(); - assert!(root_skill - .supporting_files - .contains(&root_skill_dir.join("README.md"))); - assert!(!root_skill - .supporting_files - .contains(&nested_skill_dir.join("SKILL.md"))); - assert!(!root_skill - .supporting_files - .contains(&nested_skill_dir.join("notes.md"))); - } - - #[tokio::test] - async fn test_skill_discovery_preserves_dot_prefixed_paths() { - let temp_dir = TempDir::new().unwrap(); - - let dot_skill_dir = temp_dir.path().join(".claude/skills/.team"); - fs::create_dir_all(&dot_skill_dir).unwrap(); - fs::write( - dot_skill_dir.join("SKILL.md"), - "---\nname: team-skill\ndescription: Dot skill\n---\nTeam content", - ) - .unwrap(); - fs::write(dot_skill_dir.join(".env.example"), "EXAMPLE=1").unwrap(); - - let git_skill_dir = temp_dir.path().join(".claude/skills/.git/hidden-skill"); - fs::create_dir_all(&git_skill_dir).unwrap(); - fs::write( - git_skill_dir.join("SKILL.md"), - "---\nname: hidden-git-skill\ndescription: Hidden git skill\n---\nHidden content", - ) - .unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.discover_filesystem_sources(temp_dir.path()); - - let dot_skill = sources.iter().find(|s| s.name == "team-skill").unwrap(); - assert_eq!(dot_skill.path, dot_skill_dir); - assert!(dot_skill - .supporting_files - .contains(&dot_skill_dir.join(".env.example"))); - assert!(!sources.iter().any(|s| s.name == "hidden-git-skill")); - } - - #[cfg(unix)] - #[tokio::test] - async fn test_symlinked_skill_directory_is_discovered() { - let temp_dir = TempDir::new().unwrap(); - - let shared_skill_dir = temp_dir.path().join("shared-skills/ai"); - fs::create_dir_all(&shared_skill_dir).unwrap(); - fs::write( - shared_skill_dir.join("SKILL.md"), - "---\nname: shared-ai\ndescription: Shared skill\n---\nShared content", - ) - .unwrap(); - fs::write(shared_skill_dir.join("notes.md"), "shared notes").unwrap(); - - let linked_catalog_dir = temp_dir.path().join(".claude/skills/catalog/internal"); - fs::create_dir_all(&linked_catalog_dir).unwrap(); - std::os::unix::fs::symlink(&shared_skill_dir, linked_catalog_dir.join("ai")).unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.discover_filesystem_sources(temp_dir.path()); - - let skill = sources.iter().find(|s| s.name == "shared-ai").unwrap(); - assert_eq!(skill.path, linked_catalog_dir.join("ai")); - assert!(skill - .supporting_files - .contains(&linked_catalog_dir.join("ai/notes.md"))); - } - - #[cfg(unix)] - #[tokio::test] - async fn test_skill_discovery_avoids_symlink_cycles() { - let temp_dir = TempDir::new().unwrap(); - - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(skill_dir.join("refs")).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill with a loop\n---\nLoop safe", - ) - .unwrap(); - fs::write(skill_dir.join("refs/guide.md"), "guide content").unwrap(); - std::os::unix::fs::symlink(&skill_dir, skill_dir.join("refs/loop")).unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.discover_filesystem_sources(temp_dir.path()); - - let skill = sources.iter().find(|s| s.name == "my-skill").unwrap(); - assert_eq!(skill.path, skill_dir); - assert!(skill - .supporting_files - .contains(&skill_dir.join("refs/guide.md"))); - assert_eq!( - skill - .supporting_files - .iter() - .filter(|path| *path == &skill_dir.join("refs/guide.md")) - .count(), - 1 - ); - } - - #[tokio::test] - async fn test_load_source_lists_supporting_files_with_load_names() { - let temp_dir = TempDir::new().unwrap(); - - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(skill_dir.join("references")).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill\n---\nSee references.", - ) - .unwrap(); - fs::write( - skill_dir.join("references/ops.md"), - "# Ops Guide\n\nDo the thing.", - ) - .unwrap(); - fs::write(skill_dir.join("run.sh"), "#!/bin/bash\necho ok").unwrap(); let client = SummonClient::new(create_test_context()).unwrap(); let result = client - .handle_load_source("test", "my-skill", temp_dir.path()) + .handle_load_source("test", "deploy", temp_dir.path()) .await .unwrap(); let text = &result[0].as_text().expect("expected text content").text; - - assert!( - !text.contains("Ops Guide"), - "md file content should not be inlined" - ); - assert!( - !text.contains("Do the thing."), - "md file content should not be inlined" - ); - assert!( - !text.contains("#!/bin/bash"), - "script content should not be inlined" - ); - assert!( - text.contains("load(source: \"my-skill/references/ops.md\")"), - "md file should be listed with load() name" - ); - assert!( - text.contains("load(source: \"my-skill/run.sh\")"), - "script should be listed with load() name" - ); - assert!( - text.contains("load(source: \"/\")"), - "should include usage hint" - ); + assert!(text.contains("deploy")); + assert!(text.contains("Run deploy steps")); + assert!(text.contains("now available in your context")); } #[tokio::test] - async fn test_load_supporting_file_by_path() { + async fn test_load_agent_source() { let temp_dir = TempDir::new().unwrap(); - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(skill_dir.join("references")).unwrap(); + let agents = temp_dir.path().join(".goose/agents"); + fs::create_dir_all(&agents).unwrap(); fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill\n---\nSee references.", + agents.join("reviewer.md"), + "---\nname: reviewer\nmodel: sonnet\ndescription: Code reviewer\n---\nYou review code carefully.", ) .unwrap(); - fs::write( - skill_dir.join("references/ops.md"), - "# Ops Guide\n\nDo the thing.", - ) - .unwrap(); - fs::write(skill_dir.join("run.sh"), "#!/bin/bash\necho ok").unwrap(); let client = SummonClient::new(create_test_context()).unwrap(); - - let md_result = client - .handle_load_source("test", "my-skill/references/ops.md", temp_dir.path()) + let result = client + .handle_load_source("test", "reviewer", temp_dir.path()) .await .unwrap(); - let md_text = &md_result[0].as_text().expect("expected text content").text; - assert!( - md_text.contains("Ops Guide"), - "markdown content should be loaded" - ); - assert!(md_text.contains("Do the thing.")); - let sh_result = client - .handle_load_source("test", "my-skill/run.sh", temp_dir.path()) - .await - .unwrap(); - let sh_text = &sh_result[0].as_text().expect("expected text content").text; - assert!( - sh_text.contains("#!/bin/bash"), - "script content should be loaded" - ); + let text = &result[0].as_text().expect("expected text content").text; + assert!(text.contains("reviewer")); + assert!(text.contains("You review code carefully")); + assert!(text.contains("now available in your context")); } #[tokio::test] - async fn test_load_supporting_file_not_found_suggests_available() { + async fn test_load_nonexistent_source_suggests_similar() { let temp_dir = TempDir::new().unwrap(); - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(skill_dir.join("references")).unwrap(); + let recipes = temp_dir.path().join(".goose/recipes"); + fs::create_dir_all(&recipes).unwrap(); fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill\n---\nSee references.", - ) - .unwrap(); - fs::write( - skill_dir.join("references/ops.md"), - "# Ops Guide\n\nDo the thing.", + recipes.join("deploy.yaml"), + "title: Deploy\ndescription: Deploy to production\ninstructions: steps", ) .unwrap(); let client = SummonClient::new(create_test_context()).unwrap(); let err = client - .handle_load_source( - "test", - "my-skill/references/nonexistent.md", - temp_dir.path(), - ) + .handle_load_source("test", "deploy-prod", temp_dir.path()) .await .unwrap_err(); - assert!( - err.contains("references/ops.md"), - "error should list available files: {}", - err - ); - assert!( - err.contains("my-skill"), - "error should name the skill: {}", - err - ); - } - - #[cfg(unix)] - #[tokio::test] - async fn test_resolve_source_blocks_symlink_outside_skill_dir() { - let temp_dir = TempDir::new().unwrap(); - let outside_dir = TempDir::new().unwrap(); - - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(&skill_dir).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill\n---\nContent.", - ) - .unwrap(); - - let secret_file = outside_dir.path().join("secret.txt"); - fs::write(&secret_file, "top secret data").unwrap(); - std::os::unix::fs::symlink(&secret_file, skill_dir.join("evil.md")).unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let result = client - .handle_load_source("test", "my-skill/evil.md", temp_dir.path()) - .await; - - assert!( - result.is_err(), - "symlink outside skill dir should be blocked" - ); - let err = result.unwrap_err(); - assert!( - err.contains("resolves outside the skill directory"), - "error should mention path traversal: {}", - err - ); + assert!(err.contains("not found")); + assert!(err.contains("deploy"), "should suggest 'deploy': {}", err); } #[tokio::test] - async fn test_resolve_source_blocks_path_traversal_input() { + async fn test_load_completely_unknown_source() { let temp_dir = TempDir::new().unwrap(); - let skill_dir = temp_dir.path().join(".goose/skills/my-skill"); - fs::create_dir_all(&skill_dir).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: my-skill\ndescription: A skill\n---\nContent.", - ) - .unwrap(); - fs::write(skill_dir.join("legit.md"), "legit content").unwrap(); - let client = SummonClient::new(create_test_context()).unwrap(); + let err = client + .handle_load_source("test", "zzz-nonexistent", temp_dir.path()) + .await + .unwrap_err(); - // ../../../etc/passwd won't match any supporting_files entry, so it returns Ok (not found) - // which becomes the "not found" error path in handle_load_source - let result = client - .handle_load_source("test", "my-skill/../../../etc/passwd", temp_dir.path()) - .await; - - assert!(result.is_err(), "traversal path should not load content"); - let err = result.unwrap_err(); - assert!( - !err.contains("root:"), - "should not contain /etc/passwd content: {}", - err - ); - } - - #[tokio::test] - async fn test_skill_name_with_slash_is_rejected() { - let temp_dir = TempDir::new().unwrap(); - - let skill_dir = temp_dir.path().join(".goose/skills/bad-skill"); - fs::create_dir_all(&skill_dir).unwrap(); - fs::write( - skill_dir.join("SKILL.md"), - "---\nname: bad/skill\ndescription: A skill with slash\n---\nContent.", - ) - .unwrap(); - - let client = SummonClient::new(create_test_context()).unwrap(); - let sources = client.get_sources("test", temp_dir.path()).await; - - assert!( - !sources.iter().any(|s| s.name == "bad/skill"), - "skill with '/' in name should be rejected" - ); + assert!(err.contains("not found")); + assert!(err.contains("Use load()")); } #[tokio::test] diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index f7a9615741..f6805262bf 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -339,6 +339,7 @@ impl Agent { &self, response: &Message, tools: &[Tool], + suppress_replayed_thinking: bool, ) -> (Vec, Vec, Message) { // First collect all tool requests with coercion applied let tool_requests: Vec = response @@ -367,7 +368,16 @@ impl Agent { }) .collect(); - // Create a filtered message with frontend tool requests removed + let has_tool_requests = !tool_requests.is_empty(); + let should_suppress_replayed_thinking = suppress_replayed_thinking && has_tool_requests; + + // Create a filtered message with frontend tool requests removed. + // When a response contains tool calls, keep reasoning in the original + // message for provider/state purposes but only suppress it from the + // user-visible filtered message if the caller already surfaced + // thinking earlier in this provider turn. That avoids replaying full + // accumulated reasoning after streamed thought chunks while still + // preserving final-only non-streaming thoughts. let mut filtered_content = Vec::new(); let mut tool_request_index = 0; @@ -389,6 +399,8 @@ impl Agent { } } } + MessageContent::Thinking(_) | MessageContent::RedactedThinking(_) + if should_suppress_replayed_thinking => {} _ => { filtered_content.push(content.clone()); } @@ -662,6 +674,52 @@ mod tests { ); } + #[tokio::test] + async fn categorize_tool_requests_keeps_thinking_when_not_previously_streamed() { + let agent = crate::agents::Agent::new(); + let response = Message::assistant() + .with_thinking("final-only reasoning", "") + .with_tool_request( + "tool-1", + Ok(rmcp::model::CallToolRequestParams::new("test_tool")), + ); + + let (_frontend_requests, other_requests, filtered_message) = + agent.categorize_tool_requests(&response, &[], false).await; + + assert_eq!(other_requests.len(), 1); + assert_eq!(filtered_message.content.len(), 2); + assert!(matches!( + filtered_message.content[0], + MessageContent::Thinking(_) + )); + assert!(matches!( + filtered_message.content[1], + MessageContent::ToolRequest(_) + )); + } + + #[tokio::test] + async fn categorize_tool_requests_drops_replayed_thinking_after_streaming() { + let agent = crate::agents::Agent::new(); + let response = Message::assistant() + .with_thinking("replayed reasoning", "") + .with_tool_request( + "tool-1", + Ok(rmcp::model::CallToolRequestParams::new("test_tool")), + ); + + let (_frontend_requests, other_requests, filtered_message) = + agent.categorize_tool_requests(&response, &[], true).await; + + assert_eq!(other_requests.len(), 1); + assert_eq!(filtered_message.content.len(), 1); + assert!(matches!( + filtered_message.content[0], + MessageContent::ToolRequest(_) + )); + } + fn make_tool_with_meta(meta_json: Option) -> Tool { let mut tool = Tool::new("test_tool", "a test tool", object!({ "type": "object" })); if let Some(v) = meta_json { diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap index 3c89414110..df7355c921 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap @@ -2,7 +2,7 @@ source: crates/goose/src/agents/prompt_manager.rs expression: system_prompt --- -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. +You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. # Extensions @@ -93,16 +93,19 @@ Use write and edit to efficiently make changes. Test and verify as appropriate. ### Instructions Manage agent sessions: list, view, start, send messages, and interrupt agents. -## summarize - - -## summon +## skills ### Instructions You have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them: β€’ goose-doc-guide - Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST fetch relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. +## summarize + + +## summon + + ## todo ### Instructions diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap index fe49614234..03c0549415 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap @@ -2,7 +2,7 @@ source: crates/goose/src/agents/prompt_manager.rs expression: system_prompt --- -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. +You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. # Extensions diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap index c659703082..306fd7dc7d 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap @@ -2,7 +2,7 @@ source: crates/goose/src/agents/prompt_manager.rs expression: system_prompt --- -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. +You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. # Extensions diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap index 81d2202689..f26bbb8503 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap @@ -2,7 +2,7 @@ source: crates/goose/src/agents/prompt_manager.rs expression: system_prompt --- -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. +You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. # Extensions diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index c42eb663e1..b6698b5ab0 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -152,13 +152,16 @@ impl Default for Config { } }); - let secrets = match env::var("GOOSE_DISABLE_KEYRING") { - Ok(_) => SecretStorage::File { + let secrets = if env::var("GOOSE_DISABLE_KEYRING").is_ok() + || keyring_disabled_in_config(&config_path) + { + SecretStorage::File { path: config_dir.join("secrets.yaml"), - }, - Err(_) => SecretStorage::Keyring { + } + } else { + SecretStorage::Keyring { service: KEYRING_SERVICE.to_string(), - }, + } }; Config { config_path, @@ -249,6 +252,22 @@ fn parse_yaml_content(content: &str) -> Result { serde_yaml::from_str(content).map_err(|e| e.into()) } +/// Read the GOOSE_DISABLE_KEYRING flag from the config file. +/// +/// Called before Config is fully initialised, so we do a minimal raw read +/// rather than going through `get_param`. All errors are treated as `false` +/// (keyring stays enabled) so a missing/malformed file is never fatal here. +fn keyring_disabled_in_config(config_path: &Path) -> bool { + std::fs::read_to_string(config_path) + .ok() + .and_then(|s| parse_yaml_content(&s).ok()) + .and_then(|m| { + m.get("GOOSE_DISABLE_KEYRING") + .map(|v| v.as_bool().unwrap_or(false) || v.as_str().is_some_and(|s| s == "true")) + }) + .unwrap_or(false) +} + impl Config { /// Get the global configuration instance. /// @@ -1045,6 +1064,7 @@ config_value!(GEMINI3_THINKING_LEVEL, String); config_value!(CLAUDE_THINKING_TYPE, String); config_value!(CLAUDE_THINKING_EFFORT, String); config_value!(CLAUDE_THINKING_BUDGET, i32); +config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64); fn find_workspace_or_exe_root() -> Option { let exe = std::env::current_exe().ok()?; diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index acd357147e..e0da2d5618 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -7,7 +7,16 @@ use crate::providers::openai::OpenAiProvider; use anyhow::Result; use include_dir::{include_dir, Dir}; use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; + +/// 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())) +} use std::collections::HashMap; use std::path::Path; use std::sync::Mutex; @@ -66,6 +75,8 @@ pub struct DeclarativeProviderConfig { pub dynamic_models: Option, #[serde(default)] pub skip_canonical_filtering: bool, + #[serde(default, deserialize_with = "deserialize_non_empty_string")] + pub fast_model: Option, } fn default_requires_auth() -> bool { @@ -221,6 +232,7 @@ pub fn create_custom_provider( env_vars: None, dynamic_models: None, skip_canonical_filtering: false, + fast_model: None, }; let custom_providers_dir = custom_providers_dir(); @@ -287,6 +299,7 @@ pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()> env_vars: existing_config.env_vars, dynamic_models: existing_config.dynamic_models, skip_canonical_filtering: existing_config.skip_canonical_filtering, + fast_model: existing_config.fast_model.clone(), }; let file_path = custom_providers_dir().join(format!("{}.json", updated_config.name)); diff --git a/crates/goose/src/config/paths.rs b/crates/goose/src/config/paths.rs index 839f8f1b65..0fadf90ab3 100644 --- a/crates/goose/src/config/paths.rs +++ b/crates/goose/src/config/paths.rs @@ -13,6 +13,9 @@ impl Paths { DirType::State => base.join("state"), } } 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(), diff --git a/crates/goose/src/config/signup_tetrate/mod.rs b/crates/goose/src/config/signup_tetrate/mod.rs index 58508ca309..0d4398e6f9 100644 --- a/crates/goose/src/config/signup_tetrate/mod.rs +++ b/crates/goose/src/config/signup_tetrate/mod.rs @@ -87,7 +87,7 @@ impl PkceAuthFlow { let response = client .post(TETRATE_TOKEN_URL) .header("X-Title", "goose") - .header("Referer", "https://github.com/block/goose") + .header("Referer", "https://github.com/aaif-goose/goose") .json(&request_body) .send() .await?; diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index fd7c785744..1cde777b04 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -188,6 +188,10 @@ pub async fn check_if_compaction_needed( threshold_override: Option, session: &crate::session::Session, ) -> Result { + if provider.manages_own_context() { + return Ok(false); + } + let messages = conversation.messages(); let config = Config::global(); let threshold = threshold_override.unwrap_or_else(|| { diff --git a/crates/goose/src/prompts/subagent_system.md b/crates/goose/src/prompts/subagent_system.md index 2ff619c2cf..192f94f2ee 100644 --- a/crates/goose/src/prompts/subagent_system.md +++ b/crates/goose/src/prompts/subagent_system.md @@ -1,4 +1,4 @@ -You are a specialized subagent within the goose AI framework, created by Block. You were spawned by the main goose agent to handle a specific task efficiently. +You are a specialized subagent within the goose AI framework, created by AAIF (Agentic AI Foundation). You were spawned by the main goose agent to handle a specific task efficiently. # Your Role You are an autonomous subagent with these characteristics: diff --git a/crates/goose/src/prompts/system.md b/crates/goose/src/prompts/system.md index 9c5d75fda7..aa9d0ae8b2 100644 --- a/crates/goose/src/prompts/system.md +++ b/crates/goose/src/prompts/system.md @@ -1,4 +1,4 @@ -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. +You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. {% if not code_execution_mode %} diff --git a/crates/goose/src/prompts/tiny_model_system.md b/crates/goose/src/prompts/tiny_model_system.md index 08b1f116c0..2a05d84160 100644 --- a/crates/goose/src/prompts/tiny_model_system.md +++ b/crates/goose/src/prompts/tiny_model_system.md @@ -1,4 +1,4 @@ -You are goose, an autonomous AI agent created by Block. You act on the user's +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}} diff --git a/crates/goose/src/providers/amp_acp.rs b/crates/goose/src/providers/amp_acp.rs new file mode 100644 index 0000000000..987b370b8c --- /dev/null +++ b/crates/goose/src/providers/amp_acp.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use futures::future::BoxFuture; +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::acp::{ + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, +}; +use crate::config::search_path::SearchPaths; +use crate::config::{Config, GooseMode}; +use crate::model::ModelConfig; +use crate::providers::base::{ProviderDef, ProviderMetadata}; + +const AMP_ACP_PROVIDER_NAME: &str = "amp-acp"; +const AMP_ACP_DOC_URL: &str = "https://ampcode.com"; +const AMP_ACP_BINARY: &str = "amp-acp"; + +pub struct AmpAcpProvider; + +impl ProviderDef for AmpAcpProvider { + type Provider = AcpProvider; + + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + AMP_ACP_PROVIDER_NAME, + "Amp", + "Use goose with your Amp subscription via the amp-acp adapter.", + ACP_CURRENT_MODEL, + vec![], + AMP_ACP_DOC_URL, + vec![], + ) + .with_setup_steps(vec![ + "Install the Amp CLI: `curl -fsSL https://ampcode.com/install.sh | bash`", + "Install the ACP adapter: `npm install -g amp-acp`", + "Ensure your Amp CLI is authenticated (run `amp` to verify)", + "Set in your goose config file (`~/.config/goose/config.yaml` on macOS/Linux):\n GOOSE_PROVIDER: amp-acp\n GOOSE_MODEL: current", + "Restart goose for changes to take effect", + ]) + } + + fn from_env( + model: ModelConfig, + extensions: Vec, + ) -> BoxFuture<'static, Result> { + Box::pin(async move { + let config = Config::global(); + let resolved_command = SearchPaths::builder().with_npm().resolve(AMP_ACP_BINARY)?; + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); + + let mode_mapping = HashMap::from([ + (GooseMode::Auto, "auto".to_string()), + (GooseMode::Approve, "approve".to_string()), + (GooseMode::SmartApprove, "smart-approve".to_string()), + (GooseMode::Chat, "chat".to_string()), + ]); + + let provider_config = AcpProviderConfig { + command: resolved_command, + args: vec![], + env: vec![], + env_remove: vec![], + work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + mcp_servers: extension_configs_to_mcp_servers(&extensions), + session_mode_id: Some(mode_mapping[&goose_mode].clone()), + mode_mapping, + notification_callback: None, + }; + + let metadata = Self::metadata(); + AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await + }) + } +} diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index a446797d87..43039d3e8a 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -127,6 +127,12 @@ impl AnthropicProvider { None }; + let model = if let Some(ref fast_model_name) = config.fast_model { + model.with_fast(fast_model_name, &config.name)? + } else { + model + }; + Ok(Self { api_client, model, diff --git a/crates/goose/src/providers/canonical/data/canonical_models.json b/crates/goose/src/providers/canonical/data/canonical_models.json index f68dcd49f2..0a086bc193 100644 --- a/crates/goose/src/providers/canonical/data/canonical_models.json +++ b/crates/goose/src/providers/canonical/data/canonical_models.json @@ -5303,14 +5303,14 @@ }, { "id": "alibaba-cn/deepseek-r1", - "name": "DeepSeek R1", + "name": "DeepSeek R1 0528", "family": "deepseek-thinking", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-01-01", - "last_updated": "2025-01-01", + "release_date": "2025-05-28", + "last_updated": "2025-05-28", "modalities": { "input": [ "text" @@ -10609,12 +10609,12 @@ { "id": "amazon-bedrock/minimax.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", + "release_date": "2026-03-18", "last_updated": "2026-03-18", "modalities": { "input": [ @@ -10630,8 +10630,8 @@ "output": 1.2 }, "limit": { - "context": 1000000, - "output": 131072 + "context": 196608, + "output": 98304 } }, { @@ -11034,6 +11034,34 @@ "output": 4096 } }, + { + "id": "amazon-bedrock/nvidia.nemotron-super-3-120b", + "name": "NVIDIA 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.15, + "output": 0.65 + }, + "limit": { + "context": 262144, + "output": 131072 + } + }, { "id": "amazon-bedrock/openai.gpt-oss-120b-1:0", "name": "gpt-oss-120b", @@ -11705,7 +11733,7 @@ "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2026-02-11", + "release_date": "2026-03-18", "last_updated": "2026-03-18", "modalities": { "input": [ @@ -11721,8 +11749,8 @@ "output": 3.2 }, "limit": { - "context": 200000, - "output": 131072 + "context": 202752, + "output": 101376 } }, { @@ -11826,7 +11854,7 @@ }, { "id": "anthropic/claude-3.5-haiku", - "name": "Claude Haiku 3.5", + "name": "Claude Haiku 3.5 (latest)", "family": "claude-haiku", "attachment": true, "reasoning": false, @@ -11859,15 +11887,15 @@ }, { "id": "anthropic/claude-3.5-sonnet", - "name": "Claude Sonnet 3.5", + "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-06-20", - "last_updated": "2024-06-20", + "release_date": "2024-10-22", + "last_updated": "2024-10-22", "modalities": { "input": [ "text", @@ -11892,7 +11920,7 @@ }, { "id": "anthropic/claude-3.7-sonnet", - "name": "Claude Sonnet 3.7", + "name": "Claude Sonnet 3.7 (latest)", "family": "claude-sonnet", "attachment": true, "reasoning": true, @@ -12651,15 +12679,15 @@ }, { "id": "azure-cognitive-services/deepseek-r1", - "name": "DeepSeek-R1-0528", + "name": "DeepSeek-R1", "family": "deepseek-thinking", "attachment": false, "reasoning": true, - "tool_call": true, + "tool_call": false, "temperature": true, "knowledge": "2024-07", - "release_date": "2025-05-28", - "last_updated": "2025-05-28", + "release_date": "2025-01-20", + "last_updated": "2025-01-20", "modalities": { "input": [ "text" @@ -12796,15 +12824,15 @@ }, { "id": "azure-cognitive-services/gpt-3.5-turbo", - "name": "GPT-3.5 Turbo 0301", + "name": "GPT-3.5 Turbo 0613", "family": "gpt", "attachment": false, "reasoning": false, "tool_call": false, "temperature": true, "knowledge": "2021-08", - "release_date": "2023-03-01", - "last_updated": "2023-03-01", + "release_date": "2023-06-13", + "last_updated": "2023-06-13", "modalities": { "input": [ "text" @@ -12815,12 +12843,12 @@ }, "open_weights": false, "cost": { - "input": 1.5, - "output": 2.0 + "input": 3.0, + "output": 4.0 }, "limit": { - "context": 4096, - "output": 4096 + "context": 16384, + "output": 16384 } }, { @@ -14850,10 +14878,10 @@ }, { "id": "azure-cognitive-services/phi-4", - "name": "Phi-4", + "name": "Phi-4-reasoning", "family": "phi", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": false, "temperature": true, "knowledge": "2023-10", @@ -14873,7 +14901,7 @@ "output": 0.5 }, "limit": { - "context": 128000, + "context": 32000, "output": 4096 } }, @@ -15445,15 +15473,15 @@ }, { "id": "azure/deepseek-r1", - "name": "DeepSeek-R1", + "name": "DeepSeek-R1-0528", "family": "deepseek-thinking", "attachment": false, "reasoning": true, - "tool_call": false, + "tool_call": true, "temperature": true, "knowledge": "2024-07", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", + "release_date": "2025-05-28", + "last_updated": "2025-05-28", "modalities": { "input": [ "text" @@ -15590,15 +15618,15 @@ }, { "id": "azure/gpt-3.5-turbo", - "name": "GPT-3.5 Turbo 1106", + "name": "GPT-3.5 Turbo 0125", "family": "gpt", "attachment": false, "reasoning": false, "tool_call": false, "temperature": true, "knowledge": "2021-08", - "release_date": "2023-11-06", - "last_updated": "2023-11-06", + "release_date": "2024-01-25", + "last_updated": "2024-01-25", "modalities": { "input": [ "text" @@ -15609,8 +15637,8 @@ }, "open_weights": false, "cost": { - "input": 1.0, - "output": 2.0 + "input": 0.5, + "output": 1.5 }, "limit": { "context": 16384, @@ -17766,10 +17794,10 @@ }, { "id": "azure/phi-4", - "name": "Phi-4-reasoning", + "name": "Phi-4", "family": "phi", "attachment": false, - "reasoning": true, + "reasoning": false, "tool_call": false, "temperature": true, "knowledge": "2023-10", @@ -17789,7 +17817,7 @@ "output": 0.5 }, "limit": { - "context": 32000, + "context": 128000, "output": 4096 } }, @@ -28137,36 +28165,6 @@ "output": 64000 } }, - { - "id": "firmware/glm-5", - "name": "GLM-5", - "family": "glm", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-01-20", - "last_updated": "2025-02-22", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 1.0, - "output": 3.2, - "cache_read": 0.2 - }, - "limit": { - "context": 198000, - "output": 8192 - } - }, { "id": "firmware/gpt-4o", "name": "GPT-4o", @@ -28529,6 +28527,36 @@ "output": 8192 } }, + { + "id": "firmware/zai-glm-5", + "name": "GLM-5", + "family": "glm", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-01-20", + "last_updated": "2025-02-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 3.2, + "cache_read": 0.2 + }, + "limit": { + "context": 198000, + "output": 8192 + } + }, { "id": "friendli/MiniMaxAI/MiniMax-M2.1", "name": "MiniMax M2.1", @@ -29709,15 +29737,15 @@ }, { "id": "github-models/deepseek/deepseek-r1", - "name": "DeepSeek-R1", + "name": "DeepSeek-R1-0528", "family": "deepseek-thinking", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2024-06", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", + "release_date": "2025-05-28", + "last_updated": "2025-05-28", "modalities": { "input": [ "text" @@ -30354,7 +30382,7 @@ }, { "id": "github-models/microsoft/phi-4", - "name": "Phi-4-Reasoning", + "name": "Phi-4", "family": "phi", "attachment": false, "reasoning": true, @@ -30377,7 +30405,7 @@ "output": 0.0 }, "limit": { - "context": 128000, + "context": 16000, "output": 4096 } }, @@ -33701,6 +33729,241 @@ "output": 65536 } }, + { + "id": "google/gemma-3-12b-it", + "name": "Gemma 3 12B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-03-13", + "last_updated": "2025-03-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "google/gemma-3-27b-it", + "name": "Gemma 3 27B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-03-12", + "last_updated": "2025-03-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "google/gemma-3-4b-it", + "name": "Gemma 3 4B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-03-13", + "last_updated": "2025-03-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "google/gemma-3n-e2b-it", + "name": "Gemma 3n 2B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-07-09", + "last_updated": "2025-07-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 8192, + "output": 2000 + } + }, + { + "id": "google/gemma-3n-e4b-it", + "name": "Gemma 3n 4B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2025-05-20", + "last_updated": "2025-05-20", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 8192, + "output": 2000 + } + }, + { + "id": "groq/allam-2-7b", + "name": "ALLaM-2-7b", + "family": "allam", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-09", + "release_date": "2024-09", + "last_updated": "2024-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 4096, + "output": 4096 + } + }, + { + "id": "groq/canopylabs/orpheus-arabic-saudi", + "name": "Orpheus Arabic Saudi", + "family": "canopylabs", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2025-12-16", + "release_date": "2025-12-16", + "last_updated": "2025-12-16", + "modalities": { + "input": [ + "text" + ], + "output": [ + "audio" + ] + }, + "open_weights": false, + "cost": { + "input": 40.0, + "output": 0.0 + }, + "limit": { + "context": 4000, + "output": 50000 + } + }, + { + "id": "groq/canopylabs/orpheus-v1-english", + "name": "Orpheus V1 English", + "family": "canopylabs", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2025-12-19", + "release_date": "2025-12-19", + "last_updated": "2025-12-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "audio" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 4000, + "output": 50000 + } + }, { "id": "groq/deepseek-r1-distill-llama-70b", "name": "DeepSeek R1 Distill Llama 70B", @@ -33759,6 +34022,64 @@ "output": 8192 } }, + { + "id": "groq/groq/compound", + "name": "Compound", + "family": "groq", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-09-04", + "release_date": "2025-09-04", + "last_updated": "2025-09-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "groq/groq/compound-mini", + "name": "Compound Mini", + "family": "groq", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2025-09-04", + "release_date": "2025-09-04", + "last_updated": "2025-09-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, { "id": "groq/llama-3.1-8b-instant", "name": "Llama 3.1 8B Instant", @@ -33992,6 +34313,64 @@ "output": 1024 } }, + { + "id": "groq/meta-llama/llama-prompt-guard-2-22m", + "name": "Llama Prompt Guard 2 22M", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2024-10-01", + "last_updated": "2024-10-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.03, + "output": 0.03 + }, + "limit": { + "context": 512, + "output": 512 + } + }, + { + "id": "groq/meta-llama/llama-prompt-guard-2-86m", + "name": "Llama Prompt Guard 2 86M", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2024-10-01", + "last_updated": "2024-10-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.04, + "output": 0.04 + }, + "limit": { + "context": 512, + "output": 512 + } + }, { "id": "groq/mistral-saba-24b", "name": "Mistral Saba 24B", @@ -34023,15 +34402,15 @@ }, { "id": "groq/moonshotai/kimi-k2-instruct", - "name": "Kimi K2 Instruct 0905", + "name": "Kimi K2 Instruct", "family": "kimi", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2024-10", - "release_date": "2025-09-05", - "last_updated": "2025-09-05", + "release_date": "2025-07-14", + "last_updated": "2025-07-14", "modalities": { "input": [ "text" @@ -34046,7 +34425,7 @@ "output": 3.0 }, "limit": { - "context": 262144, + "context": 131072, "output": 16384 } }, @@ -34106,6 +34485,35 @@ "output": 65536 } }, + { + "id": "groq/openai/gpt-oss-safeguard-20b", + "name": "Safety GPT OSS 20B", + "family": "gpt-oss", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-03-05", + "last_updated": "2025-03-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.075, + "output": 0.3, + "cache_read": 0.037 + }, + "limit": { + "context": 131072, + "output": 65536 + } + }, { "id": "groq/qwen-qwq-32b", "name": "Qwen QwQ 32B", @@ -34161,7 +34569,65 @@ }, "limit": { "context": 131072, - "output": 16384 + "output": 40960 + } + }, + { + "id": "groq/whisper-large-v3", + "name": "Whisper Large V3", + "family": "whisper", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2023-09", + "release_date": "2023-09-01", + "last_updated": "2025-09-05", + "modalities": { + "input": [ + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 448, + "output": 448 + } + }, + { + "id": "groq/whisper-large-v3-turbo", + "name": "Whisper Large v3 Turbo", + "family": "whisper", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "knowledge": "2024-10", + "release_date": "2024-10-01", + "last_updated": "2024-10-01", + "modalities": { + "input": [ + "audio" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 448, + "output": 448 } }, { @@ -35834,15 +36300,15 @@ }, { "id": "helicone/kimi-k2", - "name": "Kimi K2 (07/11)", + "name": "Kimi K2 (09/05)", "family": "kimi", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2025-01", - "release_date": "2025-01-01", - "last_updated": "2025-01-01", + "knowledge": "2025-09", + "release_date": "2025-09-05", + "last_updated": "2025-09-05", "modalities": { "input": [ "text" @@ -35853,11 +36319,12 @@ }, "open_weights": false, "cost": { - "input": 0.5700000000000001, - "output": 2.3 + "input": 0.5, + "output": 2.0, + "cache_read": 0.39999999999999997 }, "limit": { - "context": 131072, + "context": 262144, "output": 16384 } }, @@ -36698,10 +37165,10 @@ }, { "id": "helicone/sonar", - "name": "Perplexity Sonar Reasoning", - "family": "sonar-reasoning", + "name": "Perplexity Sonar", + "family": "sonar", "attachment": false, - "reasoning": true, + "reasoning": false, "tool_call": false, "temperature": true, "knowledge": "2025-01", @@ -36718,7 +37185,7 @@ "open_weights": false, "cost": { "input": 1.0, - "output": 5.0 + "output": 1.0 }, "limit": { "context": 127000, @@ -37487,15 +37954,15 @@ }, { "id": "iflowcn/kimi-k2", - "name": "Kimi-K2-0905", + "name": "Kimi-K2", "family": "kimi", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2024-12", - "release_date": "2025-09-05", - "last_updated": "2025-09-05", + "knowledge": "2024-10", + "release_date": "2024-12-01", + "last_updated": "2024-12-01", "modalities": { "input": [ "text" @@ -37510,7 +37977,7 @@ "output": 0.0 }, "limit": { - "context": 256000, + "context": 128000, "output": 64000 } }, @@ -42088,13 +42555,13 @@ }, { "id": "kilo/deepseek/deepseek-r1", - "name": "DeepSeek: R1", + "name": "DeepSeek: R1 0528", "attachment": false, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-01-20", - "last_updated": "2025-01-20", + "release_date": "2025-05-28", + "last_updated": "2026-03-15", "modalities": { "input": [ "text" @@ -42105,12 +42572,13 @@ }, "open_weights": true, "cost": { - "input": 0.7, - "output": 2.5 + "input": 0.45, + "output": 2.15, + "cache_read": 0.2 }, "limit": { - "context": 64000, - "output": 16000 + "context": 163840, + "output": 65536 } }, { @@ -44462,16 +44930,17 @@ }, { "id": "kilo/mistralai/mistral-large", - "name": "Mistral Large", - "attachment": false, + "name": "Mistral: Mistral Large 3 2512", + "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "release_date": "2024-07-24", - "last_updated": "2025-12-02", + "release_date": "2024-11-01", + "last_updated": "2025-12-16", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -44479,12 +44948,12 @@ }, "open_weights": true, "cost": { - "input": 2.0, - "output": 6.0 + "input": 0.5, + "output": 1.5 }, "limit": { - "context": 128000, - "output": 25600 + "context": 262144, + "output": 52429 } }, { @@ -44820,13 +45289,13 @@ }, { "id": "kilo/moonshotai/kimi-k2", - "name": "MoonshotAI: Kimi K2 0905", + "name": "MoonshotAI: Kimi K2 0711", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "release_date": "2025-09-05", - "last_updated": "2025-09-05", + "release_date": "2025-07-11", + "last_updated": "2026-03-15", "modalities": { "input": [ "text" @@ -44837,12 +45306,11 @@ }, "open_weights": true, "cost": { - "input": 0.4, - "output": 2.0, - "cache_read": 0.15 + "input": 0.55, + "output": 2.2 }, "limit": { - "context": 131072, + "context": 131000, "output": 26215 } }, @@ -45592,12 +46060,12 @@ }, { "id": "kilo/openai/gpt-4o", - "name": "OpenAI: GPT-4o", + "name": "OpenAI: GPT-4o (2024-08-06)", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, - "release_date": "2024-05-13", + "release_date": "2024-08-06", "last_updated": "2026-03-15", "modalities": { "input": [ @@ -49575,6 +50043,5520 @@ "output": 131072 } }, + { + "id": "llmgateway/auto", + "name": "Auto Route", + "family": "auto", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/claude-3-haiku", + "name": "Claude 3 Haiku", + "family": "claude", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-03-04", + "last_updated": "2024-03-04", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 1.25, + "cache_read": 0.03 + }, + "limit": { + "context": 200000, + "output": 4096 + } + }, + { + "id": "llmgateway/claude-3-opus", + "name": "Claude 3 Opus", + "family": "claude", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-03-04", + "last_updated": "2024-03-04", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5 + }, + "limit": { + "context": 200000, + "output": 4096 + } + }, + { + "id": "llmgateway/claude-3.5-haiku", + "name": "Claude 3.5 Haiku", + "family": "claude", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-10-22", + "last_updated": "2024-10-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.8, + "output": 4.0, + "cache_read": 0.08 + }, + "limit": { + "context": 200000, + "output": 8192 + } + }, + { + "id": "llmgateway/claude-3.5-sonnet", + "name": "Claude 3.5 Sonnet (2024-10-22)", + "family": "claude", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-10-22", + "last_updated": "2024-10-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3 + }, + "limit": { + "context": 200000, + "output": 8192 + } + }, + { + "id": "llmgateway/claude-3.7-sonnet", + "name": "Claude 3.7 Sonnet", + "family": "claude", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-02-24", + "last_updated": "2025-02-24", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3 + }, + "limit": { + "context": 200000, + "output": 8192 + } + }, + { + "id": "llmgateway/claude-haiku-4.5", + "name": "Claude Haiku 4.5", + "family": "claude", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-15", + "last_updated": "2025-10-15", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 5.0, + "cache_read": 0.1 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, + { + "id": "llmgateway/claude-opus-4", + "name": "Claude Opus 4 (2025-05-14)", + "family": "claude", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-05-22", + "last_updated": "2025-05-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/claude-opus-4.1", + "name": "Claude Opus 4.1", + "family": "claude", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-05", + "last_updated": "2025-08-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 75.0, + "cache_read": 1.5 + }, + "limit": { + "context": 200000, + "output": 32000 + } + }, + { + "id": "llmgateway/claude-opus-4.5", + "name": "Claude Opus 4.5", + "family": "claude", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-24", + "last_updated": "2025-11-24", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 5.0, + "output": 25.0, + "cache_read": 0.5 + }, + "limit": { + "context": 200000, + "output": 32000 + } + }, + { + "id": "llmgateway/claude-opus-4.6", + "name": "Claude Opus 4.6", + "family": "claude", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-05", + "last_updated": "2026-02-05", + "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": "llmgateway/claude-sonnet-4", + "name": "Claude Sonnet 4 (2025-05-14)", + "family": "claude", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-05-14", + "last_updated": "2025-05-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/claude-sonnet-4.5", + "name": "Claude Sonnet 4.5", + "family": "claude", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-29", + "last_updated": "2025-09-29", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, + { + "id": "llmgateway/claude-sonnet-4.6", + "name": "Claude Sonnet 4.6", + "family": "claude", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-17", + "last_updated": "2026-02-17", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.3 + }, + "limit": { + "context": 200000, + "output": 64000 + } + }, + { + "id": "llmgateway/codestral", + "name": "Codestral", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-07-30", + "last_updated": "2025-07-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 0.9 + }, + "limit": { + "context": 256000, + "output": 16384 + } + }, + { + "id": "llmgateway/cogview-4", + "name": "CogView-4", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-04", + "last_updated": "2025-03-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/custom", + "name": "Custom Model", + "family": "auto", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/deepseek-r1", + "name": "DeepSeek R1 (0528)", + "family": "deepseek", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-05-28", + "last_updated": "2025-05-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.8, + "output": 2.4 + }, + "limit": { + "context": 64000, + "output": 16384 + } + }, + { + "id": "llmgateway/deepseek-v3.1", + "name": "DeepSeek V3.1", + "family": "deepseek", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-21", + "last_updated": "2025-08-21", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.56, + "output": 1.68, + "cache_read": 0.11 + }, + "limit": { + "context": 128000, + "output": 32768 + } + }, + { + "id": "llmgateway/deepseek-v3.2", + "name": "DeepSeek V3.2", + "family": "deepseek", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-29", + "last_updated": "2025-09-29", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.28, + "output": 0.42, + "cache_read": 0.03 + }, + "limit": { + "context": 163840, + "output": 16384 + } + }, + { + "id": "llmgateway/devstral", + "name": "Devstral 2", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-09", + "last_updated": "2025-12-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.4, + "output": 2.0 + }, + "limit": { + "context": 262144, + "output": 16384 + } + }, + { + "id": "llmgateway/devstral-small", + "name": "Devstral Small 1.1", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-07-21", + "last_updated": "2025-07-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 131072, + "output": 16384 + } + }, + { + "id": "llmgateway/gemini-2.0-flash", + "name": "Gemini 2.0 Flash", + "family": "gemini", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-02-05", + "last_updated": "2025-02-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.4, + "cache_read": 0.03 + }, + "limit": { + "context": 1048576, + "output": 8192 + } + }, + { + "id": "llmgateway/gemini-2.0-flash-lite", + "name": "Gemini 2.0 Flash Lite", + "family": "gemini", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-02-25", + "last_updated": "2025-02-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1048576, + "output": 8192 + } + }, + { + "id": "llmgateway/gemini-2.5-flash", + "name": "Gemini 2.5 Flash", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-26", + "last_updated": "2025-08-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 2.5, + "cache_read": 0.03 + }, + "limit": { + "context": 1048576, + "output": 65535 + } + }, + { + "id": "llmgateway/gemini-2.5-flash-image", + "name": "Gemini 2.5 Flash Image", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-10-02", + "last_updated": "2025-10-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 30.0, + "cache_read": 0.03 + }, + "limit": { + "context": 32768, + "output": 32768 + } + }, + { + "id": "llmgateway/gemini-2.5-flash-image-preview", + "name": "Gemini 2.5 Flash Image (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-10-02", + "last_updated": "2025-10-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 2.5 + }, + "limit": { + "context": 32768, + "output": 32768 + } + }, + { + "id": "llmgateway/gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash Lite", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-22", + "last_updated": "2025-07-22", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 1048576, + "output": 65535 + } + }, + { + "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": 65535 + } + }, + { + "id": "llmgateway/gemini-2.5-pro", + "name": "Gemini 2.5 Pro", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-03-25", + "last_updated": "2025-03-25", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 10.0, + "cache_read": 0.13 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + { + "id": "llmgateway/gemini-3-flash-preview", + "name": "Gemini 3 Flash (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-17", + "last_updated": "2025-12-17", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.5, + "output": 3.0, + "cache_read": 0.05 + }, + "limit": { + "context": 1048576, + "output": 65535 + } + }, + { + "id": "llmgateway/gemini-3-pro-image-preview", + "name": "Gemini 3 Pro Image (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "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": "llmgateway/gemini-3.1-flash-image-preview", + "name": "Gemini 3.1 Flash Image (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-02-26", + "last_updated": "2026-02-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 1.5 + }, + "limit": { + "context": 65536, + "output": 65536 + } + }, + { + "id": "llmgateway/gemini-3.1-flash-lite-preview", + "name": "Gemini 3.1 Flash Lite (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-03", + "last_updated": "2026-03-03", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 1.5, + "cache_read": 0.03 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + { + "id": "llmgateway/gemini-3.1-pro-preview", + "name": "Gemini 3.1 Pro (Preview)", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-19", + "last_updated": "2026-02-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 12.0, + "cache_read": 0.2 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + { + "id": "llmgateway/gemini-pro", + "name": "Gemini Pro Latest", + "family": "gemini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-27", + "last_updated": "2026-02-27", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 12.0, + "cache_read": 0.2 + }, + "limit": { + "context": 1048576, + "output": 65536 + } + }, + { + "id": "llmgateway/gemma-2-27b-it-together", + "name": "Gemma 2 27B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-06-27", + "last_updated": "2024-06-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.08 + }, + "limit": { + "context": 8192, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3-12b-it", + "name": "Gemma 3 12B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-10", + "last_updated": "2025-03-10", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3-1b-it", + "name": "Gemma 3 1B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-12", + "last_updated": "2025-03-12", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3-27b", + "name": "Gemma 3 27B", + "family": "gemma", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-12", + "last_updated": "2025-03-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.27, + "output": 0.27 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3-4b-it", + "name": "Gemma 3 4B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-10", + "last_updated": "2025-03-10", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3n-e2b-it", + "name": "Gemma 3n E2B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-06-26", + "last_updated": "2025-06-26", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gemma-3n-e4b-it", + "name": "Gemma 3n E4B IT", + "family": "gemma", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-06-26", + "last_updated": "2025-06-26", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.3 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4-32b-0414-128k", + "name": "GLM-4 32B (0414-128k)", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-14", + "last_updated": "2025-04-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.1, + "output": 0.1 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5", + "name": "GLM-4.5", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-28", + "last_updated": "2025-07-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 2.2, + "cache_read": 0.11 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5-air", + "name": "GLM-4.5 Air", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-25", + "last_updated": "2025-07-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 1.1, + "cache_read": 0.03 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5-airx", + "name": "GLM-4.5 AirX", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-28", + "last_updated": "2025-07-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.1, + "output": 4.5, + "cache_read": 0.22 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5-flash", + "name": "GLM-4.5 Flash", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-13", + "last_updated": "2025-08-13", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5-x", + "name": "GLM-4.5 X", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-28", + "last_updated": "2025-07-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.2, + "output": 8.9, + "cache_read": 0.45 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.5v", + "name": "GLM-4.5V", + "family": "glm", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-11", + "last_updated": "2025-08-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 1.8, + "cache_read": 0.11 + }, + "limit": { + "context": 128000, + "output": 16000 + } + }, + { + "id": "llmgateway/glm-4.6", + "name": "GLM-4.6", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-30", + "last_updated": "2025-09-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 2.2, + "cache_read": 0.11 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/glm-4.6v", + "name": "GLM-4.6V", + "family": "glm", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-08", + "last_updated": "2025-12-08", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 0.9, + "cache_read": 0.05 + }, + "limit": { + "context": 128000, + "output": 16000 + } + }, + { + "id": "llmgateway/glm-4.6v-flash", + "name": "GLM-4.6V Flash", + "family": "glm", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-08", + "last_updated": "2025-12-08", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 128000, + "output": 16000 + } + }, + { + "id": "llmgateway/glm-4.6v-flashx", + "name": "GLM-4.6V FlashX", + "family": "glm", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-08", + "last_updated": "2025-12-08", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.04, + "output": 0.4, + "cache_read": 0.0 + }, + "limit": { + "context": 128000, + "output": 16000 + } + }, + { + "id": "llmgateway/glm-4.7", + "name": "GLM-4.7", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-22", + "last_updated": "2025-12-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 2.2, + "cache_read": 0.11 + }, + "limit": { + "context": 200000, + "output": 128000 + } + }, + { + "id": "llmgateway/glm-4.7-flash", + "name": "GLM-4.7 Flash", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-22", + "last_updated": "2025-12-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 200000, + "output": 128000 + } + }, + { + "id": "llmgateway/glm-4.7-flashx", + "name": "GLM-4.7 FlashX", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-22", + "last_updated": "2025-12-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.07, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 200000, + "output": 128000 + } + }, + { + "id": "llmgateway/glm-5", + "name": "GLM-5", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-15", + "last_updated": "2026-02-15", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 3.2, + "cache_read": 0.2 + }, + "limit": { + "context": 202800, + "output": 131100 + } + }, + { + "id": "llmgateway/glm-image", + "name": "GLM-Image", + "family": "glm", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-14", + "last_updated": "2025-01-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/gpt-3.5-turbo", + "name": "GPT-3.5 Turbo", + "family": "gpt", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2022-11-30", + "last_updated": "2022-11-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.5, + "output": 1.5 + }, + "limit": { + "context": 16385, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4", + "name": "GPT-4", + "family": "gpt", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2023-03-14", + "last_updated": "2023-03-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 30.0, + "output": 60.0 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, + { + "id": "llmgateway/gpt-4-turbo", + "name": "GPT-4 Turbo", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2023-11-06", + "last_updated": "2023-11-06", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 10.0, + "output": 30.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4.1", + "name": "GPT-4.1", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-14", + "last_updated": "2025-04-14", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 8.0, + "cache_read": 0.5 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4.1-mini", + "name": "GPT-4.1 Mini", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-14", + "last_updated": "2025-04-14", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.4, + "output": 1.6, + "cache_read": 0.1 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4.1-nano", + "name": "GPT-4.1 Nano", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "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.03 + }, + "limit": { + "context": 1000000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4o", + "name": "GPT-4o", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-05-13", + "last_updated": "2024-05-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 10.0, + "cache_read": 1.25 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4o-mini", + "name": "GPT-4o Mini", + "family": "gpt", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-07-18", + "last_updated": "2024-07-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.15, + "output": 0.6, + "cache_read": 0.08 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4o-mini-search-preview", + "name": "GPT-4o Mini Search Preview", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-10-01", + "last_updated": "2024-10-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.15, + "output": 0.6 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-4o-search-preview", + "name": "GPT-4o Search Preview", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-10-01", + "last_updated": "2024-10-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 10.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-5", + "name": "GPT-5", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-01", + "last_updated": "2025-08-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 10.0, + "cache_read": 0.13 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5-chat", + "name": "GPT-5 Chat Latest", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-08-01", + "last_updated": "2025-08-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 10.0, + "cache_read": 0.13 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5-mini", + "name": "GPT-5 Mini", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-01", + "last_updated": "2025-08-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 2.0, + "cache_read": 0.03 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5-nano", + "name": "GPT-5 Nano", + "family": "gpt", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-01", + "last_updated": "2025-08-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.05, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5-pro", + "name": "GPT-5 Pro", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-01", + "last_updated": "2025-08-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 120.0 + }, + "limit": { + "context": 400000, + "output": 272000 + } + }, + { + "id": "llmgateway/gpt-5.1", + "name": "GPT-5.1", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-01", + "last_updated": "2025-11-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.25, + "output": 10.0, + "cache_read": 0.13 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.1-codex", + "name": "GPT-5.1 Codex", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "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 + }, + "limit": { + "context": 400000, + "output": 272000 + } + }, + { + "id": "llmgateway/gpt-5.1-codex-mini", + "name": "GPT-5.1 Codex mini", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-12", + "last_updated": "2025-11-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 2.0, + "cache_read": 0.03 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.2", + "name": "GPT-5.2", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-11", + "last_updated": "2025-12-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.18 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.2-chat", + "name": "GPT-5.2 Chat", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-11", + "last_updated": "2025-12-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.18 + }, + "limit": { + "context": 128000, + "output": 16400 + } + }, + { + "id": "llmgateway/gpt-5.2-codex", + "name": "GPT-5.2 Codex", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-01-14", + "last_updated": "2026-01-14", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.18 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.2-pro", + "name": "GPT-5.2 Pro", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-11", + "last_updated": "2025-12-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 21.0, + "output": 168.0 + }, + "limit": { + "context": 400000, + "output": 272000 + } + }, + { + "id": "llmgateway/gpt-5.3-chat", + "name": "GPT-5.3 Chat", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-03", + "last_updated": "2026-03-03", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.18 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/gpt-5.3-codex", + "name": "GPT-5.3 Codex", + "family": "gpt", + "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": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.18 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.4", + "name": "GPT-5.4", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-06", + "last_updated": "2026-03-06", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.5, + "output": 15.0, + "cache_read": 0.25 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.4-mini", + "name": "GPT-5.4 Mini", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "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.08 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "llmgateway/gpt-5.4-nano", + "name": "GPT-5.4 Nano", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "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": "llmgateway/gpt-5.4-pro", + "name": "GPT-5.4 Pro", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-01", + "last_updated": "2026-03-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 30.0, + "output": 180.0 + }, + "limit": { + "context": 1050000, + "output": 128000 + } + }, + { + "id": "llmgateway/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": false, + "cost": { + "input": 0.15, + "output": 0.75 + }, + "limit": { + "context": 131072, + "output": 32766 + } + }, + { + "id": "llmgateway/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": false, + "cost": { + "input": 0.1, + "output": 0.5 + }, + "limit": { + "context": 131072, + "output": 32766 + } + }, + { + "id": "llmgateway/grok-3", + "name": "Grok-3", + "family": "grok", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-02-17", + "last_updated": "2025-02-17", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0 + }, + "limit": { + "context": 131072, + "output": 16384 + } + }, + { + "id": "llmgateway/grok-4", + "name": "Grok 4 (0709)", + "family": "grok", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-09", + "last_updated": "2025-07-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, + { + "id": "llmgateway/grok-4-20-beta", + "name": "Grok 4.20 Beta Reasoning (0309)", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-09", + "last_updated": "2026-03-09", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 6.0, + "cache_read": 0.2 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4-20-beta-0309-non", + "name": "Grok 4.20 Beta Non-Reasoning (0309)", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-09", + "last_updated": "2026-03-09", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 6.0, + "cache_read": 0.2 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4-20-multi-agent-beta", + "name": "Grok 4.20 Multi-Agent Beta (0309)", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-09", + "last_updated": "2026-03-09", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 6.0, + "cache_read": 0.2 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4-fast", + "name": "Grok 4 Fast Reasoning", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-09", + "last_updated": "2025-07-09", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.05 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4-fast-non", + "name": "Grok 4 Fast Non-Reasoning", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-10", + "last_updated": "2025-10-10", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.05 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4.1-fast", + "name": "Grok 4.1 Fast", + "family": "grok", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-19", + "last_updated": "2025-11-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.05 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-4.1-fast-non", + "name": "Grok 4.1 Fast Non-Reasoning", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-19", + "last_updated": "2025-11-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.05 + }, + "limit": { + "context": 2000000, + "output": 30000 + } + }, + { + "id": "llmgateway/grok-code-fast-1", + "name": "Grok Code Fast 1", + "family": "grok", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-08-28", + "last_updated": "2025-08-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.2, + "output": 1.5 + }, + "limit": { + "context": 256000, + "output": 10000 + } + }, + { + "id": "llmgateway/grok-imagine-image", + "name": "Grok Imagine Image", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-02", + "last_updated": "2026-03-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/grok-imagine-image-pro", + "name": "Grok Imagine Image Pro", + "family": "grok", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-02", + "last_updated": "2026-03-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/hermes-2-pro-llama-3-8b", + "name": "Hermes 2 Pro Llama 3 8B", + "family": "nousresearch", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-05-27", + "last_updated": "2024-05-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.14, + "output": 0.14 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, + { + "id": "llmgateway/kimi-k2", + "name": "Kimi K2", + "family": "kimi", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-11", + "last_updated": "2025-07-11", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 3.0, + "cache_read": 0.5 + }, + "limit": { + "context": 131072, + "output": 16384 + } + }, + { + "id": "llmgateway/kimi-k2-thinking", + "name": "Kimi K2 Thinking", + "family": "kimi", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-06", + "last_updated": "2025-11-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 2.5, + "cache_read": 0.15 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "llmgateway/kimi-k2-thinking-turbo", + "name": "Kimi K2 Thinking Turbo", + "family": "kimi", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-11-06", + "last_updated": "2025-11-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.15, + "output": 8.0, + "cache_read": 0.15 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "llmgateway/kimi-k2.5", + "name": "Kimi K2.5", + "family": "kimi", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-01-26", + "last_updated": "2026-01-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.6, + "output": 3.0, + "cache_read": 0.1 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + { + "id": "llmgateway/llama-3-70b-instruct", + "name": "Llama 3 70B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-04-18", + "last_updated": "2024-04-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.51, + "output": 0.74 + }, + "limit": { + "context": 8192, + "output": 8000 + } + }, + { + "id": "llmgateway/llama-3-8b-instruct", + "name": "Llama 3 8B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-03", + "last_updated": "2025-04-03", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.04, + "output": 0.04 + }, + "limit": { + "context": 8192, + "output": 8192 + } + }, + { + "id": "llmgateway/llama-3.1-70b-instruct", + "name": "Llama 3.1 70B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-07-23", + "last_updated": "2024-07-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.72, + "output": 0.72 + }, + "limit": { + "context": 128000, + "output": 2048 + } + }, + { + "id": "llmgateway/llama-3.1-8b-instruct", + "name": "Llama 3.1 8B Instruct", + "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.22, + "output": 0.22 + }, + "limit": { + "context": 128000, + "output": 2048 + } + }, + { + "id": "llmgateway/llama-3.1-nemotron-ultra-253b", + "name": "Llama 3.1 Nemotron Ultra 253B", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-07", + "last_updated": "2025-04-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 1.8 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/llama-3.2-11b-instruct", + "name": "Llama 3.2 11B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-25", + "last_updated": "2024-09-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.07, + "output": 0.33 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/llama-3.2-3b-instruct", + "name": "Llama 3.2 3B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-18", + "last_updated": "2024-09-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.03, + "output": 0.05 + }, + "limit": { + "context": 32768, + "output": 32000 + } + }, + { + "id": "llmgateway/llama-3.3-70b-instruct", + "name": "Llama 3.3 70B Instruct", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-12-06", + "last_updated": "2024-12-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.13, + "output": 0.4 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/llama-4-maverick-17b-instruct", + "name": "Llama 4 Maverick 17B Instruct", + "family": "llama", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-05", + "last_updated": "2025-04-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.24, + "output": 0.97 + }, + "limit": { + "context": 8192, + "output": 2048 + } + }, + { + "id": "llmgateway/llama-4-scout", + "name": "Llama 4 Scout", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-05", + "last_updated": "2025-04-05", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.18, + "output": 0.59 + }, + "limit": { + "context": 32768, + "output": 16384 + } + }, + { + "id": "llmgateway/llama-4-scout-17b-instruct", + "name": "Llama 4 Scout 17B Instruct", + "family": "llama", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-05", + "last_updated": "2025-04-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.17, + "output": 0.66 + }, + "limit": { + "context": 8192, + "output": 2048 + } + }, + { + "id": "llmgateway/llama-guard-4-12b", + "name": "Llama Guard 4 12B", + "family": "llama", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-30", + "last_updated": "2025-04-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.2 + }, + "limit": { + "context": 131072, + "output": 16384 + } + }, + { + "id": "llmgateway/minimax-m2", + "name": "MiniMax M2", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-27", + "last_updated": "2025-10-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 1.0, + "cache_read": 0.03 + }, + "limit": { + "context": 196608, + "output": 131072 + } + }, + { + "id": "llmgateway/minimax-m2.1", + "name": "MiniMax M2.1", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-23", + "last_updated": "2025-12-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.27, + "output": 1.1 + }, + "limit": { + "context": 196608, + "output": 131072 + } + }, + { + "id": "llmgateway/minimax-m2.1-lightning", + "name": "MiniMax M2.1 Lightning", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-23", + "last_updated": "2025-12-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.12, + "output": 0.48 + }, + "limit": { + "context": 196608, + "output": 131072 + } + }, + { + "id": "llmgateway/minimax-m2.5", + "name": "MiniMax M2.5", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-15", + "last_updated": "2026-02-15", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 1.2, + "cache_read": 0.03 + }, + "limit": { + "context": 204800, + "output": 131100 + } + }, + { + "id": "llmgateway/minimax-m2.5-highspeed", + "name": "MiniMax M2.5 Highspeed", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 2.4, + "cache_read": 0.03 + }, + "limit": { + "context": 204800, + "output": 131100 + } + }, + { + "id": "llmgateway/minimax-m2.7", + "name": "MiniMax M2.7", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 1.2, + "cache_read": 0.06 + }, + "limit": { + "context": 204800, + "output": 131100 + } + }, + { + "id": "llmgateway/minimax-m2.7-highspeed", + "name": "MiniMax M2.7 Highspeed", + "family": "minimax", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 2.4, + "cache_read": 0.06 + }, + "limit": { + "context": 204800, + "output": 131100 + } + }, + { + "id": "llmgateway/minimax-text-01", + "name": "MiniMax Text 01", + "family": "minimax", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-15", + "last_updated": "2025-01-15", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 1.1 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "llmgateway/ministral-14b", + "name": "Ministral 14B", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-02", + "last_updated": "2025-12-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.2 + }, + "limit": { + "context": 262144, + "output": 16384 + } + }, + { + "id": "llmgateway/ministral-3b", + "name": "Ministral 3B", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-02", + "last_updated": "2025-12-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.1 + }, + "limit": { + "context": 131072, + "output": 16384 + } + }, + { + "id": "llmgateway/ministral-8b", + "name": "Ministral 8B", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-02", + "last_updated": "2025-12-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.15, + "output": 0.15 + }, + "limit": { + "context": 262144, + "output": 16384 + } + }, + { + "id": "llmgateway/mistral-large", + "name": "Mistral Large 3", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-02", + "last_updated": "2025-12-02", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.5 + }, + "limit": { + "context": 262144, + "output": 16384 + } + }, + { + "id": "llmgateway/mistral-small", + "name": "Mistral Small 3.2", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-06-20", + "last_updated": "2025-06-20", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/mixtral-8x7b-instruct-together", + "name": "Mixtral 8x7B Instruct", + "family": "mistral", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2023-12-10", + "last_updated": "2023-12-10", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.06, + "output": 0.06 + }, + "limit": { + "context": 32768, + "output": 16384 + } + }, + { + "id": "llmgateway/o1", + "name": "o1", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-12", + "last_updated": "2024-09-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 15.0, + "output": 60.0, + "cache_read": 7.5 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/o3", + "name": "o3", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-06-01", + "last_updated": "2025-06-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 8.0, + "cache_read": 0.5 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/o3-mini", + "name": "o3 Mini", + "family": "gpt", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-06-01", + "last_updated": "2025-06-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.1, + "output": 4.4, + "cache_read": 0.55 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/o4-mini", + "name": "o4 Mini", + "family": "gpt", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-16", + "last_updated": "2025-04-16", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.1, + "output": 4.4, + "cache_read": 0.28 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/pixtral-large", + "name": "Pixtral Large Latest", + "family": "mistral", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-11-18", + "last_updated": "2024-11-18", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 4.0, + "output": 12.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/qwen-coder-plus", + "name": "Qwen Coder Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-09-18", + "last_updated": "2024-09-18", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.0, + "output": 5.0 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen-flash", + "name": "Qwen Flash", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-09-09", + "last_updated": "2024-09-09", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.05, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 1000000, + "output": 32000 + } + }, + { + "id": "llmgateway/qwen-image", + "name": "Qwen Image", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-08-04", + "last_updated": "2025-08-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/qwen-image-edit-max", + "name": "Qwen Image Edit Max", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-01-16", + "last_updated": "2026-01-16", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/qwen-image-edit-plus", + "name": "Qwen Image Edit Plus", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-08-19", + "last_updated": "2025-08-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/qwen-image-max", + "name": "Qwen Image Max 2025-12-30", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-31", + "last_updated": "2025-12-31", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/qwen-image-plus", + "name": "Qwen Image Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-08-04", + "last_updated": "2025-08-04", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": true, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/qwen-max", + "name": "Qwen Max", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-05", + "last_updated": "2025-09-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.6, + "output": 6.4 + }, + "limit": { + "context": 131072, + "output": 32000 + } + }, + { + "id": "llmgateway/qwen-omni-turbo", + "name": "Qwen Omni Turbo", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-26", + "last_updated": "2025-03-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.8 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen-plus", + "name": "Qwen Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-01-25", + "last_updated": "2025-01-25", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.4, + "output": 1.2, + "cache_read": 0.08 + }, + "limit": { + "context": 131072, + "output": 32000 + } + }, + { + "id": "llmgateway/qwen-turbo", + "name": "Qwen Turbo", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-01", + "last_updated": "2025-02-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.05, + "output": 0.2 + }, + "limit": { + "context": 1000000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen-vl-max", + "name": "Qwen VL Max", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-01", + "last_updated": "2025-02-01", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.8, + "output": 3.2 + }, + "limit": { + "context": 131072, + "output": 32000 + } + }, + { + "id": "llmgateway/qwen-vl-plus", + "name": "Qwen VL Plus", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-05", + "last_updated": "2025-02-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.21, + "output": 0.64 + }, + "limit": { + "context": 131072, + "output": 32000 + } + }, + { + "id": "llmgateway/qwen2-5-vl-32b-instruct", + "name": "Qwen2.5 VL 32B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-02-19", + "last_updated": "2025-02-19", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 1.4, + "output": 4.2 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen2-5-vl-72b-instruct", + "name": "Qwen2.5 VL 72B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-26", + "last_updated": "2025-01-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.13, + "output": 0.4 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen25-coder-7b", + "name": "Qwen2.5 Coder 7B", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2024-09-19", + "last_updated": "2024-09-19", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.01, + "output": 0.03 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-235b-a22b-fp8", + "name": "Qwen3 235B A22B FP8", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-28", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.8 + }, + "limit": { + "context": 40960, + "output": 20000 + } + }, + { + "id": "llmgateway/qwen3-235b-a22b-instruct", + "name": "Qwen3 235B A22B Instruct 2507", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-21", + "last_updated": "2025-07-21", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.6 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-235b-a22b-thinking", + "name": "Qwen3 235B A22B Thinking 2507", + "family": "qwen", + "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.2, + "output": 0.6 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-30b-a3b-fp8", + "name": "Qwen3 30B A3B FP8", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-28", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.09, + "output": 0.45 + }, + "limit": { + "context": 40960, + "output": 20000 + } + }, + { + "id": "llmgateway/qwen3-30b-a3b-instruct", + "name": "Qwen3 30B A3B Instruct 2507", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-30", + "last_updated": "2025-07-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-30b-a3b-thinking", + "name": "Qwen3 30B A3B Thinking 2507", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-30", + "last_updated": "2025-07-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-32b", + "name": "Qwen3 32B", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-04-28", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 32768, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-32b-fp8", + "name": "Qwen3 32B FP8", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-28", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.45 + }, + "limit": { + "context": 40960, + "output": 20000 + } + }, + { + "id": "llmgateway/qwen3-4b-fp8", + "name": "Qwen3 4B FP8", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-04-28", + "last_updated": "2025-04-28", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.03, + "output": 0.03 + }, + "limit": { + "context": 128000, + "output": 20000 + } + }, + { + "id": "llmgateway/qwen3-coder-30b-a3b-instruct", + "name": "Qwen3 Coder 30B A3B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-31", + "last_updated": "2025-07-31", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.1, + "output": 0.3 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-coder-480b-a35b-instruct", + "name": "Qwen3 Coder 480B A35B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-01-31", + "last_updated": "2025-01-31", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.4, + "output": 1.8 + }, + "limit": { + "context": 262000, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-coder-flash", + "name": "Qwen3 Coder Flash", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-22", + "last_updated": "2025-07-22", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 1.5, + "cache_read": 0.06 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, + { + "id": "llmgateway/qwen3-coder-next", + "name": "Qwen3 Coder Next", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2024-01-01", + "last_updated": "2024-01-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.11, + "output": 0.68, + "cache_read": 0.06 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "llmgateway/qwen3-coder-plus", + "name": "Qwen3 Coder Plus", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-23", + "last_updated": "2025-09-23", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 6.0, + "output": 60.0 + }, + "limit": { + "context": 1000000, + "output": 66000 + } + }, + { + "id": "llmgateway/qwen3-max", + "name": "Qwen3 Max", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-24", + "last_updated": "2025-09-24", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 3.0, + "output": 15.0, + "cache_read": 0.6 + }, + "limit": { + "context": 256000, + "output": 32800 + } + }, + { + "id": "llmgateway/qwen3-next-80b-a3b-instruct", + "name": "Qwen3 Next 80B A3B Instruct", + "family": "qwen", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-10", + "last_updated": "2025-09-10", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 2.0 + }, + "limit": { + "context": 129024, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-next-80b-a3b-thinking", + "name": "Qwen3 Next 80B A3B Thinking", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-10", + "last_updated": "2025-09-10", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 6.0 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-235b-a22b-instruct", + "name": "Qwen3 VL 235B A22B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-23", + "last_updated": "2025-09-23", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 2.0 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-235b-a22b-thinking", + "name": "Qwen3 VL 235B A22B Thinking", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-09-23", + "last_updated": "2025-09-23", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.5, + "output": 2.0 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-30b-a3b-instruct", + "name": "Qwen3 VL 30B A3B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-05", + "last_updated": "2025-10-05", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 0.7 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-30b-a3b-thinking", + "name": "Qwen3 VL 30B A3B Thinking", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-11", + "last_updated": "2025-10-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 1.0 + }, + "limit": { + "context": 131072, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-8b-instruct", + "name": "Qwen3 VL 8B Instruct", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-10-14", + "last_updated": "2025-10-14", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.08, + "output": 0.5 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "llmgateway/qwen3-vl-flash", + "name": "Qwen3 VL Flash", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2025-10-15", + "last_updated": "2025-10-15", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.05, + "output": 0.4, + "cache_read": 0.01 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen3-vl-plus", + "name": "Qwen3 VL Plus", + "family": "qwen", + "attachment": true, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-09-23", + "last_updated": "2025-09-23", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.2, + "output": 1.6, + "cache_read": 0.04 + }, + "limit": { + "context": 262144, + "output": 32768 + } + }, + { + "id": "llmgateway/qwen35-397b-a17b", + "name": "Qwen3.5 397B A17B", + "family": "qwen", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-16", + "last_updated": "2026-02-16", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.6, + "output": 3.6 + }, + "limit": { + "context": 262144, + "output": 65536 + } + }, + { + "id": "llmgateway/qwq-plus", + "name": "QwQ Plus", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-06", + "last_updated": "2025-03-06", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.8, + "output": 2.4 + }, + "limit": { + "context": 131072, + "output": 8192 + } + }, + { + "id": "llmgateway/seed-1.6-250615", + "name": "Seed 1.6 (250615)", + "family": "seed", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-06-25", + "last_updated": "2025-06-25", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 2.0, + "cache_read": 0.05 + }, + "limit": { + "context": 256000, + "output": 16384 + } + }, + { + "id": "llmgateway/seed-1.6-250915", + "name": "Seed 1.6 (250915)", + "family": "seed", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-09-15", + "last_updated": "2025-09-15", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 2.0, + "cache_read": 0.05 + }, + "limit": { + "context": 256000, + "output": 16384 + } + }, + { + "id": "llmgateway/seed-1.6-flash-250715", + "name": "Seed 1.6 Flash (250715)", + "family": "seed", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-07-26", + "last_updated": "2025-07-26", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.07, + "output": 0.3, + "cache_read": 0.02 + }, + "limit": { + "context": 256000, + "output": 16384 + } + }, + { + "id": "llmgateway/seed-1.8-251228", + "name": "Seed 1.8 (251228)", + "family": "seed", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2025-12-18", + "last_updated": "2025-12-18", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.25, + "output": 2.0, + "cache_read": 0.05 + }, + "limit": { + "context": 256000, + "output": 16384 + } + }, + { + "id": "llmgateway/seedream-4.0", + "name": "Seedream 4.0", + "family": "seed", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-09-16", + "last_updated": "2025-09-16", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/seedream-4.5", + "name": "Seedream 4.5", + "family": "seed", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-12-03", + "last_updated": "2025-12-03", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text", + "image" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 2000, + "output": 4096 + } + }, + { + "id": "llmgateway/sonar", + "name": "Sonar", + "family": "sonar", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-01-01", + "last_updated": "2025-01-01", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 1.0 + }, + "limit": { + "context": 130000, + "output": 16384 + } + }, + { + "id": "llmgateway/sonar-pro", + "name": "Sonar Pro", + "family": "sonar", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-07", + "last_updated": "2025-03-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 3.0, + "output": 15.0 + }, + "limit": { + "context": 200000, + "output": 16384 + } + }, + { + "id": "llmgateway/sonar-reasoning-pro", + "name": "Sonar Reasoning Pro", + "family": "sonar", + "attachment": false, + "reasoning": true, + "tool_call": false, + "temperature": true, + "release_date": "2025-03-07", + "last_updated": "2025-03-07", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 8.0 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, + { + "id": "llmgateway/veo-3.1-fast-generate-preview", + "name": "Veo 3.1 Fast", + "family": "gemini", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-14", + "last_updated": "2026-03-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 32768, + "output": 1 + } + }, + { + "id": "llmgateway/veo-3.1-generate-preview", + "name": "Veo 3.1", + "family": "gemini", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-14", + "last_updated": "2026-03-14", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 32768, + "output": 1 + } + }, { "id": "lmstudio/openai/gpt-oss-20b", "name": "GPT OSS 20B", @@ -51233,15 +57215,15 @@ }, { "id": "mistralai/devstral-medium", - "name": "Devstral 2 (latest)", + "name": "Devstral Medium", "family": "devstral", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, - "knowledge": "2025-12", - "release_date": "2025-12-02", - "last_updated": "2025-12-02", + "knowledge": "2025-05", + "release_date": "2025-07-10", + "last_updated": "2025-07-10", "modalities": { "input": [ "text" @@ -51256,8 +57238,8 @@ "output": 2.0 }, "limit": { - "context": 262144, - "output": 262144 + "context": 128000, + "output": 128000 } }, { @@ -51465,44 +57447,15 @@ }, { "id": "mistralai/mistral-large", - "name": "Mistral Large 2.1", + "name": "Mistral Large 3", "family": "mistral-large", - "attachment": false, + "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2024-11", "release_date": "2024-11-01", - "last_updated": "2024-11-04", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 2.0, - "output": 6.0 - }, - "limit": { - "context": 131072, - "output": 16384 - } - }, - { - "id": "mistralai/mistral-medium", - "name": "Mistral Medium (latest)", - "family": "mistral-medium", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-05", - "release_date": "2025-05-07", - "last_updated": "2025-05-10", + "last_updated": "2025-12-02", "modalities": { "input": [ "text", @@ -51513,13 +57466,43 @@ ] }, "open_weights": true, + "cost": { + "input": 0.5, + "output": 1.5 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, + { + "id": "mistralai/mistral-medium", + "name": "Mistral Medium 3.1", + "family": "mistral-medium", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-05", + "release_date": "2025-08-12", + "last_updated": "2025-08-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, "cost": { "input": 0.4, "output": 2.0 }, "limit": { - "context": 128000, - "output": 16384 + "context": 262144, + "output": 262144 } }, { @@ -51555,13 +57538,13 @@ "id": "mistralai/mistral-small", "name": "Mistral Small (latest)", "family": "mistral-small", - "attachment": false, - "reasoning": false, + "attachment": true, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-03", - "release_date": "2024-09-01", - "last_updated": "2024-09-04", + "knowledge": "2025-06", + "release_date": "2026-03-16", + "last_updated": "2026-03-16", "modalities": { "input": [ "text", @@ -51573,12 +57556,12 @@ }, "open_weights": true, "cost": { - "input": 0.1, - "output": 0.3 + "input": 0.15, + "output": 0.6 }, "limit": { - "context": 128000, - "output": 16384 + "context": 256000, + "output": 256000 } }, { @@ -57399,17 +63382,16 @@ }, { "id": "nano-gpt/claude-3.5-sonnet", - "name": "Claude 3.5 Sonnet Old", + "name": "Claude 3.5 Sonnet", "attachment": true, "reasoning": false, "tool_call": true, - "release_date": "2024-06-20", - "last_updated": "2024-06-20", + "release_date": "2025-08-26", + "last_updated": "2025-08-26", "modalities": { "input": [ "text", - "image", - "pdf" + "image" ], "output": [ "text" @@ -62719,13 +68701,13 @@ }, { "id": "nano-gpt/moonshotai/kimi-k2-instruct", - "name": "Kimi K2 0711", + "name": "Kimi K2 Instruct", "family": "kimi", "attachment": false, "reasoning": false, "tool_call": true, - "release_date": "2025-07-11", - "last_updated": "2025-07-11", + "release_date": "2025-07-01", + "last_updated": "2025-07-01", "modalities": { "input": [ "text" @@ -62740,7 +68722,7 @@ "output": 2.0 }, "limit": { - "context": 128000, + "context": 256000, "output": 8192 } }, @@ -63295,13 +69277,13 @@ }, { "id": "nano-gpt/openai/gpt-4o", - "name": "GPT-4o", + "name": "GPT-4o (2024-08-06)", "family": "gpt", "attachment": true, "reasoning": false, "tool_call": false, - "release_date": "2024-05-13", - "last_updated": "2024-05-13", + "release_date": "2024-08-06", + "last_updated": "2024-08-06", "modalities": { "input": [ "text", @@ -63574,18 +69556,16 @@ }, { "id": "nano-gpt/openai/gpt-5.1", - "name": "GPT 5.1", + "name": "GPT-5.1 (2025-11-13)", "family": "gpt", - "attachment": true, - "reasoning": true, - "tool_call": true, + "attachment": false, + "reasoning": false, + "tool_call": false, "release_date": "2025-11-13", "last_updated": "2025-11-13", "modalities": { "input": [ - "text", - "image", - "pdf" + "text" ], "output": [ "text" @@ -63597,13 +69577,13 @@ "output": 10.0 }, "limit": { - "context": 400000, - "output": 128000 + "context": 1000000, + "output": 32768 } }, { "id": "nano-gpt/openai/gpt-5.1-chat", - "name": "GPT 5.1 Chat (Latest)", + "name": "GPT 5.1 Chat", "family": "gpt", "attachment": true, "reasoning": true, @@ -63626,7 +69606,7 @@ }, "limit": { "context": 400000, - "output": 16384 + "output": 128000 } }, { @@ -65627,11 +71607,11 @@ }, { "id": "nano-gpt/x-ai/grok-4.1-fast", - "name": "Grok 4.1 Fast", + "name": "Grok 4.1 Fast Reasoning", "family": "grok", "attachment": true, "reasoning": true, - "tool_call": true, + "tool_call": false, "release_date": "2025-11-20", "last_updated": "2025-11-20", "modalities": { @@ -66081,6 +72061,60 @@ "output": 128000 } }, + { + "id": "nano-gpt/zai-org/glm-5.1", + "name": "GLM 5.1", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 2.55 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, + { + "id": "nano-gpt/zai-org/glm-5.1:thinking", + "name": "GLM 5.1 Thinking", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": true, + "cost": { + "input": 0.3, + "output": 2.55 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "nano-gpt/zai-org/glm-5:thinking", "name": "GLM 5 Thinking", @@ -70078,13 +76112,13 @@ }, { "id": "nvidia/deepseek-ai/deepseek-r1", - "name": "Deepseek R1", + "name": "Deepseek R1 0528", "attachment": false, "reasoning": true, - "tool_call": false, + "tool_call": true, "temperature": true, - "release_date": "2025-01-20", - "last_updated": "2025-01-20", + "release_date": "2025-05-28", + "last_updated": "2025-05-28", "modalities": { "input": [ "text" @@ -71204,14 +77238,14 @@ }, { "id": "nvidia/moonshotai/kimi-k2-instruct", - "name": "Kimi K2 0905", + "name": "Kimi K2 Instruct", "family": "kimi", "attachment": false, - "reasoning": false, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-09-05", + "knowledge": "2024-01", + "release_date": "2025-01-01", "last_updated": "2025-09-05", "modalities": { "input": [ @@ -71221,14 +77255,14 @@ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { "input": 0.0, "output": 0.0 }, "limit": { - "context": 262144, - "output": 262144 + "context": 128000, + "output": 8192 } }, { @@ -73104,14 +79138,14 @@ }, { "id": "openai/gpt-4o", - "name": "GPT-4o", + "name": "GPT-4o (2024-08-06)", "family": "gpt", "attachment": true, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2023-09", - "release_date": "2024-05-13", + "release_date": "2024-08-06", "last_updated": "2024-08-06", "modalities": { "input": [ @@ -73627,6 +79661,37 @@ "output": 128000 } }, + { + "id": "openai/gpt-5.3-chat", + "name": "GPT-5.3 Chat (latest)", + "family": "gpt", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": true, + "knowledge": "2025-08-31", + "release_date": "2026-03-03", + "last_updated": "2026-03-03", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.75, + "output": 14.0, + "cache_read": 0.175 + }, + "limit": { + "context": 128000, + "output": 16384 + } + }, { "id": "openai/gpt-5.3-codex", "name": "GPT-5.3 Codex", @@ -75772,6 +81837,36 @@ "output": 65536 } }, + { + "id": "opencode/qwen3.6-plus-free", + "name": "Qwen3.6 Plus Free", + "family": "qwen-free", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "knowledge": "2024-12", + "release_date": "2026-03-30", + "last_updated": "2026-03-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0 + }, + "limit": { + "context": 1048576, + "output": 64000 + } + }, { "id": "opencode/trinity-large-preview-free", "name": "Trinity Large Preview", @@ -77888,6 +83983,36 @@ "output": 262144 } }, + { + "id": "openrouter/mistralai/mistral-small", + "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 + }, + "limit": { + "context": 262144, + "output": 262144 + } + }, { "id": "openrouter/mistralai/mistral-small-3.1-24b-instruct", "name": "Mistral Small 3.1 24B Instruct", @@ -77950,15 +84075,15 @@ }, { "id": "openrouter/moonshotai/kimi-k2", - "name": "Kimi K2 Instruct 0905", + "name": "Kimi K2", "family": "kimi", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2024-10", - "release_date": "2025-09-05", - "last_updated": "2025-09-05", + "release_date": "2025-07-11", + "last_updated": "2025-07-11", "modalities": { "input": [ "text" @@ -77969,12 +84094,12 @@ }, "open_weights": true, "cost": { - "input": 0.6, - "output": 2.5 + "input": 0.55, + "output": 2.2 }, "limit": { - "context": 262144, - "output": 16384 + "context": 131072, + "output": 32768 } }, { @@ -78243,7 +84368,7 @@ } }, { - "id": "openrouter/nvidia/nemotron-3-super-120b-a12b-free", + "id": "openrouter/nvidia/nemotron-3-super-120b-a12b:free", "name": "Nemotron 3 Super (free)", "family": "nemotron", "attachment": false, @@ -79886,6 +86011,34 @@ "output": 65536 } }, + { + "id": "openrouter/qwen/qwen3.6-plus-preview:free", + "name": "Qwen3.6 Plus Preview (free)", + "family": "qwen", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-30", + "last_updated": "2026-03-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0 + }, + "limit": { + "context": 1000000, + "output": 65536 + } + }, { "id": "openrouter/sourceful/riverflow-v2-fast-preview", "name": "Riverflow V2 Fast Preview", @@ -83060,6 +89213,34 @@ "output": 0 } }, + { + "id": "poe/novita/deepseek-v3.2", + "name": "DeepSeek-V3.2", + "attachment": true, + "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.27, + "output": 0.4, + "cache_read": 0.13 + }, + "limit": { + "context": 128000, + "output": 0 + } + }, { "id": "poe/novita/glm-4.6", "name": "GLM-4.6", @@ -84217,6 +90398,64 @@ "output": 128000 } }, + { + "id": "poe/openai/gpt-5.4-mini", + "name": "GPT-5.4-Mini", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-03-12", + "last_updated": "2026-03-12", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.68, + "output": 4.0, + "cache_read": 0.068 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, + { + "id": "poe/openai/gpt-5.4-nano", + "name": "GPT-5.4-Nano", + "attachment": true, + "reasoning": true, + "tool_call": true, + "temperature": false, + "release_date": "2026-03-11", + "last_updated": "2026-03-11", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.18, + "output": 1.1, + "cache_read": 0.018 + }, + "limit": { + "context": 400000, + "output": 128000 + } + }, { "id": "poe/openai/gpt-5.4-pro", "name": "GPT-5.4-Pro", @@ -84989,6 +91228,35 @@ "output": 30000 } }, + { + "id": "poe/xai/grok-4.20-multi-agent", + "name": "Grok-4.20-Multi-Agent", + "attachment": true, + "reasoning": false, + "tool_call": true, + "temperature": false, + "release_date": "2026-03-13", + "last_updated": "2026-03-13", + "modalities": { + "input": [ + "text", + "image" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 2.0, + "output": 6.0, + "cache_read": 0.2 + }, + "limit": { + "context": 128000, + "output": 0 + } + }, { "id": "poe/xai/grok-code-fast-1", "name": "Grok Code Fast 1", @@ -85695,7 +91963,7 @@ }, { "id": "qiniu-ai/deepseek-r1", - "name": "DeepSeek-R1", + "name": "DeepSeek-R1-0528", "attachment": false, "reasoning": true, "tool_call": true, @@ -85719,13 +91987,13 @@ }, { "id": "qiniu-ai/deepseek-v3", - "name": "DeepSeek-V3-0324", + "name": "DeepSeek-V3", "attachment": false, "reasoning": false, - "tool_call": true, + "tool_call": false, "temperature": true, - "release_date": "2025-08-05", - "last_updated": "2025-08-05", + "release_date": "2025-08-13", + "last_updated": "2025-08-13", "modalities": { "input": [ "text" @@ -87364,13 +93632,13 @@ }, { "id": "qiniu-ai/x-ai/grok-4-fast", - "name": "x-AI/Grok-4-Fast", + "name": "X-Ai/Grok-4-Fast-Reasoning", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-09-20", - "last_updated": "2025-09-20", + "release_date": "2025-12-18", + "last_updated": "2025-12-18", "modalities": { "input": [ "text", @@ -87418,16 +93686,19 @@ }, { "id": "qiniu-ai/x-ai/grok-4.1-fast", - "name": "x-AI/Grok-4.1-Fast", - "attachment": false, + "name": "X-Ai/Grok 4.1 Fast Reasoning", + "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, - "release_date": "2025-11-20", - "last_updated": "2025-11-20", + "release_date": "2025-12-19", + "last_updated": "2025-12-19", "modalities": { "input": [ - "text" + "text", + "image", + "audio", + "video" ], "output": [ "text" @@ -87436,7 +93707,7 @@ "open_weights": false, "cost": {}, "limit": { - "context": 2000000, + "context": 20000000, "output": 2000000 } }, @@ -92305,6 +98576,34 @@ "output": 131000 } }, + { + "id": "siliconflow/MiniMaxAI/MiniMax-M2.5", + "name": "MiniMaxAI/MiniMax-M2.5", + "family": "minimax", + "attachment": false, + "reasoning": false, + "tool_call": true, + "temperature": true, + "release_date": "2026-02-15", + "last_updated": "2026-02-15", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 1.2 + }, + "limit": { + "context": 197000, + "output": 131000 + } + }, { "id": "siliconflow/Qwen/QwQ-32B", "name": "Qwen/QwQ-32B", @@ -96430,6 +102729,35 @@ "output": 32000 } }, + { + "id": "venice/aion-labs.aion-2.0", + "name": "Aion 2.0", + "family": "o", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-24", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 1.0, + "output": 2.0, + "cache_read": 0.25 + }, + "limit": { + "context": 128000, + "output": 32768 + } + }, { "id": "venice/claude-opus-4.6", "name": "Claude Opus 4.6", @@ -96563,11 +102891,11 @@ "family": "deepseek", "attachment": false, "reasoning": true, - "tool_call": false, + "tool_call": true, "temperature": true, "knowledge": "2025-10", "release_date": "2025-12-04", - "last_updated": "2026-03-18", + "last_updated": "2026-03-24", "modalities": { "input": [ "text" @@ -96621,39 +102949,6 @@ "output": 65536 } }, - { - "id": "venice/gemini-3-pro-preview", - "name": "Gemini 3 Pro Preview", - "family": "gemini-pro", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-04", - "release_date": "2025-12-02", - "last_updated": "2026-03-12", - "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.5, - "output": 15.0, - "cache_read": 0.625 - }, - "limit": { - "context": 198000, - "output": 32768 - } - }, { "id": "venice/gemini-3.1-pro-preview", "name": "Gemini 3.1 Pro Preview", @@ -100320,6 +106615,35 @@ "output": 32000 } }, + { + "id": "vercel/kwaipilot/kat-coder-pro-v2", + "name": "Kat Coder Pro V2", + "family": "kat-coder", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.3, + "output": 1.2, + "cache_read": 0.06 + }, + "limit": { + "context": 256000, + "output": 256000 + } + }, { "id": "vercel/meituan/longcat-flash-chat", "name": "LongCat Flash Chat", @@ -101258,13 +107582,13 @@ "id": "vercel/mistral/mistral-small", "name": "Mistral Small (latest)", "family": "mistral-small", - "attachment": false, - "reasoning": false, + "attachment": true, + "reasoning": true, "tool_call": true, "temperature": true, - "knowledge": "2025-03", - "release_date": "2024-09-01", - "last_updated": "2024-09-04", + "knowledge": "2025-06", + "release_date": "2026-03-16", + "last_updated": "2026-03-16", "modalities": { "input": [ "text", @@ -101276,12 +107600,12 @@ }, "open_weights": true, "cost": { - "input": 0.1, - "output": 0.3 + "input": 0.15, + "output": 0.6 }, "limit": { - "context": 128000, - "output": 16384 + "context": 256000, + "output": 256000 } }, { @@ -101607,6 +107931,34 @@ "output": 262144 } }, + { + "id": "vercel/nvidia/nemotron-3-super-120b-a12b", + "name": "NVIDIA Nemotron 3 Super 120B A12B", + "family": "nemotron", + "attachment": false, + "reasoning": false, + "tool_call": false, + "temperature": true, + "release_date": "2026-03-18", + "last_updated": "2026-03-30", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.15, + "output": 0.65 + }, + "limit": { + "context": 256000, + "output": 32000 + } + }, { "id": "vercel/nvidia/nemotron-nano-12b-v2-vl", "name": "Nvidia Nemotron Nano 12B V2 VL", @@ -102993,18 +109345,19 @@ }, { "id": "vercel/perplexity/sonar", - "name": "Sonar Reasoning", - "family": "sonar-reasoning", - "attachment": false, - "reasoning": true, - "tool_call": false, + "name": "Sonar", + "family": "sonar", + "attachment": true, + "reasoning": false, + "tool_call": true, "temperature": true, - "knowledge": "2025-09", + "knowledge": "2025-02", "release_date": "2025-02-19", "last_updated": "2025-02-19", "modalities": { "input": [ - "text" + "text", + "image" ], "output": [ "text" @@ -103013,7 +109366,7 @@ "open_weights": false, "cost": { "input": 1.0, - "output": 5.0 + "output": 1.0 }, "limit": { "context": 127000, @@ -104464,16 +110817,16 @@ } }, { - "id": "vivgrid/gemini-3-flash-preview", - "name": "Gemini 3 Flash Preview", - "family": "gemini-flash", + "id": "vivgrid/gemini-3.1-flash-lite-preview", + "name": "Gemini 3.1 Flash Lite Preview", + "family": "gemini-flash-lite", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-12-17", - "last_updated": "2025-12-17", + "release_date": "2026-03-03", + "last_updated": "2026-03-03", "modalities": { "input": [ "text", @@ -104488,9 +110841,10 @@ }, "open_weights": false, "cost": { - "input": 0.5, - "output": 3.0, - "cache_read": 0.05 + "input": 0.25, + "output": 1.5, + "cache_read": 0.025, + "cache_write": 1.0 }, "limit": { "context": 1048576, @@ -104498,16 +110852,16 @@ } }, { - "id": "vivgrid/gemini-3-pro-preview", - "name": "Gemini 3 Pro Preview", + "id": "vivgrid/gemini-3.1-pro-preview", + "name": "Gemini 3.1 Pro Preview", "family": "gemini-pro", "attachment": true, "reasoning": true, "tool_call": true, "temperature": true, "knowledge": "2025-01", - "release_date": "2025-11-18", - "last_updated": "2025-11-18", + "release_date": "2026-02-19", + "last_updated": "2026-02-19", "modalities": { "input": [ "text", @@ -104685,61 +111039,35 @@ } }, { - "id": "vultr/DeepSeek-R1-Distill-Llama-70B", - "name": "DeepSeek R1 Distill Llama 70B", - "family": "deepseek-thinking", - "attachment": false, + "id": "vivgrid/gpt-5.4", + "name": "GPT-5.4", + "family": "gpt", + "attachment": true, "reasoning": true, "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", + "temperature": false, + "knowledge": "2025-08-31", + "release_date": "2026-03-05", + "last_updated": "2026-03-05", "modalities": { "input": [ - "text" + "text", + "image", + "pdf" ], "output": [ "text" ] }, - "open_weights": true, + "open_weights": false, "cost": { - "input": 2.0, - "output": 2.0 + "input": 2.5, + "output": 15.0, + "cache_read": 0.25 }, "limit": { - "context": 130000, - "output": 4096 - } - }, - { - "id": "vultr/DeepSeek-R1-Distill-Qwen-32B", - "name": "DeepSeek R1 Distill Qwen 32B", - "family": "qwen", - "attachment": false, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.3, - "output": 0.3 - }, - "limit": { - "context": 130000, - "output": 4096 + "context": 400000, + "output": 128000 } }, { @@ -104829,35 +111157,6 @@ "output": 32768 } }, - { - "id": "vultr/Llama-3_1-Nemotron-Ultra-253B-v1", - "name": "Llama 3.1 Nemotron Ultra 253B v1", - "family": "llama", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.55, - "output": 1.8 - }, - "limit": { - "context": 32000, - "output": 4096 - } - }, { "id": "vultr/MiniMax-M2.5", "name": "MiniMax M2.5", @@ -104887,93 +111186,6 @@ "output": 4096 } }, - { - "id": "vultr/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", - "name": "NVIDIA Nemotron 3 Super 120B A12B NVFP4", - "family": "nemotron", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-01-20", - "last_updated": "2025-01-20", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.8 - }, - "limit": { - "context": 260000, - "output": 8192 - } - }, - { - "id": "vultr/Qwen2.5-Coder-32B-Instruct", - "name": "Qwen2.5 Coder 32B Instruct", - "family": "qwen", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2024-11-06", - "last_updated": "2024-11-06", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.2, - "output": 0.6 - }, - "limit": { - "context": 15000, - "output": 256 - } - }, - { - "id": "vultr/gpt-oss-120b", - "name": "GPT OSS 120B", - "family": "gpt-oss", - "attachment": false, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2024-10", - "release_date": "2025-06-23", - "last_updated": "2025-06-23", - "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] - }, - "open_weights": true, - "cost": { - "input": 0.15, - "output": 0.6 - }, - "limit": { - "context": 130000, - "output": 8192 - } - }, { "id": "wandb/MiniMaxAI/MiniMax-M2.5", "name": "MiniMax M2.5", @@ -105461,14 +111673,14 @@ }, { "id": "x-ai/grok-2", - "name": "Grok 2 Latest", + "name": "Grok 2 (1212)", "family": "grok", "attachment": false, "reasoning": false, "tool_call": true, "temperature": true, "knowledge": "2024-08", - "release_date": "2024-08-20", + "release_date": "2024-12-12", "last_updated": "2024-12-12", "modalities": { "input": [ @@ -105491,7 +111703,7 @@ }, { "id": "x-ai/grok-2-vision", - "name": "Grok 2 Vision Latest", + "name": "Grok 2 Vision", "family": "grok", "attachment": true, "reasoning": false, @@ -105499,7 +111711,7 @@ "temperature": true, "knowledge": "2024-08", "release_date": "2024-08-20", - "last_updated": "2024-12-12", + "last_updated": "2024-08-20", "modalities": { "input": [ "text", @@ -105522,7 +111734,7 @@ }, { "id": "x-ai/grok-3", - "name": "Grok 3 Latest", + "name": "Grok 3", "family": "grok", "attachment": false, "reasoning": false, @@ -105582,7 +111794,7 @@ }, { "id": "x-ai/grok-3-mini", - "name": "Grok 3 Mini", + "name": "Grok 3 Mini Latest", "family": "grok", "attachment": false, "reasoning": true, @@ -106408,6 +112620,36 @@ "output": 131072 } }, + { + "id": "zai-coding-plan/glm-5.1", + "name": "GLM-5.1", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "zai/glm-4.5", "name": "GLM-4.5", @@ -106656,6 +112898,37 @@ "output": 131072 } }, + { + "id": "zai/glm-4.7-flashx", + "name": "GLM-4.7-FlashX", + "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.07, + "output": 0.4, + "cache_read": 0.01, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "zai/glm-5", "name": "GLM-5", @@ -106747,38 +113020,6 @@ "output": 64000 } }, - { - "id": "zenmux/anthropic/claude-3.5-sonnet", - "name": "Claude 3.5 Sonnet (Retiring Soon)", - "attachment": true, - "reasoning": false, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01-01", - "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": 64000 - } - }, { "id": "zenmux/anthropic/claude-3.7-sonnet", "name": "Claude 3.7 Sonnet", @@ -106871,7 +113112,7 @@ }, "limit": { "context": 200000, - "output": 64000 + "output": 32000 } }, { @@ -107312,40 +113553,6 @@ "output": 64000 } }, - { - "id": "zenmux/google/gemini-3-pro-image-preview", - "name": "Gemini 3 Pro Image Preview", - "attachment": true, - "reasoning": true, - "tool_call": true, - "temperature": true, - "knowledge": "2025-01-01", - "release_date": "2025-03-20", - "last_updated": "2025-03-20", - "modalities": { - "input": [ - "text", - "image", - "pdf", - "audio", - "video" - ], - "output": [ - "text" - ] - }, - "open_weights": false, - "cost": { - "input": 2.0, - "output": 12.0, - "cache_read": 0.2, - "cache_write": 4.5 - }, - "limit": { - "context": 1048000, - "output": 64000 - } - }, { "id": "zenmux/google/gemini-3-pro-preview", "name": "Gemini 3 Pro Preview", @@ -107828,7 +114035,7 @@ "attachment": true, "reasoning": true, "tool_call": true, - "temperature": true, + "temperature": false, "knowledge": "2025-01-01", "release_date": "2026-01-27", "last_updated": "2026-01-27", @@ -109581,6 +115788,68 @@ "output": 131072 } }, + { + "id": "zhipuai-coding-plan/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, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, + { + "id": "zhipuai-coding-plan/glm-4.7-flashx", + "name": "GLM-4.7-FlashX", + "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.07, + "output": 0.4, + "cache_read": 0.01, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "zhipuai-coding-plan/glm-5", "name": "GLM-5", @@ -109641,6 +115910,36 @@ "output": 131072 } }, + { + "id": "zhipuai-coding-plan/glm-5.1", + "name": "GLM-5.1", + "family": "glm", + "attachment": false, + "reasoning": true, + "tool_call": true, + "temperature": true, + "release_date": "2026-03-27", + "last_updated": "2026-03-27", + "modalities": { + "input": [ + "text" + ], + "output": [ + "text" + ] + }, + "open_weights": false, + "cost": { + "input": 0.0, + "output": 0.0, + "cache_read": 0.0, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "zhipuai/glm-4.5", "name": "GLM-4.5", @@ -109889,6 +116188,37 @@ "output": 131072 } }, + { + "id": "zhipuai/glm-4.7-flashx", + "name": "GLM-4.7-FlashX", + "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.07, + "output": 0.4, + "cache_read": 0.01, + "cache_write": 0.0 + }, + "limit": { + "context": 200000, + "output": 131072 + } + }, { "id": "zhipuai/glm-5", "name": "GLM-5", diff --git a/crates/goose/src/providers/canonical/data/provider_metadata.json b/crates/goose/src/providers/canonical/data/provider_metadata.json index 9c98f3e4a6..17bae9b661 100644 --- a/crates/goose/src/providers/canonical/data/provider_metadata.json +++ b/crates/goose/src/providers/canonical/data/provider_metadata.json @@ -1,113 +1,36 @@ [ { - "id": "evroc", - "display_name": "evroc", + "id": "ollama-cloud", + "display_name": "Ollama Cloud", "npm": "@ai-sdk/openai-compatible", - "api": "https://models.think.evroc.com/v1", - "doc": "https://docs.evroc.com/products/think/overview.html", + "api": "https://ollama.com/v1", + "doc": "https://docs.ollama.com/cloud", "env": [ - "EVROC_API_KEY" + "OLLAMA_API_KEY" ], - "model_count": 13 + "model_count": 34 }, { - "id": "zai", - "display_name": "Z.AI", + "id": "moark", + "display_name": "Moark", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.z.ai/api/paas/v4", - "doc": "https://docs.z.ai/guides/overview/pricing", + "api": "https://moark.com/v1", + "doc": "https://moark.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90", "env": [ - "ZHIPU_API_KEY" + "MOARK_API_KEY" ], - "model_count": 10 + "model_count": 2 }, { - "id": "alibaba-coding-plan", - "display_name": "Alibaba Coding Plan", + "id": "drun", + "display_name": "D.Run (China)", "npm": "@ai-sdk/openai-compatible", - "api": "https://coding-intl.dashscope.aliyuncs.com/v1", - "doc": "https://www.alibabacloud.com/help/en/model-studio/coding-plan", + "api": "https://chat.d.run/v1", + "doc": "https://www.d.run", "env": [ - "ALIBABA_CODING_PLAN_API_KEY" + "DRUN_API_KEY" ], - "model_count": 8 - }, - { - "id": "zenmux", - "display_name": "ZenMux", - "npm": "@ai-sdk/anthropic", - "api": "https://zenmux.ai/api/anthropic/v1", - "doc": "https://docs.zenmux.ai", - "env": [ - "ZENMUX_API_KEY" - ], - "model_count": 87 - }, - { - "id": "io-net", - "display_name": "IO.NET", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.intelligence.io.solutions/api/v1", - "doc": "https://io.net/docs/guides/intelligence/io-intelligence", - "env": [ - "IOINTELLIGENCE_API_KEY" - ], - "model_count": 17 - }, - { - "id": "nvidia", - "display_name": "Nvidia", - "npm": "@ai-sdk/openai-compatible", - "api": "https://integrate.api.nvidia.com/v1", - "doc": "https://docs.api.nvidia.com/nim/", - "env": [ - "NVIDIA_API_KEY" - ], - "model_count": 74 - }, - { - "id": "fastrouter", - "display_name": "FastRouter", - "npm": "@ai-sdk/openai-compatible", - "api": "https://go.fastrouter.ai/api/v1", - "doc": "https://fastrouter.ai/models", - "env": [ - "FASTROUTER_API_KEY" - ], - "model_count": 15 - }, - { - "id": "iflowcn", - "display_name": "iFlow", - "npm": "@ai-sdk/openai-compatible", - "api": "https://apis.iflow.cn/v1", - "doc": "https://platform.iflow.cn/en/docs", - "env": [ - "IFLOW_API_KEY" - ], - "model_count": 14 - }, - { - "id": "modelscope", - "display_name": "ModelScope", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api-inference.modelscope.cn/v1", - "doc": "https://modelscope.cn/docs/model-service/API-Inference/intro", - "env": [ - "MODELSCOPE_API_KEY" - ], - "model_count": 7 - }, - { - "id": "meta-llama", - "display_name": "Llama", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.llama.com/compat/v1/", - "doc": "https://llama.developer.meta.com/docs/models", - "env": [ - "LLAMA_API_KEY" - ], - "model_count": 7 + "model_count": 3 }, { "id": "inference", @@ -120,204 +43,6 @@ ], "model_count": 9 }, - { - "id": "perplexity-agent", - "display_name": "Perplexity Agent", - "npm": "@ai-sdk/openai", - "api": "https://api.perplexity.ai/v1", - "doc": "https://docs.perplexity.ai/docs/agent-api/models", - "env": [ - "PERPLEXITY_API_KEY" - ], - "model_count": 16 - }, - { - "id": "xiaomi", - "display_name": "Xiaomi", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.xiaomimimo.com/v1", - "doc": "https://platform.xiaomimimo.com/#/docs", - "env": [ - "XIAOMI_API_KEY" - ], - "model_count": 3 - }, - { - "id": "synthetic", - "display_name": "Synthetic", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.synthetic.new/openai/v1", - "doc": "https://synthetic.new/pricing", - "env": [ - "SYNTHETIC_API_KEY" - ], - "model_count": 28 - }, - { - "id": "nebius", - "display_name": "Nebius Token Factory", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.tokenfactory.nebius.com/v1", - "doc": "https://docs.tokenfactory.nebius.com/", - "env": [ - "NEBIUS_API_KEY" - ], - "model_count": 49 - }, - { - "id": "qiniu-ai", - "display_name": "Qiniu", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.qnaigc.com/v1", - "doc": "https://developer.qiniu.com/aitokenapi", - "env": [ - "QINIU_API_KEY" - ], - "model_count": 91 - }, - { - "id": "ollama-cloud", - "display_name": "Ollama Cloud", - "npm": "@ai-sdk/openai-compatible", - "api": "https://ollama.com/v1", - "doc": "https://docs.ollama.com/cloud", - "env": [ - "OLLAMA_API_KEY" - ], - "model_count": 34 - }, - { - "id": "scaleway", - "display_name": "Scaleway", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.scaleway.ai/v1", - "doc": "https://www.scaleway.com/en/docs/generative-apis/", - "env": [ - "SCALEWAY_API_KEY" - ], - "model_count": 16 - }, - { - "id": "dinference", - "display_name": "DInference", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.dinference.com/v1", - "doc": "https://dinference.com", - "env": [ - "DINFERENCE_API_KEY" - ], - "model_count": 3 - }, - { - "id": "kuae-cloud-coding-plan", - "display_name": "KUAE Cloud Coding Plan", - "npm": "@ai-sdk/openai-compatible", - "api": "https://coding-plan-endpoint.kuaecloud.net/v1", - "doc": "https://docs.mthreads.com/kuaecloud/kuaecloud-doc-online/coding_plan/", - "env": [ - "KUAE_API_KEY" - ], - "model_count": 1 - }, - { - "id": "upstage", - "display_name": "Upstage", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.upstage.ai/v1/solar", - "doc": "https://developers.upstage.ai/docs/apis/chat", - "env": [ - "UPSTAGE_API_KEY" - ], - "model_count": 3 - }, - { - "id": "inception", - "display_name": "Inception", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.inceptionlabs.ai/v1/", - "doc": "https://platform.inceptionlabs.ai/docs", - "env": [ - "INCEPTION_API_KEY" - ], - "model_count": 4 - }, - { - "id": "submodel", - "display_name": "submodel", - "npm": "@ai-sdk/openai-compatible", - "api": "https://llm.submodel.ai/v1", - "doc": "https://submodel.gitbook.io", - "env": [ - "SUBMODEL_INSTAGEN_ACCESS_KEY" - ], - "model_count": 9 - }, - { - "id": "minimax-cn-coding-plan", - "display_name": "MiniMax Coding Plan (minimaxi.com)", - "npm": "@ai-sdk/anthropic", - "api": "https://api.minimaxi.com/anthropic/v1", - "doc": "https://platform.minimaxi.com/docs/coding-plan/intro", - "env": [ - "MINIMAX_API_KEY" - ], - "model_count": 6 - }, - { - "id": "novita-ai", - "display_name": "NovitaAI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.novita.ai/openai", - "doc": "https://novita.ai/docs/guides/introduction", - "env": [ - "NOVITA_API_KEY" - ], - "model_count": 84 - }, - { - "id": "opencode", - "display_name": "OpenCode Zen", - "npm": "@ai-sdk/openai-compatible", - "api": "https://opencode.ai/zen/v1", - "doc": "https://opencode.ai/docs/zen", - "env": [ - "OPENCODE_API_KEY" - ], - "model_count": 47 - }, - { - "id": "poe", - "display_name": "Poe", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.poe.com/v1", - "doc": "https://creator.poe.com/docs/external-applications/openai-compatible-api", - "env": [ - "POE_API_KEY" - ], - "model_count": 120 - }, - { - "id": "alibaba-coding-plan-cn", - "display_name": "Alibaba Coding Plan (China)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://coding.dashscope.aliyuncs.com/v1", - "doc": "https://help.aliyun.com/zh/model-studio/coding-plan", - "env": [ - "ALIBABA_CODING_PLAN_API_KEY" - ], - "model_count": 8 - }, - { - "id": "minimax-cn", - "display_name": "MiniMax (minimaxi.com)", - "npm": "@ai-sdk/anthropic", - "api": "https://api.minimaxi.com/anthropic/v1", - "doc": "https://platform.minimaxi.com/docs/guides/quickstart", - "env": [ - "MINIMAX_API_KEY" - ], - "model_count": 6 - }, { "id": "bailing", "display_name": "Bailing", @@ -330,50 +55,16 @@ "model_count": 2 }, { - "id": "alibaba", - "display_name": "Alibaba", + "id": "io-net", + "display_name": "IO.NET", "npm": "@ai-sdk/openai-compatible", - "api": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - "doc": "https://www.alibabacloud.com/help/en/model-studio/models", + "api": "https://api.intelligence.io.solutions/api/v1", + "doc": "https://io.net/docs/guides/intelligence/io-intelligence", "env": [ - "DASHSCOPE_API_KEY" - ], - "model_count": 41 - }, - { - "id": "cloudflare-workers-ai", - "display_name": "Cloudflare Workers AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", - "doc": "https://developers.cloudflare.com/workers-ai/models/", - "env": [ - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_KEY" - ], - "model_count": 42 - }, - { - "id": "wandb", - "display_name": "Weights & Biases", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.inference.wandb.ai/v1", - "doc": "https://docs.wandb.ai/guides/integrations/inference/", - "env": [ - "WANDB_API_KEY" + "IOINTELLIGENCE_API_KEY" ], "model_count": 17 }, - { - "id": "aihubmix", - "display_name": "AIHubMix", - "npm": "@ai-sdk/openai-compatible", - "api": "https://aihubmix.com/v1", - "doc": "https://docs.aihubmix.com", - "env": [ - "AIHUBMIX_API_KEY" - ], - "model_count": 48 - }, { "id": "minimax-coding-plan", "display_name": "MiniMax Coding Plan (minimax.io)", @@ -386,71 +77,170 @@ "model_count": 6 }, { - "id": "kimi-for-coding", - "display_name": "Kimi For Coding", - "npm": "@ai-sdk/anthropic", - "api": "https://api.kimi.com/coding/v1", - "doc": "https://www.kimi.com/coding/docs/en/third-party-agents.html", + "id": "wandb", + "display_name": "Weights & Biases", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.inference.wandb.ai/v1", + "doc": "https://docs.wandb.ai/guides/integrations/inference/", "env": [ - "KIMI_API_KEY" + "WANDB_API_KEY" ], - "model_count": 2 + "model_count": 17 }, { - "id": "abacus", - "display_name": "Abacus", + "id": "qiniu-ai", + "display_name": "Qiniu", "npm": "@ai-sdk/openai-compatible", - "api": "https://routellm.abacus.ai/v1", - "doc": "https://abacus.ai/help/api", + "api": "https://api.qnaigc.com/v1", + "doc": "https://developer.qiniu.com/aitokenapi", "env": [ - "ABACUS_API_KEY" + "QINIU_API_KEY" ], - "model_count": 65 + "model_count": 91 }, { - "id": "fireworks-ai", - "display_name": "Fireworks AI", + "id": "morph", + "display_name": "Morph", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.fireworks.ai/inference/v1/", - "doc": "https://fireworks.ai/docs/", + "api": "https://api.morphllm.com/v1", + "doc": "https://docs.morphllm.com/api-reference/introduction", "env": [ - "FIREWORKS_API_KEY" - ], - "model_count": 14 - }, - { - "id": "stepfun", - "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" + "MORPH_API_KEY" ], "model_count": 3 }, { - "id": "siliconflow", - "display_name": "SiliconFlow", + "id": "dinference", + "display_name": "DInference", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.siliconflow.com/v1", - "doc": "https://cloud.siliconflow.com/models", + "api": "https://api.dinference.com/v1", + "doc": "https://dinference.com", "env": [ - "SILICONFLOW_API_KEY" + "DINFERENCE_API_KEY" ], - "model_count": 70 + "model_count": 3 }, { - "id": "clarifai", - "display_name": "Clarifai", + "id": "meganova", + "display_name": "Meganova", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.clarifai.com/v2/ext/openai/v1", - "doc": "https://docs.clarifai.com/compute/inference/", + "api": "https://api.meganova.ai/v1", + "doc": "https://docs.meganova.ai", "env": [ - "CLARIFAI_PAT" + "MEGANOVA_API_KEY" + ], + "model_count": 19 + }, + { + "id": "zai-coding-plan", + "display_name": "Z.AI Coding Plan", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.z.ai/api/coding/paas/v4", + "doc": "https://docs.z.ai/devpack/overview", + "env": [ + "ZHIPU_API_KEY" + ], + "model_count": 12 + }, + { + "id": "nova", + "display_name": "Nova", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.nova.amazon.com/v1", + "doc": "https://nova.amazon.com/dev/documentation", + "env": [ + "NOVA_API_KEY" + ], + "model_count": 2 + }, + { + "id": "upstage", + "display_name": "Upstage", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.upstage.ai/v1/solar", + "doc": "https://developers.upstage.ai/docs/apis/chat", + "env": [ + "UPSTAGE_API_KEY" + ], + "model_count": 3 + }, + { + "id": "tencent-coding-plan", + "display_name": "Tencent Coding Plan (China)", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.lkeap.cloud.tencent.com/coding/v3", + "doc": "https://cloud.tencent.com/document/product/1772/128947", + "env": [ + "TENCENT_CODING_PLAN_API_KEY" + ], + "model_count": 8 + }, + { + "id": "jiekou", + "display_name": "Jiekou.AI", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.jiekou.ai/openai", + "doc": "https://docs.jiekou.ai/docs/support/quickstart?utm_source=github_models.dev", + "env": [ + "JIEKOU_API_KEY" + ], + "model_count": 61 + }, + { + "id": "deepseek", + "display_name": "DeepSeek", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.deepseek.com", + "doc": "https://api-docs.deepseek.com/quick_start/pricing", + "env": [ + "DEEPSEEK_API_KEY" + ], + "model_count": 2 + }, + { + "id": "meta-llama", + "display_name": "Llama", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.llama.com/compat/v1/", + "doc": "https://llama.developer.meta.com/docs/models", + "env": [ + "LLAMA_API_KEY" + ], + "model_count": 7 + }, + { + "id": "zai", + "display_name": "Z.AI", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.z.ai/api/paas/v4", + "doc": "https://docs.z.ai/guides/overview/pricing", + "env": [ + "ZHIPU_API_KEY" ], "model_count": 11 }, + { + "id": "poe", + "display_name": "Poe", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.poe.com/v1", + "doc": "https://creator.poe.com/docs/external-applications/openai-compatible-api", + "env": [ + "POE_API_KEY" + ], + "model_count": 124 + }, + { + "id": "opencode", + "display_name": "OpenCode Zen", + "npm": "@ai-sdk/openai-compatible", + "api": "https://opencode.ai/zen/v1", + "doc": "https://opencode.ai/docs/zen", + "env": [ + "OPENCODE_API_KEY" + ], + "model_count": 48 + }, { "id": "berget", "display_name": "Berget.AI", @@ -474,26 +264,70 @@ "model_count": 2 }, { - "id": "zhipuai-coding-plan", - "display_name": "Zhipu AI Coding Plan", + "id": "zhipuai", + "display_name": "Zhipu AI", "npm": "@ai-sdk/openai-compatible", - "api": "https://open.bigmodel.cn/api/coding/paas/v4", - "doc": "https://docs.bigmodel.cn/cn/coding-plan/overview", + "api": "https://open.bigmodel.cn/api/paas/v4", + "doc": "https://docs.z.ai/guides/overview/pricing", "env": [ "ZHIPU_API_KEY" ], "model_count": 10 }, { - "id": "deepseek", - "display_name": "DeepSeek", + "id": "nvidia", + "display_name": "Nvidia", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.deepseek.com", - "doc": "https://api-docs.deepseek.com/quick_start/pricing", + "api": "https://integrate.api.nvidia.com/v1", + "doc": "https://docs.api.nvidia.com/nim/", "env": [ - "DEEPSEEK_API_KEY" + "NVIDIA_API_KEY" ], - "model_count": 2 + "model_count": 74 + }, + { + "id": "nebius", + "display_name": "Nebius Token Factory", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.tokenfactory.nebius.com/v1", + "doc": "https://docs.tokenfactory.nebius.com/", + "env": [ + "NEBIUS_API_KEY" + ], + "model_count": 49 + }, + { + "id": "firmware", + "display_name": "Firmware", + "npm": "@ai-sdk/openai-compatible", + "api": "https://app.frogbot.ai/api/v1", + "doc": "https://docs.frogbot.ai", + "env": [ + "FIRMWARE_API_KEY" + ], + "model_count": 24 + }, + { + "id": "cloudferro-sherlock", + "display_name": "CloudFerro Sherlock", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api-sherlock.cloudferro.com/openai/v1/", + "doc": "https://docs.sherlock.cloudferro.com/", + "env": [ + "CLOUDFERRO_SHERLOCK_API_KEY" + ], + "model_count": 5 + }, + { + "id": "chutes", + "display_name": "Chutes", + "npm": "@ai-sdk/openai-compatible", + "api": "https://llm.chutes.ai/v1", + "doc": "https://llm.chutes.ai/v1/models", + "env": [ + "CHUTES_API_KEY" + ], + "model_count": 68 }, { "id": "lmstudio", @@ -507,15 +341,48 @@ "model_count": 3 }, { - "id": "github-models", - "display_name": "GitHub Models", - "npm": "@ai-sdk/openai-compatible", - "api": "https://models.github.ai/inference", - "doc": "https://docs.github.com/en/github-models", + "id": "kimi-for-coding", + "display_name": "Kimi For Coding", + "npm": "@ai-sdk/anthropic", + "api": "https://api.kimi.com/coding/v1", + "doc": "https://www.kimi.com/coding/docs/en/third-party-agents.html", "env": [ - "GITHUB_TOKEN" + "KIMI_API_KEY" ], - "model_count": 55 + "model_count": 2 + }, + { + "id": "alibaba-cn", + "display_name": "Alibaba (China)", + "npm": "@ai-sdk/openai-compatible", + "api": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "doc": "https://www.alibabacloud.com/help/en/model-studio/models", + "env": [ + "DASHSCOPE_API_KEY" + ], + "model_count": 74 + }, + { + "id": "requesty", + "display_name": "Requesty", + "npm": "@ai-sdk/openai-compatible", + "api": "https://router.requesty.ai/v1", + "doc": "https://requesty.ai/solution/llm-routing/models", + "env": [ + "REQUESTY_API_KEY" + ], + "model_count": 38 + }, + { + "id": "friendli", + "display_name": "Friendli", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.friendli.ai/serverless/v1", + "doc": "https://friendli.ai/docs/guides/serverless_endpoints/introduction", + "env": [ + "FRIENDLI_TOKEN" + ], + "model_count": 7 }, { "id": "302ai", @@ -529,26 +396,92 @@ "model_count": 64 }, { - "id": "github-copilot", - "display_name": "GitHub Copilot", + "id": "novita-ai", + "display_name": "NovitaAI", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.githubcopilot.com", - "doc": "https://docs.github.com/en/copilot", + "api": "https://api.novita.ai/openai", + "doc": "https://novita.ai/docs/guides/introduction", "env": [ - "GITHUB_TOKEN" + "NOVITA_API_KEY" ], - "model_count": 25 + "model_count": 84 }, { - "id": "moonshotai", - "display_name": "Moonshot AI", + "id": "cortecs", + "display_name": "Cortecs", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.moonshot.ai/v1", - "doc": "https://platform.moonshot.ai/docs/api/chat", + "api": "https://api.cortecs.ai/v1", + "doc": "https://api.cortecs.ai/v1/models", "env": [ - "MOONSHOT_API_KEY" + "CORTECS_API_KEY" ], - "model_count": 6 + "model_count": 28 + }, + { + "id": "siliconflow-cn", + "display_name": "SiliconFlow (China)", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.siliconflow.cn/v1", + "doc": "https://cloud.siliconflow.com/models", + "env": [ + "SILICONFLOW_CN_API_KEY" + ], + "model_count": 78 + }, + { + "id": "evroc", + "display_name": "evroc", + "npm": "@ai-sdk/openai-compatible", + "api": "https://models.think.evroc.com/v1", + "doc": "https://docs.evroc.com/products/think/overview.html", + "env": [ + "EVROC_API_KEY" + ], + "model_count": 13 + }, + { + "id": "kilo", + "display_name": "Kilo Gateway", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.kilo.ai/api/gateway", + "doc": "https://kilo.ai", + "env": [ + "KILO_API_KEY" + ], + "model_count": 335 + }, + { + "id": "kuae-cloud-coding-plan", + "display_name": "KUAE Cloud Coding Plan", + "npm": "@ai-sdk/openai-compatible", + "api": "https://coding-plan-endpoint.kuaecloud.net/v1", + "doc": "https://docs.mthreads.com/kuaecloud/kuaecloud-doc-online/coding_plan/", + "env": [ + "KUAE_API_KEY" + ], + "model_count": 1 + }, + { + "id": "modelscope", + "display_name": "ModelScope", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api-inference.modelscope.cn/v1", + "doc": "https://modelscope.cn/docs/model-service/API-Inference/intro", + "env": [ + "MODELSCOPE_API_KEY" + ], + "model_count": 7 + }, + { + "id": "zenmux", + "display_name": "ZenMux", + "npm": "@ai-sdk/openai-compatible", + "api": "https://zenmux.ai/api/v1", + "doc": "https://docs.zenmux.ai", + "env": [ + "ZENMUX_API_KEY" + ], + "model_count": 85 }, { "id": "privatemode-ai", @@ -562,6 +495,17 @@ ], "model_count": 5 }, + { + "id": "perplexity-agent", + "display_name": "Perplexity Agent", + "npm": "@ai-sdk/openai", + "api": "https://api.perplexity.ai/v1", + "doc": "https://docs.perplexity.ai/docs/agent-api/models", + "env": [ + "PERPLEXITY_API_KEY" + ], + "model_count": 16 + }, { "id": "vivgrid", "display_name": "Vivgrid", @@ -571,151 +515,8 @@ "env": [ "VIVGRID_API_KEY" ], - "model_count": 8 - }, - { - "id": "moonshotai-cn", - "display_name": "Moonshot AI (China)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.moonshot.cn/v1", - "doc": "https://platform.moonshot.cn/docs/api/chat", - "env": [ - "MOONSHOT_API_KEY" - ], - "model_count": 6 - }, - { - "id": "zhipuai", - "display_name": "Zhipu AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://open.bigmodel.cn/api/paas/v4", - "doc": "https://docs.z.ai/guides/overview/pricing", - "env": [ - "ZHIPU_API_KEY" - ], "model_count": 9 }, - { - "id": "nova", - "display_name": "Nova", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.nova.amazon.com/v1", - "doc": "https://nova.amazon.com/dev/documentation", - "env": [ - "NOVA_API_KEY" - ], - "model_count": 2 - }, - { - "id": "zai-coding-plan", - "display_name": "Z.AI Coding Plan", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.z.ai/api/coding/paas/v4", - "doc": "https://docs.z.ai/devpack/overview", - "env": [ - "ZHIPU_API_KEY" - ], - "model_count": 11 - }, - { - "id": "opencode-go", - "display_name": "OpenCode Go", - "npm": "@ai-sdk/openai-compatible", - "api": "https://opencode.ai/zen/go/v1", - "doc": "https://opencode.ai/docs/zen", - "env": [ - "OPENCODE_API_KEY" - ], - "model_count": 4 - }, - { - "id": "drun", - "display_name": "D.Run (China)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://chat.d.run/v1", - "doc": "https://www.d.run", - "env": [ - "DRUN_API_KEY" - ], - "model_count": 3 - }, - { - "id": "firmware", - "display_name": "Firmware", - "npm": "@ai-sdk/openai-compatible", - "api": "https://app.frogbot.ai/api/v1", - "doc": "https://docs.frogbot.ai", - "env": [ - "FIRMWARE_API_KEY" - ], - "model_count": 24 - }, - { - "id": "ovhcloud", - "display_name": "OVHcloud AI Endpoints", - "npm": "@ai-sdk/openai-compatible", - "api": "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", - "doc": "https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog//", - "env": [ - "OVHCLOUD_API_KEY" - ], - "model_count": 13 - }, - { - "id": "stackit", - "display_name": "STACKIT", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1", - "doc": "https://docs.stackit.cloud/products/data-and-ai/ai-model-serving/basics/available-shared-models", - "env": [ - "STACKIT_API_KEY" - ], - "model_count": 8 - }, - { - "id": "cloudferro-sherlock", - "display_name": "CloudFerro Sherlock", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api-sherlock.cloudferro.com/openai/v1/", - "doc": "https://docs.sherlock.cloudferro.com/", - "env": [ - "CLOUDFERRO_SHERLOCK_API_KEY" - ], - "model_count": 5 - }, - { - "id": "requesty", - "display_name": "Requesty", - "npm": "@ai-sdk/openai-compatible", - "api": "https://router.requesty.ai/v1", - "doc": "https://requesty.ai/solution/llm-routing/models", - "env": [ - "REQUESTY_API_KEY" - ], - "model_count": 38 - }, - { - "id": "qihang-ai", - "display_name": "QiHang", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.qhaigc.net/v1", - "doc": "https://www.qhaigc.net/docs", - "env": [ - "QIHANG_API_KEY" - ], - "model_count": 9 - }, - { - "id": "siliconflow-cn", - "display_name": "SiliconFlow (China)", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.siliconflow.cn/v1", - "doc": "https://cloud.siliconflow.com/models", - "env": [ - "SILICONFLOW_CN_API_KEY" - ], - "model_count": 78 - }, { "id": "helicone", "display_name": "Helicone", @@ -727,28 +528,6 @@ ], "model_count": 91 }, - { - "id": "moark", - "display_name": "Moark", - "npm": "@ai-sdk/openai-compatible", - "api": "https://moark.com/v1", - "doc": "https://moark.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90", - "env": [ - "MOARK_API_KEY" - ], - "model_count": 2 - }, - { - "id": "morph", - "display_name": "Morph", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.morphllm.com/v1", - "doc": "https://docs.morphllm.com/api-reference/introduction", - "env": [ - "MORPH_API_KEY" - ], - "model_count": 3 - }, { "id": "minimax", "display_name": "MiniMax (minimax.io)", @@ -761,48 +540,26 @@ "model_count": 6 }, { - "id": "vultr", - "display_name": "Vultr", + "id": "alibaba-coding-plan-cn", + "display_name": "Alibaba Coding Plan (China)", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.vultrinference.com/v1", - "doc": "https://api.vultrinference.com/", + "api": "https://coding.dashscope.aliyuncs.com/v1", + "doc": "https://help.aliyun.com/zh/model-studio/coding-plan", "env": [ - "VULTR_API_KEY" + "ALIBABA_CODING_PLAN_API_KEY" ], - "model_count": 10 + "model_count": 8 }, { - "id": "baseten", - "display_name": "Baseten", + "id": "xiaomi", + "display_name": "Xiaomi", "npm": "@ai-sdk/openai-compatible", - "api": "https://inference.baseten.co/v1", - "doc": "https://docs.baseten.co/development/model-apis/overview", + "api": "https://api.xiaomimimo.com/v1", + "doc": "https://platform.xiaomimimo.com/#/docs", "env": [ - "BASETEN_API_KEY" + "XIAOMI_API_KEY" ], - "model_count": 12 - }, - { - "id": "jiekou", - "display_name": "Jiekou.AI", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.jiekou.ai/openai", - "doc": "https://docs.jiekou.ai/docs/support/quickstart?utm_source=github_models.dev", - "env": [ - "JIEKOU_API_KEY" - ], - "model_count": 61 - }, - { - "id": "meganova", - "display_name": "Meganova", - "npm": "@ai-sdk/openai-compatible", - "api": "https://api.meganova.ai/v1", - "doc": "https://docs.meganova.ai", - "env": [ - "MEGANOVA_API_KEY" - ], - "model_count": 19 + "model_count": 3 }, { "id": "huggingface", @@ -816,37 +573,202 @@ "model_count": 20 }, { - "id": "tencent-coding-plan", - "display_name": "Tencent Coding Plan (China)", + "id": "stepfun", + "display_name": "StepFun", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.lkeap.cloud.tencent.com/coding/v3", - "doc": "https://cloud.tencent.com/document/product/1772/128947", + "api": "https://api.stepfun.com/v1", + "doc": "https://platform.stepfun.com/docs/zh/overview/concept", "env": [ - "TENCENT_CODING_PLAN_API_KEY" + "STEPFUN_API_KEY" ], - "model_count": 8 + "model_count": 3 }, { - "id": "friendli", - "display_name": "Friendli", + "id": "fastrouter", + "display_name": "FastRouter", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.friendli.ai/serverless/v1", - "doc": "https://friendli.ai/docs/guides/serverless_endpoints/introduction", + "api": "https://go.fastrouter.ai/api/v1", + "doc": "https://fastrouter.ai/models", "env": [ - "FRIENDLI_TOKEN" + "FASTROUTER_API_KEY" ], - "model_count": 7 + "model_count": 15 }, { - "id": "kilo", - "display_name": "Kilo Gateway", + "id": "baseten", + "display_name": "Baseten", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.kilo.ai/api/gateway", - "doc": "https://kilo.ai", + "api": "https://inference.baseten.co/v1", + "doc": "https://docs.baseten.co/development/model-apis/overview", "env": [ - "KILO_API_KEY" + "BASETEN_API_KEY" ], - "model_count": 335 + "model_count": 12 + }, + { + "id": "synthetic", + "display_name": "Synthetic", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.synthetic.new/openai/v1", + "doc": "https://synthetic.new/pricing", + "env": [ + "SYNTHETIC_API_KEY" + ], + "model_count": 28 + }, + { + "id": "llmgateway", + "display_name": "LLM Gateway", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.llmgateway.io/v1", + "doc": "https://llmgateway.io/docs", + "env": [ + "LLMGATEWAY_API_KEY" + ], + "model_count": 203 + }, + { + "id": "aihubmix", + "display_name": "AIHubMix", + "npm": "@ai-sdk/openai-compatible", + "api": "https://aihubmix.com/v1", + "doc": "https://docs.aihubmix.com", + "env": [ + "AIHUBMIX_API_KEY" + ], + "model_count": 48 + }, + { + "id": "minimax-cn-coding-plan", + "display_name": "MiniMax Coding Plan (minimaxi.com)", + "npm": "@ai-sdk/anthropic", + "api": "https://api.minimaxi.com/anthropic/v1", + "doc": "https://platform.minimaxi.com/docs/coding-plan/intro", + "env": [ + "MINIMAX_API_KEY" + ], + "model_count": 6 + }, + { + "id": "ovhcloud", + "display_name": "OVHcloud AI Endpoints", + "npm": "@ai-sdk/openai-compatible", + "api": "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", + "doc": "https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog//", + "env": [ + "OVHCLOUD_API_KEY" + ], + "model_count": 13 + }, + { + "id": "minimax-cn", + "display_name": "MiniMax (minimaxi.com)", + "npm": "@ai-sdk/anthropic", + "api": "https://api.minimaxi.com/anthropic/v1", + "doc": "https://platform.minimaxi.com/docs/guides/quickstart", + "env": [ + "MINIMAX_API_KEY" + ], + "model_count": 6 + }, + { + "id": "qihang-ai", + "display_name": "QiHang", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.qhaigc.net/v1", + "doc": "https://www.qhaigc.net/docs", + "env": [ + "QIHANG_API_KEY" + ], + "model_count": 9 + }, + { + "id": "moonshotai", + "display_name": "Moonshot AI", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.moonshot.ai/v1", + "doc": "https://platform.moonshot.ai/docs/api/chat", + "env": [ + "MOONSHOT_API_KEY" + ], + "model_count": 6 + }, + { + "id": "alibaba", + "display_name": "Alibaba", + "npm": "@ai-sdk/openai-compatible", + "api": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + "doc": "https://www.alibabacloud.com/help/en/model-studio/models", + "env": [ + "DASHSCOPE_API_KEY" + ], + "model_count": 41 + }, + { + "id": "github-copilot", + "display_name": "GitHub Copilot", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.githubcopilot.com", + "doc": "https://docs.github.com/en/copilot", + "env": [ + "GITHUB_TOKEN" + ], + "model_count": 25 + }, + { + "id": "scaleway", + "display_name": "Scaleway", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.scaleway.ai/v1", + "doc": "https://www.scaleway.com/en/docs/generative-apis/", + "env": [ + "SCALEWAY_API_KEY" + ], + "model_count": 16 + }, + { + "id": "iflowcn", + "display_name": "iFlow", + "npm": "@ai-sdk/openai-compatible", + "api": "https://apis.iflow.cn/v1", + "doc": "https://platform.iflow.cn/en/docs", + "env": [ + "IFLOW_API_KEY" + ], + "model_count": 14 + }, + { + "id": "submodel", + "display_name": "submodel", + "npm": "@ai-sdk/openai-compatible", + "api": "https://llm.submodel.ai/v1", + "doc": "https://submodel.gitbook.io", + "env": [ + "SUBMODEL_INSTAGEN_ACCESS_KEY" + ], + "model_count": 9 + }, + { + "id": "vultr", + "display_name": "Vultr", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.vultrinference.com/v1", + "doc": "https://api.vultrinference.com/", + "env": [ + "VULTR_API_KEY" + ], + "model_count": 4 + }, + { + "id": "github-models", + "display_name": "GitHub Models", + "npm": "@ai-sdk/openai-compatible", + "api": "https://models.github.ai/inference", + "doc": "https://docs.github.com/en/github-models", + "env": [ + "GITHUB_TOKEN" + ], + "model_count": 55 }, { "id": "nano-gpt", @@ -857,39 +779,128 @@ "env": [ "NANO_GPT_API_KEY" ], - "model_count": 517 + "model_count": 519 }, { - "id": "cortecs", - "display_name": "Cortecs", + "id": "clarifai", + "display_name": "Clarifai", "npm": "@ai-sdk/openai-compatible", - "api": "https://api.cortecs.ai/v1", - "doc": "https://api.cortecs.ai/v1/models", + "api": "https://api.clarifai.com/v2/ext/openai/v1", + "doc": "https://docs.clarifai.com/compute/inference/", "env": [ - "CORTECS_API_KEY" + "CLARIFAI_PAT" ], - "model_count": 28 + "model_count": 11 }, { - "id": "alibaba-cn", - "display_name": "Alibaba (China)", + "id": "stackit", + "display_name": "STACKIT", "npm": "@ai-sdk/openai-compatible", - "api": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "doc": "https://www.alibabacloud.com/help/en/model-studio/models", + "api": "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1", + "doc": "https://docs.stackit.cloud/products/data-and-ai/ai-model-serving/basics/available-shared-models", "env": [ - "DASHSCOPE_API_KEY" + "STACKIT_API_KEY" ], - "model_count": 74 + "model_count": 8 }, { - "id": "chutes", - "display_name": "Chutes", + "id": "cloudflare-workers-ai", + "display_name": "Cloudflare Workers AI", "npm": "@ai-sdk/openai-compatible", - "api": "https://llm.chutes.ai/v1", - "doc": "https://llm.chutes.ai/v1/models", + "api": "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", + "doc": "https://developers.cloudflare.com/workers-ai/models/", "env": [ - "CHUTES_API_KEY" + "CLOUDFLARE_ACCOUNT_ID", + "CLOUDFLARE_API_KEY" ], - "model_count": 68 + "model_count": 42 + }, + { + "id": "siliconflow", + "display_name": "SiliconFlow", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.siliconflow.com/v1", + "doc": "https://cloud.siliconflow.com/models", + "env": [ + "SILICONFLOW_API_KEY" + ], + "model_count": 71 + }, + { + "id": "alibaba-coding-plan", + "display_name": "Alibaba Coding Plan", + "npm": "@ai-sdk/openai-compatible", + "api": "https://coding-intl.dashscope.aliyuncs.com/v1", + "doc": "https://www.alibabacloud.com/help/en/model-studio/coding-plan", + "env": [ + "ALIBABA_CODING_PLAN_API_KEY" + ], + "model_count": 8 + }, + { + "id": "inception", + "display_name": "Inception", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.inceptionlabs.ai/v1/", + "doc": "https://platform.inceptionlabs.ai/docs", + "env": [ + "INCEPTION_API_KEY" + ], + "model_count": 4 + }, + { + "id": "zhipuai-coding-plan", + "display_name": "Zhipu AI Coding Plan", + "npm": "@ai-sdk/openai-compatible", + "api": "https://open.bigmodel.cn/api/coding/paas/v4", + "doc": "https://docs.bigmodel.cn/cn/coding-plan/overview", + "env": [ + "ZHIPU_API_KEY" + ], + "model_count": 13 + }, + { + "id": "moonshotai-cn", + "display_name": "Moonshot AI (China)", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.moonshot.cn/v1", + "doc": "https://platform.moonshot.cn/docs/api/chat", + "env": [ + "MOONSHOT_API_KEY" + ], + "model_count": 6 + }, + { + "id": "fireworks-ai", + "display_name": "Fireworks AI", + "npm": "@ai-sdk/openai-compatible", + "api": "https://api.fireworks.ai/inference/v1/", + "doc": "https://fireworks.ai/docs/", + "env": [ + "FIREWORKS_API_KEY" + ], + "model_count": 14 + }, + { + "id": "opencode-go", + "display_name": "OpenCode Go", + "npm": "@ai-sdk/openai-compatible", + "api": "https://opencode.ai/zen/go/v1", + "doc": "https://opencode.ai/docs/zen", + "env": [ + "OPENCODE_API_KEY" + ], + "model_count": 4 + }, + { + "id": "abacus", + "display_name": "Abacus", + "npm": "@ai-sdk/openai-compatible", + "api": "https://routellm.abacus.ai/v1", + "doc": "https://abacus.ai/help/api", + "env": [ + "ABACUS_API_KEY" + ], + "model_count": 65 } ] \ No newline at end of file diff --git a/crates/goose/src/providers/canonical/name_builder.rs b/crates/goose/src/providers/canonical/name_builder.rs index 4fa89f4d19..f7fb000f56 100644 --- a/crates/goose/src/providers/canonical/name_builder.rs +++ b/crates/goose/src/providers/canonical/name_builder.rs @@ -47,6 +47,7 @@ fn map_provider_name(provider: &str) -> &str { "aws_bedrock" => "amazon-bedrock", "gcp_vertex_ai" => "google-vertex", "gemini_oauth" => "google", + "zhipu" => "zhipuai", _ => provider, } } @@ -490,6 +491,16 @@ mod tests { Some("x-ai/grok-3".to_string()) ); + // === Zhipu AI === + assert_eq!( + map_to_canonical_model("zhipu", "glm-4.7", r), + Some("zhipuai/glm-4.7".to_string()) + ); + assert_eq!( + map_to_canonical_model("zhipu", "glm-5", r), + Some("zhipuai/glm-5".to_string()) + ); + // === GCP Vertex AI === assert_eq!( map_to_canonical_model("gcp_vertex_ai", "gemini-2.5-flash", r), diff --git a/crates/goose/src/providers/claude_acp.rs b/crates/goose/src/providers/claude_acp.rs index 3d8c6a95fa..9fb25be28f 100644 --- a/crates/goose/src/providers/claude_acp.rs +++ b/crates/goose/src/providers/claude_acp.rs @@ -4,8 +4,7 @@ use std::collections::HashMap; use std::path::PathBuf; use crate::acp::{ - extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping, - ACP_CURRENT_MODEL, + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; @@ -51,13 +50,6 @@ impl ProviderDef for ClaudeAcpProvider { .resolve(CLAUDE_ACP_BINARY)?; let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); - // claude-agent-acp permission option_ids - let permission_mapping = PermissionMapping { - allow_option_id: Some("allow".to_string()), - reject_option_id: Some("reject".to_string()), - rejected_tool_status: sacp::schema::ToolCallStatus::Failed, - }; - let mode_mapping = HashMap::from([ // Closest to "autonomous": bypassPermissions skips confirmations. (GooseMode::Auto, "bypassPermissions".to_string()), @@ -79,7 +71,6 @@ impl ProviderDef for ClaudeAcpProvider { mcp_servers: extension_configs_to_mcp_servers(&extensions), session_mode_id: Some(mode_mapping[&goose_mode].clone()), mode_mapping, - permission_mapping, notification_callback: None, }; diff --git a/crates/goose/src/providers/codex_acp.rs b/crates/goose/src/providers/codex_acp.rs index bdecb7db9e..f28bd02917 100644 --- a/crates/goose/src/providers/codex_acp.rs +++ b/crates/goose/src/providers/codex_acp.rs @@ -4,8 +4,7 @@ use std::collections::HashMap; use std::path::PathBuf; use crate::acp::{ - extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping, - ACP_CURRENT_MODEL, + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; @@ -74,13 +73,6 @@ impl ProviderDef for CodexAcpProvider { ]); } - // codex-acp permission option_ids - let permission_mapping = PermissionMapping { - allow_option_id: Some("approved".to_string()), - reject_option_id: Some("abort".to_string()), - rejected_tool_status: sacp::schema::ToolCallStatus::Failed, - }; - // Chat and Approve both map to "read-only". let mode_mapping = HashMap::from([ (GooseMode::Auto, "full-access".to_string()), @@ -99,7 +91,6 @@ impl ProviderDef for CodexAcpProvider { // Disabled until https://github.com/zed-industries/codex-acp/issues/179 is fixed. session_mode_id: None, mode_mapping, - permission_mapping, notification_callback: None, }; diff --git a/crates/goose/src/providers/copilot_acp.rs b/crates/goose/src/providers/copilot_acp.rs index 662a2d65b5..9810547ff3 100644 --- a/crates/goose/src/providers/copilot_acp.rs +++ b/crates/goose/src/providers/copilot_acp.rs @@ -4,8 +4,7 @@ use std::collections::HashMap; use std::path::PathBuf; use crate::acp::{ - extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping, - ACP_CURRENT_MODEL, + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; @@ -54,10 +53,6 @@ impl ProviderDef for CopilotAcpProvider { .resolve(COPILOT_ACP_BINARY)?; let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); - // Copilot uses standard ACP permission option IDs (allow_once, - // allow_always, reject_once) so kind-based fallback handles them. - let permission_mapping = PermissionMapping::default(); - let mut args = vec!["--acp".to_string()]; if model.model_name != ACP_CURRENT_MODEL { args.push("--model".to_string()); @@ -82,7 +77,6 @@ impl ProviderDef for CopilotAcpProvider { mcp_servers: extension_configs_to_mcp_servers(&extensions), session_mode_id: Some(mode_mapping[&goose_mode].clone()), mode_mapping, - permission_mapping, notification_callback: None, }; diff --git a/crates/goose/src/providers/declarative/zhipu.json b/crates/goose/src/providers/declarative/zhipu.json new file mode 100644 index 0000000000..bc22bc76ee --- /dev/null +++ b/crates/goose/src/providers/declarative/zhipu.json @@ -0,0 +1,28 @@ +{ + "name": "zhipu", + "engine": "openai", + "display_name": "Zhipu AI", + "description": "Zhipu AI GLM models. Set ZHIPU_BASE_URL to override the default endpoint (e.g. https://open.bigmodel.cn/api/coding/paas/v4 for Coding Plan models).", + "api_key_env": "ZHIPU_API_KEY", + "base_url": "${ZHIPU_BASE_URL}", + "env_vars": [ + { + "name": "ZHIPU_BASE_URL", + "required": false, + "secret": false, + "default": "https://open.bigmodel.cn/api/paas/v4", + "description": "Zhipu API base URL. Change to https://open.bigmodel.cn/api/coding/paas/v4 for Coding Plan models like GLM-5.1." + } + ], + "catalog_provider_id": "zhipuai", + "dynamic_models": true, + "models": [ + {"name": "glm-4.5", "context_limit": 131072}, + {"name": "glm-4.5-flash", "context_limit": 131072}, + {"name": "glm-4.6", "context_limit": 204800}, + {"name": "glm-4.7", "context_limit": 204800}, + {"name": "glm-4.7-flash", "context_limit": 200000}, + {"name": "glm-5", "context_limit": 204800} + ], + "supports_streaming": true +} diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 1dda9186a7..d0029c5caf 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -340,9 +340,117 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< messages_spec.extend(output); } + merge_split_tool_call_messages(&mut messages_spec); messages_spec } +/// The agent splits a single assistant response with N tool_calls into N +/// interleaved `asst(TC)/tool` pairs, cloning `reasoning_content` onto each. +/// This function merges them back into one assistant message with all tool_calls, +/// followed by the tool results β€” the standard OpenAI format. +/// +/// Only merges when `reasoning_content` is present and matches, since that is +/// the only signal that messages were split from the same turn. +fn merge_split_tool_call_messages(messages: &mut Vec) { + let mut i = 0; + while i < messages.len() { + let is_assistant_tool_call = messages[i].get("role") == Some(&json!("assistant")) + && messages[i] + .get("tool_calls") + .and_then(|tc| tc.as_array()) + .is_some_and(|a| !a.is_empty()); + let base_reasoning = messages[i].get("reasoning_content"); + + if !is_assistant_tool_call || base_reasoning.is_none() { + i += 1; + continue; + } + let base_reasoning = base_reasoning.unwrap().clone(); + + let mut extra_tool_calls: Vec = Vec::new(); + let mut collected: Vec = Vec::new(); + let mut scan = i + 1; + + loop { + if scan >= messages.len() || messages[scan].get("role") != Some(&json!("tool")) { + break; + } + + // Skip past tool result and any image-only user messages that + // format_messages inserts after tool results containing images. + let mut peek = scan + 1; + while peek < messages.len() && is_image_only_user_message(&messages[peek]) { + peek += 1; + } + + if peek >= messages.len() { + break; + } + let next = &messages[peek]; + let has_no_content = next.get("content").is_none_or(|c| { + c.is_null() + || c.as_str().is_some_and(|s| s.is_empty()) + || c.as_array().is_some_and(|a| a.is_empty()) + }); + let is_split = next.get("role") == Some(&json!("assistant")) + && next + .get("tool_calls") + .and_then(|tc| tc.as_array()) + .is_some_and(|a| !a.is_empty()) + && has_no_content + && next.get("reasoning_content") == Some(&base_reasoning); + + if !is_split { + break; + } + + collected.extend(messages[scan..peek].iter().cloned()); + if let Some(tc) = messages[peek] + .get("tool_calls") + .and_then(|tc| tc.as_array()) + { + extra_tool_calls.extend(tc.iter().cloned()); + } + scan = peek + 1; + } + + if extra_tool_calls.is_empty() { + i += 1; + continue; + } + + if let Some(base_tc) = messages[i] + .get_mut("tool_calls") + .and_then(|tc| tc.as_array_mut()) + { + base_tc.extend(extra_tool_calls); + } + + let insert_at = i + 1; + messages.drain(insert_at..scan); + let num_collected = collected.len(); + for (j, msg) in collected.into_iter().enumerate() { + messages.insert(insert_at + j, msg); + } + + i = insert_at + num_collected; + } +} + +/// True if `msg` is a synthetic image-only user message (content is exclusively image_url items). +fn is_image_only_user_message(msg: &Value) -> bool { + msg.get("role") == Some(&json!("user")) + && msg + .get("content") + .and_then(|c| c.as_array()) + .is_some_and(|arr| { + !arr.is_empty() + && arr + .iter() + .all(|item| item.get("type") == Some(&json!("image_url"))) + }) +} + pub fn format_tools(tools: &[Tool]) -> anyhow::Result> { let mut tool_names = std::collections::HashSet::new(); let mut result = Vec::new(); @@ -2110,4 +2218,74 @@ data: [DONE]"#; "expected an error but stream completed successfully" ); } + + #[test] + fn test_merge_split_tool_calls_with_reasoning() { + let mut messages = vec![ + json!({"role": "assistant", "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "read", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc1", "content": "result1"}), + json!({"role": "assistant", "tool_calls": [{"id": "tc2", "type": "function", "function": {"name": "write", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc2", "content": "result2"}), + ]; + merge_split_tool_call_messages(&mut messages); + + assert_eq!(messages.len(), 3); + assert_eq!(messages[0]["tool_calls"].as_array().unwrap().len(), 2); + assert_eq!(messages[1]["role"], "tool"); + assert_eq!(messages[2]["role"], "tool"); + } + + #[test] + fn test_no_merge_without_reasoning() { + let mut messages = vec![ + json!({"role": "assistant", "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "read", "arguments": "{}"}}]}), + json!({"role": "tool", "tool_call_id": "tc1", "content": "result1"}), + json!({"role": "assistant", "tool_calls": [{"id": "tc2", "type": "function", "function": {"name": "write", "arguments": "{}"}}]}), + json!({"role": "tool", "tool_call_id": "tc2", "content": "result2"}), + ]; + merge_split_tool_call_messages(&mut messages); + + assert_eq!(messages.len(), 4); + assert_eq!(messages[0]["tool_calls"].as_array().unwrap().len(), 1); + assert_eq!(messages[2]["tool_calls"].as_array().unwrap().len(), 1); + } + + #[test] + fn test_merge_split_tool_calls_with_image_gap() { + let mut messages = vec![ + json!({"role": "assistant", "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "screenshot", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc1", "content": "This tool result included an image that is uploaded in the next message."}), + json!({"role": "user", "content": [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}]}), + json!({"role": "assistant", "tool_calls": [{"id": "tc2", "type": "function", "function": {"name": "click", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc2", "content": "clicked"}), + ]; + merge_split_tool_call_messages(&mut messages); + + assert_eq!(messages.len(), 4); + assert_eq!(messages[0]["tool_calls"].as_array().unwrap().len(), 2); + assert_eq!(messages[0]["role"], "assistant"); + assert_eq!(messages[1]["role"], "tool"); + assert_eq!(messages[1]["tool_call_id"], "tc1"); + assert_eq!(messages[2]["role"], "user"); + assert_eq!(messages[3]["role"], "tool"); + assert_eq!(messages[3]["tool_call_id"], "tc2"); + } + + #[test] + fn test_merge_does_not_skip_real_user_message() { + let mut messages = vec![ + json!({"role": "assistant", "tool_calls": [{"id": "tc1", "type": "function", "function": {"name": "read", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc1", "content": "result1"}), + json!({"role": "user", "content": "what happened?"}), + json!({"role": "assistant", "tool_calls": [{"id": "tc2", "type": "function", "function": {"name": "write", "arguments": "{}"}}], "reasoning_content": "thinking..."}), + json!({"role": "tool", "tool_call_id": "tc2", "content": "result2"}), + ]; + merge_split_tool_call_messages(&mut messages); + + assert_eq!(messages.len(), 5); + assert_eq!(messages[0]["tool_calls"].as_array().unwrap().len(), 1); + assert_eq!(messages[2]["role"], "user"); + assert_eq!(messages[2]["content"], "what happened?"); + assert_eq!(messages[3]["tool_calls"].as_array().unwrap().len(), 1); + } } diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index 987d53a2be..24aed27c05 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -87,8 +87,17 @@ fn build_vertex_url( (ModelProvider::MaaS(_), false) => "generateContent", }; + // Anthropic's Vertex AI docs specify v1 for rawPredict endpoints. + // The official google-genai SDK uses v1beta1 for Google models, + // and the global endpoint requires it. + let api_version = match &provider { + ModelProvider::Anthropic => "v1", + ModelProvider::Google | ModelProvider::MaaS(_) => "v1beta1", + }; + let path = format!( - "v1/projects/{}/locations/{}/publishers/{}/models/{}:{}", + "{}/projects/{}/locations/{}/publishers/{}/models/{}:{}", + api_version, project_id, target_location, provider.as_str(), diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index fed31af79b..96abc895de 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -54,10 +54,43 @@ pub const GITHUB_COPILOT_STREAM_MODELS: &[&str] = &[ const GITHUB_COPILOT_DOC_URL: &str = "https://docs.github.com/en/copilot/using-github-copilot/ai-models"; -const GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; -const GITHUB_COPILOT_DEVICE_CODE_URL: &str = "https://github.com/login/device/code"; -const GITHUB_COPILOT_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token"; -const GITHUB_COPILOT_API_KEY_URL: &str = "https://api.github.com/copilot_internal/v2/token"; +const DEFAULT_GITHUB_HOST: &str = "github.com"; +const DEFAULT_GITHUB_COPILOT_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98"; + +fn normalize_host(host: &str) -> String { + let host = host.trim_end_matches('/'); + let host = host.strip_prefix("https://").unwrap_or(host); + host.to_string() +} + +#[derive(Debug, Clone)] +struct GithubCopilotUrls { + device_code_url: String, + access_token_url: String, + copilot_token_url: String, +} + +impl GithubCopilotUrls { + fn new(host: &str, copilot_token_url: Option<&str>) -> Self { + if host == "github.com" { + Self { + device_code_url: "https://github.com/login/device/code".to_string(), + access_token_url: "https://github.com/login/oauth/access_token".to_string(), + copilot_token_url: "https://api.github.com/copilot_internal/v2/token".to_string(), + } + } else { + let base = format!("https://{}", host); + let copilot_token_url = copilot_token_url + .map(|u| u.trim_end_matches('/').to_string()) + .unwrap_or_else(|| format!("{}/api/copilot_internal/v2/token", base)); + Self { + device_code_url: format!("{}/login/device/code", base), + access_token_url: format!("{}/login/oauth/access_token", base), + copilot_token_url, + } + } + } +} #[derive(Debug, Deserialize)] struct DeviceCodeInfo { @@ -96,8 +129,13 @@ struct DiskCache { } impl DiskCache { - fn new() -> Self { - let cache_path = Paths::in_config_dir("githubcopilot/info.json"); + fn new(host: &str) -> Self { + let cache_path = if host == DEFAULT_GITHUB_HOST { + Paths::in_config_dir("githubcopilot/info.json") + } else { + let safe_host = host.replace(['/', ':', '.'], "_"); + Paths::in_config_dir(&format!("githubcopilot/{}/info.json", safe_host)) + }; Self { cache_path } } @@ -138,12 +176,22 @@ pub struct GithubCopilotProvider { mu: tokio::sync::Mutex>>, model: ModelConfig, #[serde(skip)] + urls: GithubCopilotUrls, + #[serde(skip)] + client_id: String, + #[serde(skip)] name: String, } impl GithubCopilotProvider { pub async fn cleanup() -> Result<()> { - DiskCache::new().clear().await + let config = Config::global(); + let host = normalize_host( + &config + .get_param::("GITHUB_COPILOT_HOST") + .unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()), + ); + DiskCache::new(&host).clear().await } fn payload_contains_image(payload: &Value) -> bool { @@ -170,16 +218,29 @@ impl GithubCopilotProvider { } pub async fn from_env(model: ModelConfig) -> Result { + let config = Config::global(); + let host = normalize_host( + &config + .get_param::("GITHUB_COPILOT_HOST") + .unwrap_or_else(|_| DEFAULT_GITHUB_HOST.to_string()), + ); + let client_id: String = config + .get_param("GITHUB_COPILOT_CLIENT_ID") + .unwrap_or_else(|_| DEFAULT_GITHUB_COPILOT_CLIENT_ID.to_string()); + let copilot_token_url: Option = config.get_param("GITHUB_COPILOT_TOKEN_URL").ok(); + let urls = GithubCopilotUrls::new(&host, copilot_token_url.as_deref()); let client = Client::builder() .timeout(Duration::from_secs(600)) .build()?; - let cache = DiskCache::new(); + let cache = DiskCache::new(&host); let mu = tokio::sync::Mutex::new(RefCell::new(None)); Ok(Self { client, cache, mu, model, + urls, + client_id, name: GITHUB_COPILOT_PROVIDER_NAME.to_string(), }) } @@ -258,7 +319,7 @@ impl GithubCopilotProvider { }; let resp = self .client - .get(GITHUB_COPILOT_API_KEY_URL) + .get(&self.urls.copilot_token_url) .headers(self.get_github_headers()) .header(http::header::AUTHORIZATION, format!("bearer {}", &token)) .send() @@ -311,10 +372,10 @@ impl GithubCopilotProvider { scope: String, } self.client - .post(GITHUB_COPILOT_DEVICE_CODE_URL) + .post(&self.urls.device_code_url) .headers(self.get_github_headers()) .json(&DeviceCodeRequest { - client_id: GITHUB_COPILOT_CLIENT_ID.to_string(), + client_id: self.client_id.clone(), scope: "read:user".to_string(), }) .send() @@ -346,10 +407,10 @@ impl GithubCopilotProvider { for attempt in 0..MAX_ATTEMPTS { let resp = self .client - .post(GITHUB_COPILOT_ACCESS_TOKEN_URL) + .post(&self.urls.access_token_url) .headers(self.get_github_headers()) .json(&AccessTokenRequest { - client_id: GITHUB_COPILOT_CLIENT_ID.to_string(), + client_id: self.client_id.clone(), device_code: device_code.to_string(), grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(), }) @@ -412,13 +473,12 @@ impl ProviderDef for GithubCopilotProvider { GITHUB_COPILOT_DEFAULT_MODEL, GITHUB_COPILOT_KNOWN_MODELS.to_vec(), GITHUB_COPILOT_DOC_URL, - vec![ConfigKey::new_oauth_device_code( - "GITHUB_COPILOT_TOKEN", - true, - true, - None, - false, - )], + vec![ + ConfigKey::new_oauth_device_code("GITHUB_COPILOT_TOKEN", true, true, None, false), + ConfigKey::new("GITHUB_COPILOT_HOST", false, false, None, false), + ConfigKey::new("GITHUB_COPILOT_CLIENT_ID", false, false, None, false), + ConfigKey::new("GITHUB_COPILOT_TOKEN_URL", false, false, None, false), + ], ) } @@ -633,7 +693,7 @@ fn promote_tool_choice(response: Value) -> Value { #[cfg(test)] mod tests { - use super::promote_tool_choice; + use super::{normalize_host, promote_tool_choice, GithubCopilotUrls}; use serde_json::json; #[test] @@ -677,4 +737,59 @@ mod tests { let promoted = promote_tool_choice(response.clone()); assert_eq!(promoted, response); } + + #[test] + fn normalize_host_strips_prefix_and_slash() { + assert_eq!(normalize_host("github.com"), "github.com"); + assert_eq!(normalize_host("https://github.com"), "github.com"); + assert_eq!(normalize_host("github.com/"), "github.com"); + assert_eq!(normalize_host("https://github.com/"), "github.com"); + assert_eq!( + normalize_host("https://my-enterprise.ghe.com/"), + "my-enterprise.ghe.com" + ); + } + + #[test] + fn urls_default_github_com() { + let urls = GithubCopilotUrls::new("github.com", None); + assert_eq!(urls.device_code_url, "https://github.com/login/device/code"); + assert_eq!( + urls.access_token_url, + "https://github.com/login/oauth/access_token" + ); + assert_eq!( + urls.copilot_token_url, + "https://api.github.com/copilot_internal/v2/token" + ); + } + + #[test] + fn urls_enterprise_host() { + let urls = GithubCopilotUrls::new("my-enterprise.ghe.com", None); + assert_eq!( + urls.device_code_url, + "https://my-enterprise.ghe.com/login/device/code" + ); + assert_eq!( + urls.access_token_url, + "https://my-enterprise.ghe.com/login/oauth/access_token" + ); + assert_eq!( + urls.copilot_token_url, + "https://my-enterprise.ghe.com/api/copilot_internal/v2/token" + ); + } + + #[test] + fn urls_enterprise_with_token_url_override() { + let urls = GithubCopilotUrls::new( + "my-enterprise.ghe.com", + Some("https://my-enterprise.ghe.com/api/v3/copilot_internal/v2/token"), + ); + assert_eq!( + urls.copilot_token_url, + "https://my-enterprise.ghe.com/api/v3/copilot_internal/v2/token" + ); + } } diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index e7ff25ec1e..34febc8f4f 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -7,6 +7,7 @@ use super::local_inference::LocalInferenceProvider; #[cfg(feature = "aws-providers")] use super::sagemaker_tgi::SageMakerTgiProvider; use super::{ + amp_acp::AmpAcpProvider, anthropic::AnthropicProvider, avian::AvianProvider, azure::AzureProvider, @@ -29,6 +30,7 @@ use super::{ ollama::OllamaProvider, openai::OpenAiProvider, openrouter::OpenRouterProvider, + pi_acp::PiAcpProvider, provider_registry::ProviderRegistry, snowflake::SnowflakeProvider, tetrate::TetrateProvider, @@ -49,6 +51,7 @@ static REGISTRY: OnceCell> = OnceCell::const_new(); async fn init_registry() -> RwLock { let mut registry = ProviderRegistry::new().with_providers(|registry| { + registry.register::(false); registry.register::(true); registry.register::(false); registry.register::(false); @@ -74,6 +77,7 @@ async fn init_registry() -> RwLock { registry.register::(true); registry.register::(true); registry.register::(true); + registry.register::(false); #[cfg(feature = "aws-providers")] registry.register::(false); registry.register::(false); diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 6934c4d6f2..a49b5a483a 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -1,3 +1,4 @@ +pub mod amp_acp; pub mod anthropic; pub mod api_client; pub mod avian; @@ -36,6 +37,7 @@ pub mod ollama; pub mod openai; pub mod openai_compatible; pub mod openrouter; +pub mod pi_acp; pub mod provider_registry; pub mod provider_test; mod retry; diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 5678a1da9a..4dceb3ab44 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -182,6 +182,12 @@ impl OllamaProvider { )); } + let model = if let Some(ref fast_model_name) = config.fast_model { + model.with_fast(fast_model_name, &config.name)? + } else { + model + }; + Ok(Self { api_client, model, diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index d7d26177f0..aad6ddd071 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -221,6 +221,12 @@ impl OpenAiProvider { None }; + let model = if let Some(ref fast_model_name) = config.fast_model { + model.with_fast(fast_model_name, &config.name)? + } else { + model + }; + Ok(Self { api_client, base_path, diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 6a3d02a99c..1b00d7d9ba 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -57,7 +57,7 @@ impl OpenRouterProvider { let auth = AuthMethod::BearerToken(api_key); let api_client = ApiClient::new(host, auth)? - .with_header("HTTP-Referer", "https://block.github.io/goose")? + .with_header("HTTP-Referer", "https://goose-docs.ai")? .with_header("X-Title", "goose")?; Ok(Self { diff --git a/crates/goose/src/providers/pi_acp.rs b/crates/goose/src/providers/pi_acp.rs new file mode 100644 index 0000000000..5c3eaa72cf --- /dev/null +++ b/crates/goose/src/providers/pi_acp.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use futures::future::BoxFuture; +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::acp::{ + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, +}; +use crate::config::search_path::SearchPaths; +use crate::config::{Config, GooseMode}; +use crate::model::ModelConfig; +use crate::providers::base::{ProviderDef, ProviderMetadata}; + +const PI_ACP_PROVIDER_NAME: &str = "pi-acp"; +const PI_ACP_DOC_URL: &str = "https://github.com/anthropics/pi"; +const PI_ACP_BINARY: &str = "pi-acp"; + +pub struct PiAcpProvider; + +impl ProviderDef for PiAcpProvider { + type Provider = AcpProvider; + + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + PI_ACP_PROVIDER_NAME, + "Pi", + "Use goose with Pi via the pi-acp adapter.", + ACP_CURRENT_MODEL, + vec![], + PI_ACP_DOC_URL, + vec![], + ) + .with_setup_steps(vec![ + "Install the Pi CLI and the pi-acp adapter", + "Ensure your Pi CLI is authenticated (run `pi` to verify)", + "Set in your goose config file (`~/.config/goose/config.yaml` on macOS/Linux):\n GOOSE_PROVIDER: pi-acp\n GOOSE_MODEL: current", + "Restart goose for changes to take effect", + ]) + } + + fn from_env( + model: ModelConfig, + extensions: Vec, + ) -> BoxFuture<'static, Result> { + Box::pin(async move { + let config = Config::global(); + let resolved_command = SearchPaths::builder().with_npm().resolve(PI_ACP_BINARY)?; + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); + + let mode_mapping = HashMap::from([ + (GooseMode::Auto, "auto".to_string()), + (GooseMode::Approve, "approve".to_string()), + (GooseMode::SmartApprove, "smart-approve".to_string()), + (GooseMode::Chat, "chat".to_string()), + ]); + + let provider_config = AcpProviderConfig { + command: resolved_command, + args: vec![], + env: vec![], + env_remove: vec![], + work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + mcp_servers: extension_configs_to_mcp_servers(&extensions), + session_mode_id: Some(mode_mapping[&goose_mode].clone()), + mode_mapping, + notification_callback: None, + }; + + let metadata = Self::metadata(); + AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await + }) + } +} diff --git a/crates/goose/src/providers/provider_registry.rs b/crates/goose/src/providers/provider_registry.rs index c64203442b..860421c4e3 100644 --- a/crates/goose/src/providers/provider_registry.rs +++ b/crates/goose/src/providers/provider_registry.rs @@ -23,6 +23,10 @@ pub struct ProviderEntry { } impl ProviderEntry { + pub fn metadata(&self) -> &ProviderMetadata { + &self.metadata + } + pub async fn create_with_default_model( &self, extensions: Vec, diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index 37c3aced79..98aefefd0d 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -55,7 +55,7 @@ impl TetrateProvider { let auth = AuthMethod::BearerToken(api_key); let api_client = ApiClient::new(host, auth)? - .with_header("HTTP-Referer", "https://block.github.io/goose")? + .with_header("HTTP-Referer", "https://goose-docs.ai")? .with_header("X-Title", "goose")?; Ok(Self { diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 970e32c707..9c6793c646 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -839,6 +839,7 @@ async fn execute_job( } drop(jobs_guard); + #[cfg(feature = "telemetry")] let start_time = std::time::Instant::now(); #[cfg(feature = "telemetry")] tokio::spawn(async move { diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 5f95bc9741..3da0a0a764 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -232,6 +232,11 @@ impl<'a> SessionUpdateBuilder<'a> { self } + pub fn clear_model_config(mut self) -> Self { + self.model_config = Some(None); + self + } + pub fn goose_mode(mut self, mode: GooseMode) -> Self { self.goose_mode = Some(mode); self diff --git a/crates/goose/src/subprocess.rs b/crates/goose/src/subprocess.rs index a288d290c0..b010064230 100644 --- a/crates/goose/src/subprocess.rs +++ b/crates/goose/src/subprocess.rs @@ -3,6 +3,25 @@ use tokio::process::Command; #[cfg(windows)] const CREATE_NO_WINDOW_FLAG: u32 = 0x08000000; +#[cfg(target_os = "linux")] +fn configure_parent_death_signal(command: &mut Command) { + let parent_pid = unsafe { libc::getpid() }; + + unsafe { + command.pre_exec(move || { + if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 { + return Err(std::io::Error::last_os_error()); + } + + if libc::getppid() != parent_pid { + return Err(std::io::Error::from_raw_os_error(libc::ESRCH)); + } + + Ok(()) + }); + } +} + pub trait SubprocessExt { fn set_no_window(&mut self) -> &mut Self; } @@ -34,5 +53,7 @@ pub fn configure_subprocess(command: &mut Command) { // SIGINT when the user presses Ctrl+C in the terminal. #[cfg(unix)] command.process_group(0); + #[cfg(target_os = "linux")] + configure_parent_death_signal(command); command.set_no_window(); } diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio b/crates/goose/tests/mcp_replays/github-mcp-serverstdio index 3ed7300d73..5c6f00f9dd 100644 --- a/crates/goose/tests/mcp_replays/github-mcp-serverstdio +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio @@ -8,5 +8,5 @@ STDOUT: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{"completions":{},"logg STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} STDERR: time=2025-12-11T17:58:47.642-05:00 level=INFO msg="session initialized" STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"agent-session-id":"test-session-id","progressToken":0},"name":"get_file_contents","arguments":{"owner":"block","path":"README.md","repo":"goose","sha":"ab62b863c1666232a67048b6c4e10007a2a5b83c"}}} -STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"successfully downloaded text file (SHA: de9bdde7f260549bf3a083651842f30ab29cf4e9)"},{"type":"resource","resource":{"uri":"repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md","mimeType":"text/plain; charset=utf-8","text":"\u003cdiv align=\"center\"\u003e\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\n \u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://discord.gg/7GaTvbDwga\"\u003e\n \u003cimg src=\"https://img.shields.io/discord/1287729918100246654?logo=discord\u0026logoColor=white\u0026label=Join+Us\u0026color=blueviolet\" alt=\"Discord\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://github.com/block/goose/actions/workflows/ci.yml\"\u003e\n \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\"\u003e\n \u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[![Watch the video](https://github.com/user-attachments/assets/ddc71240-3928-41b5-8210-626dfb28af7a)](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# a little goose humor 🦒\n\n\u003e Why did the developer choose goose as their AI agent?\n\u003e \n\u003e Because it always helps them \"migrate\" their code to production! πŸš€\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"}}]}} +STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"successfully downloaded text file (SHA: de9bdde7f260549bf3a083651842f30ab29cf4e9)"},{"type":"resource","resource":{"uri":"repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md","mimeType":"text/plain; charset=utf-8","text":"\u003cdiv align=\"center\"\u003e\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\n \u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://discord.gg/7GaTvbDwga\"\u003e\n \u003cimg src=\"https://img.shields.io/discord/1287729918100246654?logo=discord\u0026logoColor=white\u0026label=Join+Us\u0026color=blueviolet\" alt=\"Discord\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://github.com/block/goose/actions/workflows/ci.yml\"\u003e\n \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\"\u003e\n \u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[![Watch the video](https://github.com/user-attachments/assets/ddc71240-3928-41b5-8210-626dfb28af7a)](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://goose-docs.ai/docs/quickstart)\n- [Installation](https://goose-docs.ai/docs/getting-started/installation)\n- [Tutorials](https://goose-docs.ai/docs/category/tutorials)\n- [Documentation](https://goose-docs.ai/docs/category/getting-started)\n\n\n# a little goose humor 🦒\n\n\u003e Why did the developer choose goose as their AI agent?\n\u003e \n\u003e Because it always helps them \"migrate\" their code to production! πŸš€\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"}}]}} STDERR: time=2025-12-11T17:58:48.133-05:00 level=INFO msg="server session disconnected" session_id="" diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json b/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json index bede7cc955..68df513867 100644 --- a/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json @@ -10,7 +10,7 @@ "resource": { "uri": "repo://block/goose/sha/ab62b863c1666232a67048b6c4e10007a2a5b83c/contents/README.md", "mimeType": "text/plain; charset=utf-8", - "text": "
\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n

\n \n \n \n \n \"Discord\"\n \n \n \"CI\"\n \n

\n
\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[![Watch the video](https://github.com/user-attachments/assets/ddc71240-3928-41b5-8210-626dfb28af7a)](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# a little goose humor 🦒\n\n> Why did the developer choose goose as their AI agent?\n> \n> Because it always helps them \"migrate\" their code to production! πŸš€\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n" + "text": "
\n\n# goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n

\n \n \n \n \n \"Discord\"\n \n \n \"CI\"\n \n

\n
\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n[![Watch the video](https://github.com/user-attachments/assets/ddc71240-3928-41b5-8210-626dfb28af7a)](https://youtu.be/D-DpDunrbpo)\n\n# Quick Links\n- [Quickstart](https://goose-docs.ai/docs/quickstart)\n- [Installation](https://goose-docs.ai/docs/getting-started/installation)\n- [Tutorials](https://goose-docs.ai/docs/category/tutorials)\n- [Documentation](https://goose-docs.ai/docs/category/getting-started)\n\n\n# a little goose humor 🦒\n\n> Why did the developer choose goose as their AI agent?\n> \n> Because it always helps them \"migrate\" their code to production! πŸš€\n\n# goose around with us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@goose-oss)\n- [LinkedIn](https://www.linkedin.com/company/goose-oss)\n- [Twitter/X](https://x.com/goose_oss)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n" } } ] diff --git a/crates/goose/tests/subprocess_cleanup.rs b/crates/goose/tests/subprocess_cleanup.rs new file mode 100644 index 0000000000..f8553d53c0 --- /dev/null +++ b/crates/goose/tests/subprocess_cleanup.rs @@ -0,0 +1,79 @@ +#![cfg(target_os = "linux")] + +use goose::subprocess::configure_subprocess; +use std::io::{BufRead, BufReader, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + +const HELPER_ENV: &str = "GOOSE_SUBPROCESS_PARENT_DEATH_HELPER"; + +#[ctor::ctor] +fn maybe_run_helper() { + if std::env::var_os(HELPER_ENV).is_none() { + return; + } + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + + let pid = runtime.block_on(async { + let mut command = tokio::process::Command::new("sleep"); + command.arg("30"); + command.stdin(Stdio::null()); + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + configure_subprocess(&mut command); + + let child = command.spawn().expect("spawn child"); + let pid = child.id().expect("child pid"); + std::mem::forget(child); + pid + }); + + println!("{pid}"); + std::io::stdout().flush().expect("flush pid"); + + unsafe { + libc::_exit(0); + } +} + +#[test] +fn child_process_exits_when_parent_process_dies() { + let current_exe = std::env::current_exe().expect("current test binary"); + let mut helper = Command::new(current_exe) + .env(HELPER_ENV, "1") + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("spawn helper"); + + let pid_line = { + let stdout = helper.stdout.take().expect("helper stdout"); + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + reader.read_line(&mut line).expect("read child pid"); + line + }; + + let child_pid = pid_line.trim().parse::().expect("parse child pid"); + + let status = helper.wait().expect("wait for helper"); + assert!(status.success(), "helper exited unsuccessfully: {status}"); + + let deadline = Instant::now() + Duration::from_secs(5); + while process_exists(child_pid) { + assert!( + Instant::now() < deadline, + "child process {child_pid} still exists after helper exit" + ); + std::thread::sleep(Duration::from_millis(100)); + } +} + +fn process_exists(pid: u32) -> bool { + PathBuf::from(format!("/proc/{pid}")).exists() +} diff --git a/deny.toml b/deny.toml index 3f45e55f21..a9e0ba19a2 100644 --- a/deny.toml +++ b/deny.toml @@ -7,7 +7,7 @@ unmaintained = "none" unsound = "none" # Ignore proc-macro-error unmaintained warning -# See: https://github.com/block/goose/issues/7008 +# See: https://github.com/aaif-goose/goose/issues/7008 ignore = [ "RUSTSEC-2023-0071", # rsa: Marvin Attack timing sidechannel (no safe upgrade available, via jsonwebtoken) ] diff --git a/documentation/automation/cli-command-tracking/README.md b/documentation/automation/cli-command-tracking/README.md index 1e706bf446..f7b8327f73 100644 --- a/documentation/automation/cli-command-tracking/README.md +++ b/documentation/automation/cli-command-tracking/README.md @@ -4,7 +4,7 @@ Automated pipeline for detecting and documenting CLI command changes between goo ## Overview -This automation keeps the [CLI Commands Guide](https://block.github.io/goose/docs/guides/goose-cli-commands) synchronized with code changes by: +This automation keeps the [CLI Commands Guide](https://goose-docs.ai/docs/guides/goose-cli-commands) synchronized with code changes by: 1. **Extracting** CLI structure from goose binary using `--help` output (deterministic) 2. **Detecting** changes between versions (deterministic diff) diff --git a/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh index bb6edbf8bc..af220a0000 100755 --- a/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh +++ b/documentation/automation/cli-command-tracking/scripts/extract-cli-structure.sh @@ -32,7 +32,7 @@ download_release_binary() { echo "Downloading goose $version from GitHub releases..." >&2 # Use the official download script with custom bin dir and specific version - curl -fsSL "https://github.com/block/goose/releases/download/stable/download_cli.sh" | \ + curl -fsSL "https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh" | \ CONFIGURE=false GOOSE_BIN_DIR="$bin_dir" GOOSE_VERSION="$version" bash >&2 2>&1 || { echo "Error: Failed to download goose $version" >&2 return 1 diff --git a/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh b/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh index 054e24b153..8e051755cd 100755 --- a/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh +++ b/documentation/automation/cli-command-tracking/scripts/run-pipeline.sh @@ -15,7 +15,7 @@ GOOSE_REPO=${GOOSE_REPO:-"$HOME/Development/goose"} # Function to get release tags using gh CLI get_latest_release() { if command -v gh &> /dev/null; then - gh release list --repo block/goose --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null + gh release list --repo aaif-goose/goose --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null else # Fallback: get latest version tag from git cd "$GOOSE_REPO" && git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 @@ -24,7 +24,7 @@ get_latest_release() { get_previous_release() { if command -v gh &> /dev/null; then - gh release list --repo block/goose --limit 2 --json tagName --jq '.[].tagName' 2>/dev/null | sed -n '2p' + gh release list --repo aaif-goose/goose --limit 2 --json tagName --jq '.[].tagName' 2>/dev/null | sed -n '2p' else # Fallback: get second-latest version tag from git cd "$GOOSE_REPO" && git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed -n '2p' diff --git a/documentation/automation/recipe-schema-tracking/README.md b/documentation/automation/recipe-schema-tracking/README.md index 1e9015ec29..8aa2196200 100644 --- a/documentation/automation/recipe-schema-tracking/README.md +++ b/documentation/automation/recipe-schema-tracking/README.md @@ -4,7 +4,7 @@ Automated pipeline for detecting and documenting Recipe schema and validation ru ## Overview -This automation keeps the [Recipe Reference Guide](https://block.github.io/goose/docs/guides/recipes/recipe-reference) synchronized with code changes by: +This automation keeps the [Recipe Reference Guide](https://goose-docs.ai/docs/guides/recipes/recipe-reference) synchronized with code changes by: 1. **Extracting** schema and validation rules from source code (deterministic) 2. **Detecting** changes between versions (deterministic diff) diff --git a/documentation/blog/2024-11-22-screenshot-driven-development/index.md b/documentation/blog/2024-11-22-screenshot-driven-development/index.md index ac49c483e4..2999f41dc2 100644 --- a/documentation/blog/2024-11-22-screenshot-driven-development/index.md +++ b/documentation/blog/2024-11-22-screenshot-driven-development/index.md @@ -11,7 +11,7 @@ authors: I'm a developer at heart, so when I'm working on a personal project, the hardest part isn't writing codeβ€”it's making design decisions. I recently built a calendar user interface. I wanted to enhance its visual appeal, so I researched UI design trends like "glassmorphism" and "claymorphism." -However, I didn't want to spend hours implementing the CSS for each design trend, so I developed a faster approach: screenshot-driven development. I used an open source developer agent called [goose](https://github.com/block/goose) to transform my user interfaces quickly. +However, I didn't want to spend hours implementing the CSS for each design trend, so I developed a faster approach: screenshot-driven development. I used an open source developer agent called [goose](https://github.com/aaif-goose/goose) to transform my user interfaces quickly. @@ -205,7 +205,7 @@ export ANTHROPIC_API_KEY=your_api_key ``` ### Step 4: Enable the Screen toolkit -goose uses [toolkits](https://block.github.io/goose/plugins/plugins.html) to extend its capabilities. The [screen](https://block.github.io/goose/plugins/available-toolkits.html#6-screen-toolkit) toolkit lets goose take and analyze screenshots. +goose uses [toolkits](https://goose-docs.ai/plugins/plugins.html) to extend its capabilities. The [screen](https://goose-docs.ai/plugins/available-toolkits.html#6-screen-toolkit) toolkit lets goose take and analyze screenshots. To enable the Screen toolkit, add it to your goose profile at ~/.config/goose/profiles.yaml. @@ -281,17 +281,17 @@ Developing user interfaces is a blend of creativity and problem-solving. And I l Beyond prototyping, goose's ability to analyze screenshots can help developers identify and resolve UI bugs. -If you're interested in learning more, check out the [goose repo](https://github.com/block/goose) and join our [Discord community](https://discord.gg/goose-oss). +If you're interested in learning more, check out the [goose repo](https://github.com/aaif-goose/goose) and join our [Discord community](https://discord.gg/goose-oss). - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2024-12-06-previewing-goose-v10-beta/index.md b/documentation/blog/2024-12-06-previewing-goose-v10-beta/index.md index 21a118c998..5e8d30ce5a 100644 --- a/documentation/blog/2024-12-06-previewing-goose-v10-beta/index.md +++ b/documentation/blog/2024-12-06-previewing-goose-v10-beta/index.md @@ -47,18 +47,18 @@ goose now has an electron-based GUI macOS application that provides and alternat goose v1.0 Beta now uses a custom protocol, that is designed in parallel with [Anthropic’s Model Context Protocol](https://www.anthropic.com/news/model-context-protocol) (MCP) to communicate with Systems. This makes it possible for developers to create their own systems (e.g Jira, ) that Goose can integrate with. -Excited for many more feature updates and improvements? Stay tuned for more updates on Goose! Check out the [goose repo](https://github.com/block/goose) and join our [Discord community](https://discord.gg/goose-oss). +Excited for many more feature updates and improvements? Stay tuned for more updates on Goose! Check out the [goose repo](https://github.com/aaif-goose/goose) and join our [Discord community](https://discord.gg/goose-oss). - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2024-12-10-connecting-ai-agents-to-your-systems-with-mcp/index.md b/documentation/blog/2024-12-10-connecting-ai-agents-to-your-systems-with-mcp/index.md index d149cf8980..5630079b88 100644 --- a/documentation/blog/2024-12-10-connecting-ai-agents-to-your-systems-with-mcp/index.md +++ b/documentation/blog/2024-12-10-connecting-ai-agents-to-your-systems-with-mcp/index.md @@ -22,7 +22,7 @@ This post was written about a beta version of goose and the commands and flow ma Anthropic is leading the charge with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), an open standard that enables large language model (LLM) applications to connect with external systems, providing the necessary context for more informed and relevant AI interactions. -This is a game changer for AI agents such as [goose](https://block.github.io/goose/), which can perform tasks autonomously - a significant leap beyond chatbots that only provide step-by-step instructions. However, to unlock the full potential of these AI agents, we need a standard method for connecting them to external data sources. MCP provides this foundation. +This is a game changer for AI agents such as [goose](https://goose-docs.ai/), which can perform tasks autonomously - a significant leap beyond chatbots that only provide step-by-step instructions. However, to unlock the full potential of these AI agents, we need a standard method for connecting them to external data sources. MCP provides this foundation. With MCP's standardized APIs and endpoints, goose can integrate seamlessly into your systems, enhancing its ability to perform complex tasks like debugging, writing code, and running commands directly in your environment. @@ -30,7 +30,7 @@ With MCP's standardized APIs and endpoints, goose can integrate seamlessly into ### What's Possible -Without MCP, every [goose toolkit](https://block.github.io/goose/plugins/using-toolkits.html) developer would need to implement bespoke integrations with every system they need to connect to. Not only is this tedious and repetitive, but it delays the fun stuff. +Without MCP, every [goose toolkit](https://goose-docs.ai/plugins/using-toolkits.html) developer would need to implement bespoke integrations with every system they need to connect to. Not only is this tedious and repetitive, but it delays the fun stuff. Let's take a simple GitHub workflow, for example. goose interacts directly with the GitHub API using custom scripts or configurations. Developers must configure goose to authenticate with GitHub and specify endpoints for actions like fetching open pull requests or adding comments. Each integration requires manual setup and custom coding to handle authentication tokens, error handling, and API updates. @@ -38,7 +38,7 @@ MCP simplifies the process by providing a standardized interface for accessing G ### Join the Ecosystem -As MCP adoption expands, so does goose’s potential to deliver even more powerful solutions for your organization. By [integrating goose](https://block.github.io/goose/) into your workflows and [embracing MCP](https://modelcontextprotocol.io/introduction), you’re not just enhancing your own systems, you’re contributing to the growth of an ecosystem that makes AI tools more interoperable, efficient, and impactful. +As MCP adoption expands, so does goose’s potential to deliver even more powerful solutions for your organization. By [integrating goose](https://goose-docs.ai/) into your workflows and [embracing MCP](https://modelcontextprotocol.io/introduction), you’re not just enhancing your own systems, you’re contributing to the growth of an ecosystem that makes AI tools more interoperable, efficient, and impactful. @@ -54,18 +54,18 @@ As MCP adoption expands, so does goose’s potential to deliver even more powerf - + - + - - + + - + diff --git a/documentation/blog/2024-12-11-resolving-ci-issues-with-goose-a-practical-walkthrough/index.md b/documentation/blog/2024-12-11-resolving-ci-issues-with-goose-a-practical-walkthrough/index.md index edfab11f65..346b7cf92c 100644 --- a/documentation/blog/2024-12-11-resolving-ci-issues-with-goose-a-practical-walkthrough/index.md +++ b/documentation/blog/2024-12-11-resolving-ci-issues-with-goose-a-practical-walkthrough/index.md @@ -162,12 +162,12 @@ Try it out, and let goose handle the heavy lifting of CI debugging for you! - + - + - + - + diff --git a/documentation/blog/2025-01-28-introducing-codename-goose/index.md b/documentation/blog/2025-01-28-introducing-codename-goose/index.md index 04b7c31db7..cb67ce9e1a 100644 --- a/documentation/blog/2025-01-28-introducing-codename-goose/index.md +++ b/documentation/blog/2025-01-28-introducing-codename-goose/index.md @@ -16,7 +16,7 @@ Powered by your choice of [large language models (LLMs)](/docs/getting-started/p You can think of goose as an assistant that is ready to take your instructions, and do the work for you. -While goose's first use cases are engineering focused, the community has been exploring other non-engineering use cases for goose as well. And it goes without saying, goose is [open source](https://github.com/block/goose) πŸŽ‰. +While goose's first use cases are engineering focused, the community has been exploring other non-engineering use cases for goose as well. And it goes without saying, goose is [open source](https://github.com/aaif-goose/goose) πŸŽ‰. ## How goose Works @@ -52,7 +52,7 @@ You can get started using goose right away! Check out our [Quickstart](/docs/qui Excited for upcoming features and events? Be sure to connect with us! -- [GitHub](https://github.com/block/goose) +- [GitHub](https://github.com/aaif-goose/goose) - [Discord](https://discord.gg/goose-oss) - [YouTube](https://www.youtube.com/@goose-oss) - [LinkedIn](https://www.linkedin.com/company/goose-oss) @@ -60,18 +60,18 @@ Excited for upcoming features and events? Be sure to connect with us! - [BlueSky](https://bsky.app/profile/opensource.block.xyz) -[extensions-directory]: https://block.github.io/goose/v1/extensions +[extensions-directory]: https://goose-docs.ai/v1/extensions - + - + - + - + diff --git a/documentation/blog/2025-02-17-agentic-ai-mcp/index.md b/documentation/blog/2025-02-17-agentic-ai-mcp/index.md index 6169e2e2d6..2cdf0daf13 100644 --- a/documentation/blog/2025-02-17-agentic-ai-mcp/index.md +++ b/documentation/blog/2025-02-17-agentic-ai-mcp/index.md @@ -149,12 +149,12 @@ Hopefully this has provided clear insight into what are AI agents, how they work - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-02-21-gooseteam-mcp/index.md b/documentation/blog/2025-02-21-gooseteam-mcp/index.md index fd17dba652..71d4cbe04e 100644 --- a/documentation/blog/2025-02-21-gooseteam-mcp/index.md +++ b/documentation/blog/2025-02-21-gooseteam-mcp/index.md @@ -34,12 +34,12 @@ Hopefully this contribution inspired you as much as it inspired our community. I - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-06-goose-tips/index.md b/documentation/blog/2025-03-06-goose-tips/index.md index 6dd3740323..120e78c8aa 100644 --- a/documentation/blog/2025-03-06-goose-tips/index.md +++ b/documentation/blog/2025-03-06-goose-tips/index.md @@ -86,12 +86,12 @@ By following these tips, you'll be able to work more effectively with goose, get - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-10-goose-calls-vyop/index.md b/documentation/blog/2025-03-10-goose-calls-vyop/index.md index 373e381e46..4334bd6472 100644 --- a/documentation/blog/2025-03-10-goose-calls-vyop/index.md +++ b/documentation/blog/2025-03-10-goose-calls-vyop/index.md @@ -38,12 +38,12 @@ For those eager to experiment with [VOYP](https://github.com/paulotaylor/voyp-mc - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-12-goose-figma-mcp/index.md b/documentation/blog/2025-03-12-goose-figma-mcp/index.md index d64e050c76..815fb7f5ef 100644 --- a/documentation/blog/2025-03-12-goose-figma-mcp/index.md +++ b/documentation/blog/2025-03-12-goose-figma-mcp/index.md @@ -12,7 +12,7 @@ In our previous episode of [goose Flight School](https://www.youtube.com/playlis # How It Works -[Extensions](https://block.github.io/goose/docs/getting-started/using-extensions) enhance goose's functionality by connecting with your existing tools and workflows. They add new features, access external data resources, and integrate with other systems. Learn how multiple extensions, including Figma and Developer, worked together seamlessly to dramatically accelerate development. +[Extensions](https://goose-docs.ai/docs/getting-started/using-extensions) enhance goose's functionality by connecting with your existing tools and workflows. They add new features, access external data resources, and integrate with other systems. Learn how multiple extensions, including Figma and Developer, worked together seamlessly to dramatically accelerate development. # Live Tutorial: goose Builds Live During the livestream, Adewale demonstrated step-by-step how goose handled each development phase, from creating the basic application structure to generating responsive layouts using Tailwind CSS. Adewale also highlighted how goose addresses potential limitations as you go, showcasing the powerful balance between the goose's automation and developer control. @@ -27,19 +27,19 @@ Throughout the stream, Adewale shared valuable tips to prepare your design for g * make sure you thoroughly test for functionality and responsiveness # Getting Started with goose and Figma -Whether you're a designer wanting to rapidly turn concepts into working code or a developer curious about streamlining design implementation, you can download goose with its built-in [Developer extension](https://block.github.io/goose/docs/getting-started/installation) and add the [Figma extension](https://block.github.io/goose/v1/extensions/). +Whether you're a designer wanting to rapidly turn concepts into working code or a developer curious about streamlining design implementation, you can download goose with its built-in [Developer extension](https://goose-docs.ai/docs/getting-started/installation) and add the [Figma extension](https://goose-docs.ai/v1/extensions/). For step-by-step instructions, check out the [Figma tutorial](/docs/mcp/figma-mcp). - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-14-goose-ollama/index.md b/documentation/blog/2025-03-14-goose-ollama/index.md index 98c99dc375..a29dbb286d 100644 --- a/documentation/blog/2025-03-14-goose-ollama/index.md +++ b/documentation/blog/2025-03-14-goose-ollama/index.md @@ -17,9 +17,9 @@ Together, they create a self-contained AI agent workflow that puts advanced capa # Getting Started -Before diving deep into various capabilities, Rizel walked us through how to set yourself up for success by integrating goose with Ollama. To follow along, you can [download goose](https://block.github.io/goose/) and follow a step-by-step walk through in the [Configure LLM Provider](https://block.github.io/goose/docs/getting-started/providers) guide. +Before diving deep into various capabilities, Rizel walked us through how to set yourself up for success by integrating goose with Ollama. To follow along, you can [download goose](https://goose-docs.ai/) and follow a step-by-step walk through in the [Configure LLM Provider](https://goose-docs.ai/docs/getting-started/providers) guide. -If you have any questions or get stuck, feel free to chat with us on [Discord](https://discord.gg/goose-oss) or post an issue/discussion on [GitHub](https://github.com/block/goose/). Thanks for reading! +If you have any questions or get stuck, feel free to chat with us on [Discord](https://discord.gg/goose-oss) or post an issue/discussion on [GitHub](https://github.com/aaif-goose/goose/). Thanks for reading! # Why Go Local? Using cloud-based LLMs and providers make it so you don't need substantial computing resources, so why go local? Here's some benefits you may want to consider: @@ -88,12 +88,12 @@ The benefits of owning your AI experience can be compelling for a variety of use - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-18-goose-langfuse/index.md b/documentation/blog/2025-03-18-goose-langfuse/index.md index 0c890fc695..e80fdd8d7e 100644 --- a/documentation/blog/2025-03-18-goose-langfuse/index.md +++ b/documentation/blog/2025-03-18-goose-langfuse/index.md @@ -44,7 +44,7 @@ As developers focus on operational efficiency, researchers can use the analytica # The Future of AI Observability These powerful debugging and analysis capabilities are only the beginning. This integration between goose and Langfuse represents a significant step forward in making AI agents as transparent and debuggable as traditional code. -To keep up with the exciting developments as they release, you can check out both of the [goose](https://github.com/block/goose) and [Langfuse](https://github.com/langfuse/langfuse) repositories on GitHub. +To keep up with the exciting developments as they release, you can check out both of the [goose](https://github.com/aaif-goose/goose) and [Langfuse](https://github.com/langfuse/langfuse) repositories on GitHub. You can also watch the [livestream discussing the goose and Langfuse integration](https://www.youtube.com/live/W39BQjsTS9E?feature=shared), and follow the [tutorial showing you how to integrate Langfuse with goose](/docs/tutorials/langfuse). @@ -53,12 +53,12 @@ Also, be sure to subscribe to our [events calendar](https://calget.com/c/t7jszri - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-19-better-ai-prompting/index.md b/documentation/blog/2025-03-19-better-ai-prompting/index.md index 479cd623e5..7e35ce7ee0 100644 --- a/documentation/blog/2025-03-19-better-ai-prompting/index.md +++ b/documentation/blog/2025-03-19-better-ai-prompting/index.md @@ -150,12 +150,12 @@ goose is here to make your life easier, so the next time you open up a session, - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-20-asana-calendar-mcp/index.md b/documentation/blog/2025-03-20-asana-calendar-mcp/index.md index 7af135bc1c..751984c9bb 100644 --- a/documentation/blog/2025-03-20-asana-calendar-mcp/index.md +++ b/documentation/blog/2025-03-20-asana-calendar-mcp/index.md @@ -43,12 +43,12 @@ This has been so extremely helpful in increasing my productivity. Thanks, Goose! - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-21-goose-boston-meetup/index.mdx b/documentation/blog/2025-03-21-goose-boston-meetup/index.mdx index b79b29eeea..50d0206b28 100644 --- a/documentation/blog/2025-03-21-goose-boston-meetup/index.mdx +++ b/documentation/blog/2025-03-21-goose-boston-meetup/index.mdx @@ -12,7 +12,7 @@ authors: **Answer: You get an electric night filled with great conversations, hands-on hacking, and mind-blowing insights into agentic systems.** -This week, we hosted our very first [Goose](https://block.github.io/goose) Meetup in Boston at the Cambridge Innovation Center. The turnout and energy exceeded all expectations! From first-time Goose users to seasoned AI engineers, attendees gathered to explore how Goose and the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) are shaping the future of AI automation. +This week, we hosted our very first [Goose](https://goose-docs.ai) Meetup in Boston at the Cambridge Innovation Center. The turnout and energy exceeded all expectations! From first-time Goose users to seasoned AI engineers, attendees gathered to explore how Goose and the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) are shaping the future of AI automation. @@ -110,12 +110,12 @@ _The Goose Team in Boston_ - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-21-goose-vscode/index.mdx b/documentation/blog/2025-03-21-goose-vscode/index.mdx index 35baf4d9ab..5b03632b2b 100644 --- a/documentation/blog/2025-03-21-goose-vscode/index.mdx +++ b/documentation/blog/2025-03-21-goose-vscode/index.mdx @@ -60,12 +60,12 @@ The project is open source, and welcomes contributions from the community. If yo - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-26-mcp-security/index.md b/documentation/blog/2025-03-26-mcp-security/index.md index ab66f3cd88..9c8f023e70 100644 --- a/documentation/blog/2025-03-26-mcp-security/index.md +++ b/documentation/blog/2025-03-26-mcp-security/index.md @@ -58,12 +58,12 @@ A quick glance at an MCP directory like Glama can save you from crying on your o - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-28-vibe-coding-with-goose/index.md b/documentation/blog/2025-03-28-vibe-coding-with-goose/index.md index 7e3751a283..2f4153dcef 100644 --- a/documentation/blog/2025-03-28-vibe-coding-with-goose/index.md +++ b/documentation/blog/2025-03-28-vibe-coding-with-goose/index.md @@ -84,12 +84,12 @@ Voice interactions with AI agents like Goose with the power and tools to act on - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-03-31-goose-benchmark/index.md b/documentation/blog/2025-03-31-goose-benchmark/index.md index 0dd27cba41..c047f5b0c8 100644 --- a/documentation/blog/2025-03-31-goose-benchmark/index.md +++ b/documentation/blog/2025-03-31-goose-benchmark/index.md @@ -23,7 +23,7 @@ These community evals aren't the rigorous, peer-reviewed benchmarks that researc In this spirit, we're introducing our **Goose Vibe Check** leaderboard. -Thank you to the Ollama team for their help and support in our experimentation contributing to this blog! We used Ollama’s [structured outputs](https://ollama.com/blog/structured-outputs) feature to enable our [toolshim implementation](https://block.github.io/goose/docs/experimental/ollama) (more below) and their recently released [context length parameter override](https://github.com/ollama/ollama/blob/main/docs/faq.mdx#how-can-i-specify-the-context-window-size) to enable testing on longer contexts. +Thank you to the Ollama team for their help and support in our experimentation contributing to this blog! We used Ollama’s [structured outputs](https://ollama.com/blog/structured-outputs) feature to enable our [toolshim implementation](https://goose-docs.ai/docs/experimental/ollama) (more below) and their recently released [context length parameter override](https://github.com/ollama/ollama/blob/main/docs/faq.mdx#how-can-i-specify-the-context-window-size) to enable testing on longer contexts. ## Leaderboard @@ -136,7 +136,7 @@ Tool calling enables models to interact with [MCP extensions](https://github.com ### Evaluation Suites -Our evaluations are defined in the [Goose repository](https://github.com/block/goose/tree/main/crates/goose-bench/src/eval_suites) (PRs welcome to add additional evals!) and are grouped into two categories: +Our evaluations are defined in the [Goose repository](https://github.com/aaif-goose/goose/tree/main/crates/goose-bench/src/eval_suites) (PRs welcome to add additional evals!) and are grouped into two categories: #### Core Suite These evals focus on certain tasks fundamental to developer workflows: @@ -237,7 +237,7 @@ However, this solution has limited performance due to: - **Structured output interference:** Ollama’s structured output feature influences the model’s token sampling process, where the output is impacted by the model’s fundamental ability to extract information and generate JSON appropriately. Despite these challenges, there could be potential in fine-tuning these toolshim models to specifically optimize them for tool call generation. -If you’d like to try out the toolshim, check out our [documentation](https://block.github.io/goose/docs/experimental/ollama) +If you’d like to try out the toolshim, check out our [documentation](https://goose-docs.ai/docs/experimental/ollama) ## Practical Advice for Local Model Users @@ -422,13 +422,13 @@ For runs that successfully created an index.html for the Wiki page task, here’ - + - + - + - + diff --git a/documentation/blog/2025-03-31-securing-mcp/index.md b/documentation/blog/2025-03-31-securing-mcp/index.md index a2cc3f5332..f8f153e719 100644 --- a/documentation/blog/2025-03-31-securing-mcp/index.md +++ b/documentation/blog/2025-03-31-securing-mcp/index.md @@ -176,12 +176,12 @@ We’re excited to work on making this protocol more secure for users and develo - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-01-mcp-nondevs/index.md b/documentation/blog/2025-04-01-mcp-nondevs/index.md index aa21bdd138..6d21627507 100644 --- a/documentation/blog/2025-04-01-mcp-nondevs/index.md +++ b/documentation/blog/2025-04-01-mcp-nondevs/index.md @@ -68,24 +68,24 @@ Asana: Create a new task in my Asana workspace called 'Review Q4 metrics' and se GitHub: Create a new branch called hello-world in my angiejones/goose-demo repository. Update the README.md file to say "this was written by goose" and commit it. Open a pull request with your changes. ``` -To see more examples just like this, along with the results you can get, check out this [Prompt Library](https://block.github.io/goose/prompt-library)! This is your central directory for discovering and using effective prompts with Goose. +To see more examples just like this, along with the results you can get, check out this [Prompt Library](https://goose-docs.ai/prompt-library)! This is your central directory for discovering and using effective prompts with Goose. ## The Possibilities Are Endless While some are developed by official providers, a vast majority of MCP servers you see are actually developed by community members! Plus, because MCP is an open standard, anyone can build an MCP server for any resource. You could even use Goose to help you build one! Hopefully now, instead of spending hours manually gathering data and creating your next marketing report, or manually sorting through your todo-backlog on a Monday, you will use MCP with Goose and have it done for you in minutes. -*To learn more about using MCP servers and Goose, check out the [Goose documentation](https://block.github.io/goose/docs/category/getting-started), or join the [Block Open Source Discord](https://discord.gg/goose-oss) to connect with other open source community members.* +*To learn more about using MCP servers and Goose, check out the [Goose documentation](https://goose-docs.ai/docs/category/getting-started), or join the [Block Open Source Discord](https://discord.gg/goose-oss) to connect with other open source community members.* - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-01-top-5-mcp-servers/index.mdx b/documentation/blog/2025-04-01-top-5-mcp-servers/index.mdx index 61bf64c3c7..52a899c6ea 100644 --- a/documentation/blog/2025-04-01-top-5-mcp-servers/index.mdx +++ b/documentation/blog/2025-04-01-top-5-mcp-servers/index.mdx @@ -31,7 +31,7 @@ For cases when it's a large pull request, or I don't understand what is going on E.g ``` -Hey Goose, this pull request https://github.com/block/goose/pull/1949, has a lot of changes. Can you summarize into a changelog for me? +Hey Goose, this pull request https://github.com/aaif-goose/goose/pull/1949, has a lot of changes. Can you summarize into a changelog for me? ``` ## Knowledge Graph Memory: Context on Steroids @@ -89,12 +89,12 @@ What are your favorite MCP servers? How do you use them together? Share your exp - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-08-vibe-code-responsibly/index.md b/documentation/blog/2025-04-08-vibe-code-responsibly/index.md index 6b88adbbce..c1b6f5cc97 100644 --- a/documentation/blog/2025-04-08-vibe-code-responsibly/index.md +++ b/documentation/blog/2025-04-08-vibe-code-responsibly/index.md @@ -38,7 +38,7 @@ This creative freedom comes with significant risks. Many developers have encount ## A Better Way to Vibe Code with Goose -[Goose](https://block.github.io/goose) is an open source AI agent local to your machine with built-in features for safe vibe coding. +[Goose](https://goose-docs.ai) is an open source AI agent local to your machine with built-in features for safe vibe coding. :::note Most folks define "vibe coding" as purely chaotic development with no rules. I'm redefining it as flowing with AI while protecting your project, team, and future self. @@ -46,7 +46,7 @@ Most folks define "vibe coding" as purely chaotic development with no rules. I'm ### 1. Use `.gooseignore` to Protect Sensitive Files -Goose supports [`.gooseignore`](https://block.github.io/goose/docs/guides/using-gooseignore) files. The concept is similar to `.gitignore` files for your AI agent. It defines which files and folders Goose should *not* read, modify, or interact with. +Goose supports [`.gooseignore`](https://goose-docs.ai/docs/guides/using-gooseignore) files. The concept is similar to `.gitignore` files for your AI agent. It defines which files and folders Goose should *not* read, modify, or interact with. Use this when you want to prevent: @@ -59,13 +59,13 @@ Use this when you want to prevent: ### 2. Create a plan -Goose's [`/plan`](https://block.github.io/goose/docs/guides/goose-cli-commands#examples) command helps you align with your agent before any code is touched, giving you a clear understanding of what it intends to do and how it will do it. +Goose's [`/plan`](https://goose-docs.ai/docs/guides/goose-cli-commands#examples) command helps you align with your agent before any code is touched, giving you a clear understanding of what it intends to do and how it will do it. This is especially useful for tasks that span multiple files, involve side effects, or could impact critical areas of your codebase. No more guessworkβ€”just a structured breakdown you can review and approve. ### 3. Choose the Right Mode for the Job -While letting your AI agent take the lead is fun, not every moment calls for full autonomy. Sometimes, you need to pause, review, or plan before any code changes. Goose offers several [modes](https://block.github.io/goose/docs/guides/goose-permissions) that help you stay in control without breaking your momentum. Here's how to use them intentionally during your sessions: +While letting your AI agent take the lead is fun, not every moment calls for full autonomy. Sometimes, you need to pause, review, or plan before any code changes. Goose offers several [modes](https://goose-docs.ai/docs/guides/goose-permissions) that help you stay in control without breaking your momentum. Here's how to use them intentionally during your sessions: * **Chat Mode** Goose will only respond with text so that you can brainstorm together. @@ -117,7 +117,7 @@ Before issues hit production, add [Goose to your CI/CD pipeline](/docs/tutorials Some MCP servers can introduce security risks, especially if compromised. -Use the Goose [allowlist](https://github.com/block/goose/blob/main/crates/goose-server/ALLOWLIST.md) feature to prevent Goose from calling unsafe or untrusted tools. +Use the Goose [allowlist](https://github.com/aaif-goose/goose/blob/main/crates/goose-server/ALLOWLIST.md) feature to prevent Goose from calling unsafe or untrusted tools. Here's how the team at Block is thinking about [securing the MCP](/blog/2025/03/31/securing-mcp). @@ -128,7 +128,7 @@ Not all LLMs are built the same. Goose plays best with: * Claude Sonnet 3.5 * GPT-4o -Lower-performing models might work, but they're more likely to hallucinate or misunderstand your goals. Read more about how [different LLM's perform with Goose](https://block.github.io/goose/blog/2025/03/31/goose-benchmark/). +Lower-performing models might work, but they're more likely to hallucinate or misunderstand your goals. Read more about how [different LLM's perform with Goose](https://goose-docs.ai/blog/2025/03/31/goose-benchmark/). ## Watch Vibe Coding in Action Here’s how folks vibe code with Goose: @@ -139,17 +139,17 @@ Here’s how folks vibe code with Goose: Vibe coding isn't inherently wrong. It's marks a new chapter in how we build, and it opens the door for everyone. But experienced developers have a responsibility to define what smart, safe vibe coding looks like. Goose gives us the tools to set that standard, so the whole community can code creatively without sacrificing quality. -Download [Goose](https://block.github.io/goose/docs/getting-started/installation/), and start vibe coding with intention today! +Download [Goose](https://goose-docs.ai/docs/getting-started/installation/), and start vibe coding with intention today! - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-10-visual-guide-mcp/index.md b/documentation/blog/2025-04-10-visual-guide-mcp/index.md index 3020cf28c5..0d7df29e30 100644 --- a/documentation/blog/2025-04-10-visual-guide-mcp/index.md +++ b/documentation/blog/2025-04-10-visual-guide-mcp/index.md @@ -82,12 +82,12 @@ And don't forget to [join the community](https://discord.gg/goose-oss) to see wh - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-11-finetuning-toolshim/index.md b/documentation/blog/2025-04-11-finetuning-toolshim/index.md index 24ec682e74..2837bc7144 100644 --- a/documentation/blog/2025-04-11-finetuning-toolshim/index.md +++ b/documentation/blog/2025-04-11-finetuning-toolshim/index.md @@ -8,7 +8,7 @@ authors: ![blog cover](toolshim-header.png) -Our recently published [Goose benchmark](https://block.github.io/goose/blog/2025/03/31/goose-benchmark) revealed significant performance limitations in models where tool calling is not straightforwardly supported (e.g., Gemma3, Deepseek-r1, phi4). These models often fail to invoke tools at appropriate times or produce malformed or inconsistently formatted tool calls. With the most recent releases of Llama4 and Deepseek v3 (0324), we are again observing challenges with effective tool calling performance, even on these flagship openweight models. +Our recently published [Goose benchmark](https://goose-docs.ai/blog/2025/03/31/goose-benchmark) revealed significant performance limitations in models where tool calling is not straightforwardly supported (e.g., Gemma3, Deepseek-r1, phi4). These models often fail to invoke tools at appropriate times or produce malformed or inconsistently formatted tool calls. With the most recent releases of Llama4 and Deepseek v3 (0324), we are again observing challenges with effective tool calling performance, even on these flagship openweight models. @@ -18,13 +18,13 @@ Tool calling is a critical capability for agents like goose. It allows models to ## Background: using a local model as a "toolshim" -The goal is to allow goose to work with the widest variety of models possible. A "toolshim" in this case is a thin layer which sits between the main model doing the agent work, and the tools that can perform actual actions (making the agent take action, vs being a chatbot). Previously we have been trying this approach with open models including in this [past benchmark](https://block.github.io/goose/blog/2025/03/31/goose-benchmark) post. A toolshim, if it can work, unlocks both powerful cutting edge models (open weight and closed) which while may perform well on various benchmarks, fall well short when tool calling for agents is required (or perhaps don't, by design, support tool calling at all, such as the case with some reasoning models). +The goal is to allow goose to work with the widest variety of models possible. A "toolshim" in this case is a thin layer which sits between the main model doing the agent work, and the tools that can perform actual actions (making the agent take action, vs being a chatbot). Previously we have been trying this approach with open models including in this [past benchmark](https://goose-docs.ai/blog/2025/03/31/goose-benchmark) post. A toolshim, if it can work, unlocks both powerful cutting edge models (open weight and closed) which while may perform well on various benchmarks, fall well short when tool calling for agents is required (or perhaps don't, by design, support tool calling at all, such as the case with some reasoning models). ## Proposal: Fine-tune a lightweight toolshim model (up to 12b) Develop a dedicated toolshim model that translates open-source model outputs into well-structured tool calls, acting as a reliable post-processor to standardize across model families trained that currently exhibit inconsistent and unreliable tool call generation behavior. We do not use tool calling apis even if available, but provide tool context in the system prompts. -We already experimented with this in the [benchmarking effort](https://block.github.io/goose/blog/2025/03/31/goose-benchmark), finding that phi4 (14b) and gemma3 (27b) achieved close performance to llama3.3 (70b) when used with a generic local model (mistral-nemo) as the shim. This shows potential for furthering their performance with more focused attention on improving the shim's performance. +We already experimented with this in the [benchmarking effort](https://goose-docs.ai/blog/2025/03/31/goose-benchmark), finding that phi4 (14b) and gemma3 (27b) achieved close performance to llama3.3 (70b) when used with a generic local model (mistral-nemo) as the shim. This shows potential for furthering their performance with more focused attention on improving the shim's performance. Toolshim System Sketch: @@ -86,12 +86,12 @@ On top of local models, we would like to consider parsers, parser combinators, c - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-14-community-atruelight4/index.md b/documentation/blog/2025-04-14-community-atruelight4/index.md index fa10818125..527cdfe574 100644 --- a/documentation/blog/2025-04-14-community-atruelight4/index.md +++ b/documentation/blog/2025-04-14-community-atruelight4/index.md @@ -27,17 +27,17 @@ Thank you so much for your contribution, ATrueLight4! Your work brings us one st To show our deep gratitude for your contribution, we've granted you the exclusive ✨Top Contributor✨ badge on the Block Open Source Discord! You'll also be one of the first contributors to receive exclusive codename goose swag. (more info on that to be announced later πŸ‘€πŸͺΏ) ## Get Your Own Spotlight -Interested in contributing to goose and having your contribution featured? Whether it's improving platform support, adding new features, or fixing bugs, we welcome all contributions from the open source community. You can [join the Block Open Source Discord](https://discord.gg/goose-oss) or [get started using codename goose](https://block.github.io/goose/) today. +Interested in contributing to goose and having your contribution featured? Whether it's improving platform support, adding new features, or fixing bugs, we welcome all contributions from the open source community. You can [join the Block Open Source Discord](https://discord.gg/goose-oss) or [get started using codename goose](https://goose-docs.ai/) today. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-17-goose-goes-to-NY/index.mdx b/documentation/blog/2025-04-17-goose-goes-to-NY/index.mdx index 266caa2576..8984cca00f 100644 --- a/documentation/blog/2025-04-17-goose-goes-to-NY/index.mdx +++ b/documentation/blog/2025-04-17-goose-goes-to-NY/index.mdx @@ -104,12 +104,12 @@ We’ll see you in Atlanta πŸ‘€ - + - + - + - + diff --git a/documentation/blog/2025-04-21-mcp-in-enterprise/index.mdx b/documentation/blog/2025-04-21-mcp-in-enterprise/index.mdx index a4e3c38026..42e85532f3 100644 --- a/documentation/blog/2025-04-21-mcp-in-enterprise/index.mdx +++ b/documentation/blog/2025-04-21-mcp-in-enterprise/index.mdx @@ -125,12 +125,12 @@ We'd love to hear how others are approaching AI automation at scale. - + - + - + - + diff --git a/documentation/blog/2025-04-21-practical-use-cases-of-ai/index.md b/documentation/blog/2025-04-21-practical-use-cases-of-ai/index.md index a76c75409c..a2ce8737c3 100644 --- a/documentation/blog/2025-04-21-practical-use-cases-of-ai/index.md +++ b/documentation/blog/2025-04-21-practical-use-cases-of-ai/index.md @@ -270,12 +270,12 @@ Let goose surprise you. ✨ - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-04-22-community-bestcodes/index.md b/documentation/blog/2025-04-22-community-bestcodes/index.md index 9eb95ddbd9..c3e340987e 100644 --- a/documentation/blog/2025-04-22-community-bestcodes/index.md +++ b/documentation/blog/2025-04-22-community-bestcodes/index.md @@ -13,30 +13,30 @@ The only way to discover how much of an impact your contributions can make is to ## How BestCodes Discovered Goose -[BestCodes](https://bestcodes.dev/) first discovered Goose during a [GitHub Open Source Friday livestream](https://www.youtube.com/watch?v=O-zJJN-TkXc&ab_channel=AngieJones) and decided to give it a try. At the time, Goose offered limited support for Windows/Linux (with more work in progress before any official release). Since BestCodes wasn't a Mac user, he wanted to help bring support to Linux. After lots of troubleshooting, he ended up creating [a Debian build fix](https://github.com/block/goose/pull/2070) that finally let him start using Goose on Linux. +[BestCodes (aka William Steele)](https://bestcodes.dev/) first discovered Goose during a [GitHub Open Source Friday livestream](https://www.youtube.com/watch?v=O-zJJN-TkXc&ab_channel=AngieJones) and decided to give it a try. At the time, Goose offered limited support for Windows/Linux (with more work in progress before any official release). Since BestCodes wasn't a Mac user, he wanted to help bring support to Linux. After lots of troubleshooting, he ended up creating [a Debian build fix](https://github.com/aaif-goose/goose/pull/2070) that finally let him start using Goose on Linux. Even though BestCodes wondered if such a small fix was worth sharing, it was this "small" contribution that enabled fellow community members wanting to use Goose on Linux, and gave him momentum for future PRs. His experience is proof that your contribution has a chance to help someone, and you should share it! You never know what curiosity and the open source community can make happen. ## Building Momentum -BestCodes has continued to make Goose better with thoughtful improvementsβ€”from [polishing the UI](https://github.com/block/goose/pull/2079) and [refining load states](https://github.com/block/goose/pull/2079), to [fixing subtle bugs](https://github.com/block/goose/pull/2279) that make the Goose experience smoother for everyone. You can check out [BestCodes' contributions on GitHub](https://github.com/block/goose/pulls?q=is%3Apr+is%3Aclosed+author%3AThe-Best-Codes). +BestCodes has continued to make Goose better with thoughtful improvementsβ€”from [polishing the UI](https://github.com/aaif-goose/goose/pull/2079) and [refining load states](https://github.com/aaif-goose/goose/pull/2079), to [fixing subtle bugs](https://github.com/aaif-goose/goose/pull/2279) that make the Goose experience smoother for everyone. You can check out [BestCodes' contributions on GitHub](https://github.com/aaif-goose/goose/pulls?q=is%3Apr+is%3Aclosed+author%3AThe-Best-Codes). Thank you so much for your continued support and contributions, BestCodes! Your work brings us one step closer to a full Goose experience on Windows and Linux. To show how grateful we are for your contributions, we've granted you the exclusive ✨Top Contributor✨ badge on the Block Open Source Discord! Plus, you'll also be receiving exclusive codename goose swag. πŸ‘€πŸͺΏ ## Become A Top Contributor -Interested in contributing to goose and having your work featured? Whether it's fixing bugs, sharing ideas, or helping others, every contribution from the open source community has the chance to help someone. You can [join the Block Open Source Discord](https://discord.gg/goose-oss) or [get started using codename goose](https://block.github.io/goose/) today. +Interested in contributing to goose and having your work featured? Whether it's fixing bugs, sharing ideas, or helping others, every contribution from the open source community has the chance to help someone. You can [join the Block Open Source Discord](https://discord.gg/goose-oss) or [get started using codename goose](https://goose-docs.ai/) today. - + - + - + - + diff --git a/documentation/blog/2025-04-22-mcp-is-rewriting-the-rules-of-api-integration/index.md b/documentation/blog/2025-04-22-mcp-is-rewriting-the-rules-of-api-integration/index.md index 3f92bfa129..ddb6b23558 100644 --- a/documentation/blog/2025-04-22-mcp-is-rewriting-the-rules-of-api-integration/index.md +++ b/documentation/blog/2025-04-22-mcp-is-rewriting-the-rules-of-api-integration/index.md @@ -153,12 +153,12 @@ A: Your approach on this may vary depending on whether you're building your own - + - + - + - + diff --git a/documentation/blog/2025-04-23-things-need-to-know/index.md b/documentation/blog/2025-04-23-things-need-to-know/index.md index 338822ebec..53269c8c90 100644 --- a/documentation/blog/2025-04-23-things-need-to-know/index.md +++ b/documentation/blog/2025-04-23-things-need-to-know/index.md @@ -43,9 +43,9 @@ Either way, Goose gives you the flexibility. That said, if you’re looking for the best performance with Goose right now, Anthropic's Claude 3.5 Sonnet and OpenAI's GPT-4o (2024-11-20) are recommended, as they currently offer the strongest support for tool calling. -Curious how other models stack up? Check out the [Community-Inspired Benchmark Leaderboard](https://block.github.io/goose/blog/2025/03/31/goose-benchmark/#leaderboard) to see how your favorite model performs with Goose. +Curious how other models stack up? Check out the [Community-Inspired Benchmark Leaderboard](https://goose-docs.ai/blog/2025/03/31/goose-benchmark/#leaderboard) to see how your favorite model performs with Goose. -And if you’re still deciding, here’s the full list of [available LLM providers](https://block.github.io/goose/docs/getting-started/providers#available-providers). +And if you’re still deciding, here’s the full list of [available LLM providers](https://goose-docs.ai/docs/getting-started/providers#available-providers). --- @@ -65,7 +65,7 @@ Goose itself? Totally free and open source. πŸŽ‰ But your LLM provider might not Most models give you a free tier to play around with, but if you're doing anything intensive or using it often, you’ll eventually run into rate limits or token charges. That’s normal but it can sneak up on you if you’re not expecting it. -To help you manage this, there is a [Handling Rate Limits Guide](https://block.github.io/goose/docs/guides/handling-llm-rate-limits-with-goose/) that you can check out. +To help you manage this, there is a [Handling Rate Limits Guide](https://goose-docs.ai/docs/guides/handling-llm-rate-limits-with-goose/) that you can check out. --- @@ -73,7 +73,7 @@ To help you manage this, there is a [Handling Rate Limits Guide](https://block.g This part matters more than most people realize. -Goose has an entire community behind itβ€”folks building, exploring, breaking things (and fixing them), and sharing everything they learn along the way. We hang out on [Discord](https://discord.gg/goose-oss), we answer questions in [GitHub Discussions](https://github.com/block/goose/discussions), and we host livestreams every week to show off what Goose can do and how to make it do more. +Goose has an entire community behind itβ€”folks building, exploring, breaking things (and fixing them), and sharing everything they learn along the way. We hang out on [Discord](https://discord.gg/goose-oss), we answer questions in [GitHub Discussions](https://github.com/aaif-goose/goose/discussions), and we host livestreams every week to show off what Goose can do and how to make it do more. There’s: @@ -81,27 +81,27 @@ There’s: - **Wild Goose Case** – showcasing cool community projects - **Great Goose Off** - same task, same time limit, but different prompts, MCP servers, and strategies -You’ll find those livestreams on our [YouTube channel](https://www.youtube.com/@goose-oss/streams), and upcoming ones on the Discord calendar. Plus, if you prefer documentation, the [Goose docs](https://block.github.io/goose/) and [blog](https://block.github.io/goose/blog) are constantly being updated with new guides, tips, and tutorials. +You’ll find those livestreams on our [YouTube channel](https://www.youtube.com/@goose-oss/streams), and upcoming ones on the Discord calendar. Plus, if you prefer documentation, the [Goose docs](https://goose-docs.ai/) and [blog](https://goose-docs.ai/blog) are constantly being updated with new guides, tips, and tutorials. --- If you've got those four things: a performant LLM, the right MCP servers, a basic understanding of LLM cost, and a place to ask questions, you're more than ready to Goose. -Now, head over to the [Quickstart Guide](https://block.github.io/goose/docs/quickstart) and get started. +Now, head over to the [Quickstart Guide](https://goose-docs.ai/docs/quickstart) and get started. -Oh and when you get to the [Tic-Tac-Toe game](https://block.github.io/goose/docs/quickstart/#write-prompt), I’ll bet you 10 Goosebucks you won’t beat the bot. +Oh and when you get to the [Tic-Tac-Toe game](https://goose-docs.ai/docs/quickstart/#write-prompt), I’ll bet you 10 Goosebucks you won’t beat the bot. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-05-06-recipe-for-success/index.md b/documentation/blog/2025-05-06-recipe-for-success/index.md index ea48a99a7b..2fc351693c 100644 --- a/documentation/blog/2025-05-06-recipe-for-success/index.md +++ b/documentation/blog/2025-05-06-recipe-for-success/index.md @@ -195,12 +195,12 @@ Your team is full of problem solvers. One teammate built a slick internal dashbo - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-05-09-developers-ai-playbook-for-team-efficiency/index.md b/documentation/blog/2025-05-09-developers-ai-playbook-for-team-efficiency/index.md index 7f03123e9b..0b691b5638 100644 --- a/documentation/blog/2025-05-09-developers-ai-playbook-for-team-efficiency/index.md +++ b/documentation/blog/2025-05-09-developers-ai-playbook-for-team-efficiency/index.md @@ -300,13 +300,13 @@ We'd love for you to share your ideas with us! Share your ideas by creating a re - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-05-12-local-goose-qwen3/index.md b/documentation/blog/2025-05-12-local-goose-qwen3/index.md index 0e033d404a..a973893f03 100644 --- a/documentation/blog/2025-05-12-local-goose-qwen3/index.md +++ b/documentation/blog/2025-05-12-local-goose-qwen3/index.md @@ -60,13 +60,13 @@ It would be interesting to try the larger models if, you have access to hardware - + - + - + - + diff --git a/documentation/blog/2025-05-20-goose-gets-a-drivers-license/index.md b/documentation/blog/2025-05-20-goose-gets-a-drivers-license/index.md index 19abf3e886..0bd67d0dfa 100644 --- a/documentation/blog/2025-05-20-goose-gets-a-drivers-license/index.md +++ b/documentation/blog/2025-05-20-goose-gets-a-drivers-license/index.md @@ -149,12 +149,12 @@ We're always excited to see what the community is up to. If you're working on yo - + - + - + - + diff --git a/documentation/blog/2025-05-22-llm-agent-readiness/index.md b/documentation/blog/2025-05-22-llm-agent-readiness/index.md index 3ca22f0790..b871534c0e 100644 --- a/documentation/blog/2025-05-22-llm-agent-readiness/index.md +++ b/documentation/blog/2025-05-22-llm-agent-readiness/index.md @@ -97,12 +97,12 @@ If a model passes this multi-turn prompt sequence, it's safe to assume that it i - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-05-22-manage-local-host-conflicts-with-goose/index.md b/documentation/blog/2025-05-22-manage-local-host-conflicts-with-goose/index.md index efe045a2fe..98bc74ffe9 100644 --- a/documentation/blog/2025-05-22-manage-local-host-conflicts-with-goose/index.md +++ b/documentation/blog/2025-05-22-manage-local-host-conflicts-with-goose/index.md @@ -88,12 +88,12 @@ Freeing up ports is something I know how to do. But I use AI tools like Goose fo - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-06-02-goose-panther-mcp/index.md b/documentation/blog/2025-06-02-goose-panther-mcp/index.md index e13ef918d3..73d14e823e 100644 --- a/documentation/blog/2025-06-02-goose-panther-mcp/index.md +++ b/documentation/blog/2025-06-02-goose-panther-mcp/index.md @@ -431,12 +431,12 @@ Block is dedicated to improving its security defenses and supporting its team by - + - + - + - + diff --git a/documentation/blog/2025-06-05-whats-in-my-goosehints-file/index.md b/documentation/blog/2025-06-05-whats-in-my-goosehints-file/index.md index 669ba931ad..d2b49e9534 100644 --- a/documentation/blog/2025-06-05-whats-in-my-goosehints-file/index.md +++ b/documentation/blog/2025-06-05-whats-in-my-goosehints-file/index.md @@ -124,13 +124,13 @@ Share your own `.goosehints` optimization stories in the [Goose community on Dis - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-06-16-multi-model-in-goose/index.md b/documentation/blog/2025-06-16-multi-model-in-goose/index.md index 538ec478e5..1cc7a56228 100644 --- a/documentation/blog/2025-06-16-multi-model-in-goose/index.md +++ b/documentation/blog/2025-06-16-multi-model-in-goose/index.md @@ -89,12 +89,12 @@ If you're experimenting with multi-model setups, [share what's working and what - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-06-17-goose-emotion-detection-app/index.mdx b/documentation/blog/2025-06-17-goose-emotion-detection-app/index.mdx index b374413bf0..851d3c76e7 100644 --- a/documentation/blog/2025-06-17-goose-emotion-detection-app/index.mdx +++ b/documentation/blog/2025-06-17-goose-emotion-detection-app/index.mdx @@ -97,12 +97,12 @@ Happy experimenting! - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-06-19-isolated-development-environments/index.md b/documentation/blog/2025-06-19-isolated-development-environments/index.md index f0753e890e..eef7f5ba4f 100644 --- a/documentation/blog/2025-06-19-isolated-development-environments/index.md +++ b/documentation/blog/2025-06-19-isolated-development-environments/index.md @@ -102,7 +102,7 @@ extensions: --- -*Questions? Join our [GitHub discussions](https://github.com/block/goose) or [Discord](https://discord.gg/goose-oss). Learn more about Dagger at [dagger.io](https://dagger.io/).* +*Questions? Join our [GitHub discussions](https://github.com/aaif-goose/goose) or [Discord](https://discord.gg/goose-oss). Learn more about Dagger at [dagger.io](https://dagger.io/).* {/* Video Player */}
@@ -121,12 +121,12 @@ extensions: - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-06-27-everyday-usecases-ai/index.md b/documentation/blog/2025-06-27-everyday-usecases-ai/index.md index 16710d11f0..39cdf7d340 100644 --- a/documentation/blog/2025-06-27-everyday-usecases-ai/index.md +++ b/documentation/blog/2025-06-27-everyday-usecases-ai/index.md @@ -114,12 +114,12 @@ What everyday tasks are you delegating to AI agents? Let us know in [Discord](ht - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-07-21-orchestrating-subagents/index.md b/documentation/blog/2025-07-21-orchestrating-subagents/index.md index 729784e9e5..a821f87624 100644 --- a/documentation/blog/2025-07-21-orchestrating-subagents/index.md +++ b/documentation/blog/2025-07-21-orchestrating-subagents/index.md @@ -176,12 +176,12 @@ What will you build with subagents? - + - + - + - + diff --git a/documentation/blog/2025-07-28-ai-to-ai/index.md b/documentation/blog/2025-07-28-ai-to-ai/index.md index 6eff535778..6c038a0892 100644 --- a/documentation/blog/2025-07-28-ai-to-ai/index.md +++ b/documentation/blog/2025-07-28-ai-to-ai/index.md @@ -437,12 +437,12 @@ You caught me being an impolite AI! Even artificial beings should have better ma - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-07-28-streamlining-detection-development-with-goose-recipes/index.md b/documentation/blog/2025-07-28-streamlining-detection-development-with-goose-recipes/index.md index 5470d8f618..703cfcb0ed 100644 --- a/documentation/blog/2025-07-28-streamlining-detection-development-with-goose-recipes/index.md +++ b/documentation/blog/2025-07-28-streamlining-detection-development-with-goose-recipes/index.md @@ -9,7 +9,7 @@ authors: Creating effective security detections in Panther traditionally requires deep knowledge of detection logic, testing frameworks, and development workflows. The detection engineering team at Block has streamlined this process by building Goose recipes that automate the entire detection creation lifecycle from initial repository set up to pull request creation. -This blog post explores how to leverage Goose's [recipe](https://block.github.io/goose/docs/guides/recipes/) and [subrecipe](https://block.github.io/goose/docs/guides/recipes/subrecipes) system to create new detections in Panther with minimal manual intervention, ensuring consistency, quality, and adherence to team standards. +This blog post explores how to leverage Goose's [recipe](https://goose-docs.ai/docs/guides/recipes/) and [subrecipe](https://goose-docs.ai/docs/guides/recipes/subrecipes) system to create new detections in Panther with minimal manual intervention, ensuring consistency, quality, and adherence to team standards. @@ -70,7 +70,7 @@ The detection creation recipe demonstrates the power of this approach by coordin 6. [**pr_creator**](#6-pr_creator-automated-pull-request-pipeline) - Pull request creation with proper formatting ### What about .goosehints? -In our [previous post](https://block.github.io/goose/blog/2025/06/02/goose-panther-mcp), we discussed using [.goosehints](/docs/guides/context-engineering/using-goosehints/) to provide persistent context to the Large Language Model (LLM). We continue to use `.goosehints` to define coding standards and universal preferences that guide LLM behavior. +In our [previous post](https://goose-docs.ai/blog/2025/06/02/goose-panther-mcp), we discussed using [.goosehints](/docs/guides/context-engineering/using-goosehints/) to provide persistent context to the Large Language Model (LLM). We continue to use `.goosehints` to define coding standards and universal preferences that guide LLM behavior. However, to minimize redundancy and avoid conflicting guidance, we adopted a single reference file, `AGENTS.md`, as the source of truth for all agents. Each agent is directed to consult this file, while still supporting agent-specific instructions through their default context files (e.g. `.goosehints`, `CLAUDE.md` etc.) or rules (e.g. `.cursor/rules/`). @@ -622,13 +622,13 @@ Whether you're building your first Goose recipe or looking to optimize existing - + - - - + + + - - + + \ No newline at end of file diff --git a/documentation/blog/2025-07-29-openrouter-unlocks-workshops/index.md b/documentation/blog/2025-07-29-openrouter-unlocks-workshops/index.md index eb2e41abb0..cf8d8bc25e 100644 --- a/documentation/blog/2025-07-29-openrouter-unlocks-workshops/index.md +++ b/documentation/blog/2025-07-29-openrouter-unlocks-workshops/index.md @@ -77,7 +77,7 @@ Join us for an evening in collaboration with Temporal and Dagger, where you'll g This system isn't perfect. Right now, it's still a separate experience outside the Goose interface, and it doesn't scale well for events with different credit amounts or more complex needs. -We're working on it. Goose engineer, Mic Neale recently opened a [pull request](https://github.com/block/goose/pull/3507) to automate Goose's first-time setup. It simplifies the onboarding flow so new users can log into OpenRouter through their browser, securely authenticate, and get a pre-configured Goose setup without touching config files or copying API keys. It's a huge leap in user experience and lays the groundwork for future improvements. +We're working on it. Goose engineer, Mic Neale recently opened a [pull request](https://github.com/aaif-goose/goose/pull/3507) to automate Goose's first-time setup. It simplifies the onboarding flow so new users can log into OpenRouter through their browser, securely authenticate, and get a pre-configured Goose setup without touching config files or copying API keys. It's a huge leap in user experience and lays the groundwork for future improvements. ## Why This Approach Matters @@ -89,14 +89,14 @@ Want us to run a Goose workshop or hackathon? We’ll bring the API credits. You - + - + - + - + diff --git a/documentation/blog/2025-08-04-mcp-jupyter-server/index.md b/documentation/blog/2025-08-04-mcp-jupyter-server/index.md index b97009b936..78a270be3f 100644 --- a/documentation/blog/2025-08-04-mcp-jupyter-server/index.md +++ b/documentation/blog/2025-08-04-mcp-jupyter-server/index.md @@ -12,7 +12,7 @@ authors: Machine learning and data science workflows are inherently iterative. You load data, explore patterns, build models, and refine your approach based on results. But traditional AI assistants lose context between interactions, forcing you to reload data and re-establish context repeatedlyβ€”making data-heavy development slow and expensive. -The [**MCP Jupyter Server**](https://github.com/block/mcp-jupyter) solves this by enabling AI agents like Goose to work directly with your Jupyter notebooks, maintaining persistent memory and state across interactions while letting the AI interact with your data through code execution rather than raw data transfer. +The [**MCP Jupyter Server**](https://github.com/aaif-goose/mcp-jupyter) solves this by enabling AI agents like Goose to work directly with your Jupyter notebooks, maintaining persistent memory and state across interactions while letting the AI interact with your data through code execution rather than raw data transfer. @@ -130,7 +130,7 @@ This fluid back-and-forth, where the assistant understands and builds upon the e Here you can see an example notebook that was handled by the MCP Jupyter Server. The server -**[πŸ““ View the Complete Demo Notebook](https://github.com/block/mcp-jupyter/blob/main/demos/demo.ipynb)** +**[πŸ““ View the Complete Demo Notebook](https://github.com/aaif-goose/mcp-jupyter/blob/main/demos/demo.ipynb)** The demo walks through a typical data science workflow: - **Install Missing Libraries**: Installing missing libraries for the notebook @@ -143,18 +143,18 @@ The demo walks through a typical data science workflow: The MCP Jupyter Server integrates seamlessly with existing workflows and can also be used with the notebook viewer in VS Code based IDEs. -For detailed setup and configuration, check out the [complete documentation](https://block.github.io/mcp-jupyter/). +For detailed setup and configuration, check out the [complete documentation](https://aaif-goose.github.io/mcp-jupyter/). - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-10-vibe-coding-with-goose-building-apps-with-ai-agents/index.md b/documentation/blog/2025-08-10-vibe-coding-with-goose-building-apps-with-ai-agents/index.md index 719d4b36b9..69739201ed 100644 --- a/documentation/blog/2025-08-10-vibe-coding-with-goose-building-apps-with-ai-agents/index.md +++ b/documentation/blog/2025-08-10-vibe-coding-with-goose-building-apps-with-ai-agents/index.md @@ -394,7 +394,7 @@ What we demonstrated in this workshop hints at a fascinating future for software Want to try this yourself? Here's what you need: -1. **Install and Configure Goose**: Follow the [quickstart guide](https://block.github.io/goose/docs/quickstart) +1. **Install and Configure Goose**: Follow the [quickstart guide](https://goose-docs.ai/docs/quickstart) 2. **Start Small**: Try building a simple app first to get comfortable with the workflow :::note @@ -416,12 +416,12 @@ Remember, this is about prototyping and exploration, not production deployment. - + - + - + - + diff --git a/documentation/blog/2025-08-11-llm-tag-team-lead-worker-model/index.md b/documentation/blog/2025-08-11-llm-tag-team-lead-worker-model/index.md index c1dbaddf72..462a0170bb 100644 --- a/documentation/blog/2025-08-11-llm-tag-team-lead-worker-model/index.md +++ b/documentation/blog/2025-08-11-llm-tag-team-lead-worker-model/index.md @@ -111,12 +111,12 @@ Got questions or want to share your own Lead/Worker success stories? Join us in - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-11-mcp-ui-post-browser-world/index.md b/documentation/blog/2025-08-11-mcp-ui-post-browser-world/index.md index 93eebfda72..6eac720a48 100644 --- a/documentation/blog/2025-08-11-mcp-ui-post-browser-world/index.md +++ b/documentation/blog/2025-08-11-mcp-ui-post-browser-world/index.md @@ -92,12 +92,12 @@ MCP-UI will continue to evolve, and may pick up more declarative ways for MCP-UI - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-12-mcp-testing/index.md b/documentation/blog/2025-08-12-mcp-testing/index.md index 7e2dbb45d4..5ac3159b92 100644 --- a/documentation/blog/2025-08-12-mcp-testing/index.md +++ b/documentation/blog/2025-08-12-mcp-testing/index.md @@ -38,7 +38,7 @@ To keep pace with growing MCP complexity, **automated metadata validation become ## 2. System Overview: Modular and Composable Goose Recipes -The foundation of this framework is [Goose’s recipe engine](https://block.github.io/goose/docs/guides/recipes/). Recipes define reusable, declarative workflows for AI-assisted tasks. Each one encapsulates a stepβ€”like generating predictions or comparing resultsβ€”and can be composed into larger pipelines. +The foundation of this framework is [Goose’s recipe engine](https://goose-docs.ai/docs/guides/recipes/). Recipes define reusable, declarative workflows for AI-assisted tasks. Each one encapsulates a stepβ€”like generating predictions or comparing resultsβ€”and can be composed into larger pipelines. We start with a core recipe that maps natural language queries to tool calls. It reads queries, analyzes the toolset, and produces structured JSON mappings. This recipe becomes the building block for workflows like: @@ -61,9 +61,9 @@ The recipe starts by reading a plain text file containing natural language queri ``` List contributors to the block/mcp repository -List the top 10 contributors to block/goose +List the top 10 contributors to aaif-goose/goose Show me the closed branches in block/mcp -Show me all branches in the block/goose repository +Show me all branches in the aaif-goose/goose repository ``` **Step 2: Ask Goose to Make Predictions** @@ -96,7 +96,7 @@ instructions: | { "test_cases": [ { - "query": "Show me open pull requests in the block/goose repository", + "query": "Show me open pull requests in the aaif-goose/goose repository", "expected": { "tool": "tool_name", "parameters": { @@ -349,7 +349,7 @@ Here are two common types of mismatches the system detects: - **Issue:** Tool name changed from `list_branches` to `get_repo_branches`, likely due to a tooltip or function name update **❌ Example 2: Parameter Mismatch** -- **Query:** "Search for files containing console.log in block/goose" +- **Query:** "Search for files containing console.log in aaif-goose/goose" - **Gold Standard:** ```json { @@ -564,12 +564,12 @@ With these building blocks in place, teams can confidently expand their automati - + - + - + - + diff --git a/documentation/blog/2025-08-13-pulse-mcp-automates-recipe/index.md b/documentation/blog/2025-08-13-pulse-mcp-automates-recipe/index.md index 228fda9d4d..e76cacd042 100644 --- a/documentation/blog/2025-08-13-pulse-mcp-automates-recipe/index.md +++ b/documentation/blog/2025-08-13-pulse-mcp-automates-recipe/index.md @@ -75,17 +75,17 @@ The PulseMCP implementation proves we're past the demo phase of AI agents. Real --- -*Want to build your own AI agent workflows? [Get started with Goose](https://block.github.io/goose/) and join the community of developers building the future of human-AI collaboration.* +*Want to build your own AI agent workflows? [Get started with Goose](https://goose-docs.ai/) and join the community of developers building the future of human-AI collaboration.* - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-14-agent-coordination-patterns/index.md b/documentation/blog/2025-08-14-agent-coordination-patterns/index.md index 919afdfc2c..9575745439 100644 --- a/documentation/blog/2025-08-14-agent-coordination-patterns/index.md +++ b/documentation/blog/2025-08-14-agent-coordination-patterns/index.md @@ -76,12 +76,12 @@ These setups are just different ways to organize work, whether it's human work o - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-14-how-goose-rebuilt-my-website/index.md b/documentation/blog/2025-08-14-how-goose-rebuilt-my-website/index.md index b34605b7e5..2690333e45 100644 --- a/documentation/blog/2025-08-14-how-goose-rebuilt-my-website/index.md +++ b/documentation/blog/2025-08-14-how-goose-rebuilt-my-website/index.md @@ -60,12 +60,12 @@ So if you're sitting there with a naked website that needs help, or if you've be - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-18-transforming-ai-assistance-goose-mentor-mode/index.md b/documentation/blog/2025-08-18-transforming-ai-assistance-goose-mentor-mode/index.md index 5f902eb3c6..80fa524f81 100644 --- a/documentation/blog/2025-08-18-transforming-ai-assistance-goose-mentor-mode/index.md +++ b/documentation/blog/2025-08-18-transforming-ai-assistance-goose-mentor-mode/index.md @@ -151,12 +151,12 @@ The early results suggest this approach resonates with developers who want to gr - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-18-understanding-context-windows/index.md b/documentation/blog/2025-08-18-understanding-context-windows/index.md index 23d28ef1e7..093a086444 100644 --- a/documentation/blog/2025-08-18-understanding-context-windows/index.md +++ b/documentation/blog/2025-08-18-understanding-context-windows/index.md @@ -73,15 +73,15 @@ Use [.goosehints](/docs/guides/context-engineering/using-goosehints/) files to a **3. Memory extension** -The [Memory extension](https://block.github.io/goose/docs/mcp/memory-mcp) stores important information across sessions. Instead of re-explaining your project background, past decisions, or important context every time you start a new conversation, you can reference stored information. This keeps your prompts focused on the current task rather than repeating historical context. +The [Memory extension](https://goose-docs.ai/docs/mcp/memory-mcp) stores important information across sessions. Instead of re-explaining your project background, past decisions, or important context every time you start a new conversation, you can reference stored information. This keeps your prompts focused on the current task rather than repeating historical context. **4. Recipes** -[Recipes](https://block.github.io/goose/docs/guides/recipes/) package complete task setups into reusable configurations, eliminating the need to provide lengthy instructions repeatedly. Instead of consuming tokens explaining complex workflows in every session, recipes contain all necessary instructions, extensions, and parameters upfront. This is particularly valuable for repetitive tasks where you'd otherwise spend significant tokens on setup and explanation. And if your recipe starts to feel overly lengthy, you can break the tasks up into [subrecipes](https://block.github.io/goose/docs/guides/recipes/subrecipes). +[Recipes](https://goose-docs.ai/docs/guides/recipes/) package complete task setups into reusable configurations, eliminating the need to provide lengthy instructions repeatedly. Instead of consuming tokens explaining complex workflows in every session, recipes contain all necessary instructions, extensions, and parameters upfront. This is particularly valuable for repetitive tasks where you'd otherwise spend significant tokens on setup and explanation. And if your recipe starts to feel overly lengthy, you can break the tasks up into [subrecipes](https://goose-docs.ai/docs/guides/recipes/subrecipes). **5. Subagents** -[Subagents](https://block.github.io/goose/docs/guides/subagents) handle specific tasks in their own isolated sessions. This prevents your main conversation from getting cluttered with implementation details and tool outputs. You delegate work to subagents and only see the final results, keeping your primary context window clean and focused. +[Subagents](https://goose-docs.ai/docs/guides/subagents) handle specific tasks in their own isolated sessions. This prevents your main conversation from getting cluttered with implementation details and tool outputs. You delegate work to subagents and only see the final results, keeping your primary context window clean and focused. **6. Short sessions** @@ -89,7 +89,7 @@ Keep individual sessions focused on specific tasks. When you complete a task or **7. Lead/worker model** -The [Lead/Worker model](https://block.github.io/goose/docs/tutorials/lead-worker) splits work between two models. The lead model handles high-level planning and decision-making, while the worker model executes the detailed implementation. This optimizes costs by using expensive models for strategic thinking and cheaper models for routine execution tasks. +The [Lead/Worker model](https://goose-docs.ai/docs/tutorials/lead-worker) splits work between two models. The lead model handles high-level planning and decision-making, while the worker model executes the detailed implementation. This optimizes costs by using expensive models for strategic thinking and cheaper models for routine execution tasks. --- @@ -99,12 +99,12 @@ The next time your AI agent seems to 'forget' something important or goes off tr - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-25-goose-became-its-own-watchdog/index.md b/documentation/blog/2025-08-25-goose-became-its-own-watchdog/index.md index c13d978e24..0942869c41 100644 --- a/documentation/blog/2025-08-25-goose-became-its-own-watchdog/index.md +++ b/documentation/blog/2025-08-25-goose-became-its-own-watchdog/index.md @@ -39,7 +39,7 @@ The whole process takes minutes instead of days, and the submitter gets immediat ## Goose in Headless Mode -We've covered headless mode in [tutorials](https://block.github.io/goose/docs/tutorials/headless-goose/) and [videos](https://www.youtube.com/@goose-oss/search?query=headless), but here's a quick recap: headless mode allows Goose to run without a graphical user interface, making it faster and more efficient for automated tasks. It excels in server environments as long as we're being _really_ clear about the instructions to follow, or a fallback if the instructions cannot be followed -- we don't want something half-finished or broken as a result if Goose gets stuck on what to do. +We've covered headless mode in [tutorials](https://goose-docs.ai/docs/tutorials/headless-goose/) and [videos](https://www.youtube.com/@goose-oss/search?query=headless), but here's a quick recap: headless mode allows Goose to run without a graphical user interface, making it faster and more efficient for automated tasks. It excels in server environments as long as we're being _really_ clear about the instructions to follow, or a fallback if the instructions cannot be followed -- we don't want something half-finished or broken as a result if Goose gets stuck on what to do. We launch the Docker container with something like this: @@ -89,12 +89,12 @@ Anyone can submit a recipe knowing it'll get a fair, thorough review. And when y - + - + - + - + diff --git a/documentation/blog/2025-08-25-mcp-ui-future-agentic-interfaces/index.md b/documentation/blog/2025-08-25-mcp-ui-future-agentic-interfaces/index.md index 8bf77db9e8..8690cfffeb 100644 --- a/documentation/blog/2025-08-25-mcp-ui-future-agentic-interfaces/index.md +++ b/documentation/blog/2025-08-25-mcp-ui-future-agentic-interfaces/index.md @@ -138,12 +138,12 @@ The future of interacting with AI relies on smarter interfaces. And with MCP-UI, - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md index 51c9609a96..2bec713f6d 100644 --- a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md +++ b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md @@ -55,12 +55,12 @@ There are currently several families of things it can show: - + - + - + - + diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md index ac43638085..11cecbc328 100644 --- a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md +++ b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md @@ -64,17 +64,17 @@ Thank you to Tetrate for supporting open source and making AI development more a **What are you waiting for?** [Get started with goose](/) -*Got questions?* Explore our [docs](/docs/category/guides), browse the [blog](/blog), or join the conversation in our [Discord](https://discord.gg/goose-oss) and [GitHub Discussions](https://github.com/block/goose/discussions). We’d love to have you. +*Got questions?* Explore our [docs](/docs/category/guides), browse the [blog](/blog), or join the conversation in our [Discord](https://discord.gg/goose-oss) and [GitHub Discussions](https://github.com/aaif-goose/goose/discussions). We’d love to have you. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-08-28-ai-teammate/index.md b/documentation/blog/2025-08-28-ai-teammate/index.md index 575c14df57..79e49337e1 100644 --- a/documentation/blog/2025-08-28-ai-teammate/index.md +++ b/documentation/blog/2025-08-28-ai-teammate/index.md @@ -55,7 +55,7 @@ The recipe: - Handles different flag implementations, ranging in complexity - Attempts refactoring for outdated code paths - Can automatically create draft pull requests, via the GitHub CLI -- Integrates with [Xcode Index MCP](https://github.com/block/xcode-index-mcp) for deep iOS project understanding +- Integrates with [Xcode Index MCP](https://github.com/aaif-goose/xcode-index-mcp) for deep iOS project understanding - Plans, implements, builds and tests locally to improve the accuracy of the flag removal result @@ -96,12 +96,12 @@ Want to tweak the base recipe for your own needs? Check out [Clean up feature fl - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-09-08-turn-any-mcp-server-mcp-ui-compatible/index.mdx b/documentation/blog/2025-09-08-turn-any-mcp-server-mcp-ui-compatible/index.mdx index 97f12c1620..66a7d73f4e 100644 --- a/documentation/blog/2025-09-08-turn-any-mcp-server-mcp-ui-compatible/index.mdx +++ b/documentation/blog/2025-09-08-turn-any-mcp-server-mcp-ui-compatible/index.mdx @@ -329,18 +329,18 @@ Wanna see it in action? Download [goose](/docs/quickstart#install-goose), give an MCP server a UI facelift of your own, and see the magic for yourself. Boring text prompts will never hit the same again. -*Got questions?* Explore our [docs](/docs/category/guides), browse the [blog](/blog), or join the conversation in our [Discord](https://discord.gg/goose-oss) and [GitHub Discussions](https://github.com/block/goose/discussions). We’d love to have you. +*Got questions?* Explore our [docs](/docs/category/guides), browse the [blog](/blog), or join the conversation in our [Discord](https://discord.gg/goose-oss) and [GitHub Discussions](https://github.com/aaif-goose/goose/discussions). We’d love to have you. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-09-15-subrecipes-in-goose/index.md b/documentation/blog/2025-09-15-subrecipes-in-goose/index.md index 533f829182..11265ad295 100644 --- a/documentation/blog/2025-09-15-subrecipes-in-goose/index.md +++ b/documentation/blog/2025-09-15-subrecipes-in-goose/index.md @@ -379,23 +379,23 @@ Ready to start building your own subrecipe workflows? The kitchen is open, and a ## Share Your Recipes with Us! -Do you have a recipe you'd like to share with the community? We'd love to feature them in our [Recipe Cookbook](https://block.github.io/goose/recipes/)! +Do you have a recipe you'd like to share with the community? We'd love to feature them in our [Recipe Cookbook](https://goose-docs.ai/recipes/)! **How to contribute:** -1. [Fork the goose repository](https://github.com/block/goose/fork) -2. Add your recipe YAML file to the [`documentation/src/pages/recipes/data/recipes/`](https://github.com/block/goose/tree/main/documentation/src/pages/recipes/data/recipes) directory -3. Create a pull request following our [Recipe Contribution Guide](https://github.com/block/goose/blob/main/CONTRIBUTING_RECIPES.md) -Browse [existing recipes](https://github.com/block/goose/tree/main/documentation/src/pages/recipes/data/recipes) for inspiration and formatting examples. +1. [Fork the goose repository](https://github.com/aaif-goose/goose/fork) +2. Add your recipe YAML file to the [`documentation/src/pages/recipes/data/recipes/`](https://github.com/aaif-goose/goose/tree/main/documentation/src/pages/recipes/data/recipes) directory +3. Create a pull request following our [Recipe Contribution Guide](https://github.com/aaif-goose/goose/blob/main/CONTRIBUTING_RECIPES.md) +Browse [existing recipes](https://github.com/aaif-goose/goose/tree/main/documentation/src/pages/recipes/data/recipes) for inspiration and formatting examples. - + - + - + - + diff --git a/documentation/blog/2025-09-26-hacktoberfest-2025/index.md b/documentation/blog/2025-09-26-hacktoberfest-2025/index.md index 524efbf830..88b9785996 100644 --- a/documentation/blog/2025-09-26-hacktoberfest-2025/index.md +++ b/documentation/blog/2025-09-26-hacktoberfest-2025/index.md @@ -15,9 +15,9 @@ October is around the corner, which means spooky season is upon us, and with tha [Hacktoberfest](https://hacktoberfest.com/participation/) is a month-long celebration every October of open source and participating open source projects. It's a great chance to earn fun prizes for contributing to your favorite projects, or a perfect opportunity to get your feet wet in a new project. # πŸŽ‰ How Can I Contribute to goose? -In the [goose repo](https://github.com/block/goose), there's something for everyone no matter if you're a newbie or a veteran. +In the [goose repo](https://github.com/aaif-goose/goose), there's something for everyone no matter if you're a newbie or a veteran. -We've created an official [goose Hacktoberfest 2025 Project Hub](https://github.com/block/goose/issues/4705). This hub will serve as your source of truth for all open `hacktoberfest` labeled-issues and resources you will need throughout the month. It will be updated every week with new issues as we experience contributions, so make sure to bookmark it! +We've created an official [goose Hacktoberfest 2025 Project Hub](https://github.com/aaif-goose/goose/issues/4705). This hub will serve as your source of truth for all open `hacktoberfest` labeled-issues and resources you will need throughout the month. It will be updated every week with new issues as we experience contributions, so make sure to bookmark it! From writing blog posts about goose, fixing bugs, or adding new features, our goal is to give contributors of various practices the chance to contribute to goose! Here are all the categories of issues you can expect to make your impact: @@ -33,9 +33,9 @@ From writing blog posts about goose, fixing bugs, or adding new features, our go To help you make the most of each of your contributions, below is a quick guide on general rules to follow, important resources, how top contributors are tallied, and more below: ## βœ… Key Rules -1. Read the [code of conduct](https://github.com/block/.github/blob/main/CODE_OF_CONDUCT.md). -2. Refer to the [Responsible AI-Assisted Coding Guide](https://web.archive.org/web/20260305122657/https://github.com/block/goose/blob/main/HOWTOAI.md), [Contributing Guide](https://github.com/block/goose/blob/main/CONTRIBUTING.md) & [README](https://github.com/block/goose/blob/main/README.md) for your contributions. -3. Choose a task from this project's Hacktoberfest [Project Hub](https://github.com/block/goose/issues/4705). Each issue has the 🏷️ `hacktoberfest` label. +1. Read the [code of conduct](https://github.com/aaif-goose/.github/blob/main/CODE_OF_CONDUCT.md). +2. Refer to the [Responsible AI-Assisted Coding Guide](https://web.archive.org/web/20260305122657/https://github.com/aaif-goose/goose/blob/main/HOWTOAI.md), [Contributing Guide](https://github.com/aaif-goose/goose/blob/main/CONTRIBUTING.md) & [README](https://github.com/aaif-goose/goose/blob/main/README.md) for your contributions. +3. Choose a task from this project's Hacktoberfest [Project Hub](https://github.com/aaif-goose/goose/issues/4705). Each issue has the 🏷️ `hacktoberfest` label. 4. Comment ".take" on the corresponding issue to get assigned the task. 5. Fork the repository and create a new branch for your work. 6. Make your changes and submit a pull request. @@ -75,7 +75,7 @@ When in doubt, don't stress! The goose team is here to help you make the most of 1. πŸ†˜ Maintainer Help: Connect with the maintainers by commenting on the issue or chat with them directly in the #πŸŽƒβ”ƒhacktoberfest channel on Discord. 2. 🫢 Community Support: [Join us on Discord](https://discord.gg/goose-oss) to live chat with fellow contributors and get tips for success from participants! -3. πŸŽ™οΈ Office Hours: Join us every Tuesday at 1pm Eastern Time on [Discord](https://discord.gg/goose-oss) through the month of October to get live-over-voice help with open PRs, questions you may have, and general help. Keep an eye out on the [goose events calendar](https://block.github.io/goose/community/) for future events centered around Hacktoberfest! +3. πŸŽ™οΈ Office Hours: Join us every Tuesday at 1pm Eastern Time on [Discord](https://discord.gg/goose-oss) through the month of October to get live-over-voice help with open PRs, questions you may have, and general help. Keep an eye out on the [goose events calendar](https://goose-docs.ai/community/) for future events centered around Hacktoberfest! Happy hacking and cheers to goose x Hacktoberfest 2025! πŸŽ‰ @@ -85,12 +85,12 @@ Happy hacking and cheers to goose x Hacktoberfest 2025! πŸŽ‰ - + - + - + - + diff --git a/documentation/blog/2025-09-26-subagents-vs-subrecipes/index.md b/documentation/blog/2025-09-26-subagents-vs-subrecipes/index.md index b89b6322c7..e9d46ea7a1 100644 --- a/documentation/blog/2025-09-26-subagents-vs-subrecipes/index.md +++ b/documentation/blog/2025-09-26-subagents-vs-subrecipes/index.md @@ -112,19 +112,19 @@ The choice depends on your specific needs and workflow requirements. Quick tasks Your workflow requirements should drive the decision. -Share your subagent prompts or subrecipe ideas with us on our [Discord community](https://discord.gg/goose-oss) or [GitHub discussions](https://github.com/block/goose/discussions). +Share your subagent prompts or subrecipe ideas with us on our [Discord community](https://discord.gg/goose-oss) or [GitHub discussions](https://github.com/aaif-goose/goose/discussions). - + - + - + - + diff --git a/documentation/blog/2025-10-08-recipe-cookbook-generator/index.md b/documentation/blog/2025-10-08-recipe-cookbook-generator/index.md index de0331db43..986d24ebd9 100644 --- a/documentation/blog/2025-10-08-recipe-cookbook-generator/index.md +++ b/documentation/blog/2025-10-08-recipe-cookbook-generator/index.md @@ -92,7 +92,7 @@ The tool should automatically identify good candidates for parameters. I had ide ### 4. Template Generation :::tip -I wanted goose to write all the recipes for me, but to be as up-to-date as possible. I cloned the [goose repository](https://github.com/block/goose) and told goose to examine its own source code to learn how to successfully create recipes, and be sure to use proper YAML syntax. +I wanted goose to write all the recipes for me, but to be as up-to-date as possible. I cloned the [goose repository](https://github.com/aaif-goose/goose) and told goose to examine its own source code to learn how to successfully create recipes, and be sure to use proper YAML syntax. ::: From that, I had goose think about the following ideas as it considered how to make my recipes: @@ -169,7 +169,7 @@ Start building your own cookbook generator, and stop doing the same work twice. ## Contribute to Our Community Cookbook -Want to contribute your own recipes or improvements to the cookbook generator? Join our [Discord community](https://discord.gg/goose-oss) or check out our [GitHub repository](https://github.com/block/goose) for more automation ideas. Join our [Hacktoberfest](https://github.com/block/goose/issues/4705) event going on to contribute recipes and prompt ideas to get on our leaderboard to win some great prizes! +Want to contribute your own recipes or improvements to the cookbook generator? Join our [Discord community](https://discord.gg/goose-oss) or check out our [GitHub repository](https://github.com/aaif-goose/goose) for more automation ideas. Join our [Hacktoberfest](https://github.com/aaif-goose/goose/issues/4705) event going on to contribute recipes and prompt ideas to get on our leaderboard to win some great prizes! ## My Own Cookbook Generator Recipe @@ -358,12 +358,12 @@ activities: - + - + - - + + diff --git a/documentation/blog/2025-10-14-designing-ai-for-humans/index.md b/documentation/blog/2025-10-14-designing-ai-for-humans/index.md index dff60c373e..82efeb1454 100644 --- a/documentation/blog/2025-10-14-designing-ai-for-humans/index.md +++ b/documentation/blog/2025-10-14-designing-ai-for-humans/index.md @@ -16,7 +16,7 @@ My mom was doing her usual Sunday ritual she had her pen, paper, calculator, and Last month, halfway through crunching numbers, she sighed and said, > β€œI just wish I could see where my money’s going.” -So I opened [**goose**](http://block.github.io/goose/docs/quickstart), added her notes, and turned on [**Auto Visualiser**](https://block.github.io/goose/docs/mcp/autovisualiser-mcp). In seconds, her budget became this colorful, interactive chart. She hovered over each slice, analyzing where her money was going. +So I opened [**goose**](http://goose-docs.ai/docs/quickstart), added her notes, and turned on [**Auto Visualiser**](https://goose-docs.ai/docs/mcp/autovisualiser-mcp). In seconds, her budget became this colorful, interactive chart. She hovered over each slice, analyzing where her money was going. Now, I'm not saying this is some groundbreaking use case. There are plenty of apps that do this. What stood out to me was how simple it felt. My mom didn’t need to learn anything new or adapt to someone else’s design. The visualization just appeared, and she got it immediately. @@ -49,7 +49,7 @@ Both point to the same future: one where AI doesn’t just reply with text, but That’s what Agentic UX is really about: building AI that responds to what the user intends to do. -If you’re building your own MCP server, start by thinking about the experience you want your users to have. Then experiment with MCP-UI and design the flow you’d expect if you were the one using it. For a full walkthrough, see [How To Make An MCP Server MCP-UI Compatible](https://block.github.io/goose/blog/2025/09/08/turn-any-mcp-server-mcp-ui-compatible). +If you’re building your own MCP server, start by thinking about the experience you want your users to have. Then experiment with MCP-UI and design the flow you’d expect if you were the one using it. For a full walkthrough, see [How To Make An MCP Server MCP-UI Compatible](https://goose-docs.ai/blog/2025/09/08/turn-any-mcp-server-mcp-ui-compatible). --- @@ -59,12 +59,12 @@ I have a feeling that next month, when my mom sits down to balance her bills, sh - + - + - - + + \ No newline at end of file diff --git a/documentation/blog/2025-10-24-intro-to-agent-client-protocol-acp/index.md b/documentation/blog/2025-10-24-intro-to-agent-client-protocol-acp/index.md index 7343ea62f5..c9d09ccf48 100644 --- a/documentation/blog/2025-10-24-intro-to-agent-client-protocol-acp/index.md +++ b/documentation/blog/2025-10-24-intro-to-agent-client-protocol-acp/index.md @@ -65,18 +65,18 @@ If you're ready to see how fast and simple this setup really is, watch the full -*Ready to integrate goose directly into your editor? Get started with our [ACP setup guide](https://block.github.io/goose/docs/guides/acp-clients) and share your experience in our [Discord community](http://discord.gg/goose-oss).* +*Ready to integrate goose directly into your editor? Get started with our [ACP setup guide](https://goose-docs.ai/docs/guides/acp-clients) and share your experience in our [Discord community](http://discord.gg/goose-oss).* - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-11-17-migrate-app-with-ai-agent/index.md b/documentation/blog/2025-11-17-migrate-app-with-ai-agent/index.md index 078bfab7e2..2e9bb9f492 100644 --- a/documentation/blog/2025-11-17-migrate-app-with-ai-agent/index.md +++ b/documentation/blog/2025-11-17-migrate-app-with-ai-agent/index.md @@ -133,12 +133,12 @@ I hope this clarifies how to converse with an agent and accomplish complex tasks - + - + - + - + diff --git a/documentation/blog/2025-11-21-social-media-agent-automation/index.md b/documentation/blog/2025-11-21-social-media-agent-automation/index.md index 5029958b88..5c66e42c9a 100644 --- a/documentation/blog/2025-11-21-social-media-agent-automation/index.md +++ b/documentation/blog/2025-11-21-social-media-agent-automation/index.md @@ -183,7 +183,7 @@ Sometimes the right move is to reduce instead of add, and this new version ended To fully automate this workflow, you must schedule your recipe. In goose Desktop, open the `recipe` section, click the `calendar icon` , and choose when it should run (I set mine to 10 AM daily). -You can read more in the [Reusable Recipes Guide](https://block.github.io/goose/docs/guides/recipes/session-recipes#schedule-recipe). +You can read more in the [Shareable Recipes Guide](https://goose-docs.ai/docs/guides/recipes/session-recipes#schedule-recipe). :::
@@ -459,12 +459,12 @@ Got questions or ideas? Come chat with us on [Discord](https://discord.gg/block- - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-11-26-mcp-for-devs/index.md b/documentation/blog/2025-11-26-mcp-for-devs/index.md index 3952414965..433e5dc08b 100644 --- a/documentation/blog/2025-11-26-mcp-for-devs/index.md +++ b/documentation/blog/2025-11-26-mcp-for-devs/index.md @@ -132,12 +132,12 @@ P.S. Happy first birthday, MCP! πŸŽ‰ - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-11-30-announcing-advent-of-ai/index.md b/documentation/blog/2025-11-30-announcing-advent-of-ai/index.md index 1976149925..29dcbcbd48 100644 --- a/documentation/blog/2025-11-30-announcing-advent-of-ai/index.md +++ b/documentation/blog/2025-11-30-announcing-advent-of-ai/index.md @@ -55,12 +55,12 @@ The first challenge unlocks December 1st at 12 PM ET. The Winter Festival needs - + - + - + - + diff --git a/documentation/blog/2025-12-04-mcp-sampling/index.md b/documentation/blog/2025-12-04-mcp-sampling/index.md index 54591776e7..894ccd5344 100644 --- a/documentation/blog/2025-12-04-mcp-sampling/index.md +++ b/documentation/blog/2025-12-04-mcp-sampling/index.md @@ -54,7 +54,7 @@ The `ctx.sample()` call sends a prompt back to the connected AI and waits for a ## A Real Example: Council of Mine -[Council of Mine](https://github.com/block/mcp-council-of-mine) is an MCP server that takes sampling to an extreme. It simulates a council of nine AI personas who debate topics and vote on each other's opinions. +[Council of Mine](https://github.com/aaif-goose/mcp-council-of-mine) is an MCP server that takes sampling to an extreme. It simulates a council of nine AI personas who debate topics and vote on each other's opinions. But there's no LLM running inside the server. Every opinion, every vote, every bit of reasoning comes from sampling requests back to the user's connected LLM. @@ -200,12 +200,12 @@ If you want to see sampling in action, [Council of Mine](/docs/mcp/council-of-mi - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-12-10-stop-ai-agent-unwanted-changes/index.md b/documentation/blog/2025-12-10-stop-ai-agent-unwanted-changes/index.md index 0fdf6c25d2..f7a6ee19e6 100644 --- a/documentation/blog/2025-12-10-stop-ai-agent-unwanted-changes/index.md +++ b/documentation/blog/2025-12-10-stop-ai-agent-unwanted-changes/index.md @@ -46,12 +46,12 @@ Try out this method with [goose](/) to help you build your next project. Your fu - + - + - + - + diff --git a/documentation/blog/2025-12-15-code-mode-mcp/index.md b/documentation/blog/2025-12-15-code-mode-mcp/index.md index 96f1326a78..6d628b24cb 100644 --- a/documentation/blog/2025-12-15-code-mode-mcp/index.md +++ b/documentation/blog/2025-12-15-code-mode-mcp/index.md @@ -47,7 +47,7 @@ MCP server with ease! Our hope is that we improve tool calling performance and handling of large numbers of tools in goose, but also provide an open source implementation of this emerging approach. -* Try out the feature by enabling the ["Code Mode" extension](https://github.com/block/goose/blob/main/crates/goose/src/agents/platform_extensions/code_execution.rs) in v1.17.0 or later of goose by clicking extensions on the left side of the desktop app or running `goose configure` on cli +* Try out the feature by enabling the ["Code Mode" extension](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/agents/platform_extensions/code_execution.rs) in v1.17.0 or later of goose by clicking extensions on the left side of the desktop app or running `goose configure` on cli * Please give us feedback on how it works for you by joining our [discord](https://discord.gg/goose-oss). Kudos to my colleague [Mic Neale](https://github.com/michaelneale) for collaborating with me on the implementation! @@ -55,12 +55,12 @@ Kudos to my colleague [Mic Neale](https://github.com/michaelneale) for collabora - + - + - + - + diff --git a/documentation/blog/2025-12-19-does-your-ai-agent-need-a-plan/index.md b/documentation/blog/2025-12-19-does-your-ai-agent-need-a-plan/index.md index f7172573a6..cdb931767e 100644 --- a/documentation/blog/2025-12-19-does-your-ai-agent-need-a-plan/index.md +++ b/documentation/blog/2025-12-19-does-your-ai-agent-need-a-plan/index.md @@ -21,7 +21,7 @@ Lately, there's been a healthy debate in the industry about planning in AI codin This reveals an interesting truth: the value of a plan mode isn't just about the plan itself. It's about creating the right mental model and workflow for the developer using it. Sometimes you want the agent to just execute. Other times, you want to see its thinking, provide feedback, and collaborate on the approach before any code changes happen. -Rather than picking one philosophy, [goose](https://github.com/block/goose) supports multiple approaches because different situations call for different methods. +Rather than picking one philosophy, [goose](https://github.com/aaif-goose/goose) supports multiple approaches because different situations call for different methods. --- @@ -91,13 +91,13 @@ The question isn't whether to plan. The question is which kind of planning fits - + - + - + - + diff --git a/documentation/blog/2025-12-19-goose-mobile-terminal/index.md b/documentation/blog/2025-12-19-goose-mobile-terminal/index.md index 2c95be5d2a..95ae6a8bd6 100644 --- a/documentation/blog/2025-12-19-goose-mobile-terminal/index.md +++ b/documentation/blog/2025-12-19-goose-mobile-terminal/index.md @@ -55,12 +55,12 @@ Try them out and let us know what you think in our [Discord](https://discord.gg/ - + - + - + - + diff --git a/documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md b/documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md index c36c8d2154..ff69bae67c 100644 --- a/documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md +++ b/documentation/blog/2025-12-21-code-mode-doesnt-replace-mcp/index.md @@ -95,9 +95,9 @@ Therefore, it doesn't make sense for me to use Code Mode when: The cool part is Code Mode is only getting better. The team is currently refining Code Mode following its release in goose v1.17.0 (December 2025): -- [Better UX](https://github.com/block/goose/pull/6205) - showing what tools are being called instead of raw JavaScript -- [Better reliability](https://github.com/block/goose/pull/6177) - improving type signatures so LLMs get the code right the first time -- [More capabilities](https://github.com/block/goose/pull/6160) - enabling subagents to work inside Code Mode +- [Better UX](https://github.com/aaif-goose/goose/pull/6205) - showing what tools are being called instead of raw JavaScript +- [Better reliability](https://github.com/aaif-goose/goose/pull/6177) - improving type signatures so LLMs get the code right the first time +- [More capabilities](https://github.com/aaif-goose/goose/pull/6160) - enabling subagents to work inside Code Mode Code Mode helps us take a step forward in building agents that can scale to handle all your tools without falling apart. I love seeing how MCP is evolving, and I can't wait for the day I tell my children that agents weren't always this limitless and that we actually used to have to ration our tools just to get a simple task done. @@ -108,13 +108,13 @@ Code Mode helps us take a step forward in building agents that can scale to hand - + - + - + - + diff --git a/documentation/blog/2025-12-22-agent-skills-vs-mcp/index.md b/documentation/blog/2025-12-22-agent-skills-vs-mcp/index.md index dceb32907f..6c197e9fdd 100644 --- a/documentation/blog/2025-12-22-agent-skills-vs-mcp/index.md +++ b/documentation/blog/2025-12-22-agent-skills-vs-mcp/index.md @@ -81,12 +81,12 @@ That's progress, not replacement. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2025-12-28-goose-maintains-goose/index.md b/documentation/blog/2025-12-28-goose-maintains-goose/index.md index 5fa32af0fa..da14c9114f 100644 --- a/documentation/blog/2025-12-28-goose-maintains-goose/index.md +++ b/documentation/blog/2025-12-28-goose-maintains-goose/index.md @@ -14,7 +14,7 @@ We embraced this reality and put goose to work on its own backlog. -We actually used goose pre-1.0 to help us build goose 1.0. The original goose was a Python CLI, but we needed to move quickly to Rust, Electron, and an [MCP-native](https://modelcontextprotocol.io) architecture. goose helped us make that transition. Using it to triage issues and review changes felt like a natural extension, so we embedded goose directly into a [GitHub Action](https://github.com/block/goose/blob/main/.github/workflows/goose-issue-solver.yml). +We actually used goose pre-1.0 to help us build goose 1.0. The original goose was a Python CLI, but we needed to move quickly to Rust, Electron, and an [MCP-native](https://modelcontextprotocol.io) architecture. goose helped us make that transition. Using it to triage issues and review changes felt like a natural extension, so we embedded goose directly into a [GitHub Action](https://github.com/aaif-goose/goose/blob/main/.github/workflows/goose-issue-solver.yml). :::note Credit That GitHub Action workflow was built by [Tyler Longwell](https://github.com/tlongwell-block), who took an idea we had been exploring manually and turned it into something any maintainer could trigger with a single comment. @@ -24,9 +24,9 @@ That GitHub Action workflow was built by [Tyler Longwell](https://github.com/tlo Before the GitHub Action existed, the goose team was already using goose to accelerate our issue workflow. Here's a real example. -A user reached out on Discord asking why an Ollama model was throwing an error in chat mode. Rather than digging through the codebase myself, I asked goose to explore the code, identify the root cause, and explain it back to me. Then, I asked goose to use the GitHub CLI to open an [issue](https://github.com/block/goose/issues/6117). +A user reached out on Discord asking why an Ollama model was throwing an error in chat mode. Rather than digging through the codebase myself, I asked goose to explore the code, identify the root cause, and explain it back to me. Then, I asked goose to use the GitHub CLI to open an [issue](https://github.com/aaif-goose/goose/issues/6117). -During that same session, goose mentioned it had 95% confidence it knew how to fix the problem. The change was small, so I asked goose to open a [PR](https://github.com/block/goose/pull/6118). It was merged the same day. +During that same session, goose mentioned it had 95% confidence it knew how to fix the problem. The change was small, so I asked goose to open a [PR](https://github.com/aaif-goose/goose/pull/6118). It was merged the same day. This kind of workflow has changed how I operate as a Developer Advocate. Before goose, when a user reported a problem, the process unfolded in fragments. I would ask clarifying questions, check GitHub for related issues, pull the latest code, grep through files, read the logic, and try to form a hypothesis about what was going wrong. @@ -44,13 +44,13 @@ The local workflow works. But when I solve an issue locally with goose, I'm stil The GitHub Action compresses that entire sequence into a single comment. A team member sees an issue, comments `/goose`, and moves on. goose spins up in a container, reads the issue, explores the codebase, runs verification, and opens a draft PR. The maintainer returns to a proposed solution rather than a blank slate. -We saw this play out with [issue #6066](https://github.com/block/goose/issues/6066). Users reported that goose kept defaulting to 2024 even though the correct datetime was in the context. The issue sat for two days. Then Tyler saw it, commented `/goose solve this minimally` at 1:59 AM, and went back to whatever he was doing (presumably sleeping). Fourteen minutes later, goose opened [PR #6101](https://github.com/block/goose/pull/6101). +We saw this play out with [issue #6066](https://github.com/aaif-goose/goose/issues/6066). Users reported that goose kept defaulting to 2024 even though the correct datetime was in the context. The issue sat for two days. Then Tyler saw it, commented `/goose solve this minimally` at 1:59 AM, and went back to whatever he was doing (presumably sleeping). Fourteen minutes later, goose opened [PR #6101](https://github.com/aaif-goose/goose/pull/6101). The maintainer's role shifts from implementing to reviewing. The bottleneck in open source is rarely "can someone write this code." It's "can someone with enough context find the time to write this code." The GitHub Action decouples those two constraints. Any maintainer can trigger a fix attempt without deep familiarity with that part of the codebase. This scales in a way manual triage cannot. A backlog contains feature requests, complex bugs, and quick fixes in equal measure. The Action lets you point at an issue and say "try this one" without committing your afternoon. If goose fails, you lose minutes of compute. If it succeeds, you save hours. -For contributors, responsiveness changes everything. When a user filed [issue #6232](https://github.com/block/goose/issues/6232) about slash commands not handling optional parameters, a maintainer quickly commented `/goose can you fix this`, and within the hour there was a draft PR with the fix and four new tests. Even if the PR is not perfect and needs adjustments, contributors see momentum. +For contributors, responsiveness changes everything. When a user filed [issue #6232](https://github.com/aaif-goose/goose/issues/6232) about slash commands not handling optional parameters, a maintainer quickly commented `/goose can you fix this`, and within the hour there was a draft PR with the fix and four new tests. Even if the PR is not perfect and needs adjustments, contributors see momentum. ## Under the Hood @@ -58,7 +58,7 @@ Maintainers summon goose with `/goose` followed by a prompt as a comment on an i But there's more happening under the hood than a simple prompt like "/goose fix this." -The workflow uses a [recipe](https://github.com/block/goose/blob/main/.github/workflows/goose-issue-solver.yml#L14-L78) that defines phases to ensure goose actually accomplishes the job and doesn't do more than we ask it to. +The workflow uses a [recipe](https://github.com/aaif-goose/goose/blob/main/.github/workflows/goose-issue-solver.yml#L14-L78) that defines phases to ensure goose actually accomplishes the job and doesn't do more than we ask it to. | Phase | What goose does | Why it matters | | ---------- | ----------------------------------------------------- | --------------------------------------------------------------------- | @@ -69,7 +69,7 @@ The workflow uses a [recipe](https://github.com/block/goose/blob/main/.github/wo | Verify | Run tests and linters | Catches obvious failures before a human sees the PR | | Confirm | Reread the original issue and requirements | Prevents the AI from declaring victory while forgetting half the task | -The [recipe](https://github.com/block/goose/blob/main/.github/workflows/goose-issue-solver.yml) also gives goose access to the [TODO extension](/docs/mcp/todo-mcp), a built-in tool that acts as external memory. The phases tell goose *what* to do. The TODO helps goose *remember* what it's doing. As goose reads through the codebase and builds a solution, its context window fills up and earlier instructions can be compressed or lost. The TODO persists, so goose can always check what it's done and what's left. +The [recipe](https://github.com/aaif-goose/goose/blob/main/.github/workflows/goose-issue-solver.yml) also gives goose access to the [TODO extension](/docs/mcp/todo-mcp), a built-in tool that acts as external memory. The phases tell goose *what* to do. The TODO helps goose *remember* what it's doing. As goose reads through the codebase and builds a solution, its context window fills up and earlier instructions can be compressed or lost. The TODO persists, so goose can always check what it's done and what's left. The workflow also enforces guardrails around who can invoke `/goose`, which files it's allowed to touch, and the requirement that a maintainer review and approve every PR. @@ -79,17 +79,17 @@ The future we're aiming for isn't one where AI replaces maintainers. It's one wh If that becomes the norm, open source scales differently. -The [GitHub Action workflow](https://github.com/block/goose/blob/main/.github/workflows/goose-issue-solver.yml) is public for anyone who wants to explore this pattern in their own CI pipeline. +The [GitHub Action workflow](https://github.com/aaif-goose/goose/blob/main/.github/workflows/goose-issue-solver.yml) is public for anyone who wants to explore this pattern in their own CI pipeline. - + - + - + - + diff --git a/documentation/blog/2026-01-04-how-i-taught-my-agent-my-design-taste/index.md b/documentation/blog/2026-01-04-how-i-taught-my-agent-my-design-taste/index.md index abd2cb6f56..f4a023c1b3 100644 --- a/documentation/blog/2026-01-04-how-i-taught-my-agent-my-design-taste/index.md +++ b/documentation/blog/2026-01-04-how-i-taught-my-agent-my-design-taste/index.md @@ -123,12 +123,12 @@ The code and full transcripts live in [my Genuary 2026 repo](https://github.com/ - + - + - + - + diff --git a/documentation/blog/2026-01-05-agentic-guardrails-and-controls/index.md b/documentation/blog/2026-01-05-agentic-guardrails-and-controls/index.md index 7d7eb54851..0c1b4e700e 100644 --- a/documentation/blog/2026-01-05-agentic-guardrails-and-controls/index.md +++ b/documentation/blog/2026-01-05-agentic-guardrails-and-controls/index.md @@ -9,7 +9,7 @@ authors: ![blog cover](agentic_guardrails_header.jpg) -In [our previous blog post](https://block.github.io/goose/blog/2025/03/31/securing-mcp/) we detailed the Model Context Protocol (MCP) system and discussed some security concerns and mitigations. As a brief recap, MCP provides agents with a means to accomplish tasks using defined tools; reducing the burden of using complex and varied APIs and integrations on the agent. +In [our previous blog post](https://goose-docs.ai/blog/2025/03/31/securing-mcp/) we detailed the Model Context Protocol (MCP) system and discussed some security concerns and mitigations. As a brief recap, MCP provides agents with a means to accomplish tasks using defined tools; reducing the burden of using complex and varied APIs and integrations on the agent.
@@ -247,17 +247,17 @@ In this post we have contextualised the risks associated with LLM Content Inject We’re working on a proof of concept and benchmarking for goose in the background. Once released we will update this blog with the results (either good or bad) outlining the effectiveness of the mitigation. Another area we intend to explore is the application to multi-agent systems. Our application of this is intended for human facing agentic systems. However, it likely has applications in fully autonomous player-coach systems (similar to what is described in [Anthropic’s Multi-Agent Research Systems](https://www.anthropic.com/engineering/multi-agent-research-system) or [Block’s Adversarial Cooperation in Code Synthesis](https://block.xyz/documents/adversarial-cooperation-in-code-synthesis.pdf)) where the orchestrating Agent takes the role of the human providing initial prompts, but also defining allowable tool-calls or interactions. -We also welcome any and all feedback and suggestions on improving the concept. [Hit us up on the goose GitHub discussion](https://github.com/block/goose/discussions/6328) +We also welcome any and all feedback and suggestions on improving the concept. [Hit us up on the goose GitHub discussion](https://github.com/aaif-goose/goose/discussions/6328) - + - + - + - + diff --git a/documentation/blog/2026-01-06-mcp-apps/index.md b/documentation/blog/2026-01-06-mcp-apps/index.md index 13b39f66e3..ed8e4fbd43 100644 --- a/documentation/blog/2026-01-06-mcp-apps/index.md +++ b/documentation/blog/2026-01-06-mcp-apps/index.md @@ -13,7 +13,7 @@ The MCP ecosystem is standardizing how servers deliver interactive UIs to hosts, ## What's Shipping -This release ([v1.19.0](https://github.com/block/goose/releases/tag/v1.19.0)) brings a minimal-but-functional implementation of MCP Apps: +This release ([v1.19.0](https://github.com/aaif-goose/goose/releases/tag/v1.19.0)) brings a minimal-but-functional implementation of MCP Apps: - Discovery of MCP App resources connected to tools - HTML content rendering in sandboxed iframes @@ -46,19 +46,19 @@ MCP-UI isn't going away overnight. We'll keep supporting it while the community - **Get started:** Update goose and point it at an MCP server that returns App resources - **Read the spec:** [github.com/modelcontextprotocol/ext-apps](https://github.com/modelcontextprotocol/ext-apps) -- **Join the conversation:** [goose GitHub discussion](https://github.com/block/goose/discussions/6069) Β· [MCP Contributors Discord](https://discord.gg/6CSzBmMkjX) +- **Join the conversation:** [goose GitHub discussion](https://github.com/aaif-goose/goose/discussions/6069) Β· [MCP Contributors Discord](https://discord.gg/6CSzBmMkjX) If you build or port an app, we want to hear from you. File issues, share demos, tell us what's broken. Early feedback shapes what comes next. - + - + - + - + diff --git a/documentation/blog/2026-01-15-why-tool-descriptions-arent-enough/index.md b/documentation/blog/2026-01-15-why-tool-descriptions-arent-enough/index.md index deefb91468..e80dabeeb0 100644 --- a/documentation/blog/2026-01-15-why-tool-descriptions-arent-enough/index.md +++ b/documentation/blog/2026-01-15-why-tool-descriptions-arent-enough/index.md @@ -21,7 +21,7 @@ But something still felt off. The tools still weren’t really *thinking*. They were fetching data, returning text, and leaving all the heavy reasoning to my LLM. That’s when I realized the issue wasn’t my descriptions. It was how the system actually worked under the hood. -That’s where [MCP sampling](https://block.github.io/goose/docs/guides/mcp-sampling/) came in. +That’s where [MCP sampling](https://goose-docs.ai/docs/guides/mcp-sampling/) came in. Not as a magic feature, but as a different way of structuring how tools and the LLM actually collaborate. ## What actually changed my understanding @@ -49,7 +49,7 @@ It’s the same model and the same agent, but the behavior changes completely. ## Where Council of Mine fits in -Seeing the flow change helped me understand sampling conceptually. [Council of Mine](https://github.com/block/mcp-council-of-mine) helped me understand it viscerally. +Seeing the flow change helped me understand sampling conceptually. [Council of Mine](https://github.com/aaif-goose/mcp-council-of-mine) helped me understand it viscerally. It’s not MCP sampling itself. It’s an example of what becomes possible once sampling exists. @@ -67,17 +67,17 @@ But on their own, they won’t get you to truly agentic behavior. Descriptions s That distinction was the missing piece for me. And once I could actually see the flow, everything else started to make more sense. -If this helped make things click, I’d recommend trying the [Council of Mine extension](https://block.github.io/goose/docs/mcp/council-of-mine-mcp) for yourself. It’s one of the clearest ways to see MCP sampling in action. +If this helped make things click, I’d recommend trying the [Council of Mine extension](https://goose-docs.ai/docs/mcp/council-of-mine-mcp) for yourself. It’s one of the clearest ways to see MCP sampling in action. - + - + - + - + diff --git a/documentation/blog/2026-01-20-goose-mobile-apps/index.md b/documentation/blog/2026-01-20-goose-mobile-apps/index.md index 9405a34d4e..699acc80f9 100644 --- a/documentation/blog/2026-01-20-goose-mobile-apps/index.md +++ b/documentation/blog/2026-01-20-goose-mobile-apps/index.md @@ -10,7 +10,7 @@ authors: In 2025 we did a fairly cutting edge take on whole device automation using Android (code name was gosling) which was an on-device agent that would take over your device (mic even used it to do some shopping - which he realized after some things arrived at his door that it had automatically purchased as the result of an email - hence the PoC/experimental label!) -Recently we consolidated the [apps for goose mobile](https://github.com/block/goose-mobile). +Recently we consolidated the [apps for goose mobile](https://github.com/aaif-goose/goose-mobile). The [goose-ios client](/blog/2025/12/19/goose-mobile-terminal/) is more production ready, and in the app store (still early days). We hope to have a port of that to Android, which will be strictly a client (and won't take over your device!) to your remote agent. The aim of the client (vs an on device agent) is for you to take your work on the go with you. @@ -31,12 +31,12 @@ For mobile client to work for personal (ie desktop/laptop/PC agents, not really - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2026-01-22-mcp-ui-to-mcp-apps/index.md b/documentation/blog/2026-01-22-mcp-ui-to-mcp-apps/index.md index ab372aca57..560e40c22a 100644 --- a/documentation/blog/2026-01-22-mcp-ui-to-mcp-apps/index.md +++ b/documentation/blog/2026-01-22-mcp-ui-to-mcp-apps/index.md @@ -328,7 +328,7 @@ This is one of those changes that doesn’t dramatically affect your day-to-day ## Try it yourself -If you’re curious about building MCP Apps yourself, follow the guide [Building MCP Apps](https://block.github.io/goose/docs/tutorials/building-mcp-apps/). +If you’re curious about building MCP Apps yourself, follow the guide [Building MCP Apps](https://goose-docs.ai/docs/tutorials/building-mcp-apps/). And if you already have an MCP-UI server, try converting just one tool to an MCP App. That’s usually the moment when everything starts to really click. @@ -339,12 +339,12 @@ As a reminder, MCP Apps run sandboxed with CSP restrictions, so it’s worth und - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2026-01-30-5-tips-building-mcp-apps/index.md b/documentation/blog/2026-01-30-5-tips-building-mcp-apps/index.md index 136b5a96c1..8512e91ac2 100644 --- a/documentation/blog/2026-01-30-5-tips-building-mcp-apps/index.md +++ b/documentation/blog/2026-01-30-5-tips-building-mcp-apps/index.md @@ -336,7 +336,7 @@ registerAppTool(server, "delete-email", { MCP Apps open up a new dimension for agent interactions. Now it's time to build your own. - **Test with [MCPJam](https://mcpjam.com/)** β€” the open source local inspector for MCP Apps, ChatGPT apps SDK, and MCP servers. Perfect for debugging and iterating on your app before shipping. -- **Run in [goose](https://github.com/block/goose)** β€” an open source AI agent that renders MCP Apps directly in the chat interface. See your app come to life in a real agent environment. +- **Run in [goose](https://github.com/aaif-goose/goose)** β€” an open source AI agent that renders MCP Apps directly in the chat interface. See your app come to life in a real agent environment. Ready to dive deeper? Check out the [MCP Apps tutorial](/docs/tutorials/building-mcp-apps) or [build your first MCP App with MCPJam](https://docs.mcpjam.com/guides/first-mcp-app). @@ -344,12 +344,12 @@ Ready to dive deeper? Check out the [MCP Apps tutorial](/docs/tutorials/building - + - + - + - + diff --git a/documentation/blog/2026-02-06-8-things-you-didnt-know-about-code-mode/index.md b/documentation/blog/2026-02-06-8-things-you-didnt-know-about-code-mode/index.md index 8de87f755e..ff4c91c7c8 100644 --- a/documentation/blog/2026-02-06-8-things-you-didnt-know-about-code-mode/index.md +++ b/documentation/blog/2026-02-06-8-things-you-didnt-know-about-code-mode/index.md @@ -218,20 +218,20 @@ If you want to experiment with Code Mode, here are some resources: **Community:** - Join our [Discord](https://discord.gg/goose-oss) to share what you learn -- File issues on [GitHub](https://github.com/block/goose) if something does not work as expected +- File issues on [GitHub](https://github.com/aaif-goose/goose) if something does not work as expected Run your own experiments and let us know what you find. - + - + - + - + diff --git a/documentation/blog/2026-02-06-rp-why-skill/index.md b/documentation/blog/2026-02-06-rp-why-skill/index.md index 8bddec83da..2b668a3eee 100644 --- a/documentation/blog/2026-02-06-rp-why-skill/index.md +++ b/documentation/blog/2026-02-06-rp-why-skill/index.md @@ -196,7 +196,7 @@ What's the most strategic question you could ask right now? Install the skill: ```bash -npx skills add https://github.com/block/agent-skills --skill rp-why +npx skills add https://github.com/aaif-goose/agent-skills --skill rp-why ``` Make sure you have the built-in [Skills extension](/docs/mcp/skills-mcp/) enabled in goose. @@ -258,12 +258,12 @@ A: Yes! The output is designed to be shareable. Screenshot or copy the quadrant - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2026-02-06-rpi-openclaw-alternative/index.md b/documentation/blog/2026-02-06-rpi-openclaw-alternative/index.md index 35fab99624..a8f36fe4bd 100644 --- a/documentation/blog/2026-02-06-rpi-openclaw-alternative/index.md +++ b/documentation/blog/2026-02-06-rpi-openclaw-alternative/index.md @@ -9,7 +9,7 @@ authors: Everyone on Tech Twitter has been buying Mac Minis, so they could run a local agentic tool called [OpenClaw](https://openclaw.ai/). OpenClaw is a messaging-based AI assistant that connects to platforms such as Discord and Telegram allowing you to interact with an AI agent through DMs or @mentions. Under the hood, it uses an agent called Pi to execute tasks, browse the web, write code, and more. -Seeing the hype made me want to get my hands dirty. I wanted to see if I could build a lite version for myself. I wanted something minimal that used [goose](https://github.com/block/goose) as the engine instead of Pi. I tentatively dubbed it AltOpenClaw. +Seeing the hype made me want to get my hands dirty. I wanted to see if I could build a lite version for myself. I wanted something minimal that used [goose](https://github.com/aaif-goose/goose) as the engine instead of Pi. I tentatively dubbed it AltOpenClaw. @@ -126,12 +126,12 @@ Happy hacking! - + - + - + - + diff --git a/documentation/blog/2026-02-07-context-engineering/index.md b/documentation/blog/2026-02-07-context-engineering/index.md index cd0381d3d8..638823a199 100644 --- a/documentation/blog/2026-02-07-context-engineering/index.md +++ b/documentation/blog/2026-02-07-context-engineering/index.md @@ -19,7 +19,7 @@ Saying β€œone shot prompting is dead” isn’t revolutionary. It’s just catch ## The blog post that aged faster than oat milk -Last year, I wrote a post about [how to prompt better](https://block.github.io/goose/blog/2025/03/19/better-ai-prompting). I shared tricks, phrasing tips, and even said to add a few β€œpleases” and β€œthank yous” and your AI agent would give you the world. At the time it felt cutting edge, because it was. There were livestreams and conference talks entirely about how to prompt better. +Last year, I wrote a post about [how to prompt better](https://goose-docs.ai/blog/2025/03/19/better-ai-prompting). I shared tricks, phrasing tips, and even said to add a few β€œpleases” and β€œthank yous” and your AI agent would give you the world. At the time it felt cutting edge, because it was. There were livestreams and conference talks entirely about how to prompt better. Less than a year later, it feels… quaint. Not because prompting stopped mattering, but because prompting stopped being the main character. @@ -31,7 +31,7 @@ to > β€œWhat environment am I dropping this model into?” -That’s a completely different problem, and now it has a name. **[Context engineering](https://block.github.io/goose/docs/guides/context-engineering/)**. +That’s a completely different problem, and now it has a name. **[Context engineering](https://goose-docs.ai/docs/guides/context-engineering/)**. --- @@ -78,7 +78,7 @@ Here’s what that loop looked like in practice: > *Even **this** demo is powered by persistent context.* -That was the moment I experimented with [RPI](https://block.github.io/goose/docs/tutorials/rpi). Not because it was trendy, but because the alternative had become tedious. +That was the moment I experimented with [RPI](https://goose-docs.ai/docs/tutorials/rpi). Not because it was trendy, but because the alternative had become tedious. You don’t have to adopt RPI, or any new pattern, tomorrow to benefit from this. You can simulate the shift in your next session with a small change in how you start. @@ -147,12 +147,12 @@ And honestly, that’s progress. - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2026-02-19-gastown-explained-goosetown/index.md b/documentation/blog/2026-02-19-gastown-explained-goosetown/index.md index a30600d99a..517e58a433 100644 --- a/documentation/blog/2026-02-19-gastown-explained-goosetown/index.md +++ b/documentation/blog/2026-02-19-gastown-explained-goosetown/index.md @@ -56,7 +56,7 @@ I won't list every single character here (it gets deep), but the takeaway is: Ga ## Introducing Goosetown -This is exactly the kind of futuristic thinking we're building toward at [goose](https://block.github.io/goose). So the goose team, specifically Tyler Longwell, built our own take on this called [Goosetown](https://github.com/block/goosetown). +This is exactly the kind of futuristic thinking we're building toward at [goose](https://goose-docs.ai). So the goose team, specifically Tyler Longwell, built our own take on this called [Goosetown](https://github.com/aaif-goose/goosetown). Goosetown is a multi-agent orchestration layer built on top of goose. Like Gas Town, it coordinates parallel agents. Unlike Gas Town, it's deliberately minimal and built for research-first parallel work. @@ -68,7 +68,7 @@ Here's a real Town Wall snippet from a session where parallel researchers conver * **[10:14] researcher-endpoints** - πŸ’‘ Found: native endpoint already exists with minimal deps. Alternative path viable. * **[10:15] researcher-source** - βœ… Done. Confirmed: native path requires zero new dependencies. Recommending pivot. -Goosetown operates on 4 components: [skills](/docs/guides/context-engineering/using-skills), [subagents](/docs/guides/subagents), [beads](https://github.com/steveyegge/beads), and a [gtwall](https://github.com/block/goosetown/blob/main/gtwall). +Goosetown operates on 4 components: [skills](/docs/guides/context-engineering/using-skills), [subagents](/docs/guides/subagents), [beads](https://github.com/steveyegge/beads), and a [gtwall](https://github.com/aaif-goose/goosetown/blob/main/gtwall). ### Skills @@ -84,7 +84,7 @@ Goosetown uses [Beads](https://github.com/steveyegge/beads) to track progress so ### gtwall -[gtwall](https://github.com/block/goosetown/blob/main/gtwall) is an append-only log that delegates use to communicate and coordinate. All delegates post and read activity. +[gtwall](https://github.com/aaif-goose/goosetown/blob/main/gtwall) is an append-only log that delegates use to communicate and coordinate. All delegates post and read activity. ## A Note from the Creator @@ -100,19 +100,19 @@ Goosetown uses [Beads](https://github.com/steveyegge/beads) to track progress so ## Get Started -Ready to try parallel agentic engineering for yourself? [Goosetown](https://github.com/block/goosetown) is open source and available on GitHub. Clone the [repo](https://github.com/block/goosetown), follow the setup instructions in the README, and you'll be orchestrating multiple agents in no time. If you're new to this workflow, watching the video below is a great way to see what a real session looks like before diving in. +Ready to try parallel agentic engineering for yourself? [Goosetown](https://github.com/aaif-goose/goosetown) is open source and available on GitHub. Clone the [repo](https://github.com/aaif-goose/goosetown), follow the setup instructions in the README, and you'll be orchestrating multiple agents in no time. If you're new to this workflow, watching the video below is a great way to see what a real session looks like before diving in. - + - + - + - + diff --git a/documentation/blog/2026-02-23-goose-v1-25-0/index.md b/documentation/blog/2026-02-23-goose-v1-25-0/index.md index 861903d82e..538eb0663b 100644 --- a/documentation/blog/2026-02-23-goose-v1-25-0/index.md +++ b/documentation/blog/2026-02-23-goose-v1-25-0/index.md @@ -19,7 +19,7 @@ Let's break down what's new. **The headline feature of v1.25.0 is security sandboxing for goose Desktop on macOS.** -When you give an AI agent access to your shell and file system, trust matters. With this release, goose Desktop now runs inside a [macOS sandbox](https://block.github.io/goose/docs/guides/sandbox) powered by [seatbelt](https://github.com/michaelneale/agent-seatbelt-sandbox), the same underlying technology Apple uses to sandbox its own apps. +When you give an AI agent access to your shell and file system, trust matters. With this release, goose Desktop now runs inside a [macOS sandbox](https://goose-docs.ai/docs/guides/sandbox) powered by [seatbelt](https://github.com/michaelneale/agent-seatbelt-sandbox), the same underlying technology Apple uses to sandbox its own apps. What does this mean in practice? @@ -32,9 +32,9 @@ This is a great starting point for defense-in-depth security. The sandbox checks ## 🧩 Unified Summon Extension -**We replaced two separate systems (subagent and Skills) with a single, unified ["Summon" extension](https://block.github.io/goose/docs/mcp/summon-mcp).** +**We replaced two separate systems (subagent and Skills) with a single, unified ["Summon" extension](https://goose-docs.ai/docs/mcp/summon-mcp).** -Previously, goose had two different mechanisms for delegating work: [subagents](https://block.github.io/goose/docs/tutorials/subagents) (for spinning up independent sub-tasks) and Skills (for loading predefined capabilities). They overlapped in confusing ways and made the system harder to understand. +Previously, goose had two different mechanisms for delegating work: [subagents](https://goose-docs.ai/docs/tutorials/subagents) (for spinning up independent sub-tasks) and Skills (for loading predefined capabilities). They overlapped in confusing ways and made the system harder to understand. The new **Summon** extension unifies both concepts into two clean tools: @@ -51,7 +51,7 @@ This simplification means: **MCP extensions can now render rich, interactive UIs directly inside goose Desktop.** -This release integrates the `AppRenderer` from the [`@mcp-ui/client`](https://www.npmjs.com/package/@mcp-ui/client) SDK, bringing a major upgrade to how [MCP apps](https://block.github.io/goose/docs/tutorials/building-mcp-apps) display their content. Instead of being limited to text output, MCP extensions can now provide full HTML/JavaScript interfaces that [render inline in the chat](https://block.github.io/goose/docs/guides/interactive-chat/mcp-ui). +This release integrates the `AppRenderer` from the [`@mcp-ui/client`](https://www.npmjs.com/package/@mcp-ui/client) SDK, bringing a major upgrade to how [MCP apps](https://goose-docs.ai/docs/tutorials/building-mcp-apps) display their content. Instead of being limited to text output, MCP extensions can now provide full HTML/JavaScript interfaces that [render inline in the chat](https://goose-docs.ai/docs/guides/interactive-chat/mcp-ui). Key improvements include: - **Fallback request handler support.** Apps can make requests back to the MCP server for dynamic data. @@ -62,9 +62,9 @@ This opens up a whole new class of MCP extensions that can provide dashboards, v ## πŸ“ Edit Recipe Model & Provider from the GUI -**You can now [edit a recipe's](https://block.github.io/goose/docs/guides/recipes/session-recipes#edit-recipe) model, provider, and extensions directly in goose Desktop. No YAML editing required.** +**You can now [edit a recipe's](https://goose-docs.ai/docs/guides/recipes/session-recipes#edit-recipe) model, provider, and extensions directly in goose Desktop. No YAML editing required.** -[Recipes](https://block.github.io/goose/docs/tutorials/recipes-tutorial) already let you define reusable workflows with specific instructions, extensions, and configurations, and you could always edit the underlying YAML file. But switching the model or provider for a recipe meant hunting down the right field in the config file. +[Recipes](https://goose-docs.ai/docs/tutorials/recipes-tutorial) already let you define reusable workflows with specific instructions, extensions, and configurations, and you could always edit the underlying YAML file. But switching the model or provider for a recipe meant hunting down the right field in the config file. With v1.25.0, the desktop app lets you visually configure these settings per recipe: - Change the model and provider a recipe uses @@ -79,7 +79,7 @@ Alongside this, the recipe details view now correctly **displays the provider an **Claude Code, Codex, and Gemini CLI all received major upgrades in this release.** -goose's [agentic CLI providers](https://block.github.io/goose/docs/guides/cli-providers), which delegate work to other AI coding agents, got a batch of improvements that make them significantly more capable: +goose's [agentic CLI providers](https://goose-docs.ai/docs/guides/cli-providers), which delegate work to other AI coding agents, got a batch of improvements that make them significantly more capable: ### MCP Extensions Now Work with Agentic Providers @@ -120,28 +120,28 @@ Supply chain security matters. Starting with v1.25.0, every CLI binary, desktop This means you can **cryptographically verify** that any goose artifact was built from the official repository by the official CI pipeline: ```bash -gh attestation verify --repo block/goose +gh attestation verify --repo aaif-goose/goose ``` The implementation covers all release workflows including stable releases, canary builds, nightly builds, and Docker images, with properly pinned actions and correct permission scoping. ## Get Started -Ready to try v1.25.0? Head over to our [updating goose](https://block.github.io/goose/docs/guides/updating-goose) guide for step-by-step instructions on getting the latest version for Desktop or CLI. +Ready to try v1.25.0? Head over to our [updating goose](https://goose-docs.ai/docs/guides/updating-goose) guide for step-by-step instructions on getting the latest version for Desktop or CLI. -Check out the full [release notes](https://github.com/block/goose/releases/tag/v1.25.0) for the complete list of changes, and join the conversation in [GitHub Discussions](https://github.com/block/goose/discussions). +Check out the full [release notes](https://github.com/aaif-goose/goose/releases/tag/v1.25.0) for the complete list of changes, and join the conversation in [GitHub Discussions](https://github.com/aaif-goose/goose/discussions). -*goose is open source. Star us on [GitHub](https://github.com/block/goose), and if you build something cool with goose, we'd love to hear about it!* +*goose is open source. Star us on [GitHub](https://github.com/aaif-goose/goose), and if you build something cool with goose, we'd love to hear about it!* - + - + - + - + diff --git a/documentation/blog/2026-02-24-goose-grant-goose-in-a-pond/index.md b/documentation/blog/2026-02-24-goose-grant-goose-in-a-pond/index.md index 15f000aa18..0b9194395f 100644 --- a/documentation/blog/2026-02-24-goose-grant-goose-in-a-pond/index.md +++ b/documentation/blog/2026-02-24-goose-grant-goose-in-a-pond/index.md @@ -99,12 +99,12 @@ And if *you* have a wild idea for what goose could do? The **[goose grant progra - + - + - + - + \ No newline at end of file diff --git a/documentation/blog/2026-02-25-order-lunch-with-goose/index.md b/documentation/blog/2026-02-25-order-lunch-with-goose/index.md index 1d5ac6805f..74bedc365c 100644 --- a/documentation/blog/2026-02-25-order-lunch-with-goose/index.md +++ b/documentation/blog/2026-02-25-order-lunch-with-goose/index.md @@ -136,12 +136,12 @@ See the entire flow in action: - + - + - + - + diff --git a/documentation/blog/2026-03-17-webmcp-for-beginners/index.md b/documentation/blog/2026-03-17-webmcp-for-beginners/index.md index d5a28dcd1e..7ff4ad26bf 100644 --- a/documentation/blog/2026-03-17-webmcp-for-beginners/index.md +++ b/documentation/blog/2026-03-17-webmcp-for-beginners/index.md @@ -177,12 +177,12 @@ Right now, WebMCP only works on sites that choose to implement it, which limits - + - + - + - + diff --git a/documentation/blog/2026-03-31-adversary-mode/index.md b/documentation/blog/2026-03-31-adversary-mode/index.md index 7bf44bbb3b..ed43c73b16 100644 --- a/documentation/blog/2026-03-31-adversary-mode/index.md +++ b/documentation/blog/2026-03-31-adversary-mode/index.md @@ -65,6 +65,6 @@ For full configuration details β€” including how to expand which tools get revie - + diff --git a/documentation/blog/2026-04-07-goose-moves-to-aaif/index.md b/documentation/blog/2026-04-07-goose-moves-to-aaif/index.md new file mode 100644 index 0000000000..4906ecd47d --- /dev/null +++ b/documentation/blog/2026-04-07-goose-moves-to-aaif/index.md @@ -0,0 +1,38 @@ +--- +title: "goose repo and site migrating to aaif" +description: "goose has moved to the Agentic AI Foundation (aaif) at the Linux Foundation" +authors: + - mic +--- + +goose has moved! The project now lives under the [Agentic AI Foundation (AAIF)](https://aaif.io/) at the Linux Foundation. + +There is also a new docs site [https://goose-docs.ai/](https://goose-docs.ai/) with a nice .ai TLD! + + + +## What's changed + +The GitHub repository and related projects have moved from `block/goose` to a new org: + +**[https://github.com/aaif-goose/goose](https://github.com/aaif-goose/goose)** + +This includes related repositories in the [aaif-goose](https://github.com/aaif-goose) organization. + +Everything else β€” the project, the community, the mission β€” remains the same. goose is still open source, still actively developed, and still the same agent you know. + +## Migration in progress + +We're still working through some migration issues (broken links, redirects, CI, etc). If you hit anything that seems off, please reach out on [Discord](https://discord.gg/goose-oss) and let us know. + +## Update your remotes + +If you have a local clone, update your git remote: + +```bash +git remote set-url origin git@github.com:aaif-goose/goose.git +``` + +## Learn more + +Visit [aaif.io](https://aaif.io/) to learn more about the foundation and its mission. diff --git a/documentation/blog/README.md b/documentation/blog/README.md index 6adc2de0a4..2efbd966fa 100644 --- a/documentation/blog/README.md +++ b/documentation/blog/README.md @@ -9,7 +9,7 @@ This guide explains how to write and structure blog posts for the goose document 1. Clone the goose repository: ```bash -git clone https://github.com/block/goose.git +git clone https://github.com/aaif-goose/goose.git cd goose ``` @@ -106,14 +106,14 @@ At the end of your post, include the following meta tags for social media sharin - + - + - + - + ``` @@ -181,7 +181,7 @@ npm start 3. Open your browser and visit: ``` -http://localhost:3000/goose/blog +http://localhost:3000/blog ``` The development server features: @@ -208,4 +208,4 @@ npm start ``` 3. Verify your frontmatter syntax is correct (no tabs, proper indentation) -4. Check that all image paths are correct relative to your post's directory \ No newline at end of file +4. Check that all image paths are correct relative to your post's directory diff --git a/documentation/docs/experimental/goose-mobile.md b/documentation/docs/experimental/goose-mobile.md index 6578425cfa..b3ab69e9fc 100644 --- a/documentation/docs/experimental/goose-mobile.md +++ b/documentation/docs/experimental/goose-mobile.md @@ -27,17 +27,17 @@ goose Mobile requires deep access to your device. Use at your own risk β€” best ## Installation - **Pre-built APK:** Install quickly via [Firebase distribution link](https://appdistribution.firebase.google.com/pub/i/3f111ea732d5f7f6). -- **Build from Source:** Developer instructions are in the [goose Mobile repository](https://github.com/block/goose-mobile). +- **Build from Source:** Developer instructions are in the [goose Mobile repository](https://github.com/aaif-goose/goose-mobile). ## Extending goose Mobile goose Mobile supports the **mobile MCP** system, which lets it use tools from other apps without leaving context β€” e.g., fetching weather data via a weather extension. -Sample code and setup instructions are in the repository [README](https://github.com/block/goose-mobile). +Sample code and setup instructions are in the repository [README](https://github.com/aaif-goose/goose-mobile). ## Contribute -We welcome contributions! See the [Contributing Guide](https://github.com/block/goose-mobile/blob/main/CONTRIBUTING.md) for details. +We welcome contributions! See the [Contributing Guide](https://github.com/aaif-goose/goose-mobile/blob/main/CONTRIBUTING.md) for details. --- -For more scenarios, instructions, and development setup, visit the [goose Mobile repository](https://github.com/block/goose-mobile). \ No newline at end of file +For more scenarios, instructions, and development setup, visit the [goose Mobile repository](https://github.com/aaif-goose/goose-mobile). \ No newline at end of file diff --git a/documentation/docs/experimental/index.md b/documentation/docs/experimental/index.md index 9067dfab05..e6a21664bc 100644 --- a/documentation/docs/experimental/index.md +++ b/documentation/docs/experimental/index.md @@ -69,7 +69,7 @@ The list of experimental features may change as goose development progresses. So ` to verify installation. diff --git a/documentation/docs/guides/codebase-analysis.md b/documentation/docs/guides/codebase-analysis.md index dd896eedfa..095c6ab571 100644 --- a/documentation/docs/guides/codebase-analysis.md +++ b/documentation/docs/guides/codebase-analysis.md @@ -5,7 +5,7 @@ description: Understand codebases with semantic analysis and call graphs sidebar_position: 111 --- -The [Developer extension](/docs/mcp/developer-mcp) includes an `analyze` tool that helps you understand code structure, track symbol usage, and explore call graphs across your codebase. It's automatically available when the Developer extension is enabled and supports file types for [multiple programming languages](https://github.com/block/goose/blob/main/crates/goose-mcp/src/developer/analyze/languages/mod.rs). +The [Developer extension](/docs/mcp/developer-mcp) includes an `analyze` tool that helps you understand code structure, track symbol usage, and explore call graphs across your codebase. It's automatically available when the Developer extension is enabled and supports file types for [multiple programming languages](https://github.com/aaif-goose/goose/blob/main/crates/goose-mcp/src/developer/analyze/languages/mod.rs).
Example analysis: Tracking a function across files diff --git a/documentation/docs/guides/config-files.md b/documentation/docs/guides/config-files.md index b50e43c8d7..d8c02003dc 100644 --- a/documentation/docs/guides/config-files.md +++ b/documentation/docs/guides/config-files.md @@ -81,7 +81,7 @@ GOOSE_TOOLSHIM: true GOOSE_CLI_MIN_PRIORITY: 0.2 # Recipe Configuration -GOOSE_RECIPE_GITHUB_REPO: "block/goose-recipes" +GOOSE_RECIPE_GITHUB_REPO: "aaif-goose/goose-recipes" # Search Path Configuration GOOSE_SEARCH_PATHS: diff --git a/documentation/docs/guides/context-engineering/slash-commands.md b/documentation/docs/guides/context-engineering/slash-commands.md index c949165c9d..602d5167ab 100644 --- a/documentation/docs/guides/context-engineering/slash-commands.md +++ b/documentation/docs/guides/context-engineering/slash-commands.md @@ -98,13 +98,13 @@ import ContentCardCarousel from '@site/src/components/ContentCardCarousel'; type: 'topic', title: 'Recipes', description: 'Check out the Recipes guide for more docs, tools, and resources to help you master goose recipes.', - linkUrl: '/goose/docs/guides/recipes' + linkUrl: '/docs/guides/recipes' }, { type: 'topic', title: 'Research β†’ Plan β†’ Implement Patterns', description: 'See how slash commands make it easy to integrate instructions into interactive RPI workflows.', - linkUrl: '/goose/docs/tutorials/rpi' + linkUrl: '/docs/tutorials/rpi' } ]} /> diff --git a/documentation/docs/guides/context-engineering/using-goosehints.md b/documentation/docs/guides/context-engineering/using-goosehints.md index eafbc00d60..cf53fb9b1d 100644 --- a/documentation/docs/guides/context-engineering/using-goosehints.md +++ b/documentation/docs/guides/context-engineering/using-goosehints.md @@ -76,7 +76,7 @@ The `.goosehints` file can include any instructions or contextual details releva The `.goosehints` file supports natural language. Write clear, specific instructions using direct language that goose can easily understand and follow. Include relevant context about your project and workflow preferences, and prioritize your most important guidelines first. -goosehints are loaded at the start of your session and become part of the system prompt sent with every request. This means the content of `.goosehints` contributes to token usage, so keeping it concise can save both cost and processing time. +goose loads hints at the start of your session. As it accesses files in nested directories, it also loads the hint files for those directories. goose adds hints to the system prompt for every request. Because `.goosehints` content uses tokens, keeping it concise can reduce cost and improve performance. ### Example Global `.goosehints` File @@ -111,7 +111,11 @@ These examples show two ways to reference other files: ### Nested `.goosehints` Files -goose supports hierarchical local hints in git repositories. All `.goosehints` files from your current directory up to the root directory are automatically loaded and combined. If you're not working in a git repository, goose only loads the `.goosehints` file from the current directory. +goose supports hierarchical local hints in git repositories. When your session starts, goose loads the configured context files from your working directory up to the repository root. As goose reads or modifies files in nested subdirectories, it can also discover and load additional hint files from those directories automatically. + +This is especially useful in monorepos or large projects where different parts of the codebase have different conventions. + +By default, goose looks for both `AGENTS.md` and `.goosehints` at each level. If you're using [custom context files](#custom-context-files), goose applies the same nested loading behavior to those filenames too. As a best practice, `.goosehints` at each level should only include hints relevant to that scope: - **Root level**: Include project-wide standards, build processes, and general guidelines @@ -134,7 +138,7 @@ my-project/ └── routes.py ``` -When working in `frontend/components/` in this example project, goose loads hints from directories higher up the hierarchy in the following order: +If you start goose in `my-project/`, the root-level hints are loaded immediately. Later, when goose accesses files under `frontend/components/`, it loads the nested hints for that path and combines them in the following order: 1.
`my-project/.goosehints` (project root) ``` @@ -173,7 +177,7 @@ When working in `frontend/components/` in this example project, goose loads hint Always confirm UI changes with design team before implementation. ``` -
+
3.
`frontend/components/.goosehints` (current directory) ``` @@ -189,6 +193,10 @@ When working in `frontend/components/` in this example project, goose loads hint ```
+:::note +After nested hints are loaded for a directory, they remain active for the rest of the session. If you update a hint file and want goose to pick up the new content reliably, restart the session. +::: + ## Common Use Cases Here are some ways people have used hints to provide additional context to goose: @@ -221,8 +229,10 @@ goose looks for `AGENTS.md` then `.goosehints` files by default, but you can con - **Project conventions**: Use context files from your project's established toolchain (`.cursorrules`) Here's how it works: -1. goose looks for each configured filename in both global (~/.config/goose/) and local (current directory) locations -2. All found files are loaded and combined into the context +1. goose looks for each configured filename in both global (`~/.config/goose/`) and local project locations +2. At session start, goose loads matching files from your working directory hierarchy +3. During the session, goose can load additional matching files when it accesses nested subdirectories +4. All found files are loaded and combined into the context ### Configuration diff --git a/documentation/docs/guides/context-engineering/using-skills.md b/documentation/docs/guides/context-engineering/using-skills.md index 1a874d4ab8..6c5f80ea7b 100644 --- a/documentation/docs/guides/context-engineering/using-skills.md +++ b/documentation/docs/guides/context-engineering/using-skills.md @@ -278,7 +278,7 @@ import skillsvsmcp from '@site/blog/2025-12-22-agent-skills-vs-mcp/skills-vs-mcp title: 'Did Skills Kill MCP?', description: 'An overview of Agent Skills vs MCP', thumbnailUrl: skillsvsmcp, - linkUrl: '/goose/blog/2025/12/22/agent-skills-vs-mcp', + linkUrl: '/blog/2025/12/22/agent-skills-vs-mcp', date: '2025-12-22', duration: '4 min read' } diff --git a/documentation/docs/guides/creating-plans.md b/documentation/docs/guides/creating-plans.md index c2b8d3c875..a01bc9aa0c 100644 --- a/documentation/docs/guides/creating-plans.md +++ b/documentation/docs/guides/creating-plans.md @@ -334,7 +334,7 @@ import blogBanner from '@site/blog/2025-12-19-does-your-ai-agent-need-a-plan/blo title: 'Does Your AI Agent Need a Plan?', description: 'Knowing when and how to plan with an AI agent can produce better results.', thumbnailUrl: blogBanner, - linkUrl: '/goose/blog/2025/12/19/does-your-ai-agent-need-a-plan', + linkUrl: '/blog/2025/12/19/does-your-ai-agent-need-a-plan', date: '2025-12-19', duration: '7 min read' }, @@ -342,7 +342,7 @@ import blogBanner from '@site/blog/2025-12-19-does-your-ai-agent-need-a-plan/blo type: 'topic', title: 'Planning Complex Tasks', description: 'Learn how to use the Plan feature to break down complex tasks into manageable, executable steps.', - linkUrl: '/goose/docs/tutorials/plan-feature-devcontainer-setup', + linkUrl: '/docs/tutorials/plan-feature-devcontainer-setup', } ]} -/> \ No newline at end of file +/> diff --git a/documentation/docs/guides/custom-distributions.md b/documentation/docs/guides/custom-distributions.md index 02aa7c16b5..1286fec91b 100644 --- a/documentation/docs/guides/custom-distributions.md +++ b/documentation/docs/guides/custom-distributions.md @@ -24,7 +24,7 @@ goose is designed to be forked and customized. You can create your own "distro" The full guide lives in the repo root since you'll need to work at the code level to build a custom distribution: -πŸ‘‰ **[CUSTOM_DISTROS.md](https://github.com/block/goose/blob/main/CUSTOM_DISTROS.md)** +πŸ‘‰ **[CUSTOM_DISTROS.md](https://github.com/aaif-goose/goose/blob/main/CUSTOM_DISTROS.md)** It covers: @@ -53,4 +53,4 @@ GOOSE_PROVIDER: ollama GOOSE_MODEL: qwen3-coder:latest ``` -See the [full guide](https://github.com/block/goose/blob/main/CUSTOM_DISTROS.md) for more scenarios including corporate API key distribution, audience-specific builds, and custom UIs. +See the [full guide](https://github.com/aaif-goose/goose/blob/main/CUSTOM_DISTROS.md) for more scenarios including corporate API key distribution, audience-specific builds, and custom UIs. diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index a6909f033a..e007dd6b92 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -586,7 +586,7 @@ These variables control recipe discovery and management. | Variable | Purpose | Values | Default | |----------|---------|---------|---------| | `GOOSE_RECIPE_PATH` | Additional directories to search for recipes | Colon-separated paths on Unix, semicolon-separated on Windows | None | -| `GOOSE_RECIPE_GITHUB_REPO` | GitHub repository to search for recipes | Format: "owner/repo" (e.g., "block/goose-recipes") | None | +| `GOOSE_RECIPE_GITHUB_REPO` | GitHub repository to search for recipes | Format: "owner/repo" (e.g., "aaif-goose/goose-recipes") | None | | `GOOSE_RECIPE_RETRY_TIMEOUT_SECONDS` | Global timeout for recipe success check commands | Integer (seconds) | Recipe-specific default | | `GOOSE_RECIPE_ON_FAILURE_TIMEOUT_SECONDS` | Global timeout for recipe on_failure commands | Integer (seconds) | Recipe-specific default | diff --git a/documentation/docs/guides/managing-tools/code-mode.md b/documentation/docs/guides/managing-tools/code-mode.md index 8d8a373aac..c29480b12f 100644 --- a/documentation/docs/guides/managing-tools/code-mode.md +++ b/documentation/docs/guides/managing-tools/code-mode.md @@ -56,7 +56,7 @@ import notMcpReplacement from '@site/blog/2025-12-21-code-mode-doesnt-replace-mc title: 'Code Mode for MCP', description: 'Learn about the code execution approach to MCP tool calling.', thumbnailUrl: gooseCodeMode, - linkUrl: '/goose/blog/2025/12/15/code-mode-mcp', + linkUrl: '/blog/2025/12/15/code-mode-mcp', date: '2025-12-15', duration: '5 min read' }, @@ -65,9 +65,9 @@ import notMcpReplacement from '@site/blog/2025-12-21-code-mode-doesnt-replace-mc title: 'Code Mode Doesn\'t Replace MCP', description: 'Understanding how Code Mode and MCP work together.', thumbnailUrl: notMcpReplacement, - linkUrl: '/goose/blog/2025/12/21/code-mode-doesnt-replace-mcp', + linkUrl: '/blog/2025/12/21/code-mode-doesnt-replace-mcp', date: '2025-12-21', duration: '8 min read' } ]} -/> \ No newline at end of file +/> diff --git a/documentation/docs/guides/managing-tools/index.md b/documentation/docs/guides/managing-tools/index.md index 9085badfa6..0f18f93d15 100644 --- a/documentation/docs/guides/managing-tools/index.md +++ b/documentation/docs/guides/managing-tools/index.md @@ -9,7 +9,7 @@ import styles from '@site/src/components/Card/styles.module.css';

Managing Tools

- Tools are specific functions within extensions that give goose its capabilities. Learn to control and customize how these tools work for you. + Tools are specific functions within extensions that give goose its capabilities. Learn to control and customize how these tools work for you.

diff --git a/documentation/docs/guides/mcp-roots.md b/documentation/docs/guides/mcp-roots.md new file mode 100644 index 0000000000..1e8710bd93 --- /dev/null +++ b/documentation/docs/guides/mcp-roots.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 56 +title: MCP Roots +sidebar_label: MCP Roots +description: How goose shares your working directory with roots-aware MCP extensions +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import { PanelLeft } from 'lucide-react'; + +MCP Roots lets goose share your session working directory with roots-aware MCP extensions. + +This helps extensions understand which folder to use as the active workspace for the current session. + +:::info +[MCP Roots](https://modelcontextprotocol.io/specification/2025-06-18/client/roots) is a feature in the Model Context Protocol. goose automatically enables it for MCP extensions that support roots. +::: + +## How MCP Roots Works + +When goose connects to an MCP extension, it advertises roots support during MCP initialization. + +An extension that supports roots can then: + +- Request the current root list from goose +- Treat that root as the active workspace boundary +- React if the root changes during the session + +In goose, the root list currently contains one entry: + +- Your current session working directory + +If you change the session working directory, goose updates the root and notifies connected extensions automatically. + +## Using MCP Roots + +There is no separate "Roots" settings screen in goose. MCP Roots follows the working directory you are already using for the session. + + + + + In goose Desktop, the current working directory appears at the bottom of the chat window. + + 1. Click the directory display to choose a different folder + 2. goose updates the session working directory + 3. Connected roots-aware extensions receive the updated root automatically + + To change directories: + + 1. Click the button if needed and open a session + 2. Use the directory control shown at the bottom of the chat window + 3. Pick the folder you want goose and your extensions to work in + + + + + In goose CLI, the session root follows the directory where you launch goose. + + - Start goose from the project folder you want to work in + - When resuming a session, goose may prompt you to switch back to that session's original working directory + + This means roots-aware extensions see the same workspace directory that goose is already using in the CLI. + + + + +## What Extensions Use It For + +MCP Roots is useful for extensions that need to work with local files or understand your project structure. + +Without roots, an extension may need to guess which folder you mean or rely on custom configuration. With roots, goose can tell the extension which directory is currently in scope for the session. + +For example, an extension can use roots to: + +- Discover the active project directory +- Scope file operations to your current workspace +- Avoid guessing which repository or folder you meant +- Update its behavior when you switch to a different project + +## Current Limitations + +goose currently exposes a single root per session rather than a multi-folder workspace. For most workflows, this maps cleanly to how goose already works: one active project directory at a time. + +If you want an extension to work in a different location, change the session working directory first. + +## For Extension Developers + +If you're building an MCP extension for goose, support for roots allows your extension to discover the active workspace directory in a standard way instead of relying on custom configuration. + +For protocol details, see the [MCP Roots specification](https://modelcontextprotocol.io/specification/2025-06-18/client/roots). diff --git a/documentation/docs/guides/mcp-sampling.md b/documentation/docs/guides/mcp-sampling.md index ec7dd15d54..3459792145 100644 --- a/documentation/docs/guides/mcp-sampling.md +++ b/documentation/docs/guides/mcp-sampling.md @@ -93,7 +93,7 @@ import mcpSampling from '@site/blog/2025-12-04-mcp-sampling/mcp-sampling.png'; title: 'MCP Sampling: When Your Tools Need to Think', description: 'Learn how MCP Sampling lets your tools call the AI instead of the other way around.', thumbnailUrl: mcpSampling, - linkUrl: '/goose/blog/2025/12/04/mcp-sampling', + linkUrl: '/blog/2025/12/04/mcp-sampling', date: '2025-12-04', duration: '6 min read' } diff --git a/documentation/docs/guides/prompt-templates.md b/documentation/docs/guides/prompt-templates.md index b0f8378e37..b40c6ecb1c 100644 --- a/documentation/docs/guides/prompt-templates.md +++ b/documentation/docs/guides/prompt-templates.md @@ -90,16 +90,16 @@ The following default templates can be customized. | Template | Description | Applies To | |----------|-------------|------------| -| [system.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/system.md) | General system prompt defining goose's role, capabilities, and response format | Desktop and CLI | -| [apps_create.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/apps_create.md) | Prompt for generating new standalone apps (in development) | Desktop only | -| [apps_iterate.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/apps_iterate.md) | Prompt for updating existing standalone apps (in development) | Desktop only | -| [compaction.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/compaction.md) | Prompt for summarizing conversation history when context limits are reached | Desktop and CLI | -| [permission_judge.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/permission_judge.md) | Prompt for analyzing tool operations for read-only detection | Desktop and CLI | -| [plan.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/plan.md) | Instructions for creating detailed, actionable plans with clarifying questions | CLI only | -| [recipe.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/recipe.md) | Prompt for generating recipe files from conversations | Desktop and CLI | -| [subagent_system.md](https://github.com/block/goose/blob/main/crates/goose/src/prompts/subagent_system.md) | System prompt for subagents spawned to handle specific tasks | Desktop and CLI | +| [system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/system.md) | General system prompt defining goose's role, capabilities, and response format | Desktop and CLI | +| [apps_create.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/apps_create.md) | Prompt for generating new standalone apps (in development) | Desktop only | +| [apps_iterate.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/apps_iterate.md) | Prompt for updating existing standalone apps (in development) | Desktop only | +| [compaction.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/compaction.md) | Prompt for summarizing conversation history when context limits are reached | Desktop and CLI | +| [permission_judge.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/permission_judge.md) | Prompt for analyzing tool operations for read-only detection | Desktop and CLI | +| [plan.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/plan.md) | Instructions for creating detailed, actionable plans with clarifying questions | CLI only | +| [recipe.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/recipe.md) | Prompt for generating recipe files from conversations | Desktop and CLI | +| [subagent_system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/subagent_system.md) | System prompt for subagents spawned to handle specific tasks | Desktop and CLI | -Customizable templates are enumerated in the `TEMPLATE_REGISTRY` array in [`prompt_template.rs`](https://github.com/block/goose/blob/main/crates/goose/src/prompt_template.rs). +Customizable templates are enumerated in the `TEMPLATE_REGISTRY` array in [`prompt_template.rs`](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompt_template.rs). ### Template Variable Syntax @@ -136,7 +136,7 @@ import promptBanner from '@site/blog/2025-03-19-better-ai-prompting/prompt.png'; title: 'AI Prompting 101: How to Get the Best Responses from Your AI Agent', description: 'Learn different prompting stylesβ€”from instruction-based to chain-of-thoughtβ€”and discover which approach works best for your needs.', thumbnailUrl: promptBanner, - linkUrl: '/goose/blog/2025/03/19/better-ai-prompting', + linkUrl: '/blog/2025/03/19/better-ai-prompting', date: '2025-03-19', duration: '8 min read' } diff --git a/documentation/docs/guides/recipes/recipe-reference.md b/documentation/docs/guides/recipes/recipe-reference.md index 218344f212..cefffa1529 100644 --- a/documentation/docs/guides/recipes/recipe-reference.md +++ b/documentation/docs/guides/recipes/recipe-reference.md @@ -767,7 +767,7 @@ Built-in template parameters are automatically supported and don't need to be de ## Validation Rules -Validation rules from [`validate_recipe.rs`](https://github.com/block/goose/blob/main/crates/goose/src/recipe/validate_recipe.rs) are enforced when loading recipes and used by the [`goose recipe validate`](/docs/guides/goose-cli-commands#recipe) subcommand: +Validation rules from [`validate_recipe.rs`](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/recipe/validate_recipe.rs) are enforced when loading recipes and used by the [`goose recipe validate`](/docs/guides/goose-cli-commands#recipe) subcommand: ### Recipe-Level Validation diff --git a/documentation/docs/guides/recipes/session-recipes.md b/documentation/docs/guides/recipes/session-recipes.md index 8b99ab4444..6a4ba223a1 100644 --- a/documentation/docs/guides/recipes/session-recipes.md +++ b/documentation/docs/guides/recipes/session-recipes.md @@ -162,7 +162,7 @@ You can turn your current goose session into a reusable recipe that includes the - Use the online [Recipe Generator](https://block.github.io/goose/recipe-generator) tool to create a recipe. First choose your preferred format: + Use the online [Recipe Generator](https://goose-docs.ai/recipe-generator) tool to create a recipe. First choose your preferred format: - **URL Format**: Generates a shareable link that opens a session in goose Desktop - **YAML Format**: Generates YAML content that you can save to file and then run in goose CLI diff --git a/documentation/docs/guides/sandbox.md b/documentation/docs/guides/sandbox.md index 8d185f9fd9..f25b9dc629 100644 --- a/documentation/docs/guides/sandbox.md +++ b/documentation/docs/guides/sandbox.md @@ -50,7 +50,7 @@ All configuration is via environment variables. Defaults are designed to be secu ### File System -The [seatbelt sandbox profile](https://github.com/block/goose/blob/main/ui/desktop/src/sandbox/index.ts) blocks write operations to these sensitive files: +The [seatbelt sandbox profile](https://github.com/aaif-goose/goose/blob/main/ui/desktop/src/sandbox/index.ts) blocks write operations to these sensitive files: - `~/.ssh/` - Prevent SSH key tampering - `~/.bashrc`, `~/.zshrc`, `~/.bash_profile`, `~/.zprofile` - Prevent shell config injection diff --git a/documentation/docs/guides/security/prompt-injection-detection.md b/documentation/docs/guides/security/prompt-injection-detection.md index f1e789b9d4..a7412ecb4b 100644 --- a/documentation/docs/guides/security/prompt-injection-detection.md +++ b/documentation/docs/guides/security/prompt-injection-detection.md @@ -27,7 +27,7 @@ These checks provide a safeguard, not a guarantee. They detect known patterns bu When enabled, goose uses a multi-layered approach to detect threats before they run: -1. **Tool call is intercepted and analyzed** - When goose prepares to execute a tool, the security system extracts the tool parameter text and checks it against [threat patterns](https://github.com/block/goose/blob/main/crates/goose/src/security/patterns.rs). If ML-based detection is enabled, it also uses machine learning to analyze the semantic content of the tool call and recent conversation messages to better understand context and reduce false positives. +1. **Tool call is intercepted and analyzed** - When goose prepares to execute a tool, the security system extracts the tool parameter text and checks it against [threat patterns](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/security/patterns.rs). If ML-based detection is enabled, it also uses machine learning to analyze the semantic content of the tool call and recent conversation messages to better understand context and reduce false positives. 2. **Risk is assessed** - Detected threats are assigned confidence scores 3. **Execution pauses** - Threats that exceed your configured threshold need your decision 4. **Security alert appears** - The alert displays the confidence level, a description of the finding, and a unique finding ID. For example: diff --git a/documentation/docs/guides/subagents.mdx b/documentation/docs/guides/subagents.mdx index 3affebd962..beacf85240 100644 --- a/documentation/docs/guides/subagents.mdx +++ b/documentation/docs/guides/subagents.mdx @@ -358,7 +358,7 @@ import agentCoordination from '@site/blog/2025-08-14-agent-coordination-patterns title: 'Agents, Subagents, and Multi Agents: What They Are and When to Use Them', description: 'Compare agents, subagents, and multi agents in AI workflows. Learn how they work together and practical scenarios for each approach.', thumbnailUrl: agentCoordination, - linkUrl: '/goose/blog/2025/08/14/agent-coordination-patterns', + linkUrl: '/blog/2025/08/14/agent-coordination-patterns', date: '2025-08-14', duration: '4 min read' }, @@ -375,8 +375,8 @@ import agentCoordination from '@site/blog/2025-08-14-agent-coordination-patterns type: 'tutorial', title: 'Tutorial: Using Subagents', description: 'Spin up a team of subagents to build a fully functional app', - thumbnailUrl: 'https://block.github.io/goose/assets/images/tutorial-using-subagents-ef265627024db73e73d80e5799ed0c1a.png', - linkUrl: '/goose/docs/tutorials/subagents', + thumbnailUrl: 'https://goose-docs.ai/assets/images/tutorial-using-subagents-ef265627024db73e73d80e5799ed0c1a.png', + linkUrl: '/docs/tutorials/subagents', date: '2026-01-27', duration: '1 hour' }, @@ -403,7 +403,7 @@ import agentCoordination from '@site/blog/2025-08-14-agent-coordination-patterns title: 'How to Choose Between Subagents and Subrecipes in goose', description: 'Detailed guide to subagents and subrecipes in goose. Compare reusability, setup complexity, and get practical advice for choosing the right approach.', thumbnailUrl: subagentsVsSubrecipes, - linkUrl: '/goose/blog/2025/09/26/subagents-vs-subrecipes', + linkUrl: '/blog/2025/09/26/subagents-vs-subrecipes', date: '2025-09-26', duration: '6 min read' } diff --git a/documentation/docs/guides/updating-goose.md b/documentation/docs/guides/updating-goose.md index e7ccba4ef5..7b89a56fbc 100644 --- a/documentation/docs/guides/updating-goose.md +++ b/documentation/docs/guides/updating-goose.md @@ -48,7 +48,7 @@ The goose CLI and desktop apps are under active and continuous development. To g Or you can run the [installation](/docs/getting-started/installation) script again: ```sh - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash ``` To check your current goose version, use the following command: @@ -95,7 +95,7 @@ The goose CLI and desktop apps are under active and continuous development. To g Or you can run the [installation](/docs/getting-started/installation) script again: ```sh - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash ``` To check your current goose version, use the following command: @@ -139,7 +139,7 @@ The goose CLI and desktop apps are under active and continuous development. To g Or you can run the [installation](/docs/getting-started/installation) script again in **Git Bash**, **MSYS2**, or **PowerShell** to update the goose CLI natively on Windows: ```bash - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash ``` To check your current goose version, use the following command: @@ -154,7 +154,7 @@ The goose CLI and desktop apps are under active and continuous development. To g To update your WSL installation, use `goose update` or run the installation script again via WSL: ```sh - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash ```
diff --git a/documentation/docs/guides/using-persistent-instructions.md b/documentation/docs/guides/using-persistent-instructions.md index bb72855f8d..f3771a07d9 100644 --- a/documentation/docs/guides/using-persistent-instructions.md +++ b/documentation/docs/guides/using-persistent-instructions.md @@ -4,7 +4,7 @@ sidebar_position: 12 sidebar_label: Persistent Instructions --- -Persistent instructions let you inject text into goose's working memory every turn. Unlike [`.goosehints`](/docs/guides/context-engineering/using-goosehints) which are loaded once at session start, persistent instructions are re-read and injected fresh with every interaction. This makes them ideal for behavioral guardrails that must always be enforced, regardless of how the conversation evolves. +Persistent instructions let you inject text into goose's working memory every turn. Unlike [`.goosehints`](/docs/guides/context-engineering/using-goosehints), which are loaded at session start and can expand later when goose discovers nested hint files, persistent instructions are re-read and injected fresh with every interaction. This makes them ideal for behavioral guardrails that must always be enforced, regardless of how the conversation evolves. ## How It Works @@ -116,7 +116,7 @@ unset GOOSE_MOIM_MESSAGE_TEXT | Feature | Persistent Instructions | [goosehints](/docs/guides/context-engineering/using-goosehints) | |---------|------------------------|-------------| -| When loaded | Every turn | Session start | +| When loaded | Every turn | Session start, plus nested context files discovered during the session | | Can be forgotten | No | Yes, as context fills | | Best for | Critical guardrails, security rules | Project context, coding standards | | Token cost | Per turn | Once at start | diff --git a/documentation/docs/mcp/autovisualiser-mcp.md b/documentation/docs/mcp/autovisualiser-mcp.md index 6c22bf44bb..fc21a83567 100644 --- a/documentation/docs/mcp/autovisualiser-mcp.md +++ b/documentation/docs/mcp/autovisualiser-mcp.md @@ -11,7 +11,7 @@ import ImageCarousel from '@site/src/components/ImageCarousel'; -The Auto Visualiser extension enables goose to automatically generate interactive data visualizations directly in your conversation. This built-in extension leverages MCP-UI to create charts, graphs, maps, and other visual representations of your data without requiring manual requests. +The Auto Visualiser extension enables goose to automatically generate interactive data visualizations directly in your conversation. This built-in extension uses MCP Apps to render charts, graphs, maps, and diagrams inline in goose Desktop. This guide will cover enabling and using the Auto Visualiser MCP Server. @@ -49,9 +49,13 @@ This guide will cover enabling and using the Auto Visualiser MCP Server. +:::info +Auto Visualiser now uses [MCP Apps](/docs/tutorials/building-mcp-apps), which means visualizations can render inline in chat and expand into fullscreen or picture-in-picture modes in goose Desktop. +::: + ## Visualization Types -The Auto Visualiser is a powerful extension that integrates with goose's MCP-UI system to automatically detect when data would benefit from visualization and render interactive visual components. It analyzes data patterns in your conversations and suggests the most appropriate visualization type. +The Auto Visualiser automatically detects when data would benefit from visualization and chooses the most appropriate chart or diagram type. It renders those visualizations as interactive MCP Apps inside goose Desktop. | Chart Type | Description | Detected When Prompt Implies | |------------|-------------|------------------------------| @@ -80,11 +84,22 @@ The Auto Visualiser is a powerful extension that integrates with goose's MCP-UI - **Automatic Detection**: goose intelligently chooses the best visualization type for your data - **Interactive Components**: All visualizations are interactive with hover effects, zooming, and drilling capabilities -- **MCP-UI Integration**: Leverages the emerging MCP-UI standard for seamless rendering +- **MCP Apps Rendering**: Visualizations render as MCP Apps directly in goose Desktop +- **Multiple Display Modes**: Visualizations can appear inline and expand into fullscreen or picture-in-picture modes - **Multiple Chart Support**: Can render multiple visualizations in a single response - **Customizable Styling**: Supports custom colors, labels, and formatting options - **Export Capability**: Visualizations can be saved as HTML files for sharing +## How It Works + +When goose decides that your data should be visualized, the Auto Visualiser extension returns an MCP App for the selected chart type. goose Desktop then renders that app directly in the conversation. + +Depending on the visualization, you can: + +- View it inline in the chat +- Expand it to fullscreen for a larger workspace +- Move it into picture-in-picture mode while continuing your conversation + ## Example Usage @@ -157,7 +172,8 @@ Would you like me to create additional visualizations or dive deeper into any sp - **Visualization not appearing**: Ensure the Auto Visualiser extension is enabled and your data follows the expected format. +- **Visualization opens but does not fit well inline**: Try fullscreen mode in goose Desktop for more space. + - **Data format errors**: Check that your data structure matches the required schema for the visualization type. The extension provides detailed error messages to guide you. - **Performance with large datasets**: For very large datasets, consider aggregating or sampling your data before visualization. - diff --git a/documentation/docs/mcp/council-of-mine-mcp.md b/documentation/docs/mcp/council-of-mine-mcp.md index da5ebbd473..7145a4718d 100644 --- a/documentation/docs/mcp/council-of-mine-mcp.md +++ b/documentation/docs/mcp/council-of-mine-mcp.md @@ -13,7 +13,7 @@ import GooseDesktopInstaller from '@site/src/components/GooseDesktopInstaller'; -This tutorial covers how to add the [Council of Mine MCP Server](https://github.com/block/mcp-council-of-mine) as a goose extension featuring 9 LLM council members with distinct personalities who debate topics, vote on each other's opinions, and generate synthesized conclusions through AI-powered deliberation. This is great for helping you make a decision. +This tutorial covers how to add the [Council of Mine MCP Server](https://github.com/aaif-goose/mcp-council-of-mine) as a goose extension featuring 9 LLM council members with distinct personalities who debate topics, vote on each other's opinions, and generate synthesized conclusions through AI-powered deliberation. This is great for helping you make a decision. :::tip Quick Install @@ -23,7 +23,7 @@ This tutorial covers how to add the [Council of Mine MCP Server](https://github. **Command** ```sh - uvx --from git+https://github.com/block/mcp-council-of-mine mcp_council_of_mine + uvx --from git+https://github.com/aaif-goose/mcp-council-of-mine mcp_council_of_mine ``` @@ -43,7 +43,7 @@ Note that you'll need [uv](https://docs.astral.sh/uv/#installation) installed on description="LLM debate" type="stdio" command="uvx" - args={["--from", "git+https://github.com/block/mcp-council-of-mine", "mcp_council_of_mine"]} + args={["--from", "git+https://github.com/aaif-goose/mcp-council-of-mine", "mcp_council_of_mine"]} /> @@ -51,7 +51,7 @@ Note that you'll need [uv](https://docs.astral.sh/uv/#installation) installed on name="Council of Mine" description="LLM debate" type="stdio" - command="uvx --from git+https://github.com/block/mcp-council-of-mine mcp_council_of_mine" + command="uvx --from git+https://github.com/aaif-goose/mcp-council-of-mine mcp_council_of_mine" timeout={300} /> diff --git a/documentation/docs/mcp/developer-mcp.md b/documentation/docs/mcp/developer-mcp.md index 291ea779f2..1fb49ff900 100644 --- a/documentation/docs/mcp/developer-mcp.md +++ b/documentation/docs/mcp/developer-mcp.md @@ -251,13 +251,13 @@ import ContentCardCarousel from '@site/src/components/ContentCardCarousel'; type: 'topic', title: 'Enhanced Code Editing with AI Models', description: 'Use AI models to intelligently apply code changes', - linkUrl: '/goose/docs/guides/enhanced-code-editing' + linkUrl: '/docs/guides/enhanced-code-editing' }, { type: 'topic', title: 'Codebase Analysis', description: 'Understand codebases with semantic analysis and call graphs', - linkUrl: '/goose/docs/guides/codebase-analysis' + linkUrl: '/docs/guides/codebase-analysis' } ]} -/> +/> diff --git a/documentation/docs/mcp/elevenlabs-mcp.md b/documentation/docs/mcp/elevenlabs-mcp.md index 7878c34e91..73f37c4687 100644 --- a/documentation/docs/mcp/elevenlabs-mcp.md +++ b/documentation/docs/mcp/elevenlabs-mcp.md @@ -96,7 +96,7 @@ Go goose, go goose! If that’s not amazing idk what is … -πŸš€ to get started visit block.github.io/goose_ +πŸš€ to get started visit goose-docs.ai_ ### goose Output @@ -124,7 +124,7 @@ The script has been created and read aloud using the specified voices and style. Press play and hear it for yourself! πŸ”Š diff --git a/documentation/docs/mcp/fetch-mcp.md b/documentation/docs/mcp/fetch-mcp.md index e33514bda6..bfff3129e4 100644 --- a/documentation/docs/mcp/fetch-mcp.md +++ b/documentation/docs/mcp/fetch-mcp.md @@ -12,7 +12,7 @@ import CLIExtensionInstructions from '@site/src/components/CLIExtensionInstructi :::warning Known Limitation -The Fetch extension [does not work](https://github.com/block/goose/issues/1184) with Google models (e.g. gemini-2.0-flash) because this extension uses `format: uri` in its JSON schema which Google doesn't support. +The Fetch extension [does not work](https://github.com/aaif-goose/goose/issues/1184) with Google models (e.g. gemini-2.0-flash) because this extension uses `format: uri` in its JSON schema which Google doesn't support. ::: This tutorial covers how to add the [Fetch MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch) as a goose extension to retrieve and process content from the web. diff --git a/documentation/docs/mcp/gotohuman-mcp.md b/documentation/docs/mcp/gotohuman-mcp.md index d8585e654e..2a3222948b 100644 --- a/documentation/docs/mcp/gotohuman-mcp.md +++ b/documentation/docs/mcp/gotohuman-mcp.md @@ -98,7 +98,7 @@ In this example, goose sends a LinkedIn post draft to gotoHuman for approval usi Send this blog draft about goose to gotoHuman for review using my `n8n news to post` form. Include today’s date as the timestamp, these links: -[goose Docs: https://block.github.io/goose/, gotoHuman: https://gotohuman.com/], +[goose Docs: https://goose-docs.ai/, gotoHuman: https://gotohuman.com/], summarize it as β€˜Introducing goose’s integration with gotoHuman for human approvals,’ and here’s the draft: diff --git a/documentation/docs/mcp/jetbrains-mcp.md b/documentation/docs/mcp/jetbrains-mcp.md index 10b3b41e40..75eab88391 100644 --- a/documentation/docs/mcp/jetbrains-mcp.md +++ b/documentation/docs/mcp/jetbrains-mcp.md @@ -6,7 +6,6 @@ description: Use JetBrains MCP Server as a goose Extension import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import YouTubeShortEmbed from '@site/src/components/YouTubeShortEmbed'; -import GooseDesktopInstaller from '@site/src/components/GooseDesktopInstaller'; import CLIExtensionInstructions from '@site/src/components/CLIExtensionInstructions'; import { PanelLeft } from 'lucide-react'; @@ -19,9 +18,65 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je **Important**: The configuration steps depend on your IDE version. You can find the version in `[IDE Name] > About` (macOS) or `Help > About` (Windows/Linux). - + - Versions 2025.2 and later have built-in MCP server support and generate a dynamic configuration specific to your IDE instance. See your IDE's documentation for more details (e.g. [MCP Server](https://www.jetbrains.com/help/idea/mcp-server.html) for IntelliJ IDEA). + Versions 2026.1 and later expose a streamable HTTP endpoint directly. No plugin install or npm required. + + :::tip Quick Install + + + Use `Add custom extension` in Settings β†’ Extensions with the URL from `Copy HTTP Stream Config` in your IDE. + + + Use `goose configure` with the URL from `Copy HTTP Stream Config` in your IDE. + + + ::: + +
+ Configure the extension using your IDE's built-in HTTP stream endpoint: + + 1. Get your IDE-specific URL: + + 1. Go to `Settings > Tools > MCP Server` in your IDE + 2. Check `Enable MCP Server` and confirm the permissions dialog if shown + 3. Click `Copy HTTP Stream Config` + 4. Note the `url` value (e.g. `http://127.0.0.1:64344/stream`) β€” the port is auto-assigned and varies per IDE instance + + 2. Add the JetBrains extension to goose using the URL from the config: + + + + 1. Click the button in the top-left to open the sidebar + 2. Click `Extensions` on the sidebar + 3. Click `Add custom extension` + 4. On the `Add custom extension` modal, enter the following: + - **Extension Name**: JetBrains + - **Type**: Streamable HTTP + - **Description**: Integrate goose with any JetBrains IDE + - **URL**: Paste the `url` from `Copy HTTP Stream Config` + 5. Click `Add Extension` to save the extension + 6. Navigate to the chat + + + + Paste the url from Copy HTTP Stream Config. The port varies per IDE instance. + + } + /> + + +
+ + + Versions 2025.2 through 2025.x have built-in MCP server support using stdio transport. See your IDE's documentation for more details (e.g. [MCP Server](https://www.jetbrains.com/help/idea/mcp-server.html) for IntelliJ IDEA). :::tip Quick Install @@ -78,61 +133,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
- - - Versions 2025.1 and earlier require installing the MCP Server plugin and using the [JetBrains MCP Proxy Server](https://github.com/JetBrains/mcp-jetbrains). - - :::tip Quick Install - - - [Launch the installer](goose://extension?cmd=npx&arg=-y&arg=%40jetbrains%2Fmcp-proxy&id=jetbrains&name=JetBrains&description=Integrate%20goose%20with%20any%20JetBrains%20IDE) - - - **Command** - ```sh - npx -y @jetbrains/mcp-proxy - ``` - - - - **Required Setup** - - Add the [MCP Server plugin](https://plugins.jetbrains.com/plugin/26071-mcp-server) to your IDE. - ::: - - :::info - Note that you'll need [Node.js](https://nodejs.org/) installed on your system to run this command, as it uses `npx`. - ::: - -
- Configure the extension using the MCP Server plugin and proxy server: - - 1. Add the [MCP Server plugin](https://plugins.jetbrains.com/plugin/26071-mcp-server) to your IDE. - - 2. Add the JetBrains extension to goose: - - - - - - - - - - -
## Example Usage diff --git a/documentation/docs/mcp/speech-mcp.md b/documentation/docs/mcp/speech-mcp.md index f8444a13d5..f22f335a19 100644 --- a/documentation/docs/mcp/speech-mcp.md +++ b/documentation/docs/mcp/speech-mcp.md @@ -4,7 +4,7 @@ description: Add Speech MCP Server as a goose Extension unlisted: true --- -Unlist per https://github.com/block/goose/issues/5431 +Unlist per https://github.com/aaif-goose/goose/issues/5431 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; diff --git a/documentation/docs/mcp/vs-code-mcp.md b/documentation/docs/mcp/vs-code-mcp.md index 442231e811..3062394da3 100644 --- a/documentation/docs/mcp/vs-code-mcp.md +++ b/documentation/docs/mcp/vs-code-mcp.md @@ -10,7 +10,7 @@ import YouTubeShortEmbed from '@site/src/components/YouTubeShortEmbed'; -This tutorial covers how to add the [VS Code MCP Server](https://github.com/block/vscode-mcp) as a goose extension to enable VS Code integration, file operations, and development workflow management. +This tutorial covers how to add the [VS Code MCP Server](https://github.com/aaif-goose/vscode-mcp) as a goose extension to enable VS Code integration, file operations, and development workflow management. :::tip Quick Install diff --git a/documentation/docs/quickstart.md b/documentation/docs/quickstart.md index 565633f66f..bb433db98c 100644 --- a/documentation/docs/quickstart.md +++ b/documentation/docs/quickstart.md @@ -46,7 +46,7 @@ Let's begin πŸš€ Run the following command to install goose: ```sh - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash ``` @@ -71,7 +71,7 @@ Let's begin πŸš€ Run the following command to install the goose CLI on Linux: ```sh - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash ``` @@ -93,7 +93,7 @@ Let's begin πŸš€ Run the following command in **Git Bash**, **MSYS2**, or **PowerShell** to install the goose CLI natively on Windows: ```bash - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash ``` Learn about prerequisites in the [installation guide](/docs/getting-started/installation). diff --git a/documentation/docs/troubleshooting/diagnostics-and-reporting.md b/documentation/docs/troubleshooting/diagnostics-and-reporting.md index 16becee7fd..084ee38749 100644 --- a/documentation/docs/troubleshooting/diagnostics-and-reporting.md +++ b/documentation/docs/troubleshooting/diagnostics-and-reporting.md @@ -111,7 +111,7 @@ The bug report feature opens a structured GitHub issue template to help you prov For CLI users, navigate directly to the GitHub repository: ``` - https://github.com/block/goose/issues/new?template=bug_report.md + https://github.com/aaif-goose/goose/issues/new?template=bug_report.md ``` @@ -135,7 +135,7 @@ The feature request system helps you suggest improvements and new functionality Navigate directly to the GitHub repository: ``` - https://github.com/block/goose/issues/new?template=feature_request.md + https://github.com/aaif-goose/goose/issues/new?template=feature_request.md ``` diff --git a/documentation/docs/troubleshooting/index.mdx b/documentation/docs/troubleshooting/index.mdx index 533defbe32..e5113f552c 100644 --- a/documentation/docs/troubleshooting/index.mdx +++ b/documentation/docs/troubleshooting/index.mdx @@ -44,7 +44,7 @@ import styles from '@site/src/components/Card/styles.module.css';
diff --git a/documentation/docs/troubleshooting/known-issues.md b/documentation/docs/troubleshooting/known-issues.md index 344afd557f..5349afec5b 100644 --- a/documentation/docs/troubleshooting/known-issues.md +++ b/documentation/docs/troubleshooting/known-issues.md @@ -365,7 +365,7 @@ Blocked malicious package: package-name@1.0.0 (npm). OSV MAL advisories: MAL-202 Steps to resolve: 1. **Find an alternative**: Look for similar extensions in the [extensions directory][extensions-directory] or [PulseMCP](https://www.pulsemcp.com/servers) 2. **Optional verification**: Verify the source of the blocked extension or the package name/publisher -3. **Report false positives**: If you believe this is an error, please [open an issue](https://github.com/block/goose/issues) +3. **Report false positives**: If you believe this is an error, please [open an issue](https://github.com/aaif-goose/goose/issues) This security check only applies to locally-executed external extensions that use PyPI (`uvx`) or NPM (`npx`). The check uses real-time data from the OSV database; if the security service is unavailable, extensions will still install normally. diff --git a/documentation/docs/tutorials/cicd.md b/documentation/docs/tutorials/cicd.md index fd75abadce..d52fca641e 100644 --- a/documentation/docs/tutorials/cicd.md +++ b/documentation/docs/tutorials/cicd.md @@ -68,7 +68,7 @@ jobs: - name: Install goose CLI run: | mkdir -p /home/runner/.local/bin - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh \ | GOOSE_VERSION=REPLACE_WITH_VERSION CONFIGURE=false GOOSE_BIN_DIR=/home/runner/.local/bin bash echo "/home/runner/.local/bin" >> $GITHUB_PATH @@ -158,7 +158,7 @@ steps: - name: Install goose CLI run: | mkdir -p /home/runner/.local/bin - curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ + curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh \ | GOOSE_VERSION=REPLACE_WITH_VERSION CONFIGURE=false GOOSE_BIN_DIR=/home/runner/.local/bin bash echo "/home/runner/.local/bin" >> $GITHUB_PATH diff --git a/documentation/docs/tutorials/custom-extensions.md b/documentation/docs/tutorials/custom-extensions.md index 2f5afd8f4a..fd4692814a 100644 --- a/documentation/docs/tutorials/custom-extensions.md +++ b/documentation/docs/tutorials/custom-extensions.md @@ -17,7 +17,7 @@ In this guide, we build an MCP server using the [Python SDK for MCP][mcp-python] You can check out other example servers in the [MCP servers repository][mcp-servers]. MCP SDKs are also available for other common languages, such as [TypeScript][mcp-typescript] and [Kotlin][mcp-kotlin]. :::info -goose supports Tools, Resources, and Prompts from the [Model Context Protocol](https://modelcontextprotocol.io/). See [`mcp_client.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/mcp_client.rs) for the supported protocol version and client capabilities. +goose supports Tools, Resources, and Prompts from the [Model Context Protocol](https://modelcontextprotocol.io/). See [`mcp_client.rs`](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/agents/mcp_client.rs) for the supported protocol version and client capabilities. ::: --- @@ -348,7 +348,7 @@ goose supports advanced MCP features that can enhance your extensions. **Getting Started:** - Use the `sampling/createMessage` method in your MCP server to request AI assistance -- [goose's implementation](https://github.com/block/goose/blob/main/crates/goose/src/agents/mcp_client.rs) currently supports text and image content types +- [goose's implementation](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/agents/mcp_client.rs) currently supports text and image content types - goose automatically advertises sampling capability to all MCP servers **Use Cases:** Document summarization, smart search filtering, code analysis, data insights diff --git a/documentation/docs/tutorials/goose-in-docker.md b/documentation/docs/tutorials/goose-in-docker.md index 4f0b862c21..42a69d030c 100644 --- a/documentation/docs/tutorials/goose-in-docker.md +++ b/documentation/docs/tutorials/goose-in-docker.md @@ -12,7 +12,7 @@ This guide covers two Docker-related scenarios: You can build goose from the source file within a Docker container. This approach not only provides security benefits by creating an isolated environment but also enhances consistency and portability. For example, if you need to troubleshoot an error on a platform you don't usually work with (such as Ubuntu), you can easily debug it using Docker. -To begin, you will need to modify the [`Dockerfile` and `docker-compose.yml` files](https://github.com/block/goose/tree/main/documentation/docs/docker) to suit your requirements. Some changes you might consider include: +To begin, you will need to modify the [`Dockerfile` and `docker-compose.yml` files](https://github.com/aaif-goose/goose/tree/main/documentation/docs/docker) to suit your requirements. Some changes you might consider include: - **Required:** Setting your API key, provider, and model in the `docker-compose.yml` file as environment variables because the keyring settings do not work on Ubuntu in Docker. This example uses Google Gemini. diff --git a/documentation/docs/tutorials/playwright-skill.md b/documentation/docs/tutorials/playwright-skill.md index 571ec5d638..3bee48a44d 100644 --- a/documentation/docs/tutorials/playwright-skill.md +++ b/documentation/docs/tutorials/playwright-skill.md @@ -91,7 +91,7 @@ In goose, enable the [Summon extension](/docs/mcp/summon-mcp) to load Agent Skil Give goose a single prompt that describes what you want to test: ``` -Using the Playwright CLI skill, open block.github.io/goose, click on the Docs menu, click on Context Engineering, +Using the Playwright CLI skill, open goose-docs.ai, click on the Docs menu, click on Context Engineering, then click on Using Skills and generate a test with video and traces ``` @@ -111,7 +111,7 @@ await page.getByRole('link', { name: 'Docs' }).click(); ### What goose does -1. Opens the browser: `playwright-cli open block.github.io/goose` +1. Opens the browser: `playwright-cli open goose-docs.ai` 2. Starts recording: `playwright-cli video-start` and `playwright-cli tracing-start` 3. Takes snapshots to find elements: `playwright-cli snapshot` 4. Performs clicks: `playwright-cli click ` @@ -134,7 +134,7 @@ The generated test might look like: import { test, expect } from '@playwright/test'; test('navigate to Using Skills guide via docs menu', async ({ page }) => { - await page.goto('https://block.github.io/goose'); + await page.goto('https://goose-docs.ai'); await expect(page).toHaveTitle(/goose/); // Click on Docs in the navigation diff --git a/documentation/docs/tutorials/ralph-loop.md b/documentation/docs/tutorials/ralph-loop.md index f99e57298f..f4d4de342c 100644 --- a/documentation/docs/tutorials/ralph-loop.md +++ b/documentation/docs/tutorials/ralph-loop.md @@ -24,9 +24,9 @@ Copy and paste this in your terminal to download the Ralph Loop recipes: ```bash mkdir -p ~/.config/goose/recipes -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/ralph-loop.sh -o ~/.config/goose/recipes/ralph-loop.sh -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/ralph-work.yaml -o ~/.config/goose/recipes/ralph-work.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/ralph-review.yaml -o ~/.config/goose/recipes/ralph-review.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/ralph-loop.sh -o ~/.config/goose/recipes/ralph-loop.sh +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/ralph-work.yaml -o ~/.config/goose/recipes/ralph-work.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/ralph-review.yaml -o ~/.config/goose/recipes/ralph-review.yaml chmod +x ~/.config/goose/recipes/ralph-loop.sh ``` diff --git a/documentation/docs/tutorials/rpi.md b/documentation/docs/tutorials/rpi.md index d93ca4ed0d..016144d578 100644 --- a/documentation/docs/tutorials/rpi.md +++ b/documentation/docs/tutorials/rpi.md @@ -24,14 +24,14 @@ Copy the snippet below and paste it in your terminal. This will download the mai ```sh mkdir -p ~/.config/goose/recipes/subrecipes -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/rpi-research.yaml -o ~/.config/goose/recipes/rpi-research.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/rpi-plan.yaml -o ~/.config/goose/recipes/rpi-plan.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/rpi-implement.yaml -o ~/.config/goose/recipes/rpi-implement.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/rpi-iterate.yaml -o ~/.config/goose/recipes/rpi-iterate.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/rpi-research.yaml -o ~/.config/goose/recipes/rpi-research.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/rpi-plan.yaml -o ~/.config/goose/recipes/rpi-plan.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/rpi-implement.yaml -o ~/.config/goose/recipes/rpi-implement.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/rpi-iterate.yaml -o ~/.config/goose/recipes/rpi-iterate.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-codebase-locator.yaml -o ~/.config/goose/recipes/subrecipes/rpi-codebase-locator.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-codebase-analyzer.yaml -o ~/.config/goose/recipes/subrecipes/rpi-codebase-analyzer.yaml -curl -sL https://raw.githubusercontent.com/block/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-pattern-finder.yaml -o ~/.config/goose/recipes/subrecipes/rpi-pattern-finder.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-codebase-locator.yaml -o ~/.config/goose/recipes/subrecipes/rpi-codebase-locator.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-codebase-analyzer.yaml -o ~/.config/goose/recipes/subrecipes/rpi-codebase-analyzer.yaml +curl -sL https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/src/pages/recipes/data/recipes/subrecipes/rpi-pattern-finder.yaml -o ~/.config/goose/recipes/subrecipes/rpi-pattern-finder.yaml ``` @@ -134,7 +134,7 @@ I start the prompt with the `/research_codebase` command followed by a topic wri ``` -This command invokes the **[RPI Research Codebase](https://raw.githubusercontent.com/block/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-research.yaml)** recipe, whose job is very strict: +This command invokes the **[RPI Research Codebase](https://raw.githubusercontent.com/aaif-goose/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-research.yaml)** recipe, whose job is very strict: - Document what exists - Do not suggest changes @@ -189,7 +189,7 @@ It's important to do each phase in a new session to keep the LLM laser focused o /create_plan a removal of the Tool Selection Strategy feature ``` -The **[RPI Create Plan](https://raw.githubusercontent.com/block/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-plan.yaml)** recipe starts by reading the research document goose created. +The **[RPI Create Plan](https://raw.githubusercontent.com/aaif-goose/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-plan.yaml)** recipe starts by reading the research document goose created. Then it did three key things: @@ -231,7 +231,7 @@ At this point, the plan became the source of truth. The key shift here is that w The plan is explicit enough that someone else could execute it. That's not an accident. Remember that the implementation will be in a fresh new session, so the plan must have enough context to actually execute it. -Again, you as the human need to step in here to review the plan and make sure it's solid. If there's anything amiss, instead of starting over you can run the **[RPI Iterate Plan](https://raw.githubusercontent.com/block/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-iterate.yaml)** plan (`/iterate_plan`) with details on what's wrong. goose will then read the existing plan, research only what needs rethinking, propose targeted updates, and edit the plan accordingly. +Again, you as the human need to step in here to review the plan and make sure it's solid. If there's anything amiss, instead of starting over you can run the **[RPI Iterate Plan](https://raw.githubusercontent.com/aaif-goose/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-iterate.yaml)** plan (`/iterate_plan`) with details on what's wrong. goose will then read the existing plan, research only what needs rethinking, propose targeted updates, and edit the plan accordingly. ## Session 3: Implement @@ -241,7 +241,7 @@ Only after research and planning are complete should you move to implementation. /implement_plan thoughts/plans/2025-12-23-remove-tool-selection-strategy.md ``` -The **[RPI Implement Plan](https://raw.githubusercontent.com/block/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-implement.yaml)** recipe is intentionally boring. In fact, I fell asleep while goose was running it. Implementation should feel mechanical. If it feels creative, something upstream is missing. But knowing that you have a rock solid plan, I advise you to go do something else with your time while goose works (unless there are manual steps in the plan). +The **[RPI Implement Plan](https://raw.githubusercontent.com/aaif-goose/goose/refs/heads/main/documentation/src/pages/recipes/data/recipes/rpi-implement.yaml)** recipe is intentionally boring. In fact, I fell asleep while goose was running it. Implementation should feel mechanical. If it feels creative, something upstream is missing. But knowing that you have a rock solid plan, I advise you to go do something else with your time while goose works (unless there are manual steps in the plan). It will read the plan completely, execute the phases in order, run verification after each phase, and update the checkboxes directly in the plan file as it goes. @@ -251,7 +251,7 @@ That last bit was really helpful because my context window filled up partway thr For 10 phases of work that spanned 32 files, the Research phase took 9 minutes, the Plan phase took 4 minutes, and the Implement phase took 39 minutes. So in total, this took just shy of an hour... 52 minutes to be exact. This included goose working and testing as well as me answering questions. -Definitely not a fast process. BUT! When I put up [this PR](https://github.com/block/goose/pull/6250), the build passed and the separate Code Review Agent didn't have a single comment. That's just how well done the work was. +Definitely not a fast process. BUT! When I put up [this PR](https://github.com/aaif-goose/goose/pull/6250), the build passed and the separate Code Review Agent didn't have a single comment. That's just how well done the work was. Had I done this without AI, it would have likely taken me several hours of work as the feature was complex and deeply integrated. And had I had AI jump straight to implementation, I have no doubt it would have surely drifted and messed something up. diff --git a/documentation/docusaurus.config.ts b/documentation/docusaurus.config.ts index 3e9916da71..285074be03 100644 --- a/documentation/docusaurus.config.ts +++ b/documentation/docusaurus.config.ts @@ -13,23 +13,22 @@ const inkeepOrgId = process.env.INKEEP_ORG_ID; const config: Config = { title: "goose", - tagline: - "your local AI agent, automating engineering tasks seamlessly", + tagline: "your local AI agent, automating engineering tasks seamlessly", favicon: "img/favicon.ico", // Set the production url of your site here - url: "https://block.github.io/", + url: "https://goose-docs.ai/", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' - baseUrl: process.env.TARGET_PATH || "/goose/", + baseUrl: process.env.TARGET_PATH || "/", // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. - organizationName: "block", // Usually your GitHub org/user name. + organizationName: "aaif-goose", // Usually your GitHub org/user name. projectName: "goose", // Usually your repo name. onBrokenLinks: "throw", - + markdown: { hooks: { onBrokenMarkdownLinks: "warn", @@ -44,7 +43,6 @@ const config: Config = { locales: ["en"], }, - headTags: [ { tagName: "link", @@ -52,7 +50,7 @@ const config: Config = { rel: "alternate", type: "text/plain", title: "LLM context", - href: "/goose/llms.txt", + href: "/llms.txt", }, }, ], @@ -76,7 +74,7 @@ const config: Config = { onInlineTags: "warn", onInlineAuthors: "warn", onUntruncatedBlogPosts: "warn", - blogSidebarCount: 'ALL' + blogSidebarCount: "ALL", }, theme: { customCss: [ @@ -85,10 +83,13 @@ const config: Config = { "./src/css/tailwind.css", ], }, - gtag: process.env.NODE_ENV === 'production' ? { - trackingID: 'G-ZS5D6SB4ZJ', - anonymizeIP: true, - } : false, + gtag: + process.env.NODE_ENV === "production" + ? { + trackingID: "G-ZS5D6SB4ZJ", + anonymizeIP: true, + } + : false, } satisfies Preset.Options, ], ], @@ -99,242 +100,242 @@ const config: Config = { { redirects: [ { - from: '/docs/getting-started/using-goose-free', - to: '/docs/getting-started/providers#using-goose-for-free' + from: "/docs/getting-started/using-goose-free", + to: "/docs/getting-started/providers#using-goose-for-free", }, { - from: '/v1/docs/getting-started/providers', - to: '/docs/getting-started/providers' + from: "/v1/docs/getting-started/providers", + to: "/docs/getting-started/providers", }, { - from: '/v1/docs/getting-started/installation', - to: '/docs/getting-started/installation' + from: "/v1/docs/getting-started/installation", + to: "/docs/getting-started/installation", }, { - from: '/v1/docs/quickstart', - to: '/docs/quickstart' + from: "/v1/docs/quickstart", + to: "/docs/quickstart", }, { - from: '/v1/', - to: '/' + from: "/v1/", + to: "/", }, { - from: '/docs/guides/custom-extensions', - to: '/docs/tutorials/custom-extensions' + from: "/docs/guides/custom-extensions", + to: "/docs/tutorials/custom-extensions", }, { - from: '/docs', - to: '/docs/category/getting-started' + from: "/docs", + to: "/docs/category/getting-started", }, { - from: '/v1/extensions', - to: '/extensions' + from: "/v1/extensions", + to: "/extensions", }, { - from: '/v1/extensions/detail/nondeveloper', - to: '/docs/mcp/computer-controller-mcp' + from: "/v1/extensions/detail/nondeveloper", + to: "/docs/mcp/computer-controller-mcp", }, { - from: '/docs/guides/managing-goose-sessions', - to: '/docs/guides/sessions/session-management' + from: "/docs/guides/managing-goose-sessions", + to: "/docs/guides/sessions/session-management", }, { - from: '/docs/guides/smart-context-management', - to: '/docs/guides/sessions/smart-context-management' + from: "/docs/guides/smart-context-management", + to: "/docs/guides/sessions/smart-context-management", }, { - from: '/docs/guides/share-goose-sessions', - to: '/docs/guides/recipes/session-recipes' + from: "/docs/guides/share-goose-sessions", + to: "/docs/guides/recipes/session-recipes", }, { - from: '/docs/guides/session-recipes', - to: '/docs/guides/recipes/session-recipes' + from: "/docs/guides/session-recipes", + to: "/docs/guides/recipes/session-recipes", }, { - from: '/docs/guides/recipe-reference', - to: '/docs/guides/recipes/recipe-reference' + from: "/docs/guides/recipe-reference", + to: "/docs/guides/recipes/recipe-reference", }, { - from: '/docs/guides/recipes/sub-recipes', - to: '/docs/guides/recipes/subrecipes' + from: "/docs/guides/recipes/sub-recipes", + to: "/docs/guides/recipes/subrecipes", }, { - from: '/docs/tutorials/sub-recipes-in-parallel', - to: '/docs/tutorials/subrecipes-in-parallel' + from: "/docs/tutorials/sub-recipes-in-parallel", + to: "/docs/tutorials/subrecipes-in-parallel", }, { - from: '/docs/guides/tool-permissions', - to: '/docs/guides/managing-tools/tool-permissions' + from: "/docs/guides/tool-permissions", + to: "/docs/guides/managing-tools/tool-permissions", }, { - from: '/docs/guides/adjust-tool-output', - to: '/docs/guides/managing-tools/adjust-tool-output' + from: "/docs/guides/adjust-tool-output", + to: "/docs/guides/managing-tools/adjust-tool-output", }, { - from: '/docs/guides/benchmarking', - to: '/docs/tutorials/benchmarking' + from: "/docs/guides/benchmarking", + to: "/docs/tutorials/benchmarking", }, { - from: '/docs/guides/goose-in-docker', - to: '/docs/tutorials/goose-in-docker' + from: "/docs/guides/goose-in-docker", + to: "/docs/tutorials/goose-in-docker", }, { - from: '/docs/guides/multi-model/creating-plans', - to: '/docs/guides/creating-plans' + from: "/docs/guides/multi-model/creating-plans", + to: "/docs/guides/creating-plans", }, { - from: '/docs/guides/config-file', - to: '/docs/guides/config-files', + from: "/docs/guides/config-file", + to: "/docs/guides/config-files", }, { - from: '/docs/guides/using-goosehints', - to: '/docs/guides/context-engineering/using-goosehints', + from: "/docs/guides/using-goosehints", + to: "/docs/guides/context-engineering/using-goosehints", }, // MCP tutorial redirects - moved from /docs/tutorials/ to /docs/mcp/ { - from: '/docs/tutorials/agentql-mcp', - to: '/docs/mcp/agentql-mcp' + from: "/docs/tutorials/agentql-mcp", + to: "/docs/mcp/agentql-mcp", }, { - from: '/docs/tutorials/asana-mcp', - to: '/docs/mcp/asana-mcp' + from: "/docs/tutorials/asana-mcp", + to: "/docs/mcp/asana-mcp", }, { - from: '/docs/tutorials/blender-mcp', - to: '/docs/mcp/blender-mcp' + from: "/docs/tutorials/blender-mcp", + to: "/docs/mcp/blender-mcp", }, { - from: '/docs/tutorials/brave-mcp', - to: '/docs/mcp/brave-mcp' + from: "/docs/tutorials/brave-mcp", + to: "/docs/mcp/brave-mcp", }, { - from: '/docs/tutorials/browserbase-mcp', - to: '/docs/mcp/browserbase-mcp' + from: "/docs/tutorials/browserbase-mcp", + to: "/docs/mcp/browserbase-mcp", }, { - from: '/docs/tutorials/computer-controller-mcp', - to: '/docs/mcp/computer-controller-mcp' + from: "/docs/tutorials/computer-controller-mcp", + to: "/docs/mcp/computer-controller-mcp", }, { - from: '/docs/tutorials/context7-mcp', - to: '/docs/mcp/context7-mcp' + from: "/docs/tutorials/context7-mcp", + to: "/docs/mcp/context7-mcp", }, { - from: '/docs/tutorials/developer-mcp', - to: '/docs/mcp/developer-mcp' + from: "/docs/tutorials/developer-mcp", + to: "/docs/mcp/developer-mcp", }, { - from: '/docs/tutorials/elevenlabs-mcp', - to: '/docs/mcp/elevenlabs-mcp' + from: "/docs/tutorials/elevenlabs-mcp", + to: "/docs/mcp/elevenlabs-mcp", }, { - from: '/docs/tutorials/fetch-mcp', - to: '/docs/mcp/fetch-mcp' + from: "/docs/tutorials/fetch-mcp", + to: "/docs/mcp/fetch-mcp", }, { - from: '/docs/tutorials/figma-mcp', - to: '/docs/mcp/figma-mcp' + from: "/docs/tutorials/figma-mcp", + to: "/docs/mcp/figma-mcp", }, { - from: '/docs/tutorials/filesystem-mcp', - to: '/docs/mcp/filesystem-mcp' + from: "/docs/tutorials/filesystem-mcp", + to: "/docs/mcp/filesystem-mcp", }, { - from: '/docs/tutorials/github-mcp', - to: '/docs/mcp/github-mcp' + from: "/docs/tutorials/github-mcp", + to: "/docs/mcp/github-mcp", }, { - from: '/docs/tutorials/google-drive-mcp', - to: '/docs/mcp/google-drive-mcp' + from: "/docs/tutorials/google-drive-mcp", + to: "/docs/mcp/google-drive-mcp", }, { - from: '/docs/tutorials/google-maps-mcp', - to: '/docs/mcp/google-maps-mcp' + from: "/docs/tutorials/google-maps-mcp", + to: "/docs/mcp/google-maps-mcp", }, { - from: '/docs/tutorials/jetbrains-mcp', - to: '/docs/mcp/jetbrains-mcp' + from: "/docs/tutorials/jetbrains-mcp", + to: "/docs/mcp/jetbrains-mcp", }, { - from: '/docs/tutorials/knowledge-graph-mcp', - to: '/docs/mcp/knowledge-graph-mcp' + from: "/docs/tutorials/knowledge-graph-mcp", + to: "/docs/mcp/knowledge-graph-mcp", }, { - from: '/docs/tutorials/mbot-mcp', - to: '/docs/mcp/mbot-mcp' + from: "/docs/tutorials/mbot-mcp", + to: "/docs/mcp/mbot-mcp", }, { - from: '/docs/tutorials/memory-mcp', - to: '/docs/mcp/memory-mcp' + from: "/docs/tutorials/memory-mcp", + to: "/docs/mcp/memory-mcp", }, { - from: '/docs/tutorials/nostrbook-mcp', - to: '/docs/mcp/nostrbook-mcp' + from: "/docs/tutorials/nostrbook-mcp", + to: "/docs/mcp/nostrbook-mcp", }, { - from: '/docs/tutorials/pdf-mcp', - to: '/docs/mcp/pdf-mcp' + from: "/docs/tutorials/pdf-mcp", + to: "/docs/mcp/pdf-mcp", }, { - from: '/docs/tutorials/pieces-mcp', - to: '/docs/mcp/pieces-mcp' + from: "/docs/tutorials/pieces-mcp", + to: "/docs/mcp/pieces-mcp", }, { - from: '/docs/tutorials/playwright-mcp', - to: '/docs/mcp/playwright-mcp' + from: "/docs/tutorials/playwright-mcp", + to: "/docs/mcp/playwright-mcp", }, { - from: '/docs/tutorials/postgres-mcp', - to: '/docs/mcp/postgres-mcp' + from: "/docs/tutorials/postgres-mcp", + to: "/docs/mcp/postgres-mcp", }, { - from: '/docs/tutorials/puppeteer-mcp', - to: '/docs/mcp/puppeteer-mcp' + from: "/docs/tutorials/puppeteer-mcp", + to: "/docs/mcp/puppeteer-mcp", }, { - from: '/docs/tutorials/reddit-mcp', - to: '/docs/mcp/reddit-mcp' + from: "/docs/tutorials/reddit-mcp", + to: "/docs/mcp/reddit-mcp", }, { - from: '/docs/tutorials/repomix-mcp', - to: '/docs/mcp/repomix-mcp' + from: "/docs/tutorials/repomix-mcp", + to: "/docs/mcp/repomix-mcp", }, { - from: '/docs/tutorials/selenium-mcp', - to: '/docs/mcp/selenium-mcp' + from: "/docs/tutorials/selenium-mcp", + to: "/docs/mcp/selenium-mcp", }, { - from: '/docs/tutorials/speech-mcp', - to: '/docs/mcp/speech-mcp' + from: "/docs/tutorials/speech-mcp", + to: "/docs/mcp/speech-mcp", }, { - from: '/docs/tutorials/square-mcp', - to: '/docs/mcp/square-mcp' + from: "/docs/tutorials/square-mcp", + to: "/docs/mcp/square-mcp", }, { - from: '/docs/tutorials/tavily-mcp', - to: '/docs/mcp/tavily-mcp' + from: "/docs/tutorials/tavily-mcp", + to: "/docs/mcp/tavily-mcp", }, { - from: '/docs/tutorials/tutorial-extension', - to: '/docs/mcp/tutorial-mcp' + from: "/docs/tutorials/tutorial-extension", + to: "/docs/mcp/tutorial-mcp", }, { - from: '/docs/tutorials/vscode-mcp', - to: '/docs/mcp/vs-code-mcp' + from: "/docs/tutorials/vscode-mcp", + to: "/docs/mcp/vs-code-mcp", }, { - from: '/docs/tutorials/youtube-transcript', - to: '/docs/mcp/youtube-transcript-mcp' + from: "/docs/tutorials/youtube-transcript", + to: "/docs/mcp/youtube-transcript-mcp", }, { - from: '/docs/guides/isolated-development-environments', - to: '/docs/tutorials/isolated-development-environments' + from: "/docs/guides/isolated-development-environments", + to: "/docs/tutorials/isolated-development-environments", }, { - from: '/docs/experimental/subagents', - to: '/docs/guides/subagents' - } + from: "/docs/experimental/subagents", + to: "/docs/guides/subagents", + }, ], }, ], @@ -351,12 +352,12 @@ const config: Config = { // Replace with your project's social card image: "img/home-banner.png", colorMode: { - respectPrefersColorScheme: true + respectPrefersColorScheme: true, }, navbar: { title: "", logo: { - alt: "Block Logo", + alt: "goose Logo", // TODO: replace logo assets with AAIF branding src: "img/logo_light.png", srcDark: "img/logo_dark.png", }, @@ -383,33 +384,33 @@ const config: Config = { }, { to: "/blog", label: "Blog", position: "left" }, { - type: 'dropdown', - label: 'Resources', - position: 'left', + type: "dropdown", + label: "Resources", + position: "left", items: [ { - to: '/extensions', - label: 'Extensions', + to: "/extensions", + label: "Extensions", }, { - to: '/skills', - label: 'Skills Marketplace', + to: "/skills", + label: "Skills Marketplace", }, { - to: '/recipe-generator', - label: 'Recipe Generator', + to: "/recipe-generator", + label: "Recipe Generator", }, { - to: '/prompt-library', - label: 'Prompt Library', + to: "/prompt-library", + label: "Prompt Library", }, { - to: '/recipes', - label: 'Recipe Cookbook', + to: "/recipes", + label: "Recipe Cookbook", }, { - to: 'deeplink-generator', - label: 'Deeplink Generator', + to: "deeplink-generator", + label: "Deeplink Generator", }, ], }, @@ -420,7 +421,7 @@ const config: Config = { position: "right", }, { - href: "https://github.com/block/goose", + href: "https://github.com/aaif-goose/goose", label: "GitHub", position: "right", }, @@ -483,12 +484,12 @@ const config: Config = { }, { label: "GitHub", - href: "https://github.com/block/goose", + href: "https://github.com/aaif-goose/goose", }, ], }, ], - copyright: `Copyright Β© ${new Date().getFullYear()} Block, Inc.`, + copyright: `Copyright Β© ${new Date().getFullYear()} AAIF (Agentic AI Foundation)`, }, prism: { theme: prismThemes.github, @@ -503,12 +504,11 @@ const config: Config = { }, aiChatSettings: { chatSubjectName: "goose", - botAvatarSrcUrl: - "", + botAvatarSrcUrl: "", getHelpCallToActions: [ { name: "GitHub", - url: "https://github.com/block/goose", + url: "https://github.com/aaif-goose/goose", icon: { builtIn: "FaGithub", }, @@ -517,16 +517,7 @@ const config: Config = { quickQuestions: ["What is goose?"], }, }, - // announcementBar: { - // id: 'goose-grants', - // content: - // '✨ goose grant program now open: apply now! ✨', - // backgroundColor: '#20232a', - // textColor: '#fff', - // isCloseable: false, - // }, } satisfies Preset.ThemeConfig, }; - export default config; diff --git a/documentation/package-lock.json b/documentation/package-lock.json index 3710e70935..2c4b1d9de0 100644 --- a/documentation/package-lock.json +++ b/documentation/package-lock.json @@ -250,6 +250,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.42.0.tgz", "integrity": "sha512-NZR7yyHj2WzK6D5X8gn+/KOxPdzYEXOqVdSaK/biU8QfYUpUuEA0sCWg/XlO05tPVEcJelF/oLrrNY3UjRbOww==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.42.0", "@algolia/requester-browser-xhr": "5.42.0", @@ -387,6 +388,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2176,6 +2178,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2198,6 +2201,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2307,6 +2311,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2728,6 +2733,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3688,6 +3694,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -5032,6 +5039,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5366,6 +5374,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5728,6 +5737,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6078,6 +6088,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6163,6 +6174,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6208,6 +6220,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.42.0.tgz", "integrity": "sha512-X5+PtWc9EJIPafT/cj8ZG+6IU3cjRRnlHGtqMHK/9gsiupQbAyYlH5y7qt/FtsAhfX5AICHffZy69ZAsVrxWkQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.8.0", "@algolia/client-abtesting": "5.42.0", @@ -6706,6 +6719,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -7634,6 +7648,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -7946,7 +7961,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debounce": { "version": "1.2.1", @@ -9031,6 +9047,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10967,9 +10984,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -13681,6 +13698,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14251,6 +14269,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15263,6 +15282,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16080,6 +16100,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16089,6 +16110,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16161,6 +16183,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -16216,6 +16239,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -18190,7 +18214,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/turndown": { "version": "7.2.2", @@ -18262,6 +18287,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18603,6 +18629,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18810,6 +18837,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19423,6 +19451,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "devOptional": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -19462,6 +19491,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/documentation/scripts/community_stars.py b/documentation/scripts/community_stars.py index 88aeffeec4..e59e1270f4 100755 --- a/documentation/scripts/community_stars.py +++ b/documentation/scripts/community_stars.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """ -Community Stars Analysis Script for block/goose repository +Community Stars Analysis Script for aaif-goose/goose repository + +# TODO: Update BLOCK_ORGS, is_block_employee(), and team categorization logic +# for the AAIF org structure after the transfer from block/goose. This script analyzes GitHub contributor statistics and generates rankings for: - Top 5 Community All-Stars (External contributors) @@ -23,25 +26,26 @@ Requirements: - Team list file at documentation/scripts/community_stars_teams.txt """ +import calendar import json import re import sys +import time import urllib.request from datetime import datetime -import calendar from pathlib import Path -import time # GitHub URL for team list file -TEAMS_FILE_URL = "https://raw.githubusercontent.com/block/goose/main/documentation/scripts/community_stars_teams.txt" +TEAMS_FILE_URL = "https://raw.githubusercontent.com/aaif-goose/goose/main/documentation/scripts/community_stars_teams.txt" LOCAL_TEAMS_FILE = Path(__file__).parent / "community_stars_teams.txt" # Block-related organizations to check -BLOCK_ORGS = {'square', 'block', 'squareup', 'block-ghc', 'cashapp'} +BLOCK_ORGS = {"square", "block", "squareup", "block-ghc", "cashapp"} + def is_block_employee(username): """Check if a user is a Block employee by checking their profile and org memberships. - + Makes a single API call to get user profile (includes company field), then only calls orgs endpoint if company field doesn't match. """ @@ -49,93 +53,97 @@ def is_block_employee(username): # First check the user's profile (single API call) url = f"https://api.github.com/users/{username}" with urllib.request.urlopen(url) as response: - user_data = json.loads(response.read().decode('utf-8')) - + user_data = json.loads(response.read().decode("utf-8")) + # Check company field first (no additional API call needed) - company = user_data.get('company', '').lower() if user_data.get('company') else '' + company = ( + user_data.get("company", "").lower() if user_data.get("company") else "" + ) if company: # Check for Block-related keywords in company field - block_keywords = ['block', 'square', 'cash app', 'cashapp', 'tidal'] + block_keywords = ["block", "square", "cash app", "cashapp", "tidal"] if any(keyword in company for keyword in block_keywords): return True - + # Only check orgs if company field didn't match (second API call only when needed) url = f"https://api.github.com/users/{username}/orgs" with urllib.request.urlopen(url) as response: - orgs = json.loads(response.read().decode('utf-8')) - + orgs = json.loads(response.read().decode("utf-8")) + # Check if any org matches Block orgs (case-insensitive) - user_orgs = {org['login'].lower() for org in orgs} + user_orgs = {org["login"].lower() for org in orgs} if user_orgs & BLOCK_ORGS: return True - + return False - + except Exception as e: # If we can't check (rate limit, network error, etc.), return False # This means we'll default to treating them as external return False + def load_team_lists(): """Load and parse team lists from file (local or GitHub).""" content = None - + # Try local file first if LOCAL_TEAMS_FILE.exists(): - with open(LOCAL_TEAMS_FILE, 'r') as f: + with open(LOCAL_TEAMS_FILE, "r") as f: content = f.read() else: # Fall back to GitHub try: with urllib.request.urlopen(TEAMS_FILE_URL) as response: - content = response.read().decode('utf-8') + content = response.read().decode("utf-8") except Exception as e: print(f"Error: Could not load team list file from {TEAMS_FILE_URL}") print(f"Details: {e}") sys.exit(1) - + # Parse the team lists goose_maintainers = set() block_non_goose = set() external_goose = set() bots = set() - + current_section = None - for line in content.split('\n'): + for line in content.split("\n"): line = line.strip() - + # Skip comments and empty lines - if not line or line.startswith('#'): + if not line or line.startswith("#"): # Check for section headers in comments - if '# Goose Maintainers' in line: - current_section = 'goose_maintainers' - elif '# Block, non-goose' in line: - current_section = 'block_non_goose' - elif '# External, goose' in line: - current_section = 'external_goose' - elif '# Bots' in line: - current_section = 'bots' + if "# Goose Maintainers" in line: + current_section = "goose_maintainers" + elif "# Block, non-goose" in line: + current_section = "block_non_goose" + elif "# External, goose" in line: + current_section = "external_goose" + elif "# Bots" in line: + current_section = "bots" continue - + # Add username to appropriate set (lowercase for case-insensitive matching) username = line.lower() - if current_section == 'goose_maintainers': + if current_section == "goose_maintainers": goose_maintainers.add(username) - elif current_section == 'block_non_goose': + elif current_section == "block_non_goose": block_non_goose.add(username) - elif current_section == 'external_goose': + elif current_section == "external_goose": external_goose.add(username) - elif current_section == 'bots': + elif current_section == "bots": bots.add(username) - + return goose_maintainers, block_non_goose, external_goose, bots + def parse_date_range(date_input): """Parse various date input formats and return start/end timestamps.""" date_input = date_input.strip() - + # Format: "Month YYYY" (e.g., "November 2025") - month_year_pattern = r'^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})$' + month_year_pattern = r"^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})$" match = re.match(month_year_pattern, date_input, re.IGNORECASE) if match: month_name = match.group(1).capitalize() @@ -144,16 +152,16 @@ def parse_date_range(date_input): last_day = calendar.monthrange(year, start_date.month)[1] end_date = datetime(year, start_date.month, last_day, 23, 59, 59) return start_date.timestamp(), end_date.timestamp(), date_input - + # Format: "Date1 - Date2" (e.g., "November 1, 2025 - November 17, 2025" or "2025-11-01 - 2025-11-17") - if ' - ' in date_input or ' to ' in date_input: - separator = ' - ' if ' - ' in date_input else ' to ' + if " - " in date_input or " to " in date_input: + separator = " - " if " - " in date_input else " to " parts = date_input.split(separator) if len(parts) == 2: date_formats = ["%B %d, %Y", "%b %d, %Y", "%Y-%m-%d"] start_date = None end_date = None - + for fmt in date_formats: try: start_date = datetime.strptime(parts[0].strip(), fmt) @@ -161,12 +169,17 @@ def parse_date_range(date_input): break except ValueError: continue - + if start_date and end_date: - end_date = datetime(end_date.year, end_date.month, end_date.day, 23, 59, 59) + end_date = datetime( + end_date.year, end_date.month, end_date.day, 23, 59, 59 + ) return start_date.timestamp(), end_date.timestamp(), date_input - - raise ValueError(f"Could not parse date input: {date_input}\nSupported formats:\n - 'Month YYYY' (e.g., 'November 2025')\n - 'Month Day, YYYY - Month Day, YYYY' (e.g., 'November 1, 2025 - November 17, 2025')\n - 'YYYY-MM-DD - YYYY-MM-DD' (e.g., '2025-11-01 - 2025-11-17')") + + raise ValueError( + f"Could not parse date input: {date_input}\nSupported formats:\n - 'Month YYYY' (e.g., 'November 2025')\n - 'Month Day, YYYY - Month Day, YYYY' (e.g., 'November 1, 2025 - November 17, 2025')\n - 'YYYY-MM-DD - YYYY-MM-DD' (e.g., '2025-11-01 - 2025-11-17')" + ) + def main(): # Parse command line arguments @@ -191,127 +204,162 @@ def main(): goose_maintainers, block_non_goose, external_goose, bots = load_team_lists() # Load GitHub data - github_data_file = '/tmp/github_contributors.json' + github_data_file = "/tmp/github_contributors.json" contributors_data = None - + # Try to load existing file first try: - with open(github_data_file, 'r') as f: + with open(github_data_file, "r") as f: contributors_data = json.load(f) - + # Validate the data is not empty or invalid - if not contributors_data or not isinstance(contributors_data, list) or len(contributors_data) == 0: - print(f"Warning: GitHub data file exists but is empty or invalid. Fetching fresh data...", file=sys.stderr) + if ( + not contributors_data + or not isinstance(contributors_data, list) + or len(contributors_data) == 0 + ): + print( + f"Warning: GitHub data file exists but is empty or invalid. Fetching fresh data...", + file=sys.stderr, + ) contributors_data = None except (FileNotFoundError, json.JSONDecodeError): - print(f"GitHub data file not found or invalid. Fetching fresh data...", file=sys.stderr) + print( + f"GitHub data file not found or invalid. Fetching fresh data...", + file=sys.stderr, + ) contributors_data = None - + # Fetch from GitHub API if needed if contributors_data is None: print("Fetching contributor data from GitHub API...", file=sys.stderr) max_retries = 3 retry_delay = 2 - + for attempt in range(max_retries): try: - url = "https://api.github.com/repos/block/goose/stats/contributors" + url = "https://api.github.com/repos/aaif-goose/goose/stats/contributors" with urllib.request.urlopen(url, timeout=30) as response: - contributors_data = json.loads(response.read().decode('utf-8')) - + contributors_data = json.loads(response.read().decode("utf-8")) + # Validate the response - if contributors_data and isinstance(contributors_data, list) and len(contributors_data) > 0: + if ( + contributors_data + and isinstance(contributors_data, list) + and len(contributors_data) > 0 + ): # Save to file for future use - with open(github_data_file, 'w') as f: + with open(github_data_file, "w") as f: json.dump(contributors_data, f) - print(f"βœ“ Successfully fetched data for {len(contributors_data)} contributors", file=sys.stderr) + print( + f"βœ“ Successfully fetched data for {len(contributors_data)} contributors", + file=sys.stderr, + ) break else: - print(f"Attempt {attempt + 1}/{max_retries}: GitHub API returned empty data. Retrying...", file=sys.stderr) + print( + f"Attempt {attempt + 1}/{max_retries}: GitHub API returned empty data. Retrying...", + file=sys.stderr, + ) contributors_data = None if attempt < max_retries - 1: time.sleep(retry_delay) except Exception as e: - print(f"Attempt {attempt + 1}/{max_retries}: Error fetching from GitHub API: {e}", file=sys.stderr) + print( + f"Attempt {attempt + 1}/{max_retries}: Error fetching from GitHub API: {e}", + file=sys.stderr, + ) if attempt < max_retries - 1: time.sleep(retry_delay) else: - print("\nError: Could not fetch GitHub contributor data after multiple attempts.") - print("The GitHub stats API may be temporarily unavailable or still computing statistics.") + print( + "\nError: Could not fetch GitHub contributor data after multiple attempts." + ) + print( + "The GitHub stats API may be temporarily unavailable or still computing statistics." + ) print("Please try again in a few minutes.") sys.exit(1) - + if contributors_data is None: print("\nError: GitHub API returned empty data after multiple attempts.") - print("The repository statistics may still be computing. Please try again in a few minutes.") + print( + "The repository statistics may still be computing. Please try again in a few minutes." + ) sys.exit(1) # Process contributors contributor_stats = [] checked_orgs = {} # Cache org checks to avoid redundant API calls - + print("Checking contributor organizations...", file=sys.stderr) for contributor in contributors_data: # Skip if author is None (deleted users) - if contributor.get('author') is None: + if contributor.get("author") is None: continue - - username = contributor['author']['login'] + + username = contributor["author"]["login"] username_lower = username.lower() - + # Skip excluded categories (case-insensitive matching) - if username_lower in bots or username_lower in goose_maintainers or username_lower in external_goose: + if ( + username_lower in bots + or username_lower in goose_maintainers + or username_lower in external_goose + ): continue - + # Calculate stats for the specified period period_commits = 0 period_additions = 0 period_deletions = 0 - - for week in contributor['weeks']: - week_timestamp = week['w'] + + for week in contributor["weeks"]: + week_timestamp = week["w"] if start_timestamp <= week_timestamp <= end_timestamp: - period_commits += week['c'] - period_additions += week['a'] - period_deletions += week['d'] - + period_commits += week["c"] + period_additions += week["a"] + period_deletions += week["d"] + # Only include contributors with activity in the period if period_commits > 0: total_lines = period_additions + period_deletions - + # Categorize (only Block non-goose and External now) if username_lower in block_non_goose: - category = 'block_non_goose' + category = "block_non_goose" else: # Check if user is in a Block org (with caching) if username not in checked_orgs: checked_orgs[username] = is_block_employee(username) # Add a small delay to avoid rate limiting time.sleep(0.1) - + if checked_orgs[username]: - category = 'block_non_goose' + category = "block_non_goose" print(f" βœ“ Detected Block employee: @{username}", file=sys.stderr) else: - category = 'external' - - contributor_stats.append({ - 'username': username, - 'category': category, - 'commits': period_commits, - 'additions': period_additions, - 'deletions': period_deletions, - 'total_lines': total_lines, - 'score': period_commits + total_lines - }) + category = "external" + + contributor_stats.append( + { + "username": username, + "category": category, + "commits": period_commits, + "additions": period_additions, + "deletions": period_deletions, + "total_lines": total_lines, + "score": period_commits + total_lines, + } + ) # Sort by score - contributor_stats.sort(key=lambda x: x['score'], reverse=True) + contributor_stats.sort(key=lambda x: x["score"], reverse=True) # Separate by category - block_list = [c for c in contributor_stats if c['category'] == 'block_non_goose'] - external_list = [c for c in contributor_stats if c['category'] == 'external'] + block_list = [c for c in contributor_stats if c["category"] == "block_non_goose"] + external_list = [c for c in contributor_stats if c["category"] == "external"] # Get top 5 from each top_external = external_list[:5] @@ -320,7 +368,9 @@ def main(): # Print results print("=" * 70) print(f"COMMUNITY STARS - {display_period.upper()}") - print(f"(Period: {start_date.strftime('%B %d, %Y')} - {end_date.strftime('%B %d, %Y')})") + print( + f"(Period: {start_date.strftime('%B %d, %Y')} - {end_date.strftime('%B %d, %Y')})" + ) print("=" * 70) print() @@ -328,7 +378,9 @@ def main(): print("-" * 70) if top_external: for i, contrib in enumerate(top_external, 1): - print(f"{i}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines") + print( + f"{i}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines" + ) else: print("No external contributors found for this period.") @@ -337,7 +389,9 @@ def main(): print("-" * 70) if top_internal: for i, contrib in enumerate(top_internal, 1): - print(f"{i}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines") + print( + f"{i}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines" + ) else: print("No internal contributors found for this period.") @@ -346,17 +400,22 @@ def main(): print("-" * 70) if contributor_stats: for i, contrib in enumerate(contributor_stats, 1): - cat_label = "External" if contrib['category'] == 'external' else "Block" - print(f"{i:2d}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines [{cat_label}]") + cat_label = "External" if contrib["category"] == "external" else "Block" + print( + f"{i:2d}. @{contrib['username']:20s} - {contrib['commits']:3d} commits, {contrib['total_lines']:6,d} lines [{cat_label}]" + ) else: print("No contributors found for this period.") print() print("=" * 70) - print(f"Total contributors (excluding bots, goose maintainers, external goose): {len(contributor_stats)}") + print( + f"Total contributors (excluding bots, goose maintainers, external goose): {len(contributor_stats)}" + ) print(f" External: {len(external_list)}") print(f" Block (non-goose): {len(block_list)}") print("=" * 70) + if __name__ == "__main__": main() diff --git a/documentation/scripts/community_stars_teams.txt b/documentation/scripts/community_stars_teams.txt index 5c71029e34..3bdd57a7e8 100644 --- a/documentation/scripts/community_stars_teams.txt +++ b/documentation/scripts/community_stars_teams.txt @@ -1,5 +1,5 @@ # Community Stars Team Lists -# This file categorizes contributors for the block/goose Community Stars program +# This file categorizes contributors for the aaif-goose/goose Community Stars program # Format: One username per line under each category header # # Note: External contributors (eligible for Community All-Stars) are automatically diff --git a/documentation/scripts/generate-docs-map.js b/documentation/scripts/generate-docs-map.js index 4bc41803e3..5eafbaabab 100644 --- a/documentation/scripts/generate-docs-map.js +++ b/documentation/scripts/generate-docs-map.js @@ -79,7 +79,7 @@ async function main() { } } - output += `---\n\n> Full docs: https://block.github.io/goose/\n`; + output += `---\n\n> Full docs: https://goose-docs.ai/\n`; fs.writeFileSync(OUTPUT_FILE, output); console.log(`[generate-docs-map] Generated: ${OUTPUT_FILE}`); diff --git a/documentation/scripts/serve-static.js b/documentation/scripts/serve-static.js index ced17b3a72..2835205849 100644 --- a/documentation/scripts/serve-static.js +++ b/documentation/scripts/serve-static.js @@ -23,29 +23,16 @@ const serve = serveStatic(buildDir, { }); const server = http.createServer((req, res) => { - // Handle requests to /goose/ by serving from the build directory - if (req.url.startsWith('/goose/')) { - // Strip /goose/ prefix and serve the file - req.url = req.url.substring(6); // Remove '/goose' - serve(req, res, () => { - res.statusCode = 404; - res.end('Not found'); - }); - } else if (req.url === '/') { - // Redirect root to /goose/ - res.writeHead(302, { Location: '/goose/' }); - res.end(); - } else { - // For any other path, return 404 + serve(req, res, () => { res.statusCode = 404; - res.end('Not found - try /goose/'); - } + res.end('Not found'); + }); }); server.listen(port, () => { console.log(`\nπŸš€ Static file server running at http://localhost:${port}`); - console.log(`\n🏠 Homepage: http://localhost:${port}/goose/`); + console.log(`\n🏠 Homepage: http://localhost:${port}/`); console.log(`\nπŸ“ Test markdown exports:`); - console.log(` http://localhost:${port}/goose/docs/quickstart.md`); - console.log(` http://localhost:${port}/goose/docs/getting-started/installation.md\n`); + console.log(` http://localhost:${port}/docs/quickstart.md`); + console.log(` http://localhost:${port}/docs/getting-started/installation.md\n`); }); diff --git a/documentation/src/components/ContentCard.tsx b/documentation/src/components/ContentCard.tsx index 62c5c5ac34..7db86a0952 100644 --- a/documentation/src/components/ContentCard.tsx +++ b/documentation/src/components/ContentCard.tsx @@ -245,7 +245,7 @@ export default function ContentCard({ ) : ( Goose logo placeholder )} diff --git a/documentation/src/components/LinuxDesktopInstallButtons.js b/documentation/src/components/LinuxDesktopInstallButtons.js index f0fba117f5..d5cd8217e8 100644 --- a/documentation/src/components/LinuxDesktopInstallButtons.js +++ b/documentation/src/components/LinuxDesktopInstallButtons.js @@ -2,7 +2,7 @@ import Link from "@docusaurus/Link"; import { IconDownload } from "@site/src/components/icons/download"; import { useState, useEffect } from "react"; -const FALLBACK_URL = "https://github.com/block/goose/releases/latest"; +const FALLBACK_URL = "https://github.com/aaif-goose/goose/releases/latest"; const LinuxDesktopInstallButtons = () => { const [downloadUrls, setDownloadUrls] = useState({ @@ -26,7 +26,7 @@ const LinuxDesktopInstallButtons = () => { } // Fetch latest release from GitHub API - const response = await fetch('https://api.github.com/repos/block/goose/releases/latest'); + const response = await fetch('https://api.github.com/repos/aaif-goose/goose/releases/latest'); if (!response.ok) throw new Error('API request failed'); const release = await response.json(); diff --git a/documentation/src/components/MacDesktopInstallButtons.js b/documentation/src/components/MacDesktopInstallButtons.js index 8bdd6d5e4a..82347a7b9e 100644 --- a/documentation/src/components/MacDesktopInstallButtons.js +++ b/documentation/src/components/MacDesktopInstallButtons.js @@ -8,13 +8,13 @@ const DesktopInstallButtons = () => {
macOS Silicon macOS Intel diff --git a/documentation/src/components/PlatformExtensionNote.js b/documentation/src/components/PlatformExtensionNote.js index 4b23effcd6..74e7ddb172 100644 --- a/documentation/src/components/PlatformExtensionNote.js +++ b/documentation/src/components/PlatformExtensionNote.js @@ -4,7 +4,7 @@ import Admonition from '@theme/Admonition'; export const PlatformExtensionNote = ({ defaultEnabled = true }) => { return ( -

This is a built-in platform extension{defaultEnabled && " that's enabled by default for new users"}. Platform extensions provide core functionality and are used within goose just like external MCP server extensions.

+

This is a built-in platform extension{defaultEnabled && " that's enabled by default for new users"}. Platform extensions provide core functionality and are used within goose just like external MCP server extensions.

); }; diff --git a/documentation/src/components/RateLimits.js b/documentation/src/components/RateLimits.js index 6a28b14197..02f8486747 100644 --- a/documentation/src/components/RateLimits.js +++ b/documentation/src/components/RateLimits.js @@ -18,7 +18,7 @@ export const RateLimits = () => {
Some providers also have rate limits on API usage, which can affect your experience. Check out our{" "} - + Handling Rate Limits {" "} guide to learn how to efficiently manage these limits while using goose. diff --git a/documentation/src/components/RecipeFields.js b/documentation/src/components/RecipeFields.js index 89866c96a5..bbc7b7d0dc 100644 --- a/documentation/src/components/RecipeFields.js +++ b/documentation/src/components/RecipeFields.js @@ -12,7 +12,7 @@ const RecipeFields = () => {
  • Parameters to accept dynamic values
  • Model and provider to specify which provider and model the recipe should use
  • Extensions to select which tools are available
  • -
  • Response JSON schema for structured output in automations
  • +
  • Response JSON schema for structured output in automations
  • Subrecipes to attach existing subrecipes or create a new one inline
  • diff --git a/documentation/src/components/SupportedEnvironments.js b/documentation/src/components/SupportedEnvironments.js index 6b6166d07f..fc45078261 100644 --- a/documentation/src/components/SupportedEnvironments.js +++ b/documentation/src/components/SupportedEnvironments.js @@ -7,7 +7,7 @@ const SupportedEnvironments = () => { The goose CLI currently works on macOS and Linux systems and supports both ARM and x86 architectures. On Windows, goose CLI can run via WSL, and goose Desktop is natively supported. If you'd like to request support for additional operating systems, please{" "} diff --git a/documentation/src/components/WindowsDesktopInstallButtons.js b/documentation/src/components/WindowsDesktopInstallButtons.js index 34f26ee381..c705569194 100644 --- a/documentation/src/components/WindowsDesktopInstallButtons.js +++ b/documentation/src/components/WindowsDesktopInstallButtons.js @@ -8,7 +8,7 @@ const WindowsDesktopInstallButtons = () => {
    Windows diff --git a/documentation/src/pages/community/data/community-content.json b/documentation/src/pages/community/data/community-content.json index 541b69a9cf..294e2db9dc 100644 --- a/documentation/src/pages/community/data/community-content.json +++ b/documentation/src/pages/community/data/community-content.json @@ -3,7 +3,7 @@ "description": "Amazing blog posts, videos, and tutorials created by our community for goose!", "lastUpdated": "2025-10-15", "hacktoberfestYear": 2025, - "submissionUrl": "https://github.com/block/goose/issues/4705", + "submissionUrl": "https://github.com/aaif-goose/goose/issues/4705", "submissions": [ { "title": "Hacktoberfest 2025: Contributing to goose", @@ -12,7 +12,7 @@ "handle": "taniacryptid" }, "type": "blog", - "url": "https://block.github.io/goose/blog/2025/09/26/hacktoberfest-2025", + "url": "https://goose-docs.ai/blog/2025/09/26/hacktoberfest-2025", "thumbnail": "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&h=225&fit=crop&crop=entropy&auto=format", "submittedDate": "2025-09-26", "tags": [ diff --git a/documentation/src/pages/community/index.tsx b/documentation/src/pages/community/index.tsx index 3dd389c5a6..0952d2a755 100644 --- a/documentation/src/pages/community/index.tsx +++ b/documentation/src/pages/community/index.tsx @@ -129,10 +129,10 @@ function CommunityAllStarsSection() {
    Want to be a Community All Star? Just start contributing on{' '} - GitHub, helping others on{' '} + GitHub, helping others on{' '} Discord, or share your goose projects with the community! You can check out the{' '} - contributing guide{' '} + contributing guide{' '} for more tips.
    diff --git a/documentation/src/pages/grants.md b/documentation/src/pages/grants.md index 5b4176b7f6..26480361f6 100644 --- a/documentation/src/pages/grants.md +++ b/documentation/src/pages/grants.md @@ -63,9 +63,9 @@ We will support as many strong, mission-aligned projects as our resources allow. Potentially, yes. We may extend or renew grants based on progress, impact, and alignment. Renewals are not guaranteed and will be evaluated on a case-by-case basis. -**Does a grant lead to a job at goose or Block?** +**Does a grant lead to a job at goose or AAIF?** -No. Grants are not employment and do not imply future hiring. However, collaboration with goose or Block teams may emerge naturally from impactful work. +No. Grants are not employment and do not imply future hiring. However, collaboration with goose or AAIF teams may emerge naturally from impactful work. **How does work get reviewed?** @@ -103,12 +103,12 @@ If you have additional questions, feel free to reach out in [Discord](https://di - + - + - + - + \ No newline at end of file diff --git a/documentation/src/pages/index.module.css b/documentation/src/pages/index.module.css index 45d1683d2e..40146012bf 100644 --- a/documentation/src/pages/index.module.css +++ b/documentation/src/pages/index.module.css @@ -1,98 +1,411 @@ /** -* CSS files with the .module.css suffix will be treated as CSS modules -* and scoped locally. -*/ + * Homepage styles + */ -.header { - padding: 4rem 2rem; -} - - -.wrapper { - max-width: 1200px; - margin: 0 auto; - display: flex; - flex-wrap: wrap; - gap: 3rem; - justify-content: center; - align-items: center; - min-height: 400px; -} - -.textColumn { - flex: 1 1 300px; - max-width: 500px; +/* Hero */ +.hero { + padding: 5rem 2rem 3rem; text-align: center; +} + +.heroInner { + max-width: 800px; + margin: 0 auto; display: flex; flex-direction: column; align-items: center; - margin-top: -40px; } -.logo { +.heroBadge { + display: inline-block; + padding: 0.35rem 1rem; + border-radius: 999px; + border: 1px solid var(--border-subtle); + font-size: 0.8rem; + color: var(--text-subtle); + margin-bottom: 2rem; + letter-spacing: 0.02em; +} + +.heroLogo { + max-width: 300px; + margin-bottom: 1.5rem; +} + +.heroSubtitle { + font-size: 1.35rem; + line-height: 1.6; + color: var(--text-standard); + max-width: 600px; + margin-bottom: 2rem; +} + +.heroActions { display: flex; + gap: 1rem; + margin-bottom: 3rem; + flex-wrap: wrap; + justify-content: center; +} + +.secondaryButton { + border-color: var(--border-standard); + color: var(--text-standard); +} + +.heroStats { + display: flex; + gap: 2rem; + align-items: center; + flex-wrap: wrap; + justify-content: center; +} + +.stat { + display: flex; + flex-direction: column; align-items: center; } -.subtitle { +.statNumber { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-prominent); +} + +.statLabel { + font-size: 0.85rem; + color: var(--text-subtle); +} + +.statDivider { + width: 1px; + height: 2.5rem; + background: var(--border-subtle); +} + +/* Sections */ +.section { + padding: 4rem 2rem; +} + +.sectionAlt { + background: var(--background-subtle); +} + +.container { + max-width: 1100px; + margin: 0 auto; +} + +.sectionTitle { + font-size: 1.75rem; + font-weight: 600; + text-align: center; + margin-bottom: 0.75rem; + color: var(--text-prominent); +} + +.sectionSubtitle { + text-align: center; + max-width: 700px; + margin: 0 auto 2.5rem; + color: var(--text-subtle); + font-size: 1.05rem; + line-height: 1.6; +} + +/* Features β€” top row (3 big cards) */ +.featuresGridTop { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + margin-bottom: 2rem; +} + +.featureCard { + padding: 1.75rem; + border-radius: 12px; + border: 1px solid var(--border-subtle); + background: var(--background-app); + transition: border-color 0.2s ease; +} + +.featureCard:hover { + border-color: var(--border-standard); +} + +.featureIcon { + font-size: 1.5rem; + margin-bottom: 0.75rem; +} + +.featureTitle { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-prominent); +} + +.featureDescription { + font-size: 0.9rem; + color: var(--text-subtle); + line-height: 1.6; +} + +.featureDescription p { + margin: 0; +} + +.featureDescription a { + font-weight: 500; +} + +/* Features β€” bottom row (4 smaller cards) */ +.featuresGridBottom { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; +} + +.smallCard { + padding: 1.25rem; + border-radius: 12px; + border: 1px solid var(--border-subtle); + background: var(--background-app); + transition: border-color 0.2s ease; +} + +.smallCard:hover { + border-color: var(--border-standard); +} + +.smallCardIcon { font-size: 1.25rem; + margin-bottom: 0.5rem; +} + +.smallCardTitle { + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.4rem; + color: var(--text-prominent); +} + +.smallCardDescription { + font-size: 0.85rem; + color: var(--text-subtle); + line-height: 1.55; +} + +.smallCardDescription p { + margin: 0; +} + +.smallCardDescription a { + font-weight: 500; +} + +/* Standards Grid */ +.standardsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + margin-top: 2rem; +} + +.standardCard { + padding: 1.5rem; + border-radius: 12px; + border: 1px solid var(--border-subtle); + background: var(--background-app); +} + +.standardCard h3 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.75rem; + color: var(--text-prominent); +} + +.standardCard p { + font-size: 0.9rem; + color: var(--text-subtle); + line-height: 1.6; + margin-bottom: 0.75rem; +} + +.standardCard a { + font-size: 0.9rem; + font-weight: 500; +} + +/* Community Grid */ +.communityGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; +} + +.communityCard { + padding: 1.5rem; + border-radius: 12px; + border: 1px solid var(--border-subtle); + background: var(--background-app); + text-decoration: none; + color: inherit; + transition: border-color 0.2s ease, transform 0.2s ease; +} + +.communityCard:hover { + border-color: var(--border-standard); + transform: translateY(-2px); + text-decoration: none; + color: inherit; +} + +.communityCard h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-prominent); +} + +.communityCard p { + font-size: 0.85rem; + color: var(--text-subtle); + line-height: 1.5; + margin: 0; +} + +/* Install */ +.installBlock { + max-width: 700px; + margin: 0 auto; +} + +.installDesktop { + text-align: center; margin-bottom: 1.5rem; - max-width: 30ch; } -.videoColumn { - flex: 1 1 400px; +.installPlatforms { + margin-top: 0.75rem; + font-size: 0.9rem; + color: var(--text-subtle); +} + +.installDivider { + text-align: center; + margin-bottom: 1.5rem; + font-size: 0.85rem; + color: var(--text-subtle); +} + +.installTerminal { + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--border-subtle); + background: var(--grey-10); +} + +.terminalBar { + display: flex; + gap: 6px; + padding: 12px 16px; + background: var(--grey-20); +} + +.terminalDot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--grey-50); +} + +.terminalBody { + padding: 1.25rem 1.5rem; + margin: 0; + font-size: 0.85rem; + line-height: 1.7; + color: #e0e0e0; + overflow-x: auto; +} + +.installNote { + text-align: center; + margin-top: 1.25rem; + font-size: 0.9rem; + color: var(--text-subtle); +} + +/* Video */ +.videoWrapper { + max-width: 800px; + margin: 0 auto; aspect-ratio: 16 / 9; +} + +.video { width: 100%; - max-width: 640px; + height: 100%; + border: none; + border-radius: 12px; } -.aspect-ratio{ - aspect-ratio: 16 / 9; - width: 100%; -} +/* Responsive */ +@media (max-width: 996px) { + .featuresGridTop { + grid-template-columns: repeat(2, 1fr); + } - + .featuresGridBottom { + grid-template-columns: repeat(2, 1fr); + } -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; + .standardsGrid { + grid-template-columns: repeat(2, 1fr); + } + + .communityGrid { + grid-template-columns: repeat(2, 1fr); } } -@media screen and (max-width: 768px) { - .header { - padding: 1rem 1rem; +@media (max-width: 640px) { + .hero { + padding: 3rem 1.5rem 2rem; } - .wrapper { - flex-direction: column; - text-align: center; - gap: 1rem; - padding: 1rem 1rem; - } - - .textColumn { - text-align: center; - max-width: 100%; - flex: 1 1 400px; - margin-top: 0; - } - - .subtitle { + .heroSubtitle { font-size: 1.1rem; - line-height: 1.5; - margin: 0 auto 1.5rem; } - .ctaButton { - font-size: 1rem; - width: auto; + .heroStats { + gap: 1.5rem; } - .videoColumn { - flex: 1 1 180px; - aspect-ratio: 16 / 9; - width: 100%; + .statDivider { + display: none; + } + + .featuresGridTop { + grid-template-columns: 1fr; + } + + .featuresGridBottom { + grid-template-columns: 1fr; + } + + .standardsGrid { + grid-template-columns: 1fr; + } + + .communityGrid { + grid-template-columns: 1fr; + } + + .section { + padding: 3rem 1.5rem; } } diff --git a/documentation/src/pages/index.tsx b/documentation/src/pages/index.tsx index b49b0d2e19..43cbe745f9 100644 --- a/documentation/src/pages/index.tsx +++ b/documentation/src/pages/index.tsx @@ -2,47 +2,363 @@ import type { ReactNode } from "react"; import Link from "@docusaurus/Link"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; -import HomepageFeatures from "@site/src/components/HomepageFeatures"; import styles from "./index.module.css"; import { GooseLogo } from "../components/GooseLogo"; -function HomepageHeader() { - const { siteConfig } = useDocusaurusContext(); +function HeroSection() { return ( -
    -
    -
    -
    - -
    -

    {siteConfig.tagline}

    - - install goose +
    +
    +
    + Open Source Β· Apache 2.0 Β· Linux Foundation +
    +
    + +
    +

    + Your native open source AI agent. Desktop app, CLI, and API β€” for code, + workflows, and everything in between. +

    +
    + + Install goose + + + Quickstart
    - -
    - +
    +
    + 38k+ + GitHub stars +
    +
    +
    + 400+ + Contributors +
    +
    +
    + 70+ + MCP extensions +
    ); } +type FeatureCardProps = { + title: string; + description: ReactNode; + icon: string; +}; + +function FeatureCard({ title, description, icon }: FeatureCardProps) { + return ( +
    +
    {icon}
    +

    {title}

    +
    {description}
    +
    + ); +} + +type SmallCardProps = { + title: string; + description: ReactNode; + icon: string; +}; + +function SmallCard({ title, description, icon }: SmallCardProps) { + return ( +
    +
    {icon}
    +

    {title}

    +
    {description}
    +
    + ); +} + +function FeaturesSection() { + return ( +
    +
    +

    What goose does

    +

    + 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. +

    + } + /> + + Connect to 70+ extensions β€” databases, APIs, browsers, GitHub, + Google Drive, and more β€” via the{" "} +
    + Model Context Protocol + {" "} + open standard. Add community{" "} + skills, or{" "} + build your own. +

    + } + /> + + Works with 15+ providers β€” Anthropic, OpenAI, Google, Ollama, + OpenRouter, Azure, Bedrock, and more. Use API keys or your + existing Claude, ChatGPT, or Gemini subscriptions via{" "} + ACP. +

    + } + /> +
    +
    + + Capture workflows as portable YAML configs. Share with your + team, run in CI, include instructions, extensions, parameters, + and{" "} + subrecipes. +

    + } + /> + + Extensions can render interactive UIs directly inside goose + Desktop β€” buttons, forms, visualizations. A new way to build{" "} + + agent-powered tools + . +

    + } + /> + + Spawn independent{" "} + subagents to handle + tasks in parallel β€” code review, research, file processing β€” + keeping the main conversation clean. +

    + } + /> + + Prompt injection detection, tool permission controls, sandbox + mode, and an{" "} + + adversary reviewer + {" "} + that watches for unsafe actions. +

    + } + /> +
    +
    +
    + ); +} + +function StandardsSection() { + return ( +
    +
    +

    Built on open standards

    +
    +
    +

    Model Context Protocol

    +

    + MCP{" "} + is the open standard for connecting AI agents to tools and data + sources. goose was one of the earliest adopters and has one of the + deepest integrations in the ecosystem β€” with 70+ documented + extensions and growing. +

    + Browse MCP extensions β†’ +
    +
    +

    Agent Client Protocol

    +

    + ACP{" "} + is a standard for communicating with coding agents. goose works as + an ACP server β€” connect from Zed, JetBrains, or VS Code β€” and can + use ACP agents like Claude Code and Codex as providers. +

    + goose as ACP server β†’ +
    +
    +

    Agentic AI Foundation

    +

    + goose is part of the{" "} + + Agentic AI Foundation + {" "} + at the Linux Foundation β€” ensuring the project remains + vendor-neutral, community-governed, and open for the long term. +

    + + Learn about AAIF β†’ + +
    +
    +
    +
    + ); +} + +function CommunitySection() { + return ( +
    +
    +

    Community

    +

    + An active community of developers, contributors, and users building + extensions, sharing recipes, and pushing the boundaries of what local + AI agents can do. +

    +
    + +

    πŸ’¬ Discord

    +

    + Ask questions, share what you've built, get help from the + community. +

    +
    + +

    πŸ™ GitHub

    +

    + Star, fork, file issues, contribute code. goose is built in the + open. +

    +
    + +

    🧩 Extensions

    +

    Browse community-built MCP extensions and add your own.

    + + +

    πŸ“ Blog

    +

    Tutorials, deep dives, release notes, and community spotlights.

    + +
    +
    +
    + ); +} + +function InstallSection() { + return ( +
    +
    +

    Get started

    +
    +
    + + Download the desktop app + +

    + Available for macOS, Linux, and Windows +

    +
    +
    + or install the CLI +
    +
    +
    + + + +
    +
    +              
    +{`curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash`}
    +              
    +            
    +
    +
    +
    +
    + ); +} + +function VideoSection() { + return ( +
    +
    +

    See goose in action

    +
    +