fix(inference): route vision at vision-capable models and refresh local model defaults (#5146) - #5253
Conversation
…al defaults Vision requests could be sent to models that cannot see, and the local model defaults had drifted from what Ollama actually publishes. Part 1 - vision routing (tinyhumansai#5146): - `MVP_ALLOWED_VISION_MODELS` was `&[""]`, so `effective_vision_model_id` rewrote *every* configured vision model to the empty string, including genuinely vision-capable ones. Callers then handed that empty name to `ensure_ollama_model_available`, which issued a nameless `POST /api/pull` and retried it three times before failing opaquely. Replaced with a capability check; the tier restriction it stood in for is already enforced upstream by `vision_mode_for_config`. - Added `inference::vision_models`, a registry of which local model IDs accept image input. Ollama does not reject an `images` array sent to a text-only model: it drops the images and answers from the prompt alone, so a chat-only model yields a fluent, fabricated description instead of an error. The registry encodes the two traps this hit: Gemma 3 is text-only at 270M/1B and multimodal from 4B up, and `gemma3n` is a separate text-only model despite the shared prefix. - `DEFAULT_OLLAMA_VISION_MODEL` was `""`; it is now `moondream:1.8b-v2-q4_K_S`, the smallest genuinely vision-capable model that pulls with no extra setup. - Added `resolve_vision_model_id`, used by `vision_prompt`, so an unconfigured or unavailable vision model produces a message naming the config key to set and the `ollama pull` to run, instead of an empty model id. Unavailable-model errors now read as vision problems. Part 1.3 - default model list audit (tinyhumansai#5146): Every ID below was verified against the live registry (`GET registry.ollama.ai/v2/library/<name>/manifests/<tag>` returning 200). - `MVP_ALLOWED_CHAT_MODELS` did not cover `gemma3:270m-it-qat` (1 GB tier) or `gemma3:4b-it-qat` (8-16 GB tier), so applying either preset resolved straight back to the 1B default: the user picked a tier and silently got a different model. Both added. - The 16 GB+ tier used `gemma3n:e4b-it-q8_0` for chat *and* vision. Gemma 3n is text-only, so that tier's `Bundled` vision mode pointed at a model with no vision encoder. tinyhumansai#5055 chose it because no `gemma4` namespace existed then; Gemma 4 has since been published and is multimodal at every size, so the tier is back to one model (`gemma4:e4b-it-q8_0`) serving both. `gemma3n:e4b-it-q8_0` stays allowlisted for back-compat. - The 8-16 GB and 16 GB+ tiers named `nomic-embed-text:latest`, which the embedding allowlist already rewrote to `bge-m3` at resolution time. Named `bge-m3` directly so the presets state what is actually pulled, and because the Memory Tree requires 1024-dim vectors. Download estimates updated to match. Two tests asserted "there is no gemma4 namespace", a fact that has since expired. Replaced with the durable invariants: preset model IDs are fully qualified, every preset chat model is allowlisted and resolves unchanged, and every preset that declares a vision mode names a vision-capable model. Part 3 - documentation (tinyhumansai#5146): - New `gitbooks/features/model-routing/local-and-byok-models.md`: the three routes (managed, BYOK, local), what each supports for chat/vision/ embeddings, per-model capability and RAM-tier tables, provider slugs, workload routing fields, mixing routes, and troubleshooting. - `local-ai.md`: new local-vision section, corrected embedding default and download sizes. - `model-routing/README.md` and root README now state that the subscription is a default rather than a requirement. No UI strings added, so no i18n changes.
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe change adds capability-based local vision model resolution, updates local model presets and embeddings, isolates installer test state, and documents managed, BYOK, and fully local routing options. ChangesModel routing and local vision
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VisionRequest
participant ModelResolver
participant Ollama
VisionRequest->>ModelResolver: Resolve vision-capable model
ModelResolver-->>VisionRequest: Return model ID or configuration error
VisionRequest->>Ollama: Ensure model availability and generate response
Ollama-->>VisionRequest: Return result or unavailable-model error
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
|
| Filename | Overview |
|---|---|
| src/openhuman/inference/vision_models.rs | New capability registry; uses exact family matching with TEXT_ONLY_FAMILIES denylist and size-aware gemma3 logic. Conservative (unknown = false). Well-tested including regression guards for gemma3n. |
| src/openhuman/inference/model_ids.rs | Replaces empty-string vision allowlist with enforce_vision_capability; adds resolve_vision_model_choice with VisionModelChoice.replaced for transparent capability-substitution reporting; expands MVP_ALLOWED_CHAT_MODELS to cover all preset models. |
| src/openhuman/inference/local/service/vision_embed.rs | Uses resolve_vision_model_choice instead of effective_vision_model_id; surfaces substitution_note in pull-failure errors; three new mock-server tests drive the full vision_prompt path end-to-end. |
| src/openhuman/inference/presets.rs | 8-16 GB tier embedding corrected from nomic-embed-text to bge-m3; 16 GB+ tier chat/vision updated to gemma4:e4b-it-q8_0; download sizes updated; old gemma4-namespace assertion replaced with durable invariants. |
| src/openhuman/inference/local/install_piper.rs | Adds SharedRootOverride RAII guard to redirect OPENHUMAN_WORKSPACE to TempDir for the duration of the permissions test, fixing a latent race that parallel test scheduling exposed. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[vision_prompt called] --> B{vision_mode_for_config}
B -- Disabled --> C[Return early: vision disabled]
B -- Ondemand / Bundled --> D[resolve_vision_model_choice]
D --> E{effective_vision_model_id}
E --> F{config.vision_model_id empty?}
F -- Yes --> G[Return Err: no vision model configured]
F -- No --> H{Alias normalization?}
H -- Yes --> I[Normalize to DEFAULT_LOW_VISION_MODEL]
H -- No --> J[Use raw value]
I --> K[enforce_vision_capability]
J --> K
K --> L{is_vision_capable?}
L -- Yes --> M[Return id unchanged]
L -- No --> N[Warn + return DEFAULT_OLLAMA_VISION_MODEL]
M --> O[VisionModelChoice: model=id, replaced=None]
N --> P[VisionModelChoice: model=moondream, replaced=Some configured id]
O --> Q[ensure_ollama_model_available]
P --> Q
Q -- Ok --> R[Issue vision request with images array]
Q -- Err --> S[Return Err: vision model unavailable + substitution note]
Reviews (5): Last reviewed commit: "fix(test): drop the unreachable vision r..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 131c7f9931
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/inference/presets.rs (1)
160-211: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPreset updates will reclassify existing 8-16 GB / 16 GB+ users as "Custom" tier.
current_tier_from_configexact-matchesconfig.{chat,vision,embedding}_model_idagainst the current preset definitions. Users who already applied the 8-16 GB tier (nomic-embed-text:latest) or the 16 GB+ tier (gemma3n:e4b-it-q8_0) before this change have those old values persisted in config. After this update, neither theselected_tierfast-path nor theall_presets()fallback loop will match, so they'll silently fall through toModelTier::Customeven though model resolution itself still works fine via the chat allowlist back-compat entry. This is purely a tier-detection/UI regression, but it will confuse users who see their preset badge flip to "Custom" after an update they didn't initiate.Consider having
current_tier_from_config(or a migration step in config load) also recognize the previous generation of model ids for these two tiers, so the tier label survives the model swap.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/inference/presets.rs` around lines 160 - 211, Update current_tier_from_config to recognize the previous model identifiers for the 8–16 GB and 16 GB+ presets, including nomic-embed-text:latest and gemma3n:e4b-it-q8_0, in addition to current preset values. Ensure users with those persisted configurations retain the corresponding preset tier instead of falling through all_presets() to ModelTier::Custom.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@gitbooks/features/model-routing/local-and-byok-models.md`:
- Around line 69-75: Update the tier table’s Embeddings entries to stop
recommending all-minilm for Memory Tree-compatible presets, replacing the 1 GB
and 4–8 GB entries with bge-m3 as used by the other compatible tiers.
- Around line 25-28: Update the comparison table’s “Data leaves your machine”
row to explicitly refer to inference data, such as “Inference data leaves your
machine,” while preserving the existing local, BYOK, and hosted values. Keep the
surrounding explanation of backend-dependent features unchanged.
In `@src/openhuman/inference/local/service/vision_embed.rs`:
- Around line 55-99: Add tests for the vision request flow covering both
`resolve_vision_model_id` failure and `ensure_ollama_model_available` failure,
in addition to the existing disabled-vision coverage. Assert each branch returns
the expected error and updates `vision_state` to `"missing"`, while preserving
the successful path behavior.
---
Outside diff comments:
In `@src/openhuman/inference/presets.rs`:
- Around line 160-211: Update current_tier_from_config to recognize the previous
model identifiers for the 8–16 GB and 16 GB+ presets, including
nomic-embed-text:latest and gemma3n:e4b-it-q8_0, in addition to current preset
values. Ensure users with those persisted configurations retain the
corresponding preset tier instead of falling through all_presets() to
ModelTier::Custom.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 36892b41-f3f1-4410-824f-9eba2d6abb89
📒 Files selected for processing (10)
README.mdgitbooks/SUMMARY.mdgitbooks/features/model-routing/README.mdgitbooks/features/model-routing/local-ai.mdgitbooks/features/model-routing/local-and-byok-models.mdsrc/openhuman/inference/local/service/vision_embed.rssrc/openhuman/inference/mod.rssrc/openhuman/inference/model_ids.rssrc/openhuman/inference/presets.rssrc/openhuman/inference/vision_models.rs
…d-to-end CI failure: `non_executable_workspace_binary_is_skipped_so_path_can_win` panicked with `NotFound` on its second `set_permissions`. `workspace_piper_binary_candidates` resolves through `paths::shared_root_dir`, which ignores `config.workspace_dir` unless `OPENHUMAN_WORKSPACE` is set and otherwise returns the real `~/.openhuman/bin/piper`. The test's `temp_config()` TempDir therefore never isolated it: it writes into the same shared directory that its sibling install_piper / install_whisper / paths tests use, and several of those call `wipe_shared_install_dir`. It was the only test in the module touching that directory without taking `shared_install_lock()`, so a concurrent wipe could delete the stub between its two `chmod` calls. Pre-existing latent race rather than a new break. This PR surfaced it because CI Lite derives the libtest filter from the changed paths: touching `src/openhuman/inference/...` selects `openhuman::inference`, and the tests added here changed the parallel scheduling enough to interleave the two. Fix follows the convention already established in the file: take the module lock, and wipe the shared dir on entry and exit so the executable stub cannot leak into `find_workspace_piper_binary_returns_none_without_install`. Also adds three `vision_prompt` tests driving the real path against a mock Ollama server, which pin the actual tinyhumansai#5146 defect rather than only its resolution helpers: - a configured vision-capable model reaches Ollama unchanged (this is what regressed to `model: ""` under the old `&[""]` allowlist) - a chat-only model configured for vision never receives the images - an unpullable vision model reports a vision error naming the model and the `ollama pull` that fixes it `ready_service` marks the status "ready" so `bootstrap` returns early; no process launch or network beyond the mock is involved.
|
@coderabbitai review |
✅ Action performedReview finished.
|
…ll-minilm tiers CodeRabbit review on tinyhumansai#5253, both Major and both doc-only: - The comparison table's "Data leaves your machine" row read as an absolute privacy guarantee for the local route, while the paragraph below it says sign-in, OAuth, billing, and meeting agents still reach the backend. Scoped the row to inference data and made the caveat explicit. - The tier table recommended `all-minilm:latest` for the 1 GB and 4-8 GB tiers while the text above it says the Memory Tree needs 1024-dim vectors and that all-minilm fails the dimension check. That contradiction was mine. The presets genuinely do ship all-minilm on those tiers, so the doc now states that plainly and tells the reader how to work around it rather than quietly recommending a model that will fail. Aligning the presets stays follow-up, as noted in the PR.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/inference/local/install_piper.rs`:
- Around line 661-662: Update the test setup around the stub-writing flow
containing wipe_shared_install_dir(&config) to create a Drop-based cleanup guard
before any shared-directory mutation, ensuring cleanup runs during unwinding as
well as normal completion. Make the guard perform best-effort cleanup and remove
the existing tail-only wipe_shared_install_dir call.
- Around line 628-643: Update the test’s path resolution so
workspace_piper_binary_candidates and wipe_shared_install_dir use a temporary
shared root under the TempDir rather than the real ~/.openhuman/bin/piper,
regardless of ambient environment variables. Adjust the relevant test setup and
cleanup while preserving shared_install_lock synchronization and existing
assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6753a92a-ad96-4a46-ad39-95a669feb09b
📒 Files selected for processing (3)
gitbooks/features/model-routing/local-and-byok-models.mdsrc/openhuman/inference/local/install_piper.rssrc/openhuman/inference/local/service/vision_embed.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- gitbooks/features/model-routing/local-and-byok-models.md
…ion (tinyhumansai#5146) Piper test wrote to the real user install (CodeRabbit, critical) — the permissions test resolved through `paths::shared_root_dir`, which ignores `config.workspace_dir` unless OPENHUMAN_WORKSPACE is set and otherwise returns the developer's real `~/.openhuman/bin/piper`. A normal `cargo test` could therefore delete an installed Piper and leave a stub in its place. A `SharedRootOverride` guard now points the shared root at the test's own TempDir, so writes and cleanup stay inside it. The tail `wipe_shared_install_dir` is gone with it: it only ran when every assertion passed, whereas TempDir and the guard drop on the panic path too (CodeRabbit, major). Silent vision substitution (greptile P1) — when the configured vision model is chat-only the capability guard swaps in a vision-capable default, and a later "model is not available, pull it" error named only the substitute. A user who configured `gemma3n:…` was told to pull `moondream:…` with nothing connecting the two. `resolve_vision_model_choice` now reports what it replaced and the error explains the swap. Alias rewrites (`moondream` -> the pinned tag) are not reported: same model, nothing to explain. Local vision in chat (codex P2) — `vision_provider` routes the vision workload but does not let a turn carry an image: rehydration consults the per-model registry (`model_vision_enabled` reads it exclusively), not the new capability registry. Documented the required `model_registry` vision flag rather than wiring the registries together, which would change chat behaviour well beyond this docs PR. Tests: the resolver-failure branch of `vision_prompt`, the substitution note in the unavailable-model error, and `resolve_vision_model_choice`'s reporting rules. `resolve_vision_model_id` is folded into the new API rather than left as a test-only wrapper. The `bge-m3` preset carve-out is kept and explained inline rather than retagged (greptile P2) — see the reply on that thread.
|
@coderabbitai review |
✅ Action performedReview finished.
|
…ansai#5146) CI showed `vision_prompt_reports_an_unconfigured_vision_model` failing on the RAM-tier message: a blank `vision_model_id` makes `vision_mode_for_config` report `Disabled`, so the disabled branch returns before the resolver is reached. Every config that could trip the resolver's empty case is therefore already short-circuited, making that arm defence in depth rather than a reachable branch. Removed the test and documented why at the call site so it is not re-added; the empty case stays covered directly in `model_ids::tests::resolve_vision_model_id_errors_when_unconfigured`.
|
@coderabbitai review |
✅ Action performedReview finished.
|
…bility routing The raw-coverage voice e2e expected vision state 'ondemand' for the fixture id 'vision-ready', which the pre-tinyhumansai#5253 MVP allowlist rejected (fallback model absent from mock tags -> ondemand). tinyhumansai#5253's is_vision_capable heuristic accepts the id (vision marker in the name), the mock's /api/tags advertises it, and a present Ondemand-mode vision model correctly reports 'ready'. Stale expectation, intentional behavior change — only surfaced by the full-suite fallback this PR triggers.
Summary
MVP_ALLOWED_VISION_MODELSwas&[""], so every configured vision model was rewritten to the empty string and handed to Ollama as a nameless model. Replaced with a real capability check.inference::vision_modelsregistry encoding which local model IDs actually accept image input, so a vision tool-call reaches a vision-capable model or surfaces a clear error, never a chat-only model.DEFAULT_OLLAMA_VISION_MODELwas""; it is nowmoondream:1.8b-v2-q4_K_S, the smallest genuinely vision-capable model that pulls with no extra setup.Problem
1. Vision resolution was structurally broken.
MVP_ALLOWED_VISION_MODELS: &[&str] = &[""]is an allowlist whose only member is the empty string, soeffective_vision_model_idredirected every configured vision model to"", including genuinely vision-capable ones. A user who setlocal_ai.vision_model_id = "llava:7b"on the Custom tier passed theVisionMode::Disabledguard (non-empty id implies Ondemand), then reachedensure_ollama_model_available(config, "", "vision"), which issuedPOST /api/pull {"name": ""}and retried it three times with backoff before failing with an opaque error.2. A vision tier pointed at a blind model. The 16 GB+ preset used
gemma3n:e4b-it-q8_0as itsvision_model_idwithVisionMode::Bundled. Gemma 3n is text-only on Ollama. This fails quietly rather than loudly: Ollama accepts animagesarray against a text-only model, discards it, and answers from the prompt text, so the user receives a fluent and entirely fabricated description of an image the model never saw.3. Presets disagreed with the allowlist.
gemma3:270m-it-qat(1 GB tier) andgemma3:4b-it-qat(8-16 GB tier) were not inMVP_ALLOWED_CHAT_MODELS, so applying either preset resolved straight back togemma3:1b-it-qat. The user picked a tier and silently got a different model than the one advertised.Solution
Vision capability registry
src/openhuman/inference/vision_models.rsmaps model IDs to image-input capability, keyed on whole family names rather than loose substrings so near-misses cannot collide. It encodes the two traps this bug hit:gemma3:latestis the 4B build.gemma3nis notgemma3. It is a separate, text-only model that shares the prefix. It is the load-bearing entry inTEXT_ONLY_FAMILIES.Unknown IDs resolve to "not vision-capable", so the caller reports a missing vision model rather than shipping images to a model that will ignore them.
Resolution path
enforce_vision_capabilityreplacesenforce_mvp_vision_allowlist. The tier restriction the old allowlist stood in for is already enforced upstream bypresets::vision_mode_for_config, which reportsVisionMode::Disabledfor tiers that ship no vision model; what remained was purely the capability question. A chat-only configured model now logs a warning and falls back to a vision-capable default rather than being passed through.effective_vision_model_idkeeps itsStringsignature for status/reporting surfaces (empty means "not configured", which is a legitimate state), but a non-empty return is now always vision-capable.resolve_vision_model_idis used byvision_promptfor actual requests. It never returns an empty ID: an unconfigured vision model produces a message naming the config key to set and the models to pull. A configured-but-unpullable model now reads as a vision problem with the exactollama pullcommand, rather than a generic pull failure.Default model list audit (§1.3)
Every ID below was verified against the live registry,
GET https://registry.ollama.ai/v2/library/<name>/manifests/<tag>returning200, plus the published capability badges onollama.com/library/<name>.MVP_ALLOWED_CHAT_MODELSgainsgemma3:270m-it-qat,gemma3:4b-it-qatgemma3n:e4b-it-q8_0togemma4:e4b-it-q8_0(chat + vision)nomic-embed-text:latesttobge-m3bge-m3at resolution time. Memory Tree needs 1024-dim vectorsapprox_download_gbrefreshed on both tiersOn re-adopting
gemma4: #5055 moved this tier offgemma4:e4bbecause nogemma4namespace existed on Ollama at the time. That is no longer true. Gemma 4 has since been published,gemma4:e4b-it-q8_0resolves (11.6 GB, 128K context), and it is multimodal, so the tier returns to one model serving both chat and vision, matching how the 8-16 GB tier usesgemma3:4b-it-qat.gemma3n:e4b-it-q8_0stays allowlisted for back-compat with anyone who already pulled it.Documentation (Part 3)
gitbooks/features/model-routing/local-and-byok-models.md: the three routes (managed / BYOK / local) with a capability comparison, per-model chat-vision-embeddings table, RAM tier table, provider slugs, workload routing fields, mixing routes, and troubleshooting keyed to the new error messages.local-ai.md: new local-vision section explaining why a chat-only model fails silently, plus corrected embedding default and download sizes.model-routing/README.mdand the root README now state plainly that the subscription is a default rather than a requirement.Submission Checklist
vision_promptcode paths changed here are now driven end-to-end against a mock Ollama server rather than only through their resolution helpers.N/A: behaviour-only change— no feature rows added, removed, or renamedN/A— no new matrix feature IDsN/A: no release-cut surface touched— local model defaults and docs only## RelatedImpact
app/.vision_model_idwill start actually pulling and using that model.current_tier_from_configmatches on model IDs, so users already on the 8-16 GB or 16 GB+ tiers will reverse-lookup asCustomafter this change. Both tiers are outside the current MVP ceiling (MVP_MAX_TIER = Ram2To4Gb) and so are not reachable through the normal preset flow.Related
gemma4-does-not-exist assumption from Fix local model reliability: validate local model behavior in Tiny Agents and correct provider configuration #5055.all-minilm:latest(384-dim), which the Memory Tree's 1024-dim on-disk format rejects at embed time. Left alone here because it is allowlisted for back-compat and is an embeddings concern rather than a vision one.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/GH-5146-vision-model-defaults-docsValidation Run
pnpm --filter openhuman-app format:check— N/A, no frontend files changedpnpm typecheck— N/A, no TypeScript changedRust Core Coverage(libtest filteropenhuman::inference). All 18 tests added in the first commit passed on the first run; the threevision_promptmock-server tests were added in the second.cargo fmtrun and clean (formatter only, no compilation)app/src-tauriuntouchedValidation Blocked
command:cargo check/cargo test/ any compile steperror:not run — explicitly disallowed for this task; CI is the verification pathimpact:compilation and test results are unverified locally. Update: CI has since compiled and run everything.Rust Quality (fmt, clippy)andRust Feature-Gate Smoke (gates off)passed, and all 18 new tests passed. The one failure was an unrelated pre-existing race ininstall_piper, fixed in the second commit and explained below. Original note follows: To compensate, every consumer of the changed symbols was manually audited (effective_vision_model_idcall sites inbootstrap.rs,assets.rs,model_pull.rs,diagnostics.rs,types.rs— all are gated behind a non-Disabledvision mode or are report-only), test names were checked for collisions across the inline and sibling test modules inpresets.rs, and no existing test was found to pin the preset values that changed. Please treat CI as the gate on this PR.Behavior Changes
Parity Contract
effective_vision_model_idkeeps its signature and its empty-means-not-configured contract;gemma3n:e4b-it-q8_0stays allowlisted; the LM Studio bypasses on chat and embeddings are untouched; the embedding allowlist is unchanged.vision_mode_for_configremains the tier gate, unchanged. Every existingensure_ollama_model_availablevision call site was verified to sit behind a non-Disabledvision mode. Both-directions tests pin the new invariants (preset vision models are vision-capable; preset chat models resolve unchanged).CI Fix (second commit)
non_executable_workspace_binary_is_skipped_so_path_can_winfailed withNotFoundon its secondset_permissions. It is not related to the visionchange:
workspace_piper_binary_candidatesresolves throughpaths::shared_root_dir, which ignoresconfig.workspace_dirunlessOPENHUMAN_WORKSPACEis set and otherwise returns the real~/.openhuman/bin/piper. The test'stemp_config()TempDir therefore neverisolated it, and it was the only test in the module writing to that shared
directory without taking
shared_install_lock(), while siblings callwipe_shared_install_dir.A latent race, surfaced rather than caused by this PR: CI Lite derives the
libtest filter from changed paths, so touching
src/openhuman/inference/...selects
openhuman::inference, and the tests added here changed the parallelscheduling enough to interleave the write and the wipe. Fixed by following the
convention already used by every other test in that file.
Duplicate / Superseded PR Handling
Summary by CodeRabbit