feat: per-machine hardware profiles — detect the card, fit the box (13-task plan)#110
Conversation
…nment + markdownlint + rubric gate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
#81, t7) Add lobes assess --probes: a role that is /health-healthy but semantically wrong now FAILS its probe, instead of only /health being checked. - cortex: known-answer question, temperature 0, exact-match on a forced short completion (POST /v1/chat/completions). - embedder: embeds a sentence + paraphrase + unrelated string; PASS iff cos(sentence, paraphrase) > cos(sentence, unrelated). Enforces a hard per-request timeout and treats a timeout as FAIL, not skip — this is the shape that would have caught the sm_110 FLASH_ATTN hang (request accepted, never answered). - reranker: PASS iff the one relevant document ranks first by relevance_score (POST /v1/rerank). Each probe is a pure function of (url, model) over the existing stdlib _post()/_get() transport, monkeypatchable exactly like the existing run_correctness() probes — no new dependency. The CLI wiring resolves each role's own endpoint via the existing role registry (lobes.roles.role_registry_from_env), the same builder lobes capabilities/ lobes measure use, so --probes stays read-only (no compose/env writes). 29 new tests in tests/test_role_probes.py cover pass/fail/timeout/malformed paths for all three probes (mocked transport, no network/GPU), the run_role_probes/render_role_probes glue, and the CLI surface (--json, --role filter, --timeout, no --apply, never touches docker). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
One module per card under lobes/machines/ — each a CardStrategy owning its own detection signature, per-role knobs and per-knob provenance — plus a small stdlib registry (register/unregister/get/names/strategies/detect) and a shared _traits module so sm_110 pooling quirks compose without copy-paste. - thor is now load-tested: cortex kv_cache_dtype=auto, embedder/reranker on TRITON_ATTN (+ reranker enforce_eager) via the shared SM_110 trait; the legacy single-model row no longer claims the contradicted flashinfer. - detect() is honest (returns None on no match); generic never auto-matches. - registration order == detection precedence (spark before blackwell); pinned off isort so it is never alphabetised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
MachineProfile / MACHINE_PROFILES / machine_profile / detect_machine / machines_as_dicts stop carrying a per-chip table and are rebuilt from lobes.machines on each access. MACHINE_PROFILES is served via module __getattr__ (PEP 562) so a chip registered after import is reflected with no edit here. All legacy behaviour (and its callers in switch/benchmark) is preserved; detect_machine keeps its silent generic fallback over the registry's honest None resolver. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Covers the three acceptance criteria: a synthetic chip registered at runtime flows through detection, profile resolution, MACHINE_PROFILES and knob rendering with zero edits elsewhere; the sm_110 trait is shared not copied (board override still wins); thor carries the measured knobs and its legacy row drops flashinfer while detect_machine still falls back to generic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…les derived Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Every machine-dependent knob the Thor (sm_110) box hand-edits now flows
through .env with today's exact rendered behavior as the default (GB10
sees zero behavior change):
- vllm-primary --kv-cache-dtype -> ${PRIMARY_KV_CACHE_DTYPE:-fp8}
- vllm-embed / vllm-rerank gain --attention-config, defaulting to
"auto" (verified against vllm/config/attention.py: the string
"auto" is vLLM's own automatic-selection sentinel, so this is
byte-equivalent in effect to today's flagless render, not a guess)
- vllm-rerank gains an enforce-eager toggle, defaulting to the
explicit --no-enforce-eager no-op (vLLM's enforce_eager field uses
argparse.BooleanOptionalAction, so both --enforce-eager and
--no-enforce-eager are always-valid tokens, never a conditionally-
omitted flag)
- the legacy single-model template's --kv-cache-dtype gets the same
treatment (VLLM_KV_CACHE_DTYPE)
Verified with `docker compose config`: the default render is unchanged,
and setting only PRIMARY_KV_CACHE_DTYPE=auto, EMBED_ATTENTION_BACKEND=
TRITON_ATTN, RERANK_ATTENTION_BACKEND=TRITON_ATTN, RERANK_ENFORCE_EAGER=
--enforce-eager reproduces /home/thor/.lobes/docker-compose.yml's
vllm-primary/vllm-embed/vllm-rerank commands byte-for-byte.
Deviation (binding, per the task brief): MULTIMODAL_ATTENTION_BACKEND on
the senses/vllm-multimodal gear is left exactly as-is, with a comment
pointing at #109 — its removal is gated on a GB10
live-verification, not this task.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…force-eager); MULTIMODAL_ATTENTION_BACKEND kept pending #109 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Add lobes/runtime/_detect.py: gathers host facts (nvidia-smi device name + compute_cap, /proc/meminfo total memory, /proc/device-tree/model, hostname) and resolves them via lobes.machines.detect() to a registered card name or UNKNOWN. Never reads nvidia-smi memory.used/memory.total (reports [N/A] on unified-memory boards); total memory comes from /proc/meminfo instead. Every probe is injectable and degrades to None on failure, never raising out of detect_card(). Stdlib only, no torch import. No registry extension was needed — name_markers already distinguish spark (GB10, sm_121) from thor (sm_110). 29 new tests in tests/test_detect.py; full suite 1260 passed / 14 skipped (baseline 1231 passed / 14 skipped).
…achines registry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Convert lobes/profiles.py into a package (lobes/profiles/) so the new
per-role fleet schema can live alongside the untouched legacy
WorkloadProfile/MachineProfile surface:
- lobes/profiles/schema.py: frozen Profile/RoleProfile dataclasses covering
cortex/senses/embedder/reranker, each with feasible/model plus the seven
machine knobs (gpu_mem_util, max_model_len, quantization, kv_cache_dtype,
attention_backend, enforce_eager, max_num_seqs). Unknown role/knob names
raise ModelGearError on load rather than being silently dropped.
- lobes/profiles/loader.py: resolves built-ins (packaged via
importlib.resources) + operator-defined profiles discovered under
<deploy-dir>/profiles/<name>.toml, with operator profiles overriding a
built-in of the same name. Nothing mutates a loaded Profile.
- lobes/profiles/builtin/{spark,thor}.toml: spark reproduces today's fleet
compose template values exactly (util 0.30/0.14/0.06/0.06, cortex 131072,
senses 32768, embed/rerank 8192); thor shares that baseline and the loader
overlays its 4 validated sm_110 divergences (cortex kv_cache_dtype=auto;
embedder+reranker attention_backend=TRITON_ATTN; reranker
enforce_eager=true) live from lobes.machines' thor CardStrategy/SM_110
trait, so the divergent values stay single-sourced instead of being
re-typed into the TOML.
Deviation: ships TOML, not the plan's spark.yaml/thor.yaml — the repo is
stdlib-only (dependencies = [], no YAML parser) and requires-python >=3.12
guarantees tomllib, so TOML is a zero-new-dependency stand-in for the same
data shape.
All 1231 pre-existing tests pass unmodified; 29 tests added (1260 passed, 14
skipped total). lobes/profiles/__init__.py re-exports the new schema/loader
API alongside the full legacy surface (MachineProfile, MACHINE_PROFILES,
detect_machine, resolve_serve_config, ...).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…operator override Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
detect card -> pick profile -> render .env: `lobes init` now resolves a fleet Profile (lobes.profiles, t1) via the injected host-card detector (lobes.runtime._detect, t2) and writes its per-role knobs into .env through a new pure lobes/profiles/render.py mapping (role -> PRIMARY_/ MULTIMODAL_/EMBED_/RERANK_ env prefix, knob -> env suffix, enforce_eager bool -> --enforce-eager/--no-enforce-eager flag token, feasible=False -> a <PREFIX>_FEASIBLE=false marker for a later task to honor). - --profile <name> overrides detection; forcing a profile onto a card it wasn't validated for (or an undetected one) warns on stderr but proceeds. - An UNKNOWN card with no --profile refuses (ModelGearError, EXIT_USER_ERROR) naming every detected fact and requiring --profile — never silently falls back to spark. Refuses before anything is written, dry run or apply. - Profile resolution/rendering is fleet-only; --single is unaffected (no detection call at all). - _apply_profile_env skips a key whose resolved value numerically matches what write_scaffold already copied from env.example, so a profile that agrees with the shipped template (e.g. spark on a fresh fleet .env, or thor's 3 unchanged knobs) doesn't reformat an untouched default. lobes/cli/_runtime_ops.py gains resolve_init_profile(), the shared detect+resolve+warn/refuse glue, with an injectable detect_fn (default resolved at call time so tests monkeypatch lobes.runtime._detect.detect_card directly, matching this repo's existing probe-neutralisation idiom). Verified on this Thor box: a bare `lobes init` (real detection, no flags) picks the thor profile.
…-> .env), --profile override, UNKNOWN refuses Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Pin profile_env(resolve_profile(name)) for every shipped profile (spark,
thor) plus the fleet compose template's ${VAR:-default} surface as
committed goldens, diffed byte-for-byte. Rendering is pure (no host state,
no GPU, no subprocess — a purity meta-test proves it), so this is the CI
enforcement that a change scoped to one machine's profile/trait can't
silently alter another machine's rendering: touching thor.env's source
without also updating spark.env or template-defaults.env fails the suite.
tests/goldens/regen.py regenerates all three; tests/goldens/README.md
documents the "diff is the review surface" contract.
…diffed (cross-machine no-breakage guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Extends issue #92's "advertised implies reachable" invariant to the HARDWARE dimension: a role a per-machine profile declares infeasible (RoleProfile.feasible=False) is never advertised ready in GET /capabilities / lobes capabilities, never listed in GET /v1/models, and a POST addressed to it 4xx's (role_infeasible) instead of being silently routed to a different gear. Channel: <PREFIX>_FEASIBLE=false env vars (PRIMARY_FEASIBLE / MULTIMODAL_FEASIBLE / EMBED_FEASIBLE / RERANK_FEASIBLE — see lobes.gateway._config.FEASIBLE_ENV), mirroring the existing <PREFIX>_MAX_MODEL_LEN served-context overlay convention. Computed once in build_config() onto RoutingTable.infeasible; every consumer (infeasible_owner, list_models_payload, lobes.roles.build_role_registry) reads it off the table already being threaded through — no parallel machinery, no new parameter on any existing public builder. RoleInfo gains a `feasible` field, clamping `ready` to False whenever a role's backend is hardware-infeasible, on top of the existing loaded/endpoint clamps — proven even when a live backend_ready signal reports the backend healthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…, gateway 404 role_infeasible) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
… + validation status Task t5 — lobes doctor/status now report: - Detected card (name or UNKNOWN) with device_name, compute_capability, total_memory_gb - Active profile from .env (persisted by init) - Whether profile is validated for the detected card - WARNING when card is UNKNOWN or profile doesn't match detected card Changes: 1. init: persist LOBES_PROFILE=<name> to .env (one-line hook) 2. doctor: add machine_profile section to output (text + --json) 3. status: add profile field to output (fleet + single, text + --json) 4. tests: comprehensive coverage with injected detection facts All 1388 tests pass (baseline 1383 + 5 new). doctor/status remain read-only (no filesystem writes in code path). Smoke test on Thor: correctly detects sm_110 compute capability, 122.8 GB memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…d/unvalidated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…grade (t15) Vendors main's fleet/single docker-compose.yml + env.example (commit 7fa624d) into tests/fixtures/upgrade_compat/ as the "previous version" scaffold fixture, then drives status/serve/stop/init dry-run against a temp deployment dir built from it: every read/dry-run path succeeds, none writes (hashed before/after), and every ${VAR} main's template consumed is still honoured by the current template (the rename tripwire). init --apply without --force over the old scaffold still refuses rather than silently rewriting; --force --apply remains the only, explicit write path. No source changes were needed — t3/t4 stayed additive as claimed.
…rking, env names stable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…refusing (t14)
resolve_init_profile no longer raises on an UNKNOWN card with no --profile.
It now resolves a new built-in 'base' profile (Qwen/Qwen3.5-4B cortex at
32K/util-0.30, senses infeasible, the standard 0.6B embed+rerank gears — no
27B anywhere) and returns a loud warning naming device_name/compute_capability/
total_memory_gb and the assumption made, so 'lobes init' proceeds (dry-run
prints the plan; --apply writes) instead of half-refusing. Recognised cards
(spark/thor, or an explicit --profile) are byte-identical — verified via
tests/goldens/{spark,thor,template-defaults}.env, unchanged by this commit.
Replaces the refuse branch t4 built; the broader "small default on every
card" change stays deferred to issue #107.
…e (no 27B) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…y gear's served name On a base-profile box the primary gear serves a 4B, but the template and env.example pinned the 27B id as the gateway's default model, routing no-model requests to a name nothing serves. Empty means _config.py's existing 'or primary.served_name' fallback wins, which preserves today's behavior on spark/thor byte-for-byte (their primary IS the 27B) and makes the default self-correct under any profile. Deliberate template change: template-defaults.env golden updated in the same commit (the t13 review surface); profile goldens untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
… finding) On the real thinking-mode cortex the 16-token budget was consumed inside the <think> trace and content came back empty — the probe failed a correct model. Mirror lobes route's enable_thinking=false override (#93). Found running the probes against the live Thor fleet; with the fix all three probes pass on the tuned stack (rerank included — see #105/#106 note). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…d, verified against code
Document the full machine-profile surface:
- docs/machine-profiles.md (new): deep reference covering detection flow, profile
resolution, per-knob meanings and provenance (7 knobs × 4 roles), custom profiles,
goldens contract, support table, and Thor's four validated divergences
(kv_cache_dtype=auto, TRITON_ATTN pooling, enforce_eager reranker).
- lobes/explain/catalog.py: add _PROFILES entry with detection/resolution/knobs
overview; register ("profiles") and ("profile") shortcuts in ENTRIES.
- README.md: add support table (Spark load-tested, Thor load-tested with caveats,
base conservative fallback, Orin unvalidated) + link to machine-profiles.md.
- CLAUDE.md: add machine-profiles section with same support table, custom-profile
pointer, and references (detection, knobs, Thor divergences, custom profiles).
Validation: every knob value and env-var mapping verified against lobes/machines/
(thorton.py, _traits.py:SM_110), lobes/profiles/schema.py (ROLES, KNOB_NAMES),
lobes/profiles/render.py (role→prefix, knob→suffix), and lobes/profiles/builtin/
(TOML). All detection/machine/profile tests pass (14+18+29 total). afi cli doctor
. --strict: 26/26 checks pass. Claims traced to code (issue refs #105 #106 #107
#109 on Thor memory/hangs/eager/scale).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…d evidence The t11 draft carried claims the live validation contradicts; per the plan's added validation criterion, every corrected claim now traces to a real observation: - fp8-KV on Thor: the k_scale assert did NOT reproduce on the pinned nightly; fp8 boots with scale-1.0 warnings (accuracy risk, not crash). No probe ran under fp8 — said so instead of claiming one failed. - senses' TRITON_ATTN is the cross-machine Gemma head-size default, not a Thor divergence; the four real divergences are cortex kv auto, embed TRITON, rerank TRITON + eager. - Spark 'correctness probes pass' removed — the probes postdate the GB10 run and #106 is exactly that open question. - senses was NOT confirmed healthy in the clean-boot run — replaced the 'fully operational' caveat with the real one: the concurrent-boot memory race (4/4 failures), the drop_caches + primary-first workaround, and risk r7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
PR Summary by Qodofeat: per-machine hardware profiles — detect the card, fit the box
AI Description
Diagram
High-Level Assessment
Files changed (63)
|
_render_text (doctor.py:266) tripped S3776 at complexity 19 — the loop over checks and the machine-profile section's chain of conditionals were both flattened into one function. Extract each into its own helper, _render_check_lines and _render_machine_profile_lines, and have _render_text just concatenate their output. Purely structural: same lines, same order, same JSON/exit-code contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…erving) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
_apply_machine_registry now builds the Profile with its constructor instead
of dataclasses.replace(), so the declared return type matches what static
analysis infers (S5886). test_duplicate_registration_rejected_without_replace
moves the machines.get("spark") setup outside the pytest.raises block so only
the register() call can raise (S5778).
…ises block Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…S3776) lobes/gateway/server.py:handle_post exceeded Sonar's cognitive-complexity ceiling (17 > 15) after t6's feasibility-gate additions. Extract four behavior-preserving helpers — _feasibility_response (shared 404 role_infeasible check), _resolve_tier (tier-alias branch: feasibility -> busy-shed -> served name), _resolve_plain_model (plain-id branch: unknown-id 404 -> feasibility -> served name), and _try_backends (the single-attempt per-backend loop) — so handle_post reads as a linear sequence of early returns instead of nested branching. Routing decisions, status codes, error bodies, and check ordering (feasibility outranks pressure; unknown-model checked before feasibility in the plain-id branch) are unchanged; the gateway test suite pins all of this and passes unmodified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
… (behavior-preserving) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
- lobes/cli/_commands/assess.py (S3516, S3776): split cmd_assess's three branches (--probes / --preserve-thinking / default correctness) into helper functions, dropping cognitive complexity under the threshold. --probes now exits EXIT_ENV_ERROR when any probed role fails and EXIT_SUCCESS when all pass (mirrors lobes tunnel's status-driven exit code), giving cmd_assess genuinely different return values instead of always returning 0. --json/text payload shape is unchanged. - lobes/assess.py (S5713): drop the redundant json.JSONDecodeError from the embed-probe except tuple — it already derives from ValueError, which is caught in the same tuple. - tests/test_role_probes.py: pin the new exit-code contract (probe pass -> EXIT_SUCCESS, probe fail -> EXIT_ENV_ERROR in both --json and text mode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
… redundant JSONDecodeError lobes assess --probes now exits EXIT_ENV_ERROR (2) when any probed role fails, 0 when all pass — same contract as tunnel's status exit. Payload shapes unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
|
/agentic_review |
Code Review by Qodo
Context used✅ Tickets:
🎫 verify reranker ORDERING on the DGX Spark GB10 — nothing currently checks it, it may be wrong there too 🎫 verify on the GB10 before t3 ships: is the fp8-KV crash checkpoint-driven, and is VLLM_ATTENTION_BACKEND truly dead on the pinned image? 🎫 Advertised per-role endpoint (:8000) 404s while ready=true — clients dialing it degrade instantly (lobes-cli#87 shape regressed)✅ Compliance rules (platform):
27 rules✅ Skills:
doc-test-alignment, cicd 1.
|
…ofile names RoleProfile.from_dict validated key names only, so a TOML typo like feasible = "false" (a truthy string) silently passed construction and the renderer's truthiness check flipped a role feasible; now every knob's TYPE is checked (bool strictly for feasible, int/float-not-bool for numeric knobs, str-or-None for string knobs) and a bad value raises ModelGearError naming the role, knob, expected and got types. discover_operator_profiles() keyed profiles by the raw filename stem, so an operator file like Thor.toml never matched resolve_profile()'s .strip().lower() lookup and silently failed to override the builtin; keys are now normalised the same way, with a clear error on a post-normalisation collision (Thor.toml + thor.toml). Also fixed resolve_init_profile's --profile-vs-detected-card comparison to use normalised forms so a casing-only difference (--profile Spark on a detected "spark" card) no longer triggers a spurious mismatch warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…les (Qodo #2, #3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
…AttributeError on malformed-but-200 probe bodies Finding 1 (Qodo): every --probes request travels through the gateway (RoleInfo.endpoint is always the gateway URL for the four gateway-fronted roles — lobes.roles._gateway_role sets endpoint = gateway). "cortex" is a resolvable gateway alias (lobes.catalog.TIER_ROLE -> tier_aliases), so the cortex probe now sends "cortex" as the model field instead of the concrete served id, exercising the same alias-resolution lane real callers use. embedder/reranker have no such gateway alias (TIER_ROLE only covers the generate lane) — sending those role names literally would 404 as an unknown model, so they keep sending the concrete served name. Finding 2 (Qodo): a 200-but-malformed body (e.g. "message" or a "data"/ "results" item is a bare string instead of a dict) made probe_cortex_correctness / probe_embed_correctness raise AttributeError from a `.get()` call, which their except tuples did not catch — aborting `lobes assess --probes` instead of reporting a structured FAIL. Added AttributeError to all three probes' except tuples (probe_rerank_correctness has no `.get()` call today but gets the same guard defensively, against a future refactor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
… never raise on malformed 200s (Qodo #1, #4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
|
Fixed in c9b72f8 (merged 65af1c3) — with a measured middle path. Evidence: every probe already routes through the GATEWAY (RoleInfo.endpoint is unconditionally the gateway URL in lobes/roles.py::_gateway_role), so for cortex the probe now sends the stable role alias (model=cortex), exercising the exact alias + hardware-infeasibility lane real callers use (#92/#81). For embedder/reranker the concrete served id is kept deliberately: the gateway alias table (tier_aliases() from catalog.TIER_ROLE) has no embedder/reranker entries, so a literal model=embedder would 404 as model_not_found. A _probe_model(role, info) helper encodes the rule and tests pin both sides. Extending pooling-role aliases to the gateway would be a separate feature — happy to file it if you think the alias contract should cover them too.
|
…ma.py S1192: the RoleProfile validator table repeated the "str or None" expected- type description four times; hoist it into a module-level _STR_OR_NONE constant and reuse it. S5799: the type-mismatch error message was built from two adjacent f-string literals on one line (implicit concatenation), which reads like an accidental split rather than an intentional one. It was cosmetic — a single string argument, not a tuple — so merge it into one explicit f-string; no behavior change.
…e f-string Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd
|
…spec/brain-shapes Resolves the every-PR-bump collision on CHANGELOG.md / pyproject.toml / uv.lock by taking main's side (0.41.0); the branch's own 0.40.4 bump is dropped and re-applied on top as 0.41.1. #110 landed the #108 profile substrate this spec+plan composes on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U9ybFURhQxT9UpCgQNVL67
#110 merged mid-flight (the #108 implementation, 0.41.0), so the frame and plan are re-cut against what actually shipped: the dropped-lobe contract is now flag-not-omit (capabilities feasible:false, /v1/models omits, generate lane 404 role_infeasible — c11/h3 rejected, c17/h13 recaptured and covered by t5); shapes own all six Colleague roles per user decision (stt/tts are first-class shape members over the audio overlay — c18, t1); shape data files are TOML matching lobes/profiles/builtin/; t1/t3/t5 instructions now name the landed modules instead of 'when it lands'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U9ybFURhQxT9UpCgQNVL67



Implements the converged per-machine profiles plan (spec/plan: #108; 13 tasks, 4 waves, built via /assign-to-workforce with TDD-gated merges — suite grew 1188 → 1399, zero pre-existing tests broken).
What lands
lobes/machines/— per-chip strategy registry (CardStrategyper chip + sharedSM_110trait). LegacyMachineProfile/MACHINE_PROFILES/detect_machine()derived from it; adding a chip = one file + one registration line (proven by a synthetic-chip test).lobes/profiles/— schema (per-rolefeasible/model+ 7 knobs), TOML built-ins (spark,thor,base), loader with operator override, profile→env renderer. Thor's 4 divergences stay single-sourced in the registry and overlay at load time.lobes/runtime/_detect.py— card detection: device name + compute cap +/proc/meminfototal (never nvidia-smi memory fields —[N/A]on Thor). UNKNOWN is first-class.lobes initapplies the resolved profile;--profileforces with a warning; UNKNOWN warns and serves the conservative base profile (4B + pooling gears, no 27B — default install should assume a SMALL box, not a 128 GB Blackwell — conservative base profile + a weak default model #107's unknown-card slice).doctor/statusreport card + profile.<PREFIX>_FEASIBLE=false→ role omitted from capabilities, gateway 404role_infeasible(extends Advertised per-role endpoint (:8000) 404s while ready=true — clients dialing it degrade instantly (lobes-cli#87 shape regressed) #92).lobes assess --probes): known-answer / paraphrase-ranking / rerank-ordering; timeout = FAIL.tests/goldens/): byte-diffed per profile + template defaults, GPU-less — a Thor-side change cannot silently alter Spark's rendering (and vice versa).--force --applygated.docs/machine-profiles.md,lobes explain profiles, honest support tables.Live validation on the physical Thor (t10)
cudaErrorLaunchFailure) under TRITON_ATTN + eager. Senses unconfirmed (boots last; see below).k_scaleassert did not reproduce on the pinned nightly — uncalibrated fp8-KV is now an accuracy risk (scale-1.0 warnings), not a crash. verify on the GB10 before t3 ships: is the fp8-KV crash checkpoint-driven, and is VLLM_ATTENTION_BACKEND truly dead on the pinned image? #109 updated.drop_cachesafter teardown boots clean. Not expressible as a per-gear knob — recorded as plan risk r7, follow-up work.Deliberately NOT in this PR
MULTIMODAL_ATTENTION_BACKENDenv deletion — gated on the GB10 check (verify on the GB10 before t3 ships: is the fp8-KV crash checkpoint-driven, and is VLLM_ATTENTION_BACKEND truly dead on the pinned image? #109).🤖 Generated with Claude Code
https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd