codex-copilot-dx 0.6.0 → 0.6.2

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.
@@ -0,0 +1,523 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ abortErrorStatusCode,
4
+ createRequestAbort,
5
+ createRequestAdmission,
6
+ httpError,
7
+ isAbortLikeError,
8
+ readJsonBody,
9
+ sendJsonError,
10
+ writeOrDrain,
11
+ } from "./http-transport.mjs";
12
+ import { isLoopbackAddress } from "./observability.mjs";
13
+ import { createPmStudioModelRouter } from "./profile-routing.mjs";
14
+ import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
15
+ import { requireUpstreamEventStream } from "./stream-contract.mjs";
16
+ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
17
+
18
+ export const PM_STUDIO_RELAY_PREFIX = "/pm-ccdx";
19
+ const COPILOT_ORIGIN = "https://api.githubcopilot.com";
20
+ const DEFAULT_VALIDATION_TTL_MS = 5 * 60 * 1000;
21
+ const DEFAULT_VALIDATION_CACHE_SIZE = 128;
22
+ const DEFAULT_VALIDATION_CONCURRENCY = 8;
23
+ const DEFAULT_VALIDATION_MAX_FLIGHTS = 32;
24
+ const DEFAULT_VALIDATION_MAX_WAITERS = 64;
25
+ const HOP_BY_HOP_HEADERS = new Set([
26
+ "connection",
27
+ "keep-alive",
28
+ "proxy-authenticate",
29
+ "proxy-authorization",
30
+ "te",
31
+ "trailer",
32
+ "transfer-encoding",
33
+ "upgrade",
34
+ ]);
35
+ const POST_PATHS = new Set([
36
+ "/chat/completions",
37
+ "/responses",
38
+ "/embeddings",
39
+ ]);
40
+
41
+ function localError(statusCode, code, message) {
42
+ const error = httpError(message, statusCode);
43
+ error.jsonBody = { error: { message, type: "invalid_request_error", code } };
44
+ return error;
45
+ }
46
+
47
+ function sendLocalError(res, statusCode, code, message, headers = {}) {
48
+ if (res.destroyed || res.writableEnded) return;
49
+ res.writeHead(statusCode, { "Content-Type": "application/json", ...headers });
50
+ res.end(JSON.stringify({ error: { message, type: "invalid_request_error", code } }));
51
+ }
52
+
53
+ function bearerToken(headers = {}) {
54
+ const value = headers.authorization || headers.Authorization || "";
55
+ const match = /^Bearer\s+([^\s].*)$/i.exec(String(value));
56
+ return match?.[1]?.trim() || "";
57
+ }
58
+
59
+ function headerEntries(headers = {}) {
60
+ if (headers && typeof headers[Symbol.iterator] === "function") return headers;
61
+ return Object.entries(headers);
62
+ }
63
+
64
+ function connectionHeaderNames(headers = {}) {
65
+ const value = headers.connection || headers.Connection || "";
66
+ return new Set(String(value).split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
67
+ }
68
+
69
+ function enterpriseHeaders(inbound, token, bodyText) {
70
+ const headers = new Headers();
71
+ const connectionHeaders = connectionHeaderNames(inbound);
72
+ for (const [rawName, rawValue] of headerEntries(inbound)) {
73
+ const name = String(rawName).toLowerCase();
74
+ if (HOP_BY_HOP_HEADERS.has(name)
75
+ || connectionHeaders.has(name)
76
+ || ["authorization", "host", "content-length", "content-encoding", "expect"].includes(name)) {
77
+ continue;
78
+ }
79
+ const values = Array.isArray(rawValue) ? rawValue : [rawValue];
80
+ for (const value of values) {
81
+ if (value !== undefined) headers.append(name, String(value));
82
+ }
83
+ }
84
+ headers.set("Authorization", `Bearer ${token}`);
85
+ if (typeof bodyText === "string") {
86
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json; charset=utf-8");
87
+ headers.set("Content-Length", String(Buffer.byteLength(bodyText)));
88
+ } else {
89
+ headers.delete("content-type");
90
+ headers.set("Accept", "application/json");
91
+ }
92
+ return headers;
93
+ }
94
+
95
+ function tokenFingerprint(token) {
96
+ return createHash("sha256").update(token).digest("hex");
97
+ }
98
+
99
+ function tokenExpiryMs(token) {
100
+ const semicolonExpiry = /(?:^|[;,&\s])exp=(\d{9,16})(?:$|[;,&\s])/i.exec(token)?.[1];
101
+ if (semicolonExpiry) {
102
+ const value = Number(semicolonExpiry);
103
+ return value >= 1e12 ? value : value * 1000;
104
+ }
105
+ const parts = String(token).split(".");
106
+ if (parts.length !== 3) return null;
107
+ try {
108
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
109
+ const value = Number(payload.exp);
110
+ return Number.isFinite(value) && value > 0 ? value * 1000 : null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function abortError(signal) {
117
+ if (signal?.reason instanceof Error) return signal.reason;
118
+ const error = new Error("The operation was aborted");
119
+ error.name = "AbortError";
120
+ return error;
121
+ }
122
+
123
+ function waitForFlight(flight, signal) {
124
+ if (signal?.aborted) return Promise.reject(abortError(signal));
125
+ flight.waiters += 1;
126
+ return new Promise((resolve, reject) => {
127
+ let finished = false;
128
+ const finish = (handler, value) => {
129
+ if (finished) return;
130
+ finished = true;
131
+ signal?.removeEventListener("abort", onAbort);
132
+ flight.waiters -= 1;
133
+ if (!flight.settled && flight.waiters === 0) flight.controller.abort();
134
+ handler(value);
135
+ };
136
+ const onAbort = () => finish(reject, abortError(signal));
137
+ signal?.addEventListener("abort", onAbort, { once: true });
138
+ if (signal?.aborted) onAbort();
139
+ flight.promise.then(
140
+ (value) => finish(resolve, value),
141
+ (error) => finish(reject, error),
142
+ );
143
+ });
144
+ }
145
+
146
+ function catalogData(catalog) {
147
+ if (Array.isArray(catalog)) return catalog;
148
+ return Array.isArray(catalog?.data) ? catalog.data : null;
149
+ }
150
+
151
+ async function responseSnapshot(response) {
152
+ return {
153
+ status: response.status,
154
+ ok: response.ok,
155
+ headers: new Headers(response.headers),
156
+ body: Buffer.from(await response.arrayBuffer()),
157
+ };
158
+ }
159
+
160
+ function responseHeaders(headers, body, options) {
161
+ return {
162
+ ...safeUpstreamResponseHeaders(headers, options),
163
+ "Content-Length": String(body.length),
164
+ };
165
+ }
166
+
167
+ function sendSnapshot(res, snapshot, options = {}) {
168
+ if (res.destroyed || res.writableEnded) return;
169
+ res.writeHead(snapshot.status, responseHeaders(snapshot.headers, snapshot.body, options));
170
+ res.end(snapshot.body);
171
+ }
172
+
173
+ function rejectUpstreamRedirect(response) {
174
+ if (response.status >= 300 && response.status < 400) {
175
+ throw localError(502, "upstream_redirect", "PM Studio relay rejected an upstream redirect");
176
+ }
177
+ return response;
178
+ }
179
+
180
+ function mergeModelCatalog(snapshot, modelRouter) {
181
+ if (!snapshot.ok) return null;
182
+ const allowed = modelRouter.allowedModels();
183
+ if (!allowed.length) return null;
184
+ let enterprise;
185
+ try {
186
+ enterprise = JSON.parse(snapshot.body.toString("utf8"));
187
+ } catch {
188
+ return null;
189
+ }
190
+ const data = catalogData(enterprise);
191
+ if (!data) return null;
192
+ const seen = new Set(data.map((model) => String(model?.id || "").trim()).filter(Boolean));
193
+ const additions = [];
194
+ for (const model of allowed) {
195
+ const id = String(model?.id || "").trim();
196
+ if (seen.has(id)) continue;
197
+ seen.add(id);
198
+ additions.push(model);
199
+ }
200
+ if (!additions.length) return null;
201
+ return Buffer.from(JSON.stringify(Array.isArray(enterprise)
202
+ ? [...enterprise, ...additions]
203
+ : { ...enterprise, data: [...data, ...additions] }));
204
+ }
205
+
206
+ async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTimeoutMs) {
207
+ const stream = reqBody?.stream === true;
208
+ if (!stream || !response.ok) {
209
+ const snapshot = await responseSnapshot(response);
210
+ sendSnapshot(res, snapshot);
211
+ return;
212
+ }
213
+
214
+ await requireUpstreamEventStream(response);
215
+ abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
216
+ res.writeHead(response.status, {
217
+ ...safeUpstreamResponseHeaders(response.headers, { contentType: "text/event-stream" }),
218
+ "Cache-Control": "no-cache",
219
+ });
220
+ const reader = response.body.getReader();
221
+ try {
222
+ while (true) {
223
+ const { done, value } = await reader.read();
224
+ if (done) {
225
+ res.end();
226
+ return;
227
+ }
228
+ abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
229
+ let onAbort;
230
+ const aborted = new Promise((resolve) => {
231
+ onAbort = () => {
232
+ if (!res.destroyed) res.destroy();
233
+ resolve(false);
234
+ };
235
+ if (abort.signal.aborted) onAbort();
236
+ else abort.signal.addEventListener("abort", onAbort, { once: true });
237
+ });
238
+ let writeCompleted;
239
+ try {
240
+ writeCompleted = await Promise.race([writeOrDrain(res, value), aborted]);
241
+ } finally {
242
+ abort.signal.removeEventListener("abort", onAbort);
243
+ }
244
+ if (!writeCompleted) return;
245
+ }
246
+ } finally {
247
+ await reader.cancel().catch(() => {});
248
+ reader.releaseLock();
249
+ }
250
+ }
251
+
252
+ function safeFailure(error, abort) {
253
+ if (error?.statusCode && !isAbortLikeError(error)) return error;
254
+ if (abort?.signal.aborted || isAbortLikeError(error)) {
255
+ return localError(
256
+ abortErrorStatusCode(abort?.reason),
257
+ "upstream_timeout",
258
+ "PM Studio relay upstream request timed out or was cancelled",
259
+ );
260
+ }
261
+ return localError(502, "upstream_error", "PM Studio relay upstream request failed");
262
+ }
263
+
264
+ function releaseOnce(release) {
265
+ let released = false;
266
+ return () => {
267
+ if (released) return;
268
+ released = true;
269
+ release?.();
270
+ };
271
+ }
272
+
273
+ export function createPmStudioRelayHandler(options = {}) {
274
+ const runtime = loadRuntimeConfig();
275
+ const acquireRequest = options.acquireRequest || createRequestAdmission();
276
+ const fetchImpl = options.fetchImpl || fetch;
277
+ const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, runtime.upstreamTimeoutMs);
278
+ const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, runtime.streamHandshakeTimeoutMs);
279
+ const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, runtime.streamIdleTimeoutMs);
280
+ const validationTtlMs = parsePositiveInteger(options.validationTtlMs, DEFAULT_VALIDATION_TTL_MS);
281
+ const validationCacheSize = parsePositiveInteger(options.validationCacheSize, DEFAULT_VALIDATION_CACHE_SIZE, 4096);
282
+ const validationConcurrency = parsePositiveInteger(
283
+ options.validationConcurrency, DEFAULT_VALIDATION_CONCURRENCY, 64,
284
+ );
285
+ const validationMaxFlights = parsePositiveInteger(
286
+ options.validationMaxFlights, DEFAULT_VALIDATION_MAX_FLIGHTS, 4096,
287
+ );
288
+ const validationMaxWaiters = parsePositiveInteger(
289
+ options.validationMaxWaiters, DEFAULT_VALIDATION_MAX_WAITERS, 4096,
290
+ );
291
+ const now = typeof options.now === "function" ? options.now : Date.now;
292
+ const validated = new Map();
293
+ const validationFlights = new Map();
294
+ let activeModelRequests = 0;
295
+ const modelRouter = createPmStudioModelRouter({
296
+ getCatalog: () => options.claudeModelRegistry?.models,
297
+ isClaudeEnabled: () => options.claudeMode === "isolated"
298
+ && options.claudeProfile?.valid === true
299
+ && typeof options.claudeClient?.chatCompletions === "function",
300
+ });
301
+
302
+ function isValidated(fingerprint) {
303
+ const expiresAt = validated.get(fingerprint);
304
+ if (!expiresAt) return false;
305
+ if (expiresAt <= now()) {
306
+ validated.delete(fingerprint);
307
+ return false;
308
+ }
309
+ return true;
310
+ }
311
+
312
+ function rememberValidation(fingerprint, token) {
313
+ const currentTime = now();
314
+ const parsedExpiry = tokenExpiryMs(token);
315
+ if (parsedExpiry === null) return;
316
+ const expiresAt = Math.min(currentTime + validationTtlMs, parsedExpiry);
317
+ if (expiresAt <= currentTime) return;
318
+ validated.delete(fingerprint);
319
+ while (validated.size >= validationCacheSize) validated.delete(validated.keys().next().value);
320
+ validated.set(fingerprint, expiresAt);
321
+ }
322
+
323
+ async function fetchEnterprise(pathname, method, inboundHeaders, token, bodyText, signal) {
324
+ const response = await fetchImpl(`${COPILOT_ORIGIN}${pathname}`, {
325
+ method,
326
+ headers: enterpriseHeaders(inboundHeaders, token, bodyText),
327
+ body: bodyText,
328
+ signal,
329
+ redirect: "manual",
330
+ });
331
+ return rejectUpstreamRedirect(response);
332
+ }
333
+
334
+ async function fetchEnterpriseModels(inboundHeaders, token, signal) {
335
+ if (activeModelRequests >= validationConcurrency) {
336
+ throw localError(503, "validation_busy", "PM Studio bearer validation is busy; retry shortly");
337
+ }
338
+ activeModelRequests += 1;
339
+ try {
340
+ return await fetchEnterprise(
341
+ "/models", "GET", inboundHeaders, token, undefined, signal,
342
+ ).then(responseSnapshot);
343
+ } finally {
344
+ activeModelRequests -= 1;
345
+ }
346
+ }
347
+
348
+ function trustedClaudeChatUrl() {
349
+ const rawBase = String(options.claudeClient?.getApiBase?.() || COPILOT_ORIGIN);
350
+ let base;
351
+ let target;
352
+ try {
353
+ base = new URL(rawBase);
354
+ target = new URL(`${rawBase}/chat/completions`);
355
+ } catch {
356
+ throw localError(502, "invalid_upstream", "Claude client selected an invalid upstream URL");
357
+ }
358
+ if (base.protocol !== "https:"
359
+ || base.username
360
+ || base.password
361
+ || base.search
362
+ || base.hash
363
+ || target.username
364
+ || target.password) {
365
+ throw localError(502, "invalid_upstream", "Claude client selected an unsupported upstream URL");
366
+ }
367
+ return target.href;
368
+ }
369
+
370
+ async function fetchClaudeUpstream(url, init = {}) {
371
+ let target;
372
+ try {
373
+ target = new URL(url);
374
+ } catch {
375
+ throw localError(502, "invalid_upstream", "Claude client selected an invalid upstream URL");
376
+ }
377
+ if (target.href !== trustedClaudeChatUrl()) {
378
+ throw localError(502, "invalid_upstream", "Claude client selected an unsupported upstream URL");
379
+ }
380
+ return rejectUpstreamRedirect(await fetchImpl(target.href, { ...init, redirect: "manual" }));
381
+ }
382
+
383
+ function validateBearer(token, inboundHeaders, signal) {
384
+ const fingerprint = tokenFingerprint(token);
385
+ if (isValidated(fingerprint)) return Promise.resolve(null);
386
+ let flight = validationFlights.get(fingerprint);
387
+ if (!flight) {
388
+ if (validationFlights.size >= validationMaxFlights) {
389
+ return Promise.reject(localError(
390
+ 503, "validation_busy", "PM Studio bearer validation is busy; retry shortly",
391
+ ));
392
+ }
393
+ const controller = new AbortController();
394
+ flight = { controller, waiters: 0, settled: false, promise: null };
395
+ flight.promise = fetchEnterpriseModels(inboundHeaders, token, controller.signal)
396
+ .then((snapshot) => {
397
+ if (snapshot.ok) rememberValidation(fingerprint, token);
398
+ return snapshot;
399
+ })
400
+ .finally(() => {
401
+ flight.settled = true;
402
+ if (validationFlights.get(fingerprint) === flight) validationFlights.delete(fingerprint);
403
+ });
404
+ validationFlights.set(fingerprint, flight);
405
+ }
406
+ if (flight.waiters >= validationMaxWaiters) {
407
+ return Promise.reject(localError(
408
+ 503, "validation_busy", "PM Studio bearer validation is busy; retry shortly",
409
+ ));
410
+ }
411
+ return waitForFlight(flight, signal);
412
+ }
413
+
414
+ async function handleModels(req, res, token, abort) {
415
+ const validation = await validateBearer(token, req.headers, abort.signal);
416
+ const snapshot = validation || await fetchEnterpriseModels(req.headers, token, abort.signal);
417
+ if (!snapshot.ok) {
418
+ sendSnapshot(res, snapshot);
419
+ return;
420
+ }
421
+ const merged = mergeModelCatalog(snapshot, modelRouter);
422
+ if (!merged) {
423
+ sendSnapshot(res, snapshot);
424
+ return;
425
+ }
426
+ res.writeHead(snapshot.status, responseHeaders(snapshot.headers, merged, { contentType: "application/json" }));
427
+ res.end(merged);
428
+ }
429
+
430
+ async function handlePost(req, res, pathname, token, abort) {
431
+ let releaseRequest = releaseOnce();
432
+ try {
433
+ releaseRequest = releaseOnce(await acquireRequest(req, { signal: abort.signal }));
434
+ const body = await readJsonBody(req);
435
+ const supportsStream = pathname === "/chat/completions" || pathname === "/responses";
436
+ const streaming = supportsStream && body?.stream === true;
437
+ const requestTimeoutMs = streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs;
438
+ const timeoutReason = streaming ? "stream_handshake_timeout" : "upstream_timeout";
439
+ abort.setTimeout(requestTimeoutMs, timeoutReason);
440
+
441
+ const validation = await validateBearer(token, req.headers, abort.signal);
442
+ if (validation && !validation.ok) {
443
+ releaseRequest();
444
+ sendSnapshot(res, validation);
445
+ return;
446
+ }
447
+ abort.setTimeout(requestTimeoutMs, timeoutReason);
448
+
449
+ const modelType = modelRouter.classify(body?.model);
450
+ if (modelType === "unsupported_claude" || (modelType === "claude" && pathname !== "/chat/completions")) {
451
+ throw localError(400, "model_not_supported", "The requested Claude model is not available for this endpoint");
452
+ }
453
+
454
+ let upstream;
455
+ if (modelType === "claude") {
456
+ upstream = await options.claudeClient.chatCompletions(body, {
457
+ signal: abort.signal,
458
+ fetchImpl: fetchClaudeUpstream,
459
+ });
460
+ } else {
461
+ const bodyText = JSON.stringify(body);
462
+ upstream = await fetchEnterprise(pathname, "POST", req.headers, token, bodyText, abort.signal);
463
+ }
464
+ releaseRequest();
465
+ await relayUpstreamResponse(upstream, supportsStream ? body : null, res, abort, streamIdleTimeoutMs);
466
+ } finally {
467
+ releaseRequest();
468
+ }
469
+ }
470
+
471
+ return async function handlePmStudioRelay(req, res, suppliedPathname) {
472
+ const pathname = suppliedPathname || new URL(req.url || "/", "http://localhost").pathname;
473
+ if (!pathname.startsWith(`${PM_STUDIO_RELAY_PREFIX}/`)) return false;
474
+ if (!isLoopbackAddress(req.socket?.remoteAddress)) {
475
+ sendLocalError(res, 403, "loopback_required", "PM Studio relay is available only from loopback");
476
+ return true;
477
+ }
478
+ const token = bearerToken(req.headers);
479
+ if (!token) {
480
+ sendLocalError(res, 401, "invalid_authorization", "A bearer Authorization header is required", {
481
+ "WWW-Authenticate": "Bearer",
482
+ });
483
+ return true;
484
+ }
485
+ if (/\bupgrade\b/i.test(String(req.headers?.connection || "")) || req.headers?.upgrade) {
486
+ sendLocalError(res, 426, "upgrade_not_supported", "Protocol upgrade is not supported");
487
+ return true;
488
+ }
489
+
490
+ const upstreamPath = pathname.slice(PM_STUDIO_RELAY_PREFIX.length);
491
+ const allowedMethod = upstreamPath === "/models" ? "GET" : POST_PATHS.has(upstreamPath) ? "POST" : null;
492
+ if (!allowedMethod) {
493
+ sendLocalError(res, 404, "route_not_found", "PM Studio relay route not found");
494
+ return true;
495
+ }
496
+ if (req.method !== allowedMethod) {
497
+ sendLocalError(res, 405, "method_not_allowed", "Method is not allowed for this PM Studio relay route", {
498
+ Allow: allowedMethod,
499
+ });
500
+ return true;
501
+ }
502
+
503
+ const abort = createRequestAbort(req, res);
504
+ try {
505
+ if (upstreamPath === "/models") {
506
+ abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
507
+ await handleModels(req, res, token, abort);
508
+ } else {
509
+ await handlePost(req, res, upstreamPath, token, abort);
510
+ }
511
+ } catch (error) {
512
+ const safe = safeFailure(error, abort);
513
+ if (res.headersSent) {
514
+ if (!res.destroyed) res.destroy();
515
+ } else {
516
+ sendJsonError(res, safe, safe.statusCode || 502);
517
+ }
518
+ } finally {
519
+ abort.cleanup();
520
+ }
521
+ return true;
522
+ };
523
+ }