fix: thinking controls offer unsupported provider-native efforts (#145248)

Related: #144866, #90703

## What Problem This Solves

Fixes thinking controls offering and accepting advanced levels from provider-native labels such as `MAX` or `XHIGH`, even though the actual model request silently uses a lower level.

## User Impact

The model picker, session validation, and execution now agree on which advanced levels a custom provider supports. Lowercase canonical efforts still work, and explicit mappings such as `max: "MAX"` preserve the provider's exact wire value. Unsupported selections fail without changing the session or its runtime/account selection.

## Why This Change Was Made

The shared thinking profile incorrectly applied user-input aliases to provider-native capability labels. It now reads those labels exactly, preserving the [existing explicit-mapping policy](https://github.com/openclaw/openclaw/pull/102993#issuecomment-4931608378). Provider-owned profiles, explicit maps, and null opt-outs are unchanged; no new aliases or transport policy are introduced.

The earlier lowercase capability-admission bug is already fixed on main. This update addresses the remaining inconsistent advertisement rather than restoring the older transport changes.

## Evidence

A real isolated Gateway and secretless loopback HTTP provider exercised `models.list → sessions.patch → chat.send → chat.history`:

| Case | Before | After |
| --- | --- | --- |
| Unmapped native `MAX` / `XHIGH` | Advertised and accepted `max` / `xhigh`; actual wire used `high` | Not advertised; unsupported selection rejected; accepted `high` sends `high` |
| Canonical lowercase `max` | Session and wire `max` | Unchanged |
| Explicit `max → MAX` mapping | Session `max`, wire `MAX` | Unchanged |
| Explicit null opt-out | Selection rejected | Unchanged |

Rejected candidate selections left the complete persisted session row unchanged. A saved synthetic account selected through `/model`, its explicit OpenClaw runtime, and model identity survived both rejection and a subsequent real HTTP turn. A copied pre-fix session now displays its effective supported level instead of the unavailable Maximum choice.

- [Inspected before/after screenshots from the real Gateway UI](https://github.com/openclaw/openclaw/pull/145248#issuecomment-5740680770).
- New regression fails on the original shared profile; 137 focused policy/admission/wire-contract tests pass after the fix.
- Focused formatting and diff checks pass. Full static and matrix checks remain in hosted CI.
- Local provider responses were synthetic; no external provider or live-account behavior is claimed.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
MasterSwords1 2026-09-19 10:42:11 +01:00 • committed by GitHub
parent ca2e2cc268
commit f04209e2ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 30 additions and 2 deletions

View file

@ -708,6 +708,35 @@ describe("listThinkingLevels", () => {
).toBe(true);
});
it("does not treat provider-native effort labels as user thinking aliases", () => {
const catalog = [
{
provider: "custom",
id: "native-efforts",
api: "openai-completions",
reasoning: true,
compat: { supportedReasoningEfforts: ["high", "XHIGH", "MAX", "extra-high", "auto"] },
},
];
expect(listThinkingLevels("custom", "native-efforts", catalog, "openclaw")).toEqual([
"off",
"minimal",
"low",
"medium",
"high",
]);
expect(
isThinkingLevelSupported({
provider: "custom",
model: "native-efforts",
level: "max",
catalog,
agentRuntime: "openclaw",
}),
).toBe(false);
});
it("uses advanced catalog efforts and derives OpenClaw Ultra from Max", () => {
const catalog = [
{

View file

@ -240,8 +240,7 @@ function appendCatalogAdvancedThinkingLevels(
}
}
let supportsMax = profile.levels.some(({ id }) => id === "max");
for (const effort of compat?.supportedReasoningEfforts ?? []) {
const level = normalizeThinkLevel(effort);
for (const level of compat?.supportedReasoningEfforts ?? []) {
if (
level === "ultra" ||
((level === "adaptive" || level === "xhigh" || level === "max") &&