bone-agent 1.4.0 → 2.0.0
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/llm/providers.py
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
"""Provider-specific request/response handlers.
|
|
2
|
-
|
|
3
|
-
This module isolates provider-specific API quirks into handler classes.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
from typing import Optional, Dict, Any, Iterator
|
|
8
|
-
import requests
|
|
9
|
-
|
|
10
|
-
from exceptions import LLMResponseError
|
|
11
|
-
from .codex_provider import CodexResponsesHandler
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class OpenAIHandler:
|
|
15
|
-
"""Handler for OpenAI-compatible providers.
|
|
16
|
-
|
|
17
|
-
Supports: OpenAI, OpenRouter, GLM, Gemini, Kimi, MiniMax
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def build_headers(self, config: Dict[str, Any]) -> Dict[str, str]:
|
|
21
|
-
"""Build request headers."""
|
|
22
|
-
headers = {"Content-Type": "application/json"}
|
|
23
|
-
if config.get("type") == "api" and config.get("api_key"):
|
|
24
|
-
headers["Authorization"] = f"Bearer {config['api_key']}"
|
|
25
|
-
if "headers_extra" in config:
|
|
26
|
-
headers.update(config["headers_extra"])
|
|
27
|
-
return headers
|
|
28
|
-
|
|
29
|
-
def build_payload(self, config: Dict[str, Any], messages: list,
|
|
30
|
-
tools: Optional[list] = None, stream: bool = True) -> Dict[str, Any]:
|
|
31
|
-
"""Build request payload."""
|
|
32
|
-
payload = {**config.get("payload", {}), "messages": messages, "stream": stream}
|
|
33
|
-
|
|
34
|
-
# Ensure model is set from config if not in payload
|
|
35
|
-
if "model" not in payload:
|
|
36
|
-
model_name = config.get("api_model") or config.get("model")
|
|
37
|
-
if model_name:
|
|
38
|
-
payload["model"] = model_name
|
|
39
|
-
|
|
40
|
-
# Add tools if provided (OpenAI format)
|
|
41
|
-
if tools:
|
|
42
|
-
payload["tools"] = tools
|
|
43
|
-
|
|
44
|
-
# Set default parameters if not in config
|
|
45
|
-
if "temperature" not in payload and config.get("allow_temperature", True):
|
|
46
|
-
payload["temperature"] = config.get("default_temperature", 0.1)
|
|
47
|
-
if "top_p" not in payload and config.get("allow_top_p", True):
|
|
48
|
-
payload["top_p"] = config.get("default_top_p", 0.9)
|
|
49
|
-
|
|
50
|
-
return payload
|
|
51
|
-
|
|
52
|
-
def parse_response(self, response_json: Dict[str, Any]) -> Dict[str, Any]:
|
|
53
|
-
"""Parse non-streaming response (already in OpenAI format)."""
|
|
54
|
-
return response_json
|
|
55
|
-
|
|
56
|
-
def parse_stream(self, response: requests.Response) -> Iterator[Dict[str, Any]]:
|
|
57
|
-
"""Parse streaming response.
|
|
58
|
-
|
|
59
|
-
Yields text chunks, and finally yields a dict with __usage__ key.
|
|
60
|
-
"""
|
|
61
|
-
usage_data = None
|
|
62
|
-
|
|
63
|
-
for line in response.iter_lines():
|
|
64
|
-
if line:
|
|
65
|
-
line = line.decode('utf-8')
|
|
66
|
-
|
|
67
|
-
# Skip OpenRouter comments (start with ':')
|
|
68
|
-
if line.startswith(':'):
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
if line.startswith('data: '):
|
|
72
|
-
data_str = line[6:]
|
|
73
|
-
if data_str.strip() == '[DONE]':
|
|
74
|
-
break
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
data = json.loads(data_str)
|
|
78
|
-
|
|
79
|
-
# Check for mid-stream errors
|
|
80
|
-
if 'error' in data:
|
|
81
|
-
error_msg = data.get('error', {}).get('message', 'Unknown streaming error')
|
|
82
|
-
raise LLMResponseError(
|
|
83
|
-
f"Streaming error: {error_msg}",
|
|
84
|
-
details={"error_data": data.get('error')}
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Capture usage data if present (usually in final chunk)
|
|
88
|
-
if 'usage' in data:
|
|
89
|
-
usage_data = dict(data['usage'])
|
|
90
|
-
# Promote top-level cost into usage dict (OpenRouter places it here)
|
|
91
|
-
if 'cost' in data:
|
|
92
|
-
usage_data['cost'] = data['cost']
|
|
93
|
-
|
|
94
|
-
choices = data.get('choices', [])
|
|
95
|
-
if choices:
|
|
96
|
-
delta = choices[0].get('delta', {})
|
|
97
|
-
content = delta.get('content')
|
|
98
|
-
if content is not None:
|
|
99
|
-
yield content
|
|
100
|
-
|
|
101
|
-
except json.JSONDecodeError as e:
|
|
102
|
-
raise LLMResponseError(
|
|
103
|
-
f"Failed to decode streaming response",
|
|
104
|
-
details={"original_error": str(e)}
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Yield usage data as final item if captured
|
|
108
|
-
if usage_data:
|
|
109
|
-
yield {'__usage__': usage_data}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class AnthropicHandler:
|
|
113
|
-
"""Handler for Anthropic API.
|
|
114
|
-
|
|
115
|
-
Anthropic has significant differences from OpenAI:
|
|
116
|
-
- Different endpoint (/messages vs /chat/completions)
|
|
117
|
-
- Different message format (content arrays vs strings)
|
|
118
|
-
- Different tool format (flat vs nested)
|
|
119
|
-
- Different streaming (SSE with event types vs data: lines)
|
|
120
|
-
- Different headers (x-api-key vs Authorization: Bearer)
|
|
121
|
-
- Different parameters (requires max_tokens, forbids top_p with temperature)
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
def build_headers(self, config: Dict[str, Any]) -> Dict[str, str]:
|
|
125
|
-
"""Build request headers (Anthropic uses x-api-key)."""
|
|
126
|
-
headers = {"Content-Type": "application/json"}
|
|
127
|
-
if config.get("type") == "api" and config.get("api_key"):
|
|
128
|
-
headers["x-api-key"] = config['api_key']
|
|
129
|
-
if "headers_extra" in config:
|
|
130
|
-
headers.update(config["headers_extra"])
|
|
131
|
-
return headers
|
|
132
|
-
|
|
133
|
-
def build_payload(self, config: Dict[str, Any], messages: list,
|
|
134
|
-
tools: Optional[list] = None, stream: bool = True) -> Dict[str, Any]:
|
|
135
|
-
"""Build request payload (Anthropic format)."""
|
|
136
|
-
# Extract system messages to top-level parameter
|
|
137
|
-
system_messages = [msg["content"] for msg in messages if msg.get("role") == "system"]
|
|
138
|
-
system_content = "\n".join(system_messages) if system_messages else None
|
|
139
|
-
non_system_messages = [msg for msg in messages if msg.get("role") != "system"]
|
|
140
|
-
|
|
141
|
-
# Convert messages and tools to Anthropic format
|
|
142
|
-
anthropic_messages = self._convert_messages_to_anthropic(non_system_messages)
|
|
143
|
-
anthropic_tools = self._convert_tools_to_anthropic(tools) if tools else None
|
|
144
|
-
|
|
145
|
-
payload = {**config.get("payload", {}), "messages": anthropic_messages, "stream": stream}
|
|
146
|
-
|
|
147
|
-
# Ensure model is set from config if not in payload
|
|
148
|
-
if "model" not in payload:
|
|
149
|
-
model_name = config.get("api_model") or config.get("model")
|
|
150
|
-
if model_name:
|
|
151
|
-
payload["model"] = model_name
|
|
152
|
-
|
|
153
|
-
if system_content:
|
|
154
|
-
payload["system"] = system_content
|
|
155
|
-
if anthropic_tools:
|
|
156
|
-
payload["tools"] = anthropic_tools
|
|
157
|
-
|
|
158
|
-
# Set default parameters (Anthropic requires max_tokens)
|
|
159
|
-
if "temperature" not in payload and config.get("allow_temperature", True):
|
|
160
|
-
payload["temperature"] = config.get("default_temperature", 0.1)
|
|
161
|
-
if "max_tokens" not in payload:
|
|
162
|
-
payload["max_tokens"] = config.get("max_tokens", 4096)
|
|
163
|
-
|
|
164
|
-
# Anthropic doesn't allow both temperature and top_p
|
|
165
|
-
# Only set top_p if temperature is not set
|
|
166
|
-
if "temperature" not in payload and "top_p" not in payload:
|
|
167
|
-
payload["top_p"] = config.get("default_top_p", 0.9)
|
|
168
|
-
|
|
169
|
-
return payload
|
|
170
|
-
|
|
171
|
-
def parse_response(self, response_json: Dict[str, Any]) -> Dict[str, Any]:
|
|
172
|
-
"""Convert Anthropic response format to OpenAI-style format."""
|
|
173
|
-
# Anthropic format: {"content": [{"type": "text", "text": "..."}], "usage": {...}}
|
|
174
|
-
# OpenAI format: {"choices": [{"message": {"content": "..."}}], "usage": {...}}
|
|
175
|
-
|
|
176
|
-
# Convert Anthropic usage format (input_tokens/output_tokens) to OpenAI format (prompt_tokens/completion_tokens)
|
|
177
|
-
# Anthropic's input_tokens does NOT include cache tokens; total input =
|
|
178
|
-
# input_tokens + cache_read_input_tokens + cache_creation_input_tokens
|
|
179
|
-
anthropic_usage = response_json.get("usage", {})
|
|
180
|
-
cache_read = anthropic_usage.get('cache_read_input_tokens', 0)
|
|
181
|
-
cache_creation = anthropic_usage.get('cache_creation_input_tokens', 0)
|
|
182
|
-
prompt_tokens = anthropic_usage.get('input_tokens', 0) + cache_read + cache_creation
|
|
183
|
-
completion_tokens = anthropic_usage.get('output_tokens', 0)
|
|
184
|
-
openai_format_usage = {
|
|
185
|
-
'prompt_tokens': prompt_tokens,
|
|
186
|
-
'completion_tokens': completion_tokens,
|
|
187
|
-
'total_tokens': prompt_tokens + completion_tokens,
|
|
188
|
-
}
|
|
189
|
-
# Preserve Anthropic cache token fields for the token tracker
|
|
190
|
-
if 'cache_read_input_tokens' in anthropic_usage:
|
|
191
|
-
openai_format_usage['cache_read_input_tokens'] = anthropic_usage['cache_read_input_tokens']
|
|
192
|
-
if 'cache_creation_input_tokens' in anthropic_usage:
|
|
193
|
-
openai_format_usage['cache_creation_input_tokens'] = anthropic_usage['cache_creation_input_tokens']
|
|
194
|
-
# Preserve non-cache input count so cost estimation can bill only the
|
|
195
|
-
# non-cache portion without relying on fragile prompt_tokens subtraction.
|
|
196
|
-
if 'input_tokens' in anthropic_usage:
|
|
197
|
-
openai_format_usage['input_tokens'] = anthropic_usage['input_tokens']
|
|
198
|
-
|
|
199
|
-
result = {
|
|
200
|
-
"choices": [],
|
|
201
|
-
"usage": openai_format_usage
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
# Extract content from Anthropic's content array
|
|
205
|
-
content_blocks = response_json.get("content", [])
|
|
206
|
-
text_parts = []
|
|
207
|
-
tool_calls = []
|
|
208
|
-
|
|
209
|
-
for block in content_blocks:
|
|
210
|
-
if block.get("type") == "text":
|
|
211
|
-
text_parts.append(block.get("text", ""))
|
|
212
|
-
elif block.get("type") == "tool_use":
|
|
213
|
-
# Convert Anthropic tool_use to OpenAI tool_calls format
|
|
214
|
-
tool_calls.append({
|
|
215
|
-
"id": block.get("id"),
|
|
216
|
-
"type": "function",
|
|
217
|
-
"function": {
|
|
218
|
-
"name": block.get("name"),
|
|
219
|
-
"arguments": json.dumps(block.get("input", {}))
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
# Build OpenAI-style message
|
|
224
|
-
message = {"role": "assistant"}
|
|
225
|
-
|
|
226
|
-
# Include either text content or tool calls
|
|
227
|
-
if tool_calls:
|
|
228
|
-
message["content"] = None
|
|
229
|
-
message["tool_calls"] = tool_calls
|
|
230
|
-
else:
|
|
231
|
-
message["content"] = "".join(text_parts)
|
|
232
|
-
|
|
233
|
-
result["choices"].append({"message": message})
|
|
234
|
-
|
|
235
|
-
return result
|
|
236
|
-
|
|
237
|
-
def parse_stream(self, response: requests.Response) -> Iterator[Dict[str, Any]]:
|
|
238
|
-
"""Parse Anthropic's SSE-based streaming response.
|
|
239
|
-
|
|
240
|
-
Yields text chunks, and finally yields a dict with __usage__ key.
|
|
241
|
-
|
|
242
|
-
Anthropic splits usage across two events:
|
|
243
|
-
- message_start: contains input_tokens
|
|
244
|
-
- message_delta: contains output_tokens
|
|
245
|
-
We merge both and convert to OpenAI format (prompt_tokens/completion_tokens).
|
|
246
|
-
"""
|
|
247
|
-
usage_data = {}
|
|
248
|
-
|
|
249
|
-
for line in response.iter_lines():
|
|
250
|
-
if line:
|
|
251
|
-
line = line.decode('utf-8')
|
|
252
|
-
|
|
253
|
-
# Anthropic uses SSE format: "event: <type>" followed by "data: <json>"
|
|
254
|
-
if line.startswith('data: '):
|
|
255
|
-
data_str = line[6:]
|
|
256
|
-
try:
|
|
257
|
-
data = json.loads(data_str)
|
|
258
|
-
|
|
259
|
-
# Check for errors
|
|
260
|
-
if data.get('type') == 'error':
|
|
261
|
-
error_msg = data.get('error', {}).get('message', 'Unknown error')
|
|
262
|
-
raise LLMResponseError(
|
|
263
|
-
f"Anthropic streaming error: {error_msg}",
|
|
264
|
-
details={"error_data": data.get('error')}
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# Capture input_tokens from message_start events
|
|
268
|
-
if data.get('type') == 'message_start':
|
|
269
|
-
message_usage = data.get('message', {}).get('usage', {})
|
|
270
|
-
if message_usage:
|
|
271
|
-
usage_data.update(message_usage)
|
|
272
|
-
|
|
273
|
-
# Capture output_tokens from message_delta events
|
|
274
|
-
if data.get('type') == 'message_delta' and 'usage' in data:
|
|
275
|
-
usage_data.update(data['usage'])
|
|
276
|
-
|
|
277
|
-
# Extract text from content_block_delta events
|
|
278
|
-
if data.get('type') == 'content_block_delta':
|
|
279
|
-
delta = data.get('delta', {})
|
|
280
|
-
if delta.get('type') == 'text_delta':
|
|
281
|
-
text = delta.get('text', '')
|
|
282
|
-
if text:
|
|
283
|
-
yield text
|
|
284
|
-
|
|
285
|
-
except json.JSONDecodeError as e:
|
|
286
|
-
raise LLMResponseError(
|
|
287
|
-
f"Failed to decode Anthropic streaming response",
|
|
288
|
-
details={"original_error": str(e)}
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Yield usage data as final item if captured
|
|
292
|
-
# Convert Anthropic format (input_tokens/output_tokens) to OpenAI format (prompt_tokens/completion_tokens)
|
|
293
|
-
# Anthropic's input_tokens does NOT include cache tokens; total input =
|
|
294
|
-
# input_tokens + cache_read_input_tokens + cache_creation_input_tokens
|
|
295
|
-
if usage_data:
|
|
296
|
-
cache_read = usage_data.get('cache_read_input_tokens', 0)
|
|
297
|
-
cache_creation = usage_data.get('cache_creation_input_tokens', 0)
|
|
298
|
-
prompt_tokens = usage_data.get('input_tokens', 0) + cache_read + cache_creation
|
|
299
|
-
completion_tokens = usage_data.get('output_tokens', 0)
|
|
300
|
-
openai_format_usage = {
|
|
301
|
-
'prompt_tokens': prompt_tokens,
|
|
302
|
-
'completion_tokens': completion_tokens,
|
|
303
|
-
'total_tokens': prompt_tokens + completion_tokens,
|
|
304
|
-
}
|
|
305
|
-
# Preserve Anthropic cache token fields for the token tracker
|
|
306
|
-
if 'cache_read_input_tokens' in usage_data:
|
|
307
|
-
openai_format_usage['cache_read_input_tokens'] = usage_data['cache_read_input_tokens']
|
|
308
|
-
if 'cache_creation_input_tokens' in usage_data:
|
|
309
|
-
openai_format_usage['cache_creation_input_tokens'] = usage_data['cache_creation_input_tokens']
|
|
310
|
-
# Preserve non-cache input count for accurate cost estimation
|
|
311
|
-
if 'input_tokens' in usage_data:
|
|
312
|
-
openai_format_usage['input_tokens'] = usage_data['input_tokens']
|
|
313
|
-
yield {'__usage__': openai_format_usage}
|
|
314
|
-
|
|
315
|
-
@staticmethod
|
|
316
|
-
def _convert_tools_to_anthropic(openai_tools: list) -> list:
|
|
317
|
-
"""Convert OpenAI-style tool definitions to Anthropic format.
|
|
318
|
-
|
|
319
|
-
OpenAI format: {"type": "function", "function": {"name": "...", "parameters": {...}}}
|
|
320
|
-
Anthropic format: {"name": "...", "description": "...", "input_schema": {...}}
|
|
321
|
-
"""
|
|
322
|
-
anthropic_tools = []
|
|
323
|
-
|
|
324
|
-
for openai_tool in openai_tools:
|
|
325
|
-
if openai_tool.get("type") == "function":
|
|
326
|
-
func = openai_tool.get("function", {})
|
|
327
|
-
anthropic_tool = {
|
|
328
|
-
"name": func.get("name"),
|
|
329
|
-
"description": func.get("description", ""),
|
|
330
|
-
"input_schema": func.get("parameters", {"type": "object", "properties": {}})
|
|
331
|
-
}
|
|
332
|
-
anthropic_tools.append(anthropic_tool)
|
|
333
|
-
|
|
334
|
-
return anthropic_tools
|
|
335
|
-
|
|
336
|
-
@staticmethod
|
|
337
|
-
def _convert_messages_to_anthropic(openai_messages: list) -> list:
|
|
338
|
-
"""Convert OpenAI-style messages to Anthropic format.
|
|
339
|
-
|
|
340
|
-
Anthropic requires all content to be an array, not a string.
|
|
341
|
-
|
|
342
|
-
OpenAI format:
|
|
343
|
-
{"role": "user", "content": "text"}
|
|
344
|
-
{"role": "tool", "content": "...", "tool_call_id": "..."}
|
|
345
|
-
|
|
346
|
-
Anthropic format:
|
|
347
|
-
{"role": "user", "content": [{"type": "text", "text": "..."}]}
|
|
348
|
-
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "..."}]}
|
|
349
|
-
"""
|
|
350
|
-
anthropic_messages = []
|
|
351
|
-
|
|
352
|
-
for msg in openai_messages:
|
|
353
|
-
# Handle tool result messages
|
|
354
|
-
if msg.get("role") == "tool":
|
|
355
|
-
anthropic_msg = {
|
|
356
|
-
"role": "user",
|
|
357
|
-
"content": [
|
|
358
|
-
{
|
|
359
|
-
"type": "tool_result",
|
|
360
|
-
"tool_use_id": msg.get("tool_call_id"),
|
|
361
|
-
"content": msg.get("content", "")
|
|
362
|
-
}
|
|
363
|
-
]
|
|
364
|
-
}
|
|
365
|
-
anthropic_messages.append(anthropic_msg)
|
|
366
|
-
# Handle user and assistant messages - convert string content to array
|
|
367
|
-
elif msg.get("role") in ("user", "assistant"):
|
|
368
|
-
content = msg.get("content", "")
|
|
369
|
-
tool_calls = msg.get("tool_calls")
|
|
370
|
-
|
|
371
|
-
# Build content blocks array
|
|
372
|
-
content_blocks = []
|
|
373
|
-
|
|
374
|
-
# Add text content if present
|
|
375
|
-
if isinstance(content, str) and content.strip():
|
|
376
|
-
content_blocks.append({
|
|
377
|
-
"type": "text",
|
|
378
|
-
"text": content
|
|
379
|
-
})
|
|
380
|
-
elif isinstance(content, list):
|
|
381
|
-
# Already an array (Anthropic format), use as-is
|
|
382
|
-
anthropic_messages.append(msg)
|
|
383
|
-
continue
|
|
384
|
-
|
|
385
|
-
# Add tool_use blocks if present (for assistant messages with tool calls)
|
|
386
|
-
if tool_calls:
|
|
387
|
-
for tool_call in tool_calls:
|
|
388
|
-
content_blocks.append({
|
|
389
|
-
"type": "tool_use",
|
|
390
|
-
"id": tool_call.get("id"),
|
|
391
|
-
"name": tool_call.get("function", {}).get("name"),
|
|
392
|
-
"input": json.loads(tool_call.get("function", {}).get("arguments", "{}"))
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
# Only add message if we have content blocks (text or tool_use)
|
|
396
|
-
if content_blocks:
|
|
397
|
-
anthropic_msg = {
|
|
398
|
-
"role": msg.get("role"),
|
|
399
|
-
"content": content_blocks
|
|
400
|
-
}
|
|
401
|
-
anthropic_messages.append(anthropic_msg)
|
|
402
|
-
else:
|
|
403
|
-
# Other message types, pass through
|
|
404
|
-
anthropic_messages.append(msg)
|
|
405
|
-
|
|
406
|
-
return anthropic_messages
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# Handler registry - maps provider names to handler classes
|
|
410
|
-
HANDLER_REGISTRY = {
|
|
411
|
-
"openai": OpenAIHandler,
|
|
412
|
-
"openrouter": OpenAIHandler,
|
|
413
|
-
"glm": OpenAIHandler,
|
|
414
|
-
"glm_plan": OpenAIHandler,
|
|
415
|
-
"gemini": OpenAIHandler,
|
|
416
|
-
"minimax": AnthropicHandler,
|
|
417
|
-
"minimax_plan": AnthropicHandler,
|
|
418
|
-
"kimi": OpenAIHandler,
|
|
419
|
-
"anthropic": AnthropicHandler,
|
|
420
|
-
"local": OpenAIHandler,
|
|
421
|
-
"codex": CodexResponsesHandler,
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def get_handler(provider_name: str):
|
|
426
|
-
"""Get handler instance for the given provider.
|
|
427
|
-
|
|
428
|
-
Args:
|
|
429
|
-
provider_name: Name of the provider
|
|
430
|
-
|
|
431
|
-
Returns:
|
|
432
|
-
Handler instance for the provider
|
|
433
|
-
"""
|
|
434
|
-
handler_class = HANDLER_REGISTRY.get(provider_name.lower(), OpenAIHandler)
|
|
435
|
-
return handler_class()
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
__all__ = ['OpenAIHandler', 'AnthropicHandler', 'CodexResponsesHandler', 'get_handler']
|
package/src/llm/streaming.py
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
"""Streaming response assembler for agentic mode.
|
|
2
|
-
|
|
3
|
-
Consumes a StreamWrapper yielding mixed delta dicts and assembles them into
|
|
4
|
-
a complete message dict (content + tool_calls), matching the format that
|
|
5
|
-
non-streaming responses already produce.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
stream = client.chat_completion(messages, stream=True, tools=tools)
|
|
9
|
-
assembler = StreamingResponse(stream, console, debug_mode=False)
|
|
10
|
-
message = assembler.consume() # iterates stream, prints text, assembles tool_calls
|
|
11
|
-
tool_calls = message.get("tool_calls")
|
|
12
|
-
usage = assembler.usage
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
import sys
|
|
17
|
-
from typing import Any, Dict, List, Optional
|
|
18
|
-
|
|
19
|
-
from rich.text import Text
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class StreamingResponse:
|
|
23
|
-
"""Assemble streaming deltas into a complete message dict.
|
|
24
|
-
|
|
25
|
-
Text deltas are printed to stderr immediately (raw, no formatting).
|
|
26
|
-
Tool call deltas are buffered and reassembled across chunks.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self, stream, console=None, debug_mode: bool = False,
|
|
30
|
-
on_text=None, live=None):
|
|
31
|
-
"""
|
|
32
|
-
Args:
|
|
33
|
-
stream: StreamWrapper (or any iterable yielding deltas / __usage__ dicts).
|
|
34
|
-
console: Rich Console instance (used for debug logging only).
|
|
35
|
-
debug_mode: If True, log assembly details.
|
|
36
|
-
on_text: Optional callback(str) invoked for each text token.
|
|
37
|
-
Defaults to printing to stderr.
|
|
38
|
-
live: Optional Rich Live context. When set, streaming text is
|
|
39
|
-
rendered through Live (raw during streaming, swappable to
|
|
40
|
-
Markdown on completion) instead of raw stderr.
|
|
41
|
-
"""
|
|
42
|
-
self._stream = stream
|
|
43
|
-
self._console = console
|
|
44
|
-
self._debug = debug_mode
|
|
45
|
-
self._on_text = on_text
|
|
46
|
-
self._live = live
|
|
47
|
-
|
|
48
|
-
# Accumulated state
|
|
49
|
-
self._text_parts: List[str] = []
|
|
50
|
-
self._tool_calls: Dict[int, Dict[str, Any]] = {} # index -> partial tool call
|
|
51
|
-
self._usage: Optional[Dict[str, Any]] = None
|
|
52
|
-
|
|
53
|
-
def consume(self) -> Dict[str, Any]:
|
|
54
|
-
"""Iterate the stream, print text tokens, assemble tool calls.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
A message dict with 'role', 'content', and optionally 'tool_calls'
|
|
58
|
-
— same shape as a non-streaming response["choices"][0]["message"].
|
|
59
|
-
"""
|
|
60
|
-
for item in self._stream:
|
|
61
|
-
if isinstance(item, dict) and '__usage__' in item:
|
|
62
|
-
self._usage = item['__usage__']
|
|
63
|
-
continue
|
|
64
|
-
|
|
65
|
-
# OpenAI-style delta: {"content": "...", "tool_calls": [...]}
|
|
66
|
-
if isinstance(item, dict):
|
|
67
|
-
self._process_delta(item)
|
|
68
|
-
elif isinstance(item, str):
|
|
69
|
-
# Fallback: plain text string (legacy parse_stream behavior)
|
|
70
|
-
self._print(item)
|
|
71
|
-
self._text_parts.append(item)
|
|
72
|
-
|
|
73
|
-
return self._build_message()
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def usage(self) -> Optional[Dict[str, Any]]:
|
|
77
|
-
"""Usage data captured from the stream's final chunk."""
|
|
78
|
-
return self._usage
|
|
79
|
-
|
|
80
|
-
def _process_delta(self, delta: Dict[str, Any]):
|
|
81
|
-
"""Process a single streaming delta dict.
|
|
82
|
-
|
|
83
|
-
Expected shapes (OpenAI format):
|
|
84
|
-
{"content": "some text"}
|
|
85
|
-
{"tool_calls": [{"index": 0, "id": "call_xxx", "function": {"name": "f"}}]}
|
|
86
|
-
{"tool_calls": [{"index": 0, "function": {"arguments": "{..."}}]}
|
|
87
|
-
{"content": "text", "tool_calls": [...]}
|
|
88
|
-
"""
|
|
89
|
-
# Handle text content
|
|
90
|
-
content = delta.get("content")
|
|
91
|
-
if content is not None:
|
|
92
|
-
self._print(content)
|
|
93
|
-
self._text_parts.append(content)
|
|
94
|
-
|
|
95
|
-
# Handle tool call fragments
|
|
96
|
-
tool_calls = delta.get("tool_calls")
|
|
97
|
-
if tool_calls:
|
|
98
|
-
for tc_delta in tool_calls:
|
|
99
|
-
idx = tc_delta.get("index", 0)
|
|
100
|
-
if idx not in self._tool_calls:
|
|
101
|
-
self._tool_calls[idx] = {
|
|
102
|
-
"id": "",
|
|
103
|
-
"type": "function",
|
|
104
|
-
"function": {"name": "", "arguments": ""},
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
entry = self._tool_calls[idx]
|
|
108
|
-
|
|
109
|
-
# Tool call id (sent once at the start)
|
|
110
|
-
if tc_delta.get("id"):
|
|
111
|
-
entry["id"] = tc_delta["id"]
|
|
112
|
-
|
|
113
|
-
# Function name (sent once at the start)
|
|
114
|
-
func = tc_delta.get("function", {})
|
|
115
|
-
if func.get("name"):
|
|
116
|
-
entry["function"]["name"] = func["name"]
|
|
117
|
-
|
|
118
|
-
# Arguments (sent incrementally, concatenated)
|
|
119
|
-
if func.get("arguments"):
|
|
120
|
-
entry["function"]["arguments"] += func["arguments"]
|
|
121
|
-
|
|
122
|
-
def _build_message(self) -> Dict[str, Any]:
|
|
123
|
-
"""Build the final message dict from assembled parts."""
|
|
124
|
-
message: Dict[str, Any] = {"role": "assistant"}
|
|
125
|
-
|
|
126
|
-
# Collect assembled tool calls in index order
|
|
127
|
-
assembled_tool_calls = []
|
|
128
|
-
if self._tool_calls:
|
|
129
|
-
for idx in sorted(self._tool_calls.keys()):
|
|
130
|
-
assembled_tool_calls.append(self._tool_calls[idx])
|
|
131
|
-
|
|
132
|
-
if assembled_tool_calls:
|
|
133
|
-
message["tool_calls"] = assembled_tool_calls
|
|
134
|
-
# Content may be None or a string alongside tool calls
|
|
135
|
-
text = "".join(self._text_parts).strip()
|
|
136
|
-
message["content"] = text if text else None
|
|
137
|
-
else:
|
|
138
|
-
message["content"] = "".join(self._text_parts)
|
|
139
|
-
|
|
140
|
-
return message
|
|
141
|
-
|
|
142
|
-
def _print(self, text: str):
|
|
143
|
-
"""Output text token via the configured callback (default: stderr).
|
|
144
|
-
|
|
145
|
-
When a Rich Live context is provided, text is rendered through Live
|
|
146
|
-
for atomic screen updates (raw text during streaming, swappable to
|
|
147
|
-
Markdown on completion).
|
|
148
|
-
"""
|
|
149
|
-
if self._live is not None:
|
|
150
|
-
# Render through Rich Live — update with accumulated text so far
|
|
151
|
-
self._live.update(Text("".join(self._text_parts) + text))
|
|
152
|
-
elif self._on_text is None:
|
|
153
|
-
# Default: print to stderr
|
|
154
|
-
sys.stderr.write(text)
|
|
155
|
-
sys.stderr.flush()
|
|
156
|
-
elif callable(self._on_text):
|
|
157
|
-
self._on_text(text)
|
|
158
|
-
# If on_text is False, silently drop output (subagent mode)
|
|
159
|
-
|
|
160
|
-
def close(self):
|
|
161
|
-
"""Close the underlying stream."""
|
|
162
|
-
if hasattr(self._stream, 'close'):
|
|
163
|
-
self._stream.close()
|