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,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
|
+
|