workbuddy2api 2.0.0__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.
- codebuddy_proxy/__main__.py +1572 -0
- codebuddy_proxy/anthropic_adapter.py +439 -0
- codebuddy_proxy/codebuddy_client_demo.py +312 -0
- codebuddy_proxy/desensitize.py +532 -0
- codebuddy_proxy/dsml_parser.py +888 -0
- codebuddy_proxy/projection_metadata.py +410 -0
- codebuddy_proxy/responses_adapter.py +487 -0
- codebuddy_proxy/responses_projection.py +746 -0
- workbuddy2api-2.0.0.dist-info/METADATA +634 -0
- workbuddy2api-2.0.0.dist-info/RECORD +14 -0
- workbuddy2api-2.0.0.dist-info/WHEEL +5 -0
- workbuddy2api-2.0.0.dist-info/entry_points.txt +2 -0
- workbuddy2api-2.0.0.dist-info/licenses/LICENSE +21 -0
- workbuddy2api-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
anthropic_adapter.py — Anthropic Messages API ↔ Chat Completions API 适配层。
|
|
3
|
+
|
|
4
|
+
Claude Code / CC Switch 使用 Anthropic Messages API,而 CodeBuddy 后端只支持
|
|
5
|
+
Chat Completions 协议。本模块做双向转换:
|
|
6
|
+
请求:Anthropic system/messages/tools → Chat messages/tools
|
|
7
|
+
响应:Chat SSE delta → Anthropic SSE 事件流(message_start / content_block_delta / …)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _rand_id(prefix: str = "msg_") -> str:
|
|
19
|
+
"""生成随机ID"""
|
|
20
|
+
return prefix + os.urandom(12).hex()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _now_s() -> int:
|
|
24
|
+
"""当前时间戳(秒)"""
|
|
25
|
+
return int(time.time())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _extract_text(value: Any) -> str:
|
|
29
|
+
"""从content中提取文本"""
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
return value
|
|
32
|
+
if isinstance(value, list):
|
|
33
|
+
parts = []
|
|
34
|
+
for item in value:
|
|
35
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
36
|
+
parts.append(item.get("text", ""))
|
|
37
|
+
elif isinstance(item, str):
|
|
38
|
+
parts.append(item)
|
|
39
|
+
return "\n".join(parts) if parts else ""
|
|
40
|
+
return "" if value is None else str(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _convert_anthropic_message(msg: dict) -> list[dict]:
|
|
44
|
+
"""将单条Anthropic消息转换为Chat消息(可能返回多条)
|
|
45
|
+
|
|
46
|
+
Anthropic的content可能包含:
|
|
47
|
+
- 字符串(纯文本)
|
|
48
|
+
- [{"type": "text", "text": "..."}, {"type": "tool_result", ...}]
|
|
49
|
+
|
|
50
|
+
tool_result块需要分离成独立的tool消息
|
|
51
|
+
"""
|
|
52
|
+
role = msg.get("role", "user")
|
|
53
|
+
content = msg.get("content", "")
|
|
54
|
+
|
|
55
|
+
# 简单字符串content
|
|
56
|
+
if isinstance(content, str):
|
|
57
|
+
if not content:
|
|
58
|
+
return []
|
|
59
|
+
return [{"role": role, "content": content}]
|
|
60
|
+
|
|
61
|
+
# 空content
|
|
62
|
+
if not content:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
# 复杂content blocks
|
|
66
|
+
if not isinstance(content, list):
|
|
67
|
+
content = [content]
|
|
68
|
+
|
|
69
|
+
messages = []
|
|
70
|
+
|
|
71
|
+
# assistant消息处理
|
|
72
|
+
if role == "assistant":
|
|
73
|
+
text_parts = []
|
|
74
|
+
tool_calls = []
|
|
75
|
+
|
|
76
|
+
for block in content:
|
|
77
|
+
if not isinstance(block, dict):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
block_type = block.get("type")
|
|
81
|
+
|
|
82
|
+
if block_type == "text":
|
|
83
|
+
text_parts.append(block.get("text", ""))
|
|
84
|
+
|
|
85
|
+
elif block_type == "tool_use":
|
|
86
|
+
tool_calls.append({
|
|
87
|
+
"id": block.get("id", _rand_id("call_")),
|
|
88
|
+
"type": "function",
|
|
89
|
+
"function": {
|
|
90
|
+
"name": block.get("name", ""),
|
|
91
|
+
"arguments": json.dumps(block.get("input", {})),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# 构造assistant消息
|
|
96
|
+
assistant_msg: dict[str, Any] = {
|
|
97
|
+
"role": "assistant",
|
|
98
|
+
"content": "\n".join(text_parts) if text_parts else None,
|
|
99
|
+
}
|
|
100
|
+
if tool_calls:
|
|
101
|
+
assistant_msg["tool_calls"] = tool_calls
|
|
102
|
+
messages.append(assistant_msg)
|
|
103
|
+
|
|
104
|
+
# user消息处理
|
|
105
|
+
elif role == "user":
|
|
106
|
+
text_parts = []
|
|
107
|
+
tool_results = []
|
|
108
|
+
|
|
109
|
+
for block in content:
|
|
110
|
+
if not isinstance(block, dict):
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
block_type = block.get("type")
|
|
114
|
+
|
|
115
|
+
if block_type == "text":
|
|
116
|
+
text_parts.append(block.get("text", ""))
|
|
117
|
+
|
|
118
|
+
elif block_type == "tool_result":
|
|
119
|
+
# tool_result块 → 独立的tool消息
|
|
120
|
+
result_content = block.get("content", "")
|
|
121
|
+
if isinstance(result_content, list):
|
|
122
|
+
# 提取text块
|
|
123
|
+
result_text = []
|
|
124
|
+
for part in result_content:
|
|
125
|
+
if isinstance(part, dict) and part.get("type") == "text":
|
|
126
|
+
result_text.append(part.get("text", ""))
|
|
127
|
+
result_content = "\n".join(result_text)
|
|
128
|
+
|
|
129
|
+
tool_results.append({
|
|
130
|
+
"role": "tool",
|
|
131
|
+
"tool_call_id": block.get("tool_use_id", ""),
|
|
132
|
+
"content": str(result_content),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
# 添加user文本消息
|
|
136
|
+
if text_parts:
|
|
137
|
+
messages.append({"role": "user", "content": "\n".join(text_parts)})
|
|
138
|
+
|
|
139
|
+
# 添加tool结果消息
|
|
140
|
+
messages.extend(tool_results)
|
|
141
|
+
|
|
142
|
+
# 其他角色(system等)
|
|
143
|
+
else:
|
|
144
|
+
text = _extract_text(content)
|
|
145
|
+
if text:
|
|
146
|
+
messages.append({"role": role, "content": text})
|
|
147
|
+
|
|
148
|
+
return messages
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def anthropic_request_to_chat(body: dict) -> dict:
|
|
152
|
+
"""将Anthropic Messages API请求体转换为Chat Completions请求体
|
|
153
|
+
|
|
154
|
+
关键映射:
|
|
155
|
+
system → system message(置顶)
|
|
156
|
+
messages → messages(展开content blocks)
|
|
157
|
+
tools → Chat格式tools
|
|
158
|
+
tool_choice → Chat格式
|
|
159
|
+
"""
|
|
160
|
+
messages: list[dict] = []
|
|
161
|
+
|
|
162
|
+
# system参数 → system message
|
|
163
|
+
system = body.get("system")
|
|
164
|
+
if system:
|
|
165
|
+
system_text = _extract_text(system)
|
|
166
|
+
if system_text:
|
|
167
|
+
messages.append({"role": "system", "content": system_text})
|
|
168
|
+
|
|
169
|
+
# messages转换
|
|
170
|
+
for msg in body.get("messages", []):
|
|
171
|
+
messages.extend(_convert_anthropic_message(msg))
|
|
172
|
+
|
|
173
|
+
# 构造Chat body
|
|
174
|
+
chat: dict[str, Any] = {
|
|
175
|
+
"messages": messages,
|
|
176
|
+
"stream": True, # Anthropic默认也是流式
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# model
|
|
180
|
+
if "model" in body:
|
|
181
|
+
chat["model"] = body["model"]
|
|
182
|
+
|
|
183
|
+
# tools转换
|
|
184
|
+
tools = body.get("tools")
|
|
185
|
+
if tools:
|
|
186
|
+
chat["tools"] = _convert_tools_for_chat(tools)
|
|
187
|
+
|
|
188
|
+
# tool_choice转换
|
|
189
|
+
tool_choice = body.get("tool_choice")
|
|
190
|
+
if tool_choice is not None:
|
|
191
|
+
chat["tool_choice"] = _convert_tool_choice(tool_choice)
|
|
192
|
+
|
|
193
|
+
# 透传参数
|
|
194
|
+
for key in ("max_tokens", "temperature", "top_p", "stop", "top_k"):
|
|
195
|
+
if key in body:
|
|
196
|
+
chat[key] = body[key]
|
|
197
|
+
|
|
198
|
+
return chat
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _convert_tools_for_chat(tools: list[dict]) -> list[dict]:
|
|
202
|
+
"""将Anthropic工具格式转换为Chat格式
|
|
203
|
+
|
|
204
|
+
Anthropic: {name, description, input_schema}
|
|
205
|
+
Chat: {type: "function", function: {name, description, parameters}}
|
|
206
|
+
"""
|
|
207
|
+
result = []
|
|
208
|
+
for tool in tools:
|
|
209
|
+
# 已经是Chat格式
|
|
210
|
+
if tool.get("type") == "function" and tool.get("function"):
|
|
211
|
+
result.append(tool)
|
|
212
|
+
# Anthropic格式
|
|
213
|
+
elif tool.get("name"):
|
|
214
|
+
result.append({
|
|
215
|
+
"type": "function",
|
|
216
|
+
"function": {
|
|
217
|
+
"name": tool["name"],
|
|
218
|
+
"description": tool.get("description", ""),
|
|
219
|
+
"parameters": tool.get("input_schema", {}),
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
else:
|
|
223
|
+
result.append(tool)
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _convert_tool_choice(tool_choice: Any) -> Any:
|
|
228
|
+
"""转换tool_choice格式
|
|
229
|
+
|
|
230
|
+
Anthropic支持:
|
|
231
|
+
- "auto" / "any" / "required" (字符串)
|
|
232
|
+
- {"type": "tool", "name": "..."} (指定工具)
|
|
233
|
+
|
|
234
|
+
Chat支持:
|
|
235
|
+
- "auto" / "none" / "required" (字符串)
|
|
236
|
+
- {"type": "function", "function": {"name": "..."}}
|
|
237
|
+
"""
|
|
238
|
+
if isinstance(tool_choice, str):
|
|
239
|
+
# Anthropic的"any" → Chat的"required"
|
|
240
|
+
if tool_choice == "any":
|
|
241
|
+
return "required"
|
|
242
|
+
# "auto" / "none" 直接映射
|
|
243
|
+
return tool_choice
|
|
244
|
+
|
|
245
|
+
if isinstance(tool_choice, dict):
|
|
246
|
+
# {"type": "tool", "name": "..."} → {"type": "function", "function": {"name": "..."}}
|
|
247
|
+
if tool_choice.get("type") == "tool" and tool_choice.get("name"):
|
|
248
|
+
return {
|
|
249
|
+
"type": "function",
|
|
250
|
+
"function": {"name": tool_choice["name"]},
|
|
251
|
+
}
|
|
252
|
+
# 已经是Chat格式
|
|
253
|
+
return tool_choice
|
|
254
|
+
|
|
255
|
+
return tool_choice
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# 响应转换:Chat SSE → Anthropic SSE事件流
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
class AnthropicStreamConverter:
|
|
263
|
+
"""将Chat SSE流转换为Anthropic Messages API事件流
|
|
264
|
+
|
|
265
|
+
事件序列:
|
|
266
|
+
1. message_start
|
|
267
|
+
2. content_block_start (text)
|
|
268
|
+
3. content_block_delta (text_delta,多次)
|
|
269
|
+
4. content_block_stop
|
|
270
|
+
5. content_block_start (tool_use)
|
|
271
|
+
6. content_block_delta (input_json_delta,多次)
|
|
272
|
+
7. content_block_stop
|
|
273
|
+
8. message_delta (stop_reason + usage)
|
|
274
|
+
9. message_stop
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, model: str):
|
|
278
|
+
self.message_id = _rand_id("msg_")
|
|
279
|
+
self.model = model
|
|
280
|
+
|
|
281
|
+
# 状态跟踪
|
|
282
|
+
self.started = False
|
|
283
|
+
self.text_block_index: int | None = None # 当前text块的index
|
|
284
|
+
self.text = ""
|
|
285
|
+
self.tool_blocks: dict[int, dict] = {} # Chat index -> {Anthropic index, id, name, arguments}
|
|
286
|
+
self.next_anthropic_index = 0 # Anthropic content_block的index计数器
|
|
287
|
+
self.finish_reason: str | None = None
|
|
288
|
+
self.usage: dict | None = None
|
|
289
|
+
self.open_blocks: set[int] = set() # 已打开但未关闭的块index
|
|
290
|
+
|
|
291
|
+
def feed_chunk(self, chunk: dict) -> list[tuple[str, dict]]:
|
|
292
|
+
"""处理一个Chat SSE chunk,返回Anthropic事件列表"""
|
|
293
|
+
events: list[tuple[str, dict]] = []
|
|
294
|
+
|
|
295
|
+
# 首次chunk:发出message_start
|
|
296
|
+
if not self.started:
|
|
297
|
+
self.started = True
|
|
298
|
+
events.append(("message_start", {
|
|
299
|
+
"type": "message_start",
|
|
300
|
+
"message": {
|
|
301
|
+
"id": self.message_id,
|
|
302
|
+
"type": "message",
|
|
303
|
+
"role": "assistant",
|
|
304
|
+
"content": [],
|
|
305
|
+
"model": self.model,
|
|
306
|
+
"stop_reason": None,
|
|
307
|
+
"stop_sequence": None,
|
|
308
|
+
"usage": {"input_tokens": 0, "output_tokens": 0},
|
|
309
|
+
},
|
|
310
|
+
}))
|
|
311
|
+
|
|
312
|
+
# 提取usage
|
|
313
|
+
if chunk.get("usage"):
|
|
314
|
+
self.usage = chunk["usage"]
|
|
315
|
+
|
|
316
|
+
# 处理choices
|
|
317
|
+
for choice in chunk.get("choices", []):
|
|
318
|
+
self.finish_reason = choice.get("finish_reason") or self.finish_reason
|
|
319
|
+
delta = choice.get("delta", {})
|
|
320
|
+
|
|
321
|
+
# 文本内容
|
|
322
|
+
if delta.get("content"):
|
|
323
|
+
text_delta = str(delta["content"])
|
|
324
|
+
self.text += text_delta
|
|
325
|
+
|
|
326
|
+
# 首次文本:打开text块
|
|
327
|
+
if self.text_block_index is None:
|
|
328
|
+
self.text_block_index = self.next_anthropic_index
|
|
329
|
+
self.next_anthropic_index += 1
|
|
330
|
+
self.open_blocks.add(self.text_block_index)
|
|
331
|
+
events.append(("content_block_start", {
|
|
332
|
+
"type": "content_block_start",
|
|
333
|
+
"index": self.text_block_index,
|
|
334
|
+
"content_block": {"type": "text", "text": ""},
|
|
335
|
+
}))
|
|
336
|
+
|
|
337
|
+
# 发出text_delta
|
|
338
|
+
events.append(("content_block_delta", {
|
|
339
|
+
"type": "content_block_delta",
|
|
340
|
+
"index": self.text_block_index,
|
|
341
|
+
"delta": {"type": "text_delta", "text": text_delta},
|
|
342
|
+
}))
|
|
343
|
+
|
|
344
|
+
# 工具调用
|
|
345
|
+
for call in delta.get("tool_calls", []):
|
|
346
|
+
chat_index = int(call.get("index", 0))
|
|
347
|
+
|
|
348
|
+
# 初始化工具块
|
|
349
|
+
if chat_index not in self.tool_blocks:
|
|
350
|
+
anthropic_index = self.next_anthropic_index
|
|
351
|
+
self.next_anthropic_index += 1
|
|
352
|
+
self.tool_blocks[chat_index] = {
|
|
353
|
+
"anthropic_index": anthropic_index,
|
|
354
|
+
"id": None,
|
|
355
|
+
"name": None,
|
|
356
|
+
"arguments": "",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
slot = self.tool_blocks[chat_index]
|
|
360
|
+
anthropic_index = slot["anthropic_index"]
|
|
361
|
+
|
|
362
|
+
call_id = call.get("id")
|
|
363
|
+
if call_id and not slot["id"]:
|
|
364
|
+
slot["id"] = call_id
|
|
365
|
+
|
|
366
|
+
fn = call.get("function", {})
|
|
367
|
+
fn_name = fn.get("name")
|
|
368
|
+
fn_args = fn.get("arguments")
|
|
369
|
+
|
|
370
|
+
# 首次见到工具名:打开tool_use块
|
|
371
|
+
if fn_name and not slot["name"]:
|
|
372
|
+
slot["name"] = fn_name
|
|
373
|
+
self.open_blocks.add(anthropic_index)
|
|
374
|
+
events.append(("content_block_start", {
|
|
375
|
+
"type": "content_block_start",
|
|
376
|
+
"index": anthropic_index,
|
|
377
|
+
"content_block": {
|
|
378
|
+
"type": "tool_use",
|
|
379
|
+
"id": slot["id"] or f"toolu_{self.message_id}_{chat_index}",
|
|
380
|
+
"name": fn_name,
|
|
381
|
+
"input": {},
|
|
382
|
+
},
|
|
383
|
+
}))
|
|
384
|
+
|
|
385
|
+
# arguments增量
|
|
386
|
+
if fn_args:
|
|
387
|
+
slot["arguments"] += fn_args
|
|
388
|
+
events.append(("content_block_delta", {
|
|
389
|
+
"type": "content_block_delta",
|
|
390
|
+
"index": anthropic_index,
|
|
391
|
+
"delta": {"type": "input_json_delta", "partial_json": fn_args},
|
|
392
|
+
}))
|
|
393
|
+
|
|
394
|
+
return events
|
|
395
|
+
|
|
396
|
+
def finish(self) -> list[tuple[str, dict]]:
|
|
397
|
+
"""流结束,发出stop事件"""
|
|
398
|
+
events: list[tuple[str, dict]] = []
|
|
399
|
+
|
|
400
|
+
# 关闭所有打开的块
|
|
401
|
+
for index in sorted(self.open_blocks):
|
|
402
|
+
events.append(("content_block_stop", {
|
|
403
|
+
"type": "content_block_stop",
|
|
404
|
+
"index": index,
|
|
405
|
+
}))
|
|
406
|
+
|
|
407
|
+
# 映射finish_reason
|
|
408
|
+
stop_reason_map = {
|
|
409
|
+
"stop": "end_turn",
|
|
410
|
+
"tool_calls": "tool_use",
|
|
411
|
+
"length": "max_tokens",
|
|
412
|
+
}
|
|
413
|
+
stop_reason = stop_reason_map.get(self.finish_reason or "stop", "end_turn")
|
|
414
|
+
|
|
415
|
+
# 映射usage
|
|
416
|
+
usage_delta = {"output_tokens": 0}
|
|
417
|
+
if self.usage:
|
|
418
|
+
# Chat使用completion_tokens,Anthropic使用output_tokens
|
|
419
|
+
usage_delta["output_tokens"] = self.usage.get("completion_tokens", 0)
|
|
420
|
+
|
|
421
|
+
# 发出message_delta
|
|
422
|
+
events.append(("message_delta", {
|
|
423
|
+
"type": "message_delta",
|
|
424
|
+
"delta": {
|
|
425
|
+
"stop_reason": stop_reason,
|
|
426
|
+
"stop_sequence": None,
|
|
427
|
+
},
|
|
428
|
+
"usage": usage_delta,
|
|
429
|
+
}))
|
|
430
|
+
|
|
431
|
+
# 发出message_stop
|
|
432
|
+
events.append(("message_stop", {"type": "message_stop"}))
|
|
433
|
+
|
|
434
|
+
return events
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# 向后兼容别名
|
|
439
|
+
anthropic_to_chat = anthropic_request_to_chat
|