Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,38 @@ The release procedure (prompt template and step-by-step instructions) lives in [
native** produces `Failed to parse model parameters` at load time and nothing earlier catches it.
The knob sweep did — it is what surfaced the mismatch here.

- **`net.ladenthin:llama` 5.1.0 → 5.2.0-SNAPSHOT.** Deliberately a snapshot: the binding change this
release depends on is not yet published. Building srcmorph therefore requires that snapshot to be
resolvable, and CI stays red until it is — recorded here so nobody mistakes it for a regression.
- **`net.ladenthin:llama` 5.1.0 → 5.2.0.** Deliberately the *release* version, not `5.2.0-SNAPSHOT`:
this repo declares no `<repositories>` element (the `distributionManagement/snapshotRepository` is
deploy-only), so the Central snapshot channel is not resolvable here at all — and a `-SNAPSHOT`
dependency on `main` would block every srcmorph release, since Central rejects one. The build
therefore stays red until `net.ladenthin:llama:5.2.0` is published and goes green with no further
edit the moment it is — recorded here so nobody mistakes it for a regression.

- **BREAKING: `chatTemplateEnableThinking` is a tri-state; the `enable_thinking` kwarg is only sent
when it was actually set.** It used to be a plain `boolean` defaulting to `true`, so every run put
`enable_thinking` into the chat-template kwargs -- including runs whose chat template has never
heard of the kwarg, which llama.cpp's Jinja layer has been moving from "silently ignored" toward
"warned about". The only way to stop that noise was to set the knob to `false`, which means
something else entirely.

The obvious phrasing -- "send it only when it differs from the template's default" -- is not
implementable: srcmorph would have to parse and evaluate the template to know that default, which
is exactly the work it delegates to the binding. So the rule is "send it only when the user
actually set it".

`AiGenerationConfig`, `AiModelDefinition` and `LlamaCppJniConfig` (and its builder) now carry
`@Nullable Boolean` instead of `boolean`, `DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING` is `null`
(unset) instead of `true`, and the two `isChatTemplateEnableThinking()` getters were renamed to
`getChatTemplateEnableThinking()` to match the wrapper type. `true` and `false` are both still
forwarded verbatim -- unset is the only value that omits the kwarg. A configuration that never
mentioned the knob keeps working and simply stops sending it; one that set it explicitly is
unaffected.

Guarded by four model-free tests over the extracted `buildChatTemplateKwargs()` -- including the
`true` case, so a future "send it only when false" shortcut fails rather than silently swallowing
a configured value.

---

## [1.2.0] - 2026-09-01

Expand Down
18 changes: 0 additions & 18 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,6 @@ recorded in git history and `crossrepostatus.md`, not here.
not dropping the idea. Deliberately out of scope for 1.2.0: it is a build-time question, not a
correctness one.

- **`enable_thinking` is sent unconditionally, including at its own default.**
`LlamaCppJniAiGenerationProvider.model()` always puts `enable_thinking` into
`chatTemplateKwargs`, at whatever `chatTemplateEnableThinking` says — and its default is `true`.
A model whose chat template does not know the kwarg gets it anyway; llama.cpp's Jinja layer has
been moving such unknown kwargs from "ignored" toward "warned about", so a default run emits noise
that the user did not ask for and cannot switch off without setting the knob to a value that means
something else.

The obvious phrasing of the fix -- "send it only when it differs from the template's default" --
is **not implementable**: srcmorph cannot know a template's default without parsing and evaluating
the template, which is exactly the work it delegates to the binding. The implementable fix is
"send it only when the user actually set it", and that needs the config field to become a tri-state
(`Boolean` rather than `boolean`, `null` = unset), which changes `AiGenerationConfig`,
`AiModelDefinition`, `LlamaCppJniConfig` and its builder, plus the plugin's `@Parameter`. That is a
public-API change, so it belongs in a minor release with the deprecation story written out, not in
a patch. **Was announced during the 1.2.0 audit cycle and never landed** -- recorded here rather
than left as a claim in a chat log.

- **`srcmorph:calibrate` reports only through the log.** `CalibrateEngine` builds a
`CalibrationReport` and `CalibrateMojo` prints it as `INFO` lines. There is no machine-readable
output, so the numbers a calibration run produces (prefill / decode throughput, chars per token per
Expand Down
2 changes: 1 addition & 1 deletion srcmorph-maven-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ below are the shipped values (`AiGenerationConfig.DEFAULT_*`).
| `lazyMode` | *(empty)* | On-demand reading of lazy-loadable tensors (`--tensor-read-lazy`): `off`, `auto` or `on`; empty leaves the default. Trades resident memory for disk reads and shortens model load time; **requires mmap** |
| `mainGpu` | `-1` | Primary GPU index (`--main-gpu`); `-1` = leave default. Matters on multi-GPU hosts (e.g. a Vulkan build enumerates every GPU) |
| `devices` | *(empty)* | Explicit device selection (`--device`), comma-separated backend device names (e.g. `Vulkan1`); takes precedence over `mainGpu` |
| `chatTemplateEnableThinking` | `true` | Enable the chat template's thinking mode |
| `chatTemplateEnableThinking` | *(unset)* | Chat-template thinking mode (`enable_thinking` kwarg): `true` or `false` is forwarded verbatim, unset omits the kwarg so the model's own template default applies. Set `false` for Gemma 4 to keep thought tokens out of stored output |
| `reasoningEffort` | `low` | gpt-oss harmony reasoning effort (`low`/`medium`/`high`); empty omits the kwarg (e.g. for non-gpt-oss models) |
| `reasoningBudgetTokens` | `-1` | Cap on harmony reasoning tokens (`-1` = unrestricted) |
| `dryMultiplier` | `0.0` | DRY repetition-penalty multiplier (`0.0` = disabled); the other `dry*` knobs only apply when this is `> 0` |
Expand Down
6 changes: 3 additions & 3 deletions srcmorph/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ SPDX-License-Identifier: Apache-2.0
InferenceParameters, ModelParameters, ChatResponse/Timings/Pair, ChatResponseParser,
ReasoningFormat) is untouched by 5.1.0.
5.2.0 is a BREAKING bump this module needs, not an optional one: the binding removed
ModelParameters.enableFlashAttn()/ModelFlag.FLASH_ATTN (--flash-attn takes a mandatory
ModelParameters.enableFlashAttn()/ModelFlag.FLASH_ATTN (the flash-attn flag takes a mandatory
on|off|auto value since llama.cpp b10273, so the valueless flag emitted a broken argv)
and renamed setTensorReadLazy/TensorReadLazyMode to setLazyMode/LazyMode (llama.cpp
b10731 renamed --tensor-read-lazy to --lazy-mode with no alias). The provider calls
b10731 renamed the tensor-read-lazy flag to lazy-mode with no alias). The provider calls
both, so it cannot compile against 5.1.0 any more.
This deliberately names the RELEASE, not 5.2.0-SNAPSHOT: a snapshot would need a
<repositories> entry for the Central snapshot channel (this repo declares none -- the
<repositories> entry for the Central snapshot channel (this repo declares none; the
distributionManagement/snapshotRepository below is deploy-only) AND would block every
srcmorph release, since Central rejects a release with a -SNAPSHOT dependency. The
build therefore stays red until net.ladenthin:llama:5.2.0 is published, and goes green
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,24 @@ public AiGenerationConfig() {
public static final float DEFAULT_REPEAT_PENALTY = 1.0f;

/**
* Default setting for whether the model's chat-template thinking mode is enabled.
* Default setting for the model's chat-template thinking mode: {@code null}, meaning
* <em>unset</em>.
*
* <p>When enabled, the model uses its own chat-template default for chain-of-thought
* reasoning. When disabled, {@link net.ladenthin.llama.parameters.ModelParameters#setChatTemplateKwargs}
* is called with {@code {"enable_thinking": "false"}} to suppress the thinking block at the
* Jinja template level — set to {@code false} for Gemma 4 definitions to prevent
* {@code <|channel>thought} tokens from leaking into stored output.</p>
* <p>The value is a tri-state on purpose. While it was a plain {@code boolean} defaulting to
* {@code true}, every run put {@code enable_thinking} into
* {@link net.ladenthin.llama.parameters.ModelParameters#setChatTemplateKwargs} &#x2014; including
* runs whose chat template has never heard of the kwarg, which llama.cpp's Jinja layer has been
* moving from "silently ignored" toward "warned about". There was no way to switch that noise
* off except by setting the knob to {@code false}, which means something else entirely.</p>
*
* <p>"Send it only when it differs from the template's default" is not implementable here:
* srcmorph would have to parse and evaluate the template to know that default, which is exactly
* the work it delegates to the binding. So the rule is "send it only when the user actually set
* it": {@code null} omits the kwarg and the model's own chat-template default applies, while
* {@code true} and {@code false} are both forwarded verbatim. Set {@code false} for Gemma 4
* definitions to keep {@code <|channel>thought} tokens out of stored output.</p>
*/
public static final boolean DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING = true;
public static final @Nullable Boolean DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING = null;

/**
* Default for whether llama.cpp prompt caching ({@code cache_prompt}) is enabled.
Expand Down Expand Up @@ -387,7 +396,7 @@ public AiGenerationConfig() {
private float minP = DEFAULT_MIN_P;
private float topNSigma = DEFAULT_TOP_N_SIGMA;
private float repeatPenalty = DEFAULT_REPEAT_PENALTY;
private boolean chatTemplateEnableThinking = DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING;
private @Nullable Boolean chatTemplateEnableThinking = DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING;
private boolean cachePrompt = DEFAULT_CACHE_PROMPT;
private boolean swaFull = DEFAULT_SWA_FULL;
private int cacheReuse = DEFAULT_CACHE_REUSE;
Expand Down Expand Up @@ -655,20 +664,26 @@ public void setTopNSigma(final float topNSigma) {
}

/**
* Returns whether the model's chat-template thinking mode is enabled.
* Returns the configured chat-template thinking mode, or {@code null} when it was never set.
*
* @return {@code true} if chat-template thinking mode is enabled
* @return {@code true} or {@code false} when the knob was set, {@code null} when it is unset
* &#x2014; in which case the {@code enable_thinking} kwarg is not sent at all and the
* model's own chat-template default applies
* @see #DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING
*/
public boolean isChatTemplateEnableThinking() {
public @Nullable Boolean getChatTemplateEnableThinking() {
return chatTemplateEnableThinking;
}

/**
* Sets whether the model's chat-template thinking mode is enabled.
* Sets the model's chat-template thinking mode.
*
* @param chatTemplateEnableThinking {@code true} to enable chat-template thinking mode
* @param chatTemplateEnableThinking {@code true} or {@code false} to forward
* {@code enable_thinking} to the chat template, {@code null} to omit the kwarg entirely
* and leave the template's own default in place
* @see #DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING
*/
public void setChatTemplateEnableThinking(final boolean chatTemplateEnableThinking) {
public void setChatTemplateEnableThinking(final @Nullable Boolean chatTemplateEnableThinking) {
this.chatTemplateEnableThinking = chatTemplateEnableThinking;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public AiModelDefinition() {
private float minP = AiGenerationConfig.DEFAULT_MIN_P;
private float topNSigma = AiGenerationConfig.DEFAULT_TOP_N_SIGMA;
private float repeatPenalty = AiGenerationConfig.DEFAULT_REPEAT_PENALTY;
private boolean chatTemplateEnableThinking = AiGenerationConfig.DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING;
private @Nullable Boolean chatTemplateEnableThinking =
AiGenerationConfig.DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING;
private boolean cachePrompt = AiGenerationConfig.DEFAULT_CACHE_PROMPT;
private boolean swaFull = AiGenerationConfig.DEFAULT_SWA_FULL;
private int cacheReuse = AiGenerationConfig.DEFAULT_CACHE_REUSE;
Expand Down Expand Up @@ -324,24 +325,26 @@ public void setTopNSigma(final float topNSigma) {
}

/**
* Returns whether the model's chat-template thinking mode is enabled.
* Returns this definition's chat-template thinking mode, or {@code null} when it was never set.
*
* @return {@code true} to keep thinking enabled via the model's chat-template default;
* @return {@code true} or {@code false} when the knob was set, {@code null} when it is unset
* &#x2014; the kwarg is then not sent and the model's own chat-template default applies;
* defaults to {@link AiGenerationConfig#DEFAULT_CHAT_TEMPLATE_ENABLE_THINKING}
*/
public boolean isChatTemplateEnableThinking() {
public @Nullable Boolean getChatTemplateEnableThinking() {
return chatTemplateEnableThinking;
}

/**
* Sets whether the model's chat-template thinking mode is enabled.
* Sets this definition's chat-template thinking mode.
*
* @param chatTemplateEnableThinking {@code false} passes
* {@code enable_thinking=false} to
* {@link net.ladenthin.llama.parameters.ModelParameters#setChatTemplateKwargs} to suppress
* chain-of-thought reasoning at the Jinja template level
* chain-of-thought reasoning at the Jinja template level, {@code true} passes it as
* {@code true}, and {@code null} omits the kwarg entirely
*/
public void setChatTemplateEnableThinking(final boolean chatTemplateEnableThinking) {
public void setChatTemplateEnableThinking(final @Nullable Boolean chatTemplateEnableThinking) {
this.chatTemplateEnableThinking = chatTemplateEnableThinking;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private static AiGenerationConfig toConfig(final AiModelDefinition definition) {
config.setTopNSigma(definition.getTopNSigma());
config.setRepeatPenalty(definition.getRepeatPenalty());
config.setStopStrings(definition.getStopStrings());
config.setChatTemplateEnableThinking(definition.isChatTemplateEnableThinking());
config.setChatTemplateEnableThinking(definition.getChatTemplateEnableThinking());
config.setCachePrompt(definition.isCachePrompt());
config.setSwaFull(definition.isSwaFull());
config.setCacheReuse(definition.getCacheReuse());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,43 @@ public LlamaCppJniAiGenerationProvider(final LlamaCppJniConfig config, final AiP
this.promptSupport = Objects.requireNonNull(promptSupport, "promptSupport");
}

/**
* Builds the chat-template kwargs passed to
* {@link net.ladenthin.llama.parameters.ModelParameters#setChatTemplateKwargs}.
*
* <p>Both kwargs are opt-in: an entry is written only when the user actually configured it, so a
* chat template that has never heard of the kwarg is not handed it. That matters because
* llama.cpp's Jinja layer has been moving unknown kwargs from "silently ignored" toward "warned
* about" -- while {@code enable_thinking} was a plain {@code boolean} defaulting to {@code true},
* every run sent it, and the only way to stop the noise was to set the knob to {@code false},
* which means something else entirely.</p>
*
* <p>Package-private so it can be pinned without loading a GGUF; {@link #model()} is the only
* production caller.</p>
*
* @return the kwargs to send; empty when neither knob is configured
*/
Map<String, String> buildChatTemplateKwargs() {
final Map<String, String> chatTemplateKwargs =
new HashMap<>(compatibilityHelper.hashMapCapacityFor(CHAT_TEMPLATE_KWARG_COUNT));
// Qwen-style thinking. Unset (null) omits the kwarg so the model's own template default applies.
final Boolean enableThinking = config.chatTemplateEnableThinking();
if (enableThinking != null) {
chatTemplateKwargs.put(ENABLE_THINKING_KWARG, String.valueOf(enableThinking.booleanValue()));
}
// gpt-oss honors reasoning_effort; non-gpt-oss chat templates ignore it. An empty
// configured value omits the kwarg so the model's own template default applies.
if (!compatibilityHelper.isBlank(config.reasoningEffort())) {
chatTemplateKwargs.put(REASONING_EFFORT_KWARG, config.reasoningEffort());
}
return chatTemplateKwargs;
}

/** Loads the GGUF model on first use and caches it for subsequent calls. */
private LlamaModel model() {
LlamaModel current = model;
if (current == null) {
final Map<String, String> chatTemplateKwargs =
new HashMap<>(compatibilityHelper.hashMapCapacityFor(CHAT_TEMPLATE_KWARG_COUNT));
chatTemplateKwargs.put(ENABLE_THINKING_KWARG, String.valueOf(config.chatTemplateEnableThinking()));
// gpt-oss honors reasoning_effort; non-gpt-oss chat templates ignore it. An empty
// configured value omits the kwarg so the model's own template default applies.
if (!compatibilityHelper.isBlank(config.reasoningEffort())) {
chatTemplateKwargs.put(REASONING_EFFORT_KWARG, config.reasoningEffort());
}
final Map<String, String> chatTemplateKwargs = buildChatTemplateKwargs();
final ModelParameters modelParameters = new ModelParameters()
.setModel(config.modelPath())
.setCtxSize(config.contextSize())
Expand Down
Loading
Loading