Actual
Custom backend → reasoning ON → effort menu
- Off
- Minimal
- Low
- Medium
- High
- Extra high
- Maximum
- Ultra
Picking a noise value sends that literal reasoning_effort to
https://api.deepseek.com.
OpenGUI · Model access
The empty-array guard in connectionsToModelProviders stops one overwrite. It
does not make the menu correct for custom DeepSeek backends. The real bug is split
ownership: the editor cannot express intent, the catalog matcher unions reseller noise,
and the selector falls back to a fantasy scale.
Custom backend → reasoning ON → effort menu
Picking a noise value sends that literal reasoning_effort to
https://api.deepseek.com.
Canonical models.dev · deepseek/deepseek-v4-pro
Catalog publishes toggle + effort [high, max]. Nothing else is
advertised for the first-party DeepSeek entry.
The patch only changes merge order when connection capabilities carry an empty list:
// current band-aid
...(caps.reasoningEfforts?.length
? { reasoningEfforts: caps.reasoningEfforts }
: {}),
That unblocks catalog-derived values for the common “editor saved
[]” path. Three independent defects remain, any one of which still yields a
wrong menu.
reasoningMetadataForModel matches
key.endsWith("/" + modelId) and folds all hits into one
Set. Live models.dev currently has 51 entries ending
in deepseek-v4-pro. Effort signatures diverge wildly:
| Count | Published options |
|---|---|
| 10 | [] / bare reasoning |
| 8 | toggle + [high, max] ← canonical shape |
| 6 | toggle only |
| 3+ | includes low/medium/xhigh, full OpenAI-ish ladders |
| union | almost the entire Host scale — including values no DeepSeek first-party entry publishes |
After the empty-array guard, the menu can still show Minimal / Low / Medium / Extra high because those values appear on some reseller row. Ultra may disappear (not in the union today) while the ladder remains wrong.
CustomBackendEditor has a Reasoning toggle and no efforts control.
buildCustomModelConnection always writes
reasoningEfforts: [] when reasoning is on. Downstream code cannot
distinguish:
First-party presets (Codex) correctly persist non-empty lists. Custom backends never can, so they always depend on inference — and inference is broken (A).
ReasoningEffortSelector treats missing/empty efforts as “show the
full Host enum”:
const efforts = model?.reasoningEfforts?.length
? model.reasoningEfforts
: [...EFFORTS]; // none…ultra
That is how the original screenshot got Ultra even though no models.dev entry for this model publishes it. Fallback must be conservative, not maximal — or the menu must refuse to open without resolved metadata.
Enrichment lives only in the Frontend (connectionsToModelProviders).
Host resolve still forwards
connection.modelCapabilities[modelId].reasoningEfforts as stored —
often [] / omitted. The wire payload uses the
selected effort from the Session/PromptBox, not a Host-side clamp against
catalog. So a polluted UI can ship unsupported literals even when transport never
saw models.dev.
Pick one source of truth per field, with a single merge function and no silent maximal fallbacks.
Durable user / preset intent
reasoning: boolean — user said this model reasonsreasoningEfforts?: ReasoningEffort[] —
only when explicit
[] as a fake explicit list
Advisory inference
What UI and clamp both read
resolveModelCapabilities(connection, modelId, catalog)
resolved.reasoningEffortsreasoning === false → no selector; no efforts.["none","high"] (toggle-shaped), never the full Host scale.
models.dev is useful and noisy. Treat it as a ranked lookup, not a bag union.
*/modelId reseller row// target shape (illustrative)
function pickCatalogModel(catalog, modelId, hints?: { baseUrl?: string; providerHint?: string }) {
const candidates = entriesEndingWith(catalog, modelId);
if (candidates.length === 0) return null;
const ranked = rankCandidates(candidates, hints); // vendor URL, provider id, option richness
return ranked[0]; // ONE entry — do not fold options across the set
}
function effortsFromEntry(entry): ReasoningEffort[] | undefined {
// toggle → include "none"; effort.values filtered to SUPPORTED_EFFORTS
// no effort values but reasoning → ["none","high"]
// reasoning false → undefined
}
| Stored | Meaning | Resolved efforts |
|---|---|---|
reasoning: false |
Not a reasoning model | hidden / none |
reasoning: true, field omitted |
Auto | catalog → conservative default |
reasoning: true, ["high","max"] |
Explicit override / preset | exactly that list (+ none if product wants Off) |
reasoningEfforts: [] |
Illegal on write. Normalize away in
buildCustomModelConnection and Host ingest.
|
|
Move pure resolution out of “frontend fetch helper” identity. Suggested home:
packages/protocol or a small shared lib both Frontend and Host import.
loadModelsDevCatalog() may stay environment-specific (fetch vs cache
file), but resolveModelCapabilities must be pure and unit-tested without
network.
connectionsToModelProviders becomes a thin adapter over the
shared resolver.
resolved.reasoningEfforts. Unknown effort → nearest supported or reject.
[]// stop inventing a menu
if (model.capabilities.reasoning === false) return null;
const efforts = model.reasoningEfforts; // already resolved
if (!efforts?.length) return null; // or show Off-only — never full EFFORTS
The full EFFORTS constant remains the allow-list for parsing, not
the default menu.
Small static map is enough for v1; do not scrape HTML.
const VENDOR_HOST_HINTS: Record<string, string> = {
"api.deepseek.com": "deepseek",
"api.openai.com": "openai",
"api.anthropic.com": "anthropic",
"api.x.ai": "xai",
// ...
};
Custom OpenAI-compatible proxies that re-host DeepSeek still get model-id matching; the hint only breaks ties toward the vendor when the URL is first-party.
reasoningEfforts: [] from custom backend builder["none","high"]Exit: DeepSeek custom backend menu is Off / High / Maximum against live or fixture catalog.
resolveModelCapabilities to shared packageExit: UI cannot select an effort Host would not accept; transport never sees unsupported literals from PromptBox.
Exit: Power users can pin an explicit ladder; Auto remains default and correct.
budget_tokens options in catalog)| Area | Files | Change |
|---|---|---|
| Catalog resolve | src/lib/models-dev.ts → shared module |
Single-entry pick, hints, no union |
| Custom backend write | src/features/model-access/custom-backend.ts |
Omit empty efforts; optional mode later |
| Selector | src/components/ReasoningEffortSelector.tsx |
No maximal fallback |
| Host ingest | packages/backend/src/routes/host-product.ts |
Normalize empty → omit |
| Host clamp | packages/backend/src/host/opengui-host.ts |
Resolve + clamp on set/send (P1) |
| Tests | src/lib/__tests__/models-dev.test.ts + shared package tests |
Reseller pollution, deepseek, omit-empty, presets |
Do not ship “empty array no longer overwrites” as the fix. Ship Auto that picks one catalog entry, never persist empty lists, and never invent the full effort ladder in the selector. Host clamp is the hardening layer so UI bugs cannot poison the wire.
The DeepSeek repro is the acceptance needle: Off · High · Maximum — nothing else.