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
221 changes: 191 additions & 30 deletions core/providers/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
from pathlib import Path
from typing import Any

from core.providers.reasoning import ModelReasoningCapabilities


@dataclass(frozen=True, slots=True)
class ModelInfo:
Expand All @@ -46,6 +48,12 @@ class ModelInfo:
unpriced model). ``source`` records where the value came from — ``seed``,
``family:<prefix>``, ``default``, or ``snapshot`` — so a surprising
compaction budget is traceable to its origin.

``reasoning`` describes the thinking controls the model advertises, or
``None`` for a model with no reasoning mode. It lives here rather than in
a separate resolver so that one model id yields one answer through one
cascade: a family rule that grants ``deepseek-r2`` the right context
window grants it the right effort levels in the same step.
"""

id: str
Expand All @@ -54,7 +62,58 @@ class ModelInfo:
input_cost_per_1m: float | None = None
output_cost_per_1m: float | None = None
source: str = "seed"
reasoning: ModelReasoningCapabilities | None = None


# --------------------------------------------------------------------------
# Reasoning profiles shared by the seed rows below.
#
# Naming them keeps the table scannable and makes "these models expose the
# same controls" an explicit statement instead of repeated literals. A ladder
# is only claimed where the vendor publishes named effort levels; everything
# else gets the honest Auto/Off surface that ``ModelReasoningCapabilities``
# documents for "reasoning exists, levels unpublished".
# --------------------------------------------------------------------------

#: OpenAI ``reasoning_effort`` — gpt-5 family and the o-series.
_REASONING_OPENAI = ModelReasoningCapabilities(
supported_efforts=("minimal", "low", "medium", "high"),
default_effort="medium",
default_enabled=True,
supports_summary=True,
)
#: Anthropic extended thinking, mapped onto DeepCode's semantic ladder.
_REASONING_ANTHROPIC = ModelReasoningCapabilities(
supported_efforts=("low", "medium", "high", "max"),
default_effort="high",
default_enabled=True,
supports_summary=True,
)
#: Same ladder, but the model streams raw thinking blocks rather than the
#: summarised channel. Used for the family fallback: an unrecognised
#: ``claude-sonnet-4-<date>`` predates summarisation, and claiming it would
#: route real trace text through the summary channel.
_REASONING_ANTHROPIC_TRACE = ModelReasoningCapabilities(
supported_efforts=_REASONING_ANTHROPIC.supported_efforts,
default_effort=_REASONING_ANTHROPIC.default_effort,
default_enabled=True,
supports_summary=False,
)
#: Moonshot Kimi K3 publishes three levels.
_REASONING_KIMI_K3 = ModelReasoningCapabilities(
supported_efforts=("low", "high", "max"),
default_effort="max",
default_enabled=True,
supports_summary=True,
)
#: A binary thinking switch with no published ladder (``thinking_style`` on
#: the ProviderSpec is the wire mechanism). Product surface: Auto / Off.
_REASONING_TOGGLE = ModelReasoningCapabilities(default_enabled=True)
#: Reasoning is intrinsic and cannot be switched off (DeepSeek R, Grok 4).
_REASONING_ALWAYS_ON = ModelReasoningCapabilities(
default_enabled=True,
mandatory=True,
)

# --------------------------------------------------------------------------
# Seed catalog — exact ids DeepCode targets, curated from models.dev.
Expand All @@ -66,44 +125,130 @@ class ModelInfo:
# --------------------------------------------------------------------------
_SEED: dict[str, ModelInfo] = {
# OpenAI GPT-5 family (400K context / 128K output).
"gpt-5": ModelInfo("gpt-5", 400_000, 128_000, 1.25, 10.0),
"gpt-5-mini": ModelInfo("gpt-5-mini", 400_000, 128_000, 0.25, 2.0),
"gpt-5-nano": ModelInfo("gpt-5-nano", 400_000, 128_000, 0.05, 0.40),
"gpt-5.1": ModelInfo("gpt-5.1", 400_000, 128_000, 1.25, 10.0),
"gpt-5.2": ModelInfo("gpt-5.2", 400_000, 128_000, 1.25, 10.0),
"gpt-5.4": ModelInfo("gpt-5.4", 400_000, 128_000, 1.25, 10.0),
"gpt-5": ModelInfo(
"gpt-5", 400_000, 128_000, 1.25, 10.0, reasoning=_REASONING_OPENAI
),
"gpt-5-mini": ModelInfo(
"gpt-5-mini", 400_000, 128_000, 0.25, 2.0, reasoning=_REASONING_OPENAI
),
"gpt-5-nano": ModelInfo(
"gpt-5-nano", 400_000, 128_000, 0.05, 0.40, reasoning=_REASONING_OPENAI
),
"gpt-5.1": ModelInfo(
"gpt-5.1", 400_000, 128_000, 1.25, 10.0, reasoning=_REASONING_OPENAI
),
"gpt-5.2": ModelInfo(
"gpt-5.2", 400_000, 128_000, 1.25, 10.0, reasoning=_REASONING_OPENAI
),
"gpt-5.4": ModelInfo(
"gpt-5.4", 400_000, 128_000, 1.25, 10.0, reasoning=_REASONING_OPENAI
),
# OpenAI reasoning o-series (200K context / 100K output).
"o1": ModelInfo("o1", 200_000, 100_000, 15.0, 60.0),
"o3": ModelInfo("o3", 200_000, 100_000, 2.0, 8.0),
"o3-mini": ModelInfo("o3-mini", 200_000, 100_000, 1.1, 4.4),
"o4-mini": ModelInfo("o4-mini", 200_000, 100_000, 1.1, 4.4),
"o1": ModelInfo("o1", 200_000, 100_000, 15.0, 60.0, reasoning=_REASONING_OPENAI),
"o3": ModelInfo("o3", 200_000, 100_000, 2.0, 8.0, reasoning=_REASONING_OPENAI),
"o3-mini": ModelInfo(
"o3-mini", 200_000, 100_000, 1.1, 4.4, reasoning=_REASONING_OPENAI
),
"o4-mini": ModelInfo(
"o4-mini", 200_000, 100_000, 1.1, 4.4, reasoning=_REASONING_OPENAI
),
# OpenAI GPT-4 family.
"gpt-4o": ModelInfo("gpt-4o", 128_000, 16_384, 2.5, 10.0),
"gpt-4o-mini": ModelInfo("gpt-4o-mini", 128_000, 16_384, 0.15, 0.60),
"gpt-4.1": ModelInfo("gpt-4.1", 1_047_576, 32_768, 2.0, 8.0),
# Anthropic Claude (200K context; output varies by tier).
"claude-opus-4-1": ModelInfo("claude-opus-4-1", 200_000, 32_000, 15.0, 75.0),
"claude-opus-4-8": ModelInfo("claude-opus-4-8", 200_000, 32_000, 15.0, 75.0),
"claude-sonnet-4-5": ModelInfo("claude-sonnet-4-5", 200_000, 64_000, 3.0, 15.0),
"claude-sonnet-5": ModelInfo("claude-sonnet-5", 200_000, 64_000, 3.0, 15.0),
"claude-haiku-4-5": ModelInfo("claude-haiku-4-5", 200_000, 64_000, 1.0, 5.0),
"claude-opus-4-1": ModelInfo(
"claude-opus-4-1", 200_000, 32_000, 15.0, 75.0, reasoning=_REASONING_ANTHROPIC
),
"claude-opus-4-6": ModelInfo(
"claude-opus-4-6", 200_000, 32_000, 15.0, 75.0, reasoning=_REASONING_ANTHROPIC
),
"claude-opus-4-8": ModelInfo(
"claude-opus-4-8", 200_000, 32_000, 15.0, 75.0, reasoning=_REASONING_ANTHROPIC
),
"claude-sonnet-4-5": ModelInfo(
"claude-sonnet-4-5", 200_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ANTHROPIC
),
"claude-sonnet-4-6": ModelInfo(
"claude-sonnet-4-6", 200_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ANTHROPIC
),
"claude-sonnet-5": ModelInfo(
"claude-sonnet-5", 200_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ANTHROPIC
),
"claude-haiku-4-5": ModelInfo(
"claude-haiku-4-5", 200_000, 64_000, 1.0, 5.0, reasoning=_REASONING_ANTHROPIC
),
# Dotted spellings of the same Anthropic releases. Listed rather than
# normalised away, because the dot is meaningful elsewhere in this table
# (``gpt-5.4``, ``kimi-k2.5`` are distinct models, not separator noise).
"claude-opus-4.6": ModelInfo(
"claude-opus-4.6", 200_000, 32_000, 15.0, 75.0, reasoning=_REASONING_ANTHROPIC
),
"claude-sonnet-4.6": ModelInfo(
"claude-sonnet-4.6", 200_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ANTHROPIC
),
"claude-sonnet-4.5": ModelInfo(
"claude-sonnet-4.5", 200_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ANTHROPIC
),
"claude-haiku-4.5": ModelInfo(
"claude-haiku-4.5", 200_000, 64_000, 1.0, 5.0, reasoning=_REASONING_ANTHROPIC
),
# Google Gemini (very large windows).
"gemini-2.5-pro": ModelInfo("gemini-2.5-pro", 1_048_576, 65_536, 1.25, 10.0),
"gemini-2.5-flash": ModelInfo("gemini-2.5-flash", 1_048_576, 65_536, 0.30, 2.5),
"gemini-3-pro": ModelInfo("gemini-3-pro", 1_048_576, 65_536, 1.25, 10.0),
"gemini-2.5-pro": ModelInfo(
"gemini-2.5-pro", 1_048_576, 65_536, 1.25, 10.0, reasoning=_REASONING_TOGGLE
),
"gemini-2.5-flash": ModelInfo(
"gemini-2.5-flash", 1_048_576, 65_536, 0.30, 2.5, reasoning=_REASONING_TOGGLE
),
"gemini-3-pro": ModelInfo(
"gemini-3-pro", 1_048_576, 65_536, 1.25, 10.0, reasoning=_REASONING_TOGGLE
),
# Moonshot Kimi.
"kimi-k2": ModelInfo("kimi-k2", 256_000, 128_000, 0.60, 2.5),
"kimi-k2.5": ModelInfo("kimi-k2.5", 256_000, 128_000, 0.60, 2.5),
"kimi-k2.6": ModelInfo("kimi-k2.6", 256_000, 128_000, 0.60, 2.5),
"kimi-k3": ModelInfo("kimi-k3", 1_048_576, 128_000, 3.0, 15.0),
"kimi-k2.5": ModelInfo(
"kimi-k2.5", 256_000, 128_000, 0.60, 2.5, reasoning=_REASONING_TOGGLE
),
"kimi-k2.6": ModelInfo(
"kimi-k2.6", 256_000, 128_000, 0.60, 2.5, reasoning=_REASONING_TOGGLE
),
"kimi-k3": ModelInfo(
"kimi-k3", 1_048_576, 128_000, 3.0, 15.0, reasoning=_REASONING_KIMI_K3
),
# DeepSeek.
"deepseek-v3": ModelInfo("deepseek-v3", 128_000, 8_192, 0.27, 1.10),
"deepseek-r1": ModelInfo("deepseek-r1", 128_000, 65_536, 0.55, 2.19),
"deepseek-v3": ModelInfo(
"deepseek-v3", 128_000, 8_192, 0.27, 1.10, reasoning=_REASONING_TOGGLE
),
"deepseek-r1": ModelInfo(
"deepseek-r1", 128_000, 65_536, 0.55, 2.19, reasoning=_REASONING_ALWAYS_ON
),
# Alibaba Qwen.
"qwen3-max": ModelInfo("qwen3-max", 256_000, 32_768, 1.2, 6.0),
"qwen3-coder": ModelInfo("qwen3-coder", 256_000, 65_536, 1.0, 5.0),
"qwen3-max": ModelInfo(
"qwen3-max", 256_000, 32_768, 1.2, 6.0, reasoning=_REASONING_TOGGLE
),
"qwen3-coder": ModelInfo(
"qwen3-coder", 256_000, 65_536, 1.0, 5.0, reasoning=_REASONING_TOGGLE
),
# Zhipu GLM — 200K context / 128K output, thinking toggled with
# ``thinking: {"type": ...}`` (docs.z.ai/guides/llm/glm-4.6).
"glm-4.6": ModelInfo(
"glm-4.6", 204_800, 131_072, 0.43, 1.75, reasoning=_REASONING_TOGGLE
),
# MiniMax — the M2 line shares one window; M3 is the long-context tier
# (platform.minimax.io/docs/api-reference/api-overview). The published
# limit is the input+output total.
"minimax-m2": ModelInfo(
"minimax-m2", 204_800, 131_072, 0.30, 1.20, reasoning=_REASONING_TOGGLE
),
"minimax-m2.7": ModelInfo(
"minimax-m2.7", 204_800, 131_072, 0.30, 1.20, reasoning=_REASONING_TOGGLE
),
"minimax-m3": ModelInfo(
"minimax-m3", 1_000_000, 131_072, 0.30, 1.20, reasoning=_REASONING_TOGGLE
),
# xAI Grok.
"grok-4": ModelInfo("grok-4", 256_000, 64_000, 3.0, 15.0),
"grok-4": ModelInfo(
"grok-4", 256_000, 64_000, 3.0, 15.0, reasoning=_REASONING_ALWAYS_ON
),
}


Expand All @@ -118,10 +263,22 @@ class ModelInfo:
("o1", _SEED["o1"]),
("o3", _SEED["o3"]),
("o4", _SEED["o4-mini"]),
("claude-opus", _SEED["claude-opus-4-8"]),
("claude-sonnet", _SEED["claude-sonnet-5"]),
("claude-haiku", _SEED["claude-haiku-4-5"]),
("claude", _SEED["claude-sonnet-5"]),
# Limits carry over from the newest tier member, but summarised thinking
# does not: a dated id like ``claude-sonnet-4-20250514`` predates it and
# streams raw trace. Exact seed rows above keep the full capability.
(
"claude-opus",
replace(_SEED["claude-opus-4-8"], reasoning=_REASONING_ANTHROPIC_TRACE),
),
(
"claude-sonnet",
replace(_SEED["claude-sonnet-5"], reasoning=_REASONING_ANTHROPIC_TRACE),
),
(
"claude-haiku",
replace(_SEED["claude-haiku-4-5"], reasoning=_REASONING_ANTHROPIC_TRACE),
),
("claude", replace(_SEED["claude-sonnet-5"], reasoning=_REASONING_ANTHROPIC_TRACE)),
("gemini-3", _SEED["gemini-3-pro"]),
("gemini", _SEED["gemini-2.5-pro"]),
("kimi-k3", _SEED["kimi-k3"]),
Expand All @@ -131,6 +288,10 @@ class ModelInfo:
("deepseek", _SEED["deepseek-v3"]),
("qwen", _SEED["qwen3-max"]),
("grok", _SEED["grok-4"]),
# Longest prefix first: M3 is the 1M tier, the rest of the line is 200K.
("minimax-m3", _SEED["minimax-m3"]),
("minimax", _SEED["minimax-m2.7"]),
("glm", _SEED["glm-4.6"]),
)

# Conservative fallback for a genuinely unknown model: 128K is the smallest
Expand Down
71 changes: 31 additions & 40 deletions core/providers/reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,55 +134,46 @@ def resolve_reasoning_effort(
)


#: Parameter names a remote catalog uses to advertise a reasoning control.
_REASONING_PARAMETERS = frozenset(
{"reasoning", "reasoning_effort", "include_reasoning", "thinking"}
)


def infer_reasoning_capabilities(
model_id: str,
*,
provider_name: str | None = None,
supported_parameters: Iterable[str] = (),
) -> ModelReasoningCapabilities | None:
"""Conservative offline fallback when a catalog has no structured data.

