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.
@@ -0,0 +1,487 @@
1
+ """
2
+ responses_adapter.py — OpenAI Responses API ↔ Chat Completions API 适配层。
3
+
4
+ Codex CLI 使用 Responses API(POST /v1/responses),而 CodeBuddy 后端只支持
5
+ Chat Completions 协议。本模块做双向转换:
6
+ 请求:Responses input/instructions/tools → Chat messages/tools
7
+ 响应:Chat SSE delta → Responses 语义事件流(response.created / output_text.delta / …)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import time
15
+ import uuid
16
+ from typing import Any, Iterator
17
+
18
+
19
+ def _rand_id(prefix: str = "resp_") -> str:
20
+ """生成随机ID"""
21
+ return prefix + os.urandom(12).hex()
22
+
23
+
24
+ def _now_s() -> int:
25
+ """当前时间戳(秒)"""
26
+ return int(time.time())
27
+
28
+
29
+ def _extract_text(value: Any) -> str:
30
+ """从各种content格式中提取纯文本"""
31
+ if isinstance(value, str):
32
+ return value
33
+ if isinstance(value, list):
34
+ parts = []
35
+ for item in value:
36
+ if isinstance(item, dict):
37
+ if item.get("type") in ("text", "input_text"):
38
+ parts.append(item.get("text", ""))
39
+ else:
40
+ parts.append(str(item))
41
+ return "".join(parts)
42
+ return "" if value is None else str(value)
43
+
44
+
45
+ def _convert_tools_for_chat(tools: list[dict]) -> list[dict]:
46
+ """将Responses工具格式转换为Chat格式,清理CodeBuddy后端不兼容的字段
47
+
48
+ Responses: {type, name, description, parameters, strict}
49
+ Chat: {type: "function", function: {name, description, parameters}}
50
+
51
+ 清理不兼容字段:
52
+ - additionalProperties (CodeBuddy 后端不支持)
53
+ - strict (OpenAI 扩展,其他后端不认识)
54
+ """
55
+ result = []
56
+ for tool in tools:
57
+ # 提取工具定义的核心字段
58
+ if tool.get("type") == "function" and tool.get("function"):
59
+ # 已经是Chat格式: {type: "function", function: {...}}
60
+ func = tool["function"]
61
+ name = func.get("name", "")
62
+ description = func.get("description", "")
63
+ parameters = func.get("parameters", {})
64
+ elif tool.get("name"):
65
+ # Responses格式: {type, name, description, parameters, strict}
66
+ name = tool["name"]
67
+ description = tool.get("description", "")
68
+ parameters = tool.get("parameters", {})
69
+ else:
70
+ # 未知格式,原样保留
71
+ result.append(tool)
72
+ continue
73
+
74
+ # 清理 parameters,移除 additionalProperties
75
+ cleaned_params = dict(parameters) if isinstance(parameters, dict) else {}
76
+ cleaned_params.pop("additionalProperties", None)
77
+
78
+ # 构造干净的 Chat 格式工具定义(不包含 strict)
79
+ result.append({
80
+ "type": "function",
81
+ "function": {
82
+ "name": name,
83
+ "description": description,
84
+ "parameters": cleaned_params,
85
+ }
86
+ })
87
+
88
+ return result
89
+
90
+
91
+ def _convert_input_items(items: list) -> list[dict]:
92
+ """将Responses API的input数组转换为Chat messages
93
+
94
+ 处理:
95
+ - {"role": "user/system/developer", "content": ...}
96
+ - {"type": "message", ...}
97
+ - {"type": "function_call", ...} → 合并到前面的assistant消息
98
+ - {"type": "function_call_output", ...} → tool角色
99
+ """
100
+ messages: list[dict] = []
101
+ # 临时缓存:合并相邻的assistant message和function_call
102
+ pending_assistant_content: str | None = None
103
+ pending_tool_calls: list[dict] = []
104
+
105
+ def _flush_assistant():
106
+ nonlocal pending_assistant_content, pending_tool_calls
107
+ if pending_assistant_content is not None or pending_tool_calls:
108
+ msg: dict[str, Any] = {
109
+ "role": "assistant",
110
+ "content": pending_assistant_content or "",
111
+ }
112
+ if pending_tool_calls:
113
+ msg["tool_calls"] = pending_tool_calls[:]
114
+ messages.append(msg)
115
+ pending_assistant_content = None
116
+ pending_tool_calls.clear()
117
+
118
+ for item in items:
119
+ if not isinstance(item, dict):
120
+ continue
121
+
122
+ item_type = item.get("type")
123
+ role = item.get("role", "")
124
+
125
+ # 普通消息(无type标记)
126
+ if item_type is None and role in ("user", "system", "developer"):
127
+ _flush_assistant()
128
+ mapped_role = "system" if role == "developer" else role
129
+ content = _extract_text(item.get("content", ""))
130
+ messages.append({"role": mapped_role, "content": content})
131
+ continue
132
+
133
+ # typed message
134
+ if item_type == "message":
135
+ if role == "assistant":
136
+ _flush_assistant()
137
+ content = _extract_text(item.get("content", ""))
138
+ pending_assistant_content = content
139
+ elif role in ("user", "system", "developer"):
140
+ _flush_assistant()
141
+ mapped_role = "system" if role == "developer" else role
142
+ content = _extract_text(item.get("content", ""))
143
+ messages.append({"role": mapped_role, "content": content})
144
+ continue
145
+
146
+ # input_text - Responses API 的简化输入格式
147
+ if item_type == "input_text":
148
+ _flush_assistant()
149
+ content = item.get("text", "")
150
+ messages.append({"role": "user", "content": content})
151
+ continue
152
+
153
+ # function_call — 合并到前面的assistant消息
154
+ if item_type == "function_call":
155
+ if pending_assistant_content is None:
156
+ pending_assistant_content = ""
157
+ pending_tool_calls.append({
158
+ "id": item.get("call_id", item.get("id", _rand_id("call_"))),
159
+ "type": "function",
160
+ "function": {
161
+ "name": item.get("name", ""),
162
+ "arguments": json.dumps(item.get("arguments", {})) if isinstance(item.get("arguments"), dict) else str(item.get("arguments", "")),
163
+ },
164
+ })
165
+ continue
166
+
167
+ # function_call_output → tool消息
168
+ if item_type == "function_call_output":
169
+ _flush_assistant()
170
+ messages.append({
171
+ "role": "tool",
172
+ "tool_call_id": item.get("call_id", ""),
173
+ "content": _extract_text(item.get("output", "")),
174
+ })
175
+ continue
176
+
177
+ # 其他未知类型,如果有role就当普通消息处理
178
+ if role:
179
+ _flush_assistant()
180
+ content = _extract_text(item.get("content", ""))
181
+ messages.append({"role": role, "content": content})
182
+
183
+ # 刷新最后一条assistant消息
184
+ _flush_assistant()
185
+ return messages
186
+
187
+
188
+ def responses_request_to_chat(body: dict) -> dict:
189
+ """将Responses API请求体转换为Chat Completions请求体
190
+
191
+ 关键映射:
192
+ input → messages
193
+ instructions → system message(置顶)
194
+ max_output_tokens → max_tokens
195
+ tools格式微调
196
+ """
197
+ messages: list[dict] = []
198
+
199
+ # instructions → system message
200
+ instructions = body.get("instructions")
201
+ if instructions:
202
+ messages.append({"role": "system", "content": instructions})
203
+
204
+ # input → messages
205
+ inp = body.get("input", [])
206
+ if isinstance(inp, str):
207
+ messages.append({"role": "user", "content": inp})
208
+ elif isinstance(inp, list):
209
+ messages.extend(_convert_input_items(inp))
210
+
211
+ # 构造Chat body
212
+ chat: dict[str, Any] = {"messages": messages, "stream": True}
213
+
214
+ # model
215
+ if "model" in body:
216
+ chat["model"] = body["model"]
217
+
218
+ # tools
219
+ tools = body.get("tools")
220
+ if tools:
221
+ chat["tools"] = _convert_tools_for_chat(tools)
222
+ if "tool_choice" in body:
223
+ chat["tool_choice"] = body["tool_choice"]
224
+
225
+ # 透传常见参数
226
+ for key in ("temperature", "top_p", "stop", "seed",
227
+ "presence_penalty", "frequency_penalty",
228
+ "response_format", "reasoning_effort"):
229
+ if key in body:
230
+ chat[key] = body[key]
231
+
232
+ # max_output_tokens → max_tokens
233
+ if "max_output_tokens" in body:
234
+ chat["max_tokens"] = body["max_output_tokens"]
235
+ elif "max_tokens" in body:
236
+ chat["max_tokens"] = body["max_tokens"]
237
+
238
+ return chat
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # 响应转换:Chat SSE → Responses事件流
243
+ # ---------------------------------------------------------------------------
244
+
245
+ class ResponsesStreamConverter:
246
+ """将Chat SSE流转换为Responses API事件流
247
+
248
+ 事件序列:
249
+ 1. response.created
250
+ 2. response.in_progress
251
+ 3. response.output_item.added (message)
252
+ 4. response.content_part.added
253
+ 5. response.output_text.delta (多次)
254
+ 6. response.output_item.added (function_call,如有)
255
+ 7. response.function_call_arguments.delta (多次)
256
+ 8. response.output_text.done
257
+ 9. response.content_part.done
258
+ 10. response.output_item.done (各项)
259
+ 11. response.completed
260
+ """
261
+
262
+ def __init__(self, model: str):
263
+ self.response_id = _rand_id("resp_")
264
+ self.model = model
265
+ self.created_at = _now_s()
266
+
267
+ # 状态跟踪
268
+ self.started = False
269
+ self.message_item_added = False
270
+ self.content_part_added = False
271
+ self.text = ""
272
+ self.function_calls: dict[int, dict] = {} # index -> {id, name, arguments}
273
+ self.finish_reason: str | None = None
274
+ self.usage: dict | None = None
275
+
276
+ def feed_chunk(self, chunk: dict) -> list[tuple[str, dict]]:
277
+ """处理一个Chat SSE chunk,返回Responses事件列表"""
278
+ events: list[tuple[str, dict]] = []
279
+
280
+ # 首次chunk:发出created和in_progress
281
+ if not self.started:
282
+ self.started = True
283
+ events.append(("response.created", {
284
+ "type": "response.created",
285
+ "response": {
286
+ "id": self.response_id,
287
+ "object": "realtime.response",
288
+ "status": "in_progress",
289
+ "created_at": self.created_at,
290
+ },
291
+ }))
292
+ events.append(("response.in_progress", {
293
+ "type": "response.in_progress",
294
+ "response": {
295
+ "id": self.response_id,
296
+ "object": "realtime.response",
297
+ "status": "in_progress",
298
+ },
299
+ }))
300
+
301
+ # 提取usage
302
+ if chunk.get("usage"):
303
+ self.usage = chunk["usage"]
304
+
305
+ # 处理choices
306
+ for choice in chunk.get("choices", []):
307
+ self.finish_reason = choice.get("finish_reason") or self.finish_reason
308
+ delta = choice.get("delta", {})
309
+
310
+ # 文本内容
311
+ if delta.get("content"):
312
+ text_delta = str(delta["content"])
313
+ self.text += text_delta
314
+
315
+ # 首次文本:发出output_item.added + content_part.added
316
+ if not self.message_item_added:
317
+ self.message_item_added = True
318
+ events.append(("response.output_item.added", {
319
+ "type": "response.output_item.added",
320
+ "item": {
321
+ "id": self.response_id + "_msg",
322
+ "type": "message",
323
+ "role": "assistant",
324
+ "content": [],
325
+ },
326
+ }))
327
+
328
+ if not self.content_part_added:
329
+ self.content_part_added = True
330
+ events.append(("response.content_part.added", {
331
+ "type": "response.content_part.added",
332
+ "part": {"type": "text", "text": ""},
333
+ }))
334
+
335
+ # 发出文本delta
336
+ events.append(("response.output_text.delta", {
337
+ "type": "response.output_text.delta",
338
+ "item_id": self.response_id + "_msg",
339
+ "output_index": 0,
340
+ "content_index": 0,
341
+ "delta": text_delta,
342
+ }))
343
+
344
+ # 工具调用
345
+ for call in delta.get("tool_calls", []):
346
+ index = int(call.get("index", 0))
347
+ slot = self.function_calls.setdefault(index, {
348
+ "id": None,
349
+ "name": "",
350
+ "arguments": "",
351
+ })
352
+
353
+ call_id = call.get("id")
354
+ if call_id:
355
+ slot["id"] = call_id
356
+
357
+ fn = call.get("function", {})
358
+ fn_name = fn.get("name")
359
+ fn_args = fn.get("arguments")
360
+
361
+ # 首次见到这个调用:发出output_item.added
362
+ if fn_name and not slot["name"]:
363
+ slot["name"] = fn_name
364
+ item_id = slot["id"] or f"fc_{self.response_id}_{index}"
365
+ slot["id"] = item_id
366
+ events.append(("response.output_item.added", {
367
+ "type": "response.output_item.added",
368
+ "item": {
369
+ "id": item_id,
370
+ "type": "function_call",
371
+ "call_id": item_id,
372
+ "name": fn_name,
373
+ "arguments": "",
374
+ },
375
+ }))
376
+
377
+ # arguments增量
378
+ if fn_args:
379
+ slot["arguments"] += fn_args
380
+ events.append(("response.function_call_arguments.delta", {
381
+ "type": "response.function_call_arguments.delta",
382
+ "item_id": slot["id"] or f"fc_{self.response_id}_{index}",
383
+ "output_index": 0,
384
+ "delta": fn_args,
385
+ }))
386
+
387
+ return events
388
+
389
+ def finish(self) -> list[tuple[str, dict]]:
390
+ """流结束,发出done和completed事件"""
391
+ events: list[tuple[str, dict]] = []
392
+
393
+ # 关闭文本块
394
+ if self.content_part_added:
395
+ events.append(("response.output_text.done", {
396
+ "type": "response.output_text.done",
397
+ "item_id": self.response_id + "_msg",
398
+ "output_index": 0,
399
+ "content_index": 0,
400
+ "text": self.text,
401
+ }))
402
+ events.append(("response.content_part.done", {
403
+ "type": "response.content_part.done",
404
+ "part": {"type": "text", "text": self.text},
405
+ }))
406
+
407
+ # 关闭消息item
408
+ if self.message_item_added:
409
+ events.append(("response.output_item.done", {
410
+ "type": "response.output_item.done",
411
+ "item": {
412
+ "id": self.response_id + "_msg",
413
+ "type": "message",
414
+ "role": "assistant",
415
+ "content": [{"type": "text", "text": self.text}] if self.text else [],
416
+ },
417
+ }))
418
+
419
+ # 关闭各个function_call item
420
+ for index, call in sorted(self.function_calls.items()):
421
+ # 1. 先发出 arguments.done 事件(参数接收完成)
422
+ events.append(("response.function_call_arguments.done", {
423
+ "type": "response.function_call_arguments.done",
424
+ "item_id": call["id"],
425
+ "call_id": call["id"],
426
+ "arguments": call["arguments"],
427
+ }))
428
+
429
+ # 2. 再发出 output_item.done 事件(工具调用项完成)
430
+ events.append(("response.output_item.done", {
431
+ "type": "response.output_item.done",
432
+ "item": {
433
+ "id": call["id"],
434
+ "type": "function_call",
435
+ "call_id": call["id"],
436
+ "name": call["name"],
437
+ "arguments": call["arguments"],
438
+ },
439
+ }))
440
+
441
+ # 构造完整response对象
442
+ output_items = []
443
+ if self.message_item_added:
444
+ output_items.append({
445
+ "id": self.response_id + "_msg",
446
+ "type": "message",
447
+ "role": "assistant",
448
+ "content": [{"type": "text", "text": self.text}] if self.text else [],
449
+ })
450
+ for index, call in sorted(self.function_calls.items()):
451
+ output_items.append({
452
+ "id": call["id"],
453
+ "type": "function_call",
454
+ "call_id": call["id"],
455
+ "name": call["name"],
456
+ "arguments": call["arguments"],
457
+ })
458
+
459
+ # 映射usage
460
+ usage_out = {
461
+ "input_tokens": 0,
462
+ "output_tokens": 0,
463
+ "total_tokens": 0,
464
+ "input_tokens_details": {"cached_tokens": 0},
465
+ "output_tokens_details": {"reasoning_tokens": 0},
466
+ }
467
+ if self.usage:
468
+ usage_out["input_tokens"] = self.usage.get("prompt_tokens", 0)
469
+ usage_out["output_tokens"] = self.usage.get("completion_tokens", 0)
470
+ usage_out["total_tokens"] = self.usage.get("total_tokens", 0)
471
+
472
+ # 发出completed
473
+ events.append(("response.completed", {
474
+ "type": "response.completed",
475
+ "response": {
476
+ "id": self.response_id,
477
+ "object": "realtime.response",
478
+ "status": "completed",
479
+ "created_at": self.created_at,
480
+ "output": output_items,
481
+ "usage": usage_out,
482
+ "parallel_tool_calls": True,
483
+ },
484
+ }))
485
+
486
+ return events
487
+