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,577 @@
1
+ """Middleware for runtime model selection via LangGraph runtime context.
2
+
3
+ Allows switching the model per invocation by passing a `CLIContext` via
4
+ `context=` on `agent.astream()` / `agent.invoke()` without recompiling
5
+ the graph.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any
15
+ from urllib.parse import urlsplit
16
+
17
+ from deepagents._models import ( # noqa: PLC2701
18
+ get_model_identifier,
19
+ model_matches_spec,
20
+ )
21
+ from langchain.agents.middleware.types import (
22
+ AgentMiddleware,
23
+ ExtendedModelResponse,
24
+ ModelRequest,
25
+ ModelResponse,
26
+ )
27
+ from langgraph.types import Command
28
+
29
+ from deepagents_code._cli_context import CLIContextSchema
30
+
31
+ if TYPE_CHECKING:
32
+ from collections.abc import Awaitable, Callable
33
+
34
+ from langchain_core.language_models import BaseChatModel
35
+
36
+ from deepagents_code.config import ModelResult
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class _ResolvedModelRequest:
44
+ """Model request plus the checkpoint metadata it should persist."""
45
+
46
+ request: ModelRequest
47
+ """Request to pass to the downstream model handler."""
48
+
49
+ model_spec: str | None
50
+ """Resolved `provider:model` spec to persist for resume, when known."""
51
+
52
+ model_params: dict[str, Any] | None = None
53
+ """Invocation params to persist, or `None` to clear checkpointed params."""
54
+
55
+ model_params_known: bool = False
56
+ """Whether `model_params` is known and should be written to the checkpoint."""
57
+
58
+
59
+ def _get_ls_provider(model: object) -> str | None:
60
+ """Return the LangSmith provider name reported by a chat model.
61
+
62
+ Returns:
63
+ The `ls_provider` string when the model reports one, otherwise `None`
64
+ (including when `_get_ls_params` is missing, raises, or yields a
65
+ non-string provider).
66
+ """
67
+ try:
68
+ ls_params = model._get_ls_params() # ty: ignore[unresolved-attribute]
69
+ except (AttributeError, TypeError, RuntimeError, NotImplementedError):
70
+ logger.debug("_get_ls_params raised for %s", type(model).__name__)
71
+ return None
72
+ if isinstance(ls_params, dict):
73
+ provider = ls_params.get("ls_provider")
74
+ if isinstance(provider, str):
75
+ return provider
76
+ return None
77
+
78
+
79
+ def _is_anthropic_model(model: object) -> bool:
80
+ """Check whether a resolved model reports `'anthropic'` as its provider.
81
+
82
+ Uses `_get_ls_params` from `BaseChatModel` to read the provider name.
83
+
84
+ Args:
85
+ model: A model instance to inspect.
86
+
87
+ Typed as `object` (rather than `BaseChatModel`) so the caller can
88
+ pass any model without an import-time dependency on a specific
89
+ provider package.
90
+
91
+ Returns:
92
+ `True` if the model's `ls_provider` is `'anthropic'`.
93
+ """
94
+ return _get_ls_provider(model) == "anthropic"
95
+
96
+
97
+ def _is_fireworks_model(model: object) -> bool:
98
+ """Check whether a resolved model reports `'fireworks'` as its provider.
99
+
100
+ Returns:
101
+ `True` if the model's `ls_provider` is `'fireworks'`.
102
+ """
103
+ return _get_ls_provider(model) == "fireworks"
104
+
105
+
106
+ def _is_openai_model(model: object) -> bool:
107
+ """Check whether a resolved model targets OpenAI's official API.
108
+
109
+ `ChatOpenAI` reports `'openai'` even when configured with a custom base URL,
110
+ so provider metadata alone cannot establish support for OpenAI-specific
111
+ request fields. Unknown endpoints are treated conservatively as
112
+ incompatible.
113
+
114
+ Returns:
115
+ `True` if the model reports `'openai'` and uses `api.openai.com` or an
116
+ official regional subdomain.
117
+ """
118
+ if _get_ls_provider(model) != "openai":
119
+ return False
120
+
121
+ # Prefer the SDK client's resolved base URL: a default `ChatOpenAI()` leaves
122
+ # `openai_api_base` unset, but its `root_client.base_url` still resolves to
123
+ # the official `api.openai.com` default. Fall back to the constructor field
124
+ # only when the client is unavailable.
125
+ client = getattr(model, "root_client", None)
126
+ base_url = getattr(client, "base_url", None)
127
+ if base_url is None:
128
+ base_url = getattr(model, "openai_api_base", None)
129
+ if base_url is None:
130
+ # The provider is 'openai' yet no endpoint is discoverable. A genuine
131
+ # `ChatOpenAI` always exposes one, so this points to an unexpected model
132
+ # shape (e.g. an attribute renamed by an upstream upgrade). Skip the
133
+ # optimization instead of guessing, and leave a trace so the silent
134
+ # regression is diagnosable.
135
+ logger.debug("OpenAI model exposes no base URL; skipping prompt_cache_key")
136
+ return False
137
+
138
+ try:
139
+ hostname = urlsplit(str(base_url)).hostname
140
+ except ValueError:
141
+ logger.debug("OpenAI base URL is unparseable; skipping prompt_cache_key")
142
+ return False
143
+ return hostname == "api.openai.com" or (
144
+ hostname is not None and hostname.endswith(".api.openai.com")
145
+ )
146
+
147
+
148
+ _ANTHROPIC_ONLY_SETTINGS: set[str] = {"cache_control"}
149
+ """Keys injected by Anthropic-specific middleware (e.g.
150
+ `AnthropicPromptCachingMiddleware`) that are not accepted by other providers and
151
+ must be stripped on cross-provider swap."""
152
+
153
+ _FIREWORKS_SESSION_AFFINITY_HEADER = "x-session-affinity"
154
+ """Fireworks prompt-cache affinity header populated from the active thread ID."""
155
+
156
+
157
+ def _has_header(headers: Mapping[object, object], target: str) -> bool:
158
+ """Return whether a headers mapping already includes `target`.
159
+
160
+ Comparison is case-insensitive; `target` must be supplied in lowercase.
161
+
162
+ Returns:
163
+ `True` if a string key case-insensitively equal to `target` is present.
164
+ """
165
+ return any(isinstance(key, str) and key.lower() == target for key in headers)
166
+
167
+
168
+ def _with_fireworks_session_settings(
169
+ model_settings: dict[str, Any], thread_id: str
170
+ ) -> dict[str, Any] | None:
171
+ """Return model settings with Fireworks session settings added if needed.
172
+
173
+ Existing settings are preserved and never overwritten. Missing
174
+ `x-session-affinity` headers are populated directly so Fireworks can route
175
+ the conversation to the prompt-cache session for the active thread.
176
+
177
+ Returns:
178
+ A new `model_settings` dict with the missing session settings added, or
179
+ `None` when nothing needed adding or `extra_headers` is present but
180
+ not a mapping (leaving the request untouched).
181
+ """
182
+ raw_headers = model_settings.get("extra_headers")
183
+ if raw_headers is None:
184
+ headers: dict[object, object] = {}
185
+ elif isinstance(raw_headers, Mapping):
186
+ headers = dict(raw_headers)
187
+ else:
188
+ logger.warning(
189
+ "Cannot inject Fireworks session settings because extra_headers is %s",
190
+ type(raw_headers).__name__,
191
+ )
192
+ return None
193
+
194
+ updated: dict[str, Any] = {}
195
+ has_session_affinity = _has_header(headers, _FIREWORKS_SESSION_AFFINITY_HEADER)
196
+ if "prompt_cache_key" not in model_settings and not has_session_affinity:
197
+ updated["prompt_cache_key"] = thread_id
198
+
199
+ if not has_session_affinity:
200
+ headers[_FIREWORKS_SESSION_AFFINITY_HEADER] = thread_id
201
+ updated["extra_headers"] = headers
202
+
203
+ if not updated:
204
+ return None
205
+ return {**model_settings, **updated}
206
+
207
+
208
+ def _with_openai_prompt_cache_key(
209
+ model: object, model_settings: dict[str, Any], thread_id: str
210
+ ) -> dict[str, Any] | None:
211
+ """Return model settings with an OpenAI `prompt_cache_key` added if needed.
212
+
213
+ Setting `prompt_cache_key` lets OpenAI route a conversation to a stable
214
+ prompt-cache prefix across turns, giving more reliable cache hits than the
215
+ automatic (keyless) matching; it is optional, and requests still cache
216
+ without it. It is a supported top-level `ChatOpenAI` invocation setting
217
+ forwarded to the OpenAI request payload, so passing it through
218
+ `model_settings` reaches the wire unchanged. `prompt_cache_key` is an
219
+ optional, additive request field: it sharpens prefix-cache routing on model
220
+ families that support it (GPT-5.6 and later) and is otherwise inert, so the
221
+ same key is sent to every OpenAI model without a version gate. This mirrors
222
+ the Fireworks path (`_with_fireworks_session_settings`), which sets
223
+ `prompt_cache_key` the same way and additionally sends an
224
+ `x-session-affinity` header.
225
+
226
+ A user-supplied `prompt_cache_key` is always preserved, whether it was
227
+ configured on the model (`model_kwargs`) or supplied for this invocation
228
+ (`model_settings`).
229
+
230
+ Returns:
231
+ A new `model_settings` dict with `prompt_cache_key` added, or `None` when
232
+ a key is already present on the model or in the settings (nothing to
233
+ add).
234
+ """
235
+ model_kwargs = getattr(model, "model_kwargs", None)
236
+ if model_kwargs is not None and not isinstance(model_kwargs, Mapping):
237
+ # A non-mapping `model_kwargs` cannot carry a user-supplied key, so it is
238
+ # treated as "no key present" and injection proceeds. Trace the anomaly
239
+ # since a real `ChatOpenAI` always exposes a mapping here.
240
+ logger.debug(
241
+ "Ignoring non-mapping model_kwargs (%s) when checking for a "
242
+ "user-supplied prompt_cache_key",
243
+ type(model_kwargs).__name__,
244
+ )
245
+ if "prompt_cache_key" in model_settings or (
246
+ isinstance(model_kwargs, Mapping) and "prompt_cache_key" in model_kwargs
247
+ ):
248
+ return None
249
+ return {**model_settings, "prompt_cache_key": thread_id}
250
+
251
+
252
+ def _get_context(request: ModelRequest) -> CLIContextSchema | None:
253
+ """Return runtime context when it matches the CLI context shape."""
254
+ runtime = request.runtime
255
+ if runtime is None:
256
+ return None
257
+
258
+ ctx = runtime.context
259
+ if isinstance(ctx, CLIContextSchema):
260
+ return ctx
261
+ if isinstance(ctx, dict):
262
+ raw_key = ctx.get("approval_mode_key")
263
+ raw_thread_id = ctx.get("thread_id")
264
+ return CLIContextSchema(
265
+ model=ctx.get("model"),
266
+ model_params=ctx.get("model_params") or {},
267
+ auto_approve=bool(ctx.get("auto_approve", False)),
268
+ approval_mode_key=raw_key if isinstance(raw_key, str) else None,
269
+ thread_id=raw_thread_id if isinstance(raw_thread_id, str) else None,
270
+ )
271
+ return None
272
+
273
+
274
+ def _model_spec_from_model(model: BaseChatModel) -> str | None:
275
+ """Return a resumable `provider:model` spec for a model object."""
276
+ provider = _get_ls_provider(model)
277
+ model_name = get_model_identifier(model)
278
+ if provider and model_name:
279
+ return f"{provider}:{model_name}"
280
+
281
+ from deepagents_code.config import settings
282
+
283
+ settings_provider = settings.model_provider or ""
284
+ settings_model = settings.model_name or ""
285
+ if settings_provider and settings_model:
286
+ return f"{settings_provider}:{settings_model}"
287
+ return None
288
+
289
+
290
+ def _model_spec_from_result(
291
+ model_result: ModelResult | None, model: BaseChatModel
292
+ ) -> str | None:
293
+ """Return the resolved spec from `create_model`, falling back to model metadata."""
294
+ if model_result is not None and model_result.provider and model_result.model_name:
295
+ return f"{model_result.provider}:{model_result.model_name}"
296
+ return _model_spec_from_model(model)
297
+
298
+
299
+ def _build_overrides(
300
+ request: ModelRequest, ctx: CLIContextSchema, model_result: ModelResult | None
301
+ ) -> ModelRequest:
302
+ """Build the overridden request from a (possibly resolved) model result.
303
+
304
+ Holds the post-construction logic shared by the sync and async override
305
+ paths: applying the model swap, merging `model_params`, stripping
306
+ Anthropic-only settings on a cross-provider swap, and patching the
307
+ `### Model Identity` system-prompt section. The only thing that differs
308
+ between the two callers is how `model_result` is produced (a direct
309
+ `create_model` call vs. an `asyncio.to_thread` offload).
310
+
311
+ Args:
312
+ request: The incoming model request from the middleware chain.
313
+ ctx: Runtime CLI context carrying the requested overrides.
314
+ model_result: The resolved model result from `create_model`, or `None`
315
+ when no model swap was requested.
316
+
317
+ Returns:
318
+ The original request when no overrides apply, otherwise a new request
319
+ with overrides applied via `request.override()`.
320
+ """
321
+ overrides: dict[str, Any] = {}
322
+
323
+ new_model = model_result.model if model_result is not None else None
324
+ if new_model is not None:
325
+ overrides["model"] = new_model
326
+
327
+ # Param merge
328
+ model_params = ctx.model_params
329
+ if model_params:
330
+ overrides["model_settings"] = {**request.model_settings, **model_params}
331
+
332
+ # Inject the provider's prompt-cache routing hint from the active thread.
333
+ # Only one provider path applies per call; both share the fetch/guard/log
334
+ # tail below. `overrides.get` is side-effect-free, so resolving `settings`
335
+ # before the provider check is equivalent to doing it inside each branch.
336
+ effective_model = new_model if new_model is not None else request.model
337
+ if ctx.thread_id:
338
+ settings = overrides.get("model_settings", request.model_settings)
339
+ if _is_fireworks_model(effective_model):
340
+ updated_settings = _with_fireworks_session_settings(settings, ctx.thread_id)
341
+ injected = "Fireworks session settings"
342
+ elif _is_openai_model(effective_model):
343
+ updated_settings = _with_openai_prompt_cache_key(
344
+ effective_model, settings, ctx.thread_id
345
+ )
346
+ injected = "OpenAI prompt_cache_key"
347
+ else:
348
+ updated_settings = None
349
+ injected = ""
350
+ if updated_settings is not None:
351
+ overrides["model_settings"] = updated_settings
352
+ # The thread ID is a sensitive session identifier, so it is kept out
353
+ # of the log line; the line firing at all confirms injection ran.
354
+ logger.debug("Injected %s", injected)
355
+
356
+ if not overrides:
357
+ return request
358
+
359
+ # When switching away from Anthropic, strip provider-specific settings
360
+ # that would cause errors on other providers (e.g. cache_control passed
361
+ # to the OpenAI SDK raises TypeError).
362
+ if new_model is not None and not _is_anthropic_model(new_model):
363
+ settings = overrides.get("model_settings", request.model_settings)
364
+ dropped = settings.keys() & _ANTHROPIC_ONLY_SETTINGS
365
+ if dropped:
366
+ logger.debug(
367
+ "Stripped Anthropic-only settings %s for non-Anthropic model",
368
+ dropped,
369
+ )
370
+ overrides["model_settings"] = {
371
+ k: v for k, v in settings.items() if k not in dropped
372
+ }
373
+
374
+ # Patch the Model Identity section in the system prompt so the new model
375
+ # sees its own name/provider/context-limit, not the original's.
376
+ # We read metadata from model_result (not the app's settings singleton)
377
+ # because the middleware runs in the server subprocess where settings
378
+ # are never updated by /model.
379
+ if model_result is not None and request.system_prompt:
380
+ from deepagents_code.agent import (
381
+ MODEL_IDENTITY_RE,
382
+ build_model_identity_section,
383
+ )
384
+
385
+ prompt = request.system_prompt
386
+ new_identity = build_model_identity_section(
387
+ model_result.model_name,
388
+ provider=model_result.provider,
389
+ context_limit=model_result.context_limit,
390
+ unsupported_modalities=model_result.unsupported_modalities,
391
+ )
392
+ patched = MODEL_IDENTITY_RE.sub(new_identity, prompt, count=1)
393
+ if patched != prompt:
394
+ overrides["system_prompt"] = patched
395
+ elif "### Model Identity" in prompt:
396
+ logger.warning(
397
+ "System prompt contains '### Model Identity' but regex "
398
+ "did not match; identity section was NOT updated for "
399
+ "model '%s'. The regex may be out of sync with the "
400
+ "prompt template.",
401
+ model_result.model_name,
402
+ )
403
+
404
+ return request.override(**overrides)
405
+
406
+
407
+ def _apply_overrides(request: ModelRequest) -> _ResolvedModelRequest:
408
+ """Apply model/param overrides and return checkpoint persistence metadata.
409
+
410
+ Reads `'model'` and `'model_params'` from `runtime.context` and, when
411
+ present, swaps the model and/or merges extra settings into the request.
412
+ On a cross-provider swap away from Anthropic, Anthropic-only settings
413
+ (e.g. `cache_control`) are stripped. The `### Model Identity` section
414
+ in the system prompt is also patched to reflect the new model.
415
+
416
+ Args:
417
+ request: The incoming model request from the middleware chain.
418
+
419
+ Returns:
420
+ The request to send downstream plus the actual model spec and user-supplied
421
+ model params that should be recorded for resume.
422
+ """
423
+ ctx = _get_context(request)
424
+ if ctx is None:
425
+ return _ResolvedModelRequest(request, _model_spec_from_model(request.model))
426
+
427
+ model_result = None
428
+ model = ctx.model
429
+ if model and not model_matches_spec(request.model, model):
430
+ from deepagents_code.config import create_model
431
+ from deepagents_code.model_config import ModelConfigError
432
+
433
+ logger.debug("Overriding model to %s", model)
434
+ try:
435
+ model_result = create_model(model)
436
+ except ModelConfigError:
437
+ logger.exception(
438
+ "Failed to resolve runtime model override '%s'; "
439
+ "continuing with current model",
440
+ model,
441
+ )
442
+ return _ResolvedModelRequest(
443
+ request,
444
+ _model_spec_from_model(request.model),
445
+ model_params_known=True,
446
+ )
447
+
448
+ updated = _build_overrides(request, ctx, model_result)
449
+ params = dict(ctx.model_params) if ctx.model_params else None
450
+ return _ResolvedModelRequest(
451
+ updated,
452
+ _model_spec_from_result(model_result, updated.model),
453
+ params,
454
+ model_params_known=True,
455
+ )
456
+
457
+
458
+ async def _apply_overrides_async(request: ModelRequest) -> _ResolvedModelRequest:
459
+ """Async variant of `_apply_overrides` that offloads model construction.
460
+
461
+ Returns:
462
+ The request to send downstream plus the actual model spec and user-supplied
463
+ model params that should be recorded for resume.
464
+ """
465
+ ctx = _get_context(request)
466
+ if ctx is None:
467
+ return _ResolvedModelRequest(request, _model_spec_from_model(request.model))
468
+
469
+ model_result = None
470
+ model = ctx.model
471
+ if model and not model_matches_spec(request.model, model):
472
+ from deepagents_code.config import create_model
473
+ from deepagents_code.model_config import ModelConfigError
474
+
475
+ logger.debug("Overriding model to %s", model)
476
+ try:
477
+ model_result = await asyncio.to_thread(create_model, model)
478
+ except ModelConfigError:
479
+ logger.exception(
480
+ "Failed to resolve runtime model override '%s'; "
481
+ "continuing with current model",
482
+ model,
483
+ )
484
+ return _ResolvedModelRequest(
485
+ request,
486
+ _model_spec_from_model(request.model),
487
+ model_params_known=True,
488
+ )
489
+
490
+ updated = _build_overrides(request, ctx, model_result)
491
+ params = dict(ctx.model_params) if ctx.model_params else None
492
+ return _ResolvedModelRequest(
493
+ updated,
494
+ _model_spec_from_result(model_result, updated.model),
495
+ params,
496
+ model_params_known=True,
497
+ )
498
+
499
+
500
+ def _checkpoint_command(resolved: _ResolvedModelRequest) -> Command[Any] | None:
501
+ """Build the private resume-state update for a completed model call.
502
+
503
+ Returns:
504
+ Command with private checkpoint updates, or `None` when nothing is known.
505
+ """
506
+ update: dict[str, Any] = {}
507
+ if resolved.model_spec:
508
+ update["_model_spec"] = resolved.model_spec
509
+ if resolved.model_params_known:
510
+ update["_model_params"] = resolved.model_params
511
+ if not update:
512
+ return None
513
+ return Command(update=update)
514
+
515
+
516
+ class ConfigurableModelMiddleware(AgentMiddleware):
517
+ """Swap the model or per-call settings from `runtime.context`.
518
+
519
+ Reads two optional keys from the runtime context dict:
520
+
521
+ - `'model'` — a `provider:model` spec (e.g. `"openai:gpt-5"`).
522
+ When present and different from the current model, the request is
523
+ re-routed to the new model.
524
+ - `'model_params'` — a dict of extra model settings (e.g.
525
+ `{"temperature": 0}`) that are shallow-merged into the
526
+ request's `model_settings`.
527
+
528
+ This middleware is typically the outermost layer so it intercepts every
529
+ model call before provider-specific middleware (like
530
+ `AnthropicPromptCachingMiddleware`) runs.
531
+ """
532
+
533
+ def __init__(self, *, persist_model_state: bool = True) -> None:
534
+ """Initialize the middleware.
535
+
536
+ Args:
537
+ persist_model_state: Whether completed calls should write private
538
+ resume metadata. Subagent instances disable this because they do
539
+ not own the parent thread's resume state.
540
+ """
541
+ self._persist_model_state = persist_model_state
542
+
543
+ def wrap_model_call(
544
+ self,
545
+ request: ModelRequest,
546
+ handler: Callable[[ModelRequest], ModelResponse],
547
+ ) -> ModelResponse | ExtendedModelResponse:
548
+ """Apply runtime overrides and delegate to the next handler.
549
+
550
+ Returns:
551
+ The downstream response plus a private resume-state update when the
552
+ completed call has model metadata to checkpoint.
553
+ """
554
+ resolved = _apply_overrides(request)
555
+ response = handler(resolved.request)
556
+ command = _checkpoint_command(resolved) if self._persist_model_state else None
557
+ if command is None:
558
+ return response
559
+ return ExtendedModelResponse(model_response=response, command=command)
560
+
561
+ async def awrap_model_call(
562
+ self,
563
+ request: ModelRequest,
564
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
565
+ ) -> ModelResponse | ExtendedModelResponse:
566
+ """Apply runtime overrides and delegate to the next async handler.
567
+
568
+ Returns:
569
+ The downstream response plus a private resume-state update when the
570
+ completed call has model metadata to checkpoint.
571
+ """
572
+ resolved = await _apply_overrides_async(request)
573
+ response = await handler(resolved.request)
574
+ command = _checkpoint_command(resolved) if self._persist_model_state else None
575
+ if command is None:
576
+ return response
577
+ return ExtendedModelResponse(model_response=response, command=command)
@@ -0,0 +1,12 @@
1
+ # Project Notes
2
+
3
+ This file is for tracking project-specific context as you work.
4
+ You can update this file to remember decisions, patterns, and context about this project.
5
+
6
+ ## Architecture Notes
7
+
8
+ (Add notes about project structure, key files, patterns as you discover them)
9
+
10
+ ## Decisions
11
+
12
+ (Track important decisions and their rationale)