mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
refactor: kilo compat for v1.14.51
This commit is contained in:
@@ -23,7 +23,7 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
|
||||
bun-download-url: ${{ steps.bun-url.outputs.url }}
|
||||
@@ -33,8 +33,9 @@ runs:
|
||||
shell: bash
|
||||
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
- name: Restore Bun dependencies
|
||||
id: bun-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
@@ -56,3 +57,10 @@ runs:
|
||||
bun install ${{ inputs.install-flags }}
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Save Bun dependencies
|
||||
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
|
||||
@@ -19,7 +19,7 @@ runs:
|
||||
steps:
|
||||
- name: Create app token
|
||||
id: apptoken
|
||||
uses: actions/create-github-app-token@v2
|
||||
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
|
||||
with:
|
||||
app-id: ${{ inputs.opencode-app-id }}
|
||||
private-key: ${{ inputs.opencode-app-secret }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Close inactive PRs
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close non-compliant issues and PRs after 2 hours
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const { data: items } = await github.rest.issues.listForRepo({
|
||||
|
||||
@@ -21,18 +21,18 @@ jobs:
|
||||
REGISTRY: ghcr.io/${{ github.repository_owner }}
|
||||
TAG: "24.04"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
|
||||
- name: Evaluate flake outputs (all systems)
|
||||
run: |
|
||||
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
|
||||
- name: Compute node_modules hash
|
||||
id: hash
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
echo "Computed hash for ${SYSTEM}: $HASH"
|
||||
|
||||
- name: Upload hash
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: hash-${{ matrix.system }}
|
||||
path: hash.txt
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||
|
||||
- name: Download hash artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: hashes
|
||||
pattern: hash-*
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Send nicely-formatted embed to Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1
|
||||
uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Add Contributor Label
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const isPR = !!context.payload.pull_request;
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR standards
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check PR template compliance
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
with:
|
||||
fetch-tags: true
|
||||
|
||||
@@ -95,14 +95,14 @@ jobs:
|
||||
GH_REPO: ${{ needs.version.outputs.repo }}
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: |
|
||||
packages/opencode/dist/opencode-darwin*
|
||||
packages/opencode/dist/opencode-linux*
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist/opencode-windows*
|
||||
@@ -123,9 +123,9 @@ jobs:
|
||||
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
@@ -138,13 +138,13 @@ jobs:
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- uses: azure/artifact-signing-action@v1
|
||||
- uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
with:
|
||||
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
--clobber `
|
||||
--repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-cli-signed-windows
|
||||
path: |
|
||||
@@ -244,14 +244,14 @@ jobs:
|
||||
- host: "blacksmith-4vcpu-ubuntu-2404"
|
||||
target: x86_64-unknown-linux-gnu
|
||||
platform_flag: --linux
|
||||
- host: "blacksmith-4vcpu-ubuntu-2404"
|
||||
- host: "blacksmith-4vcpu-ubuntu-2404-arm"
|
||||
target: aarch64-unknown-linux-gnu
|
||||
platform_flag: --linux
|
||||
platform_flag: --linux --arm64
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
keychain: build
|
||||
@@ -268,19 +268,19 @@ jobs:
|
||||
|
||||
- name: Azure login
|
||||
if: runner.os == 'Windows'
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
client-id: ${{ env.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ env.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Cache apt packages
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/apt-cache
|
||||
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||
@@ -388,12 +388,12 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-desktop-${{ matrix.settings.target }}
|
||||
path: packages/desktop/dist/*
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: needs.version.outputs.release
|
||||
with:
|
||||
name: latest-yml-${{ matrix.settings.target }}
|
||||
@@ -408,44 +408,44 @@ jobs:
|
||||
if: always() && !failure() && !cancelled()
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-cli
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-cli-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-cli-signed-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.version.outputs.release
|
||||
with:
|
||||
pattern: latest-yml-*
|
||||
@@ -459,7 +459,7 @@ jobs:
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Cache apt packages (AUR)
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
name: Release Zed Extension
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: node_modules/.cache/turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Publish unit reports
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0
|
||||
with:
|
||||
report_paths: packages/*/.artifacts/unit/junit.xml
|
||||
check_name: "unit results (${{ matrix.settings.name }})"
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Upload unit artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
include-hidden-files: true
|
||||
@@ -111,12 +111,12 @@ jobs:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/.playwright-browsers
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
if-no-files-found: ignore
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v1.14.48
|
||||
v1.14.51
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {},
|
||||
"permission": {
|
||||
"edit": {
|
||||
"packages/opencode/migration/*": "ask",
|
||||
},
|
||||
},
|
||||
"permission": {},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Deepening
|
||||
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
|
||||
## Dependency categories
|
||||
|
||||
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
|
||||
|
||||
### 1. In-process
|
||||
|
||||
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
|
||||
|
||||
### 2. Local-substitutable
|
||||
|
||||
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
|
||||
|
||||
### 3. Remote but owned (Ports & Adapters)
|
||||
|
||||
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
|
||||
|
||||
Recommendation shape: _"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."_
|
||||
|
||||
### 4. True external (Mock)
|
||||
|
||||
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
|
||||
|
||||
## Seam discipline
|
||||
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
|
||||
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
|
||||
|
||||
## Testing strategy: replace, don't layer
|
||||
|
||||
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them.
|
||||
- Write new tests at the deepened module's interface. The **interface is the test surface**.
|
||||
- Tests assert on observable outcomes through the interface, not internal state.
|
||||
- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Interface Design
|
||||
|
||||
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
||||
|
||||
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Frame the problem space
|
||||
|
||||
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
||||
|
||||
- The constraints any new interface would need to satisfy
|
||||
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
|
||||
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
|
||||
|
||||
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
||||
|
||||
### 2. Spawn sub-agents
|
||||
|
||||
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
|
||||
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
|
||||
- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point."
|
||||
- Agent 2: "Maximise flexibility — support many use cases and extension."
|
||||
- Agent 3: "Optimise for the most common caller — make the default case trivial."
|
||||
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
||||
|
||||
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
|
||||
Each sub-agent outputs:
|
||||
|
||||
1. Interface (types, methods, params — plus invariants, ordering, error modes)
|
||||
2. Usage example showing how callers use it
|
||||
3. What the implementation hides behind the seam
|
||||
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
|
||||
5. Trade-offs — where leverage is high, where it's thin
|
||||
|
||||
### 3. Present and compare
|
||||
|
||||
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
|
||||
|
||||
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Language
|
||||
|
||||
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
|
||||
|
||||
## Terms
|
||||
|
||||
**Module**
|
||||
Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice.
|
||||
_Avoid_: unit, component, service.
|
||||
|
||||
**Interface**
|
||||
Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics.
|
||||
_Avoid_: API, signature (too narrow — those refer only to the type-level surface).
|
||||
|
||||
**Implementation**
|
||||
What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
|
||||
|
||||
**Depth**
|
||||
Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
|
||||
|
||||
**Seam** _(from Michael Feathers)_
|
||||
A place where you can alter behaviour without editing in that place. The _location_ at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it.
|
||||
_Avoid_: boundary (overloaded with DDD's bounded context).
|
||||
|
||||
**Adapter**
|
||||
A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside).
|
||||
|
||||
**Leverage**
|
||||
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
|
||||
|
||||
**Locality**
|
||||
What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
|
||||
- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
|
||||
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape.
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
|
||||
- **Depth** is a property of a **Module**, measured against its **Interface**.
|
||||
- A **Seam** is where a **Module**'s **Interface** lives.
|
||||
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
|
||||
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
|
||||
|
||||
## Rejected framings
|
||||
|
||||
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
|
||||
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.
|
||||
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: improve-codebase-architecture
|
||||
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
|
||||
---
|
||||
|
||||
# Improve Codebase Architecture
|
||||
|
||||
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
|
||||
|
||||
## Glossary
|
||||
|
||||
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md).
|
||||
|
||||
- **Module** — anything with an interface and an implementation (function, class, package, slice).
|
||||
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
|
||||
- **Implementation** — the code inside.
|
||||
- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation.
|
||||
- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.")
|
||||
- **Adapter** — a concrete thing satisfying an interface at a seam.
|
||||
- **Leverage** — what callers get from depth.
|
||||
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
|
||||
|
||||
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list):
|
||||
|
||||
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
|
||||
- **The interface is the test surface.**
|
||||
- **One adapter = hypothetical seam. Two adapters = real seam.**
|
||||
|
||||
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Explore
|
||||
|
||||
Read the project's domain glossary and any ADRs in the area you're touching first.
|
||||
|
||||
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
|
||||
|
||||
- Where does understanding one concept require bouncing between many small modules?
|
||||
- Where are modules **shallow** — interface nearly as complex as the implementation?
|
||||
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
|
||||
- Where do tightly-coupled modules leak across their seams?
|
||||
- Which parts of the codebase are untested, or hard to test through their current interface?
|
||||
|
||||
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
|
||||
|
||||
### 2. Present candidates
|
||||
|
||||
Present a numbered list of deepening opportunities. For each candidate:
|
||||
|
||||
- **Files** — which files/modules are involved
|
||||
- **Problem** — why the current architecture is causing friction
|
||||
- **Solution** — plain English description of what would change
|
||||
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
|
||||
|
||||
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
|
||||
|
||||
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
|
||||
|
||||
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
|
||||
|
||||
### 3. Grilling loop
|
||||
|
||||
Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
|
||||
|
||||
Side effects happen inline as decisions crystallize:
|
||||
|
||||
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist.
|
||||
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
|
||||
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md).
|
||||
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md).
|
||||
@@ -73,6 +73,29 @@ function foo() {
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Logic
|
||||
|
||||
When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
|
||||
|
||||
```ts
|
||||
// Good
|
||||
export function loadThing(input: unknown) {
|
||||
const config = requireConfig(input)
|
||||
const metadata = readMetadata(input)
|
||||
return createThing({ config, metadata })
|
||||
}
|
||||
|
||||
function requireConfig(input: unknown) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- Keep helpers close to the code they support, below the main export when that improves readability.
|
||||
- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
|
||||
- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
|
||||
- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
|
||||
- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
|
||||
|
||||
### Schema Definitions (Drizzle)
|
||||
|
||||
Use snake_case for field names so column names don't need to be redefined as strings.
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
[install]
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-baGxh+hk/rPhg0xI/OdMDz6dPwncgercYNBdTPnLX9o=",
|
||||
"aarch64-linux": "sha256-VTWKq679B3Q4ZnAoQzC4VSCYA09wWecNJ+JajvjNB1U=",
|
||||
"aarch64-darwin": "sha256-orf2zIBMTiiQrt/6qCzE+o0oKhv6u8zXF9DH1Bo3lbo=",
|
||||
"x86_64-darwin": "sha256-1MZC1fadRoY4lhkmjlcUQTLYH9Q8pDI1bxd5f94f1xU="
|
||||
"x86_64-linux": "sha256-Hw7sVV9rTm6qBMtdwfLIV2QvxvLQY5qrywXzuyYbhcs=",
|
||||
"aarch64-linux": "sha256-++oXnY7YqrYt0Qv7ZISmoHliARM9qEP8FacqLxGZH1c=",
|
||||
"aarch64-darwin": "sha256-kZVa0R1YbuvtTzpETqK6ddj4ISje5jBFHBdlynkhW7Q=",
|
||||
"x86_64-darwin": "sha256-94eagNDa8GGJxF8BsMX2BF5Pa+QTl48lXL1+6HgEn0I="
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -25,15 +25,15 @@
|
||||
"packages/sdk/js"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.57",
|
||||
"@effect/platform-node": "4.0.0-beta.57",
|
||||
"@effect/opentelemetry": "4.0.0-beta.65",
|
||||
"@effect/platform-node": "4.0.0-beta.65",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.14",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.6",
|
||||
"@opentui/solid": "0.2.6",
|
||||
"@opentui/core": "0.2.10",
|
||||
"@opentui/solid": "0.2.10",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -51,7 +51,7 @@
|
||||
"dompurify": "3.4.2",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.59",
|
||||
"effect": "4.0.0-beta.65",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.12.12",
|
||||
@@ -78,7 +78,7 @@
|
||||
"solid-js": "1.9.12",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@opentui/keymap": "0.2.6"
|
||||
"@opentui/keymap": "0.2.10"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -143,7 +143,8 @@
|
||||
"happy-dom": ">=20.8.9",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"solid-js": "catalog:"
|
||||
"solid-js": "catalog:",
|
||||
"@opentui/keymap": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
|
||||
@@ -43,7 +43,34 @@
|
||||
"rotating-file-stream": "3.2.9",
|
||||
"semver": "^7.6.3",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"zod": "catalog:"
|
||||
"zod": "catalog:",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
"@ai-sdk/openai-compatible": "2.0.41",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.82",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@openrouter/ai-sdk-provider": "2.8.1",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"gitlab-ai-provider": "6.6.0",
|
||||
"google-auth-library": "10.5.0",
|
||||
"immer": "11.1.4",
|
||||
"venice-ai-sdk-provider": "2.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"drizzle-orm": "catalog:"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
export * as AISDK from "./aisdk"
|
||||
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Cause, Context, Effect, Layer, Schema } from "effect"
|
||||
import { ModelV2 } from "./model"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
|
||||
type SDK = any
|
||||
|
||||
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||
if (typeof ms !== "number" || ms <= 0) return res
|
||||
if (!res.body) return res
|
||||
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
async pull(ctrl) {
|
||||
const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
const err = new Error("SSE read timed out")
|
||||
ctl.abort(err)
|
||||
void reader.cancel(err)
|
||||
reject(err)
|
||||
}, ms)
|
||||
|
||||
reader.read().then(
|
||||
(part) => {
|
||||
clearTimeout(id)
|
||||
resolve(part)
|
||||
},
|
||||
(err) => {
|
||||
clearTimeout(id)
|
||||
reject(err)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
if (part.done) {
|
||||
ctrl.close()
|
||||
return
|
||||
}
|
||||
|
||||
ctrl.enqueue(part.value)
|
||||
},
|
||||
async cancel(reason) {
|
||||
ctl.abort(reason)
|
||||
await reader.cancel(reason)
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(body, {
|
||||
headers: new Headers(res.headers),
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
})
|
||||
}
|
||||
|
||||
function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||
const options: Record<string, any> = { name: model.providerID, ...model.options.aisdk.provider }
|
||||
if (model.endpoint.type === "aisdk" && model.endpoint.url) options.baseURL = model.endpoint.url
|
||||
|
||||
const customFetch = options.fetch
|
||||
const chunkTimeout = options.chunkTimeout
|
||||
delete options.chunkTimeout
|
||||
options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
const opts = { ...(init ?? {}) }
|
||||
const signals = [
|
||||
opts.signal,
|
||||
typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined,
|
||||
options.timeout !== undefined && options.timeout !== null && options.timeout !== false
|
||||
? AbortSignal.timeout(options.timeout)
|
||||
: undefined,
|
||||
].filter((item): item is AbortSignal | AbortController => Boolean(item))
|
||||
const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController)
|
||||
const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item))
|
||||
if (abortSignals.length === 1) opts.signal = abortSignals[0]
|
||||
if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
|
||||
|
||||
if ((pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure") && opts.body && opts.method === "POST") {
|
||||
const body = JSON.parse(opts.body as string)
|
||||
if (body.store !== true && Array.isArray(body.input)) {
|
||||
for (const item of body.input) {
|
||||
if ("id" in item) delete item.id
|
||||
}
|
||||
opts.body = JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
|
||||
...opts,
|
||||
timeout: false,
|
||||
})
|
||||
if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
|
||||
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
|
||||
providerID: ProviderV2.ID,
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
function initError(providerID: ProviderV2.ID) {
|
||||
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const languages = new Map<string, LanguageModelV3>()
|
||||
const sdks = new Map<string, SDK>()
|
||||
|
||||
return Service.of({
|
||||
language: Effect.fn("AISDK.language")(function* (model) {
|
||||
const key = `${model.providerID}/${model.id}/${model.options.variant ?? "default"}`
|
||||
const existing = languages.get(key)
|
||||
if (existing) return existing
|
||||
if (model.endpoint.type !== "aisdk")
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error(`Unsupported endpoint ${model.endpoint.type}`),
|
||||
})
|
||||
|
||||
const options = prepareOptions(model, model.endpoint.package)
|
||||
const sdkKey = JSON.stringify({
|
||||
providerID: model.providerID,
|
||||
endpoint: model.endpoint,
|
||||
options,
|
||||
})
|
||||
const sdk =
|
||||
sdks.get(sdkKey) ??
|
||||
(yield* plugin
|
||||
.trigger("aisdk.sdk", { model, package: model.endpoint.package, options }, {})
|
||||
.pipe(initError(model.providerID))).sdk
|
||||
if (!sdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error("No AISDK provider plugin returned an SDK"),
|
||||
})
|
||||
sdks.set(sdkKey, sdk)
|
||||
const result = yield* plugin
|
||||
.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model,
|
||||
sdk,
|
||||
options,
|
||||
},
|
||||
{},
|
||||
)
|
||||
.pipe(initError(model.providerID))
|
||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.apiID)).pipe(
|
||||
initError(model.providerID),
|
||||
)
|
||||
languages.set(key, language)
|
||||
return language
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from "path"
|
||||
import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect"
|
||||
import { Identifier } from "@opencode-ai/core/util/identifier"
|
||||
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import { NonNegativeInt, withStatics } from "./schema"
|
||||
import { Global } from "./global"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
|
||||
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
|
||||
|
||||
@@ -106,27 +106,45 @@ export const layer = Layer.effect(
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const file = path.join(global.data, "auth-v2.json")
|
||||
const legacyFile = path.join(global.data, "auth.json")
|
||||
|
||||
const load: () => Effect.Effect<Writable, AuthError> = Effect.fnUntraced(function* () {
|
||||
if (process.env.KILO_AUTH_CONTENT) {
|
||||
try {
|
||||
return JSON.parse(process.env.KILO_AUTH_CONTENT)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
|
||||
|
||||
if (!raw || typeof raw !== "object") return { version: 2, accounts: {}, active: {} }
|
||||
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
|
||||
const migrated = migrate(raw as Record<string, unknown>)
|
||||
const writeMigrated = Effect.fnUntraced(function* (raw: Record<string, unknown>) {
|
||||
const migrated = migrate(raw)
|
||||
yield* fsys
|
||||
.writeJson(file, migrated, 0o600)
|
||||
.pipe(Effect.mapError((cause) => new AuthFileWriteError({ operation: "migrate", cause })))
|
||||
return migrated
|
||||
})
|
||||
|
||||
const parseAuthContent = () => {
|
||||
try {
|
||||
return JSON.parse(process.env.KILO_AUTH_CONTENT ?? "")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const load: () => Effect.Effect<Writable, AuthError> = Effect.fnUntraced(function* () {
|
||||
if (process.env.KILO_AUTH_CONTENT) {
|
||||
const raw = parseAuthContent()
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
}
|
||||
|
||||
const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null))
|
||||
if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record<string, unknown>)
|
||||
|
||||
const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null))
|
||||
|
||||
if (raw && typeof raw === "object") {
|
||||
if ("version" in raw && raw.version === 2) return raw as Writable
|
||||
return yield* writeMigrated(raw as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return { version: 2, accounts: {}, active: {} }
|
||||
})
|
||||
|
||||
const write = (data: Writable) =>
|
||||
fsys
|
||||
.writeJson(file, data, 0o600)
|
||||
@@ -0,0 +1,260 @@
|
||||
export * as Catalog from "./catalog"
|
||||
|
||||
import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array } from "effect"
|
||||
import { produce, type Draft } from "immer"
|
||||
import { ModelV2 } from "./model"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { Instance } from "./instance"
|
||||
|
||||
type ProviderRecord = {
|
||||
provider: ProviderV2.Info
|
||||
models: HashMap.HashMap<ModelV2.ID, ModelV2.Info>
|
||||
}
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||
"CatalogV2.ProviderNotFound",
|
||||
{
|
||||
providerID: ProviderV2.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("CatalogV2.ModelNotFound", {
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly provider: {
|
||||
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
|
||||
readonly update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => Effect.Effect<void>
|
||||
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ProviderV2.Info[]>
|
||||
}
|
||||
readonly model: {
|
||||
readonly get: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
|
||||
readonly update: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
fn: (model: Draft<ModelV2.Info>) => void,
|
||||
) => Effect.Effect<void, ProviderNotFoundError>
|
||||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
readonly setDefault: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<void, ProviderNotFoundError | ModelNotFoundError>
|
||||
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
yield* Instance.Service
|
||||
let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
|
||||
let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
|
||||
const plugin = yield* PluginV2.Service
|
||||
|
||||
const resolve = (model: ModelV2.Info) => {
|
||||
const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider
|
||||
const endpoint =
|
||||
model.endpoint.type === "unknown"
|
||||
? provider.endpoint
|
||||
: model.endpoint.type === "aisdk" && provider.endpoint.type === "aisdk" && !model.endpoint.url
|
||||
? { ...model.endpoint, url: provider.endpoint.url }
|
||||
: model.endpoint
|
||||
const options = {
|
||||
headers: {
|
||||
...provider.options.headers,
|
||||
...model.options.headers,
|
||||
},
|
||||
body: {
|
||||
...provider.options.body,
|
||||
...model.options.body,
|
||||
},
|
||||
aisdk: {
|
||||
provider: {
|
||||
...provider.options.aisdk.provider,
|
||||
...model.options.aisdk.provider,
|
||||
},
|
||||
request: model.options.aisdk.request,
|
||||
},
|
||||
variant: model.options.variant,
|
||||
}
|
||||
return new ModelV2.Info({
|
||||
...model,
|
||||
endpoint,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
function* getRecord(providerID: ProviderV2.ID) {
|
||||
const match = HashMap.get(records, providerID)
|
||||
if (!match.valueOrUndefined) return yield* new ProviderNotFoundError({ providerID })
|
||||
return match.value
|
||||
}
|
||||
|
||||
const result: Interface = {
|
||||
provider: {
|
||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||
const record = yield* getRecord(providerID)
|
||||
return record.provider
|
||||
}),
|
||||
|
||||
update: Effect.fnUntraced(function* (providerID, fn) {
|
||||
const current = Option.getOrUndefined(HashMap.get(records, providerID))
|
||||
const provider = produce(current?.provider ?? ProviderV2.Info.empty(providerID), (draft) => {
|
||||
fn(draft)
|
||||
if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") {
|
||||
draft.endpoint.url = draft.options.aisdk.provider.baseURL
|
||||
delete draft.options.aisdk.provider.baseURL
|
||||
}
|
||||
})
|
||||
const updated = yield* plugin.trigger("provider.update", {}, { provider, cancel: false })
|
||||
records = HashMap.set(records, providerID, {
|
||||
provider: updated.provider,
|
||||
models: current?.models ?? HashMap.empty<ModelV2.ID, ModelV2.Info>(),
|
||||
})
|
||||
}),
|
||||
|
||||
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records)).map((record) => record.provider)
|
||||
}),
|
||||
|
||||
available: Effect.fn("CatalogV2.provider.available")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records))
|
||||
.map((record) => record.provider)
|
||||
.filter((provider) => provider.enabled)
|
||||
}),
|
||||
},
|
||||
|
||||
model: {
|
||||
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
|
||||
const record = yield* getRecord(providerID)
|
||||
const model = Option.getOrUndefined(HashMap.get(record.models, modelID))
|
||||
if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
|
||||
return resolve(model)
|
||||
}),
|
||||
|
||||
update: Effect.fnUntraced(function* (providerID, modelID, fn) {
|
||||
const record = yield* getRecord(providerID)
|
||||
const model = produce(
|
||||
HashMap.get(record.models, modelID).pipe(Option.getOrElse(() => ModelV2.Info.empty(providerID, modelID))),
|
||||
(draft) => {
|
||||
fn(draft)
|
||||
if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") {
|
||||
draft.endpoint.url = draft.options.aisdk.provider.baseURL
|
||||
delete draft.options.aisdk.provider.baseURL
|
||||
}
|
||||
},
|
||||
)
|
||||
const updated = yield* plugin.trigger("model.update", {}, { model, cancel: false })
|
||||
if (updated.cancel) return
|
||||
records = HashMap.set(records, providerID, {
|
||||
provider: record.provider,
|
||||
models: HashMap.set(
|
||||
record.models,
|
||||
modelID,
|
||||
new ModelV2.Info({ ...updated.model, id: modelID, providerID }),
|
||||
),
|
||||
})
|
||||
return
|
||||
}),
|
||||
|
||||
all: Effect.fn("CatalogV2.model.all")(function* () {
|
||||
return pipe(
|
||||
records,
|
||||
HashMap.toValues,
|
||||
Array.flatMap((record) => HashMap.toValues(record.models)),
|
||||
Array.map(resolve),
|
||||
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||
)
|
||||
}),
|
||||
|
||||
available: Effect.fn("CatalogV2.model.available")(function* () {
|
||||
return (yield* result.model.all()).filter((model) => {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, model.providerID))
|
||||
return record?.provider.enabled !== false && model.enabled
|
||||
})
|
||||
}),
|
||||
|
||||
default: Effect.fn("CatalogV2.model.default")(function* () {
|
||||
if (defaultModel) {
|
||||
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
||||
if (Option.isSome(model) && model.value.enabled) return model
|
||||
}
|
||||
|
||||
return pipe(
|
||||
yield* result.model.available(),
|
||||
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||
Array.head,
|
||||
)
|
||||
}),
|
||||
|
||||
setDefault: Effect.fn("CatalogV2.model.setDefault")(function* (providerID, modelID) {
|
||||
yield* result.model.get(providerID, modelID)
|
||||
defaultModel = { providerID, modelID }
|
||||
}),
|
||||
|
||||
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, providerID))
|
||||
if (!record) return Option.none<ModelV2.Info>()
|
||||
|
||||
if (providerID === ProviderV2.ID.opencode) {
|
||||
const gpt5Nano = Option.getOrUndefined(HashMap.get(record.models, ModelV2.ID.make("gpt-5-nano")))
|
||||
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano))
|
||||
}
|
||||
|
||||
const candidates = pipe(
|
||||
HashMap.toValues(record.models),
|
||||
Array.filter(
|
||||
(model) =>
|
||||
model.providerID === providerID &&
|
||||
model.enabled &&
|
||||
model.status === "active" &&
|
||||
model.capabilities.input.some((item) => item.startsWith("text")) &&
|
||||
model.capabilities.output.some((item) => item.startsWith("text")),
|
||||
),
|
||||
Array.map((model) => ({
|
||||
model,
|
||||
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
|
||||
age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30),
|
||||
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
|
||||
})),
|
||||
Array.filter((item) => item.cost > 0 && item.age <= 18),
|
||||
)
|
||||
|
||||
const pick = (items: typeof candidates) => {
|
||||
const maxCost = Math.max(...items.map((item) => item.cost), 0.01)
|
||||
const maxAge = Math.max(...items.map((item) => item.age), 0.01)
|
||||
return pipe(
|
||||
items,
|
||||
Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number),
|
||||
Array.map((item) => resolve(item.model)),
|
||||
Array.head,
|
||||
)
|
||||
}
|
||||
|
||||
return pipe(
|
||||
candidates,
|
||||
Array.filter((item) => item.small),
|
||||
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
|
||||
)
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
}),
|
||||
)
|
||||
|
||||
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
|
||||
@@ -1,370 +0,0 @@
|
||||
import { Effect, Option, Schema, SchemaAST } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
/**
|
||||
* Annotation key for providing a hand-crafted Zod schema that the walker
|
||||
* should use instead of re-deriving from the AST. Attach it via
|
||||
* `Schema.String.annotate({ [ZodOverride]: z.string().startsWith("per") })`.
|
||||
*/
|
||||
export const ZodOverride: unique symbol = Symbol.for("effect-zod/override")
|
||||
|
||||
// AST nodes are immutable and frequently shared across schemas (e.g. a single
|
||||
// Schema.Class embedded in multiple parents). Memoizing by node identity
|
||||
// avoids rebuilding equivalent Zod subtrees and keeps derived children stable
|
||||
// by reference across callers.
|
||||
const walkCache = new WeakMap<SchemaAST.AST, z.ZodTypeAny>()
|
||||
|
||||
// Shared empty ParseOptions for the rare callers that need one — avoids
|
||||
// allocating a fresh object per parse inside refinements and transforms.
|
||||
const EMPTY_PARSE_OPTIONS = {} as SchemaAST.ParseOptions
|
||||
|
||||
export function zod<S extends Schema.Top>(schema: S): z.ZodType<Schema.Schema.Type<S>> {
|
||||
return walk(schema.ast) as z.ZodType<Schema.Schema.Type<S>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a Zod value from an Effect Schema (or a Schema-backed export with a
|
||||
* `.zod` static) and narrow the result to `z.ZodObject<any>` so `.shape`,
|
||||
* `.omit`, `.extend`, and friends are accessible.
|
||||
*
|
||||
* The `zod()` walker returns `z.ZodType<T>` because not every AST node decodes
|
||||
* to an object; this helper keeps the "I started from a `Schema.Struct`" cast
|
||||
* in one place instead of sprinkling `as unknown as z.ZodObject<any>` across
|
||||
* call sites.
|
||||
*
|
||||
* The return is intentionally loose — carrying Schema field types through the
|
||||
* mapped `.omit()` / `.extend()` surface triggers brand-intersection
|
||||
* explosions for branded primitives (`string & Brand<"SessionID">` extends
|
||||
* `object` via the brand and gets walked into the prototype by `DeepPartial`,
|
||||
* `updateSchema`, etc.), and zod's inference through `z.ZodType<T | undefined>`
|
||||
* wrappers also can't reconstruct `T` cleanly. Consumers that care about the
|
||||
* post-`.omit()` shape should cast `c.req.valid(...)` to the expected type.
|
||||
*/
|
||||
export function zodObject<S extends Schema.Top>(schema: S): z.ZodObject<any> {
|
||||
const derived: z.ZodTypeAny = "zod" in schema && isZodType(schema.zod) ? schema.zod : walk(schema.ast)
|
||||
return derived as unknown as z.ZodObject<any>
|
||||
}
|
||||
|
||||
function isZodType(value: unknown): value is z.ZodTypeAny {
|
||||
return typeof value === "object" && value !== null && "_zod" in value
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a JSON Schema for a tool/route parameter schema — derives the zod form
|
||||
* via the walker so Effect Schema inputs flow through the same zod-openapi
|
||||
* pipeline the LLM/SDK layer already depends on. `io: "input"` mirrors what
|
||||
* `session/prompt.ts` has always passed to `ai`'s `jsonSchema()` helper.
|
||||
*/
|
||||
export function toJsonSchema<S extends Schema.Top>(schema: S) {
|
||||
return z.toJSONSchema(zod(schema), { io: "input" })
|
||||
}
|
||||
|
||||
function walk(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const cached = walkCache.get(ast)
|
||||
if (cached) return cached
|
||||
const result = walkUncached(ast)
|
||||
walkCache.set(ast, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined
|
||||
// `description` annotations layer on top of an override so callers can
|
||||
// reuse a shared override schema (e.g. `SessionID`) and still add a
|
||||
// per-field description on the outer wrapper.
|
||||
const base = override ?? bodyWithChecks(ast)
|
||||
const desc = SchemaAST.resolveDescription(ast)
|
||||
const ref = SchemaAST.resolveIdentifier(ast)
|
||||
const described = desc ? base.describe(desc) : base
|
||||
return ref ? described.meta({ ref }) : described
|
||||
}
|
||||
|
||||
function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
// Schema.Class wraps its fields in a Declaration AST plus an encoding that
|
||||
// constructs the class instance. For the Zod derivation we want the plain
|
||||
// field shape (the decoded/consumer view), not the class instance — so
|
||||
// Declarations fall through to body(), not encoded(). User-level
|
||||
// Schema.decodeTo / Schema.transform attach encoding to non-Declaration
|
||||
// nodes, where we do apply the transform.
|
||||
//
|
||||
// Schema.withDecodingDefault also attaches encoding, but we want `.default(v)`
|
||||
// on the inner Zod rather than a transform wrapper — so optional ASTs whose
|
||||
// encoding resolves a default from Option.none() route through body()/opt().
|
||||
const hasEncoding = ast.encoding?.length && (ast._tag !== "Declaration" || ast.typeParameters.length === 0)
|
||||
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
||||
const base = hasTransform ? encoded(ast) : body(ast)
|
||||
return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
}
|
||||
|
||||
// Walk the encoded side and apply each link's decode to produce the decoded
|
||||
// shape. A node `Target` produced by `from.decodeTo(Target)` carries
|
||||
// `Target.encoding = [Link(from, transformation)]`. Chained decodeTo calls
|
||||
// nest the encoding via `Link.to` so walking it recursively threads all
|
||||
// prior transforms — typical encoding.length is 1.
|
||||
function encoded(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const encoding = ast.encoding!
|
||||
return encoding.reduce<z.ZodTypeAny>(
|
||||
(acc, link) => acc.transform((v) => decode(link.transformation, v)),
|
||||
walk(encoding[0].to),
|
||||
)
|
||||
}
|
||||
|
||||
// Transformations built via pure `SchemaGetter.transform(fn)` (the common
|
||||
// decodeTo case) resolve synchronously, so running with no services is safe.
|
||||
// Effectful / middleware-based transforms will surface as Effect defects.
|
||||
function decode(transformation: SchemaAST.Link["transformation"], value: unknown): unknown {
|
||||
const exit = Effect.runSyncExit(
|
||||
(transformation.decode as any).run(Option.some(value), EMPTY_PARSE_OPTIONS) as Effect.Effect<
|
||||
Option.Option<unknown>
|
||||
>,
|
||||
)
|
||||
if (exit._tag === "Failure") throw new Error(`effect-zod: transform failed: ${String(exit.cause)}`)
|
||||
return Option.getOrElse(exit.value, () => value)
|
||||
}
|
||||
|
||||
// Flatten FilterGroups and any nested variants into a linear list of Filters.
|
||||
// Well-known filters (Schema.isInt, isGreaterThan, isPattern, …) are
|
||||
// translated into native Zod methods so their JSON Schema output includes
|
||||
// the corresponding constraint (type: integer, exclusiveMinimum, pattern, …).
|
||||
// Anything else falls back to a single .superRefine layer — runtime-only,
|
||||
// emits no JSON Schema constraint.
|
||||
function applyChecks(out: z.ZodTypeAny, checks: SchemaAST.Checks, ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const filters: SchemaAST.Filter<unknown>[] = []
|
||||
const collect = (c: SchemaAST.Check<unknown>) => {
|
||||
if (c._tag === "FilterGroup") c.checks.forEach(collect)
|
||||
else filters.push(c)
|
||||
}
|
||||
checks.forEach(collect)
|
||||
|
||||
const unhandled: SchemaAST.Filter<unknown>[] = []
|
||||
const translated = filters.reduce<z.ZodTypeAny>((acc, filter) => {
|
||||
const next = translateFilter(acc, filter)
|
||||
if (next) return next
|
||||
unhandled.push(filter)
|
||||
return acc
|
||||
}, out)
|
||||
|
||||
if (unhandled.length === 0) return translated
|
||||
|
||||
return translated.superRefine((value, ctx) => {
|
||||
for (const filter of unhandled) {
|
||||
const issue = filter.run(value, ast, EMPTY_PARSE_OPTIONS)
|
||||
if (!issue) continue
|
||||
const message = issueMessage(issue) ?? (filter.annotations as any)?.message ?? "Validation failed"
|
||||
ctx.addIssue({ code: "custom", message })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Translate a well-known Effect Schema filter into a native Zod method call on
|
||||
// `out`. Dispatch is keyed on `filter.annotations.meta._tag`, which every
|
||||
// built-in check factory (isInt, isGreaterThan, isPattern, …) attaches at
|
||||
// construction time. Returns `undefined` for unrecognised filters so the
|
||||
// caller can fall back to the generic .superRefine path.
|
||||
function translateFilter(out: z.ZodTypeAny, filter: SchemaAST.Filter<unknown>): z.ZodTypeAny | undefined {
|
||||
const meta = (filter.annotations as { meta?: Record<string, unknown> } | undefined)?.meta
|
||||
if (!meta || typeof meta._tag !== "string") return undefined
|
||||
switch (meta._tag) {
|
||||
case "isInt":
|
||||
return call(out, "int")
|
||||
case "isFinite":
|
||||
return call(out, "finite")
|
||||
case "isGreaterThan":
|
||||
return call(out, "gt", meta.exclusiveMinimum)
|
||||
case "isGreaterThanOrEqualTo":
|
||||
return call(out, "gte", meta.minimum)
|
||||
case "isLessThan":
|
||||
return call(out, "lt", meta.exclusiveMaximum)
|
||||
case "isLessThanOrEqualTo":
|
||||
return call(out, "lte", meta.maximum)
|
||||
case "isBetween": {
|
||||
const lo = meta.exclusiveMinimum ? call(out, "gt", meta.minimum) : call(out, "gte", meta.minimum)
|
||||
if (!lo) return undefined
|
||||
return meta.exclusiveMaximum ? call(lo, "lt", meta.maximum) : call(lo, "lte", meta.maximum)
|
||||
}
|
||||
case "isMultipleOf":
|
||||
return call(out, "multipleOf", meta.divisor)
|
||||
case "isMinLength":
|
||||
return call(out, "min", meta.minLength)
|
||||
case "isMaxLength":
|
||||
return call(out, "max", meta.maxLength)
|
||||
case "isLengthBetween": {
|
||||
const lo = call(out, "min", meta.minimum)
|
||||
if (!lo) return undefined
|
||||
return call(lo, "max", meta.maximum)
|
||||
}
|
||||
case "isPattern":
|
||||
return call(out, "regex", meta.regExp)
|
||||
case "isStartsWith":
|
||||
return call(out, "startsWith", meta.startsWith)
|
||||
case "isEndsWith":
|
||||
return call(out, "endsWith", meta.endsWith)
|
||||
case "isIncludes":
|
||||
return call(out, "includes", meta.includes)
|
||||
case "isUUID":
|
||||
return call(out, "uuid")
|
||||
case "isULID":
|
||||
return call(out, "ulid")
|
||||
case "isBase64":
|
||||
return call(out, "base64")
|
||||
case "isBase64Url":
|
||||
return call(out, "base64url")
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Invoke a named Zod method on `target` if it exists, otherwise return
|
||||
// undefined so the caller can fall back. Using this helper instead of a
|
||||
// typed cast keeps `translateFilter` free of per-case narrowing noise.
|
||||
function call(target: z.ZodTypeAny, method: string, ...args: unknown[]): z.ZodTypeAny | undefined {
|
||||
const fn = (target as unknown as Record<string, ((...a: unknown[]) => z.ZodTypeAny) | undefined>)[method]
|
||||
return typeof fn === "function" ? fn.apply(target, args) : undefined
|
||||
}
|
||||
|
||||
function issueMessage(issue: any): string | undefined {
|
||||
if (typeof issue?.annotations?.message === "string") return issue.annotations.message
|
||||
if (typeof issue?.message === "string") return issue.message
|
||||
return undefined
|
||||
}
|
||||
|
||||
function body(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
if (SchemaAST.isOptional(ast)) return opt(ast)
|
||||
|
||||
switch (ast._tag) {
|
||||
case "String":
|
||||
return z.string()
|
||||
case "Number":
|
||||
return z.number()
|
||||
case "Boolean":
|
||||
return z.boolean()
|
||||
case "Null":
|
||||
return z.null()
|
||||
case "Undefined":
|
||||
return z.undefined()
|
||||
case "Any":
|
||||
case "Unknown":
|
||||
return z.unknown()
|
||||
case "Never":
|
||||
return z.never()
|
||||
case "Literal":
|
||||
return z.literal(ast.literal)
|
||||
case "Union":
|
||||
return union(ast)
|
||||
case "Objects":
|
||||
return object(ast)
|
||||
case "Arrays":
|
||||
return array(ast)
|
||||
case "Declaration":
|
||||
return decl(ast)
|
||||
default:
|
||||
return fail(ast)
|
||||
}
|
||||
}
|
||||
|
||||
function opt(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
if (ast._tag !== "Union") return fail(ast)
|
||||
const items = ast.types.filter((item) => item._tag !== "Undefined")
|
||||
const inner =
|
||||
items.length === 1
|
||||
? walk(items[0])
|
||||
: items.length > 1
|
||||
? z.union(items.map(walk) as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>])
|
||||
: z.undefined()
|
||||
// Schema.withDecodingDefault attaches an encoding `Link` whose transformation
|
||||
// decode Getter resolves `Option.none()` to `Option.some(default)`. Invoke
|
||||
// it to extract the default and emit `.default(...)` instead of `.optional()`.
|
||||
const fallback = extractDefault(ast)
|
||||
if (fallback !== undefined) return inner.default(fallback.value)
|
||||
return inner.optional()
|
||||
}
|
||||
|
||||
type DecodeLink = {
|
||||
readonly transformation: {
|
||||
readonly decode: {
|
||||
readonly run: (
|
||||
input: Option.Option<unknown>,
|
||||
options: SchemaAST.ParseOptions,
|
||||
) => Effect.Effect<Option.Option<unknown>, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractDefault(ast: SchemaAST.AST): { value: unknown } | undefined {
|
||||
const encoding = (ast as { encoding?: ReadonlyArray<DecodeLink> }).encoding
|
||||
if (!encoding?.length) return undefined
|
||||
// Walk the chain of encoding Links in order; the first Getter that produces
|
||||
// a value from Option.none wins. withDecodingDefault always puts its
|
||||
// defaulting Link adjacent to the optional Union.
|
||||
for (const link of encoding) {
|
||||
const probe = Effect.runSyncExit(link.transformation.decode.run(Option.none(), {}))
|
||||
if (probe._tag !== "Success") continue
|
||||
if (Option.isSome(probe.value)) return { value: probe.value.value }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function union(ast: SchemaAST.Union): z.ZodTypeAny {
|
||||
// When every member is a string literal, emit z.enum() so that
|
||||
// JSON Schema produces { "enum": [...] } instead of { "anyOf": [{ "const": ... }] }.
|
||||
if (ast.types.length >= 2 && ast.types.every((t) => t._tag === "Literal" && typeof t.literal === "string")) {
|
||||
return z.enum(ast.types.map((t) => (t as SchemaAST.Literal).literal as string) as [string, ...string[]])
|
||||
}
|
||||
|
||||
const items = ast.types.map(walk)
|
||||
if (items.length === 1) return items[0]
|
||||
if (items.length < 2) return fail(ast)
|
||||
|
||||
const discriminator = ast.annotations?.discriminator
|
||||
if (typeof discriminator === "string") {
|
||||
return z.discriminatedUnion(discriminator, items as [z.ZodObject<any>, z.ZodObject<any>, ...z.ZodObject<any>[]])
|
||||
}
|
||||
|
||||
return z.union(items as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>])
|
||||
}
|
||||
|
||||
function object(ast: SchemaAST.Objects): z.ZodTypeAny {
|
||||
// Pure record: { [k: string]: V }
|
||||
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 1) {
|
||||
const sig = ast.indexSignatures[0]
|
||||
if (sig.parameter._tag !== "String") return fail(ast)
|
||||
return z.record(z.string(), walk(sig.type))
|
||||
}
|
||||
|
||||
// Pure object with known fields and no index signatures.
|
||||
if (ast.indexSignatures.length === 0) {
|
||||
return z.object(Object.fromEntries(ast.propertySignatures.map((sig) => [String(sig.name), walk(sig.type)])))
|
||||
}
|
||||
|
||||
// Struct with a catchall (StructWithRest): known fields + index signature.
|
||||
// Only supports a single string-keyed index signature; multi-signature or
|
||||
// symbol/number keys fall through to fail.
|
||||
if (ast.indexSignatures.length !== 1) return fail(ast)
|
||||
const sig = ast.indexSignatures[0]
|
||||
if (sig.parameter._tag !== "String") return fail(ast)
|
||||
return z
|
||||
.object(Object.fromEntries(ast.propertySignatures.map((p) => [String(p.name), walk(p.type)])))
|
||||
.catchall(walk(sig.type))
|
||||
}
|
||||
|
||||
function array(ast: SchemaAST.Arrays): z.ZodTypeAny {
|
||||
// Pure variadic arrays: { elements: [], rest: [item] }
|
||||
if (ast.elements.length === 0) {
|
||||
if (ast.rest.length !== 1) return fail(ast)
|
||||
return z.array(walk(ast.rest[0]))
|
||||
}
|
||||
// Fixed-length tuples: { elements: [a, b, ...], rest: [] }
|
||||
// Tuples with a variadic tail (...rest) are not yet supported.
|
||||
if (ast.rest.length > 0) return fail(ast)
|
||||
const items = ast.elements.map(walk)
|
||||
return z.tuple(items as [z.ZodTypeAny, ...Array<z.ZodTypeAny>])
|
||||
}
|
||||
|
||||
function decl(ast: SchemaAST.Declaration): z.ZodTypeAny {
|
||||
if (ast.typeParameters.length !== 1) return fail(ast)
|
||||
return walk(ast.typeParameters[0])
|
||||
}
|
||||
|
||||
function fail(ast: SchemaAST.AST): never {
|
||||
const ref = SchemaAST.resolveIdentifier(ast)
|
||||
throw new Error(`unsupported effect schema: ${ref ?? ast._tag}`)
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { Config } from "effect"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
return value === "true" || value === "1"
|
||||
}
|
||||
|
||||
function falsy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
return value === "false" || value === "0"
|
||||
}
|
||||
|
||||
// Channels where new experiments default to ON (unstable / internal users).
|
||||
// Stable channels (`prod`, `latest`) stay opt-in.
|
||||
const UNSTABLE_CHANNELS = new Set(["dev", "beta", "local"])
|
||||
function unstableDefault(key: string) {
|
||||
return truthy(key) || (!falsy(key) && UNSTABLE_CHANNELS.has(InstallationChannel))
|
||||
}
|
||||
|
||||
function number(key: string) {
|
||||
const value = process.env[key]
|
||||
if (!value) return undefined
|
||||
@@ -27,15 +14,12 @@ function number(key: string) {
|
||||
|
||||
const KILO_EXPERIMENTAL = truthy("KILO_EXPERIMENTAL")
|
||||
const KILO_DISABLE_CLAUDE_CODE = truthy("KILO_DISABLE_CLAUDE_CODE")
|
||||
const KILO_DISABLE_CLAUDE_CODE_SKILLS =
|
||||
KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_SKILLS")
|
||||
const copy = process.env["KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"]
|
||||
|
||||
export const Flag = {
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
|
||||
OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"],
|
||||
|
||||
KILO_AUTO_SHARE: truthy("KILO_AUTO_SHARE"),
|
||||
KILO_AUTO_HEAP_SNAPSHOT: truthy("KILO_AUTO_HEAP_SNAPSHOT"),
|
||||
KILO_GIT_BASH_PATH: process.env["KILO_GIT_BASH_PATH"],
|
||||
KILO_CONFIG: process.env["KILO_CONFIG"],
|
||||
@@ -48,17 +32,12 @@ export const Flag = {
|
||||
KILO_PERMISSION: process.env["KILO_PERMISSION"],
|
||||
KILO_DISABLE_DEFAULT_PLUGINS: truthy("KILO_DISABLE_DEFAULT_PLUGINS"),
|
||||
KILO_DISABLE_LSP_DOWNLOAD: truthy("KILO_DISABLE_LSP_DOWNLOAD"),
|
||||
KILO_ENABLE_EXPERIMENTAL_MODELS: truthy("KILO_ENABLE_EXPERIMENTAL_MODELS"),
|
||||
KILO_DISABLE_AUTOCOMPACT: truthy("KILO_DISABLE_AUTOCOMPACT"),
|
||||
KILO_DISABLE_MODELS_FETCH: truthy("KILO_DISABLE_MODELS_FETCH"),
|
||||
KILO_DISABLE_MOUSE: truthy("KILO_DISABLE_MOUSE"),
|
||||
KILO_DISABLE_CLAUDE_CODE,
|
||||
KILO_DISABLE_CLAUDE_CODE_PROMPT: KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_PROMPT"),
|
||||
KILO_DISABLE_CLAUDE_CODE_SKILLS,
|
||||
KILO_DISABLE_EXTERNAL_SKILLS: truthy("KILO_DISABLE_EXTERNAL_SKILLS"),
|
||||
// Default-on for dev/beta/local; opt-in for stable. Set
|
||||
// KILO_EXPERIMENTAL_CUSTOMIZE_SKILL=false to force off, =true to force on.
|
||||
KILO_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("KILO_EXPERIMENTAL_CUSTOMIZE_SKILL"),
|
||||
KILO_FAKE_VCS: process.env["KILO_FAKE_VCS"],
|
||||
KILO_SERVER_PASSWORD: process.env["KILO_SERVER_PASSWORD"],
|
||||
KILO_SERVER_USERNAME: process.env["KILO_SERVER_USERNAME"],
|
||||
@@ -72,30 +51,24 @@ export const Flag = {
|
||||
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("KILO_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe(
|
||||
Config.withDefault(false),
|
||||
),
|
||||
KILO_EXPERIMENTAL_ICON_DISCOVERY: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_ICON_DISCOVERY"),
|
||||
KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT:
|
||||
copy === undefined ? process.platform === "win32" : truthy("KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
|
||||
KILO_ENABLE_EXA: truthy("KILO_ENABLE_EXA") || KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EXA"),
|
||||
KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("KILO_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
|
||||
KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("KILO_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
|
||||
KILO_EXPERIMENTAL_OXFMT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_OXFMT"),
|
||||
KILO_EXPERIMENTAL_LSP_TY: truthy("KILO_EXPERIMENTAL_LSP_TY"),
|
||||
KILO_EXPERIMENTAL_LSP_TOOL: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_LSP_TOOL"),
|
||||
KILO_EXPERIMENTAL_PLAN_MODE: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_PLAN_MODE"),
|
||||
KILO_EXPERIMENTAL_SCOUT: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SCOUT"),
|
||||
KILO_EXPERIMENTAL_MARKDOWN: !falsy("KILO_EXPERIMENTAL_MARKDOWN"),
|
||||
KILO_ENABLE_PARALLEL: truthy("KILO_ENABLE_PARALLEL") || truthy("KILO_EXPERIMENTAL_PARALLEL"),
|
||||
KILO_MODELS_URL: process.env["KILO_MODELS_URL"],
|
||||
KILO_MODELS_PATH: process.env["KILO_MODELS_PATH"],
|
||||
KILO_DISABLE_EMBEDDED_WEB_UI: truthy("KILO_DISABLE_EMBEDDED_WEB_UI"),
|
||||
KILO_DB: process.env["KILO_DB"],
|
||||
KILO_DISABLE_CHANNEL_DB: truthy("KILO_DISABLE_CHANNEL_DB"),
|
||||
KILO_SKIP_MIGRATIONS: truthy("KILO_SKIP_MIGRATIONS"),
|
||||
KILO_STRICT_CONFIG_DEPS: truthy("KILO_STRICT_CONFIG_DEPS"),
|
||||
|
||||
KILO_WORKSPACE_ID: process.env["KILO_WORKSPACE_ID"],
|
||||
KILO_EXPERIMENTAL_WORKSPACES: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_WORKSPACES"),
|
||||
KILO_EXPERIMENTAL_EVENT_SYSTEM: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EVENT_SYSTEM"),
|
||||
KILO_EXPERIMENTAL_SESSION_SWITCHING: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SESSION_SWITCHING"),
|
||||
|
||||
// Evaluated at access time (not module load) because tests, the CLI, and
|
||||
// external tooling set these env vars at runtime.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Layer, LayerMap } from "effect"
|
||||
import { Instance } from "./instance"
|
||||
import { Catalog } from "./catalog"
|
||||
import { PluginBoot } from "./plugin/boot"
|
||||
|
||||
export class InstanceServiceMap extends LayerMap.Service<InstanceServiceMap>()("@opencode/example/InstanceServiceMap", {
|
||||
lookup: (ref: Instance.Ref) => {
|
||||
const instance = Layer.succeed(Instance.Service, Instance.Service.of(ref))
|
||||
return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(instance))
|
||||
},
|
||||
idleTimeToLive: "5 minutes",
|
||||
}) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Context } from "effect"
|
||||
|
||||
export * as Instance from "./instance"
|
||||
|
||||
export type Ref = {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Ref>()("@opencode/Instance") {}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { DateTime, Schema } from "effect"
|
||||
import { DateTimeUtcFromMillis } from "effect/Schema"
|
||||
import { ProviderV2 } from "./provider"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
|
||||
export type VariantID = typeof VariantID.Type
|
||||
|
||||
// Grouping of models, eg claude opus, claude sonnet
|
||||
export const Family = Schema.String.pipe(Schema.brand("Family"))
|
||||
export type Family = typeof Family.Type
|
||||
|
||||
export const Capabilities = Schema.Struct({
|
||||
tools: Schema.Boolean,
|
||||
// mime patterns, image, audio, video/*, text/*
|
||||
input: Schema.String.pipe(Schema.Array),
|
||||
output: Schema.String.pipe(Schema.Array),
|
||||
})
|
||||
export type Capabilities = typeof Capabilities.Type
|
||||
|
||||
export const Cost = Schema.Struct({
|
||||
tier: Schema.Struct({
|
||||
type: Schema.Literal("context"),
|
||||
size: Schema.Int,
|
||||
}).pipe(Schema.optional),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
})
|
||||
|
||||
export const Ref = Schema.Struct({
|
||||
id: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: VariantID,
|
||||
})
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("ModelV2.Info")({
|
||||
id: ID,
|
||||
apiID: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
family: Family.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
endpoint: ProviderV2.Endpoint,
|
||||
capabilities: Capabilities,
|
||||
options: Schema.Struct({
|
||||
...ProviderV2.Options.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
variants: Schema.Struct({
|
||||
id: VariantID,
|
||||
...ProviderV2.Options.fields,
|
||||
}).pipe(Schema.Array),
|
||||
time: Schema.Struct({
|
||||
released: DateTimeUtcFromMillis,
|
||||
}),
|
||||
cost: Cost.pipe(Schema.Array),
|
||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||
enabled: Schema.Boolean,
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Int,
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
output: Schema.Int,
|
||||
}),
|
||||
}) {
|
||||
static empty(providerID: ProviderV2.ID, modelID: ID) {
|
||||
return new Info({
|
||||
id: modelID,
|
||||
apiID: modelID,
|
||||
providerID,
|
||||
name: modelID,
|
||||
endpoint: {
|
||||
type: "unknown",
|
||||
},
|
||||
capabilities: {
|
||||
tools: false,
|
||||
input: [],
|
||||
output: [],
|
||||
},
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
},
|
||||
variants: [],
|
||||
time: {
|
||||
released: DateTime.makeUnsafe(0),
|
||||
},
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: {
|
||||
context: 0,
|
||||
output: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||
const [providerID, ...modelID] = input.split("/")
|
||||
return {
|
||||
providerID: ProviderV2.ID.make(providerID),
|
||||
modelID: ID.make(modelID.join("/")),
|
||||
}
|
||||
}
|
||||
|
||||
export * as ModelV2 from "./model"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Auto-generated by build.ts - do not edit
|
||||
export declare const snapshot: Record<string, unknown>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,35 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import path from "path"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Installation } from "../installation"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Flock } from "@opencode-ai/core/util/flock"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { CatalogModelStatus } from "./model-status"
|
||||
import { Global } from "./global"
|
||||
import { Flag } from "./flag/flag"
|
||||
import { Flock } from "./util/flock"
|
||||
import { Hash } from "./util/hash"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { InstallationChannel, InstallationVersion } from "./installation/version"
|
||||
|
||||
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
|
||||
export type CatalogModelStatus = typeof CatalogModelStatus.Type
|
||||
|
||||
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.KILO_CLIENT}`
|
||||
|
||||
const CostTier = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache_read: Schema.optional(Schema.Finite),
|
||||
cache_write: Schema.optional(Schema.Finite),
|
||||
tier: Schema.Struct({
|
||||
type: Schema.Literal("context"),
|
||||
size: Schema.Finite,
|
||||
}),
|
||||
})
|
||||
|
||||
const Cost = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache_read: Schema.optional(Schema.Finite),
|
||||
cache_write: Schema.optional(Schema.Finite),
|
||||
tiers: Schema.optional(Schema.Array(CostTier)),
|
||||
context_over_200k: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
@@ -97,11 +112,21 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClient.HttpClient> = Layer.effect(
|
||||
type Requirements = AppFileSystem.Service | HttpClient.HttpClient
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Requirements> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
|
||||
const http = HttpClient.filterStatusOk(
|
||||
(yield* HttpClient.HttpClient).pipe(
|
||||
HttpClient.retryTransient({
|
||||
retryOn: "errors-and-responses",
|
||||
times: 2,
|
||||
schedule: Schedule.exponential(200).pipe(Schedule.jittered),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const source = Flag.KILO_MODELS_URL || "https://models.dev"
|
||||
const filepath = path.join(
|
||||
@@ -120,7 +145,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
|
||||
|
||||
const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () {
|
||||
return yield* HttpClientRequest.get(`${source}/api.json`).pipe(
|
||||
HttpClientRequest.setHeader("User-Agent", Installation.USER_AGENT),
|
||||
HttpClientRequest.setHeader("User-Agent", USER_AGENT),
|
||||
http.execute,
|
||||
Effect.flatMap((res) => res.text),
|
||||
Effect.timeout("10 seconds"),
|
||||
@@ -177,7 +202,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
|
||||
yield* invalidate
|
||||
}),
|
||||
).pipe(
|
||||
Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause })),
|
||||
Effect.tapCause((cause) =>
|
||||
Effect.logError("Failed to fetch models.dev").pipe(Effect.annotateLogs("cause", cause)),
|
||||
),
|
||||
Effect.ignore,
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
export * as PluginV2 from "./plugin"
|
||||
|
||||
import { createDraft, finishDraft, type Draft } from "immer"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { type ProviderV2 } from "./provider"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import type { ModelV2 } from "./model"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
type HookSpec = {
|
||||
"provider.update": {
|
||||
input: {}
|
||||
output: {
|
||||
provider: ProviderV2.Info
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
"model.update": {
|
||||
input: {}
|
||||
output: {
|
||||
model: ModelV2.Info
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
"aisdk.language": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
sdk: any
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}
|
||||
"aisdk.sdk": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
package: string
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
sdk?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Hooks = {
|
||||
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
|
||||
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
|
||||
? Draft<HookSpec[Name]["output"][Field]>
|
||||
: HookSpec[Name]["output"][Field]
|
||||
}
|
||||
}
|
||||
|
||||
export type HookFunctions = {
|
||||
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
||||
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
||||
|
||||
export type Effect = Effect.Effect<HookFunctions | void, never, never>
|
||||
|
||||
export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
|
||||
return input
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (input: { id: ID; effect: Effect }) => Effect.Effect<void>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
readonly trigger: <Name extends keyof Hooks>(
|
||||
name: Name,
|
||||
input: HookInput<Name>,
|
||||
output: HookOutput<Name>,
|
||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
let hooks: {
|
||||
id: ID
|
||||
hooks: HookFunctions
|
||||
}[] = []
|
||||
|
||||
const svc = Service.of({
|
||||
add: Effect.fn("Plugin.add")(function* (input) {
|
||||
const result = yield* input.effect
|
||||
if (!result) return
|
||||
hooks = [
|
||||
...hooks.filter((item) => item.id !== input.id),
|
||||
{
|
||||
id: input.id,
|
||||
hooks: result,
|
||||
},
|
||||
]
|
||||
}),
|
||||
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
|
||||
const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
|
||||
const event = {
|
||||
...input,
|
||||
...output,
|
||||
} as Record<string, unknown>
|
||||
|
||||
for (const [field, value] of Object.entries(output)) {
|
||||
if (value && typeof value === "object") {
|
||||
draftEntries.set(field, createDraft(value))
|
||||
event[field] = draftEntries.get(field)
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of hooks) {
|
||||
const match = item.hooks[name]
|
||||
if (!match) continue
|
||||
yield* match(event as any).pipe(
|
||||
Effect.withSpan(`Plugin.hook.${name}`, {
|
||||
attributes: {
|
||||
plugin: item.id,
|
||||
hook: name,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const [field, draft] of draftEntries) {
|
||||
event[field] = finishDraft(draft)
|
||||
}
|
||||
|
||||
return event as any
|
||||
}),
|
||||
remove: Effect.fn("Plugin.remove")(function* (id) {
|
||||
hooks = hooks.filter((item) => item.id !== id)
|
||||
}),
|
||||
})
|
||||
return svc
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
// opencode
|
||||
// sdcok
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import { AuthV2 } from "../auth"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
export const AuthPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("auth"),
|
||||
effect: Effect.gen(function* () {
|
||||
const auth = yield* AuthV2.Service
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
const account = yield* auth.active(AuthV2.ServiceID.make(evt.provider.id)).pipe(Effect.orDie)
|
||||
if (!account) return
|
||||
evt.provider.enabled = {
|
||||
via: "auth",
|
||||
service: account.serviceID,
|
||||
}
|
||||
if (account.credential.type === "api") {
|
||||
evt.provider.options.aisdk.provider.apiKey = account.credential.key
|
||||
Object.assign(evt.provider.options.aisdk.provider, account.credential.metadata ?? {})
|
||||
}
|
||||
if (account.credential.type === "oauth") {
|
||||
evt.provider.options.aisdk.provider.apiKey = account.credential.access
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
export * as PluginBoot from "./boot"
|
||||
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { AuthV2 } from "../auth"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { AuthPlugin } from "./auth"
|
||||
import { EnvPlugin } from "./env"
|
||||
import { ModelsDevPlugin } from "./models-dev"
|
||||
import { ProviderPlugins } from "./provider"
|
||||
|
||||
type Plugin = {
|
||||
id: PluginV2.ID
|
||||
effect: Effect.Effect<PluginV2.HookFunctions | void, never, Catalog.Service | AuthV2.Service | Npm.Service>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly wait: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Catalog.Service | PluginV2.Service | AuthV2.Service | Npm.Service> =
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const auth = yield* AuthV2.Service
|
||||
const npm = yield* Npm.Service
|
||||
const done = yield* Deferred.make<void>()
|
||||
|
||||
const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
|
||||
yield* plugin.add({
|
||||
id: input.id,
|
||||
effect: input.effect.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(AuthV2.Service, auth),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
const boot = Effect.gen(function* () {
|
||||
yield* add(EnvPlugin)
|
||||
yield* add(AuthPlugin)
|
||||
for (const item of ProviderPlugins) {
|
||||
yield* add(item)
|
||||
}
|
||||
yield* add(ModelsDevPlugin)
|
||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||
|
||||
yield* boot.pipe(
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => Deferred.done(done, exit)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
wait: () => Deferred.await(done),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Catalog.defaultLayer),
|
||||
Layer.provide(PluginV2.defaultLayer),
|
||||
Layer.provide(Layer.orDie(AuthV2.defaultLayer)),
|
||||
Layer.provide(Npm.defaultLayer),
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
export const EnvPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("env"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
const key = evt.provider.env.find((item) => process.env[item])
|
||||
if (!key) return
|
||||
evt.provider.enabled = {
|
||||
via: "env",
|
||||
name: key,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
export * as LayerMapExample from "./layer-map.example"
|
||||
|
||||
import { Context, Effect, Layer, LayerMap } from "effect"
|
||||
import { Npm } from "../npm"
|
||||
|
||||
/**
|
||||
* Tutorial: split global services from context-specific services.
|
||||
*
|
||||
* Use this pattern when part of the app should be constructed once at the app edge,
|
||||
* while another part should be cached per request/project/workspace key.
|
||||
*
|
||||
* In this example:
|
||||
* - Npm.Service is the global service. It is not keyed by request context and should
|
||||
* be provided once by the application runtime.
|
||||
* - ConfigService is context-specific. It is built from a RequestContext key and is
|
||||
* cached by LayerMap for that key.
|
||||
* - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
|
||||
* request can provide ConfigServiceMap.get(context) to select the right instance.
|
||||
*
|
||||
* Lifetime model:
|
||||
* - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
|
||||
* - ConfigServiceMap.get(context) has the request/context lifetime and provides
|
||||
* ConfigService for exactly that context key.
|
||||
* - The cached ConfigService entry stays alive while something is using it. Once idle,
|
||||
* it remains cached for idleTimeToLive, then its scope is finalized.
|
||||
* - invalidate(context) removes the cache entry for future lookups. Active users keep
|
||||
* running on the old instance; the next lookup can create a fresh instance.
|
||||
*
|
||||
* Key model:
|
||||
* - Keys can be strings, structs, classes, arrays, etc.
|
||||
* - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
|
||||
* lookup, so mutating an object after it has been used as a key is a bug.
|
||||
*/
|
||||
|
||||
export type RequestContext = {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
}
|
||||
|
||||
export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
|
||||
"@opencode/example/RequestContextRef",
|
||||
) {}
|
||||
|
||||
export interface ConfigServiceShape {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
readonly nextUse: () => Effect.Effect<number>
|
||||
readonly which: Npm.Interface["which"]
|
||||
}
|
||||
|
||||
export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
|
||||
"@opencode/example/ConfigService",
|
||||
) {}
|
||||
|
||||
const configServiceLayer = Layer.effect(
|
||||
ConfigService,
|
||||
Effect.gen(function* () {
|
||||
const context = yield* RequestContextRef
|
||||
const npm = yield* Npm.Service
|
||||
|
||||
let useCount = 0
|
||||
|
||||
return ConfigService.of({
|
||||
directory: context.directory,
|
||||
workspace: context.workspace,
|
||||
nextUse: () => Effect.succeed(++useCount),
|
||||
which: npm.which,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
|
||||
lookup: (context: RequestContext) =>
|
||||
configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
|
||||
idleTimeToLive: "5 minutes",
|
||||
}) {}
|
||||
|
||||
export const appLayer = ConfigServiceMap.layer
|
||||
|
||||
export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
|
||||
const config = yield* ConfigService
|
||||
|
||||
return {
|
||||
directory: config.directory,
|
||||
workspace: config.workspace,
|
||||
useCount: yield* config.nextUse(),
|
||||
}
|
||||
})
|
||||
|
||||
export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
|
||||
return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
|
||||
})
|
||||
|
||||
export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)
|
||||
@@ -0,0 +1,108 @@
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ModelsDev } from "../models"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { ProviderV2 } from "../provider"
|
||||
|
||||
function released(date: string) {
|
||||
const time = Date.parse(date)
|
||||
return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0)
|
||||
}
|
||||
|
||||
function cost(input: ModelsDev.Model["cost"]) {
|
||||
const base = {
|
||||
input: input?.input ?? 0,
|
||||
output: input?.output ?? 0,
|
||||
cache: {
|
||||
read: input?.cache_read ?? 0,
|
||||
write: input?.cache_write ?? 0,
|
||||
},
|
||||
}
|
||||
if (!input?.context_over_200k) return [base]
|
||||
return [
|
||||
base,
|
||||
{
|
||||
tier: {
|
||||
type: "context" as const,
|
||||
size: 200_000,
|
||||
},
|
||||
input: input.context_over_200k.input,
|
||||
output: input.context_over_200k.output,
|
||||
cache: {
|
||||
read: input.context_over_200k.cache_read ?? 0,
|
||||
write: input.context_over_200k.cache_write ?? 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function variants(model: ModelsDev.Model) {
|
||||
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
headers: { ...(item.provider?.headers ?? {}) },
|
||||
body: { ...(item.provider?.body ?? {}) },
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export const ModelsDevPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("models-dev"),
|
||||
effect: Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
for (const item of Object.values(yield* modelsDev.get())) {
|
||||
const providerID = ProviderV2.ID.make(item.id)
|
||||
yield* catalog.provider.update(providerID, (provider) => {
|
||||
provider.name = item.name
|
||||
provider.env = [...item.env]
|
||||
provider.endpoint = item.npm
|
||||
? {
|
||||
type: "aisdk",
|
||||
package: item.npm,
|
||||
url: item.api,
|
||||
}
|
||||
: {
|
||||
type: "unknown",
|
||||
}
|
||||
})
|
||||
|
||||
for (const model of Object.values(item.models)) {
|
||||
const modelID = ModelV2.ID.make(model.id)
|
||||
yield* catalog.model
|
||||
.update(providerID, modelID, (draft) => {
|
||||
draft.name = model.name
|
||||
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
||||
draft.endpoint = model.provider?.npm
|
||||
? {
|
||||
type: "aisdk",
|
||||
package: model.provider?.npm,
|
||||
url: model.provider.api,
|
||||
}
|
||||
: {
|
||||
type: "unknown",
|
||||
}
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
output: [...(model.modalities?.output ?? [])],
|
||||
}
|
||||
draft.variants = variants(model)
|
||||
draft.time.released = released(model.release_date)
|
||||
draft.cost = cost(model.cost)
|
||||
draft.status = model.status ?? "active"
|
||||
draft.enabled = true
|
||||
draft.limit = {
|
||||
context: model.limit.context,
|
||||
input: model.limit.input,
|
||||
output: model.limit.output,
|
||||
}
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
}
|
||||
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export { ProviderPlugins } from "./provider/index"
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const AlibabaPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("alibaba"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/alibaba") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||
evt.sdk = mod.createAlibaba(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
// Bedrock cross-region inference profiles require regional prefixes only for
|
||||
// specific model/region combinations. Keep the mapping narrow and avoid
|
||||
// double-prefixing model IDs that models.dev already marks as global/us/eu/etc.
|
||||
function resolveModelID(modelID: string, region: string | undefined) {
|
||||
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
||||
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID
|
||||
|
||||
const resolvedRegion = region ?? "us-east-1"
|
||||
const regionPrefix = resolvedRegion.split("-")[0]
|
||||
if (regionPrefix === "us") {
|
||||
const requiresPrefix = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some(
|
||||
(item) => modelID.includes(item),
|
||||
)
|
||||
if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}`
|
||||
return modelID
|
||||
}
|
||||
if (regionPrefix === "eu") {
|
||||
const regionRequiresPrefix = [
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"eu-north-1",
|
||||
"eu-central-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
].some((item) => resolvedRegion.includes(item))
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) =>
|
||||
modelID.includes(item),
|
||||
)
|
||||
return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID
|
||||
}
|
||||
if (regionPrefix !== "ap") return modelID
|
||||
|
||||
const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion)
|
||||
if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) {
|
||||
return `au.${modelID}`
|
||||
}
|
||||
|
||||
const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac"
|
||||
return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item))
|
||||
? `${prefix}.${modelID}`
|
||||
: modelID
|
||||
}
|
||||
|
||||
export const AmazonBedrockPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("amazon-bedrock"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.provider.endpoint.type !== "aisdk") return
|
||||
if (typeof evt.provider.options.aisdk.provider.endpoint !== "string") return
|
||||
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
|
||||
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
|
||||
evt.provider.endpoint.url = evt.provider.options.aisdk.provider.endpoint
|
||||
delete evt.provider.options.aisdk.provider.endpoint
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/amazon-bedrock") return
|
||||
const options = { ...evt.options }
|
||||
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
|
||||
const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1")
|
||||
const bearerToken =
|
||||
process.env.AWS_BEARER_TOKEN_BEDROCK ??
|
||||
(typeof options.bearerToken === "string" ? options.bearerToken : undefined)
|
||||
if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken
|
||||
const containerCreds = Boolean(
|
||||
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
|
||||
)
|
||||
|
||||
options.region = region
|
||||
if (typeof options.endpoint === "string") options.baseURL = options.endpoint
|
||||
if (!bearerToken && options.credentialProvider === undefined) {
|
||||
// Do not gate SDK creation on explicit AWS env vars. The default chain
|
||||
// also handles ~/.aws/credentials, SSO, process creds, and instance roles.
|
||||
const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
|
||||
options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {})
|
||||
}
|
||||
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
|
||||
evt.sdk = mod.createAmazonBedrock(options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.apiID, region))
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const AnthropicPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("anthropic"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.anthropic) return
|
||||
evt.provider.options.headers["anthropic-beta"] =
|
||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||
evt.sdk = mod.createAnthropic(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||
if (useChat && sdk.chat) return sdk.chat(modelID)
|
||||
if (sdk.responses) return sdk.responses(modelID)
|
||||
if (sdk.messages) return sdk.messages(modelID)
|
||||
if (sdk.chat) return sdk.chat(modelID)
|
||||
return sdk.languageModel(modelID)
|
||||
}
|
||||
|
||||
export const AzurePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("azure"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.azure) return
|
||||
const configured = evt.provider.options.aisdk.provider.resourceName
|
||||
const resourceName =
|
||||
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
|
||||
if (resourceName) evt.provider.options.aisdk.provider.resourceName = resourceName
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/azure") return
|
||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||
if (
|
||||
!evt.options.resourceName &&
|
||||
!evt.options.baseURL &&
|
||||
(evt.model.endpoint.type !== "aisdk" || !evt.model.endpoint.url)
|
||||
) {
|
||||
throw new Error(
|
||||
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
|
||||
)
|
||||
}
|
||||
}
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
|
||||
evt.sdk = mod.createAzure(evt.options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls))
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const AzureCognitiveServicesPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("azure-cognitive-services"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
if (resourceName)
|
||||
evt.provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls))
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const CerebrasPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cerebras"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("cerebras")) return
|
||||
evt.provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
evt.sdk = mod.createCerebras(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const CloudflareAIGatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cloudflare-ai-gateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "ai-gateway-provider") return
|
||||
if (evt.options.baseURL) return
|
||||
|
||||
const config = gatewayConfig(evt.options)
|
||||
if (!config) return
|
||||
const metadata = gatewayMetadata(evt.options)
|
||||
const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")).pipe(Effect.orDie)
|
||||
const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
const gateway = createAiGateway({
|
||||
accountId: config.accountId,
|
||||
gateway: config.gatewayId,
|
||||
apiKey: config.apiKey,
|
||||
options: gatewayOptions(evt.options, metadata),
|
||||
} as any)
|
||||
const unified = createUnified()
|
||||
evt.sdk = {
|
||||
languageModel(modelID: string) {
|
||||
return gateway(unified(modelID))
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
type GatewayConfig = {
|
||||
accountId: string
|
||||
gatewayId: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
|
||||
function gatewayConfig(options: Record<string, unknown>): GatewayConfig | undefined {
|
||||
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
|
||||
// AuthPlugin copies CLI prompt metadata into options. The prompt stores the
|
||||
// gateway as gatewayId, while older config examples may use gateway.
|
||||
const gatewayId =
|
||||
process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway")
|
||||
const apiKey = process.env.CLOUDFLARE_API_TOKEN ?? process.env.CF_AIG_TOKEN ?? stringOption(options, "apiKey")
|
||||
if (!accountId || !gatewayId || !apiKey) return undefined
|
||||
|
||||
return { accountId, gatewayId, apiKey }
|
||||
}
|
||||
|
||||
function gatewayMetadata(options: Record<string, unknown>) {
|
||||
// Preserve the legacy cf-aig-metadata header escape hatch for gateway logging
|
||||
// metadata, but prefer the typed metadata option when present.
|
||||
if (options.metadata !== undefined) return options.metadata
|
||||
const raw = (options.headers as Record<string, string> | undefined)?.["cf-aig-metadata"]
|
||||
return raw ? Option.getOrUndefined(decodeJson(raw)) : undefined
|
||||
}
|
||||
|
||||
function gatewayOptions(options: Record<string, unknown>, metadata: unknown) {
|
||||
return {
|
||||
metadata,
|
||||
cacheTtl: options.cacheTtl,
|
||||
cacheKey: options.cacheKey,
|
||||
skipCache: options.skipCache,
|
||||
collectLog: options.collectLog,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stringOption(options: Record<string, unknown>, key: string) {
|
||||
return typeof options[key] === "string" ? options[key] : undefined
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||
|
||||
export const CloudflareWorkersAIPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cloudflare-workers-ai"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== providerID) return
|
||||
if (evt.provider.endpoint.type !== "aisdk") return
|
||||
if (evt.provider.endpoint.url) return
|
||||
|
||||
const accountId = resolveAccountId(evt.provider.options.aisdk.provider)
|
||||
if (accountId) evt.provider.endpoint.url = workersEndpoint(accountId)
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
|
||||
if (!hasWorkersEndpoint(evt.model.endpoint)) return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(sdkOptions(evt.options) as any)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.apiID)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
function resolveAccountId(options: Record<string, unknown>) {
|
||||
return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
|
||||
}
|
||||
|
||||
function workersEndpoint(accountId: string) {
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(endpoint: ProviderV2.Endpoint) {
|
||||
return endpoint.type === "aisdk" && Boolean(endpoint.url)
|
||||
}
|
||||
|
||||
function sdkOptions(options: Record<string, any>) {
|
||||
return {
|
||||
...options,
|
||||
baseURL: expandAccountId(options.baseURL),
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
...options.headers,
|
||||
},
|
||||
name: providerID,
|
||||
}
|
||||
}
|
||||
|
||||
function expandAccountId(baseURL: unknown) {
|
||||
if (typeof baseURL !== "string") return baseURL
|
||||
return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}")
|
||||
}
|
||||
|
||||
function stringOption(options: Record<string, unknown>, key: string) {
|
||||
return typeof options[key] === "string" ? options[key] : undefined
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const CoherePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cohere"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cohere") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||
evt.sdk = mod.createCohere(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const DeepInfraPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("deepinfra"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||
evt.sdk = mod.createDeepInfra(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Npm } from "../../npm"
|
||||
import { Effect, Option } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const DynamicProviderPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("dynamic-provider"),
|
||||
effect: Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
return (await import(
|
||||
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
|
||||
)) as Record<string, (options: any) => any>
|
||||
}).pipe(Effect.orDie)
|
||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
||||
|
||||
evt.sdk = mod[match](evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("gateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/gateway") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||
evt.sdk = mod.createGateway(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
// which still need the chat-completions endpoint.
|
||||
const match = /^gpt-(\d+)/.exec(modelID)
|
||||
if (!match) return false
|
||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||
}
|
||||
|
||||
export const GithubCopilotPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("github-copilot"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.githubCopilot) return
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||
evt.language = evt.sdk.languageModel(evt.model.apiID)
|
||||
return
|
||||
}
|
||||
evt.language = shouldUseResponses(evt.model.apiID)
|
||||
? evt.sdk.responses(evt.model.apiID)
|
||||
: evt.sdk.chat(evt.model.apiID)
|
||||
}),
|
||||
"model.update": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
|
||||
// so hide it only for Copilot rather than for every provider catalog.
|
||||
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const GitLabPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("gitlab"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "gitlab-ai-provider") return
|
||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||
evt.sdk = mod.createGitLab({
|
||||
...evt.options,
|
||||
instanceUrl:
|
||||
typeof evt.options.instanceUrl === "string"
|
||||
? evt.options.instanceUrl
|
||||
: (process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com"),
|
||||
apiKey: typeof evt.options.apiKey === "string" ? evt.options.apiKey : process.env.GITLAB_TOKEN,
|
||||
aiGatewayHeaders: {
|
||||
"User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
...evt.options.aiGatewayHeaders,
|
||||
},
|
||||
featureFlags: {
|
||||
duo_agent_platform_agentic_chat: true,
|
||||
duo_agent_platform: true,
|
||||
...evt.options.featureFlags,
|
||||
},
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags =
|
||||
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
||||
if (evt.model.apiID.startsWith("duo-workflow-")) {
|
||||
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
|
||||
const workflowRef =
|
||||
typeof evt.model.options.aisdk.request.workflowRef === "string"
|
||||
? evt.model.options.aisdk.request.workflowRef
|
||||
: undefined
|
||||
const workflowDefinition =
|
||||
typeof evt.model.options.aisdk.request.workflowDefinition === "string"
|
||||
? evt.model.options.aisdk.request.workflowDefinition
|
||||
: undefined
|
||||
const language = evt.sdk.workflowChat(
|
||||
gitlab.isWorkflowModel(evt.model.apiID) ? evt.model.apiID : "duo-workflow",
|
||||
{
|
||||
featureFlags,
|
||||
workflowDefinition,
|
||||
},
|
||||
)
|
||||
if (workflowRef) language.selectedModelRef = workflowRef
|
||||
evt.language = language
|
||||
return
|
||||
}
|
||||
evt.language = evt.sdk.agenticChat(evt.model.apiID, {
|
||||
aiGatewayHeaders: evt.options.aiGatewayHeaders,
|
||||
featureFlags,
|
||||
})
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function resolveProject(options: Record<string, any>) {
|
||||
// models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs
|
||||
// and ADC examples commonly use the broader Google Cloud project aliases.
|
||||
return (
|
||||
options.project ??
|
||||
process.env.GOOGLE_VERTEX_PROJECT ??
|
||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||
process.env.GCP_PROJECT ??
|
||||
process.env.GCLOUD_PROJECT
|
||||
)
|
||||
}
|
||||
|
||||
function resolveLocation(options: Record<string, any>) {
|
||||
return (
|
||||
options.location ??
|
||||
process.env.GOOGLE_VERTEX_LOCATION ??
|
||||
process.env.GOOGLE_CLOUD_LOCATION ??
|
||||
process.env.VERTEX_LOCATION ??
|
||||
"us-central1"
|
||||
)
|
||||
}
|
||||
|
||||
function vertexEndpoint(location: string) {
|
||||
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
||||
}
|
||||
|
||||
function replaceVertexVars(value: string, project: string | undefined, location: string) {
|
||||
// Vertex OpenAI-compatible endpoints are stored as templates in the catalog;
|
||||
// expand them after provider config/env project and location have been resolved.
|
||||
return value
|
||||
.replaceAll("${GOOGLE_VERTEX_PROJECT}", project ?? "${GOOGLE_VERTEX_PROJECT}")
|
||||
.replaceAll("${GOOGLE_VERTEX_LOCATION}", location)
|
||||
.replaceAll("${GOOGLE_VERTEX_ENDPOINT}", vertexEndpoint(location))
|
||||
}
|
||||
|
||||
function authFetch(fetchWithRuntimeOptions?: unknown) {
|
||||
// Native Vertex SDKs handle ADC internally. OpenAI-compatible Vertex endpoints
|
||||
// do not, so inject a Google access token into their fetch path.
|
||||
return async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
const { GoogleAuth } = await import("google-auth-library")
|
||||
const auth = new GoogleAuth()
|
||||
const client = await auth.getApplicationDefault()
|
||||
const token = await client.credential.getAccessToken()
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set("Authorization", `Bearer ${token.token}`)
|
||||
return typeof fetchWithRuntimeOptions === "function"
|
||||
? fetchWithRuntimeOptions(input, { ...init, headers })
|
||||
: fetch(input, { ...init, headers })
|
||||
}
|
||||
}
|
||||
|
||||
export const GoogleVertexPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google-vertex"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.googleVertex) return
|
||||
const project = resolveProject(evt.provider.options.aisdk.provider)
|
||||
const location = String(resolveLocation(evt.provider.options.aisdk.provider))
|
||||
if (project) evt.provider.options.aisdk.provider.project = project
|
||||
evt.provider.options.aisdk.provider.location = location
|
||||
if (evt.provider.endpoint.type === "aisdk" && evt.provider.endpoint.url) {
|
||||
evt.provider.endpoint.url = replaceVertexVars(evt.provider.endpoint.url, project, location)
|
||||
}
|
||||
if (
|
||||
evt.provider.endpoint.type === "aisdk" &&
|
||||
evt.provider.endpoint.package.includes("@ai-sdk/openai-compatible")
|
||||
) {
|
||||
evt.provider.options.aisdk.provider.fetch = authFetch(evt.provider.options.aisdk.provider.fetch)
|
||||
}
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.options.fetch = authFetch(evt.options.fetch)
|
||||
return
|
||||
}
|
||||
if (evt.package !== "@ai-sdk/google-vertex") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex"))
|
||||
const project = resolveProject(evt.options)
|
||||
const location = resolveLocation(evt.options)
|
||||
const options = { ...evt.options }
|
||||
delete options.fetch
|
||||
evt.sdk = mod.createVertex({
|
||||
...options,
|
||||
project,
|
||||
location,
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim())
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google-vertex-anthropic"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
const project =
|
||||
evt.provider.options.aisdk.provider.project ??
|
||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||
process.env.GCP_PROJECT ??
|
||||
process.env.GCLOUD_PROJECT
|
||||
const location =
|
||||
evt.provider.options.aisdk.provider.location ??
|
||||
process.env.GOOGLE_CLOUD_LOCATION ??
|
||||
process.env.VERTEX_LOCATION ??
|
||||
"global"
|
||||
if (project) evt.provider.options.aisdk.provider.project = project
|
||||
evt.provider.options.aisdk.provider.location = location
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
evt.sdk = mod.createVertexAnthropic({
|
||||
...evt.options,
|
||||
project:
|
||||
typeof evt.options.project === "string"
|
||||
? evt.options.project
|
||||
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT),
|
||||
location:
|
||||
typeof evt.options.location === "string"
|
||||
? evt.options.location
|
||||
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"),
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim())
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GooglePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GroqPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("groq"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/groq") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||
evt.sdk = mod.createGroq(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AlibabaPlugin } from "./alibaba"
|
||||
import { AmazonBedrockPlugin } from "./amazon-bedrock"
|
||||
import { AnthropicPlugin } from "./anthropic"
|
||||
import { AzureCognitiveServicesPlugin, AzurePlugin } from "./azure"
|
||||
import { CerebrasPlugin } from "./cerebras"
|
||||
import { CloudflareAIGatewayPlugin } from "./cloudflare-ai-gateway"
|
||||
import { CloudflareWorkersAIPlugin } from "./cloudflare-workers-ai"
|
||||
import { CoherePlugin } from "./cohere"
|
||||
import { DeepInfraPlugin } from "./deepinfra"
|
||||
import { DynamicProviderPlugin } from "./dynamic"
|
||||
import { GatewayPlugin } from "./gateway"
|
||||
import { GithubCopilotPlugin } from "./github-copilot"
|
||||
import { GitLabPlugin } from "./gitlab"
|
||||
import { GooglePlugin } from "./google"
|
||||
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./google-vertex"
|
||||
import { GroqPlugin } from "./groq"
|
||||
import { KiloPlugin } from "./kilo"
|
||||
import { LLMGatewayPlugin } from "./llmgateway"
|
||||
import { MistralPlugin } from "./mistral"
|
||||
import { NvidiaPlugin } from "./nvidia"
|
||||
import { OpenAIPlugin } from "./openai"
|
||||
import { OpenAICompatiblePlugin } from "./openai-compatible"
|
||||
import { OpencodePlugin } from "./opencode"
|
||||
import { OpenRouterPlugin } from "./openrouter"
|
||||
import { PerplexityPlugin } from "./perplexity"
|
||||
import { SapAICorePlugin } from "./sap-ai-core"
|
||||
import { TogetherAIPlugin } from "./togetherai"
|
||||
import { VercelPlugin } from "./vercel"
|
||||
import { VenicePlugin } from "./venice"
|
||||
import { XAIPlugin } from "./xai"
|
||||
import { ZenmuxPlugin } from "./zenmux"
|
||||
|
||||
export const ProviderPlugins = [
|
||||
AlibabaPlugin,
|
||||
AmazonBedrockPlugin,
|
||||
AnthropicPlugin,
|
||||
AzureCognitiveServicesPlugin,
|
||||
AzurePlugin,
|
||||
CerebrasPlugin,
|
||||
CloudflareAIGatewayPlugin,
|
||||
CloudflareWorkersAIPlugin,
|
||||
CoherePlugin,
|
||||
DeepInfraPlugin,
|
||||
GatewayPlugin,
|
||||
GithubCopilotPlugin,
|
||||
GitLabPlugin,
|
||||
GooglePlugin,
|
||||
GoogleVertexAnthropicPlugin,
|
||||
GoogleVertexPlugin,
|
||||
GroqPlugin,
|
||||
KiloPlugin,
|
||||
LLMGatewayPlugin,
|
||||
MistralPlugin,
|
||||
NvidiaPlugin,
|
||||
OpencodePlugin,
|
||||
OpenAICompatiblePlugin,
|
||||
OpenAIPlugin,
|
||||
OpenRouterPlugin,
|
||||
PerplexityPlugin,
|
||||
SapAICorePlugin,
|
||||
TogetherAIPlugin,
|
||||
VercelPlugin,
|
||||
VenicePlugin,
|
||||
XAIPlugin,
|
||||
ZenmuxPlugin,
|
||||
DynamicProviderPlugin,
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const KiloPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("kilo"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("kilo")) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const LLMGatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("llmgateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("llmgateway")) return
|
||||
if (evt.provider.enabled === false) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
evt.provider.options.headers["X-Source"] = "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const MistralPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("mistral"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/mistral") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||
evt.sdk = mod.createMistral(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const NvidiaPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("nvidia"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("nvidia")) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
evt.provider.options.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user