Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(agents): wire multi-device selection end-to-end and validate at runtime#1338

Merged
kovtcharov-amd merged 3 commits into
mainfrom
claudia/task-aa875787
Jun 2, 2026
Merged

fix(agents): wire multi-device selection end-to-end and validate at runtime#1338
kovtcharov-amd merged 3 commits into
mainfrom
claudia/task-aa875787

Conversation

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Why this matters

Multi-device support shipped in #1252 but the device selector never actually changed anything. Before: picking NPU in the Agent UI dropdown (or passing --device npu) silently rebuilt the agent on the GPU model — the choice was a no-op, and a request for hardware the host didn't have ran somewhere else without warning. After: selecting a device switches the model (and context window) to that device's registered config, and an unavailable device fails loudly with an actionable remedy instead of degrading silently.

This addresses the v0.20.0 release-review blockers B1, B2, and H1, plus one medium GPU-tier bug:

  • B1 — UI dropdown was a silent no-op. The UI wrote session.device and evicted the agent, but every build site resolved the model from session["model"], never the device's DeviceConfig.model. Now the device's model/ctx drive the rebuilt agent, and a device switch rewrites the session model so eviction/recreation picks it up. A guard keeps an agent's own pinned model from being clobbered on the default GPU device.
  • B2 — runtime never validated the chosen device. ChatAgentConfig gained a device field that threads through the base Agent into LemonadeManager.ensure_ready(device=...). An NPU request on a host without an NPU now raises an actionable HardwareRequirementError ("NPU not available… run gaia init --profile npu, or choose --device gpu") instead of silently running elsewhere.
  • H1 — CLI swallowed all device-probe errors. except Exception: pass is narrowed to only treat genuine "server not reachable yet" (connection refused / timeout) as a soft pass; a reachable-but-broken Lemonade (HTTP 5xx, 401) now surfaces. The default GPU→CPU fallback says why ("No GPU detected; falling back to CPU (slower). Run gaia init…").
  • Medium — gpu rejected discrete Radeon. _DEVICE_TO_MIN mapped gpuamd_igpu, so an explicit GPU requirement misvalidated on a dGPU-only box. It now maps to the lowest GPU tier (amd_dgpu), accepting integrated or discrete graphics.

NPU quality eval is out of scope here and must be run by a maintainer on Ryzen AI hardware before tag:
gaia eval agent --device npu --category context_retention,rag_quality
NPU runs a 2B FLM model at 4K ctx vs GPU's 4B at 32K, so a quality delta is expected and needs hardware to measure — it can't run in CI.

Test plan

  • python util/lint.py --black --isort --flake8 — all pass
  • python -m pytest tests/unit/ -k "device or chat or agent" — 1519 passed (2 failures are pre-existing on main and unrelated: a Windows-only read_text()/cp1252 quirk over an existing ❌ emoji, and an unrelated memory-reconcile test)
  • New tests/unit/test_multi_device_wiring.py — device→model resolution (UI + CLI), gpu accepts igpu and dgpu, unavailable device raises an actionable error, device threaded base Agent → ensure_ready
  • New cases in tests/unit/chat/ui/test_chat_helpers_model_resolution.py — full _get_chat_response flow: NPU switches the model + threads device/min_context_size; explicit non-GPU device overrides a pinned model; default GPU does not clobber a pinned model; custom_model still wins
  • tests/unit/chat/ui/test_database.pyupdate_session(model=...) device-switch rewrite
  • Maintainer: NPU quality eval on Ryzen AI hardware (see note above)

@github-actions github-actions Bot added installer Installer changes llm LLM backend changes cli CLI changes tests Test changes performance Performance-critical changes agents labels Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

The core logic is correct and the fail-loudly approach is rigorously followed — NPU requests on CPU-only hosts now raise actionable errors with concrete remedies, and the long-standing B1 silent no-op is fixed end-to-end. Four items are worth addressing before merge, one of which is a real bug.


Issues

🟡 Bug: min_context_size falsy check rejects 0 (src/gaia/agents/chat/agent.py:366)

The guard if config.min_context_size else 32768 treats 0 as absent. Optional[int] fields should use is not None:

                config.min_context_size if config.min_context_size is not None else 32768

min_context_size=0 is never a valid real-world value, so this won't bite today, but the pattern is incorrect and will confuse the next reader who adds a device with an unusual ctx size.


🟡 DRY: multi-device resolution block is copy-pasted verbatim (src/gaia/ui/_chat_helpers.py:1108 and :1495)

The 14-line "Multi-device: the selected device dictates the model + ctx" block — including the nearly identical comment — is duplicated between _get_chat_response and _stream_chat_response. The only difference is the log message suffix "(streaming)". Extract to a helper:

def _apply_device_model(
    session: dict,
    agent_type: str,
    model_id: str | None,
    custom_model: str | None,
    registry=None,
    streaming: bool = False,
) -> tuple[str | None, int | None]:
    """Return (model_id, device_ctx) adjusted for the session's device."""
    device = session.get("device")
    if custom_model or not device:
        return model_id, None
    dev_model, dev_ctx = _resolve_device_model(agent_type, device, registry)
    if not dev_model:
        return model_id, None
    is_default_model = model_id in (None, _DB_DEFAULT_MODEL)
    device_is_explicit = device != "gpu"
    if dev_model == model_id or is_default_model or device_is_explicit:
        if dev_model != model_id:
            suffix = " (streaming)" if streaming else ""
            logger.info("chat: device=%s -> model %s (was %s)%s", device, dev_model, model_id, suffix)
        return dev_model, dev_ctx
    return model_id, None

Then both call sites collapse to one line each. The duplication isn't a bug now, but it's a divergence risk: the next change to the guard condition has to be made in two places.


🟡 Private function imported across module boundary (src/gaia/ui/routers/sessions.py:14)

_resolve_device_model is prefixed with _ (module-private by convention) but is imported in sessions.py. Either rename it resolve_device_model to signal it's part of the semi-public _chat_helpers interface, or move it to a shared _device_utils.py module. The current import is not wrong — Python doesn't enforce underscore privacy — but it breaks the "caller doesn't touch private helpers" convention and will confuse grep or auto-refactoring tools.


🟢 Fragile string heuristic for "server not reachable" (src/gaia/cli.py:657–660)

The _unreachable classification relies on two implementation-specific strings from lemonade_client.py:

_unreachable = (
    _probe_msg.startswith("Request failed:")
    and "with status" not in _probe_msg
)

This works today because lemonade_client.py:2498 uses "Request failed: {str(e)}" for RequestException and lemonade_client.py:3820 uses "Request failed with status {code}" for HTTP errors. But both strings can change independently of this guard, and any future LemonadeClientError subclass that formats its message differently could silently swallow an important error.

A more stable approach: check the exception type chain. requests.exceptions.ConnectionError and requests.exceptions.Timeout are what "server not reachable" looks like; they're wrapped by LemonadeClientError but the original exception is preserved in __cause__/__context__. Alternatively, add an is_connection_error: bool attribute to LemonadeClientError at the raise site and check that here. No suggestion block because this is an architectural decision — flagging for maintainer awareness.


🟢 Over-long comment block in ChatAgentConfig (src/gaia/agents/chat/agent.py:58–62)

CLAUDE.md calls for one short WHY comment per field, not a 5-line block covering both fields. The NPU 4K ctx constraint is the genuinely non-obvious part; the rest is derivable from the field name:

    # NPU FLM build requires 4K ctx; device is validated loudly at startup.
    device: Optional[str] = None
    min_context_size: Optional[int] = None

Strengths

  • Fail-loudly throughout: every path that accepted except Exception: pass is now narrowed — H1 is properly scoped to genuine "server not running yet" cases, and B2 raises HardwareRequirementError with a named remedy and a gaia init call-to-action.
  • _DEVICE_TO_MIN fix is clean: mapping "gpu" → "amd_dgpu" (the lowest GPU tier) correctly accepts either integrated or discrete Radeon without changing the existing _DEVICE_PRIORITY ordering. The added dgpu_only.json fixture pins the regression.
  • Test suite is thorough and well-structured: TestDeviceModelOverride covers the four non-trivial guard conditions (NPU overrides default, NPU overrides pinned, GPU preserves pinned, custom_model beats device), and TestGpuSelectorAcceptsBothGpuTiers gives the dgpu fix its own dedicated class with the pre-fix behaviour described in context.
  • Installer path fix is correct: both scripts had $SCRIPT_DIR/.. but the scripts live in installer/scripts/, so ../.. is the right root.

Verdict

Approve with suggestions. The is not None bug is the only issue that can cause wrong behaviour (though it requires min_context_size=0, which no current device config uses). The DRY violation and private-function import are the most likely sources of future maintenance pain. Everything else is correctly implemented and the test coverage is solid.

Reminder (from PR description): NPU quality eval must be run by a maintainer on Ryzen AI hardware before the tag. gaia eval agent --device npu --category context_retention,rag_quality.

@kovtcharov-amd kovtcharov-amd requested a review from itomek-amd June 1, 2026 22:27
@kovtcharov-amd kovtcharov-amd self-assigned this Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Four real release-blocker bugs fixed cleanly, with 347 lines of new tests that pin the behaviour before and after. The core logic is correct throughout — no blocking issues, two things worth addressing before the next PR touches this code.


Summary

This delivers on all four v0.20.0 blockers: _DEVICE_TO_MIN["gpu"] now maps to amd_dgpu instead of amd_igpu so a dGPU-only host isn't rejected; device validation runs on every ensure_ready call (memoised per tier, so the cost is one probe per distinct device per process); ChatAgentConfig carries device and threads it base Agent → LemonadeManager; and the CLI's blanket except Exception: pass is narrowed to treat only genuine connection failures as soft. Actionable error messages name the device, the remedy, and the next step — exactly the fail-loudly pattern CLAUDE.md requires.


Issues

🟡 Duplicate multi-device resolution block (_chat_helpers.py:1108–1136, 1509–1521)

The same ~26-line block appears verbatim in _get_chat_response and _stream_chat_response, differing only in one log suffix "(streaming)". If either path needs a bug fix it must be applied twice, and they've already drifted on that log string. Extract to a helper:

def _apply_device_model(
    session: dict,
    agent_type: str,
    registry,
    custom_model: str | None,
    model_id: str | None,
) -> tuple[str | None, int | None]:
    """Return (model_id, ctx_size) after applying the session device override."""
    if custom_model:
        return model_id, None
    device = session.get("device")
    dev_model, dev_ctx = _resolve_device_model(agent_type, device, registry)
    if not dev_model:
        return model_id, None
    is_default_model = model_id in (None, _DB_DEFAULT_MODEL)
    device_is_explicit = bool(device) and device != "gpu"
    if dev_model == model_id or is_default_model or device_is_explicit:
        if dev_model != model_id:
            logger.info("chat: device=%s -> model %s (was %s)", device, dev_model, model_id)
        return dev_model, dev_ctx
    return model_id, None

Then both call sites become two lines. The per-site device read can be a local before the call so the streaming log suffix can be dropped (it adds no value).


🟢 except Exception in _maybe_validate_device silently swallows programming errors (lemonade_manager.py:313–318)

The intent is to pass through when the server is unreachable — but except Exception also catches AttributeError (if get_system_info() returns a non-dict), ValueError, and any future bug introduced inside _validate_device_requirement. These will be logged at DEBUG and silently skipped, making them invisible in normal operation. The old code at least surfaced them as HardwareRequirementError. A minimum fix is to log at WARNING for anything that doesn't look like a connection error:

        except HardwareRequirementError:
            raise
        except Exception as e:
            # Only connection-level failures (refused / timeout) are soft here.
            # Unexpected errors get a WARNING so they're visible in logs.
            msg = str(e)
            is_conn = "refused" in msg or "timeout" in msg or "Request failed:" in msg
            level = cls._log.debug if is_conn else cls._log.warning
            level("Skipping device validation (%s): %s", "unreachable" if is_conn else "unexpected error", e)
            return

🟢 Issue/blocker references in docstrings will become stale

_resolve_device_model (_chat_helpers.py:473), _build_create_kwargs docstring (_chat_helpers.py:453), and the ChatAgentConfig comment block (chat/agent.py:71–76) all mention issue #1220, B1, release blocker B1. Per CLAUDE.md these belong in the PR description and commit message, not the code — they rot as soon as the issue is closed. One-line WHY comments are fine; internal ticket IDs and blocker labels are not.


🟢 Sessions router model-rewrite logic is untested at the HTTP layer

sessions.py:100–109 has non-trivial logic (guards on is_default_model, device_is_explicit, same-model no-op). The database-layer test in test_database.py covers db.update_session(model=...), but no test drives the PUT /api/sessions/{id} endpoint with device=npu and verifies the session's model field is rewritten. The FastAPI TestClient fixture in the existing test suite makes this straightforward to add in a follow-up.


🟢 CLI error-string matching is fragile (cli.py:629–133)

The _unreachable check parses LemonadeClientError message text with startswith("Request failed:") and "with status" not in _probe_msg. This works today but breaks silently if LemonadeClient's error-format changes. A LemonadeConnectionError subclass on the client side would let callers except LemonadeConnectionError without any string inspection. Worth raising as a follow-on issue.


Strengths

  • _DEVICE_TO_MIN bug fix is correct — mapping gpu → amd_dgpu (index 2) with detected_idx <= req_idx means iGPU (index 1) and dGPU (index 2) both satisfy the requirement; CPU (index 3) does not. The logic is sound and the new fixture dgpu_only.json pins the regression.
  • Memoisation is thread-safe and properly reset_validated_min_devices is checked and mutated inside with cls._lock:, and reset() reinitialises it to set(). Hardware is static per process so the "at most one probe per tier" invariant holds.
  • Test suite is comprehensivetest_multi_device_wiring.py (347 lines) covers device→tier mapping, both GPU tier satisfaction, unavailable device raises actionable errors, post-init validation, memoisation probe count, and the unreachable-server-≠-hardware-error distinction. The test_chat_helpers_model_resolution.py additions cover all four model-selection cases cleanly.
  • Error messages are actionable — every HardwareRequirementError names the requested device, the detected hardware, and a concrete gaia init remedy. Matches CLAUDE.md's three-part fail-loudly requirement exactly.
  • Installer path depth fixes are correct../.. is the right root from installer/scripts/, confirmed by existing tests that pass.

Verdict

Approve with suggestions. No blocking issues — the logic is correct and the four regressions are genuinely fixed. The duplicate device-resolution block (🟡) should be extracted before this area grows again; the other items are low-priority follow-ups. NPU quality eval on Ryzen AI hardware remains a mandatory pre-tag step per the PR description.

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough reviews — addressed the actionable items in b18f197:

  • min_context_size falsy bug → now uses is not None.
  • Duplicated device-resolution block → extracted to _apply_device_model; both chat paths are one line and can't drift.
  • _resolve_device_model private-name import → renamed to resolve_device_model (the sessions router imports it; matches the module convention where cross-module helpers drop the underscore).
  • _maybe_validate_device swallowing programming errors → genuine connection/timeout stays DEBUG; anything else now logs WARNING so it stays visible.
  • Fragile CLI error-string match → now prefers the preserved requests ConnectionError/Timeout in the exception cause chain, with the string check kept only as a fallback.
  • Stale issue/blocker IDs in code → stripped from comments/docstrings (kept in the PR/commit history).
  • Sessions router untested at HTTP layer → added TestSessionsRouterDeviceRewrite driving PUT /api/sessions/{id} (switch-to-NPU rewrites the model, GPU keeps default, title-only update leaves model untouched).

Deferred as a follow-on: a dedicated LemonadeConnectionError subclass on the client (cleaner than any cause-chain/string inspection) — worth its own issue since it touches every LemonadeClient raise site. The cause-chain check removes the immediate fragility in the meantime.

Lint passes; chat/ui suite green (675 passed) apart from two pre-existing failures unrelated to this change (a Windows-only read_text()/cp1252 quirk and an unrelated memory-reconcile test, both failing identically on main).

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Multi-device wiring is correctly end-to-end and the core logic is sound — the B1/B2/H1 blockers are genuinely fixed. One behavioral inconsistency in the soft-error path is worth tightening before merge; everything else is minor.


Summary

The PR correctly threads device from the UI/CLI through ChatAgentConfig → base AgentLemonadeManager.ensure_ready, validates hardware on every call (not just first init), and rewrites the session model on a device switch so the rebuilt agent loads the right model. The _DEVICE_TO_MIN promotion to module level and the amd_dgpu fix for the GPU tier make the behavior consistent with what a discrete-Radeon user would expect. Test coverage is thorough and the fixture-driven hardware simulation is a pattern worth replicating elsewhere.

The one issue worth addressing before tag: _maybe_validate_device soft-passes HTTP auth/5xx errors with only a WARNING, while cli.py correctly re-raises them. On a 401-protected Lemonade server the user gets a confusing warning log and then fails later in init instead of immediately.


Issues Found

🟡 _maybe_validate_device soft-passes HTTP errors — inconsistent with cli.py (lemonade_manager.py:344–350)

In cli.py (lines ~132–146), a probe that returns an HTTP 4xx/5xx re-raises because _unreachable = False (the "with status" not in str(_probe_err) guard catches it). In _maybe_validate_device the equivalent block has no such guard:

is_conn = (
    "Request failed:" in msg          # matches connection errors only — good
    or "refused" in msg.lower()
    or "timeout" in msg.lower()
    or "timed out" in msg.lower()
)

LemonadeClient.get_system_info raises LemonadeClientError("Request failed with status 401: …") on auth failure. "Request failed:" in "Request failed with status 401: …" is False (no colon immediately after "failed"), so the 401 case correctly falls to is_conn=False and logs a WARNING — but then the function still returns (soft pass). The device validation is skipped with only a warning, and the user sees a confusing sequence instead of an immediate actionable error.

Suggested fix — re-raise on HTTP errors so the failure surfaces at device-validation time, matching cli.py:

            is_conn = (
                "Request failed:" in msg
                or "refused" in msg.lower()
                or "timeout" in msg.lower()
                or "timed out" in msg.lower()
            )
            if not is_conn and "with status" in msg:
                # Reachable server returned an HTTP error (auth, 5xx) —
                # surface it now rather than hiding it behind a WARNING.
                raise
            if is_conn:

🟢 Probe LemonadeClient created with keep_alive=True but immediately discarded (lemonade_manager.py:327,331)

The probe client is a throwaway — it exists only to call get_system_info and is never stored or reused. Using keep_alive=True opens a persistent connection that lingers for the life of the process, consuming a connection slot unnecessarily.

            if base_url:
                probe = LemonadeClient(
                    base_url=base_url, keep_alive=False, verbose=not quiet
                )
            else:
                probe = LemonadeClient(
                    host=host, port=port, keep_alive=False, verbose=not quiet
                )

🟢 Logic duplicated between _apply_device_model and the sessions router (routers/sessions.py:107–108)

Both files contain:

device_is_explicit = device != "gpu"
if resolved != current_model and (is_default_model or device_is_explicit):

The sessions router can't call _apply_device_model directly (it operates on a request dict, not a live session), but extracting the guard into a small _should_override_model(device, current_model) helper would keep the rule in one place and prevent the two from drifting. Not blocking — worth a follow-up issue.

🟢 Nit: _validated_min_devices missing ClassVar annotation (lemonade_manager.py:149)

    _validated_min_devices: ClassVar[set] = set()

Requires adding ClassVar to the from typing import at line 15.


Strengths

  • Comparison direction is correct. detected_idx <= req_idx satisfies the requirement when the host's best device has equal or lower index (higher capability) — the iGPU/dGPU tier equivalence works exactly as described in the module-level comment.
  • Memoisation is correctly scoped. _validated_min_devices is keyed by required_min_device tier, not raw device string, so gpu from two different call sites shares one probe. It's cleared by reset(), so test isolation is clean.
  • Test suite is comprehensive. test_multi_device_wiring.py covers all four axes (tier mapping, GPU accepts igpu+dgpu, unavailable device raises actionably, device threaded through the agent, unreachable server ≠ hardware error, memoisation, sessions HTTP layer). The dgpu_only.json fixture fills the one gap the pre-existing fixtures left.
  • _format_device_error follows the CLAUDE.md fail-loudly rule. The error names what's missing, what the user should do, and where to go next.

Verdict

Approve with suggestions. The 🟡 _maybe_validate_device HTTP-error path is the only item worth addressing before the v0.20.0 tag — everything else is a nit. The core hardware-validation logic, device→model resolution, and memoisation are all correct.

Ovtcharov added 3 commits June 1, 2026 18:24
…untime

Multi-device support (#1252) shipped non-functional: the Agent UI device
dropdown and CLI --device flag never changed the model, and an unavailable
device degraded silently. Fixes v0.20.0 release-review blockers B1/B2/H1
plus a GPU-tier validation bug.

B1: resolve the device's DeviceConfig.model (and ctx_size) at every UI
agent-build site instead of always using the session/GPU model; rewrite the
session model on a device switch so eviction/recreation picks it up. A guard
keeps an agent's own pinned model from being clobbered on the default GPU.

B2: add a device field to ChatAgentConfig, thread it through the base Agent
into LemonadeManager.ensure_ready(device=...), and fail loudly with an
actionable HardwareRequirementError when the requested device is absent.

H1: narrow the CLI device-probe except clause to only swallow connection/
timeout errors; a reachable-but-broken Lemonade now surfaces. Make the
GPU->CPU fallback state its reason.

Medium: map the gpu selector to amd_dgpu (lowest GPU tier) so a discrete-
Radeon-only host satisfies an explicit GPU requirement.
…st init

Self-review caught that B2 only worked on the first agent construction per
process. LemonadeManager.ensure_ready has an `_initialized` singleton
fast-path that returns before the device-validation block, so in the
long-running Agent UI server a device switch after warm-up (the dropdown's
whole purpose) silently bypassed validation — picking NPU on a non-NPU box
would fail later at model-load instead of with the actionable error.

Hoist device validation ahead of the fast-path so it runs on every call,
memoised per device tier (hardware is static per process, so a passing tier
is probed at most once). A server that's unreachable during the probe is no
longer mislabelled as missing hardware — it returns not-ready as before.
Self-review + bot review follow-ups:

- Fix Optional[int] falsy bug: ChatAgentConfig.min_context_size now uses
  `is not None` so a future device with ctx 0 isn't silently treated as unset.
- DRY: extract the duplicated device-resolution block from the streaming and
  non-streaming chat paths into `_apply_device_model`; both call sites are now
  one line and can't drift.
- Rename `_resolve_device_model` -> `resolve_device_model` (drop the private
  underscore) since the sessions router imports it; matches the module's
  convention where cross-module helpers are public.
- `_maybe_validate_device` now logs unexpected probe errors at WARNING (only
  genuine connection/timeout failures stay at DEBUG), so a malformed response
  or a bug isn't silently swallowed.
- CLI device probe prefers the preserved requests ConnectionError/Timeout in
  the exception cause chain over wrapper-message string matching, with the
  string check kept only as a fallback.
- Strip internal issue/blocker IDs from code comments/docstrings (they belong
  in the PR/commit, not the source); add HTTP-layer tests for the sessions
  router device->model rewrite.
@kovtcharov-amd kovtcharov-amd force-pushed the claudia/task-aa875787 branch from b18f197 to 0eadc87 Compare June 2, 2026 01:27
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Multi-device wiring is now end-to-end and correct. All three release blockers (B1/B2/H1) and the dGPU tier mapping bug are addressed cleanly. Two nits below — neither blocks merge.


Summary

Before this PR, the device dropdown was purely cosmetic: session["device"] was written and the agent was evicted, but every rebuild site resolved the model from session["model"] (always the GPU default), so NPU selection silently ran on GPU. The runtime never validated whether the requested device existed, and the CLI swallowed every device-probe exception — including auth errors that should have surfaced. All four problems are fixed here with a coherent set of changes across the UI layer (_chat_helpers.py, sessions.py), the manager (lemonade_manager.py), the base agent, and the CLI.

The _validated_min_devices memoisation design is the clearest way to solve "validate on every call but only probe once per tier" without adding a per-request round-trip, and the implementation is correct. The _DEVICE_TO_MIN["gpu"] → "amd_dgpu" fix is the right direction: requiring the lowest GPU tier and using detected_idx ≤ req_idx correctly accepts iGPU, dGPU, or NPU-with-iGPU hosts when gpu is selected.


Issues Found

🟢 Minor — deferred import requests inside except handler (src/gaia/cli.py:660)

requests is a direct dependency (setup.py:126), so the import is safe — but placing it inside an except block is non-idiomatic and slightly obscures the dependency. Because the string-matching fallback already handles the case where __cause__ isn't preserved, the requests type-check is an extra safety net rather than the load-bearing path. Two clean options:

Option A — move the import to the module top (already has from gaia.llm.lemonade_client import LemonadeClientError there):

import requests

(with the other stdlib/third-party imports at the file top)

Option B — drop the isinstance check entirely and rely solely on the string fallback, which covers the same cases without a new import:

                    _unreachable = (
                        str(_probe_err).startswith("Request failed:")
                        and "with status" not in str(_probe_err)
                    )

Either is fine; the current code is not broken.


🟢 Minor — f-string in logger.debug call inside _validate_device_requirement (src/gaia/llm/lemonade_manager.py:301–303)

The string is always formatted even when the debug level is disabled. This is a pre-existing pattern that was moved here during the refactor, so the PR has a natural opportunity to fix it:

            cls._log.debug(
                "Hardware requirement satisfied: %s -> recipe=%s", highest, recipe
            )

Strengths

  • _validated_min_devices memoisation elegantly solves the "validate on every ensure_ready call, but don't re-probe the same tier" requirement without any per-request overhead. The reset in LemonadeManager.reset() keeps the lifecycle clean.
  • _format_device_error produces messages that satisfy the CLAUDE.md fail-loudly rule precisely: they name what failed, what the user should do, and where to look (gaia init --profile npu, --device gpu).
  • Test coverage is comprehensive and purposeful. TestDeviceValidatedAfterWarmInit.test_device_switch_validated_when_already_initialized directly pins the regression scenario (UI switch after warm init); test_passing_device_is_memoized guards against per-request probes; test_unreachable_server_does_not_become_hardware_error ensures a down server doesn't mislabel as missing hardware. The class-level docstrings also communicate the pre-fix behaviour clearly.

Verdict

Approve with suggestions. Both issues are nits; neither affects correctness or the fix for B1/B2/H1. The NPU quality eval note is appropriate — this is a CI-unrunnable check that the maintainer owns before tag, not a reason to block the code change.

@kovtcharov-amd kovtcharov-amd enabled auto-merge June 2, 2026 14:20

@itomek itomek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. Verified the full chain locally on HEAD (0eadc87): B1 (device→model resolution + persisted-model rewrite on switch), B2 (device threaded into ensure_ready, _maybe_validate_device runs before the _initialized fast-path, memoised per tier, raises HardwareRequirementError; warm-init case tested), H1 (CLI re-raise on with status), and the dGPU gpuamd_dgpu mapping. Touched tests: 109 passed. lint.py --black --isort: clean. Description is exemplary — leads with user-observable before/after per blocker.

One follow-up to consider before/after merge (your call):

  • The bot's still-live round-3 point on lemonade_manager.py:344-350 is real. The device-validation path keys "reachable-but-broken" on "Request failed:", so a "Request failed with status 401: ..." falls through to a WARNING soft-return instead of re-raising — the opposite of what cli.py does for the same condition. It's bounded (a downstream model load still fails, so it's delayed-not-silent) and the H1 claim is correctly scoped to the CLI probe path, so nothing in the description is false. But it's a genuine fail-loudly inconsistency. Two ways to close it:
    • A (fix): align the device path with cli.py — treat "with status" (5xx/401) as re-raise, not soft-pass.
    • B (defer): fold it into the same follow-up issue tracking the LemonadeConnectionError subclass, with a one-line note that the device path currently soft-passes reachable-but-broken servers.

Optional/non-blocking:

  • No Closes #N auto-link — the body references #1252/#1220 and B1/B2/H1 informally; adding Fixes #1252 (or the relevant tracker) would wire up auto-close.
  • No docs/ touch. This is a bug-fix on the feature shipped in #1252, but --device is arguably newly-functional end-to-end now — worth a quick check on whether docs/reference/cli.mdx needs a line.

The remaining round-3 nits (probe keep_alive, _validated_min_devices ClassVar, deferred import requests, debug f-string, _apply_device_model/sessions guard dup) are minor and fine to sweep into the follow-up. Nice work threading this through cleanly.


Generated by Claude Code

@kovtcharov-amd kovtcharov-amd added this pull request to the merge queue Jun 2, 2026
Merged via the queue into main with commit 1fe50b6 Jun 2, 2026
43 checks passed
@kovtcharov-amd kovtcharov-amd deleted the claudia/task-aa875787 branch June 2, 2026 15:32
kovtcharov-amd pushed a commit that referenced this pull request Jun 3, 2026
19 PRs merged to main after the v0.20.0 notes were drafted; merge main into
the release branch and update the notes to match.

- Multi-Device: note #1338 wired the selector end-to-end (it was a no-op
  after #1252 alone) and added loud runtime validation for unavailable devices.
- PowerPoint RAG: note #1366 unblocked .pptx in the Agent UI file picker
  (backend already accepted it; the frontend rejected it pre-upload).
- Activations: note #1310 widened the "Active for" panel to MCP-consuming
  agents (e.g. the chat agent), not just static REQUIRED_CONNECTORS.
- New bug fixes: RAG embedding reuse (#1306), overlapping-chat-turn 409
  (#1304), Memory/Settings mutual exclusivity (#1368).
- Tooling/CI: doc-vs-code realignment (#1298/#1337/#1340), gaia-testing
  skill (#1372), Claude CI + Codecov hardening.
- Full Changelog regenerated: 44 -> 63 commits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents cli CLI changes installer Installer changes llm LLM backend changes performance Performance-critical changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants