@openhoo/hoopilot 2.1.6 → 2.1.8
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 +85 -4
- package/dist/{chunk-4ZG5QEYJ.js → chunk-2GLKVNAA.js} +5 -2
- package/dist/chunk-2GLKVNAA.js.map +1 -0
- package/dist/cli.js +461 -394
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.d.ts +4 -64
- package/dist/index.js +2746 -3171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4ZG5QEYJ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_MODEL,
|
|
3
4
|
asRecord,
|
|
4
5
|
envValue,
|
|
5
6
|
errorMessage,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
safeJsonParse,
|
|
16
17
|
trimTrailingSlash,
|
|
17
18
|
truncatedResponseText
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-2GLKVNAA.js";
|
|
19
20
|
|
|
20
21
|
// src/cli.ts
|
|
21
22
|
import { spawn } from "child_process";
|
|
@@ -666,6 +667,32 @@ var DEFAULT_LOG_FORMAT = "pretty";
|
|
|
666
667
|
var DEFAULT_LOG_LEVEL = "info";
|
|
667
668
|
var LOG_FORMATS = ["json", "pretty"];
|
|
668
669
|
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
670
|
+
var PRETTY_INLINE_FIELDS = [
|
|
671
|
+
"component",
|
|
672
|
+
"command",
|
|
673
|
+
"event",
|
|
674
|
+
"method",
|
|
675
|
+
"path",
|
|
676
|
+
"status",
|
|
677
|
+
"durationMs",
|
|
678
|
+
"stream",
|
|
679
|
+
"route",
|
|
680
|
+
"requestId",
|
|
681
|
+
"upstreamPath",
|
|
682
|
+
"upstreamStatus",
|
|
683
|
+
"url",
|
|
684
|
+
"baseUrl",
|
|
685
|
+
"origin",
|
|
686
|
+
"currentVersion",
|
|
687
|
+
"installKind",
|
|
688
|
+
"latestVersion",
|
|
689
|
+
"assetName",
|
|
690
|
+
"count",
|
|
691
|
+
"plan",
|
|
692
|
+
"apiBaseUrl",
|
|
693
|
+
"authStorePath"
|
|
694
|
+
];
|
|
695
|
+
var PRETTY_IGNORED_FIELDS = ["pid", "hostname", "service", ...PRETTY_INLINE_FIELDS];
|
|
669
696
|
var REDACT_PATHS = [
|
|
670
697
|
"apiKey",
|
|
671
698
|
"authorization",
|
|
@@ -729,9 +756,11 @@ function createHoopilotLogger(options = {}) {
|
|
|
729
756
|
// stream's TTY-ness is unknown, so default to no color there.
|
|
730
757
|
colorize: options.colorize ?? (options.stream ? false : process.stdout.isTTY),
|
|
731
758
|
destination: options.stream ?? 1,
|
|
732
|
-
ignore: "
|
|
759
|
+
ignore: PRETTY_IGNORED_FIELDS.join(","),
|
|
760
|
+
levelFirst: true,
|
|
761
|
+
messageFormat: formatPrettyMessage,
|
|
733
762
|
singleLine: true,
|
|
734
|
-
translateTime: "SYS:
|
|
763
|
+
translateTime: "SYS:HH:MM:ss"
|
|
735
764
|
})
|
|
736
765
|
)
|
|
737
766
|
);
|
|
@@ -777,6 +806,45 @@ function errorDetails(error) {
|
|
|
777
806
|
}
|
|
778
807
|
return { message: String(error) };
|
|
779
808
|
}
|
|
809
|
+
function formatPrettyMessage(log, messageKey) {
|
|
810
|
+
const message = formatPrettyLogMessage(log[messageKey]);
|
|
811
|
+
const fields = PRETTY_INLINE_FIELDS.flatMap((field) => {
|
|
812
|
+
const value = log[field];
|
|
813
|
+
if (value === void 0) {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
return `${prettyFieldLabel(field)}=${formatPrettyFieldValue(field, value)}`;
|
|
817
|
+
});
|
|
818
|
+
return fields.length > 0 ? `${message} ${fields.join(" ")}` : message;
|
|
819
|
+
}
|
|
820
|
+
function formatPrettyLogMessage(value) {
|
|
821
|
+
return typeof value === "string" ? value : formatPrettyValue(value);
|
|
822
|
+
}
|
|
823
|
+
function prettyFieldLabel(field) {
|
|
824
|
+
return field === "durationMs" ? "duration" : field;
|
|
825
|
+
}
|
|
826
|
+
function formatPrettyFieldValue(field, value) {
|
|
827
|
+
const formatted = formatPrettyValue(value);
|
|
828
|
+
return field === "durationMs" && typeof value === "number" ? `${formatted}ms` : formatted;
|
|
829
|
+
}
|
|
830
|
+
function formatPrettyValue(value) {
|
|
831
|
+
if (typeof value === "number") {
|
|
832
|
+
return Number.isFinite(value) ? String(value) : JSON.stringify(value);
|
|
833
|
+
}
|
|
834
|
+
if (typeof value === "boolean") {
|
|
835
|
+
return String(value);
|
|
836
|
+
}
|
|
837
|
+
if (typeof value === "string") {
|
|
838
|
+
return isBarePrettyValue(value) ? value : JSON.stringify(value);
|
|
839
|
+
}
|
|
840
|
+
if (value === null) {
|
|
841
|
+
return "null";
|
|
842
|
+
}
|
|
843
|
+
return JSON.stringify(value) ?? String(value);
|
|
844
|
+
}
|
|
845
|
+
function isBarePrettyValue(value) {
|
|
846
|
+
return /^[A-Za-z0-9._~:/?#[\]@!$&'()*+,;=%-]+$/.test(value);
|
|
847
|
+
}
|
|
780
848
|
function isLogFormat(value) {
|
|
781
849
|
return LOG_FORMATS.includes(value);
|
|
782
850
|
}
|
|
@@ -785,11 +853,51 @@ function isLogLevel(value) {
|
|
|
785
853
|
}
|
|
786
854
|
|
|
787
855
|
// src/server.ts
|
|
788
|
-
import { createHash, timingSafeEqual } from "crypto";
|
|
789
856
|
import { Elysia } from "elysia";
|
|
790
857
|
|
|
858
|
+
// src/sse.ts
|
|
859
|
+
function parseSseBlock(block) {
|
|
860
|
+
let event = "message";
|
|
861
|
+
const data = [];
|
|
862
|
+
for (const line of block.split(/\r?\n/)) {
|
|
863
|
+
const trimmed = line.trim();
|
|
864
|
+
if (trimmed.startsWith("event:")) {
|
|
865
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const value = sseDataFromLine(trimmed);
|
|
869
|
+
if (value !== void 0) {
|
|
870
|
+
data.push(value);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return { data: data.join("\n"), event };
|
|
874
|
+
}
|
|
875
|
+
function sseDataFromLine(line) {
|
|
876
|
+
const trimmed = line.trim();
|
|
877
|
+
if (!trimmed.startsWith("data:")) {
|
|
878
|
+
return void 0;
|
|
879
|
+
}
|
|
880
|
+
return trimmed.slice("data:".length).trim();
|
|
881
|
+
}
|
|
882
|
+
function encodeSseEvent(event, data) {
|
|
883
|
+
if (data === "[DONE]") {
|
|
884
|
+
return "data: [DONE]\n\n";
|
|
885
|
+
}
|
|
886
|
+
return `event: ${event}
|
|
887
|
+
data: ${JSON.stringify(data)}
|
|
888
|
+
|
|
889
|
+
`;
|
|
890
|
+
}
|
|
891
|
+
function encodeSseData(data) {
|
|
892
|
+
if (data === "[DONE]") {
|
|
893
|
+
return "data: [DONE]\n\n";
|
|
894
|
+
}
|
|
895
|
+
return `data: ${JSON.stringify(data)}
|
|
896
|
+
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
|
|
791
900
|
// src/openai.ts
|
|
792
|
-
var DEFAULT_MODEL = "gpt-4.1";
|
|
793
901
|
var COMPACTION_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
794
902
|
|
|
795
903
|
Include:
|
|
@@ -904,7 +1012,7 @@ function responsesCompactionSseText(upstreamText, isSse, model) {
|
|
|
904
1012
|
const item = compactionOutputItem(compactionSummaryText(upstreamText, isSse));
|
|
905
1013
|
const createdAt = epochSeconds();
|
|
906
1014
|
let sequenceNumber = 0;
|
|
907
|
-
const event = (name, data) =>
|
|
1015
|
+
const event = (name, data) => encodeSseEvent(name, data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ });
|
|
908
1016
|
return [
|
|
909
1017
|
event("response.created", {
|
|
910
1018
|
response: baseStreamResponse(responseId, model, createdAt, "in_progress", []),
|
|
@@ -942,7 +1050,7 @@ function compactionSummaryTextFromResponsesSse(text) {
|
|
|
942
1050
|
let deltas = "";
|
|
943
1051
|
let completedResponse;
|
|
944
1052
|
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
945
|
-
const data = block
|
|
1053
|
+
const { data } = parseSseBlock(block);
|
|
946
1054
|
if (!data || data === "[DONE]") {
|
|
947
1055
|
continue;
|
|
948
1056
|
}
|
|
@@ -989,7 +1097,7 @@ function completionStreamFromChatStream(chatStream) {
|
|
|
989
1097
|
return new ReadableStream({
|
|
990
1098
|
async start(controller) {
|
|
991
1099
|
const enqueue = (data) => {
|
|
992
|
-
controller.enqueue(encoder.encode(
|
|
1100
|
+
controller.enqueue(encoder.encode(encodeSseData(data)));
|
|
993
1101
|
};
|
|
994
1102
|
const markTerminal = () => {
|
|
995
1103
|
sawTerminalEvent = true;
|
|
@@ -1030,7 +1138,7 @@ function completionSseTextFromChatSseText(text) {
|
|
|
1030
1138
|
const chunks = [];
|
|
1031
1139
|
let sawTerminalEvent = false;
|
|
1032
1140
|
const enqueue = (data) => {
|
|
1033
|
-
chunks.push(
|
|
1141
|
+
chunks.push(encodeSseData(data));
|
|
1034
1142
|
};
|
|
1035
1143
|
const markTerminal = () => {
|
|
1036
1144
|
sawTerminalEvent = true;
|
|
@@ -1233,17 +1341,7 @@ function completionChoices(completion) {
|
|
|
1233
1341
|
return choices.map((choice) => asRecord(choice));
|
|
1234
1342
|
}
|
|
1235
1343
|
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
1236
|
-
|
|
1237
|
-
const dataLines = [];
|
|
1238
|
-
for (const line of block.split(/\r?\n/)) {
|
|
1239
|
-
const trimmed = line.trim();
|
|
1240
|
-
if (trimmed.startsWith("event:")) {
|
|
1241
|
-
event = trimmed.slice("event:".length).trim() || event;
|
|
1242
|
-
} else if (trimmed.startsWith("data:")) {
|
|
1243
|
-
dataLines.push(trimmed.slice("data:".length).trim());
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
const data = dataLines.join("\n");
|
|
1344
|
+
const { data, event } = parseSseBlock(block);
|
|
1247
1345
|
if (!data) {
|
|
1248
1346
|
return;
|
|
1249
1347
|
}
|
|
@@ -1328,23 +1426,6 @@ function baseStreamResponse(id, model, createdAt, status, output) {
|
|
|
1328
1426
|
top_p: null
|
|
1329
1427
|
};
|
|
1330
1428
|
}
|
|
1331
|
-
function encodeSse(event, data) {
|
|
1332
|
-
if (data === "[DONE]") {
|
|
1333
|
-
return "data: [DONE]\n\n";
|
|
1334
|
-
}
|
|
1335
|
-
return `event: ${event}
|
|
1336
|
-
data: ${JSON.stringify(data)}
|
|
1337
|
-
|
|
1338
|
-
`;
|
|
1339
|
-
}
|
|
1340
|
-
function encodeDataSse(data) {
|
|
1341
|
-
if (data === "[DONE]") {
|
|
1342
|
-
return "data: [DONE]\n\n";
|
|
1343
|
-
}
|
|
1344
|
-
return `data: ${JSON.stringify(data)}
|
|
1345
|
-
|
|
1346
|
-
`;
|
|
1347
|
-
}
|
|
1348
1429
|
function epochSeconds() {
|
|
1349
1430
|
return Math.floor(Date.now() / 1e3);
|
|
1350
1431
|
}
|
|
@@ -1398,7 +1479,7 @@ function responsesStreamToAnthropicStream(stream, options) {
|
|
|
1398
1479
|
return new ReadableStream({
|
|
1399
1480
|
async start(controller) {
|
|
1400
1481
|
const enqueue = (event, data) => {
|
|
1401
|
-
controller.enqueue(encoder.encode(
|
|
1482
|
+
controller.enqueue(encoder.encode(encodeSseEvent(event, data)));
|
|
1402
1483
|
};
|
|
1403
1484
|
const reader = stream.getReader();
|
|
1404
1485
|
try {
|
|
@@ -1434,7 +1515,7 @@ function responsesSseTextToAnthropicSseText(text, options) {
|
|
|
1434
1515
|
const chunks = [];
|
|
1435
1516
|
const state = createAnthropicStreamState(options);
|
|
1436
1517
|
const enqueue = (event, data) => {
|
|
1437
|
-
chunks.push(
|
|
1518
|
+
chunks.push(encodeSseEvent(event, data));
|
|
1438
1519
|
};
|
|
1439
1520
|
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
1440
1521
|
if (block.trim()) {
|
|
@@ -2018,19 +2099,6 @@ function stopBlock(block, enqueue) {
|
|
|
2018
2099
|
type: "content_block_stop"
|
|
2019
2100
|
});
|
|
2020
2101
|
}
|
|
2021
|
-
function parseSseBlock(block) {
|
|
2022
|
-
let event = "message";
|
|
2023
|
-
const data = [];
|
|
2024
|
-
for (const line of block.split(/\r?\n/)) {
|
|
2025
|
-
const trimmed = line.trim();
|
|
2026
|
-
if (trimmed.startsWith("event:")) {
|
|
2027
|
-
event = trimmed.slice("event:".length).trim() || event;
|
|
2028
|
-
} else if (trimmed.startsWith("data:")) {
|
|
2029
|
-
data.push(trimmed.slice("data:".length).trim());
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
return { data: data.join("\n"), event };
|
|
2033
|
-
}
|
|
2034
2102
|
function parseToolInput(argumentsText) {
|
|
2035
2103
|
const parsed = parseJsonObject(argumentsText);
|
|
2036
2104
|
return parsed ?? {};
|
|
@@ -2065,12 +2133,6 @@ function textValue(value) {
|
|
|
2065
2133
|
function indexValue(value) {
|
|
2066
2134
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2067
2135
|
}
|
|
2068
|
-
function encodeSse2(event, data) {
|
|
2069
|
-
return `event: ${event}
|
|
2070
|
-
data: ${JSON.stringify(data)}
|
|
2071
|
-
|
|
2072
|
-
`;
|
|
2073
|
-
}
|
|
2074
2136
|
|
|
2075
2137
|
// src/dashboard.ts
|
|
2076
2138
|
var DASHBOARD_HTML = `<!doctype html>
|
|
@@ -2881,115 +2943,353 @@ footer.foot .end { margin-left:auto; }
|
|
|
2881
2943
|
</html>
|
|
2882
2944
|
`;
|
|
2883
2945
|
|
|
2884
|
-
// src/
|
|
2885
|
-
var
|
|
2886
|
-
var
|
|
2887
|
-
var
|
|
2888
|
-
var
|
|
2889
|
-
var
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
function emptyModelTotals() {
|
|
2894
|
-
return { cached: 0, completion: 0, prompt: 0, reasoning: 0, requests: 0, total: 0 };
|
|
2895
|
-
}
|
|
2896
|
-
var MetricsRegistry = class {
|
|
2897
|
-
#startedAtMs;
|
|
2898
|
-
#inFlight = 0;
|
|
2899
|
-
#requests = /* @__PURE__ */ new Map();
|
|
2900
|
-
#durations = /* @__PURE__ */ new Map();
|
|
2901
|
-
#inFlightByRoute = /* @__PURE__ */ new Map();
|
|
2902
|
-
#tokens = /* @__PURE__ */ new Map();
|
|
2903
|
-
#upstream = /* @__PURE__ */ new Map();
|
|
2904
|
-
#copilotQuota;
|
|
2905
|
-
#githubRateLimit = /* @__PURE__ */ new Map();
|
|
2906
|
-
#extraction = { extracted: 0, missing: 0 };
|
|
2907
|
-
constructor(options = {}) {
|
|
2908
|
-
this.#startedAtMs = (options.now ?? Date.now)();
|
|
2946
|
+
// src/http/body.ts
|
|
2947
|
+
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
2948
|
+
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
2949
|
+
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
2950
|
+
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
2951
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
2952
|
+
constructor() {
|
|
2953
|
+
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
2954
|
+
this.name = "RequestBodyTooLargeError";
|
|
2909
2955
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
}
|
|
2956
|
+
};
|
|
2957
|
+
var InvalidJsonError = class extends Error {
|
|
2958
|
+
constructor() {
|
|
2959
|
+
super(INVALID_JSON_MESSAGE);
|
|
2960
|
+
this.name = "InvalidJsonError";
|
|
2916
2961
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
const inFlightForRoute = this.#inFlightByRoute.get(observation.route) ?? 0;
|
|
2923
|
-
if (inFlightForRoute > 1) {
|
|
2924
|
-
this.#inFlightByRoute.set(observation.route, inFlightForRoute - 1);
|
|
2925
|
-
} else if (inFlightForRoute === 1) {
|
|
2926
|
-
this.#inFlightByRoute.delete(observation.route);
|
|
2927
|
-
}
|
|
2928
|
-
const key = labelKey(observation.route, observation.method, String(observation.status));
|
|
2929
|
-
this.#requests.set(key, (this.#requests.get(key) ?? 0) + 1);
|
|
2930
|
-
this.#observeDuration(observation.route, observation.durationMs / 1e3);
|
|
2962
|
+
};
|
|
2963
|
+
var JsonNotObjectError = class extends Error {
|
|
2964
|
+
constructor() {
|
|
2965
|
+
super(JSON_OBJECT_MESSAGE);
|
|
2966
|
+
this.name = "JsonNotObjectError";
|
|
2931
2967
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2968
|
+
};
|
|
2969
|
+
async function readJson(request) {
|
|
2970
|
+
const text = await readRequestText(request);
|
|
2971
|
+
return parseJsonObject2(text);
|
|
2972
|
+
}
|
|
2973
|
+
async function readJsonText(request) {
|
|
2974
|
+
const text = await readRequestText(request);
|
|
2975
|
+
return { json: parseJsonObject2(text), text };
|
|
2976
|
+
}
|
|
2977
|
+
async function readRequestText(request) {
|
|
2978
|
+
const contentLength = request.headers.get("content-length");
|
|
2979
|
+
if (contentLength) {
|
|
2980
|
+
const declaredBytes = Number(contentLength);
|
|
2981
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
|
|
2982
|
+
throw new RequestBodyTooLargeError();
|
|
2943
2983
|
}
|
|
2944
2984
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
const totals = this.#tokens.get(name) ?? emptyModelTotals();
|
|
2949
|
-
totals.requests += 1;
|
|
2950
|
-
totals.prompt += nonNegative(usage.promptTokens);
|
|
2951
|
-
totals.completion += nonNegative(usage.completionTokens);
|
|
2952
|
-
totals.total += nonNegative(usage.totalTokens);
|
|
2953
|
-
totals.reasoning += nonNegative(usage.reasoningTokens ?? 0);
|
|
2954
|
-
totals.cached += nonNegative(usage.cachedTokens ?? 0);
|
|
2955
|
-
this.#tokens.set(name, totals);
|
|
2956
|
-
}
|
|
2957
|
-
/** Record one upstream Copilot call and whether it succeeded. */
|
|
2958
|
-
recordUpstream(path, ok) {
|
|
2959
|
-
const key = labelKey(path, ok ? "ok" : "error");
|
|
2960
|
-
this.#upstream.set(key, (this.#upstream.get(key) ?? 0) + 1);
|
|
2961
|
-
}
|
|
2962
|
-
/** Store the latest Copilot quota so /metrics can expose it as gauges. */
|
|
2963
|
-
recordCopilotQuota(usage) {
|
|
2964
|
-
this.#copilotQuota = usage;
|
|
2985
|
+
const body = request.body;
|
|
2986
|
+
if (!body) {
|
|
2987
|
+
return "";
|
|
2965
2988
|
}
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2989
|
+
const reader = body.getReader();
|
|
2990
|
+
const decoder = new TextDecoder();
|
|
2991
|
+
let bytes = 0;
|
|
2992
|
+
const chunks = [];
|
|
2993
|
+
try {
|
|
2994
|
+
while (true) {
|
|
2995
|
+
const { done, value } = await reader.read();
|
|
2996
|
+
if (done) {
|
|
2997
|
+
const tail = decoder.decode();
|
|
2998
|
+
if (tail) {
|
|
2999
|
+
chunks.push(tail);
|
|
3000
|
+
}
|
|
3001
|
+
return chunks.join("");
|
|
3002
|
+
}
|
|
3003
|
+
bytes += value.byteLength;
|
|
3004
|
+
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
3005
|
+
await reader.cancel().catch(() => {
|
|
3006
|
+
});
|
|
3007
|
+
throw new RequestBodyTooLargeError();
|
|
3008
|
+
}
|
|
3009
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
2974
3010
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
3011
|
+
} finally {
|
|
3012
|
+
reader.releaseLock();
|
|
2977
3013
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
}
|
|
2986
|
-
return cleaned;
|
|
3014
|
+
}
|
|
3015
|
+
function parseJsonObject2(text) {
|
|
3016
|
+
let parsed;
|
|
3017
|
+
try {
|
|
3018
|
+
parsed = JSON.parse(text);
|
|
3019
|
+
} catch {
|
|
3020
|
+
throw new InvalidJsonError();
|
|
2987
3021
|
}
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
return this.#boundedLabel(model, this.#tokens, MAX_TRACKED_MODELS);
|
|
3022
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3023
|
+
throw new JsonNotObjectError();
|
|
2991
3024
|
}
|
|
2992
|
-
|
|
3025
|
+
return parsed;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/http/security.ts
|
|
3029
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
3030
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
|
|
3031
|
+
var MIN_NON_LOOPBACK_API_KEY_LENGTH = 24;
|
|
3032
|
+
var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set([
|
|
3033
|
+
"changeme",
|
|
3034
|
+
"demo",
|
|
3035
|
+
"example",
|
|
3036
|
+
"hoopilot",
|
|
3037
|
+
"local-key",
|
|
3038
|
+
"password",
|
|
3039
|
+
"password123",
|
|
3040
|
+
"secret",
|
|
3041
|
+
"test"
|
|
3042
|
+
]);
|
|
3043
|
+
function corsHeaders() {
|
|
3044
|
+
return {
|
|
3045
|
+
"access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
|
|
3046
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
3047
|
+
"access-control-expose-headers": "x-request-id"
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
function isAuthorized(request, apiKey) {
|
|
3051
|
+
if (!apiKey) {
|
|
3052
|
+
return true;
|
|
3053
|
+
}
|
|
3054
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
3055
|
+
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
3056
|
+
return bearer !== void 0 && secretEquals(bearer, apiKey) || secretEquals(request.headers.get("x-api-key") ?? "", apiKey);
|
|
3057
|
+
}
|
|
3058
|
+
function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
|
|
3059
|
+
if (origin) {
|
|
3060
|
+
return isAllowedOrigin(origin, allowedOrigins) ? void 0 : origin;
|
|
3061
|
+
}
|
|
3062
|
+
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
3063
|
+
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
3064
|
+
}
|
|
3065
|
+
function parseAllowedOrigins(env) {
|
|
3066
|
+
const raw = envValue(env?.HOOPILOT_ALLOWED_ORIGINS);
|
|
3067
|
+
if (!raw) {
|
|
3068
|
+
return /* @__PURE__ */ new Set();
|
|
3069
|
+
}
|
|
3070
|
+
return new Set(
|
|
3071
|
+
raw.split(",").map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
function resolveCorsAllowOrigin(origin, allowedOrigins) {
|
|
3075
|
+
if (!origin) {
|
|
3076
|
+
return "*";
|
|
3077
|
+
}
|
|
3078
|
+
return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
|
|
3079
|
+
}
|
|
3080
|
+
function apiKeyRejectionReason(apiKey) {
|
|
3081
|
+
const normalized = apiKey.trim();
|
|
3082
|
+
if (WELL_KNOWN_DEMO_API_KEYS.has(normalized.toLowerCase())) {
|
|
3083
|
+
return "HOOPILOT_API_KEY is a well-known demo value. Set a strong, unique API key.";
|
|
3084
|
+
}
|
|
3085
|
+
if (normalized.length < MIN_NON_LOOPBACK_API_KEY_LENGTH) {
|
|
3086
|
+
return `HOOPILOT_API_KEY must be at least ${MIN_NON_LOOPBACK_API_KEY_LENGTH} characters when listening on a non-loopback host.`;
|
|
3087
|
+
}
|
|
3088
|
+
if (/^(.)\1+$/.test(normalized)) {
|
|
3089
|
+
return "HOOPILOT_API_KEY must not be a repeated single character. Set a strong, unique API key.";
|
|
3090
|
+
}
|
|
3091
|
+
return void 0;
|
|
3092
|
+
}
|
|
3093
|
+
function isLoopbackHost(host) {
|
|
3094
|
+
return isLoopbackHostname(host);
|
|
3095
|
+
}
|
|
3096
|
+
function urlHost(host) {
|
|
3097
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
3098
|
+
}
|
|
3099
|
+
function secretEquals(candidate, secret) {
|
|
3100
|
+
const a = createHash("sha256").update(candidate).digest();
|
|
3101
|
+
const b = createHash("sha256").update(secret).digest();
|
|
3102
|
+
return timingSafeEqual(a, b);
|
|
3103
|
+
}
|
|
3104
|
+
function isAllowedOrigin(origin, allowedOrigins) {
|
|
3105
|
+
return isLoopbackOrigin(origin) || allowedOrigins.has(origin.toLowerCase());
|
|
3106
|
+
}
|
|
3107
|
+
function isLoopbackOrigin(origin) {
|
|
3108
|
+
try {
|
|
3109
|
+
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
3110
|
+
} catch {
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/http/responses.ts
|
|
3116
|
+
function jsonResponse(body, status = 200) {
|
|
3117
|
+
return new Response(JSON.stringify(body), {
|
|
3118
|
+
headers: {
|
|
3119
|
+
...corsHeaders(),
|
|
3120
|
+
"content-type": "application/json; charset=utf-8"
|
|
3121
|
+
},
|
|
3122
|
+
status
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
function textResponse(body, contentType, status = 200) {
|
|
3126
|
+
return new Response(body, {
|
|
3127
|
+
headers: {
|
|
3128
|
+
...corsHeaders(),
|
|
3129
|
+
"content-type": `${contentType}; charset=utf-8`
|
|
3130
|
+
},
|
|
3131
|
+
status
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
function jsonError(status, code, message) {
|
|
3135
|
+
return jsonResponse(
|
|
3136
|
+
{
|
|
3137
|
+
error: {
|
|
3138
|
+
code,
|
|
3139
|
+
message,
|
|
3140
|
+
type: code
|
|
3141
|
+
}
|
|
3142
|
+
},
|
|
3143
|
+
status
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
function responseFromText(source, text) {
|
|
3147
|
+
return new Response(text, {
|
|
3148
|
+
headers: source.headers,
|
|
3149
|
+
status: source.status,
|
|
3150
|
+
statusText: source.statusText
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
function proxyResponse(upstream) {
|
|
3154
|
+
const headers = new Headers(upstream.headers);
|
|
3155
|
+
headers.delete("content-encoding");
|
|
3156
|
+
headers.delete("content-length");
|
|
3157
|
+
headers.delete("transfer-encoding");
|
|
3158
|
+
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
3159
|
+
headers.set(key, value);
|
|
3160
|
+
}
|
|
3161
|
+
return new Response(upstream.body, {
|
|
3162
|
+
headers,
|
|
3163
|
+
status: upstream.status,
|
|
3164
|
+
statusText: upstream.statusText
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
function upstreamErrorResponse(status, text) {
|
|
3168
|
+
const parsedError = asRecord(asRecord(safeJsonParse(text)).error);
|
|
3169
|
+
if (Object.keys(parsedError).length > 0) {
|
|
3170
|
+
return jsonResponse({ error: parsedError }, status);
|
|
3171
|
+
}
|
|
3172
|
+
return jsonError(status, "copilot_error", text);
|
|
3173
|
+
}
|
|
3174
|
+
function websocketUnsupportedResponse() {
|
|
3175
|
+
const response = jsonError(
|
|
3176
|
+
426,
|
|
3177
|
+
"websocket_not_supported",
|
|
3178
|
+
"Hoopilot does not support Responses WebSocket transport; retry with HTTP Responses API."
|
|
3179
|
+
);
|
|
3180
|
+
response.headers.set("upgrade", "websocket");
|
|
3181
|
+
return response;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/metrics.ts
|
|
3185
|
+
var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
3186
|
+
var DURATION_BUCKETS_SECONDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
|
|
3187
|
+
var USAGE_BUFFER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
3188
|
+
var MAX_TRACKED_MODELS = 200;
|
|
3189
|
+
var MAX_MODEL_LABEL_LENGTH = 200;
|
|
3190
|
+
var MAX_TRACKED_RATELIMIT_RESOURCES = 32;
|
|
3191
|
+
var LABEL_SEPARATOR = "";
|
|
3192
|
+
var UNKNOWN_MODEL = "unknown";
|
|
3193
|
+
function emptyModelTotals() {
|
|
3194
|
+
return { cached: 0, completion: 0, prompt: 0, reasoning: 0, requests: 0, total: 0 };
|
|
3195
|
+
}
|
|
3196
|
+
var MetricsRegistry = class {
|
|
3197
|
+
#startedAtMs;
|
|
3198
|
+
#inFlight = 0;
|
|
3199
|
+
#requests = /* @__PURE__ */ new Map();
|
|
3200
|
+
#durations = /* @__PURE__ */ new Map();
|
|
3201
|
+
#inFlightByRoute = /* @__PURE__ */ new Map();
|
|
3202
|
+
#tokens = /* @__PURE__ */ new Map();
|
|
3203
|
+
#upstream = /* @__PURE__ */ new Map();
|
|
3204
|
+
#copilotQuota;
|
|
3205
|
+
#githubRateLimit = /* @__PURE__ */ new Map();
|
|
3206
|
+
#extraction = { extracted: 0, missing: 0 };
|
|
3207
|
+
constructor(options = {}) {
|
|
3208
|
+
this.#startedAtMs = (options.now ?? Date.now)();
|
|
3209
|
+
}
|
|
3210
|
+
/** Mark a request as started; pair with exactly one {@link observe}. */
|
|
3211
|
+
startRequest(route) {
|
|
3212
|
+
this.#inFlight += 1;
|
|
3213
|
+
if (route) {
|
|
3214
|
+
this.#inFlightByRoute.set(route, (this.#inFlightByRoute.get(route) ?? 0) + 1);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
/** Record a completed request and clear its in-flight slot. */
|
|
3218
|
+
observe(observation) {
|
|
3219
|
+
if (this.#inFlight > 0) {
|
|
3220
|
+
this.#inFlight -= 1;
|
|
3221
|
+
}
|
|
3222
|
+
const inFlightForRoute = this.#inFlightByRoute.get(observation.route) ?? 0;
|
|
3223
|
+
if (inFlightForRoute > 1) {
|
|
3224
|
+
this.#inFlightByRoute.set(observation.route, inFlightForRoute - 1);
|
|
3225
|
+
} else if (inFlightForRoute === 1) {
|
|
3226
|
+
this.#inFlightByRoute.delete(observation.route);
|
|
3227
|
+
}
|
|
3228
|
+
const key = labelKey(observation.route, observation.method, String(observation.status));
|
|
3229
|
+
this.#requests.set(key, (this.#requests.get(key) ?? 0) + 1);
|
|
3230
|
+
this.#observeDuration(observation.route, observation.durationMs / 1e3);
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Record whether one upstream completion reported token usage. `missing`
|
|
3234
|
+
* counts responses that carried no usage object — most often streamed Chat
|
|
3235
|
+
* Completions sent without `stream_options: {"include_usage": true}` — so a
|
|
3236
|
+
* rising miss rate flags clients whose token usage is going unaccounted.
|
|
3237
|
+
*/
|
|
3238
|
+
recordTokenExtraction(extracted) {
|
|
3239
|
+
if (extracted) {
|
|
3240
|
+
this.#extraction.extracted += 1;
|
|
3241
|
+
} else {
|
|
3242
|
+
this.#extraction.missing += 1;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
/** Accumulate token counts for a model from one upstream completion. */
|
|
3246
|
+
recordTokens(model, usage) {
|
|
3247
|
+
const name = this.#modelLabel(model);
|
|
3248
|
+
const totals = this.#tokens.get(name) ?? emptyModelTotals();
|
|
3249
|
+
totals.requests += 1;
|
|
3250
|
+
totals.prompt += nonNegative(usage.promptTokens);
|
|
3251
|
+
totals.completion += nonNegative(usage.completionTokens);
|
|
3252
|
+
totals.total += nonNegative(usage.totalTokens);
|
|
3253
|
+
totals.reasoning += nonNegative(usage.reasoningTokens ?? 0);
|
|
3254
|
+
totals.cached += nonNegative(usage.cachedTokens ?? 0);
|
|
3255
|
+
this.#tokens.set(name, totals);
|
|
3256
|
+
}
|
|
3257
|
+
/** Record one upstream Copilot call and whether it succeeded. */
|
|
3258
|
+
recordUpstream(path, ok) {
|
|
3259
|
+
const key = labelKey(path, ok ? "ok" : "error");
|
|
3260
|
+
this.#upstream.set(key, (this.#upstream.get(key) ?? 0) + 1);
|
|
3261
|
+
}
|
|
3262
|
+
/** Store the latest Copilot quota so /metrics can expose it as gauges. */
|
|
3263
|
+
recordCopilotQuota(usage) {
|
|
3264
|
+
this.#copilotQuota = usage;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Store the latest GitHub REST rate-limit budget, keyed by its resource bucket.
|
|
3268
|
+
* A no-op when `rateLimit` is undefined (the response carried no rate-limit
|
|
3269
|
+
* headers) so callers can pass {@link parseRateLimitHeaders} output directly.
|
|
3270
|
+
*/
|
|
3271
|
+
recordGithubRateLimit(rateLimit) {
|
|
3272
|
+
if (!rateLimit) {
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const resource = this.#rateLimitResource(rateLimit.resource);
|
|
3276
|
+
this.#githubRateLimit.set(resource, { ...rateLimit, resource });
|
|
3277
|
+
}
|
|
3278
|
+
// Clean a raw value into a bounded exposition-format label: cap its length,
|
|
3279
|
+
// strip characters that would corrupt the format, and fold overflow past the
|
|
3280
|
+
// cardinality limit into UNKNOWN_MODEL so the series count stays bounded.
|
|
3281
|
+
#boundedLabel(value, tracked, maxEntries) {
|
|
3282
|
+
const cleaned = cleanLabel(value).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
|
|
3283
|
+
if (!tracked.has(cleaned) && tracked.size >= maxEntries) {
|
|
3284
|
+
return UNKNOWN_MODEL;
|
|
3285
|
+
}
|
|
3286
|
+
return cleaned;
|
|
3287
|
+
}
|
|
3288
|
+
// The model can originate from a (possibly hostile) client request.
|
|
3289
|
+
#modelLabel(model) {
|
|
3290
|
+
return this.#boundedLabel(model, this.#tokens, MAX_TRACKED_MODELS);
|
|
3291
|
+
}
|
|
3292
|
+
// The resource comes from a trusted upstream header, but is bounded the same way.
|
|
2993
3293
|
#rateLimitResource(resource) {
|
|
2994
3294
|
return this.#boundedLabel(resource, this.#githubRateLimit, MAX_TRACKED_RATELIMIT_RESOURCES);
|
|
2995
3295
|
}
|
|
@@ -3484,11 +3784,7 @@ function safeFinishAccumulator(accumulator) {
|
|
|
3484
3784
|
}
|
|
3485
3785
|
}
|
|
3486
3786
|
function considerSseLine(line, consider) {
|
|
3487
|
-
const
|
|
3488
|
-
if (!trimmed.startsWith("data:")) {
|
|
3489
|
-
return;
|
|
3490
|
-
}
|
|
3491
|
-
const data = trimmed.slice("data:".length).trim();
|
|
3787
|
+
const data = sseDataFromLine(line);
|
|
3492
3788
|
if (!data || data === "[DONE]") {
|
|
3493
3789
|
return;
|
|
3494
3790
|
}
|
|
@@ -3600,24 +3896,7 @@ async function getVersion() {
|
|
|
3600
3896
|
// src/server.ts
|
|
3601
3897
|
var DEFAULT_HOST = "127.0.0.1";
|
|
3602
3898
|
var DEFAULT_PORT = 4141;
|
|
3603
|
-
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Cross-origin browser requests are blocked unless the Origin is loopback or listed in HOOPILOT_ALLOWED_ORIGINS.";
|
|
3604
|
-
var MIN_NON_LOOPBACK_API_KEY_LENGTH = 24;
|
|
3605
|
-
var WELL_KNOWN_DEMO_API_KEYS = /* @__PURE__ */ new Set([
|
|
3606
|
-
"changeme",
|
|
3607
|
-
"demo",
|
|
3608
|
-
"example",
|
|
3609
|
-
"hoopilot",
|
|
3610
|
-
"local-key",
|
|
3611
|
-
"password",
|
|
3612
|
-
"password123",
|
|
3613
|
-
"secret",
|
|
3614
|
-
"test"
|
|
3615
|
-
]);
|
|
3616
|
-
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
3617
|
-
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
3618
|
-
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
3619
3899
|
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
3620
|
-
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
3621
3900
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
3622
3901
|
var DASHBOARD_USAGE_VIEW = "dashboard";
|
|
3623
3902
|
var DASHBOARD_EXCLUDED_ROUTES = [
|
|
@@ -3628,24 +3907,6 @@ var DASHBOARD_EXCLUDED_ROUTES = [
|
|
|
3628
3907
|
"usage"
|
|
3629
3908
|
];
|
|
3630
3909
|
var DASHBOARD_EXCLUDED_UPSTREAM_PATHS = ["/copilot_internal/user"];
|
|
3631
|
-
var RequestBodyTooLargeError = class extends Error {
|
|
3632
|
-
constructor() {
|
|
3633
|
-
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
3634
|
-
this.name = "RequestBodyTooLargeError";
|
|
3635
|
-
}
|
|
3636
|
-
};
|
|
3637
|
-
var InvalidJsonError = class extends Error {
|
|
3638
|
-
constructor() {
|
|
3639
|
-
super(INVALID_JSON_MESSAGE);
|
|
3640
|
-
this.name = "InvalidJsonError";
|
|
3641
|
-
}
|
|
3642
|
-
};
|
|
3643
|
-
var JsonNotObjectError = class extends Error {
|
|
3644
|
-
constructor() {
|
|
3645
|
-
super(JSON_OBJECT_MESSAGE);
|
|
3646
|
-
this.name = "JsonNotObjectError";
|
|
3647
|
-
}
|
|
3648
|
-
};
|
|
3649
3910
|
function createHoopilotHandler(options = {}) {
|
|
3650
3911
|
const client = new CopilotClient(options);
|
|
3651
3912
|
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
@@ -4128,13 +4389,6 @@ async function responseWithObservedUsage(response, fallbackModel, recordTokens,
|
|
|
4128
4389
|
}
|
|
4129
4390
|
return observeResponseUsage(response, fallbackModel, recordTokens, signal, recordExtraction);
|
|
4130
4391
|
}
|
|
4131
|
-
function responseFromText(source, text) {
|
|
4132
|
-
return new Response(text, {
|
|
4133
|
-
headers: source.headers,
|
|
4134
|
-
status: source.status,
|
|
4135
|
-
statusText: source.statusText
|
|
4136
|
-
});
|
|
4137
|
-
}
|
|
4138
4392
|
async function proxyError(upstream, logger) {
|
|
4139
4393
|
const text = await upstream.text();
|
|
4140
4394
|
if (isUpstreamAuthStatus(upstream.status)) {
|
|
@@ -4150,201 +4404,12 @@ async function proxyError(upstream, logger) {
|
|
|
4150
4404
|
);
|
|
4151
4405
|
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
4152
4406
|
}
|
|
4153
|
-
function proxyResponse(upstream) {
|
|
4154
|
-
const headers = new Headers(upstream.headers);
|
|
4155
|
-
headers.delete("content-encoding");
|
|
4156
|
-
headers.delete("content-length");
|
|
4157
|
-
headers.delete("transfer-encoding");
|
|
4158
|
-
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
4159
|
-
headers.set(key, value);
|
|
4160
|
-
}
|
|
4161
|
-
return new Response(upstream.body, {
|
|
4162
|
-
headers,
|
|
4163
|
-
status: upstream.status,
|
|
4164
|
-
statusText: upstream.statusText
|
|
4165
|
-
});
|
|
4166
|
-
}
|
|
4167
|
-
async function readJson(request) {
|
|
4168
|
-
const text = await readRequestText(request);
|
|
4169
|
-
return parseJsonObject2(text);
|
|
4170
|
-
}
|
|
4171
|
-
function parseJsonObject2(text) {
|
|
4172
|
-
let parsed;
|
|
4173
|
-
try {
|
|
4174
|
-
parsed = JSON.parse(text);
|
|
4175
|
-
} catch {
|
|
4176
|
-
throw new InvalidJsonError();
|
|
4177
|
-
}
|
|
4178
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4179
|
-
throw new JsonNotObjectError();
|
|
4180
|
-
}
|
|
4181
|
-
return parsed;
|
|
4182
|
-
}
|
|
4183
|
-
async function readJsonText(request) {
|
|
4184
|
-
const text = await readRequestText(request);
|
|
4185
|
-
return { json: parseJsonObject2(text), text };
|
|
4186
|
-
}
|
|
4187
|
-
async function readRequestText(request) {
|
|
4188
|
-
const contentLength = request.headers.get("content-length");
|
|
4189
|
-
if (contentLength) {
|
|
4190
|
-
const declaredBytes = Number(contentLength);
|
|
4191
|
-
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
|
|
4192
|
-
throw new RequestBodyTooLargeError();
|
|
4193
|
-
}
|
|
4194
|
-
}
|
|
4195
|
-
const body = request.body;
|
|
4196
|
-
if (!body) {
|
|
4197
|
-
return "";
|
|
4198
|
-
}
|
|
4199
|
-
const reader = body.getReader();
|
|
4200
|
-
const decoder = new TextDecoder();
|
|
4201
|
-
let bytes = 0;
|
|
4202
|
-
const chunks = [];
|
|
4203
|
-
try {
|
|
4204
|
-
while (true) {
|
|
4205
|
-
const { done, value } = await reader.read();
|
|
4206
|
-
if (done) {
|
|
4207
|
-
const tail = decoder.decode();
|
|
4208
|
-
if (tail) {
|
|
4209
|
-
chunks.push(tail);
|
|
4210
|
-
}
|
|
4211
|
-
return chunks.join("");
|
|
4212
|
-
}
|
|
4213
|
-
bytes += value.byteLength;
|
|
4214
|
-
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
4215
|
-
await reader.cancel().catch(() => {
|
|
4216
|
-
});
|
|
4217
|
-
throw new RequestBodyTooLargeError();
|
|
4218
|
-
}
|
|
4219
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
4220
|
-
}
|
|
4221
|
-
} finally {
|
|
4222
|
-
reader.releaseLock();
|
|
4223
|
-
}
|
|
4224
|
-
}
|
|
4225
|
-
function jsonResponse(body, status = 200) {
|
|
4226
|
-
return new Response(JSON.stringify(body), {
|
|
4227
|
-
headers: {
|
|
4228
|
-
...corsHeaders(),
|
|
4229
|
-
"content-type": "application/json; charset=utf-8"
|
|
4230
|
-
},
|
|
4231
|
-
status
|
|
4232
|
-
});
|
|
4233
|
-
}
|
|
4234
|
-
function textResponse(body, contentType, status = 200) {
|
|
4235
|
-
return new Response(body, {
|
|
4236
|
-
headers: {
|
|
4237
|
-
...corsHeaders(),
|
|
4238
|
-
"content-type": `${contentType}; charset=utf-8`
|
|
4239
|
-
},
|
|
4240
|
-
status
|
|
4241
|
-
});
|
|
4242
|
-
}
|
|
4243
|
-
function jsonError(status, code, message) {
|
|
4244
|
-
return jsonResponse(
|
|
4245
|
-
{
|
|
4246
|
-
error: {
|
|
4247
|
-
code,
|
|
4248
|
-
message,
|
|
4249
|
-
type: code
|
|
4250
|
-
}
|
|
4251
|
-
},
|
|
4252
|
-
status
|
|
4253
|
-
);
|
|
4254
|
-
}
|
|
4255
|
-
function upstreamErrorResponse(status, text) {
|
|
4256
|
-
const parsedError = asRecord(asRecord(safeJsonParse(text)).error);
|
|
4257
|
-
if (Object.keys(parsedError).length > 0) {
|
|
4258
|
-
return jsonResponse({ error: parsedError }, status);
|
|
4259
|
-
}
|
|
4260
|
-
return jsonError(status, "copilot_error", text);
|
|
4261
|
-
}
|
|
4262
|
-
function websocketUnsupportedResponse() {
|
|
4263
|
-
const response = jsonError(
|
|
4264
|
-
426,
|
|
4265
|
-
"websocket_not_supported",
|
|
4266
|
-
"Hoopilot does not support Responses WebSocket transport; retry with HTTP Responses API."
|
|
4267
|
-
);
|
|
4268
|
-
response.headers.set("upgrade", "websocket");
|
|
4269
|
-
return response;
|
|
4270
|
-
}
|
|
4271
|
-
function corsHeaders() {
|
|
4272
|
-
return {
|
|
4273
|
-
"access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
|
|
4274
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
4275
|
-
"access-control-expose-headers": "x-request-id"
|
|
4276
|
-
};
|
|
4277
|
-
}
|
|
4278
|
-
function secretEquals(candidate, secret) {
|
|
4279
|
-
const a = createHash("sha256").update(candidate).digest();
|
|
4280
|
-
const b = createHash("sha256").update(secret).digest();
|
|
4281
|
-
return timingSafeEqual(a, b);
|
|
4282
|
-
}
|
|
4283
|
-
function isAuthorized(request, apiKey) {
|
|
4284
|
-
if (!apiKey) {
|
|
4285
|
-
return true;
|
|
4286
|
-
}
|
|
4287
|
-
const authorization = request.headers.get("authorization") ?? "";
|
|
4288
|
-
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
4289
|
-
return bearer !== void 0 && secretEquals(bearer, apiKey) || secretEquals(request.headers.get("x-api-key") ?? "", apiKey);
|
|
4290
|
-
}
|
|
4291
|
-
function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
|
|
4292
|
-
if (origin) {
|
|
4293
|
-
return isAllowedOrigin(origin, allowedOrigins) ? void 0 : origin;
|
|
4294
|
-
}
|
|
4295
|
-
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
4296
|
-
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
4297
|
-
}
|
|
4298
|
-
function parseAllowedOrigins(env) {
|
|
4299
|
-
const raw = envValue(env?.HOOPILOT_ALLOWED_ORIGINS);
|
|
4300
|
-
if (!raw) {
|
|
4301
|
-
return /* @__PURE__ */ new Set();
|
|
4302
|
-
}
|
|
4303
|
-
return new Set(
|
|
4304
|
-
raw.split(",").map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)
|
|
4305
|
-
);
|
|
4306
|
-
}
|
|
4307
|
-
function isAllowedOrigin(origin, allowedOrigins) {
|
|
4308
|
-
return isLoopbackOrigin(origin) || allowedOrigins.has(origin.toLowerCase());
|
|
4309
|
-
}
|
|
4310
|
-
function resolveCorsAllowOrigin(origin, allowedOrigins) {
|
|
4311
|
-
if (!origin) {
|
|
4312
|
-
return "*";
|
|
4313
|
-
}
|
|
4314
|
-
return isAllowedOrigin(origin, allowedOrigins) ? origin : void 0;
|
|
4315
|
-
}
|
|
4316
|
-
function apiKeyRejectionReason(apiKey) {
|
|
4317
|
-
const normalized = apiKey.trim();
|
|
4318
|
-
if (WELL_KNOWN_DEMO_API_KEYS.has(normalized.toLowerCase())) {
|
|
4319
|
-
return "HOOPILOT_API_KEY is a well-known demo value. Set a strong, unique API key.";
|
|
4320
|
-
}
|
|
4321
|
-
if (normalized.length < MIN_NON_LOOPBACK_API_KEY_LENGTH) {
|
|
4322
|
-
return `HOOPILOT_API_KEY must be at least ${MIN_NON_LOOPBACK_API_KEY_LENGTH} characters when listening on a non-loopback host.`;
|
|
4323
|
-
}
|
|
4324
|
-
if (/^(.)\1+$/.test(normalized)) {
|
|
4325
|
-
return "HOOPILOT_API_KEY must not be a repeated single character. Set a strong, unique API key.";
|
|
4326
|
-
}
|
|
4327
|
-
return void 0;
|
|
4328
|
-
}
|
|
4329
4407
|
function isUpstreamAuthStatus(status) {
|
|
4330
4408
|
return status === 401 || status === 403;
|
|
4331
4409
|
}
|
|
4332
4410
|
function upstreamAuthMessage(message) {
|
|
4333
4411
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
4334
4412
|
}
|
|
4335
|
-
function isLoopbackHost(host) {
|
|
4336
|
-
return isLoopbackHostname(host);
|
|
4337
|
-
}
|
|
4338
|
-
function urlHost(host) {
|
|
4339
|
-
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
4340
|
-
}
|
|
4341
|
-
function isLoopbackOrigin(origin) {
|
|
4342
|
-
try {
|
|
4343
|
-
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
4344
|
-
} catch {
|
|
4345
|
-
return false;
|
|
4346
|
-
}
|
|
4347
|
-
}
|
|
4348
4413
|
function normalizeServerPort(value) {
|
|
4349
4414
|
const port = Number(value);
|
|
4350
4415
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
@@ -5180,6 +5245,7 @@ async function runUpdate(currentVersion, logger) {
|
|
|
5180
5245
|
}
|
|
5181
5246
|
|
|
5182
5247
|
// src/cli.ts
|
|
5248
|
+
var COPILOT_VERIFY_TIMEOUT_MS = 15e3;
|
|
5183
5249
|
async function main2(argv = Bun.argv.slice(2)) {
|
|
5184
5250
|
cleanupOldBinary();
|
|
5185
5251
|
const command = argv[0];
|
|
@@ -5520,7 +5586,8 @@ async function verifyCopilotOAuthToken(token, options = {}) {
|
|
|
5520
5586
|
const fetcher = options.fetch ?? fetch;
|
|
5521
5587
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
5522
5588
|
headers: applyCopilotHeaders(new Headers(), token),
|
|
5523
|
-
method: "GET"
|
|
5589
|
+
method: "GET",
|
|
5590
|
+
signal: AbortSignal.timeout(options.verifyTimeoutMs ?? COPILOT_VERIFY_TIMEOUT_MS)
|
|
5524
5591
|
});
|
|
5525
5592
|
if (!response.ok) {
|
|
5526
5593
|
await throwForCopilotResponse(response, "GitHub Copilot API verification");
|