@openhoo/hoopilot 0.2.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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/cli.js +1162 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1078 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +1038 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CopilotAuth: () => CopilotAuth,
|
|
24
|
+
CopilotAuthError: () => CopilotAuthError,
|
|
25
|
+
CopilotClient: () => CopilotClient,
|
|
26
|
+
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
27
|
+
chatCompletionToCompletion: () => chatCompletionToCompletion,
|
|
28
|
+
chatCompletionToResponse: () => chatCompletionToResponse,
|
|
29
|
+
completionsRequestToChatCompletion: () => completionsRequestToChatCompletion,
|
|
30
|
+
createHoopilotHandler: () => createHoopilotHandler,
|
|
31
|
+
fallbackModels: () => fallbackModels,
|
|
32
|
+
normalizeModelsResponse: () => normalizeModelsResponse,
|
|
33
|
+
responsesRequestToChatCompletion: () => responsesRequestToChatCompletion,
|
|
34
|
+
responsesStreamFromChatStream: () => responsesStreamFromChatStream,
|
|
35
|
+
splitCommand: () => splitCommand,
|
|
36
|
+
startHoopilotServer: () => startHoopilotServer
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/auth.ts
|
|
41
|
+
var import_node_child_process = require("child_process");
|
|
42
|
+
var DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
|
|
43
|
+
var DEFAULT_TOKEN_EXCHANGE_URL = "https://api.github.com/copilot_internal/v2/token";
|
|
44
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
45
|
+
var defaultLogger = {
|
|
46
|
+
info: () => void 0,
|
|
47
|
+
warn: () => void 0,
|
|
48
|
+
error: () => void 0
|
|
49
|
+
};
|
|
50
|
+
var CopilotAuthError = class extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "CopilotAuthError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var CopilotAuth = class {
|
|
57
|
+
#authMode;
|
|
58
|
+
#copilotApiBaseUrl;
|
|
59
|
+
#copilotToken;
|
|
60
|
+
#env;
|
|
61
|
+
#fetch;
|
|
62
|
+
#githubToken;
|
|
63
|
+
#githubTokenCommand;
|
|
64
|
+
#logger;
|
|
65
|
+
#tokenExchangeUrl;
|
|
66
|
+
#cachedAccess;
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.#authMode = options.authMode ?? "auto";
|
|
69
|
+
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
70
|
+
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
71
|
+
);
|
|
72
|
+
this.#copilotToken = options.copilotToken;
|
|
73
|
+
this.#env = options.env ?? process.env;
|
|
74
|
+
this.#fetch = options.fetch ?? fetch;
|
|
75
|
+
this.#githubToken = options.githubToken;
|
|
76
|
+
this.#githubTokenCommand = options.githubTokenCommand ?? "gh auth token";
|
|
77
|
+
this.#logger = options.logger ?? defaultLogger;
|
|
78
|
+
this.#tokenExchangeUrl = options.tokenExchangeUrl ?? options.env?.COPILOT_TOKEN_EXCHANGE_URL ?? DEFAULT_TOKEN_EXCHANGE_URL;
|
|
79
|
+
}
|
|
80
|
+
async getAccess() {
|
|
81
|
+
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
82
|
+
return this.#cachedAccess;
|
|
83
|
+
}
|
|
84
|
+
const directCopilotToken = this.#resolveDirectCopilotToken();
|
|
85
|
+
if (this.#authMode === "copilot-token") {
|
|
86
|
+
if (!directCopilotToken) {
|
|
87
|
+
throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
|
|
88
|
+
}
|
|
89
|
+
return this.#cacheAccess({
|
|
90
|
+
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
91
|
+
expiresAtMs: Date.now() + 10 * 6e4,
|
|
92
|
+
source: "copilot-token",
|
|
93
|
+
token: directCopilotToken
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (directCopilotToken) {
|
|
97
|
+
return this.#cacheAccess({
|
|
98
|
+
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
99
|
+
expiresAtMs: Date.now() + 10 * 6e4,
|
|
100
|
+
source: "copilot-token",
|
|
101
|
+
token: directCopilotToken
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const githubToken = this.#resolveGithubToken();
|
|
105
|
+
if (!githubToken) {
|
|
106
|
+
throw new CopilotAuthError(
|
|
107
|
+
"No Copilot credential found. Set COPILOT_GITHUB_TOKEN, GITHUB_TOKEN, or sign in with gh auth login."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (this.#authMode === "direct-github-token") {
|
|
111
|
+
return this.#cacheAccess({
|
|
112
|
+
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
113
|
+
expiresAtMs: Date.now() + 10 * 6e4,
|
|
114
|
+
source: "direct-github-token",
|
|
115
|
+
token: githubToken
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const exchanged = await this.#exchangeGithubToken(githubToken);
|
|
120
|
+
return this.#cacheAccess(exchanged);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (this.#authMode === "github-token") {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
this.#logger.warn(
|
|
126
|
+
`Copilot token exchange failed; falling back to direct GitHub token mode: ${errorMessage(error)}`
|
|
127
|
+
);
|
|
128
|
+
return this.#cacheAccess({
|
|
129
|
+
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
130
|
+
expiresAtMs: Date.now() + 10 * 6e4,
|
|
131
|
+
source: "direct-github-token",
|
|
132
|
+
token: githubToken
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
#cacheAccess(access) {
|
|
137
|
+
this.#cachedAccess = access;
|
|
138
|
+
return access;
|
|
139
|
+
}
|
|
140
|
+
async #exchangeGithubToken(githubToken) {
|
|
141
|
+
const response = await this.#fetch(this.#tokenExchangeUrl, {
|
|
142
|
+
headers: {
|
|
143
|
+
accept: "application/vnd.github+json",
|
|
144
|
+
authorization: `token ${githubToken}`,
|
|
145
|
+
"editor-plugin-version": "hoopilot/0.1.0",
|
|
146
|
+
"editor-version": "Hoopilot/0.1.0",
|
|
147
|
+
"user-agent": "hoopilot/0.1.0"
|
|
148
|
+
},
|
|
149
|
+
method: "GET"
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new CopilotAuthError(
|
|
153
|
+
`GitHub Copilot token exchange failed with ${response.status}: ${await safeResponseText(
|
|
154
|
+
response
|
|
155
|
+
)}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const body = asRecord(await response.json());
|
|
159
|
+
const token = getString(body, "token");
|
|
160
|
+
if (!token) {
|
|
161
|
+
throw new CopilotAuthError("GitHub Copilot token exchange response did not include a token.");
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
apiBaseUrl: endpointFromResponse(body) ?? this.#copilotApiBaseUrl,
|
|
165
|
+
expiresAtMs: expiresAtFromResponse(body),
|
|
166
|
+
source: "github-token",
|
|
167
|
+
token
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
#resolveDirectCopilotToken() {
|
|
171
|
+
return firstNonEmpty(
|
|
172
|
+
this.#copilotToken,
|
|
173
|
+
this.#env.COPILOT_API_TOKEN,
|
|
174
|
+
this.#env.GITHUB_COPILOT_API_TOKEN,
|
|
175
|
+
this.#env.GITHUB_COPILOT_TOKEN
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
#resolveGithubToken() {
|
|
179
|
+
return firstNonEmpty(
|
|
180
|
+
this.#githubToken,
|
|
181
|
+
this.#env.COPILOT_GITHUB_TOKEN,
|
|
182
|
+
this.#env.GITHUB_COPILOT_GITHUB_TOKEN,
|
|
183
|
+
this.#env.GH_TOKEN,
|
|
184
|
+
this.#env.GITHUB_TOKEN,
|
|
185
|
+
this.#readGithubTokenCommand()
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
#readGithubTokenCommand() {
|
|
189
|
+
if (this.#githubTokenCommand === false) {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
const parts = splitCommand(this.#githubTokenCommand);
|
|
193
|
+
const [command, ...args] = parts;
|
|
194
|
+
if (!command) {
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const output = (0, import_node_child_process.execFileSync)(command, args, {
|
|
199
|
+
encoding: "utf8",
|
|
200
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
201
|
+
timeout: 5e3
|
|
202
|
+
});
|
|
203
|
+
return output.trim() || void 0;
|
|
204
|
+
} catch {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
function splitCommand(command) {
|
|
210
|
+
const parts = [];
|
|
211
|
+
let current = "";
|
|
212
|
+
let quote;
|
|
213
|
+
let escaping = false;
|
|
214
|
+
for (const character of command.trim()) {
|
|
215
|
+
if (escaping) {
|
|
216
|
+
current += character;
|
|
217
|
+
escaping = false;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (character === "\\") {
|
|
221
|
+
escaping = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (quote) {
|
|
225
|
+
if (character === quote) {
|
|
226
|
+
quote = void 0;
|
|
227
|
+
} else {
|
|
228
|
+
current += character;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (character === "'" || character === '"') {
|
|
233
|
+
quote = character;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (/\s/.test(character)) {
|
|
237
|
+
if (current) {
|
|
238
|
+
parts.push(current);
|
|
239
|
+
current = "";
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
current += character;
|
|
244
|
+
}
|
|
245
|
+
if (current) {
|
|
246
|
+
parts.push(current);
|
|
247
|
+
}
|
|
248
|
+
return parts;
|
|
249
|
+
}
|
|
250
|
+
function endpointFromResponse(body) {
|
|
251
|
+
const endpoints = asRecord(body.endpoints);
|
|
252
|
+
const apiUrl = getString(endpoints, "api") ?? getString(endpoints, "proxy");
|
|
253
|
+
return apiUrl ? trimTrailingSlash(apiUrl) : void 0;
|
|
254
|
+
}
|
|
255
|
+
function expiresAtFromResponse(body) {
|
|
256
|
+
const expiresAt = body.expires_at;
|
|
257
|
+
if (typeof expiresAt === "number") {
|
|
258
|
+
return expiresAt < 1e10 ? expiresAt * 1e3 : expiresAt;
|
|
259
|
+
}
|
|
260
|
+
if (typeof expiresAt === "string") {
|
|
261
|
+
const asNumber = Number(expiresAt);
|
|
262
|
+
if (Number.isFinite(asNumber)) {
|
|
263
|
+
return asNumber < 1e10 ? asNumber * 1e3 : asNumber;
|
|
264
|
+
}
|
|
265
|
+
const parsed = Date.parse(expiresAt);
|
|
266
|
+
if (Number.isFinite(parsed)) {
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const refreshIn = body.refresh_in;
|
|
271
|
+
if (typeof refreshIn === "number" && Number.isFinite(refreshIn)) {
|
|
272
|
+
return Date.now() + refreshIn * 1e3;
|
|
273
|
+
}
|
|
274
|
+
return Date.now() + 10 * 6e4;
|
|
275
|
+
}
|
|
276
|
+
function firstNonEmpty(...values) {
|
|
277
|
+
for (const value of values) {
|
|
278
|
+
const trimmed = value?.trim();
|
|
279
|
+
if (trimmed) {
|
|
280
|
+
return trimmed;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
function asRecord(value) {
|
|
286
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
287
|
+
}
|
|
288
|
+
function getString(record, key) {
|
|
289
|
+
const value = record[key];
|
|
290
|
+
return typeof value === "string" && value ? value : void 0;
|
|
291
|
+
}
|
|
292
|
+
function trimTrailingSlash(value) {
|
|
293
|
+
return value.replace(/\/+$/, "");
|
|
294
|
+
}
|
|
295
|
+
async function safeResponseText(response) {
|
|
296
|
+
const text = await response.text();
|
|
297
|
+
return text.slice(0, 500);
|
|
298
|
+
}
|
|
299
|
+
function errorMessage(error) {
|
|
300
|
+
return error instanceof Error ? error.message : String(error);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/copilot.ts
|
|
304
|
+
var CopilotClient = class {
|
|
305
|
+
#auth;
|
|
306
|
+
#fetch;
|
|
307
|
+
constructor(options = {}) {
|
|
308
|
+
this.#auth = new CopilotAuth(options);
|
|
309
|
+
this.#fetch = options.fetch ?? fetch;
|
|
310
|
+
}
|
|
311
|
+
async chatCompletions(body, signal) {
|
|
312
|
+
return this.fetchCopilot("/chat/completions", {
|
|
313
|
+
body: JSON.stringify(body),
|
|
314
|
+
headers: {
|
|
315
|
+
"content-type": "application/json"
|
|
316
|
+
},
|
|
317
|
+
method: "POST",
|
|
318
|
+
signal
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async forwardChatCompletions(body, signal) {
|
|
322
|
+
return this.fetchCopilot("/chat/completions", {
|
|
323
|
+
body,
|
|
324
|
+
headers: {
|
|
325
|
+
"content-type": "application/json"
|
|
326
|
+
},
|
|
327
|
+
method: "POST",
|
|
328
|
+
signal
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async models(signal) {
|
|
332
|
+
return this.fetchCopilot("/models", {
|
|
333
|
+
headers: {
|
|
334
|
+
accept: "application/json"
|
|
335
|
+
},
|
|
336
|
+
method: "GET",
|
|
337
|
+
signal
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async fetchCopilot(path, init) {
|
|
341
|
+
const access = await this.#auth.getAccess();
|
|
342
|
+
const headers = new Headers(init.headers);
|
|
343
|
+
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
344
|
+
headers.set("authorization", `Bearer ${access.token}`);
|
|
345
|
+
headers.set("copilot-integration-id", "vscode-chat");
|
|
346
|
+
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
347
|
+
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
348
|
+
headers.set("openai-intent", "conversation-panel");
|
|
349
|
+
headers.set("user-agent", "hoopilot/0.1.0");
|
|
350
|
+
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
351
|
+
...init,
|
|
352
|
+
headers
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/openai.ts
|
|
358
|
+
var DEFAULT_MODEL = "gpt-4.1";
|
|
359
|
+
function responsesRequestToChatCompletion(request) {
|
|
360
|
+
const messages = [];
|
|
361
|
+
const instructions = contentToText(request.instructions);
|
|
362
|
+
if (instructions) {
|
|
363
|
+
messages.push({ content: instructions, role: "system" });
|
|
364
|
+
}
|
|
365
|
+
for (const message of inputToMessages(request.input)) {
|
|
366
|
+
messages.push(message);
|
|
367
|
+
}
|
|
368
|
+
return removeUndefined({
|
|
369
|
+
frequency_penalty: request.frequency_penalty,
|
|
370
|
+
max_tokens: request.max_output_tokens ?? request.max_tokens,
|
|
371
|
+
messages,
|
|
372
|
+
metadata: request.metadata,
|
|
373
|
+
model: contentToText(request.model) || DEFAULT_MODEL,
|
|
374
|
+
presence_penalty: request.presence_penalty,
|
|
375
|
+
reasoning_effort: asRecord2(request.reasoning).effort,
|
|
376
|
+
response_format: asRecord2(request.text).format,
|
|
377
|
+
seed: request.seed,
|
|
378
|
+
stream: request.stream === true,
|
|
379
|
+
temperature: request.temperature,
|
|
380
|
+
tool_choice: chatToolChoice(request.tool_choice),
|
|
381
|
+
tools: chatTools(request.tools),
|
|
382
|
+
top_p: request.top_p
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function completionsRequestToChatCompletion(request) {
|
|
386
|
+
return removeUndefined({
|
|
387
|
+
max_tokens: request.max_tokens,
|
|
388
|
+
messages: [{ content: promptToText(request.prompt), role: "user" }],
|
|
389
|
+
model: contentToText(request.model) || DEFAULT_MODEL,
|
|
390
|
+
stream: request.stream === true,
|
|
391
|
+
temperature: request.temperature,
|
|
392
|
+
top_p: request.top_p
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function chatCompletionToResponse(completion, responseId) {
|
|
396
|
+
const id = responseId ?? `resp_${randomId()}`;
|
|
397
|
+
const choice = firstChoice(completion);
|
|
398
|
+
const message = asRecord2(choice.message);
|
|
399
|
+
const model = contentToText(completion.model) || DEFAULT_MODEL;
|
|
400
|
+
const output = outputItemsFromMessage(message);
|
|
401
|
+
const usage = responseUsage(completion.usage);
|
|
402
|
+
return removeUndefined({
|
|
403
|
+
created_at: epochSeconds(),
|
|
404
|
+
error: null,
|
|
405
|
+
id,
|
|
406
|
+
incomplete_details: null,
|
|
407
|
+
instructions: null,
|
|
408
|
+
max_output_tokens: null,
|
|
409
|
+
metadata: {},
|
|
410
|
+
model,
|
|
411
|
+
object: "response",
|
|
412
|
+
output,
|
|
413
|
+
output_text: outputText(output),
|
|
414
|
+
parallel_tool_calls: true,
|
|
415
|
+
status: "completed",
|
|
416
|
+
temperature: null,
|
|
417
|
+
tool_choice: "auto",
|
|
418
|
+
tools: [],
|
|
419
|
+
top_p: null,
|
|
420
|
+
usage
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function chatCompletionToCompletion(completion) {
|
|
424
|
+
const choice = firstChoice(completion);
|
|
425
|
+
const message = asRecord2(choice.message);
|
|
426
|
+
return removeUndefined({
|
|
427
|
+
choices: [
|
|
428
|
+
{
|
|
429
|
+
finish_reason: choice.finish_reason ?? "stop",
|
|
430
|
+
index: 0,
|
|
431
|
+
logprobs: null,
|
|
432
|
+
text: contentToText(message.content)
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
created: completion.created ?? epochSeconds(),
|
|
436
|
+
id: completion.id ?? `cmpl_${randomId()}`,
|
|
437
|
+
model: completion.model ?? DEFAULT_MODEL,
|
|
438
|
+
object: "text_completion",
|
|
439
|
+
usage: completion.usage
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function normalizeModelsResponse(upstream) {
|
|
443
|
+
const record = asRecord2(upstream);
|
|
444
|
+
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
445
|
+
const models = data.map((model) => asRecord2(model)).filter((model) => typeof model.id === "string").map((model) => ({
|
|
446
|
+
created: model.created ?? 0,
|
|
447
|
+
id: model.id,
|
|
448
|
+
object: "model",
|
|
449
|
+
owned_by: model.owned_by ?? "github-copilot"
|
|
450
|
+
}));
|
|
451
|
+
return {
|
|
452
|
+
data: models.length > 0 ? models : fallbackModels(),
|
|
453
|
+
object: "list"
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function fallbackModels() {
|
|
457
|
+
return [
|
|
458
|
+
{
|
|
459
|
+
created: 0,
|
|
460
|
+
id: DEFAULT_MODEL,
|
|
461
|
+
object: "model",
|
|
462
|
+
owned_by: "github-copilot"
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
function responsesStreamFromChatStream(chatStream, options) {
|
|
467
|
+
const encoder = new TextEncoder();
|
|
468
|
+
const decoder = new TextDecoder();
|
|
469
|
+
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
470
|
+
const messageId = `msg_${randomId()}`;
|
|
471
|
+
const createdAt = epochSeconds();
|
|
472
|
+
let buffer = "";
|
|
473
|
+
let text = "";
|
|
474
|
+
const tools = /* @__PURE__ */ new Map();
|
|
475
|
+
return new ReadableStream({
|
|
476
|
+
async start(controller) {
|
|
477
|
+
const enqueue = (event, data) => {
|
|
478
|
+
controller.enqueue(encoder.encode(encodeSse(event, data)));
|
|
479
|
+
};
|
|
480
|
+
enqueue("response.created", {
|
|
481
|
+
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
482
|
+
type: "response.created"
|
|
483
|
+
});
|
|
484
|
+
enqueue("response.output_item.added", {
|
|
485
|
+
item: {
|
|
486
|
+
content: [],
|
|
487
|
+
id: messageId,
|
|
488
|
+
role: "assistant",
|
|
489
|
+
status: "in_progress",
|
|
490
|
+
type: "message"
|
|
491
|
+
},
|
|
492
|
+
output_index: 0,
|
|
493
|
+
type: "response.output_item.added"
|
|
494
|
+
});
|
|
495
|
+
enqueue("response.content_part.added", {
|
|
496
|
+
content_index: 0,
|
|
497
|
+
item_id: messageId,
|
|
498
|
+
output_index: 0,
|
|
499
|
+
part: {
|
|
500
|
+
annotations: [],
|
|
501
|
+
text: "",
|
|
502
|
+
type: "output_text"
|
|
503
|
+
},
|
|
504
|
+
type: "response.content_part.added"
|
|
505
|
+
});
|
|
506
|
+
const reader = chatStream.getReader();
|
|
507
|
+
try {
|
|
508
|
+
while (true) {
|
|
509
|
+
const result = await reader.read();
|
|
510
|
+
if (result.done) {
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
514
|
+
const lines = buffer.split(/\r?\n/);
|
|
515
|
+
buffer = lines.pop() ?? "";
|
|
516
|
+
for (const line of lines) {
|
|
517
|
+
processChatSseLine(line, enqueue, tools, (delta) => {
|
|
518
|
+
text += delta;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (buffer) {
|
|
523
|
+
processChatSseLine(buffer, enqueue, tools, (delta) => {
|
|
524
|
+
text += delta;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const output = streamOutputItems(messageId, text, [...tools.values()]);
|
|
528
|
+
enqueue("response.output_text.done", {
|
|
529
|
+
content_index: 0,
|
|
530
|
+
item_id: messageId,
|
|
531
|
+
output_index: 0,
|
|
532
|
+
text,
|
|
533
|
+
type: "response.output_text.done"
|
|
534
|
+
});
|
|
535
|
+
enqueue("response.content_part.done", {
|
|
536
|
+
content_index: 0,
|
|
537
|
+
item_id: messageId,
|
|
538
|
+
output_index: 0,
|
|
539
|
+
part: {
|
|
540
|
+
annotations: [],
|
|
541
|
+
text,
|
|
542
|
+
type: "output_text"
|
|
543
|
+
},
|
|
544
|
+
type: "response.content_part.done"
|
|
545
|
+
});
|
|
546
|
+
enqueue("response.output_item.done", {
|
|
547
|
+
item: output[0],
|
|
548
|
+
output_index: 0,
|
|
549
|
+
type: "response.output_item.done"
|
|
550
|
+
});
|
|
551
|
+
tools.forEach((tool, index) => {
|
|
552
|
+
const item = functionCallItem(tool);
|
|
553
|
+
const outputIndex = index + 1;
|
|
554
|
+
enqueue("response.output_item.added", {
|
|
555
|
+
item,
|
|
556
|
+
output_index: outputIndex,
|
|
557
|
+
type: "response.output_item.added"
|
|
558
|
+
});
|
|
559
|
+
enqueue("response.function_call_arguments.done", {
|
|
560
|
+
arguments: tool.arguments,
|
|
561
|
+
item_id: item.id,
|
|
562
|
+
output_index: outputIndex,
|
|
563
|
+
type: "response.function_call_arguments.done"
|
|
564
|
+
});
|
|
565
|
+
enqueue("response.output_item.done", {
|
|
566
|
+
item,
|
|
567
|
+
output_index: outputIndex,
|
|
568
|
+
type: "response.output_item.done"
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
enqueue("response.completed", {
|
|
572
|
+
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
573
|
+
type: "response.completed"
|
|
574
|
+
});
|
|
575
|
+
enqueue("done", "[DONE]");
|
|
576
|
+
controller.close();
|
|
577
|
+
} catch (error) {
|
|
578
|
+
controller.error(error);
|
|
579
|
+
} finally {
|
|
580
|
+
reader.releaseLock();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function inputToMessages(input) {
|
|
586
|
+
if (typeof input === "string") {
|
|
587
|
+
return [{ content: input, role: "user" }];
|
|
588
|
+
}
|
|
589
|
+
if (!Array.isArray(input)) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const messages = [];
|
|
593
|
+
for (const item of input) {
|
|
594
|
+
const record = asRecord2(item);
|
|
595
|
+
if (record.type === "function_call_output") {
|
|
596
|
+
messages.push({
|
|
597
|
+
content: contentToText(record.output),
|
|
598
|
+
role: "tool",
|
|
599
|
+
tool_call_id: contentToText(record.call_id)
|
|
600
|
+
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (record.type === "function_call") {
|
|
604
|
+
messages.push({
|
|
605
|
+
role: "assistant",
|
|
606
|
+
tool_calls: [
|
|
607
|
+
{
|
|
608
|
+
function: {
|
|
609
|
+
arguments: contentToText(record.arguments),
|
|
610
|
+
name: contentToText(record.name)
|
|
611
|
+
},
|
|
612
|
+
id: contentToText(record.call_id) || contentToText(record.id),
|
|
613
|
+
type: "function"
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const role = roleToChatRole(contentToText(record.role));
|
|
620
|
+
const content = chatMessageContent(record.content);
|
|
621
|
+
if (role && content !== void 0) {
|
|
622
|
+
messages.push({ content, role });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return messages;
|
|
626
|
+
}
|
|
627
|
+
function chatMessageContent(content) {
|
|
628
|
+
if (typeof content === "string") {
|
|
629
|
+
return content;
|
|
630
|
+
}
|
|
631
|
+
if (!Array.isArray(content)) {
|
|
632
|
+
return contentToText(content) || void 0;
|
|
633
|
+
}
|
|
634
|
+
const parts = [];
|
|
635
|
+
for (const part of content) {
|
|
636
|
+
const record = asRecord2(part);
|
|
637
|
+
const type = contentToText(record.type);
|
|
638
|
+
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
639
|
+
parts.push({ text: contentToText(record.text), type: "text" });
|
|
640
|
+
}
|
|
641
|
+
if (type === "input_image") {
|
|
642
|
+
const imageUrl = contentToText(record.image_url);
|
|
643
|
+
if (imageUrl) {
|
|
644
|
+
parts.push({ image_url: { url: imageUrl }, type: "image_url" });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (parts.length === 0) {
|
|
649
|
+
return void 0;
|
|
650
|
+
}
|
|
651
|
+
if (parts.every((part) => part.type === "text")) {
|
|
652
|
+
return parts.map((part) => contentToText(part.text)).join("\n");
|
|
653
|
+
}
|
|
654
|
+
return parts;
|
|
655
|
+
}
|
|
656
|
+
function promptToText(prompt) {
|
|
657
|
+
if (Array.isArray(prompt)) {
|
|
658
|
+
return prompt.map((item) => contentToText(item)).join("\n");
|
|
659
|
+
}
|
|
660
|
+
return contentToText(prompt);
|
|
661
|
+
}
|
|
662
|
+
function contentToText(content) {
|
|
663
|
+
if (typeof content === "string") {
|
|
664
|
+
return content;
|
|
665
|
+
}
|
|
666
|
+
if (typeof content === "number" || typeof content === "boolean") {
|
|
667
|
+
return String(content);
|
|
668
|
+
}
|
|
669
|
+
if (Array.isArray(content)) {
|
|
670
|
+
return content.map((item) => contentToText(item)).filter(Boolean).join("\n");
|
|
671
|
+
}
|
|
672
|
+
if (content && typeof content === "object") {
|
|
673
|
+
const record = content;
|
|
674
|
+
if (typeof record.text === "string") {
|
|
675
|
+
return record.text;
|
|
676
|
+
}
|
|
677
|
+
if (typeof record.output_text === "string") {
|
|
678
|
+
return record.output_text;
|
|
679
|
+
}
|
|
680
|
+
return JSON.stringify(content);
|
|
681
|
+
}
|
|
682
|
+
return "";
|
|
683
|
+
}
|
|
684
|
+
function roleToChatRole(role) {
|
|
685
|
+
if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
|
|
686
|
+
return role === "developer" ? "system" : role;
|
|
687
|
+
}
|
|
688
|
+
return "user";
|
|
689
|
+
}
|
|
690
|
+
function chatTools(tools) {
|
|
691
|
+
if (!Array.isArray(tools)) {
|
|
692
|
+
return void 0;
|
|
693
|
+
}
|
|
694
|
+
const converted = tools.map((tool) => asRecord2(tool)).filter((tool) => tool.type === "function").map((tool) => ({
|
|
695
|
+
function: removeUndefined({
|
|
696
|
+
description: tool.description,
|
|
697
|
+
name: tool.name,
|
|
698
|
+
parameters: tool.parameters,
|
|
699
|
+
strict: tool.strict
|
|
700
|
+
}),
|
|
701
|
+
type: "function"
|
|
702
|
+
}));
|
|
703
|
+
return converted.length > 0 ? converted : void 0;
|
|
704
|
+
}
|
|
705
|
+
function chatToolChoice(toolChoice) {
|
|
706
|
+
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
707
|
+
return toolChoice;
|
|
708
|
+
}
|
|
709
|
+
const record = asRecord2(toolChoice);
|
|
710
|
+
if (record.type === "function" && typeof record.name === "string") {
|
|
711
|
+
return { function: { name: record.name }, type: "function" };
|
|
712
|
+
}
|
|
713
|
+
return toolChoice;
|
|
714
|
+
}
|
|
715
|
+
function outputItemsFromMessage(message) {
|
|
716
|
+
const output = [];
|
|
717
|
+
const text = contentToText(message.content);
|
|
718
|
+
if (text) {
|
|
719
|
+
output.push(messageOutputItem(text));
|
|
720
|
+
}
|
|
721
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
722
|
+
for (const toolCall of toolCalls) {
|
|
723
|
+
const record = asRecord2(toolCall);
|
|
724
|
+
const fn = asRecord2(record.function);
|
|
725
|
+
output.push(
|
|
726
|
+
functionCallItem({
|
|
727
|
+
arguments: contentToText(fn.arguments),
|
|
728
|
+
id: contentToText(record.id) || `call_${randomId()}`,
|
|
729
|
+
index: output.length,
|
|
730
|
+
name: contentToText(fn.name)
|
|
731
|
+
})
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return output;
|
|
735
|
+
}
|
|
736
|
+
function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
737
|
+
return {
|
|
738
|
+
content: [
|
|
739
|
+
{
|
|
740
|
+
annotations: [],
|
|
741
|
+
text,
|
|
742
|
+
type: "output_text"
|
|
743
|
+
}
|
|
744
|
+
],
|
|
745
|
+
id,
|
|
746
|
+
role: "assistant",
|
|
747
|
+
status: "completed",
|
|
748
|
+
type: "message"
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function functionCallItem(tool) {
|
|
752
|
+
return {
|
|
753
|
+
arguments: tool.arguments,
|
|
754
|
+
call_id: tool.id,
|
|
755
|
+
id: `fc_${randomId()}`,
|
|
756
|
+
name: tool.name,
|
|
757
|
+
status: "completed",
|
|
758
|
+
type: "function_call"
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function outputText(output) {
|
|
762
|
+
return output.flatMap((item) => {
|
|
763
|
+
const content = item.content;
|
|
764
|
+
return Array.isArray(content) ? content : [];
|
|
765
|
+
}).map((part) => contentToText(asRecord2(part).text)).filter(Boolean).join("");
|
|
766
|
+
}
|
|
767
|
+
function responseUsage(usage) {
|
|
768
|
+
const record = asRecord2(usage);
|
|
769
|
+
if (Object.keys(record).length === 0) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
return removeUndefined({
|
|
773
|
+
input_tokens: record.prompt_tokens,
|
|
774
|
+
input_tokens_details: record.prompt_tokens_details,
|
|
775
|
+
output_tokens: record.completion_tokens,
|
|
776
|
+
output_tokens_details: record.completion_tokens_details,
|
|
777
|
+
total_tokens: record.total_tokens
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function firstChoice(completion) {
|
|
781
|
+
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
782
|
+
return asRecord2(choices[0]);
|
|
783
|
+
}
|
|
784
|
+
function processChatSseLine(line, enqueue, tools, appendText) {
|
|
785
|
+
const trimmed = line.trim();
|
|
786
|
+
if (!trimmed.startsWith("data:")) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const data = trimmed.slice("data:".length).trim();
|
|
790
|
+
if (!data || data === "[DONE]") {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const parsed = parseJson(data);
|
|
794
|
+
if (!parsed) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const choice = firstChoice(parsed);
|
|
798
|
+
const delta = asRecord2(choice.delta);
|
|
799
|
+
const content = contentToText(delta.content);
|
|
800
|
+
if (content) {
|
|
801
|
+
appendText(content);
|
|
802
|
+
enqueue("response.output_text.delta", {
|
|
803
|
+
content_index: 0,
|
|
804
|
+
delta: content,
|
|
805
|
+
item_id: "",
|
|
806
|
+
output_index: 0,
|
|
807
|
+
type: "response.output_text.delta"
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
811
|
+
for (const toolCall of toolCalls) {
|
|
812
|
+
const record = asRecord2(toolCall);
|
|
813
|
+
const fn = asRecord2(record.function);
|
|
814
|
+
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
815
|
+
const existing = tools.get(index) ?? {
|
|
816
|
+
arguments: "",
|
|
817
|
+
id: contentToText(record.id) || `call_${randomId()}`,
|
|
818
|
+
index,
|
|
819
|
+
name: ""
|
|
820
|
+
};
|
|
821
|
+
existing.id = contentToText(record.id) || existing.id;
|
|
822
|
+
existing.name += contentToText(fn.name);
|
|
823
|
+
existing.arguments += contentToText(fn.arguments);
|
|
824
|
+
tools.set(index, existing);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function streamOutputItems(messageId, text, tools) {
|
|
828
|
+
return [messageOutputItem(text, messageId), ...tools.map((tool) => functionCallItem(tool))];
|
|
829
|
+
}
|
|
830
|
+
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
831
|
+
return {
|
|
832
|
+
created_at: createdAt,
|
|
833
|
+
error: null,
|
|
834
|
+
id,
|
|
835
|
+
incomplete_details: null,
|
|
836
|
+
instructions: null,
|
|
837
|
+
max_output_tokens: null,
|
|
838
|
+
metadata: {},
|
|
839
|
+
model,
|
|
840
|
+
object: "response",
|
|
841
|
+
output,
|
|
842
|
+
parallel_tool_calls: true,
|
|
843
|
+
status,
|
|
844
|
+
temperature: null,
|
|
845
|
+
tool_choice: "auto",
|
|
846
|
+
tools: [],
|
|
847
|
+
top_p: null
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function encodeSse(event, data) {
|
|
851
|
+
if (data === "[DONE]") {
|
|
852
|
+
return "data: [DONE]\n\n";
|
|
853
|
+
}
|
|
854
|
+
return `event: ${event}
|
|
855
|
+
data: ${JSON.stringify(data)}
|
|
856
|
+
|
|
857
|
+
`;
|
|
858
|
+
}
|
|
859
|
+
function parseJson(data) {
|
|
860
|
+
try {
|
|
861
|
+
return asRecord2(JSON.parse(data));
|
|
862
|
+
} catch {
|
|
863
|
+
return void 0;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function removeUndefined(record) {
|
|
867
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
868
|
+
}
|
|
869
|
+
function asRecord2(value) {
|
|
870
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
871
|
+
}
|
|
872
|
+
function randomId() {
|
|
873
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
874
|
+
}
|
|
875
|
+
function epochSeconds() {
|
|
876
|
+
return Math.floor(Date.now() / 1e3);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/server.ts
|
|
880
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
881
|
+
var DEFAULT_PORT = 4141;
|
|
882
|
+
function createHoopilotHandler(options = {}) {
|
|
883
|
+
const client = new CopilotClient(options);
|
|
884
|
+
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
885
|
+
return async (request) => {
|
|
886
|
+
const url = new URL(request.url);
|
|
887
|
+
if (request.method === "OPTIONS") {
|
|
888
|
+
return new Response(null, { headers: corsHeaders() });
|
|
889
|
+
}
|
|
890
|
+
if (!isAuthorized(request, apiKey)) {
|
|
891
|
+
return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/healthz")) {
|
|
895
|
+
return jsonResponse({
|
|
896
|
+
name: "hoopilot",
|
|
897
|
+
object: "health",
|
|
898
|
+
status: "ok"
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if (request.method === "GET" && url.pathname === "/v1/models") {
|
|
902
|
+
return await handleModels(client, request.signal);
|
|
903
|
+
}
|
|
904
|
+
if (request.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
905
|
+
return await handleChatCompletions(client, request);
|
|
906
|
+
}
|
|
907
|
+
if (request.method === "POST" && url.pathname === "/v1/completions") {
|
|
908
|
+
return await handleCompletions(client, request);
|
|
909
|
+
}
|
|
910
|
+
if (request.method === "POST" && url.pathname === "/v1/responses") {
|
|
911
|
+
return await handleResponses(client, request);
|
|
912
|
+
}
|
|
913
|
+
return jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`);
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (error instanceof CopilotAuthError) {
|
|
916
|
+
return jsonError(401, "copilot_auth_error", error.message);
|
|
917
|
+
}
|
|
918
|
+
return jsonError(500, "internal_error", errorMessage2(error));
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function startHoopilotServer(options = {}) {
|
|
923
|
+
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
924
|
+
const port = Number(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
925
|
+
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
926
|
+
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
927
|
+
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const server = Bun.serve({
|
|
933
|
+
fetch: createHoopilotHandler({
|
|
934
|
+
...options,
|
|
935
|
+
apiKey,
|
|
936
|
+
host,
|
|
937
|
+
port
|
|
938
|
+
}),
|
|
939
|
+
hostname: host,
|
|
940
|
+
port
|
|
941
|
+
});
|
|
942
|
+
return {
|
|
943
|
+
server,
|
|
944
|
+
url: `http://${host}:${server.port}`
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async function handleModels(client, signal) {
|
|
948
|
+
const upstream = await client.models(signal);
|
|
949
|
+
if (!upstream.ok) {
|
|
950
|
+
return jsonResponse({ data: fallbackModels(), object: "list" });
|
|
951
|
+
}
|
|
952
|
+
return jsonResponse(normalizeModelsResponse(await upstream.json()));
|
|
953
|
+
}
|
|
954
|
+
async function handleChatCompletions(client, request) {
|
|
955
|
+
const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
|
|
956
|
+
return proxyResponse(upstream);
|
|
957
|
+
}
|
|
958
|
+
async function handleCompletions(client, request) {
|
|
959
|
+
const body = await readJson(request);
|
|
960
|
+
const upstream = await client.chatCompletions(
|
|
961
|
+
completionsRequestToChatCompletion(body),
|
|
962
|
+
request.signal
|
|
963
|
+
);
|
|
964
|
+
if (!upstream.ok) {
|
|
965
|
+
return proxyError(upstream);
|
|
966
|
+
}
|
|
967
|
+
return jsonResponse(chatCompletionToCompletion(await upstream.json()));
|
|
968
|
+
}
|
|
969
|
+
async function handleResponses(client, request) {
|
|
970
|
+
const body = await readJson(request);
|
|
971
|
+
const chatRequest = responsesRequestToChatCompletion(body);
|
|
972
|
+
const upstream = await client.chatCompletions(chatRequest, request.signal);
|
|
973
|
+
if (!upstream.ok) {
|
|
974
|
+
return proxyError(upstream);
|
|
975
|
+
}
|
|
976
|
+
if (body.stream === true && upstream.body) {
|
|
977
|
+
return new Response(
|
|
978
|
+
responsesStreamFromChatStream(upstream.body, {
|
|
979
|
+
model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
|
|
980
|
+
}),
|
|
981
|
+
{
|
|
982
|
+
headers: {
|
|
983
|
+
...corsHeaders(),
|
|
984
|
+
"cache-control": "no-cache",
|
|
985
|
+
connection: "keep-alive",
|
|
986
|
+
"content-type": "text/event-stream; charset=utf-8"
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
return jsonResponse(chatCompletionToResponse(await upstream.json()));
|
|
992
|
+
}
|
|
993
|
+
async function proxyError(upstream) {
|
|
994
|
+
const text = await upstream.text();
|
|
995
|
+
return jsonError(upstream.status, "copilot_error", text || upstream.statusText);
|
|
996
|
+
}
|
|
997
|
+
function proxyResponse(upstream) {
|
|
998
|
+
const headers = new Headers(upstream.headers);
|
|
999
|
+
headers.delete("content-encoding");
|
|
1000
|
+
headers.delete("content-length");
|
|
1001
|
+
headers.delete("transfer-encoding");
|
|
1002
|
+
for (const [key, value] of Object.entries(corsHeaders())) {
|
|
1003
|
+
headers.set(key, value);
|
|
1004
|
+
}
|
|
1005
|
+
return new Response(upstream.body, {
|
|
1006
|
+
headers,
|
|
1007
|
+
status: upstream.status,
|
|
1008
|
+
statusText: upstream.statusText
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
async function readJson(request) {
|
|
1012
|
+
try {
|
|
1013
|
+
const value = await request.json();
|
|
1014
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1015
|
+
} catch {
|
|
1016
|
+
throw new Error("Request body must be valid JSON.");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function jsonResponse(body, status = 200) {
|
|
1020
|
+
return new Response(JSON.stringify(body), {
|
|
1021
|
+
headers: {
|
|
1022
|
+
...corsHeaders(),
|
|
1023
|
+
"content-type": "application/json; charset=utf-8"
|
|
1024
|
+
},
|
|
1025
|
+
status
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
function jsonError(status, code, message) {
|
|
1029
|
+
return jsonResponse(
|
|
1030
|
+
{
|
|
1031
|
+
error: {
|
|
1032
|
+
code,
|
|
1033
|
+
message,
|
|
1034
|
+
type: code
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
status
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
function corsHeaders() {
|
|
1041
|
+
return {
|
|
1042
|
+
"access-control-allow-headers": "authorization, content-type, x-api-key",
|
|
1043
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
1044
|
+
"access-control-allow-origin": "*"
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function isAuthorized(request, apiKey) {
|
|
1048
|
+
if (!apiKey) {
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
1052
|
+
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1053
|
+
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1054
|
+
}
|
|
1055
|
+
function isLoopbackHost(host) {
|
|
1056
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1057
|
+
}
|
|
1058
|
+
function errorMessage2(error) {
|
|
1059
|
+
return error instanceof Error ? error.message : String(error);
|
|
1060
|
+
}
|
|
1061
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1062
|
+
0 && (module.exports = {
|
|
1063
|
+
CopilotAuth,
|
|
1064
|
+
CopilotAuthError,
|
|
1065
|
+
CopilotClient,
|
|
1066
|
+
DEFAULT_MODEL,
|
|
1067
|
+
chatCompletionToCompletion,
|
|
1068
|
+
chatCompletionToResponse,
|
|
1069
|
+
completionsRequestToChatCompletion,
|
|
1070
|
+
createHoopilotHandler,
|
|
1071
|
+
fallbackModels,
|
|
1072
|
+
normalizeModelsResponse,
|
|
1073
|
+
responsesRequestToChatCompletion,
|
|
1074
|
+
responsesStreamFromChatStream,
|
|
1075
|
+
splitCommand,
|
|
1076
|
+
startHoopilotServer
|
|
1077
|
+
});
|
|
1078
|
+
//# sourceMappingURL=index.cjs.map
|