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/README.md +9 -9
- package/dist/main.js +381 -45
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
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() || !
|
|
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$
|
|
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 &&
|
|
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() || !
|
|
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() || !
|
|
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() || !
|
|
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 || !
|
|
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 || !
|
|
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 (!
|
|
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 (!
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3393
|
-
const capabilities = isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4583
|
-
const upstream = isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
4608
|
+
const usage$1 = isRecord$4(upstream.usage) ? upstream.usage : {};
|
|
4593
4609
|
const inputTokens = reportedTokens(usage$1.input_tokens);
|
|
4594
|
-
const cachedInputTokens = reportedTokens(isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
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,
|