talkops-opscloud 0.3.0b1__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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Reasoning effort support and provider-native thinking configuration.
|
|
2
|
+
|
|
3
|
+
Translates standard `reasoning_effort` ('low', 'medium', 'high', 'max')
|
|
4
|
+
into provider-native request shapes:
|
|
5
|
+
- Google GenAI (Gemini): `include_thoughts=True`, `thinking_level="HIGH"`, `thinking_budget=8192`
|
|
6
|
+
- Anthropic (Claude): `thinking={"type": "enabled", "budget_tokens": 8192}`
|
|
7
|
+
- OpenAI: `reasoning={"effort": "high"}`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from opscloud.model.config import (
|
|
16
|
+
MODEL_PROFILES,
|
|
17
|
+
ModelSpec,
|
|
18
|
+
detect_provider,
|
|
19
|
+
get_model_profile as _config_get_model_profile,
|
|
20
|
+
get_model_profiles,
|
|
21
|
+
normalize_model_spec,
|
|
22
|
+
)
|
|
23
|
+
from opscloud.utils.logger import get_logger
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
SUPPORTED_EFFORT_LEVELS = ("none", "off", "minimal", "low", "medium", "high", "xhigh", "max")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_provider(model_spec: str | None) -> str:
|
|
31
|
+
"""Resolve provider name from model spec string, supporting bare or prefixed formats."""
|
|
32
|
+
if not model_spec:
|
|
33
|
+
return ""
|
|
34
|
+
parsed = ModelSpec.try_parse(model_spec)
|
|
35
|
+
if parsed is not None and parsed.provider:
|
|
36
|
+
return parsed.provider
|
|
37
|
+
return detect_provider(model_spec) or ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_model_profile(spec: str) -> dict[str, Any] | None:
|
|
41
|
+
"""Return model profile for spec."""
|
|
42
|
+
if not spec:
|
|
43
|
+
return None
|
|
44
|
+
entry = _config_get_model_profile(spec)
|
|
45
|
+
if entry and entry.get("profile"):
|
|
46
|
+
return dict(entry["profile"])
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _model_profile(
|
|
51
|
+
model_spec: str | None, *, cli_override: dict[str, Any] | None = None
|
|
52
|
+
) -> Mapping[str, Any] | None:
|
|
53
|
+
"""Return the reasoning-capable profile for `model_spec`."""
|
|
54
|
+
if not model_spec:
|
|
55
|
+
return None
|
|
56
|
+
entry = get_model_profiles(cli_override=cli_override).get(model_spec)
|
|
57
|
+
if entry is None:
|
|
58
|
+
entry = _config_get_model_profile(model_spec)
|
|
59
|
+
profile = cli_override if entry is None else entry.get("profile")
|
|
60
|
+
if profile is None:
|
|
61
|
+
return None
|
|
62
|
+
if not isinstance(profile, Mapping):
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Ignoring model profile for %s with unexpected type %s",
|
|
65
|
+
model_spec,
|
|
66
|
+
type(profile).__name__,
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
reasoning_output = profile.get("reasoning_output")
|
|
70
|
+
if reasoning_output is not None and not isinstance(reasoning_output, bool):
|
|
71
|
+
logger.warning(
|
|
72
|
+
"Ignoring reasoning_output for %s with unexpected type %s",
|
|
73
|
+
model_spec,
|
|
74
|
+
type(reasoning_output).__name__,
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
if reasoning_output is not True:
|
|
78
|
+
return None
|
|
79
|
+
return profile
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def supported_efforts_for_model(
|
|
83
|
+
model_spec: str | None, *, cli_override: dict[str, Any] | None = None
|
|
84
|
+
) -> tuple[str, ...]:
|
|
85
|
+
"""Return the ordered reasoning effort levels supported by `model_spec`."""
|
|
86
|
+
profile = _model_profile(model_spec, cli_override=cli_override)
|
|
87
|
+
if profile is None or "reasoning_effort_levels" not in profile:
|
|
88
|
+
return ()
|
|
89
|
+
levels = profile["reasoning_effort_levels"]
|
|
90
|
+
if not isinstance(levels, list):
|
|
91
|
+
return ()
|
|
92
|
+
for level in levels:
|
|
93
|
+
if not isinstance(level, str):
|
|
94
|
+
return ()
|
|
95
|
+
return tuple(levels)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def default_effort_for_model(
|
|
99
|
+
model_spec: str | None, *, cli_override: dict[str, Any] | None = None
|
|
100
|
+
) -> str | None:
|
|
101
|
+
"""Return the profile's reasoning effort default independently of its levels."""
|
|
102
|
+
profile = _model_profile(model_spec, cli_override=cli_override)
|
|
103
|
+
if profile is None or "reasoning_effort_default" not in profile:
|
|
104
|
+
return None
|
|
105
|
+
default = profile.get("reasoning_effort_default")
|
|
106
|
+
if default is not None:
|
|
107
|
+
return str(default)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_effort_supported_for_model(
|
|
112
|
+
model_spec: str, effort: str, *, cli_override: dict[str, Any] | None = None
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""Check whether a given reasoning effort level is supported by the specified model."""
|
|
115
|
+
if not model_spec:
|
|
116
|
+
return False
|
|
117
|
+
supported = supported_efforts_for_model(model_spec, cli_override=cli_override)
|
|
118
|
+
return any(s.casefold() == effort.casefold() for s in supported)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _str_or_none(value: object) -> str | None:
|
|
122
|
+
if value is None:
|
|
123
|
+
return None
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
return value
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _effort_paths(provider: str) -> tuple[tuple[str, ...], ...]:
|
|
130
|
+
if provider in ("openai", "openai_codex", "azure_openai"):
|
|
131
|
+
return (("reasoning", "effort"), ("reasoning_effort",))
|
|
132
|
+
if provider == "anthropic":
|
|
133
|
+
return (("effort",), ("reasoning_effort",), ("output_config", "effort"), ("thinking",))
|
|
134
|
+
if provider in ("google_genai", "google", "google_vertexai"):
|
|
135
|
+
return (("thinking_level",), ("reasoning_effort",), ("thinking_config", "thinking_level"), ("include_thoughts",))
|
|
136
|
+
return (("reasoning_effort",),)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _path_is_present(model_params: Mapping[str, Any], path: tuple[str, ...]) -> bool:
|
|
140
|
+
if len(path) == 1:
|
|
141
|
+
return path[0] in model_params
|
|
142
|
+
nested = model_params.get(path[0])
|
|
143
|
+
return isinstance(nested, Mapping) and path[1] in nested
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def has_explicit_effort_model_params(model_spec: str | None, model_params: dict[str, Any] | None) -> bool:
|
|
147
|
+
"""Return whether canonical or native effort parameters are present."""
|
|
148
|
+
if not model_spec or not model_params:
|
|
149
|
+
return False
|
|
150
|
+
provider = _resolve_provider(model_spec)
|
|
151
|
+
return any(_path_is_present(model_params, path) for path in _effort_paths(provider))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def current_effort_from_model_params(model_spec: str | None, model_params: dict[str, Any] | None) -> str | None:
|
|
155
|
+
"""Read canonical or native effort settings using integration precedence."""
|
|
156
|
+
if not model_spec or not model_params:
|
|
157
|
+
return None
|
|
158
|
+
provider = _resolve_provider(model_spec)
|
|
159
|
+
|
|
160
|
+
if provider in {"openai", "openai_codex", "azure_openai"}:
|
|
161
|
+
reasoning = model_params.get("reasoning")
|
|
162
|
+
if isinstance(reasoning, Mapping) and "effort" in reasoning:
|
|
163
|
+
return _str_or_none(reasoning["effort"])
|
|
164
|
+
elif provider == "anthropic":
|
|
165
|
+
if "effort" in model_params and model_params["effort"] is not None:
|
|
166
|
+
return _str_or_none(model_params["effort"])
|
|
167
|
+
output_config = model_params.get("output_config")
|
|
168
|
+
if isinstance(output_config, Mapping) and "effort" in output_config:
|
|
169
|
+
return _str_or_none(output_config["effort"])
|
|
170
|
+
thinking = model_params.get("thinking")
|
|
171
|
+
if isinstance(thinking, Mapping):
|
|
172
|
+
budget = thinking.get("budget_tokens")
|
|
173
|
+
if budget:
|
|
174
|
+
budget_map = {1024: "low", 2048: "low", 4096: "medium", 8192: "high", 16384: "max"}
|
|
175
|
+
if budget in budget_map:
|
|
176
|
+
return budget_map[budget]
|
|
177
|
+
elif provider in {"google_genai", "google", "google_vertexai"}:
|
|
178
|
+
if model_params.get("thinking_budget") == 0 or model_params.get("include_thoughts") is False:
|
|
179
|
+
return "off"
|
|
180
|
+
if "reasoning_effort" in model_params and model_params["reasoning_effort"] is not None:
|
|
181
|
+
return _str_or_none(model_params["reasoning_effort"])
|
|
182
|
+
if "thinking_level" in model_params and model_params["thinking_level"] is not None:
|
|
183
|
+
return _str_or_none(model_params["thinking_level"])
|
|
184
|
+
thinking_config = model_params.get("thinking_config")
|
|
185
|
+
if isinstance(thinking_config, Mapping) and "thinking_level" in thinking_config:
|
|
186
|
+
return _str_or_none(thinking_config["thinking_level"])
|
|
187
|
+
elif provider == "fireworks":
|
|
188
|
+
mk = model_params.get("model_kwargs")
|
|
189
|
+
if isinstance(mk, Mapping) and "reasoning_effort" in mk:
|
|
190
|
+
return _str_or_none(mk["reasoning_effort"])
|
|
191
|
+
elif provider == "xai":
|
|
192
|
+
eb = model_params.get("extra_body")
|
|
193
|
+
if isinstance(eb, Mapping) and "reasoning_effort" in eb:
|
|
194
|
+
return _str_or_none(eb["reasoning_effort"])
|
|
195
|
+
|
|
196
|
+
return _str_or_none(model_params.get("reasoning_effort"))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _remove_nested_key(params: dict[str, Any], container: str, key: str) -> None:
|
|
200
|
+
nested = params.get(container)
|
|
201
|
+
if not isinstance(nested, Mapping):
|
|
202
|
+
return
|
|
203
|
+
remaining = dict(nested)
|
|
204
|
+
remaining.pop(key, None)
|
|
205
|
+
if remaining:
|
|
206
|
+
params[container] = remaining
|
|
207
|
+
else:
|
|
208
|
+
params.pop(container, None)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def without_effort_model_params(model_spec: str | None, existing: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
212
|
+
"""Remove canonical and native effort settings without changing siblings."""
|
|
213
|
+
if not existing:
|
|
214
|
+
return None
|
|
215
|
+
cleaned = dict(existing)
|
|
216
|
+
cleaned.pop("reasoning_effort", None)
|
|
217
|
+
|
|
218
|
+
provider = _resolve_provider(model_spec)
|
|
219
|
+
if provider in ("openai", "openai_codex", "azure_openai"):
|
|
220
|
+
_remove_nested_key(cleaned, "reasoning", "effort")
|
|
221
|
+
elif provider == "anthropic":
|
|
222
|
+
cleaned.pop("effort", None)
|
|
223
|
+
_remove_nested_key(cleaned, "output_config", "effort")
|
|
224
|
+
cleaned.pop("thinking", None)
|
|
225
|
+
elif provider in ("google_genai", "google", "google_vertexai"):
|
|
226
|
+
cleaned.pop("thinking_level", None)
|
|
227
|
+
cleaned.pop("thinking_budget", None)
|
|
228
|
+
cleaned.pop("include_thoughts", None)
|
|
229
|
+
_remove_nested_key(cleaned, "thinking_config", "thinking_level")
|
|
230
|
+
_remove_nested_key(cleaned, "thinking_config", "thinking_budget")
|
|
231
|
+
elif provider == "fireworks":
|
|
232
|
+
_remove_nested_key(cleaned, "model_kwargs", "reasoning_effort")
|
|
233
|
+
elif provider == "xai":
|
|
234
|
+
_remove_nested_key(cleaned, "extra_body", "reasoning_effort")
|
|
235
|
+
return cleaned or None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def with_effort_model_params(model_spec: str | None, existing: dict[str, Any] | None, effort: str) -> dict[str, Any]:
|
|
239
|
+
"""Replace existing effort settings with the standard flat parameter or provider-native parameter.
|
|
240
|
+
|
|
241
|
+
If effort is off/none/clear/0/reset, or if the model does not support reasoning effort,
|
|
242
|
+
returns a cleaned parameter dictionary with all reasoning and thinking keys stripped.
|
|
243
|
+
"""
|
|
244
|
+
updated = without_effort_model_params(model_spec, existing) or {}
|
|
245
|
+
eff_lower = effort.lower()
|
|
246
|
+
|
|
247
|
+
# Disabling / clearing reasoning
|
|
248
|
+
if eff_lower in ("off", "none", "clear", "0", "reset", ""):
|
|
249
|
+
return updated
|
|
250
|
+
|
|
251
|
+
# If model is specified and does not support this effort level, do not inject effort
|
|
252
|
+
if model_spec and not is_effort_supported_for_model(model_spec, effort):
|
|
253
|
+
return updated
|
|
254
|
+
|
|
255
|
+
provider = _resolve_provider(model_spec)
|
|
256
|
+
if provider in ("google_genai", "google", "google_vertexai"):
|
|
257
|
+
valid_level = eff_lower if eff_lower in ("minimal", "low", "medium", "high") else "medium"
|
|
258
|
+
updated["reasoning_effort"] = valid_level
|
|
259
|
+
updated["include_thoughts"] = True
|
|
260
|
+
elif provider == "anthropic":
|
|
261
|
+
updated["reasoning_effort"] = effort
|
|
262
|
+
budget = {
|
|
263
|
+
"low": 1024,
|
|
264
|
+
"medium": 4096,
|
|
265
|
+
"high": 8192,
|
|
266
|
+
"max": 16384,
|
|
267
|
+
}.get(eff_lower, 4096)
|
|
268
|
+
updated["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
|
269
|
+
elif provider in ("openai", "openai_codex", "azure_openai"):
|
|
270
|
+
updated["reasoning_effort"] = effort
|
|
271
|
+
elif provider == "fireworks":
|
|
272
|
+
mk = dict(updated.get("model_kwargs", {}))
|
|
273
|
+
mk["reasoning_effort"] = effort
|
|
274
|
+
updated["model_kwargs"] = mk
|
|
275
|
+
elif provider == "xai":
|
|
276
|
+
eb = dict(updated.get("extra_body", {}))
|
|
277
|
+
eb["reasoning_effort"] = effort
|
|
278
|
+
updated["extra_body"] = eb
|
|
279
|
+
else:
|
|
280
|
+
updated["reasoning_effort"] = effort
|
|
281
|
+
|
|
282
|
+
return updated
|
opscloud/offload.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Storage paths for offloaded conversation history in OpsCode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path, PurePath
|
|
10
|
+
|
|
11
|
+
from opscloud.config.paths import DATA_DIR
|
|
12
|
+
from opscloud.utils.logger import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
_FALLBACK_ARTIFACTS_ROOT = "/opscloud-artifacts-fallback"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class _ArtifactsStorage:
|
|
21
|
+
"""Agent-visible artifacts root and optional routed large-result directory."""
|
|
22
|
+
|
|
23
|
+
root: str
|
|
24
|
+
large_results_dir: Path | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _filesystem_tool_path(path: PurePath) -> str:
|
|
28
|
+
"""Represent an absolute host path in the filesystem tool path format."""
|
|
29
|
+
normalized = path.as_posix()
|
|
30
|
+
if path.drive and not path.drive.startswith("\\\\"):
|
|
31
|
+
return f"//?/{normalized}"
|
|
32
|
+
return normalized
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_EPHEMERAL_OFFLOAD_STORAGE = False
|
|
36
|
+
_UNIQUE_OFFLOAD_FALLBACK_ROOT: Path | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def offload_storage_is_ephemeral() -> bool:
|
|
40
|
+
"""Return whether offload history is routed to non-persistent storage."""
|
|
41
|
+
return _EPHEMERAL_OFFLOAD_STORAGE
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _harden_dir(path: Path) -> None:
|
|
45
|
+
"""Create `path` if needed and restrict it to the current user."""
|
|
46
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
47
|
+
info = path.lstat()
|
|
48
|
+
if not stat.S_ISDIR(info.st_mode):
|
|
49
|
+
msg = f"Path is not a directory: {path}"
|
|
50
|
+
raise OSError(msg)
|
|
51
|
+
getuid = getattr(os, "getuid", None)
|
|
52
|
+
if getuid is not None and info.st_uid != getuid():
|
|
53
|
+
msg = f"Directory is owned by another user: {path}"
|
|
54
|
+
raise PermissionError(msg)
|
|
55
|
+
path.chmod(0o700)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _probe_writable(path: Path) -> None:
|
|
59
|
+
"""Confirm `path` accepts new files (catches read-only mounts)."""
|
|
60
|
+
with tempfile.NamedTemporaryFile(dir=path, prefix=".write-test-"):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _artifacts_root() -> _ArtifactsStorage:
|
|
65
|
+
"""Return storage configuration for offloaded artifacts."""
|
|
66
|
+
getuid = getattr(os, "getuid", None)
|
|
67
|
+
suffix = str(getuid()) if getuid is not None else str(os.getpid())
|
|
68
|
+
temp_root = Path(tempfile.gettempdir())
|
|
69
|
+
root = temp_root / f"opscloud-artifacts-{suffix}"
|
|
70
|
+
try:
|
|
71
|
+
_harden_dir(root)
|
|
72
|
+
_probe_writable(root)
|
|
73
|
+
except (OSError, RuntimeError):
|
|
74
|
+
logger.warning(
|
|
75
|
+
"Predictable per-user artifacts directory is unavailable; routing "
|
|
76
|
+
"large results from a stable virtual prefix to private temporary storage",
|
|
77
|
+
exc_info=True,
|
|
78
|
+
)
|
|
79
|
+
unique = Path(
|
|
80
|
+
tempfile.mkdtemp(prefix=f"opscloud-artifacts-{suffix}-", dir=temp_root)
|
|
81
|
+
)
|
|
82
|
+
_harden_dir(unique)
|
|
83
|
+
_probe_writable(unique)
|
|
84
|
+
return _ArtifactsStorage(
|
|
85
|
+
root=_FALLBACK_ARTIFACTS_ROOT,
|
|
86
|
+
large_results_dir=unique,
|
|
87
|
+
)
|
|
88
|
+
return _ArtifactsStorage(root=_filesystem_tool_path(root))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _offload_fallback_root() -> Path:
|
|
92
|
+
"""Return a writable base directory for offloaded conversation history."""
|
|
93
|
+
|
|
94
|
+
def _prepare_user_dir() -> Path:
|
|
95
|
+
base = DATA_DIR
|
|
96
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
archive_dir = base / "conversation_history"
|
|
98
|
+
_harden_dir(archive_dir)
|
|
99
|
+
_probe_writable(archive_dir)
|
|
100
|
+
return base
|
|
101
|
+
|
|
102
|
+
def _prepare_temp_dir(path: Path) -> Path:
|
|
103
|
+
_harden_dir(path)
|
|
104
|
+
_probe_writable(path)
|
|
105
|
+
return path
|
|
106
|
+
|
|
107
|
+
global _EPHEMERAL_OFFLOAD_STORAGE, _UNIQUE_OFFLOAD_FALLBACK_ROOT # noqa: PLW0603
|
|
108
|
+
if _UNIQUE_OFFLOAD_FALLBACK_ROOT is not None:
|
|
109
|
+
_EPHEMERAL_OFFLOAD_STORAGE = True
|
|
110
|
+
return _UNIQUE_OFFLOAD_FALLBACK_ROOT
|
|
111
|
+
try:
|
|
112
|
+
root = _prepare_user_dir()
|
|
113
|
+
except (RuntimeError, OSError):
|
|
114
|
+
logger.warning(
|
|
115
|
+
"User data directory is not writable; falling back to temporary "
|
|
116
|
+
"offload storage, which may not persist across restarts",
|
|
117
|
+
exc_info=True,
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
_EPHEMERAL_OFFLOAD_STORAGE = False
|
|
121
|
+
return root
|
|
122
|
+
|
|
123
|
+
_EPHEMERAL_OFFLOAD_STORAGE = True
|
|
124
|
+
getuid = getattr(os, "getuid", None)
|
|
125
|
+
suffix = str(getuid()) if getuid is not None else str(os.getpid())
|
|
126
|
+
temp_root = Path(tempfile.gettempdir())
|
|
127
|
+
path = temp_root / f"opscloud-{suffix}"
|
|
128
|
+
try:
|
|
129
|
+
return _prepare_temp_dir(path)
|
|
130
|
+
except (OSError, RuntimeError):
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Per-user temporary offload directory is unavailable; creating "
|
|
133
|
+
"a private unique directory",
|
|
134
|
+
exc_info=True,
|
|
135
|
+
)
|
|
136
|
+
unique = Path(tempfile.mkdtemp(prefix=f"opscloud-{suffix}-", dir=temp_root))
|
|
137
|
+
_UNIQUE_OFFLOAD_FALLBACK_ROOT = _prepare_temp_dir(unique)
|
|
138
|
+
return _UNIQUE_OFFLOAD_FALLBACK_ROOT
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def delete_offloaded_history(thread_id: str) -> bool:
|
|
142
|
+
"""Remove a thread's offloaded conversation-history archive."""
|
|
143
|
+
if not thread_id:
|
|
144
|
+
return False
|
|
145
|
+
try:
|
|
146
|
+
archive_dir = _offload_fallback_root() / "conversation_history"
|
|
147
|
+
except (OSError, RuntimeError):
|
|
148
|
+
logger.warning(
|
|
149
|
+
"Could not resolve offload root to clean history for thread %s",
|
|
150
|
+
thread_id,
|
|
151
|
+
exc_info=True,
|
|
152
|
+
)
|
|
153
|
+
return False
|
|
154
|
+
archive_path = archive_dir / f"{thread_id}.md"
|
|
155
|
+
if archive_path.parent != archive_dir:
|
|
156
|
+
logger.warning(
|
|
157
|
+
"Refusing to delete offloaded history for suspicious thread id %r",
|
|
158
|
+
thread_id,
|
|
159
|
+
)
|
|
160
|
+
return False
|
|
161
|
+
try:
|
|
162
|
+
archive_path.unlink()
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
return False
|
|
165
|
+
except OSError:
|
|
166
|
+
logger.warning(
|
|
167
|
+
"Failed to delete offloaded conversation history for thread %s",
|
|
168
|
+
thread_id,
|
|
169
|
+
exc_info=True,
|
|
170
|
+
)
|
|
171
|
+
return False
|
|
172
|
+
logger.debug("Deleted offloaded conversation history for thread %s", thread_id)
|
|
173
|
+
return True
|
opscloud/output.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Machine-readable JSON output helpers for CLI subcommands.
|
|
2
|
+
|
|
3
|
+
This module deliberately stays stdlib-only so it can be imported from CLI
|
|
4
|
+
startup paths without pulling in unnecessary dependency trees.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
OutputFormat = Literal["text", "json"]
|
|
15
|
+
"""Accepted internal output modes for CLI subcommands."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_json_output_arg(
|
|
19
|
+
parser: argparse.ArgumentParser, *, default: OutputFormat | None = None
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Add a `--json` flag to an argparse parser.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
parser: Parser to update.
|
|
25
|
+
default: Default output format for this parser.
|
|
26
|
+
Pass `None` for subparsers so parent parser values are preserved.
|
|
27
|
+
"""
|
|
28
|
+
if default is None:
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--json",
|
|
31
|
+
dest="output_format",
|
|
32
|
+
action="store_const",
|
|
33
|
+
const="json",
|
|
34
|
+
default=argparse.SUPPRESS,
|
|
35
|
+
help="Emit machine-readable JSON for this command",
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--json",
|
|
40
|
+
dest="output_format",
|
|
41
|
+
action="store_const",
|
|
42
|
+
const="json",
|
|
43
|
+
default=default,
|
|
44
|
+
help="Emit machine-readable JSON for this command",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_json(command: str, data: list[Any] | dict[str, Any]) -> None:
|
|
49
|
+
"""Write a JSON envelope to stdout and flush.
|
|
50
|
+
|
|
51
|
+
The envelope is a single-line JSON object with a stable schema:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{"schema_version": 1, "command": "...", "data": ...}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
command: Self-documenting command name (e.g. `'list'`, `'threads list'`).
|
|
59
|
+
data: Payload — typically a list for listing commands or a dict
|
|
60
|
+
for action/info commands.
|
|
61
|
+
"""
|
|
62
|
+
envelope = {"schema_version": 1, "command": command, "data": data}
|
|
63
|
+
sys.stdout.write(json.dumps(envelope, default=str) + "\n")
|
|
64
|
+
sys.stdout.flush()
|