copilot-relay 0.4.7 → 0.5.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/dist/main.js CHANGED
@@ -49,6 +49,24 @@ var PromptTooLongError = class extends HTTPError {
49
49
  }
50
50
  };
51
51
 
52
+ //#endregion
53
+ //#region src/lib/file-identity.ts
54
+ /** True when two stats of a path name the same file: the same device and file index. */
55
+ const sameFile$1 = (left, right) => left.dev === right.dev && left.ino === right.ino;
56
+ /**
57
+ * True when a stat of an open handle and a stat of its path name the same file.
58
+ *
59
+ * On Windows the two can disagree on `dev` for the same file: on Node 22.13.1 a path stat
60
+ * reports `dev` 0 while a handle stat reports the volume serial number. There a `dev` of 0 on
61
+ * either side counts as unreported and the file index (`ino`) alone decides, which is weaker
62
+ * evidence than both. Two nonzero `dev` values must still match, and other platforms compare both.
63
+ */
64
+ const sameOpenedFile = (left, right, platform = process.platform) => {
65
+ if (left.ino !== right.ino) return false;
66
+ if (platform === "win32" && (left.dev === 0 || right.dev === 0)) return true;
67
+ return left.dev === right.dev;
68
+ };
69
+
52
70
  //#endregion
53
71
  //#region src/lib/paths.ts
54
72
  const homeDir = os.homedir();
@@ -371,7 +389,6 @@ const formatLogValue = (value) => typeof value === "string" ? value : inspect(va
371
389
  });
372
390
  const escapeLogLineSeparators = (value) => value.replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll(String.fromCodePoint(8232), "\\u2028").replaceAll(String.fromCodePoint(8233), "\\u2029");
373
391
  const isMissing$2 = (error) => error.code === "ENOENT";
