codex-copilot-dx 0.6.21 → 0.7.1

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