This is intentionally centralized. Remote provider metadata wins; these
families only keep manual/custom catalogs usable without scattering
``model_id`` checks through product code.
"""Resolve the reasoning controls a model advertises, offline.

Two ordered sources, no model-name conditionals:

1. The model catalog (:mod:`core.providers.catalog`), whose
normalize → snapshot → seed → family cascade already answers "what is
this model" for context window and pricing. An unseen ``deepseek-r2``
inherits the ``deepseek-r`` family row here exactly as it does there.
2. ``supported_parameters`` published by a remote catalog, which proves a
reasoning control exists even when the model is absent from the seed.
No named levels come with it, so the surface is Auto/Off.

Returns ``None`` when neither source knows of a reasoning mode. Callers
treat that as "no thinking controls", so guessing here would put dead
options in the product; a missing model belongs in the seed table, not in
a branch added to this function.
"""

model = model_id.strip().lower()
provider = (provider_name or "").strip().lower()
parameters = {str(item).strip().lower() for item in supported_parameters}
# Imported lazily: the catalog imports this module for ``ModelInfo``.
from core.providers.catalog import resolve_model_info

known = resolve_model_info(model_id).reasoning
if known is not None:
return known

if "kimi-k3" in model:
return ModelReasoningCapabilities(
supported_efforts=("low", "high", "max"),
default_effort="max",
default_enabled=True,
supports_summary=True,
)
if model.startswith(("o1", "o3", "o4")) or "gpt-5" in model:
return ModelReasoningCapabilities(
supported_efforts=("minimal", "low", "medium", "high"),
default_effort="medium",
default_enabled=True,
supports_summary=True,
)
if "claude-" in model and any(
family in model
for family in (
"opus-4-6",
"opus-4.6",
"sonnet-4-6",
"sonnet-4.6",
)
):
return ModelReasoningCapabilities(
supported_efforts=("low", "medium", "high", "max"),
default_effort="high",
default_enabled=True,
supports_summary=True,
)
if parameters.intersection(
{"reasoning", "reasoning_effort", "include_reasoning", "thinking"}
):
parameters = {str(item).strip().lower() for item in supported_parameters}
if parameters & _REASONING_PARAMETERS:
provider = (provider_name or "").strip().lower()
return ModelReasoningCapabilities(supports_summary=provider != "anthropic")
return None

