references/engine-axis.md
references/engine-axis.mdBrowse 11 files
13,697 bytes
Token encoding: o200k_base
Snapshot a9fb1c3
Engine extension: add a new playground feature axis
Loaded on demand by the cookbook-add-model skill. Rare — adding a model
cookbook is data-only and never needs this. The current 7 built-in axes
(attention, moe, parsers, speculative, pdDisagg, hicache,
hisparse) already cover the SGLang feature surface most cookbooks need.
Model-specific features are config DATA, not engine code. The axis
handlers read options / flags / env / gating straight from
config.playgroundFeatures, so a model-specific feature is added as data on
an existing axis with NO engine edit — MegaMoE W4A4 is not its own axis, it's
config data on moe (a megamoe backend option + a megamoeQuant
sub-select). Reach for this file ONLY when a feature's shape — its
title + the flag family it strips + its option/state model — is something no
existing axis can express.
When you do extend, build a GENERIC primitive, never a model-named handler.
The right unit is a reusable shape (e.g. "a titled single-select that strips a
configurable flag family and splices the picked option's flags" — exactly the
speculative handler's shape, minus its hardcoded title + --speculative-*
strip list). Parameterize title, strip-prefixes, and options from config so
the next model of that shape is pure config. Do NOT add a kvcache /
<model-feature> handler that hardcodes one model's flag — that's the
model-specific-code-in-the-engine anti-pattern this architecture exists to
avoid. (Precedent: Nemotron3's "KV Cache DType" and Qwen3's mamba-cache select
are the SAME single-select shape → one generic primitive serves both, then
both are config.)
A new axis is backward-compatible — zero churn on existing configs. The
runtime is opt-in per key: the apply/render loop does const fc = pgFeatures[axisId]; if (!fc) continue;, so any config that doesn't declare
the key never sees the axis. And a model-specific axis does NOT join the
opt-out "general axes ship on every cookbook" set (that set is an authoring
convention for NEW configs, not a runtime default) — so review-pr won't flag
existing pages for lacking it, and you never touch a merged config. Only the
models that expose the control declare it. Touches _playground.jsx only.
For the per-model config/cells/MDX reference see authoring-reference.md.
3.1 Decide
Before touching the engine, confirm:
- The feature cannot be expressed as data on an existing axis (a new MoE
backend belongs in
moe.backend.options; a new parser inparsers.items; a new spec preset inspeculative.options). This is the common case — most "new features" are new options, not new shapes. - The shape is genuinely new (state model + strip pattern), AND you are
adding it as a GENERIC config-parameterized axis (title / strip-prefixes /
options all from config), not a one-model handler. If you'd hardcode a
specific flag like
--kv-cache-dtype, stop — generalize the shape instead. - The shape has a clean strip-prefix → emit-flag pattern.
If unsure, add it as data first (in one cookbook's config under an existing axis) before promoting it to a built-in axis.
3.2 Pick the axis id and state shape
The axis id is the key in both config.playgroundFeatures and the
internal deltas object. Use camelCase, descriptive but short:
mambaCache, attentionBackend, kvCacheDtype.
The state shape is whatever initState returns. Common shapes:
- Single-select: a string sentinel (e.g.
"disabled"/"current"/ an option id). - Multi-toggle:
{[itemId]: bool}. - Sub-knobs:
{[knobId]: value | null}. - Compound (axis with its own internal sub-state, like PD-Disagg's
{mode, ibDevice}): a plain object.
Pick ONE "inherit base" sentinel and document it in the handler comment.
3.3 Implement the handler
Add one entry to AXIS_HANDLERS in _playground.jsx.
The handler owns everything: state init, apply (strip+insert), hidden-revert,
AND the JSX render. Engine main loop iterates AXIS_HANDLERS and calls each
method by name — adding a new axis is genuinely a one-place change.
Template:
// ---- Axis: <Title> ----------------------------------------------------
// <one-paragraph description of what this axis controls and why it
// exists. Mention the SGLang feature it wraps and the strip/insert
// policy.>
<axisId>: {
initState: (fc) => /* initial state value */,
// Called when base cell changes. Return new value if the picked option
// is now hidden by a constraint; otherwise return value unchanged.
// Disabled picks are intentionally NOT auto-reverted (soft warning).
revertHidden: (value, fc, base, h) => {
// ... return value or a new value
return value;
},
// Pure function. Receives the current (flags, env) and returns the next
// (flags, env). Do NOT mutate inputs. The `value` argument is whatever
// initState returned. The `fc` argument is config.playgroundFeatures[axisId].
// The `sel` argument is the current base cell selection. The `h`
// argument is the helpers bundle (strip/insert primitives + anchors).
apply: ({ flags, env, value, fc, sel, h, derived }) => {
if (/* value is the inherit-base sentinel */) return { flags, env };
flags = h.stripFlagsByFirstToken(flags, [/* prefixes this axis owns */]);
if (/* an option is picked */) {
flags = h.insertAfter(flags, h.ANCHOR_NEAR_<X>, [/* new flags */]);
// or: flags = h.insertBeforeTail(flags, [/* new flags */]);
// if the axis mutates env:
// env = h.stripEnvByPrefix(env, fc.stripEnv || []);
// env = [...env, /* additional env vars */];
}
return { flags, env };
},
// Optional: read the base cell's flag array back into the same shape
// initState/apply use. Render shows this as the default selection
// (dropdown option or checked chip) when the state slot is the inherit
// sentinel — so the user sees the cell's actual --tp / MoE backend /
// spec preset instead of an opaque "Auto." When derive returns a real
// value, the inherit-sentinel option is hidden from the control. Apply
// also receives the derived
// value (as `derived`) and may use it as a no-op shortcut when the
// user's pick matches base. Skip when your axis owns flags that never
// appear in base cells (PD-Disagg / HiCache).
// deriveFromBase: (cell, fc, h) => ({ ... }) | null,
// Optional: hints for the renderer. Currently only pdDisagg uses this
// to report its role banner. Omit if not needed.
// getRenderHints: (value, fc) => ({ pdMode: ... }) | null,
// Returns the axis card JSX. The outer div MUST have key={axisId} so
// React can track it in the engine's map loop. Return null for
// axis-level gating (e.g. HiSparse when the live PD mode isn't `decode`). Lay out as a single
// compact horizontal row: title on the left, fields after.
render: ({ axisId, value, setValue, fc, base, s, h, renderChip, renderSelect, derived }) => {
if (/* axis-level gating fails */) return null;
return (
<div key={axisId} style={s.card}>
<div style={s.compactRow}>
<span style={s.axisTitle}>Axis Title</span>
{/* For multi-option fields, use renderSelect(...) — the default.
For on/off toggles or single-select chip groups, use
renderChip instead (see "Control choice" in the conventions
below). Read state from `value`; write via `setValue(next)`
(replaces the whole axis slot). */}
<span style={s.field}>
<span style={s.fieldLabel}>Field</span>
{renderSelect(value.slot, fc.entries, (v) =>
setValue({ ...value, slot: v }), base)}
</span>
</div>
</div>
);
},
},
Important conventions:
- Insert the entry in the position you want it rendered.
AXIS_HANDLERSis iterated in insertion order for both render and apply. - Use
h.ANCHOR_NEAR_*constants for insertion. Add a new anchor to the helpers bundle if your axis needs to land somewhere new in the flag block. - Use lowercase HTML JSX tags only. Capitalized tags get rebound by Mintlify.
- Inside
render, read state viavalue(the slice for this axis). Write state viasetValue(next)(replaces the whole slice). For compound axes, dosetValue({ ...value, [k]: nextK }). - Layout: one
s.compactRowper axis card,s.axisTitlefor the leading label, ones.fieldper (label + input) pair. - Control choice —
renderSelectvsrenderChip:renderSelect(current, entries, onPick, base, labelFor?, opts?)is the default compact control (a<select>dropdown). It filters hidden chips and disables greyed-out ones internally — no per-chipevaluateChiploop needed in the render body. Most axes use it (attention, moe, pdDisagg, hisparse, hicache). Pass{ hideValues: [<sentinel>] }when yourderiveFromBaseresolved to a real value, so the inherit-sentinel ("Auto" / "Inherited" / "current") doesn't clutter the dropdown.renderChip(label, current, value, onPick, { disabled?, disabledReason? })renders a button instead of a dropdown row. Use it for a chip group when you want the options laid out as buttons. It serves two shapes:- Multi-toggle (Parsers) — one independent on/off chip per item;
currentis that item's effective bool,valueistrue, so the chip is "checked" when the item is on. - Single-select (Speculative) — a radio-style group; pass the
group's effective value as
currentand each option's id asvalue, so exactly one chip is checked (current === value). Chip groups own their visibility/disable filtering: looph.evaluateChip(opt, base)in the render body, skipc.hidden, filter the inherit-sentinel yourself whenderiveFromBaseresolved to a real value, and forwardc.disabled/c.disableReasonintorenderChip's opts (this is what surfaces a disabled chip's tooltip, e.g. a "Coming soon" entry).
- Multi-toggle (Parsers) — one independent on/off chip per item;
- Selected chips use the same terracotta (
#D45D44) as the Deploy panel's selected button, so both widgets read as one visual system. Don't introduce a per-axis accent color. - Default-from-base: if your axis can be read out of base cells'
flags, implement
deriveFromBaseand have your render show the derived value when state is the sentinel (e.g.const eff = value.tp !== null ? value.tp : (derived && derived.tp)). This is what makes a fresh playground load show the user's actual recipe instead of "auto." Flag-parsing helpers onh:parseIntFlag,hasFlag,findFlagArg. - Avoid the
inoperator wrapped in unary (!(x in y)). Mintlify's AST walker crashes on it (TypeError: this[e] is not a function). Useobj.key === undefinedorobj.id !== undefinedinstead. Bareif (key in obj)(no surrounding!) is fine.
3.4 Document the per-cookbook schema
Edit the file header in _playground.jsx to add your new axis to the
"Recognised keys" list, with a one-line description of its schema.
Optionally add a paragraph below explaining its strip/insert policy.
Update the §2.3 axis table in authoring-reference.md to list the new axis.
3.5 Migrate cookbooks that need it
For each cookbook that should expose this axis, add a
playgroundFeatures.<axisId> entry to its config. Verify the chip group
renders, options apply correctly, and the diff matches expectations.
Pitfalls (engine work)
Insertion anchor misses — insertAfter falls back to right-after
--model-path if none of its anchor prefixes are present. If your axis
emits flags that should land somewhere specific, include the most likely
anchor prefixes in your call. Order doesn't matter (set semantics).
Conditional strips — Some axes strip ONLY when overridden
(attention.tp, moe.backend (incl. the MegaMoE quant env), speculative).
Others strip
UNCONDITIONALLY whenever declared (parsers, pdDisagg, hicache). The
header comment in AXIS_HANDLERS documents which policy each axis uses;
follow the same pattern when adding a new axis. If unsure, prefer
conditional strip — it preserves base behavior when the user does not
opt in.
Closure of AXIS_HANDLERS — Inside a handler method, you can
reference AXIS_HANDLERS.<otherAxis> for cross-handler calls (no built-in
axis currently needs this, but it works because AXIS_HANDLERS is in
lexical scope). Do NOT use this for general logic — it tightly couples
handlers. Reserve it for one handler's helpers shared between its own
render and revertHidden.
Review checklist for a new-axis PR
-
AXIS_HANDLERSis the ONLY place that mentions the new axis id (apart from per-cookbook config). Noif (axisId === '<new>')branches anywhere in the engine. -
initStateis deterministic and idempotent (does not depend on the base cell). -
applyis pure — does not mutate inputs. -
revertHiddenreturns the same reference when nothing changed (avoids unnecessary re-renders). -
renderreturnsnullwhen axis-level gating fails (whole card hidden) — does not render an empty placeholder. -
rendersetskey={axisId}on its outer element. - No
!(x in y)patterns introduced (Mintlify AST walker crashes). - File header lists the new axis in "Recognised keys".
- The §2.3 table in
authoring-reference.mdlists the new axis. - One existing cookbook config is updated to consume the new axis, and visual verification shows the diff is correct.