374
- const sameFile$2 = (left, right) => left.dev === right.dev && left.ino === right.ino;
375
392
  const ensurePrivateDirectory = async (directory) => {
376
393
  await fs.mkdir(directory, { mode: 448 }).catch((error) => {
377
394
  if (error.code !== "EEXIST") throw error;
@@ -382,7 +399,7 @@ const ensurePrivateDirectory = async (directory) => {
382
399
  const handle = await fs.open(directory, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
383
400
  try {
384
401
  const opened = await handle.stat();
385
- if (!opened.isDirectory() || !sameFile$2(observed, opened)) throw new Error("Log directory changed while opening");
402
+ if (!opened.isDirectory() || !sameOpenedFile(observed, opened)) throw new Error("Log directory changed while opening");
386
403
  await handle.chmod(448);
387
404
  } finally {
388
405
  await handle.close();
@@ -401,10 +418,10 @@ let activeLog;
401
418
  let logIdleClose;
402
419
  const maxLogWriteBytes = 256 * 1024;
403
420
  const hasMode = (stats, mode) => process.platform === "win32" || (stats.mode & 511) === mode;
404
- const isSameDirectory = (current, checked) => current !== void 0 && current.isDirectory() && sameFile$2(current, checked) && hasMode(current, 448);
421
+ const isSameDirectory = (current, checked) => current !== void 0 && current.isDirectory() && sameFile$1(current, checked) && hasMode(current, 448);
405
422
  const isStillActive = async (active) => {
406
423
  const [current, ...directories] = await Promise.all([active.filePath, ...logDirectories].map((target) => fs.lstat(target).catch(() => void 0)));
407
- return current !== void 0 && current.isFile() && current.nlink === 1 && sameFile$2(current, active.identity) && hasMode(current, 384) && active.directories.every((checked, index) => isSameDirectory(directories[index], checked));
424
+ return current !== void 0 && current.isFile() && current.nlink === 1 && sameOpenedFile(current, active.identity) && hasMode(current, 384) && active.directories.every((checked, index) => isSameDirectory(directories[index], checked));
408
425
  };
409
426
  const openPrivateLog = async (filePath, directories) => {
410
427
  const observed = await fs.lstat(filePath).catch((error) => {
@@ -416,7 +433,7 @@ const openPrivateLog = async (filePath, directories) => {
416
433
  try {
417
434
  const opened = await handle.stat();
418
435
  const current = await fs.lstat(filePath);
419
- if (!opened.isFile() || opened.nlink !== 1 || current.isSymbolicLink() || !sameFile$2(opened, current) || observed && !sameFile$2(observed, opened)) throw new Error("Log file changed while opening");
436
+ if (!opened.isFile() || opened.nlink !== 1 || current.isSymbolicLink() || !sameOpenedFile(opened, current) || observed && !sameOpenedFile(observed, opened)) throw new Error("Log file changed while opening");
420
437
  if (process.platform !== "win32") await handle.chmod(384);
421
438
  return {
422
439
  filePath,
@@ -2296,7 +2313,6 @@ const markDiscardedResponse = (response) => {
2296
2313
  };
2297
2314
  const credentialPattern = /(?:gh[pousr]_|github_pat_|sk-|eyJ)[A-Za-z0-9_-]+/;
2298
2315
  const uuidPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
2299
- const sameFile$1 = (left, right) => left.dev === right.dev && left.ino === right.ino;
2300
2316
  const noFollow = process.platform === "win32" ? 0 : fs.constants.O_NOFOLLOW;
2301
2317
  const safeValue = (value) => typeof value === "string" && /^[A-Za-z0-9_.:/[\]-]{1,160}$/.test(value) && !/(?:gh[pousr]_|github_pat_|sk-|eyJ)[A-Za-z0-9_-]+/.test(value) ? value : void 0;
2302
2318
  const errorName = (error) => error instanceof Error && [
@@ -2444,7 +2460,7 @@ const privateDirectory = async (directory) => {
2444
2460
  const handle = await fs.open(directory, fs.constants.O_RDONLY | noFollow);
2445
2461
  try {
2446
2462
  const opened = await handle.stat();
2447
- if (!opened.isDirectory() || !sameFile$1(observed, opened)) throw new Error("Capture directory changed.");
2463
+ if (!opened.isDirectory() || !sameOpenedFile(observed, opened)) throw new Error("Capture directory changed.");
2448
2464
  await handle.chmod(448);
2449
2465
  return opened;
2450
2466
  } finally {
@@ -2456,7 +2472,7 @@ const createPrivateFile = async (file) => {
2456
2472
  try {
2457
2473
  const opened = await handle.stat();
2458
2474
  const current = await fs.lstat(file);
2459
- if (!opened.isFile() || opened.nlink !== 1 || current.isSymbolicLink() || !sameFile$1(opened, current)) throw new Error("Unsafe capture file.");
2475
+ if (!opened.isFile() || opened.nlink !== 1 || current.isSymbolicLink() || !sameOpenedFile(opened, current)) throw new Error("Unsafe capture file.");
2460
2476
  return handle;
2461
2477
  } catch (error) {
2462
2478
  await handle.close();
@@ -2914,7 +2930,7 @@ var RequestTrace = class RequestTrace {
2914
2930
  }
2915
2931
  const opened = await handle.stat();
2916
2932
  const current = await fs.lstat(file);
2917
- if (!current.isFile() || current.isSymbolicLink() || opened.nlink !== 1 || !sameFile$1(opened, current)) throw new Error("Capture file changed.");
2933
+ if (!current.isFile() || current.isSymbolicLink() || opened.nlink !== 1 || !sameOpenedFile(opened, current)) throw new Error("Capture file changed.");
2918
2934
  await handle.writeFile(buffer);
2919
2935
  }).catch(() => {
2920
2936
  this.manifest.captureError = "capture_write_failed";
@@ -2967,7 +2983,7 @@ var RequestTrace = class RequestTrace {
2967
2983
  await this.checkDirectories();
2968
2984
  const manifest = path.join(this.captureDirectory, "meta.json");
2969
2985
  const current = await fs.lstat(manifest);
2970
- if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || !this.metadataStat || !sameFile$1(current, this.metadataStat) || current.size !== this.metadataStat.size || current.mtimeMs !== this.metadataStat.mtimeMs || current.ctimeMs !== this.metadataStat.ctimeMs) throw new Error("Unsafe capture metadata.");
2986
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || !this.metadataStat || !sameOpenedFile(current, this.metadataStat) || current.size !== this.metadataStat.size || current.mtimeMs !== this.metadataStat.mtimeMs || current.ctimeMs !== this.metadataStat.ctimeMs) throw new Error("Unsafe capture metadata.");
2971
2987
  await fs.rename(path.join(this.captureDirectory, "meta.next.json"), manifest);
2972
2988
  }
2973
2989
  }).catch(() => {
@@ -3005,7 +3021,7 @@ const readRetentionManifest = async (file) => {
3005
3021
  const handle = await fs.open(file, fs.constants.O_RDONLY | noFollow);
3006
3022
  try {
3007
3023
  const opened = await handle.stat();
3008
- if (!sameFile$1(observed, opened) || opened.nlink !== 1 || opened.size !== observed.size) throw new Error("Capture metadata changed.");
3024
+ if (!sameOpenedFile(observed, opened) || opened.nlink !== 1 || opened.size !== observed.size) throw new Error("Capture metadata changed.");
3009
3025
  const bytes = Buffer.alloc(opened.size);
3010
3026
  let offset = 0;
3011
3027
  while (offset < bytes.length) {
@@ -3014,7 +3030,7 @@ const readRetentionManifest = async (file) => {
3014
3030
  offset += bytesRead;
3015
3031
  }
3016
3032
  const current = await fs.lstat(file);
3017
- if (!sameFile$1(opened, current) || current.size !== opened.size || current.mtimeMs !== opened.mtimeMs) throw new Error("Capture metadata changed.");
3033
+ if (!sameOpenedFile(opened, current) || current.size !== opened.size || current.mtimeMs !== opened.mtimeMs) throw new Error("Capture metadata changed.");
3018
3034
  return JSON.parse(bytes.toString("utf8"));
3019
3035
  } finally {
3020
3036
  await handle.close();
@@ -3327,10 +3343,10 @@ const pinCopilotModelCatalog = (config) => {
3327
3343
  pinnedCatalogs.set(config, config.modelCatalog);
3328
3344
  };
3329
3345
  const pendingCatalogs = /* @__PURE__ */ new WeakMap();
3330
- const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3346
+ const isRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3331
3347
  const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
3332
3348
  function parseModelTokenLimits(value) {
3333
- if (!isRecord$5(value)) return;
3349
+ if (!isRecord$6(value)) return;
3334
3350
  const context = value.max_context_window_tokens;
3335
3351
  const prompt = value.max_prompt_tokens;
3336
3352
  const output = value.max_output_tokens;
@@ -3386,13 +3402,13 @@ async function loadCopilotModelCatalog(config) {
3386
3402
  });
3387
3403
  if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
3388
3404
  const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
3389
- if (!isRecord$5(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
3405
+ if (!isRecord$6(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
3390
3406
  const models$1 = /* @__PURE__ */ new Map();
3391
3407
  for (const model of payload.data) {
3392
- if (!isRecord$5(model) || typeof model.id !== "string" || !model.id) continue;
3393
- const capabilities = isRecord$5(model.capabilities) ? model.capabilities : void 0;
3408
+ if (!isRecord$6(model) || typeof model.id !== "string" || !model.id) continue;
3409
+ const capabilities = isRecord$6(model.capabilities) ? model.capabilities : void 0;
3394
3410
  const limits = parseModelTokenLimits(capabilities?.limits);
3395
- const supports = isRecord$5(capabilities?.supports) ? capabilities.supports : void 0;
3411
+ const supports = isRecord$6(capabilities?.supports) ? capabilities.supports : void 0;
3396
3412
  const supportedEndpoints = parseStringArray(model.supported_endpoints);
3397
3413
  const reasoningEfforts = parseReasoningEfforts(supports);
3398
3414
  models$1.set(model.id, {
@@ -3737,7 +3753,7 @@ const claudeCodeContext = [
3737
3753
  const claudeCodeContextPatterns = [/^\S+ hook additional context:/];
3738
3754
  const statusProbePrompt = "Reply with the single word: ok";
3739
3755
  const statusProbeMaxTokens = 16;
3740
- const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3756
+ const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3741
3757
  const innermostReminder = /<system-reminder>(?:(?!<system-reminder>)[\s\S])*?<\/system-reminder>/g;
3742
3758
  const withoutReminders = (text$1) => {
3743
3759
  let previous = text$1;
@@ -3755,14 +3771,14 @@ const contentBlocks = (content) => {
3755
3771
  type: "text",
3756
3772
  text: content
3757
3773
  }];
3758
- return Array.isArray(content) ? content.filter(isRecord$4) : [];
3774
+ return Array.isArray(content) ? content.filter(isRecord$5) : [];
3759
3775
  };
3760
3776
  const originatingTurn = (messages) => {
3761
3777
  const blocks = [];
3762
3778
  let sawUser = false;
3763
3779
  for (let index = messages.length - 1; index >= 0; index--) {
3764
3780
  const message = messages[index];
3765
- if (!isRecord$4(message) || message.role === "system") continue;
3781
+ if (!isRecord$5(message) || message.role === "system") continue;
3766
3782
  if (message.role !== "user") break;
3767
3783
  sawUser = true;
3768
3784
  blocks.unshift(...contentBlocks(message.content));
@@ -3782,11 +3798,11 @@ const blockOrigin = (block) => {
3782
3798
  const isStatusProbe = (body) => {
3783
3799
  if (Object.keys(body).sort().join(",") !== "max_tokens,messages,model" || body.max_tokens !== statusProbeMaxTokens) return false;
3784
3800
  const messages = body.messages;
3785
- return Array.isArray(messages) && messages.length === 1 && isRecord$4(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
3801
+ return Array.isArray(messages) && messages.length === 1 && isRecord$5(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
3786
3802
  };
3787
3803
  const systemTexts = (system) => contentBlocks(system).flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
3788
3804
  const getClaudeRequestInitiator = (body) => {
3789
- if (!isRecord$4(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
3805
+ if (!isRecord$5(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
3790
3806
  const payload = body;
3791
3807
  if (readBillingLineField(payload, "cc_is_subagent") === "true" || readBillingLineField(payload, "cc_workload") === "cron") return "agent";
3792
3808
  if (systemTexts(removeBillingLine(payload).system).some((text$1) => startsWithAny(text$1.trimStart(), ownRequestSystemPrompts))) return "agent";
@@ -4390,7 +4406,7 @@ const webSearchInputSchema = {
4390
4406
  required: ["query"],
4391
4407
  additionalProperties: false
4392
4408
  };
4393
- const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4409
+ const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4394
4410
  const isAnthropicNativeWebSearchTool = (tool) => tool.name === "web_search" && typeof tool.type === "string" && anthropicWebSearchToolPattern.test(tool.type);
4395
4411
  const isClaudeCodeWebSearchTool = (tool) => tool.name === claudeCodeWebSearchToolName;
4396
4412
  const isClaudeWebSearchTool = (tool) => isAnthropicNativeWebSearchTool(tool) || isClaudeCodeWebSearchTool(tool);
@@ -4430,7 +4446,7 @@ const getQueryFromToolArguments = (value) => {
4430
4446
  if (!trimmed) return;
4431
4447
  try {
4432
4448
  const parsed = JSON.parse(trimmed);
4433
- if (isRecord$3(parsed) && typeof parsed.query === "string" && parsed.query.trim()) return parsed.query.trim();
4449
+ if (isRecord$4(parsed) && typeof parsed.query === "string" && parsed.query.trim()) return parsed.query.trim();
4434
4450
  } catch {
4435
4451
  return trimmed;
4436
4452
  }
@@ -4487,7 +4503,7 @@ const getReportedQuery = (action, queries) => {
4487
4503
  const getSearchQuery = (response, requestedQuery) => {
4488
4504
  for (const item of response.output ?? []) {
4489
4505
  if (item.type !== "web_search_call") continue;
4490
- const action = isRecord$3(item.action) ? item.action : void 0;
4506
+ const action = isRecord$4(item.action) ? item.action : void 0;
4491
4507
  const query = getReportedQuery(action, Array.isArray(action?.queries) ? action.queries : []);
4492
4508
  if (query) return query;
4493
4509
  }
@@ -4495,7 +4511,7 @@ const getSearchQuery = (response, requestedQuery) => {
4495
4511
  };
4496
4512
  const getResponseText = (response) => (response.output ?? []).flatMap((item) => {
4497
4513
  if (item.type !== "message") return [];
4498
- return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$3(part) && part.type === "output_text" && typeof part.text === "string" ? [part.text] : []);
4514
+ return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$4(part) && part.type === "output_text" && typeof part.text === "string" ? [part.text] : []);
4499
4515
  }).join("\n").trim();
4500
4516
  const cleanTitle = (line, url) => {
4501
4517
  return line.slice(0, line.indexOf(url)).replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").replace(/\s*(?:[-:|])\s*$/, "").trim() || new URL(url).hostname;
@@ -4519,15 +4535,15 @@ const parseSearchResults = (text$1) => {
4519
4535
  const getStructuredSearchResults = (response) => {
4520
4536
  const sources = [];
4521
4537
  for (const item of response.output ?? []) {
4522
- if (item.type === "web_search_call" && isRecord$3(item.action) && Array.isArray(item.action.sources)) sources.push(...item.action.sources);
4538
+ if (item.type === "web_search_call" && isRecord$4(item.action) && Array.isArray(item.action.sources)) sources.push(...item.action.sources);
4523
4539
  if (item.type === "message" && Array.isArray(item.content)) {
4524
- for (const part of item.content) if (isRecord$3(part) && Array.isArray(part.annotations)) sources.push(...part.annotations.filter((annotation) => isRecord$3(annotation) && annotation.type === "url_citation"));
4540
+ for (const part of item.content) if (isRecord$4(part) && Array.isArray(part.annotations)) sources.push(...part.annotations.filter((annotation) => isRecord$4(annotation) && annotation.type === "url_citation"));
4525
4541
  }
4526
4542
  }
4527
4543
  const results = [];
4528
4544
  const seen = /* @__PURE__ */ new Set();
4529
4545
  for (const source of sources) {
4530
- if (!isRecord$3(source) || typeof source.url !== "string") continue;
4546
+ if (!isRecord$4(source) || typeof source.url !== "string") continue;
4531
4547
  try {
4532
4548
  const url = new URL(source.url);
4533
4549
  if (!/^https?:$/.test(url.protocol) || url.username || url.password || seen.has(url.href)) continue;
@@ -4579,21 +4595,21 @@ const getResultSource = (structured, results) => {
4579
4595
  return results.length ? "text" : "none";
4580
4596
  };
4581
4597
  const interpretSearchResponse = (raw, config, requestedQuery, request, requestedEffort, requestId) => {
4582
- const malformed = !isRecord$3(raw) || raw.output !== void 0 && (!Array.isArray(raw.output) || !raw.output.every(isRecord$3));
4583
- const upstream = isRecord$3(raw) ? {
4598
+ const malformed = !isRecord$4(raw) || raw.output !== void 0 && (!Array.isArray(raw.output) || !raw.output.every(isRecord$4));
4599
+ const upstream = isRecord$4(raw) ? {
4584
4600
  ...raw,
4585
- output: Array.isArray(raw.output) ? raw.output.filter(isRecord$3) : []
4601
+ output: Array.isArray(raw.output) ? raw.output.filter(isRecord$4) : []
4586
4602
  } : {};
4587
4603
  const status$1 = recognizedValue(upstream.status, responseStates);
4588
- const reason = recognizedValue(isRecord$3(upstream.incomplete_details) ? upstream.incomplete_details.reason : void 0, incompleteReasons);
4604
+ const reason = recognizedValue(isRecord$4(upstream.incomplete_details) ? upstream.incomplete_details.reason : void 0, incompleteReasons);
4589
4605
  const items = upstream.output ?? [];
4590
4606
  const calls = items.filter((item) => item.type === "web_search_call");
4591
4607
  const callStates = calls.map((item) => recognizedValue(item.status, searchStates));
4592
- const usage$1 = isRecord$3(upstream.usage) ? upstream.usage : {};
4608
+ const usage$1 = isRecord$4(upstream.usage) ? upstream.usage : {};
4593
4609
  const inputTokens = reportedTokens(usage$1.input_tokens);
4594
- const cachedInputTokens = reportedTokens(isRecord$3(usage$1.input_tokens_details) ? usage$1.input_tokens_details.cached_tokens : void 0);
4610
+ const cachedInputTokens = reportedTokens(isRecord$4(usage$1.input_tokens_details) ? usage$1.input_tokens_details.cached_tokens : void 0);
4595
4611
  const outputTokens = reportedTokens(usage$1.output_tokens);
4596
- const reasoningTokens = reportedTokens(isRecord$3(usage$1.output_tokens_details) ? usage$1.output_tokens_details.reasoning_tokens : void 0);
4612
+ const reasoningTokens = reportedTokens(isRecord$4(usage$1.output_tokens_details) ? usage$1.output_tokens_details.reasoning_tokens : void 0);
4597
4613
  const upstreamResponseId = safeResponseId(upstream.id, config.copilotToken);
4598
4614
  const safeRequestId = typeof requestId === "string" && /^[a-f0-9-]{36}$/i.test(requestId) ? requestId : "unreported";
4599
4615
  const text$1 = getResponseText(upstream);
@@ -4681,7 +4697,7 @@ const getSearchFailureDetail = (body, token) => {
4681
4697
  let detail = body.trim();
4682
4698
  try {
4683
4699
  const parsed = JSON.parse(detail);
4684
- detail = getErrorMessageOrCode(isRecord$3(parsed) && isRecord$3(parsed.error) ? parsed.error : void 0);
4700
+ detail = getErrorMessageOrCode(isRecord$4(parsed) && isRecord$4(parsed.error) ? parsed.error : void 0);
4685
4701
  } catch {
4686
4702
  if (/^[{[<]/.test(detail)) return "";
4687
4703
  }
@@ -5253,7 +5269,7 @@ const lookaroundPrefixes = [
5253
5269
  "(?<=",
5254
5270
  "(?<!"
5255
5271
  ];
5256
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5272
+ const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5257
5273
  function hasUnsupportedPatternFeatures(pattern) {
5258
5274
  let inCharacterClass = false;
5259
5275
  for (let index = 0; index < pattern.length; index++) {
@@ -5299,12 +5315,12 @@ function mapArray(values) {
5299
5315
  }
5300
5316
  function normalizeSchemaValue(value) {
5301
5317
  if (Array.isArray(value)) return mapArray(value);
5302
- return isRecord$2(value) ? normalizeResponsesToolSchema(value) : value;
5318
+ return isRecord$3(value) ? normalizeResponsesToolSchema(value) : value;
5303
5319
  }
5304
5320
  function normalizeResponsesToolSchema(schema) {
5305
5321
  return mapRecord(schema, (key, value) => {
5306
5322
  if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return removed;
5307
- if (schemaMapKeywords.has(key) && isRecord$2(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
5323
+ if (schemaMapKeywords.has(key) && isRecord$3(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
5308
5324
  if (nestedSchemaKeywords.has(key)) return normalizeSchemaValue(value);
5309
5325
  return value;
5310
5326
  });
@@ -6881,6 +6897,320 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
6881
6897
  }
6882
6898
  });
6883
6899
 
6900
+ //#endregion
6901
+ //#region src/codex/request.ts
6902
+ const copilotReasoningEfforts = [
6903
+ "none",
6904
+ "minimal",
6905
+ "low",
6906
+ "medium",
6907
+ "high",
6908
+ "xhigh",
6909
+ "max"
6910
+ ];
6911
+ const effortOrder = [
6912
+ "none",
6913
+ "minimal",
6914
+ "low",
6915
+ "medium",
6916
+ "high",
6917
+ "xhigh",
6918
+ "max",
6919
+ "ultra"
6920
+ ];
6921
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6922
+ const codexRequestError = (message, code, status$1 = 400) => new HTTPError(message, Response.json({ error: {
6923
+ message,
6924
+ type: "invalid_request_error",
6925
+ code
6926
+ } }, { status: status$1 }), message);
6927
+ const parseCodexPayload = (text$1) => {
6928
+ let body;
6929
+ try {
6930
+ body = JSON.parse(text$1);
6931
+ } catch {
6932
+ throw codexRequestError("The request body is not valid JSON.", "invalid_json");
6933
+ }
6934
+ if (!isRecord$2(body) || typeof body.model !== "string" || body.model.trim() === "") throw codexRequestError("The request needs a model.", "missing_model");
6935
+ if (typeof body.input !== "string" && !Array.isArray(body.input)) throw codexRequestError("The request needs an input string or array.", "missing_input");
6936
+ return body;
6937
+ };
6938
+ const catalogDescribesUpstream = (config) => config.modelCatalog?.baseUrl === config.copilotBaseUrl;
6939
+ /**
6940
+ * The Copilot model a Codex request is sent to.
6941
+ *
6942
+ * A model Copilot's catalog lists is used as named, so Codex's own model IDs reach the same
6943
+ * Copilot model. A name the catalog does not list, such as Codex's internal codex-auto-review,
6944
+ * goes to the configured gptModel, as Claude Code's aliases do. Without a catalog the name is
6945
+ * forwarded unchanged. Claude models are refused: Copilot serves them only on Messages and Chat
6946
+ * Completions.
6947
+ */
6948
+ const resolveCodexModel = async (config, requested) => {
6949
+ if (isClaudeModelId(requested)) throw codexRequestError(`${requested} is not available through the Responses API: Copilot serves Claude models only on Messages and Chat Completions. Choose a GPT model.`, "relay_unsupported_model");
6950
+ const normalized = normalizeCopilotModelId(requested);
6951
+ await ensureCopilotModelCatalog(config, normalized);
6952
+ pinCopilotModelCatalog(config);
6953
+ const model = !(getCachedCopilotModel(config, normalized) !== void 0) && catalogDescribesUpstream(config) ? getModelRouting().gptModel : normalized;
6954
+ const capabilities = getCachedCopilotModel(config, model);
6955
+ const notChat = capabilities?.type !== void 0 && capabilities.type !== "chat";
6956
+ const noResponses = capabilities?.supportedEndpoints !== void 0 && !capabilities.supportedEndpoints.includes("/responses");
6957
+ if (notChat || noResponses) throw codexRequestError(`${model} does not serve the Responses API on Copilot. Choose a model that lists /responses in copilot-relay models.`, "relay_unsupported_endpoint");
6958
+ return model;
6959
+ };
6960
+ const effortRank = (effort) => effortOrder.indexOf(effort);
6961
+ /**
6962
+ * The reasoning effort to send for a requested one.
6963
+ *
6964
+ * A level the model advertises, or one this relay does not know, is sent as requested. Any other
6965
+ * level becomes the highest advertised level below it, or the lowest advertised level when none is
6966
+ * below it. A model that advertises no levels gets none, since Copilot rejects the field for it.
6967
+ */
6968
+ const clampReasoningEffort = (requested, advertised) => {
6969
+ if (advertised?.length === 0) return;
6970
+ const accepted = advertised ?? copilotReasoningEfforts;
6971
+ if (accepted.includes(requested) || effortRank(requested) === -1) return requested;
6972
+ const ranked = accepted.filter((effort) => effortRank(effort) !== -1).sort((left, right) => effortRank(left) - effortRank(right));
6973
+ return ranked.filter((effort) => effortRank(effort) <= effortRank(requested)).at(-1) ?? ranked[0];
6974
+ };
6975
+ /**
6976
+ * The body sent to Copilot for a Codex request.
6977
+ *
6978
+ * Copilot answers HTTP 400 to `service_tier`, to an `image_generation` tool, and to an effort it
6979
+ * does not accept; those are the only changes. Every other field, including Codex's
6980
+ * prompt_cache_key and its encrypted reasoning, is forwarded unchanged.
6981
+ */
6982
+ const prepareCodexPayload = (payload, model, capabilities) => {
6983
+ const prepared = {
6984
+ ...payload,
6985
+ model
6986
+ };
6987
+ const removed$1 = [];
6988
+ if (Object.hasOwn(prepared, "service_tier")) {
6989
+ delete prepared.service_tier;
6990
+ removed$1.push("service_tier");
6991
+ }
6992
+ if (Array.isArray(prepared.tools)) {
6993
+ const tools = prepared.tools.filter((tool) => !(isRecord$2(tool) && tool.type === "image_generation"));
6994
+ if (tools.length !== prepared.tools.length) {
6995
+ prepared.tools = tools;
6996
+ removed$1.push("image_generation");
6997
+ }
6998
+ }
6999
+ const reasoning = isRecord$2(prepared.reasoning) ? prepared.reasoning : void 0;
7000
+ const requestedEffort = typeof reasoning?.effort === "string" ? reasoning.effort : void 0;
7001
+ let effectiveEffort = requestedEffort;
7002
+ if (reasoning !== void 0 && requestedEffort !== void 0) {
7003
+ effectiveEffort = clampReasoningEffort(requestedEffort, capabilities?.reasoningEfforts);
7004
+ if (effectiveEffort === void 0) {
7005
+ const withoutEffort = { ...reasoning };
7006
+ delete withoutEffort.effort;
7007
+ prepared.reasoning = withoutEffort;
7008
+ } else if (effectiveEffort !== requestedEffort) prepared.reasoning = {
7009
+ ...reasoning,
7010
+ effort: effectiveEffort
7011
+ };
7012
+ }
7013
+ return {
7014
+ payload: prepared,
7015
+ removed: removed$1,
7016
+ requestedEffort,
7017
+ effectiveEffort
7018
+ };
7019
+ };
7020
+ /**
7021
+ * Who started a Codex request, for Copilot's x-initiator header.
7022
+ *
7023
+ * A request continues the agent's own work unless its last input item is a user message: after a
7024
+ * tool call the last item is the tool's output. Codex marks its subagents (review, compaction,
7025
+ * memory consolidation, spawned agents) with x-openai-subagent, and those are always the agent.
7026
+ */
7027
+ const getCodexInitiator = (input, subagent) => {
7028
+ if (subagent !== void 0) return "agent";
7029
+ if (typeof input === "string") return "user";
7030
+ const last = Array.isArray(input) ? input.at(-1) : void 0;
7031
+ return isRecord$2(last) && last.role === "user" && (last.type === void 0 || last.type === "message") ? "user" : "agent";
7032
+ };
7033
+ const includesImage = (value) => {
7034
+ if (Array.isArray(value)) return value.some(includesImage);
7035
+ if (!isRecord$2(value)) return false;
7036
+ return value.type === "input_image" || Object.values(value).some(includesImage);
7037
+ };
7038
+ /**
7039
+ * A stream holding only `response.failed` with code `context_length_exceeded`.
7040
+ *
7041
+ * Copilot reports a prompt over the model's input limit as HTTP 400, which Codex treats as a fatal
7042
+ * error. Codex recognizes a context-window overflow only from this event, and answers it by
7043
+ * compacting the conversation.
7044
+ */
7045
+ const contextOverflowStream = (requestId, message) => {
7046
+ const event = {
7047
+ type: "response.failed",
7048
+ response: {
7049
+ id: `resp_relay_${requestId}`,
7050
+ object: "response",
7051
+ status: "failed",
7052
+ error: {
7053
+ code: "context_length_exceeded",
7054
+ message
7055
+ }
7056
+ }
7057
+ };
7058
+ return `event: response.failed\ndata: ${JSON.stringify(event)}\n\n`;
7059
+ };
7060
+
7061
+ //#endregion
7062
+ //#region src/routes/responses.ts
7063
+ const responsesRoutes = new Hono();
7064
+ const forwardedHeaders = [
7065
+ "content-type",
7066
+ "retry-after",
7067
+ "openai-model",
7068
+ "x-request-id",
7069
+ "x-github-request-id",
7070
+ "x-copilot-service-request-id"
7071
+ ];
7072
+ const copyForwardedHeaders = (source) => {
7073
+ const headers = new Headers();
7074
+ for (const name of forwardedHeaders) {
7075
+ const value = source.get(name);
7076
+ if (value !== null) headers.set(name, value);
7077
+ }
7078
+ return headers;
7079
+ };
7080
+ const readSubagent = (value) => {
7081
+ if (value === void 0) return;
7082
+ return /^[\w.-]{1,64}$/.test(value) ? value : "other";
7083
+ };
7084
+ /**
7085
+ * Passes the upstream body to the client and settles the request trace when it ends.
7086
+ *
7087
+ * The trace cancels every upstream body still open once the handler settles, so a streamed answer
7088
+ * must settle the handler only after its last byte, an error, or the client's cancellation.
7089
+ */
7090
+ const settleWhenDone = (body, trace) => {
7091
+ const reader = body.getReader();
7092
+ let settled = false;
7093
+ const settle = (error) => {
7094
+ if (settled) return;
7095
+ settled = true;
7096
+ trace?.handlerSettled(error);
7097
+ };
7098
+ return new ReadableStream({
7099
+ pull: async (controller) => {
7100
+ try {
7101
+ const next = await reader.read();
7102
+ if (settled) return;
7103
+ if (next.done) {
7104
+ controller.close();
7105
+ settle();
7106
+ return;
7107
+ }
7108
+ controller.enqueue(next.value);
7109
+ } catch (error) {
7110
+ if (!settled) controller.error(error);
7111
+ settle(error);
7112
+ }
7113
+ },
7114
+ cancel: async (reason) => {
7115
+ settle();
7116
+ await reader.cancel(reason).catch(() => {});
7117
+ }
7118
+ }, { highWaterMark: 0 });
7119
+ };
7120
+ responsesRoutes.post("/responses", async (c) => {
7121
+ const config = c.get("config");
7122
+ const requestId = c.get("requestId");
7123
+ const trace = c.get("requestTrace");
7124
+ const signal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
7125
+ let response;
7126
+ let stream = false;
7127
+ try {
7128
+ const payload = parseCodexPayload(await c.req.text());
7129
+ stream = payload.stream === true;
7130
+ const model = await resolveCodexModel(config, payload.model);
7131
+ const prepared = prepareCodexPayload(payload, model, getCachedCopilotModel(config, model));
7132
+ const subagent = readSubagent(c.req.header("x-openai-subagent"));
7133
+ const initiator = getCodexInitiator(payload.input, subagent);
7134
+ log.info(sanitizeTerminalString([
7135
+ `request_id=${requestId}`,
7136
+ "Model request client=codex",
7137
+ `requested_model=${payload.model}`,
7138
+ `upstream_model=${model}`,
7139
+ "upstream_api=responses",
7140
+ `requested_think_effort=${prepared.requestedEffort ?? "unset"}`,
7141
+ `effective_think_effort=${prepared.effectiveEffort ?? "omitted"}`,
7142
+ `initiator=${initiator}`,
7143
+ ...subagent === void 0 ? [] : [`subagent=${subagent}`],
7144
+ ...prepared.removed.length === 0 ? [] : [`removed=${prepared.removed.join(",")}`]
7145
+ ].join(" ")));
7146
+ response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
7147
+ method: "POST",
7148
+ headers: {
7149
+ accept: stream ? "text/event-stream" : "application/json",
7150
+ "content-type": "application/json"
7151
+ },
7152
+ body: JSON.stringify(prepared.payload)
7153
+ }, {
7154
+ initiator,
7155
+ requestId,
7156
+ signal,
7157
+ timeoutMs: config.upstreamTimeoutMs,
7158
+ vision: includesImage(prepared.payload.input)
7159
+ });
7160
+ if (!response.ok) {
7161
+ const detail = await readCopilotText(response, signal, config.upstreamTimeoutMs);
7162
+ const overflow = toPromptTooLongError(response.status, detail);
7163
+ if (overflow && stream) {
7164
+ c.set("requestErrorMessage", overflow.message);
7165
+ log.error(`request_id=${requestId} ${overflow.message}; sent to Codex as context_length_exceeded`);
7166
+ return new Response(contextOverflowStream(requestId, overflow.message), {
7167
+ status: 200,
7168
+ headers: {
7169
+ "content-type": "text/event-stream",
7170
+ "cache-control": "no-cache"
7171
+ }
7172
+ });
7173
+ }
7174
+ const message = `Copilot /responses returned ${response.status}`;
7175
+ c.set("requestErrorMessage", message);
7176
+ log.error(`request_id=${requestId} ${message}`);
7177
+ return new Response(detail, {
7178
+ status: response.status,
7179
+ headers: copyForwardedHeaders(response.headers)
7180
+ });
7181
+ }
7182
+ } catch (error) {
7183
+ if (error instanceof ProxyNotImplementedError) {
7184
+ c.set("requestErrorMessage", error.message);
7185
+ return c.json({ error: {
7186
+ message: error.message,
7187
+ type: "server_error",
7188
+ code: "relay_not_configured"
7189
+ } }, 501);
7190
+ }
7191
+ if (!(error instanceof HTTPError)) throw error;
7192
+ const message = sanitizeTerminalString(error.message);
7193
+ c.set("requestErrorMessage", message);
7194
+ log.error(`request_id=${requestId} ${message}`);
7195
+ if (error.response.status === 400 && trace?.manifest.exchanges.length === 0) trace.recordFailure("local-validation");
7196
+ return new Response(await error.response.arrayBuffer(), {
7197
+ status: error.response.status,
7198
+ headers: copyForwardedHeaders(error.response.headers)
7199
+ });
7200
+ }
7201
+ const headers = copyForwardedHeaders(response.headers);
7202
+ if (stream) headers.set("cache-control", "no-cache");
7203
+ if (!response.body) return new Response(null, {
7204
+ status: response.status,
7205
+ headers
7206
+ });
7207
+ trace?.deferHandler();
7208
+ return new Response(settleWhenDone(response.body, trace), {
7209
+ status: response.status,
7210
+ headers
7211
+ });
7212
+ });
7213
+
6884
7214
  //#endregion
6885
7215
  //#region src/server.ts
6886
7216
  const loggedRequestHeaders = [
@@ -6935,6 +7265,11 @@ const presentsApiKey = (headers, apiKey) => {
6935
7265
  for (const presented of [headers.get("x-api-key"), bearer]) if (typeof presented === "string" && timingSafeEqual(sha256(presented), expected)) matched = true;
6936
7266
  return matched;
6937
7267
  };
7268
+ const jsonBodyPaths = new Set([
7269
+ "/v1/messages",
7270
+ "/v1/messages/count_tokens",
7271
+ "/v1/responses"
7272
+ ]);
6938
7273
  const isOpenProbe = (method, path$1) => method === "GET" && path$1 === "/healthz" || (method === "GET" || method === "HEAD") && path$1 === "/api/hello";
6939
7274
  const createServer = (config) => {
6940
7275
  const app = new Hono();
@@ -6991,7 +7326,7 @@ const createServer = (config) => {
6991
7326
  }
6992
7327
  }, 401, { "www-authenticate": "Bearer" });
6993
7328
  }
6994
- if (c.req.method === "POST" && (c.req.path === "/v1/messages" || c.req.path === "/v1/messages/count_tokens") && (incoming ? Boolean(incoming.headers["transfer-encoding"]) || Number(incoming.headers["content-length"] ?? 0) > 0 : c.req.raw.body !== null) && c.req.header("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
7329
+ if (c.req.method === "POST" && jsonBodyPaths.has(c.req.path) && (incoming ? Boolean(incoming.headers["transfer-encoding"]) || Number(incoming.headers["content-length"] ?? 0) > 0 : c.req.raw.body !== null) && c.req.header("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
6995
7330
  const message = "Content-Type must be application/json";
6996
7331
  c.set("requestErrorMessage", message);
6997
7332
  return c.json({ error: { message } }, 415);
@@ -7035,6 +7370,7 @@ const createServer = (config) => {
7035
7370
  }));
7036
7371
  app.on(["GET", "HEAD"], "/api/hello", (c) => c.body(null, 200));
7037
7372
  app.route("/v1", claudeRoutes);
7373
+ app.route("/v1", responsesRoutes);
7038
7374
  app.notFound(async (c) => {
7039
7375
  const message = "Unsupported Claude API route";
7040
7376
  c.set("requestErrorMessage", message);
@@ -7741,7 +8077,7 @@ const readCaptureFile = async (directory, file, maximum, optional = false) => {
7741
8077
  const handle = await fs.open(filePath, fs.constants.O_RDONLY | (process.platform === "win32" ? 0 : fs.constants.O_NOFOLLOW));
7742
8078
  try {
7743
8079
  const opened = await handle.stat();
7744
- requireValid(opened.isFile() && opened.ino === before.ino && opened.dev === before.dev && opened.size === before.size);
8080
+ requireValid(opened.isFile() && sameOpenedFile(opened, before) && opened.size === before.size);
7745
8081
  const bytes = Buffer.alloc(opened.size);
7746
8082
  let offset = 0;
7747
8083
  while (offset < bytes.length) {
@@ -7750,7 +8086,7 @@ const readCaptureFile = async (directory, file, maximum, optional = false) => {
7750
8086
  offset += read.bytesRead;
7751
8087
  }
7752
8088
  const after = await fs.lstat(filePath);
7753
- requireValid(!after.isSymbolicLink() && after.ino === opened.ino && after.dev === opened.dev && after.size === opened.size && after.mtimeMs === opened.mtimeMs);
8089
+ requireValid(!after.isSymbolicLink() && sameOpenedFile(after, opened) && after.size === opened.size && after.mtimeMs === opened.mtimeMs);
7754
8090
  return bytes;
7755
8091
  } finally {
7756
8092
  await handle.close();
@@ -9498,7 +9834,7 @@ const usage = defineCommand({
9498
9834
  await runMain(defineCommand({
9499
9835
  meta: {
9500
9836
  name: "copilot-relay",
9501
- description: "Yet, just another relay for Claude Code to use a GitHub Copilot subscription."
9837
+ description: "Yet, just another relay for Claude Code and Codex to use a GitHub Copilot subscription."
9502
9838
  },
9503
9839
  subCommands: {
9504
9840
  auth,