@pasko70/pibo 2.5.2 → 3.0.0
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.
- package/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +304 -30
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/process.js +14 -0
- package/dist/agent-runtimes/codex-native/thread.js +59 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/session-router.js +457 -37
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +259 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation intentionally stays below the measured quadratic cost knee.
|
|
3
|
+
* Adapters may page more history, but product identity requires a complete
|
|
4
|
+
* proof and timing set within this shared bound.
|
|
5
|
+
*/
|
|
6
|
+
export const TRACE_RECONCILIATION_ENTRY_CAP = 500;
|
|
7
|
+
export const TRACE_RECONCILIATION_TIMING_CAP = 500;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
|
|
2
|
+
import { applySingleEventToNodes, contentDeltaPatchNodeId, eventsCanAffectAsyncAgentRunStatus, isConfirmedUserMessageEcho, latestTraceStreamId, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
3
|
+
import { flattenTraceNodes } from "./trace-nodes.js";
|
|
4
|
+
import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
|
|
5
|
+
export function patchTraceViewWithEvent(view, event, sessionStatus) {
|
|
6
|
+
return patchTraceViewWithEvents(view, [event], sessionStatus);
|
|
7
|
+
}
|
|
8
|
+
export function patchTraceViewWithEvents(view, events, sessionStatus) {
|
|
9
|
+
if (!events.length)
|
|
10
|
+
return view;
|
|
11
|
+
const seenEventKeys = new Set(view.rawEvents.map((event) => traceEventDedupeKey(event)));
|
|
12
|
+
const candidateEvents = [];
|
|
13
|
+
for (const event of events) {
|
|
14
|
+
const eventKey = traceEventDedupeKey(event);
|
|
15
|
+
if (seenEventKeys.has(eventKey))
|
|
16
|
+
continue;
|
|
17
|
+
seenEventKeys.add(eventKey);
|
|
18
|
+
candidateEvents.push(event);
|
|
19
|
+
}
|
|
20
|
+
if (!candidateEvents.length)
|
|
21
|
+
return view;
|
|
22
|
+
const previousFlatNodes = flattenTraceNodes(view.nodes);
|
|
23
|
+
const allNodes = [];
|
|
24
|
+
const byId = new Map();
|
|
25
|
+
const previousById = new Map();
|
|
26
|
+
for (const previousNode of previousFlatNodes) {
|
|
27
|
+
previousById.set(previousNode.id, previousNode);
|
|
28
|
+
const nextNode = { ...previousNode, children: [] };
|
|
29
|
+
allNodes.push(nextNode);
|
|
30
|
+
byId.set(nextNode.id, nextNode);
|
|
31
|
+
}
|
|
32
|
+
const childByParent = new Map();
|
|
33
|
+
const linkedChildByToolCallId = new Map();
|
|
34
|
+
const openTranscriptEventIds = new Set();
|
|
35
|
+
const emptyHistoryCoverage = { mode: "none", eventIds: new Set(), toolCallIds: new Set() };
|
|
36
|
+
const appliedEvents = [];
|
|
37
|
+
let contentDeltaChangedNodeIds = new Set();
|
|
38
|
+
for (const event of candidateEvents) {
|
|
39
|
+
if (isConfirmedUserMessageEcho(allNodes, event))
|
|
40
|
+
continue;
|
|
41
|
+
appliedEvents.push(event);
|
|
42
|
+
const contentDeltaNodeId = contentDeltaPatchNodeId(event.payload);
|
|
43
|
+
if (contentDeltaChangedNodeIds && contentDeltaNodeId)
|
|
44
|
+
contentDeltaChangedNodeIds.add(contentDeltaNodeId);
|
|
45
|
+
else
|
|
46
|
+
contentDeltaChangedNodeIds = undefined;
|
|
47
|
+
applySingleEventToNodes(allNodes, byId, view.piboSessionId, event, childByParent, linkedChildByToolCallId, emptyHistoryCoverage, openTranscriptEventIds, sessionStatus);
|
|
48
|
+
}
|
|
49
|
+
if (!appliedEvents.length)
|
|
50
|
+
return view;
|
|
51
|
+
const nestedNodes = nestMutableCopiedTraceNodes(allNodes);
|
|
52
|
+
if (eventsCanAffectAsyncAgentRunStatus(appliedEvents))
|
|
53
|
+
reconcileAsyncAgentRunStatuses(nestedNodes);
|
|
54
|
+
const sharedNodes = shareUnchangedTraceNodes(previousById, nestedNodes, contentDeltaChangedNodeIds);
|
|
55
|
+
return {
|
|
56
|
+
...view,
|
|
57
|
+
rawEvents: view.rawEvents.length ? [...view.rawEvents, ...appliedEvents] : appliedEvents,
|
|
58
|
+
nodes: sharedNodes,
|
|
59
|
+
latestStreamId: latestTraceStreamId(appliedEvents, view.latestStreamId),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TRACE_RECONCILIATION_ENTRY_CAP } from "./trace-limits.js";
|
|
2
|
+
const QUALIFIED_TOOL_PREFIX = "history-tool:";
|
|
3
|
+
const TOOL_PREFIX = "tool:";
|
|
4
|
+
const TOOL_INVOCATION_TAG = "event-tool";
|
|
5
|
+
export function qualifiedHistoryToolNodeId(toolCallId, eventId, invocationOrdinal) {
|
|
6
|
+
const projectionIdentity = JSON.stringify([TOOL_INVOCATION_TAG, eventId, invocationOrdinal]);
|
|
7
|
+
return `${QUALIFIED_TOOL_PREFIX}${encodeURIComponent(JSON.stringify([toolCallId, projectionIdentity]))}`;
|
|
8
|
+
}
|
|
9
|
+
export function parseTraceToolNodeIdentity(nodeId) {
|
|
10
|
+
if (nodeId.startsWith(TOOL_PREFIX)) {
|
|
11
|
+
const toolCallId = nodeId.slice(TOOL_PREFIX.length);
|
|
12
|
+
return toolCallId ? { toolCallId } : undefined;
|
|
13
|
+
}
|
|
14
|
+
if (!nodeId.startsWith(QUALIFIED_TOOL_PREFIX))
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const decoded = JSON.parse(decodeURIComponent(nodeId.slice(QUALIFIED_TOOL_PREFIX.length)));
|
|
18
|
+
if (!Array.isArray(decoded)
|
|
19
|
+
|| decoded.length !== 2
|
|
20
|
+
|| typeof decoded[0] !== "string"
|
|
21
|
+
|| decoded[0].length === 0
|
|
22
|
+
|| typeof decoded[1] !== "string")
|
|
23
|
+
return undefined;
|
|
24
|
+
const qualifier = JSON.parse(decoded[1]);
|
|
25
|
+
if (!Array.isArray(qualifier)
|
|
26
|
+
|| qualifier.length !== 3
|
|
27
|
+
|| qualifier[0] !== TOOL_INVOCATION_TAG
|
|
28
|
+
|| typeof qualifier[1] !== "string"
|
|
29
|
+
|| qualifier[1].length === 0
|
|
30
|
+
|| !Number.isSafeInteger(qualifier[2])
|
|
31
|
+
|| Number(qualifier[2]) < 0
|
|
32
|
+
|| Number(qualifier[2]) >= TRACE_RECONCILIATION_ENTRY_CAP)
|
|
33
|
+
return undefined;
|
|
34
|
+
const identity = {
|
|
35
|
+
toolCallId: decoded[0],
|
|
36
|
+
qualifier: { eventId: qualifier[1], invocationOrdinal: Number(qualifier[2]) },
|
|
37
|
+
};
|
|
38
|
+
return qualifiedHistoryToolNodeId(identity.toolCallId, identity.qualifier.eventId, identity.qualifier.invocationOrdinal) === nodeId ? identity : undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
const CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex/";
|
|
4
|
+
const CODEX_REALTIME_MODEL = "gpt-live-1-codex";
|
|
5
|
+
const MAX_REQUEST_BYTES = 512 * 1024;
|
|
6
|
+
const MAX_RESPONSE_BYTES = 512 * 1024;
|
|
7
|
+
const REQUEST_BODY_TIMEOUT_MS = 10_000;
|
|
8
|
+
const FORWARD_TIMEOUT_MS = 30_000;
|
|
9
|
+
const OMITTED_RESPONSE_HEADERS = new Set([
|
|
10
|
+
"connection",
|
|
11
|
+
"content-encoding",
|
|
12
|
+
"content-length",
|
|
13
|
+
"keep-alive",
|
|
14
|
+
"proxy-authenticate",
|
|
15
|
+
"proxy-authorization",
|
|
16
|
+
"te",
|
|
17
|
+
"trailer",
|
|
18
|
+
"transfer-encoding",
|
|
19
|
+
"upgrade",
|
|
20
|
+
]);
|
|
21
|
+
class ProxyRequestError extends Error {
|
|
22
|
+
status;
|
|
23
|
+
constructor(status, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
async function readRequestBody(request, signal, timeoutMs) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let bytes = 0;
|
|
34
|
+
const timeoutController = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
timeoutController.abort(new ProxyRequestError(408, "Realtime call request timed out"));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
timer.unref?.();
|
|
39
|
+
const combinedSignal = AbortSignal.any([signal, timeoutController.signal]);
|
|
40
|
+
const abortRead = () => request.destroy(combinedSignal.reason instanceof Error ? combinedSignal.reason : undefined);
|
|
41
|
+
if (combinedSignal.aborted)
|
|
42
|
+
abortRead();
|
|
43
|
+
else
|
|
44
|
+
combinedSignal.addEventListener("abort", abortRead, { once: true });
|
|
45
|
+
try {
|
|
46
|
+
for await (const chunk of request) {
|
|
47
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
48
|
+
bytes += buffer.length;
|
|
49
|
+
if (bytes > MAX_REQUEST_BYTES)
|
|
50
|
+
throw new ProxyRequestError(413, "Realtime call request is too large");
|
|
51
|
+
chunks.push(buffer);
|
|
52
|
+
}
|
|
53
|
+
if (timeoutController.signal.aborted)
|
|
54
|
+
throw timeoutController.signal.reason;
|
|
55
|
+
if (signal.aborted)
|
|
56
|
+
throw signal.reason;
|
|
57
|
+
return Buffer.concat(chunks);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
combinedSignal.removeEventListener("abort", abortRead);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function requestHeaders(request) {
|
|
65
|
+
const authorization = request.headers.authorization;
|
|
66
|
+
if (typeof authorization !== "string" || !authorization) {
|
|
67
|
+
throw new ProxyRequestError(401, "Codex subscription authentication is required");
|
|
68
|
+
}
|
|
69
|
+
const headers = new Headers({
|
|
70
|
+
accept: "application/sdp",
|
|
71
|
+
"accept-encoding": "identity",
|
|
72
|
+
authorization,
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
"openai-alpha": "quicksilver=v2",
|
|
75
|
+
originator: "codex_cli_rs",
|
|
76
|
+
});
|
|
77
|
+
for (const name of ["chatgpt-account-id", "session-id", "thread-id", "user-agent", "x-openai-fedramp"]) {
|
|
78
|
+
const value = request.headers[name];
|
|
79
|
+
if (typeof value === "string" && value)
|
|
80
|
+
headers.set(name, value);
|
|
81
|
+
}
|
|
82
|
+
return headers;
|
|
83
|
+
}
|
|
84
|
+
function realtimeCallBody(value) {
|
|
85
|
+
if (!isRecord(value) || typeof value.sdp !== "string" || !isRecord(value.session)) {
|
|
86
|
+
throw new ProxyRequestError(400, "Codex returned an invalid realtime call request");
|
|
87
|
+
}
|
|
88
|
+
const session = { ...value.session, model: CODEX_REALTIME_MODEL };
|
|
89
|
+
delete session.id;
|
|
90
|
+
return JSON.stringify({ ...value, session });
|
|
91
|
+
}
|
|
92
|
+
function writeResponseHeaders(response, outgoing) {
|
|
93
|
+
for (const [name, value] of response.headers) {
|
|
94
|
+
if (!OMITTED_RESPONSE_HEADERS.has(name))
|
|
95
|
+
outgoing.setHeader(name, value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function readResponseBody(response) {
|
|
99
|
+
const contentLength = response.headers.get("content-length");
|
|
100
|
+
if (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {
|
|
101
|
+
await response.body?.cancel().catch(() => { });
|
|
102
|
+
throw new ProxyRequestError(502, "Realtime call response is too large");
|
|
103
|
+
}
|
|
104
|
+
if (!response.body)
|
|
105
|
+
return Buffer.alloc(0);
|
|
106
|
+
const reader = response.body.getReader();
|
|
107
|
+
const chunks = [];
|
|
108
|
+
let bytes = 0;
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done)
|
|
113
|
+
break;
|
|
114
|
+
const chunk = Buffer.from(value);
|
|
115
|
+
bytes += chunk.length;
|
|
116
|
+
if (bytes > MAX_RESPONSE_BYTES) {
|
|
117
|
+
await reader.cancel().catch(() => { });
|
|
118
|
+
throw new ProxyRequestError(502, "Realtime call response is too large");
|
|
119
|
+
}
|
|
120
|
+
chunks.push(chunk);
|
|
121
|
+
}
|
|
122
|
+
return Buffer.concat(chunks);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function forwardRealtimeCall(request, response, targetUrl, signal, requestBodyTimeoutMs) {
|
|
129
|
+
const body = await readRequestBody(request, signal, requestBodyTimeoutMs);
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
payload = JSON.parse(body.toString("utf8"));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new ProxyRequestError(400, "Codex returned malformed realtime call JSON");
|
|
136
|
+
}
|
|
137
|
+
const timeoutController = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => timeoutController.abort(new Error("Realtime call forwarding timed out")), FORWARD_TIMEOUT_MS);
|
|
139
|
+
timer.unref?.();
|
|
140
|
+
try {
|
|
141
|
+
const upstream = await fetch(targetUrl, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: requestHeaders(request),
|
|
144
|
+
body: realtimeCallBody(payload),
|
|
145
|
+
redirect: "manual",
|
|
146
|
+
signal: AbortSignal.any([signal, timeoutController.signal]),
|
|
147
|
+
});
|
|
148
|
+
response.statusCode = upstream.status;
|
|
149
|
+
writeResponseHeaders(upstream, response);
|
|
150
|
+
response.end(await readResponseBody(upstream));
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function sendProxyError(response, error) {
|
|
157
|
+
if (response.headersSent) {
|
|
158
|
+
response.destroy();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
response.statusCode = error instanceof ProxyRequestError ? error.status : 502;
|
|
162
|
+
response.setHeader("content-type", "application/json");
|
|
163
|
+
response.end(JSON.stringify({ error: error instanceof ProxyRequestError ? error.message : "Realtime call forwarding failed" }));
|
|
164
|
+
}
|
|
165
|
+
export async function startOpenAiCodexRealtimeCallProxy(options = {}) {
|
|
166
|
+
if (options.signal?.aborted)
|
|
167
|
+
throw options.signal.reason;
|
|
168
|
+
const targetBaseUrl = new URL(options.targetBaseUrl ?? CHATGPT_CODEX_BASE_URL);
|
|
169
|
+
if (targetBaseUrl.protocol !== "https:" && targetBaseUrl.protocol !== "http:") {
|
|
170
|
+
throw new Error("Codex realtime call target must use HTTP or HTTPS");
|
|
171
|
+
}
|
|
172
|
+
if (!targetBaseUrl.pathname.endsWith("/"))
|
|
173
|
+
targetBaseUrl.pathname += "/";
|
|
174
|
+
const requestBodyTimeoutMs = options.requestBodyTimeoutMs ?? REQUEST_BODY_TIMEOUT_MS;
|
|
175
|
+
if (!Number.isFinite(requestBodyTimeoutMs) || requestBodyTimeoutMs <= 0) {
|
|
176
|
+
throw new Error("Codex realtime call request timeout must be a positive finite number");
|
|
177
|
+
}
|
|
178
|
+
const routePrefix = `/speech-${randomUUID()}/backend-api/codex`;
|
|
179
|
+
const sockets = new Set();
|
|
180
|
+
const activeRequests = new Set();
|
|
181
|
+
const server = createServer((request, response) => {
|
|
182
|
+
const requestController = new AbortController();
|
|
183
|
+
activeRequests.add(requestController);
|
|
184
|
+
void (async () => {
|
|
185
|
+
if (request.method !== "POST")
|
|
186
|
+
throw new ProxyRequestError(405, "Method not allowed");
|
|
187
|
+
const incomingUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
188
|
+
if (incomingUrl.pathname !== `${routePrefix}/realtime/calls`) {
|
|
189
|
+
throw new ProxyRequestError(404, "Not found");
|
|
190
|
+
}
|
|
191
|
+
const targetUrl = new URL("realtime/calls", targetBaseUrl);
|
|
192
|
+
targetUrl.search = incomingUrl.search;
|
|
193
|
+
await forwardRealtimeCall(request, response, targetUrl, requestController.signal, requestBodyTimeoutMs);
|
|
194
|
+
})()
|
|
195
|
+
.catch((error) => sendProxyError(response, error))
|
|
196
|
+
.finally(() => activeRequests.delete(requestController));
|
|
197
|
+
});
|
|
198
|
+
server.on("connection", (socket) => {
|
|
199
|
+
sockets.add(socket);
|
|
200
|
+
socket.once("close", () => sockets.delete(socket));
|
|
201
|
+
});
|
|
202
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
203
|
+
await new Promise((resolve, reject) => {
|
|
204
|
+
server.once("error", reject);
|
|
205
|
+
server.listen(0, "127.0.0.1", () => {
|
|
206
|
+
server.off("error", reject);
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
if (options.signal?.aborted) {
|
|
211
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
212
|
+
throw options.signal.reason;
|
|
213
|
+
}
|
|
214
|
+
const address = server.address();
|
|
215
|
+
if (!address || typeof address === "string") {
|
|
216
|
+
server.close();
|
|
217
|
+
throw new Error("Codex realtime call proxy did not bind to a TCP port");
|
|
218
|
+
}
|
|
219
|
+
let closePromise;
|
|
220
|
+
const close = () => {
|
|
221
|
+
if (!closePromise) {
|
|
222
|
+
options.signal?.removeEventListener("abort", closeOnAbort);
|
|
223
|
+
for (const controller of activeRequests)
|
|
224
|
+
controller.abort(new Error("Codex realtime call proxy closed"));
|
|
225
|
+
closePromise = new Promise((resolve) => {
|
|
226
|
+
if (!server.listening) {
|
|
227
|
+
for (const socket of sockets)
|
|
228
|
+
socket.destroy();
|
|
229
|
+
resolve();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
server.close(() => resolve());
|
|
233
|
+
for (const socket of sockets)
|
|
234
|
+
socket.destroy();
|
|
235
|
+
server.closeAllConnections();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return closePromise;
|
|
239
|
+
};
|
|
240
|
+
const closeOnAbort = () => {
|
|
241
|
+
void close();
|
|
242
|
+
};
|
|
243
|
+
options.signal?.addEventListener("abort", closeOnAbort, { once: true });
|
|
244
|
+
return {
|
|
245
|
+
baseUrl: `http://127.0.0.1:${address.port}${routePrefix}`,
|
|
246
|
+
close,
|
|
247
|
+
};
|
|
248
|
+
}
|