@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/cli.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
import {
|
|
3
3
|
asRecord,
|
|
4
4
|
envValue,
|
|
5
|
+
errorMessage,
|
|
6
|
+
firstNumber,
|
|
7
|
+
isLoopbackHostname,
|
|
5
8
|
isTrustedTokenBaseUrl,
|
|
6
9
|
main,
|
|
10
|
+
modelIdsFromResponse,
|
|
11
|
+
parseJsonObject,
|
|
12
|
+
parseStreamingProxyMode,
|
|
13
|
+
randomId,
|
|
14
|
+
removeUndefined,
|
|
15
|
+
safeJsonParse,
|
|
7
16
|
trimTrailingSlash,
|
|
8
17
|
truncatedResponseText
|
|
9
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6ALEIJJM.js";
|
|
10
19
|
|
|
11
20
|
// src/cli.ts
|
|
12
21
|
import { spawn } from "child_process";
|
|
13
22
|
import { readFileSync as readFileSync2 } from "fs";
|
|
14
23
|
|
|
15
24
|
// src/auth-store.ts
|
|
16
|
-
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
25
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
17
26
|
import { dirname, join } from "path";
|
|
18
27
|
var StoredCopilotAuthError = class extends Error {
|
|
19
28
|
constructor(message) {
|
|
@@ -64,7 +73,7 @@ function readStoredCopilotAuth(path = authStorePath()) {
|
|
|
64
73
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
65
74
|
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
66
75
|
}
|
|
67
|
-
const record = parsed;
|
|
76
|
+
const record = asRecord(parsed);
|
|
68
77
|
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
69
78
|
if (!token) {
|
|
70
79
|
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
@@ -90,7 +99,15 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
90
99
|
`;
|
|
91
100
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
92
101
|
writeFileSync(tmpPath, data, { mode: 384 });
|
|
93
|
-
|
|
102
|
+
try {
|
|
103
|
+
renameSync(tmpPath, path);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
try {
|
|
106
|
+
rmSync(tmpPath, { force: true });
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
94
111
|
try {
|
|
95
112
|
chmodSync(path, 384);
|
|
96
113
|
} catch {
|
|
@@ -135,23 +152,20 @@ var CopilotAuth = class {
|
|
|
135
152
|
throw error;
|
|
136
153
|
}
|
|
137
154
|
if (stored) {
|
|
138
|
-
|
|
155
|
+
this.#cachedAccess = {
|
|
139
156
|
apiBaseUrl: trimTrailingSlash(
|
|
140
157
|
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
141
158
|
),
|
|
142
159
|
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
143
160
|
source: "github-copilot-oauth",
|
|
144
161
|
token: stored.token
|
|
145
|
-
}
|
|
162
|
+
};
|
|
163
|
+
return this.#cachedAccess;
|
|
146
164
|
}
|
|
147
165
|
throw new CopilotAuthError(
|
|
148
166
|
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
149
167
|
);
|
|
150
168
|
}
|
|
151
|
-
#cacheAccess(access) {
|
|
152
|
-
this.#cachedAccess = access;
|
|
153
|
-
return access;
|
|
154
|
-
}
|
|
155
169
|
};
|
|
156
170
|
|
|
157
171
|
// src/copilot.ts
|
|
@@ -159,23 +173,26 @@ var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
|
159
173
|
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
160
174
|
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
161
175
|
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
176
|
+
var EDITOR_PLUGIN_VERSION = "hoopilot/0.1.0";
|
|
177
|
+
var EDITOR_VERSION = "Hoopilot/0.1.0";
|
|
178
|
+
var HOOPILOT_USER_AGENT = "hoopilot/0.1.0";
|
|
162
179
|
function applyCopilotHeaders(headers, token) {
|
|
163
180
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
164
181
|
headers.set("authorization", `Bearer ${token}`);
|
|
165
182
|
headers.set("copilot-integration-id", "vscode-chat");
|
|
166
|
-
headers.set("editor-plugin-version",
|
|
167
|
-
headers.set("editor-version",
|
|
183
|
+
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
|
|
184
|
+
headers.set("editor-version", EDITOR_VERSION);
|
|
168
185
|
headers.set("openai-intent", "conversation-panel");
|
|
169
|
-
headers.set("user-agent",
|
|
186
|
+
headers.set("user-agent", HOOPILOT_USER_AGENT);
|
|
170
187
|
headers.set("x-github-api-version", "2026-06-01");
|
|
171
188
|
return headers;
|
|
172
189
|
}
|
|
173
190
|
function applyGithubApiHeaders(headers, token) {
|
|
174
191
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
175
192
|
headers.set("authorization", `token ${token}`);
|
|
176
|
-
headers.set("editor-plugin-version",
|
|
177
|
-
headers.set("editor-version",
|
|
178
|
-
headers.set("user-agent",
|
|
193
|
+
headers.set("editor-plugin-version", EDITOR_PLUGIN_VERSION);
|
|
194
|
+
headers.set("editor-version", EDITOR_VERSION);
|
|
195
|
+
headers.set("user-agent", HOOPILOT_USER_AGENT);
|
|
179
196
|
headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
|
|
180
197
|
return headers;
|
|
181
198
|
}
|
|
@@ -188,7 +205,7 @@ function parseRateLimitHeaders(headers, nowMs = Date.now()) {
|
|
|
188
205
|
if (limit === void 0 && remaining === void 0 && used === void 0 && resetEpochSeconds === void 0 && retryAfterSeconds === void 0) {
|
|
189
206
|
return void 0;
|
|
190
207
|
}
|
|
191
|
-
return
|
|
208
|
+
return removeUndefined({
|
|
192
209
|
limit,
|
|
193
210
|
observedAtMs: nowMs,
|
|
194
211
|
remaining,
|
|
@@ -206,11 +223,6 @@ function headerInt(headers, name) {
|
|
|
206
223
|
const value = Number.parseInt(raw.trim(), 10);
|
|
207
224
|
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
208
225
|
}
|
|
209
|
-
function removeUndefinedRateLimit(rateLimit) {
|
|
210
|
-
return Object.fromEntries(
|
|
211
|
-
Object.entries(rateLimit).filter(([, value]) => value !== void 0)
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
226
|
var CopilotClient = class {
|
|
215
227
|
#auth;
|
|
216
228
|
#allowUnsafeUpstream;
|
|
@@ -307,7 +319,7 @@ function normalizeCopilotUsage(body) {
|
|
|
307
319
|
for (const category of /* @__PURE__ */ new Set([...Object.keys(remaining), ...Object.keys(monthly)])) {
|
|
308
320
|
const entitlement = numberOrUndefined(monthly[category]);
|
|
309
321
|
const left = numberOrUndefined(remaining[category]);
|
|
310
|
-
quotas[category] =
|
|
322
|
+
quotas[category] = removeUndefined({
|
|
311
323
|
entitlement,
|
|
312
324
|
percentRemaining: entitlement !== void 0 && entitlement > 0 && left !== void 0 ? left / entitlement * 100 : void 0,
|
|
313
325
|
remaining: left,
|
|
@@ -315,7 +327,7 @@ function normalizeCopilotUsage(body) {
|
|
|
315
327
|
});
|
|
316
328
|
}
|
|
317
329
|
}
|
|
318
|
-
return
|
|
330
|
+
return removeUndefined({
|
|
319
331
|
accessTypeSku: stringOrUndefined(record.access_type_sku),
|
|
320
332
|
chatEnabled: typeof record.chat_enabled === "boolean" ? record.chat_enabled : void 0,
|
|
321
333
|
plan: stringOrUndefined(record.copilot_plan),
|
|
@@ -327,7 +339,7 @@ function normalizeQuotaDetail(detail) {
|
|
|
327
339
|
const entitlement = numberOrUndefined(detail.entitlement);
|
|
328
340
|
const overageCount = numberOrUndefined(detail.overage_count);
|
|
329
341
|
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
330
|
-
return
|
|
342
|
+
return removeUndefined({
|
|
331
343
|
entitlement,
|
|
332
344
|
hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
|
|
333
345
|
overageCount,
|
|
@@ -351,21 +363,10 @@ function usedFrom(entitlement, remaining, overageCount) {
|
|
|
351
363
|
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
352
364
|
return Math.max(0, base + overage);
|
|
353
365
|
}
|
|
354
|
-
|
|
355
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
356
|
-
}
|
|
366
|
+
var numberOrUndefined = firstNumber;
|
|
357
367
|
function stringOrUndefined(value) {
|
|
358
368
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
359
369
|
}
|
|
360
|
-
function removeUndefinedQuota(quota) {
|
|
361
|
-
return Object.fromEntries(
|
|
362
|
-
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
function removeUndefinedUsage(usage) {
|
|
366
|
-
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
367
|
-
return Object.fromEntries(entries);
|
|
368
|
-
}
|
|
369
370
|
|
|
370
371
|
// src/github-device.ts
|
|
371
372
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -497,11 +498,16 @@ function positiveSeconds(value, fallback) {
|
|
|
497
498
|
}
|
|
498
499
|
async function parseJsonResponse(response, context) {
|
|
499
500
|
const text = await response.text();
|
|
501
|
+
let value;
|
|
500
502
|
try {
|
|
501
|
-
|
|
503
|
+
value = JSON.parse(text);
|
|
502
504
|
} catch {
|
|
503
505
|
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
504
506
|
}
|
|
507
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
508
|
+
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
509
|
+
}
|
|
510
|
+
return value;
|
|
505
511
|
}
|
|
506
512
|
|
|
507
513
|
// src/logger.ts
|
|
@@ -565,21 +571,29 @@ function createHoopilotLogger(options = {}) {
|
|
|
565
571
|
timestamp: pino.stdTimeFunctions.isoTime
|
|
566
572
|
};
|
|
567
573
|
if (format === "pretty") {
|
|
568
|
-
return
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
574
|
+
return asHoopilotLogger(
|
|
575
|
+
pino(
|
|
576
|
+
pinoOptions,
|
|
577
|
+
pretty({
|
|
578
|
+
// Probe the same sink we write to (stdout / fd 1), so colors are not
|
|
579
|
+
// emitted into a redirected file when only stderr is a TTY. A custom
|
|
580
|
+
// stream's TTY-ness is unknown, so default to no color there.
|
|
581
|
+
colorize: options.colorize ?? (options.stream ? false : process.stdout.isTTY),
|
|
582
|
+
destination: options.stream ?? 1,
|
|
583
|
+
ignore: "pid,hostname",
|
|
584
|
+
singleLine: true,
|
|
585
|
+
translateTime: "SYS:standard"
|
|
586
|
+
})
|
|
587
|
+
)
|
|
577
588
|
);
|
|
578
589
|
}
|
|
579
590
|
if (options.stream) {
|
|
580
|
-
return pino(pinoOptions, options.stream);
|
|
591
|
+
return asHoopilotLogger(pino(pinoOptions, options.stream));
|
|
581
592
|
}
|
|
582
|
-
return pino(pinoOptions);
|
|
593
|
+
return asHoopilotLogger(pino(pinoOptions));
|
|
594
|
+
}
|
|
595
|
+
function asHoopilotLogger(logger) {
|
|
596
|
+
return logger;
|
|
583
597
|
}
|
|
584
598
|
function parseLogFormat(value) {
|
|
585
599
|
if (!value) {
|
|
@@ -621,6 +635,10 @@ function isLogLevel(value) {
|
|
|
621
635
|
return LOG_LEVELS.includes(value);
|
|
622
636
|
}
|
|
623
637
|
|
|
638
|
+
// src/server.ts
|
|
639
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
640
|
+
import { Elysia } from "elysia";
|
|
641
|
+
|
|
624
642
|
// src/openai.ts
|
|
625
643
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
626
644
|
var OpenAICompatibilityError = class extends Error {
|
|
@@ -693,13 +711,6 @@ function compactionOutputFromResponsesSse(text) {
|
|
|
693
711
|
}
|
|
694
712
|
return deltas ? [messageOutputItem(deltas)] : [];
|
|
695
713
|
}
|
|
696
|
-
function safeJsonParse(text) {
|
|
697
|
-
try {
|
|
698
|
-
return JSON.parse(text);
|
|
699
|
-
} catch {
|
|
700
|
-
return void 0;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
714
|
function chatCompletionToCompletion(completion) {
|
|
704
715
|
return removeUndefined({
|
|
705
716
|
choices: completionChoices(completion).map((choice, index) => {
|
|
@@ -895,21 +906,18 @@ function extractTokenUsage(usage) {
|
|
|
895
906
|
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
896
907
|
asRecord(record.input_tokens_details).cached_tokens
|
|
897
908
|
);
|
|
898
|
-
|
|
899
|
-
cachedTokens: cached,
|
|
909
|
+
const result = {
|
|
900
910
|
completionTokens,
|
|
901
911
|
promptTokens,
|
|
902
|
-
reasoningTokens: reasoning,
|
|
903
912
|
totalTokens: total ?? promptTokens + completionTokens
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
for (const value of values) {
|
|
908
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
909
|
-
return value;
|
|
910
|
-
}
|
|
913
|
+
};
|
|
914
|
+
if (cached !== void 0) {
|
|
915
|
+
result.cachedTokens = cached;
|
|
911
916
|
}
|
|
912
|
-
|
|
917
|
+
if (reasoning !== void 0) {
|
|
918
|
+
result.reasoningTokens = reasoning;
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
913
921
|
}
|
|
914
922
|
function completionChoices(completion) {
|
|
915
923
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
@@ -935,7 +943,7 @@ function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
|
935
943
|
enqueue("[DONE]");
|
|
936
944
|
return;
|
|
937
945
|
}
|
|
938
|
-
const parsed =
|
|
946
|
+
const parsed = parseJsonObject(data);
|
|
939
947
|
if (!parsed) {
|
|
940
948
|
return;
|
|
941
949
|
}
|
|
@@ -999,19 +1007,6 @@ function encodeDataSse(data) {
|
|
|
999
1007
|
|
|
1000
1008
|
`;
|
|
1001
1009
|
}
|
|
1002
|
-
function parseJson(data) {
|
|
1003
|
-
try {
|
|
1004
|
-
return asRecord(JSON.parse(data));
|
|
1005
|
-
} catch {
|
|
1006
|
-
return void 0;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
function removeUndefined(record) {
|
|
1010
|
-
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
1011
|
-
}
|
|
1012
|
-
function randomId() {
|
|
1013
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
1014
|
-
}
|
|
1015
1010
|
function epochSeconds() {
|
|
1016
1011
|
return Math.floor(Date.now() / 1e3);
|
|
1017
1012
|
}
|
|
@@ -1024,13 +1019,13 @@ var AnthropicCompatibilityError = class extends Error {
|
|
|
1024
1019
|
}
|
|
1025
1020
|
};
|
|
1026
1021
|
function anthropicMessagesToResponsesRequest(request) {
|
|
1027
|
-
return
|
|
1022
|
+
return removeUndefined({
|
|
1028
1023
|
input: anthropicMessagesToResponsesInput(request.messages),
|
|
1029
1024
|
instructions: anthropicSystemToInstructions(request.system),
|
|
1030
1025
|
max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
|
|
1031
1026
|
metadata: request.metadata,
|
|
1032
1027
|
model: normalizeRequestedModel(request.model),
|
|
1033
|
-
parallel_tool_calls: true,
|
|
1028
|
+
parallel_tool_calls: asRecord(request.tool_choice).disable_parallel_tool_use !== true,
|
|
1034
1029
|
reasoning: anthropicThinkingToReasoning(request.thinking),
|
|
1035
1030
|
stop: anthropicStopSequences(request.stop_sequences),
|
|
1036
1031
|
stream: request.stream === true,
|
|
@@ -1045,7 +1040,7 @@ function responsesResponseToAnthropicMessage(response, fallbackModel) {
|
|
|
1045
1040
|
const usage = anthropicUsage(response.usage);
|
|
1046
1041
|
return {
|
|
1047
1042
|
content,
|
|
1048
|
-
id: textValue(response.id) || `msg_${
|
|
1043
|
+
id: textValue(response.id) || `msg_${randomId()}`,
|
|
1049
1044
|
model: textValue(response.model) || fallbackModel,
|
|
1050
1045
|
role: "assistant",
|
|
1051
1046
|
stop_reason: anthropicStopReason(response, content),
|
|
@@ -1122,7 +1117,7 @@ function createAnthropicStreamState(options) {
|
|
|
1122
1117
|
return {
|
|
1123
1118
|
blocks: /* @__PURE__ */ new Map(),
|
|
1124
1119
|
completed: false,
|
|
1125
|
-
messageId: options.messageId ?? `msg_${
|
|
1120
|
+
messageId: options.messageId ?? `msg_${randomId()}`,
|
|
1126
1121
|
model: options.model,
|
|
1127
1122
|
nextBlockIndex: 0,
|
|
1128
1123
|
sawToolUse: false,
|
|
@@ -1176,7 +1171,7 @@ function anthropicMessagesToResponsesInput(messages) {
|
|
|
1176
1171
|
flushMessage();
|
|
1177
1172
|
input.push({
|
|
1178
1173
|
arguments: JSON.stringify(asRecord(part.input)),
|
|
1179
|
-
call_id: textValue(part.id) || `call_${
|
|
1174
|
+
call_id: textValue(part.id) || `call_${randomId()}`,
|
|
1180
1175
|
name: textValue(part.name),
|
|
1181
1176
|
type: "function_call"
|
|
1182
1177
|
});
|
|
@@ -1287,7 +1282,7 @@ function anthropicTools(tools) {
|
|
|
1287
1282
|
}
|
|
1288
1283
|
const converted = tools.map((tool) => {
|
|
1289
1284
|
const record = asRecord(tool);
|
|
1290
|
-
return
|
|
1285
|
+
return removeUndefined({
|
|
1291
1286
|
description: record.description,
|
|
1292
1287
|
name: record.name,
|
|
1293
1288
|
parameters: record.input_schema,
|
|
@@ -1358,7 +1353,7 @@ function anthropicContentFromResponsesOutput(response) {
|
|
|
1358
1353
|
}
|
|
1359
1354
|
if (type === "function_call") {
|
|
1360
1355
|
content.push({
|
|
1361
|
-
id: textValue(record.call_id) || textValue(record.id) || `call_${
|
|
1356
|
+
id: textValue(record.call_id) || textValue(record.id) || `call_${randomId()}`,
|
|
1362
1357
|
input: parseToolInput(textValue(record.arguments)),
|
|
1363
1358
|
name: textValue(record.name),
|
|
1364
1359
|
type: "tool_use"
|
|
@@ -1385,12 +1380,12 @@ function anthropicStopReason(response, content) {
|
|
|
1385
1380
|
}
|
|
1386
1381
|
function anthropicUsage(usage) {
|
|
1387
1382
|
const record = asRecord(usage);
|
|
1388
|
-
const inputTokens =
|
|
1389
|
-
const outputTokens =
|
|
1383
|
+
const inputTokens = firstNumber(record.input_tokens, record.prompt_tokens) ?? 0;
|
|
1384
|
+
const outputTokens = firstNumber(record.output_tokens, record.completion_tokens) ?? 0;
|
|
1390
1385
|
const details = asRecord(record.input_tokens_details);
|
|
1391
|
-
return
|
|
1392
|
-
cache_creation_input_tokens:
|
|
1393
|
-
cache_read_input_tokens:
|
|
1386
|
+
return removeUndefined({
|
|
1387
|
+
cache_creation_input_tokens: firstNumber(record.cache_creation_input_tokens),
|
|
1388
|
+
cache_read_input_tokens: firstNumber(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
|
|
1394
1389
|
input_tokens: inputTokens,
|
|
1395
1390
|
output_tokens: outputTokens
|
|
1396
1391
|
});
|
|
@@ -1572,7 +1567,7 @@ function ensureToolBlock(state, payload, item, enqueue) {
|
|
|
1572
1567
|
state.blocks.set(key, block);
|
|
1573
1568
|
enqueue("content_block_start", {
|
|
1574
1569
|
content_block: {
|
|
1575
|
-
id: textValue(item.call_id) || textValue(item.id) || `call_${
|
|
1570
|
+
id: textValue(item.call_id) || textValue(item.id) || `call_${randomId()}`,
|
|
1576
1571
|
input: {},
|
|
1577
1572
|
name: textValue(item.name),
|
|
1578
1573
|
type: "tool_use"
|
|
@@ -1606,13 +1601,6 @@ function parseSseBlock(block) {
|
|
|
1606
1601
|
}
|
|
1607
1602
|
return { data: data.join("\n"), event };
|
|
1608
1603
|
}
|
|
1609
|
-
function parseJsonObject(text) {
|
|
1610
|
-
try {
|
|
1611
|
-
return asRecord(JSON.parse(text));
|
|
1612
|
-
} catch {
|
|
1613
|
-
return void 0;
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
1604
|
function parseToolInput(argumentsText) {
|
|
1617
1605
|
const parsed = parseJsonObject(argumentsText);
|
|
1618
1606
|
return parsed ?? {};
|
|
@@ -1644,29 +1632,15 @@ function textValue(value) {
|
|
|
1644
1632
|
}
|
|
1645
1633
|
return "";
|
|
1646
1634
|
}
|
|
1647
|
-
function firstNumber2(...values) {
|
|
1648
|
-
for (const value of values) {
|
|
1649
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1650
|
-
return value;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
return void 0;
|
|
1654
|
-
}
|
|
1655
1635
|
function indexValue(value) {
|
|
1656
1636
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1657
1637
|
}
|
|
1658
|
-
function removeUndefined2(record) {
|
|
1659
|
-
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
1660
|
-
}
|
|
1661
1638
|
function encodeSse(event, data) {
|
|
1662
1639
|
return `event: ${event}
|
|
1663
1640
|
data: ${JSON.stringify(data)}
|
|
1664
1641
|
|
|
1665
1642
|
`;
|
|
1666
1643
|
}
|
|
1667
|
-
function randomId2() {
|
|
1668
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
1669
|
-
}
|
|
1670
1644
|
|
|
1671
1645
|
// src/dashboard.ts
|
|
1672
1646
|
var DASHBOARD_HTML = `<!doctype html>
|
|
@@ -2068,21 +2042,24 @@ footer.foot .end { margin-left:auto; }
|
|
|
2068
2042
|
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; }
|
|
2069
2043
|
|
|
2070
2044
|
// Set numeric text and flash on discrete change.
|
|
2071
|
-
function setNum(id, value, kind){
|
|
2045
|
+
function setNum(id, value, kind, num){
|
|
2072
2046
|
var el = byId(id); if (!el) return;
|
|
2073
2047
|
el.classList.remove("skel");
|
|
2074
2048
|
var s = String(value);
|
|
2075
2049
|
if (el.textContent !== s){
|
|
2076
2050
|
el.textContent = s;
|
|
2051
|
+
// Compare on the raw number (num) when provided, so directional flash works
|
|
2052
|
+
// even when value is a pre-formatted display string.
|
|
2053
|
+
var n = (num !== undefined) ? num : value;
|
|
2077
2054
|
var prev = lastRender[id];
|
|
2078
2055
|
if (prev !== undefined){
|
|
2079
2056
|
var cls = "flash";
|
|
2080
|
-
if (kind === "delta" && typeof
|
|
2081
|
-
cls =
|
|
2057
|
+
if (kind === "delta" && typeof n === "number" && typeof prev === "number"){
|
|
2058
|
+
cls = n > prev ? "flash-up" : (n < prev ? "flash-down" : null);
|
|
2082
2059
|
}
|
|
2083
2060
|
if (cls){ el.classList.remove("flash","flash-up","flash-down"); void el.offsetWidth; el.classList.add(cls); }
|
|
2084
2061
|
}
|
|
2085
|
-
lastRender[id] =
|
|
2062
|
+
lastRender[id] = n;
|
|
2086
2063
|
}
|
|
2087
2064
|
}
|
|
2088
2065
|
function setText(id, s){ var el = byId(id); if (el){ el.classList.remove("skel"); el.textContent = s; } }
|
|
@@ -2163,7 +2140,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
2163
2140
|
if (beat && dot){ dot.classList.remove("heartbeat"); void dot.offsetWidth; dot.classList.add("heartbeat"); }
|
|
2164
2141
|
}
|
|
2165
2142
|
function showBanner(text, ok){
|
|
2166
|
-
var b = byId("banner"); b.textContent = text; b.className = "
|
|
2143
|
+
var b = byId("banner"); b.textContent = text; b.className = "show" + (ok ? " ok" : "");
|
|
2167
2144
|
if (ok){ setTimeout(function(){ b.classList.remove("show"); }, 2000); }
|
|
2168
2145
|
}
|
|
2169
2146
|
function hideBanner(){ byId("banner").classList.remove("show"); }
|
|
@@ -2270,7 +2247,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
2270
2247
|
if (isFinite(reqPerSec)){ pushHist(hist.req, reqPerSec); setNum("req-num", rate(reqPerSec)); } else setText("req-num","\\u2014");
|
|
2271
2248
|
if (isFinite(tokPerSec)){ pushHist(hist.tok, tokPerSec); setNum("tok-num", humanInt(tokPerSec)); } else setText("tok-num","\\u2014");
|
|
2272
2249
|
var inflight = proxy.inFlight || 0;
|
|
2273
|
-
pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta");
|
|
2250
|
+
pushHist(hist.inflight, inflight); setNum("inflight-num", String(inflight), "delta", inflight);
|
|
2274
2251
|
byId("v-inflight").classList.toggle("active", inflight > 0);
|
|
2275
2252
|
setText("uptime-num", fmtUptime(proxy.uptimeSeconds || 0));
|
|
2276
2253
|
|
|
@@ -2413,7 +2390,7 @@ footer.foot .end { margin-left:auto; }
|
|
|
2413
2390
|
|
|
2414
2391
|
function renderUpstream(up, delta, restarted){
|
|
2415
2392
|
setNum("up-total", humanInt(up.total||0));
|
|
2416
|
-
setNum("up-errors", humanInt(up.errors||0), "delta");
|
|
2393
|
+
setNum("up-errors", humanInt(up.errors||0), "delta", up.errors||0);
|
|
2417
2394
|
var er = up.total ? (up.errors/up.total*100) : 0;
|
|
2418
2395
|
var rt = byId("up-rate"); rt.textContent = pct(er); rt.className = "v rate " + (er > 5 ? "danger" : er > 1 ? "warn" : "ok");
|
|
2419
2396
|
byId("up-errblk").classList.toggle("hot", (up.errors||0) > 0);
|
|
@@ -2558,26 +2535,23 @@ var MetricsRegistry = class {
|
|
|
2558
2535
|
const resource = this.#rateLimitResource(rateLimit.resource);
|
|
2559
2536
|
this.#githubRateLimit.set(resource, { ...rateLimit, resource });
|
|
2560
2537
|
}
|
|
2561
|
-
//
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
if (!this.#tokens.has(cleaned) && this.#tokens.size >= MAX_TRACKED_MODELS) {
|
|
2538
|
+
// Clean a raw value into a bounded exposition-format label: cap its length,
|
|
2539
|
+
// strip characters that would corrupt the format, and fold overflow past the
|
|
2540
|
+
// cardinality limit into UNKNOWN_MODEL so the series count stays bounded.
|
|
2541
|
+
#boundedLabel(value, tracked, maxEntries) {
|
|
2542
|
+
const cleaned = cleanLabel(value).slice(0, MAX_MODEL_LABEL_LENGTH) || UNKNOWN_MODEL;
|
|
2543
|
+
if (!tracked.has(cleaned) && tracked.size >= maxEntries) {
|
|
2568
2544
|
return UNKNOWN_MODEL;
|
|
2569
2545
|
}
|
|
2570
2546
|
return cleaned;
|
|
2571
2547
|
}
|
|
2572
|
-
// The
|
|
2573
|
-
|
|
2574
|
-
|
|
2548
|
+
// The model can originate from a (possibly hostile) client request.
|
|
2549
|
+
#modelLabel(model) {
|
|
2550
|
+
return this.#boundedLabel(model, this.#tokens, MAX_TRACKED_MODELS);
|
|
2551
|
+
}
|
|
2552
|
+
// The resource comes from a trusted upstream header, but is bounded the same way.
|
|
2575
2553
|
#rateLimitResource(resource) {
|
|
2576
|
-
|
|
2577
|
-
if (!this.#githubRateLimit.has(cleaned) && this.#githubRateLimit.size >= MAX_TRACKED_RATELIMIT_RESOURCES) {
|
|
2578
|
-
return UNKNOWN_MODEL;
|
|
2579
|
-
}
|
|
2580
|
-
return cleaned;
|
|
2554
|
+
return this.#boundedLabel(resource, this.#githubRateLimit, MAX_TRACKED_RATELIMIT_RESOURCES);
|
|
2581
2555
|
}
|
|
2582
2556
|
#observeDuration(route, seconds) {
|
|
2583
2557
|
const value = Number.isFinite(seconds) && seconds >= 0 ? seconds : 0;
|
|
@@ -2903,7 +2877,7 @@ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage, onOutcome)
|
|
|
2903
2877
|
considerSseLine(line, accumulator.consider);
|
|
2904
2878
|
}
|
|
2905
2879
|
} else {
|
|
2906
|
-
const parsed =
|
|
2880
|
+
const parsed = safeJsonParse(text);
|
|
2907
2881
|
if (parsed !== void 0) {
|
|
2908
2882
|
accumulator.consider(parsed);
|
|
2909
2883
|
}
|
|
@@ -2965,7 +2939,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal, onOut
|
|
|
2965
2939
|
considerSseLine(finalBuffer, accumulator.consider);
|
|
2966
2940
|
}
|
|
2967
2941
|
} else if (!overflowed && finalBuffer) {
|
|
2968
|
-
const parsed =
|
|
2942
|
+
const parsed = safeJsonParse(finalBuffer);
|
|
2969
2943
|
if (parsed !== void 0) {
|
|
2970
2944
|
accumulator.consider(parsed);
|
|
2971
2945
|
}
|
|
@@ -3008,18 +2982,11 @@ function considerSseLine(line, consider) {
|
|
|
3008
2982
|
if (!data || data === "[DONE]") {
|
|
3009
2983
|
return;
|
|
3010
2984
|
}
|
|
3011
|
-
const parsed =
|
|
2985
|
+
const parsed = safeJsonParse(data);
|
|
3012
2986
|
if (parsed !== void 0) {
|
|
3013
2987
|
consider(parsed);
|
|
3014
2988
|
}
|
|
3015
2989
|
}
|
|
3016
|
-
function safeParse(text) {
|
|
3017
|
-
try {
|
|
3018
|
-
return JSON.parse(text);
|
|
3019
|
-
} catch {
|
|
3020
|
-
return void 0;
|
|
3021
|
-
}
|
|
3022
|
-
}
|
|
3023
2990
|
function modelText(value) {
|
|
3024
2991
|
return typeof value === "string" ? value.trim() : "";
|
|
3025
2992
|
}
|
|
@@ -3109,8 +3076,9 @@ async function getVersion() {
|
|
|
3109
3076
|
resolved = BAKED_VERSION;
|
|
3110
3077
|
} else {
|
|
3111
3078
|
try {
|
|
3112
|
-
const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
3113
|
-
|
|
3079
|
+
const manifest = asRecord(await Bun.file(new URL("../package.json", import.meta.url)).json());
|
|
3080
|
+
const version = manifest.version;
|
|
3081
|
+
resolved = typeof version === "string" ? version : "0.0.0";
|
|
3114
3082
|
} catch {
|
|
3115
3083
|
resolved = "0.0.0";
|
|
3116
3084
|
}
|
|
@@ -3136,6 +3104,18 @@ var RequestBodyTooLargeError = class extends Error {
|
|
|
3136
3104
|
this.name = "RequestBodyTooLargeError";
|
|
3137
3105
|
}
|
|
3138
3106
|
};
|
|
3107
|
+
var InvalidJsonError = class extends Error {
|
|
3108
|
+
constructor() {
|
|
3109
|
+
super(INVALID_JSON_MESSAGE);
|
|
3110
|
+
this.name = "InvalidJsonError";
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
var JsonNotObjectError = class extends Error {
|
|
3114
|
+
constructor() {
|
|
3115
|
+
super(JSON_OBJECT_MESSAGE);
|
|
3116
|
+
this.name = "JsonNotObjectError";
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3139
3119
|
function createHoopilotHandler(options = {}) {
|
|
3140
3120
|
const client = new CopilotClient(options);
|
|
3141
3121
|
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
@@ -3145,8 +3125,19 @@ function createHoopilotHandler(options = {}) {
|
|
|
3145
3125
|
const readUsage = createUsageReader(client, metrics);
|
|
3146
3126
|
const recordTokens = (model, usage) => metrics.recordTokens(model, usage);
|
|
3147
3127
|
const recordExtraction = (extracted) => metrics.recordTokenExtraction(extracted);
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3128
|
+
const bufferProxyBodies = shouldBufferProxyBodies(resolveStreamingProxyMode(options));
|
|
3129
|
+
const requestContext = /* @__PURE__ */ new WeakMap();
|
|
3130
|
+
const app = buildApp({
|
|
3131
|
+
apiKey,
|
|
3132
|
+
allowedOrigins,
|
|
3133
|
+
bufferProxyBodies,
|
|
3134
|
+
client,
|
|
3135
|
+
metrics,
|
|
3136
|
+
readUsage,
|
|
3137
|
+
recordExtraction,
|
|
3138
|
+
recordTokens,
|
|
3139
|
+
requestContext
|
|
3140
|
+
});
|
|
3150
3141
|
return async (request) => {
|
|
3151
3142
|
const startedAt = performance.now();
|
|
3152
3143
|
const url = new URL(request.url);
|
|
@@ -3162,7 +3153,24 @@ function createHoopilotHandler(options = {}) {
|
|
|
3162
3153
|
metrics.startRequest();
|
|
3163
3154
|
const origin = request.headers.get("origin")?.trim() || void 0;
|
|
3164
3155
|
const corsOrigin = resolveCorsAllowOrigin(origin, allowedOrigins);
|
|
3165
|
-
const
|
|
3156
|
+
const inner = normalizeInnerRequest(request, apiPath, url);
|
|
3157
|
+
requestContext.set(inner, {
|
|
3158
|
+
apiPath,
|
|
3159
|
+
logger: requestLogger,
|
|
3160
|
+
origin,
|
|
3161
|
+
originalPath: url.pathname
|
|
3162
|
+
});
|
|
3163
|
+
let response;
|
|
3164
|
+
try {
|
|
3165
|
+
response = await app.handle(inner);
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
requestLogger.error(
|
|
3168
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3169
|
+
"request failed"
|
|
3170
|
+
);
|
|
3171
|
+
response = jsonError(500, "internal_error", errorMessage(error));
|
|
3172
|
+
}
|
|
3173
|
+
return finishResponse(response, {
|
|
3166
3174
|
corsOrigin,
|
|
3167
3175
|
logger: requestLogger,
|
|
3168
3176
|
method: request.method,
|
|
@@ -3173,144 +3181,175 @@ function createHoopilotHandler(options = {}) {
|
|
|
3173
3181
|
closeConnection: bufferProxyBodies,
|
|
3174
3182
|
trackStreamingBody: !bufferProxyBodies
|
|
3175
3183
|
});
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function buildApp(deps) {
|
|
3187
|
+
const {
|
|
3188
|
+
apiKey,
|
|
3189
|
+
allowedOrigins,
|
|
3190
|
+
bufferProxyBodies,
|
|
3191
|
+
client,
|
|
3192
|
+
metrics,
|
|
3193
|
+
readUsage,
|
|
3194
|
+
recordExtraction,
|
|
3195
|
+
recordTokens,
|
|
3196
|
+
requestContext
|
|
3197
|
+
} = deps;
|
|
3198
|
+
const contextFor = (request) => {
|
|
3199
|
+
const stored = requestContext.get(request);
|
|
3200
|
+
if (stored) {
|
|
3201
|
+
return stored;
|
|
3202
|
+
}
|
|
3203
|
+
const originalPath = new URL(request.url).pathname;
|
|
3204
|
+
return {
|
|
3205
|
+
apiPath: canonicalApiPath(originalPath),
|
|
3206
|
+
logger: noopLogger,
|
|
3207
|
+
origin: request.headers.get("origin")?.trim() || void 0,
|
|
3208
|
+
originalPath
|
|
3209
|
+
};
|
|
3210
|
+
};
|
|
3211
|
+
const loggerFor = (request) => contextFor(request).logger;
|
|
3212
|
+
const noBody = { parse: "none" };
|
|
3213
|
+
return new Elysia().onRequest(({ request }) => {
|
|
3214
|
+
const { apiPath, logger, origin } = contextFor(request);
|
|
3176
3215
|
const browserOrigin = forbiddenBrowserOrigin(origin, request, allowedOrigins);
|
|
3177
3216
|
if (browserOrigin) {
|
|
3178
|
-
|
|
3217
|
+
logger.warn(
|
|
3179
3218
|
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
3180
3219
|
"blocked cross-origin browser request"
|
|
3181
3220
|
);
|
|
3182
|
-
return
|
|
3221
|
+
return jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE);
|
|
3183
3222
|
}
|
|
3184
3223
|
if (request.method === "OPTIONS") {
|
|
3185
|
-
return
|
|
3224
|
+
return new Response(null, { headers: corsHeaders() });
|
|
3186
3225
|
}
|
|
3187
3226
|
if (request.method === "GET" && apiPath === "/dashboard") {
|
|
3188
|
-
return
|
|
3227
|
+
return dashboardResponse();
|
|
3189
3228
|
}
|
|
3190
3229
|
if (!isAuthorized(request, apiKey)) {
|
|
3191
|
-
|
|
3192
|
-
return
|
|
3230
|
+
logger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
|
|
3231
|
+
return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
|
|
3232
|
+
}
|
|
3233
|
+
}).onError(({ code, error, request }) => {
|
|
3234
|
+
const { logger, originalPath } = contextFor(request);
|
|
3235
|
+
if (code === "NOT_FOUND") {
|
|
3236
|
+
return jsonError(404, "not_found", `No route for ${request.method} ${originalPath}.`);
|
|
3237
|
+
}
|
|
3238
|
+
if (error instanceof CopilotAuthError) {
|
|
3239
|
+
logger.warn(
|
|
3240
|
+
{ err: errorDetails(error), event: "copilot.auth.missing" },
|
|
3241
|
+
"copilot auth failed"
|
|
3242
|
+
);
|
|
3243
|
+
return jsonError(401, "copilot_auth_error", error.message);
|
|
3193
3244
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
return finish(await handleModels(client, metrics, request.signal, requestLogger));
|
|
3209
|
-
}
|
|
3210
|
-
if (request.method === "POST" && apiPath === "/v1/messages") {
|
|
3211
|
-
return finish(
|
|
3212
|
-
await handleAnthropicMessages(
|
|
3213
|
-
client,
|
|
3214
|
-
metrics,
|
|
3215
|
-
recordTokens,
|
|
3216
|
-
recordExtraction,
|
|
3217
|
-
request,
|
|
3218
|
-
requestLogger,
|
|
3219
|
-
bufferProxyBodies
|
|
3220
|
-
)
|
|
3221
|
-
);
|
|
3222
|
-
}
|
|
3223
|
-
if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
|
|
3224
|
-
return finish(handleAnthropicCountTokens(await readJson(request)));
|
|
3225
|
-
}
|
|
3226
|
-
if (request.method === "POST" && apiPath === "/v1/chat/completions") {
|
|
3227
|
-
return finish(
|
|
3228
|
-
await handleChatCompletions(
|
|
3229
|
-
client,
|
|
3230
|
-
metrics,
|
|
3231
|
-
recordTokens,
|
|
3232
|
-
recordExtraction,
|
|
3233
|
-
request,
|
|
3234
|
-
requestLogger,
|
|
3235
|
-
bufferProxyBodies
|
|
3236
|
-
)
|
|
3237
|
-
);
|
|
3238
|
-
}
|
|
3239
|
-
if (request.method === "POST" && apiPath === "/v1/completions") {
|
|
3240
|
-
return finish(
|
|
3241
|
-
await handleCompletions(
|
|
3242
|
-
client,
|
|
3243
|
-
metrics,
|
|
3244
|
-
recordTokens,
|
|
3245
|
-
recordExtraction,
|
|
3246
|
-
request,
|
|
3247
|
-
requestLogger,
|
|
3248
|
-
bufferProxyBodies
|
|
3249
|
-
)
|
|
3250
|
-
);
|
|
3251
|
-
}
|
|
3252
|
-
if (request.method === "POST" && apiPath === "/v1/responses/compact") {
|
|
3253
|
-
return finish(
|
|
3254
|
-
await handleResponsesCompact(
|
|
3255
|
-
client,
|
|
3256
|
-
metrics,
|
|
3257
|
-
recordTokens,
|
|
3258
|
-
recordExtraction,
|
|
3259
|
-
request,
|
|
3260
|
-
requestLogger
|
|
3261
|
-
)
|
|
3262
|
-
);
|
|
3263
|
-
}
|
|
3264
|
-
if (request.method === "POST" && apiPath === "/v1/responses") {
|
|
3265
|
-
return finish(
|
|
3266
|
-
await handleResponses(
|
|
3267
|
-
client,
|
|
3268
|
-
metrics,
|
|
3269
|
-
recordTokens,
|
|
3270
|
-
recordExtraction,
|
|
3271
|
-
request,
|
|
3272
|
-
requestLogger,
|
|
3273
|
-
bufferProxyBodies
|
|
3274
|
-
)
|
|
3275
|
-
);
|
|
3276
|
-
}
|
|
3277
|
-
return finish(jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`));
|
|
3278
|
-
} catch (error) {
|
|
3279
|
-
if (error instanceof CopilotAuthError) {
|
|
3280
|
-
requestLogger.warn(
|
|
3281
|
-
{ err: errorDetails(error), event: "copilot.auth.missing" },
|
|
3282
|
-
"copilot auth failed"
|
|
3283
|
-
);
|
|
3284
|
-
return finish(jsonError(401, "copilot_auth_error", error.message));
|
|
3285
|
-
}
|
|
3286
|
-
const message = errorMessage(error);
|
|
3287
|
-
if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
|
|
3288
|
-
requestLogger.warn(
|
|
3289
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3290
|
-
"request body was not usable json"
|
|
3291
|
-
);
|
|
3292
|
-
return finish(jsonError(400, "invalid_request_error", message));
|
|
3293
|
-
} else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
3294
|
-
requestLogger.warn(
|
|
3295
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3296
|
-
"request body used unsupported compatibility fields"
|
|
3297
|
-
);
|
|
3298
|
-
return finish(jsonError(400, "invalid_request_error", message));
|
|
3299
|
-
} else if (error instanceof RequestBodyTooLargeError) {
|
|
3300
|
-
requestLogger.warn(
|
|
3301
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3302
|
-
"request body exceeded size limit"
|
|
3303
|
-
);
|
|
3304
|
-
return finish(jsonError(413, "request_too_large", message));
|
|
3305
|
-
} else {
|
|
3306
|
-
requestLogger.error(
|
|
3307
|
-
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3308
|
-
"request failed"
|
|
3309
|
-
);
|
|
3310
|
-
}
|
|
3311
|
-
return finish(jsonError(500, "internal_error", message));
|
|
3245
|
+
const message = errorMessage(error);
|
|
3246
|
+
if (error instanceof InvalidJsonError || error instanceof JsonNotObjectError) {
|
|
3247
|
+
logger.warn(
|
|
3248
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3249
|
+
"request body was not usable json"
|
|
3250
|
+
);
|
|
3251
|
+
return jsonError(400, "invalid_request_error", message);
|
|
3252
|
+
}
|
|
3253
|
+
if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
3254
|
+
logger.warn(
|
|
3255
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3256
|
+
"request body used unsupported compatibility fields"
|
|
3257
|
+
);
|
|
3258
|
+
return jsonError(400, "invalid_request_error", message);
|
|
3312
3259
|
}
|
|
3260
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
3261
|
+
logger.warn(
|
|
3262
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
3263
|
+
"request body exceeded size limit"
|
|
3264
|
+
);
|
|
3265
|
+
return jsonError(413, "request_too_large", message);
|
|
3266
|
+
}
|
|
3267
|
+
logger.error({ err: errorDetails(error), event: "http.request.failed" }, "request failed");
|
|
3268
|
+
return jsonError(500, "internal_error", message);
|
|
3269
|
+
}).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(
|
|
3270
|
+
"/v1/models",
|
|
3271
|
+
({ request }) => handleModels(client, metrics, request.signal, loggerFor(request))
|
|
3272
|
+
).get("/v1/responses", () => websocketUnsupportedResponse()).post(
|
|
3273
|
+
"/v1/messages",
|
|
3274
|
+
({ request }) => handleAnthropicMessages(
|
|
3275
|
+
client,
|
|
3276
|
+
metrics,
|
|
3277
|
+
recordTokens,
|
|
3278
|
+
recordExtraction,
|
|
3279
|
+
request,
|
|
3280
|
+
loggerFor(request),
|
|
3281
|
+
bufferProxyBodies
|
|
3282
|
+
),
|
|
3283
|
+
noBody
|
|
3284
|
+
).post(
|
|
3285
|
+
"/v1/messages/count_tokens",
|
|
3286
|
+
({ request }) => handleAnthropicCountTokens(request),
|
|
3287
|
+
noBody
|
|
3288
|
+
).post(
|
|
3289
|
+
"/v1/chat/completions",
|
|
3290
|
+
({ request }) => handleChatCompletions(
|
|
3291
|
+
client,
|
|
3292
|
+
metrics,
|
|
3293
|
+
recordTokens,
|
|
3294
|
+
recordExtraction,
|
|
3295
|
+
request,
|
|
3296
|
+
loggerFor(request),
|
|
3297
|
+
bufferProxyBodies
|
|
3298
|
+
),
|
|
3299
|
+
noBody
|
|
3300
|
+
).post(
|
|
3301
|
+
"/v1/completions",
|
|
3302
|
+
({ request }) => handleCompletions(
|
|
3303
|
+
client,
|
|
3304
|
+
metrics,
|
|
3305
|
+
recordTokens,
|
|
3306
|
+
recordExtraction,
|
|
3307
|
+
request,
|
|
3308
|
+
loggerFor(request),
|
|
3309
|
+
bufferProxyBodies
|
|
3310
|
+
),
|
|
3311
|
+
noBody
|
|
3312
|
+
).post(
|
|
3313
|
+
"/v1/responses/compact",
|
|
3314
|
+
({ request }) => handleResponsesCompact(
|
|
3315
|
+
client,
|
|
3316
|
+
metrics,
|
|
3317
|
+
recordTokens,
|
|
3318
|
+
recordExtraction,
|
|
3319
|
+
request,
|
|
3320
|
+
loggerFor(request)
|
|
3321
|
+
),
|
|
3322
|
+
noBody
|
|
3323
|
+
).post(
|
|
3324
|
+
"/v1/responses",
|
|
3325
|
+
({ request }) => handleResponses(
|
|
3326
|
+
client,
|
|
3327
|
+
metrics,
|
|
3328
|
+
recordTokens,
|
|
3329
|
+
recordExtraction,
|
|
3330
|
+
request,
|
|
3331
|
+
loggerFor(request),
|
|
3332
|
+
bufferProxyBodies
|
|
3333
|
+
),
|
|
3334
|
+
noBody
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
function normalizeInnerRequest(request, canonicalPath, url) {
|
|
3338
|
+
if (canonicalPath === url.pathname) {
|
|
3339
|
+
return request;
|
|
3340
|
+
}
|
|
3341
|
+
const target = new URL(url);
|
|
3342
|
+
target.pathname = canonicalPath;
|
|
3343
|
+
const init = {
|
|
3344
|
+
headers: request.headers,
|
|
3345
|
+
method: request.method,
|
|
3346
|
+
signal: request.signal
|
|
3313
3347
|
};
|
|
3348
|
+
if (request.body) {
|
|
3349
|
+
init.body = request.body;
|
|
3350
|
+
init.duplex = "half";
|
|
3351
|
+
}
|
|
3352
|
+
return new Request(target, init);
|
|
3314
3353
|
}
|
|
3315
3354
|
function startHoopilotServer(options = {}) {
|
|
3316
3355
|
const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
|
|
@@ -3389,7 +3428,8 @@ async function handleAnthropicMessages(client, metrics, recordTokens, recordExtr
|
|
|
3389
3428
|
recordExtraction(usage !== void 0);
|
|
3390
3429
|
return jsonResponse(responsesResponseToAnthropicMessage(body, model));
|
|
3391
3430
|
}
|
|
3392
|
-
function handleAnthropicCountTokens(
|
|
3431
|
+
async function handleAnthropicCountTokens(request) {
|
|
3432
|
+
const body = await readJson(request);
|
|
3393
3433
|
return jsonResponse(estimateAnthropicMessageTokens(body));
|
|
3394
3434
|
}
|
|
3395
3435
|
async function handleModels(client, metrics, signal, logger) {
|
|
@@ -3475,14 +3515,14 @@ async function handleCompletions(client, metrics, recordTokens, recordExtraction
|
|
|
3475
3515
|
return jsonResponse(chatCompletionToCompletion(completion));
|
|
3476
3516
|
}
|
|
3477
3517
|
async function handleResponses(client, metrics, recordTokens, recordExtraction, request, logger, bufferProxyBodies) {
|
|
3478
|
-
const body = await readJsonText(request);
|
|
3518
|
+
const { json, text: body } = await readJsonText(request);
|
|
3479
3519
|
const upstream = await client.responses(body, request.signal);
|
|
3480
3520
|
metrics.recordUpstream("/responses", upstream.ok);
|
|
3481
3521
|
if (!upstream.ok) {
|
|
3482
3522
|
return proxyError(upstream, logger);
|
|
3483
3523
|
}
|
|
3484
3524
|
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
3485
|
-
const model = normalizeRequestedModel(
|
|
3525
|
+
const model = normalizeRequestedModel(json.model);
|
|
3486
3526
|
return proxyResponse(
|
|
3487
3527
|
await responseWithObservedUsage(
|
|
3488
3528
|
upstream,
|
|
@@ -3570,17 +3610,16 @@ function parseJsonObject2(text) {
|
|
|
3570
3610
|
try {
|
|
3571
3611
|
parsed = JSON.parse(text);
|
|
3572
3612
|
} catch {
|
|
3573
|
-
throw new
|
|
3613
|
+
throw new InvalidJsonError();
|
|
3574
3614
|
}
|
|
3575
3615
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3576
|
-
throw new
|
|
3616
|
+
throw new JsonNotObjectError();
|
|
3577
3617
|
}
|
|
3578
3618
|
return parsed;
|
|
3579
3619
|
}
|
|
3580
3620
|
async function readJsonText(request) {
|
|
3581
3621
|
const text = await readRequestText(request);
|
|
3582
|
-
parseJsonObject2(text);
|
|
3583
|
-
return text;
|
|
3622
|
+
return { json: parseJsonObject2(text), text };
|
|
3584
3623
|
}
|
|
3585
3624
|
async function readRequestText(request) {
|
|
3586
3625
|
const contentLength = request.headers.get("content-length");
|
|
@@ -3638,7 +3677,7 @@ function jsonError(status, code, message) {
|
|
|
3638
3677
|
);
|
|
3639
3678
|
}
|
|
3640
3679
|
function upstreamErrorResponse(status, text) {
|
|
3641
|
-
const parsedError = asRecord(asRecord(
|
|
3680
|
+
const parsedError = asRecord(asRecord(safeJsonParse(text)).error);
|
|
3642
3681
|
if (Object.keys(parsedError).length > 0) {
|
|
3643
3682
|
return jsonResponse({ error: parsedError }, status);
|
|
3644
3683
|
}
|
|
@@ -3660,13 +3699,18 @@ function corsHeaders() {
|
|
|
3660
3699
|
"access-control-expose-headers": "x-request-id"
|
|
3661
3700
|
};
|
|
3662
3701
|
}
|
|
3702
|
+
function secretEquals(candidate, secret) {
|
|
3703
|
+
const a = createHash("sha256").update(candidate).digest();
|
|
3704
|
+
const b = createHash("sha256").update(secret).digest();
|
|
3705
|
+
return timingSafeEqual(a, b);
|
|
3706
|
+
}
|
|
3663
3707
|
function isAuthorized(request, apiKey) {
|
|
3664
3708
|
if (!apiKey) {
|
|
3665
3709
|
return true;
|
|
3666
3710
|
}
|
|
3667
3711
|
const authorization = request.headers.get("authorization") ?? "";
|
|
3668
3712
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
3669
|
-
return bearer
|
|
3713
|
+
return bearer !== void 0 && secretEquals(bearer, apiKey) || secretEquals(request.headers.get("x-api-key") ?? "", apiKey);
|
|
3670
3714
|
}
|
|
3671
3715
|
function forbiddenBrowserOrigin(origin, request, allowedOrigins) {
|
|
3672
3716
|
if (origin) {
|
|
@@ -3703,7 +3747,7 @@ function upstreamAuthMessage(message) {
|
|
|
3703
3747
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
3704
3748
|
}
|
|
3705
3749
|
function isLoopbackHost(host) {
|
|
3706
|
-
return host
|
|
3750
|
+
return isLoopbackHostname(host);
|
|
3707
3751
|
}
|
|
3708
3752
|
function urlHost(host) {
|
|
3709
3753
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
@@ -3722,9 +3766,6 @@ function normalizeServerPort(value) {
|
|
|
3722
3766
|
}
|
|
3723
3767
|
return port;
|
|
3724
3768
|
}
|
|
3725
|
-
function errorMessage(error) {
|
|
3726
|
-
return error instanceof Error ? error.message : String(error);
|
|
3727
|
-
}
|
|
3728
3769
|
function serverLogger(options) {
|
|
3729
3770
|
if (options.logger) {
|
|
3730
3771
|
return options.logger.child({ component: "server" });
|
|
@@ -3740,10 +3781,7 @@ function serverLogger(options) {
|
|
|
3740
3781
|
}
|
|
3741
3782
|
function resolveStreamingProxyMode(options) {
|
|
3742
3783
|
const value = options.streamingProxyMode ?? envValue(options.env?.HOOPILOT_STREAM_MODE) ?? envValue(options.env?.HOOPILOT_STREAMING_PROXY_MODE) ?? "auto";
|
|
3743
|
-
|
|
3744
|
-
return value;
|
|
3745
|
-
}
|
|
3746
|
-
throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
|
|
3784
|
+
return parseStreamingProxyMode(value);
|
|
3747
3785
|
}
|
|
3748
3786
|
function shouldBufferProxyBodies(mode) {
|
|
3749
3787
|
if (mode === "buffer") {
|
|
@@ -3801,11 +3839,13 @@ function responseWithRequestId(response, requestId, closeConnection, corsOrigin)
|
|
|
3801
3839
|
function trackStreamCompletion(body, onComplete) {
|
|
3802
3840
|
const reader = body.getReader();
|
|
3803
3841
|
let fired = false;
|
|
3804
|
-
const
|
|
3805
|
-
if (
|
|
3806
|
-
|
|
3807
|
-
onComplete();
|
|
3842
|
+
const release = () => {
|
|
3843
|
+
if (fired) {
|
|
3844
|
+
return;
|
|
3808
3845
|
}
|
|
3846
|
+
fired = true;
|
|
3847
|
+
onComplete();
|
|
3848
|
+
reader.releaseLock();
|
|
3809
3849
|
};
|
|
3810
3850
|
return new ReadableStream({
|
|
3811
3851
|
async pull(controller) {
|
|
@@ -3813,18 +3853,25 @@ function trackStreamCompletion(body, onComplete) {
|
|
|
3813
3853
|
const { done, value } = await reader.read();
|
|
3814
3854
|
if (done) {
|
|
3815
3855
|
controller.close();
|
|
3816
|
-
|
|
3856
|
+
release();
|
|
3817
3857
|
return;
|
|
3818
3858
|
}
|
|
3819
3859
|
controller.enqueue(value);
|
|
3820
3860
|
} catch (error) {
|
|
3821
|
-
|
|
3861
|
+
release();
|
|
3822
3862
|
controller.error(error);
|
|
3823
3863
|
}
|
|
3824
3864
|
},
|
|
3825
|
-
cancel(reason) {
|
|
3826
|
-
|
|
3827
|
-
|
|
3865
|
+
async cancel(reason) {
|
|
3866
|
+
if (!fired) {
|
|
3867
|
+
fired = true;
|
|
3868
|
+
onComplete();
|
|
3869
|
+
}
|
|
3870
|
+
try {
|
|
3871
|
+
await reader.cancel(reason);
|
|
3872
|
+
} finally {
|
|
3873
|
+
reader.releaseLock();
|
|
3874
|
+
}
|
|
3828
3875
|
}
|
|
3829
3876
|
});
|
|
3830
3877
|
}
|
|
@@ -3872,47 +3919,26 @@ function canonicalApiPath(path) {
|
|
|
3872
3919
|
return withoutTrailingSlash;
|
|
3873
3920
|
}
|
|
3874
3921
|
}
|
|
3922
|
+
var API_ROUTES = [
|
|
3923
|
+
{ method: "GET", path: "/", name: "health" },
|
|
3924
|
+
{ method: "GET", path: "/healthz", name: "health" },
|
|
3925
|
+
{ method: "GET", path: "/dashboard", name: "dashboard" },
|
|
3926
|
+
{ method: "GET", path: "/metrics", name: "metrics" },
|
|
3927
|
+
{ method: "GET", path: "/v1/usage", name: "usage" },
|
|
3928
|
+
{ method: "GET", path: "/v1/models", name: "models" },
|
|
3929
|
+
{ method: "GET", path: "/v1/responses", name: "responses_websocket" },
|
|
3930
|
+
{ method: "POST", path: "/v1/messages", name: "anthropic_messages" },
|
|
3931
|
+
{ method: "POST", path: "/v1/messages/count_tokens", name: "anthropic_count_tokens" },
|
|
3932
|
+
{ method: "POST", path: "/v1/chat/completions", name: "chat_completions" },
|
|
3933
|
+
{ method: "POST", path: "/v1/completions", name: "completions" },
|
|
3934
|
+
{ method: "POST", path: "/v1/responses/compact", name: "responses_compact" },
|
|
3935
|
+
{ method: "POST", path: "/v1/responses", name: "responses" }
|
|
3936
|
+
];
|
|
3875
3937
|
function routeFor(method, path) {
|
|
3876
3938
|
if (method === "OPTIONS") {
|
|
3877
3939
|
return "cors.preflight";
|
|
3878
3940
|
}
|
|
3879
|
-
|
|
3880
|
-
return "health";
|
|
3881
|
-
}
|
|
3882
|
-
if (method === "GET" && path === "/dashboard") {
|
|
3883
|
-
return "dashboard";
|
|
3884
|
-
}
|
|
3885
|
-
if (method === "GET" && path === "/metrics") {
|
|
3886
|
-
return "metrics";
|
|
3887
|
-
}
|
|
3888
|
-
if (method === "GET" && path === "/v1/usage") {
|
|
3889
|
-
return "usage";
|
|
3890
|
-
}
|
|
3891
|
-
if (method === "GET" && path === "/v1/models") {
|
|
3892
|
-
return "models";
|
|
3893
|
-
}
|
|
3894
|
-
if (method === "POST" && path === "/v1/messages") {
|
|
3895
|
-
return "anthropic_messages";
|
|
3896
|
-
}
|
|
3897
|
-
if (method === "POST" && path === "/v1/messages/count_tokens") {
|
|
3898
|
-
return "anthropic_count_tokens";
|
|
3899
|
-
}
|
|
3900
|
-
if (method === "POST" && path === "/v1/chat/completions") {
|
|
3901
|
-
return "chat_completions";
|
|
3902
|
-
}
|
|
3903
|
-
if (method === "POST" && path === "/v1/completions") {
|
|
3904
|
-
return "completions";
|
|
3905
|
-
}
|
|
3906
|
-
if (method === "POST" && path === "/v1/responses/compact") {
|
|
3907
|
-
return "responses_compact";
|
|
3908
|
-
}
|
|
3909
|
-
if (method === "POST" && path === "/v1/responses") {
|
|
3910
|
-
return "responses";
|
|
3911
|
-
}
|
|
3912
|
-
if (method === "GET" && path === "/v1/responses") {
|
|
3913
|
-
return "responses_websocket";
|
|
3914
|
-
}
|
|
3915
|
-
return "not_found";
|
|
3941
|
+
return API_ROUTES.find((entry) => entry.method === method && entry.path === path)?.name ?? "not_found";
|
|
3916
3942
|
}
|
|
3917
3943
|
function isStreamingResponse(response) {
|
|
3918
3944
|
return response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
@@ -3990,17 +4016,10 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
3990
4016
|
}
|
|
3991
4017
|
};
|
|
3992
4018
|
}
|
|
3993
|
-
function safeParseJson(text) {
|
|
3994
|
-
try {
|
|
3995
|
-
return JSON.parse(text);
|
|
3996
|
-
} catch {
|
|
3997
|
-
return void 0;
|
|
3998
|
-
}
|
|
3999
|
-
}
|
|
4000
4019
|
|
|
4001
4020
|
// src/update.ts
|
|
4002
4021
|
import { execFileSync } from "child_process";
|
|
4003
|
-
import { createHash } from "crypto";
|
|
4022
|
+
import { createHash as createHash2 } from "crypto";
|
|
4004
4023
|
import {
|
|
4005
4024
|
chmodSync as chmodSync2,
|
|
4006
4025
|
copyFileSync,
|
|
@@ -4008,7 +4027,7 @@ import {
|
|
|
4008
4027
|
mkdirSync as mkdirSync2,
|
|
4009
4028
|
realpathSync,
|
|
4010
4029
|
renameSync as renameSync2,
|
|
4011
|
-
rmSync,
|
|
4030
|
+
rmSync as rmSync2,
|
|
4012
4031
|
writeFileSync as writeFileSync2
|
|
4013
4032
|
} from "fs";
|
|
4014
4033
|
import { readFile, writeFile } from "fs/promises";
|
|
@@ -4182,10 +4201,11 @@ Run: ${upgradeCommandFor(kind)}
|
|
|
4182
4201
|
function parseState(text) {
|
|
4183
4202
|
try {
|
|
4184
4203
|
const data = JSON.parse(text);
|
|
4204
|
+
const record = data && typeof data === "object" ? data : {};
|
|
4185
4205
|
return {
|
|
4186
|
-
lastCheck: typeof
|
|
4187
|
-
latestVersion: typeof
|
|
4188
|
-
etag: typeof
|
|
4206
|
+
lastCheck: typeof record.lastCheck === "number" ? record.lastCheck : 0,
|
|
4207
|
+
latestVersion: typeof record.latestVersion === "string" ? record.latestVersion : null,
|
|
4208
|
+
etag: typeof record.etag === "string" ? record.etag : null
|
|
4189
4209
|
};
|
|
4190
4210
|
} catch {
|
|
4191
4211
|
return { lastCheck: 0, latestVersion: null, etag: null };
|
|
@@ -4239,6 +4259,7 @@ function latestReleaseApiUrl() {
|
|
|
4239
4259
|
|
|
4240
4260
|
// src/update.ts
|
|
4241
4261
|
var REQUEST_TIMEOUT_MS2 = 8e3;
|
|
4262
|
+
var DOWNLOAD_TIMEOUT_MS = REQUEST_TIMEOUT_MS2 * 10;
|
|
4242
4263
|
var SHA256SUMS = "SHA256SUMS";
|
|
4243
4264
|
function userAgent(version) {
|
|
4244
4265
|
return `hoopilot/${version}`;
|
|
@@ -4376,15 +4397,15 @@ async function downloadToFile(url, dest, version) {
|
|
|
4376
4397
|
const response = await fetch(url, {
|
|
4377
4398
|
headers: { "User-Agent": userAgent(version) },
|
|
4378
4399
|
redirect: "follow",
|
|
4379
|
-
signal: AbortSignal.timeout(
|
|
4400
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
|
|
4380
4401
|
});
|
|
4381
4402
|
if (!response.ok || !response.body) {
|
|
4382
4403
|
throw new Error(`Download failed (${response.status}) for ${url}`);
|
|
4383
4404
|
}
|
|
4384
|
-
await
|
|
4405
|
+
await Bun.write(dest, response);
|
|
4385
4406
|
}
|
|
4386
4407
|
async function sha256File(path) {
|
|
4387
|
-
return
|
|
4408
|
+
return createHash2("sha256").update(await readFile(path)).digest("hex");
|
|
4388
4409
|
}
|
|
4389
4410
|
async function verifyChecksum(release, assetName, file, version) {
|
|
4390
4411
|
const sums = release.assets.find((asset) => asset.name === SHA256SUMS);
|
|
@@ -4396,7 +4417,7 @@ async function verifyChecksum(release, assetName, file, version) {
|
|
|
4396
4417
|
const response = await fetch(sums.url, {
|
|
4397
4418
|
headers: { "User-Agent": userAgent(version) },
|
|
4398
4419
|
redirect: "follow",
|
|
4399
|
-
signal: AbortSignal.timeout(
|
|
4420
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
|
|
4400
4421
|
});
|
|
4401
4422
|
if (!response.ok) {
|
|
4402
4423
|
throw new Error(`Could not download ${SHA256SUMS} (${response.status}).`);
|
|
@@ -4414,7 +4435,7 @@ function swapBinary(tmpFile, exePath) {
|
|
|
4414
4435
|
if (process.platform === "win32") {
|
|
4415
4436
|
const oldExe = `${exePath}.old`;
|
|
4416
4437
|
try {
|
|
4417
|
-
|
|
4438
|
+
rmSync2(oldExe, { force: true });
|
|
4418
4439
|
} catch {
|
|
4419
4440
|
}
|
|
4420
4441
|
renameSync2(exePath, oldExe);
|
|
@@ -4471,7 +4492,7 @@ function refreshCodexxShim(dir, logger) {
|
|
|
4471
4492
|
{ err: errorDetails(error), event: "update.codexx_shim_failed" },
|
|
4472
4493
|
"could not refresh codexx shim"
|
|
4473
4494
|
);
|
|
4474
|
-
console.warn(`Updated hoopilot, but could not refresh the codexx shim: ${
|
|
4495
|
+
console.warn(`Updated hoopilot, but could not refresh the codexx shim: ${errorMessage(error)}`);
|
|
4475
4496
|
}
|
|
4476
4497
|
}
|
|
4477
4498
|
function cleanupOldBinary() {
|
|
@@ -4479,7 +4500,7 @@ function cleanupOldBinary() {
|
|
|
4479
4500
|
return;
|
|
4480
4501
|
}
|
|
4481
4502
|
try {
|
|
4482
|
-
|
|
4503
|
+
rmSync2(`${realpathSync(process.execPath)}.old`, { force: true });
|
|
4483
4504
|
} catch {
|
|
4484
4505
|
}
|
|
4485
4506
|
}
|
|
@@ -4543,7 +4564,7 @@ async function runUpdate(currentVersion, logger) {
|
|
|
4543
4564
|
throw error;
|
|
4544
4565
|
} finally {
|
|
4545
4566
|
try {
|
|
4546
|
-
|
|
4567
|
+
rmSync2(tmpFile, { force: true });
|
|
4547
4568
|
} catch {
|
|
4548
4569
|
}
|
|
4549
4570
|
}
|
|
@@ -4556,12 +4577,8 @@ async function runUpdate(currentVersion, logger) {
|
|
|
4556
4577
|
console.log("Restart hoopilot to run the new version.");
|
|
4557
4578
|
}
|
|
4558
4579
|
}
|
|
4559
|
-
function errorMessage2(error) {
|
|
4560
|
-
return error instanceof Error ? error.message : String(error);
|
|
4561
|
-
}
|
|
4562
4580
|
|
|
4563
4581
|
// src/cli.ts
|
|
4564
|
-
var ALLOWED_COPILOT_API_HOSTS2 = ["api.githubcopilot.com"];
|
|
4565
4582
|
async function main2(argv = Bun.argv.slice(2)) {
|
|
4566
4583
|
cleanupOldBinary();
|
|
4567
4584
|
const command = argv[0];
|
|
@@ -4694,7 +4711,7 @@ function parseArgs(argv) {
|
|
|
4694
4711
|
args.logLevel = parseLogLevel(optionValue(name, inlineValue, rest));
|
|
4695
4712
|
break;
|
|
4696
4713
|
case "--stream-mode":
|
|
4697
|
-
args.streamingProxyMode =
|
|
4714
|
+
args.streamingProxyMode = parseStreamingProxyMode(optionValue(name, inlineValue, rest));
|
|
4698
4715
|
break;
|
|
4699
4716
|
case "--host":
|
|
4700
4717
|
args.host = optionValue(name, inlineValue, rest);
|
|
@@ -4714,12 +4731,6 @@ function parseArgs(argv) {
|
|
|
4714
4731
|
}
|
|
4715
4732
|
return args;
|
|
4716
4733
|
}
|
|
4717
|
-
function parseStreamMode(value) {
|
|
4718
|
-
if (value === "auto" || value === "buffer" || value === "live") {
|
|
4719
|
-
return value;
|
|
4720
|
-
}
|
|
4721
|
-
throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
|
|
4722
|
-
}
|
|
4723
4734
|
function optionValue(name, inlineValue, rest) {
|
|
4724
4735
|
const value = inlineValue ?? rest.shift();
|
|
4725
4736
|
if (!value) {
|
|
@@ -4780,11 +4791,7 @@ async function runModels(options = {}) {
|
|
|
4780
4791
|
logger.debug({ event: "models.list.started" }, "fetching github copilot models");
|
|
4781
4792
|
const response = await new CopilotClient(options).models();
|
|
4782
4793
|
if (!response.ok) {
|
|
4783
|
-
|
|
4784
|
-
if (response.status === 401 || response.status === 403) {
|
|
4785
|
-
throw new CopilotAuthError(message);
|
|
4786
|
-
}
|
|
4787
|
-
throw new Error(message);
|
|
4794
|
+
await throwForCopilotResponse(response, "GitHub Copilot API model list");
|
|
4788
4795
|
}
|
|
4789
4796
|
const ids = modelIdsFromResponse(await response.json().catch(() => void 0));
|
|
4790
4797
|
if (ids.length === 0) {
|
|
@@ -4804,11 +4811,7 @@ async function runUsage(options = {}) {
|
|
|
4804
4811
|
logger.debug({ event: "usage.fetch.started" }, "fetching github copilot quota");
|
|
4805
4812
|
const response = await new CopilotClient(options).usage();
|
|
4806
4813
|
if (!response.ok) {
|
|
4807
|
-
|
|
4808
|
-
if (response.status === 401 || response.status === 403) {
|
|
4809
|
-
throw new CopilotAuthError(message);
|
|
4810
|
-
}
|
|
4811
|
-
throw new Error(message);
|
|
4814
|
+
await throwForCopilotResponse(response, "GitHub Copilot usage request");
|
|
4812
4815
|
}
|
|
4813
4816
|
const rateLimit = parseRateLimitHeaders(response.headers);
|
|
4814
4817
|
const usage = normalizeCopilotUsage(await response.json().catch(() => ({})));
|
|
@@ -4908,7 +4911,7 @@ async function verifyCopilotOAuthToken(token, options = {}) {
|
|
|
4908
4911
|
options.copilotApiBaseUrl ?? envValue(options.env?.COPILOT_API_BASE_URL) ?? DEFAULT_COPILOT_API_BASE_URL
|
|
4909
4912
|
);
|
|
4910
4913
|
const allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
4911
|
-
if (!isTrustedTokenBaseUrl(apiBaseUrl,
|
|
4914
|
+
if (!isTrustedTokenBaseUrl(apiBaseUrl, ALLOWED_COPILOT_API_HOSTS, allowUnsafeUpstream)) {
|
|
4912
4915
|
throw new Error(
|
|
4913
4916
|
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${apiBaseUrl}`
|
|
4914
4917
|
);
|
|
@@ -4919,19 +4922,22 @@ async function verifyCopilotOAuthToken(token, options = {}) {
|
|
|
4919
4922
|
method: "GET"
|
|
4920
4923
|
});
|
|
4921
4924
|
if (!response.ok) {
|
|
4922
|
-
|
|
4923
|
-
if (response.status === 401 || response.status === 403) {
|
|
4924
|
-
throw new CopilotAuthError(message);
|
|
4925
|
-
}
|
|
4926
|
-
throw new Error(message);
|
|
4925
|
+
await throwForCopilotResponse(response, "GitHub Copilot API verification");
|
|
4927
4926
|
}
|
|
4928
4927
|
return {
|
|
4929
4928
|
apiBaseUrl,
|
|
4930
|
-
expiresAtMs: Date.now() +
|
|
4929
|
+
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
4931
4930
|
source: "github-copilot-oauth",
|
|
4932
4931
|
token
|
|
4933
4932
|
};
|
|
4934
4933
|
}
|
|
4934
|
+
async function throwForCopilotResponse(response, label) {
|
|
4935
|
+
const message = `${label} failed with ${response.status}: ${await truncatedResponseText(response)}`;
|
|
4936
|
+
if (response.status === 401 || response.status === 403) {
|
|
4937
|
+
throw new CopilotAuthError(message);
|
|
4938
|
+
}
|
|
4939
|
+
throw new Error(message);
|
|
4940
|
+
}
|
|
4935
4941
|
function openBrowserBestEffort(url, spawnOpener = spawn) {
|
|
4936
4942
|
const platform = process.platform;
|
|
4937
4943
|
const command = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -4947,21 +4953,6 @@ function openBrowserBestEffort(url, spawnOpener = spawn) {
|
|
|
4947
4953
|
} catch {
|
|
4948
4954
|
}
|
|
4949
4955
|
}
|
|
4950
|
-
function modelIdsFromResponse(body) {
|
|
4951
|
-
const record = asRecord(body);
|
|
4952
|
-
const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];
|
|
4953
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4954
|
-
const ids = [];
|
|
4955
|
-
for (const model of data) {
|
|
4956
|
-
const id = asRecord(model).id;
|
|
4957
|
-
if (typeof id !== "string" || id.length === 0 || seen.has(id)) {
|
|
4958
|
-
continue;
|
|
4959
|
-
}
|
|
4960
|
-
seen.add(id);
|
|
4961
|
-
ids.push(id);
|
|
4962
|
-
}
|
|
4963
|
-
return ids;
|
|
4964
|
-
}
|
|
4965
4956
|
function withRuntimeEnv(args) {
|
|
4966
4957
|
return { ...args, env: process.env };
|
|
4967
4958
|
}
|
|
@@ -5045,7 +5036,7 @@ Environment:
|
|
|
5045
5036
|
}
|
|
5046
5037
|
if (import.meta.main) {
|
|
5047
5038
|
main2().catch((error) => {
|
|
5048
|
-
console.error(
|
|
5039
|
+
console.error(errorMessage(error));
|
|
5049
5040
|
process.exit(1);
|
|
5050
5041
|
});
|
|
5051
5042
|
}
|