Commit graph

5,302 commits

Author SHA1 Message Date
danielhanchen
a4e63ec997 ci: retrigger Windows Studio UI after transient Playwright tab-lookup flake 2026-05-17 09:18:43 +00:00
danielhanchen
10b50c84df studio: skip tilelang on HIP / ROCm torch (Strix Halo crash report)
h34v3nzc0dex tested PR 5434 on Strix Halo (gfx1151, ROCm 7.13,
torch 2.11.0+rocm7.13.0) and hit a hard regression:

  File ".../fla/ops/common/backends/tilelang/__init__.py", line 92,
    in chunk_bwd_dqkwg
  File ".../tilelang/jit/kernel.py", line 137, in __init__
  File ".../tilelang/tileop/gemm/__init__.py", line 143,
    in _select_gemm_instruction
  tvm.error.InternalError: Check failed: (0) is false:
    Unsupported target for gemm:
    hip -keys=hip,gpu -mcpu=gfx1151 ...

`tilelang==0.1.8` ships no HIP GEMM instruction; `_select_gemm_instruction`
raises at lower-time, not import-time. So:
  - pip install succeeds
  - `import tilelang` succeeds
  - `TileLangBackend.is_available()` returns True
  - FLA's dispatcher picks TileLang for `chunk_bwd_dqkwg`
  - training subprocess dies at first GDN backward, no graceful fallback

The PR's existing platform gate (`_tilelang_platform_supported`)
checked only `sys.platform == "linux"` and `platform.machine()`, both
of which look identical on a ROCm box.

Fix has two layers:

1. INSTALL GATE: new `_torch_has_hip()` helper checks
   `torch.version.hip is not None`. `_tilelang_platform_supported`
   now returns False on HIP torch, so the install never fires.

2. RUNTIME GATE: even with the install skipped, a user could have
   tilelang already present (e.g. venv carried over from a CUDA box).
   `_install_fast_path_hooks` now calls
   `os.environ.setdefault("FLA_TILELANG", "0")` when HIP is detected,
   which is the env-var FLA's `TileLangBackend` already honors. Users
   who know they have a HIP-aware tilelang fork can override by
   setting `FLA_TILELANG=1` explicitly.

This costs nothing on CUDA (the gate is a no-op when
`torch.version.hip is None`), and removes the crash for AMD users.
The benchmark numbers in the PR description (1.43x on B200 sm_100)
are not affected.

The other halves of the PR are confirmed working on gfx1151 by the
same report:
  - `flash-linear-attention 0.5.0` runs at production scale
    (B=1 T=8192 H=16 K=128 V=128 and others) with no patches.
  - `causal-conv1d` runs at the shapes the fast-path gate cares
    about. (A separate Ubuntu 24.04 `--gcc-install-dir` build
    workaround is needed for the source-build path; that mirrors
    bbf004c's llama.cpp fix and is out of scope here.)

Tests added:
  - test_tilelang_platform_unsupported_on_hip_torch
  - test_tilelang_install_skipped_on_hip_torch
  - test_install_fast_path_hooks_sets_fla_tilelang_zero_on_hip
  - test_install_fast_path_hooks_respects_user_fla_tilelang_override
  - test_install_fast_path_hooks_does_not_set_fla_tilelang_on_cuda

Total 50 passing (was 45).
2026-05-17 08:43:24 +00:00
danielhanchen
379cbb23eb Merge branch 'studio-fla-tilelang-qwen3.5' of https://github.com/unslothai/unsloth into studio-fla-tilelang-qwen3.5 2026-05-17 01:47:07 +00:00
danielhanchen
d73935c4b7 ci: retrigger Mac Studio GGUF after transient HF DNS resolve flake 2026-05-17 01:47:02 +00:00
pre-commit-ci[bot]
800fc98a52 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-17 01:26:40 +00:00
danielhanchen
3913a66119 Merge branch 'studio-fla-tilelang-qwen3.5' of https://github.com/unslothai/unsloth into studio-fla-tilelang-qwen3.5
# Conflicts:
#	studio/backend/tests/test_training_worker_flash_attn.py
2026-05-17 01:25:47 +00:00
danielhanchen
85cdafc184 studio: fix double-install of tilelang on the FLA hook install path
Backend CI surfaced a test-isolation bug introduced by the
post_available_fn mechanism for finding #7. The wrapper ran
`post_available_fn` in BOTH paths (install ran AND gate already True),
but `_fla_install` already chains tilelang on the install path, so the
post-available step then called tilelang install AGAIN.

This was masked locally because tilelang was installed in the
workspace venv (post_available short-circuited on
`_tilelang_importable()` returning True). CI starts with no tilelang,
so the second call actually fired and the mock recorded two calls.

Fix: only run `post_available_fn` when the install path did NOT run.
That preserves the finding #7 semantics (tilelang repair when FLA
already True but tilelang missing or tvm-ffi broken) without
duplicating the chained install on the gate-was-False path.

Also tightened `test_hook_skips_install_when_gate_already_true` to
monkeypatch `_tilelang_importable=True` and
`_installed_tvm_ffi_version=0.1.9` so it stays a pure "no install at
all" test regardless of the venv's actual state.
2026-05-17 01:25:25 +00:00
pre-commit-ci[bot]
78b07a286c [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-17 00:49:44 +00:00
danielhanchen
69811499b6 Merge branch 'studio-fla-tilelang-qwen3.5' of https://github.com/unslothai/unsloth into studio-fla-tilelang-qwen3.5
# Conflicts:
#	studio/backend/core/training/worker.py
#	studio/backend/tests/test_training_worker_flash_attn.py
2026-05-17 00:49:06 +00:00
danielhanchen
29e9f318dd studio: address reviewer.py n=12 findings on the FLA hook path
Eight issues reproduced by parallel reviewers against 6ce495a; all
fixed and covered by regression tests. 45 pytest cases pass (was 36);
end-to-end Qwen3.5_MoE modeling-import drill still loads all five
fast-path symbols.

P1 fixes:

1. TileLang loses the Qwen-family guard on the normal FLA hook path
   (10/12 reviewers, reproduced with allenai/OLMo-Hybrid-1B). The
   hook unconditionally installed tilelang for any FLA-using model.
   - Threaded `model_name` through `_install_fast_path_hooks(event_queue,
     model_name)`.
   - `_fla_install` now gates tilelang on
     `_model_wants_tilelang(model_name)` AND a successful FLA install.

2. TileLang repair `--force-reinstall` (without `--no-deps`) could
   replace `torch==2.12.0+cu130` with `torch==2.12.0`. Split repair
   into TWO steps:
     step 1: `--force-reinstall --no-deps apache-tvm-ffi==0.1.9`
     step 2: regular install of tilelang + apache-tvm-ffi
   Step 1 surgically downgrades the broken package; step 2 resolves
   missing transitive deps (z3-solver, ml-dtypes) without
   --force-reinstall, so it never replaces torch.

3. Hook could return True after the installer's deep import probe
   failed: when pip exits 0 but `import fla.modules` raises, the old
   wrapper re-called `original()` (transformers' metadata check) and
   trusted it. Refactored:
     - `_ensure_flash_linear_attention_unconditional(...) -> bool`
     - `_ensure_tilelang_backend_unconditional(...) -> bool`
   The wrapper now uses the installer's bool directly.

4. SSM models (Nemotron-H, Falcon-H1, Granite-H) use
   `lazy_load_kernel("causal-conv1d")` and never call
   `is_causal_conv1d_available()`, so the hook never fires for them.
   The orchestrator now always runs `_ensure_causal_conv1d_fast_path`
   outside the hook-mode if/else.

P2 fixes:

5. `_rebind_in_already_imported_modules` invoked transformers' lazy
   module `__getattr__` (hundreds of "Accessing X from .models..."
   warnings, ~3.4s overhead). Switched to `module.__dict__.get(...)`
   which only sees real module-level bindings.

6. TileLang installed even when FLA was skipped (Torch <2.7) or
   failed (timeout, post-install probe failed). Now gated on the
   installer's bool return.

7. TileLang repair was skipped when FLA was already True but tilelang
   missing or apache-tvm-ffi on the broken list. Added an optional
   `post_available_fn` to the wrapper; the FLA hook's
   `_fla_post_available` runs `_ensure_tilelang_backend_unconditional`
   when (model wants tilelang) AND (tilelang missing OR tvm-ffi broken).

8. `_flash_linear_attention_importable()` only checks deep import,
   not version. Added `_flash_linear_attention_current()` that
   compares against the pinned `flash-linear-attention==0.5.0` /
   `fla-core==0.5.0`; older versions trigger `--force-reinstall
   --no-deps` so torch stays untouched.

Helpers extracted to keep the surface tight:
  - `_pip_install_cmd(*args)` builds `uv pip install` or
    `python -m pip install` depending on uv availability.
  - `_run_pip(cmd, event_queue, label)` runs a pip command with
    timeout / failure handling and a status emission.

Regression tests added:

  - test_hook_does_not_install_tilelang_for_non_qwen_fla_model
  - test_hook_does_install_tilelang_for_qwen35
  - test_tilelang_repair_does_not_touch_torch_cuda_stack
  - test_hook_trusts_installer_bool_not_metadata
  - test_rebind_does_not_trigger_module_getattr
  - test_hook_skips_tilelang_when_fla_install_is_skipped
  - test_hook_runs_tilelang_repair_when_fla_already_true
  - test_fla_installer_force_reinstalls_when_older_version_present
  - test_run_training_process_eagerly_installs_causal_conv1d_in_normal_mode

Existing tests updated for the new `_install_fast_path_hooks` signature
and the two-step tilelang repair flow.

End-to-end re-verified against transformers.models.qwen3_5_moe:
PRE_STATE fla=False, hook fires for both gates, FLA + tilelang +
causal-conv1d install, all 5 fast-path symbols non-None.
2026-05-17 00:47:39 +00:00
danielhanchen
8e7859d4b7 Merge remote-tracking branch 'origin/main' into studio-fla-tilelang-qwen3.5 2026-05-17 00:32:43 +00:00
Daniel Han
aba57d0872
disable_torchcodec_if_broken: also patch datasets and clean sys.modules (#5483)
* disable_torchcodec_if_broken: also patch datasets and clean sys.modules (#5446)

transformers's _torchcodec_available was being flipped to False already,
but datasets keeps its own datasets.config.TORCHCODEC_AVAILABLE flag
(datasets >= 4.0) that gates every torchcodec call site inside
datasets/features/{audio,video}.py, datasets/features/features.py and the
three datasets formatters. Without flipping that flag, transformers
falls back to librosa but datasets still routes through torchcodec and
re-raises the same RuntimeError, which is what users hit on Colab when
libavutil is missing.

Also pops half-loaded torchcodec submodules + datasets.features._torchcodec
from sys.modules so a later re-import does not re-trigger the failed
native-library dlopen.

Verified live state after the patch on a Colab-like broken-torchcodec env:
  transformers._torchcodec_available     = False
  transformers.is_torchcodec_available() = False
  datasets.config.TORCHCODEC_AVAILABLE   = False
  stale torchcodec entries in sys.modules = []

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* disable_torchcodec_if_broken: seat sys.modules[torchcodec]=None sentinel

The first commit on this branch flipped the transformers and datasets
availability flags, but a few unconditional torchcodec call sites in
datasets / torchaudio (e.g. Audio.encode_example does
"from torchcodec.encoders import AudioEncoder" outside the
TORCHCODEC_AVAILABLE gate) still hit a cryptic RuntimeError from the
broken native library load.

Seating sys.modules["torchcodec"] = None makes any subsequent
"import torchcodec" / "from torchcodec.X import Y" raise
ModuleNotFoundError (subclass of ImportError) which the existing
try/except ImportError blocks in datasets / torchaudio catch and
re-raise as the clean "please install torchcodec" message users get
when they uninstall torchcodec manually. This was the workaround in
issue #5446.

Also makes find_spec("torchcodec") return None on re-entry, so the
function is a strict no-op on the second call.

Verified across 12 scenarios in temp/torchcodec_test/: healthy
torchcodec untouched, broken torchcodec sentinel-blocked, datasets 3.x
without the flag handled, no-datasets-installed handled, py3.11 Colab-
exact env (transformers==4.56.2 + trl==0.22.2) handled. Side-by-side
on the user's exact stack: Audio.encode_example switches from
"RuntimeError: Could not load libtorchcodec" to
"ImportError: To support encoding audio data, please install 'torchcodec'.".

* disable_torchcodec_if_broken: trim comments

Same behaviour, shorter docstring and inline comments.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-16 16:24:48 -07:00
Lee Jackson
bc28a5d80e
Studio: code execution config visual polish (#5471)
* style: polish code execution

* studio/chat: optimistic insert for created OpenAI containers

Container creation now prepends the row immediately with a "Creating"
pill instead of waiting on /v1/containers, which is eventually
consistent and can lag the create response by several seconds.
A 5s follow-up refresh reconciles with the server view.

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
2026-05-16 21:14:01 +04:00
Lee Jackson
62def21608
Studio: auto-load models when adding a cloud provider (#5472)
* studio: auto-load models when adding a cloud provider

* fix: drop auto-load

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
2026-05-16 19:14:10 +04:00
Daniel Han
f7069b242c intel-gpu: add xpu extras for torch 2.7.1 / 2.9.1 / 2.11.0 / 2.12.0
Builds on the intelgputorch210 fix in the previous commit by also
filling in the four other intel-gpu-torch* wheel stacks that PyTorch
has published but Unsloth was not yet exposing:

* intelgputorch271 / intel-gpu-torch271:
    torch 2.7.1+xpu  with pytorch-triton-xpu 3.3.1
* intelgputorch291 / intel-gpu-torch291:
    torch 2.9.1+xpu  with pytorch-triton-xpu 3.5.0
* intelgputorch2110 / intel-gpu-torch2110:
    torch 2.11.0+xpu with triton-xpu 3.7.0
* intelgputorch2120 / intel-gpu-torch2120:
    torch 2.12.0+xpu with triton-xpu 3.7.1

Each torch + triton pair was cross-checked against the wheel's PEP 658
.metadata sidecar to confirm the upstream Requires-Dist matches what
this block pins. Verified via `uv pip compile` across cp310/cp311/cp312/
cp313 on Linux x86_64 and Windows AMD64.
2026-05-16 06:18:38 -07:00
stoicAI1776
e87c0dd418 fix(pyproject): use triton-xpu 3.6.0 for intelgputorch210 (#4931) 2026-05-16 06:18:38 -07:00
pre-commit-ci[bot]
d2d758d0d8 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-16 12:55:53 +00:00
danielhanchen
6ce495a42d studio: hook transformers' fast-path gates for just-in-time FLA + causal-conv1d install
The substring-based detection in this PR (`_model_wants_tilelang` /
`_model_wants_causal_conv1d`) is brittle: it depends on what the user
typed for the model name, not on what the architecture actually needs.
Users typing custom model paths, future Qwen3.7 / non-Qwen GDN
architectures, and any model whose author renamed it would silently
fall back to the torch loop.

The correct signal is the one transformers itself uses to gate the
fast path. `transformers/models/qwen3_5_moe/modeling_qwen3_5_moe.py`
does at module import time:

    if is_causal_conv1d_available():
        from causal_conv1d import causal_conv1d_fn, causal_conv1d_update
    if is_flash_linear_attention_available():
        from fla.modules import FusedRMSNormGated
        from fla.ops.gated_delta_rule import (
            chunk_gated_delta_rule, fused_recurrent_gated_delta_rule,
        )

Wrap both gates so the first call (always at modeling import, before
any forward pass) installs the matching kernel synchronously and
delegates to the original function. Any model whose architecture
queries those gates auto-triggers the install; models that never
query them (Llama, Gemma, dense Qwen, ...) never pay the cost.

Mechanics:

  - Split `_ensure_flash_linear_attention` and `_ensure_tilelang_backend`
    into `_unconditional` variants (no substring gate, retains python
    / torch / platform / skip-env guards) plus thin substring wrappers
    used by the legacy fallback path.
  - New `_install_fast_path_hooks(event_queue)` patches both gates on
    `transformers.utils.import_utils` AND sweeps `sys.modules` so any
    modeling file that already did `from ... import is_X` sees the
    wrapper (the local binding survives a module-level reassignment).
  - Wrappers clear the original's `lru_cache` before delegating, install
    on False, re-check, and short-circuit on subsequent calls.
  - Set `UNSLOTH_STUDIO_SKIP_FAST_PATH_HOOKS=1` to fall back to the
    substring path.

Verified end-to-end against `transformers.models.qwen3_5_moe`:

  PRE_STATE fla=False tilelang=False causal_conv1d=False
  HOOK_INSTALLED
  Hook fired for is_causal_conv1d_available; installing kernel...
  Installing prebuilt causal-conv1d wheel...
  Hook fired for is_flash_linear_attention_available; installing kernel...
  Installing flash-linear-attention==0.5.0 (with fla-core==0.5.0) for the fast path...
  Installed flash-linear-attention for the FLA fast path
  Installing TileLang backend (apache-tvm-ffi==0.1.9, tilelang==0.1.8)...
  Installed TileLang backend for FLA fast path
  MODELING_IMPORT_OK
  FAST_PATH_SYMBOLS {"chunk_gated_delta_rule": true,
                     "fused_recurrent_gated_delta_rule": true,
                     "FusedRMSNormGated": true,
                     "causal_conv1d_fn": true,
                     "causal_conv1d_update": true}
  POST_STATE fla=True tilelang=True causal_conv1d=True

Adds 9 new tests covering: install-on-False, skip-on-True, idempotency,
install-failure handling, env-disable, lru_cache clear, sys.modules
rebind, missing-transformers fallback, substring fallback. Total
test count is now 36 (was 27).
2026-05-16 12:54:44 +00:00
Daniel Han
d2e25ee131
studio/frontend: drop unused dependencies, move type pkg to devDeps (#5477)
* studio/frontend: drop unused dependencies, move type pkg to devDeps

Removes 11 declared deps that are not imported anywhere in src/, the
Tauri config, src-tauri Rust, backend, scripts, CI workflows, or
sibling workspaces. Moves @types/canvas-confetti to devDependencies
since it ships TypeScript types only.

Removed from dependencies:
  @assistant-ui/react-markdown   (no imports; not a peer of any used pkg)
  @assistant-ui/react-streamdown (no imports; not a peer of any used pkg)
  @langchain/core                (no imports anywhere)
  @streamdown/cjk                (no imports; not a peer of streamdown)
  @radix-ui/react-checkbox       (re-exported by the radix-ui umbrella;
                                  no direct imports)
  @radix-ui/react-label          (same)
  @radix-ui/react-select         (same)
  @radix-ui/react-separator      (same)
  date-fns                       (already a direct dep of react-day-picker)
  remark-gfm                     (already a direct dep of streamdown)

Removed from devDependencies:
  playwright                     (CI installs the pip playwright; the
                                  npm one is unused)

Moved to devDependencies:
  @types/canvas-confetti         (TypeScript types only; not a runtime dep)

Verified with npm install + npm run build (tsc -b && vite build),
clean exit, dist/ produced. Live unsloth studio launch returns 200
on /, on the main JS / CSS bundles, and on /api/health.

* studio/frontend: keep @radix-ui packages (per maintainer)

Maintainer asked to keep the four @radix-ui packages this PR was
originally dropping:

  @radix-ui/react-checkbox  ^1.3.3
  @radix-ui/react-label     ^2.1.8
  @radix-ui/react-select    ^2.2.6
  @radix-ui/react-separator ^1.1.8

Restored to dependencies and refreshed the lockfile. Build still
green (1044 packages, vite build 2.1s, same dist contents).
2026-05-16 05:49:23 -07:00
Daniel Han
44989ea2cb
ci: deterministic check for studio/frontend dep removals (#5478)
* ci: deterministic check for studio/frontend dep removals

Adds a CI gate that catches the common foot-gun: a dep dropped from
studio/frontend/package.json that something in src/ still imports.

scripts/check_frontend_dep_removal.py
  Diffs package.json against a git base ref, collects every package
  no longer declared, and for each one:
    1. Greps the entire repo for any usage pattern (static / dynamic /
       side-effect imports, require, CSS @import, HTML script/link
       src, new URL(), triple-slash references, template literals,
       bare quoted strings in JS-like files).
    2. Resolves whether the package would still install by BFS'ing
       the dep graph in the new lockfile starting from the new
       package.json's declared deps (so a stale lockfile does not
       give false OK-via-transitive results).
    3. Distinguishes top-level node_modules/<name> from nested copies
       under other packages. Bare src/ imports only resolve to the
       top-level path.
    4. Pip-installed playwright references are filtered, so removing
       the npm playwright (CI uses the pip one) is reported correctly.

  Additional hygiene checks (warnings, fail with --strict):
    - lockfile <root> dep map matches package.json (catches drift).
    - @types/X is not orphaned when X is no longer declared.
    - No src/ import points at a package not declared in any field.

tests/studio/test_frontend_dep_removal.py
  24 deterministic cases. Each patches a copy of the head
  package.json, runs the script, and asserts (exit status,
  reported FAIL list). Covers:
    - Genuinely-breaking removals: next-themes, @xyflow/react,
      @huggingface/hub, dexie, motion, canvas-confetti, recharts,
      node-forge, mammoth, unpdf.
    - Safe-via-transitive removals: katex, clsx, react,
      @radix-ui/react-slot, zustand, tailwind-merge, remark-gfm,
      date-fns, js-yaml, @tauri-apps/api.
    - Mixed multi-removal failing on the unsafe entries only.
    - Non-existent / not-in-base names (no-op).
    - Move from deps to devDeps (not a removal).

.github/workflows/studio-frontend-ci.yml
  Runs the checker on pull_request events against
  origin/${{ github.base_ref }}, plus the edge-case suite.

* scripts: harden frontend dep removal check + adversarial suite

classify() now catches sneaky shapes that an earlier line-only scan
would miss:
  - multi-line `import { a, b } from "pkg"` and the same shape for
    `export { ... } from "pkg"` / `export * from "pkg"` /
    `export type ... from "pkg"`.
  - JSDoc `@import("pkg")` references.
  - Word-boundary fix so `foo` no longer matches `foobar` (subpath gate:
    after the package name we require closing quote or `/`).
  - Negative-lookbehind on `(?<!@)\bimport\b` so CSS `@import "X"` is
    classified as css_import, not side_effect_import.

find_usage() now feeds an 8-line window (4 above / 4 below the grep
hit) into classify() so multi-line import statements are picked up
even though the initial grep is line-based.

tests/studio/test_frontend_dep_removal.py now exercises three suites:
  - 24 edge cases: subprocess-driven, full-pipeline.
  - 28 classify() unit cases: direct function call against hand-crafted
    snippets. Covers static / side-effect / dynamic / require /
    css_import / html_script / html_link / re_export (4 variants) /
    template_literal / new_url / tsc_triple_slash / jsdoc_import /
    string_literal, plus false-positive guards (substring collision,
    plain-text comments, URL path tails, Python files, markdown).
  - 12 adversarial cases: write synthetic files under
    studio/frontend/src/__dep_check_adversarial__/, run the full
    script, then clean up. Confirms multi-line imports, re-exports,
    JSDoc @import, new URL, dynamic imports all FAIL when the
    underlying package is removed.

Current total: 64 / 64 cases pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* scripts: detect bin references in package.json scripts

Catches the last common false-negative: removing a package whose
bin is only referenced through `package.json` scripts (e.g. dropping
typescript while `"build": "tsc -b && vite build"` calls tsc).

Cross-checked the patterns Vercel/Next.js, Vite, and TanStack use
in their own manifests; the bin/scripts pairing is the one
consumer-side pattern dep checkers commonly miss.

How it works:
  - Build a bin-to-package map from each lockfile entry's `bin`
    field. The map is global so a stale lockfile still resolves
    bins from packages about to be pruned.
  - Tokenize each script value, splitting on `&&`, `||`, `;`, `|`.
    Strip env-var assignments and `npx / pnpx / yarn / pnpm / bunx`
    prefixes, plus `./node_modules/.bin/` and `node_modules/.bin/`
    path prefixes. Look up the leading token in the bin map.
  - Hits are reported as `script_bin` and feed the same reachability
    gate as source imports. A bin still installed transitively
    (e.g. vite via @vitejs/plugin-react peer) is OK-via-transitive;
    an orphaned bin is FAIL.

Test additions:
  - 5 new edge cases: removing vite, typescript, eslint, @biomejs/biome,
    and (@biomejs/biome + @vitejs/plugin-react) together. Correctly
    flags @biomejs/biome and the combo as FAIL while vite / typescript
    / eslint are kept by peers.
  - 8 new classify() unit cases: TypeScript ambient `declare module`,
    namespace imports, combined default+named, default-as-named,
    re-export default (4 forms), `.then()` dynamic imports without
    await, and TypeScript `import()` in type position.

Current total: 29 edge + 36 classify-unit + 12 adversarial = 77 / 77.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* scripts: detect package.json field references to packages

After surveying package.json patterns in 10+ popular repos (React,
Vue/Svelte/Astro/Next.js, Vite, Storybook, TanStack/Query, Tailwind,
ESLint, TypeScript, Prettier, SvelteKit), several config fields in
package.json itself can reference packages by string. My checker
filtered all of package.json out of the string_literal fallback,
so removing a package that is only referenced from one of these
fields was a false negative.

Now covered (new pkg_json_field kind):
  - overrides / resolutions / pnpm.overrides keys
  - pnpm.patchedDependencies keys
  - peerDependenciesMeta keys
  - prettier: "@my/prettier-config" string
  - eslintConfig.extends (string or array)
  - stylelint.extends / stylelint.plugins
  - babel.presets / babel.plugins
  - jest.preset / jest.setupFiles / jest.transform
  - commitlint.extends
  - renovate.extends
  - remarkConfig.plugins
  - any other tool config field whose strings/keys equal the pkg
    name or `pkg/subpath`

False-positive guards (do not flag string values inside):
  - browserslist (browser queries)
  - keywords (free-form strings)
  - engines / engineStrict / packageManager / volta (version pins)
  - files / directories / publishConfig (paths)
  - workspaces (paths/globs)
  - main / module / browser / types / typings / exports / imports /
    bin / man (author-side fields)
  - scripts (already handled separately via scripts_bin_refs)
  - name / version / description / author / repository / homepage etc.

Test additions: new PkgFieldCase suite with 19 cases covering each
tool config field, subpath references, and the 5 false-positive
guards. Combined with the existing 29 edge / 36 classify / 12
adversarial cases, the suite is 96 / 96.

* scripts: enumerate dead deps in studio/frontend

Adds an opt-in dead-dep enumeration to the existing safety check.
Iterates every package declared in studio/frontend/package.json
(all four dep fields combined) and reports each as one of:

  used               at least one detected reference -- in src/, a
                     config file, package.json scripts (bin), a
                     package.json tool-config field (overrides /
                     prettier / eslintConfig / stylelint / babel /
                     jest / commitlint / renovate / etc.), or
                     tsconfig.compilerOptions.types

  unused             no detected reference anywhere

  type_pkg_kept      @types/X where X is still declared (or X = node,
                     always implicit)

  type_pkg_orphan    @types/X where X is no longer declared --
                     candidate for removal alongside X

Wiring:
  - New CLI flag `--enumerate-dead` (off by default).
  - CI workflow now passes `--enumerate-dead` so the report shows on
    every PR run; the report is informational unless `--strict` is
    also set.
  - With `--strict`, unused / type_pkg_orphan entries fail the run.

Tests:
  - 5 new EnumCase scenarios:
    E01 fake dep with no usage -> reported unused
    E02 fake dep imported by a synthetic src file -> reported used
    E03 fake dep referenced only in overrides -> reported used
    E04 @types/X paired with X (also imported) -> kept
    E05 @types/X without X -> orphan

Running the new flag against the current main reproduces exactly the
11 deps PR #5477 removed, validating the heuristic end to end.

Current total: 29 edge + 36 classify + 12 adversarial + 19 pkg-json
field + 5 enumeration = 101 / 101.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ci: fetch base ref before running dep removal safety check

actions/checkout uses fetch-depth: 1 by default, so when the
dependency removal check ran `git show origin/main:.../package.json`
the ref wasn't available locally and the script exited 2 with
"could not read base package.json at origin/main:...".

Fetch the single base commit before invoking the check so the
git-show lookup resolves. --depth=1 keeps the extra fetch cheap.

* ci: address bot review on PR 5478

Five issues flagged across gemini and codex:

  * --base-lock argparse arg was defined and advertised in the
    docstring, but main() always read args.head_lock in both branches
    -- the flag did nothing. Dropped the dead arg and the misleading
    docstring line; the lockfile-reachability analysis only needs the
    head lockfile.

  * lock_resolvable() was defined but never called. Removed.

  * read_pkg_file() did not specify an encoding for read_text().
    Added encoding="utf-8" for cross-platform stability.

  * read_pkg_file() returned {} when the path did not exist, so a
    bad --head-lock value silently bypassed the reachability checks
    (false PASS for removals that resolve through npm script bins).
    main() now exits 2 with a clear message when the head lockfile
    is missing, matching the existing behavior for the head pkg.

  * studio-frontend-ci.yml pull_request paths filter only matched
    studio/frontend/** and the workflow file, so PRs that modified
    the checker script or its test could skip this job. Added both
    files to the trigger.

* ci: address 10x reviewer findings on dep removal safety check

Eight P1s and three P2s surfaced across 10 codex reviewers; this
commit addresses all of them.

P1s:

1. Workflow refspec. `git fetch --depth=1 origin <base_ref>` may only
   create FETCH_HEAD in shallow PR checkouts; the checker then dies
   with `fatal: invalid object name 'origin/main'`. Use the explicit
   refspec `<base>:refs/remotes/origin/<base>` so origin/<base> is
   reliably created.

2. `_deps_of()` was counting optional peer dependencies as reachable.
   npm only installs an optional peer when another package declares
   the same dep, so for "is this removed package still in the tree"
   they cannot keep it alive on their own. Skip entries marked
   `optional: true` in `peerDependenciesMeta`.

3. JS-syntactic classifiers (static_import, side_effect_import,
   dynamic_import, require, re_export, jsdoc_import, template_literal,
   tsc_triple_slash, new_url) now gate on file extension. Previously
   only the final string-literal fallback was gated, so a JS-shaped
   string inside a Python fixture or a Markdown code fence triggered
   a false FAIL. Added U37-U40 covering .py / .md / .sh / .yml.

4. HTML `<script src=>` and `<link href=>` patterns now respect a
   package-name boundary so `/node_modules/foo-extra/...` is not
   treated as a usage of `foo`. Added U41-U43.

5. New `find_command_usage()` detects CLI invocations in .sh / .yml
   / .yaml / .ps1 / .bat / Dockerfile* (npx pkg, bunx pkg, pnpm exec
   pkg, yarn dlx pkg, or a bare pkg --flag). Also covers scoped CLI
   packages exposed by their unscoped tail (@biomejs/biome -> biome).

6. `build_bin_to_pkg(head_lock)` was losing the bin -> package map
   for packages the PR correctly removed from the lockfile, so
   `scripts.biome:check` no longer flagged when @biomejs/biome was
   being dropped. Now also read the base lockfile (via `git show` or
   the new `--base-lock` override) and layer its bin map on top for
   any package in the removed set.

7. `--strict` now runs hygiene checks (lockfile sync, @types
   orphans, undeclared imports, dead-deps) on the no-removal path
   too. Previously the early return at "[OK] no dependencies removed"
   skipped them, so `--strict` silently passed on a tree with
   uncommitted lockfile drift or unused deps.

8. Removed `@types/X` packages are now matched against the runtime
   target name `X`: `/// <reference types="X" />`, tsconfig
   compilerOptions.types entries, AND runtime `import "X"` shapes.
   Handles the npm scope encoding (`@types/foo__bar` -> `@foo/bar`).

P2s:

9. CSS `url(...)` now accepts both quoted and unquoted forms (added
   U44-U45). The previous regex required `/{pkg}/` after a slash,
   missing bare-package urls like `url(katex/fonts/x.woff2)`.

10. `find_imports_without_decl()` now covers all static-import
    shapes: `import "pkg"`, `import Foo from "pkg"`,
    `import { Foo } from "pkg"`, `import type { Foo } from "pkg"`,
    `await import("pkg")`, `require("pkg")`.

11. (Same as #8.) Removed `@types/X` is also linked to runtime
    imports of `X`, not just type-only references.

Test suite expanded from 101 to 110 cases; all pass. Real-world
enumerate-dead still flags the same 11 unused packages on
studio/dep-removal-safety-check (matches PR 5477's removal set).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ci: address 4x Opus reviewer findings on dep removal check

Three blockers from the parallel Opus review batch:

1. scripts_bin_refs ignored every script that began with a wrapper.
   The original "first non-env token wins" heuristic credited
   cross-env / dotenv / dotenvx / env-cmd as the bin, so a script like
   `cross-env CI=1 biome check` left @biomejs/biome looking unused.
   Rewrote into _next_real_bin(), which peels env prefixes, the
   leading package-manager runner (npx / pnpx / bunx / pnpm exec /
   yarn dlx), and the known wrapper bins (with --/-flag-arg handling)
   before returning the real CLI. shlex tokenization preserves quoted
   env values like `FOO="a b"`.

2. enumerate_dep_usage skipped find_command_usage. The non-enumerate
   path already credited deps used only from CI / Dockerfile / shell
   scripts, but `--enumerate-dead` did not, so packages referenced
   only from a workflow were silently listed as dead. Added the same
   call (gated against @types/* to avoid the unscoped-tail false
   positive).

3. classify multi-line window was ±4 lines. Prettier formats long
   named-import lists one identifier per line, so a 20-import block
   pushed the `import` keyword out of the window and the dep dropped
   to the string-literal fallback (or worse, was missed entirely).
   Widened to ±25 -- still bounded enough to keep false-positives
   negligible, wide enough for the realistic Prettier ceiling.

Tests: added 10 _next_real_bin unit cases + 4 scripts_bin_refs
end-to-end cases (W01-W10 + I01-I04) and a 22-identifier multi-line
import adversarial case (A13). Full suite: 125/125.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-16 05:46:22 -07:00
danielhanchen
66dface7d7 studio: pin packaging + triton with FLA --no-deps install
An end-to-end install simulation in a fresh venv caught a real
regression: `fla/utils.py` does `from packaging import version` and
`import triton` at module load, but fla-core's METADATA only declares
einops + torch. With `--no-deps` the worker would land FLA in any
runtime that lacks packaging (e.g. minimal torch builds) and the
post-install import probe would fall back to the torch GDN loop
silently.

Add `packaging` and `triton` to `_FLA_RUNTIME_DEPS` so the install
spec list always carries them. Tests updated to assert both are now in
the install command.
2026-05-16 11:25:09 +00:00
pre-commit-ci[bot]
27dc546356 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-16 09:43:41 +00:00
danielhanchen
0f246a6c95 studio: address reviewer.py P1/P2 findings on FLA + tilelang installers
Twelve-reviewer aggregated review on this PR flagged several real
correctness bugs in the first hardening pass. Fixes:

P1:
  * Add UNSLOTH_STUDIO_SKIP_FLA_INSTALL escape hatch for symmetry
    with UNSLOTH_STUDIO_SKIP_TILELANG_INSTALL and the existing
    UNSLOTH_STUDIO_SKIP_FLASHATTN_INSTALL.
  * Install einops alongside fla-core. `--no-deps` was suppressing
    fla-core's only non-torch runtime dep, so on a clean venv
    `import fla.modules` raised ModuleNotFoundError even though pip
    exited 0.
  * Drop --no-deps from the tilelang force-reinstall path. tilelang
    needs z3-solver, ml-dtypes, cloudpickle, etc. at runtime;
    --force-reinstall --no-deps left libz3.so missing and
    `import tilelang` raised OSError on the next training subprocess.
  * Skip FLA install when installed torch is below 2.7.0
    (fla-core declares torch>=2.7.0). Otherwise users on Studio's
    supported torch 2.4/2.5/2.6 stacks get an incompatible FLA
    installed silently.

P2:
  * Replace bare `except ImportError` probes with helpers that catch
    `Exception` so a broken native package (OSError on missing
    .so, RuntimeError in __init__, ...) does not kill the worker
    before the fallback path can run.
  * Tighten the tilelang platform guard from "any linux" to
    "linux + machine in {x86_64, aarch64, ...}" so ppc64le / s390x /
    armv7 do not fall through and download the 93 MB tilelang sdist.
  * Add --only-binary=:all: to the tilelang install command. The
    comment already said we never want the sdist; now the pip
    invocation enforces it.
  * Verify both FLA and tilelang are importable after pip exits 0;
    if not, report and continue on the fallback path.

6 new tests bring the suite to 27 passing (was 21).
2026-05-16 09:43:15 +00:00
pre-commit-ci[bot]
d137a67c91 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-05-16 07:03:44 +00:00
danielhanchen
3fde3439e8 studio: harden FLA + tilelang installers per reviewer feedback
Addresses bot review on #5434:

  * Narrow `_ensure_flash_linear_attention` from `_model_wants_causal_conv1d`
    (which also matches Nemotron-H / Falcon-H1 / Granite-H / LFM2) to
    `_model_wants_tilelang` (Qwen3.5 / Qwen3.6 / Qwen3-Next only). True
    SSM families take the mamba_ssm path and never call FLA's GDN
    kernels, so installing FLA there is wasted bandwidth.

  * Pin both `flash-linear-attention==0.5.0` and `fla-core==0.5.0` and
    install with `--no-deps`. Otherwise pip resolves fla-core's
    declared `torch>=2.7.0` requirement and may silently upgrade the
    Studio venv's torch on environments running torch 2.4/2.5/2.6.

  * Skip both installs on Python <3.10 (FLA, fla-core, and tilelang
    all declare `Requires-Python: >=3.10`). On older interpreters the
    pip install would fail every launch and leave the worker on the
    slow torch fallback while still claiming to have set up the fast
    path.

  * Skip tilelang install on non-Linux platforms. `tilelang==0.1.8`
    only publishes Linux x86_64 / aarch64 and macOS arm64 wheels.
    Falling back to its 93MB sdist on a Studio worker is undesirable.

  * Detect an existing `apache-tvm-ffi` 0.1.10 / 0.1.11 install and
    force a reinstall to 0.1.9 with `--force-reinstall --no-deps`.
    Previously the import-only probe returned early and left the
    broken version in place, which crashes Triton on sm_100.

  * Add a 600s timeout to the tilelang and FLA subprocess.run calls,
    matching the existing flash-attn install pattern, so a network
    hang cannot block the training subprocess indefinitely.

  * 13 new / updated tests covering all six guards plus the
    pinned-spec, timeout, and force-reinstall code paths.

Total: 21 passing tests (8 original + 13 new / updated).
2026-05-16 07:03:26 +00:00
Daniel Han
fc2ee99b98
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 21:13:27 -07:00
Daniel Han
54a86c3514
ci: route every hf download through xet-tuned stall-retry wrapper (#5476)
Root cause of the Mac json-images 30 min timeout (run 25950714888 /
PR #5430): huggingface_hub>=1.15 deprecated `hf_transfer` and routes
every transfer through `hf-xet`. The CI step's unpinned
`pip install --upgrade huggingface_hub hf_transfer` jumped to 1.15.0
+ hf-xet 1.5.0, the 940 MB mmproj finished in ~21s, then the 3 GB
gemma-4 GGUF made it to ~46% and went completely silent for the
remaining 29 minutes -- no progress bytes, no error, no exit -- until
the job timeout fired.

This wraps every CI `hf download` in a new
`.github/scripts/hf-download-with-retry.sh`:

  * Drops the no-op `HF_HUB_ENABLE_HF_TRANSFER=1` prefix and the
    `hf_transfer` install (both are deprecated on 1.15+ and only
    emit a FutureWarning now).
  * Exports the hf-xet high-performance knobs Daniel asked for:
        HF_XET_HIGH_PERFORMANCE=1
        HF_XET_CHUNK_CACHE_SIZE_BYTES=0
        HF_XET_NUM_CONCURRENT_RANGE_GETS=64
        HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY=0
        HF_XET_CLIENT_READ_TIMEOUT=500
  * Watchdogs each attempt: if `hf download` has not exited after
    HF_DOWNLOAD_STALL_SECONDS (default 180s = 3 min), SIGTERM,
    sleep 2, SIGKILL, then loop. Retries are unbounded; the
    enclosing job's `timeout-minutes` is the real cap.
  * Optional 3rd positional `LOCAL_DIR` -- omitted lets `hf` use
    the default HF_HUB_CACHE, which is what the HF_HOME-priming
    jobs need.

19 call sites migrated across mlx-ci.yml + 9 studio-*-smoke.yml
workflows. The inline `python -c "from huggingface_hub import
hf_hub_download; ..."` block in mlx-ci.yml is also routed through
the wrapper so every hf transfer in CI gets the same treatment.

Also reverts the json-images timeout 45 -> 30 from #5475: the bump
was masking this hang, not fixing it.
2026-05-15 21:11:56 -07:00
Daniel Han
5ed13a9732
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 20:53:00 -07:00
Daniel Han
295844670b
ci: bump Mac json-images timeout 30 -> 45 min (cache-miss path) (#5475)
The `JSON, images` job in `studio-mac-inference-smoke.yml` (Job 3
of Mac Studio GGUF CI) downloads ~4 GB on a cache miss: 3 GB
gemma-4-E2B-it-UD-Q4_K_XL.gguf + ~1 GB mmproj-F16.gguf. The 30 min
cap was tight even with `HF_HUB_ENABLE_HF_TRANSFER=1` and parallel
downloads, and timed out the cache-miss run on PR #5430 mid-download
(run 25950714888) before Studio install or the smoke assertions ran.

Once the actions/cache restore hits, the job comes in under 10 min,
so 45 min only costs runner time on the first run after a cache
key bump (v1->v2 was just bumped in #5459, which is what produced
this failure). Jobs 1 (openai-anthropic, 270M model) and 2
(tool-calling, ~1.5 GB model) are not bumped -- their 25 min cap
has been comfortable.
2026-05-15 20:52:36 -07:00
Daniel Han
1a4df61a21
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 20:49:25 -07:00
Daniel Han
fb4bd0b777
ci: drop cache: 'npm' from setup-node (silent abort on Windows) (#5474)
`actions/setup-node@v6.4.0` with `cache: 'npm'` silently aborts the
entire job on Windows runners when the npm cache path returned by
`npm config get cache` (`C:\npm\cache`) does not yet exist on a fresh
runner -- the step exits 24s in with no error message and every
following step gets skipped. See npm/cli#7308 for the underlying
EEXIST / missing-dir race in the npm cache directory.

This mirrors the existing precedent in
`studio-windows-ui-smoke.yml`'s `setup-python` block, which already
dropped `cache: 'pip'` for the same reason (post-step fatal error on
a missing pip cache dir). The frontend `npm ci` is fast enough
without the cache that the reliability gain is worth the ~30s.
2026-05-15 20:49:05 -07:00
Daniel Han
a415f6ba86
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 19:41:23 -07:00
Daniel Han
77e0929735
revert: stop touching DEVICE_TYPE == "cuda" branches for CPU CI (#5473)
#5429 (cb15a7a5) tightened three production-path branches to
DEVICE_TYPE == "cuda" and torch.cuda.is_available() and added a
new else: SUPPORTS_BFLOAT16 = False arm to let `import unsloth.trainer`
survive on a CPU-only CI host. We already ship the package on Intel
XPU / AMD HIP / NVIDIA CUDA and don't want any extra branching in
those hot paths.

Move the entire CPU-CI handling to one place -- the top of
unsloth/device_type.get_device_type() -- so the UNSLOTH_ALLOW_CPU=1
sentinel short-circuits detection and returns "cuda" before any
torch probe runs. Every downstream DEVICE_TYPE == "cuda" branch
then behaves identically to a real CUDA host, with no additional
checks. The two existing duplicate UNSLOTH_ALLOW_CPU returns
later in the function are dropped (the new top-of-function check
covers both).

Revert the three call-site changes:
- unsloth/_gpu_init.py:212  -> back to `if DEVICE_TYPE == "cuda":`
- unsloth/_gpu_init.py:247  -> back to `if DEVICE_TYPE == "cuda":`
- unsloth/models/_utils.py:1207 -> back to `if DEVICE_TYPE == "cuda":`
- unsloth/_gpu_init.py: drop the new `else: SUPPORTS_BFLOAT16 = False`
  branch (dead under the top-of-function short-circuit).

Keep the two env-var gates that are needed for zoo's drift detectors
to inspect pristine TRL source (no behavioural change on production
hosts that never set UNSLOTH_ALLOW_CPU):
- unsloth/_gpu_init.py: `if env != "1": _patch_trl_trainer()`
- unsloth/models/rl.py:PatchFastRL: `if env == "1": return`

Verified:
- CUDA_VISIBLE_DEVICES=5 python -c "import unsloth.trainer" produces
  UnslothSFTTrainer.__init__ (TRL still patched on real hosts).
- UNSLOTH_ALLOW_CPU=1 + aggressive cuda spoof import succeeds and
  trl.SFTTrainer.__init__.__qualname__ stays SFTTrainer.__init__.
- pytest tests/_zoo_compiler_cache_shim.py -> 5 passed, 1 skipped.
2026-05-15 19:41:09 -07:00
Daniel Han
66814219a8
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 15:54:10 -07:00
Daniel Han
e775f941a4
tests/openai: patch httpx.AsyncClient ctor so delete tests hit mock (#5469)
delete_openai_container intentionally creates a fresh
httpx.AsyncClient per call (see external_provider docstring: shared
pool produced false 'deleted: true' responses while the container
survived). The existing _mock_http_client only swapped the shared
module-level _http_client, so the four delete tests bypassed the
mock entirely and hit the real OpenAI API, returning 401
Unauthorized on Python 3.10 / 3.12 / 3.13.

Extend the helper to also monkey-patch httpx.AsyncClient itself
to a factory that injects the test's MockTransport into any
freshly constructed client. List/create paths still use the
shared client and pass unchanged.

Verified locally: pytest tests/test_openai_container_crud.py
-> 8 passed.
2026-05-15 15:53:54 -07:00
Lee Jackson
ba0cae1aff
Stop: drop Ollama API key, clean up code execution UI (#5464)
* chat: drop Ollama API key, clean up code execution UI

* studio/chat: fix undefined candidateId + keyboard a11y on container list

- Auto-bind effect referenced `candidateId`, which is not declared in
  this scope (only `candidate` is) — would fail the TS/Next build.
  Use `candidate.id` to match the variable that's actually defined.
- Container list items get `role="button"` when `canActivate` is true
  but had no keyboard activation. Add `onKeyDown` for Enter/Space and
  `tabIndex={0}` so the row is focusable and activatable from the
  keyboard, matching the existing onClick behavior.

* studio/chat: restore declarations dropped by the main merge

The 75646444d auto-merge with main (#5466) silently dropped the
declarations a4f19171c added in regions #5466 also rewrote, while
leaving the usages further down in the file. No textual conflict
markers, but the result referenced undeclared names:

- REFRESH_POLL_MS constant (drives the 30s list refresh interval).
- pendingDelete / setPendingDelete / deleting / setDeleting state
  (drives the in-sheet AlertDialog delete confirm — replaces the
  window.confirm() that landed via #5466).
- Per-row locals inside the container list .map callback: running,
  isActive (recomputed with running), ttlMinutes, canActivate,
  statusLabel (drive click-to-activate, expired/active badges, and
  the muted styling for expired containers).

Also wire setDeleting(false) + setPendingDelete(null) into the
confirmDelete finally so the AlertDialog closes after the delete
call resolves; previously the busy state never cleared.

The all-containers list now iterates sortedContainers (matches the
picker above and the "newest-active first" UX) instead of the
unsorted visibleContainers.

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
2026-05-16 02:17:03 +04:00
Daniel Han
ec9e643b89
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 15:10:06 -07:00
Daniel Han
2de99a23d8
studio/install: strip top-level dir from repaired symlink target (#5467)
The repair in 5465 returned the full archive entry name (e.g.
"llama-b9165 libggml-rpc.0.11.1.dylib") but safe_link_target joins
the return value with target.parent (which already lives under
base llama-b9165). That doubled the prefix to
base llama-b9165 llama-b9165 libggml-rpc.0.11.1.dylib, the
resolved path never existed, and extract_tar_safely still raised
'tar archive contained unresolved link entries'.

Strip the top-level dir before returning so the linkname is
relative to target.parent, mirroring how unmangled symlinks are
stored in the tar (basename-only relative to the symlink).

Verified end-to-end against the upstream b9165 tarball: extraction
succeeds and every symlink resolves to an existing file.
2026-05-15 15:09:50 -07:00
Roland Tannous
a70bf02bb8
studio/chat: OpenAI container picker delete reliability (#5466)
* studio/chat: fix OpenAI container delete UX (expired filter, TTL cap, idempotent 404, refresh-on-error)

- Filter status="expired" from /containers/list so the picker only
  shows usable containers. OpenAI keeps expired entries in the list
  indefinitely, which made delete look broken.
- Cap ttl_minutes at 20 (backend Field + frontend TTL_MAX + persistence
  clamp). OpenAI's actual hard limit is 20; the prior 10080 cap caused
  integer_above_max_value rejections on create.
- Treat 404 on delete as idempotent success in the frontend client so
  already-gone containers don't surface a scary error toast.
- Run refresh() in finally for onCreate/onDelete so the picker stays
  in sync with OpenAI even when the call errors.
- Add route-level test for the expired filter.

* studio/chat: add diagnostic logging for OpenAI /containers DELETE

Trace what arrives at /external/openai/containers/delete (subject,
container_id, base_url) and what we send to OpenAI (URL, presence
of Authorization, value of OpenAI-Beta) plus the full response
status + body (capped at 300 chars). Helps confirm whether the
beta header is on the wire and whether OpenAI's response actually
reports deleted=true, when users report the delete "not taking".

No secrets are logged — Authorization is reported as a boolean.

* studio/chat: log raw /containers list response from OpenAI

Sibling to the delete diagnostics. After a confirmed delete
(deleted=true on the wire), we want to see whether the very next
list call returns the just-deleted id — that distinguishes
"OpenAI eventually-consistent list" from "frontend stale state".
Logs each entry's id + status only; no names, no timestamps.

* studio/chat: fingerprint decrypted API key for container CRUD

Logs kind (sk-proj-/sk-/other), length, and last-4 chars only —
never the full secret. Lets us compare what the backend actually
uses against the key the user expects, since the same DELETE
request shape can produce different results across keys
(project-scoped containers: list is permissive but delete requires
the owning project's key).

* studio/chat: use fresh httpx client for /v1/containers DELETE

Same key, same headers, same URL via the shared _http_client
returned deleted=true but the container persisted in subsequent
list calls. A fresh httpx.AsyncClient with the identical request
shape (verified with a standalone reproducer) deleted the same
container cleanly. Suspect connection-pool state from earlier
chat-completion streams interferes at the edge — switching to a
per-call client side-steps it entirely. Scoped to delete only;
list/create keep using the shared pool until we can confirm the
same fix is needed there.

* studio/chat: log OpenAI response headers on container DELETE

Adds cf-ray / x-request-id / openai-organization / openai-project /
openai-processing-ms to the delete-response diagnostic line. Lets
us cross-reference a failing delete against OpenAI support (or
against a working standalone reproducer) using the unique
request-id and edge node.

* studio/chat: client-side tombstone for just-deleted OpenAI containers

OpenAI's /v1/containers DELETE returns {"deleted": true} but the
list endpoint can keep returning the same container for several
minutes (replica lag or in-use silent no-op — undocumented per
developers.openai.com/api/docs/guides/tools-shell). Our backend
sends the correct DELETE with OpenAI-Beta: containers=v1 and a
standalone reproducer shows the same behavior, so the right fix
is UI-side rather than waiting on OpenAI.

After a successful delete, the id goes into a per-component
tombstone map with a 5-minute expiry. visibleContainers (now the
single chokepoint feeding sortedContainers, auto-bind, and the
all-containers list) filters those ids out. A 30s sweep clears
expired tombstones so the picker recovers automatically if OpenAI
eventually catches up (or the container's TTL elapses).

* studio/chat: tombstones live for the page lifetime; drop API key fingerprint log

- Tombstones change from Map<id, expiry> to Set<id>: once tombstoned,
  the id stays hidden from the picker until page reload. OpenAI's list
  can keep returning a deleted id for an undocumented and variable
  amount of time; automatically un-tombstoning after a fixed window
  surfaces it again and creates more confusion than it solves. The
  container's own TTL eventually expires the entry on OpenAI's side,
  and the expired-status filter at the backend list route hides it
  anyway.
- Remove the periodic sweep effect (dead code without expiries).
- Remove the api-key fingerprint log added during debugging — it
  served its purpose (confirmed parity) and isn't needed long-term.
2026-05-16 01:53:13 +04:00
Daniel Han
17d421359e
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 14:45:06 -07:00
Daniel Han
4f59c8e539
studio/install: repair upstream llama.cpp prebuilt mangled symlinks (#5465)
The macos-arm64 prebuilt tarball for llama.cpp b9165 and b9169 ships
symlinks whose linkname is missing both the directory separator AND
the leading character of the target basename:

  llama-b9165/libggml-rpc.0.dylib -> llama-b9165ibggml-rpc.0.11.1.dylib

extract_tar_safely correctly classified those as unresolved and made
install.sh fall back to source-build, which Mac CI then fails as a
hard error (Studio must use the prebuilt llama-bNNNN-bin-macos-arm64
on Apple Silicon).

Add _try_repair_missing_slash inside safe_link_target: when a
linkname starts with the member's top-level dir but no following
slash, search the archive for an entry under that dir whose name
ends with the mangled suffix. Accept only when the suffix uniquely
identifies a real archive entry, so legitimate archives are
untouched.

Verified against /tmp/llama-b9165.tar.gz: all 18 link entries
repair to real files in the archive.
2026-05-15 14:44:52 -07:00
Daniel Han
67606821a3
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 14:18:21 -07:00
Daniel Han
4b23af48b1
tests: raise pwsh/bash subprocess timeout from 10s to 60s (#5463)
CI surfaced a flaky failure on Linux 'Repo tests (CPU)':
  TestPwshPrForcePromotion.test_baked_in_pr_force_promotes ->
  subprocess.TimeoutExpired after 10s on /usr/bin/pwsh startup.

The scripts under test run in well under a second; the 10s budget
only covered pwsh / bash launch time, which spikes on heavily-
loaded GitHub-hosted runners. Raise the default helper timeout to
60s for both run_bash and run_pwsh. Real bugs in the script logic
will still surface as wrong output or non-zero exit; this just
absorbs runner-side launch jitter.
2026-05-15 14:18:04 -07:00
Daniel Han
2f9a6b0a25
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 13:14:51 -07:00
Daniel Han
85cf0a41ea
ci: switch Windows Stop Studio to a cmd no-op marker (#5462)
The prior set +e + redirect + exit 0 fix in #5460 did not stop the
Stop Studio step from exiting 143 (SIGTERM) on Git Bash; bash on
windows-latest exits with that signal before any inline guard
runs, regardless of redirection. The teardown does not gate
correctness -- the runner reclaims the Studio child process at
job end -- so swap the shell from Git Bash to cmd and just emit
a marker line.

After this, Job 3 (JSON, images) and the two other Windows GGUF
CI jobs cannot fail at the teardown step.
2026-05-15 13:14:34 -07:00
Daniel Han
a8a15f703c ci: retrigger MLX dispatch after pytorch CDN DNS flake 2026-05-15 19:51:40 +00:00
Roland Tannous
2622b79606
studio/chat: built-in code execution for OpenAI + Anthropic (#5461)
* studio/chat: built-in code execution for Anthropic Claude 4.x

Wire Anthropic's server-side code_execution_20250825 tool to the
existing Code pill in the composer. Pill lights up only for Claude
Opus/Sonnet/Haiku 4.x models that the docs list as compatible; pairs
independently with Search. Backend appends the tool entry plus the
code-execution-2025-08-25 beta header, and translates the SSE
server_tool_use / *_tool_result blocks (bash + text_editor sub-tools)
into the _toolEvent shape the frontend renderer consumes. File
uploads via the Files API are a deliberate follow-up.

* studio/chat: enable code execution pill in in-thread composer too

thread.tsx renders its own composer with a separate CodeToolsToggle
that was still gated on supportsTools only, so the pill stayed
disabled inside an active thread even after picking Anthropic 4.x.
Surface the capability through the runtime store
(supportsBuiltinCodeExecution, set from chat-page alongside
supportsBuiltinWebSearch) and read it in the toggle.

* studio/chat: built-in code execution for OpenAI cloud gpt-5.5

Extend the Code pill to OpenAI cloud's gpt-5.5 / gpt-5.5-pro via the
shell tool on /v1/responses. Per-thread container reuse: capture the
container_id from each response on a synthetic container_ready event,
persist it onto the ThreadRecord, and pass it back as
environment.type="container_reference" on follow-up turns so the
model sees filesystem state from prior turns until OpenAI's idle
expiry. Stale ids surface a container_invalidated event that clears
the thread record so the next turn falls back to container_auto.

Gated strictly on OpenAI cloud (api.openai.com base URL) — Ollama,
llama.cpp, vLLM, and custom OpenAI-compat presets won't see the
shell tool entry even when their providerType collapses to "openai".

* studio/chat: OpenAI shell-tool container management UI

Side-panel section (settings sheet → Code Execution) for managing
OpenAI's shell-tool containers per thread. Three controls:

- New-container idle timeout (provider-level default, pre-fills the
  create dialog and is used by the lazy-create path on a thread's
  first turn when set to a non-default value).
- Active container picker for the active thread — pick any existing
  container or stay on "Auto-create per thread".
- Inline create form (name + idle TTL) and per-row delete actions.

Three new backend endpoints under /api/inference/external/openai/
containers/{list,create,delete} proxy to OpenAI /v1/containers using
the encrypted API key. All three reject non-cloud base URLs up front
so the picker stays scoped to api.openai.com.

Deleting a container clears all thread bindings pointing at it; the
next turn falls back to auto-create.

* studio/chat: inherit container across threads + styled active picker

New threads on the same OpenAI provider now default to the most
recently used container instead of "Auto-create per thread" — both
in the chat-adapter (so a send works even if the side panel was
never opened) and in the side panel itself (auto-binds the active
thread when the dropdown loads on a thread that has no container).

Picker is visually emphasized with an accent panel and the
currently-active row in the list below is highlighted with the same
accent so the two views stay in sync.

* studio/chat: friendly English-word names for auto-created containers

Replaces the "chat-<thread-id-slug>" auto-name with a random
English-word + short hex suffix (e.g. "kestrel-3f9c"). Applies only
to the chat-adapter's lazy-create path; the OpenAI container_auto
path stays unnamed (only fires when no custom TTL is set).

* studio/chat: always pre-create OpenAI containers via frontend

Drops the TTL-based gate on the chat-adapter's lazy-create path so
every code-execution container the user ever sees in the picker has
a friendly English-word name. The backend's container_auto fallback
stays as a safety net (used only if the POST /v1/containers call
fails); in practice that branch should be rare.

* studio/chat: send OpenAI-Beta header for /v1/containers CRUD

Without OpenAI-Beta: containers=v1, OpenAI returns 200
{"deleted": true} for DELETE /v1/containers/{id} but does not
actually remove the container. The list call then keeps returning it,
making it look like Studio's "Delete container" button is broken.

Verified 2026-05-15 against api.openai.com: DELETE with the beta
header returns 200 and removes the container; the same DELETE without
the header returns the same 200 deleted:true body but the container
stays alive.

- Add _container_headers() that merges OpenAI-Beta on top of the
  shared auth headers; route list / create / delete through it.
- Verify the DELETE response body reports {"deleted": true}; raise
  httpx.HTTPError otherwise so the route surfaces a 5xx instead of
  silently reporting success on a silent no-op.
- Add tests covering header propagation and the deleted-flag guard
  (true, false, missing key, non-JSON body, 4xx passthrough).

* studio/chat: surface unpersisted-thread picker no-op as a toast

The "Active for this thread" container picker uses
db.threads.update(activeThreadId, ...), which silently returns 0 rows
affected when the thread record isn't yet in IndexedDB. That happens
on a brand-new thread where the user toggles code execution on and
opens settings before sending the first message — the chat adapter
only materializes the thread row on first send. The picker would
appear to ignore the user's selection and snap back to "Auto-create
per thread".

- onPick now awaits the update and toasts an actionable hint
  ("Send a message first to pin a container to this thread.") when
  the update affected zero rows.
- Auto-bind effect comment clarifies why it stays best-effort silent.

The auto-bind effect itself is unchanged: it's a heuristic that
should not nag the user when it can't apply.

* studio/chat: let user pick OpenAI container before first send

Previously the picker silently no-op'd until the user sent the first
message, because Dexie's ThreadRecord is only materialized inside the
runtime-provider's `initialize` hook (assistant-ui's first-message
callback). That kept users from binding a thread to an existing
OpenAI container up front; they had to either send a message and
risk the chat adapter auto-creating one, or accept the cross-thread
inheritance default.

- Export `ensureThreadRecord` from runtime-provider so other surfaces
  can materialize the row idempotently.
- In OpenAICodeExecSection.onPick, await ensureThreadRecord before
  the update, with modelType="base" (the settings sheet that hosts
  this section is only rendered in single-thread mode).

Behaviour after this commit:
- New thread + user picks a container in the sidebar → thread row is
  created with that container_id; first send uses it, no auto-create.
- New thread + user does nothing → row still absent; first send goes
  through the existing inherit/lazy-create path as before.
- The auto-bind effect remains silent best-effort: it does not
  eagerly create the thread row, so it cannot pre-empt the user's
  pick on a fresh thread.

* studio/chat: drop "Auto-create per thread" option, default to latest

The dropdown previously offered "Auto-create per thread" as an
explicit value (null in storage), with the chat-adapter then
inheriting from the most recent container at send-time. That made
the picker display disagree with what the backend would actually do:
the picker said "auto", but the backend was reusing an existing
container.

Behaviour after this commit, when code execution is enabled on an
OpenAI cloud provider:
- Containers list non-empty: dropdown defaults to the container with
  the latest lastActiveAt, eagerly bound via ensureThreadRecord +
  db.threads.update so the bind survives even when the thread row
  has not been materialized by the chat adapter yet. User can pick
  any other container in the list.
- Containers list empty: render a disabled placeholder "(none yet —
  will be created on first send)". The chat-adapter's lazy-create
  path (chat-adapter.ts:1040-1082) mints the first container on
  first send and writes it back to the thread; the next refresh
  surfaces it in the picker.

Expiration mid-operation is unchanged: the existing
container_invalidated _toolEvent clears the thread's stored id and
the next turn re-creates.

* studio/chat: fix picker stuck on "Selecting most recent…" + manual-create binding

Two follow-up fixes to the picker rework in d0cbeb99b.

1) The dropdown was getting stuck on the "Selecting most recent…"
   placeholder option even after the auto-bind write completed,
   because the select was controlled by `activeContainerId` (whatever
   sits in Dexie) and there's a brief window between the auto-bind
   firing and useLiveQuery propagating the new row back. Decoupled
   the rendered value from the Dexie state: compute the displayed id
   locally as `activeContainerId ?? sortedContainers[0]?.id`, so the
   most-recent container's name shows up immediately. The auto-bind
   effect still writes the bind to Dexie so the chat adapter sees it
   on send. Dropped the placeholder option entirely.

2) The manual "Create container" flow (`onCreate`) bound the new
   container to the active thread with a bare `db.threads.update`.
   On a brand-new thread that hadn't been materialized yet, the
   update affected 0 rows; the user's next send then went through
   cross-thread inheritance / lazy-create and could land on a stale
   container, surfacing as "container does not exist". Same fix as
   `onPick`: ensureThreadRecord before update so the bind lands.
2026-05-15 23:39:06 +04:00
Lee Jackson
a9b8c9a221
Studio: make API key optional for local providers (llama.cpp/vLLM/Ollama) (#5457)
* make API key optional for local providers (llama.cpp/vLLM/Ollama)D

* chore: reduce comments

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-15 23:33:22 +04:00
Daniel Han
f9b3d2614f
Merge branch 'main' into studio-fla-tilelang-qwen3.5 2026-05-15 11:47:09 -07:00
Daniel Han
ac3e9e98f2
ci: make Windows Stop Studio teardown tolerate Git Bash signal exit (#5460)
The Windows-runner "Stop Studio" step's kill + sleep block has
been observed to exit 143 (SIGTERM) even when the upstream test
work passed. Most recently caught on PR #5432 Job 3 "JSON, images":
all four assertions (json_object, plain inference, image/openai,
image/anthropic) printed PASS, then the kill step ran for ~2
seconds and exited 143, failing the job.

Teardown does not gate correctness. Wrap all three Stop Studio
steps with set +e + redirected error streams + explicit exit 0
so transient Git Bash signal weirdness no longer masks a green
test run.
2026-05-15 11:46:52 -07:00