@openhoo/hoopilot 0.7.2 → 0.7.4
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 +3 -0
- package/dist/{chunk-TEDEVCKM.js → chunk-7GSQVYYT.js} +61 -7
- package/dist/chunk-7GSQVYYT.js.map +1 -0
- package/dist/cli.js +273 -114
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.cjs +352 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +352 -117
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-TEDEVCKM.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
asRecord,
|
|
4
|
+
envValue,
|
|
5
|
+
isTrustedTokenBaseUrl,
|
|
6
|
+
main,
|
|
7
|
+
trimTrailingSlash,
|
|
8
|
+
truncatedResponseText
|
|
9
|
+
} from "./chunk-7GSQVYYT.js";
|
|
5
10
|
|
|
6
11
|
// src/cli.ts
|
|
7
12
|
import { spawn } from "child_process";
|
|
@@ -17,10 +22,25 @@ var StoredCopilotAuthError = class extends Error {
|
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
24
|
function authStorePath(env = process.env) {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const explicit = envValue(env.HOOPILOT_AUTH_FILE);
|
|
26
|
+
if (explicit) {
|
|
27
|
+
return explicit;
|
|
22
28
|
}
|
|
23
|
-
const
|
|
29
|
+
const xdg = envValue(env.XDG_CONFIG_HOME);
|
|
30
|
+
if (xdg) {
|
|
31
|
+
return join(xdg, "hoopilot", "auth.json");
|
|
32
|
+
}
|
|
33
|
+
const appdata = envValue(env.APPDATA);
|
|
34
|
+
if (appdata) {
|
|
35
|
+
return join(appdata, "hoopilot", "auth.json");
|
|
36
|
+
}
|
|
37
|
+
const home = envValue(env.HOME);
|
|
38
|
+
if (!home) {
|
|
39
|
+
throw new StoredCopilotAuthError(
|
|
40
|
+
"Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const base = join(home, ".config");
|
|
24
44
|
return join(base, "hoopilot", "auth.json");
|
|
25
45
|
}
|
|
26
46
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
@@ -77,30 +97,6 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
// src/util.ts
|
|
81
|
-
function trimTrailingSlash(value) {
|
|
82
|
-
return value.replace(/\/+$/, "");
|
|
83
|
-
}
|
|
84
|
-
function isHttpsOrLoopbackUrl(rawUrl) {
|
|
85
|
-
let url;
|
|
86
|
-
try {
|
|
87
|
-
url = new URL(rawUrl);
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if (url.protocol === "https:") {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
|
|
95
|
-
}
|
|
96
|
-
async function truncatedResponseText(response, max = 500) {
|
|
97
|
-
const text = await response.text();
|
|
98
|
-
return text.slice(0, max);
|
|
99
|
-
}
|
|
100
|
-
function asRecord(value) {
|
|
101
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
100
|
// src/auth.ts
|
|
105
101
|
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
106
102
|
var REFRESH_SKEW_MS = 6e4;
|
|
@@ -114,11 +110,15 @@ var CopilotAuthError = class extends Error {
|
|
|
114
110
|
var CopilotAuth = class {
|
|
115
111
|
#authStorePath;
|
|
116
112
|
#copilotApiBaseUrl;
|
|
113
|
+
#hasCopilotApiBaseUrlOverride;
|
|
117
114
|
#cachedAccess;
|
|
118
115
|
constructor(options = {}) {
|
|
119
|
-
|
|
116
|
+
const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
|
|
117
|
+
const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
|
|
118
|
+
this.#authStorePath = options.authStorePath ?? envAuthStorePath;
|
|
119
|
+
this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
|
|
120
120
|
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
121
|
-
options.copilotApiBaseUrl ??
|
|
121
|
+
options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
async getAccess() {
|
|
@@ -136,7 +136,9 @@ var CopilotAuth = class {
|
|
|
136
136
|
}
|
|
137
137
|
if (stored) {
|
|
138
138
|
return this.#cacheAccess({
|
|
139
|
-
apiBaseUrl: trimTrailingSlash(
|
|
139
|
+
apiBaseUrl: trimTrailingSlash(
|
|
140
|
+
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
141
|
+
),
|
|
140
142
|
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
141
143
|
source: "github-copilot-oauth",
|
|
142
144
|
token: stored.token
|
|
@@ -154,6 +156,8 @@ var CopilotAuth = class {
|
|
|
154
156
|
|
|
155
157
|
// src/copilot.ts
|
|
156
158
|
var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
159
|
+
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
160
|
+
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
157
161
|
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
158
162
|
function applyCopilotHeaders(headers, token) {
|
|
159
163
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
@@ -177,13 +181,15 @@ function applyGithubApiHeaders(headers, token) {
|
|
|
177
181
|
}
|
|
178
182
|
var CopilotClient = class {
|
|
179
183
|
#auth;
|
|
184
|
+
#allowUnsafeUpstream;
|
|
180
185
|
#fetch;
|
|
181
186
|
#githubApiBaseUrl;
|
|
182
187
|
constructor(options = {}) {
|
|
183
188
|
this.#auth = new CopilotAuth(options);
|
|
189
|
+
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
184
190
|
this.#fetch = options.fetch ?? fetch;
|
|
185
191
|
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
186
|
-
options.githubApiBaseUrl ?? options.env?.HOOPILOT_GITHUB_API_BASE_URL ?? DEFAULT_GITHUB_API_BASE_URL
|
|
192
|
+
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
187
193
|
);
|
|
188
194
|
}
|
|
189
195
|
/**
|
|
@@ -192,9 +198,13 @@ var CopilotClient = class {
|
|
|
192
198
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
193
199
|
*/
|
|
194
200
|
async usage(signal) {
|
|
195
|
-
if (!
|
|
201
|
+
if (!isTrustedTokenBaseUrl(
|
|
202
|
+
this.#githubApiBaseUrl,
|
|
203
|
+
ALLOWED_GITHUB_API_HOSTS,
|
|
204
|
+
this.#allowUnsafeUpstream
|
|
205
|
+
)) {
|
|
196
206
|
throw new Error(
|
|
197
|
-
`Refusing to send the GitHub OAuth token to
|
|
207
|
+
`Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
|
|
198
208
|
);
|
|
199
209
|
}
|
|
200
210
|
const access = await this.#auth.getAccess();
|
|
@@ -236,9 +246,13 @@ var CopilotClient = class {
|
|
|
236
246
|
}
|
|
237
247
|
async fetchCopilot(path, init) {
|
|
238
248
|
const access = await this.#auth.getAccess();
|
|
239
|
-
if (!
|
|
249
|
+
if (!isTrustedTokenBaseUrl(
|
|
250
|
+
access.apiBaseUrl,
|
|
251
|
+
ALLOWED_COPILOT_API_HOSTS,
|
|
252
|
+
this.#allowUnsafeUpstream
|
|
253
|
+
)) {
|
|
240
254
|
throw new Error(
|
|
241
|
-
`Refusing to send the GitHub OAuth token to
|
|
255
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
|
|
242
256
|
);
|
|
243
257
|
}
|
|
244
258
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
@@ -324,9 +338,9 @@ async function githubCopilotDeviceLogin(options = {}) {
|
|
|
324
338
|
const fetcher = options.fetch ?? fetch;
|
|
325
339
|
const sleeper = options.sleep ?? sleep;
|
|
326
340
|
const domain = normalizeDomain(
|
|
327
|
-
options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
|
|
341
|
+
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
328
342
|
);
|
|
329
|
-
const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
343
|
+
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
330
344
|
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
331
345
|
const verificationUrl = device.verification_uri;
|
|
332
346
|
const userCode = device.user_code;
|
|
@@ -484,8 +498,8 @@ var noopLogger = {
|
|
|
484
498
|
};
|
|
485
499
|
function createHoopilotLogger(options = {}) {
|
|
486
500
|
const env = options.env ?? process.env;
|
|
487
|
-
const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
|
|
488
|
-
const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
|
|
501
|
+
const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
|
|
502
|
+
const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
|
|
489
503
|
const pinoOptions = {
|
|
490
504
|
base: {
|
|
491
505
|
service: "hoopilot",
|
|
@@ -535,7 +549,7 @@ function parseLogLevel(value) {
|
|
|
535
549
|
}
|
|
536
550
|
function shouldCreateLogger(options) {
|
|
537
551
|
return Boolean(
|
|
538
|
-
options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
|
|
552
|
+
options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
|
|
539
553
|
);
|
|
540
554
|
}
|
|
541
555
|
function errorDetails(error) {
|
|
@@ -557,6 +571,12 @@ function isLogLevel(value) {
|
|
|
557
571
|
|
|
558
572
|
// src/openai.ts
|
|
559
573
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
574
|
+
var OpenAICompatibilityError = class extends Error {
|
|
575
|
+
constructor(message) {
|
|
576
|
+
super(message);
|
|
577
|
+
this.name = "OpenAICompatibilityError";
|
|
578
|
+
}
|
|
579
|
+
};
|
|
560
580
|
function normalizeChatCompletionRequest(request) {
|
|
561
581
|
return removeUndefined({
|
|
562
582
|
...request,
|
|
@@ -564,13 +584,22 @@ function normalizeChatCompletionRequest(request) {
|
|
|
564
584
|
});
|
|
565
585
|
}
|
|
566
586
|
function completionsRequestToChatCompletion(request) {
|
|
587
|
+
assertSupportedLegacyCompletionRequest(request);
|
|
567
588
|
return removeUndefined({
|
|
589
|
+
frequency_penalty: request.frequency_penalty,
|
|
590
|
+
logit_bias: request.logit_bias,
|
|
568
591
|
max_tokens: request.max_tokens,
|
|
569
|
-
messages: [{ content:
|
|
592
|
+
messages: [{ content: legacyPromptToText(request.prompt), role: "user" }],
|
|
570
593
|
model: normalizeRequestedModel(request.model),
|
|
594
|
+
n: request.n,
|
|
595
|
+
presence_penalty: request.presence_penalty,
|
|
596
|
+
seed: request.seed,
|
|
597
|
+
stop: request.stop,
|
|
571
598
|
stream: request.stream === true,
|
|
599
|
+
stream_options: request.stream_options,
|
|
572
600
|
temperature: request.temperature,
|
|
573
|
-
top_p: request.top_p
|
|
601
|
+
top_p: request.top_p,
|
|
602
|
+
user: request.user
|
|
574
603
|
});
|
|
575
604
|
}
|
|
576
605
|
function normalizeRequestedModel(model) {
|
|
@@ -578,21 +607,21 @@ function normalizeRequestedModel(model) {
|
|
|
578
607
|
return requested || DEFAULT_MODEL;
|
|
579
608
|
}
|
|
580
609
|
function chatCompletionToCompletion(completion) {
|
|
581
|
-
const choice = firstChoice(completion);
|
|
582
|
-
const message = asRecord(choice.message);
|
|
583
610
|
return removeUndefined({
|
|
584
|
-
choices:
|
|
585
|
-
|
|
611
|
+
choices: completionChoices(completion).map((choice, index) => {
|
|
612
|
+
const message = asRecord(choice.message);
|
|
613
|
+
return {
|
|
586
614
|
finish_reason: choice.finish_reason ?? "stop",
|
|
587
|
-
index:
|
|
588
|
-
logprobs: null,
|
|
589
|
-
text: contentToText(message.content)
|
|
590
|
-
}
|
|
591
|
-
|
|
615
|
+
index: typeof choice.index === "number" ? choice.index : index,
|
|
616
|
+
logprobs: choice.logprobs ?? null,
|
|
617
|
+
text: contentToText(choice.text) || contentToText(message.content)
|
|
618
|
+
};
|
|
619
|
+
}),
|
|
592
620
|
created: completion.created ?? epochSeconds(),
|
|
593
621
|
id: completion.id ?? `cmpl_${randomId()}`,
|
|
594
622
|
model: completion.model ?? DEFAULT_MODEL,
|
|
595
623
|
object: "text_completion",
|
|
624
|
+
system_fingerprint: completion.system_fingerprint,
|
|
596
625
|
usage: completion.usage
|
|
597
626
|
});
|
|
598
627
|
}
|
|
@@ -600,12 +629,15 @@ function completionStreamFromChatStream(chatStream) {
|
|
|
600
629
|
const encoder = new TextEncoder();
|
|
601
630
|
const decoder = new TextDecoder();
|
|
602
631
|
let buffer = "";
|
|
603
|
-
let
|
|
632
|
+
let sawTerminalEvent = false;
|
|
604
633
|
return new ReadableStream({
|
|
605
634
|
async start(controller) {
|
|
606
635
|
const enqueue = (data) => {
|
|
607
636
|
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
608
637
|
};
|
|
638
|
+
const markTerminal = () => {
|
|
639
|
+
sawTerminalEvent = true;
|
|
640
|
+
};
|
|
609
641
|
const reader = chatStream.getReader();
|
|
610
642
|
try {
|
|
611
643
|
while (true) {
|
|
@@ -614,20 +646,17 @@ function completionStreamFromChatStream(chatStream) {
|
|
|
614
646
|
break;
|
|
615
647
|
}
|
|
616
648
|
buffer += decoder.decode(result.value, { stream: true });
|
|
617
|
-
const
|
|
618
|
-
buffer =
|
|
619
|
-
for (const
|
|
620
|
-
|
|
621
|
-
sawDone = true;
|
|
622
|
-
});
|
|
649
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
650
|
+
buffer = blocks.pop() ?? "";
|
|
651
|
+
for (const block of blocks) {
|
|
652
|
+
processCompletionSseBlock(block, enqueue, markTerminal);
|
|
623
653
|
}
|
|
624
654
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
});
|
|
655
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
656
|
+
if (tail.trim()) {
|
|
657
|
+
processCompletionSseBlock(tail, enqueue, markTerminal);
|
|
629
658
|
}
|
|
630
|
-
if (!
|
|
659
|
+
if (!sawTerminalEvent) {
|
|
631
660
|
enqueue("[DONE]");
|
|
632
661
|
}
|
|
633
662
|
controller.close();
|
|
@@ -665,11 +694,38 @@ function fallbackModels() {
|
|
|
665
694
|
}
|
|
666
695
|
];
|
|
667
696
|
}
|
|
668
|
-
function
|
|
669
|
-
if (
|
|
670
|
-
return prompt
|
|
697
|
+
function legacyPromptToText(prompt) {
|
|
698
|
+
if (typeof prompt === "string") {
|
|
699
|
+
return prompt;
|
|
700
|
+
}
|
|
701
|
+
if (Array.isArray(prompt) && prompt.length === 1 && typeof prompt[0] === "string") {
|
|
702
|
+
return prompt[0];
|
|
703
|
+
}
|
|
704
|
+
throw new OpenAICompatibilityError(
|
|
705
|
+
"Hoopilot legacy completions compatibility supports exactly one string prompt per request."
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
function assertSupportedLegacyCompletionRequest(request) {
|
|
709
|
+
if (request.echo === true) {
|
|
710
|
+
throw new OpenAICompatibilityError(
|
|
711
|
+
"Hoopilot legacy completions compatibility does not support echo=true."
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (typeof request.best_of === "number" && request.best_of > 1) {
|
|
715
|
+
throw new OpenAICompatibilityError(
|
|
716
|
+
"Hoopilot legacy completions compatibility does not support best_of greater than 1."
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
if (typeof request.logprobs === "number" && request.logprobs > 0) {
|
|
720
|
+
throw new OpenAICompatibilityError(
|
|
721
|
+
"Hoopilot legacy completions compatibility does not support legacy logprobs."
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (contentToText(request.suffix)) {
|
|
725
|
+
throw new OpenAICompatibilityError(
|
|
726
|
+
"Hoopilot legacy completions compatibility does not support suffix."
|
|
727
|
+
);
|
|
671
728
|
}
|
|
672
|
-
return contentToText(prompt);
|
|
673
729
|
}
|
|
674
730
|
function contentToText(content) {
|
|
675
731
|
if (typeof content === "string") {
|
|
@@ -727,21 +783,27 @@ function firstNumber(...values) {
|
|
|
727
783
|
}
|
|
728
784
|
return void 0;
|
|
729
785
|
}
|
|
730
|
-
function
|
|
786
|
+
function completionChoices(completion) {
|
|
731
787
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
732
|
-
return asRecord(
|
|
788
|
+
return choices.map((choice) => asRecord(choice));
|
|
733
789
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
790
|
+
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
791
|
+
let event = "message";
|
|
792
|
+
const dataLines = [];
|
|
793
|
+
for (const line of block.split(/\r?\n/)) {
|
|
794
|
+
const trimmed = line.trim();
|
|
795
|
+
if (trimmed.startsWith("event:")) {
|
|
796
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
797
|
+
} else if (trimmed.startsWith("data:")) {
|
|
798
|
+
dataLines.push(trimmed.slice("data:".length).trim());
|
|
799
|
+
}
|
|
738
800
|
}
|
|
739
|
-
const data =
|
|
801
|
+
const data = dataLines.join("\n");
|
|
740
802
|
if (!data) {
|
|
741
803
|
return;
|
|
742
804
|
}
|
|
743
805
|
if (data === "[DONE]") {
|
|
744
|
-
|
|
806
|
+
markTerminal();
|
|
745
807
|
enqueue("[DONE]");
|
|
746
808
|
return;
|
|
747
809
|
}
|
|
@@ -749,25 +811,34 @@ function processCompletionSseLine(line, enqueue, markDone) {
|
|
|
749
811
|
if (!parsed) {
|
|
750
812
|
return;
|
|
751
813
|
}
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
814
|
+
const error = completionStreamError(event, parsed);
|
|
815
|
+
if (error) {
|
|
816
|
+
markTerminal();
|
|
817
|
+
enqueue({ error });
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const choices = completionChoices(parsed).map((choice, index) => {
|
|
821
|
+
const delta = asRecord(choice.delta);
|
|
822
|
+
const text = contentToText(delta.content);
|
|
823
|
+
const finishReason = choice.finish_reason ?? null;
|
|
824
|
+
if (!text && finishReason === null) {
|
|
825
|
+
return void 0;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
finish_reason: finishReason,
|
|
829
|
+
index: typeof choice.index === "number" ? choice.index : index,
|
|
830
|
+
logprobs: choice.logprobs ?? null,
|
|
831
|
+
text
|
|
832
|
+
};
|
|
833
|
+
}).filter((choice) => choice !== void 0);
|
|
756
834
|
const usage = asRecord(parsed.usage);
|
|
757
835
|
const hasUsage = Object.keys(usage).length > 0;
|
|
758
|
-
if (
|
|
836
|
+
if (choices.length === 0 && !hasUsage) {
|
|
759
837
|
return;
|
|
760
838
|
}
|
|
761
839
|
enqueue(
|
|
762
840
|
removeUndefined({
|
|
763
|
-
choices
|
|
764
|
-
{
|
|
765
|
-
finish_reason: finishReason,
|
|
766
|
-
index: typeof choice.index === "number" ? choice.index : 0,
|
|
767
|
-
logprobs: null,
|
|
768
|
-
text
|
|
769
|
-
}
|
|
770
|
-
] : [],
|
|
841
|
+
choices,
|
|
771
842
|
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
772
843
|
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
773
844
|
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
@@ -776,6 +847,22 @@ function processCompletionSseLine(line, enqueue, markDone) {
|
|
|
776
847
|
})
|
|
777
848
|
);
|
|
778
849
|
}
|
|
850
|
+
function completionStreamError(event, parsed) {
|
|
851
|
+
const responseError = asRecord(asRecord(parsed.response).error);
|
|
852
|
+
const directError = asRecord(parsed.error);
|
|
853
|
+
const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
|
|
854
|
+
if (error) {
|
|
855
|
+
return error;
|
|
856
|
+
}
|
|
857
|
+
if (event === "error" || parsed.type === "response.failed") {
|
|
858
|
+
return removeUndefined({
|
|
859
|
+
code: contentToText(parsed.code) || void 0,
|
|
860
|
+
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
861
|
+
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return void 0;
|
|
865
|
+
}
|
|
779
866
|
function encodeDataSse(data) {
|
|
780
867
|
if (data === "[DONE]") {
|
|
781
868
|
return "data: [DONE]\n\n";
|
|
@@ -1172,10 +1259,19 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
1172
1259
|
var DEFAULT_PORT = 4141;
|
|
1173
1260
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1174
1261
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1262
|
+
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1263
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1264
|
+
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
1175
1265
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1266
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
1267
|
+
constructor() {
|
|
1268
|
+
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
1269
|
+
this.name = "RequestBodyTooLargeError";
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1176
1272
|
function createHoopilotHandler(options = {}) {
|
|
1177
1273
|
const client = new CopilotClient(options);
|
|
1178
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1274
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1179
1275
|
const logger = serverLogger(options);
|
|
1180
1276
|
const metrics = options.metrics ?? new MetricsRegistry();
|
|
1181
1277
|
const readUsage = createUsageReader(client, metrics);
|
|
@@ -1261,6 +1357,18 @@ function createHoopilotHandler(options = {}) {
|
|
|
1261
1357
|
"request body was invalid json"
|
|
1262
1358
|
);
|
|
1263
1359
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1360
|
+
} else if (error instanceof OpenAICompatibilityError) {
|
|
1361
|
+
requestLogger.warn(
|
|
1362
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1363
|
+
"request body used unsupported OpenAI compatibility fields"
|
|
1364
|
+
);
|
|
1365
|
+
return finish(jsonError(400, "invalid_request_error", message));
|
|
1366
|
+
} else if (error instanceof RequestBodyTooLargeError) {
|
|
1367
|
+
requestLogger.warn(
|
|
1368
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1369
|
+
"request body exceeded size limit"
|
|
1370
|
+
);
|
|
1371
|
+
return finish(jsonError(413, "request_too_large", message));
|
|
1264
1372
|
} else {
|
|
1265
1373
|
requestLogger.error(
|
|
1266
1374
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
@@ -1272,10 +1380,10 @@ function createHoopilotHandler(options = {}) {
|
|
|
1272
1380
|
};
|
|
1273
1381
|
}
|
|
1274
1382
|
function startHoopilotServer(options = {}) {
|
|
1275
|
-
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1276
|
-
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1277
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1278
|
-
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1383
|
+
const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
|
|
1384
|
+
const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
|
|
1385
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1386
|
+
const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
|
|
1279
1387
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
1280
1388
|
throw new Error(
|
|
1281
1389
|
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
@@ -1293,7 +1401,7 @@ function startHoopilotServer(options = {}) {
|
|
|
1293
1401
|
});
|
|
1294
1402
|
return {
|
|
1295
1403
|
server,
|
|
1296
|
-
url: `http://${host}:${server.port}`
|
|
1404
|
+
url: `http://${urlHost(host)}:${server.port}`
|
|
1297
1405
|
};
|
|
1298
1406
|
}
|
|
1299
1407
|
async function handleModels(client, metrics, signal, logger) {
|
|
@@ -1402,14 +1510,15 @@ function proxyResponse(upstream) {
|
|
|
1402
1510
|
});
|
|
1403
1511
|
}
|
|
1404
1512
|
async function readJson(request) {
|
|
1513
|
+
const text = await readRequestText(request);
|
|
1405
1514
|
try {
|
|
1406
|
-
return asRecord(
|
|
1515
|
+
return asRecord(JSON.parse(text));
|
|
1407
1516
|
} catch {
|
|
1408
1517
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1409
1518
|
}
|
|
1410
1519
|
}
|
|
1411
1520
|
async function readJsonText(request) {
|
|
1412
|
-
const text = await request
|
|
1521
|
+
const text = await readRequestText(request);
|
|
1413
1522
|
try {
|
|
1414
1523
|
JSON.parse(text);
|
|
1415
1524
|
return text;
|
|
@@ -1417,6 +1526,40 @@ async function readJsonText(request) {
|
|
|
1417
1526
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1418
1527
|
}
|
|
1419
1528
|
}
|
|
1529
|
+
async function readRequestText(request) {
|
|
1530
|
+
const contentLength = request.headers.get("content-length");
|
|
1531
|
+
if (contentLength) {
|
|
1532
|
+
const declaredBytes = Number(contentLength);
|
|
1533
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
|
|
1534
|
+
throw new RequestBodyTooLargeError();
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const body = request.body;
|
|
1538
|
+
if (!body) {
|
|
1539
|
+
return "";
|
|
1540
|
+
}
|
|
1541
|
+
const reader = body.getReader();
|
|
1542
|
+
const decoder = new TextDecoder();
|
|
1543
|
+
let bytes = 0;
|
|
1544
|
+
let text = "";
|
|
1545
|
+
try {
|
|
1546
|
+
while (true) {
|
|
1547
|
+
const { done, value } = await reader.read();
|
|
1548
|
+
if (done) {
|
|
1549
|
+
return `${text}${decoder.decode()}`;
|
|
1550
|
+
}
|
|
1551
|
+
bytes += value.byteLength;
|
|
1552
|
+
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
1553
|
+
await reader.cancel().catch(() => {
|
|
1554
|
+
});
|
|
1555
|
+
throw new RequestBodyTooLargeError();
|
|
1556
|
+
}
|
|
1557
|
+
text += decoder.decode(value, { stream: true });
|
|
1558
|
+
}
|
|
1559
|
+
} finally {
|
|
1560
|
+
reader.releaseLock();
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1420
1563
|
function jsonResponse(body, status = 200) {
|
|
1421
1564
|
return new Response(JSON.stringify(body), {
|
|
1422
1565
|
headers: {
|
|
@@ -1489,6 +1632,9 @@ function upstreamAuthMessage(message) {
|
|
|
1489
1632
|
function isLoopbackHost(host) {
|
|
1490
1633
|
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1491
1634
|
}
|
|
1635
|
+
function urlHost(host) {
|
|
1636
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1637
|
+
}
|
|
1492
1638
|
function isLoopbackOrigin(origin) {
|
|
1493
1639
|
try {
|
|
1494
1640
|
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
@@ -1596,7 +1742,7 @@ function logRequestCompleted(logger, status, stream, durationMs) {
|
|
|
1596
1742
|
}
|
|
1597
1743
|
function requestIdFor(request) {
|
|
1598
1744
|
const existing = request.headers.get("x-request-id")?.trim();
|
|
1599
|
-
return existing
|
|
1745
|
+
return existing && REQUEST_ID_PATTERN.test(existing) ? existing : crypto.randomUUID();
|
|
1600
1746
|
}
|
|
1601
1747
|
function canonicalApiPath(path) {
|
|
1602
1748
|
const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
@@ -2297,6 +2443,7 @@ function errorMessage2(error) {
|
|
|
2297
2443
|
}
|
|
2298
2444
|
|
|
2299
2445
|
// src/cli.ts
|
|
2446
|
+
var ALLOWED_COPILOT_API_HOSTS2 = ["api.githubcopilot.com"];
|
|
2300
2447
|
async function main2(argv = Bun.argv.slice(2)) {
|
|
2301
2448
|
cleanupOldBinary();
|
|
2302
2449
|
const command = argv[0];
|
|
@@ -2399,46 +2546,54 @@ function parseArgs(argv) {
|
|
|
2399
2546
|
args.noUpdateCheck = true;
|
|
2400
2547
|
continue;
|
|
2401
2548
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
if (!value) {
|
|
2405
|
-
throw new Error(`Missing value for ${arg}.`);
|
|
2549
|
+
if (!arg.startsWith("-")) {
|
|
2550
|
+
throw new Error(`Unknown argument: ${arg}.`);
|
|
2406
2551
|
}
|
|
2552
|
+
const [name, inlineValue] = splitOption(arg);
|
|
2407
2553
|
switch (name) {
|
|
2408
2554
|
case "--api-key":
|
|
2409
|
-
args.apiKey =
|
|
2555
|
+
args.apiKey = optionValue(name, inlineValue, rest);
|
|
2410
2556
|
break;
|
|
2411
2557
|
case "--api-key-file":
|
|
2412
|
-
args.apiKey = readApiKeyFile(
|
|
2558
|
+
args.apiKey = readApiKeyFile(optionValue(name, inlineValue, rest));
|
|
2413
2559
|
break;
|
|
2414
2560
|
case "--auth-file":
|
|
2415
|
-
args.authStorePath =
|
|
2561
|
+
args.authStorePath = optionValue(name, inlineValue, rest);
|
|
2416
2562
|
break;
|
|
2417
2563
|
case "--copilot-api-base-url":
|
|
2418
|
-
args.copilotApiBaseUrl =
|
|
2564
|
+
args.copilotApiBaseUrl = optionValue(name, inlineValue, rest);
|
|
2419
2565
|
break;
|
|
2420
2566
|
case "--log-format":
|
|
2421
|
-
args.logFormat = parseLogFormat(
|
|
2567
|
+
args.logFormat = parseLogFormat(optionValue(name, inlineValue, rest));
|
|
2422
2568
|
break;
|
|
2423
2569
|
case "--log-level":
|
|
2424
|
-
args.logLevel = parseLogLevel(
|
|
2570
|
+
args.logLevel = parseLogLevel(optionValue(name, inlineValue, rest));
|
|
2425
2571
|
break;
|
|
2426
2572
|
case "--host":
|
|
2427
|
-
args.host =
|
|
2573
|
+
args.host = optionValue(name, inlineValue, rest);
|
|
2428
2574
|
break;
|
|
2429
2575
|
case "--port":
|
|
2430
|
-
case "-p":
|
|
2576
|
+
case "-p": {
|
|
2577
|
+
const value = optionValue(name, inlineValue, rest);
|
|
2431
2578
|
args.port = Number(value);
|
|
2432
2579
|
if (!Number.isInteger(args.port) || args.port <= 0 || args.port > 65535) {
|
|
2433
2580
|
throw new Error(`Invalid port: ${value}.`);
|
|
2434
2581
|
}
|
|
2435
2582
|
break;
|
|
2583
|
+
}
|
|
2436
2584
|
default:
|
|
2437
2585
|
throw new Error(`Unknown option: ${name}.`);
|
|
2438
2586
|
}
|
|
2439
2587
|
}
|
|
2440
2588
|
return args;
|
|
2441
2589
|
}
|
|
2590
|
+
function optionValue(name, inlineValue, rest) {
|
|
2591
|
+
const value = inlineValue ?? rest.shift();
|
|
2592
|
+
if (!value) {
|
|
2593
|
+
throw new Error(`Missing value for ${name}.`);
|
|
2594
|
+
}
|
|
2595
|
+
return value;
|
|
2596
|
+
}
|
|
2442
2597
|
function splitOption(arg) {
|
|
2443
2598
|
const separator = arg.indexOf("=");
|
|
2444
2599
|
if (separator === -1) {
|
|
@@ -2589,10 +2744,13 @@ function roundQuota(value) {
|
|
|
2589
2744
|
}
|
|
2590
2745
|
async function verifyCopilotOAuthToken(token, options = {}) {
|
|
2591
2746
|
const apiBaseUrl = trimTrailingSlash(
|
|
2592
|
-
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2747
|
+
options.copilotApiBaseUrl ?? envValue(options.env?.COPILOT_API_BASE_URL) ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2593
2748
|
);
|
|
2594
|
-
|
|
2595
|
-
|
|
2749
|
+
const allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
2750
|
+
if (!isTrustedTokenBaseUrl(apiBaseUrl, ALLOWED_COPILOT_API_HOSTS2, allowUnsafeUpstream)) {
|
|
2751
|
+
throw new Error(
|
|
2752
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${apiBaseUrl}`
|
|
2753
|
+
);
|
|
2596
2754
|
}
|
|
2597
2755
|
const fetcher = options.fetch ?? fetch;
|
|
2598
2756
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
@@ -2700,6 +2858,7 @@ Environment:
|
|
|
2700
2858
|
HOOPILOT_LOG_LEVEL trace, debug, info, warn, error, fatal, or silent
|
|
2701
2859
|
COPILOT_API_BASE_URL
|
|
2702
2860
|
HOOPILOT_GITHUB_API_BASE_URL GitHub REST base for the usage/quota lookup. Default: https://api.github.com
|
|
2861
|
+
HOOPILOT_ALLOW_UNSAFE_UPSTREAM Set to 1 to allow nonstandard HTTPS token hosts
|
|
2703
2862
|
HOOPILOT_NO_UPDATE_CHECK Set to disable update checks (also NO_UPDATE_NOTIFIER)
|
|
2704
2863
|
`;
|
|
2705
2864
|
}
|