@openhoo/hoopilot 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -67
- package/dist/cli.js +724 -291
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +556 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -10
- package/dist/index.d.ts +65 -10
- package/dist/index.js +535 -263
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -23,270 +33,126 @@ __export(index_exports, {
|
|
|
23
33
|
CopilotAuth: () => CopilotAuth,
|
|
24
34
|
CopilotAuthError: () => CopilotAuthError,
|
|
25
35
|
CopilotClient: () => CopilotClient,
|
|
36
|
+
DEFAULT_LOG_FORMAT: () => DEFAULT_LOG_FORMAT,
|
|
37
|
+
DEFAULT_LOG_LEVEL: () => DEFAULT_LOG_LEVEL,
|
|
26
38
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
39
|
+
authStorePath: () => authStorePath,
|
|
27
40
|
chatCompletionToCompletion: () => chatCompletionToCompletion,
|
|
28
41
|
chatCompletionToResponse: () => chatCompletionToResponse,
|
|
29
42
|
completionsRequestToChatCompletion: () => completionsRequestToChatCompletion,
|
|
30
43
|
createHoopilotHandler: () => createHoopilotHandler,
|
|
44
|
+
createHoopilotLogger: () => createHoopilotLogger,
|
|
31
45
|
fallbackModels: () => fallbackModels,
|
|
46
|
+
githubCopilotDeviceLogin: () => githubCopilotDeviceLogin,
|
|
47
|
+
noopLogger: () => noopLogger,
|
|
32
48
|
normalizeModelsResponse: () => normalizeModelsResponse,
|
|
49
|
+
parseLogFormat: () => parseLogFormat,
|
|
50
|
+
parseLogLevel: () => parseLogLevel,
|
|
51
|
+
readStoredCopilotAuth: () => readStoredCopilotAuth,
|
|
33
52
|
responsesRequestToChatCompletion: () => responsesRequestToChatCompletion,
|
|
34
53
|
responsesStreamFromChatStream: () => responsesStreamFromChatStream,
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
startHoopilotServer: () => startHoopilotServer,
|
|
55
|
+
writeStoredCopilotAuth: () => writeStoredCopilotAuth
|
|
37
56
|
});
|
|
38
57
|
module.exports = __toCommonJS(index_exports);
|
|
39
58
|
|
|
59
|
+
// src/auth-store.ts
|
|
60
|
+
var import_node_fs = require("fs");
|
|
61
|
+
var import_node_path = require("path");
|
|
62
|
+
function authStorePath(env = process.env) {
|
|
63
|
+
if (env.HOOPILOT_AUTH_FILE) {
|
|
64
|
+
return env.HOOPILOT_AUTH_FILE;
|
|
65
|
+
}
|
|
66
|
+
const base = env.XDG_CONFIG_HOME ?? env.APPDATA ?? (env.HOME ? (0, import_node_path.join)(env.HOME, ".config") : (0, import_node_path.join)(process.cwd(), ".config"));
|
|
67
|
+
return (0, import_node_path.join)(base, "hoopilot", "auth.json");
|
|
68
|
+
}
|
|
69
|
+
function readStoredCopilotAuth(path = authStorePath()) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
72
|
+
if (!parsed || typeof parsed !== "object") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
|
|
76
|
+
if (!token) {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : void 0,
|
|
81
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0,
|
|
82
|
+
githubDomain: typeof parsed.githubDomain === "string" ? parsed.githubDomain : void 0,
|
|
83
|
+
source: typeof parsed.source === "string" ? parsed.source : void 0,
|
|
84
|
+
token
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
91
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
92
|
+
(0, import_node_fs.writeFileSync)(
|
|
93
|
+
path,
|
|
94
|
+
`${JSON.stringify(
|
|
95
|
+
{
|
|
96
|
+
...auth,
|
|
97
|
+
createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2
|
|
101
|
+
)}
|
|
102
|
+
`,
|
|
103
|
+
{ mode: 384 }
|
|
104
|
+
);
|
|
105
|
+
try {
|
|
106
|
+
(0, import_node_fs.chmodSync)(path, 384);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
40
111
|
// src/auth.ts
|
|
41
|
-
var
|
|
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";
|
|
112
|
+
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
44
113
|
var REFRESH_SKEW_MS = 6e4;
|
|
114
|
+
var STORED_TOKEN_TTL_MS = 10 * 6e4;
|
|
45
115
|
var CopilotAuthError = class extends Error {
|
|
46
116
|
constructor(message) {
|
|
47
117
|
super(message);
|
|
48
118
|
this.name = "CopilotAuthError";
|
|
49
119
|
}
|
|
50
120
|
};
|
|
51
|
-
var CopilotTokenExchangeHttpError = class extends CopilotAuthError {
|
|
52
|
-
};
|
|
53
121
|
var CopilotAuth = class {
|
|
54
|
-
#
|
|
122
|
+
#authStorePath;
|
|
55
123
|
#copilotApiBaseUrl;
|
|
56
|
-
#copilotToken;
|
|
57
|
-
#env;
|
|
58
|
-
#fetch;
|
|
59
|
-
#githubToken;
|
|
60
|
-
#githubTokenCommand;
|
|
61
|
-
#logger;
|
|
62
|
-
#tokenExchangeUrl;
|
|
63
124
|
#cachedAccess;
|
|
64
125
|
constructor(options = {}) {
|
|
65
|
-
this.#
|
|
126
|
+
this.#authStorePath = options.authStorePath ?? options.env?.HOOPILOT_AUTH_FILE;
|
|
66
127
|
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
67
128
|
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
68
129
|
);
|
|
69
|
-
this.#copilotToken = options.copilotToken;
|
|
70
|
-
this.#env = options.env ?? process.env;
|
|
71
|
-
this.#fetch = options.fetch ?? fetch;
|
|
72
|
-
this.#githubToken = options.githubToken;
|
|
73
|
-
this.#githubTokenCommand = options.githubTokenCommand ?? "gh auth token";
|
|
74
|
-
this.#logger = options.logger;
|
|
75
|
-
this.#tokenExchangeUrl = options.tokenExchangeUrl ?? options.env?.COPILOT_TOKEN_EXCHANGE_URL ?? DEFAULT_TOKEN_EXCHANGE_URL;
|
|
76
130
|
}
|
|
77
131
|
async getAccess() {
|
|
78
132
|
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
79
133
|
return this.#cachedAccess;
|
|
80
134
|
}
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return this.#cacheAccess({
|
|
84
|
-
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
85
|
-
expiresAtMs: Date.now() + 10 * 6e4,
|
|
86
|
-
source: "copilot-token",
|
|
87
|
-
token: directCopilotToken
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
if (this.#authMode === "copilot-token") {
|
|
91
|
-
throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
|
|
92
|
-
}
|
|
93
|
-
const githubToken = this.#resolveGithubToken();
|
|
94
|
-
if (!githubToken) {
|
|
95
|
-
throw new CopilotAuthError(
|
|
96
|
-
"No Copilot credential found. Set COPILOT_API_TOKEN, set COPILOT_GITHUB_TOKEN from gh auth token, or sign in with gh auth login."
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (isPersonalAccessToken(githubToken)) {
|
|
100
|
-
throw new CopilotAuthError(
|
|
101
|
-
"GitHub personal access tokens are not supported for Copilot authentication. Use gh auth login or COPILOT_API_TOKEN."
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
return this.#cacheAccess(await this.#exchangeGithubToken(githubToken));
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (!(error instanceof CopilotTokenExchangeHttpError)) {
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
this.#logger?.warn(
|
|
111
|
-
`Copilot token exchange failed; falling back to GitHub CLI token mode: ${errorMessage(
|
|
112
|
-
error
|
|
113
|
-
)}`
|
|
114
|
-
);
|
|
135
|
+
const stored = readStoredCopilotAuth(this.#authStorePath);
|
|
136
|
+
if (stored) {
|
|
115
137
|
return this.#cacheAccess({
|
|
116
|
-
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
117
|
-
expiresAtMs: Date.now() +
|
|
118
|
-
source: "
|
|
119
|
-
token:
|
|
138
|
+
apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
|
|
139
|
+
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
140
|
+
source: "github-copilot-oauth",
|
|
141
|
+
token: stored.token
|
|
120
142
|
});
|
|
121
143
|
}
|
|
144
|
+
throw new CopilotAuthError(
|
|
145
|
+
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
146
|
+
);
|
|
122
147
|
}
|
|
123
148
|
#cacheAccess(access) {
|
|
124
149
|
this.#cachedAccess = access;
|
|
125
150
|
return access;
|
|
126
151
|
}
|
|
127
|
-
async #exchangeGithubToken(githubToken) {
|
|
128
|
-
const response = await this.#fetch(this.#tokenExchangeUrl, {
|
|
129
|
-
headers: {
|
|
130
|
-
accept: "application/vnd.github+json",
|
|
131
|
-
authorization: `token ${githubToken}`,
|
|
132
|
-
"editor-plugin-version": "hoopilot/0.1.0",
|
|
133
|
-
"editor-version": "Hoopilot/0.1.0",
|
|
134
|
-
"user-agent": "hoopilot/0.1.0"
|
|
135
|
-
},
|
|
136
|
-
method: "GET"
|
|
137
|
-
});
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
throw new CopilotTokenExchangeHttpError(
|
|
140
|
-
`GitHub Copilot token exchange failed with ${response.status}: ${await safeResponseText(
|
|
141
|
-
response
|
|
142
|
-
)}`
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
const body = asRecord(await response.json());
|
|
146
|
-
const token = getString(body, "token");
|
|
147
|
-
if (!token) {
|
|
148
|
-
throw new CopilotAuthError("GitHub Copilot token exchange response did not include a token.");
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
apiBaseUrl: endpointFromResponse(body) ?? this.#copilotApiBaseUrl,
|
|
152
|
-
expiresAtMs: expiresAtFromResponse(body),
|
|
153
|
-
source: "github-token",
|
|
154
|
-
token
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
#resolveDirectCopilotToken() {
|
|
158
|
-
return firstNonEmpty(
|
|
159
|
-
this.#copilotToken,
|
|
160
|
-
this.#env.COPILOT_API_TOKEN,
|
|
161
|
-
this.#env.GITHUB_COPILOT_API_TOKEN,
|
|
162
|
-
this.#env.GITHUB_COPILOT_TOKEN
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
#resolveGithubToken() {
|
|
166
|
-
return firstNonEmpty(
|
|
167
|
-
this.#githubToken,
|
|
168
|
-
this.#env.COPILOT_GITHUB_TOKEN,
|
|
169
|
-
this.#env.GITHUB_COPILOT_GITHUB_TOKEN,
|
|
170
|
-
this.#readGithubTokenCommand()
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
#readGithubTokenCommand() {
|
|
174
|
-
if (this.#githubTokenCommand === false) {
|
|
175
|
-
return void 0;
|
|
176
|
-
}
|
|
177
|
-
const parts = splitCommand(this.#githubTokenCommand);
|
|
178
|
-
const [command, ...args] = parts;
|
|
179
|
-
if (!command) {
|
|
180
|
-
return void 0;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
const output = (0, import_node_child_process.execFileSync)(command, args, {
|
|
184
|
-
encoding: "utf8",
|
|
185
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
186
|
-
timeout: 5e3
|
|
187
|
-
});
|
|
188
|
-
return output.trim() || void 0;
|
|
189
|
-
} catch {
|
|
190
|
-
return void 0;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
152
|
};
|
|
194
|
-
function splitCommand(command) {
|
|
195
|
-
const parts = [];
|
|
196
|
-
let current = "";
|
|
197
|
-
let quote;
|
|
198
|
-
let escaping = false;
|
|
199
|
-
for (const character of command.trim()) {
|
|
200
|
-
if (escaping) {
|
|
201
|
-
current += character;
|
|
202
|
-
escaping = false;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (character === "\\") {
|
|
206
|
-
escaping = true;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (quote) {
|
|
210
|
-
if (character === quote) {
|
|
211
|
-
quote = void 0;
|
|
212
|
-
} else {
|
|
213
|
-
current += character;
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (character === "'" || character === '"') {
|
|
218
|
-
quote = character;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (/\s/.test(character)) {
|
|
222
|
-
if (current) {
|
|
223
|
-
parts.push(current);
|
|
224
|
-
current = "";
|
|
225
|
-
}
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
current += character;
|
|
229
|
-
}
|
|
230
|
-
if (current) {
|
|
231
|
-
parts.push(current);
|
|
232
|
-
}
|
|
233
|
-
return parts;
|
|
234
|
-
}
|
|
235
|
-
function endpointFromResponse(body) {
|
|
236
|
-
const endpoints = asRecord(body.endpoints);
|
|
237
|
-
const apiUrl = getString(endpoints, "api") ?? getString(endpoints, "proxy");
|
|
238
|
-
return apiUrl ? trimTrailingSlash(apiUrl) : void 0;
|
|
239
|
-
}
|
|
240
|
-
function expiresAtFromResponse(body) {
|
|
241
|
-
const expiresAt = body.expires_at;
|
|
242
|
-
if (typeof expiresAt === "number") {
|
|
243
|
-
return expiresAt < 1e10 ? expiresAt * 1e3 : expiresAt;
|
|
244
|
-
}
|
|
245
|
-
if (typeof expiresAt === "string") {
|
|
246
|
-
const asNumber = Number(expiresAt);
|
|
247
|
-
if (Number.isFinite(asNumber)) {
|
|
248
|
-
return asNumber < 1e10 ? asNumber * 1e3 : asNumber;
|
|
249
|
-
}
|
|
250
|
-
const parsed = Date.parse(expiresAt);
|
|
251
|
-
if (Number.isFinite(parsed)) {
|
|
252
|
-
return parsed;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const refreshIn = body.refresh_in;
|
|
256
|
-
if (typeof refreshIn === "number" && Number.isFinite(refreshIn)) {
|
|
257
|
-
return Date.now() + refreshIn * 1e3;
|
|
258
|
-
}
|
|
259
|
-
return Date.now() + 10 * 6e4;
|
|
260
|
-
}
|
|
261
|
-
function firstNonEmpty(...values) {
|
|
262
|
-
for (const value of values) {
|
|
263
|
-
const trimmed = value?.trim();
|
|
264
|
-
if (trimmed) {
|
|
265
|
-
return trimmed;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return void 0;
|
|
269
|
-
}
|
|
270
|
-
function asRecord(value) {
|
|
271
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
272
|
-
}
|
|
273
|
-
function getString(record, key) {
|
|
274
|
-
const value = record[key];
|
|
275
|
-
return typeof value === "string" && value ? value : void 0;
|
|
276
|
-
}
|
|
277
153
|
function trimTrailingSlash(value) {
|
|
278
154
|
return value.replace(/\/+$/, "");
|
|
279
155
|
}
|
|
280
|
-
async function safeResponseText(response) {
|
|
281
|
-
const text = await response.text();
|
|
282
|
-
return text.slice(0, 500);
|
|
283
|
-
}
|
|
284
|
-
function isPersonalAccessToken(token) {
|
|
285
|
-
return token.startsWith("github_pat_") || token.startsWith("ghp_");
|
|
286
|
-
}
|
|
287
|
-
function errorMessage(error) {
|
|
288
|
-
return error instanceof Error ? error.message : String(error);
|
|
289
|
-
}
|
|
290
156
|
|
|
291
157
|
// src/copilot.ts
|
|
292
158
|
var CopilotClient = class {
|
|
@@ -335,6 +201,7 @@ var CopilotClient = class {
|
|
|
335
201
|
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
336
202
|
headers.set("openai-intent", "conversation-panel");
|
|
337
203
|
headers.set("user-agent", "hoopilot/0.1.0");
|
|
204
|
+
headers.set("x-github-api-version", "2026-06-01");
|
|
338
205
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
339
206
|
...init,
|
|
340
207
|
headers
|
|
@@ -342,6 +209,226 @@ var CopilotClient = class {
|
|
|
342
209
|
}
|
|
343
210
|
};
|
|
344
211
|
|
|
212
|
+
// src/github-device.ts
|
|
213
|
+
var import_promises = require("timers/promises");
|
|
214
|
+
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Iv23lijnNxm2e9UX3CF8";
|
|
215
|
+
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
216
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
217
|
+
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
218
|
+
async function githubCopilotDeviceLogin(options = {}) {
|
|
219
|
+
const env = options.env ?? process.env;
|
|
220
|
+
const fetcher = options.fetch ?? fetch;
|
|
221
|
+
const sleeper = options.sleep ?? import_promises.setTimeout;
|
|
222
|
+
const domain = normalizeDomain(
|
|
223
|
+
options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
|
|
224
|
+
);
|
|
225
|
+
const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
226
|
+
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
227
|
+
const verificationUrl = device.verification_uri;
|
|
228
|
+
const userCode = device.user_code;
|
|
229
|
+
const deviceCode = device.device_code;
|
|
230
|
+
if (!verificationUrl || !userCode || !deviceCode) {
|
|
231
|
+
throw new Error("GitHub device authorization response is missing required fields.");
|
|
232
|
+
}
|
|
233
|
+
options.logger?.info(`First copy your one-time code: ${userCode}`);
|
|
234
|
+
options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
|
|
235
|
+
await options.openBrowser?.(verificationUrl);
|
|
236
|
+
return {
|
|
237
|
+
domain,
|
|
238
|
+
token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
|
|
239
|
+
deviceCode,
|
|
240
|
+
expiresIn: positiveSeconds(device.expires_in, 900),
|
|
241
|
+
interval: positiveSeconds(device.interval, 5)
|
|
242
|
+
})
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function requestDeviceCode(fetcher, domain, clientId) {
|
|
246
|
+
const response = await fetcher(`https://${domain}/login/device/code`, {
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
client_id: clientId,
|
|
249
|
+
scope: "read:user"
|
|
250
|
+
}),
|
|
251
|
+
headers: oauthHeaders(),
|
|
252
|
+
method: "POST"
|
|
253
|
+
});
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`GitHub device authorization failed with ${response.status}: ${await safeResponseText(
|
|
257
|
+
response
|
|
258
|
+
)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return await response.json();
|
|
262
|
+
}
|
|
263
|
+
async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
|
|
264
|
+
let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
|
|
265
|
+
const deadline = Date.now() + device.expiresIn * 1e3;
|
|
266
|
+
while (Date.now() < deadline) {
|
|
267
|
+
await sleeper(intervalMs);
|
|
268
|
+
const response = await fetcher(`https://${domain}/login/oauth/access_token`, {
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
client_id: clientId,
|
|
271
|
+
device_code: device.deviceCode,
|
|
272
|
+
grant_type: DEVICE_GRANT_TYPE
|
|
273
|
+
}),
|
|
274
|
+
headers: oauthHeaders(),
|
|
275
|
+
method: "POST"
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`GitHub device token exchange failed with ${response.status}: ${await safeResponseText(
|
|
280
|
+
response
|
|
281
|
+
)}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const data = await response.json();
|
|
285
|
+
if (data.access_token) {
|
|
286
|
+
return data.access_token;
|
|
287
|
+
}
|
|
288
|
+
if (data.error === "authorization_pending") {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (data.error === "slow_down") {
|
|
292
|
+
intervalMs = positiveSeconds(data.interval, device.interval + 5) * 1e3 + POLLING_SAFETY_MARGIN_MS;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (data.error === "expired_token") {
|
|
296
|
+
throw new Error("GitHub device login expired. Run `hoopilot login` again.");
|
|
297
|
+
}
|
|
298
|
+
if (data.error === "access_denied") {
|
|
299
|
+
throw new Error("GitHub device login was cancelled.");
|
|
300
|
+
}
|
|
301
|
+
if (data.error) {
|
|
302
|
+
throw new Error(data.error_description || `GitHub device login failed: ${data.error}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw new Error("GitHub device login timed out. Run `hoopilot login` again.");
|
|
306
|
+
}
|
|
307
|
+
function oauthHeaders() {
|
|
308
|
+
const headers = new Headers();
|
|
309
|
+
headers.set("accept", "application/json");
|
|
310
|
+
headers.set("content-type", "application/json");
|
|
311
|
+
headers.set("user-agent", "hoopilot");
|
|
312
|
+
return headers;
|
|
313
|
+
}
|
|
314
|
+
function normalizeDomain(value) {
|
|
315
|
+
return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
316
|
+
}
|
|
317
|
+
function positiveSeconds(value, fallback) {
|
|
318
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
319
|
+
}
|
|
320
|
+
async function safeResponseText(response) {
|
|
321
|
+
const text = await response.text();
|
|
322
|
+
return text.slice(0, 500);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/logger.ts
|
|
326
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
327
|
+
var import_pino_pretty = __toESM(require("pino-pretty"), 1);
|
|
328
|
+
var DEFAULT_LOG_FORMAT = "pretty";
|
|
329
|
+
var DEFAULT_LOG_LEVEL = "info";
|
|
330
|
+
var LOG_FORMATS = ["json", "pretty"];
|
|
331
|
+
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
332
|
+
var REDACT_PATHS = [
|
|
333
|
+
"apiKey",
|
|
334
|
+
"authorization",
|
|
335
|
+
"cookie",
|
|
336
|
+
"headers.authorization",
|
|
337
|
+
"headers.Authorization",
|
|
338
|
+
"headers.cookie",
|
|
339
|
+
"headers.Cookie",
|
|
340
|
+
"headers.x-api-key",
|
|
341
|
+
"headers.X-Api-Key",
|
|
342
|
+
"token",
|
|
343
|
+
"*.apiKey",
|
|
344
|
+
"*.authorization",
|
|
345
|
+
"*.cookie",
|
|
346
|
+
"*.token",
|
|
347
|
+
"*.headers.authorization",
|
|
348
|
+
"*.headers.Authorization",
|
|
349
|
+
"*.headers.cookie",
|
|
350
|
+
"*.headers.Cookie",
|
|
351
|
+
"*.headers.x-api-key",
|
|
352
|
+
"*.headers.X-Api-Key"
|
|
353
|
+
];
|
|
354
|
+
var noopLogger = {
|
|
355
|
+
child: () => noopLogger,
|
|
356
|
+
debug: () => {
|
|
357
|
+
},
|
|
358
|
+
error: () => {
|
|
359
|
+
},
|
|
360
|
+
fatal: () => {
|
|
361
|
+
},
|
|
362
|
+
info: () => {
|
|
363
|
+
},
|
|
364
|
+
trace: () => {
|
|
365
|
+
},
|
|
366
|
+
warn: () => {
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
function createHoopilotLogger(options = {}) {
|
|
370
|
+
const env = options.env ?? process.env;
|
|
371
|
+
const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
|
|
372
|
+
const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
|
|
373
|
+
const pinoOptions = {
|
|
374
|
+
base: {
|
|
375
|
+
service: "hoopilot",
|
|
376
|
+
...options.base
|
|
377
|
+
},
|
|
378
|
+
level,
|
|
379
|
+
redact: {
|
|
380
|
+
censor: "[Redacted]",
|
|
381
|
+
paths: REDACT_PATHS
|
|
382
|
+
},
|
|
383
|
+
timestamp: import_pino.default.stdTimeFunctions.isoTime
|
|
384
|
+
};
|
|
385
|
+
if (format === "pretty") {
|
|
386
|
+
return (0, import_pino.default)(
|
|
387
|
+
pinoOptions,
|
|
388
|
+
(0, import_pino_pretty.default)({
|
|
389
|
+
colorize: options.colorize ?? process.stderr.isTTY,
|
|
390
|
+
destination: options.stream ?? 1,
|
|
391
|
+
ignore: "pid,hostname",
|
|
392
|
+
singleLine: true,
|
|
393
|
+
translateTime: "SYS:standard"
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (options.stream) {
|
|
398
|
+
return (0, import_pino.default)(pinoOptions, options.stream);
|
|
399
|
+
}
|
|
400
|
+
return (0, import_pino.default)(pinoOptions);
|
|
401
|
+
}
|
|
402
|
+
function parseLogFormat(value) {
|
|
403
|
+
if (!value) {
|
|
404
|
+
return DEFAULT_LOG_FORMAT;
|
|
405
|
+
}
|
|
406
|
+
if (isLogFormat(value)) {
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
throw new Error(`Invalid log format: ${value}. Expected one of: ${LOG_FORMATS.join(", ")}.`);
|
|
410
|
+
}
|
|
411
|
+
function parseLogLevel(value) {
|
|
412
|
+
if (!value) {
|
|
413
|
+
return DEFAULT_LOG_LEVEL;
|
|
414
|
+
}
|
|
415
|
+
if (isLogLevel(value)) {
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
throw new Error(`Invalid log level: ${value}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
|
|
419
|
+
}
|
|
420
|
+
function shouldCreateLogger(options) {
|
|
421
|
+
return Boolean(
|
|
422
|
+
options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
function isLogFormat(value) {
|
|
426
|
+
return LOG_FORMATS.includes(value);
|
|
427
|
+
}
|
|
428
|
+
function isLogLevel(value) {
|
|
429
|
+
return LOG_LEVELS.includes(value);
|
|
430
|
+
}
|
|
431
|
+
|
|
345
432
|
// src/openai.ts
|
|
346
433
|
var DEFAULT_MODEL = "gpt-4.1";
|
|
347
434
|
function responsesRequestToChatCompletion(request) {
|
|
@@ -360,8 +447,8 @@ function responsesRequestToChatCompletion(request) {
|
|
|
360
447
|
metadata: request.metadata,
|
|
361
448
|
model: contentToText(request.model) || DEFAULT_MODEL,
|
|
362
449
|
presence_penalty: request.presence_penalty,
|
|
363
|
-
reasoning_effort:
|
|
364
|
-
response_format:
|
|
450
|
+
reasoning_effort: asRecord(request.reasoning).effort,
|
|
451
|
+
response_format: asRecord(request.text).format,
|
|
365
452
|
seed: request.seed,
|
|
366
453
|
stream: request.stream === true,
|
|
367
454
|
temperature: request.temperature,
|
|
@@ -383,7 +470,7 @@ function completionsRequestToChatCompletion(request) {
|
|
|
383
470
|
function chatCompletionToResponse(completion, responseId) {
|
|
384
471
|
const id = responseId ?? `resp_${randomId()}`;
|
|
385
472
|
const choice = firstChoice(completion);
|
|
386
|
-
const message =
|
|
473
|
+
const message = asRecord(choice.message);
|
|
387
474
|
const model = contentToText(completion.model) || DEFAULT_MODEL;
|
|
388
475
|
const output = outputItemsFromMessage(message);
|
|
389
476
|
const usage = responseUsage(completion.usage);
|
|
@@ -410,7 +497,7 @@ function chatCompletionToResponse(completion, responseId) {
|
|
|
410
497
|
}
|
|
411
498
|
function chatCompletionToCompletion(completion) {
|
|
412
499
|
const choice = firstChoice(completion);
|
|
413
|
-
const message =
|
|
500
|
+
const message = asRecord(choice.message);
|
|
414
501
|
return removeUndefined({
|
|
415
502
|
choices: [
|
|
416
503
|
{
|
|
@@ -428,9 +515,9 @@ function chatCompletionToCompletion(completion) {
|
|
|
428
515
|
});
|
|
429
516
|
}
|
|
430
517
|
function normalizeModelsResponse(upstream) {
|
|
431
|
-
const record =
|
|
518
|
+
const record = asRecord(upstream);
|
|
432
519
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
433
|
-
const models = data.map((model) =>
|
|
520
|
+
const models = data.map((model) => asRecord(model)).filter((model) => typeof model.id === "string").map((model) => ({
|
|
434
521
|
created: model.created ?? 0,
|
|
435
522
|
id: model.id,
|
|
436
523
|
object: "model",
|
|
@@ -579,7 +666,7 @@ function inputToMessages(input) {
|
|
|
579
666
|
}
|
|
580
667
|
const messages = [];
|
|
581
668
|
for (const item of input) {
|
|
582
|
-
const record =
|
|
669
|
+
const record = asRecord(item);
|
|
583
670
|
if (record.type === "function_call_output") {
|
|
584
671
|
messages.push({
|
|
585
672
|
content: contentToText(record.output),
|
|
@@ -621,7 +708,7 @@ function chatMessageContent(content) {
|
|
|
621
708
|
}
|
|
622
709
|
const parts = [];
|
|
623
710
|
for (const part of content) {
|
|
624
|
-
const record =
|
|
711
|
+
const record = asRecord(part);
|
|
625
712
|
const type = contentToText(record.type);
|
|
626
713
|
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
627
714
|
parts.push({ text: contentToText(record.text), type: "text" });
|
|
@@ -679,7 +766,7 @@ function chatTools(tools) {
|
|
|
679
766
|
if (!Array.isArray(tools)) {
|
|
680
767
|
return void 0;
|
|
681
768
|
}
|
|
682
|
-
const converted = tools.map((tool) =>
|
|
769
|
+
const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
|
|
683
770
|
function: removeUndefined({
|
|
684
771
|
description: tool.description,
|
|
685
772
|
name: tool.name,
|
|
@@ -694,7 +781,7 @@ function chatToolChoice(toolChoice) {
|
|
|
694
781
|
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
695
782
|
return toolChoice;
|
|
696
783
|
}
|
|
697
|
-
const record =
|
|
784
|
+
const record = asRecord(toolChoice);
|
|
698
785
|
if (record.type === "function" && typeof record.name === "string") {
|
|
699
786
|
return { function: { name: record.name }, type: "function" };
|
|
700
787
|
}
|
|
@@ -708,8 +795,8 @@ function outputItemsFromMessage(message) {
|
|
|
708
795
|
}
|
|
709
796
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
710
797
|
for (const toolCall of toolCalls) {
|
|
711
|
-
const record =
|
|
712
|
-
const fn =
|
|
798
|
+
const record = asRecord(toolCall);
|
|
799
|
+
const fn = asRecord(record.function);
|
|
713
800
|
output.push(
|
|
714
801
|
functionCallItem({
|
|
715
802
|
arguments: contentToText(fn.arguments),
|
|
@@ -750,10 +837,10 @@ function outputText(output) {
|
|
|
750
837
|
return output.flatMap((item) => {
|
|
751
838
|
const content = item.content;
|
|
752
839
|
return Array.isArray(content) ? content : [];
|
|
753
|
-
}).map((part) => contentToText(
|
|
840
|
+
}).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
|
|
754
841
|
}
|
|
755
842
|
function responseUsage(usage) {
|
|
756
|
-
const record =
|
|
843
|
+
const record = asRecord(usage);
|
|
757
844
|
if (Object.keys(record).length === 0) {
|
|
758
845
|
return null;
|
|
759
846
|
}
|
|
@@ -767,7 +854,7 @@ function responseUsage(usage) {
|
|
|
767
854
|
}
|
|
768
855
|
function firstChoice(completion) {
|
|
769
856
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
770
|
-
return
|
|
857
|
+
return asRecord(choices[0]);
|
|
771
858
|
}
|
|
772
859
|
function processChatSseLine(line, enqueue, tools, appendText) {
|
|
773
860
|
const trimmed = line.trim();
|
|
@@ -783,7 +870,7 @@ function processChatSseLine(line, enqueue, tools, appendText) {
|
|
|
783
870
|
return;
|
|
784
871
|
}
|
|
785
872
|
const choice = firstChoice(parsed);
|
|
786
|
-
const delta =
|
|
873
|
+
const delta = asRecord(choice.delta);
|
|
787
874
|
const content = contentToText(delta.content);
|
|
788
875
|
if (content) {
|
|
789
876
|
appendText(content);
|
|
@@ -797,8 +884,8 @@ function processChatSseLine(line, enqueue, tools, appendText) {
|
|
|
797
884
|
}
|
|
798
885
|
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
799
886
|
for (const toolCall of toolCalls) {
|
|
800
|
-
const record =
|
|
801
|
-
const fn =
|
|
887
|
+
const record = asRecord(toolCall);
|
|
888
|
+
const fn = asRecord(record.function);
|
|
802
889
|
const index = typeof record.index === "number" ? record.index : tools.size;
|
|
803
890
|
const existing = tools.get(index) ?? {
|
|
804
891
|
arguments: "",
|
|
@@ -846,7 +933,7 @@ data: ${JSON.stringify(data)}
|
|
|
846
933
|
}
|
|
847
934
|
function parseJson(data) {
|
|
848
935
|
try {
|
|
849
|
-
return
|
|
936
|
+
return asRecord(JSON.parse(data));
|
|
850
937
|
} catch {
|
|
851
938
|
return void 0;
|
|
852
939
|
}
|
|
@@ -854,7 +941,7 @@ function parseJson(data) {
|
|
|
854
941
|
function removeUndefined(record) {
|
|
855
942
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
856
943
|
}
|
|
857
|
-
function
|
|
944
|
+
function asRecord(value) {
|
|
858
945
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
859
946
|
}
|
|
860
947
|
function randomId() {
|
|
@@ -867,43 +954,111 @@ function epochSeconds() {
|
|
|
867
954
|
// src/server.ts
|
|
868
955
|
var DEFAULT_HOST = "127.0.0.1";
|
|
869
956
|
var DEFAULT_PORT = 4141;
|
|
957
|
+
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
870
958
|
function createHoopilotHandler(options = {}) {
|
|
871
959
|
const client = new CopilotClient(options);
|
|
872
960
|
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
961
|
+
const logger = serverLogger(options);
|
|
873
962
|
return async (request) => {
|
|
963
|
+
const startedAt = performance.now();
|
|
874
964
|
const url = new URL(request.url);
|
|
965
|
+
const requestId = requestIdFor(request);
|
|
966
|
+
const requestLogger = logger.child({
|
|
967
|
+
method: request.method,
|
|
968
|
+
path: url.pathname,
|
|
969
|
+
requestId,
|
|
970
|
+
route: routeFor(request.method, url.pathname)
|
|
971
|
+
});
|
|
875
972
|
if (request.method === "OPTIONS") {
|
|
876
|
-
return new Response(null, { headers: corsHeaders() })
|
|
973
|
+
return finishResponse(new Response(null, { headers: corsHeaders() }), {
|
|
974
|
+
logger: requestLogger,
|
|
975
|
+
requestId,
|
|
976
|
+
startedAt
|
|
977
|
+
});
|
|
877
978
|
}
|
|
878
979
|
if (!isAuthorized(request, apiKey)) {
|
|
879
|
-
|
|
980
|
+
requestLogger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
|
|
981
|
+
return finishResponse(
|
|
982
|
+
jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key."),
|
|
983
|
+
{
|
|
984
|
+
logger: requestLogger,
|
|
985
|
+
requestId,
|
|
986
|
+
startedAt
|
|
987
|
+
}
|
|
988
|
+
);
|
|
880
989
|
}
|
|
881
990
|
try {
|
|
882
991
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/healthz")) {
|
|
883
|
-
return
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
992
|
+
return finishResponse(
|
|
993
|
+
jsonResponse({
|
|
994
|
+
name: "hoopilot",
|
|
995
|
+
object: "health",
|
|
996
|
+
status: "ok"
|
|
997
|
+
}),
|
|
998
|
+
{ logger: requestLogger, requestId, startedAt }
|
|
999
|
+
);
|
|
888
1000
|
}
|
|
889
1001
|
if (request.method === "GET" && url.pathname === "/v1/models") {
|
|
890
|
-
return await handleModels(client, request.signal)
|
|
1002
|
+
return finishResponse(await handleModels(client, request.signal, requestLogger), {
|
|
1003
|
+
logger: requestLogger,
|
|
1004
|
+
requestId,
|
|
1005
|
+
startedAt
|
|
1006
|
+
});
|
|
891
1007
|
}
|
|
892
1008
|
if (request.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
893
|
-
return await handleChatCompletions(client, request)
|
|
1009
|
+
return finishResponse(await handleChatCompletions(client, request, requestLogger), {
|
|
1010
|
+
logger: requestLogger,
|
|
1011
|
+
requestId,
|
|
1012
|
+
startedAt
|
|
1013
|
+
});
|
|
894
1014
|
}
|
|
895
1015
|
if (request.method === "POST" && url.pathname === "/v1/completions") {
|
|
896
|
-
return await handleCompletions(client, request)
|
|
1016
|
+
return finishResponse(await handleCompletions(client, request, requestLogger), {
|
|
1017
|
+
logger: requestLogger,
|
|
1018
|
+
requestId,
|
|
1019
|
+
startedAt
|
|
1020
|
+
});
|
|
897
1021
|
}
|
|
898
1022
|
if (request.method === "POST" && url.pathname === "/v1/responses") {
|
|
899
|
-
return await handleResponses(client, request)
|
|
1023
|
+
return finishResponse(await handleResponses(client, request, requestLogger), {
|
|
1024
|
+
logger: requestLogger,
|
|
1025
|
+
requestId,
|
|
1026
|
+
startedAt
|
|
1027
|
+
});
|
|
900
1028
|
}
|
|
901
|
-
return
|
|
1029
|
+
return finishResponse(
|
|
1030
|
+
jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`),
|
|
1031
|
+
{ logger: requestLogger, requestId, startedAt }
|
|
1032
|
+
);
|
|
902
1033
|
} catch (error) {
|
|
903
1034
|
if (error instanceof CopilotAuthError) {
|
|
904
|
-
|
|
1035
|
+
requestLogger.warn(
|
|
1036
|
+
{ err: errorDetails(error), event: "copilot.auth.missing" },
|
|
1037
|
+
"copilot auth failed"
|
|
1038
|
+
);
|
|
1039
|
+
return finishResponse(jsonError(401, "copilot_auth_error", error.message), {
|
|
1040
|
+
logger: requestLogger,
|
|
1041
|
+
requestId,
|
|
1042
|
+
startedAt
|
|
1043
|
+
});
|
|
905
1044
|
}
|
|
906
|
-
|
|
1045
|
+
const message = errorMessage(error);
|
|
1046
|
+
if (message === INVALID_JSON_MESSAGE) {
|
|
1047
|
+
requestLogger.warn(
|
|
1048
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1049
|
+
"request body was invalid json"
|
|
1050
|
+
);
|
|
1051
|
+
} else {
|
|
1052
|
+
requestLogger.error(
|
|
1053
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1054
|
+
"request failed"
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
return finishResponse(jsonError(500, "internal_error", message), {
|
|
1058
|
+
logger: requestLogger,
|
|
1059
|
+
requestId,
|
|
1060
|
+
startedAt
|
|
1061
|
+
});
|
|
907
1062
|
}
|
|
908
1063
|
};
|
|
909
1064
|
}
|
|
@@ -932,35 +1087,53 @@ function startHoopilotServer(options = {}) {
|
|
|
932
1087
|
url: `http://${host}:${server.port}`
|
|
933
1088
|
};
|
|
934
1089
|
}
|
|
935
|
-
async function handleModels(client, signal) {
|
|
1090
|
+
async function handleModels(client, signal, logger) {
|
|
936
1091
|
const upstream = await client.models(signal);
|
|
937
1092
|
if (!upstream.ok) {
|
|
1093
|
+
if (isUpstreamAuthStatus(upstream.status)) {
|
|
1094
|
+
return proxyError(upstream, logger);
|
|
1095
|
+
}
|
|
1096
|
+
logger.warn(
|
|
1097
|
+
{
|
|
1098
|
+
event: "copilot.models.fallback",
|
|
1099
|
+
upstreamPath: "/models",
|
|
1100
|
+
upstreamStatus: upstream.status
|
|
1101
|
+
},
|
|
1102
|
+
"falling back to built-in model list"
|
|
1103
|
+
);
|
|
938
1104
|
return jsonResponse({ data: fallbackModels(), object: "list" });
|
|
939
1105
|
}
|
|
1106
|
+
logUpstreamSuccess(logger, "/models", upstream.status);
|
|
940
1107
|
return jsonResponse(normalizeModelsResponse(await upstream.json()));
|
|
941
1108
|
}
|
|
942
|
-
async function handleChatCompletions(client, request) {
|
|
1109
|
+
async function handleChatCompletions(client, request, logger) {
|
|
943
1110
|
const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
|
|
1111
|
+
if (!upstream.ok) {
|
|
1112
|
+
return proxyError(upstream, logger);
|
|
1113
|
+
}
|
|
1114
|
+
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
944
1115
|
return proxyResponse(upstream);
|
|
945
1116
|
}
|
|
946
|
-
async function handleCompletions(client, request) {
|
|
1117
|
+
async function handleCompletions(client, request, logger) {
|
|
947
1118
|
const body = await readJson(request);
|
|
948
1119
|
const upstream = await client.chatCompletions(
|
|
949
1120
|
completionsRequestToChatCompletion(body),
|
|
950
1121
|
request.signal
|
|
951
1122
|
);
|
|
952
1123
|
if (!upstream.ok) {
|
|
953
|
-
return proxyError(upstream);
|
|
1124
|
+
return proxyError(upstream, logger);
|
|
954
1125
|
}
|
|
1126
|
+
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
955
1127
|
return jsonResponse(chatCompletionToCompletion(await upstream.json()));
|
|
956
1128
|
}
|
|
957
|
-
async function handleResponses(client, request) {
|
|
1129
|
+
async function handleResponses(client, request, logger) {
|
|
958
1130
|
const body = await readJson(request);
|
|
959
1131
|
const chatRequest = responsesRequestToChatCompletion(body);
|
|
960
1132
|
const upstream = await client.chatCompletions(chatRequest, request.signal);
|
|
961
1133
|
if (!upstream.ok) {
|
|
962
|
-
return proxyError(upstream);
|
|
1134
|
+
return proxyError(upstream, logger);
|
|
963
1135
|
}
|
|
1136
|
+
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
964
1137
|
if (body.stream === true && upstream.body) {
|
|
965
1138
|
return new Response(
|
|
966
1139
|
responsesStreamFromChatStream(upstream.body, {
|
|
@@ -978,8 +1151,19 @@ async function handleResponses(client, request) {
|
|
|
978
1151
|
}
|
|
979
1152
|
return jsonResponse(chatCompletionToResponse(await upstream.json()));
|
|
980
1153
|
}
|
|
981
|
-
async function proxyError(upstream) {
|
|
1154
|
+
async function proxyError(upstream, logger) {
|
|
982
1155
|
const text = await upstream.text();
|
|
1156
|
+
if (isUpstreamAuthStatus(upstream.status)) {
|
|
1157
|
+
logger.warn(
|
|
1158
|
+
{ event: "copilot.auth.rejected", upstreamStatus: upstream.status },
|
|
1159
|
+
"copilot rejected credential or account access"
|
|
1160
|
+
);
|
|
1161
|
+
return jsonError(401, "copilot_auth_error", upstreamAuthMessage(text || upstream.statusText));
|
|
1162
|
+
}
|
|
1163
|
+
logger.warn(
|
|
1164
|
+
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1165
|
+
"copilot upstream request failed"
|
|
1166
|
+
);
|
|
983
1167
|
return jsonError(upstream.status, "copilot_error", text || upstream.statusText);
|
|
984
1168
|
}
|
|
985
1169
|
function proxyResponse(upstream) {
|
|
@@ -1001,7 +1185,7 @@ async function readJson(request) {
|
|
|
1001
1185
|
const value = await request.json();
|
|
1002
1186
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1003
1187
|
} catch {
|
|
1004
|
-
throw new Error(
|
|
1188
|
+
throw new Error(INVALID_JSON_MESSAGE);
|
|
1005
1189
|
}
|
|
1006
1190
|
}
|
|
1007
1191
|
function jsonResponse(body, status = 200) {
|
|
@@ -1040,27 +1224,134 @@ function isAuthorized(request, apiKey) {
|
|
|
1040
1224
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1041
1225
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1042
1226
|
}
|
|
1227
|
+
function isUpstreamAuthStatus(status) {
|
|
1228
|
+
return status === 401 || status === 403;
|
|
1229
|
+
}
|
|
1230
|
+
function upstreamAuthMessage(message) {
|
|
1231
|
+
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1232
|
+
}
|
|
1043
1233
|
function isLoopbackHost(host) {
|
|
1044
1234
|
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1045
1235
|
}
|
|
1046
|
-
function
|
|
1236
|
+
function errorMessage(error) {
|
|
1047
1237
|
return error instanceof Error ? error.message : String(error);
|
|
1048
1238
|
}
|
|
1239
|
+
function serverLogger(options) {
|
|
1240
|
+
if (options.logger) {
|
|
1241
|
+
return options.logger.child({ component: "server" });
|
|
1242
|
+
}
|
|
1243
|
+
if (shouldCreateLogger(options)) {
|
|
1244
|
+
return createHoopilotLogger({
|
|
1245
|
+
env: options.env,
|
|
1246
|
+
format: options.logFormat,
|
|
1247
|
+
level: options.logLevel
|
|
1248
|
+
}).child({ component: "server" });
|
|
1249
|
+
}
|
|
1250
|
+
return noopLogger;
|
|
1251
|
+
}
|
|
1252
|
+
function finishResponse(response, options) {
|
|
1253
|
+
const withRequestId = responseWithRequestId(response, options.requestId);
|
|
1254
|
+
logRequestCompleted(options.logger, withRequestId, options.startedAt);
|
|
1255
|
+
return withRequestId;
|
|
1256
|
+
}
|
|
1257
|
+
function responseWithRequestId(response, requestId) {
|
|
1258
|
+
const headers = new Headers(response.headers);
|
|
1259
|
+
headers.set("x-request-id", requestId);
|
|
1260
|
+
return new Response(response.body, {
|
|
1261
|
+
headers,
|
|
1262
|
+
status: response.status,
|
|
1263
|
+
statusText: response.statusText
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function logRequestCompleted(logger, response, startedAt) {
|
|
1267
|
+
const fields = {
|
|
1268
|
+
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
|
|
1269
|
+
event: "http.request.completed",
|
|
1270
|
+
status: response.status,
|
|
1271
|
+
stream: isStreamingResponse(response)
|
|
1272
|
+
};
|
|
1273
|
+
if (response.status >= 500) {
|
|
1274
|
+
logger.error(fields, "request completed with server error");
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (response.status >= 400) {
|
|
1278
|
+
logger.warn(fields, "request completed with client error");
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
logger.info(fields, "request completed");
|
|
1282
|
+
}
|
|
1283
|
+
function requestIdFor(request) {
|
|
1284
|
+
const existing = request.headers.get("x-request-id")?.trim();
|
|
1285
|
+
return existing || crypto.randomUUID();
|
|
1286
|
+
}
|
|
1287
|
+
function routeFor(method, path) {
|
|
1288
|
+
if (method === "OPTIONS") {
|
|
1289
|
+
return "cors.preflight";
|
|
1290
|
+
}
|
|
1291
|
+
if (method === "GET" && (path === "/" || path === "/healthz")) {
|
|
1292
|
+
return "health";
|
|
1293
|
+
}
|
|
1294
|
+
if (method === "GET" && path === "/v1/models") {
|
|
1295
|
+
return "models";
|
|
1296
|
+
}
|
|
1297
|
+
if (method === "POST" && path === "/v1/chat/completions") {
|
|
1298
|
+
return "chat_completions";
|
|
1299
|
+
}
|
|
1300
|
+
if (method === "POST" && path === "/v1/completions") {
|
|
1301
|
+
return "completions";
|
|
1302
|
+
}
|
|
1303
|
+
if (method === "POST" && path === "/v1/responses") {
|
|
1304
|
+
return "responses";
|
|
1305
|
+
}
|
|
1306
|
+
return "not_found";
|
|
1307
|
+
}
|
|
1308
|
+
function isStreamingResponse(response) {
|
|
1309
|
+
return response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
1310
|
+
}
|
|
1311
|
+
function logUpstreamSuccess(logger, upstreamPath, status) {
|
|
1312
|
+
logger.debug(
|
|
1313
|
+
{
|
|
1314
|
+
event: "copilot.request.completed",
|
|
1315
|
+
upstreamPath,
|
|
1316
|
+
upstreamStatus: status
|
|
1317
|
+
},
|
|
1318
|
+
"copilot upstream request completed"
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
function errorDetails(error) {
|
|
1322
|
+
if (error instanceof Error) {
|
|
1323
|
+
return {
|
|
1324
|
+
message: error.message,
|
|
1325
|
+
name: error.name,
|
|
1326
|
+
stack: error.stack
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
return { message: String(error) };
|
|
1330
|
+
}
|
|
1049
1331
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1050
1332
|
0 && (module.exports = {
|
|
1051
1333
|
CopilotAuth,
|
|
1052
1334
|
CopilotAuthError,
|
|
1053
1335
|
CopilotClient,
|
|
1336
|
+
DEFAULT_LOG_FORMAT,
|
|
1337
|
+
DEFAULT_LOG_LEVEL,
|
|
1054
1338
|
DEFAULT_MODEL,
|
|
1339
|
+
authStorePath,
|
|
1055
1340
|
chatCompletionToCompletion,
|
|
1056
1341
|
chatCompletionToResponse,
|
|
1057
1342
|
completionsRequestToChatCompletion,
|
|
1058
1343
|
createHoopilotHandler,
|
|
1344
|
+
createHoopilotLogger,
|
|
1059
1345
|
fallbackModels,
|
|
1346
|
+
githubCopilotDeviceLogin,
|
|
1347
|
+
noopLogger,
|
|
1060
1348
|
normalizeModelsResponse,
|
|
1349
|
+
parseLogFormat,
|
|
1350
|
+
parseLogLevel,
|
|
1351
|
+
readStoredCopilotAuth,
|
|
1061
1352
|
responsesRequestToChatCompletion,
|
|
1062
1353
|
responsesStreamFromChatStream,
|
|
1063
|
-
|
|
1064
|
-
|
|
1354
|
+
startHoopilotServer,
|
|
1355
|
+
writeStoredCopilotAuth
|
|
1065
1356
|
});
|
|
1066
1357
|
//# sourceMappingURL=index.cjs.map
|