Switch openCodeMlx default model to Qwen3.8-27B (mlx-vlm) - #54
Open
perNyfelt wants to merge 18 commits into
Open
Switch openCodeMlx default model to Qwen3.8-27B (mlx-vlm)#54perNyfelt wants to merge 18 commits into
perNyfelt wants to merge 18 commits into
Conversation
…mlx-vlm Qwen3.8-27B-8bit is a vision-language checkpoint that mlx_lm.server can't load (unsupported architecture), so serving it requires mlx-vlm's own OpenAI-compatible server instead. Auto-detect the backend per model from its downloaded config.json (vision_config presence) so future MLX_MODEL swaps between text-only and vision-capable checkpoints keep working with just the one-line change the script already supports. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Member
Author
|
Reviewed PR #54 ✅ Verified against the actual environment:
Review notes (non-blocking):
|
Switch to mlx-community/Qwen3.8-27B-4bit paired with the Qwen3.8-27B-MTP-4bit draft head via mlx_vlm.server's --draft-model/--draft-kind mtp, since the 8-bit model alone was too slow for practical use. Verified live: 17.8 tok/s decode (up from ~12 tok/s baseline) with a ~77% draft-token acceptance rate. Also bind model downloads to a specific network interface (MLX_DOWNLOAD_INTERFACE, default en7) via a socket.socket subclass in the huggingface_hub/modelscope download snippets, since GlobalProtect VPN was found to reset every connection to huggingface.co outright. Disables hf_xet when binding is active, since its separate Rust networking stack bypasses the socket patch and was observed to hang indefinitely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Member
Author
|
Follow-up: the 8-bit model was too slow in practice, so this branch now switches to Also fixed model downloads to bind to a specific network interface ( Live-verified on real hardware:
|
…default This is an open-source repo; defaulting to a specific machine's network interface name doesn't generalize. MLX_DOWNLOAD_INTERFACE now defaults to empty (default route), and users can opt in with their own interface name if they hit VPN issues like the GlobalProtect one described in the comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidates scattered Ollama model config (names, context sizes, custom model recipes) into src/main/bin/lca as the single canonical source, with models.sh deriving from it and application.properties picking it up via LCA_* env vars lca exports at launch. Switches the chat/review models from the mlx variant to llama.cpp, raises the review model's context to 128k, and applies num_gpu/num_batch tuning to the main model for full GPU offload on Apple Silicon. Fixes rebuild_custom_model_if_changed() to fingerprint the full desired Modelfile recipe (base id + context + extra params), not just the base model's id — previously a context/parameter-only change went undetected and a stale custom model silently kept running. Adds a /benchmark command (ModelRegistry.benchmark() calling Ollama's /api/generate directly) to measure real tokens/sec for a given model, optionally against a real file's contents via --prompt-file, so future model/config changes can be measured instead of guessed at. Also fixes two intent-routing bugs found while wiring /benchmark in: literal, well-formed slash commands (e.g. "/benchmark --model x") no longer risk being misrouted by the LLM intent classifier — they're dispatched directly when CommandExecutor already recognizes them. And COMMAND_PATTERN's \w+ was truncating hyphenated command names at the first hyphen, so /git-apply and the entirely-unwired /git-push could never actually be dispatched by name; both are fixed and wired up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…k PR - models.sh's createCustomModel now fingerprints the same base_id|ctx|extra signature lca's rebuild_custom_model_if_changed uses, sharing its MODEL_STATE_DIR — previously it only checked "does a model with this name exist?", so the exact staleness bug fixed on the lca side was still live when running models.sh directly (a context/param-only edit silently kept serving the old custom model, requiring -f to notice). - models.sh now asserts every variable it reads from lca via grep+eval actually resolved to a non-empty value, failing fast instead of silently proceeding with an empty model name/context if the grep alternation and lca's variable names ever drift apart. - /benchmark --max-tokens 0 no longer silently becomes 200 (Groovy truthiness treats 0 as falsy under "?: 200"); negative/zero values are now rejected with a clear message instead of passing num_predict: -1 through to Ollama unbounded. - /benchmark's tokens/sec output now formats with Locale.ROOT so it's consistent across JVM locales — the point of the numbers is comparing them across config/model changes. - Added a test guarding the known asymmetric drift risk in CommandExecutor's KNOWN_COMMANDS bypass set: every command it claims to recognize is asserted to actually dispatch instead of falling through to the unknown-command fallback. - Fixed a stale qwen3.6-128k reference in application-batch-test.properties' comment (model was renamed to qwen3.8-192k earlier in this branch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…smatch, stale label
- ensure_mlx_vlm_current() now carries the same "transformers>=5.7,<5.13"
pin as ensure_mlx_lm_current: both share one venv, so an unconstrained
"pip install mlx-vlm" could upgrade transformers past 5.13 to satisfy its
own metadata, silently breaking mlx-lm's AutoTokenizer.register call at
import time - and mlx_lm.server is still used unconditionally for the
small background model regardless of which backend serves the main model.
- MLX_DRAFT_MODEL is no longer downloaded when the main model isn't served
via mlx_vlm: --draft-model is only ever passed on that path, so gating on
MLX_DRAFT_MODEL alone downloaded a multi-GB drafter that start_mlx_server_instance
would then silently discard on the mlx_lm path. Warns instead when this happens.
- Added a fail-fast quant-suffix check (new _quant_suffix helper) so a
MLX_DRAFT_MODEL whose quant level doesn't match MLX_MODEL's is rejected
with a clear error before downloading anything, instead of silently
serving a mismatched drafter/base-model pair.
- --max-kv-size (the mlx_vlm path's closest analogue to --prompt-cache-bytes)
is now overridable via MLX_VLM_MAX_KV_SIZE, with the comment now stating
plainly that reusing MLX_CONTEXT_LIMIT as a token-count stand-in is an
unverified guess at the real per-token KV footprint, not a measured bound.
- cleanup()'s stop_and_report label now reflects the actually-detected
backend ("mlx_vlm.server (main)" vs "mlx_lm.server (main)") instead of
always saying mlx_lm.server, matching the startup message.
- Added test_updates_mlxvlm.sh (mirrors test_updates_mlxlm.sh, asserting
the transformers pin survives both the install and update paths) and
test_quant_suffix.sh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-suggest, /applyBlocks
These seven commands already existed as working ShellCommands methods (and
were listed in help() and assistant.intent.allowed-commands) but had no
case in CommandExecutor's dispatch switch and were absent from
KNOWN_COMMANDS. That's the same structural gap /benchmark hit earlier:
typing one of them verbatim fell through to "Unknown command" instead of
either dispatching directly or reaching the LLM intent classifier for a
command it doesn't describe either - closing part of the coverage risk
flagged in the /benchmark misrouting review (a case added to the switch
without a KNOWN_COMMANDS entry, or vice versa, silently breaks dispatch).
While wiring --confirm/--dry-run/--secret-scan flags for /stage, /revert,
/commit-suggest and /applyBlocks, hit the exact truthiness trap already
fixed once for /benchmark's --max-tokens 0: "parseBoolean(x) ?: true"
silently turns an explicit "--flag false" back into true, since Groovy's
Elvis operator treats a parsed `false` as absent. Added a shared
parseBooleanFlag(value, default) helper using an explicit null check
instead, and used it everywhere a flag's default is `true` (so an explicit
false actually takes effect) in this newly-wired code.
Also fixed a stale test: `isKnownCommand('/model --set foo')` was
asserting false from before /model was wired in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Benchmarked via mlx_vlm.generate --verbose against MLX_MODEL: the Qwen3.8-27B-MTP-4bit drafter only achieved ~55% draft-token acceptance, making it a net loss - 12.5 tok/s generation with the drafter enabled vs 21.3 tok/s without it. MLX_DRAFT_MODEL now defaults to empty (speculative decoding off) instead of always attempting it. Also replaces LOCAL_MODEL_PROMPT with a looser variant (still grounds claims in shown content only, but drops the no-subagents/hard-limits rules that turned out to be too restrictive for this model). The prior working tree had two back-to-back assignments to the same variable - the first was dead (immediately shadowed by the second) and is removed here; only the surviving, actually-in-effect prompt remains. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main() runs under set -e, and its call sites assign _quant_suffix's result via bare "main_quant=$(_quant_suffix "$MLX_MODEL")" - not inside an if/while condition, so the assignment takes the exit status of the command substitution directly. _quant_suffix's body was "[[ ... ]] && echo ...", which returns 1 (the [[ ]]'s own status) on a no-match model id, silently killing the whole script right after the multi-GB model download - no error message, and no cleanup trap installed yet (that happens later in main()). Trigger: mlx_vlm backend + MLX_DRAFT_MODEL set (the default) + an MLX_MODEL id not ending in "<N>bit" (e.g. an unquantized/bf16 VLM checkpoint). _quant_suffix now always returns 0, echoing the match (or nothing) without letting a non-match propagate as a command failure. Added a set -e regression test to test_quant_suffix.sh (asserts a bare assignment under set -e still reaches the next line on a non-matching model id) - the existing tests only checked stdout via $(...), which never runs under set -e and so never exercised this path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
parseBoolean(parsed.confirm) ?: true hits the same Groovy truthiness trap parseBooleanFlag was introduced to fix two commits ago: parseBoolean returns Boolean.FALSE for an explicit "--confirm false", and Elvis treats that false as absent, re-enabling the confirmation prompt the caller asked to skip. Switched to parseBooleanFlag(parsed.confirm, true). Found and fixed the identical pre-existing bug in executeGitApply (--check, --confirm) and executeApply (--dry-run, --confirm) while looking - same class of defect, same fix already available. Added dispatch/regression tests for /git-push, /gitapply, and /apply covering both the happy path and an explicit "false" override of each previously-broken flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
([0-9]+bit)$ didn't match ids like "...-4bit-DWQ" or "...-8bit_dwq", so
the draft/main quant-mismatch guard silently no-op'd for those - a
mismatched drafter could still be served without the fail-fast check
ever firing. Now allows exactly one optional "-word"/"_word" qualifier
after the bit count.
Deliberately NOT "([0-9]+bit)([-_][A-Za-z]+)*$" (zero-or-more): tested
that against the existing "does not match a mid-string bit token" case
and it wrongly matches "8bit-prefixed-model" as an 8bit id, since a
multi-word non-quant tail ("-prefixed-model") also satisfies a
repeated-group pattern. The tightened single-optional-group version
passes both the DWQ cases and that existing negative case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverts the prompt half of dee6966 (no-subagents, exploration-budget, and response-length rules removed there). That change was bundled into an otherwise evidence-backed drafter-perf commit with no evidence of its own beyond "turned out to be too restrictive," and it landed on exactly the model (4-bit) already flagged in this project's own history as unable to follow even the surviving grounding rules regardless of prompt phrasing - not the model to be loosening guardrails on without data showing it helps. The drafter/perf fix from that commit is unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ecutor Same trap fixed in the last two commits, now closed everywhere it was still reachable from newly-touched or newly-wired code: - /review --log-review false was silently forced back to true (parseBoolean(x) ?: true). - /search --headless false was silently forced back to true, same pattern. - /run --confirm false was silently forced back to true - the same user-visible defect just fixed for /apply, /gitapply and /git-push. - /context --padding 0 fell back to the default 2: ShellCommands.context explicitly allows 0 (requireMin(padding, 0, ...)), but "parseInt(x) ?: 2" treats a parsed 0 as absent, the int-flavored version of the same bug already fixed for /benchmark's --max-tokens 0. All four switched to parseBooleanFlag / an explicit null check, with a dispatch test asserting the explicit "false"/"0" case for each. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_quant_suffix.sh existed and would have caught the set -e/exit-1 regression fixed a few commits back, but nothing ran it as part of the build - it only ran when someone remembered to invoke it by hand, which is exactly how that regression shipped unnoticed. Discovers every demo/test_*.sh (except the shared test_helpers.sh library) at test time and asserts each exits 0 with no "FAIL:" lines, rather than hardcoding a list that would itself go stale the next time a demo test file is added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bient state test_updates_mlxvlm.sh, test_updates_mlxlm.sh and test_updates_opencode.sh's "not installed" scenarios all relied on a tool's absence from a fixed system PATH being a property of the CI/dev machine, not of the test. Verified by reproducing the exact flip: stubbing python3 to succeed made test_updates_mlxvlm.sh's scenario B assert 1, get 0 - the "already installed -> warn and continue" branch fired instead of the intended "not installed -> hard error" branch. This was harmless while nobody ran these scripts as part of a build; wiring them into ./mvnw test (DemoTestScriptsSpec) made a real machine's local Python/tooling state load-bearing for whether the build passes. - test_updates_mlxlm.sh / test_updates_mlxvlm.sh: explicitly stub mlx_lm.server / python3 to fail in the "not installed" scenarios, instead of relying on the real system binary being absent. - test_updates_opencode.sh: can't stub "opencode is absent" the same way (opencode itself is the thing being checked, not something routed through a shared interpreter/pip). Added a guard that checks whether a real opencode is resolvable on the test's REAL_PATH before running the "not installed" scenarios (A, B, E) and skips them with a clear message if so, rather than letting them silently exercise the "already installed -> real opencode upgrade" branch - a real network call against whatever opencode install actually exists on that machine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… to tracked files process.waitFor() had no timeout, and since one of these scripts (test_updates_opencode.sh) can reach a real, network-touching "opencode upgrade" on a machine where opencode is on PATH, an unbounded wait turns that into a hung ./mvnw test instead of a failure. Note that process.inputStream.text (the prior way of collecting output) itself blocks until the stream closes - a timeout on waitFor() alone wouldn't have helped, since a hung process would never reach that call. Output is now collected on a background thread concurrently with a bounded process.waitFor(120s), with destroyForcibly() on expiry. Also pins script discovery to `git ls-files demo` output, so an untracked scratch script (e.g. a developer's local demo/test_whatever.sh) can't silently join the build - falls back to unfiltered disk listing only if git itself is unavailable. Verified by dropping an untracked test_scratch_untracked.sh (that would fail loudly if run) into demo/ and confirming it's excluded from the discovered script list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… just fixed gitTrackedDemoTestScripts()'s `git ls-files` call did "String output = process.inputStream.text" (a blocking read to EOF) followed by a bounded waitFor(30s) - the exact blocking-read-before- bounded-wait bug the main test-runner path had just been fixed for in the same commit. A hung git (index lock, credential prompt) would hang on the read before the timeout was ever reached, making it decorative. Extracted the concurrent-reader pattern into a shared runBounded() helper used by both the demo-script runner and the git subprocess, so there's one place this gets fixed instead of two copies to keep in sync. Also: - Switched the shared output buffer from StringBuilder to StringBuffer. reader.join(5000) only supplies a happens-before edge when the reader thread actually terminates in time; on the timeout/force-kill path it may not have, leaving outputBuffer.toString() as an unsynchronized read of a StringBuilder that could still be mid-append on another thread. StringBuffer's synchronized methods close that gap. - Added an explicit assertion when the git-tracked-name filter narrows discovery to zero scripts, with a message pointing at the tracked-files lookup specifically (not "demo/ has no tests"). Verified first that Spock actually already throws "Data provider has no data" on an empty where: list rather than silently running zero iterations - so the silent-green-build failure mode itself doesn't occur, but the assertion gives a far clearer diagnosis than that generic message would. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…the rarest case
The previous fix made scenarios A/B/E skip (rather than silently
mis-exercise) when a real opencode is resolvable on REAL_PATH - but
DemoTestScriptsSpec only asserts exit 0 and no "FAIL:", and a skip
satisfies both, so on a machine with opencode on REAL_PATH those three
scenarios would quietly disappear from coverage while the build still
reported green.
Narrowed the PATH used by scenarios A/B/E (and the guard's own check)
from REAL_PATH ("/usr/bin:/bin:/usr/local/bin") to
"/usr/bin:/bin" - dropping the one directory (/usr/local/bin) a
system-wide opencode install might realistically symlink into on this
project's target platform. The official installer already puts opencode
in ~/.opencode/bin, off REAL_PATH to begin with; /usr/bin and /bin alone
still cover every coreutil (mkdir/cat/chmod/bash/sh) these scenarios use,
verified by confirming each resolves under /bin on this machine. This
converts the skip from "silently hit on common developer/Homebrew setups"
to "only hit if opencode is symlinked directly into /usr/bin or /bin" -
essentially never, while keeping the guard itself as a defense-in-depth
fallback rather than removing it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
demo/openCodeMlx's defaultMLX_MODELtomlx-community/Qwen3.8-27B-4bit, a vision-language checkpoint that plainmlx_lm.servercan't load (unsupported architecture).detect_model_backend(), which inspects the downloaded model'sconfig.jsonfor avision_configkey to auto-pickmlx_vlmvsmlx_lmas the serving backend — so futureMLX_MODELswaps between text-only and vision-capable checkpoints keep working with the existing "swap one line" UX, no second env var to keep in sync.ensure_mlx_vlm_current()(mirrorsensure_mlx_lm_current's not-installed→hard-error / already-installed→warn-and-continue pattern), only invoked when the detected backend needs it.start_mlx_server_instance()now branches betweenmlx_lm.server(--prompt-cache-bytes) andpython3 -m mlx_vlm.server(--max-kv-size,--trust-remote-code) based on the detected backend. The small background model (Qwen2.5-Coder-1.5B-Instruct-4bit) is explicitly pinned tomlx_lmand unaffected.Known tradeoff (accepted):
mlx-vlm's prompt-prefix caching is limited, so opencode's system prompt may get reprocessed every turn instead of cached like themlx_lm.serverpath — a documented upstream opencode+mlx-vlm behavior. This is a real latency cost for the coding-agent workflow, not fixed here.Also in this PR: LCA model-config consolidation, GPU tuning, and /benchmark
Unrelated to the
demo/openCodeMlxwork above, but landed on this branch:src/main/bin/lcaas the single canonical source.models.shnow derives its model list fromlcavia a targeted grep+eval instead of duplicating it by hand, andapplication.propertiespicks the same values up throughLCA_*env vars thatlcaexports at launch (with literal fallbacks for./run.sh/mvnw test/IDE runs that bypasslca).mlxvariant tollama.cpp, raises the review model's context window to 128k, and appliesnum_gpu/num_batchModelfile tuning to the main model for full GPU offload on Apple Silicon.rebuild_custom_model_if_changed()inlcato fingerprint the full desired Modelfile recipe (base model id + context size + extra params) instead of just the base model's id — previously a context- or parameter-only change went undetected, so a stale custom model kept running silently after a config edit./benchmarkcommand (ModelRegistry.benchmark(), calling Ollama's/api/generatedirectly since Spring AI/Embabel doesn't surface Ollama's raw timing fields) to measure real prompt-eval/generation tokens-per-second for a given model — optionally against a real file's contents via--prompt-file, so future model or config changes can be measured instead of guessed at./benchmarkin:/health) was being routed through the LLM-based intent classifier before dispatch; a command absent from its hand-curated description list could get reinterpreted as something else entirely (observed:/benchmark --model xwas misrouted to/run, which then tried to executebenchmarkas a literal shell binary). Literal, well-formed slash commandsCommandExecutoralready recognizes now dispatch directly, skipping the classifier.CommandExecutor's command-parsing regex used\w+, which doesn't match hyphens, so/git-applywas silently truncated to/gitand could never dispatch by name;/git-pushwas additionally never wired into the dispatch switch at all despite having a workingShellCommands.gitPush(...)handler. Both are fixed.Follow-up fixes from code review
demo/openCodeMlx:ensure_mlx_vlm_current()now carries the sametransformers>=5.7,<5.13pin asensure_mlx_lm_current— both packages share one venv, so an unconstrainedpip install mlx-vlmcould upgradetransformerspast the versionmlx-lm'sAutoTokenizer.registercall tolerates, silently breakingmlx_lm.server(still used unconditionally for the small background model regardless of the main model's backend).MLX_DRAFT_MODELis no longer downloaded when the main model isn't served viamlx_vlm(speculative decoding is only wired up there); a mismatched quant level betweenMLX_DRAFT_MODELandMLX_MODELis now rejected up front with a clear error instead of silently serving a mismatched pair.--max-kv-sizeOOM guard is now overridable viaMLX_VLM_MAX_KV_SIZE, with the comment now honestly stating that reusingMLX_CONTEXT_LIMITas a stand-in is an unverified guess, not a measured bound.cleanup()'s shutdown message now names the actually-detected backend (mlx_vlm.server (main)vsmlx_lm.server (main)) instead of always sayingmlx_lm.server./model,/context,/version,/stage,/revert,/commit-suggest, and/applyBlocksintoCommandExecutor's dispatch switch andKNOWN_COMMANDSset — these already worked asShellCommandsmethods (and were documented/allow-listed) but fell through to "Unknown command" when typed directly, the same class of gap/benchmarkhit. While wiring their--confirm/--dry-run/--secret-scanflags, fixed the same Groovy truthiness trap already fixed once for/benchmark's--max-tokens 0:parseBoolean(x) ?: truewas silently turning an explicit--flag falseback intotrue.Second round of review fixes
demo/openCodeMlx:_quant_suffix()'s regex used a bare[[ ... ]] && echo ..., so a non-matching model id returned exit status 1; assigned via a barex=$(_quant_suffix ...)undermain()'sset -e, that silently killed the whole script right after the multi-GB model download with no error message. Now always returns 0._quant_suffix()also now matches DWQ/AWQ-style qualified ids (...-4bit-DWQ,...-8bit_dwq), which the original([0-9]+bit)$missed, letting the draft/main quant-mismatch guard silently no-op for them.LOCAL_MODEL_PROMPTrewrite that had been bundled into the drafter-perf commit — it removed exploration-budget/response-length/no-subagents rules with no evidence backing the change, on the exact 4-bit model this project's own history already flags as unable to follow prompt-level fixes reliably. The drafter perf fix itself (disabling speculative decoding by default) is unaffected.parseBoolean(x) ?: true/parseInt(x) ?: Ntruthiness-trap sites inCommandExecutor—/review --log-review false,/search --headless false,/run --confirm false(all silently forced back to their default), and/context --padding 0(silently reset to 2).DemoTestScriptsSpecso everydemo/test_*.shnow runs as part of./mvnw test— the_quant_suffixset -eregression above had a passing hand-written test the whole time, but nothing executed it automatically.Third round of review fixes
test_updates_mlxvlm.sh,test_updates_mlxlm.shandtest_updates_opencode.sh's "not installed" scenarios all silently depended on a tool's absence from the real system PATH — a property of the machine running them, not of the code under test. Verified by reproducing the exact flip (stubbingpython3to succeed madetest_updates_mlxvlm.shassert1, get0). Harmless while nobody ran these by hand; load-bearing now that they're wired into./mvnw test. Fixed by stubbing absence explicitly (mlx_lm.server/python3) where possible, and by adding a real-environment guard fortest_updates_opencode.sh(opencode itself is what's being checked, so its "not installed" scenarios now skip with a clear message if a realopencodeis actually resolvable on the test's PATH, rather than silently running a realopencode upgrade).DemoTestScriptsSpechang risk:process.waitFor()had no timeout, and one of these scripts can reach a real network call on a misconfigured machine. Output is now collected on a background thread concurrently with a boundedwaitFor(120s)plusdestroyForcibly()on expiry — a timeout onwaitFor()alone wouldn't have been enough, since the prior blockingprocess.inputStream.textread would hang before ever reaching it.DemoTestScriptsSpecdiscovery scope: pinned togit ls-files demooutput so an untracked scratch script can't silently join the build (verified by dropping in an untracked failing script and confirming it's excluded).Fourth round of review fixes
DemoTestScriptsSpec's own git subprocess had the same bug it had just fixed:gitTrackedDemoTestScripts()'sgit ls-filescall blocked onprocess.inputStream.textbefore its boundedwaitFor(30s)— a hunggit(index lock, credential prompt) would hang there regardless of the timeout. Extracted a sharedrunBounded()helper (concurrent reader thread + bounded wait + forced kill) used by both process invocations, so there's one place this gets fixed instead of two to keep in sync. Also switched the shared output buffer fromStringBuildertoStringBuffer—reader.join(5000)only gives a happens-before guarantee when the reader actually finishes in time, and on the timeout path it might not have."Data provider has no data"on an emptywhere:list (doesn't silently pass), then added an explicit assertion anyway for a much clearer diagnosis pointing at the tracked-files lookup specifically.DemoTestScriptsSpeconly asserts exit 0 and noFAIL:, and the guard's skip path satisfied both — so on a machine with a realopencodeonREAL_PATH, those three scenarios would quietly vanish from coverage while the build stayed green. Narrowed the PATH those scenarios use from/usr/bin:/bin:/usr/local/binto/usr/bin:/bin(verifiedmkdir/cat/chmod/bash/shall still resolve there) — the official opencode installer already targets~/.opencode/bin, offREAL_PATHentirely, so this converts an environment-dependent skip that could fire on a normal Homebrew setup into one that essentially never fires, while keeping the guard itself as a defense-in-depth fallback.Test plan
bash -n demo/openCodeMlxsyntax checkdemo/test_updates_opencode.sh— 8/8 passing (no regression)demo/test_updates_mlxvlm.sh(new) — 9/9 passing, asserts the sharedtransformerspin survives both install and update pathsdemo/test_quant_suffix.sh(new) — 5/5 passingmlx_vlm,mlx-vlmauto-installedmlx_vlm.server --helpoutput matches the flags used in the script/v1/modelsresponds (what the script polls)tool_callsresponse, confirming the agentic use case works end-to-end./mvnw test— full suite green (1180 tests) across all four review rounds, including demo/test_*.sh now running as bounded, deterministic JVM tests viaDemoTestScriptsSpecwith no unbounded subprocess waits anywhere in that spec/benchmark --model qwen3.8-review:latest --prompt-file <real source file>reports sensible tokens/sec against the tuned model🤖 Generated with Claude Code