ripperdoc 0.2.8__py3-none-any.whl → 0.2.9__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +28 -115
- ripperdoc/cli/commands/__init__.py +0 -1
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +5 -3
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +110 -59
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +23 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +82 -38
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +26 -16
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +8 -4
- ripperdoc/tools/file_read_tool.py +8 -4
- ripperdoc/tools/file_write_tool.py +9 -5
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +8 -4
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +1 -1
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
ripperdoc/core/hooks/manager.py
CHANGED
|
@@ -6,7 +6,7 @@ throughout the application lifecycle.
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
10
|
|
|
11
11
|
from ripperdoc.core.hooks.config import (
|
|
12
12
|
HooksConfig,
|
|
@@ -47,10 +47,7 @@ class HookResult:
|
|
|
47
47
|
@property
|
|
48
48
|
def should_block(self) -> bool:
|
|
49
49
|
"""Check if any hook returned a blocking decision."""
|
|
50
|
-
return any(
|
|
51
|
-
o.decision in (HookDecision.DENY, HookDecision.BLOCK)
|
|
52
|
-
for o in self.outputs
|
|
53
|
-
)
|
|
50
|
+
return any(o.decision in (HookDecision.DENY, HookDecision.BLOCK) for o in self.outputs)
|
|
54
51
|
|
|
55
52
|
@property
|
|
56
53
|
def should_allow(self) -> bool:
|
|
@@ -431,9 +428,7 @@ class HookManager:
|
|
|
431
428
|
|
|
432
429
|
# --- Notification ---
|
|
433
430
|
|
|
434
|
-
def run_notification(
|
|
435
|
-
self, message: str, notification_type: str = "info"
|
|
436
|
-
) -> HookResult:
|
|
431
|
+
def run_notification(self, message: str, notification_type: str = "info") -> HookResult:
|
|
437
432
|
"""Run Notification hooks synchronously.
|
|
438
433
|
|
|
439
434
|
Args:
|
|
@@ -478,11 +473,18 @@ class HookManager:
|
|
|
478
473
|
|
|
479
474
|
# --- Stop ---
|
|
480
475
|
|
|
481
|
-
def run_stop(
|
|
476
|
+
def run_stop(
|
|
477
|
+
self,
|
|
478
|
+
stop_hook_active: bool = False,
|
|
479
|
+
reason: Optional[str] = None,
|
|
480
|
+
stop_sequence: Optional[str] = None,
|
|
481
|
+
) -> HookResult:
|
|
482
482
|
"""Run Stop hooks synchronously.
|
|
483
483
|
|
|
484
484
|
Args:
|
|
485
485
|
stop_hook_active: True if already continuing from a stop hook
|
|
486
|
+
reason: Reason for stopping
|
|
487
|
+
stop_sequence: Stop sequence that triggered the stop
|
|
486
488
|
"""
|
|
487
489
|
hooks = self._get_hooks(HookEvent.STOP)
|
|
488
490
|
if not hooks:
|
|
@@ -490,6 +492,8 @@ class HookManager:
|
|
|
490
492
|
|
|
491
493
|
input_data = StopInput(
|
|
492
494
|
stop_hook_active=stop_hook_active,
|
|
495
|
+
reason=reason,
|
|
496
|
+
stop_sequence=stop_sequence,
|
|
493
497
|
session_id=self.session_id,
|
|
494
498
|
transcript_path=self.transcript_path,
|
|
495
499
|
cwd=self._get_cwd(),
|
|
@@ -499,7 +503,12 @@ class HookManager:
|
|
|
499
503
|
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
500
504
|
return HookResult(outputs)
|
|
501
505
|
|
|
502
|
-
async def run_stop_async(
|
|
506
|
+
async def run_stop_async(
|
|
507
|
+
self,
|
|
508
|
+
stop_hook_active: bool = False,
|
|
509
|
+
reason: Optional[str] = None,
|
|
510
|
+
stop_sequence: Optional[str] = None,
|
|
511
|
+
) -> HookResult:
|
|
503
512
|
"""Run Stop hooks asynchronously."""
|
|
504
513
|
hooks = self._get_hooks(HookEvent.STOP)
|
|
505
514
|
if not hooks:
|
|
@@ -507,6 +516,8 @@ class HookManager:
|
|
|
507
516
|
|
|
508
517
|
input_data = StopInput(
|
|
509
518
|
stop_hook_active=stop_hook_active,
|
|
519
|
+
reason=reason,
|
|
520
|
+
stop_sequence=stop_sequence,
|
|
510
521
|
session_id=self.session_id,
|
|
511
522
|
transcript_path=self.transcript_path,
|
|
512
523
|
cwd=self._get_cwd(),
|
|
@@ -558,9 +569,7 @@ class HookManager:
|
|
|
558
569
|
|
|
559
570
|
# --- Pre Compact ---
|
|
560
571
|
|
|
561
|
-
def run_pre_compact(
|
|
562
|
-
self, trigger: str, custom_instructions: str = ""
|
|
563
|
-
) -> HookResult:
|
|
572
|
+
def run_pre_compact(self, trigger: str, custom_instructions: str = "") -> HookResult:
|
|
564
573
|
"""Run PreCompact hooks synchronously.
|
|
565
574
|
|
|
566
575
|
Args:
|
|
@@ -645,11 +654,18 @@ class HookManager:
|
|
|
645
654
|
|
|
646
655
|
# --- Session End ---
|
|
647
656
|
|
|
648
|
-
def run_session_end(
|
|
657
|
+
def run_session_end(
|
|
658
|
+
self,
|
|
659
|
+
reason: str,
|
|
660
|
+
duration_seconds: Optional[float] = None,
|
|
661
|
+
message_count: Optional[int] = None,
|
|
662
|
+
) -> HookResult:
|
|
649
663
|
"""Run SessionEnd hooks synchronously.
|
|
650
664
|
|
|
651
665
|
Args:
|
|
652
666
|
reason: "clear", "logout", "prompt_input_exit", or "other"
|
|
667
|
+
duration_seconds: How long the session lasted
|
|
668
|
+
message_count: Number of messages in the session
|
|
653
669
|
"""
|
|
654
670
|
hooks = self._get_hooks(HookEvent.SESSION_END)
|
|
655
671
|
if not hooks:
|
|
@@ -657,6 +673,8 @@ class HookManager:
|
|
|
657
673
|
|
|
658
674
|
input_data = SessionEndInput(
|
|
659
675
|
reason=reason,
|
|
676
|
+
duration_seconds=duration_seconds,
|
|
677
|
+
message_count=message_count,
|
|
660
678
|
session_id=self.session_id,
|
|
661
679
|
transcript_path=self.transcript_path,
|
|
662
680
|
cwd=self._get_cwd(),
|
|
@@ -666,7 +684,12 @@ class HookManager:
|
|
|
666
684
|
outputs = self.executor.execute_hooks_sync(hooks, input_data)
|
|
667
685
|
return HookResult(outputs)
|
|
668
686
|
|
|
669
|
-
async def run_session_end_async(
|
|
687
|
+
async def run_session_end_async(
|
|
688
|
+
self,
|
|
689
|
+
reason: str,
|
|
690
|
+
duration_seconds: Optional[float] = None,
|
|
691
|
+
message_count: Optional[int] = None,
|
|
692
|
+
) -> HookResult:
|
|
670
693
|
"""Run SessionEnd hooks asynchronously."""
|
|
671
694
|
hooks = self._get_hooks(HookEvent.SESSION_END)
|
|
672
695
|
if not hooks:
|
|
@@ -674,6 +697,8 @@ class HookManager:
|
|
|
674
697
|
|
|
675
698
|
input_data = SessionEndInput(
|
|
676
699
|
reason=reason,
|
|
700
|
+
duration_seconds=duration_seconds,
|
|
701
|
+
message_count=message_count,
|
|
677
702
|
session_id=self.session_id,
|
|
678
703
|
transcript_path=self.transcript_path,
|
|
679
704
|
cwd=self._get_cwd(),
|
ripperdoc/core/permissions.py
CHANGED
|
@@ -26,8 +26,29 @@ class PermissionResult:
|
|
|
26
26
|
decision: Optional[PermissionDecision] = None
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _format_input_preview(parsed_input: Any) -> str:
|
|
30
|
-
"""Create a
|
|
29
|
+
def _format_input_preview(parsed_input: Any, tool_name: Optional[str] = None) -> str:
|
|
30
|
+
"""Create a human-friendly preview for prompts.
|
|
31
|
+
|
|
32
|
+
For Bash commands, shows full details for security review.
|
|
33
|
+
For other tools, shows a concise preview.
|
|
34
|
+
"""
|
|
35
|
+
# For Bash tool, show full command details for security review
|
|
36
|
+
if tool_name == "Bash" and hasattr(parsed_input, "command"):
|
|
37
|
+
lines = [f"Command: {getattr(parsed_input, 'command')}"]
|
|
38
|
+
|
|
39
|
+
# Add other relevant parameters
|
|
40
|
+
if hasattr(parsed_input, "timeout") and parsed_input.timeout:
|
|
41
|
+
lines.append(f"Timeout: {parsed_input.timeout}ms")
|
|
42
|
+
if hasattr(parsed_input, "sandbox"):
|
|
43
|
+
lines.append(f"Sandbox: {parsed_input.sandbox}")
|
|
44
|
+
if hasattr(parsed_input, "run_in_background"):
|
|
45
|
+
lines.append(f"Background: {parsed_input.run_in_background}")
|
|
46
|
+
if hasattr(parsed_input, "shell_executable") and parsed_input.shell_executable:
|
|
47
|
+
lines.append(f"Shell: {parsed_input.shell_executable}")
|
|
48
|
+
|
|
49
|
+
return "\n ".join(lines)
|
|
50
|
+
|
|
51
|
+
# For other tools with commands, show concise preview
|
|
31
52
|
if hasattr(parsed_input, "command"):
|
|
32
53
|
return f"command='{getattr(parsed_input, 'command')}'"
|
|
33
54
|
if hasattr(parsed_input, "file_path"):
|
|
@@ -94,10 +115,13 @@ def _rule_strings(rule_suggestions: Optional[Any]) -> list[str]:
|
|
|
94
115
|
|
|
95
116
|
def make_permission_checker(
|
|
96
117
|
project_path: Path,
|
|
97
|
-
|
|
118
|
+
yolo_mode: bool,
|
|
98
119
|
prompt_fn: Optional[Callable[[str], str]] = None,
|
|
99
120
|
) -> Callable[[Tool[Any, Any], Any], Awaitable[PermissionResult]]:
|
|
100
|
-
"""Create a permission checking function for the current project.
|
|
121
|
+
"""Create a permission checking function for the current project.
|
|
122
|
+
|
|
123
|
+
In yolo mode, all tool calls are allowed without prompting.
|
|
124
|
+
"""
|
|
101
125
|
|
|
102
126
|
project_path = project_path.resolve()
|
|
103
127
|
config_manager.get_project_config(project_path)
|
|
@@ -120,7 +144,7 @@ def make_permission_checker(
|
|
|
120
144
|
"""Check and optionally persist permission for a tool invocation."""
|
|
121
145
|
config = config_manager.get_project_config(project_path)
|
|
122
146
|
|
|
123
|
-
if
|
|
147
|
+
if yolo_mode:
|
|
124
148
|
return PermissionResult(result=True)
|
|
125
149
|
|
|
126
150
|
try:
|
|
@@ -130,7 +154,11 @@ def make_permission_checker(
|
|
|
130
154
|
# Tool implementation error - log and deny for safety
|
|
131
155
|
logger.warning(
|
|
132
156
|
"[permissions] Tool needs_permissions check failed",
|
|
133
|
-
extra={
|
|
157
|
+
extra={
|
|
158
|
+
"tool": getattr(tool, "name", None),
|
|
159
|
+
"error": str(exc),
|
|
160
|
+
"error_type": type(exc).__name__,
|
|
161
|
+
},
|
|
134
162
|
)
|
|
135
163
|
return PermissionResult(
|
|
136
164
|
result=False,
|
|
@@ -172,7 +200,11 @@ def make_permission_checker(
|
|
|
172
200
|
# Tool implementation error - fall back to asking user
|
|
173
201
|
logger.warning(
|
|
174
202
|
"[permissions] Tool check_permissions failed",
|
|
175
|
-
extra={
|
|
203
|
+
extra={
|
|
204
|
+
"tool": getattr(tool, "name", None),
|
|
205
|
+
"error": str(exc),
|
|
206
|
+
"error_type": type(exc).__name__,
|
|
207
|
+
},
|
|
176
208
|
)
|
|
177
209
|
decision = PermissionDecision(
|
|
178
210
|
behavior="ask",
|
|
@@ -203,7 +235,7 @@ def make_permission_checker(
|
|
|
203
235
|
)
|
|
204
236
|
|
|
205
237
|
# Ask/passthrough flows prompt the user.
|
|
206
|
-
input_preview = _format_input_preview(parsed_input)
|
|
238
|
+
input_preview = _format_input_preview(parsed_input, tool_name=tool.name)
|
|
207
239
|
prompt_lines = [
|
|
208
240
|
f"{tool.name}",
|
|
209
241
|
"",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import time
|
|
7
8
|
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
8
9
|
from uuid import uuid4
|
|
@@ -73,17 +74,21 @@ def _content_blocks_from_stream_state(
|
|
|
73
74
|
|
|
74
75
|
# Add thinking block if present
|
|
75
76
|
if collected_thinking:
|
|
76
|
-
blocks.append(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
blocks.append(
|
|
78
|
+
{
|
|
79
|
+
"type": "thinking",
|
|
80
|
+
"thinking": "".join(collected_thinking),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
80
83
|
|
|
81
84
|
# Add text block if present
|
|
82
85
|
if collected_text:
|
|
83
|
-
blocks.append(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
blocks.append(
|
|
87
|
+
{
|
|
88
|
+
"type": "text",
|
|
89
|
+
"text": "".join(collected_text),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
87
92
|
|
|
88
93
|
# Add tool_use blocks
|
|
89
94
|
for idx in sorted(collected_tool_calls.keys()):
|
|
@@ -92,12 +97,14 @@ def _content_blocks_from_stream_state(
|
|
|
92
97
|
if not name:
|
|
93
98
|
continue
|
|
94
99
|
tool_use_id = call.get("id") or str(uuid4())
|
|
95
|
-
blocks.append(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
blocks.append(
|
|
101
|
+
{
|
|
102
|
+
"type": "tool_use",
|
|
103
|
+
"tool_use_id": tool_use_id,
|
|
104
|
+
"name": name,
|
|
105
|
+
"input": call.get("input", {}),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
101
108
|
|
|
102
109
|
return blocks
|
|
103
110
|
|
|
@@ -110,25 +117,31 @@ def _content_blocks_from_response(response: Any) -> List[Dict[str, Any]]:
|
|
|
110
117
|
if btype == "text":
|
|
111
118
|
blocks.append({"type": "text", "text": getattr(block, "text", "")})
|
|
112
119
|
elif btype == "thinking":
|
|
113
|
-
blocks.append(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
blocks.append(
|
|
121
|
+
{
|
|
122
|
+
"type": "thinking",
|
|
123
|
+
"thinking": getattr(block, "thinking", None) or "",
|
|
124
|
+
"signature": getattr(block, "signature", None),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
118
127
|
elif btype == "redacted_thinking":
|
|
119
|
-
blocks.append(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
blocks.append(
|
|
129
|
+
{
|
|
130
|
+
"type": "redacted_thinking",
|
|
131
|
+
"data": getattr(block, "data", None),
|
|
132
|
+
"signature": getattr(block, "signature", None),
|
|
133
|
+
}
|
|
134
|
+
)
|
|
124
135
|
elif btype == "tool_use":
|
|
125
136
|
raw_input = getattr(block, "input", {}) or {}
|
|
126
|
-
blocks.append(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
blocks.append(
|
|
138
|
+
{
|
|
139
|
+
"type": "tool_use",
|
|
140
|
+
"tool_use_id": getattr(block, "id", None) or str(uuid4()),
|
|
141
|
+
"name": getattr(block, "name", None),
|
|
142
|
+
"input": raw_input if isinstance(raw_input, dict) else {},
|
|
143
|
+
}
|
|
144
|
+
)
|
|
132
145
|
return blocks
|
|
133
146
|
|
|
134
147
|
|
|
@@ -188,6 +201,15 @@ class AnthropicClient(ProviderClient):
|
|
|
188
201
|
except Exception as exc:
|
|
189
202
|
duration_ms = (time.time() - start_time) * 1000
|
|
190
203
|
error_code, error_message = _classify_anthropic_error(exc)
|
|
204
|
+
logger.debug(
|
|
205
|
+
"[anthropic_client] Exception details",
|
|
206
|
+
extra={
|
|
207
|
+
"model": model_profile.model,
|
|
208
|
+
"exception_type": type(exc).__name__,
|
|
209
|
+
"exception_str": str(exc),
|
|
210
|
+
"error_code": error_code,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
191
213
|
logger.error(
|
|
192
214
|
"[anthropic_client] API call failed",
|
|
193
215
|
extra={
|
|
@@ -222,6 +244,17 @@ class AnthropicClient(ProviderClient):
|
|
|
222
244
|
tool_schemas = await build_anthropic_tool_schemas(tools)
|
|
223
245
|
response_metadata: Dict[str, Any] = {}
|
|
224
246
|
|
|
247
|
+
logger.debug(
|
|
248
|
+
"[anthropic_client] Preparing request",
|
|
249
|
+
extra={
|
|
250
|
+
"model": model_profile.model,
|
|
251
|
+
"tool_mode": tool_mode,
|
|
252
|
+
"stream": stream,
|
|
253
|
+
"max_thinking_tokens": max_thinking_tokens,
|
|
254
|
+
"num_tools": len(tool_schemas),
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
|
|
225
258
|
anthropic_kwargs: Dict[str, Any] = {}
|
|
226
259
|
if model_profile.api_base:
|
|
227
260
|
anthropic_kwargs["base_url"] = model_profile.api_base
|
|
@@ -239,9 +272,9 @@ class AnthropicClient(ProviderClient):
|
|
|
239
272
|
# The read timeout applies to waiting for each chunk from the server
|
|
240
273
|
timeout_config = httpx.Timeout(
|
|
241
274
|
connect=60.0, # 60 seconds to establish connection
|
|
242
|
-
read=600.0,
|
|
243
|
-
write=60.0,
|
|
244
|
-
pool=60.0,
|
|
275
|
+
read=600.0, # 10 minutes to wait for each chunk (model may be thinking)
|
|
276
|
+
write=60.0, # 60 seconds to send request
|
|
277
|
+
pool=60.0, # 60 seconds to get connection from pool
|
|
245
278
|
)
|
|
246
279
|
anthropic_kwargs["timeout"] = timeout_config
|
|
247
280
|
elif request_timeout and request_timeout > 0:
|
|
@@ -267,6 +300,21 @@ class AnthropicClient(ProviderClient):
|
|
|
267
300
|
if thinking_payload:
|
|
268
301
|
request_kwargs["thinking"] = thinking_payload
|
|
269
302
|
|
|
303
|
+
logger.debug(
|
|
304
|
+
"[anthropic_client] Request parameters",
|
|
305
|
+
extra={
|
|
306
|
+
"model": model_profile.model,
|
|
307
|
+
"request_kwargs": json.dumps(
|
|
308
|
+
{k: v for k, v in request_kwargs.items() if k != "messages"},
|
|
309
|
+
ensure_ascii=False,
|
|
310
|
+
default=str,
|
|
311
|
+
)[:1000],
|
|
312
|
+
"thinking_payload": json.dumps(thinking_payload, ensure_ascii=False)
|
|
313
|
+
if thinking_payload
|
|
314
|
+
else None,
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
270
318
|
async with await self._client(anthropic_kwargs) as client:
|
|
271
319
|
if stream:
|
|
272
320
|
# Streaming mode: use event-based streaming with per-token timeout
|
|
@@ -294,6 +342,16 @@ class AnthropicClient(ProviderClient):
|
|
|
294
342
|
model_profile.model, duration_ms=duration_ms, cost_usd=cost_usd, **usage_tokens
|
|
295
343
|
)
|
|
296
344
|
|
|
345
|
+
logger.debug(
|
|
346
|
+
"[anthropic_client] Response content blocks",
|
|
347
|
+
extra={
|
|
348
|
+
"model": model_profile.model,
|
|
349
|
+
"content_blocks": json.dumps(content_blocks, ensure_ascii=False)[:1000],
|
|
350
|
+
"usage_tokens": json.dumps(usage_tokens, ensure_ascii=False),
|
|
351
|
+
"metadata": json.dumps(response_metadata, ensure_ascii=False)[:500],
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
297
355
|
logger.info(
|
|
298
356
|
"[anthropic_client] Response received",
|
|
299
357
|
extra={
|
|
@@ -354,6 +412,13 @@ class AnthropicClient(ProviderClient):
|
|
|
354
412
|
event_count = 0
|
|
355
413
|
message_stop_received = False
|
|
356
414
|
|
|
415
|
+
logger.debug(
|
|
416
|
+
"[anthropic_client] Initiating stream request",
|
|
417
|
+
extra={
|
|
418
|
+
"model": request_kwargs.get("model"),
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
357
422
|
# Create the stream - this initiates the connection
|
|
358
423
|
stream = client.messages.stream(**request_kwargs)
|
|
359
424
|
|
|
@@ -448,7 +513,12 @@ class AnthropicClient(ProviderClient):
|
|
|
448
513
|
else:
|
|
449
514
|
raise
|
|
450
515
|
|
|
451
|
-
if
|
|
516
|
+
if (
|
|
517
|
+
last_error
|
|
518
|
+
and not collected_text
|
|
519
|
+
and not collected_thinking
|
|
520
|
+
and not collected_tool_calls
|
|
521
|
+
):
|
|
452
522
|
raise RuntimeError(f"Stream failed after {attempts} attempts") from last_error
|
|
453
523
|
|
|
454
524
|
# Store reasoning content in metadata
|
|
@@ -542,7 +612,8 @@ class AnthropicClient(ProviderClient):
|
|
|
542
612
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
543
613
|
logger.warning(
|
|
544
614
|
"[anthropic_client] Progress callback failed: %s: %s",
|
|
545
|
-
type(cb_exc).__name__,
|
|
615
|
+
type(cb_exc).__name__,
|
|
616
|
+
cb_exc,
|
|
546
617
|
)
|
|
547
618
|
|
|
548
619
|
elif delta_type == "text_delta":
|
|
@@ -556,7 +627,8 @@ class AnthropicClient(ProviderClient):
|
|
|
556
627
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
557
628
|
logger.warning(
|
|
558
629
|
"[anthropic_client] Progress callback failed: %s: %s",
|
|
559
|
-
type(cb_exc).__name__,
|
|
630
|
+
type(cb_exc).__name__,
|
|
631
|
+
cb_exc,
|
|
560
632
|
)
|
|
561
633
|
|
|
562
634
|
elif delta_type == "input_json_delta":
|
|
@@ -599,6 +671,7 @@ class AnthropicClient(ProviderClient):
|
|
|
599
671
|
# Parse accumulated JSON for tool calls
|
|
600
672
|
if index in collected_tool_calls:
|
|
601
673
|
import json
|
|
674
|
+
|
|
602
675
|
json_str = collected_tool_calls[index].get("input_json", "")
|
|
603
676
|
if json_str:
|
|
604
677
|
try:
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import copy
|
|
7
7
|
import inspect
|
|
8
|
+
import json
|
|
8
9
|
import os
|
|
9
10
|
import time
|
|
10
11
|
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, cast
|
|
@@ -240,9 +241,7 @@ async def _async_build_tool_declarations(tools: List[Tool[Any, Any]]) -> List[Di
|
|
|
240
241
|
description=description,
|
|
241
242
|
parameters_json_schema=parameters_schema,
|
|
242
243
|
)
|
|
243
|
-
declarations.append(
|
|
244
|
-
func_decl.model_dump(mode="json", exclude_none=True)
|
|
245
|
-
)
|
|
244
|
+
declarations.append(func_decl.model_dump(mode="json", exclude_none=True))
|
|
246
245
|
else:
|
|
247
246
|
declarations.append(
|
|
248
247
|
{
|
|
@@ -385,6 +384,17 @@ class GeminiClient(ProviderClient):
|
|
|
385
384
|
) -> ProviderResponse:
|
|
386
385
|
start_time = time.time()
|
|
387
386
|
|
|
387
|
+
logger.debug(
|
|
388
|
+
"[gemini_client] Preparing request",
|
|
389
|
+
extra={
|
|
390
|
+
"model": model_profile.model,
|
|
391
|
+
"tool_mode": tool_mode,
|
|
392
|
+
"stream": stream,
|
|
393
|
+
"max_thinking_tokens": max_thinking_tokens,
|
|
394
|
+
"num_tools": len(tools),
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
388
398
|
try:
|
|
389
399
|
client = await self._client(model_profile)
|
|
390
400
|
except asyncio.CancelledError:
|
|
@@ -392,6 +402,15 @@ class GeminiClient(ProviderClient):
|
|
|
392
402
|
except Exception as exc:
|
|
393
403
|
duration_ms = (time.time() - start_time) * 1000
|
|
394
404
|
error_code, error_message = _classify_gemini_error(exc)
|
|
405
|
+
logger.debug(
|
|
406
|
+
"[gemini_client] Exception details during init",
|
|
407
|
+
extra={
|
|
408
|
+
"model": model_profile.model,
|
|
409
|
+
"exception_type": type(exc).__name__,
|
|
410
|
+
"exception_str": str(exc),
|
|
411
|
+
"error_code": error_code,
|
|
412
|
+
},
|
|
413
|
+
)
|
|
395
414
|
logger.error(
|
|
396
415
|
"[gemini_client] Initialization failed",
|
|
397
416
|
extra={
|
|
@@ -422,7 +441,12 @@ class GeminiClient(ProviderClient):
|
|
|
422
441
|
from google.genai import types as genai_types # type: ignore
|
|
423
442
|
|
|
424
443
|
config["thinking_config"] = genai_types.ThinkingConfig(**thinking_config)
|
|
425
|
-
except (
|
|
444
|
+
except (
|
|
445
|
+
ImportError,
|
|
446
|
+
ModuleNotFoundError,
|
|
447
|
+
TypeError,
|
|
448
|
+
ValueError,
|
|
449
|
+
): # pragma: no cover - fallback when SDK not installed
|
|
426
450
|
config["thinking_config"] = thinking_config
|
|
427
451
|
if declarations:
|
|
428
452
|
config["tools"] = [{"function_declarations": declarations}]
|
|
@@ -432,6 +456,23 @@ class GeminiClient(ProviderClient):
|
|
|
432
456
|
"contents": contents,
|
|
433
457
|
"config": config,
|
|
434
458
|
}
|
|
459
|
+
|
|
460
|
+
logger.debug(
|
|
461
|
+
"[gemini_client] Request parameters",
|
|
462
|
+
extra={
|
|
463
|
+
"model": model_profile.model,
|
|
464
|
+
"config": json.dumps(
|
|
465
|
+
{k: v for k, v in config.items() if k != "system_instruction"},
|
|
466
|
+
ensure_ascii=False,
|
|
467
|
+
default=str,
|
|
468
|
+
)[:1000],
|
|
469
|
+
"num_declarations": len(declarations),
|
|
470
|
+
"thinking_config": json.dumps(thinking_config, ensure_ascii=False)
|
|
471
|
+
if thinking_config
|
|
472
|
+
else None,
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
435
476
|
usage_tokens: Dict[str, int] = {}
|
|
436
477
|
collected_text: List[str] = []
|
|
437
478
|
function_calls: List[Dict[str, Any]] = []
|
|
@@ -483,6 +524,10 @@ class GeminiClient(ProviderClient):
|
|
|
483
524
|
|
|
484
525
|
try:
|
|
485
526
|
if stream:
|
|
527
|
+
logger.debug(
|
|
528
|
+
"[gemini_client] Initiating stream request",
|
|
529
|
+
extra={"model": model_profile.model},
|
|
530
|
+
)
|
|
486
531
|
stream_resp = await _call_generate(streaming=True)
|
|
487
532
|
|
|
488
533
|
# Normalize streams into an async iterator to avoid StopIteration surfacing through
|
|
@@ -523,7 +568,8 @@ class GeminiClient(ProviderClient):
|
|
|
523
568
|
except (RuntimeError, ValueError, TypeError, OSError) as cb_exc:
|
|
524
569
|
logger.warning(
|
|
525
570
|
"[gemini_client] Stream callback failed: %s: %s",
|
|
526
|
-
type(cb_exc).__name__,
|
|
571
|
+
type(cb_exc).__name__,
|
|
572
|
+
cb_exc,
|
|
527
573
|
)
|
|
528
574
|
if text_chunk:
|
|
529
575
|
collected_text.append(text_chunk)
|
|
@@ -552,6 +598,15 @@ class GeminiClient(ProviderClient):
|
|
|
552
598
|
except Exception as exc:
|
|
553
599
|
duration_ms = (time.time() - start_time) * 1000
|
|
554
600
|
error_code, error_message = _classify_gemini_error(exc)
|
|
601
|
+
logger.debug(
|
|
602
|
+
"[gemini_client] Exception details",
|
|
603
|
+
extra={
|
|
604
|
+
"model": model_profile.model,
|
|
605
|
+
"exception_type": type(exc).__name__,
|
|
606
|
+
"exception_str": str(exc),
|
|
607
|
+
"error_code": error_code,
|
|
608
|
+
},
|
|
609
|
+
)
|
|
555
610
|
logger.error(
|
|
556
611
|
"[gemini_client] API call failed",
|
|
557
612
|
extra={
|
|
@@ -595,6 +650,16 @@ class GeminiClient(ProviderClient):
|
|
|
595
650
|
**(usage_tokens or {}),
|
|
596
651
|
)
|
|
597
652
|
|
|
653
|
+
logger.debug(
|
|
654
|
+
"[gemini_client] Response content blocks",
|
|
655
|
+
extra={
|
|
656
|
+
"model": model_profile.model,
|
|
657
|
+
"content_blocks": json.dumps(content_blocks, ensure_ascii=False)[:1000],
|
|
658
|
+
"usage_tokens": json.dumps(usage_tokens, ensure_ascii=False),
|
|
659
|
+
"metadata": json.dumps(response_metadata, ensure_ascii=False)[:500],
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
598
663
|
logger.info(
|
|
599
664
|
"[gemini_client] Response received",
|
|
600
665
|
extra={
|