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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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