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,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)
|