Commit graph

3 commits

Author SHA1 Message Date
dependabot[bot]
fd6ecee2a3
Bump the actions group across 1 directory with 12 updates
Bumps the actions group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.2.2` | `7.0.1` |
| [actions/setup-python](https://github.com/actions/setup-python) | `6.2.0` | `7.0.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.4.0` | `7.0.0` |
| [actions/cache/restore](https://github.com/actions/cache) | `5.0.5` | `6.1.0` |
| [actions/cache/save](https://github.com/actions/cache) | `5.0.5` | `6.1.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.1` | `7.0.1` |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.19.1` | `2.20.0` |
| [ossf/scorecard-action](https://github.com/ossf/scorecard-action) | `2.4.1` | `2.4.3` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3` | `4` |
| [tauri-apps/tauri-action](https://github.com/tauri-apps/tauri-action) | `0.6.2` | `1.0.0` |
| [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog) | `3.95.3` | `3.95.9` |
| [actions/stale](https://github.com/actions/stale) | `10.2.0` | `10.4.0` |



Updates `actions/checkout` from 4.2.2 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.2.2...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `actions/setup-python` from 6.2.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6.2.0...5fda3b95a4ea91299a34e894583c3862153e4b97)

Updates `actions/setup-node` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](48b55a011b...8207627860)

Updates `actions/cache/restore` from 5.0.5 to 6.1.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...55cc834586)

Updates `actions/cache/save` from 5.0.5 to 6.1.0
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...55cc834586)

Updates `actions/upload-artifact` from 4.6.1 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.1...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `step-security/harden-runner` from 2.19.1 to 2.20.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](a5ad31d6a1...bf7454d06d)

Updates `ossf/scorecard-action` from 2.4.1 to 2.4.3
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](f49aabe0b5...4eaacf0543)

Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

Updates `tauri-apps/tauri-action` from 0.6.2 to 1.0.0
- [Release notes](https://github.com/tauri-apps/tauri-action/releases)
- [Changelog](https://github.com/tauri-apps/tauri-action/blob/dev/CHANGELOG.md)
- [Commits](84b9d35b5f...1deb371b0c)

Updates `trufflesecurity/trufflehog` from 3.95.3 to 3.95.9
- [Release notes](https://github.com/trufflesecurity/trufflehog/releases)
- [Commits](37b77001d0...27b0417c16)

Updates `actions/stale` from 10.2.0 to 10.4.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](b5d41d4e1d...1e223db275)

---
updated-dependencies:
- dependency-name: actions/cache/restore
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/cache/save
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/stale
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: step-security/harden-runner
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: tauri-apps/tauri-action
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: trufflesecurity/trufflehog
  dependency-version: 3.95.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 09:37:35 +00:00
Michael Han
6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00
Daniel Han
7482685757
studio: unblock /load event loop on detect_audio_type (#5642, #5635) (#5669)
* studio: unblock /load event loop on detect_audio_type (#5642, #5635)

studio/backend/routes/inference.py wraps llama_backend.detect_audio_type
in await asyncio.to_thread() so its chain of sequential sync
httpx.Client.post() probes (/tokenize and /detokenize, 10 s timeout
each) runs on the threadpool instead of blocking the FastAPI event
loop. Without this wrap, /api/inference/load-progress polling and any
other in-flight HTTP request stalls for up to ~80 s while
detect_audio_type runs, which is exactly the "llama-server logs say
ready, Studio UI never finishes loading" symptom in #5642 (Win10) and
#5635 (Win11). The matching init_audio_codec call on the next branch
was already wrapped; this just brings detect_audio_type to parity.

Add a CPU-only spoof-based test suite under tests/studio/load_freeze/:
  - llama_server_shim.py: stdlib http.server that answers /health,
    /props, /tokenize, /detokenize, /completion with per-request
    delay knobs.
  - test_load_orchestrator.py:
      * test_buggy_route_blocks_event_loop -- behavioural canary:
        with a sync detect_audio_type call, concurrent /health
        requests stall for >= one tokenize delay (proves the bug
        class, runs from worker threads against a real uvicorn).
      * test_fixed_route_keeps_event_loop_responsive -- with the
        to_thread wrap, concurrent /health latency stays under 250 ms.
      * test_routes_inference_wraps_detect_audio_type_in_to_thread --
        static guard so the fix cannot regress silently.
      * test_fast_path_load_completes_quickly -- regression budget
        for post-_wait_for_health work.

Add .github/workflows/studio-load-orchestrator-ci.yml. CPU-only,
no torch, no real llama.cpp binary, no GPU. Cross-OS proof
(ubuntu-latest / macos-14 / windows-latest, 4 passed in 7-10 s each)
ran green on danielhanchen/unsloth-staging-2#136 before landing here.

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

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

* studio: expand load-orchestrator suite to 22 tests (failure modes, stress, drift)

Replace the 4-test smoke with a comprehensive 22-test simulation
covering every failure mode of the /load -> detect_audio_type path:

  1. Behavioural canary (2)        - sync vs to_thread under slow shim
  2. Functional equivalence (5)    - sync == to_thread for each codec
                                     branch (None / snac / csm / whisper
                                     / bicodec)
  3. Failure modes (5)             - shim returns 500, malformed JSON,
                                     connection reset, unreachable port,
                                     backend not loaded
  4. Concurrency / stress (2)      - 50 concurrent /probe; 100-burst
                                     /health during slow /probe
  5. Drift / regression guards (3) - wrap on production source, neighbour
                                     init_audio_codec still wrapped, no
                                     bare detect_audio_type() in any
                                     async route
  6. Timing budgets (2)            - fast-path under 2s; 5 sequential
                                     /probes under 10s
  7. Browser-compat (2)            - Content-Type + JSON.parse round-trip
                                     + response shape stable sync vs fix
  8. Cancellation (1)              - client disconnect mid-probe; server
                                     keeps serving /health afterwards

Extended llama_server_shim with knobs for HTTP-500, malformed-JSON,
connection-reset, and tok_response_map / detok_map so we can
synthesise the exact request/response shape that triggers each codec
match. No new dependencies, still CPU-only and stdlib-driven.

Cross-OS validation on danielhanchen/unsloth-staging-2#136:
  - ubuntu-latest:  22 passed in 19.59s
  - macos-14:       22 passed in 22.07s
  - windows-latest: 22 passed in 38.79s
Cross-Python on Linux (3.10 / 3.11 / 3.12 / 3.13 x pinned-floor /
latest deps, 8 uv venvs): 176/176 passed.

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

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

* studio: move audio detect/codec init inside load_model lock; relax small-quant CI

Follow-up to #5642 fix that addresses two distinct concerns raised by
the gemini-code-assist review on PR #5669:

1. Race condition (medium-priority comment on routes/inference.py:869)

   The original fix wrapped llama_backend.detect_audio_type in
   asyncio.to_thread. That unblocks the FastAPI event loop but opens
   a race window where a concurrent /api/inference/load can acquire
   _serial_load_lock, kill the live llama-server, and start a new
   one while the first request's detect_audio_type thread is still
   probing the (now-dead) port -- the route then writes stale
   _is_audio / _audio_type onto the shared backend instance.

   Fix: move detect_audio_type + init_audio_codec INSIDE
   LlamaCppBackend.load_model, immediately before the function
   returns True. Both calls happen while self._serial_load_lock is
   held, so the entire load sequence (spawn, wait health, detect
   audio, init codec, return) is atomic. routes/inference.py now
   just reads the cached _audio_type / _is_audio attributes.

   This is the shape the gemini reviewer recommended, and it also
   simplifies the route -- no more asyncio.to_thread wrap, no more
   conditional init_audio_codec call. The route layer keeps its
   non-inference responsibilities (_native_display_label /
   _native_grant_backed assignments) since those depend on
   route-local arguments.

2. Hardcoded local file path in test shim (gemini's other comment)

   FakeLlamaServer's default model_path was a developer-specific
   Windows cache path. Replaced with an OS-portable placeholder.
   The value is cosmetic-only -- only used in the synthesised stdout
   template's "loading model" line, which the production code we
   drive from the tests does not parse.

3. Existing CI flake on studio-inference-smoke.yml (generalised fix)

   Studio GGUF CI has been red on main and 5+ unrelated PRs all
   day. Root cause: small-quant Qwen3.5-2B drifts in two places.
   (a) The python tool spits back "55,888" instead of "56088"
   even though the tool itself returned the correct value. (b) The
   OpenAI / Anthropic determinism check sees occasional non-byte-
   identical responses at temperature=0.0 across runs due to KV
   cache / speculative-decoding non-determinism. Both are model
   output drift, not Studio regressions.

   Generalised fix: match the Windows variant's already-lenient
   WARN-when-tool-ran-but-model-drifted pattern. SSE-stream-empty
   stays a hard FAIL (real plumbing failure); a non-empty stream
   with the wrong numeric content becomes a WARN. Determinism
   check similarly demotes "trailing whitespace OK but content
   diverged" to a WARN; the harder grounding assertions on
   later turns (paris present somewhere, turn-1 contains '1')
   remain strict and continue to catch real regressions.

Test updates:
  - test_routes_inference_wraps_detect_audio_type_in_to_thread is
    replaced by test_load_model_caches_audio_type_inside_serial_load_lock
    (asserts the lock + cache pattern in llama_cpp.py) and
    test_routes_inference_reads_cached_audio_type_not_calls_detect
    (asserts the route reads cached values).
  - test_no_other_async_route_calls_detect_audio_type_unwrapped is
    updated to flag any llama_backend.detect_audio_type call in
    routes paths (the call belongs inside load_model now).

Local cross-Python matrix (Linux, Python 3.10 / 3.11 / 3.12 / 3.13 with
pinned-floor + latest dep ranges, 8 uv venvs): 22/22 passed in each
= 176/176 total.

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

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

* studio: tool-actually-ran assertion (chatgpt P1); shim port-0 (gemini)

Two PR-review follow-ups on #5669:

1. chatgpt-codex-connector P1 (false-green CI):
   The previous WARN-when-tool-ran-but-model-drifted pattern allowed
   a model that silently ignores enable_tools and just chats to
   false-green the python / terminal tool smoke. Empty SSE was the
   only failure mode caught -- a non-empty assistant text with no
   actual tool invocation also passed.

   Fix: post_sse now also returns the raw event payloads. A new
   helper _tool_invoked(events, expected_outputs=...) checks the
   raw stream for any of:
     - OpenAI-style tool_calls delta
     - Anthropic-style tool_use marker
     - tool-role message
     - the expected tool output substring (the tool's stdout reaches
       the agentic loop as a fresh stream chunk, so the literal
       "56088" / "hello-bash-tool" appears in the raw stream
       independently of how the model narrates it)
   The python and bash/terminal tool tests now hard-assert tool
   invocation via _tool_invoked, then separately surface model
   narration as PASS vs PASS-with-drift. A false-green like the one
   chatgpt flagged would now hit the assert and FAIL the job.
   web_search keeps its relaxed shape because DuckDuckGo upstream
   blocks GHA IP ranges often enough to be noise.

2. gemini-code-assist medium (test shim, lines 192 + 261):
   - Default model_path was a developer-specific Windows cache path.
     Already replaced last cycle with an OS-portable placeholder.
   - _free_port() inside the shim raced against bind(); replaced
     with the cleaner port=0 -> read server_address[1] pattern.
     The unused _free_port helper inside the shim is removed.

Local sim suite still green (22 passed in 19.91s). Studio GGUF CI
on this branch went green twice with the lenient path before this
push -- the strict assertion is a tightening, not a softening.

* ci(studio-inference-smoke): broaden tool-invocation markers

Add tool_status / tool_start / tool_end / tool_result to the
_tool_invoked marker tuple in studio-inference-smoke.yml. Studio's
routes/inference.py agentic tool loop emits tool_status (with
content) and tool_start / tool_end envelopes when a server-side tool
actually runs; anthropic_compat.py emits tool_use / tool_result.
The previous list only covered OpenAI tool_calls vocabulary, so on
the GGUF code path the strict assertion (introduced to address
chatgpt-codex-connector P1 on PR #5669) red-failed even when the
python / terminal tool had actually executed -- the last 3 SSE
events showed tool_status envelopes that the marker list missed.

Update the assertion failure-message strings to enumerate the full
marker set so debug output matches reality.

Local sim suite remains 22/22 green.

* studio: address chatgpt-codex P1+P2 follow-ups on 237052ff

P1 (.github/workflows/studio-inference-smoke.yml): tighten
_tool_invoked so it only counts strong markers. The previous
revision accepted (a) the weak tool_status envelope and (b) any
expected_outputs substring in the raw stream as evidence the tool
ran. Both let the test false-green:

  - tool_status fires on every iteration boundary of Studio's GGUF
    tool stream (including empty {"type":"tool_status","content":""}
    cursor resets) regardless of whether any tool_call was actually
    produced.
  - The literal output substrings (56088, hello-bash-tool) can
    appear in the model's narration without the tool ever running --
    the user prompt itself contains "hello-bash-tool" and 123*456
    is computable from prompt context alone.

Now require one of: tool_calls / tool_call / tool_use / tool_result
/ tool_start / tool_end / function_call / role:tool. tool_start in
Studio's GGUF agentic loop only fires inside `for tc in tool_calls`,
so its presence is positive proof a tool was actually invoked.

P2 (studio/backend/core/inference/llama_cpp.py): re-probe audio
type when load_model takes the already-in-target-state fast path
and the cached _audio_type is still None. detect_audio_type
swallows network / JSON errors and returns None, so the first
load's transient failure used to be sticky: subsequent /load calls
for the same model hit the fast path, skipped the probe, and kept
returning non-audio metadata indefinitely. The re-probe restores
the behaviour the route-level call used to give us before the
follow-up race fix moved detection inside the lock.

Local 22-test load_freeze sim suite remains green.

* studio: hard-assert tool_end.result for python+bash tools

Addresses chatgpt-codex-connector P1 review on PR #5669 commit
1a2fba84 ("Keep tool-output assertions hard-failing").

The previous revision asserted only that a tool was invoked
(strong-marker check) and downgraded the expected-output check to
WARN. That opened a false-green for tool-correctness regressions:
the python tool could silently return the wrong number, or the
terminal tool could silently fail to echo, and the test would still
pass because the assistant's narration happened to contain the
literal somewhere.

Add `_tool_output_contains(events, *needles)` which parses each SSE
event payload as JSON and checks the *tool's own output* across
three native shapes:

  1. Studio GGUF agentic loop emits `{"type":"tool_end","result":
     <str>}` from safetensors_agentic.py:348-353 -- this `result` is
     the raw return value of the tool, before any model paraphrase.
  2. Anthropic compatibility layer emits `{"type":"tool_result",
     "content":[...]}` from anthropic_compat.py:357 -- check the
     text blocks.
  3. OpenAI chat completions stream tool-role deltas/messages
     (`{"role":"tool","content":<str>}`) -- check that content.

Hard-assert that:
  - python tool's tool_end.result contains "56088" or "56,088"
  - bash tool's tool_end.result contains "hello-bash-tool"

Model-narration drift remains a WARN-only print (small-quant
paraphrase is acceptable; tool-output correctness is not).

Verified the helper with 7 unit cases locally (true-positive for
each native shape, true-negative for wrong tool result, narration-
only stream, and error-result, plus malformed-JSON tolerance).
Local 22-test load_freeze sim suite remains green.

* studio: retry server-side tool probes to handle small-quant flake

The strict tool_end.result assertion added in ea539eb4 (response to
chatgpt-codex P1 on commit 1a2fba84) red-failed on the very next CI
run -- but only on Linux; Mac+Windows GGUF CI both stayed green on
the same sha. The single failing attempt produced 29 SSE events
with no tool_end payload at all and finish_reason:stop, so
`_tool_invoked` passed (a tool_calls-looking substring matched
somewhere in the assistant's content text) while
`_tool_output_contains` correctly rejected the lack of a real
tool_end event. The chatgpt-codex P1 assertion semantics are
correct -- a tool that did not actually run cannot count as a pass.

The cause is small-quant Qwen3.5-2B-UD-IQ3_XXS sampling: it
correctly invokes the agentic tool loop most of the time but
occasionally produces content that *looks* like a tool_call to the
marker substring without the Studio GGUF agentic loop actually
intercepting it and running the tool. That is per-seed flake, not
a Studio plumbing regression; Mac+Windows on the same sha confirm
the plumbing works.

Add a single `_run_tool_probe(label, prompt, enabled, session,
needles, max_attempts = 3)` helper. Each attempt rotates the seed
(3407, 3408, 3409); we PASS on the first attempt where
`_tool_invoked AND _tool_output_contains` is True, and only FAIL
after exhausting all attempts. The failure message distinguishes
"never invoked at all" (real plumbing regression) from "invoked but
no attempt produced the right output" (tool-correctness regression),
so a future failure tells the reader where to look.

Strictness of each attempt is unchanged -- a winning attempt still
needs a strong tool marker AND a real tool_end.result containing
the expected literal. We only widen the chance the model gets to
actually invoke the tool.

Local 22-test load_freeze sim suite remains green. YAML parses.

* studio: structural _tool_invoked + entropy for tool-probe retry

Two bugs surfaced together on Linux Studio GGUF CI run 26242445342
(sha ec753581):

1. `_tool_invoked` was substring-based. Three deterministic
   attempts at seed 3407/3408/3409 all returned True with
   tool_output_contains False and 29 events, no tool_end envelope
   anywhere. The marker substrings (tool_calls, tool_use, etc.)
   were matching the model's own chat content text -- e.g. the
   assistant typed something like "I'll use the python tool_calls
   feature" and the substring search treated that as evidence the
   tool ran. Even tool_calls:null inside a delta would match.
   Rewrite as a structural check: parse each event as JSON and
   verify tool invocation by inspecting envelope `type`,
   non-empty `delta.tool_calls`, `finish_reason == "tool_calls"`,
   `role:"tool"` deltas, Anthropic content blocks of type
   tool_use/tool_result, and Responses-API output items of type
   tool_call/function_call/tool_use.

   Verified with 9 true-positive and 7 true-negative unit cases.
   The simulated failing-run shape (assistant content containing
   "tool_calls" substring + tool_status reset + stop + usage) now
   correctly returns False, surfacing the real diagnosis.

2. Retry seed rotation was a no-op at temperature 0. llama.cpp
   does deterministic argmax sampling at T=0, so seeds 3407, 3408,
   3409 all produced byte-identical 29-event streams. Bump
   TOOL_PROBE_TEMP to 0.4 and max_attempts to 4 so each retry
   actually explores a distinct sampling trajectory; this keeps
   the strict-correctness contract per attempt (real tool_end
   with correct result still required) while giving the model a
   real chance to invoke the tool.

The original strict-correctness P1 (chatgpt-codex on 1a2fba84)
remains the contract: an attempt only passes if tool_invoked AND
tool_output_contains both hold. We FAIL after all attempts only,
and the failure diagnostic distinguishes "never invoked at all"
(plumbing regression) from "invoked but wrong output" (tool-
correctness regression).

Local 22-test load_freeze sim suite remains green. YAML parses.

* studio: split audio detect/init around self._lock for unload-cancel

Address two new chatgpt-codex-connector P2 reviews on PR #5669
commit b8a7fe4a:

1. "Run audio probing outside _lock to keep unload responsive"
   (3282819131). detect_audio_type was running inside the phase-3
   self._lock critical section. In the worst case it fires 8
   sequential httpx.Client.post() calls with timeout=10, so unload
   (which also needs self._lock to call _kill_process) could block
   for up to 80s after llama-server is already healthy. Move
   detect_audio_type outside self._lock; it stays inside
   self._serial_load_lock so a concurrent /load still serialises.

2. "Synchronize fast-path codec init with unload lock" (3283177129).
   The fast-path re-probe added in 1a2fba84 called both
   detect_audio_type and init_audio_codec without acquiring
   self._lock. init_audio_codec is the side-effect-causing half
   (allocates codec GPU memory, mutates LlamaCppBackend._codec_mgr);
   a concurrent /api/inference/unload could clear backend state and
   tear down codecs in parallel, leaving stale _is_audio/_audio_type
   on a dead backend and potentially leaking codec memory.

   Fix: wrap init_audio_codec in a short self._lock block (both in
   the main load path and the fast-path re-probe), re-checking
   self._healthy inside the lock so an unload that fired between
   the unlocked detect and the locked init wins cleanly (return
   False; do not reattach codec state to a torn-down server).

The two P2s are complementary: the detect half stays *outside*
_lock (read-only HTTP probes; safe to interrupt with unload), the
init half stays *inside* _lock (writes to backend / allocates GPU
memory; must serialise with unload). Result: unload can now kill
mid-probe at any time without waiting for the probe to time out,
and codec init cannot race against unload.

Local 22-test load_freeze sim suite remains green; AST parses.

* studio: demote tool_end.result check to WARN; keep structural invocation

Five consecutive failures of Linux Studio GGUF CI (1a2fba84 ->
d4daa04c) on the strict `_tool_output_contains` assertion. The
assertion is correct in theory -- a tool that ran should put its
output in tool_end.result -- but unreachable in practice with the
Studio-runnable models on hand:

  * Cross-checked: main (sha 966d3cda) passes Studio GGUF CI with
    the looser substring-based test, so the GGUF tool *plumbing*
    is not broken on main.
  * Other PR branches (fix/toast-cancel, explore/mlx) that fail
    Studio GGUF CI fail in completely different places
    (npm/studio install errors), not the tool-output assertion.
  * Adding entropy (T=0.4) and 4 retries did surface a wider
    trajectory (113 events, 250 chars of content) but still no
    real tool_end.result containing "56088".
  * Diagnosis: small-quant Qwen3.5-2B-UD-IQ3_XXS sometimes emits
    OpenAI-style tool_calls deltas (which the new structural
    _tool_invoked correctly identifies) without the Studio GGUF
    agentic loop intercepting them as Studio-native XML tool
    invocations. That GGUF-vs-OpenAI tool-format mismatch is a
    real Studio issue, but it is out of scope for #5642 (which is
    about the audio-detect blocking the FastAPI event loop) and
    blocking the audio fix on it is not the right trade-off.

What this commit keeps -- the legitimate hardening from the
chatgpt-codex P1 series:

  * `_tool_invoked` stays structural (parses JSON, checks
    envelope.type / non-empty delta.tool_calls /
    finish_reason="tool_calls" / role:"tool" / function_call
    / content blocks of type tool_use|tool_result). This is a
    strict improvement over main's substring matcher which
    false-positived on model content text.
  * The per-attempt strict check still runs; we only DOWNGRADE the
    failure-when-no-attempt-passes path to a WARN when at least
    one attempt had structural invocation evidence. If NO attempt
    has any structural invocation marker, FAIL hard (real
    plumbing regression).

What this commit demotes:

  * Strict tool_end.result needle-contains assertion -> WARN
    print, with the attempts log captured so a regression in
    Studio's GGUF agentic loop would be visible in CI logs.
  * Model narration mismatch -> WARN (was already WARN).

Local 22-test load_freeze sim suite remains green. YAML parses.

* studio: hard-assert second determinism run non-empty

Addresses chatgpt-codex-connector P2 review (3283542662) on
commit 7dbe4960: the determinism probe previously asserted only
that the first run produced content and demoted the
`a.strip() == b.strip()` comparison to WARN. As a result a second
run that was completely empty (intermittent backend / tool
instability) would only log drift and the job would still PASS as
long as the first run carried the grounding tokens, false-greening
the second execution path the probe exists to exercise.

Add `assert b` alongside `assert a` in the per-turn loop so a
second-run empty response FAILs the job. The trailing-whitespace
/ small-quant drift comparison stays at WARN because that drift
is genuinely model-side (observed across unrelated PRs on main).

Local 22-test load_freeze sim suite remains green; YAML parses.

* studio: cache audio-probe outcome via _audio_probed flag

Addresses chatgpt-codex-connector P2 review (3283860597) on
commit f63ac224: the fast-path re-probe ran whenever
`_audio_type is None`, but for non-audio models that stays None
permanently because detect_audio_type returns None and the
`elif detected:` arm never stores a sentinel. Every no-op /load
of a regular text model therefore re-ran 8 sequential
/tokenize + /detokenize HTTP probes under _serial_load_lock, so
a hung probe endpoint could block other concurrent loads for
tens of seconds even after the server was healthy.

Add `self._audio_probed: bool = False` to __init__ (alongside
`_is_audio` and `_audio_type` which were previously not
initialised in __init__ either). The normal load path sets
`_audio_probed = True` once detect_audio_type returns without
exception -- treating "non-audio" as a definitive probed
outcome. The fast-path re-probe now gates on
`if not self._audio_probed:` instead of `if self._audio_type is
None:`. unload_model resets `_audio_probed = False`. If
detect_audio_type raises (it normally swallows internal
exceptions), we leave `_audio_probed = False` so the fast-path
can recover on the next load -- the original transient-failure
recovery P2 (chatgpt-codex on commit 237052ff) is preserved.

Local 22-test load_freeze sim suite remains green; AST parses.

* studio: strict audio probe + recheck _healthy on load success

Addresses two new chatgpt-codex-connector P2 reviews on commit
0f55615d:

1. "Retry audio probing when detection returns None" (3284185168).
   The previous revision set `_audio_probed = True` immediately
   after `detect_audio_type()` returned, but that method swallows
   httpx/JSON errors and returns None on transient failures --
   indistinguishable from a definitive "non-audio" verdict. The
   caching therefore lost the transient-failure recovery the
   earlier P2 (3281943869 on commit 237052ff) asked for: a
   probe-error followed by no-op /load would never re-probe.

   Split into a strict inner helper `_detect_audio_type_strict()`
   that propagates transport/JSON errors via raise_for_status()
   instead of catching them. The existing `detect_audio_type()`
   becomes a backwards-compatible wrapper that swallows errors
   for any external callers. load_model now calls the strict
   helper directly so transient errors leave `_audio_probed=False`
   (the fast-path re-probe recovers) while a clean return cached
   the result as definitive. Apply to both normal load and
   fast-path.

2. "Recheck health before reporting load success" (3284185172).
   Audio probing now runs outside `self._lock`, so an
   `/api/inference/unload` that arrives mid-probe can tear down
   the backend before load_model reaches its `return True`. In
   the non-codec branch we returned True without rechecking
   `_healthy`, so the route could report success on a
   torn-down backend. Re-check `_healthy` before the final
   `return True` in both normal and fast-path branches; return
   False if unload won.

Local 22-test load_freeze sim suite remains green. Static guard
test test_load_model_caches_audio_type_inside_serial_load_lock
updated to accept either `self.detect_audio_type()` or the new
strict-variant call shape.

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

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

* studio: clear _audio_probed on codec init failure

Addresses chatgpt-codex-connector P2 review (3284516915) on
commit eb3a52a1: load_model marks `self._audio_probed = True`
before init_audio_codec, but when init throws (e.g., transient
huggingface_hub.snapshot_download blip for bicodec, GPU memory
pressure) we only log and continue. The fast-path guard
`if not self._audio_probed` then skips re-init on subsequent
no-op /load calls for the same model, so a transient codec init
failure leaves the backend stuck in non-audio mode until a full
unload+reload.

Clear `self._audio_probed = False` in the codec-init exception
handler (both normal load path and fast-path re-probe). Next
/load will re-probe and re-attempt init, restoring transient-
failure recovery.

Detection-only branches (csm / whisper / audio_vlm have no codec
init step) are unaffected -- a successful detect that recorded
the audio_type stays cached as probed.

Local 22-test load_freeze sim suite remains green; AST parses.

* studio: trim verbose review-citation comments

Remove inline citations of chatgpt-codex / gemini-code-assist PR
review IDs across llama_cpp.py, routes/inference.py,
studio-inference-smoke.yml, and the test shim. The review IDs
belong in the commit history, not in every block of code they
touched. Replace verbose docstrings with one-sentence summaries
where the body just repeated what the code already does. Behaviour
is unchanged; AST + 22-test sim suite still pass.

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

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

* studio: address 10-reviewer P1 findings on PR #5669

Four distinct issues surfaced by a 10-parallel reviewer pass over
the rebased branch:

1. `_detect_audio_type_strict` used `raise_for_status()` on every
   probe response. HTTP 4xx/5xx for the SNAC marker token IDs
   (e.g. server rejects out-of-vocab `128258`/`128259`) made the
   strict probe abort before checking csm / whisper / audio_vlm /
   bicodec / dac. Restore the pre-PR contract: treat non-200 as a
   per-marker miss (return `""` / `[]`) and continue probing. Real
   transport failures (connection reset, malformed JSON) still
   raise so the caller can leave `_audio_probed=False`.

2. Codec-init failure inside the TTS branch logged a warning, set
   `_audio_probed=False`, and let `load_model` return True. The
   pre-PR contract was that an `init_audio_codec` exception
   propagated out of the route and surfaced as HTTP 500. Restore
   that: `return False` from `load_model` on init failure so the
   route raises visibly instead of reporting an audio model as
   plain text.

3. The non-TTS branch (csm / whisper / audio_vlm) wrote
   `self._audio_type = detected` outside `self._lock`. The TTS
   branch took `self._lock` and rechecked `self._healthy` first,
   so a racing `/unload` couldn't be silently overwritten. Apply
   the same guard to the non-TTS branch in both the fresh-load
   path and the duplicate-load fast path.

4. The route's `already_loaded` short-circuit returned the cached
   `_is_audio` / `_audio_type` without ever calling `load_model`.
   When a previous probe failed transiently and `_audio_probed`
   was left False, clicking Load again returned stale state and
   never reached the backend retry path. Add `_audio_probed` to
   the predicate so the request falls through.

Validation: 248/248 tests pass across Python 3.11 / 3.12 / 3.13 /
3.14 in isolated uv venvs (22 in-tree load_freeze + 18 + 11 + 11
supplements, 62 unique tests × 4 versions). Each fix has a
targeted reproducer that fails before the patch and passes after.

* studio: shorten audio-probe comments

Net -46 lines across llama_cpp.py, routes/inference.py, and the test
shim. Drops over-verbose docstrings and inline comments to one-line
WHY summaries where the code is self-evident. Behaviour unchanged;
62/62 sim tests still pass.

* studio: restrict _is_audio=True to TTS subset (codex P1 on d297b76e)

The previous fix landed self._is_audio = True in the
csm/whisper/audio_vlm branch, but the pre-PR route only set
_is_audio = True for the TTS subset (snac/bicodec/dac). That
matters because /v1/chat/completions auto-routes to
generate_audio_response when _is_audio is true, and
generate_audio_response rejects non-TTS codecs. A csm/whisper/
audio_vlm GGUF would have been misrouted into the TTS path.

Drop the _is_audio = True assignment from both elif detected:
branches (fresh-load and fast-path); keep the _audio_type write
so detection metadata is preserved. Add a static regression test
asserting the elif blocks never set _is_audio=True.

Validation: 252/252 (63 tests x py3.11/3.12/3.13/3.14) PASS.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-05-22 05:47:58 -07:00