@openhoo/hoopilot 1.3.0 → 2.1.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 +67 -23
- package/dist/{chunk-JU6F5L34.js → chunk-6ALEIJJM.js} +82 -20
- package/dist/chunk-6ALEIJJM.js.map +1 -0
- package/dist/cli.js +394 -403
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.js +410 -354
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-JU6F5L34.js.map +0 -1
- package/dist/index.cjs +0 -4653
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -388
package/dist/index.js
CHANGED
|
@@ -35,8 +35,12 @@ function parseUrl(rawUrl) {
|
|
|
35
35
|
}
|
|
36
36
|
return url;
|
|
37
37
|
}
|
|
38
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
39
|
+
function isLoopbackHostname(host) {
|
|
40
|
+
return LOOPBACK_HOSTNAMES.has(host);
|
|
41
|
+
}
|
|
38
42
|
function isLoopbackHttpUrl(url) {
|
|
39
|
-
return url.protocol === "http:" && (url.hostname
|
|
43
|
+
return url.protocol === "http:" && isLoopbackHostname(url.hostname);
|
|
40
44
|
}
|
|
41
45
|
async function truncatedResponseText(response, max = 500) {
|
|
42
46
|
const text = await response.text();
|
|
@@ -45,6 +49,48 @@ async function truncatedResponseText(response, max = 500) {
|
|
|
45
49
|
function asRecord(value) {
|
|
46
50
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
47
51
|
}
|
|
52
|
+
function errorMessage(error) {
|
|
53
|
+
return error instanceof Error ? error.message : String(error);
|
|
54
|
+
}
|
|
55
|
+
function firstNumber(...values) {
|
|
56
|
+
for (const value of values) {
|
|
57
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
function randomId() {
|
|
64
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
65
|
+
}
|
|
66
|
+
function removeUndefined(value) {
|
|
67
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
68
|
+
}
|
|
69
|
+
function safeJsonParse(text) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text);
|
|
72
|
+
} catch {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function parseJsonObject(text) {
|
|
77
|
+
try {
|
|
78
|
+
return asRecord(JSON.parse(text));
|
|
79
|
+
} catch {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var STREAMING_PROXY_MODES = [
|
|
84
|
+
"auto",
|
|
85
|
+
"buffer",
|
|
86
|
+
"live"
|
|
87
|
+
];
|
|
88
|
+
function parseStreamingProxyMode(value) {
|
|
89
|
+
if (STREAMING_PROXY_MODES.includes(value)) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(", ")}.`);
|
|
93
|
+
}
|
|
48
94
|
|
|
49
95
|
// src/openai.ts
|
|
50
96
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
@@ -172,13 +218,6 @@ function compactionOutputFromResponsesSse(text) {
|
|
|
172
218
|
}
|
|
173
219
|
return deltas ? [messageOutputItem(deltas)] : [];
|
|
174
220
|
}
|
|
175
|
-
function safeJsonParse(text) {
|
|
176
|
-
try {
|
|
177
|
-
return JSON.parse(text);
|
|
178
|
-
} catch {
|
|
179
|
-
return void 0;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
221
|
function chatCompletionToCompletion(completion) {
|
|
183
222
|
return removeUndefined({
|
|
184
223
|
choices: completionChoices(completion).map((choice, index) => {
|
|
@@ -372,7 +411,7 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
372
411
|
if (isNew) {
|
|
373
412
|
enqueue("response.output_item.added", {
|
|
374
413
|
item: functionCallItem(existing, "in_progress"),
|
|
375
|
-
output_index: existing.outputIndex
|
|
414
|
+
output_index: existing.outputIndex,
|
|
376
415
|
type: "response.output_item.added"
|
|
377
416
|
});
|
|
378
417
|
}
|
|
@@ -382,7 +421,7 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
382
421
|
enqueue("response.function_call_arguments.delta", {
|
|
383
422
|
delta: argumentDelta,
|
|
384
423
|
item_id: existing.itemId,
|
|
385
|
-
output_index: existing.outputIndex
|
|
424
|
+
output_index: existing.outputIndex,
|
|
386
425
|
type: "response.function_call_arguments.delta"
|
|
387
426
|
});
|
|
388
427
|
}
|
|
@@ -432,11 +471,9 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
432
471
|
type: "response.output_item.done"
|
|
433
472
|
});
|
|
434
473
|
}
|
|
435
|
-
for (const tool of [...tools.values()].sort(
|
|
436
|
-
(a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
|
|
437
|
-
)) {
|
|
474
|
+
for (const tool of [...tools.values()].sort((a, b) => a.outputIndex - b.outputIndex)) {
|
|
438
475
|
const item = functionCallItem(tool);
|
|
439
|
-
const outputIndex = tool.outputIndex
|
|
476
|
+
const outputIndex = tool.outputIndex;
|
|
440
477
|
outputEntries.push([outputIndex, item]);
|
|
441
478
|
enqueue("response.function_call_arguments.done", {
|
|
442
479
|
arguments: tool.arguments,
|
|
@@ -679,7 +716,6 @@ function outputItemsFromMessage(message) {
|
|
|
679
716
|
functionCallItem({
|
|
680
717
|
arguments: contentToText(fn.arguments),
|
|
681
718
|
id: contentToText(record.id) || `call_${randomId()}`,
|
|
682
|
-
index: output.length,
|
|
683
719
|
name: contentToText(fn.name)
|
|
684
720
|
})
|
|
685
721
|
);
|
|
@@ -761,21 +797,18 @@ function extractTokenUsage(usage) {
|
|
|
761
797
|
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
762
798
|
asRecord(record.input_tokens_details).cached_tokens
|
|
763
799
|
);
|
|
764
|
-
|
|
765
|
-
cachedTokens: cached,
|
|
800
|
+
const result = {
|
|
766
801
|
completionTokens,
|
|
767
802
|
promptTokens,
|
|
768
|
-
reasoningTokens: reasoning,
|
|
769
803
|
totalTokens: total ?? promptTokens + completionTokens
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
for (const value of values) {
|
|
774
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
775
|
-
return value;
|
|
776
|
-
}
|
|
804
|
+
};
|
|
805
|
+
if (cached !== void 0) {
|
|
806
|
+
result.cachedTokens = cached;
|
|
777
807
|
}
|
|
778
|
-
|
|
808
|
+
if (reasoning !== void 0) {
|
|
809
|
+
result.reasoningTokens = reasoning;
|
|
810
|
+
}
|
|
811
|
+
return result;
|
|
779
812
|
}
|
|
780
813
|
function firstChoice(completion) {
|
|
781
814
|
return completionChoices(completion)[0] ?? {};
|
|
@@ -804,7 +837,7 @@ function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
|
804
837
|
enqueue("[DONE]");
|
|
805
838
|
return;
|
|
806
839
|
}
|
|
807
|
-
const parsed =
|
|
840
|
+
const parsed = parseJsonObject(data);
|
|
808
841
|
if (!parsed) {
|
|
809
842
|
return;
|
|
810
843
|
}
|
|
@@ -869,7 +902,7 @@ function processChatSseLine(line, handlers) {
|
|
|
869
902
|
if (!data || data === "[DONE]") {
|
|
870
903
|
return;
|
|
871
904
|
}
|
|
872
|
-
const parsed =
|
|
905
|
+
const parsed = parseJsonObject(data);
|
|
873
906
|
if (!parsed) {
|
|
874
907
|
return;
|
|
875
908
|
}
|
|
@@ -921,19 +954,6 @@ function encodeDataSse(data) {
|
|
|
921
954
|
|
|
922
955
|
`;
|
|
923
956
|
}
|
|
924
|
-
function parseJson(data) {
|
|
925
|
-
try {
|
|
926
|
-
return asRecord(JSON.parse(data));
|
|
927
|
-
} catch {
|
|
928
|
-
return void 0;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
function removeUndefined(record) {
|
|
932
|
-
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
933
|
-
}
|
|
934
|
-
function randomId() {
|
|
935
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
936
|
-
}
|
|
937
957
|
function epochSeconds() {
|
|
938
958
|
return Math.floor(Date.now() / 1e3);
|
|
939
959
|
}
|
|
@@ -946,13 +966,13 @@ var AnthropicCompatibilityError = class extends Error {
|
|
|
946
966
|
}
|
|
947
967
|
};
|
|
948
968
|
function anthropicMessagesToResponsesRequest(request) {
|
|
949
|
-
return
|
|
969
|
+
return removeUndefined({
|
|
950
970
|
input: anthropicMessagesToResponsesInput(request.messages),
|
|
951
971
|
instructions: anthropicSystemToInstructions(request.system),
|
|
952
972
|
max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
|
|
953
973
|
metadata: request.metadata,
|
|
954
974
|
model: normalizeRequestedModel(request.model),
|
|
955
|
-
parallel_tool_calls: true,
|
|
975
|
+
parallel_tool_calls: asRecord(request.tool_choice).disable_parallel_tool_use !== true,
|
|
956
976
|
reasoning: anthropicThinkingToReasoning(request.thinking),
|
|
957
977
|
stop: anthropicStopSequences(request.stop_sequences),
|
|
958
978
|
stream: request.stream === true,
|
|
@@ -967,7 +987,7 @@ function responsesResponseToAnthropicMessage(response, fallbackModel) {
|
|
|
967
987
|
const usage = anthropicUsage(response.usage);
|
|
968
988
|
return {
|
|
969
989
|
content,
|
|
970
|
-
id: textValue(response.id) || `msg_${
|
|
990
|
+
id: textValue(response.id) || `msg_${randomId()}`,
|
|
971
991
|
model: textValue(response.model) || fallbackModel,
|
|
972
992
|
role: "assistant",
|
|
973
993
|
stop_reason: anthropicStopReason(response, content),
|
|
@@ -1044,7 +1064,7 @@ function createAnthropicStreamState(options) {
|
|
|
1044
1064
|
return {
|
|
1045
1065
|
blocks: /* @__PURE__ */ new Map(),
|
|
1046
1066
|
completed: false,
|
|
1047
|
-
messageId: options.messageId ?? `msg_${
|
|
1067
|
+
messageId: options.messageId ?? `msg_${randomId()}`,
|
|
1048
1068
|
model: options.model,
|
|
1049
1069
|
nextBlockIndex: 0,
|
|
1050
1070
|
sawToolUse: false,
|
|
@@ -1098,7 +1118,7 @@ function anthropicMessagesToResponsesInput(messages) {
|
|
|
1098
1118
|
flushMessage();
|
|
1099
1119
|
input.push({
|
|
1100
1120
|
arguments: JSON.stringify(asRecord(part.input)),
|
|
1101
|
-
call_id: textValue(part.id) || `call_${
|
|
1121
|
+
call_id: textValue(part.id) || `call_${randomId()}`,
|
|
1102
1122
|
name: textValue(part.name),
|
|
1103
1123
|
type: "function_call"
|
|
1104
1124
|
});
|
|
@@ -1209,7 +1229,7 @@ function anthropicTools(tools) {
|
|
|
1209
1229
|
}
|
|
1210
1230
|
const converted = tools.map((tool) => {
|
|
1211
1231
|
const record = asRecord(tool);
|
|
1212
|
-
return
|
|
1232
|
+
return removeUndefined({
|
|
1213
1233
|
description: record.description,
|
|
1214
1234
|
name: record.name,
|
|
1215
1235
|
parameters: record.input_schema,
|
|
@@ -1280,7 +1300,7 @@ function anthropicContentFromResponsesOutput(response) {
|
|
|
1280
1300
|
}
|
|
1281
1301
|
if (type === "function_call") {
|
|
1282
1302
|
content.push({
|
|
1283
|
-
id: textValue(record.call_id) || textValue(record.id) || `call_${
|
|
1303
|
+
id: textValue(record.call_id) || textValue(record.id) || `call_${randomId()}`,
|
|
1284
1304
|
input: parseToolInput(textValue(record.arguments)),
|
|
1285
1305
|
name: textValue(record.name),
|
|
1286
1306
|
type: "tool_use"
|
|
@@ -1307,12 +1327,12 @@ function anthropicStopReason(response, content) {
|
|
|
1307
1327
|
}
|
|
1308
1328
|
function anthropicUsage(usage) {
|
|
1309
1329
|
const record = asRecord(usage);
|
|
1310
|
-
const inputTokens =
|
|
1311
|
-
const outputTokens =
|
|
1330
|
+
const inputTokens = firstNumber(record.input_tokens, record.prompt_tokens) ?? 0;
|
|
1331
|
+
const outputTokens = firstNumber(record.output_tokens, record.completion_tokens) ?? 0;
|
|
1312
1332
|
const details = asRecord(record.input_tokens_details);
|
|
1313
|
-
return
|
|
1314
|
-
cache_creation_input_tokens:
|
|
1315
|
-
cache_read_input_tokens:
|
|
1333
|
+
return removeUndefined({
|
|
1334
|
+
cache_creation_input_tokens: firstNumber(record.cache_creation_input_tokens),
|
|
1335
|
+
cache_read_input_tokens: firstNumber(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
|
|
1316
1336
|
input_tokens: inputTokens,
|
|
1317
1337
|
output_tokens: outputTokens
|
|
1318
1338
|
});
|
|
@@ -1494,7 +1514,7 @@ function ensureToolBlock(state, payload, item, enqueue) {
|
|
|
1494
1514
|
state.blocks.set(key, block);
|
|
1495
1515
|
enqueue("content_block_start", {
|
|
1496
1516
|
content_block: {
|
|
1497
|
-
id: textValue(item.call_id) || textValue(item.id) || `call_${
|
|
1517
|
+
id: textValue(item.call_id) || textValue(item.id) || `call_${randomId()}`,
|
|
1498
1518
|
input: {},
|
|
1499
1519
|
name: textValue(item.name),
|
|
1500
1520
|
type: "tool_use"
|
|
@@ -1528,13 +1548,6 @@ function parseSseBlock(block) {
|
|
|
1528
1548
|
}
|
|
1529
1549
|
return { data: data.join("\n"), event };
|
|
1530
1550
|
}
|
|
1531
|
-
function parseJsonObject(text) {
|
|
1532
|
-
try {
|
|
1533
|
-
return asRecord(JSON.parse(text));
|
|
1534
|
-
} catch {
|
|
1535
|
-
return void 0;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
1551
|
function parseToolInput(argumentsText) {
|
|
1539
1552
|
const parsed = parseJsonObject(argumentsText);
|
|
1540
1553
|
return parsed ?? {};
|
|
@@ -1566,32 +1579,18 @@ function textValue(value) {
|
|
|
1566
1579
|
}
|
|
1567
1580
|
return "";
|
|
1568
1581
|
}
|
|
1569
|
-
function firstNumber2(...values) {
|
|
1570
|
-
for (const value of values) {
|
|
1571
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1572
|
-
return value;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
return void 0;
|
|
1576
|
-
}
|
|
1577
1582
|
function indexValue(value) {
|
|
1578
1583
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1579
1584
|
}
|
|
1580
|
-
function removeUndefined2(record) {
|
|
1581
|
-
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
1582
|
-
}
|
|
1583
1585
|
function encodeSse2(event, data) {
|
|
1584
1586
|
return `event: ${event}
|
|
1585
1587
|
data: ${JSON.stringify(data)}
|
|
1586
1588
|
|
|
1587
1589
|
`;
|
|
1588
1590
|
}
|
|
1589
|
-
function randomId2() {
|
|
1590
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
1591
|
-
}
|
|
1592
1591
|
|
|
1593
1592
|
// src/auth-store.ts
|
|
1594
|
-
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1593
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
1595
1594
|
import { dirname, join } from "path";
|
|
1596
1595
|
var StoredCopilotAuthError = class extends Error {
|
|
1597
1596
|
constructor(message) {
|
|
@@ -1642,7 +1641,7 @@ function readStoredCopilotAuth(path = authStorePath()) {
|
|
|
1642
1641
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1643
1642
|
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
1644
1643
|
}
|
|
1645
|
-
const record = parsed;
|
|
1644
|
+
const record = asRecord(parsed);
|
|
1646
1645
|
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
1647
1646
|
if (!token) {
|
|
1648
1647
|
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
@@ -1668,7 +1667,15 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
1668
1667
|
`;
|
|
1669
1668
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
1670
1669
|
writeFileSync(tmpPath, data, { mode: 384 });
|
|
1671
|
-
|
|
1670
|
+
try {
|
|
1671
|
+
renameSync(tmpPath, path);
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
try {
|
|
1674
|
+
rmSync(tmpPath, { force: true });
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
throw error;
|
|
1678
|
+
}
|
|
1672
1679
|
try {
|
|
1673
1680
|
chmodSync(path, 384);
|
|
1674
1681
|
} catch {
|
|
@@ -1713,23 +1720,20 @@ var CopilotAuth = class {
|
|
|
1713
1720
|
throw error;
|
|
1714
1721
|
}
|
|
1715
1722
|
if (stored) {
|
|
1716
|
-
|
|
1723
|
+
this.#cachedAccess = {
|
|
1717
1724
|
apiBaseUrl: trimTrailingSlash(
|
|
1718
1725
|
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
1719
1726
|
),
|
|
1720
1727
|
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
1721
1728
|
source: "github-copilot-oauth",
|
|
1722
1729
|
token: stored.token
|
|
1723
|
-
}
|
|
1730
|
+
};
|
|
1731
|
+
return this.#cachedAccess;
|
|
1724
1732
|
}
|
|
1725
1733
|
throw new CopilotAuthError(
|
|
1726
1734
|
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
1727
1735
|
);
|
|
1728
1736
|
}
|
|
1729
|
-
#cacheAccess(access) {
|
|
1730
|
-
this.#cachedAccess = access;
|
|
1731
|
-
return access;
|
|
1732
|
-
}
|
|
1733
1737
|
};
|
|
1734
1738
|
|
|
1735
1739
|
// src/copilot.ts
|
|
@@ -1737,23 +1741,26 @@ var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
|
1737
1741
|
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
1738
1742
|
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
1739
1743
|
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
1744
|
+
var EDITOR_PLUGIN_VERSION = "hoopilot/0.1.0";
|
|
1745
|
+
var EDITOR_VERSION = "Hoopilot/0.1.0";
|
|
1746
|
+
var HOOPILOT_USER_AGENT = "hoopilot/0.1.0";
|
|
1740
1747
|
function applyCopilotHeaders(headers, token) {
|
|
1741
1748
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
1742
1749
|
headers.set("authorization", `Bearer ${token}`);
|
|
1743
1750
|
headers.set("copilot-integration-id", "vscode-chat");
|
|
1744
|
-
headers.set("editor-plugin-version",
|
|
1745
|
-
headers.set("editor-version",
|
|
1751
|
+
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
|
|
1752
|
+
headers.set("editor-version", EDITOR_VERSION);
|
|
1746
1753
|
headers.set("openai-intent", "conversation-panel");
|
|
1747
|
-
headers.set("user-agent",
|
|
1754
|
+
headers.set("user-agent", HOOPILOT_USER_AGENT);
|
|
1748
1755
|
headers.set("x-github-api-version", "2026-06-01");
|
|
1749
1756
|
return headers;
|
|
1750
1757
|
}
|
|
1751
1758
|
function applyGithubApiHeaders(headers, token) {
|
|
1752
1759
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
1753
1760
|
headers.set("authorization", `token ${token}`);
|
|
1754
|
-
headers.set("editor-plugin-version",
|
|
1755
|
-
headers.set("editor-version",
|
|
1756
|
-
headers.set("user-agent",
|
|
1761
|
+
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
|
|
1762
|
+
headers.set("editor-version", EDITOR_VERSION);
|
|
1763
|
+
headers.set("user-agent", HOOPILOT_USER_AGENT);
|
|
1757
1764
|
headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
|
|
1758
1765
|
return headers;
|
|
1759
1766
|
}
|
|
@@ -1766,7 +1773,7 @@ function parseRateLimitHeaders(headers, nowMs = Date.now()) {
|
|
|
1766
1773
|
if (limit === void 0 && remaining === void 0 && used === void 0 && resetEpochSeconds === void 0 && retryAfterSeconds === void 0) {
|
|
1767
1774
|
return void 0;
|
|
1768
1775
|
}
|
|
1769
|
-
return
|
|
1776
|
+
return removeUndefined({
|
|
1770
1777
|
limit,
|
|
1771
1778
|
observedAtMs: nowMs,
|
|
1772
1779
|
remaining,
|
|
@@ -1784,11 +1791,6 @@ function headerInt(headers, name) {
|
|
|
1784
1791
|
const value = Number.parseInt(raw.trim(), 10);
|
|
1785
1792
|
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
1786
1793
|
}
|
|
1787
|
-
function removeUndefinedRateLimit(rateLimit) {
|
|
1788
|
-
return Object.fromEntries(
|
|
1789
|
-
Object.entries(rateLimit).filter(([, value]) => value !== void 0)
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
1794
|
var CopilotClient = class {
|
|
1793
1795
|
#auth;
|
|
1794
1796
|
#allowUnsafeUpstream;
|
|
@@ -1885,7 +1887,7 @@ function normalizeCopilotUsage(body) {
|
|
|
1885
1887
|
for (const category of /* @__PURE__ */ new Set([...Object.keys(remaining), ...Object.keys(monthly)])) {
|
|
1886
1888
|
const entitlement = numberOrUndefined(monthly[category]);
|
|
1887
1889
|
const left = numberOrUndefined(remaining[category]);
|
|
1888
|
-
quotas[category] =
|
|
1890
|
+
quotas[category] = removeUndefined({
|
|
1889
1891
|
entitlement,
|
|
1890
1892
|
percentRemaining: entitlement !== void 0 && entitlement > 0 && left !== void 0 ? left / entitlement * 100 : void 0,
|
|
1891
1893
|
remaining: left,
|
|
@@ -1893,7 +1895,7 @@ function normalizeCopilotUsage(body) {
|
|
|
1893
1895
|
});
|
|
1894
1896
|
}
|
|
1895
1897
|
}
|
|
1896
|
-
return
|
|
1898
|
+
return removeUndefined({
|
|
1897
1899
|
accessTypeSku: stringOrUndefined(record.access_type_sku),
|
|
1898
1900
|
chatEnabled: typeof record.chat_enabled === "boolean" ? record.chat_enabled : void 0,
|
|
1899
1901
|
plan: stringOrUndefined(record.copilot_plan),
|
|
@@ -1905,7 +1907,7 @@ function normalizeQuotaDetail(detail) {
|
|
|
1905
1907
|
const entitlement = numberOrUndefined(detail.entitlement);
|
|
1906
1908
|
const overageCount = numberOrUndefined(detail.overage_count);
|
|
1907
1909
|
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
1908
|
-
return
|
|
1910
|
+
return removeUndefined({
|
|
1909
1911
|
entitlement,
|
|
1910
1912
|
hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
|
|
1911
1913
|
overageCount,
|
|
@@ -1929,21 +1931,10 @@ function usedFrom(entitlement, remaining, overageCount) {
|
|
|
1929
1931
|
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
1930
1932
|
return Math.max(0, base + overage);
|
|
1931
1933
|
}
|
|
1932
|
-
|
|
1933
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1934
|
-
}
|
|
1934
|
+
var numberOrUndefined = firstNumber;
|
|
1935
1935
|
function stringOrUndefined(value) {
|
|
1936
1936
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1937
1937
|
}
|
|
1938
|
-
function removeUndefinedQuota(quota) {
|
|
1939
|
-
return Object.fromEntries(
|
|
1940
|
-
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
1941
|
-
);
|
|
1942
|
-
}
|
|
1943
|
-
function removeUndefinedUsage(usage) {
|
|
1944
|
-
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
1945
|
-
return Object.fromEntries(entries);
|
|
1946
|
-
}
|
|
1947
1938
|
|
|
1948
1939
|
// src/github-device.ts
|
|
1949
1940
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -2075,11 +2066,16 @@ function positiveSeconds(value, fallback) {
|
|
|
2075
2066
|
}
|
|
2076
2067
|
async function parseJsonResponse(response, context) {
|
|
2077
2068
|
const text = await response.text();
|
|
2069
|
+
let value;
|
|
2078
2070
|
try {
|
|
2079
|
-
|
|
2071
|
+
value = JSON.parse(text);
|
|
2080
2072
|
} catch {
|
|
2081
2073
|
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
2082
2074
|
}
|
|
2075
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2076
|
+
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
2077
|
+
}
|
|
2078
|
+
return value;
|
|
2083
2079
|
}
|
|
2084
2080
|
|
|
2085
2081
|
// src/logger.ts
|
|
@@ -2143,21 +2139,29 @@ function createHoopilotLogger(options = {}) {
|
|
|
2143
2139
|
timestamp: pino.stdTimeFunctions.isoTime
|
|
2144
2140
|
};
|
|
2145
2141
|
if (format === "pretty") {
|
|
2146
|
-
return
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2142
|
+
return asHoopilotLogger(
|
|
2143
|
+
pino(
|
|
2144
|
+
pinoOptions,
|
|
2145
|
+
pretty({
|
|
2146
|
+
// Probe the same sink we write to (stdout / fd 1), so colors are not
|
|
2147
|
+
// emitted into a redirected file when only stderr is a TTY. A custom
|
|
2148
|
+
// stream's TTY-ness is unknown, so default to no color there.
|
|
2149
|
+
colorize: options.colorize ?? (options.stream ? false : process.stdout.isTTY),
|
|
2150
|
+
destination: options.stream ?? 1,
|
|
2151
|
+
ignore: "pid,hostname",
|
|
2152
|
+
singleLine: true,
|
|
2153
|
+
translateTime: "SYS:standard"
|
|
2154
|
+
})
|
|
2155
|
+
)
|
|
2155
2156
|
);
|
|
2156
2157
|
}
|
|
2157
2158
|
if (options.stream) {
|
|
2158
|
-
return pino(pinoOptions, options.stream);
|
|
2159
|
+
return asHoopilotLogger(pino(pinoOptions, options.stream));
|
|
2159
2160
|
}
|
|
2160
|
-
return pino(pinoOptions);
|
|
2161
|
+
return asHoopilotLogger(pino(pinoOptions));
|
|
2162
|
+
}
|
|
2163
|
+
function asHoopilotLogger(logger) {
|
|
2164
|
+
return logger;
|
|
2161
2165
|
}
|
|
2162
2166
|
function parseLogFormat(value) {
|
|
2163
2167
|
if (!value) {
|
|
@@ -2283,26 +2287,23 @@ var MetricsRegistry = class {
|
|
|
2283
2287
|
const resource = this.#rateLimitResource(rateLimit.resource);
|
|
2284
2288
|
this.#githubRateLimit.set(resource, { ...rateLimit, resource });
|
|
2285
2289
|
}
|
|
2286
|
-
//
|
|
2287
|
-
//
|
|
2288
|
-
//
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
if (!this.#tokens.has(cleaned) && this.#tokens.size >= MAX_TRACKED_MODELS) {
|
|
2290
|
+
// Clean a raw value into a bounded exposition-format label: cap its length,
|
|
2291
|
+
// strip characters that would corrupt the format, and fold overflow past the
|
|
2292
|
+
// cardinality limit into UNKNOWN_MODEL so the series count stays bounded.
|
|
2293
|
+
#boundedLabel(value, tracked, maxEntries) {
|
|
2294
|
+
const cleaned = cleanLabel(value).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
|
|
2295
|
+
if (!tracked.has(cleaned) && tracked.size >= maxEntries) {
|
|
2293
2296
|
return UNKNOWN_MODEL;
|
|
2294
2297
|
}
|
|
2295
2298
|
return cleaned;
|
|
2296
2299
|
}
|
|
2297
|
-
// The
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
+
// The model can originate from a (possibly hostile) client request.
|
|
2301
|
+
#modelLabel(model) {
|
|
2302
|
+
return this.#boundedLabel(model, this.#tokens, MAX_TRACKED_MODELS);
|
|
2303
|
+
}
|
|
2304
|
+
// The resource comes from a trusted upstream header, but is bounded the same way.
|
|
2300
2305
|
#rateLimitResource(resource) {
|
|
2301
|
-
|
|
2302
|
-
if (!this.#githubRateLimit.has(cleaned) && this.#githubRateLimit.size >= MAX_TRACKED_RATELIMIT_RESOURCES) {
|
|
2303
|
-
return UNKNOWN_MODEL;
|
|
2304
|
-
}
|
|
2305
|
-
return cleaned;
|
|
2306
|
+
return this.#boundedLabel(resource, this.#githubRateLimit, MAX_TRACKED_RATELIMIT_RESOURCES);
|
|
2306
2307
|
}
|
|
2307
2308
|
#observeDuration(route, seconds) {
|
|
2308
2309
|
const value = Number.isFinite(seconds) && seconds >= 0 ? seconds : 0;
|
|
@@ -2628,7 +2629,7 @@ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage, onOutcome)
|
|
|
2628
2629
|
considerSseLine(line, accumulator.consider);
|
|
2629
2630
|
}
|
|
2630
2631
|
} else {
|
|
2631
|
-
const parsed =
|
|
2632
|
+
const parsed = safeJsonParse(text);
|
|
2632
2633
|
if (parsed !== void 0) {
|
|
2633
2634
|
accumulator.consider(parsed);
|
|
2634
2635
|
}
|
|
@@ -2690,7 +2691,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
|
|
|
2690
2691
|
considerSseLine(finalBuffer, accumulator.consider);
|
|
2691
2692
|
}
|
|
2692
2693
|
} else if (!overflowed && finalBuffer) {
|
|
2693
|
-
const parsed =
|
|
2694
|
+
const parsed = safeJsonParse(finalBuffer);
|
|
2694
2695
|
if (parsed !== void 0) {
|
|
2695
2696
|
accumulator.consider(parsed);
|
|
2696
2697
|
}
|
|
@@ -2733,18 +2734,11 @@ function considerSseLine(line, consider) {
|
|
|
2733
2734
|
if (!data || data === "[DONE]") {
|
|
2734
2735
|
return;
|
|
2735
2736
|
}
|
|
2736
|
-
const parsed =
|
|
2737
|
+
const parsed = safeJsonParse(data);
|
|
2737
2738
|
if (parsed !== void 0) {
|
|
2738
2739
|
consider(parsed);
|
|
2739
2740
|
}
|
|
2740
2741
|
}
|
|
2741
|
-
function safeParse(text) {
|
|
2742
|
-
try {
|
|
2743
|
-
return JSON.parse(text);
|
|
2744
|
-
} catch {
|
|
2745
|
-
return void 0;
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
2742
|
function modelText(value) {
|
|
2749
2743
|
return typeof value === "string" ? value.trim() : "";
|
|
2750
2744
|
}
|
|
@@ -2820,6 +2814,10 @@ function formatNumber(value) {
|
|
|
2820
2814
|
return Number.isInteger(value) ? value.toString() : String(value);
|
|
2821
2815
|
}
|
|
2822
2816
|
|
|
2817
|
+
// src/server.ts
|
|
2818
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
2819
|
+
import { Elysia } from "elysia";
|
|
2820
|
+
|
|
2823
2821
|
// src/dashboard.ts
|
|
2824
2822
|
var DASHBOARD_HTML = `<!doctype html>
|
|
2825
2823
|
<html lang="en">
|
|
@@ -3220,21 +3218,24 @@ footer.foot .end { margin-left:auto; }
|
|
|
3220
3218
|
function mk(tag, cls, txt){ var e = document.createElement(tag); if (cls) e.className = cls; if (txt !== undefined && txt !== null) e.textContent = txt; return e; }
|
|
3221
3219
|
|
|
3222
3220
|
// Set numeric text and flash on discrete change.
|
|
3223
|
-
function setNum(id, value, kind){
|
|
3221
|
+
function setNum(id, value, kind, num){
|
|
3224
3222
|
var el = byId(id); if (!el) return;
|
|
3225
3223
|
el.classList.remove("skel");
|
|
3226
3224
|
var s = String(value);
|
|
3227
3225
|
if (el.textContent !== s){
|
|
3228
3226
|
el.textContent = s;
|
|
3227
|
+
// Compare on the raw number (num) when provided, so directional flash works
|
|
3228
|
+
// even when value is a pre-formatted display string.
|
|
3229
|
+
var n = (num !== undefined) ? num : value;
|
|
3229
3230
|
var prev = lastRender[id];
|
|
3230
3231
|
if (prev !== undefined){
|
|
3231
3232
|
var cls = "flash";
|
|
3232
|
-
if (kind === "delta" && typeof
|
|
3233
|
-
cls =
|
|
3233
|
+
if (kind === "delta" && typeof n === "number" && typeof prev === "number"){
|
|
3234
|
+
cls = n > prev ? "flash-up" : (n < prev ? "flash-down" : null);
|
|
3234
3235
|
}
|
|
3235
3236
|
if (cls){ el.classList.remove("flash","flash-up","flash-down"); void el.offsetWidth; el.classList.add(cls); }
|
|
3236
3237
|
}
|
|
3237
|
-
lastRender[id] =
|
|
3238
|
+
lastRender[id] = n;
|
|
3238
3239
|
}
|
|
3239
3240
|
}
|
|
3240
3241
|
function setText(id, s){ var el = byId(id); if (el){ el.classList.remove("skel"); el.textContent = s; } }
|
|
@@ -3315,7 +3316,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
3315
3316
|
if (beat && dot){ dot.classList.remove("heartbeat"); void dot.offsetWidth; dot.classList.add("heartbeat"); }
|
|
3316
3317
|
}
|
|
3317
3318
|
function showBanner(text, ok){
|
|
3318
|
-
var b = byId("banner"); b.textContent = text; b.className = "
|
|
3319
|
+
var b = byId("banner"); b.textContent = text; b.className = "show" + (ok ? " ok" : "");
|
|
3319
3320
|
if (ok){ setTimeout(function(){ b.classList.remove("show"); }, 2000); }
|
|
3320
3321
|
}
|
|
3321
3322
|
function hideBanner(){ byId("banner").classList.remove("show"); }
|
|
@@ -3422,7 +3423,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
3422
3423
|
if (isFinite(reqPerSec)){ pushHist(hist.req, reqPerSec); setNum("req-num", rate(reqPerSec)); } else setText("req-num","\\u2014");
|
|
3423
3424
|
if (isFinite(tokPerSec)){ pushHist(hist.tok, tokPerSec); setNum("tok-num", humanInt(tokPerSec)); } else setText("tok-num","\\u2014");
|
|
3424
3425
|
var inflight = proxy.inFlight || 0;
|
|
3425
|
-
pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta");
|
|
3426
|
+
pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta", inflight);
|
|
3426
3427
|
byId("v-inflight").classList.toggle("active", inflight > 0);
|
|
3427
3428
|
setText("uptime-num", fmtUptime(proxy.uptimeSeconds || 0));
|
|
3428
3429
|
|
|
@@ -3565,7 +3566,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
3565
3566
|
|
|
3566
3567
|
function renderUpstream(up, delta, restarted){
|
|
3567
3568
|
setNum("up-total", humanInt(up.total||0));
|
|
3568
|
-
setNum("up-errors", humanInt(up.errors||0), "delta");
|
|
3569
|
+
setNum("up-errors", humanInt(up.errors||0), "delta", up.errors||0);
|
|
3569
3570
|
var er = up.total ? (up.errors/up.total*100) : 0;
|
|
3570
3571
|
var rt = byId("up-rate"); rt.textContent = pct(er); rt.className = "v rate " + (er > 5 ? "danger" : er > 1 ? "warn" : "ok");
|
|
3571
3572
|
byId("up-errblk").classList.toggle("hot", (up.errors||0) > 0);
|
|
@@ -3639,8 +3640,9 @@ async function getVersion() {
|
|
|
3639
3640
|
resolved = BAKED_VERSION;
|
|
3640
3641
|
} else {
|
|
3641
3642
|
try {
|
|
3642
|
-
const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
3643
|
-
|
|
3643
|
+
const manifest = asRecord(await Bun.file(new URL("../package.json", import.meta.url)).json());
|
|
3644
|
+
const version = manifest.version;
|
|
3645
|
+
resolved = typeof version === "string" ? version : "0.0.0";
|
|
3644
3646
|
} catch {
|
|
3645
3647
|
resolved = "0.0.0";
|
|
3646
3648
|
}
|
|
@@ -3666,6 +3668,18 @@ var RequestBodyTooLargeError = class extends Error {
|
|
|
3666
3668
|
this.name = "RequestBodyTooLargeError";
|
|
3667
3669
|
}
|
|
3668
3670
|
};
|
|
3671
|
+
var InvalidJsonError = class extends Error {
|
|
3672
|
+
constructor() {
|
|
3673
|
+
super(INVALID_JSON_MESSAGE);
|
|
3674
|
+
this.name = "InvalidJsonError";
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
var JsonNotObjectError = class extends Error {
|
|
3678
|
+
constructor() {
|
|
3679
|
+
super(JSON_OBJECT_MESSAGE);
|
|
3680
|
+
this.name = "JsonNotObjectError";
|
|
3681
|
+
}
|
|
3682
|
+
};
|
|
3669
3683
|
function createHoopilotHandler(options = {}) {
|
|
3670
3684
|
const client = new CopilotClient(options);
|
|
3671
3685
|
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
@@ -3675,8 +3689,19 @@ function createHoopilotHandler(options = {}) {
|
|
|
3675
3689
|
const readUsage = createUsageReader(client, metrics);
|
|
3676
3690
|
const recordTokens = (model, usage) => metrics.recordTokens(model, usage);
|
|
3677
3691
|
const recordExtraction = (extracted) => metrics.recordTokenExtraction(extracted);
|
|
3678
|
-
const
|
|
3679
|
-
const
|
|
3692
|
+
const bufferProxyBodies = shouldBufferProxyBodies(resolveStreamingProxyMode(options));
|
|
3693
|
+
const requestContext = /* @__PURE__ */ new WeakMap();
|
|
3694
|
+
const app = buildApp({
|
|
3695
|
+
apiKey,
|
|
3696
|
+
allowedOrigins,
|
|
3697
|
+
bufferProxyBodies,
|
|
3698
|
+
client,
|
|
3699
|
+
metrics,
|
|
3700
|
+
readUsage,
|
|
3701
|
+
recordExtraction,
|
|
3702
|
+
recordTokens,
|
|
3703
|
+
requestContext
|
|
3704
|
+
});
|
|
3680
3705
|
return async (request) => {
|
|
3681
3706
|
const startedAt = performance.now();
|
|
3682
3707
|
const url = new URL(request.url);
|
|
@@ -3692,7 +3717,24 @@ function createHoopilotHandler(options = {}) {
|
|
|
3692
3717
|
metrics.startRequest();
|
|
3693
3718
|
const origin = request.headers.get("origin")?.trim() || void 0;
|
|
3694
3719
|
const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
|
|
3695
|
-
const
|
|
3720
|
+
const inner = normalizeInnerRequest(request, apiPath, url);
|
|
3721
|
+
requestContext.set(inner, {
|
|
3722
|
+
apiPath,
|
|
3723
|
+
logger: requestLogger,
|
|
3724
|
+
origin,
|
|
3725
|
+
originalPath: url.pathname
|
|
3726
|
+
});
|
|
3727
|
+
let response;
|
|
3728
|
+
try {
|
|
3729
|
+
response = await app.handle(inner);
|
|
3730
|
+
} catch (error) {
|
|
3731
|
+
requestLogger.error(
|
|
3732
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3733
|
+
"request failed"
|
|
3734
|
+
);
|
|
3735
|
+
response = jsonError(500, "internal_error", errorMessage(error));
|
|
3736
|
+
}
|
|
3737
|
+
return finishResponse(response, {
|
|
3696
3738
|
corsOrigin,
|
|
3697
3739
|
logger: requestLogger,
|
|
3698
3740
|
method: request.method,
|
|
@@ -3703,144 +3745,175 @@ function createHoopilotHandler(options = {}) {
|
|
|
3703
3745
|
closeConnection: bufferProxyBodies,
|
|
3704
3746
|
trackStreamingBody: !bufferProxyBodies
|
|
3705
3747
|
});
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
function buildApp(deps) {
|
|
3751
|
+
const {
|
|
3752
|
+
apiKey,
|
|
3753
|
+
allowedOrigins,
|
|
3754
|
+
bufferProxyBodies,
|
|
3755
|
+
client,
|
|
3756
|
+
metrics,
|
|
3757
|
+
readUsage,
|
|
3758
|
+
recordExtraction,
|
|
3759
|
+
recordTokens,
|
|
3760
|
+
requestContext
|
|
3761
|
+
} = deps;
|
|
3762
|
+
const contextFor = (request) => {
|
|
3763
|
+
const stored = requestContext.get(request);
|
|
3764
|
+
if (stored) {
|
|
3765
|
+
return stored;
|
|
3766
|
+
}
|
|
3767
|
+
const originalPath = new URL(request.url).pathname;
|
|
3768
|
+
return {
|
|
3769
|
+
apiPath: canonicalApiPath(originalPath),
|
|
3770
|
+
logger: noopLogger,
|
|
3771
|
+
origin: request.headers.get("origin")?.trim() || void 0,
|
|
3772
|
+
originalPath
|
|
3773
|
+
};
|
|
3774
|
+
};
|
|
3775
|
+
const loggerFor = (request) => contextFor(request).logger;
|
|
3776
|
+
const noBody = { parse: "none" };
|
|
3777
|
+
return new Elysia().onRequest(({ request }) => {
|
|
3778
|
+
const { apiPath, logger, origin } = contextFor(request);
|
|
3706
3779
|
const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
|
|
3707
3780
|
if (browserOrigin) {
|
|
3708
|
-
|
|
3781
|
+
logger.warn(
|
|
3709
3782
|
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
3710
3783
|
"blocked cross-origin browser request"
|
|
3711
3784
|
);
|
|
3712
|
-
return
|
|
3785
|
+
return jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE);
|
|
3713
3786
|
}
|
|
3714
3787
|
if (request.method === "OPTIONS") {
|
|
3715
|
-
return
|
|
3788
|
+
return new Response(null, { headers: corsHeaders() });
|
|
3716
3789
|
}
|
|
3717
3790
|
if (request.method === "GET" && apiPath === "/dashboard") {
|
|
3718
|
-
return
|
|
3791
|
+
return dashboardResponse();
|
|
3719
3792
|
}
|
|
3720
3793
|
if (!isAuthorized(request, apiKey)) {
|
|
3721
|
-
|
|
3722
|
-
return
|
|
3794
|
+
logger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
|
|
3795
|
+
return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
|
|
3723
3796
|
}
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
)
|
|
3751
|
-
);
|
|
3752
|
-
}
|
|
3753
|
-
if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
|
|
3754
|
-
return finish(handleAnthropicCountTokens(await readJson(request)));
|
|
3755
|
-
}
|
|
3756
|
-
if (request.method === "POST" && apiPath === "/v1/chat/completions") {
|
|
3757
|
-
return finish(
|
|
3758
|
-
await handleChatCompletions(
|
|
3759
|
-
client,
|
|
3760
|
-
metrics,
|
|
3761
|
-
recordTokens,
|
|
3762
|
-
recordExtraction,
|
|
3763
|
-
request,
|
|
3764
|
-
requestLogger,
|
|
3765
|
-
bufferProxyBodies
|
|
3766
|
-
)
|
|
3767
|
-
);
|
|
3768
|
-
}
|
|
3769
|
-
if (request.method === "POST" && apiPath === "/v1/completions") {
|
|
3770
|
-
return finish(
|
|
3771
|
-
await handleCompletions(
|
|
3772
|
-
client,
|
|
3773
|
-
metrics,
|
|
3774
|
-
recordTokens,
|
|
3775
|
-
recordExtraction,
|
|
3776
|
-
request,
|
|
3777
|
-
requestLogger,
|
|
3778
|
-
bufferProxyBodies
|
|
3779
|
-
)
|
|
3780
|
-
);
|
|
3781
|
-
}
|
|
3782
|
-
if (request.method === "POST" && apiPath === "/v1/responses/compact") {
|
|
3783
|
-
return finish(
|
|
3784
|
-
await handleResponsesCompact(
|
|
3785
|
-
client,
|
|
3786
|
-
metrics,
|
|
3787
|
-
recordTokens,
|
|
3788
|
-
recordExtraction,
|
|
3789
|
-
request,
|
|
3790
|
-
requestLogger
|
|
3791
|
-
)
|
|
3792
|
-
);
|
|
3793
|
-
}
|
|
3794
|
-
if (request.method === "POST" && apiPath === "/v1/responses") {
|
|
3795
|
-
return finish(
|
|
3796
|
-
await handleResponses(
|
|
3797
|
-
client,
|
|
3798
|
-
metrics,
|
|
3799
|
-
recordTokens,
|
|
3800
|
-
recordExtraction,
|
|
3801
|
-
request,
|
|
3802
|
-
requestLogger,
|
|
3803
|
-
bufferProxyBodies
|
|
3804
|
-
)
|
|
3805
|
-
);
|
|
3806
|
-
}
|
|
3807
|
-
return finish(jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`));
|
|
3808
|
-
} catch (error) {
|
|
3809
|
-
if (error instanceof CopilotAuthError) {
|
|
3810
|
-
requestLogger.warn(
|
|
3811
|
-
{ err: errorDetails(error), event: "copilot.auth.missing" },
|
|
3812
|
-
"copilot auth failed"
|
|
3813
|
-
);
|
|
3814
|
-
return finish(jsonError(401, "copilot_auth_error", error.message));
|
|
3815
|
-
}
|
|
3816
|
-
const message = errorMessage(error);
|
|
3817
|
-
if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
|
|
3818
|
-
requestLogger.warn(
|
|
3819
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3820
|
-
"request body was not usable json"
|
|
3821
|
-
);
|
|
3822
|
-
return finish(jsonError(400, "invalid_request_error", message));
|
|
3823
|
-
} else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
3824
|
-
requestLogger.warn(
|
|
3825
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3826
|
-
"request body used unsupported compatibility fields"
|
|
3827
|
-
);
|
|
3828
|
-
return finish(jsonError(400, "invalid_request_error", message));
|
|
3829
|
-
} else if (error instanceof RequestBodyTooLargeError) {
|
|
3830
|
-
requestLogger.warn(
|
|
3831
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3832
|
-
"request body exceeded size limit"
|
|
3833
|
-
);
|
|
3834
|
-
return finish(jsonError(413, "request_too_large", message));
|
|
3835
|
-
} else {
|
|
3836
|
-
requestLogger.error(
|
|
3837
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3838
|
-
"request failed"
|
|
3839
|
-
);
|
|
3840
|
-
}
|
|
3841
|
-
return finish(jsonError(500, "internal_error", message));
|
|
3797
|
+
}).onError(({ code, error, request }) => {
|
|
3798
|
+
const { logger, originalPath } = contextFor(request);
|
|
3799
|
+
if (code === "NOT_FOUND") {
|
|
3800
|
+
return jsonError(404, "not_found", `No route for ${request.method} ${originalPath}.`);
|
|
3801
|
+
}
|
|
3802
|
+
if (error instanceof CopilotAuthError) {
|
|
3803
|
+
logger.warn(
|
|
3804
|
+
{ err: errorDetails(error), event: "copilot.auth.missing" },
|
|
3805
|
+
"copilot auth failed"
|
|
3806
|
+
);
|
|
3807
|
+
return jsonError(401, "copilot_auth_error", error.message);
|
|
3808
|
+
}
|
|
3809
|
+
const message = errorMessage(error);
|
|
3810
|
+
if (error instanceof InvalidJsonError || error instanceof JsonNotObjectError) {
|
|
3811
|
+
logger.warn(
|
|
3812
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3813
|
+
"request body was not usable json"
|
|
3814
|
+
);
|
|
3815
|
+
return jsonError(400, "invalid_request_error", message);
|
|
3816
|
+
}
|
|
3817
|
+
if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
3818
|
+
logger.warn(
|
|
3819
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3820
|
+
"request body used unsupported compatibility fields"
|
|
3821
|
+
);
|
|
3822
|
+
return jsonError(400, "invalid_request_error", message);
|
|
3842
3823
|
}
|
|
3824
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
3825
|
+
logger.warn(
|
|
3826
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3827
|
+
"request body exceeded size limit"
|
|
3828
|
+
);
|
|
3829
|
+
return jsonError(413, "request_too_large", message);
|
|
3830
|
+
}
|
|
3831
|
+
logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
|
|
3832
|
+
return jsonError(500, "internal_error", message);
|
|
3833
|
+
}).get("/", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/healthz", () => jsonResponse({ name: "hoopilot", object: "health", status: "ok" })).get("/metrics", () => metricsResponse(metrics)).get("/v1/usage", ({ request }) => handleUsage(metrics, readUsage, request.signal)).get(
|
|
3834
|
+
"/v1/models",
|
|
3835
|
+
({ request }) => handleModels(client, metrics, request.signal, loggerFor(request))
|
|
3836
|
+
).get("/v1/responses", () => websocketUnsupportedResponse()).post(
|
|
3837
|
+
"/v1/messages",
|
|
3838
|
+
({ request }) => handleAnthropicMessages(
|
|
3839
|
+
client,
|
|
3840
|
+
metrics,
|
|
3841
|
+
recordTokens,
|
|
3842
|
+
recordExtraction,
|
|
3843
|
+
request,
|
|
3844
|
+
loggerFor(request),
|
|
3845
|
+
bufferProxyBodies
|
|
3846
|
+
),
|
|
3847
|
+
noBody
|
|
3848
|
+
).post(
|
|
3849
|
+
"/v1/messages/count_tokens",
|
|
3850
|
+
({ request }) => handleAnthropicCountTokens(request),
|
|
3851
|
+
noBody
|
|
3852
|
+
).post(
|
|
3853
|
+
"/v1/chat/completions",
|
|
3854
|
+
({ request }) => handleChatCompletions(
|
|
3855
|
+
client,
|
|
3856
|
+
metrics,
|
|
3857
|
+
recordTokens,
|
|
3858
|
+
recordExtraction,
|
|
3859
|
+
request,
|
|
3860
|
+
loggerFor(request),
|
|
3861
|
+
bufferProxyBodies
|
|
3862
|
+
),
|
|
3863
|
+
noBody
|
|
3864
|
+
).post(
|
|
3865
|
+
"/v1/completions",
|
|
3866
|
+
({ request }) => handleCompletions(
|
|
3867
|
+
client,
|
|
3868
|
+
metrics,
|
|
3869
|
+
recordTokens,
|
|
3870
|
+
recordExtraction,
|
|
3871
|
+
request,
|
|
3872
|
+
loggerFor(request),
|
|
3873
|
+
bufferProxyBodies
|
|
3874
|
+
),
|
|
3875
|
+
noBody
|
|
3876
|
+
).post(
|
|
3877
|
+
"/v1/responses/compact",
|
|
3878
|
+
({ request }) => handleResponsesCompact(
|
|
3879
|
+
client,
|
|
3880
|
+
metrics,
|
|
3881
|
+
recordTokens,
|
|
3882
|
+
recordExtraction,
|
|
3883
|
+
request,
|
|
3884
|
+
loggerFor(request)
|
|
3885
|
+
),
|
|
3886
|
+
noBody
|
|
3887
|
+
).post(
|
|
3888
|
+
"/v1/responses",
|
|
3889
|
+
({ request }) => handleResponses(
|
|
3890
|
+
client,
|
|
3891
|
+
metrics,
|
|
3892
|
+
recordTokens,
|
|
3893
|
+
recordExtraction,
|
|
3894
|
+
request,
|
|
3895
|
+
loggerFor(request),
|
|
3896
|
+
bufferProxyBodies
|
|
3897
|
+
),
|
|
3898
|
+
noBody
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
function normalizeInnerRequest(request, canonicalPath, url) {
|
|
3902
|
+
if (canonicalPath === url.pathname) {
|
|
3903
|
+
return request;
|
|
3904
|
+
}
|
|
3905
|
+
const target = new URL(url);
|
|
3906
|
+
target.pathname = canonicalPath;
|
|
3907
|
+
const init = {
|
|
3908
|
+
headers: request.headers,
|
|
3909
|
+
method: request.method,
|
|
3910
|
+
signal: request.signal
|
|
3843
3911
|
};
|
|
3912
|
+
if (request.body) {
|
|
3913
|
+
init.body = request.body;
|
|
3914
|
+
init.duplex = "half";
|
|
3915
|
+
}
|
|
3916
|
+
return new Request(target, init);
|
|
3844
3917
|
}
|
|
3845
3918
|
function startHoopilotServer(options = {}) {
|
|
3846
3919
|
const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
|
|
@@ -3919,7 +3992,8 @@ async function handleAnthropicMessages(client, metrics, recordTokens, recordExtr
|
|
|
3919
3992
|
recordExtraction(usage !== void 0);
|
|
3920
3993
|
return jsonResponse(responsesResponseToAnthropicMessage(body, model));
|
|
3921
3994
|
}
|
|
3922
|
-
function handleAnthropicCountTokens(
|
|
3995
|
+
async function handleAnthropicCountTokens(request) {
|
|
3996
|
+
const body = await readJson(request);
|
|
3923
3997
|
return jsonResponse(estimateAnthropicMessageTokens(body));
|
|
3924
3998
|
}
|
|
3925
3999
|
async function handleModels(client, metrics, signal, logger) {
|
|
@@ -4005,14 +4079,14 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
|
|
|
4005
4079
|
return jsonResponse(chatCompletionToCompletion(completion));
|
|
4006
4080
|
}
|
|
4007
4081
|
async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
|
|
4008
|
-
const body = await readJsonText(request);
|
|
4082
|
+
const { json, text: body } = await readJsonText(request);
|
|
4009
4083
|
const upstream = await client.responses(body, request.signal);
|
|
4010
4084
|
metrics.recordUpstream("/responses", upstream.ok);
|
|
4011
4085
|
if (!upstream.ok) {
|
|
4012
4086
|
return proxyError(upstream, logger);
|
|
4013
4087
|
}
|
|
4014
4088
|
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
4015
|
-
const model = normalizeRequestedModel(
|
|
4089
|
+
const model = normalizeRequestedModel(json.model);
|
|
4016
4090
|
return proxyResponse(
|
|
4017
4091
|
await responseWithObservedUsage(
|
|
4018
4092
|
upstream,
|
|
@@ -4100,17 +4174,16 @@ function parseJsonObject2(text) {
|
|
|
4100
4174
|
try {
|
|
4101
4175
|
parsed = JSON.parse(text);
|
|
4102
4176
|
} catch {
|
|
4103
|
-
throw new
|
|
4177
|
+
throw new InvalidJsonError();
|
|
4104
4178
|
}
|
|
4105
4179
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4106
|
-
throw new
|
|
4180
|
+
throw new JsonNotObjectError();
|
|
4107
4181
|
}
|
|
4108
4182
|
return parsed;
|
|
4109
4183
|
}
|
|
4110
4184
|
async function readJsonText(request) {
|
|
4111
4185
|
const text = await readRequestText(request);
|
|
4112
|
-
parseJsonObject2(text);
|
|
4113
|
-
return text;
|
|
4186
|
+
return { json: parseJsonObject2(text), text };
|
|
4114
4187
|
}
|
|
4115
4188
|
async function readRequestText(request) {
|
|
4116
4189
|
const contentLength = request.headers.get("content-length");
|
|
@@ -4168,7 +4241,7 @@ function jsonError(status, code, message) {
|
|
|
4168
4241
|
);
|
|
4169
4242
|
}
|
|
4170
4243
|
function upstreamErrorResponse(status, text) {
|
|
4171
|
-
const parsedError = asRecord(asRecord(
|
|
4244
|
+
const parsedError = asRecord(asRecord(safeJsonParse(text)).error);
|
|
4172
4245
|
if (Object.keys(parsedError).length > 0) {
|
|
4173
4246
|
return jsonResponse({ error: parsedError }, status);
|
|
4174
4247
|
}
|
|
@@ -4190,13 +4263,18 @@ function corsHeaders() {
|
|
|
4190
4263
|
"access-control-expose-headers": "x-request-id"
|
|
4191
4264
|
};
|
|
4192
4265
|
}
|
|
4266
|
+
function secretEquals(candidate, secret) {
|
|
4267
|
+
const a = createHash("sha256").update(candidate).digest();
|
|
4268
|
+
const b = createHash("sha256").update(secret).digest();
|
|
4269
|
+
return timingSafeEqual(a, b);
|
|
4270
|
+
}
|
|
4193
4271
|
function isAuthorized(request, apiKey) {
|
|
4194
4272
|
if (!apiKey) {
|
|
4195
4273
|
return true;
|
|
4196
4274
|
}
|
|
4197
4275
|
const authorization = request.headers.get("authorization") ?? "";
|
|
4198
4276
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
4199
|
-
return bearer
|
|
4277
|
+
return bearer !== void 0 && secretEquals(bearer, apiKey) || secretEquals(request.headers.get("x-api-key") ?? "", apiKey);
|
|
4200
4278
|
}
|
|
4201
4279
|
function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
|
|
4202
4280
|
if (origin) {
|
|
@@ -4233,7 +4311,7 @@ function upstreamAuthMessage(message) {
|
|
|
4233
4311
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
4234
4312
|
}
|
|
4235
4313
|
function isLoopbackHost(host) {
|
|
4236
|
-
return host
|
|
4314
|
+
return isLoopbackHostname(host);
|
|
4237
4315
|
}
|
|
4238
4316
|
function urlHost(host) {
|
|
4239
4317
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
@@ -4252,9 +4330,6 @@ function normalizeServerPort(value) {
|
|
|
4252
4330
|
}
|
|
4253
4331
|
return port;
|
|
4254
4332
|
}
|
|
4255
|
-
function errorMessage(error) {
|
|
4256
|
-
return error instanceof Error ? error.message : String(error);
|
|
4257
|
-
}
|
|
4258
4333
|
function serverLogger(options) {
|
|
4259
4334
|
if (options.logger) {
|
|
4260
4335
|
return options.logger.child({ component: "server" });
|
|
@@ -4270,10 +4345,7 @@ function serverLogger(options) {
|
|
|
4270
4345
|
}
|
|
4271
4346
|
function resolveStreamingProxyMode(options) {
|
|
4272
4347
|
const value = options.streamingProxyMode ?? envValue(options.env?.HOOPILOT_STREAM_MODE) ?? envValue(options.env?.HOOPILOT_STREAMING_PROXY_MODE) ?? "auto";
|
|
4273
|
-
|
|
4274
|
-
return value;
|
|
4275
|
-
}
|
|
4276
|
-
throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
|
|
4348
|
+
return parseStreamingProxyMode(value);
|
|
4277
4349
|
}
|
|
4278
4350
|
function shouldBufferProxyBodies(mode) {
|
|
4279
4351
|
if (mode === "buffer") {
|
|
@@ -4331,11 +4403,13 @@ function responseWithRequestId(response, requestId, closeConnection, corsOrigin)
|
|
|
4331
4403
|
function trackStreamCompletion(body, onComplete) {
|
|
4332
4404
|
const reader = body.getReader();
|
|
4333
4405
|
let fired = false;
|
|
4334
|
-
const
|
|
4335
|
-
if (
|
|
4336
|
-
|
|
4337
|
-
onComplete();
|
|
4406
|
+
const release = () => {
|
|
4407
|
+
if (fired) {
|
|
4408
|
+
return;
|
|
4338
4409
|
}
|
|
4410
|
+
fired = true;
|
|
4411
|
+
onComplete();
|
|
4412
|
+
reader.releaseLock();
|
|
4339
4413
|
};
|
|
4340
4414
|
return new ReadableStream({
|
|
4341
4415
|
async pull(controller) {
|
|
@@ -4343,18 +4417,25 @@ function trackStreamCompletion(body, onComplete) {
|
|
|
4343
4417
|
const { done, value } = await reader.read();
|
|
4344
4418
|
if (done) {
|
|
4345
4419
|
controller.close();
|
|
4346
|
-
|
|
4420
|
+
release();
|
|
4347
4421
|
return;
|
|
4348
4422
|
}
|
|
4349
4423
|
controller.enqueue(value);
|
|
4350
4424
|
} catch (error) {
|
|
4351
|
-
|
|
4425
|
+
release();
|
|
4352
4426
|
controller.error(error);
|
|
4353
4427
|
}
|
|
4354
4428
|
},
|
|
4355
|
-
cancel(reason) {
|
|
4356
|
-
|
|
4357
|
-
|
|
4429
|
+
async cancel(reason) {
|
|
4430
|
+
if (!fired) {
|
|
4431
|
+
fired = true;
|
|
4432
|
+
onComplete();
|
|
4433
|
+
}
|
|
4434
|
+
try {
|
|
4435
|
+
await reader.cancel(reason);
|
|
4436
|
+
} finally {
|
|
4437
|
+
reader.releaseLock();
|
|
4438
|
+
}
|
|
4358
4439
|
}
|
|
4359
4440
|
});
|
|
4360
4441
|
}
|
|
@@ -4402,47 +4483,26 @@ function canonicalApiPath(path) {
|
|
|
4402
4483
|
return withoutTrailingSlash;
|
|
4403
4484
|
}
|
|
4404
4485
|
}
|
|
4486
|
+
var API_ROUTES = [
|
|
4487
|
+
{ method: "GET", path: "/", name: "health" },
|
|
4488
|
+
{ method: "GET", path: "/healthz", name: "health" },
|
|
4489
|
+
{ method: "GET", path: "/dashboard", name: "dashboard" },
|
|
4490
|
+
{ method: "GET", path: "/metrics", name: "metrics" },
|
|
4491
|
+
{ method: "GET", path: "/v1/usage", name: "usage" },
|
|
4492
|
+
{ method: "GET", path: "/v1/models", name: "models" },
|
|
4493
|
+
{ method: "GET", path: "/v1/responses", name: "responses_websocket" },
|
|
4494
|
+
{ method: "POST", path: "/v1/messages", name: "anthropic_messages" },
|
|
4495
|
+
{ method: "POST", path: "/v1/messages/count_tokens", name: "anthropic_count_tokens" },
|
|
4496
|
+
{ method: "POST", path: "/v1/chat/completions", name: "chat_completions" },
|
|
4497
|
+
{ method: "POST", path: "/v1/completions", name: "completions" },
|
|
4498
|
+
{ method: "POST", path: "/v1/responses/compact", name: "responses_compact" },
|
|
4499
|
+
{ method: "POST", path: "/v1/responses", name: "responses" }
|
|
4500
|
+
];
|
|
4405
4501
|
function routeFor(method, path) {
|
|
4406
4502
|
if (method === "OPTIONS") {
|
|
4407
4503
|
return "cors.preflight";
|
|
4408
4504
|
}
|
|
4409
|
-
|
|
4410
|
-
return "health";
|
|
4411
|
-
}
|
|
4412
|
-
if (method === "GET" && path === "/dashboard") {
|
|
4413
|
-
return "dashboard";
|
|
4414
|
-
}
|
|
4415
|
-
if (method === "GET" && path === "/metrics") {
|
|
4416
|
-
return "metrics";
|
|
4417
|
-
}
|
|
4418
|
-
if (method === "GET" && path === "/v1/usage") {
|
|
4419
|
-
return "usage";
|
|
4420
|
-
}
|
|
4421
|
-
if (method === "GET" && path === "/v1/models") {
|
|
4422
|
-
return "models";
|
|
4423
|
-
}
|
|
4424
|
-
if (method === "POST" && path === "/v1/messages") {
|
|
4425
|
-
return "anthropic_messages";
|
|
4426
|
-
}
|
|
4427
|
-
if (method === "POST" && path === "/v1/messages/count_tokens") {
|
|
4428
|
-
return "anthropic_count_tokens";
|
|
4429
|
-
}
|
|
4430
|
-
if (method === "POST" && path === "/v1/chat/completions") {
|
|
4431
|
-
return "chat_completions";
|
|
4432
|
-
}
|
|
4433
|
-
if (method === "POST" && path === "/v1/completions") {
|
|
4434
|
-
return "completions";
|
|
4435
|
-
}
|
|
4436
|
-
if (method === "POST" && path === "/v1/responses/compact") {
|
|
4437
|
-
return "responses_compact";
|
|
4438
|
-
}
|
|
4439
|
-
if (method === "POST" && path === "/v1/responses") {
|
|
4440
|
-
return "responses";
|
|
4441
|
-
}
|
|
4442
|
-
if (method === "GET" && path === "/v1/responses") {
|
|
4443
|
-
return "responses_websocket";
|
|
4444
|
-
}
|
|
4445
|
-
return "not_found";
|
|
4505
|
+
return API_ROUTES.find((entry) => entry.method === method && entry.path === path)?.name ?? "not_found";
|
|
4446
4506
|
}
|
|
4447
4507
|
function isStreamingResponse(response) {
|
|
4448
4508
|
return response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
@@ -4520,24 +4580,19 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
4520
4580
|
}
|
|
4521
4581
|
};
|
|
4522
4582
|
}
|
|
4523
|
-
function safeParseJson(text) {
|
|
4524
|
-
try {
|
|
4525
|
-
return JSON.parse(text);
|
|
4526
|
-
} catch {
|
|
4527
|
-
return void 0;
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
4583
|
export {
|
|
4531
4584
|
AnthropicCompatibilityError,
|
|
4532
4585
|
COPILOT_USAGE_API_VERSION,
|
|
4533
4586
|
CopilotAuth,
|
|
4534
4587
|
CopilotAuthError,
|
|
4535
4588
|
CopilotClient,
|
|
4589
|
+
DEFAULT_COPILOT_API_BASE_URL,
|
|
4536
4590
|
DEFAULT_GITHUB_API_BASE_URL,
|
|
4537
4591
|
DEFAULT_LOG_FORMAT,
|
|
4538
4592
|
DEFAULT_LOG_LEVEL,
|
|
4539
4593
|
DEFAULT_MODEL,
|
|
4540
4594
|
MetricsRegistry,
|
|
4595
|
+
OpenAICompatibilityError,
|
|
4541
4596
|
PROMETHEUS_CONTENT_TYPE,
|
|
4542
4597
|
anthropicMessagesToResponsesRequest,
|
|
4543
4598
|
applyCopilotHeaders,
|
|
@@ -4563,6 +4618,7 @@ export {
|
|
|
4563
4618
|
parseLogLevel,
|
|
4564
4619
|
parseRateLimitHeaders,
|
|
4565
4620
|
readStoredCopilotAuth,
|
|
4621
|
+
recordResponseTextUsage,
|
|
4566
4622
|
responsesCompactionResult,
|
|
4567
4623
|
responsesRequestToChatCompletion,
|
|
4568
4624
|
responsesResponseToAnthropicMessage,
|