zjcode 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""Provider-specific reasoning effort support for `/effort`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeAlias, get_args
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from deepagents_code._constants import FIREWORKS_MODEL_ID_PREFIXES
|
|
13
|
+
from deepagents_code.model_config import CODEX_PROVIDER, ModelSpec
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
EffortLabel: TypeAlias = Literal["none", "low", "medium", "high", "xhigh", "max"]
|
|
18
|
+
"""Closed vocabulary of effort labels across all supported providers.
|
|
19
|
+
|
|
20
|
+
Typing the per-provider tuples with this alias catches typos in the vocabulary
|
|
21
|
+
at check time. It does not express the deeper invariant that a label must be
|
|
22
|
+
supported by a *specific* model — that is enforced at runtime by
|
|
23
|
+
`supported_efforts_for_model`.
|
|
24
|
+
|
|
25
|
+
This vocabulary is also hand-duplicated as display text in the `/effort`
|
|
26
|
+
`argument_hint` (`command_registry.py`) and in `COMMANDS.md`; those are not
|
|
27
|
+
type-checked against this alias, so update them in lockstep when it changes.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ReasoningProvider: TypeAlias = Literal[
|
|
31
|
+
"anthropic", "fireworks", "google_genai", "openai", "openai_codex", "xai"
|
|
32
|
+
]
|
|
33
|
+
"""Provider identifiers that support model-specific reasoning effort controls.
|
|
34
|
+
|
|
35
|
+
Values must stay byte-identical to the provider strings from `ModelSpec.parse`
|
|
36
|
+
used throughout `model_config.py` (e.g. `CODEX_PROVIDER`).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReasoningProviderConfig(NamedTuple):
|
|
41
|
+
"""Provider-specific reasoning effort behavior."""
|
|
42
|
+
|
|
43
|
+
supported_efforts: Callable[[str], tuple[EffortLabel, ...]]
|
|
44
|
+
"""Return supported effort labels for a lowercased model name."""
|
|
45
|
+
|
|
46
|
+
default_effort: Callable[[str], EffortLabel | None]
|
|
47
|
+
"""Return the provider default effort for a lowercased model name, if known."""
|
|
48
|
+
|
|
49
|
+
model_params: Callable[[str], dict[str, Any]]
|
|
50
|
+
"""Translate an effort label into provider-specific model params."""
|
|
51
|
+
|
|
52
|
+
current_effort: Callable[[dict[str, Any]], str | None]
|
|
53
|
+
"""Read the configured effort label from provider-specific model params."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
OPENAI_EFFORTS: tuple[EffortLabel, ...] = ("none", "low", "medium", "high", "xhigh")
|
|
57
|
+
"""OpenAI GPT-5 effort labels before GPT-5.6 for `reasoning.effort`.
|
|
58
|
+
|
|
59
|
+
See https://platform.openai.com/docs/guides/reasoning.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
OPENAI_56_EFFORTS: tuple[EffortLabel, ...] = (*OPENAI_EFFORTS, "max")
|
|
63
|
+
"""OpenAI GPT-5.6 effort labels for `reasoning.effort`."""
|
|
64
|
+
|
|
65
|
+
ANTHROPIC_EFFORTS: tuple[EffortLabel, ...] = ("low", "medium", "high", "xhigh", "max")
|
|
66
|
+
"""Anthropic `output_config.effort` labels for Opus 4.7+ and Sonnet 5.
|
|
67
|
+
|
|
68
|
+
See https://platform.claude.com/docs/en/build-with-claude/effort.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
ANTHROPIC_EFFORTS_NO_XHIGH: tuple[EffortLabel, ...] = ("low", "medium", "high", "max")
|
|
72
|
+
"""Anthropic effort labels for Opus 4.6 and Sonnet 4.6.
|
|
73
|
+
|
|
74
|
+
These models predate `xhigh`; Sonnet 4.5 rejects `effort` entirely.
|
|
75
|
+
See https://platform.claude.com/docs/en/build-with-claude/effort.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
ANTHROPIC_EFFORTS_NO_MAX: tuple[EffortLabel, ...] = ("low", "medium", "high")
|
|
79
|
+
"""Anthropic effort labels for Opus 4.5.
|
|
80
|
+
|
|
81
|
+
Opus 4.5 predates both `max` (Opus 4.6+) and `xhigh` (Opus 4.7+).
|
|
82
|
+
See https://platform.claude.com/docs/en/build-with-claude/effort.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
GOOGLE_EFFORTS: tuple[EffortLabel, ...] = ("low", "medium", "high")
|
|
86
|
+
"""Gemini `thinking_level` labels.
|
|
87
|
+
|
|
88
|
+
Applied to every `gemini-3*` model (the gate in `_classify_reasoning_provider`),
|
|
89
|
+
including Gemini 3 Pro/Flash, 3.1 Pro, and 3.5 Flash — all accept
|
|
90
|
+
low/medium/high. `minimal` is Flash-Lite / original-Pro territory, neither of
|
|
91
|
+
which is offered here. See https://ai.google.dev/gemini-api/docs/thinking.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
FIREWORKS_REASONING_EFFORTS: tuple[EffortLabel, ...] = (
|
|
95
|
+
"none",
|
|
96
|
+
"low",
|
|
97
|
+
"medium",
|
|
98
|
+
"high",
|
|
99
|
+
"xhigh",
|
|
100
|
+
"max",
|
|
101
|
+
)
|
|
102
|
+
"""Fireworks `reasoning_effort` labels for DeepSeek V4 Pro.
|
|
103
|
+
|
|
104
|
+
See https://docs.fireworks.ai/guides/reasoning.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
FIREWORKS_KIMI_EFFORTS: tuple[EffortLabel, ...] = ("low", "medium", "high")
|
|
108
|
+
"""Fireworks `reasoning_effort` labels for Kimi K2 models.
|
|
109
|
+
|
|
110
|
+
See https://docs.fireworks.ai/guides/reasoning.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
FIREWORKS_GLM_EFFORTS: tuple[EffortLabel, ...] = ("none", "high", "max")
|
|
114
|
+
"""Fireworks `reasoning_effort` labels for GLM 5 models.
|
|
115
|
+
|
|
116
|
+
See https://docs.fireworks.ai/guides/reasoning.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
XAI_EFFORTS: tuple[EffortLabel, ...] = ("low", "medium", "high")
|
|
120
|
+
"""xAI `reasoning_effort` labels for Grok 4.5.
|
|
121
|
+
|
|
122
|
+
See https://docs.x.ai/developers/model-capabilities/text/reasoning.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
_REASONING_KEYS: frozenset[str] = frozenset(
|
|
126
|
+
{
|
|
127
|
+
"effort",
|
|
128
|
+
"extra_body",
|
|
129
|
+
"output_config",
|
|
130
|
+
"reasoning",
|
|
131
|
+
"reasoning_effort",
|
|
132
|
+
"thinking",
|
|
133
|
+
"thinking_level",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
"""Runtime config keys that may already carry provider reasoning settings."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _openai_supported_efforts(model: str) -> tuple[EffortLabel, ...]:
|
|
140
|
+
"""Return OpenAI reasoning effort levels."""
|
|
141
|
+
return OPENAI_56_EFFORTS if _has_version(model, "gpt-5.6") else OPENAI_EFFORTS
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _openai_default_effort(model: str) -> EffortLabel | None:
|
|
145
|
+
"""Return the OpenAI default reasoning effort when known."""
|
|
146
|
+
if _has_version(model, "gpt-5.5") or _has_version(model, "gpt-5.6"):
|
|
147
|
+
return "medium"
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _openai_model_params(effort: str) -> dict[str, Any]:
|
|
152
|
+
"""Return OpenAI reasoning params for an effort label."""
|
|
153
|
+
if effort == "none":
|
|
154
|
+
return {"reasoning": {"effort": "none"}}
|
|
155
|
+
return {"reasoning": {"effort": effort, "summary": "auto"}}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _openai_current_effort(model_params: dict[str, Any]) -> str | None:
|
|
159
|
+
"""Read the OpenAI reasoning effort from model params.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The configured effort label, or `None` when unset.
|
|
163
|
+
"""
|
|
164
|
+
reasoning = model_params.get("reasoning")
|
|
165
|
+
if isinstance(reasoning, dict):
|
|
166
|
+
value = reasoning.get("effort")
|
|
167
|
+
if value is not None and not isinstance(value, str):
|
|
168
|
+
# Present but mistyped (e.g. a hand-edited config or bad
|
|
169
|
+
# `--model-params` JSON). Discard it, but log the *type* — never the
|
|
170
|
+
# value — so the drop is greppable instead of silently read as "no
|
|
171
|
+
# effort set" while the malformed param still ships on the wire.
|
|
172
|
+
logger.warning(
|
|
173
|
+
"Ignoring non-str OpenAI reasoning.effort of type %s",
|
|
174
|
+
type(value).__name__,
|
|
175
|
+
)
|
|
176
|
+
return value if isinstance(value, str) else None
|
|
177
|
+
if reasoning is not None:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"Ignoring OpenAI reasoning params of unexpected type %s",
|
|
180
|
+
type(reasoning).__name__,
|
|
181
|
+
)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _has_version(model: str, token: str) -> bool:
|
|
186
|
+
"""Return whether `model` carries version `token` not followed by a digit.
|
|
187
|
+
|
|
188
|
+
A plain substring test would match a longer version by accident — e.g.
|
|
189
|
+
`"opus-4-1" in "claude-opus-4-16"` is true. Anchoring on a non-digit
|
|
190
|
+
boundary keeps `opus-4-1` from matching a future `opus-4-16` while still
|
|
191
|
+
matching a dated suffix like `opus-4-1-20250805`. `token` is always a
|
|
192
|
+
hardcoded constant, but `re.escape` keeps the match literal regardless.
|
|
193
|
+
"""
|
|
194
|
+
return re.search(rf"{re.escape(token)}(?!\d)", model) is not None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _anthropic_supported_efforts(model: str) -> tuple[EffortLabel, ...]:
|
|
198
|
+
"""Return the effort levels an Anthropic model accepts.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
model: Lowercased Anthropic model name (e.g. `claude-opus-4-8`).
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Supported effort labels, or an empty tuple when the model does not
|
|
205
|
+
accept `effort` (e.g. Sonnet 4.5).
|
|
206
|
+
"""
|
|
207
|
+
if model.startswith("claude-opus-"):
|
|
208
|
+
if _has_version(model, "opus-4-0") or _has_version(model, "opus-4-1"):
|
|
209
|
+
# Opus 4.0/4.1 predate reasoning effort entirely.
|
|
210
|
+
return ()
|
|
211
|
+
if _has_version(model, "opus-4-5"):
|
|
212
|
+
# Opus 4.5 predates both `max` (4.6+) and `xhigh` (4.7+).
|
|
213
|
+
return ANTHROPIC_EFFORTS_NO_MAX
|
|
214
|
+
# Opus 4.6 predates `xhigh`; 4.7+ (and newer, unrecognized versions)
|
|
215
|
+
# get the full range.
|
|
216
|
+
return (
|
|
217
|
+
ANTHROPIC_EFFORTS_NO_XHIGH
|
|
218
|
+
if _has_version(model, "opus-4-6")
|
|
219
|
+
else ANTHROPIC_EFFORTS
|
|
220
|
+
)
|
|
221
|
+
if model.startswith("claude-sonnet-"):
|
|
222
|
+
if (
|
|
223
|
+
_has_version(model, "sonnet-4-0")
|
|
224
|
+
or _has_version(model, "sonnet-4-1")
|
|
225
|
+
or _has_version(model, "sonnet-4-5")
|
|
226
|
+
):
|
|
227
|
+
# Sonnet 4.0/4.1 predate effort; Sonnet 4.5 rejects it.
|
|
228
|
+
return ()
|
|
229
|
+
# Sonnet 4.6 predates `xhigh`; Sonnet 5 (and newer) get the full range.
|
|
230
|
+
return (
|
|
231
|
+
ANTHROPIC_EFFORTS_NO_XHIGH
|
|
232
|
+
if _has_version(model, "sonnet-4-6")
|
|
233
|
+
else ANTHROPIC_EFFORTS
|
|
234
|
+
)
|
|
235
|
+
return ()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _anthropic_default_effort(model: str) -> EffortLabel | None:
|
|
239
|
+
"""Return the Anthropic default reasoning effort when known."""
|
|
240
|
+
return "high" if _anthropic_supported_efforts(model) else None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _anthropic_model_params(effort: str) -> dict[str, Any]:
|
|
244
|
+
"""Return Anthropic reasoning params for an effort label."""
|
|
245
|
+
return {
|
|
246
|
+
"thinking": {"type": "adaptive", "display": "summarized"},
|
|
247
|
+
"output_config": {"effort": effort},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _anthropic_current_effort(model_params: dict[str, Any]) -> str | None:
|
|
252
|
+
"""Read the Anthropic reasoning effort from model params.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The configured effort label, or `None` when unset.
|
|
256
|
+
"""
|
|
257
|
+
output_config = model_params.get("output_config")
|
|
258
|
+
if isinstance(output_config, dict):
|
|
259
|
+
value = output_config.get("effort")
|
|
260
|
+
if value is not None and not isinstance(value, str):
|
|
261
|
+
logger.warning(
|
|
262
|
+
"Ignoring non-str Anthropic output_config.effort of type %s",
|
|
263
|
+
type(value).__name__,
|
|
264
|
+
)
|
|
265
|
+
return value if isinstance(value, str) else None
|
|
266
|
+
if output_config is not None:
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Ignoring Anthropic output_config params of unexpected type %s",
|
|
269
|
+
type(output_config).__name__,
|
|
270
|
+
)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _google_supported_efforts(_model: str) -> tuple[EffortLabel, ...]:
|
|
275
|
+
"""Return Gemini thinking levels."""
|
|
276
|
+
return GOOGLE_EFFORTS
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _google_default_effort(model: str) -> EffortLabel | None:
|
|
280
|
+
"""Return the Gemini default thinking level when known."""
|
|
281
|
+
if model.startswith("gemini-3.5-flash"):
|
|
282
|
+
return "medium"
|
|
283
|
+
if model.startswith(("gemini-3.1-pro", "gemini-3-flash", "gemini-3-pro")):
|
|
284
|
+
return "high"
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _google_model_params(effort: str) -> dict[str, Any]:
|
|
289
|
+
"""Return Gemini thinking params for an effort label."""
|
|
290
|
+
return {"thinking_level": effort}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _google_current_effort(model_params: dict[str, Any]) -> str | None:
|
|
294
|
+
"""Read the Gemini thinking level from model params.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
The configured effort label, or `None` when unset.
|
|
298
|
+
"""
|
|
299
|
+
value = model_params.get("thinking_level")
|
|
300
|
+
if value is not None and not isinstance(value, str):
|
|
301
|
+
logger.warning(
|
|
302
|
+
"Ignoring non-str Gemini thinking_level of type %s",
|
|
303
|
+
type(value).__name__,
|
|
304
|
+
)
|
|
305
|
+
return value if isinstance(value, str) else None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _fireworks_supported_efforts(model: str) -> tuple[EffortLabel, ...]:
|
|
309
|
+
"""Return Fireworks reasoning effort levels for a model."""
|
|
310
|
+
if "kimi-k2" in model:
|
|
311
|
+
return FIREWORKS_KIMI_EFFORTS
|
|
312
|
+
if "glm-5" in model:
|
|
313
|
+
return FIREWORKS_GLM_EFFORTS
|
|
314
|
+
if "deepseek-v4-pro" in model:
|
|
315
|
+
return FIREWORKS_REASONING_EFFORTS
|
|
316
|
+
return ()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _fireworks_default_effort(model: str) -> EffortLabel | None:
|
|
320
|
+
"""Return the Fireworks default reasoning effort when known."""
|
|
321
|
+
if "deepseek-v4-pro" in model:
|
|
322
|
+
return "high"
|
|
323
|
+
if "glm-5p2" in model:
|
|
324
|
+
return "max"
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _fireworks_model_params(effort: str) -> dict[str, Any]:
|
|
329
|
+
"""Return Fireworks reasoning params for an effort label."""
|
|
330
|
+
return {"model_kwargs": {"reasoning_effort": effort}}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _fireworks_current_effort(model_params: dict[str, Any]) -> str | None:
|
|
334
|
+
"""Read the Fireworks reasoning effort from model params.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The configured effort label, or `None` when unset.
|
|
338
|
+
"""
|
|
339
|
+
kwargs = model_params.get("model_kwargs")
|
|
340
|
+
if isinstance(kwargs, dict):
|
|
341
|
+
value = kwargs.get("reasoning_effort")
|
|
342
|
+
if value is not None and not isinstance(value, str):
|
|
343
|
+
logger.warning(
|
|
344
|
+
"Ignoring non-str Fireworks reasoning_effort of type %s",
|
|
345
|
+
type(value).__name__,
|
|
346
|
+
)
|
|
347
|
+
return value if isinstance(value, str) else None
|
|
348
|
+
# A non-dict `model_kwargs` is a legitimate shape here (it may hold
|
|
349
|
+
# unrelated params, or be preserved verbatim by `without_effort_model_params`),
|
|
350
|
+
# so treat it as "no effort configured" without warning.
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _is_xai_grok_45(model: str) -> bool:
|
|
355
|
+
"""Return whether `model` is Grok 4.5 or a documented alias."""
|
|
356
|
+
return model in {"grok-4.5", "grok-4.5-latest", "grok-build-latest"}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _xai_supported_efforts(model: str) -> tuple[EffortLabel, ...]:
|
|
360
|
+
"""Return xAI reasoning effort levels for a model."""
|
|
361
|
+
return XAI_EFFORTS if _is_xai_grok_45(model) else ()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _xai_default_effort(model: str) -> EffortLabel | None:
|
|
365
|
+
"""Return the xAI default reasoning effort when known."""
|
|
366
|
+
return "high" if _is_xai_grok_45(model) else None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _xai_model_params(effort: str) -> dict[str, Any]:
|
|
370
|
+
"""Return xAI reasoning params for an effort label."""
|
|
371
|
+
return {"extra_body": {"reasoning_effort": effort}}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _xai_current_effort(model_params: dict[str, Any]) -> str | None:
|
|
375
|
+
"""Read the xAI reasoning effort from model params.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The configured effort label, or `None` when unset.
|
|
379
|
+
"""
|
|
380
|
+
extra = model_params.get("extra_body")
|
|
381
|
+
if isinstance(extra, dict):
|
|
382
|
+
value = extra.get("reasoning_effort")
|
|
383
|
+
if value is not None and not isinstance(value, str):
|
|
384
|
+
logger.warning(
|
|
385
|
+
"Ignoring non-str xAI extra_body.reasoning_effort of type %s",
|
|
386
|
+
type(value).__name__,
|
|
387
|
+
)
|
|
388
|
+
return value if isinstance(value, str) else None
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
_OPENAI_CONFIG = ReasoningProviderConfig(
|
|
393
|
+
supported_efforts=_openai_supported_efforts,
|
|
394
|
+
default_effort=_openai_default_effort,
|
|
395
|
+
model_params=_openai_model_params,
|
|
396
|
+
current_effort=_openai_current_effort,
|
|
397
|
+
)
|
|
398
|
+
"""Shared config for OpenAI-compatible GPT-5 reasoning providers.
|
|
399
|
+
|
|
400
|
+
`openai` and `openai_codex` use different provider names so model selection can
|
|
401
|
+
route to the right client, but `/effort` maps both to the same reasoning params.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
_PROVIDER_CONFIGS: dict[ReasoningProvider, ReasoningProviderConfig] = {
|
|
405
|
+
"openai": _OPENAI_CONFIG,
|
|
406
|
+
"openai_codex": _OPENAI_CONFIG,
|
|
407
|
+
"anthropic": ReasoningProviderConfig(
|
|
408
|
+
supported_efforts=_anthropic_supported_efforts,
|
|
409
|
+
default_effort=_anthropic_default_effort,
|
|
410
|
+
model_params=_anthropic_model_params,
|
|
411
|
+
current_effort=_anthropic_current_effort,
|
|
412
|
+
),
|
|
413
|
+
"google_genai": ReasoningProviderConfig(
|
|
414
|
+
supported_efforts=_google_supported_efforts,
|
|
415
|
+
default_effort=_google_default_effort,
|
|
416
|
+
model_params=_google_model_params,
|
|
417
|
+
current_effort=_google_current_effort,
|
|
418
|
+
),
|
|
419
|
+
"fireworks": ReasoningProviderConfig(
|
|
420
|
+
supported_efforts=_fireworks_supported_efforts,
|
|
421
|
+
default_effort=_fireworks_default_effort,
|
|
422
|
+
model_params=_fireworks_model_params,
|
|
423
|
+
current_effort=_fireworks_current_effort,
|
|
424
|
+
),
|
|
425
|
+
"xai": ReasoningProviderConfig(
|
|
426
|
+
supported_efforts=_xai_supported_efforts,
|
|
427
|
+
default_effort=_xai_default_effort,
|
|
428
|
+
model_params=_xai_model_params,
|
|
429
|
+
current_effort=_xai_current_effort,
|
|
430
|
+
),
|
|
431
|
+
}
|
|
432
|
+
"""Provider-specific reasoning effort behavior keyed by `ModelSpec` provider."""
|
|
433
|
+
|
|
434
|
+
if set(_PROVIDER_CONFIGS) != set(get_args(ReasoningProvider)): # pragma: no cover
|
|
435
|
+
# `_classify_reasoning_provider` only ever returns members of the
|
|
436
|
+
# `ReasoningProvider` vocabulary, and `_reasoning_config` indexes
|
|
437
|
+
# `_PROVIDER_CONFIGS` with the result — so the two must stay in lockstep or
|
|
438
|
+
# that lookup raises `KeyError` at runtime. Fail loudly at import instead.
|
|
439
|
+
msg = "_PROVIDER_CONFIGS keys must match the ReasoningProvider vocabulary"
|
|
440
|
+
raise RuntimeError(msg)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _classify_reasoning_provider(provider: str, model: str) -> ReasoningProvider | None:
|
|
444
|
+
"""Classify provider/model parts into a reasoning-capable provider.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
The registry key for supported reasoning models, or `None` otherwise.
|
|
448
|
+
"""
|
|
449
|
+
model_lower = model.lower()
|
|
450
|
+
if provider == "openai" and model_lower.startswith("gpt-5"):
|
|
451
|
+
return "openai"
|
|
452
|
+
if provider == CODEX_PROVIDER and model_lower.startswith("gpt-5"):
|
|
453
|
+
return "openai_codex"
|
|
454
|
+
if provider == "anthropic" and model_lower.startswith(
|
|
455
|
+
("claude-opus-", "claude-sonnet-")
|
|
456
|
+
):
|
|
457
|
+
return "anthropic"
|
|
458
|
+
if provider == "google_genai" and model_lower.startswith("gemini-3"):
|
|
459
|
+
return "google_genai"
|
|
460
|
+
if provider == "fireworks" and model_lower.startswith(FIREWORKS_MODEL_ID_PREFIXES):
|
|
461
|
+
return "fireworks"
|
|
462
|
+
if provider == "xai" and _is_xai_grok_45(model_lower):
|
|
463
|
+
return "xai"
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _reasoning_config(model_spec: str) -> tuple[ReasoningProviderConfig, str] | None:
|
|
468
|
+
"""Return provider config and lowercased model when reasoning is supported."""
|
|
469
|
+
parsed = ModelSpec.try_parse(model_spec)
|
|
470
|
+
if parsed is None:
|
|
471
|
+
return None
|
|
472
|
+
provider = _classify_reasoning_provider(parsed.provider, parsed.model)
|
|
473
|
+
if provider is None:
|
|
474
|
+
return None
|
|
475
|
+
return _PROVIDER_CONFIGS[provider], parsed.model.lower()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def supported_efforts_for_model(model_spec: str | None) -> tuple[str, ...]:
|
|
479
|
+
"""Return reasoning efforts supported by `model_spec`.
|
|
480
|
+
|
|
481
|
+
Returns plain `str` labels rather than `EffortLabel`: this is the public
|
|
482
|
+
boundary where the label vocabulary is intentionally dropped, since the
|
|
483
|
+
values flow straight to the UI.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
model_spec: `provider:model` spec for the active model.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Supported effort labels, or an empty tuple when the model is unsupported.
|
|
490
|
+
"""
|
|
491
|
+
if not model_spec:
|
|
492
|
+
return ()
|
|
493
|
+
context = _reasoning_config(model_spec)
|
|
494
|
+
if context is None:
|
|
495
|
+
return ()
|
|
496
|
+
config, model = context
|
|
497
|
+
efforts = config.supported_efforts(model)
|
|
498
|
+
if not efforts:
|
|
499
|
+
# A recognized reasoning provider that yields no configurable efforts
|
|
500
|
+
# usually means the model-version heuristics need updating for a newer
|
|
501
|
+
# release. Log at info so the maintenance gap is visible at default
|
|
502
|
+
# verbosity rather than silently reporting "not configurable".
|
|
503
|
+
logger.info("No configurable reasoning efforts for %s", model_spec)
|
|
504
|
+
return efforts
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def default_effort_for_model(model_spec: str | None) -> str | None:
|
|
508
|
+
"""Return the documented default reasoning effort when known.
|
|
509
|
+
|
|
510
|
+
Returns a plain `str` rather than `EffortLabel`: like
|
|
511
|
+
`supported_efforts_for_model`, this is the public boundary where the label
|
|
512
|
+
vocabulary is intentionally dropped, since every caller treats the value as
|
|
513
|
+
display text.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
model_spec: `provider:model` spec for the active model.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
The provider default effort label, or `None` when the default is unknown.
|
|
520
|
+
"""
|
|
521
|
+
if not model_spec:
|
|
522
|
+
return None
|
|
523
|
+
context = _reasoning_config(model_spec)
|
|
524
|
+
if context is None:
|
|
525
|
+
return None
|
|
526
|
+
config, model = context
|
|
527
|
+
return config.default_effort(model)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def model_params_for_effort(model_spec: str, effort: str) -> dict[str, Any] | None:
|
|
531
|
+
"""Translate an effort label into provider-specific model params.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
model_spec: `provider:model` spec for the active model.
|
|
535
|
+
effort: Effort label accepted by `supported_efforts_for_model`.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Model params to merge into the per-session override, or `None` when the
|
|
539
|
+
model/effort pair is unsupported.
|
|
540
|
+
"""
|
|
541
|
+
context = _reasoning_config(model_spec)
|
|
542
|
+
if context is None:
|
|
543
|
+
return None
|
|
544
|
+
config, model = context
|
|
545
|
+
if effort not in config.supported_efforts(model):
|
|
546
|
+
return None
|
|
547
|
+
return config.model_params(effort)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def current_effort_from_model_params(
|
|
551
|
+
model_spec: str | None, model_params: dict[str, Any] | None
|
|
552
|
+
) -> str | None:
|
|
553
|
+
"""Read the configured effort from model params when present.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
model_spec: `provider:model` spec for the active model.
|
|
557
|
+
model_params: Per-session model params.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
The configured effort, or `None` when no recognized effort override is set.
|
|
561
|
+
"""
|
|
562
|
+
if not model_spec or not model_params:
|
|
563
|
+
return None
|
|
564
|
+
context = _reasoning_config(model_spec)
|
|
565
|
+
if context is None:
|
|
566
|
+
return None
|
|
567
|
+
config, _ = context
|
|
568
|
+
return config.current_effort(model_params)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def merge_effort_model_params(
|
|
572
|
+
existing: dict[str, Any] | None, effort_params: dict[str, Any]
|
|
573
|
+
) -> dict[str, Any]:
|
|
574
|
+
"""Merge effort params into existing per-session model params.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
existing: Current per-session model params.
|
|
578
|
+
effort_params: Params returned by `model_params_for_effort`.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
A new merged dictionary preserving unrelated nested config objects.
|
|
582
|
+
"""
|
|
583
|
+
merged = dict(existing) if existing else {}
|
|
584
|
+
for key, value in effort_params.items():
|
|
585
|
+
if key in {"extra_body", "model_kwargs", "output_config"} and isinstance(
|
|
586
|
+
value, dict
|
|
587
|
+
):
|
|
588
|
+
current = merged.get(key)
|
|
589
|
+
base = dict(current) if isinstance(current, dict) else {}
|
|
590
|
+
base.update(value)
|
|
591
|
+
merged[key] = base
|
|
592
|
+
else:
|
|
593
|
+
merged[key] = value
|
|
594
|
+
return merged
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def without_effort_model_params(
|
|
598
|
+
existing: dict[str, Any] | None,
|
|
599
|
+
) -> dict[str, Any] | None:
|
|
600
|
+
"""Remove known effort params while preserving unrelated model params.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
existing: Current per-session model params.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
A cleaned dictionary, or `None` when no params remain.
|
|
607
|
+
"""
|
|
608
|
+
if not existing:
|
|
609
|
+
return None
|
|
610
|
+
# Exclude nested config objects from the comprehension and rebuild them below.
|
|
611
|
+
# Leaving them here would retain stale nested effort keys when the cleaned
|
|
612
|
+
# nested dict ends up empty — the empty-check would then skip the overwrite
|
|
613
|
+
# and the original (still-populated) copy would survive.
|
|
614
|
+
cleaned = {
|
|
615
|
+
key: (dict(value) if isinstance(value, dict) else value)
|
|
616
|
+
for key, value in existing.items()
|
|
617
|
+
if key not in _REASONING_KEYS
|
|
618
|
+
and key not in {"extra_body", "model_kwargs", "output_config"}
|
|
619
|
+
}
|
|
620
|
+
kwargs = existing.get("model_kwargs")
|
|
621
|
+
if isinstance(kwargs, dict):
|
|
622
|
+
model_kwargs = {k: v for k, v in kwargs.items() if k != "reasoning_effort"}
|
|
623
|
+
if model_kwargs:
|
|
624
|
+
cleaned["model_kwargs"] = model_kwargs
|
|
625
|
+
elif kwargs is not None:
|
|
626
|
+
cleaned["model_kwargs"] = kwargs
|
|
627
|
+
output_config = existing.get("output_config")
|
|
628
|
+
if isinstance(output_config, dict):
|
|
629
|
+
output_config_params = {k: v for k, v in output_config.items() if k != "effort"}
|
|
630
|
+
if output_config_params:
|
|
631
|
+
cleaned["output_config"] = output_config_params
|
|
632
|
+
elif output_config is not None:
|
|
633
|
+
cleaned["output_config"] = output_config
|
|
634
|
+
extra = existing.get("extra_body")
|
|
635
|
+
if isinstance(extra, dict):
|
|
636
|
+
extra_params = {k: v for k, v in extra.items() if k != "reasoning_effort"}
|
|
637
|
+
if extra_params:
|
|
638
|
+
cleaned["extra_body"] = extra_params
|
|
639
|
+
elif extra is not None:
|
|
640
|
+
cleaned["extra_body"] = extra
|
|
641
|
+
return cleaned or None
|