Expand Down
2 changes: 2 additions & 0 deletions core/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def label(self) -> str:
backend="openai_compat",
env_extras=(("ZHIPUAI_API_KEY", "{api_key}"),),
default_api_base="https://open.bigmodel.cn/api/paas/v4",
# GLM takes the same ``thinking: {"type": ...}`` body as DeepSeek.
thinking_style="thinking_type",
),
ProviderSpec(
name="dashscope",
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { lazy, Suspense, useState } from "react";

import { projectCanExecute } from "./app/projectPresentation";
import { latestExecutingTurn } from "./app/interactiveTurnRouter";
import { useAppearance } from "./app/useAppearance";
import { useDesktopUi } from "./app/useDesktopUi";
import { useComposerCommands } from "./app/useComposerCommands";
import { useWorkspaceController } from "./app/useWorkspaceController";
Expand Down Expand Up @@ -59,6 +60,8 @@ function LoadingSurface({

export function App({ runtime = tauriRuntime }: { runtime?: DesktopRuntime }) {
const controller = useWorkspaceController(runtime);
// Mounted for its effect: paints saved appearance preferences at startup.
useAppearance();
const ui = useDesktopUi();
const transcript = useTranscriptMode();
const runComposerCommand = useComposerCommands(controller, ui);
Expand Down
Loading
Loading