codex-copilot-dx 0.9.16 → 0.9.18
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/README.md +2 -0
- package/bin/cli.mjs +10 -5
- package/docs/REFERENCE.md +7 -1
- package/package.json +1 -1
- package/src/adapter.mjs +5 -6
- package/src/auth-status.mjs +175 -0
- package/src/cli-auth.mjs +2 -180
- package/src/cli-models.mjs +1 -131
- package/src/cli-table.mjs +3 -11
- package/src/copilot-responses-compat.mjs +1 -1
- package/src/dashboard-api.mjs +4 -4
- package/src/dashboard.mjs +1 -1
- package/src/http-errors.mjs +5 -0
- package/src/http-transport.mjs +6 -350
- package/src/image-mcp.mjs +33 -1
- package/src/image-provider.mjs +34 -17
- package/src/live-models.mjs +137 -0
- package/src/observability.mjs +2 -22
- package/src/request-admission.mjs +348 -0
- package/src/response-failures.mjs +1 -1
- package/src/response-history.mjs +1 -1
- package/src/responses-bridge.mjs +14 -1
- package/src/responses-compaction.mjs +1 -1
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +2 -2
- package/src/security.mjs +22 -0
- package/src/terminal-text.mjs +11 -0
- package/src/usage-analytics.mjs +1 -1
- package/src/usage-store.mjs +369 -0
- package/src/usage.mjs +17 -368
package/src/observability.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isIP } from "node:net";
|
|
2
1
|
import { ADAPTER_STATUS_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
3
2
|
import { copilotRuntimeStatus } from "./copilot.mjs";
|
|
4
3
|
import { imageOptimizationStats } from "./image-optimization.mjs";
|
|
@@ -6,10 +5,11 @@ import { responseHistoryStats } from "./response-history.mjs";
|
|
|
6
5
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
6
|
import { decodedBodyLimitPreference } from "./user-settings.mjs";
|
|
8
7
|
import { profileRouting } from "./profile-routing.mjs";
|
|
9
|
-
import { usageLoggingStats } from "./usage.mjs";
|
|
8
|
+
import { usageLoggingStats } from "./usage-store.mjs";
|
|
10
9
|
import { debugLoggingStats } from "./log.mjs";
|
|
11
10
|
|
|
12
11
|
export { ADAPTER_STATUS_PATH };
|
|
12
|
+
export { isLoopbackAddress, isLoopbackHostHeader } from "./security.mjs";
|
|
13
13
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
14
14
|
const OBSERVABILITY_DECODED_BODY_BYTES = decodedBodyLimitPreference().bytes;
|
|
15
15
|
|
|
@@ -103,26 +103,6 @@ export function classifyAdapterRoute(method, pathname) {
|
|
|
103
103
|
return "not_found";
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export function isLoopbackAddress(address) {
|
|
107
|
-
const normalized = String(address || "").trim().toLowerCase();
|
|
108
|
-
if (normalized === "::1" || normalized === "[::1]") return true;
|
|
109
|
-
const ipv4 = normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
|
|
110
|
-
return isIP(ipv4) === 4 && ipv4.startsWith("127.");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function isLoopbackHostHeader(host) {
|
|
114
|
-
if (host === undefined) return true; // HTTP/1.0 and in-process callers may omit Host.
|
|
115
|
-
if (typeof host !== "string" || !host || host.includes(",")) return false;
|
|
116
|
-
try {
|
|
117
|
-
const url = new URL(`http://${host}`);
|
|
118
|
-
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
119
|
-
return !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash
|
|
120
|
-
&& (hostname === "localhost" || isLoopbackAddress(hostname));
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
106
|
function modelRegistryStatus(modelRegistry) {
|
|
127
107
|
const modelData = Array.isArray(modelRegistry?.models)
|
|
128
108
|
? modelRegistry.models
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
2
|
+
import { httpError } from "./http-errors.mjs";
|
|
3
|
+
|
|
4
|
+
const HTTP_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
5
|
+
const MAX_INFLIGHT_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxInflightBodyBytes;
|
|
6
|
+
const MAX_QUEUED_REQUESTS = HTTP_RUNTIME_CONFIG.maxQueuedRequests;
|
|
7
|
+
const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
|
|
8
|
+
const COMPRESSED_BODY_WEIGHT_MULTIPLIER = 4;
|
|
9
|
+
|
|
10
|
+
export function contentEncodings(contentEncoding) {
|
|
11
|
+
return String(contentEncoding || "identity")
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((value) => value.trim().toLowerCase())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function requestContentLength(req) {
|
|
18
|
+
return Number.parseInt(req.headers?.["content-length"] || "", 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requestAdmissionWeight(req, maxBytes) {
|
|
22
|
+
const encodings = contentEncodings(req.headers?.["content-encoding"]);
|
|
23
|
+
const contentLength = requestContentLength(req);
|
|
24
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) return maxBytes;
|
|
25
|
+
const compressed = encodings.some((encoding) => encoding !== "identity");
|
|
26
|
+
const weightedLength = compressed ? contentLength * COMPRESSED_BODY_WEIGHT_MULTIPLIER : contentLength;
|
|
27
|
+
return Math.max(1, Math.min(weightedLength, maxBytes));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function admissionAbortError(signal) {
|
|
31
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
32
|
+
const error = new Error("The operation was aborted");
|
|
33
|
+
error.name = "AbortError";
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label, blockBehindExclusive = false }) {
|
|
38
|
+
const queue = [];
|
|
39
|
+
let activeWeight = 0;
|
|
40
|
+
let activeRequestedWeight = 0;
|
|
41
|
+
let activeRequests = 0;
|
|
42
|
+
|
|
43
|
+
const activate = (entry) => {
|
|
44
|
+
entry.cleanup();
|
|
45
|
+
activeWeight += entry.weight;
|
|
46
|
+
activeRequestedWeight += entry.requestedWeight;
|
|
47
|
+
activeRequests += 1;
|
|
48
|
+
let released = false;
|
|
49
|
+
const release = () => {
|
|
50
|
+
if (released) return;
|
|
51
|
+
released = true;
|
|
52
|
+
activeWeight = Math.max(0, activeWeight - entry.weight);
|
|
53
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight - entry.requestedWeight);
|
|
54
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
55
|
+
drain();
|
|
56
|
+
};
|
|
57
|
+
release.resize = (requestedWeight) => {
|
|
58
|
+
if (released) return 0;
|
|
59
|
+
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
60
|
+
? Math.ceil(requestedWeight)
|
|
61
|
+
: maxWeight;
|
|
62
|
+
const requested = Math.max(1, parsedWeight);
|
|
63
|
+
const weight = Math.min(requested, maxWeight);
|
|
64
|
+
if (weight > entry.weight) {
|
|
65
|
+
throw new RangeError(`${label} admission weight cannot grow`);
|
|
66
|
+
}
|
|
67
|
+
activeWeight = Math.max(0, activeWeight - (entry.weight - weight));
|
|
68
|
+
activeRequestedWeight = Math.max(0, activeRequestedWeight + requested - entry.requestedWeight);
|
|
69
|
+
entry.weight = weight;
|
|
70
|
+
entry.requestedWeight = requested;
|
|
71
|
+
drain();
|
|
72
|
+
return weight;
|
|
73
|
+
};
|
|
74
|
+
Object.defineProperties(release, {
|
|
75
|
+
weight: { get: () => released ? 0 : entry.weight },
|
|
76
|
+
requestedWeight: { get: () => released ? 0 : entry.requestedWeight },
|
|
77
|
+
maxWeight: { value: maxWeight },
|
|
78
|
+
});
|
|
79
|
+
entry.resolve(release);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const drain = () => {
|
|
83
|
+
for (let index = 0; index < queue.length;) {
|
|
84
|
+
const entry = queue[index];
|
|
85
|
+
if (activeWeight + entry.weight > maxWeight) {
|
|
86
|
+
if (blockBehindExclusive && entry.weight === maxWeight) break;
|
|
87
|
+
index += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
queue.splice(index, 1);
|
|
91
|
+
activate(entry);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const acquire = (requestedWeight, { signal } = {}) => {
|
|
96
|
+
if (signal?.aborted) return Promise.reject(admissionAbortError(signal));
|
|
97
|
+
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
98
|
+
? Math.ceil(requestedWeight)
|
|
99
|
+
: maxWeight;
|
|
100
|
+
const requested = Math.max(1, parsedWeight);
|
|
101
|
+
const weight = Math.min(requested, maxWeight);
|
|
102
|
+
const blockedByExclusive = blockBehindExclusive
|
|
103
|
+
&& queue.some((entry) => entry.weight === maxWeight);
|
|
104
|
+
const mustWait = blockedByExclusive || activeWeight + weight > maxWeight;
|
|
105
|
+
if (queue.length >= maxQueued && mustWait) {
|
|
106
|
+
return Promise.reject(httpError(`${label} queue is full (${maxQueued} waiting)`, 503));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
let timer;
|
|
111
|
+
const entry = {
|
|
112
|
+
weight,
|
|
113
|
+
requestedWeight: requested,
|
|
114
|
+
resolve,
|
|
115
|
+
cleanup: () => {
|
|
116
|
+
if (timer) clearTimeout(timer);
|
|
117
|
+
signal?.removeEventListener("abort", onAbort);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const remove = () => {
|
|
121
|
+
const index = queue.indexOf(entry);
|
|
122
|
+
if (index >= 0) queue.splice(index, 1);
|
|
123
|
+
};
|
|
124
|
+
const fail = (error) => {
|
|
125
|
+
entry.cleanup();
|
|
126
|
+
remove();
|
|
127
|
+
reject(error);
|
|
128
|
+
drain();
|
|
129
|
+
};
|
|
130
|
+
const onAbort = () => fail(admissionAbortError(signal));
|
|
131
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
132
|
+
timer = setTimeout(() => {
|
|
133
|
+
fail(httpError(`${label} admission timed out after ${waitTimeoutMs}ms`, 503));
|
|
134
|
+
}, waitTimeoutMs);
|
|
135
|
+
timer.unref?.();
|
|
136
|
+
queue.push(entry);
|
|
137
|
+
if (signal?.aborted) onAbort();
|
|
138
|
+
else drain();
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
acquire.stats = () => ({
|
|
143
|
+
activeWeight,
|
|
144
|
+
activeRequestedWeight,
|
|
145
|
+
activeRequests,
|
|
146
|
+
queued: queue.length,
|
|
147
|
+
maxWeight,
|
|
148
|
+
});
|
|
149
|
+
return acquire;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createRequestAdmission({
|
|
153
|
+
maxBytes = MAX_INFLIGHT_BODY_BYTES,
|
|
154
|
+
maxQueued = MAX_QUEUED_REQUESTS,
|
|
155
|
+
waitTimeoutMs = REQUEST_QUEUE_TIMEOUT_MS,
|
|
156
|
+
} = {}) {
|
|
157
|
+
const byteLimit = parsePositiveInteger(maxBytes, MAX_INFLIGHT_BODY_BYTES);
|
|
158
|
+
const queueLimit = parsePositiveInteger(maxQueued, MAX_QUEUED_REQUESTS);
|
|
159
|
+
const timeoutMs = parsePositiveInteger(waitTimeoutMs, REQUEST_QUEUE_TIMEOUT_MS);
|
|
160
|
+
const acquireDecompression = createSupplementalAdmission({
|
|
161
|
+
maxWeight: 1,
|
|
162
|
+
maxQueued: queueLimit,
|
|
163
|
+
waitTimeoutMs: timeoutMs,
|
|
164
|
+
label: "Request decompression",
|
|
165
|
+
});
|
|
166
|
+
const acquireDecodedBody = createSupplementalAdmission({
|
|
167
|
+
maxWeight: byteLimit,
|
|
168
|
+
maxQueued: queueLimit,
|
|
169
|
+
waitTimeoutMs: timeoutMs,
|
|
170
|
+
label: "Decoded request body",
|
|
171
|
+
blockBehindExclusive: true,
|
|
172
|
+
});
|
|
173
|
+
const acquireResponseHistory = createSupplementalAdmission({
|
|
174
|
+
maxWeight: byteLimit,
|
|
175
|
+
maxQueued: queueLimit,
|
|
176
|
+
waitTimeoutMs: timeoutMs,
|
|
177
|
+
label: "Response history",
|
|
178
|
+
});
|
|
179
|
+
const queue = [];
|
|
180
|
+
let activeBytes = 0;
|
|
181
|
+
let activeRequests = 0;
|
|
182
|
+
const counters = {
|
|
183
|
+
total: 0,
|
|
184
|
+
activated: 0,
|
|
185
|
+
queued: 0,
|
|
186
|
+
rejected: 0,
|
|
187
|
+
timedOut: 0,
|
|
188
|
+
aborted: 0,
|
|
189
|
+
waitMsTotal: 0,
|
|
190
|
+
waitMsMax: 0,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const remove = (entry) => {
|
|
194
|
+
const index = queue.indexOf(entry);
|
|
195
|
+
if (index >= 0) queue.splice(index, 1);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const activate = (entry) => {
|
|
199
|
+
entry.cleanup();
|
|
200
|
+
activeBytes += entry.weight;
|
|
201
|
+
activeRequests += 1;
|
|
202
|
+
counters.activated += 1;
|
|
203
|
+
const waitMs = Math.max(0, Date.now() - entry.startedAt);
|
|
204
|
+
counters.waitMsTotal += waitMs;
|
|
205
|
+
counters.waitMsMax = Math.max(counters.waitMsMax, waitMs);
|
|
206
|
+
let released = false;
|
|
207
|
+
const supplementalReleases = new Set();
|
|
208
|
+
const release = () => {
|
|
209
|
+
if (released) return;
|
|
210
|
+
released = true;
|
|
211
|
+
for (const releaseSupplemental of supplementalReleases) releaseSupplemental();
|
|
212
|
+
supplementalReleases.clear();
|
|
213
|
+
activeBytes = Math.max(0, activeBytes - entry.weight);
|
|
214
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
215
|
+
drain();
|
|
216
|
+
};
|
|
217
|
+
const reserve = async (gate, weight, options) => {
|
|
218
|
+
const releaseSupplemental = await gate(weight, options);
|
|
219
|
+
if (released) {
|
|
220
|
+
releaseSupplemental();
|
|
221
|
+
throw admissionAbortError(options?.signal);
|
|
222
|
+
}
|
|
223
|
+
let supplementalReleased = false;
|
|
224
|
+
const releaseReservation = () => {
|
|
225
|
+
if (supplementalReleased) return;
|
|
226
|
+
supplementalReleased = true;
|
|
227
|
+
supplementalReleases.delete(releaseReservation);
|
|
228
|
+
releaseSupplemental();
|
|
229
|
+
};
|
|
230
|
+
releaseReservation.resize = (nextWeight) => {
|
|
231
|
+
if (supplementalReleased) return 0;
|
|
232
|
+
return releaseSupplemental.resize(nextWeight);
|
|
233
|
+
};
|
|
234
|
+
Object.defineProperties(releaseReservation, {
|
|
235
|
+
weight: { get: () => supplementalReleased ? 0 : releaseSupplemental.weight },
|
|
236
|
+
requestedWeight: {
|
|
237
|
+
get: () => supplementalReleased ? 0 : releaseSupplemental.requestedWeight,
|
|
238
|
+
},
|
|
239
|
+
maxWeight: { value: releaseSupplemental.maxWeight },
|
|
240
|
+
});
|
|
241
|
+
supplementalReleases.add(releaseReservation);
|
|
242
|
+
return releaseReservation;
|
|
243
|
+
};
|
|
244
|
+
release.acquireDecompression = (options) => acquireDecompression(1, options);
|
|
245
|
+
release.reserveDecodedBody = (bytes, options) => reserve(acquireDecodedBody, bytes, options);
|
|
246
|
+
release.reserveDecodedBody.supportsResize = true;
|
|
247
|
+
release.reserveResponseHistory = (bytes, options) => reserve(acquireResponseHistory, bytes, options);
|
|
248
|
+
entry.resolve(release);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const drain = () => {
|
|
252
|
+
for (let index = 0; index < queue.length;) {
|
|
253
|
+
const entry = queue[index];
|
|
254
|
+
if (entry.cancelled) {
|
|
255
|
+
queue.splice(index, 1);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (activeBytes + entry.weight > byteLimit) {
|
|
259
|
+
// Unknown-length requests reserve the whole budget. Once one is waiting,
|
|
260
|
+
// do not let later arrivals keep extending its wait indefinitely.
|
|
261
|
+
if (entry.weight === byteLimit) break;
|
|
262
|
+
index += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
queue.splice(index, 1);
|
|
266
|
+
activate(entry);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const acquire = (req, { signal } = {}) => {
|
|
271
|
+
counters.total += 1;
|
|
272
|
+
if (signal?.aborted) {
|
|
273
|
+
counters.aborted += 1;
|
|
274
|
+
return Promise.reject(admissionAbortError(signal));
|
|
275
|
+
}
|
|
276
|
+
const weight = requestAdmissionWeight(req, byteLimit);
|
|
277
|
+
const blockedByExclusive = queue.some((entry) => !entry.cancelled && entry.weight === byteLimit);
|
|
278
|
+
const mustWait = blockedByExclusive || activeBytes + weight > byteLimit;
|
|
279
|
+
if (queue.length >= queueLimit && mustWait) {
|
|
280
|
+
counters.rejected += 1;
|
|
281
|
+
return Promise.reject(httpError(`Request queue is full (${queueLimit} waiting)`, 503));
|
|
282
|
+
}
|
|
283
|
+
if (mustWait) counters.queued += 1;
|
|
284
|
+
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
let timer;
|
|
287
|
+
const entry = {
|
|
288
|
+
cancelled: false,
|
|
289
|
+
startedAt: Date.now(),
|
|
290
|
+
weight,
|
|
291
|
+
resolve,
|
|
292
|
+
cleanup: () => {
|
|
293
|
+
if (timer) clearTimeout(timer);
|
|
294
|
+
signal?.removeEventListener("abort", onAbort);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
const cancel = (error, reason) => {
|
|
298
|
+
if (entry.cancelled) return;
|
|
299
|
+
entry.cancelled = true;
|
|
300
|
+
if (reason === "aborted") counters.aborted += 1;
|
|
301
|
+
if (reason === "timed_out") counters.timedOut += 1;
|
|
302
|
+
entry.cleanup();
|
|
303
|
+
remove(entry);
|
|
304
|
+
reject(error);
|
|
305
|
+
drain();
|
|
306
|
+
};
|
|
307
|
+
const onAbort = () => cancel(admissionAbortError(signal), "aborted");
|
|
308
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
309
|
+
timer = setTimeout(() => {
|
|
310
|
+
cancel(httpError(`Request admission timed out after ${timeoutMs}ms`, 503), "timed_out");
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
timer.unref?.();
|
|
313
|
+
queue.push(entry);
|
|
314
|
+
if (signal?.aborted) onAbort();
|
|
315
|
+
else drain();
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
acquire.stats = () => ({ activeBytes, queued: queue.length, maxBytes: byteLimit });
|
|
320
|
+
acquire.diagnostics = () => ({
|
|
321
|
+
activeBytes,
|
|
322
|
+
activeRequests,
|
|
323
|
+
queued: queue.length,
|
|
324
|
+
maxBytes: byteLimit,
|
|
325
|
+
maxQueued: queueLimit,
|
|
326
|
+
waitTimeoutMs: timeoutMs,
|
|
327
|
+
total: counters.total,
|
|
328
|
+
activated: counters.activated,
|
|
329
|
+
queuedTotal: counters.queued,
|
|
330
|
+
rejected: counters.rejected,
|
|
331
|
+
timedOut: counters.timedOut,
|
|
332
|
+
aborted: counters.aborted,
|
|
333
|
+
waitMsAvg: counters.activated > 0
|
|
334
|
+
? Number((counters.waitMsTotal / counters.activated).toFixed(1))
|
|
335
|
+
: 0,
|
|
336
|
+
waitMsMax: counters.waitMsMax,
|
|
337
|
+
decompressionsActive: acquireDecompression.stats().activeRequests,
|
|
338
|
+
decompressionsQueued: acquireDecompression.stats().queued,
|
|
339
|
+
decodedBodyBytes: acquireDecodedBody.stats().activeRequestedWeight,
|
|
340
|
+
decodedBodyAdmissionBytes: acquireDecodedBody.stats().activeWeight,
|
|
341
|
+
decodedBodiesActive: acquireDecodedBody.stats().activeRequests,
|
|
342
|
+
decodedBodiesQueued: acquireDecodedBody.stats().queued,
|
|
343
|
+
responseHistoryBytes: acquireResponseHistory.stats().activeWeight,
|
|
344
|
+
responseHistoriesActive: acquireResponseHistory.stats().activeRequests,
|
|
345
|
+
responseHistoriesQueued: acquireResponseHistory.stats().queued,
|
|
346
|
+
});
|
|
347
|
+
return acquire;
|
|
348
|
+
}
|
package/src/response-history.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { httpError } from "./http-
|
|
1
|
+
import { httpError } from "./http-errors.mjs";
|
|
2
2
|
import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
|
|
3
3
|
import { loadRuntimeConfig, parsePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
4
4
|
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -233,6 +233,9 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
233
233
|
delete chatReq.max_tokens;
|
|
234
234
|
const requestModel = chatReq.model || "unknown";
|
|
235
235
|
let upstreamReq = withChatStreamUsage(chatReq);
|
|
236
|
+
// Retain names only, not the request's schemas, images or message history.
|
|
237
|
+
const declaredToolNames = new Set((Array.isArray(upstreamReq.tools) ? upstreamReq.tools : [])
|
|
238
|
+
.map((tool) => tool?.function?.name).filter((name) => typeof name === "string" && name));
|
|
236
239
|
let {
|
|
237
240
|
abort,
|
|
238
241
|
bodyText,
|
|
@@ -450,7 +453,17 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
450
453
|
for (const tc of delta.tool_calls) {
|
|
451
454
|
const { tool, created } = await ensureToolCall(tc);
|
|
452
455
|
if (tc.id) tool.callId = tc.id;
|
|
453
|
-
if (!created && tc.function?.name)
|
|
456
|
+
if (!created && tc.function?.name) {
|
|
457
|
+
const name = tc.function.name;
|
|
458
|
+
let completeRepeat = name === tool.name && declaredToolNames.has(name);
|
|
459
|
+
if (completeRepeat) {
|
|
460
|
+
// An overlap may be a real fragment of a longer declared name.
|
|
461
|
+
for (const declared of declaredToolNames) {
|
|
462
|
+
if (declared.startsWith(tool.name + name)) { completeRepeat = false; break; }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!completeRepeat) tool.name += name;
|
|
466
|
+
}
|
|
454
467
|
const argumentDelta = tc.function?.arguments;
|
|
455
468
|
toolArgumentGuard.observe(tool.outputIndex, argumentDelta);
|
|
456
469
|
if (argumentDelta) {
|
|
@@ -38,7 +38,7 @@ import { status } from "./status.mjs";
|
|
|
38
38
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
39
39
|
import { createSseKeepalive } from "./sse-keepalive.mjs";
|
|
40
40
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
41
|
-
import { recordResponsesUsage } from "./usage.mjs";
|
|
41
|
+
import { recordResponsesUsage } from "./usage-store.mjs";
|
|
42
42
|
import {
|
|
43
43
|
markResponseErrorOrigin,
|
|
44
44
|
markResponseModel,
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { responses as copilotResponses } from "./copilot.mjs";
|
|
2
2
|
import {
|
|
3
|
-
httpError,
|
|
4
3
|
logRequestFailure,
|
|
5
4
|
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
6
5
|
MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
sendUpstreamError,
|
|
9
8
|
writeOrDrain,
|
|
10
9
|
} from "./http-transport.mjs";
|
|
10
|
+
import { httpError } from "./http-errors.mjs";
|
|
11
11
|
import {
|
|
12
12
|
hasCopilotResponseRetryPolicy,
|
|
13
13
|
openCopilotResponse,
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
markUpstreamActivity,
|
|
49
49
|
} from "./stream-performance.mjs";
|
|
50
50
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
51
|
-
import { recordResponsesUsage } from "./usage.mjs";
|
|
51
|
+
import { recordResponsesUsage } from "./usage-store.mjs";
|
|
52
52
|
import { status } from "./status.mjs";
|
|
53
53
|
|
|
54
54
|
const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
|
package/src/security.mjs
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
|
|
3
|
+
export function isLoopbackAddress(address) {
|
|
4
|
+
const normalized = String(address || "").trim().toLowerCase();
|
|
5
|
+
if (normalized === "::1" || normalized === "[::1]") return true;
|
|
6
|
+
const ipv4 = normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
|
|
7
|
+
return isIP(ipv4) === 4 && ipv4.startsWith("127.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isLoopbackHostHeader(host) {
|
|
11
|
+
if (host === undefined) return true; // HTTP/1.0 and in-process callers may omit Host.
|
|
12
|
+
if (typeof host !== "string" || !host || host.includes(",")) return false;
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(`http://${host}`);
|
|
15
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
16
|
+
return !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash
|
|
17
|
+
&& (hostname === "localhost" || isLoopbackAddress(hostname));
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
export function isLoopbackHost(host) {
|
|
2
24
|
const normalized = String(host || "").toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
3
25
|
return ["127.0.0.1", "localhost", "::1"].includes(normalized);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
|
|
3
|
+
export function terminalCell(value, { fallback = "—" } = {}) {
|
|
4
|
+
if (value === undefined || value === null) return fallback;
|
|
5
|
+
const text = stripVTControlCharacters(String(value))
|
|
6
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
7
|
+
.replace(/[\u061c\u200b\u200e\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/g, "")
|
|
8
|
+
.replace(/\s+/g, " ")
|
|
9
|
+
.trim();
|
|
10
|
+
return text || fallback;
|
|
11
|
+
}
|
package/src/usage-analytics.mjs
CHANGED