fix(studio): enable local tool calling for OAI-compat remote models #7287
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-License-Identifier: AGPL-3.0-only | |
| # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. | |
| # Frontend PR gate: lockfile freshness, typecheck, build, and a bundle grep | |
| # that catches the 2026.5.1 chat-history regression at the JS level. | |
| # | |
| # biome runs as non-blocking for now: the codebase currently has accumulated | |
| # ~470 errors and ~1650 warnings against the existing biome config. Surfacing | |
| # the count in CI lets us drive it down without forcing a fleet-wide cleanup | |
| # in the same PR. Drop `continue-on-error` once that number is zero. | |
| name: Frontend CI | |
| on: | |
| pull_request: | |
| paths: | |
| - 'studio/frontend/**' | |
| - 'scripts/check_frontend_dep_removal.py' | |
| - 'tests/studio/test_frontend_dep_removal.py' | |
| - 'scripts/sync_allow_scripts_pins.py' | |
| - 'tests/studio/test_sync_allow_scripts_pins.py' | |
| - '.github/workflows/studio-frontend-ci.yml' | |
| push: | |
| branches: [main, pip] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| name: Frontend build + bundle sanity | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| defaults: | |
| run: | |
| working-directory: studio/frontend | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| # FIXME: drop this step once @assistant-ui/* and assistant-stream | |
| # leave 0.x -- on 1.x, caret ranges are conventional. Until then, | |
| # every 0.minor on this surface is a SemVer-major (this is exactly | |
| # how 2026.5.1 shipped a broken chat runtime: ^0.12.19 quietly | |
| # resolved to 0.12.28). | |
| - name: '@assistant-ui must be pinned exactly (no caret/tilde)' | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| set -e | |
| if grep -nE '"(@assistant-ui/[a-z-]+|assistant-stream)":[[:space:]]*"[\^~]' studio/frontend/package.json; then | |
| echo "::error file=studio/frontend/package.json::These packages must be pinned to exact versions until they leave 0.x. Drop the leading ^ or ~." | |
| exit 1 | |
| fi | |
| echo "All assistant-ui packages are pinned exactly." | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '22' | |
| # node 22 bundles npm 10.x, which predates allowScripts. Move to the | |
| # 11.x line and fail loudly if the gate is still missing, so the | |
| # strict flag below can never silently degrade into a warning. | |
| - name: Upgrade npm to 11.x (allowScripts enforcement) | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| npm install -g npm@^11 --no-fund --no-audit | |
| V=$(npm -v) | |
| case "$V" in | |
| 11.1[6-9].*|11.[2-9][0-9].*|1[2-9].*) echo "npm $V has allowScripts" ;; | |
| *) echo "::error::npm $V lacks allowScripts (need >=11.16)"; exit 1 ;; | |
| esac | |
| # Run the structural lockfile scan BEFORE npm ci. A compromised | |
| # tarball runs its `prepare` / `postinstall` during `npm ci`, | |
| # so any catch has to fire upstream of that. The scanner is | |
| # pure-Python read-only; safe to call ahead of every install. | |
| - name: Lockfile supply-chain audit (pre-install scan) | |
| working-directory: ${{ github.workspace }} | |
| run: python3 scripts/lockfile_supply_chain_audit.py | |
| # Dependency bumps strand the version-pinned allowScripts entries. | |
| # The paired pre-commit hook auto-fixes PRs; this is the backstop. | |
| - name: allowScripts pins must match the lockfile | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| python3 tests/studio/test_sync_allow_scripts_pins.py | |
| python3 scripts/sync_allow_scripts_pins.py --check | |
| - name: Lockfile must agree with package.json (npm ci is strict) | |
| # The vite 8 chain (rolldown, lightningcss, tailwind oxide) ships napi | |
| # binaries with no install scripts. The only script-bearing deps are | |
| # covered by `allowScripts` in package.json (npm >=11.16, default in | |
| # npm 12). The pre-install lockfile audit above stays the first line | |
| # of defence -- it fires before any tarball can run code. | |
| # --strict-allow-scripts: any unreviewed install script hard-fails | |
| # the job; the sync hook keeps the pins fresh after bumps. | |
| run: npm ci --strict-allow-scripts --no-fund --no-audit | |
| - name: npm ci must not have modified the working tree | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| if ! git diff --quiet -- studio/frontend; then | |
| echo "::error::npm ci modified files; commit the updated lockfile" | |
| git status -- studio/frontend | |
| exit 1 | |
| fi | |
| # Catch the common foot-gun: a dep dropped from package.json that is | |
| # still imported somewhere. The script walks the lockfile dep graph | |
| # from the new top-level deps and only counts top-level node_modules | |
| # paths as valid resolution targets for bare src/ imports. | |
| # | |
| # actions/checkout uses fetch-depth: 1 by default, so the base branch | |
| # is not available locally. Fetch the single base commit with an | |
| # explicit refspec so origin/<base> is reliably created (a bare | |
| # `git fetch origin <ref>` only updates FETCH_HEAD in some configs). | |
| - name: Dependency removal safety check | |
| if: github.event_name == 'pull_request' | |
| working-directory: ${{ github.workspace }} | |
| run: | | |
| git fetch --no-tags --depth=1 origin \ | |
| "${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" | |
| python3 scripts/check_frontend_dep_removal.py \ | |
| --base "origin/${{ github.base_ref }}" \ | |
| --enumerate-dead | |
| python3 tests/studio/test_frontend_dep_removal.py | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Build | |
| run: npm run build | |
| - name: Built bundle must not contain Unsloth's unstable_Provider call site | |
| run: | | |
| set -e | |
| JS=$(ls dist/assets/index-*.js | head -1) | |
| HITS=$(grep -c 'unstable_Provider:' "$JS" || echo 0) | |
| echo "main bundle: $JS" | |
| echo "unstable_Provider: hits=$HITS (assistant-ui internals contribute up to 3)" | |
| if [ "$HITS" -gt 3 ]; then | |
| echo "::error file=studio/frontend/src/features/chat/runtime-provider.tsx::Unsloth bundle still passes unstable_Provider through useRemoteThreadListRuntime; this is the 2026.5.1 chat-history regression. Pass adapters directly into useLocalRuntime instead." | |
| exit 1 | |
| fi | |
| - name: Bundle size budget (75 MB) | |
| run: | | |
| SIZE=$(du -sb dist | cut -f1) | |
| BUDGET=$((75 * 1024 * 1024)) | |
| echo "dist size: $SIZE bytes ($((SIZE/1024/1024)) MB), budget: $BUDGET bytes (75 MB)" | |
| if [ "$SIZE" -gt "$BUDGET" ]; then | |
| echo "::error::studio/frontend/dist/ exceeded the 75 MB budget. Drop dead deps (e.g. the unused next dep) or split chunks." | |
| exit 1 | |
| fi | |
| - name: Biome (non-blocking until accumulated drift is cleared) | |
| continue-on-error: true | |
| run: npm run biome:check | |
| - name: Upload built dist | |
| # Always upload so a green run is reviewable too -- the dist | |
| # output catches "tests passed but bundle changed unexpectedly" | |
| # regressions that would be invisible if we only kept artifacts | |
| # on failure. | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: studio-frontend-dist | |
| path: studio/frontend/dist | |
| retention-days: 3 |