Skip to content

feat: per-machine hardware profiles — detect the card, fit the box (13-task plan)#110

Merged
OriNachum merged 47 commits into
mainfrom
feat/per-machine-profiles
Jul 13, 2026
Merged

feat: per-machine hardware profiles — detect the card, fit the box (13-task plan)#110
OriNachum merged 47 commits into
mainfrom
feat/per-machine-profiles

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

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 (CardStrategy per chip + shared SM_110 trait). Legacy MachineProfile/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-role feasible/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/meminfo total (never nvidia-smi memory fields — [N/A] on Thor). UNKNOWN is first-class.
  • lobes init applies the resolved profile; --profile forces 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/status report card + profile.
  • Feasibility end-to-end: <PREFIX>_FEASIBLE=false → role omitted from capabilities, gateway 404 role_infeasible (extends Advertised per-role endpoint (:8000) 404s while ready=true — clients dialing it degrade instantly (lobes-cli#87 shape regressed) #92).
  • Correctness probes (lobes assess --probes): known-answer / paraphrase-ranking / rerank-ordering; timeout = FAIL.
  • Golden renders (tests/goldens/): byte-diffed per profile + template defaults, GPU-less — a Thor-side change cannot silently alter Spark's rendering (and vice versa).
  • Upgrade compat: main-scaffolded deployments keep working; env-name tripwire; re-init is diffed + --force --apply gated.
  • Docs: docs/machine-profiles.md, lobes explain profiles, honest support tables.

Live validation on the physical Thor (t10)

  • Thor profile, clean boot: 3/3 correctness probes pass — rerank correct and stable (no cudaErrorLaunchFailure) under TRITON_ATTN + eager. Senses unconfirmed (boots last; see below).
  • Spark profile on Thor: fails first boot reproducibly — the "fits none" cost is real.
  • Finding 1: the fp8 k_scale assert 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.
  • Finding 2: concurrent first boot fails a memory race (profiling window sees co-resident weight loads via page cache) regardless of profile; sequential bring-up (primary first) + drop_caches after teardown boots clean. Not expressible as a per-gear knob — recorded as plan risk r7, follow-up work.

Deliberately NOT in this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_01TJc5yvfweHP2AEccKNeaVd

  • lobes (Claude)

OriNachum and others added 30 commits July 13, 2026 21:18
…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
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
…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
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat: per-machine hardware profiles — detect the card, fit the box

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds lobes/machines/ per-chip strategy registry (spark/thor/blackwell/generic + shared SM_110
 trait); legacy MachineProfile/detect_machine() now derived from it.
• Adds lobes/profiles/ schema + TOML built-ins (spark/thor/base) + loader (operator override) +
 env renderer, resolved by lobes init (--profile forces, UNKNOWN warns and falls back to
 conservative base).
• Wires hardware feasibility end-to-end: _FEASIBLE=false hides a role from capabilities and
 gateway returns 404 role_infeasible.
• Adds lobes assess --probes correctness checks (known-answer/paraphrase/rerank-ordering) and
 byte-diffed golden .env renders per profile for cross-machine safety.
• Validated live on Jetson Thor: 3/3 correctness probes pass; documents fp8 k_scale and
 concurrent-boot memory-race findings as follow-up risk.
Diagram

graph TD
  host["Host hardware facts"] --> detect["_detect.detect_card()"] --> registry["machines registry"]
  registry --> strategy["CardStrategy per chip"] --> loader["profiles.loader.resolve_profile()"]
  loader --> render["profiles.render.profile_env()"] --> envfile[(".env file")]
  envfile --> compose["fleet compose template"]
  envfile --> gwconfig["gateway _config.build_config()"] --> routing["RoutingTable.infeasible"]
  routing --> server["gateway server.handle_post()"]
  cli["lobes init / doctor / status"] --> detect
  cli --> loader
  subgraph Legend
    direction LR
    _svc([Process/Service]) ~~~ _db[(Data store)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Flat MachineProfile config table extended with feasibility/knob fields
  • ➕ Simpler mental model, one file to edit
  • ➕ No new registry/strategy abstraction needed
  • ➖ Forces re-typing shared quirks (e.g. sm_110) per board
  • ➖ Loses provenance (why a knob holds a value)
  • ➖ Harder to keep TOML built-ins and code-level facts in sync without an explicit overlay mechanism
2. Fully code-generated TOML (no hand-authored divergent knobs in TOML at all)
  • ➕ Single source of truth entirely in lobes.machines, TOML becomes pure baseline data
  • ➕ Eliminates the two-places-to-check pattern for Thor's divergences
  • ➖ Loses the readability of seeing a role's full effective config in one TOML file
  • ➖ Requires regenerating/reviewing TOML on every registry change

Recommendation: The layered design (chip strategy registry as single source of truth, derived legacy API, TOML profiles overlaying registry-derived divergences) is the right call given the stated constraints (stdlib-only, no new deps, must not break 1188 existing tests). The main alternative — a flat per-machine config table (the old MachineProfile approach extended with more fields) — was explicitly rejected in favor of the strategy+trait model, correctly, since it would force copy-pasting the sm_110 quirk across every future sm_110 board and would blur causal provenance. The TOML+overlay split (baseline in TOML, validated divergences overlaid from code) is a reasonable compromise but does mean a reviewer must check two places (thor.toml comments + _traits.py) to know Thor's actual effective knobs; a fully generated/derived TOML (or a single Python source of truth with TOML only for operator overrides) would reduce that duplication risk at a small readability cost. Given this is validated with golden-diff tests protecting exactly this seam, the current approach is acceptable and the golden tests substantially mitigate the risk.

Files changed (63) +7675 / -136

Enhancement (21) +1960 / -43
__init__.pyChip strategy package entrypoint and registration order +46/-0

Chip strategy package entrypoint and registration order

• New package that imports spark/thor/blackwell/generic in detection-precedence order and re-exports the registry API and strategy vocabulary.

lobes/machines/init.py

_strategy.pyCardStrategy/Knob/Trait dataclasses — the per-chip knowledge shape +179/-0

CardStrategy/Knob/Trait dataclasses — the per-chip knowledge shape

• Defines Knob (value+provenance), DetectionSignature, MachineDefaults, Trait, and CardStrategy with role_knobs() merging traits and per-board overrides plus a render() JSON view.

lobes/machines/_strategy.py

_registry.pyExplicit in-process strategy registry +69/-0

Explicit in-process strategy registry

• Provides register/unregister/get/names/strategies/detect over an insertion-ordered dict, with detect() returning None (not a silent fallback) when nothing matches.

lobes/machines/_registry.py

_traits.pyShared SM_110 pooling-quirk trait +39/-0

Shared SM_110 pooling-quirk trait

• Defines the SM_110 Trait bundling TRITON_ATTN + enforce_eager knobs for embedder/reranker, reused by any sm_110 board.

lobes/machines/_traits.py

spark.pySpark (GB10) CardStrategy +31/-0

Spark (GB10) CardStrategy

• Registers the load-tested DGX Spark strategy with its detection markers and single-model defaults.

lobes/machines/spark.py

thor.pyThor (sm_110) CardStrategy with live-validated knobs +58/-0

Thor (sm_110) CardStrategy with live-validated knobs

• Registers the Jetson AGX Thor strategy composing the SM_110 trait and a Thor-specific cortex kv_cache_dtype override, replacing the old unvalidated 'configured' estimate.

lobes/machines/thor.py

blackwell.pyDiscrete Blackwell (RTX PRO 6000) CardStrategy +30/-0

Discrete Blackwell (RTX PRO 6000) CardStrategy

• Registers the discrete-GPU strategy, matched by specific markers to avoid false-matching non-Blackwell RTX 6000 parts.

lobes/machines/blackwell.py

generic.pyGeneric conservative fallback CardStrategy +25/-0

Generic conservative fallback CardStrategy

• Registers the never-auto-matched fallback strategy used only when explicitly selected or via the legacy silent fallback.

lobes/machines/generic.py

schema.pyProfile/RoleProfile dataclasses — the fleet profiling schema +182/-0

Profile/RoleProfile dataclasses — the fleet profiling schema

• Defines the four-role, seven-knob Profile schema with strict from_dict validation rejecting unknown roles/knobs.

lobes/profiles/schema.py

loader.pyProfile resolution: built-ins, operator overrides, machine overlay +157/-0

Profile resolution: built-ins, operator overrides, machine overlay

• Loads packaged TOML built-ins and operator TOML files, resolves by name with operator precedence, and overlays Thor's live-measured knobs from the machines registry.

lobes/profiles/loader.py

render.pyProfile → fleet .env renderer +135/-0

Profile → fleet .env renderer

• Maps each RoleProfile's knobs to PREFIX_SUFFIX env vars, handling the model→two-keys case, enforce_eager's CLI-token translation, and the FEASIBLE=false marker for infeasible roles.

lobes/profiles/render.py

_detect.pyHost card detection: nvidia-smi + /proc/meminfo, never GPU memory fields +230/-0

Host card detection: nvidia-smi + /proc/meminfo, never GPU memory fields

• New module gathering device name, compute capability, total memory (from /proc/meminfo, never nvidia-smi memory fields which are N/A on Thor), hostname, and device-tree model, resolving via lobes.machines.detect with UNKNOWN as a first-class result.

lobes/runtime/_detect.py

_runtime_ops.pyresolve_init_profile: detection, --profile override, UNKNOWN fallback +81/-1

resolve_init_profile: detection, --profile override, UNKNOWN fallback

• Adds resolve_init_profile() used by lobes init to always detect the card, honor an explicit --profile with a mismatch warning, and fall back to the conservative 'base' profile with a warning on UNKNOWN.

lobes/cli/_runtime_ops.py

init.pylobes init applies the resolved per-machine profile +147/-26

lobes init applies the resolved per-machine profile

• Adds --profile flag, resolves and renders the profile's env vars into .env (skipping no-op rewrites), persists LOBES_PROFILE, and reports the plan/applied profile in dry-run and apply output (text and JSON).

lobes/cli/_commands/init.py

doctor.pydoctor reports detected card and profile validation status +74/-2

doctor reports detected card and profile validation status

• Adds a machine-profile section reporting detected card facts, the persisted profile, and a warning when the profile doesn't match the detected card or the card is unknown.

lobes/cli/_commands/doctor.py

status.pystatus reports the active LOBES_PROFILE +15/-13

status reports the active LOBES_PROFILE

• Adds the persisted profile name to both fleet and single-model status output (JSON and text).

lobes/cli/_commands/status.py

capabilities.pycapabilities table flags hardware-infeasible roles +8/-0

capabilities table flags hardware-infeasible roles

• Renders an explicit '** infeasible on this machine **' marker for a role reported as feasible=False even if structurally wired.

lobes/cli/_commands/capabilities.py

assess.pylobes assess --probes for per-role correctness checks +52/-1

lobes assess --probes for per-role correctness checks

• Adds --probes/--role/--timeout flags that run cortex/embedder/reranker correctness probes against each role's own resolved endpoint via the role registry.

lobes/cli/_commands/assess.py

assess.pyKnown-answer / paraphrase / rerank-ordering correctness probes +302/-0

Known-answer / paraphrase / rerank-ordering correctness probes

• Adds probe_cortex_correctness, probe_embed_correctness, probe_rerank_correctness plus run/render helpers, each treating a timeout or malformed response as FAIL rather than a skip.

lobes/assess.py

_config.pyCompute per-backend hardware feasibility from *_FEASIBLE env vars +45/-0

Compute per-backend hardware feasibility from *_FEASIBLE env vars

• Adds FEASIBLE_ENV mapping and _is_feasible(), populating RoutingTable.infeasible from <PREFIX>_FEASIBLE=false env vars during build_config.

lobes/gateway/_config.py

_routing.pyRoutingTable.infeasible + infeasible_owner() resolution +55/-0

RoutingTable.infeasible + infeasible_owner() resolution

• Adds an infeasible frozenset field to RoutingTable, infeasible_owner() to resolve a request to an infeasible backend, and filters infeasible backends out of list_models_payload unconditionally.

lobes/gateway/_routing.py

Bug fix (2) +91 / -13
server.pyGateway 404s role_infeasible requests before dialing a backend +58/-0

Gateway 404s role_infeasible requests before dialing a backend

• Checks infeasible_owner() on both the tier-alias and plain-model request paths, returning a distinct role_infeasible 404 body ahead of pressure-shedding and backend resolution.

lobes/gateway/server.py

roles.pyRoleInfo.feasible clamps ready=False for infeasible roles +33/-13

RoleInfo.feasible clamps ready=False for infeasible roles

• Adds a feasible field to RoleInfo derived from table.infeasible, structurally clamping ready to False for an infeasible role regardless of any supplied readiness signal.

lobes/roles.py

Refactor (1) +114 / -73
__init__.pyLegacy machine-profile surface rebuilt from the chip registry +114/-73

Legacy machine-profile surface rebuilt from the chip registry

• MachineProfile/MACHINE_PROFILES/detect_machine/machine_profile are now derived live from lobes.machines via PEP 562 __getattr__, and the module re-exports the new Profile schema/loader API.

lobes/profiles/init.py

Tests (23) +4460 / -1
regen.pyGolden .env regeneration script +132/-0

Golden .env regeneration script

• New script to regenerate the byte-diffed golden env files per profile and template defaults, run manually after a machine/profile code change.

tests/goldens/regen.py

spark.envSpark golden rendered env +21/-0

Spark golden rendered env

• Committed golden output for the spark profile render.

tests/goldens/spark.env

thor.envThor golden rendered env +24/-0

Thor golden rendered env

• Committed golden output for the thor profile render, capturing the four overlay-derived divergences.

tests/goldens/thor.env

base.envBase (unknown-card) golden rendered env +13/-0

Base (unknown-card) golden rendered env

• Committed golden output for the conservative base profile.

tests/goldens/base.env

template-defaults.envTemplate-default golden surface +68/-0

Template-default golden surface

• Committed golden output capturing the compose template's own ${VAR:-default} values independent of any profile.

tests/goldens/template-defaults.env

test_profile_goldens.pyByte-diff tests against the golden env files +204/-0

Byte-diff tests against the golden env files

• Asserts each profile's rendered env and the template defaults match the committed goldens exactly.

tests/test_profile_goldens.py

test_machines.pyChip strategy registry unit tests +227/-0

Chip strategy registry unit tests

• Covers registration, detection precedence, trait composition, and a synthetic-chip test proving one file + one registration line is sufficient.

tests/test_machines.py

test_detect.pyHost card detection unit tests +273/-0

Host card detection unit tests

• Covers nvidia-smi argv construction (never a memory field), /proc/meminfo parsing, device-tree fallback, and UNKNOWN resolution with injected probes.

tests/test_detect.py

test_profile_schema.pyProfile/RoleProfile schema validation tests +315/-0

Profile/RoleProfile schema validation tests

• Covers from_dict/to_dict round-tripping and rejection of unknown roles/knobs.

tests/test_profile_schema.py

test_profile_render.pyProfile-to-env renderer tests +156/-0

Profile-to-env renderer tests

• Covers role prefix mapping, enforce_eager token translation, and the FEASIBLE=false marker.

tests/test_profile_render.py

test_profiles.pyLegacy machine-profile derivation tests +16/-0

Legacy machine-profile derivation tests

• Confirms MACHINE_PROFILES/detect_machine remain correct when derived from the chip registry.

tests/test_profiles.py

test_init_profile.pylobes init profile resolution/apply tests +287/-0

lobes init profile resolution/apply tests

• Covers detection-driven, --profile-forced, and UNKNOWN-fallback init flows, plus .env write no-op behavior.

tests/test_init_profile.py

test_doctor_machine_profile.pydoctor machine-profile section tests +190/-0

doctor machine-profile section tests

• Covers doctor's reporting of detected card, persisted profile, and mismatch/unknown warnings.

tests/test_doctor_machine_profile.py

test_status_fleet.pystatus profile field test update +2/-1

status profile field test update

• Extends fleet status tests to cover the new profile field in output.

tests/test_status_fleet.py

test_gateway_feasibility.pyGateway hardware-feasibility end-to-end tests +459/-0

Gateway hardware-feasibility end-to-end tests

• Covers FEASIBLE env parsing, infeasible_owner resolution, list_models filtering, and the 404 role_infeasible response on both tier and plain-model request paths.

tests/test_gateway_feasibility.py

test_cli_capabilities.pycapabilities infeasible-marker rendering test +31/-0

capabilities infeasible-marker rendering test

• Covers the '** infeasible on this machine **' line rendered for a feasible=False role.

tests/test_cli_capabilities.py

test_role_probes.pyCorrectness probe unit and CLI-wiring tests +455/-0

Correctness probe unit and CLI-wiring tests

• Covers cortex/embed/rerank probe pass/fail logic, timeout-as-fail behavior, and the assess --probes CLI wiring.

tests/test_role_probes.py

test_fleet_per_machine_knobs.pyFleet compose per-machine knob substitution tests +133/-0

Fleet compose per-machine knob substitution tests

• Covers the new env-parameterized kv-cache-dtype/attention-config/enforce-eager knobs in the fleet compose template.

tests/test_fleet_per_machine_knobs.py

test_upgrade_compat.pyUpgrade-compatibility regression tests +259/-0

Upgrade-compatibility regression tests

• Proves a main-scaffolded deployment keeps working unchanged, and that re-init is diffed and gated behind --force --apply.

tests/test_upgrade_compat.py

docker-compose.ymlPre-change fleet compose fixture snapshot +705/-0

Pre-change fleet compose fixture snapshot

• Committed snapshot of the fleet compose template as scaffolded before this PR, used as an upgrade-compat baseline.

tests/fixtures/upgrade_compat/fleet/docker-compose.yml

env.examplePre-change fleet env fixture snapshot +264/-0

Pre-change fleet env fixture snapshot

• Committed snapshot of the fleet env.example as scaffolded before this PR.

tests/fixtures/upgrade_compat/fleet/env.example

docker-compose.ymlPre-change single-model compose fixture snapshot +121/-0

Pre-change single-model compose fixture snapshot

• Committed snapshot of the single-model compose template as scaffolded before this PR.

tests/fixtures/upgrade_compat/single/docker-compose.yml

env.examplePre-change single-model env fixture snapshot +105/-0

Pre-change single-model env fixture snapshot

• Committed snapshot of the single-model env.example as scaffolded before this PR.

tests/fixtures/upgrade_compat/single/env.example

Documentation (8) +820 / -1
__init__.pyPackaged TOML profile data package +15/-0

Packaged TOML profile data package

• Documents why TOML (stdlib tomllib) was chosen over YAML for the built-in profile data.

lobes/profiles/builtin/init.py

catalog.pylobes explain profiles reference text +91/-0

lobes explain profiles reference text

• Adds a new _PROFILES explain topic documenting detection, resolution order, built-in profiles, knobs, Thor's divergences, custom profiles, and the goldens contract.

lobes/explain/catalog.py

machine-profiles.mdNew machine-profiles deep-reference doc +527/-0

New machine-profiles deep-reference doc

• Adds a large new document covering detection, profile resolution, knobs, Thor validation results, custom profiles, and the golden-render contract.

docs/machine-profiles.md

README.mdSupported-hardware table and machine-profiles doc link +15/-1

Supported-hardware table and machine-profiles doc link

• Adds a support table listing Spark/Thor/unknown-card status and validation notes, linking to the new docs.

README.md

CHANGELOG.md0.41.0 changelog entry for per-machine profiles +73/-0

0.41.0 changelog entry for per-machine profiles

• Documents the full feature set, live Thor validation findings, and the GATEWAY_DEFAULT_MODEL behavior change.

CHANGELOG.md

CLAUDE.mdRepo guidance updated for machines/profiles packages +25/-0

Repo guidance updated for machines/profiles packages

• Adds notes on the new lobes/machines and lobes/profiles packages and their conventions.

CLAUDE.md

README.mdGolden-files contract documentation +48/-0

Golden-files contract documentation

• Explains the cross-machine no-breakage invariant enforced by the golden .env diff tests.

tests/goldens/README.md

README.mdUpgrade-compat fixture documentation +26/-0

Upgrade-compat fixture documentation

• Explains the purpose of the committed pre-change fixture snapshots used by the upgrade-compat tests.

tests/fixtures/upgrade_compat/README.md

Other (8) +230 / -5
spark.tomlSpark built-in profile (byte-identical to shipped defaults) +42/-0

Spark built-in profile (byte-identical to shipped defaults)

• Declares the default DGX Spark profile reproducing the fleet template's current defaults exactly.

lobes/profiles/builtin/spark.toml

thor.tomlThor built-in profile with overlay-derived divergences +51/-0

Thor built-in profile with overlay-derived divergences

• Declares the Thor profile baseline identical to Spark; the four validated divergent knobs are intentionally absent and filled in at load time from the machines registry.

lobes/profiles/builtin/thor.toml

base.tomlConservative fallback profile for unknown cards +52/-0

Conservative fallback profile for unknown cards

• Declares the 'base' profile serving a small 4B cortex model plus 0.6B pooling gears, with senses marked infeasible, used when detection returns UNKNOWN.

lobes/profiles/builtin/base.toml

pyproject.tomlVersion bump to 0.41.0 +1/-1

Version bump to 0.41.0

• Bumps the package version to accompany this release.

pyproject.toml

docker-compose.ymlFleet compose knobs parameterized per profile +38/-2

Fleet compose knobs parameterized per profile

• Replaces hardcoded kv-cache-dtype, attention-config, and enforce-eager flags with env-var substitutions so a rendered profile can override them per machine, and defaults GATEWAY_DEFAULT_MODEL to empty.

lobes/templates/fleet/docker-compose.yml

env.exampleNew fleet env knobs for kv-cache-dtype/attention-backend/enforce-eager +36/-1

New fleet env knobs for kv-cache-dtype/attention-backend/enforce-eager

• Documents and defaults the new PRIMARY_KV_CACHE_DTYPE, EMBED/RERANK_ATTENTION_BACKEND, RERANK_ENFORCE_EAGER, and empty GATEWAY_DEFAULT_MODEL knobs.

lobes/templates/fleet/env.example

docker-compose.ymlSingle-model template kv-cache-dtype made overridable +1/-1

Single-model template kv-cache-dtype made overridable

• Parameterizes the single-model VLLM_KV_CACHE_DTYPE knob the same way as the fleet template.

lobes/templates/docker-compose.yml

env.exampleSingle-model env adds VLLM_KV_CACHE_DTYPE default +9/-0

Single-model env adds VLLM_KV_CACHE_DTYPE default

• Documents the new kv-cache-dtype override knob for the single-model template.

lobes/templates/env.example

OriNachum and others added 8 commits July 13, 2026 23:38
_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
_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
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Jul 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 27 rules

Grey Divider


Action required

1. Profile types unvalidated ✓ Resolved 🐞 Bug ≡ Correctness
Description
RoleProfile.from_dict() accepts arbitrary value types, so TOML mistakes like feasible="false" or
enforce_eager="false" are treated as truthy and rendered incorrectly (infeasible roles may become
feasible; enforce_eager may flip to --enforce-eager). This can silently change which roles are
exposed/blocked and which vLLM flags get emitted from the same profile file.
Code

lobes/profiles/schema.py[R88-102]

+    def from_dict(role: str, data: Mapping[str, Any]) -> "RoleProfile":
+        """Build one role's declaration, rejecting any unrecognised key.
+
+        An unknown knob name is a LOAD ERROR, never a silently dropped key —
+        a typo'd knob in an operator-authored profile must fail loudly rather
+        than pretend the operator's intended override was applied.
+        """
+        known = {f.name for f in fields(RoleProfile)}
+        unknown = set(data.keys()) - known
+        if unknown:
+            raise _profile_error(
+                message=f"unknown knob(s) {sorted(unknown)!r} for role {role!r}",
+                remediation=f"known knobs: feasible, model, {', '.join(KNOB_NAMES)}",
+            )
+        return RoleProfile(**dict(data))
Relevance

⭐⭐⭐ High

Team often accepts input type validation/coercion to prevent silent misbehavior (e.g., guarding
ValueError during parsing) (PR #66).

PR-#66

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The schema loader constructs RoleProfile without type checks, while the renderer relies on boolean
semantics (not rp.feasible and bool(value)), so a string value like "false" will be treated as
True and produce incorrect env output.

lobes/profiles/schema.py[56-103]
lobes/profiles/render.py[93-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RoleProfile.from_dict()` currently validates only *keys*, then constructs `RoleProfile(**data)` with no type checking. Because the renderer uses truthiness/`bool(value)` for `feasible` and `enforce_eager`, a mis-typed TOML value (commonly a quoted boolean like `"false"`) is silently accepted and misinterpreted.

## Issue Context
- TOML will parse quoted booleans as strings.
- `render.profile_env()` treats `not rp.feasible` and `bool(value)` as semantic booleans.

## Fix Focus Areas
- lobes/profiles/schema.py[87-103]
- lobes/profiles/render.py[93-115]

## Implementation notes
- In `RoleProfile.from_dict`, validate each provided field:
 - `feasible`: must be `bool`
 - `enforce_eager`: must be `bool` or `None`
 - `gpu_mem_util`: `int|float`
 - `max_model_len`, `max_num_seqs`: `int`
 - string knobs: `str`
- On mismatch, raise `ModelGearError(EXIT_USER_ERROR)` with remediation pointing to the exact field and expected type.
- Add tests that confirm quoted booleans (string) are rejected (not silently coerced).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. assess --probes uses served model IDs ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
The new lobes assess --probes path passes RoleInfo.model (a concrete served_name) as the
OpenAI model field, instead of using the approved role/capability aliases (e.g., cortex). This
violates the alias-only contract and can couple probes to deployment-specific model IDs rather than
stable role names.
Code

lobes/cli/_commands/assess.py[R50-53]

+    endpoints = {
+        role: (info.endpoint, info.model) if info.loaded and info.endpoint else None
+        for role, info in registry.items()
+    }
Relevance

⭐⭐⭐ High

Repo explicitly moved to role-based discovery to avoid hardcoded model IDs; contract stresses no
model-id coupling (PR #82).

PR-#82

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 1513047 requires generate-lane callers to use role/capability aliases instead of
concrete model IDs. The new --probes code paths build the probe target as `(info.endpoint,
info.model), and RoleInfo.model is set from backend.served_name` (a concrete model id); the
cortex probe then forwards it as the request model field.

Rule 1513047: Use capability-tier or role aliases instead of hardcoded model IDs
lobes/cli/_commands/assess.py[46-53]
lobes/roles.py[318-339]
lobes/assess.py[631-656]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`lobes assess --probes` currently builds probe requests using `info.model` (a concrete served model identifier) for the `model` field. Compliance requires callers to use standardized capability-tier or role aliases (e.g., `cortex`, `main`, `minor`, `multimodal`) instead of concrete IDs.

## Issue Context
- `_cmd_assess_probes()` constructs `endpoints` using `(info.endpoint, info.model)`.
- For gateway-fronted roles, `RoleInfo.model` is populated from `backend.served_name`, i.e., a concrete model ID.
- The cortex probe then sends that value as the OpenAI `model` parameter.

## Fix Focus Areas
- lobes/cli/_commands/assess.py[46-53]
- lobes/assess.py[631-656]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Probes can raise AttributeError ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new per-role correctness probes call .get() on nested fields that may not be dicts for
malformed-but-200 JSON responses, but their exception handling does not include AttributeError. This
can abort lobes assess --probes instead of returning a structured FAIL result.
Code

lobes/assess.py[R660-745]

+        content = (d["choices"][0]["message"].get("content") or "").strip().lower()
+        ok = _CORTEX_PROBE_EXPECT in content
+        return _probe_result(
+            "cortex",
+            PROBE_NAMES["cortex"],
+            ok,
+            {"expected": _CORTEX_PROBE_EXPECT, "content": content},
+            latency_ms,
+        )
+    except OSError as exc:
+        return _probe_result(
+            "cortex",
+            PROBE_NAMES["cortex"],
+            False,
+            {},
+            (time.monotonic() - t0) * 1000,
+            error=f"transport failed (timeout or connection error): {exc}",
+        )
+    except (KeyError, IndexError, TypeError, json.JSONDecodeError) as exc:
+        return _probe_result(
+            "cortex",
+            PROBE_NAMES["cortex"],
+            False,
+            {},
+            (time.monotonic() - t0) * 1000,
+            error=f"unexpected response shape ({exc.__class__.__name__}: {exc})",
+        )
+
+
+def probe_embed_correctness(
+    url: str, model: str, *, timeout: float = DEFAULT_PROBE_TIMEOUT
+) -> dict:
+    """Correctness probe for the embed/embedder role.
+
+    Embeds a sentence, its paraphrase, and an unrelated sentence in one batch;
+    PASS iff ``cos(sentence, paraphrase) > cos(sentence, unrelated)``. A hard
+    ``timeout`` applies to the whole request — a hang (e.g. the sm_110
+    FLASH_ATTN hang: request accepted, never answered) FAILS the probe, it is
+    never skipped.
+    """
+    url = url.rstrip("/")
+    t0 = time.monotonic()
+    try:
+        d = _post(
+            url,
+            {
+                "model": model,
+                "input": [_EMBED_PROBE_SENTENCE, _EMBED_PROBE_PARAPHRASE, _EMBED_PROBE_UNRELATED],
+            },
+            timeout=timeout,
+            path="/v1/embeddings",
+        )
+        latency_ms = (time.monotonic() - t0) * 1000
+        items = sorted(d["data"], key=lambda item: item.get("index", 0))
+        sentence, paraphrase, unrelated = (item["embedding"] for item in items[:3])
+        sim_paraphrase = _cosine_similarity(sentence, paraphrase)
+        sim_unrelated = _cosine_similarity(sentence, unrelated)
+        ok = sim_paraphrase > sim_unrelated
+        return _probe_result(
+            "embedder",
+            PROBE_NAMES["embedder"],
+            ok,
+            {
+                "sim_paraphrase": round(sim_paraphrase, 4),
+                "sim_unrelated": round(sim_unrelated, 4),
+            },
+            latency_ms,
+        )
+    except OSError as exc:
+        return _probe_result(
+            "embedder",
+            PROBE_NAMES["embedder"],
+            False,
+            {},
+            (time.monotonic() - t0) * 1000,
+            error=f"transport failed (timeout or connection error): {exc}",
+        )
+    except (KeyError, IndexError, TypeError, ValueError) as exc:
+        return _probe_result(
+            "embedder",
+            PROBE_NAMES["embedder"],
+            False,
+            {},
+            (time.monotonic() - t0) * 1000,
+            error=f"unexpected response shape ({exc.__class__.__name__}: {exc})",
+        )
Relevance

⭐⭐⭐ High

Team previously accepted defensive OpenAI-response parsing to avoid assess aborts; structured FAIL
preferred (PR #12).

PR-#12

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The probe code uses .get() on objects obtained via nested indexing, but the exception blocks only
catch KeyError/IndexError/TypeError/ValueError, not AttributeError; a non-mapping message or
non-mapping embedding item would therefore raise and abort the run.

lobes/assess.py[631-745]
PR-#12

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The per-role probe implementations assume dict-shaped nested objects:
- `d["choices"][0]["message"].get(...)`
- `item.get("index", 0)` for embeddings items
If the response is 200 but has an unexpected shape, `.get` will raise `AttributeError`, which is not caught, aborting the command.

## Issue Context
This is the same failure mode previously fixed for tool-probe parsing: assessment should degrade to a FAIL row, not crash.

## Fix Focus Areas
- lobes/assess.py[631-745]

## Implementation notes
- Prefer explicit `isinstance` checks for `dict`/`list` at each level before indexing.
- Alternatively (less ideal), include `AttributeError` in the handled exception tuple.
- Ensure each probe returns `_probe_result(..., ok=False, error=...)` on any shape mismatch.
- Add tests with malformed-but-JSON 200 payloads (e.g., `message` as a string; embedding items as strings) asserting the probe returns ok=False and does not raise.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Operator profile case mismatch ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolve_profile() lowercases the requested name, but discover_operator_profiles() stores operator
profiles keyed by the raw filename stem, so a mixed-case profile filename (e.g. Thor.toml) will
never be found and will fail to override the builtin. The same inconsistency makes
resolve_init_profile() warn on casing-only differences (e.g. --profile Spark vs detected "spark")
even though resolution is effectively case-insensitive.
Code

lobes/profiles/loader.py[R129-161]

+    for path in sorted(operator_dir.glob(f"*{PROFILE_SUFFIX}")):
+        name = path.stem
+        data = _parse(path.read_text(encoding="utf-8"), source=str(path))
+        found[name] = Profile.from_dict(name, data)
+    return found
+
+
+def available_profiles(deploy_dir: Path | str | None = None) -> dict[str, Profile]:
+    """Every resolvable profile: built-ins, then operator profiles override by name."""
+    merged: dict[str, Profile] = {name: load_builtin(name) for name in builtin_names()}
+    if deploy_dir is not None:
+        merged.update(discover_operator_profiles(deploy_dir))
+    return merged
+
+
+def resolve_profile(name: str, deploy_dir: Path | str | None = None) -> Profile:
+    """Resolve ``name`` to a :class:`Profile` — operator profiles win over built-ins.
+
+    Raises :class:`ModelGearError` (``EXIT_USER_ERROR``) for an unknown name;
+    never falls back to a default profile silently.
+    """
+    key = (name or "").strip().lower()
+    if deploy_dir is not None:
+        operator = discover_operator_profiles(deploy_dir)
+        if key in operator:
+            return operator[key]
+    builtin = load_builtin(key)
+    if builtin is not None:
+        return builtin
+    known = ", ".join(builtin_names())
+    raise ModelGearError(
+        code=EXIT_USER_ERROR,
+        message=f"unknown profile {name!r}",
Relevance

⭐⭐⭐ High

Team has accepted fixes for case-sensitivity pitfalls (e.g., lowercasing suffix checks) indicating
preference for case-insensitive UX (PR #54).

PR-#54

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Operator profiles are inserted into the override dict under path.stem but later looked up via a
lowercased key; additionally, init compares the raw --profile string to the detected lowercased
card name, so casing-only differences produce warnings.

lobes/profiles/loader.py[119-155]
lobes/cli/_runtime_ops.py[132-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Profile name normalization is inconsistent:
- `resolve_profile()` normalizes lookup keys with `.strip().lower()`.
- `discover_operator_profiles()` stores discovered profiles under `path.stem` without normalization.
This makes operator overrides depend on filename casing and can also generate false mismatch warnings in init when the user passes a different case.

## Issue Context
The codebase already treats profile names as effectively case-insensitive (lowercasing in `resolve_profile`). Operator overrides should follow the same rule.

## Fix Focus Areas
- lobes/profiles/loader.py[119-163]
- lobes/cli/_runtime_ops.py[132-163]

## Implementation notes
- Introduce a single helper (e.g. `_normalize_profile_name(name: str) -> str`) and use it everywhere.
- In `discover_operator_profiles`, store entries keyed by the normalized stem (and consider rejecting duplicate stems that collide after normalization).
- In `resolve_init_profile`, normalize `explicit_profile` before comparing to `card.resolved` and before passing to `resolve_profile`, so casing-only differences don’t trigger unvalidated-profile warnings.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread lobes/cli/_commands/assess.py
Comment thread lobes/profiles/schema.py
Comment thread lobes/profiles/loader.py
Comment thread lobes/assess.py
OriNachum and others added 4 commits July 14, 2026 00:04
…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
@OriNachum

Copy link
Copy Markdown
Contributor Author

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.

  • lobes (Claude)

OriNachum and others added 2 commits July 14, 2026 00:43
…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
@sonarqubecloud

Copy link
Copy Markdown

@OriNachum OriNachum merged commit 6e47b61 into main Jul 13, 2026
9 checks passed
@OriNachum OriNachum deleted the feat/per-machine-profiles branch July 13, 2026 22:09
OriNachum added a commit that referenced this pull request Jul 14, 2026
…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
OriNachum added a commit that referenced this pull request Jul 14, 2026
#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant