Skip to content

fix(providers): resolve reasoning through the catalog; add appearance preferences - #161

Merged
Zongwei9888 merged 3 commits into
mainfrom
fix/reasoning-capability-catalog
Aug 6, 2026
Merged

fix(providers): resolve reasoning through the catalog; add appearance preferences#161
Zongwei9888 merged 3 commits into
mainfrom
fix/reasoning-capability-catalog

Conversation

@Zongwei9888

Copy link
Copy Markdown
Collaborator

Description

Addresses #153, #151, #152 and #155.

#153 reports that DeepSeek's thinking selector has no levels. That turned out to be one symptom of a resolver that had rotted, and chasing it surfaced two more gaps. The Desktop half of the other three reports is a separate, self-contained change in the same PR.

#153 — reasoning capabilities resolved through the model catalog

infer_reasoning_capabilities() was a chain of substring checks kept apart from the model catalog. It recognised claude-sonnet-4-6 while the catalog had moved on to claude-sonnet-5, so on main today all of these advertise no reasoning at all:

claude-sonnet-5   claude-opus-4-8   claude-sonnet-4-5   claude-haiku-4-5
deepseek-r1       deepseek-reasoner  deepseek-chat
qwen3-max         grok-4

The Desktop picker builds its options from model.reasoning, so an empty capability renders a control with nothing in it — exactly what #153 describes. The provider side was never at fault: deepseek already carried thinking_style="thinking_type", so the wire format had been wired all along.

Both catalog.py and model_compat.py open by naming model_id.includes(...) as the anti-pattern to avoid, and catalog.py already resolves every other per-model fact — context window, output cap, pricing — through a normalize → snapshot → seed → family → default cascade. Reasoning now joins it as a ModelInfo field. profiles.py had been calling resolve_model_info() and the name chain on adjacent lines for the same model.

Consequences beyond the report:

  • An unseen deepseek-r2 or gpt-5.9 inherits its family row instead of silently losing its controls. Shipping a model is a table row, not a code edit.
  • Family fallback deliberately advertises less than an exact seed row: claude-sonnet-4-<date> keeps the effort ladder but not summarised thinking, which it predates. Claiming it would route raw trace text through the summary channel.
  • Dotted aliases (claude-sonnet-4.6) are listed rather than normalised away — the dot is load-bearing elsewhere in the table, where gpt-5.4 and kimi-k2.5 are distinct models.

Two gaps this exposed

GLM and MiniMax were resolving to the 128K default — a 200K model trimmed to 128K, and a thinking-capable one advertising nothing. Limits taken from the vendors, not guessed: GLM-4.6 is 200K/128K (docs.z.ai); the MiniMax M2 line is 204,800 with M3 at 1M (platform.minimax.io).

Zhipu never declared a thinking_style. GLM enables reasoning with thinking: {"type": "enabled"} — the same body DeepSeek takes, and the shape _THINKING_STYLE_BUILDERS["thinking_type"] already builds. Every GLM request had been going out with reasoning silently omitted whatever effort the user picked. One field; the mechanism was already there.

What this does not fix

DeepSeek still shows Auto/Off rather than low/medium/high, because its API is a binary switch — there are no named levels to expose, and inventing three would put dead options in the picker. deepseek-reasoner has no user-facing choice at all: reasoning is intrinsic and cannot be turned off.

The dramatic change is elsewhere: Claude's current line-up goes from a single "Auto" back to the full six-option ladder.

#151 / #152 / #155 — appearance preferences

One mechanism, not three. desktop/src/app/appearance.ts holds the settings as a table; each row owns its label, range, sanitiser and CSS custom property, and the settings UI renders from the same table.

Testing

  • Python: 1189 passed, 3 skipped
  • Desktop: 138 passed across 23 files; tsc --noEmit, eslint and npm run build all clean
  • Every new suite verified in reverse — reverting the fix fails the tests (13/23 for the catalog move, 4/6 for the Zhipu wire, and the palette-drift test catches a single changed value)

Not verified: the Desktop changes have not been seen running in Tauri. Unit tests and the build cover syntax and logic, not appearance. Worth a look at the dark toggle and a narrow conversation width before release.

tests/application/test_automation_goal_runs.py::test_legacy_unreserved_turn_is_never_adopted_as_automation_initial_turn fails intermittently — measured at 2/20 on clean main, unrelated to this work, and deserving its own issue.

Known gaps, deliberately left

thinking_style is applied per provider without consulting the model, so gpt-4o routed through a thinking-capable gateway still receives a thinking body. Pre-existing, but this PR widens it by giving Zhipu a style. Both halves of the information now exist in one place — the catalog knows the model, the spec knows the wire — so intersecting them is a focused follow-up rather than more surface here.

Zongwei9888 and others added 3 commits August 6, 2026 19:57
Reported in #153 as "DeepSeek shows no thinking levels". The cause is wider
than DeepSeek: infer_reasoning_capabilities() was a chain of substring checks
kept apart from the catalog, and it had rotted. It recognised
claude-sonnet-4-6 while the catalog had moved on to claude-sonnet-5, so on
main today these all advertise no reasoning at all:

  claude-sonnet-5, claude-opus-4-8, claude-sonnet-4-5, claude-haiku-4-5,
  deepseek-r1, deepseek-reasoner, deepseek-chat, qwen3-max, grok-4

The Desktop effort picker renders reasoningOptions(model), so an empty
capability means the control appears with nothing in it — exactly what #153
describes. The provider side was never the problem: deepseek already carries
thinking_style="thinking_type", so the wire format was wired all along.

Both catalog.py and model_compat.py open by naming `model_id.includes(...)`
as the anti-pattern to avoid, and catalog.py already resolves every other
per-model fact — context window, output cap, pricing — through a
normalize → snapshot → seed → family → default cascade. Reasoning now joins
it as a ModelInfo field, so one id yields one answer from one lookup.
profiles.py had been calling resolve_model_info() and the name chain on
adjacent lines for the same model.

Consequences beyond the reported bug:

* An unseen deepseek-r2 or gpt-5.9 inherits its family row instead of
  silently losing its controls. Shipping a model no longer needs a code edit.
* Family fallback deliberately advertises less than an exact seed row:
  claude-sonnet-4-<date> keeps the effort ladder but not summarised
  thinking, which it predates. Claiming it would route raw trace text
  through the summary channel.
* Dotted aliases (claude-sonnet-4.6) are listed rather than normalised,
  because the dot is load-bearing elsewhere in the table — gpt-5.4 and
  kimi-k2.5 are distinct models, not separator noise.

A ladder is only claimed where the vendor publishes named levels; the rest
get the Auto/Off surface ModelReasoningCapabilities already documents for
"reasoning exists, levels unpublished". GLM and MiniMax stay absent: they
need verified context/output limits before they earn a seed row, and that is
now a table edit rather than a branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the catalog move, closing the two real gaps it exposed. Both
lines were resolving to the 128K default with no reasoning at all — a 200K
model trimmed to 128K, and a thinking-capable one advertising nothing.

Limits taken from the vendors, not inferred:

* GLM-4.6 — 200K in / 128K out (docs.z.ai/guides/llm/glm-4.6)
* MiniMax M2 line — 204,800 total; M3 is the 1M tier
  (platform.minimax.io/docs/api-reference/api-overview). The published figure
  is input+output combined, which is how this table already reads it.

Family rules follow, longest prefix first so ``minimax-m3`` keeps its 1M
window while the rest of the line takes 200K. ``glm-4.7`` and ``glm-5.2``
inherit rather than falling off the table.

Separately: Zhipu enables reasoning with ``thinking: {"type": "enabled"}``,
the same body DeepSeek takes and the shape
``_THINKING_STYLE_BUILDERS["thinking_type"]`` already builds. The spec never
declared it, so every GLM request went out with reasoning silently omitted
whatever effort the user chose. One field; the mechanism was already there.

MiniMax stays on the Auto/Off surface — it publishes no named effort ladder,
and inventing one would put dead options in the picker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the display half of #151, #152 and #155. They are one problem wearing
three hats — a value the user picks, persisted locally, surfaced to CSS — so
they share one mechanism rather than three copies of read/validate/persist.

src/app/appearance.ts holds the settings as a table. Each row owns its label,
its range, its sanitiser and the custom property it feeds; the machinery
around it is generic. Adding a preference is a row, and the settings UI
renders from the same table instead of growing another block of JSX.

Notes on the three:

* Width (#151) — .conversation, its composer and the goal rail each hardcoded
  `min(820px, 100%)`. They now read one `--conversation-width`, which is the
  point: three copies of a layout constant cannot stay in step by hand. 100%
  restores the built-in cap rather than stretching edge to edge, so the
  default is byte-identical to today.

* Theme (#152) — the report says there is no dark theme. There is: tokens.css
  has carried a full dark palette behind `prefers-color-scheme` all along.
  What was missing is overriding the OS, which is what this adds. CSS cannot
  share a declaration block between a media query and a selector, so the dark
  palette is now written twice; styles/tokens.test.ts fails if the copies ever
  disagree, because two tables drifting apart is the exact bug this project
  just spent a day fixing in its model catalog. VS Code theme import is not
  here — mapping a foreign schema onto these tokens is its own piece of work.

* Typography (#155) — a free-text family list rather than a dropdown. A
  curated menu would be a hardcoded guess about fonts the user's machine may
  not have; the value is a prefix of the built-in stack, so anything missing
  falls through instead of leaving the UI unstyled. Size is a slider.

Preferences apply immediately and live in localStorage: per-machine display
choices, not project configuration. The store is read through
useSyncExternalStore so the shell and the settings page share one copy —
per-component state would let them disagree, and threading a controller down
through the workspace would be prop drilling for something neither server
state nor scoped to a subtree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Zongwei9888
Zongwei9888 merged commit f3e1d55 into main Aug 6, 2026
14 checks passed
@Zongwei9888
Zongwei9888 deleted the fix/reasoning-capability-catalog branch August 6, 2026 16:39
pull Bot pushed a commit to ari1988/DeepCode that referenced this pull request Aug 6, 2026
`ProviderSpec.thinking_style` describes the endpoint's dialect — how this API
spells a thinking toggle. It says nothing about whether the model behind it
has one, and the two were never intersected, so any model routed through a
DeepSeek- or Zhipu-compatible endpoint received:

    thinking: {"type": "enabled"}

including gpt-4o and llama3, which have no thinking mode at all. The empty
`reasoning_content` echo rode along with it.

Pre-existing, but HKUDS#161 widened it by giving Zhipu a style, which is why this
is not waiting for a larger cleanup. Both halves of the answer now live in one
place: the catalog knows whether the model thinks, the spec knows how the
endpoint writes it. `model_supports_thinking()` asks the first, and the style
only applies when both agree.

Deliberately not narrowed to a vendor. A capable model reached through an
endpoint that speaks a given dialect should get that dialect — gpt-5.4 behind
a DeepSeek-compatible gateway still gets the DeepSeek body, because that is
the endpoint's contract, not a claim about who made the model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to ari1988/DeepCode that referenced this pull request Aug 6, 2026
HKUDS#155 asked for a font dropdown. HKUDS#161 shipped free text instead, on the
grounds that a fixed menu is a hardcoded guess about the user's machine. The
premise was right and the conclusion was not: the browser can be asked.

`document.fonts.check()` answers whether a family resolves here, so the
candidate list is filtered to what is actually installed and grouped by
Interface / Monospace / CJK. Picking one appends it to the field rather than
replacing it, which keeps the CJK case HKUDS#155 raises working — put the CJK face
alongside the Latin one and mixed text renders through a single fallback
chain.

The list stays advisory. Free text remains the source of truth, so a family
outside the candidates is still reachable, and the candidates themselves cost
nothing when absent because they are filtered out. Where the Font Loading API
is missing the picker hides rather than claiming every family exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant