codex-multi-auth 2.16.0 → 2.17.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/.codex-plugin/plugin.json +1 -1
- package/README.md +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/lib/account-policy.d.ts +4 -0
- package/dist/lib/account-policy.d.ts.map +1 -1
- package/dist/lib/account-policy.js +8 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
- package/dist/lib/accounts/rate-limits.js +2 -0
- package/dist/lib/accounts.d.ts +21 -3
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +229 -17
- package/dist/lib/api-route-store.d.ts +24 -0
- package/dist/lib/api-route-store.d.ts.map +1 -0
- package/dist/lib/api-route-store.js +145 -0
- package/dist/lib/auth/account-access.d.ts +115 -0
- package/dist/lib/auth/account-access.d.ts.map +1 -0
- package/dist/lib/auth/account-access.js +214 -0
- package/dist/lib/auth/token-utils.d.ts +54 -1
- package/dist/lib/auth/token-utils.d.ts.map +1 -1
- package/dist/lib/auth/token-utils.js +92 -7
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +4 -0
- package/dist/lib/codex-cli/state.d.ts +7 -0
- package/dist/lib/codex-cli/state.d.ts.map +1 -1
- package/dist/lib/codex-cli/state.js +18 -0
- package/dist/lib/codex-cli/writer.d.ts.map +1 -1
- package/dist/lib/codex-cli/writer.js +60 -8
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts +3 -1
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +4 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts +14 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts.map +1 -0
- package/dist/lib/codex-manager/api-login-menu.js +224 -0
- package/dist/lib/codex-manager/commands/account.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/account.js +27 -0
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +6 -2
- package/dist/lib/codex-manager/commands/check.d.ts +7 -1
- package/dist/lib/codex-manager/commands/check.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/check.js +24 -3
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +5 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +5 -1
- package/dist/lib/codex-manager/commands/resets.d.ts +2 -0
- package/dist/lib/codex-manager/commands/resets.d.ts.map +1 -0
- package/dist/lib/codex-manager/commands/resets.js +67 -0
- package/dist/lib/codex-manager/commands/rotation.d.ts +2 -2
- package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/rotation.js +63 -12
- package/dist/lib/codex-manager/commands/status.d.ts +12 -1
- package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/status.js +166 -22
- package/dist/lib/codex-manager/commands/uninstall.js +1 -1
- package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -1
- package/dist/lib/codex-manager/formatters/quota-formatters.js +3 -2
- package/dist/lib/codex-manager/health-check.d.ts +3 -0
- package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
- package/dist/lib/codex-manager/health-check.js +56 -6
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +4 -2
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +69 -7
- package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-data.js +14 -5
- package/dist/lib/codex-manager/login-oauth.d.ts +3 -0
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +6 -1
- package/dist/lib/codex-manager/login-workspace-choice.d.ts +11 -0
- package/dist/lib/codex-manager/login-workspace-choice.d.ts.map +1 -0
- package/dist/lib/codex-manager/login-workspace-choice.js +37 -0
- package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
- package/dist/lib/codex-manager/persist-selected-account.js +2 -1
- package/dist/lib/codex-manager/repair-commands.d.ts +13 -0
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +143 -8
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +25 -6
- package/dist/lib/concurrency.d.ts +3 -0
- package/dist/lib/concurrency.d.ts.map +1 -0
- package/dist/lib/concurrency.js +31 -0
- package/dist/lib/model-route-policy.d.ts +52 -0
- package/dist/lib/model-route-policy.d.ts.map +1 -0
- package/dist/lib/model-route-policy.js +159 -0
- package/dist/lib/policy/runtime-policy.d.ts +1 -0
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +3 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts +2 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
- package/dist/lib/preemptive-quota-scheduler.js +4 -0
- package/dist/lib/quota-probe.d.ts +5 -0
- package/dist/lib/quota-probe.d.ts.map +1 -1
- package/dist/lib/quota-probe.js +26 -2
- package/dist/lib/request/client-cancellation.d.ts +5 -0
- package/dist/lib/request/client-cancellation.d.ts.map +1 -0
- package/dist/lib/request/client-cancellation.js +7 -0
- package/dist/lib/request/response-outcome.d.ts +25 -0
- package/dist/lib/request/response-outcome.d.ts.map +1 -0
- package/dist/lib/request/response-outcome.js +49 -0
- package/dist/lib/request/stream-failover-runtime.d.ts +2 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +29 -10
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +2 -8
- package/dist/lib/runtime/account-model-catalog.d.ts +64 -0
- package/dist/lib/runtime/account-model-catalog.d.ts.map +1 -0
- package/dist/lib/runtime/account-model-catalog.js +154 -0
- package/dist/lib/runtime/account-pool.d.ts +3 -1
- package/dist/lib/runtime/account-pool.d.ts.map +1 -1
- package/dist/lib/runtime/account-pool.js +4 -0
- package/dist/lib/runtime/account-reset-credits.d.ts +42 -0
- package/dist/lib/runtime/account-reset-credits.d.ts.map +1 -0
- package/dist/lib/runtime/account-reset-credits.js +82 -0
- package/dist/lib/runtime/api-model-capabilities.d.ts +30 -0
- package/dist/lib/runtime/api-model-capabilities.d.ts.map +1 -0
- package/dist/lib/runtime/api-model-capabilities.js +434 -0
- package/dist/lib/runtime/api-model-runtime.d.ts +42 -0
- package/dist/lib/runtime/api-model-runtime.d.ts.map +1 -0
- package/dist/lib/runtime/api-model-runtime.js +358 -0
- package/dist/lib/runtime/app-bind.d.ts +19 -1
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +79 -9
- package/dist/lib/runtime/automatic-account-checks.d.ts +31 -0
- package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -0
- package/dist/lib/runtime/automatic-account-checks.js +113 -0
- package/dist/lib/runtime/automatic-subscription-checks.d.ts +7 -0
- package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -0
- package/dist/lib/runtime/automatic-subscription-checks.js +57 -0
- package/dist/lib/runtime/catalog-capabilities.d.ts +8 -0
- package/dist/lib/runtime/catalog-capabilities.d.ts.map +1 -0
- package/dist/lib/runtime/catalog-capabilities.js +61 -0
- package/dist/lib/runtime/inference-activity.d.ts +15 -0
- package/dist/lib/runtime/inference-activity.d.ts.map +1 -0
- package/dist/lib/runtime/inference-activity.js +95 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +100 -0
- package/dist/lib/runtime/model-discovery-status.d.ts.map +1 -0
- package/dist/lib/runtime/model-discovery-status.js +387 -0
- package/dist/lib/runtime/native-account-storage.d.ts +11 -0
- package/dist/lib/runtime/native-account-storage.d.ts.map +1 -0
- package/dist/lib/runtime/native-account-storage.js +50 -0
- package/dist/lib/runtime/native-account-sync.d.ts +19 -0
- package/dist/lib/runtime/native-account-sync.d.ts.map +1 -0
- package/dist/lib/runtime/native-account-sync.js +62 -0
- package/dist/lib/runtime/native-binding-lock.d.ts +3 -0
- package/dist/lib/runtime/native-binding-lock.d.ts.map +1 -0
- package/dist/lib/runtime/native-binding-lock.js +5 -0
- package/dist/lib/runtime/native-client-auth.d.ts +3 -0
- package/dist/lib/runtime/native-client-auth.d.ts.map +1 -0
- package/dist/lib/runtime/native-client-auth.js +42 -0
- package/dist/lib/runtime/native-provider-config.d.ts +4 -0
- package/dist/lib/runtime/native-provider-config.d.ts.map +1 -0
- package/dist/lib/runtime/native-provider-config.js +52 -0
- package/dist/lib/runtime/native-rate-limits.d.ts +18 -0
- package/dist/lib/runtime/native-rate-limits.d.ts.map +1 -0
- package/dist/lib/runtime/native-rate-limits.js +135 -0
- package/dist/lib/runtime/reset-credit-routing.d.ts +41 -0
- package/dist/lib/runtime/reset-credit-routing.d.ts.map +1 -0
- package/dist/lib/runtime/reset-credit-routing.js +66 -0
- package/dist/lib/runtime/reset-credits.d.ts +101 -0
- package/dist/lib/runtime/reset-credits.d.ts.map +1 -0
- package/dist/lib/runtime/reset-credits.js +162 -0
- package/dist/lib/runtime/response-output-history.d.ts +17 -0
- package/dist/lib/runtime/response-output-history.d.ts.map +1 -0
- package/dist/lib/runtime/response-output-history.js +85 -0
- package/dist/lib/runtime/responses-websocket.d.ts +31 -0
- package/dist/lib/runtime/responses-websocket.d.ts.map +1 -0
- package/dist/lib/runtime/responses-websocket.js +636 -0
- package/dist/lib/runtime/resume-picker.d.ts +41 -0
- package/dist/lib/runtime/resume-picker.d.ts.map +1 -0
- package/dist/lib/runtime/resume-picker.js +169 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts +6 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-account-selection.js +63 -0
- package/dist/lib/runtime/rotation-proxy-state.d.ts +33 -0
- package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +18 -0
- package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-capability-failures.d.ts +14 -0
- package/dist/lib/runtime/runtime-capability-failures.d.ts.map +1 -0
- package/dist/lib/runtime/runtime-capability-failures.js +76 -0
- package/dist/lib/runtime/runtime-observability.d.ts +7 -0
- package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-observability.js +42 -2
- package/dist/lib/runtime/subscription-first-use.d.ts +10 -0
- package/dist/lib/runtime/subscription-first-use.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-first-use.js +64 -0
- package/dist/lib/runtime/subscription-quota-event.d.ts +6 -0
- package/dist/lib/runtime/subscription-quota-event.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-quota-event.js +31 -0
- package/dist/lib/runtime/subscription-quota-order.d.ts +17 -0
- package/dist/lib/runtime/subscription-quota-order.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-quota-order.js +31 -0
- package/dist/lib/runtime/workspace-model-scopes.d.ts +15 -0
- package/dist/lib/runtime/workspace-model-scopes.d.ts.map +1 -0
- package/dist/lib/runtime/workspace-model-scopes.js +34 -0
- package/dist/lib/runtime-constants.d.ts +3 -0
- package/dist/lib/runtime-constants.d.ts.map +1 -1
- package/dist/lib/runtime-constants.js +5 -0
- package/dist/lib/runtime-paths.d.ts.map +1 -1
- package/dist/lib/runtime-paths.js +4 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +852 -42
- package/dist/lib/schemas.d.ts +20 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +3 -0
- package/dist/lib/storage/credential-sidecars.d.ts +13 -0
- package/dist/lib/storage/credential-sidecars.d.ts.map +1 -0
- package/dist/lib/storage/credential-sidecars.js +52 -0
- package/dist/lib/storage/file-lock.d.ts +9 -0
- package/dist/lib/storage/file-lock.d.ts.map +1 -0
- package/dist/lib/storage/file-lock.js +186 -0
- package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
- package/dist/lib/storage/flagged-storage.js +12 -0
- package/dist/lib/storage/path-state.d.ts +2 -0
- package/dist/lib/storage/path-state.d.ts.map +1 -1
- package/dist/lib/storage/path-state.js +9 -0
- package/dist/lib/storage/paths.js +4 -4
- package/dist/lib/storage/pending-auth.d.ts +17 -0
- package/dist/lib/storage/pending-auth.d.ts.map +1 -0
- package/dist/lib/storage/pending-auth.js +153 -0
- package/dist/lib/storage/public-types.d.ts +13 -0
- package/dist/lib/storage/public-types.d.ts.map +1 -1
- package/dist/lib/storage/snapshot-merge.d.ts +16 -0
- package/dist/lib/storage/snapshot-merge.d.ts.map +1 -0
- package/dist/lib/storage/snapshot-merge.js +206 -0
- package/dist/lib/storage/transactions.d.ts.map +1 -1
- package/dist/lib/storage/transactions.js +5 -4
- package/dist/lib/storage.d.ts +6 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +77 -7
- package/dist/lib/ui/auth-menu-builder.d.ts +2 -0
- package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu-builder.js +1 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +1 -0
- package/dist/lib/ui/check-progress.d.ts +8 -0
- package/dist/lib/ui/check-progress.d.ts.map +1 -0
- package/dist/lib/ui/check-progress.js +30 -0
- package/dist/lib/ui/format.d.ts +3 -1
- package/dist/lib/ui/format.d.ts.map +1 -1
- package/dist/lib/ui/format.js +8 -0
- package/dist/lib/ui/ui-copy.d.ts +2 -2
- package/dist/lib/ui/ui-copy.js +2 -2
- package/dist/lib/usage/pricing.js +2 -2
- package/dist/lib/usage/usage-extraction.d.ts +3 -1
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.js +46 -9
- package/package.json +7 -4
- package/scripts/codex-app-router.js +7 -0
- package/scripts/codex-bin-resolver.js +40 -8
- package/scripts/codex-routing.js +1 -0
- package/scripts/codex.js +100 -22
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { ClientCancellationError } from "../request/client-cancellation.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
5
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
6
|
+
import { parseModelRoute } from "../model-route-policy.js";
|
|
7
|
+
import { isRecord } from "../utils.js";
|
|
8
|
+
import { ResponseOutputHistory, hasOrphanToolResult } from "./response-output-history.js";
|
|
9
|
+
const CONTEXT_HEADER = "x-local-responses-transport";
|
|
10
|
+
const terminal = new Set([
|
|
11
|
+
"response.completed",
|
|
12
|
+
"response.failed",
|
|
13
|
+
"response.incomplete",
|
|
14
|
+
"response.cancelled",
|
|
15
|
+
"error",
|
|
16
|
+
]);
|
|
17
|
+
/** Headers that describe the synthetic body or the upgrade itself, never the upstream decision. */
|
|
18
|
+
const LOCAL_ONLY_HEADERS = /^(connection|upgrade|keep-alive|sec-websocket-.*|content-length|content-encoding|content-type|transfer-encoding|set-cookie)$/i;
|
|
19
|
+
function upstreamHeaders(source) {
|
|
20
|
+
const headers = {};
|
|
21
|
+
if (!isRecord(source))
|
|
22
|
+
return headers;
|
|
23
|
+
for (const [key, value] of Object.entries(source)) {
|
|
24
|
+
if (LOCAL_ONLY_HEADERS.test(key))
|
|
25
|
+
continue;
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
headers[key] = value;
|
|
28
|
+
else if (typeof value === "number")
|
|
29
|
+
headers[key] = String(value);
|
|
30
|
+
else if (Array.isArray(value) && value.every(item => typeof item === "string"))
|
|
31
|
+
headers[key] = value.join(", ");
|
|
32
|
+
}
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
/** Union of comma-separated OpenAI-Beta features; the client's own flags are kept. */
|
|
36
|
+
function mergeBeta(current, client) {
|
|
37
|
+
if (!client)
|
|
38
|
+
return current;
|
|
39
|
+
const features = [...(current ?? "").split(","), ...client.split(",")].map(item => item.trim()).filter(Boolean);
|
|
40
|
+
return [...new Set(features)].join(", ") || null;
|
|
41
|
+
}
|
|
42
|
+
function wireError(code, status = 400) {
|
|
43
|
+
return {
|
|
44
|
+
type: "error",
|
|
45
|
+
status,
|
|
46
|
+
error: {
|
|
47
|
+
type: "invalid_request_error",
|
|
48
|
+
code,
|
|
49
|
+
message: code === "previous_response_not_found"
|
|
50
|
+
? "Retry with complete input and no previous_response_id."
|
|
51
|
+
: "The local WebSocket request could not be served.",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function bodyJson(body) {
|
|
56
|
+
const value = JSON.parse(typeof body === "string"
|
|
57
|
+
? body
|
|
58
|
+
: Buffer.from(body).toString("utf8"));
|
|
59
|
+
if (!isRecord(value))
|
|
60
|
+
throw Error("Invalid response body");
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/** Per-client memory only; credentials never leave the existing routing boundary. */
|
|
64
|
+
class SocketSession {
|
|
65
|
+
maxBytes;
|
|
66
|
+
stats;
|
|
67
|
+
httpFetch;
|
|
68
|
+
channels = new Map();
|
|
69
|
+
owners = new Map();
|
|
70
|
+
chains = new Map();
|
|
71
|
+
/** Credentials whose WebSocket handshake was refused; they use HTTP for the session. */
|
|
72
|
+
httpOnly = new Set();
|
|
73
|
+
bytes = 0;
|
|
74
|
+
controller;
|
|
75
|
+
closed = false;
|
|
76
|
+
constructor(maxBytes, stats, httpFetch = fetch) {
|
|
77
|
+
this.maxBytes = maxBytes;
|
|
78
|
+
this.stats = stats;
|
|
79
|
+
this.httpFetch = httpFetch;
|
|
80
|
+
}
|
|
81
|
+
close() {
|
|
82
|
+
this.closed = true;
|
|
83
|
+
this.controller?.abort();
|
|
84
|
+
for (const socket of this.channels.values())
|
|
85
|
+
socket.terminate();
|
|
86
|
+
this.channels.clear();
|
|
87
|
+
this.owners.clear();
|
|
88
|
+
this.chains.clear();
|
|
89
|
+
this.bytes = 0;
|
|
90
|
+
}
|
|
91
|
+
remember(id, body, output, pool) {
|
|
92
|
+
const bytes = Buffer.byteLength(JSON.stringify({ body, output }));
|
|
93
|
+
if (bytes > this.maxBytes)
|
|
94
|
+
return;
|
|
95
|
+
const old = this.chains.get(id);
|
|
96
|
+
if (old)
|
|
97
|
+
this.bytes -= old.bytes;
|
|
98
|
+
this.chains.delete(id);
|
|
99
|
+
this.chains.set(id, { body, output, pool, bytes });
|
|
100
|
+
this.bytes += bytes;
|
|
101
|
+
while (this.bytes > this.maxBytes || this.chains.size > 32) {
|
|
102
|
+
const first = this.chains.keys().next().value;
|
|
103
|
+
if (!first)
|
|
104
|
+
break;
|
|
105
|
+
const entry = this.chains.get(first);
|
|
106
|
+
this.bytes -= entry?.bytes ?? 0;
|
|
107
|
+
this.chains.delete(first);
|
|
108
|
+
this.owners.delete(first);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async fetch(input, init, context) {
|
|
112
|
+
if (context.signal.aborted || this.closed)
|
|
113
|
+
throw new ClientCancellationError();
|
|
114
|
+
const url = new URL(String(input));
|
|
115
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
116
|
+
const headers = new Headers(init.headers);
|
|
117
|
+
// The routing pipeline stamps its HTTP beta flag; the native client's
|
|
118
|
+
// WebSocket flag (e.g. responses_websockets=<date>) must survive the hop.
|
|
119
|
+
const beta = mergeBeta(headers.get("openai-beta"), context.clientBeta);
|
|
120
|
+
if (beta)
|
|
121
|
+
headers.set("openai-beta", beta);
|
|
122
|
+
const key = createHash("sha256")
|
|
123
|
+
.update(url.toString())
|
|
124
|
+
.update("\0")
|
|
125
|
+
.update(headers.get("authorization") ?? "")
|
|
126
|
+
.update("\0")
|
|
127
|
+
.update(headers.get("chatgpt-account-id") ?? "")
|
|
128
|
+
.digest("hex");
|
|
129
|
+
// API/ZDR credentials are not ChatGPT workspaces: when their WebSocket
|
|
130
|
+
// handshake is refused, the turn (and the session) uses plain HTTP.
|
|
131
|
+
const apiCredential = !headers.has("chatgpt-account-id");
|
|
132
|
+
if (apiCredential && this.httpOnly.has(key))
|
|
133
|
+
return this.httpFetch(input, init);
|
|
134
|
+
const body = bodyJson(init.body);
|
|
135
|
+
delete body.stream;
|
|
136
|
+
delete body.background;
|
|
137
|
+
body.store = false;
|
|
138
|
+
body.type = "response.create";
|
|
139
|
+
let socket = this.channels.get(key);
|
|
140
|
+
if (socket?.readyState !== WebSocket.OPEN) {
|
|
141
|
+
socket?.terminate();
|
|
142
|
+
this.channels.delete(key);
|
|
143
|
+
socket = undefined;
|
|
144
|
+
}
|
|
145
|
+
if (context.previousId &&
|
|
146
|
+
socket &&
|
|
147
|
+
this.owners.get(context.previousId) === socket) {
|
|
148
|
+
body.previous_response_id = context.previousId;
|
|
149
|
+
body.input = context.delta;
|
|
150
|
+
}
|
|
151
|
+
else if (context.previousId && hasOrphanToolResult(body.input)) {
|
|
152
|
+
return Response.json(wireError("previous_response_not_found"), { status: 400 });
|
|
153
|
+
}
|
|
154
|
+
let handshakeHeaders;
|
|
155
|
+
if (!socket) {
|
|
156
|
+
if (context.signal.aborted || this.closed)
|
|
157
|
+
throw new ClientCancellationError();
|
|
158
|
+
if (this.channels.size >= 16) {
|
|
159
|
+
const first = this.channels.keys().next().value;
|
|
160
|
+
if (first) {
|
|
161
|
+
this.channels.get(first)?.terminate();
|
|
162
|
+
this.channels.delete(first);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const name of [
|
|
166
|
+
"connection",
|
|
167
|
+
"upgrade",
|
|
168
|
+
"content-length",
|
|
169
|
+
"content-type",
|
|
170
|
+
"content-encoding",
|
|
171
|
+
"host",
|
|
172
|
+
])
|
|
173
|
+
headers.delete(name);
|
|
174
|
+
this.stats.upstreamConnections++;
|
|
175
|
+
socket = new WebSocket(url, {
|
|
176
|
+
headers: Object.fromEntries(headers),
|
|
177
|
+
followRedirects: false,
|
|
178
|
+
handshakeTimeout: 15000,
|
|
179
|
+
maxPayload: this.maxBytes,
|
|
180
|
+
perMessageDeflate: false,
|
|
181
|
+
});
|
|
182
|
+
const opening = socket;
|
|
183
|
+
const rejected = await new Promise((resolve, reject) => {
|
|
184
|
+
const abort = () => {
|
|
185
|
+
opening.terminate();
|
|
186
|
+
cleanup();
|
|
187
|
+
reject(context.signal.aborted ? new ClientCancellationError() : Error("WebSocket request cancelled"));
|
|
188
|
+
};
|
|
189
|
+
const cleanup = () => {
|
|
190
|
+
init.signal?.removeEventListener("abort", abort);
|
|
191
|
+
context.signal.removeEventListener("abort", abort);
|
|
192
|
+
};
|
|
193
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
194
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
195
|
+
opening.once("upgrade", (response) => {
|
|
196
|
+
handshakeHeaders = upstreamHeaders(response.headers);
|
|
197
|
+
});
|
|
198
|
+
opening.once("open", () => {
|
|
199
|
+
cleanup();
|
|
200
|
+
resolve(undefined);
|
|
201
|
+
});
|
|
202
|
+
opening.once("error", () => {
|
|
203
|
+
cleanup();
|
|
204
|
+
reject(context.signal.aborted ? new ClientCancellationError() : Error("Upstream WebSocket connection failed"));
|
|
205
|
+
});
|
|
206
|
+
opening.once("unexpected-response", (_request, response) => {
|
|
207
|
+
cleanup();
|
|
208
|
+
response.resume();
|
|
209
|
+
const status = response.statusCode ?? 502;
|
|
210
|
+
resolve(Response.json({
|
|
211
|
+
error: {
|
|
212
|
+
code: "websocket_handshake_rejected",
|
|
213
|
+
message: "Upstream rejected the WebSocket handshake.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
// Retry-After and x-codex-* quota headers drive cooldowns.
|
|
217
|
+
{ status: status >= 400 && status <= 599 ? status : 502, headers: upstreamHeaders(response.headers) }));
|
|
218
|
+
opening.terminate();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
if (rejected) {
|
|
222
|
+
if (!apiCredential)
|
|
223
|
+
return rejected;
|
|
224
|
+
this.httpOnly.add(key);
|
|
225
|
+
return this.httpFetch(input, init);
|
|
226
|
+
}
|
|
227
|
+
this.channels.set(key, socket);
|
|
228
|
+
}
|
|
229
|
+
const active = socket;
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
let accepted = false, finished = false;
|
|
232
|
+
let streamController;
|
|
233
|
+
const stream = new ReadableStream({
|
|
234
|
+
start(controller) {
|
|
235
|
+
streamController = controller;
|
|
236
|
+
},
|
|
237
|
+
cancel() {
|
|
238
|
+
active.terminate();
|
|
239
|
+
},
|
|
240
|
+
}, { highWaterMark: this.maxBytes, size: (chunk) => chunk.byteLength });
|
|
241
|
+
const cleanup = () => {
|
|
242
|
+
active.off("message", onMessage);
|
|
243
|
+
active.off("close", onClose);
|
|
244
|
+
active.off("error", onClose);
|
|
245
|
+
init.signal?.removeEventListener("abort", abort);
|
|
246
|
+
context.signal.removeEventListener("abort", abort);
|
|
247
|
+
};
|
|
248
|
+
const onClose = () => {
|
|
249
|
+
if (finished)
|
|
250
|
+
return;
|
|
251
|
+
finished = true;
|
|
252
|
+
cleanup();
|
|
253
|
+
this.channels.delete(key);
|
|
254
|
+
const cancelled = context.signal.aborted || this.closed;
|
|
255
|
+
// Once create has been sent, a disconnect cannot prove generation
|
|
256
|
+
// did not start. Never replay it, even before the first event.
|
|
257
|
+
const error = cancelled
|
|
258
|
+
? new ClientCancellationError()
|
|
259
|
+
: Error("Upstream WebSocket disconnected");
|
|
260
|
+
if (accepted)
|
|
261
|
+
streamController.error(error);
|
|
262
|
+
else if (cancelled)
|
|
263
|
+
reject(error);
|
|
264
|
+
else {
|
|
265
|
+
// A sent create has an ambiguous outcome. Return a terminal
|
|
266
|
+
// stream error so the HTTP router cannot retry generation.
|
|
267
|
+
resolve(new Response(`data: ${JSON.stringify(wireError("upstream_websocket_disconnected", 502))}\n\n`, {
|
|
268
|
+
headers: { "content-type": "text/event-stream" },
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const abort = () => active.terminate();
|
|
273
|
+
const onMessage = (raw, isBinary) => {
|
|
274
|
+
try {
|
|
275
|
+
if (isBinary)
|
|
276
|
+
throw Error("Unexpected binary response");
|
|
277
|
+
const text = raw.toString();
|
|
278
|
+
const event = JSON.parse(text);
|
|
279
|
+
if (!isRecord(event) || typeof event.type !== "string")
|
|
280
|
+
throw Error("Invalid response event");
|
|
281
|
+
if (event.type === "error" && !accepted) {
|
|
282
|
+
finished = true;
|
|
283
|
+
cleanup();
|
|
284
|
+
const status = typeof event.status === "number" &&
|
|
285
|
+
event.status >= 400 &&
|
|
286
|
+
event.status <= 599
|
|
287
|
+
? event.status
|
|
288
|
+
: 400;
|
|
289
|
+
resolve(Response.json(event, { status, headers: upstreamHeaders(event.headers) }));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!accepted) {
|
|
293
|
+
accepted = true;
|
|
294
|
+
// Handshake headers describe the account only at connect time,
|
|
295
|
+
// so they ride on the first response of a fresh socket.
|
|
296
|
+
resolve(new Response(stream, {
|
|
297
|
+
headers: { ...handshakeHeaders, "content-type": "text/event-stream" },
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
if ((streamController.desiredSize ?? 0) < -this.maxBytes)
|
|
301
|
+
throw Error("Upstream buffer exceeded");
|
|
302
|
+
streamController.enqueue(Buffer.from(`data: ${text}\n\n`));
|
|
303
|
+
if (isRecord(event.response) &&
|
|
304
|
+
typeof event.response.id === "string") {
|
|
305
|
+
this.owners.set(event.response.id, active);
|
|
306
|
+
if (this.owners.size > 64) {
|
|
307
|
+
const first = this.owners.keys().next().value;
|
|
308
|
+
if (first)
|
|
309
|
+
this.owners.delete(first);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (terminal.has(event.type)) {
|
|
313
|
+
finished = true;
|
|
314
|
+
cleanup();
|
|
315
|
+
streamController.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
active.terminate();
|
|
320
|
+
onClose();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
active.on("message", onMessage);
|
|
324
|
+
active.once("close", onClose);
|
|
325
|
+
active.once("error", onClose);
|
|
326
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
327
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
328
|
+
if (init.signal?.aborted || context.signal.aborted) {
|
|
329
|
+
abort();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.stats.upstreamRequests++;
|
|
333
|
+
active.send(JSON.stringify(body), (error) => {
|
|
334
|
+
if (error)
|
|
335
|
+
onClose();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** Adapts only the transport; authenticated HTTP routing remains the policy authority. */
|
|
341
|
+
export class ResponsesWebSocketGateway {
|
|
342
|
+
upstreamFetch;
|
|
343
|
+
options;
|
|
344
|
+
stats = {
|
|
345
|
+
connections: 0,
|
|
346
|
+
upstreamConnections: 0,
|
|
347
|
+
upstreamRequests: 0,
|
|
348
|
+
};
|
|
349
|
+
contexts = new Map();
|
|
350
|
+
scope = new AsyncLocalStorage();
|
|
351
|
+
sessions = new Set();
|
|
352
|
+
wss;
|
|
353
|
+
maxBytes;
|
|
354
|
+
/** Loopback hops must never go through an environment HTTP proxy (NODE_USE_ENV_PROXY). */
|
|
355
|
+
loopback = new Agent();
|
|
356
|
+
loopbackFetch = ((input, init) => undiciFetch(input, { ...init, dispatcher: this.loopback }));
|
|
357
|
+
constructor(upstreamFetch, options) {
|
|
358
|
+
this.upstreamFetch = upstreamFetch;
|
|
359
|
+
this.options = options;
|
|
360
|
+
this.maxBytes = options.maxPayloadBytes;
|
|
361
|
+
this.wss = new WebSocketServer({
|
|
362
|
+
noServer: true,
|
|
363
|
+
maxPayload: this.maxBytes,
|
|
364
|
+
perMessageDeflate: false,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
fetch = async (input, init) => {
|
|
368
|
+
const context = this.scope.getStore();
|
|
369
|
+
if (context &&
|
|
370
|
+
init?.method === "POST" &&
|
|
371
|
+
new URL(String(input)).pathname.endsWith("/responses"))
|
|
372
|
+
return context.session.fetch(input, init, context);
|
|
373
|
+
return this.upstreamFetch(input, init);
|
|
374
|
+
};
|
|
375
|
+
handle(req, handler) {
|
|
376
|
+
const id = req.headers[CONTEXT_HEADER];
|
|
377
|
+
delete req.headers[CONTEXT_HEADER];
|
|
378
|
+
const context = typeof id === "string" ? this.contexts.get(id) : undefined;
|
|
379
|
+
if (typeof id === "string")
|
|
380
|
+
this.contexts.delete(id);
|
|
381
|
+
if (context)
|
|
382
|
+
this.scope.run(context, () => {
|
|
383
|
+
void handler();
|
|
384
|
+
});
|
|
385
|
+
else
|
|
386
|
+
void handler();
|
|
387
|
+
}
|
|
388
|
+
attach(server, baseUrl) {
|
|
389
|
+
server.on("upgrade", (req, socket, head) => {
|
|
390
|
+
// Node relinquishes its socket error handler before emitting upgrade.
|
|
391
|
+
socket.on("error", () => socket.destroy());
|
|
392
|
+
const reject = (status) => {
|
|
393
|
+
if (socket.destroyed)
|
|
394
|
+
return;
|
|
395
|
+
socket.end(`HTTP/1.1 ${status} Rejected\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
396
|
+
};
|
|
397
|
+
void (async () => {
|
|
398
|
+
const credentials = {};
|
|
399
|
+
for (const name of ["authorization", "x-api-key"]) {
|
|
400
|
+
const value = req.headers[name];
|
|
401
|
+
if (typeof value === "string")
|
|
402
|
+
credentials[name] = value;
|
|
403
|
+
}
|
|
404
|
+
// An authenticated unknown HTTP path returns 404; all unknown credentials return 401.
|
|
405
|
+
const auth = await this.loopbackFetch(`${baseUrl}/__websocket_auth_check__`, {
|
|
406
|
+
headers: credentials,
|
|
407
|
+
signal: AbortSignal.timeout(5000),
|
|
408
|
+
});
|
|
409
|
+
await auth.body?.cancel();
|
|
410
|
+
if (auth.status !== 404) {
|
|
411
|
+
// Only a real 401 means bad credentials; a storage or policy outage is
|
|
412
|
+
// transient and must not make the native client distrust its login.
|
|
413
|
+
reject(auth.status === 401 ? 401 : auth.status >= 500 ? 503 : auth.status);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (req.headers.origin) {
|
|
417
|
+
reject(403);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (await this.options.upgrade?.(req, socket, head))
|
|
421
|
+
return;
|
|
422
|
+
const path = new URL(req.url ?? "/", baseUrl).pathname;
|
|
423
|
+
if (![
|
|
424
|
+
"/responses",
|
|
425
|
+
"/v1/responses",
|
|
426
|
+
"/backend-api/codex/responses",
|
|
427
|
+
].includes(path)) {
|
|
428
|
+
reject(404);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (req.headers.origin || this.sessions.size >= 64) {
|
|
432
|
+
reject(403);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (socket.destroyed)
|
|
436
|
+
return;
|
|
437
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => this.serve(ws, req, baseUrl));
|
|
438
|
+
})().catch(() => reject(503));
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
serve(ws, req, baseUrl) {
|
|
442
|
+
this.stats.connections++;
|
|
443
|
+
const session = new SocketSession(this.maxBytes, this.stats, this.upstreamFetch);
|
|
444
|
+
this.sessions.add(session);
|
|
445
|
+
let queue = Promise.resolve(), queued = 0, queuedBytes = 0;
|
|
446
|
+
let generation = 0;
|
|
447
|
+
const send = (event) => {
|
|
448
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
449
|
+
return;
|
|
450
|
+
if (ws.bufferedAmount > this.maxBytes) {
|
|
451
|
+
ws.terminate();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
ws.send(JSON.stringify(event));
|
|
455
|
+
};
|
|
456
|
+
ws.on("error", () => ws.terminate());
|
|
457
|
+
ws.once("close", () => {
|
|
458
|
+
session.close();
|
|
459
|
+
this.sessions.delete(session);
|
|
460
|
+
});
|
|
461
|
+
ws.on("message", (raw, isBinary) => {
|
|
462
|
+
let event;
|
|
463
|
+
try {
|
|
464
|
+
const parsed = JSON.parse(raw.toString());
|
|
465
|
+
if (isBinary || !isRecord(parsed))
|
|
466
|
+
throw Error();
|
|
467
|
+
event = parsed;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
send(wireError("invalid_websocket_event"));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (event.type === "response.cancel") {
|
|
474
|
+
// Invalidate work accepted before cancellation, including queued creates.
|
|
475
|
+
generation++;
|
|
476
|
+
session.controller?.abort();
|
|
477
|
+
for (const channel of session.channels.values())
|
|
478
|
+
channel.terminate();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (event.type !== "response.create") {
|
|
482
|
+
send(wireError("unsupported_websocket_event"));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const eventBytes = Buffer.byteLength(raw.toString());
|
|
486
|
+
if (++queued > 16 || queuedBytes + eventBytes > this.maxBytes) {
|
|
487
|
+
queued--;
|
|
488
|
+
send(wireError("websocket_queue_full", 429));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
queuedBytes += eventBytes;
|
|
492
|
+
const acceptedGeneration = generation;
|
|
493
|
+
queue = queue
|
|
494
|
+
.then(async () => {
|
|
495
|
+
if (session.closed || acceptedGeneration !== generation)
|
|
496
|
+
return;
|
|
497
|
+
try {
|
|
498
|
+
if (typeof event.model !== "string" || !Array.isArray(event.input))
|
|
499
|
+
throw Error("invalid_request_body");
|
|
500
|
+
const routingHeaders = new Headers();
|
|
501
|
+
for (const [key, value] of Object.entries(req.headers))
|
|
502
|
+
if (typeof value === "string")
|
|
503
|
+
routingHeaders.set(key, value);
|
|
504
|
+
event.model =
|
|
505
|
+
this.options.routeModel?.(event.model, routingHeaders) ??
|
|
506
|
+
event.model;
|
|
507
|
+
const pool = parseModelRoute(event.model).kind;
|
|
508
|
+
const previousId = typeof event.previous_response_id === "string"
|
|
509
|
+
? event.previous_response_id
|
|
510
|
+
: undefined;
|
|
511
|
+
const prior = previousId
|
|
512
|
+
? session.chains.get(previousId)
|
|
513
|
+
: undefined;
|
|
514
|
+
if (previousId && !prior) {
|
|
515
|
+
send(wireError("previous_response_not_found"));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (prior && prior.pool !== pool) {
|
|
519
|
+
send(wireError("model_route_pool_mismatch"));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const body = {
|
|
523
|
+
...prior?.body,
|
|
524
|
+
...event,
|
|
525
|
+
input: prior
|
|
526
|
+
? [
|
|
527
|
+
...(Array.isArray(prior.body.input)
|
|
528
|
+
? prior.body.input
|
|
529
|
+
: []),
|
|
530
|
+
...prior.output,
|
|
531
|
+
...event.input,
|
|
532
|
+
]
|
|
533
|
+
: event.input,
|
|
534
|
+
stream: true,
|
|
535
|
+
store: false,
|
|
536
|
+
};
|
|
537
|
+
delete body.type;
|
|
538
|
+
delete body.previous_response_id;
|
|
539
|
+
// Native prewarm sets generate=false; omission on the next create
|
|
540
|
+
// means generate normally, not inherit the warm-up's control flag.
|
|
541
|
+
if (event.generate === undefined)
|
|
542
|
+
delete body.generate;
|
|
543
|
+
if (Buffer.byteLength(JSON.stringify(body)) > this.maxBytes) {
|
|
544
|
+
send(wireError("websocket_context_too_large", 413));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const id = randomUUID();
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
session.controller = controller;
|
|
550
|
+
const clientBeta = req.headers["openai-beta"];
|
|
551
|
+
this.contexts.set(id, { session, previousId, delta: event.input, signal: controller.signal, clientBeta: typeof clientBeta === "string" ? clientBeta : undefined });
|
|
552
|
+
const headers = new Headers();
|
|
553
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
554
|
+
if (typeof value === "string" &&
|
|
555
|
+
!/^(host|connection|upgrade|sec-websocket-|content-|origin)/i.test(key))
|
|
556
|
+
headers.set(key, value);
|
|
557
|
+
}
|
|
558
|
+
headers.set(CONTEXT_HEADER, id);
|
|
559
|
+
headers.set("content-type", "application/json");
|
|
560
|
+
try {
|
|
561
|
+
const response = await this.loopbackFetch(`${baseUrl}/responses`, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers,
|
|
564
|
+
body: JSON.stringify(body),
|
|
565
|
+
signal: controller.signal,
|
|
566
|
+
});
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
// An empty or non-JSON error body must not turn a real 401 into a 502.
|
|
569
|
+
const data = await response.json().catch(() => undefined);
|
|
570
|
+
send(isRecord(data)
|
|
571
|
+
? { ...data, type: "error", status: response.status }
|
|
572
|
+
: wireError("websocket_request_failed", response.status));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (!response.body)
|
|
576
|
+
throw Error("missing_response_body");
|
|
577
|
+
let buffer = "";
|
|
578
|
+
const outputHistory = new ResponseOutputHistory(this.maxBytes);
|
|
579
|
+
const decoder = new TextDecoder();
|
|
580
|
+
for await (const chunk of response.body) {
|
|
581
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
582
|
+
if (Buffer.byteLength(buffer) > this.maxBytes)
|
|
583
|
+
throw Error("websocket_event_too_large");
|
|
584
|
+
let index;
|
|
585
|
+
while ((index = buffer.indexOf("\n\n")) >= 0) {
|
|
586
|
+
const frame = buffer.slice(0, index);
|
|
587
|
+
buffer = buffer.slice(index + 2);
|
|
588
|
+
const data = frame
|
|
589
|
+
.split("\n")
|
|
590
|
+
.filter((line) => line.startsWith("data:"))
|
|
591
|
+
.map((line) => line.slice(5).trimStart())
|
|
592
|
+
.join("\n");
|
|
593
|
+
if (!data || data === "[DONE]")
|
|
594
|
+
continue;
|
|
595
|
+
const parsed = JSON.parse(data);
|
|
596
|
+
if (!isRecord(parsed))
|
|
597
|
+
throw Error("invalid_response_event");
|
|
598
|
+
outputHistory.observe(parsed);
|
|
599
|
+
if (parsed.type === "response.completed" &&
|
|
600
|
+
isRecord(parsed.response) &&
|
|
601
|
+
typeof parsed.response.id === "string") {
|
|
602
|
+
const output = outputHistory.finish(parsed.response.output);
|
|
603
|
+
if (output)
|
|
604
|
+
session.remember(parsed.response.id, body, output, pool);
|
|
605
|
+
}
|
|
606
|
+
send(parsed);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
this.contexts.delete(id);
|
|
612
|
+
if (session.controller === controller)
|
|
613
|
+
session.controller = undefined;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
if (!session.closed)
|
|
618
|
+
send(wireError("websocket_request_failed", 502));
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
.finally(() => {
|
|
622
|
+
queued--;
|
|
623
|
+
queuedBytes -= eventBytes;
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
close() {
|
|
628
|
+
for (const ws of this.wss.clients)
|
|
629
|
+
ws.terminate();
|
|
630
|
+
for (const session of this.sessions)
|
|
631
|
+
session.close();
|
|
632
|
+
this.contexts.clear();
|
|
633
|
+
this.wss.close();
|
|
634
|
+
void this.loopback.close().catch(() => undefined);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type MenuItem } from "../ui/select.js";
|
|
2
|
+
export interface ResumeThread {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string | null;
|
|
5
|
+
preview?: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
modelProvider?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ResumePage {
|
|
10
|
+
data: ResumeThread[];
|
|
11
|
+
nextCursor?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ResumePickerOptions {
|
|
14
|
+
codexBin: {
|
|
15
|
+
path: string;
|
|
16
|
+
launchWithNode?: boolean;
|
|
17
|
+
};
|
|
18
|
+
cwd: string;
|
|
19
|
+
configArgs: string[];
|
|
20
|
+
showAll: boolean;
|
|
21
|
+
includeNonInteractive: boolean;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Read the canonical index through Codex, rather than depending on SQLite schemas. */
|
|
26
|
+
export declare function createResumeCatalog(options: ResumePickerOptions): {
|
|
27
|
+
page(cursor?: string | null): Promise<ResumePage>;
|
|
28
|
+
close(): void;
|
|
29
|
+
};
|
|
30
|
+
type Choice = {
|
|
31
|
+
kind: "thread";
|
|
32
|
+
id: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "next" | "previous" | "cancel";
|
|
35
|
+
};
|
|
36
|
+
export declare function pickResumeThread(options: ResumePickerOptions, deps?: {
|
|
37
|
+
createCatalog: typeof createResumeCatalog;
|
|
38
|
+
select: (items: MenuItem<Choice>[], options: import("../ui/select.js").SelectOptions<Choice>) => Promise<Choice | null>;
|
|
39
|
+
}): Promise<string | null>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=resume-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-picker.d.ts","sourceRoot":"","sources":["../../../lib/runtime/resume-picker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB;kBAsD3C,MAAM,GAAG,IAAI,GAAU,OAAO,CAAC,UAAU,CAAC;;EAyC9D;AAOD,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CAAE,CAAC;AAExF,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,mBAAmB,EAC5B,IAAI;;;CAAiE,GACnE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkDxB"}
|