codex-proxy 3.1.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.
- codex_proxy/__init__.py +3 -0
- codex_proxy/__main__.py +66 -0
- codex_proxy/circuit_breaker.py +83 -0
- codex_proxy/compaction.py +42 -0
- codex_proxy/config.py +313 -0
- codex_proxy/key_rotation.py +108 -0
- codex_proxy/plugins.py +110 -0
- codex_proxy/plugins_builtin.py +34 -0
- codex_proxy/providers.py +130 -0
- codex_proxy/server.py +647 -0
- codex_proxy/store.py +97 -0
- codex_proxy/translator.py +360 -0
- codex_proxy/tui.py +262 -0
- codex_proxy-3.1.0.dist-info/METADATA +25 -0
- codex_proxy-3.1.0.dist-info/RECORD +18 -0
- codex_proxy-3.1.0.dist-info/WHEEL +4 -0
- codex_proxy-3.1.0.dist-info/entry_points.txt +2 -0
- codex_proxy-3.1.0.dist-info/licenses/LICENSE +21 -0
codex_proxy/store.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""In-memory response store for previous_response_id support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResponseStore:
|
|
10
|
+
"""Stores completed responses so Codex can reference them in multi-turn."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, ttl_seconds: int = 600, max_entries: int = 100):
|
|
13
|
+
self._store: OrderedDict[str, dict] = OrderedDict()
|
|
14
|
+
self.ttl_seconds = ttl_seconds
|
|
15
|
+
self.max_entries = max_entries
|
|
16
|
+
|
|
17
|
+
def put(self, response_id: str, response: dict) -> None:
|
|
18
|
+
self._store[response_id] = {
|
|
19
|
+
"response": response,
|
|
20
|
+
"timestamp": time.time(),
|
|
21
|
+
}
|
|
22
|
+
if len(self._store) > self.max_entries:
|
|
23
|
+
self._store.popitem(last=False)
|
|
24
|
+
|
|
25
|
+
def get(self, response_id: str) -> dict | None:
|
|
26
|
+
entry = self._store.get(response_id)
|
|
27
|
+
if not entry:
|
|
28
|
+
return None
|
|
29
|
+
if time.time() - entry["timestamp"] > self.ttl_seconds:
|
|
30
|
+
del self._store[response_id]
|
|
31
|
+
return None
|
|
32
|
+
res = entry["response"]
|
|
33
|
+
assert isinstance(res, dict)
|
|
34
|
+
return res
|
|
35
|
+
|
|
36
|
+
def resolve_input(self, body: dict) -> dict:
|
|
37
|
+
"""If body has previous_response_id, prepend that response's output to input."""
|
|
38
|
+
prev_id = body.get("previous_response_id")
|
|
39
|
+
if not prev_id:
|
|
40
|
+
return body
|
|
41
|
+
|
|
42
|
+
prev = self.get(prev_id)
|
|
43
|
+
if not prev:
|
|
44
|
+
return body
|
|
45
|
+
|
|
46
|
+
current_input = body.get("input", [])
|
|
47
|
+
prev_output = prev.get("output", [])
|
|
48
|
+
|
|
49
|
+
# Convert previous output items to input items for context
|
|
50
|
+
context_items = []
|
|
51
|
+
for item in prev_output:
|
|
52
|
+
t = item.get("type", "")
|
|
53
|
+
if t == "message":
|
|
54
|
+
role = item.get("role", "assistant")
|
|
55
|
+
parts = item.get("content", [])
|
|
56
|
+
text = ""
|
|
57
|
+
for p in parts:
|
|
58
|
+
if isinstance(p, dict):
|
|
59
|
+
text += p.get("text", "")
|
|
60
|
+
if text:
|
|
61
|
+
context_items.append({
|
|
62
|
+
"type": "message", "role": role,
|
|
63
|
+
"content": [{"type": "input_text", "text": text}]
|
|
64
|
+
})
|
|
65
|
+
elif t == "function_call":
|
|
66
|
+
context_items.append({
|
|
67
|
+
"type": "function_call",
|
|
68
|
+
"id": item.get("id", ""),
|
|
69
|
+
"call_id": item.get("call_id", ""),
|
|
70
|
+
"name": item.get("name", ""),
|
|
71
|
+
"arguments": item.get("arguments", "{}"),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
# Get previous input (the conversation before the response)
|
|
75
|
+
prev_input = prev.get("_original_input", [])
|
|
76
|
+
|
|
77
|
+
# Combine: previous input + previous output + current input
|
|
78
|
+
combined = prev_input + context_items
|
|
79
|
+
if isinstance(current_input, list):
|
|
80
|
+
combined += current_input
|
|
81
|
+
elif isinstance(current_input, str):
|
|
82
|
+
combined.append({"type": "message", "role": "user",
|
|
83
|
+
"content": [{"type": "input_text",
|
|
84
|
+
"text": current_input}]})
|
|
85
|
+
|
|
86
|
+
body = dict(body)
|
|
87
|
+
body["input"] = combined
|
|
88
|
+
body.pop("previous_response_id", None)
|
|
89
|
+
return body
|
|
90
|
+
|
|
91
|
+
def size(self) -> int:
|
|
92
|
+
return len(self._store)
|
|
93
|
+
|
|
94
|
+
def entries(self) -> list[tuple[str, float]]:
|
|
95
|
+
"""Return list of (response_id, age_seconds) for all cached entries."""
|
|
96
|
+
now = time.time()
|
|
97
|
+
return [(rid, now - entry["timestamp"]) for rid, entry in self._store.items()]
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Responses API <-> Chat Completions protocol translator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from .compaction import compact_messages
|
|
10
|
+
|
|
11
|
+
# ── Responses API input -> Chat Completions messages ────────────────────
|
|
12
|
+
|
|
13
|
+
def input_to_messages(input_data, instructions: str | None = None) -> list[dict]:
|
|
14
|
+
"""Convert Responses API `input` to Chat Completions `messages`."""
|
|
15
|
+
messages: list[dict] = []
|
|
16
|
+
if instructions:
|
|
17
|
+
messages.append({"role": "system", "content": instructions})
|
|
18
|
+
|
|
19
|
+
if isinstance(input_data, str):
|
|
20
|
+
messages.append({"role": "user", "content": input_data})
|
|
21
|
+
return messages
|
|
22
|
+
|
|
23
|
+
if not isinstance(input_data, list):
|
|
24
|
+
messages.append({"role": "user", "content": str(input_data)})
|
|
25
|
+
return messages
|
|
26
|
+
|
|
27
|
+
for item in input_data:
|
|
28
|
+
if isinstance(item, str):
|
|
29
|
+
messages.append({"role": "user", "content": item})
|
|
30
|
+
continue
|
|
31
|
+
if not isinstance(item, dict):
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
t = item.get("type", "")
|
|
35
|
+
|
|
36
|
+
if t == "message":
|
|
37
|
+
role = item.get("role", "user")
|
|
38
|
+
parts = item.get("content", [])
|
|
39
|
+
text = ""
|
|
40
|
+
for p in parts:
|
|
41
|
+
if isinstance(p, str):
|
|
42
|
+
text += p
|
|
43
|
+
elif isinstance(p, dict) and p.get("type") in ("input_text", "text"):
|
|
44
|
+
text += p.get("text", "")
|
|
45
|
+
messages.append({"role": role, "content": text})
|
|
46
|
+
|
|
47
|
+
elif t == "function_call":
|
|
48
|
+
tc = {
|
|
49
|
+
"id": item.get("call_id", item.get("id", "")),
|
|
50
|
+
"type": "function",
|
|
51
|
+
"function": {
|
|
52
|
+
"name": item.get("name", ""),
|
|
53
|
+
"arguments": item.get("arguments", "{}"),
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
if (messages and messages[-1].get("role") == "assistant"
|
|
57
|
+
and "tool_calls" in messages[-1]):
|
|
58
|
+
messages[-1]["tool_calls"].append(tc)
|
|
59
|
+
else:
|
|
60
|
+
messages.append({"role": "assistant", "content": None,
|
|
61
|
+
"tool_calls": [tc]})
|
|
62
|
+
|
|
63
|
+
elif t == "function_call_output":
|
|
64
|
+
messages.append({"role": "tool",
|
|
65
|
+
"tool_call_id": item.get("call_id", ""),
|
|
66
|
+
"content": item.get("output", "")})
|
|
67
|
+
|
|
68
|
+
return messages
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Tool format conversion ──────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def convert_tools(tools: list | None) -> list | None:
|
|
74
|
+
"""Convert Responses API tools -> Chat Completions tools."""
|
|
75
|
+
if not tools:
|
|
76
|
+
return None
|
|
77
|
+
out = []
|
|
78
|
+
for t in tools:
|
|
79
|
+
tt = t.get("type", "")
|
|
80
|
+
if tt == "function":
|
|
81
|
+
fn = t.get("function", {})
|
|
82
|
+
if not fn and t.get("name"):
|
|
83
|
+
fn = {"name": t["name"],
|
|
84
|
+
"description": t.get("description", ""),
|
|
85
|
+
"parameters": t.get("parameters", {})}
|
|
86
|
+
out.append({"type": "function", "function": fn})
|
|
87
|
+
return out or None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── Request builder ─────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def build_cc_request(
|
|
93
|
+
body: dict,
|
|
94
|
+
compaction_enabled: bool = True,
|
|
95
|
+
compaction_max_messages: int = 50,
|
|
96
|
+
compaction_keep_last: int = 20,
|
|
97
|
+
) -> dict:
|
|
98
|
+
"""Parse Responses API body, return Chat Completions request dict."""
|
|
99
|
+
model = body.get("model", "glm-5.1")
|
|
100
|
+
messages = input_to_messages(body.get("input"), body.get("instructions"))
|
|
101
|
+
if compaction_enabled:
|
|
102
|
+
messages = compact_messages(
|
|
103
|
+
messages,
|
|
104
|
+
max_messages=compaction_max_messages,
|
|
105
|
+
keep_last=compaction_keep_last,
|
|
106
|
+
)
|
|
107
|
+
cc: dict = {"model": model, "messages": messages,
|
|
108
|
+
"stream": body.get("stream", True)}
|
|
109
|
+
|
|
110
|
+
if cc["stream"]:
|
|
111
|
+
cc["stream_options"] = {"include_usage": True}
|
|
112
|
+
|
|
113
|
+
for k in ("temperature", "top_p"):
|
|
114
|
+
if k in body:
|
|
115
|
+
cc[k] = body[k]
|
|
116
|
+
if "max_output_tokens" in body:
|
|
117
|
+
cc["max_tokens"] = body["max_output_tokens"]
|
|
118
|
+
elif "max_tokens" in body:
|
|
119
|
+
cc["max_tokens"] = body["max_tokens"]
|
|
120
|
+
|
|
121
|
+
tools = convert_tools(body.get("tools"))
|
|
122
|
+
if tools:
|
|
123
|
+
cc["tools"] = tools
|
|
124
|
+
if "tool_choice" in body:
|
|
125
|
+
cc["tool_choice"] = body["tool_choice"]
|
|
126
|
+
|
|
127
|
+
return cc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def unwrap_envelope(raw: str) -> dict:
|
|
131
|
+
"""Parse JSON, unwrap Realtime API envelope if present."""
|
|
132
|
+
body = json.loads(raw)
|
|
133
|
+
if not isinstance(body, dict):
|
|
134
|
+
return {}
|
|
135
|
+
if body.get("type") == "response.create":
|
|
136
|
+
res = body.get("response")
|
|
137
|
+
if isinstance(res, dict):
|
|
138
|
+
return res
|
|
139
|
+
return body
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── Response conversion: CC -> Responses API ────────────────────────────
|
|
143
|
+
|
|
144
|
+
def generate_response_id() -> str:
|
|
145
|
+
return f"resp_{uuid.uuid4().hex[:24]}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cc_to_response(body: dict, model: str) -> dict:
|
|
149
|
+
"""Non-streaming Chat Completions response -> Responses API object."""
|
|
150
|
+
out: list[dict] = []
|
|
151
|
+
if body.get("choices"):
|
|
152
|
+
msg = body["choices"][0].get("message", {})
|
|
153
|
+
content = msg.get("content")
|
|
154
|
+
reasoning = msg.get("reasoning_content")
|
|
155
|
+
|
|
156
|
+
if reasoning:
|
|
157
|
+
out.append({
|
|
158
|
+
"type": "reasoning", "id": f"rs_{uuid.uuid4().hex[:24]}",
|
|
159
|
+
"summary": [{"type": "summary_text", "text": reasoning}],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if content:
|
|
163
|
+
out.append({
|
|
164
|
+
"type": "message", "id": f"msg_{uuid.uuid4().hex[:24]}",
|
|
165
|
+
"status": "completed", "role": "assistant",
|
|
166
|
+
"content": [{"type": "output_text", "text": content,
|
|
167
|
+
"annotations": []}],
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
for tc in msg.get("tool_calls", []):
|
|
171
|
+
out.append({
|
|
172
|
+
"type": "function_call",
|
|
173
|
+
"id": tc.get("id", ""), "call_id": tc.get("id", ""),
|
|
174
|
+
"name": tc["function"]["name"],
|
|
175
|
+
"arguments": tc["function"].get("arguments", "{}"),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if not content and not reasoning and not msg.get("tool_calls"):
|
|
179
|
+
out.append({
|
|
180
|
+
"type": "message", "id": f"msg_{uuid.uuid4().hex[:24]}",
|
|
181
|
+
"status": "completed", "role": "assistant",
|
|
182
|
+
"content": [{"type": "output_text", "text": "",
|
|
183
|
+
"annotations": []}],
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
usage = body.get("usage", {})
|
|
187
|
+
return {
|
|
188
|
+
"id": generate_response_id(), "object": "response",
|
|
189
|
+
"created_at": int(time.time()), "model": model,
|
|
190
|
+
"status": "completed", "output": out,
|
|
191
|
+
"usage": {"input_tokens": usage.get("prompt_tokens", 0),
|
|
192
|
+
"output_tokens": usage.get("completion_tokens", 0),
|
|
193
|
+
"total_tokens": usage.get("total_tokens", 0)},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── Streaming core: shared parser + helpers ─────────────────────────────
|
|
198
|
+
|
|
199
|
+
async def parse_cc_stream(line_iter):
|
|
200
|
+
"""Core SSE parser — yields (event_type, data) tuples.
|
|
201
|
+
|
|
202
|
+
Event types: "text", "reasoning", "tool_call", "usage"
|
|
203
|
+
"""
|
|
204
|
+
async for line in line_iter:
|
|
205
|
+
if not line.startswith("data: "):
|
|
206
|
+
continue
|
|
207
|
+
payload = line[6:].strip()
|
|
208
|
+
if payload == "[DONE]":
|
|
209
|
+
break
|
|
210
|
+
try:
|
|
211
|
+
chunk = json.loads(payload)
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
continue
|
|
214
|
+
for choice in chunk.get("choices", []):
|
|
215
|
+
delta = choice.get("delta", {})
|
|
216
|
+
rc = delta.get("reasoning_content")
|
|
217
|
+
if rc:
|
|
218
|
+
yield ("reasoning", rc)
|
|
219
|
+
txt = delta.get("content")
|
|
220
|
+
if txt:
|
|
221
|
+
yield ("text", txt)
|
|
222
|
+
for tc in delta.get("tool_calls", []):
|
|
223
|
+
yield ("tool_call", tc)
|
|
224
|
+
chunk_usage = chunk.get("usage")
|
|
225
|
+
if chunk_usage:
|
|
226
|
+
yield ("usage", chunk_usage)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def accumulate_tool_call(tool_calls: list[dict], tc: dict) -> None:
|
|
230
|
+
"""Accumulate a streaming tool_call delta into the tool_calls list."""
|
|
231
|
+
idx = tc.get("index", 0)
|
|
232
|
+
while len(tool_calls) <= idx:
|
|
233
|
+
tool_calls.append({"id": "", "function": {"name": "", "arguments": ""}})
|
|
234
|
+
if tc.get("id"):
|
|
235
|
+
tool_calls[idx]["id"] = tc["id"]
|
|
236
|
+
fn = tc.get("function", {})
|
|
237
|
+
if fn.get("name"):
|
|
238
|
+
tool_calls[idx]["function"]["name"] = fn["name"]
|
|
239
|
+
if fn.get("arguments"):
|
|
240
|
+
tool_calls[idx]["function"]["arguments"] += fn["arguments"]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_final_output(mid: str, full_text: str, reasoning_text: str,
|
|
244
|
+
tool_calls: list[dict]) -> list[dict]:
|
|
245
|
+
"""Build the Responses API output list from accumulated stream data."""
|
|
246
|
+
final_out: list[dict] = []
|
|
247
|
+
if reasoning_text:
|
|
248
|
+
final_out.append({
|
|
249
|
+
"type": "reasoning", "id": f"rs_{uuid.uuid4().hex[:24]}",
|
|
250
|
+
"summary": [{"type": "summary_text", "text": reasoning_text}],
|
|
251
|
+
})
|
|
252
|
+
final_out.append({
|
|
253
|
+
"type": "message", "id": mid, "status": "completed",
|
|
254
|
+
"role": "assistant",
|
|
255
|
+
"content": [{"type": "output_text", "text": full_text,
|
|
256
|
+
"annotations": []}],
|
|
257
|
+
})
|
|
258
|
+
for tc in tool_calls:
|
|
259
|
+
fc_id = tc["id"] or f"fc_{uuid.uuid4().hex[:24]}"
|
|
260
|
+
final_out.append({
|
|
261
|
+
"type": "function_call", "id": fc_id, "call_id": fc_id,
|
|
262
|
+
"name": tc["function"]["name"],
|
|
263
|
+
"arguments": tc["function"]["arguments"],
|
|
264
|
+
"status": "completed",
|
|
265
|
+
})
|
|
266
|
+
return final_out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ── Streaming: CC SSE -> Responses API SSE ──────────────────────────────
|
|
270
|
+
|
|
271
|
+
def _ev(event: str, data: dict) -> str:
|
|
272
|
+
return f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def stream_cc_to_response(cc_iter, model: str, result: dict | None = None):
|
|
276
|
+
"""Convert Chat Completions SSE stream -> Responses API SSE events.
|
|
277
|
+
|
|
278
|
+
If *result* is provided, it will be populated with the completed response
|
|
279
|
+
dict (including id and output) after the generator finishes.
|
|
280
|
+
"""
|
|
281
|
+
rid = generate_response_id()
|
|
282
|
+
mid = f"msg_{uuid.uuid4().hex[:24]}"
|
|
283
|
+
now = int(time.time())
|
|
284
|
+
|
|
285
|
+
init = {"id": rid, "object": "response", "created_at": now,
|
|
286
|
+
"model": model, "status": "in_progress", "output": [],
|
|
287
|
+
"usage": {"input_tokens": 0, "output_tokens": 0,
|
|
288
|
+
"total_tokens": 0}}
|
|
289
|
+
|
|
290
|
+
yield _ev("response.created", {"type": "response.created", "response": init})
|
|
291
|
+
yield _ev("response.in_progress", {"type": "response.in_progress", "response": init})
|
|
292
|
+
yield _ev("response.output_item.added", {
|
|
293
|
+
"type": "response.output_item.added", "output_index": 0,
|
|
294
|
+
"item": {"type": "message", "id": mid, "status": "in_progress",
|
|
295
|
+
"role": "assistant", "content": []}})
|
|
296
|
+
yield _ev("response.content_part.added", {
|
|
297
|
+
"type": "response.content_part.added", "output_index": 0,
|
|
298
|
+
"content_index": 0,
|
|
299
|
+
"part": {"type": "output_text", "text": "", "annotations": []}})
|
|
300
|
+
|
|
301
|
+
full_text = ""
|
|
302
|
+
reasoning_text = ""
|
|
303
|
+
tool_calls: list[dict] = []
|
|
304
|
+
usage_data = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
|
|
305
|
+
|
|
306
|
+
async for event_type, data in parse_cc_stream(cc_iter):
|
|
307
|
+
if event_type == "reasoning":
|
|
308
|
+
reasoning_text += data
|
|
309
|
+
elif event_type == "text":
|
|
310
|
+
full_text += data
|
|
311
|
+
yield _ev("response.output_text.delta", {
|
|
312
|
+
"type": "response.output_text.delta",
|
|
313
|
+
"output_index": 0, "content_index": 0, "delta": data})
|
|
314
|
+
elif event_type == "tool_call":
|
|
315
|
+
accumulate_tool_call(tool_calls, data)
|
|
316
|
+
elif event_type == "usage":
|
|
317
|
+
usage_data = {
|
|
318
|
+
"input_tokens": data.get("prompt_tokens", 0),
|
|
319
|
+
"output_tokens": data.get("completion_tokens", 0),
|
|
320
|
+
"total_tokens": data.get("total_tokens", 0),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Finish text
|
|
324
|
+
yield _ev("response.output_text.done", {
|
|
325
|
+
"type": "response.output_text.done",
|
|
326
|
+
"output_index": 0, "content_index": 0, "text": full_text})
|
|
327
|
+
yield _ev("response.content_part.done", {
|
|
328
|
+
"type": "response.content_part.done", "output_index": 0,
|
|
329
|
+
"content_index": 0,
|
|
330
|
+
"part": {"type": "output_text", "text": full_text, "annotations": []}})
|
|
331
|
+
yield _ev("response.output_item.done", {
|
|
332
|
+
"type": "response.output_item.done", "output_index": 0,
|
|
333
|
+
"item": {"type": "message", "id": mid, "status": "completed",
|
|
334
|
+
"role": "assistant",
|
|
335
|
+
"content": [{"type": "output_text", "text": full_text,
|
|
336
|
+
"annotations": []}]}})
|
|
337
|
+
|
|
338
|
+
# Build final output
|
|
339
|
+
final_out = build_final_output(mid, full_text, reasoning_text, tool_calls)
|
|
340
|
+
|
|
341
|
+
# Emit tool call items as separate events
|
|
342
|
+
text_and_reasoning_count = len(final_out) - len(tool_calls)
|
|
343
|
+
for i, item in enumerate(final_out[text_and_reasoning_count:], text_and_reasoning_count):
|
|
344
|
+
fc = {k: v for k, v in item.items() if k != "status"}
|
|
345
|
+
yield _ev("response.output_item.added", {
|
|
346
|
+
"type": "response.output_item.added",
|
|
347
|
+
"output_index": i, "item": fc})
|
|
348
|
+
yield _ev("response.output_item.done", {
|
|
349
|
+
"type": "response.output_item.done",
|
|
350
|
+
"output_index": i, "item": fc})
|
|
351
|
+
|
|
352
|
+
# Completed
|
|
353
|
+
completed = {"id": rid, "object": "response", "created_at": now,
|
|
354
|
+
"model": model, "status": "completed", "output": final_out,
|
|
355
|
+
"usage": usage_data}
|
|
356
|
+
yield _ev("response.completed", {
|
|
357
|
+
"type": "response.completed", "response": completed})
|
|
358
|
+
|
|
359
|
+
if result is not None:
|
|
360
|
+
result["response"] = completed
|