@openhoo/hoopilot 0.7.0 → 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 +322 -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 +361 -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 +360 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
authStorePath: () => authStorePath,
|
|
46
46
|
chatCompletionToCompletion: () => chatCompletionToCompletion,
|
|
47
47
|
chatCompletionToResponse: () => chatCompletionToResponse,
|
|
48
|
+
completionStreamFromChatStream: () => completionStreamFromChatStream,
|
|
48
49
|
completionsRequestToChatCompletion: () => completionsRequestToChatCompletion,
|
|
49
50
|
createHoopilotHandler: () => createHoopilotHandler,
|
|
50
51
|
createHoopilotLogger: () => createHoopilotLogger,
|
|
@@ -70,6 +71,12 @@ module.exports = __toCommonJS(index_exports);
|
|
|
70
71
|
// src/auth-store.ts
|
|
71
72
|
var import_node_fs = require("fs");
|
|
72
73
|
var import_node_path = require("path");
|
|
74
|
+
var StoredCopilotAuthError = class extends Error {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "StoredCopilotAuthError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
73
80
|
function authStorePath(env = process.env) {
|
|
74
81
|
if (env.HOOPILOT_AUTH_FILE) {
|
|
75
82
|
return env.HOOPILOT_AUTH_FILE;
|
|
@@ -78,25 +85,38 @@ function authStorePath(env = process.env) {
|
|
|
78
85
|
return (0, import_node_path.join)(base, "hoopilot", "auth.json");
|
|
79
86
|
}
|
|
80
87
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
88
|
+
let text;
|
|
81
89
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
|
|
87
|
-
if (!token) {
|
|
90
|
+
text = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code === "ENOENT") {
|
|
88
93
|
return void 0;
|
|
89
94
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
token
|
|
96
|
-
};
|
|
95
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
96
|
+
}
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(text);
|
|
97
100
|
} catch {
|
|
98
|
-
|
|
101
|
+
throw new StoredCopilotAuthError(
|
|
102
|
+
`Hoopilot auth file at ${path} is not valid JSON. Run \`hoopilot login\` to replace it.`
|
|
103
|
+
);
|
|
99
104
|
}
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
106
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
107
|
+
}
|
|
108
|
+
const record = parsed;
|
|
109
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
110
|
+
if (!token) {
|
|
111
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
apiBaseUrl: typeof record.apiBaseUrl === "string" ? record.apiBaseUrl : void 0,
|
|
115
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : void 0,
|
|
116
|
+
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
117
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
118
|
+
token
|
|
119
|
+
};
|
|
100
120
|
}
|
|
101
121
|
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
102
122
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
@@ -122,6 +142,18 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
122
142
|
function trimTrailingSlash(value) {
|
|
123
143
|
return value.replace(/\/+$/, "");
|
|
124
144
|
}
|
|
145
|
+
function isHttpsOrLoopbackUrl(rawUrl) {
|
|
146
|
+
let url;
|
|
147
|
+
try {
|
|
148
|
+
url = new URL(rawUrl);
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (url.protocol === "https:") {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
|
|
156
|
+
}
|
|
125
157
|
async function truncatedResponseText(response, max = 500) {
|
|
126
158
|
const text = await response.text();
|
|
127
159
|
return text.slice(0, max);
|
|
@@ -154,7 +186,15 @@ var CopilotAuth = class {
|
|
|
154
186
|
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
155
187
|
return this.#cachedAccess;
|
|
156
188
|
}
|
|
157
|
-
|
|
189
|
+
let stored;
|
|
190
|
+
try {
|
|
191
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
194
|
+
throw new CopilotAuthError(error.message);
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
158
198
|
if (stored) {
|
|
159
199
|
return this.#cacheAccess({
|
|
160
200
|
apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
|
|
@@ -213,7 +253,7 @@ var CopilotClient = class {
|
|
|
213
253
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
214
254
|
*/
|
|
215
255
|
async usage(signal) {
|
|
216
|
-
if (!
|
|
256
|
+
if (!isHttpsOrLoopbackUrl(this.#githubApiBaseUrl)) {
|
|
217
257
|
throw new Error(
|
|
218
258
|
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${this.#githubApiBaseUrl}`
|
|
219
259
|
);
|
|
@@ -257,6 +297,11 @@ var CopilotClient = class {
|
|
|
257
297
|
}
|
|
258
298
|
async fetchCopilot(path, init) {
|
|
259
299
|
const access = await this.#auth.getAccess();
|
|
300
|
+
if (!isHttpsOrLoopbackUrl(access.apiBaseUrl)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${access.apiBaseUrl}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
260
305
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
261
306
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
262
307
|
...init,
|
|
@@ -312,18 +357,6 @@ function usedFrom(entitlement, remaining) {
|
|
|
312
357
|
}
|
|
313
358
|
return Math.max(0, entitlement - remaining);
|
|
314
359
|
}
|
|
315
|
-
function isHttpsOrLoopback(rawUrl) {
|
|
316
|
-
let url;
|
|
317
|
-
try {
|
|
318
|
-
url = new URL(rawUrl);
|
|
319
|
-
} catch {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
if (url.protocol === "https:") {
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1");
|
|
326
|
-
}
|
|
327
360
|
function numberOrUndefined(value) {
|
|
328
361
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
329
362
|
}
|
|
@@ -678,6 +711,51 @@ function chatCompletionToCompletion(completion) {
|
|
|
678
711
|
usage: completion.usage
|
|
679
712
|
});
|
|
680
713
|
}
|
|
714
|
+
function completionStreamFromChatStream(chatStream) {
|
|
715
|
+
const encoder = new TextEncoder();
|
|
716
|
+
const decoder = new TextDecoder();
|
|
717
|
+
let buffer = "";
|
|
718
|
+
let sawDone = false;
|
|
719
|
+
return new ReadableStream({
|
|
720
|
+
async start(controller) {
|
|
721
|
+
const enqueue = (data) => {
|
|
722
|
+
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
723
|
+
};
|
|
724
|
+
const reader = chatStream.getReader();
|
|
725
|
+
try {
|
|
726
|
+
while (true) {
|
|
727
|
+
const result = await reader.read();
|
|
728
|
+
if (result.done) {
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
732
|
+
const lines = buffer.split(/\r?\n/);
|
|
733
|
+
buffer = lines.pop() ?? "";
|
|
734
|
+
for (const line of lines) {
|
|
735
|
+
processCompletionSseLine(line, enqueue, () => {
|
|
736
|
+
sawDone = true;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (buffer) {
|
|
741
|
+
processCompletionSseLine(buffer, enqueue, () => {
|
|
742
|
+
sawDone = true;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (!sawDone) {
|
|
746
|
+
enqueue("[DONE]");
|
|
747
|
+
}
|
|
748
|
+
controller.close();
|
|
749
|
+
} catch (error) {
|
|
750
|
+
await reader.cancel(error).catch(() => {
|
|
751
|
+
});
|
|
752
|
+
controller.error(error);
|
|
753
|
+
} finally {
|
|
754
|
+
reader.releaseLock();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
681
759
|
function normalizeModelsResponse(upstream) {
|
|
682
760
|
const record = asRecord(upstream);
|
|
683
761
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
@@ -710,38 +788,99 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
710
788
|
const createdAt = epochSeconds();
|
|
711
789
|
let buffer = "";
|
|
712
790
|
let text = "";
|
|
791
|
+
let messageOutputIndex;
|
|
792
|
+
let nextOutputIndex = 0;
|
|
793
|
+
let sequenceNumber = 0;
|
|
713
794
|
const tools = /* @__PURE__ */ new Map();
|
|
714
795
|
return new ReadableStream({
|
|
715
796
|
async start(controller) {
|
|
716
797
|
const enqueue = (event, data) => {
|
|
717
|
-
controller.enqueue(
|
|
798
|
+
controller.enqueue(
|
|
799
|
+
encoder.encode(
|
|
800
|
+
encodeSse(
|
|
801
|
+
event,
|
|
802
|
+
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
803
|
+
)
|
|
804
|
+
)
|
|
805
|
+
);
|
|
718
806
|
};
|
|
719
807
|
enqueue("response.created", {
|
|
720
808
|
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
721
809
|
type: "response.created"
|
|
722
810
|
});
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
811
|
+
const ensureMessageStarted = () => {
|
|
812
|
+
if (messageOutputIndex !== void 0) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
messageOutputIndex = nextOutputIndex++;
|
|
816
|
+
enqueue("response.output_item.added", {
|
|
817
|
+
item: {
|
|
818
|
+
content: [],
|
|
819
|
+
id: messageId,
|
|
820
|
+
role: "assistant",
|
|
821
|
+
status: "in_progress",
|
|
822
|
+
type: "message"
|
|
823
|
+
},
|
|
824
|
+
output_index: messageOutputIndex,
|
|
825
|
+
type: "response.output_item.added"
|
|
826
|
+
});
|
|
827
|
+
enqueue("response.content_part.added", {
|
|
828
|
+
content_index: 0,
|
|
829
|
+
item_id: messageId,
|
|
830
|
+
output_index: messageOutputIndex,
|
|
831
|
+
part: {
|
|
832
|
+
annotations: [],
|
|
833
|
+
text: "",
|
|
834
|
+
type: "output_text"
|
|
835
|
+
},
|
|
836
|
+
type: "response.content_part.added"
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
const appendText = (delta) => {
|
|
840
|
+
ensureMessageStarted();
|
|
841
|
+
text += delta;
|
|
842
|
+
enqueue("response.output_text.delta", {
|
|
843
|
+
content_index: 0,
|
|
844
|
+
delta,
|
|
845
|
+
item_id: messageId,
|
|
846
|
+
output_index: messageOutputIndex ?? 0,
|
|
847
|
+
type: "response.output_text.delta"
|
|
848
|
+
});
|
|
849
|
+
};
|
|
850
|
+
const appendToolCall = (toolCall) => {
|
|
851
|
+
const fn = asRecord(toolCall.function);
|
|
852
|
+
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
853
|
+
let existing = tools.get(index);
|
|
854
|
+
const isNew = !existing;
|
|
855
|
+
existing ??= {
|
|
856
|
+
arguments: "",
|
|
857
|
+
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
858
|
+
index,
|
|
859
|
+
itemId: `fc_${randomId()}`,
|
|
860
|
+
name: "",
|
|
861
|
+
outputIndex: nextOutputIndex++
|
|
862
|
+
};
|
|
863
|
+
existing.id = contentToText(toolCall.id) || existing.id;
|
|
864
|
+
existing.name += contentToText(fn.name);
|
|
865
|
+
tools.set(index, existing);
|
|
866
|
+
if (isNew) {
|
|
867
|
+
enqueue("response.output_item.added", {
|
|
868
|
+
item: functionCallItem(existing, "in_progress"),
|
|
869
|
+
output_index: existing.outputIndex ?? 0,
|
|
870
|
+
type: "response.output_item.added"
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
const argumentDelta = contentToText(fn.arguments);
|
|
874
|
+
if (argumentDelta) {
|
|
875
|
+
existing.arguments += argumentDelta;
|
|
876
|
+
enqueue("response.function_call_arguments.delta", {
|
|
877
|
+
delta: argumentDelta,
|
|
878
|
+
item_id: existing.itemId,
|
|
879
|
+
output_index: existing.outputIndex ?? 0,
|
|
880
|
+
type: "response.function_call_arguments.delta"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
};
|
|
745
884
|
const reader = chatStream.getReader();
|
|
746
885
|
try {
|
|
747
886
|
while (true) {
|
|
@@ -753,50 +892,48 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
753
892
|
const lines = buffer.split(/\r?\n/);
|
|
754
893
|
buffer = lines.pop() ?? "";
|
|
755
894
|
for (const line of lines) {
|
|
756
|
-
processChatSseLine(
|
|
757
|
-
text += delta;
|
|
758
|
-
});
|
|
895
|
+
processChatSseLine(line, { appendText, appendToolCall });
|
|
759
896
|
}
|
|
760
897
|
}
|
|
761
898
|
if (buffer) {
|
|
762
|
-
processChatSseLine(
|
|
763
|
-
text += delta;
|
|
764
|
-
});
|
|
899
|
+
processChatSseLine(buffer, { appendText, appendToolCall });
|
|
765
900
|
}
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
});
|
|
775
|
-
enqueue("response.content_part.done", {
|
|
776
|
-
content_index: 0,
|
|
777
|
-
item_id: messageId,
|
|
778
|
-
output_index: 0,
|
|
779
|
-
part: {
|
|
780
|
-
annotations: [],
|
|
901
|
+
const outputEntries = [];
|
|
902
|
+
if (messageOutputIndex !== void 0) {
|
|
903
|
+
const item = messageOutputItem(text, messageId);
|
|
904
|
+
outputEntries.push([messageOutputIndex, item]);
|
|
905
|
+
enqueue("response.output_text.done", {
|
|
906
|
+
content_index: 0,
|
|
907
|
+
item_id: messageId,
|
|
908
|
+
output_index: messageOutputIndex,
|
|
781
909
|
text,
|
|
782
|
-
type: "output_text"
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
910
|
+
type: "response.output_text.done"
|
|
911
|
+
});
|
|
912
|
+
enqueue("response.content_part.done", {
|
|
913
|
+
content_index: 0,
|
|
914
|
+
item_id: messageId,
|
|
915
|
+
output_index: messageOutputIndex,
|
|
916
|
+
part: {
|
|
917
|
+
annotations: [],
|
|
918
|
+
text,
|
|
919
|
+
type: "output_text"
|
|
920
|
+
},
|
|
921
|
+
type: "response.content_part.done"
|
|
922
|
+
});
|
|
923
|
+
enqueue("response.output_item.done", {
|
|
794
924
|
item,
|
|
795
|
-
output_index:
|
|
796
|
-
type: "response.output_item.
|
|
925
|
+
output_index: messageOutputIndex,
|
|
926
|
+
type: "response.output_item.done"
|
|
797
927
|
});
|
|
928
|
+
}
|
|
929
|
+
for (const tool of [...tools.values()].sort(
|
|
930
|
+
(a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
|
|
931
|
+
)) {
|
|
932
|
+
const item = functionCallItem(tool);
|
|
933
|
+
const outputIndex = tool.outputIndex ?? 0;
|
|
934
|
+
outputEntries.push([outputIndex, item]);
|
|
798
935
|
enqueue("response.function_call_arguments.done", {
|
|
799
|
-
arguments:
|
|
936
|
+
arguments: tool.arguments,
|
|
800
937
|
item_id: item.id,
|
|
801
938
|
output_index: outputIndex,
|
|
802
939
|
type: "response.function_call_arguments.done"
|
|
@@ -806,7 +943,8 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
806
943
|
output_index: outputIndex,
|
|
807
944
|
type: "response.output_item.done"
|
|
808
945
|
});
|
|
809
|
-
}
|
|
946
|
+
}
|
|
947
|
+
const output = outputEntries.sort(([left], [right]) => left - right).map(([, item]) => item);
|
|
810
948
|
enqueue("response.completed", {
|
|
811
949
|
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
812
950
|
type: "response.completed"
|
|
@@ -989,13 +1127,13 @@ function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
|
989
1127
|
type: "message"
|
|
990
1128
|
};
|
|
991
1129
|
}
|
|
992
|
-
function functionCallItem(tool) {
|
|
1130
|
+
function functionCallItem(tool, status = "completed") {
|
|
993
1131
|
return {
|
|
994
1132
|
arguments: tool.arguments,
|
|
995
1133
|
call_id: tool.id,
|
|
996
|
-
id: `fc_${randomId()}`,
|
|
1134
|
+
id: tool.itemId ?? `fc_${randomId()}`,
|
|
997
1135
|
name: tool.name,
|
|
998
|
-
status
|
|
1136
|
+
status,
|
|
999
1137
|
type: "function_call"
|
|
1000
1138
|
};
|
|
1001
1139
|
}
|
|
@@ -1010,14 +1148,27 @@ function responseUsage(usage) {
|
|
|
1010
1148
|
if (Object.keys(record).length === 0) {
|
|
1011
1149
|
return null;
|
|
1012
1150
|
}
|
|
1151
|
+
const inputTokens = record.prompt_tokens;
|
|
1152
|
+
const outputTokens = record.completion_tokens;
|
|
1013
1153
|
return removeUndefined({
|
|
1014
|
-
input_tokens:
|
|
1015
|
-
input_tokens_details: record.prompt_tokens_details,
|
|
1016
|
-
|
|
1017
|
-
|
|
1154
|
+
input_tokens: inputTokens,
|
|
1155
|
+
input_tokens_details: responseUsageDetails(record.prompt_tokens_details, inputTokens, {
|
|
1156
|
+
cached_tokens: 0
|
|
1157
|
+
}),
|
|
1158
|
+
output_tokens: outputTokens,
|
|
1159
|
+
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
1160
|
+
reasoning_tokens: 0
|
|
1161
|
+
}),
|
|
1018
1162
|
total_tokens: record.total_tokens
|
|
1019
1163
|
});
|
|
1020
1164
|
}
|
|
1165
|
+
function responseUsageDetails(value, tokenCount, fallback) {
|
|
1166
|
+
const record = asRecord(value);
|
|
1167
|
+
if (Object.keys(record).length > 0) {
|
|
1168
|
+
return record;
|
|
1169
|
+
}
|
|
1170
|
+
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
1171
|
+
}
|
|
1021
1172
|
function extractTokenUsage(usage) {
|
|
1022
1173
|
const record = asRecord(usage);
|
|
1023
1174
|
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
@@ -1056,7 +1207,52 @@ function firstChoice(completion) {
|
|
|
1056
1207
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
1057
1208
|
return asRecord(choices[0]);
|
|
1058
1209
|
}
|
|
1059
|
-
function
|
|
1210
|
+
function processCompletionSseLine(line, enqueue, markDone) {
|
|
1211
|
+
const trimmed = line.trim();
|
|
1212
|
+
if (!trimmed.startsWith("data:")) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const data = trimmed.slice("data:".length).trim();
|
|
1216
|
+
if (!data) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (data === "[DONE]") {
|
|
1220
|
+
markDone();
|
|
1221
|
+
enqueue("[DONE]");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const parsed = parseJson(data);
|
|
1225
|
+
if (!parsed) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const choice = firstChoice(parsed);
|
|
1229
|
+
const delta = asRecord(choice.delta);
|
|
1230
|
+
const text = contentToText(delta.content);
|
|
1231
|
+
const finishReason = choice.finish_reason ?? null;
|
|
1232
|
+
const usage = asRecord(parsed.usage);
|
|
1233
|
+
const hasUsage = Object.keys(usage).length > 0;
|
|
1234
|
+
if (!text && finishReason === null && !hasUsage) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
enqueue(
|
|
1238
|
+
removeUndefined({
|
|
1239
|
+
choices: text || finishReason !== null ? [
|
|
1240
|
+
{
|
|
1241
|
+
finish_reason: finishReason,
|
|
1242
|
+
index: typeof choice.index === "number" ? choice.index : 0,
|
|
1243
|
+
logprobs: null,
|
|
1244
|
+
text
|
|
1245
|
+
}
|
|
1246
|
+
] : [],
|
|
1247
|
+
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
1248
|
+
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
1249
|
+
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
1250
|
+
object: "text_completion",
|
|
1251
|
+
usage: hasUsage ? usage : void 0
|
|
1252
|
+
})
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
function processChatSseLine(line, handlers) {
|
|
1060
1256
|
const trimmed = line.trim();
|
|
1061
1257
|
if (!trimmed.startsWith("data:")) {
|
|
1062
1258
|
return;
|
|
@@ -1073,30 +1269,11 @@ function processChatSseLine(messageId, line, enqueue, tools, appendText) {
|
|
|
1073
1269
|
const delta = asRecord(choice.delta);
|
|
1074
1270
|
const content = contentToText(delta.content);
|
|
1075
1271
|
if (content) {
|
|
1076
|
-
appendText(content);
|
|
1077
|
-
enqueue("response.output_text.delta", {
|
|
1078
|
-
content_index: 0,
|
|
1079
|
-
delta: content,
|
|
1080
|
-
item_id: messageId,
|
|
1081
|
-
output_index: 0,
|
|
1082
|
-
type: "response.output_text.delta"
|
|
1083
|
-
});
|
|
1272
|
+
handlers.appendText(content);
|
|
1084
1273
|
}
|
|
1085
1274
|
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
1086
1275
|
for (const toolCall of toolCalls) {
|
|
1087
|
-
|
|
1088
|
-
const fn = asRecord(record.function);
|
|
1089
|
-
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
1090
|
-
const existing = tools.get(index) ?? {
|
|
1091
|
-
arguments: "",
|
|
1092
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
1093
|
-
index,
|
|
1094
|
-
name: ""
|
|
1095
|
-
};
|
|
1096
|
-
existing.id = contentToText(record.id) || existing.id;
|
|
1097
|
-
existing.name += contentToText(fn.name);
|
|
1098
|
-
existing.arguments += contentToText(fn.arguments);
|
|
1099
|
-
tools.set(index, existing);
|
|
1276
|
+
handlers.appendToolCall(asRecord(toolCall));
|
|
1100
1277
|
}
|
|
1101
1278
|
}
|
|
1102
1279
|
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
@@ -1126,6 +1303,14 @@ function encodeSse(event, data) {
|
|
|
1126
1303
|
return `event: ${event}
|
|
1127
1304
|
data: ${JSON.stringify(data)}
|
|
1128
1305
|
|
|
1306
|
+
`;
|
|
1307
|
+
}
|
|
1308
|
+
function encodeDataSse(data) {
|
|
1309
|
+
if (data === "[DONE]") {
|
|
1310
|
+
return "data: [DONE]\n\n";
|
|
1311
|
+
}
|
|
1312
|
+
return `data: ${JSON.stringify(data)}
|
|
1313
|
+
|
|
1129
1314
|
`;
|
|
1130
1315
|
}
|
|
1131
1316
|
function parseJson(data) {
|
|
@@ -1514,6 +1699,7 @@ function formatNumber(value) {
|
|
|
1514
1699
|
// src/server.ts
|
|
1515
1700
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1516
1701
|
var DEFAULT_PORT = 4141;
|
|
1702
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1517
1703
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1518
1704
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1519
1705
|
function createHoopilotHandler(options = {}) {
|
|
@@ -1544,6 +1730,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1544
1730
|
route,
|
|
1545
1731
|
startedAt
|
|
1546
1732
|
});
|
|
1733
|
+
const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
|
|
1734
|
+
if (browserOrigin) {
|
|
1735
|
+
requestLogger.warn(
|
|
1736
|
+
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
1737
|
+
"blocked unauthenticated browser-origin request"
|
|
1738
|
+
);
|
|
1739
|
+
return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
|
|
1740
|
+
}
|
|
1547
1741
|
if (request.method === "OPTIONS") {
|
|
1548
1742
|
return finish(new Response(null, { headers: corsHeaders() }));
|
|
1549
1743
|
}
|
|
@@ -1595,6 +1789,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1595
1789
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1596
1790
|
"request body was invalid json"
|
|
1597
1791
|
);
|
|
1792
|
+
return finish(jsonError(400, "invalid_request_error", message));
|
|
1598
1793
|
} else {
|
|
1599
1794
|
requestLogger.error(
|
|
1600
1795
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
@@ -1607,7 +1802,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1607
1802
|
}
|
|
1608
1803
|
function startHoopilotServer(options = {}) {
|
|
1609
1804
|
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1610
|
-
const port =
|
|
1805
|
+
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1611
1806
|
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1612
1807
|
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1613
1808
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
@@ -1673,8 +1868,19 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
|
|
|
1673
1868
|
}
|
|
1674
1869
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
1675
1870
|
const model = normalizeRequestedModel(body.model);
|
|
1676
|
-
if (isStreamingResponse(upstream)) {
|
|
1677
|
-
return proxyResponse(
|
|
1871
|
+
if (isStreamingResponse(upstream) && upstream.body) {
|
|
1872
|
+
return proxyResponse(
|
|
1873
|
+
observeResponseUsage(
|
|
1874
|
+
new Response(completionStreamFromChatStream(upstream.body), {
|
|
1875
|
+
headers: upstream.headers,
|
|
1876
|
+
status: upstream.status,
|
|
1877
|
+
statusText: upstream.statusText
|
|
1878
|
+
}),
|
|
1879
|
+
model,
|
|
1880
|
+
recordTokens,
|
|
1881
|
+
request.signal
|
|
1882
|
+
)
|
|
1883
|
+
);
|
|
1678
1884
|
}
|
|
1679
1885
|
const completion = asRecord(await upstream.json());
|
|
1680
1886
|
const usage = extractTokenUsage(completion.usage);
|
|
@@ -1708,7 +1914,7 @@ async function proxyError(upstream, logger) {
|
|
|
1708
1914
|
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1709
1915
|
"copilot upstream request failed"
|
|
1710
1916
|
);
|
|
1711
|
-
return
|
|
1917
|
+
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
1712
1918
|
}
|
|
1713
1919
|
function proxyResponse(upstream) {
|
|
1714
1920
|
const headers = new Headers(upstream.headers);
|
|
@@ -1761,6 +1967,13 @@ function jsonError(status, code, message) {
|
|
|
1761
1967
|
status
|
|
1762
1968
|
);
|
|
1763
1969
|
}
|
|
1970
|
+
function upstreamErrorResponse(status, text) {
|
|
1971
|
+
const parsedError = asRecord(asRecord(safeParseJson(text)).error);
|
|
1972
|
+
if (Object.keys(parsedError).length > 0) {
|
|
1973
|
+
return jsonResponse({ error: parsedError }, status);
|
|
1974
|
+
}
|
|
1975
|
+
return jsonError(status, "copilot_error", text);
|
|
1976
|
+
}
|
|
1764
1977
|
function websocketUnsupportedResponse() {
|
|
1765
1978
|
const response = jsonError(
|
|
1766
1979
|
426,
|
|
@@ -1785,6 +1998,17 @@ function isAuthorized(request, apiKey) {
|
|
|
1785
1998
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1786
1999
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1787
2000
|
}
|
|
2001
|
+
function forbiddenBrowserOrigin(request, apiKey) {
|
|
2002
|
+
if (apiKey) {
|
|
2003
|
+
return void 0;
|
|
2004
|
+
}
|
|
2005
|
+
const origin = request.headers.get("origin")?.trim();
|
|
2006
|
+
if (origin) {
|
|
2007
|
+
return isLoopbackOrigin(origin) ? void 0 : origin;
|
|
2008
|
+
}
|
|
2009
|
+
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
2010
|
+
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
2011
|
+
}
|
|
1788
2012
|
function isUpstreamAuthStatus(status) {
|
|
1789
2013
|
return status === 401 || status === 403;
|
|
1790
2014
|
}
|
|
@@ -1792,7 +2016,21 @@ function upstreamAuthMessage(message) {
|
|
|
1792
2016
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1793
2017
|
}
|
|
1794
2018
|
function isLoopbackHost(host) {
|
|
1795
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
2019
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
2020
|
+
}
|
|
2021
|
+
function isLoopbackOrigin(origin) {
|
|
2022
|
+
try {
|
|
2023
|
+
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
2024
|
+
} catch {
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function normalizeServerPort(value) {
|
|
2029
|
+
const port = Number(value);
|
|
2030
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
2031
|
+
throw new Error(`Invalid port: ${value}.`);
|
|
2032
|
+
}
|
|
2033
|
+
return port;
|
|
1796
2034
|
}
|
|
1797
2035
|
function errorMessage(error) {
|
|
1798
2036
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2017,6 +2255,7 @@ function safeParseJson(text) {
|
|
|
2017
2255
|
authStorePath,
|
|
2018
2256
|
chatCompletionToCompletion,
|
|
2019
2257
|
chatCompletionToResponse,
|
|
2258
|
+
completionStreamFromChatStream,
|
|
2020
2259
|
completionsRequestToChatCompletion,
|
|
2021
2260
|
createHoopilotHandler,
|
|
2022
2261
|
createHoopilotLogger,
|