@openhoo/hoopilot 0.7.1 → 0.7.2
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 -2
- package/dist/chunk-TEDEVCKM.js +167 -0
- package/dist/chunk-TEDEVCKM.js.map +1 -0
- package/dist/cli.js +321 -39
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +5 -161
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +360 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +359 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// src/auth-store.ts
|
|
2
2
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
|
+
var StoredCopilotAuthError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "StoredCopilotAuthError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
4
10
|
function authStorePath(env = process.env) {
|
|
5
11
|
if (env.HOOPILOT_AUTH_FILE) {
|
|
6
12
|
return env.HOOPILOT_AUTH_FILE;
|
|
@@ -9,25 +15,38 @@ function authStorePath(env = process.env) {
|
|
|
9
15
|
return join(base, "hoopilot", "auth.json");
|
|
10
16
|
}
|
|
11
17
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
18
|
+
let text;
|
|
12
19
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
|
|
18
|
-
if (!token) {
|
|
20
|
+
text = readFileSync(path, "utf8");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
19
23
|
return void 0;
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
token
|
|
27
|
-
};
|
|
25
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
26
|
+
}
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(text);
|
|
28
30
|
} catch {
|
|
29
|
-
|
|
31
|
+
throw new StoredCopilotAuthError(
|
|
32
|
+
`Hoopilot auth file at ${path} is not valid JSON. Run \`hoopilot login\` to replace it.`
|
|
33
|
+
);
|
|
30
34
|
}
|
|
35
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
36
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
37
|
+
}
|
|
38
|
+
const record = parsed;
|
|
39
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
apiBaseUrl: typeof record.apiBaseUrl === "string" ? record.apiBaseUrl : void 0,
|
|
45
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : void 0,
|
|
46
|
+
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
47
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
48
|
+
token
|
|
49
|
+
};
|
|
31
50
|
}
|
|
32
51
|
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
33
52
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -53,6 +72,18 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
53
72
|
function trimTrailingSlash(value) {
|
|
54
73
|
return value.replace(/\/+$/, "");
|
|
55
74
|
}
|
|
75
|
+
function isHttpsOrLoopbackUrl(rawUrl) {
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(rawUrl);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (url.protocol === "https:") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
|
|
86
|
+
}
|
|
56
87
|
async function truncatedResponseText(response, max = 500) {
|
|
57
88
|
const text = await response.text();
|
|
58
89
|
return text.slice(0, max);
|
|
@@ -85,7 +116,15 @@ var CopilotAuth = class {
|
|
|
85
116
|
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
86
117
|
return this.#cachedAccess;
|
|
87
118
|
}
|
|
88
|
-
|
|
119
|
+
let stored;
|
|
120
|
+
try {
|
|
121
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
124
|
+
throw new CopilotAuthError(error.message);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
89
128
|
if (stored) {
|
|
90
129
|
return this.#cacheAccess({
|
|
91
130
|
apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
|
|
@@ -144,7 +183,7 @@ var CopilotClient = class {
|
|
|
144
183
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
145
184
|
*/
|
|
146
185
|
async usage(signal) {
|
|
147
|
-
if (!
|
|
186
|
+
if (!isHttpsOrLoopbackUrl(this.#githubApiBaseUrl)) {
|
|
148
187
|
throw new Error(
|
|
149
188
|
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${this.#githubApiBaseUrl}`
|
|
150
189
|
);
|
|
@@ -188,6 +227,11 @@ var CopilotClient = class {
|
|
|
188
227
|
}
|
|
189
228
|
async fetchCopilot(path, init) {
|
|
190
229
|
const access = await this.#auth.getAccess();
|
|
230
|
+
if (!isHttpsOrLoopbackUrl(access.apiBaseUrl)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${access.apiBaseUrl}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
191
235
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
192
236
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
193
237
|
...init,
|
|
@@ -243,18 +287,6 @@ function usedFrom(entitlement, remaining) {
|
|
|
243
287
|
}
|
|
244
288
|
return Math.max(0, entitlement - remaining);
|
|
245
289
|
}
|
|
246
|
-
function isHttpsOrLoopback(rawUrl) {
|
|
247
|
-
let url;
|
|
248
|
-
try {
|
|
249
|
-
url = new URL(rawUrl);
|
|
250
|
-
} catch {
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
if (url.protocol === "https:") {
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1");
|
|
257
|
-
}
|
|
258
290
|
function numberOrUndefined(value) {
|
|
259
291
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
260
292
|
}
|
|
@@ -609,6 +641,51 @@ function chatCompletionToCompletion(completion) {
|
|
|
609
641
|
usage: completion.usage
|
|
610
642
|
});
|
|
611
643
|
}
|
|
644
|
+
function completionStreamFromChatStream(chatStream) {
|
|
645
|
+
const encoder = new TextEncoder();
|
|
646
|
+
const decoder = new TextDecoder();
|
|
647
|
+
let buffer = "";
|
|
648
|
+
let sawDone = false;
|
|
649
|
+
return new ReadableStream({
|
|
650
|
+
async start(controller) {
|
|
651
|
+
const enqueue = (data) => {
|
|
652
|
+
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
653
|
+
};
|
|
654
|
+
const reader = chatStream.getReader();
|
|
655
|
+
try {
|
|
656
|
+
while (true) {
|
|
657
|
+
const result = await reader.read();
|
|
658
|
+
if (result.done) {
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
662
|
+
const lines = buffer.split(/\r?\n/);
|
|
663
|
+
buffer = lines.pop() ?? "";
|
|
664
|
+
for (const line of lines) {
|
|
665
|
+
processCompletionSseLine(line, enqueue, () => {
|
|
666
|
+
sawDone = true;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (buffer) {
|
|
671
|
+
processCompletionSseLine(buffer, enqueue, () => {
|
|
672
|
+
sawDone = true;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (!sawDone) {
|
|
676
|
+
enqueue("[DONE]");
|
|
677
|
+
}
|
|
678
|
+
controller.close();
|
|
679
|
+
} catch (error) {
|
|
680
|
+
await reader.cancel(error).catch(() => {
|
|
681
|
+
});
|
|
682
|
+
controller.error(error);
|
|
683
|
+
} finally {
|
|
684
|
+
reader.releaseLock();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
612
689
|
function normalizeModelsResponse(upstream) {
|
|
613
690
|
const record = asRecord(upstream);
|
|
614
691
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
@@ -641,38 +718,99 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
641
718
|
const createdAt = epochSeconds();
|
|
642
719
|
let buffer = "";
|
|
643
720
|
let text = "";
|
|
721
|
+
let messageOutputIndex;
|
|
722
|
+
let nextOutputIndex = 0;
|
|
723
|
+
let sequenceNumber = 0;
|
|
644
724
|
const tools = /* @__PURE__ */ new Map();
|
|
645
725
|
return new ReadableStream({
|
|
646
726
|
async start(controller) {
|
|
647
727
|
const enqueue = (event, data) => {
|
|
648
|
-
controller.enqueue(
|
|
728
|
+
controller.enqueue(
|
|
729
|
+
encoder.encode(
|
|
730
|
+
encodeSse(
|
|
731
|
+
event,
|
|
732
|
+
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
);
|
|
649
736
|
};
|
|
650
737
|
enqueue("response.created", {
|
|
651
738
|
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
652
739
|
type: "response.created"
|
|
653
740
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
741
|
+
const ensureMessageStarted = () => {
|
|
742
|
+
if (messageOutputIndex !== void 0) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
messageOutputIndex = nextOutputIndex++;
|
|
746
|
+
enqueue("response.output_item.added", {
|
|
747
|
+
item: {
|
|
748
|
+
content: [],
|
|
749
|
+
id: messageId,
|
|
750
|
+
role: "assistant",
|
|
751
|
+
status: "in_progress",
|
|
752
|
+
type: "message"
|
|
753
|
+
},
|
|
754
|
+
output_index: messageOutputIndex,
|
|
755
|
+
type: "response.output_item.added"
|
|
756
|
+
});
|
|
757
|
+
enqueue("response.content_part.added", {
|
|
758
|
+
content_index: 0,
|
|
759
|
+
item_id: messageId,
|
|
760
|
+
output_index: messageOutputIndex,
|
|
761
|
+
part: {
|
|
762
|
+
annotations: [],
|
|
763
|
+
text: "",
|
|
764
|
+
type: "output_text"
|
|
765
|
+
},
|
|
766
|
+
type: "response.content_part.added"
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
const appendText = (delta) => {
|
|
770
|
+
ensureMessageStarted();
|
|
771
|
+
text += delta;
|
|
772
|
+
enqueue("response.output_text.delta", {
|
|
773
|
+
content_index: 0,
|
|
774
|
+
delta,
|
|
775
|
+
item_id: messageId,
|
|
776
|
+
output_index: messageOutputIndex ?? 0,
|
|
777
|
+
type: "response.output_text.delta"
|
|
778
|
+
});
|
|
779
|
+
};
|
|
780
|
+
const appendToolCall = (toolCall) => {
|
|
781
|
+
const fn = asRecord(toolCall.function);
|
|
782
|
+
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
783
|
+
let existing = tools.get(index);
|
|
784
|
+
const isNew = !existing;
|
|
785
|
+
existing ??= {
|
|
786
|
+
arguments: "",
|
|
787
|
+
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
788
|
+
index,
|
|
789
|
+
itemId: `fc_${randomId()}`,
|
|
790
|
+
name: "",
|
|
791
|
+
outputIndex: nextOutputIndex++
|
|
792
|
+
};
|
|
793
|
+
existing.id = contentToText(toolCall.id) || existing.id;
|
|
794
|
+
existing.name += contentToText(fn.name);
|
|
795
|
+
tools.set(index, existing);
|
|
796
|
+
if (isNew) {
|
|
797
|
+
enqueue("response.output_item.added", {
|
|
798
|
+
item: functionCallItem(existing, "in_progress"),
|
|
799
|
+
output_index: existing.outputIndex ?? 0,
|
|
800
|
+
type: "response.output_item.added"
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const argumentDelta = contentToText(fn.arguments);
|
|
804
|
+
if (argumentDelta) {
|
|
805
|
+
existing.arguments += argumentDelta;
|
|
806
|
+
enqueue("response.function_call_arguments.delta", {
|
|
807
|
+
delta: argumentDelta,
|
|
808
|
+
item_id: existing.itemId,
|
|
809
|
+
output_index: existing.outputIndex ?? 0,
|
|
810
|
+
type: "response.function_call_arguments.delta"
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
};
|
|
676
814
|
const reader = chatStream.getReader();
|
|
677
815
|
try {
|
|
678
816
|
while (true) {
|
|
@@ -684,50 +822,48 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
684
822
|
const lines = buffer.split(/\r?\n/);
|
|
685
823
|
buffer = lines.pop() ?? "";
|
|
686
824
|
for (const line of lines) {
|
|
687
|
-
processChatSseLine(
|
|
688
|
-
text += delta;
|
|
689
|
-
});
|
|
825
|
+
processChatSseLine(line, { appendText, appendToolCall });
|
|
690
826
|
}
|
|
691
827
|
}
|
|
692
828
|
if (buffer) {
|
|
693
|
-
processChatSseLine(
|
|
694
|
-
text += delta;
|
|
695
|
-
});
|
|
829
|
+
processChatSseLine(buffer, { appendText, appendToolCall });
|
|
696
830
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
});
|
|
706
|
-
enqueue("response.content_part.done", {
|
|
707
|
-
content_index: 0,
|
|
708
|
-
item_id: messageId,
|
|
709
|
-
output_index: 0,
|
|
710
|
-
part: {
|
|
711
|
-
annotations: [],
|
|
831
|
+
const outputEntries = [];
|
|
832
|
+
if (messageOutputIndex !== void 0) {
|
|
833
|
+
const item = messageOutputItem(text, messageId);
|
|
834
|
+
outputEntries.push([messageOutputIndex, item]);
|
|
835
|
+
enqueue("response.output_text.done", {
|
|
836
|
+
content_index: 0,
|
|
837
|
+
item_id: messageId,
|
|
838
|
+
output_index: messageOutputIndex,
|
|
712
839
|
text,
|
|
713
|
-
type: "output_text"
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
840
|
+
type: "response.output_text.done"
|
|
841
|
+
});
|
|
842
|
+
enqueue("response.content_part.done", {
|
|
843
|
+
content_index: 0,
|
|
844
|
+
item_id: messageId,
|
|
845
|
+
output_index: messageOutputIndex,
|
|
846
|
+
part: {
|
|
847
|
+
annotations: [],
|
|
848
|
+
text,
|
|
849
|
+
type: "output_text"
|
|
850
|
+
},
|
|
851
|
+
type: "response.content_part.done"
|
|
852
|
+
});
|
|
853
|
+
enqueue("response.output_item.done", {
|
|
725
854
|
item,
|
|
726
|
-
output_index:
|
|
727
|
-
type: "response.output_item.
|
|
855
|
+
output_index: messageOutputIndex,
|
|
856
|
+
type: "response.output_item.done"
|
|
728
857
|
});
|
|
858
|
+
}
|
|
859
|
+
for (const tool of [...tools.values()].sort(
|
|
860
|
+
(a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
|
|
861
|
+
)) {
|
|
862
|
+
const item = functionCallItem(tool);
|
|
863
|
+
const outputIndex = tool.outputIndex ?? 0;
|
|
864
|
+
outputEntries.push([outputIndex, item]);
|
|
729
865
|
enqueue("response.function_call_arguments.done", {
|
|
730
|
-
arguments:
|
|
866
|
+
arguments: tool.arguments,
|
|
731
867
|
item_id: item.id,
|
|
732
868
|
output_index: outputIndex,
|
|
733
869
|
type: "response.function_call_arguments.done"
|
|
@@ -737,7 +873,8 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
737
873
|
output_index: outputIndex,
|
|
738
874
|
type: "response.output_item.done"
|
|
739
875
|
});
|
|
740
|
-
}
|
|
876
|
+
}
|
|
877
|
+
const output = outputEntries.sort(([left], [right]) => left - right).map(([, item]) => item);
|
|
741
878
|
enqueue("response.completed", {
|
|
742
879
|
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
743
880
|
type: "response.completed"
|
|
@@ -920,13 +1057,13 @@ function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
|
920
1057
|
type: "message"
|
|
921
1058
|
};
|
|
922
1059
|
}
|
|
923
|
-
function functionCallItem(tool) {
|
|
1060
|
+
function functionCallItem(tool, status = "completed") {
|
|
924
1061
|
return {
|
|
925
1062
|
arguments: tool.arguments,
|
|
926
1063
|
call_id: tool.id,
|
|
927
|
-
id: `fc_${randomId()}`,
|
|
1064
|
+
id: tool.itemId ?? `fc_${randomId()}`,
|
|
928
1065
|
name: tool.name,
|
|
929
|
-
status
|
|
1066
|
+
status,
|
|
930
1067
|
type: "function_call"
|
|
931
1068
|
};
|
|
932
1069
|
}
|
|
@@ -941,14 +1078,27 @@ function responseUsage(usage) {
|
|
|
941
1078
|
if (Object.keys(record).length === 0) {
|
|
942
1079
|
return null;
|
|
943
1080
|
}
|
|
1081
|
+
const inputTokens = record.prompt_tokens;
|
|
1082
|
+
const outputTokens = record.completion_tokens;
|
|
944
1083
|
return removeUndefined({
|
|
945
|
-
input_tokens:
|
|
946
|
-
input_tokens_details: record.prompt_tokens_details,
|
|
947
|
-
|
|
948
|
-
|
|
1084
|
+
input_tokens: inputTokens,
|
|
1085
|
+
input_tokens_details: responseUsageDetails(record.prompt_tokens_details, inputTokens, {
|
|
1086
|
+
cached_tokens: 0
|
|
1087
|
+
}),
|
|
1088
|
+
output_tokens: outputTokens,
|
|
1089
|
+
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
1090
|
+
reasoning_tokens: 0
|
|
1091
|
+
}),
|
|
949
1092
|
total_tokens: record.total_tokens
|
|
950
1093
|
});
|
|
951
1094
|
}
|
|
1095
|
+
function responseUsageDetails(value, tokenCount, fallback) {
|
|
1096
|
+
const record = asRecord(value);
|
|
1097
|
+
if (Object.keys(record).length > 0) {
|
|
1098
|
+
return record;
|
|
1099
|
+
}
|
|
1100
|
+
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
1101
|
+
}
|
|
952
1102
|
function extractTokenUsage(usage) {
|
|
953
1103
|
const record = asRecord(usage);
|
|
954
1104
|
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
@@ -987,7 +1137,52 @@ function firstChoice(completion) {
|
|
|
987
1137
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
988
1138
|
return asRecord(choices[0]);
|
|
989
1139
|
}
|
|
990
|
-
function
|
|
1140
|
+
function processCompletionSseLine(line, enqueue, markDone) {
|
|
1141
|
+
const trimmed = line.trim();
|
|
1142
|
+
if (!trimmed.startsWith("data:")) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const data = trimmed.slice("data:".length).trim();
|
|
1146
|
+
if (!data) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (data === "[DONE]") {
|
|
1150
|
+
markDone();
|
|
1151
|
+
enqueue("[DONE]");
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const parsed = parseJson(data);
|
|
1155
|
+
if (!parsed) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const choice = firstChoice(parsed);
|
|
1159
|
+
const delta = asRecord(choice.delta);
|
|
1160
|
+
const text = contentToText(delta.content);
|
|
1161
|
+
const finishReason = choice.finish_reason ?? null;
|
|
1162
|
+
const usage = asRecord(parsed.usage);
|
|
1163
|
+
const hasUsage = Object.keys(usage).length > 0;
|
|
1164
|
+
if (!text && finishReason === null && !hasUsage) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
enqueue(
|
|
1168
|
+
removeUndefined({
|
|
1169
|
+
choices: text || finishReason !== null ? [
|
|
1170
|
+
{
|
|
1171
|
+
finish_reason: finishReason,
|
|
1172
|
+
index: typeof choice.index === "number" ? choice.index : 0,
|
|
1173
|
+
logprobs: null,
|
|
1174
|
+
text
|
|
1175
|
+
}
|
|
1176
|
+
] : [],
|
|
1177
|
+
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
1178
|
+
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
1179
|
+
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
1180
|
+
object: "text_completion",
|
|
1181
|
+
usage: hasUsage ? usage : void 0
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
function processChatSseLine(line, handlers) {
|
|
991
1186
|
const trimmed = line.trim();
|
|
992
1187
|
if (!trimmed.startsWith("data:")) {
|
|
993
1188
|
return;
|
|
@@ -1004,30 +1199,11 @@ function processChatSseLine(messageId, line, enqueue, tools, appendText) {
|
|
|
1004
1199
|
const delta = asRecord(choice.delta);
|
|
1005
1200
|
const content = contentToText(delta.content);
|
|
1006
1201
|
if (content) {
|
|
1007
|
-
appendText(content);
|
|
1008
|
-
enqueue("response.output_text.delta", {
|
|
1009
|
-
content_index: 0,
|
|
1010
|
-
delta: content,
|
|
1011
|
-
item_id: messageId,
|
|
1012
|
-
output_index: 0,
|
|
1013
|
-
type: "response.output_text.delta"
|
|
1014
|
-
});
|
|
1202
|
+
handlers.appendText(content);
|
|
1015
1203
|
}
|
|
1016
1204
|
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
1017
1205
|
for (const toolCall of toolCalls) {
|
|
1018
|
-
|
|
1019
|
-
const fn = asRecord(record.function);
|
|
1020
|
-
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
1021
|
-
const existing = tools.get(index) ?? {
|
|
1022
|
-
arguments: "",
|
|
1023
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
1024
|
-
index,
|
|
1025
|
-
name: ""
|
|
1026
|
-
};
|
|
1027
|
-
existing.id = contentToText(record.id) || existing.id;
|
|
1028
|
-
existing.name += contentToText(fn.name);
|
|
1029
|
-
existing.arguments += contentToText(fn.arguments);
|
|
1030
|
-
tools.set(index, existing);
|
|
1206
|
+
handlers.appendToolCall(asRecord(toolCall));
|
|
1031
1207
|
}
|
|
1032
1208
|
}
|
|
1033
1209
|
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
@@ -1057,6 +1233,14 @@ function encodeSse(event, data) {
|
|
|
1057
1233
|
return `event: ${event}
|
|
1058
1234
|
data: ${JSON.stringify(data)}
|
|
1059
1235
|
|
|
1236
|
+
`;
|
|
1237
|
+
}
|
|
1238
|
+
function encodeDataSse(data) {
|
|
1239
|
+
if (data === "[DONE]") {
|
|
1240
|
+
return "data: [DONE]\n\n";
|
|
1241
|
+
}
|
|
1242
|
+
return `data: ${JSON.stringify(data)}
|
|
1243
|
+
|
|
1060
1244
|
`;
|
|
1061
1245
|
}
|
|
1062
1246
|
function parseJson(data) {
|
|
@@ -1445,6 +1629,7 @@ function formatNumber(value) {
|
|
|
1445
1629
|
// src/server.ts
|
|
1446
1630
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1447
1631
|
var DEFAULT_PORT = 4141;
|
|
1632
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1448
1633
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1449
1634
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1450
1635
|
function createHoopilotHandler(options = {}) {
|
|
@@ -1475,6 +1660,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1475
1660
|
route,
|
|
1476
1661
|
startedAt
|
|
1477
1662
|
});
|
|
1663
|
+
const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
|
|
1664
|
+
if (browserOrigin) {
|
|
1665
|
+
requestLogger.warn(
|
|
1666
|
+
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
1667
|
+
"blocked unauthenticated browser-origin request"
|
|
1668
|
+
);
|
|
1669
|
+
return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
|
|
1670
|
+
}
|
|
1478
1671
|
if (request.method === "OPTIONS") {
|
|
1479
1672
|
return finish(new Response(null, { headers: corsHeaders() }));
|
|
1480
1673
|
}
|
|
@@ -1539,7 +1732,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1539
1732
|
}
|
|
1540
1733
|
function startHoopilotServer(options = {}) {
|
|
1541
1734
|
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1542
|
-
const port =
|
|
1735
|
+
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1543
1736
|
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1544
1737
|
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1545
1738
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
@@ -1605,8 +1798,19 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
|
|
|
1605
1798
|
}
|
|
1606
1799
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
1607
1800
|
const model = normalizeRequestedModel(body.model);
|
|
1608
|
-
if (isStreamingResponse(upstream)) {
|
|
1609
|
-
return proxyResponse(
|
|
1801
|
+
if (isStreamingResponse(upstream) && upstream.body) {
|
|
1802
|
+
return proxyResponse(
|
|
1803
|
+
observeResponseUsage(
|
|
1804
|
+
new Response(completionStreamFromChatStream(upstream.body), {
|
|
1805
|
+
headers: upstream.headers,
|
|
1806
|
+
status: upstream.status,
|
|
1807
|
+
statusText: upstream.statusText
|
|
1808
|
+
}),
|
|
1809
|
+
model,
|
|
1810
|
+
recordTokens,
|
|
1811
|
+
request.signal
|
|
1812
|
+
)
|
|
1813
|
+
);
|
|
1610
1814
|
}
|
|
1611
1815
|
const completion = asRecord(await upstream.json());
|
|
1612
1816
|
const usage = extractTokenUsage(completion.usage);
|
|
@@ -1640,7 +1844,7 @@ async function proxyError(upstream, logger) {
|
|
|
1640
1844
|
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1641
1845
|
"copilot upstream request failed"
|
|
1642
1846
|
);
|
|
1643
|
-
return
|
|
1847
|
+
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
1644
1848
|
}
|
|
1645
1849
|
function proxyResponse(upstream) {
|
|
1646
1850
|
const headers = new Headers(upstream.headers);
|
|
@@ -1693,6 +1897,13 @@ function jsonError(status, code, message) {
|
|
|
1693
1897
|
status
|
|
1694
1898
|
);
|
|
1695
1899
|
}
|
|
1900
|
+
function upstreamErrorResponse(status, text) {
|
|
1901
|
+
const parsedError = asRecord(asRecord(safeParseJson(text)).error);
|
|
1902
|
+
if (Object.keys(parsedError).length > 0) {
|
|
1903
|
+
return jsonResponse({ error: parsedError }, status);
|
|
1904
|
+
}
|
|
1905
|
+
return jsonError(status, "copilot_error", text);
|
|
1906
|
+
}
|
|
1696
1907
|
function websocketUnsupportedResponse() {
|
|
1697
1908
|
const response = jsonError(
|
|
1698
1909
|
426,
|
|
@@ -1717,6 +1928,17 @@ function isAuthorized(request, apiKey) {
|
|
|
1717
1928
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1718
1929
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1719
1930
|
}
|
|
1931
|
+
function forbiddenBrowserOrigin(request, apiKey) {
|
|
1932
|
+
if (apiKey) {
|
|
1933
|
+
return void 0;
|
|
1934
|
+
}
|
|
1935
|
+
const origin = request.headers.get("origin")?.trim();
|
|
1936
|
+
if (origin) {
|
|
1937
|
+
return isLoopbackOrigin(origin) ? void 0 : origin;
|
|
1938
|
+
}
|
|
1939
|
+
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
1940
|
+
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
1941
|
+
}
|
|
1720
1942
|
function isUpstreamAuthStatus(status) {
|
|
1721
1943
|
return status === 401 || status === 403;
|
|
1722
1944
|
}
|
|
@@ -1724,7 +1946,21 @@ function upstreamAuthMessage(message) {
|
|
|
1724
1946
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1725
1947
|
}
|
|
1726
1948
|
function isLoopbackHost(host) {
|
|
1727
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1949
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1950
|
+
}
|
|
1951
|
+
function isLoopbackOrigin(origin) {
|
|
1952
|
+
try {
|
|
1953
|
+
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
1954
|
+
} catch {
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function normalizeServerPort(value) {
|
|
1959
|
+
const port = Number(value);
|
|
1960
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1961
|
+
throw new Error(`Invalid port: ${value}.`);
|
|
1962
|
+
}
|
|
1963
|
+
return port;
|
|
1728
1964
|
}
|
|
1729
1965
|
function errorMessage(error) {
|
|
1730
1966
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1948,6 +2184,7 @@ export {
|
|
|
1948
2184
|
authStorePath,
|
|
1949
2185
|
chatCompletionToCompletion,
|
|
1950
2186
|
chatCompletionToResponse,
|
|
2187
|
+
completionStreamFromChatStream,
|
|
1951
2188
|
completionsRequestToChatCompletion,
|
|
1952
2189
|
createHoopilotHandler,
|
|
1953
2190
|
createHoopilotLogger,
|