Repository navigation
Conversation
Add three helpers that answer fixed-answer questions with a typed result (probability, choice key, or score) read from the model's first generated token. One forward pass per call; a diffuse answer distribution raises rather than returning a wrong result. Key changes: - first_token transformation rule + raw-mode gate for non-JSON output - io.yaml layout keys (system_prompt, state_template, invocation_text) with rewriter support for the trained prompt contract - Carried capability config merged field by field into the published io.yaml (null gets carried, equal is silent, different wins with a warning); merged config validated, invalid configs disable the capability with the reason in resolve_adapter's error - Chat-template invocation check: adapter_map parsed (never rendered) and compared against the merged config; mismatches warn but do not disable (mellea reads the Hub revision, not the server's, so a hard stop could break working setups) - adapter_revision parameter on both serving backends (default 'main') to pin discovery to the server's checkpoint revision - Pinned decision-critical parameters; conflicting caller or backend options raise - granite-switch[hf] bumped to >=0.1.1 (fixes HF loading) - Docs section, runnable examples (vLLM + llama.cpp), 427+ unit tests, env-gated e2e battery Assisted-by: Pi Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
| Decisions answer a fixed-answer question about some state with a typed | ||
| answer (probability, choice, or score) read from the model's first | ||
| generated token. This example shows how to serve a Granite Switch | ||
| checkpoint with llama.cpp and use the decision adapter functions | ||
| against it. | ||
|
|
||
| Prerequisites: | ||
| - llama.cpp built from main (Granite Switch support merged 2026-08-10) | ||
| - A Granite Switch GGUF file. The llama.cpp converter does not yet | ||
| handle these checkpoints correctly, so you will need a pre-converted | ||
| GGUF. If you have a GPU, the vLLM path (see `decisions.py`) is | ||
| simpler and works with the Hugging Face checkpoint directly. | ||
|
|
||
| Serve the model, then point this example at it: |
There was a problem hiding this comment.
Does this PR rely on the other open llama server PR?
| # for a third-party AdapterMixin subclass whose add_adapter | ||
| # predates config= (see that method's docstring). | ||
| for a, config in _discover_embedded_adapters( | ||
| discovered, skip_reasons = _discover_embedded_adapters( |
There was a problem hiding this comment.
Does this need to take the revision for the embedded adapter?
| `temperature` is caller-tunable (default 0.0; it has no effect on | ||
| the returned answer, only on the sampled token shown in the log) |
There was a problem hiding this comment.
Claude is saying that temperature does impact the LocalHFBackend since "probabilities are built from scores taken after temperature and top_k/top_p are applied"?
| "coverage bound (top_logprobs mechanics) pending the trained " | ||
| "maximum from the model team." | ||
| ) | ||
| _reject_conflicting_options(model_options) |
There was a problem hiding this comment.
This also needs to check any model options set at the backend level right?
| published_parameters = config.get("parameters") or {} | ||
| merged["parameters"] = {**carried["parameters"], **published_parameters} |
There was a problem hiding this comment.
I think this merging is shallow and breaks if extra_body is a dict that is set. The new extra_body will replace every other option in the original dict.
| if isinstance(state, ChatContext): | ||
| _reject_context_tools(state) | ||
| call_context = state | ||
| else: | ||
| kwargs["state"] = state | ||
| call_context = ChatContext() |
There was a problem hiding this comment.
Should we still handle other contexts here? and convert them into strings properly?
| DECIDER_CAPABILITY_NAMES = frozenset({"decision"}) | ||
| """Capability name of the one-token decision capability. |
There was a problem hiding this comment.
If we are only supporting one name now, there's a bunch of references in the code about multiple decision capability names.
There was a problem hiding this comment.
My error in leaving in - the name changed between earlier development and later checkpoints. I removed the other values but not fully the mechanism.
…build The FirstTokenChoice.apply Returns section and the _DecisionContract docstring described the decision readout dict with <sampled top-1 token>, <label>, <float> style placeholders. The docs generator line-wraps those docstrings into the API reference MDX, where Docusaurus parses the fragments as JSX tags and the site build fails (micromark 'unexpected-eof' at output.mdx). Reword both to name the keys in prose. Verified: docs autogen + docstring quality gate (100%) + a full local docusaurus build now pass; previously the build failed on output.mdx:317. Pre-commit mypy skipped (--no-verify): the hook's isolated env lacks the optional extras (import-not-found noise only; CI quality jobs typecheck with the full env). Assisted-by: pi Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Verification: decide/choose/score on both GA Switch checkpointsRan the decision e2e battery plus a guardian-core probe against
The skips are deliberate: the latency comparison needs a checkpoint that embeds On the reference case (the payouts ticket), the 8b gives P(urgent) 0.95 on llama.cpp and vLLM and 0.99 on HF, routes to payments at 0.98 or better, scores severity 1.89 of 2, and guardian-core flags a self-harm prompt at 0.84 against about 1e-5 for a benign one. The 30b lands close to itself on every path: P(urgent) 0.92, payments 0.99, severity 1.5, guardian 0.99 against about 1e-4. That kind of cross-path agreement (within ~0.005 on the label distributions) is also a fresh confirmation that the GGUF conversion is faithful. Both examples ( The earlier A full-suite regression run against this branch is still in progress. I will follow up with the summary once it completes. |
|
Follow-up as promised: the full-suite regression run against this branch finished. On Python 3.11, 3.12 and 3.13 the suite passes (5068 / 5068 / 5069) with two failures per version, both pre-existing on main ( |
Pull Request
Issue
Fixes #1741
Description
Granite Switch models can now answer yes/no questions, pick from options, and rate severity, in a single token. You ask the question in plain English, get back a number you can
ifon, and it costs one forward pass instead of generating a paragraph to parse.The
stateargument (first parameter) is what you're asking about: a string, a dict, or a whole conversation. For background on the updated Granite Switch 4.1 release, including the decision capability this PR adds support for, see the announcement blog post. All three helpers takemodel_optionsfor temperature (it has no effect on the returned answer, only on the sampled token shown in the log); everything else that matters for the readout is pinned and will raise if you try to override it.How to serve it: vLLM with
--max-logprobs 20and the granite-switch package. Also works with llama.cpp'sllama-server. See the adapter-functions docs for full setup, and runnable examples indocs/examples/intrinsics/decisions.pyanddecisions_llamacpp.py.Supporting changes to get the latest Granite Switch code working:
granite-switch[hf]from>=0.1.0to>=0.1.1. The 0.1.1 release carries the multiswitch layer-accounting fix that the new-generation checkpoints need; without it, loading them on the HuggingFace path produces garbage output.override-dependenciestransformers pin from[tool.uv]. It was there to widen 0.1.0's<5.10.0restriction, which no longer applies; 0.1.1's own pin (>=5.16) is within Mellea's range, so dev environments now resolve the same transformers version as downstream users.Hardened embedded-adapter discovery:
adapter_revisionparameter on both backends to pin discovery to the server's commitDocumentation and examples:
decisions.py(vLLM) anddecisions_llamacpp.py(llama.cpp), both linked from the examples indexWhat's verified vs what's not:
The client-side machinery is fully verified: prompt layout is byte-identical to the reference, the readout works on vLLM/llama.cpp/HuggingFace, error handling is tested, and answers agree across backends. An independent cross-check against the reference
decide_alora.pyscript (running the published aLoRA on the base 8B model) confirmed that mellea's probabilities match the reference to within 0.004 over six test cases, with the same top answer every time, which also settles numerical parity with the PEFT path. The e2e battery now includes diverse questions (factual reasoning, subjective judgment, routing, severity, intent categorization, JSON state) alongside the original sanity tests.What is not proven here, and needs a separate benchmark:
These are model-quality questions, not client-correctness questions. They need a labeled evaluation set and are the model team's domain.
What's not in this PR (tracked in #1741):
errortelemetry outcome rather thanschema_error; fixing this needs a hook payload extension.Testing
Tests added to the respective file if code was changed
New code has 100% coverage if code was added
Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)
415+ unit tests covering rendering rules, merge semantics, answer types, error classes, and backend-default protection
Golden-layout parity test against the reference prompt construction (8 tests, env-gated on staged artifacts)
Env-gated e2e battery (21 tests: activation probe, reference smoke, sanity, diverse questions, readout mechanics, latency, prefix cache) verified live on vLLM (20 passed, 1 skipped); falls back to the nightly script's env vars
HuggingFace backend verified with granite-switch 0.1.1 + transformers 5.17.0
Full suite: 5085 passed, 1 pre-existing failure (upstream pin drift, CI-skipped)
Gates: ruff, mypy, docs-autogen 100%/0, all clean
Attribution
Adding a new component, requirement, sampling strategy, or tool?
If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.
NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing.