The eight first-class, Colleague-facing roles lobes exposes over the fleet —
cortex/senses/muse/worker/embedder/reranker/stt/tts— how a caller discovers them, drives them, measures them, and the before→after context migration that shipped alongside this contract (issue #81;musejoined as the seventh, opt-in-hosted role, andworkeras the eighth, thor-worker-lobe plan).museis currently DORMANT/unhosted mesh-wide — see the callout below the role table.
This doc is the role contract reference. For the fleet's Docker topology,
tuning knobs, and memory budget, see docs/gateway-fleet.md;
for the raw OpenAI wire endpoints (/v1/chat/completions, /v1/embeddings, …)
see docs/openai-api.md.
Before issue #81, a Colleague client that wanted the vision-capable gear had to
know its literal served name (coolthor/gemma-4-12B-it-NVFP4A16) and hardcode
it. That breaks the moment an operator swaps the checkpoint. The role
vocabulary fixes this: a client asks for a capability (cortex — reasoning
and decisions; senses — perception and intake) and lobes resolves it to
whichever concrete model, endpoint, and context that capability currently
serves. Renaming or re-quantizing the underlying checkpoint is then an
operator-side change with zero client-code change — see "Client flow"
below.
| Role | Backend / service | Endpoint path | What it's for |
|---|---|---|---|
cortex |
primary (generate) |
POST /v1/chat/completions |
Reasoning, deciding, planning, tool use, repo actions — the final authority. |
senses |
multimodal (generate) |
POST /v1/chat/completions |
Intake/perception (text+image) and speaking back to the user. Does not decide or act. |
muse |
muse (generate, opt-in hosting, currently DORMANT/unhosted) |
POST /v1/chat/completions |
Creative generation, long-form writing, ideation, a divergent second opinion. Proposes; never decides or acts. |
worker |
worker (generate, opt-in hosting) |
POST /v1/chat/completions |
Fast ground-work execution — bulk transforms, drafting, image/video understanding — and repo actions, under cortex's direction. Never the final decision or a security call. |
embedder |
embed (pooling) |
POST /v1/embeddings |
Dense text embeddings for memory/retrieval. |
reranker |
rerank (pooling) |
POST /v1/rerank (+ /v1/score) |
Reordering/scoring retrieved candidates. |
stt |
Parakeet (audio overlay, opt-in) | POST /v1/audio/transcriptions |
Speech-to-text. |
tts |
Chatterbox (audio overlay, opt-in) | POST /v1/audio/speech |
Text-to-speech. |
cortex is the final decision authority, not the only lobe that acts.
Since worker joined as the eighth role, two lobes may act on the repo:
cortex (unrestricted — code_repo_actions plus final_authority) and
worker (repo_action allowed, but forbidden final_decision and
security_decision — it executes ground work under cortex's direction, it
never decides on its own authority). Every other role — senses, muse,
embedder, reranker, stt, tts — still carries no acting authority at
all. See "Responsibilities and forbidden responsibilities" below for the
full division of labour.
sensesis vision-only intake — audio is not currently served (issue #101). Thecoolthor/gemma-4-12B-it-NVFP4A16checkpoint behindsensesdeclares anaudio_configin its own model config, but on this vLLM serving path (gemma4_unified) aninput_audiocontent part is silently dropped rather than rejected: a caller gets200 OKand a fluent answer that ignored the audio. Live evidence and the tracking issue are indocs/gemma-4-12b-nvfp4.md. For speech, use the purpose-builtsttrole (Parakeet,POST /v1/audio/transcriptions) instead — it remains first-class and is unaffected by this gap.
cortex, senses, muse, worker, embedder, and reranker are always
enumerated (present with loaded=false if their gear isn't wired in this
deployment — muse and worker additionally report feasible=false unless
their respective hosting shape declares them, see the note below); stt/tts
require lobes init --fleet --audio. brain is not a valid role name —
cortex is the only reasoning/decision-authority role (worker may act,
per the callout above, but it never decides).
museandworkerare OPT-IN CORE ROLES — machine-as-brain never hosts either. Thenvidia/Gemma-4-31B-IT-NVFP4checkpoint behindmuseand theunsloth/Qwen3.6-35B-A3B-NVFP4checkpoint behindworkerare both too heavy to co-reside with the defaultcortex+sensesduo on a 128 GB box, so the default shape's hosted set stays the SIX default roles (DEFAULT_HOSTED_ROLESinlobes/profiles/shapes.py) while the contract set capabilities reports (COLLEAGUE_ROLES) is eight. Only an explicit hosting shape serves either:lobes init --shape thor-museformuse— DECLARED/UNVALIDATED as of this writing, and now additionally DORMANT (see below) — a 2026-07-17 live boot measured the budget, but the acceptance run/transcript never landed, #108 (seedocs/gemma-4-31b-nvfp4.md); athor-workershape forworkeris forthcoming (thor-worker-lobe plan task t7) with no budget numbers committed yet — the role/tier/gateway-config plumbing is shipped, the shape and its live-measured budget are not (seedocs/qwen3.6-35b-a3b-nvfp4.md). On every non-hosting deployment both are honestlyfeasible: false(and, uniquely among the six other roles, an unwiredmuse/workerdefaults to infeasible even on a stale pre-muse/pre-worker.env— seedocs/gateway-fleet.md), somodel=muse/model=worker404srole_infeasible— referable and proxyable like every core role — rather than silently falling back tocortex.
museis currently DORMANT/unhosted mesh-wide. The physical Jetson AGX Thor — the one box that ran (unvalidated)thor-muse— moved to hostingworkerinstead: an operator decision (thor-worker-lobe plan) that no box in the mesh currently hosts the 31Bmuse, and the Thor's deployment declares noMUSE_PEER_ORIGIN, somodel=muse404srole_infeasiblewith nohosted_byreferral anywhere. Themuserole, its catalog entry, and thethor-museshape all stay in-tree (cite-don't-delete) — dormant, not deleted — so this contract still enumeratesmusewithloaded=false,feasible=false, and nohosted_by, exactly like any other unhosted opt-in core role with no declared peer.
Each role carries a declared division of labour — what it is expected to own,
and (for senses, muse, and worker) what it must not do. These are
runtime-descriptor tokens, not correctness claims — lobes does not grade
whether a role did its job well; that judgment is Colleague's (see
"Runtime-only, always" below).
| Role | responsibilities |
forbidden_responsibilities |
|---|---|---|
cortex |
reasoning, deciding, planning, tool_use, code_repo_actions, validation, final_authority |
(none — cortex is the final authority) |
senses |
intake, normalize_input, classify_intent, prepare_context_packet, speak_back |
final_decision, repo_action, security_decision |
muse |
creative_generation, long_form_writing, ideation, style_variation, divergent_second_opinion, tool_use |
final_decision, repo_action, security_decision — muse proposes, cortex decides |
worker |
execution, ground_work, bulk_transform, drafting, image_understanding, video_understanding, tool_use, repo_action |
final_decision, security_decision — worker acts under cortex's direction, never on its own authority |
embedder |
vectorization, memory_retrieval_input |
(none) |
reranker |
retrieval_ordering, relevance_refinement |
(none) |
stt |
transcribe, audio_input_to_text (+ realtime_vad_session when the audio overlay is wired and feasible — see below) |
(none) |
tts |
speech_output, synthesize |
(none) |
worker is the first role besides cortex permitted repo_action.
Every other non-cortex role (senses, muse, embedder, reranker,
stt, tts) forbids it or has no acting authority to begin with. worker's
repo_action is deliberately narrower than cortex's: cortex also carries
final_authority and no forbidden list at all, while worker's forbidden
list still bars final_decision and security_decision — worker executes
ground work (bulk transforms, drafting, image/video-informed edits) UNDER
cortex's direction, it never decides on its own authority. It is also
MULTIMODAL (the checkpoint's ViT gives it image+video intake, no audio),
hence image_understanding/video_understanding — a "seeing doer," unlike
text-only cortex.
stt's realtime_vad_session responsibility is additive and
honesty-gated (issue #149). It names the /v1/realtime WebSocket
server-side-VAD session surface — one connection streams PCM in and
receives speech-start/speech-stop boundary events plus committed-turn
transcriptions, replacing client-side energy-threshold endpointing. Unlike
the rest of this table, it is not a static entry in
ROLE_RESPONSIBILITIES — lobes.roles._resolve_audio_role appends it to
stt's responsibilities tuple only when the audio overlay is actually
wired on this deployment (AUDIO_URL configured, i.e. lobes init --fleet --audio) and the lane hasn't been declared off (STT_FEASIBLE=false).
A text-only fleet (no audio overlay) or a declared-off stt lane reports the
base two-token tuple only, on both lobes capabilities and GET /capabilities — the same loaded/feasible honesty discipline every other
field in this contract already follows, not a new signal. See
tests/test_cli_capabilities.py and tests/test_colleague_contract.py for
the positive (audio-enabled) and negative (text-only) proof.
Three roles carry tool_use: cortex, muse, and worker. They are not
equivalent, and the forbidden_responsibilities column is what separates
them — cortex may act on a tool result with full authority (repo_action
via code_repo_actions, plus final_authority); worker may also act
(repo_action is present, not forbidden) but never decides
(final_decision/security_decision stay forbidden) — it executes under
cortex's direction; muse may only research with a tool call
(repo_action is forbidden outright). muse calling read_file to ground a
proposal is in-contract; muse calling anything that writes is not. worker
calling a write tool to execute ground work IS in-contract, but worker
choosing to merge, deploy, or otherwise make the final call is not — that
authority stays cortex's alone. senses has no tool_use at all: it is
intake/perception, even though its Gemma lane can serve tool calls (see tools,
below — a capability of the lane, not a licence for the role).
cortex and senses are new names layered on the existing primary /
multimodal backends and the main / multimodal capability tiers — no
internal service, container, or env var was renamed. All three vocabularies
resolve to the same backend (lobes/catalog.py's TIER_ROLE):
Backend (role_hint) |
Primary tier alias | Back-compat tier alias | Colleague role name |
|---|---|---|---|
primary |
main |
hard |
cortex |
multimodal |
multimodal |
normal |
senses |
worker |
worker |
(none — new with the role) | worker — like muse, its name IS the backend/tier name; capability order is minor < multimodal < worker < muse < primary |
muse |
muse |
(none — new with the role) | muse — the first role whose name IS the backend/tier name; capability order is minor < multimodal < worker < muse < primary |
minor |
minor |
cheap |
(no role name — minor has no Colleague role; it's the servable floor under pressure, not a first-class capability) |
A caller can send model=cortex, model=main, or model=hard to
/v1/chat/completions and reach the exact same warm backend. All the old
aliases keep working — this is additive vocabulary, not a rename. See
docs/gateway-fleet.md for the
full tier-alias fallback contract (busy backpressure under pressure,
multimodal falling back to main when unwired, etc.) — that mechanism is
unchanged by the role layer.
A client that wants to drive any role needs exactly one thing: the fleet's base URL. Everything else — which model backs a role, whether it's loaded, what context it's served at — comes from the contract itself.
lobes capabilities # human-readable table, all eight roles
lobes capabilities --json # the machine-readable contract
lobes endpoint cortex # just the base URL for one role
curl -s http://localhost:8000/capabilities # the same contract, over HTTPlobes capabilities and GET /capabilities are the same payload — both
are built by the one canonical registry builder,
lobes.roles.build_role_registry (lobes/roles.py), so there is exactly one
source of truth for the role→endpoint contract. The CLI reads the deployment's
.env off disk (soft-resolved — an unscaffolded deployment still answers,
with every role but cortex reported loaded=false); the gateway reads its
own container environment.
GET /capabilities (and lobes capabilities --json) returns an object keyed
by role name, each value carrying exactly these fields:
{
"<role>": {
"role": str, # "cortex" | "senses" | "muse" | "worker" | "embedder" | "reranker" | "stt" | "tts"
"model": str, # the served model id this role resolves to (never blank)
"runtime": str, # "vllm" | "parakeet" | "chatterbox"
"endpoint": str, # client-reachable base URL to dial ("" when not wired)
"path": str, # the OpenAI path, e.g. "/v1/chat/completions"
"context": int, # SERVED context in tokens (deployment override, else catalog native)
"quant": str, # vLLM quantization; "" for pooling/audio roles
"mtp": bool, # speculative decoding (MTP draft head) active
"tools": bool, # does this endpoint accept OpenAI `tools`? see the note below
"responsibilities": [str, ...],
"forbidden_responsibilities": [str, ...],
"ready": bool | null, # see the note below
"loaded": bool, # is this role's backend wired in THIS deployment? (LOCAL wiring only — see below)
"feasible": bool, # can THIS MACHINE serve this role at all? (deployment-shapes)
"hosted_by": str, # OPTIONAL — present only when feasible=false and a peer origin is declared
"proxied": bool # OPTIONAL — present (and true) only when this box also forwards to that peer
},
...
}
feasible is always present (a hardware/deployment fact — see
docs/deployment-shapes.md); hosted_by and
proxied are optional keys, added only for a role this box does not
host, and only when the operator declared a peer for it — see
A third role state: proxied below for the full
three-state contract.
tools answers "can I put an OpenAI tools array on a request to this
role?" — true for the four generate lobes (cortex/senses/muse/worker), false
for embedder/reranker (pooling lanes, no chat endpoint) and stt/tts. It
is a fact about the MODEL the role resolves to, derived from the catalog's
tool_parser — the same field the served --tool-call-parser flag is built
from — so it reports true for a role this box does not host, exactly like
model/context/quant/mtp do; feasible/ready are what tell you whether
you can reach it. It is deliberately a bool rather than the parser's name: the
served parser can legitimately diverge from the catalog's (the primary lane
defaults to the qwen3_coder_thinking plugin over the catalog's base
qwen3_coder, and PRIMARY_TOOL_CALL_PARSER can override it), so naming one
here would be a claim lobes.roles cannot honestly make — while whether tools
are accepted does not vary under that divergence. Like every field in this
contract it is runtime-only: it says the endpoint accepts tools, never that a
given call will be correct or succeed.
Note tools and tool_use are different questions, and a role can have one
without the other. tools is a CAPABILITY of the lane; tool_use is a
RESPONSIBILITY of the role. senses has tools: true and no tool_use: its
Gemma lane can serve tool calls, but the division of labour doesn't ask it to.
Every role's endpoint is the one client-reachable gateway origin — dial
it directly (issue #87). All eight roles (cortex/senses/muse/worker/
embedder/reranker and stt/tts) report the same base URL because routing happens via the
model field / the OpenAI path, not distinct per-role URLs; the internal
upstream hosts (vllm-primary:8000, realtime:8080) are never leaked. When you
fetch GET /capabilities, the gateway advertises the origin you actually
dialed (from the request Host header), so endpoint is reachable as-is; set
GATEWAY_PUBLIC_URL to override it for a tunnel / Host-rewriting reverse proxy.
A role is "" only when it isn't wired (e.g. stt/tts without --audio).
ready differs by transport, deliberately. lobes capabilities --json
reports a configured signal (ready == loaded) — a read-only CLI on the
host can't reach the internal backends to probe them, so it doesn't try; use
lobes measure for a CLI-side live probe. GET /capabilities is the honest
one for consumers: for stt/tts it now reports a live readiness probe of
the audio backend (issue #89) — ready: true only when an audio round-trip
would actually succeed (Chatterbox + Parakeet both up, no poisoned CUDA
context), false while they warm — so an advertised-ready audio role is truly
consumable. The gateway-fronted roles (cortex/senses/muse/worker/
embedder/reranker) still report ready as a same-cost-as-loaded boolean
unless the role is proxied (below), in which case ready reflects a live
probe of the peer, not a local boolean. No ready value is a task-quality
claim.
Example (cortex, fully wired, default fleet):
{
"role": "cortex",
"model": "sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP",
"runtime": "vllm",
"endpoint": "http://localhost:8000",
"path": "/v1/chat/completions",
"context": 131072,
"quant": "modelopt",
"mtp": true,
"tools": true,
"responsibilities": ["reasoning", "deciding", "planning", "tool_use", "code_repo_actions", "validation", "final_authority"],
"forbidden_responsibilities": [],
"ready": true,
"loaded": true
}An unwired role (e.g. stt/tts without --audio, or senses before the
multimodal gear is up) is never omitted — it's returned with
loaded: false and the model it would serve named from the catalog, so a
client can always render all eight roles. (An unwired muse or worker
additionally defaults to feasible: false — the opt-in-hosting honesty rule
above.)
A role's feasible: false (this box's deployment shape dropped it — see
docs/deployment-shapes.md) has always meant one of
two things a client can tell apart by key presence alone:
- referral-only —
hosted_by: "<peer origin>"is present,proxiedis absent (neverfalse— a key that doesn't apply is omitted, not set to a falsy sentinel). The caller must dial the peer origin itself; this box answers the role with404 role_infeasible. - proxied —
hosted_byandproxied: trueare both present. This box has opted in to following its own referral (issues #115/#127): a request for the role is forwarded to the peer named inhosted_by, and the answer comes back through this box's ownendpoint— the caller never has to learn the peer exists or change its request.
{
"role": "senses",
"model": "coolthor/gemma-4-12B-it-NVFP4A16",
"endpoint": "http://localhost:8000",
"feasible": false,
"hosted_by": "http://thor.example.ts.net:8000",
"proxied": true,
"ready": true,
"loaded": false
}loaded is a LOCAL wiring fact, and it does not tell you whether a proxied
role works. It answers "does a backend for this role exist in this box's
routing table" — never "is this role usable". For a proxied role the usable
signal is ready (which for a proxied role is the peer probe's verdict:
the peer answered and its own /v1/models lists exactly this id). Two roles
in the same proxied state, both forwarding happily, can therefore report
different loaded values:
loaded: falseis the common case — a dropped role realistically has no<PREFIX>_BASE_URL, so no localBackendis built (the example above).loaded: truealso occurs, and is not a bug in the payload: any<PREFIX>_BASE_URLstill present wires a localBackendeven when no such container runs here. Notemultimodalis the one optional generate backend whose fleet-compose default is non-empty (MULTIMODAL_BASE_URL=${MULTIMODAL_BASE_URL:-http://vllm-multimodal:8000}, whereMINOR_BASE_URLandMUSE_BASE_URLboth default to empty), and because${VAR:-default}ignores an explicit empty value too, a proxiedsensesreportsloaded: trueon every fleet deployment and cannot be unwired from.env.
Because of that, lobes capabilities does not render loaded verbatim: a
role this box serves by forwarding shows by-proxy in the loaded
column (derived from feasible: false + proxied: true), so the table reads
the same for every proxied role regardless of leftover local wiring. That is
a CLI display state only — this JSON contract is unchanged, loaded
stays a bool, and a programmatic consumer branching on it is unaffected.
What a caller actually sees. A proxied role's endpoint/path are
unchanged — a client that already discovered them via GET /capabilities
keeps POSTing to the same URL it always would; proxied: true is purely
informational (a client that ignores it still works). The one visible
difference on the wire is a response header the raw OpenAI endpoint
carries — never surfaced in the /capabilities JSON itself —
X-Lobes-Proxied-By: <peer origin>, present on every answer this box
produced by forwarding, absent on every locally-served answer. See
docs/gateway-fleet.md#proxy-lobes-the-third-lobe-state-opt-in
for the full marker-header and failure-mode contract.
ready is the live peer probe, not a hardcoded claim. A proxied role's
ready is never forced true just because a local process happens to be
healthy — a background thread probes the declared peer's own GET /v1/models and ready reflects whether the peer actually lists the id this
box would forward to it. A dead or misconfigured peer means ready: false
(or the id drops off /v1/models entirely) even though proxied: true is
still declared — declaring the intent to proxy is not evidence the peer is
reachable right now.
The honesty invariants this state carries forward, unchanged:
- #91 (no silent substitution) — a proxied
sensesrequest is answered bysensesrunning on the peer, never quietly served by a different, locally-feasible model. The peer's own served id is what comes back; a peer that itself declines the role (404 role_infeasible) is relayed terminally, naming the peer, never silently retried against something else. - #92 (operator-declared origins, never derived) —
hosted_byis always the literal<PREFIX>_PEER_ORIGINan operator typed into.env; nothing here infers a peer from hostnames, interfaces, or DNS. - Single-hop — a role proxied on this box is never proxied a second time: a request already carrying the internal hop marker that would need to depart again is refused rather than forwarded onward, so two misconfigured boxes pointing at each other fail fast instead of looping.
Default off, byte-identical. With no <PREFIX>_PEER_PROXY armed
anywhere, no role in this deployment is ever proxied — every payload here
looks exactly as it did before this state existed (a feasible: false role
carries hosted_by at most, never proxied).
lobes up starts (or, with --down, stops) one role's gear without
touching the rest of the fleet:
lobes up cortex --apply # docker compose up -d vllm-primary
lobes up senses --apply # docker compose up -d vllm-multimodal
lobes up muse --apply # docker compose up -d vllm-muse (muse-hosting shape only — currently no box hosts one, see the dormant callout above)
lobes up worker --apply # docker compose up -d vllm-worker (verb wired; errors helpfully off a non-hosting box — a worker-hosting shape is forthcoming, thor-worker-lobe plan t7)
lobes up embedder --apply # docker compose up -d vllm-embed
lobes up reranker --apply # docker compose up -d vllm-rerank
lobes up stt --apply # requires the --audio overlay
lobes up tts --apply # requires the --audio overlay
lobes up colleague-stack --apply # the SIX default roles at once (requires --audio scaffolded)Dry-run by default (prints the exact docker compose … command); --apply
commits. colleague-stack is a first-class bundle — the four default fleet
roles plus the audio overlay's stt/tts — not a compose profiles:
tag, because tagging the already-default-on services with a profile would
demote them out of the default fleet (a regression). If the audio overlay
isn't scaffolded, colleague-stack (and up stt/up tts) fail with a
remediation pointing at lobes init --fleet --audio --apply, rather than
silently starting only four of the six roles. colleague-stack stays the
six default-hosted roles — muse and worker are deliberately excluded
(their services are compose-profile-gated, so bundling either would break the
target on every non-hosting deployment). lobes up muse works on a
muse-hosting deployment and errors helpfully — naming the fix — when the
deployment's COMPOSE_PROFILES doesn't include muse; lobes up worker is
designed to mirror that exact mechanic once the vllm-worker compose service
and a worker-hosting shape land (thor-worker-lobe plan).
Read-only, per-role runtime metrics — never a task-quality or correctness claim (lobes measures serving performance; whether an answer was good is Colleague's call):
lobes measure # all eight roles, table
lobes measure --json # all eight roles, JSON
lobes measure --role cortex --jsonMetrics are grouped by the role's family:
- LLM roles (
cortex,senses,muse,worker):ttft_ms,decode_tps,prefill_tps,context,mem_usage_pct(when the vLLM/metricsscrape is cheaply reachable);restart_count/error_countare alwaysnull(not cheaply available without a docker inspect, which this verb deliberately never does). - Pooling roles (
embedder,reranker):reqs_per_sec,docs_per_sec,latency_ms,batch_size,loaded. - Audio roles (
stt,tts):rtf(real-time factor),latency_ms,duration_ms,failure_rate.
An unloaded or unreachable role degrades to ready: false with every metric
null — this is the normal case in CI or on a partial deployment, not an
error; a dead senses backend never stops cortex from being measured.
lobes benchmark --profile <name> runs a RUNTIME-ONLY, side-by-side
comparison across a fleet profile — built on the same per-role probes
lobes measure uses, not the load-test engine --all-lobes uses:
| Profile | What it compares |
|---|---|
cortex-only |
The cortex generate lane alone. |
cortex+senses |
cortex and senses side by side (the machine-as-brain default duo — a mesh-brain deployment shape can drop one of the two; see docs/deployment-shapes.md). |
senses-direct |
senses addressed directly (cheap/front-door tasks). |
qwen-nvfp4-vs-bf16 |
The current cortex endpoint probed as both a quantized and an unquantized catalog variant — reported available: false with a reason when the catalog doesn't carry both sides (never fabricated). |
all |
Every profile above. |
lobes benchmark --profile cortex+senses --json
lobes benchmark --profile allDegrades gracefully offline: an unreachable role or a catalog-missing variant
is reported unavailable with a reason, never a crash or an invented number.
A Colleague client needs only the fleet's base URL. The whole discovery and dispatch flow:
GET <base_url>/capabilitiesonce.- Read the role you want (
cortex,senses,muse,worker,embedder,reranker,stt,tts) out of the response — itsendpoint,model, andpath. POST <endpoint><path>with"model": <model>and the role-appropriate body shape (chat messages forcortex/senses/muse/worker,inputforembedder,query+documentsforreranker).
No model id is ever hardcoded in the client. Concretely (Python, stdlib-only):
import json
import urllib.request
def call_role(base_url: str, role: str, body_extra: dict) -> dict:
with urllib.request.urlopen(base_url.rstrip("/") + "/capabilities") as r:
contract = json.load(r)
info = contract[role]
body = {"model": info["model"], **body_extra}
req = urllib.request.Request(
info["endpoint"].rstrip("/") + info["path"],
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
return json.load(r)
call_role("http://localhost:8000", "cortex",
{"messages": [{"role": "user", "content": "ping"}]})Because cortex, senses, muse, and worker are gateway-fronted, they
(along with embedder/reranker) share the same endpoint — the
gateway's base URL — and routing between them happens purely via the model
field the contract handed back. stt/tts resolve to the audio-overlay
bridge URL instead.
Rename-safety, proven. If an operator swaps cortex's served checkpoint
(PRIMARY_SERVED_NAME in the fleet .env) and re-runs lobes fleet up --apply, the very next GET /capabilities call reflects the new model id —
the client above needs zero code changes to keep working. This is proven
end-to-end in tests/test_colleague_contract.py
(test_colleague_follows_an_operator_rename_with_no_client_code_change).
Every field lobes emits about a role — in /capabilities, lobes measure, or
lobes benchmark --profile — is a serving/runtime descriptor: latency,
throughput, context, quant, load state, or a declared (not graded)
responsibility. Nothing lobes emits asserts answer correctness, task
quality, or agent-task success — judging whether a response was actually
good is Colleague's job. This boundary is enforced by test
(tests/test_colleague_contract.py::test_capabilities_contract_is_runtime_descriptor_only
and ::test_measure_registry_emits_only_allowed_runtime_metric_keys), which
scans every emitted key for quality-flavoured tokens (accuracy, correct,
quality, task_success, success_rate, grade, score) and fails if one
appears.
The Colleague-role contract shipped alongside a context rebalance. Both
cortex and senses serve less context than the legacy single-model
scaffold did solo, in exchange for co-residency:
| Deployment shape | cortex / primary served context |
senses / multimodal served context |
|---|---|---|
Legacy single-model scaffold (lobes init / lobes serve, no fleet) |
256K (VLLM_MAX_MODEL_LEN=262144, solo, util 0.6) |
(not served — single model only) |
| Fleet duo, pre-rebalance | 64K, util 0.30 | 128K (MULTIMODAL_MAX_MODEL_LEN=131072, util 0.22) |
| Fleet duo, current (this doc) | 128K (PRIMARY_MAX_MODEL_LEN=131072, util 0.30 — util-bound, not context-bound) |
32K (MULTIMODAL_MAX_MODEL_LEN=32768, util 0.14) |
The pre-rebalance duo gave the vision gear its full native 128K at the
cost of trimming cortex to 64K; the current default flips that trade-off —
cortex (the final-authority reasoning role) now gets its full native 128K,
and senses (intake/perception) is trimmed to 32K, which is ample for the
"normalize input, classify intent, prepare a context packet" responsibilities
it's actually scoped to. Running cortex solo (no fleet, no senses)
still restores the legacy single-model 256K/util-0.6 footprint — the role
contract doesn't change what a solo deployment can serve, only what the
default duo budgets. See
docs/gateway-fleet.md for the full budget table
and live-validation history behind this rebalance.
docs/gateway-fleet.md— Docker topology, tier-alias fallback contract, pressure busy-backpressure policy, memory budget, live validation history.docs/openai-api.md— the raw OpenAI-compatible wire endpoints each role sits behind.docs/deployment-shapes.md— the orthogonal deployment-shape axis: which of these eight roles a given box hosts at all, the cross-box honest-referral surface for a role it doesn't, and the opt-in proxy-lobes extension (the awake/asleep/proxy table, the pairwise key contract, a worked example).lobes explain roles— the in-CLI version of this doc.lobes explain fleet/lobes explain gateway— routing semantics.tests/test_colleague_contract.py— the end-to-end proof of the client flow and the runtime-only boundary described above.tests/test_roles_proxied.py/tests/test_gateway_proxy.py— the proxied role state and the data-plane forward it rides on.