crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""OpenAI API adapter — translates between internal Anthropic-style messages and OpenAI format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, AsyncGenerator
|
|
8
|
+
|
|
9
|
+
from crabcode_core.api.base import (
|
|
10
|
+
APIAdapter,
|
|
11
|
+
ModelConfig,
|
|
12
|
+
StreamChunk,
|
|
13
|
+
normalize_openai_usage,
|
|
14
|
+
)
|
|
15
|
+
from crabcode_core.types.config import ApiConfig
|
|
16
|
+
from crabcode_core.utf8_sanitize import safe_utf8_json_tree, safe_utf8_str
|
|
17
|
+
from crabcode_core.types.message import (
|
|
18
|
+
Message,
|
|
19
|
+
MessageRole,
|
|
20
|
+
TextBlock,
|
|
21
|
+
ToolResultBlock,
|
|
22
|
+
ToolUseBlock,
|
|
23
|
+
ThinkingBlock,
|
|
24
|
+
ImageBlock,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _messages_to_openai(
|
|
29
|
+
messages: list[Message],
|
|
30
|
+
system: list[str],
|
|
31
|
+
pass_reasoning_content: bool = False,
|
|
32
|
+
) -> list[dict[str, Any]]:
|
|
33
|
+
"""Convert internal messages + system to OpenAI chat format."""
|
|
34
|
+
result: list[dict[str, Any]] = []
|
|
35
|
+
pending_tool_ids: set[str] = set()
|
|
36
|
+
deferred_images: list[dict[str, Any]] = []
|
|
37
|
+
|
|
38
|
+
def require_complete_image_batch() -> None:
|
|
39
|
+
if deferred_images:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Cannot send tool images: missing tool results for "
|
|
42
|
+
+ ", ".join(sorted(pending_tool_ids))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
system_text = "\n\n".join(s for s in system if s)
|
|
46
|
+
if system_text:
|
|
47
|
+
result.append({"role": "system", "content": system_text})
|
|
48
|
+
|
|
49
|
+
for msg in messages:
|
|
50
|
+
if msg.role == MessageRole.SYSTEM:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if isinstance(msg.content, str):
|
|
54
|
+
require_complete_image_batch()
|
|
55
|
+
result.append({"role": msg.role.value, "content": msg.content})
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
text_parts: list[str] = []
|
|
59
|
+
reasoning_parts: list[str] = []
|
|
60
|
+
tool_calls: list[dict[str, Any]] = []
|
|
61
|
+
tool_results: list[dict[str, Any]] = []
|
|
62
|
+
|
|
63
|
+
for block in msg.content:
|
|
64
|
+
if isinstance(block, TextBlock):
|
|
65
|
+
text_parts.append(block.text)
|
|
66
|
+
elif isinstance(block, ToolUseBlock):
|
|
67
|
+
tool_calls.append({
|
|
68
|
+
"id": block.id,
|
|
69
|
+
"type": "function",
|
|
70
|
+
"function": {
|
|
71
|
+
"name": block.name,
|
|
72
|
+
"arguments": json.dumps(block.input),
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
elif isinstance(block, ToolResultBlock):
|
|
76
|
+
tool_results.append({
|
|
77
|
+
"role": "tool",
|
|
78
|
+
"tool_call_id": block.tool_use_id,
|
|
79
|
+
"content": block.content,
|
|
80
|
+
})
|
|
81
|
+
elif isinstance(block, ThinkingBlock):
|
|
82
|
+
reasoning_parts.append(block.thinking)
|
|
83
|
+
|
|
84
|
+
if msg.role == MessageRole.ASSISTANT:
|
|
85
|
+
require_complete_image_batch()
|
|
86
|
+
pending_tool_ids = {call["id"] for call in tool_calls}
|
|
87
|
+
entry: dict[str, Any] = {"role": "assistant"}
|
|
88
|
+
if text_parts:
|
|
89
|
+
entry["content"] = "".join(text_parts)
|
|
90
|
+
elif tool_calls:
|
|
91
|
+
# OpenAI expects explicit null when the turn is tool-only.
|
|
92
|
+
entry["content"] = None
|
|
93
|
+
if tool_calls:
|
|
94
|
+
entry["tool_calls"] = tool_calls
|
|
95
|
+
if pass_reasoning_content and reasoning_parts:
|
|
96
|
+
entry["reasoning_content"] = "".join(reasoning_parts)
|
|
97
|
+
result.append(entry)
|
|
98
|
+
elif msg.role == MessageRole.USER:
|
|
99
|
+
if not tool_results:
|
|
100
|
+
require_complete_image_batch()
|
|
101
|
+
if tool_results:
|
|
102
|
+
result.extend(tool_results)
|
|
103
|
+
pending_tool_ids.difference_update(
|
|
104
|
+
item["tool_call_id"] for item in tool_results
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Chat Completions requires all tool results before the next user
|
|
108
|
+
# message. Hold tool images until the entire call batch is closed.
|
|
109
|
+
has_images = isinstance(msg.content, list) and any(
|
|
110
|
+
isinstance(b, ImageBlock) for b in msg.content
|
|
111
|
+
)
|
|
112
|
+
if has_images:
|
|
113
|
+
content_parts: list[dict[str, Any]] = []
|
|
114
|
+
for block in msg.content if isinstance(msg.content, list) else []:
|
|
115
|
+
if isinstance(block, TextBlock):
|
|
116
|
+
content_parts.append({"type": "text", "text": block.text})
|
|
117
|
+
elif isinstance(block, ImageBlock):
|
|
118
|
+
# Build data URL from base64 source
|
|
119
|
+
source = block.source
|
|
120
|
+
if source.get("type") == "base64":
|
|
121
|
+
data_url = f"data:{source.get('media_type', 'image/png')};base64,{source.get('data', '')}"
|
|
122
|
+
else:
|
|
123
|
+
data_url = source.get("url", "")
|
|
124
|
+
content_parts.append({
|
|
125
|
+
"type": "image_url",
|
|
126
|
+
"image_url": {"url": data_url},
|
|
127
|
+
})
|
|
128
|
+
if content_parts:
|
|
129
|
+
image_message = {"role": "user", "content": content_parts}
|
|
130
|
+
if tool_results:
|
|
131
|
+
deferred_images.append(image_message)
|
|
132
|
+
else:
|
|
133
|
+
result.append(image_message)
|
|
134
|
+
elif text_parts and not tool_results:
|
|
135
|
+
result.append({"role": "user", "content": "".join(text_parts)})
|
|
136
|
+
|
|
137
|
+
if not pending_tool_ids:
|
|
138
|
+
result.extend(deferred_images)
|
|
139
|
+
deferred_images.clear()
|
|
140
|
+
|
|
141
|
+
require_complete_image_batch()
|
|
142
|
+
return safe_utf8_json_tree(result)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _tools_to_openai(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
146
|
+
"""Convert tool schemas to OpenAI function format."""
|
|
147
|
+
result = []
|
|
148
|
+
for tool in tools:
|
|
149
|
+
schema = tool.get("input_schema", {"type": "object", "properties": {}})
|
|
150
|
+
result.append({
|
|
151
|
+
"type": "function",
|
|
152
|
+
"function": {
|
|
153
|
+
"name": tool["name"],
|
|
154
|
+
"description": tool.get("description", ""),
|
|
155
|
+
"parameters": schema,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
return safe_utf8_json_tree(result)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class OpenAIAdapter(APIAdapter):
|
|
162
|
+
"""Adapter for OpenAI-compatible APIs.
|
|
163
|
+
|
|
164
|
+
Designed to be subclassed by providers that use an OpenAI-compatible
|
|
165
|
+
chat completions surface (Azure, Ollama, Gemini, etc.). Subclasses
|
|
166
|
+
can override :meth:`_create_client` and :meth:`_resolve_model` to
|
|
167
|
+
customise client construction and model/deployment name resolution.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, config: ApiConfig):
|
|
171
|
+
self.config = config
|
|
172
|
+
self.client = self._create_client(config)
|
|
173
|
+
|
|
174
|
+
def _create_client(self, config: ApiConfig) -> Any:
|
|
175
|
+
"""Create the async OpenAI client. Override in subclasses."""
|
|
176
|
+
import openai
|
|
177
|
+
|
|
178
|
+
api_key: str | None = None
|
|
179
|
+
if config.api_key_env:
|
|
180
|
+
api_key = os.environ.get(config.api_key_env)
|
|
181
|
+
if not api_key:
|
|
182
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
183
|
+
|
|
184
|
+
kwargs: dict[str, Any] = {}
|
|
185
|
+
if api_key:
|
|
186
|
+
kwargs["api_key"] = api_key
|
|
187
|
+
if config.base_url:
|
|
188
|
+
kwargs["base_url"] = config.base_url
|
|
189
|
+
if config.http_headers:
|
|
190
|
+
kwargs["default_headers"] = config.http_headers
|
|
191
|
+
|
|
192
|
+
from crabcode_core.api.network import sdk_options
|
|
193
|
+
return openai.AsyncOpenAI(**kwargs, **sdk_options(config))
|
|
194
|
+
|
|
195
|
+
def _resolve_model(self, config: ModelConfig) -> str:
|
|
196
|
+
"""Resolve the model name for the API call. Override in subclasses."""
|
|
197
|
+
return config.model or self.config.model or "gpt-4o"
|
|
198
|
+
|
|
199
|
+
async def stream_message(
|
|
200
|
+
self,
|
|
201
|
+
messages: list[Message],
|
|
202
|
+
system: list[str],
|
|
203
|
+
tools: list[dict[str, Any]],
|
|
204
|
+
config: ModelConfig,
|
|
205
|
+
) -> AsyncGenerator[StreamChunk, None]:
|
|
206
|
+
model = self._resolve_model(config)
|
|
207
|
+
|
|
208
|
+
params: dict[str, Any] = {
|
|
209
|
+
"model": model,
|
|
210
|
+
"max_tokens": config.max_tokens,
|
|
211
|
+
"messages": _messages_to_openai(
|
|
212
|
+
messages,
|
|
213
|
+
system,
|
|
214
|
+
pass_reasoning_content=self.config.pass_reasoning_content,
|
|
215
|
+
),
|
|
216
|
+
"stream": True,
|
|
217
|
+
"stream_options": {"include_usage": True},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
api_tools = _tools_to_openai(tools)
|
|
221
|
+
if api_tools:
|
|
222
|
+
params["tools"] = api_tools
|
|
223
|
+
|
|
224
|
+
if config.temperature is not None:
|
|
225
|
+
params["temperature"] = config.temperature
|
|
226
|
+
|
|
227
|
+
extra_body = safe_utf8_json_tree(dict(self.config.extra_body or {}))
|
|
228
|
+
if extra_body:
|
|
229
|
+
params["extra_body"] = extra_body
|
|
230
|
+
|
|
231
|
+
tool_call_buffers: dict[int, dict[str, str]] = {}
|
|
232
|
+
|
|
233
|
+
stream = await self.client.chat.completions.create(**params)
|
|
234
|
+
async for chunk in stream:
|
|
235
|
+
if not chunk.choices:
|
|
236
|
+
if chunk.usage:
|
|
237
|
+
yield StreamChunk(
|
|
238
|
+
type="message_delta",
|
|
239
|
+
usage=normalize_openai_usage(chunk.usage),
|
|
240
|
+
model_id=str(getattr(chunk, "model", "") or ""),
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
delta = chunk.choices[0].delta
|
|
245
|
+
finish_reason = chunk.choices[0].finish_reason
|
|
246
|
+
|
|
247
|
+
reasoning_content = getattr(delta, "reasoning_content", None)
|
|
248
|
+
if reasoning_content:
|
|
249
|
+
yield StreamChunk(
|
|
250
|
+
type="thinking",
|
|
251
|
+
text=safe_utf8_str(reasoning_content),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if delta.content:
|
|
255
|
+
yield StreamChunk(type="text", text=safe_utf8_str(delta.content))
|
|
256
|
+
|
|
257
|
+
if delta.tool_calls:
|
|
258
|
+
for tc in delta.tool_calls:
|
|
259
|
+
idx = tc.index
|
|
260
|
+
if idx not in tool_call_buffers:
|
|
261
|
+
tool_call_buffers[idx] = {
|
|
262
|
+
"id": tc.id or "",
|
|
263
|
+
"name": "",
|
|
264
|
+
"arguments": "",
|
|
265
|
+
}
|
|
266
|
+
buf = tool_call_buffers[idx]
|
|
267
|
+
if tc.id:
|
|
268
|
+
buf["id"] = tc.id
|
|
269
|
+
if tc.function:
|
|
270
|
+
if tc.function.name:
|
|
271
|
+
buf["name"] = tc.function.name
|
|
272
|
+
yield StreamChunk(
|
|
273
|
+
type="tool_use_start",
|
|
274
|
+
tool_use_id=buf["id"],
|
|
275
|
+
tool_name=buf["name"],
|
|
276
|
+
)
|
|
277
|
+
if tc.function.arguments:
|
|
278
|
+
buf["arguments"] += tc.function.arguments
|
|
279
|
+
yield StreamChunk(
|
|
280
|
+
type="tool_use_delta",
|
|
281
|
+
tool_use_id=buf["id"],
|
|
282
|
+
tool_input_json=tc.function.arguments,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if finish_reason == "tool_calls":
|
|
286
|
+
for buf in tool_call_buffers.values():
|
|
287
|
+
yield StreamChunk(
|
|
288
|
+
type="tool_use_end",
|
|
289
|
+
tool_use_id=buf["id"],
|
|
290
|
+
tool_name=buf["name"],
|
|
291
|
+
tool_input_json=buf["arguments"],
|
|
292
|
+
)
|
|
293
|
+
tool_call_buffers.clear()
|
|
294
|
+
|
|
295
|
+
if finish_reason:
|
|
296
|
+
# In particular, length is a completed but truncated response.
|
|
297
|
+
# Preserve it so the query loop reports output_limit instead
|
|
298
|
+
# of mistaking the end of the iterator for a successful turn.
|
|
299
|
+
# Keep consuming the stream for the separate usage-only chunk.
|
|
300
|
+
usage = normalize_openai_usage(chunk.usage) if chunk.usage else {}
|
|
301
|
+
yield StreamChunk(
|
|
302
|
+
type="message_stop",
|
|
303
|
+
stop_reason={"stop": "end_turn", "tool_calls": "tool_use"}.get(
|
|
304
|
+
finish_reason, finish_reason,
|
|
305
|
+
),
|
|
306
|
+
usage=usage,
|
|
307
|
+
model_id=str(getattr(chunk, "model", "") or ""),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def count_tokens(
|
|
311
|
+
self,
|
|
312
|
+
messages: list[Message],
|
|
313
|
+
system: list[str],
|
|
314
|
+
) -> int:
|
|
315
|
+
total = sum(len(s) for s in system)
|
|
316
|
+
for msg in messages:
|
|
317
|
+
if isinstance(msg.content, str):
|
|
318
|
+
total += len(msg.content)
|
|
319
|
+
else:
|
|
320
|
+
for block in msg.content:
|
|
321
|
+
if isinstance(block, TextBlock):
|
|
322
|
+
total += len(block.text)
|
|
323
|
+
return total // 4
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""API adapter factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from crabcode_core.api.base import APIAdapter
|
|
6
|
+
from crabcode_core.types.config import ApiConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_adapter(config: ApiConfig) -> APIAdapter:
|
|
10
|
+
"""Create an API adapter based on configuration."""
|
|
11
|
+
provider = config.provider or "anthropic"
|
|
12
|
+
|
|
13
|
+
if provider == "anthropic":
|
|
14
|
+
from crabcode_core.api.anthropic_adapter import AnthropicAdapter
|
|
15
|
+
return AnthropicAdapter(config)
|
|
16
|
+
elif provider == "bedrock":
|
|
17
|
+
from crabcode_core.api.anthropic_adapter import BedrockAdapter
|
|
18
|
+
return BedrockAdapter(config)
|
|
19
|
+
elif provider == "vertex":
|
|
20
|
+
from crabcode_core.api.anthropic_adapter import VertexAdapter
|
|
21
|
+
return VertexAdapter(config)
|
|
22
|
+
elif provider == "openai":
|
|
23
|
+
from crabcode_core.api.openai_adapter import OpenAIAdapter
|
|
24
|
+
return OpenAIAdapter(config)
|
|
25
|
+
elif provider == "azure":
|
|
26
|
+
from crabcode_core.api.azure_adapter import AzureOpenAIAdapter
|
|
27
|
+
return AzureOpenAIAdapter(config)
|
|
28
|
+
elif provider == "gemini":
|
|
29
|
+
from crabcode_core.api.gemini_adapter import GeminiAdapter
|
|
30
|
+
return GeminiAdapter(config)
|
|
31
|
+
elif provider == "ollama":
|
|
32
|
+
from crabcode_core.api.ollama_adapter import OllamaAdapter
|
|
33
|
+
return OllamaAdapter(config)
|
|
34
|
+
elif provider == "codex":
|
|
35
|
+
from crabcode_core.api.codex_adapter import CodexAdapter
|
|
36
|
+
return CodexAdapter(config)
|
|
37
|
+
elif provider == "router":
|
|
38
|
+
fmt = config.format or "openai"
|
|
39
|
+
if fmt == "anthropic":
|
|
40
|
+
from crabcode_core.api.anthropic_adapter import AnthropicAdapter
|
|
41
|
+
return AnthropicAdapter(config)
|
|
42
|
+
elif fmt == "codex":
|
|
43
|
+
from crabcode_core.api.codex_adapter import CodexAdapter
|
|
44
|
+
return CodexAdapter(config)
|
|
45
|
+
elif fmt == "ollama":
|
|
46
|
+
from crabcode_core.api.ollama_adapter import OllamaAdapter
|
|
47
|
+
return OllamaAdapter(config)
|
|
48
|
+
elif fmt == "gemini":
|
|
49
|
+
from crabcode_core.api.gemini_adapter import GeminiAdapter
|
|
50
|
+
return GeminiAdapter(config)
|
|
51
|
+
elif fmt == "azure":
|
|
52
|
+
from crabcode_core.api.azure_adapter import AzureOpenAIAdapter
|
|
53
|
+
return AzureOpenAIAdapter(config)
|
|
54
|
+
else:
|
|
55
|
+
from crabcode_core.api.openai_adapter import OpenAIAdapter
|
|
56
|
+
return OpenAIAdapter(config)
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"Unknown API provider: {provider}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Slash-command argument parsing, preserving Windows path separators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def split_command_arguments(value: str) -> list[str]:
|
|
10
|
+
if os.name != "nt":
|
|
11
|
+
return shlex.split(value)
|
|
12
|
+
lexer = shlex.shlex(value, posix=True)
|
|
13
|
+
lexer.whitespace_split = True
|
|
14
|
+
lexer.commenters = ""
|
|
15
|
+
# Slash commands are not a shell: backslashes in paths are literal,
|
|
16
|
+
# including a trailing separator inside quotes. Both quote styles work.
|
|
17
|
+
lexer.escape = ""
|
|
18
|
+
return list(lexer)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from crabcode_core.compact.compact import (
|
|
2
|
+
compact_summary_text,
|
|
3
|
+
compact_conversation,
|
|
4
|
+
compaction_input_limit,
|
|
5
|
+
conversation_turn_starts,
|
|
6
|
+
estimate_token_count,
|
|
7
|
+
should_auto_compact,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"compact_conversation",
|
|
12
|
+
"compact_summary_text",
|
|
13
|
+
"compaction_input_limit",
|
|
14
|
+
"conversation_turn_starts",
|
|
15
|
+
"estimate_token_count",
|
|
16
|
+
"should_auto_compact",
|
|
17
|
+
]
|