mnemonic-proxy 0.3.2__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.
- ctxproxy/__init__.py +8 -0
- ctxproxy/api_anthropic.py +426 -0
- ctxproxy/api_responses.py +436 -0
- ctxproxy/autosave.py +350 -0
- ctxproxy/check.py +356 -0
- ctxproxy/compat.py +91 -0
- ctxproxy/core.py +2218 -0
- ctxproxy/demo.py +193 -0
- ctxproxy/engines.py +314 -0
- ctxproxy/fake_engine.py +304 -0
- ctxproxy/kvarchive.py +855 -0
- ctxproxy/lang.py +57 -0
- ctxproxy/mcp_server.py +381 -0
- ctxproxy/packtok.py +29 -0
- ctxproxy/paging.py +508 -0
- ctxproxy/recall2.py +1003 -0
- ctxproxy/render.py +274 -0
- ctxproxy/server.py +797 -0
- ctxproxy/tooldefs.py +111 -0
- ctxproxy/upstream.py +106 -0
- mnemonic_proxy-0.3.2.dist-info/METADATA +841 -0
- mnemonic_proxy-0.3.2.dist-info/RECORD +26 -0
- mnemonic_proxy-0.3.2.dist-info/WHEEL +5 -0
- mnemonic_proxy-0.3.2.dist-info/entry_points.txt +4 -0
- mnemonic_proxy-0.3.2.dist-info/licenses/LICENSE +21 -0
- mnemonic_proxy-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# Author: Maurizio Verde — LastReload
|
|
2
|
+
"""Ingresso OpenAI Responses (Codex) -> formato interno Chat Completions -> cuore invariato -> uscita riconvertita
|
|
3
|
+
(anche in streaming).
|
|
4
|
+
|
|
5
|
+
POST /v1/responses (stream o no)
|
|
6
|
+
|
|
7
|
+
Codex 0.160 accetta solo `wire_api = "responses"` (`"chat"` è rifiutato: «`wire_api = "chat"` is no longer
|
|
8
|
+
supported»), quindi per usarlo davanti a un motore Chat Completions serve questa conversione.
|
|
9
|
+
Scelte:
|
|
10
|
+
- `instructions` -> messaggio system; messaggi `developer`/`system` -> system (il primo) o user con prefisso.
|
|
11
|
+
- `function_call` consecutivi (e il testo/ragionamento che li precede) -> un solo messaggio assistant con più
|
|
12
|
+
tool_calls; `function_call_output` -> messaggio tool. `call_id` resta identico in entrata e in uscita.
|
|
13
|
+
- `reasoning` (content `reasoning_text` o, in mancanza, `summary_text`) -> reasoning_content dell'assistant
|
|
14
|
+
che segue. `encrypted_content` ignorato. In uscita il ragionamento è un item `reasoning` con `content`
|
|
15
|
+
reasoning_text (e lo stesso testo come summary, perché Codex mostra i summary).
|
|
16
|
+
- strumenti `custom` (testo libero, es. apply_patch di Codex) -> funzione con un parametro `input`; la chiamata
|
|
17
|
+
torna al client come `custom_tool_call`. Strumenti lato server (web_search, local_shell…) scartati.
|
|
18
|
+
- Immagini in un messaggio utente: errore 400 chiaro; dentro un output di strumento: nota testuale.
|
|
19
|
+
- Niente stato lato server: `previous_response_id` non è supportato (Codex rimanda tutta la storia, `store=false`).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
import uuid
|
|
27
|
+
|
|
28
|
+
from .api_anthropic import IMAGE_NOTE, BadRequest, _err_message
|
|
29
|
+
|
|
30
|
+
CUSTOM_PARAMS = {"type": "object", "properties": {"input": {"type": "string",
|
|
31
|
+
"description": "testo libero passato allo strumento"}},
|
|
32
|
+
"required": ["input"]}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parts_text(content, where: str, allow_images: bool) -> str:
|
|
36
|
+
if content is None:
|
|
37
|
+
return ""
|
|
38
|
+
if isinstance(content, str):
|
|
39
|
+
return content
|
|
40
|
+
out = []
|
|
41
|
+
for p in content:
|
|
42
|
+
if isinstance(p, str):
|
|
43
|
+
out.append(p)
|
|
44
|
+
continue
|
|
45
|
+
t = p.get("type")
|
|
46
|
+
if t in ("input_text", "output_text", "text", "summary_text", "reasoning_text", "refusal"):
|
|
47
|
+
out.append(p.get("text") or p.get("refusal") or "")
|
|
48
|
+
elif t in ("input_image", "input_file", "image_url"):
|
|
49
|
+
if not allow_images:
|
|
50
|
+
raise BadRequest("%s: '%s' content is not supported by this proxy (the engine receives text only)"
|
|
51
|
+
% (where, t))
|
|
52
|
+
out.append(IMAGE_NOTE)
|
|
53
|
+
else:
|
|
54
|
+
raise BadRequest("%s: unsupported content type: %r" % (where, t))
|
|
55
|
+
return "".join(out)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def to_chat_messages(instructions, items, custom_names=()) -> list[dict]:
|
|
59
|
+
out: list[dict] = []
|
|
60
|
+
if instructions:
|
|
61
|
+
out.append({"role": "system", "content": instructions})
|
|
62
|
+
if isinstance(items, str):
|
|
63
|
+
items = [{"type": "message", "role": "user", "content": items}]
|
|
64
|
+
items_l: list[dict] = list(items or [])
|
|
65
|
+
pend: dict | None = None # assistant in costruzione
|
|
66
|
+
think: list[str] = [] # ragionamento in attesa del suo assistant
|
|
67
|
+
|
|
68
|
+
def flush():
|
|
69
|
+
nonlocal pend, think
|
|
70
|
+
if pend is None and think:
|
|
71
|
+
pend = {"role": "assistant", "content": ""}
|
|
72
|
+
if pend is not None:
|
|
73
|
+
if think:
|
|
74
|
+
pend["reasoning_content"] = "".join(think)
|
|
75
|
+
out.append(pend)
|
|
76
|
+
pend, think = None, []
|
|
77
|
+
|
|
78
|
+
def assistant():
|
|
79
|
+
nonlocal pend
|
|
80
|
+
if pend is None:
|
|
81
|
+
pend = {"role": "assistant", "content": ""}
|
|
82
|
+
return pend
|
|
83
|
+
|
|
84
|
+
for k, it in enumerate(items_l):
|
|
85
|
+
where = "input[%d]" % k
|
|
86
|
+
t = it.get("type") or ("message" if it.get("role") else None)
|
|
87
|
+
if t == "message":
|
|
88
|
+
role = it.get("role")
|
|
89
|
+
if role == "assistant":
|
|
90
|
+
a = assistant()
|
|
91
|
+
if a.get("tool_calls"): # testo dopo chiamate: nuovo turno assistant
|
|
92
|
+
flush()
|
|
93
|
+
a = assistant()
|
|
94
|
+
a["content"] += _parts_text(it.get("content"), where, False)
|
|
95
|
+
continue
|
|
96
|
+
flush()
|
|
97
|
+
text = _parts_text(it.get("content"), where, False)
|
|
98
|
+
if role in ("system", "developer"):
|
|
99
|
+
if all(m["role"] == "system" for m in out): # ancora in testa: si unisce al system
|
|
100
|
+
if out:
|
|
101
|
+
out[-1] = {"role": "system", "content": out[-1]["content"] + "\n\n" + text}
|
|
102
|
+
else:
|
|
103
|
+
out.append({"role": "system", "content": text})
|
|
104
|
+
else:
|
|
105
|
+
out.append({"role": "user", "content": "[%s]\n%s" % (role, text)})
|
|
106
|
+
elif role == "user":
|
|
107
|
+
out.append({"role": "user", "content": text})
|
|
108
|
+
else:
|
|
109
|
+
raise BadRequest("%s: ruolo non supportato: %r" % (where, role))
|
|
110
|
+
elif t == "reasoning":
|
|
111
|
+
if pend is not None and (pend.get("tool_calls") or pend.get("content")):
|
|
112
|
+
flush()
|
|
113
|
+
txt = _parts_text(it.get("content"), where, False) if it.get("content") else \
|
|
114
|
+
"\n".join((s.get("text") or "") for s in it.get("summary") or [])
|
|
115
|
+
if txt:
|
|
116
|
+
think.append(txt)
|
|
117
|
+
elif t in ("function_call", "custom_tool_call"):
|
|
118
|
+
a = assistant()
|
|
119
|
+
args = it.get("arguments") if t == "function_call" else json.dumps({"input": it.get("input") or ""},
|
|
120
|
+
ensure_ascii=False)
|
|
121
|
+
a.setdefault("tool_calls", []).append({"id": it.get("call_id"), "type": "function",
|
|
122
|
+
"function": {"name": it.get("name"), "arguments": args or ""}})
|
|
123
|
+
elif t in ("function_call_output", "custom_tool_call_output"):
|
|
124
|
+
flush()
|
|
125
|
+
o = it.get("output")
|
|
126
|
+
txt = o if isinstance(o, str) else (_parts_text(o, where, True) if isinstance(o, list)
|
|
127
|
+
else json.dumps(o, ensure_ascii=False))
|
|
128
|
+
out.append({"role": "tool", "tool_call_id": it.get("call_id"), "content": txt})
|
|
129
|
+
elif t in ("item_reference",):
|
|
130
|
+
raise BadRequest("%s: item_reference not supported (the proxy keeps no state: resend the history)"
|
|
131
|
+
% where)
|
|
132
|
+
else:
|
|
133
|
+
# web_search_call, local_shell_call, compaction… : non rappresentabili per il motore, ignorati
|
|
134
|
+
continue
|
|
135
|
+
flush()
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def to_chat_tools(tools) -> tuple[list | None, set]:
|
|
140
|
+
out, custom = [], set()
|
|
141
|
+
for t in tools or []:
|
|
142
|
+
ty = t.get("type")
|
|
143
|
+
if ty == "function":
|
|
144
|
+
out.append({"type": "function", "function": {"name": t["name"], "description": t.get("description") or "",
|
|
145
|
+
"parameters": t.get("parameters") or {"type": "object"}}})
|
|
146
|
+
elif ty == "custom":
|
|
147
|
+
custom.add(t["name"])
|
|
148
|
+
desc = t.get("description") or ""
|
|
149
|
+
fmt = t.get("format") or {}
|
|
150
|
+
if fmt.get("type") == "grammar" and fmt.get("definition"):
|
|
151
|
+
desc += "\n\nIl parametro `input` deve rispettare questa grammatica (%s):\n%s" % (
|
|
152
|
+
fmt.get("syntax") or "", fmt["definition"])
|
|
153
|
+
out.append({"type": "function", "function": {"name": t["name"], "description": desc,
|
|
154
|
+
"parameters": CUSTOM_PARAMS}})
|
|
155
|
+
return (out or None), custom
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def to_chat_request(r: dict) -> tuple[dict, set]:
|
|
159
|
+
if r.get("previous_response_id"):
|
|
160
|
+
raise BadRequest("previous_response_id not supported: the proxy keeps no state, resend the whole history")
|
|
161
|
+
if r.get("input") is None:
|
|
162
|
+
raise BadRequest("input: campo obbligatorio")
|
|
163
|
+
tools, custom = to_chat_tools(r.get("tools"))
|
|
164
|
+
req = {"model": r.get("model"), "messages": to_chat_messages(r.get("instructions"), r["input"], custom)}
|
|
165
|
+
if tools:
|
|
166
|
+
req["tools"] = tools
|
|
167
|
+
tc = r.get("tool_choice")
|
|
168
|
+
if tc in ("auto", "none", "required"):
|
|
169
|
+
req["tool_choice"] = tc
|
|
170
|
+
elif isinstance(tc, dict) and tc.get("name"):
|
|
171
|
+
req["tool_choice"] = {"type": "function", "function": {"name": tc["name"]}}
|
|
172
|
+
if r.get("parallel_tool_calls") is False:
|
|
173
|
+
req["parallel_tool_calls"] = False
|
|
174
|
+
if r.get("max_output_tokens"):
|
|
175
|
+
req["max_tokens"] = int(r["max_output_tokens"])
|
|
176
|
+
for k in ("temperature", "top_p"):
|
|
177
|
+
if r.get(k) is not None:
|
|
178
|
+
req[k] = r[k]
|
|
179
|
+
if isinstance(r.get("reasoning"), dict) and r["reasoning"].get("effort"):
|
|
180
|
+
req["reasoning_effort"] = r["reasoning"]["effort"]
|
|
181
|
+
if r.get("stream"):
|
|
182
|
+
req["stream"] = True
|
|
183
|
+
return req, custom
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------- risposta
|
|
187
|
+
def _usage(u: dict | None) -> dict:
|
|
188
|
+
u = u or {}
|
|
189
|
+
pt, ct = int(u.get("prompt_tokens") or 0), int(u.get("completion_tokens") or 0)
|
|
190
|
+
return {"input_tokens": pt,
|
|
191
|
+
"input_tokens_details": {"cached_tokens": int((u.get("prompt_tokens_details") or {})
|
|
192
|
+
.get("cached_tokens") or 0)},
|
|
193
|
+
"output_tokens": ct,
|
|
194
|
+
"output_tokens_details": {"reasoning_tokens": int((u.get("completion_tokens_details") or {})
|
|
195
|
+
.get("reasoning_tokens") or 0)},
|
|
196
|
+
"total_tokens": pt + ct}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _rid(p):
|
|
200
|
+
return p + "_" + uuid.uuid4().hex[:24]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def reasoning_item(text, iid=None, status="completed"):
|
|
204
|
+
return {"type": "reasoning", "id": iid or _rid("rs"), "status": status,
|
|
205
|
+
"summary": [{"type": "summary_text", "text": text}] if text else [],
|
|
206
|
+
"content": [{"type": "reasoning_text", "text": text}] if text else []}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def message_item(text, iid=None, status="completed"):
|
|
210
|
+
return {"type": "message", "id": iid or _rid("msg"), "status": status, "role": "assistant",
|
|
211
|
+
"content": [{"type": "output_text", "text": text, "annotations": []}] if text is not None else []}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def call_item(cid, name, args, custom, iid=None, status="completed"):
|
|
215
|
+
if name in custom:
|
|
216
|
+
try:
|
|
217
|
+
inp = (json.loads(args) or {}).get("input", "") if args else ""
|
|
218
|
+
except (ValueError, AttributeError):
|
|
219
|
+
inp = args
|
|
220
|
+
return {"type": "custom_tool_call", "id": iid or _rid("ctc"), "status": status, "call_id": cid,
|
|
221
|
+
"name": name, "input": inp if isinstance(inp, str) else json.dumps(inp, ensure_ascii=False)}
|
|
222
|
+
return {"type": "function_call", "id": iid or _rid("fc"), "status": status, "call_id": cid, "name": name,
|
|
223
|
+
"arguments": args}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def response_obj(rid, model, output, usage, status="completed", created=None, extra=None):
|
|
227
|
+
o = {"id": rid, "object": "response", "created_at": created or int(time.time()), "status": status,
|
|
228
|
+
"model": model, "output": output, "usage": usage, "error": None, "incomplete_details": None,
|
|
229
|
+
"parallel_tool_calls": True, "tool_choice": "auto", "tools": []}
|
|
230
|
+
if status == "incomplete":
|
|
231
|
+
o["incomplete_details"] = {"reason": "max_output_tokens"}
|
|
232
|
+
o.update(extra or {})
|
|
233
|
+
return o
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def from_chat_response(resp: dict, model, custom) -> dict:
|
|
237
|
+
ch = (resp.get("choices") or [{}])[0]
|
|
238
|
+
msg = ch.get("message") or {}
|
|
239
|
+
out = []
|
|
240
|
+
if msg.get("reasoning_content"):
|
|
241
|
+
out.append(reasoning_item(msg["reasoning_content"]))
|
|
242
|
+
if msg.get("content"):
|
|
243
|
+
out.append(message_item(msg["content"]))
|
|
244
|
+
for c in msg.get("tool_calls") or []:
|
|
245
|
+
f = c.get("function") or {}
|
|
246
|
+
out.append(call_item(c.get("id") or _rid("call"), f.get("name"), f.get("arguments") or "", custom))
|
|
247
|
+
extra = {"strata_context": resp["strata_context"]} if resp.get("strata_context") else None
|
|
248
|
+
return response_obj(_rid("resp"), model or resp.get("model"), out, _usage(resp.get("usage")),
|
|
249
|
+
"incomplete" if ch.get("finish_reason") == "length" else "completed", extra=extra)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def error_body(status: int, message: str) -> dict:
|
|
253
|
+
return {"error": {"message": message, "type": "invalid_request_error" if status == 400 else "server_error",
|
|
254
|
+
"param": None, "code": None}}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class StreamWriter:
|
|
258
|
+
"""Chunk OpenAI Chat (dal cuore) -> eventi SSE Responses. feed(None) = keep-alive (commento SSE)."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, write, model, custom):
|
|
261
|
+
self.write, self.model, self.custom = write, model, custom
|
|
262
|
+
self.rid = _rid("resp")
|
|
263
|
+
self.created = int(time.time())
|
|
264
|
+
self.seq = 0
|
|
265
|
+
self.output: list[dict] = []
|
|
266
|
+
self.cur = None # {"kind", "oi", "item", "buf"}
|
|
267
|
+
self.calls: dict[int, dict] = {}
|
|
268
|
+
self.started = False
|
|
269
|
+
self.done = False
|
|
270
|
+
|
|
271
|
+
def event(self, name, data):
|
|
272
|
+
d = {"type": name, "sequence_number": self.seq, **data}
|
|
273
|
+
self.seq += 1
|
|
274
|
+
self.write(("event: %s\ndata: %s\n\n" % (name, json.dumps(d, ensure_ascii=False))).encode("utf-8"))
|
|
275
|
+
|
|
276
|
+
def start(self):
|
|
277
|
+
if self.started:
|
|
278
|
+
return
|
|
279
|
+
self.started = True
|
|
280
|
+
r = response_obj(self.rid, self.model, [], None, "in_progress", self.created)
|
|
281
|
+
self.event("response.created", {"response": r})
|
|
282
|
+
self.event("response.in_progress", {"response": r})
|
|
283
|
+
|
|
284
|
+
# -- apertura/chiusura degli item
|
|
285
|
+
def _close(self):
|
|
286
|
+
c = self.cur
|
|
287
|
+
if c is None:
|
|
288
|
+
return
|
|
289
|
+
self.cur = None
|
|
290
|
+
oi, it, text = c["oi"], c["item"], "".join(c["buf"])
|
|
291
|
+
if c["kind"] == "reasoning":
|
|
292
|
+
self.event("response.reasoning_text.done", {"item_id": it["id"], "output_index": oi,
|
|
293
|
+
"content_index": 0, "text": text})
|
|
294
|
+
done = reasoning_item(text, it["id"])
|
|
295
|
+
elif c["kind"] == "message":
|
|
296
|
+
part = {"type": "output_text", "text": text, "annotations": []}
|
|
297
|
+
self.event("response.output_text.done", {"item_id": it["id"], "output_index": oi, "content_index": 0,
|
|
298
|
+
"text": text})
|
|
299
|
+
self.event("response.content_part.done", {"item_id": it["id"], "output_index": oi,
|
|
300
|
+
"content_index": 0, "part": part})
|
|
301
|
+
done = message_item(text, it["id"])
|
|
302
|
+
else:
|
|
303
|
+
if it["type"] == "function_call":
|
|
304
|
+
self.event("response.function_call_arguments.done", {"item_id": it["id"], "output_index": oi,
|
|
305
|
+
"arguments": text})
|
|
306
|
+
done = call_item(it["call_id"], it["name"], text, self.custom, it["id"])
|
|
307
|
+
self.output[oi] = done
|
|
308
|
+
self.event("response.output_item.done", {"output_index": oi, "item": done})
|
|
309
|
+
|
|
310
|
+
def _open(self, kind, item):
|
|
311
|
+
self._close()
|
|
312
|
+
oi = len(self.output)
|
|
313
|
+
self.output.append(item)
|
|
314
|
+
self.cur = {"kind": kind, "oi": oi, "item": item, "buf": []}
|
|
315
|
+
self.event("response.output_item.added", {"output_index": oi, "item": item})
|
|
316
|
+
if kind == "message":
|
|
317
|
+
self.event("response.content_part.added", {"item_id": item["id"], "output_index": oi,
|
|
318
|
+
"content_index": 0,
|
|
319
|
+
"part": {"type": "output_text", "text": "", "annotations": []}})
|
|
320
|
+
return self.cur
|
|
321
|
+
|
|
322
|
+
def feed(self, ch):
|
|
323
|
+
self.start()
|
|
324
|
+
if ch is None:
|
|
325
|
+
self.write(b": keep-alive\n\n")
|
|
326
|
+
return
|
|
327
|
+
if ch.get("error"):
|
|
328
|
+
self.error(_err_message(ch))
|
|
329
|
+
return
|
|
330
|
+
choice = (ch.get("choices") or [None])[0]
|
|
331
|
+
if not choice:
|
|
332
|
+
return
|
|
333
|
+
d = choice.get("delta") or {}
|
|
334
|
+
if d.get("reasoning_content"):
|
|
335
|
+
c = self.cur if self.cur and self.cur["kind"] == "reasoning" else \
|
|
336
|
+
self._open("reasoning", {"type": "reasoning", "id": _rid("rs"), "summary": [], "content": []})
|
|
337
|
+
c["buf"].append(d["reasoning_content"])
|
|
338
|
+
self.event("response.reasoning_text.delta", {"item_id": c["item"]["id"], "output_index": c["oi"],
|
|
339
|
+
"content_index": 0, "delta": d["reasoning_content"]})
|
|
340
|
+
if d.get("content"):
|
|
341
|
+
c = self.cur if self.cur and self.cur["kind"] == "message" else \
|
|
342
|
+
self._open("message", {"type": "message", "id": _rid("msg"), "status": "in_progress",
|
|
343
|
+
"role": "assistant", "content": []})
|
|
344
|
+
c["buf"].append(d["content"])
|
|
345
|
+
self.event("response.output_text.delta", {"item_id": c["item"]["id"], "output_index": c["oi"],
|
|
346
|
+
"content_index": 0, "delta": d["content"]})
|
|
347
|
+
for tc in d.get("tool_calls") or []:
|
|
348
|
+
i = tc.get("index", 0)
|
|
349
|
+
f = tc.get("function") or {}
|
|
350
|
+
c = self.calls.get(i)
|
|
351
|
+
if c is None:
|
|
352
|
+
name = f.get("name") or ""
|
|
353
|
+
cid = tc.get("id") or _rid("call")
|
|
354
|
+
if name in self.custom:
|
|
355
|
+
item = {"type": "custom_tool_call", "id": _rid("ctc"), "status": "in_progress", "call_id": cid,
|
|
356
|
+
"name": name, "input": ""}
|
|
357
|
+
else:
|
|
358
|
+
item = {"type": "function_call", "id": _rid("fc"), "status": "in_progress", "call_id": cid,
|
|
359
|
+
"name": name, "arguments": ""}
|
|
360
|
+
c = self.calls[i] = self._open("call", item)
|
|
361
|
+
if f.get("arguments"):
|
|
362
|
+
if c is not self.cur:
|
|
363
|
+
continue # argomenti intercalati (non accade col cuore): scartati
|
|
364
|
+
c["buf"].append(f["arguments"])
|
|
365
|
+
if c["item"]["type"] == "function_call":
|
|
366
|
+
self.event("response.function_call_arguments.delta", {
|
|
367
|
+
"item_id": c["item"]["id"], "output_index": c["oi"], "delta": f["arguments"]})
|
|
368
|
+
if choice.get("finish_reason"):
|
|
369
|
+
self._close()
|
|
370
|
+
st = "incomplete" if choice["finish_reason"] == "length" else "completed"
|
|
371
|
+
extra = {"strata_context": ch["strata_context"]} if ch.get("strata_context") else None
|
|
372
|
+
r = response_obj(self.rid, self.model, self.output, _usage(ch.get("usage")), st, self.created, extra)
|
|
373
|
+
self.event("response.completed" if st == "completed" else "response.incomplete", {"response": r})
|
|
374
|
+
self.done = True
|
|
375
|
+
|
|
376
|
+
def error(self, message, code="server_error"):
|
|
377
|
+
if self.done:
|
|
378
|
+
return
|
|
379
|
+
self._close()
|
|
380
|
+
r = response_obj(self.rid, self.model, self.output, None, "failed", self.created)
|
|
381
|
+
r["error"] = {"code": code, "message": message}
|
|
382
|
+
self.event("response.failed", {"response": r})
|
|
383
|
+
self.done = True
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------- HTTP
|
|
387
|
+
def handle(h, proxy, path: str, raw: bytes):
|
|
388
|
+
try:
|
|
389
|
+
r = json.loads(raw or b"{}")
|
|
390
|
+
req, custom = to_chat_request(r)
|
|
391
|
+
except (ValueError, TypeError, KeyError, AttributeError) as e:
|
|
392
|
+
return h._send(400, error_body(400, str(e)))
|
|
393
|
+
hint = h.headers.get("X-Strata-Conversation") or r.get("prompt_cache_key")
|
|
394
|
+
model = r.get("model")
|
|
395
|
+
if not req.get("stream"):
|
|
396
|
+
try:
|
|
397
|
+
st, resp, hdr = proxy.chat(req, hint)
|
|
398
|
+
except Exception as e: # noqa: BLE001
|
|
399
|
+
proxy.journal.log("proxy_error", api="responses", error=repr(e)[:500])
|
|
400
|
+
return h._send(500, error_body(500, "proxy: %r" % e))
|
|
401
|
+
if st != 200:
|
|
402
|
+
return h._send(st, error_body(st, _err_message(resp)))
|
|
403
|
+
return h._send(200, from_chat_response(resp, model, custom), hdr)
|
|
404
|
+
from .server import ClientGone
|
|
405
|
+
h.send_response(200)
|
|
406
|
+
h.send_header("Content-Type", "text/event-stream")
|
|
407
|
+
h.send_header("Cache-Control", "no-cache")
|
|
408
|
+
h.send_header("Connection", "close")
|
|
409
|
+
h.end_headers()
|
|
410
|
+
h.close_connection = True
|
|
411
|
+
wlock = threading.Lock()
|
|
412
|
+
|
|
413
|
+
def write(data: bytes):
|
|
414
|
+
try:
|
|
415
|
+
with wlock:
|
|
416
|
+
h.wfile.write(data)
|
|
417
|
+
h.wfile.flush()
|
|
418
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
419
|
+
raise ClientGone()
|
|
420
|
+
|
|
421
|
+
w = StreamWriter(write, model, custom)
|
|
422
|
+
try:
|
|
423
|
+
w.start()
|
|
424
|
+
st, resp, _ = proxy.chat(req, hint, emit=w.feed)
|
|
425
|
+
if st != 200:
|
|
426
|
+
w.error(_err_message(resp))
|
|
427
|
+
elif not w.done:
|
|
428
|
+
w.feed({"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], "usage": resp.get("usage")})
|
|
429
|
+
except ClientGone:
|
|
430
|
+
pass
|
|
431
|
+
except Exception as e: # noqa: BLE001
|
|
432
|
+
proxy.journal.log("proxy_error", api="responses", error=repr(e)[:500])
|
|
433
|
+
try:
|
|
434
|
+
w.error("proxy: %r" % e)
|
|
435
|
+
except ClientGone:
|
|
436
|
+
pass
|