@openhoo/hoopilot 0.7.4 → 0.8.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 +17 -3
- package/dist/cli.js +805 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2017 -1240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +2012 -1240
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AnthropicCompatibilityError: () => AnthropicCompatibilityError,
|
|
33
34
|
COPILOT_USAGE_API_VERSION: () => COPILOT_USAGE_API_VERSION,
|
|
34
35
|
CopilotAuth: () => CopilotAuth,
|
|
35
36
|
CopilotAuthError: () => CopilotAuthError,
|
|
@@ -40,6 +41,7 @@ __export(index_exports, {
|
|
|
40
41
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
41
42
|
MetricsRegistry: () => MetricsRegistry,
|
|
42
43
|
PROMETHEUS_CONTENT_TYPE: () => PROMETHEUS_CONTENT_TYPE,
|
|
44
|
+
anthropicMessagesToResponsesRequest: () => anthropicMessagesToResponsesRequest,
|
|
43
45
|
applyCopilotHeaders: () => applyCopilotHeaders,
|
|
44
46
|
applyGithubApiHeaders: () => applyGithubApiHeaders,
|
|
45
47
|
authStorePath: () => authStorePath,
|
|
@@ -49,6 +51,7 @@ __export(index_exports, {
|
|
|
49
51
|
completionsRequestToChatCompletion: () => completionsRequestToChatCompletion,
|
|
50
52
|
createHoopilotHandler: () => createHoopilotHandler,
|
|
51
53
|
createHoopilotLogger: () => createHoopilotLogger,
|
|
54
|
+
estimateAnthropicMessageTokens: () => estimateAnthropicMessageTokens,
|
|
52
55
|
extractTokenUsage: () => extractTokenUsage,
|
|
53
56
|
fallbackModels: () => fallbackModels,
|
|
54
57
|
githubCopilotDeviceLogin: () => githubCopilotDeviceLogin,
|
|
@@ -62,16 +65,14 @@ __export(index_exports, {
|
|
|
62
65
|
parseLogLevel: () => parseLogLevel,
|
|
63
66
|
readStoredCopilotAuth: () => readStoredCopilotAuth,
|
|
64
67
|
responsesRequestToChatCompletion: () => responsesRequestToChatCompletion,
|
|
68
|
+
responsesResponseToAnthropicMessage: () => responsesResponseToAnthropicMessage,
|
|
65
69
|
responsesStreamFromChatStream: () => responsesStreamFromChatStream,
|
|
70
|
+
responsesStreamToAnthropicStream: () => responsesStreamToAnthropicStream,
|
|
66
71
|
startHoopilotServer: () => startHoopilotServer,
|
|
67
72
|
writeStoredCopilotAuth: () => writeStoredCopilotAuth
|
|
68
73
|
});
|
|
69
74
|
module.exports = __toCommonJS(index_exports);
|
|
70
75
|
|
|
71
|
-
// src/auth-store.ts
|
|
72
|
-
var import_node_fs = require("fs");
|
|
73
|
-
var import_node_path = require("path");
|
|
74
|
-
|
|
75
76
|
// src/util.ts
|
|
76
77
|
function trimTrailingSlash(value) {
|
|
77
78
|
return value.replace(/\/+$/, "");
|
|
@@ -120,633 +121,786 @@ function asRecord(value) {
|
|
|
120
121
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
// src/
|
|
124
|
-
var
|
|
124
|
+
// src/openai.ts
|
|
125
|
+
var DEFAULT_MODEL = "gpt-4.1";
|
|
126
|
+
var OpenAICompatibilityError = class extends Error {
|
|
125
127
|
constructor(message) {
|
|
126
128
|
super(message);
|
|
127
|
-
this.name = "
|
|
129
|
+
this.name = "OpenAICompatibilityError";
|
|
128
130
|
}
|
|
129
131
|
};
|
|
130
|
-
function
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const xdg = envValue(env.XDG_CONFIG_HOME);
|
|
136
|
-
if (xdg) {
|
|
137
|
-
return (0, import_node_path.join)(xdg, "hoopilot", "auth.json");
|
|
138
|
-
}
|
|
139
|
-
const appdata = envValue(env.APPDATA);
|
|
140
|
-
if (appdata) {
|
|
141
|
-
return (0, import_node_path.join)(appdata, "hoopilot", "auth.json");
|
|
132
|
+
function responsesRequestToChatCompletion(request) {
|
|
133
|
+
const messages = [];
|
|
134
|
+
const instructions = contentToText(request.instructions);
|
|
135
|
+
if (instructions) {
|
|
136
|
+
messages.push({ content: instructions, role: "system" });
|
|
142
137
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
throw new StoredCopilotAuthError(
|
|
146
|
-
"Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
|
|
147
|
-
);
|
|
138
|
+
for (const message of inputToMessages(request.input)) {
|
|
139
|
+
messages.push(message);
|
|
148
140
|
}
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
return removeUndefined({
|
|
142
|
+
frequency_penalty: request.frequency_penalty,
|
|
143
|
+
max_tokens: request.max_output_tokens ?? request.max_tokens,
|
|
144
|
+
messages,
|
|
145
|
+
metadata: request.metadata,
|
|
146
|
+
model: normalizeRequestedModel(request.model),
|
|
147
|
+
presence_penalty: request.presence_penalty,
|
|
148
|
+
reasoning_effort: asRecord(request.reasoning).effort,
|
|
149
|
+
response_format: asRecord(request.text).format,
|
|
150
|
+
seed: request.seed,
|
|
151
|
+
stream: request.stream === true,
|
|
152
|
+
temperature: request.temperature,
|
|
153
|
+
tool_choice: chatToolChoice(request.tool_choice),
|
|
154
|
+
tools: chatTools(request.tools),
|
|
155
|
+
top_p: request.top_p
|
|
156
|
+
});
|
|
151
157
|
}
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
function normalizeChatCompletionRequest(request) {
|
|
159
|
+
return removeUndefined({
|
|
160
|
+
...request,
|
|
161
|
+
model: normalizeRequestedModel(request.model)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function completionsRequestToChatCompletion(request) {
|
|
165
|
+
assertSupportedLegacyCompletionRequest(request);
|
|
166
|
+
return removeUndefined({
|
|
167
|
+
frequency_penalty: request.frequency_penalty,
|
|
168
|
+
logit_bias: request.logit_bias,
|
|
169
|
+
max_tokens: request.max_tokens,
|
|
170
|
+
messages: [{ content: legacyPromptToText(request.prompt), role: "user" }],
|
|
171
|
+
model: normalizeRequestedModel(request.model),
|
|
172
|
+
n: request.n,
|
|
173
|
+
presence_penalty: request.presence_penalty,
|
|
174
|
+
seed: request.seed,
|
|
175
|
+
stop: request.stop,
|
|
176
|
+
stream: request.stream === true,
|
|
177
|
+
stream_options: request.stream_options,
|
|
178
|
+
temperature: request.temperature,
|
|
179
|
+
top_p: request.top_p,
|
|
180
|
+
user: request.user
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function normalizeRequestedModel(model) {
|
|
184
|
+
const requested = contentToText(model).trim();
|
|
185
|
+
return requested || DEFAULT_MODEL;
|
|
186
|
+
}
|
|
187
|
+
function chatCompletionToResponse(completion, responseId) {
|
|
188
|
+
const id = responseId ?? `resp_${randomId()}`;
|
|
189
|
+
const choice = firstChoice(completion);
|
|
190
|
+
const message = asRecord(choice.message);
|
|
191
|
+
const model = contentToText(completion.model) || DEFAULT_MODEL;
|
|
192
|
+
const output = outputItemsFromMessage(message);
|
|
193
|
+
const usage = responseUsage(completion.usage);
|
|
194
|
+
return removeUndefined({
|
|
195
|
+
created_at: epochSeconds(),
|
|
196
|
+
error: null,
|
|
197
|
+
id,
|
|
198
|
+
incomplete_details: null,
|
|
199
|
+
instructions: null,
|
|
200
|
+
max_output_tokens: null,
|
|
201
|
+
metadata: {},
|
|
202
|
+
model,
|
|
203
|
+
object: "response",
|
|
204
|
+
output,
|
|
205
|
+
output_text: outputText(output),
|
|
206
|
+
parallel_tool_calls: true,
|
|
207
|
+
status: "completed",
|
|
208
|
+
temperature: null,
|
|
209
|
+
tool_choice: "auto",
|
|
210
|
+
tools: [],
|
|
211
|
+
top_p: null,
|
|
212
|
+
usage
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function chatCompletionToCompletion(completion) {
|
|
216
|
+
return removeUndefined({
|
|
217
|
+
choices: completionChoices(completion).map((choice, index) => {
|
|
218
|
+
const message = asRecord(choice.message);
|
|
219
|
+
return {
|
|
220
|
+
finish_reason: choice.finish_reason ?? "stop",
|
|
221
|
+
index: typeof choice.index === "number" ? choice.index : index,
|
|
222
|
+
logprobs: choice.logprobs ?? null,
|
|
223
|
+
text: contentToText(choice.text) || contentToText(message.content)
|
|
224
|
+
};
|
|
225
|
+
}),
|
|
226
|
+
created: completion.created ?? epochSeconds(),
|
|
227
|
+
id: completion.id ?? `cmpl_${randomId()}`,
|
|
228
|
+
model: completion.model ?? DEFAULT_MODEL,
|
|
229
|
+
object: "text_completion",
|
|
230
|
+
system_fingerprint: completion.system_fingerprint,
|
|
231
|
+
usage: completion.usage
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function completionStreamFromChatStream(chatStream) {
|
|
235
|
+
const encoder = new TextEncoder();
|
|
236
|
+
const decoder = new TextDecoder();
|
|
237
|
+
let buffer = "";
|
|
238
|
+
let sawTerminalEvent = false;
|
|
239
|
+
return new ReadableStream({
|
|
240
|
+
async start(controller) {
|
|
241
|
+
const enqueue = (data) => {
|
|
242
|
+
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
243
|
+
};
|
|
244
|
+
const markTerminal = () => {
|
|
245
|
+
sawTerminalEvent = true;
|
|
246
|
+
};
|
|
247
|
+
const reader = chatStream.getReader();
|
|
248
|
+
try {
|
|
249
|
+
while (true) {
|
|
250
|
+
const result = await reader.read();
|
|
251
|
+
if (result.done) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
255
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
256
|
+
buffer = blocks.pop() ?? "";
|
|
257
|
+
for (const block of blocks) {
|
|
258
|
+
processCompletionSseBlock(block, enqueue, markTerminal);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
262
|
+
if (tail.trim()) {
|
|
263
|
+
processCompletionSseBlock(tail, enqueue, markTerminal);
|
|
264
|
+
}
|
|
265
|
+
if (!sawTerminalEvent) {
|
|
266
|
+
enqueue("[DONE]");
|
|
267
|
+
}
|
|
268
|
+
controller.close();
|
|
269
|
+
} catch (error) {
|
|
270
|
+
await reader.cancel(error).catch(() => {
|
|
271
|
+
});
|
|
272
|
+
controller.error(error);
|
|
273
|
+
} finally {
|
|
274
|
+
reader.releaseLock();
|
|
275
|
+
}
|
|
159
276
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
172
|
-
}
|
|
173
|
-
const record = parsed;
|
|
174
|
-
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
175
|
-
if (!token) {
|
|
176
|
-
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
177
|
-
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function normalizeModelsResponse(upstream) {
|
|
280
|
+
const record = asRecord(upstream);
|
|
281
|
+
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
282
|
+
const models = data.map((model) => asRecord(model)).filter((model) => typeof model.id === "string").map((model) => ({
|
|
283
|
+
created: model.created ?? 0,
|
|
284
|
+
id: model.id,
|
|
285
|
+
object: "model",
|
|
286
|
+
owned_by: model.owned_by ?? "github-copilot"
|
|
287
|
+
}));
|
|
178
288
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
182
|
-
source: typeof record.source === "string" ? record.source : void 0,
|
|
183
|
-
token
|
|
289
|
+
data: models.length > 0 ? models : fallbackModels(),
|
|
290
|
+
object: "list"
|
|
184
291
|
};
|
|
185
292
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
const data = `${JSON.stringify(
|
|
293
|
+
function fallbackModels() {
|
|
294
|
+
return [
|
|
189
295
|
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
`;
|
|
197
|
-
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
198
|
-
(0, import_node_fs.writeFileSync)(tmpPath, data, { mode: 384 });
|
|
199
|
-
(0, import_node_fs.renameSync)(tmpPath, path);
|
|
200
|
-
try {
|
|
201
|
-
(0, import_node_fs.chmodSync)(path, 384);
|
|
202
|
-
} catch {
|
|
203
|
-
}
|
|
296
|
+
created: 0,
|
|
297
|
+
id: DEFAULT_MODEL,
|
|
298
|
+
object: "model",
|
|
299
|
+
owned_by: "github-copilot"
|
|
300
|
+
}
|
|
301
|
+
];
|
|
204
302
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return this.#cachedAccess;
|
|
233
|
-
}
|
|
234
|
-
let stored;
|
|
235
|
-
try {
|
|
236
|
-
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (error instanceof StoredCopilotAuthError) {
|
|
239
|
-
throw new CopilotAuthError(error.message);
|
|
240
|
-
}
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
if (stored) {
|
|
244
|
-
return this.#cacheAccess({
|
|
245
|
-
apiBaseUrl: trimTrailingSlash(
|
|
246
|
-
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
247
|
-
),
|
|
248
|
-
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
249
|
-
source: "github-copilot-oauth",
|
|
250
|
-
token: stored.token
|
|
303
|
+
function responsesStreamFromChatStream(chatStream, options) {
|
|
304
|
+
const encoder = new TextEncoder();
|
|
305
|
+
const decoder = new TextDecoder();
|
|
306
|
+
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
307
|
+
const messageId = `msg_${randomId()}`;
|
|
308
|
+
const createdAt = epochSeconds();
|
|
309
|
+
let buffer = "";
|
|
310
|
+
let text = "";
|
|
311
|
+
let messageOutputIndex;
|
|
312
|
+
let nextOutputIndex = 0;
|
|
313
|
+
let sequenceNumber = 0;
|
|
314
|
+
const tools = /* @__PURE__ */ new Map();
|
|
315
|
+
return new ReadableStream({
|
|
316
|
+
async start(controller) {
|
|
317
|
+
const enqueue = (event, data) => {
|
|
318
|
+
controller.enqueue(
|
|
319
|
+
encoder.encode(
|
|
320
|
+
encodeSse(
|
|
321
|
+
event,
|
|
322
|
+
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
enqueue("response.created", {
|
|
328
|
+
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
329
|
+
type: "response.created"
|
|
251
330
|
});
|
|
331
|
+
const ensureMessageStarted = () => {
|
|
332
|
+
if (messageOutputIndex !== void 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
messageOutputIndex = nextOutputIndex++;
|
|
336
|
+
enqueue("response.output_item.added", {
|
|
337
|
+
item: {
|
|
338
|
+
content: [],
|
|
339
|
+
id: messageId,
|
|
340
|
+
role: "assistant",
|
|
341
|
+
status: "in_progress",
|
|
342
|
+
type: "message"
|
|
343
|
+
},
|
|
344
|
+
output_index: messageOutputIndex,
|
|
345
|
+
type: "response.output_item.added"
|
|
346
|
+
});
|
|
347
|
+
enqueue("response.content_part.added", {
|
|
348
|
+
content_index: 0,
|
|
349
|
+
item_id: messageId,
|
|
350
|
+
output_index: messageOutputIndex,
|
|
351
|
+
part: {
|
|
352
|
+
annotations: [],
|
|
353
|
+
text: "",
|
|
354
|
+
type: "output_text"
|
|
355
|
+
},
|
|
356
|
+
type: "response.content_part.added"
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
const appendText = (delta) => {
|
|
360
|
+
ensureMessageStarted();
|
|
361
|
+
text += delta;
|
|
362
|
+
enqueue("response.output_text.delta", {
|
|
363
|
+
content_index: 0,
|
|
364
|
+
delta,
|
|
365
|
+
item_id: messageId,
|
|
366
|
+
output_index: messageOutputIndex ?? 0,
|
|
367
|
+
type: "response.output_text.delta"
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
const appendToolCall = (toolCall) => {
|
|
371
|
+
const fn = asRecord(toolCall.function);
|
|
372
|
+
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
373
|
+
let existing = tools.get(index);
|
|
374
|
+
const isNew = !existing;
|
|
375
|
+
existing ??= {
|
|
376
|
+
arguments: "",
|
|
377
|
+
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
378
|
+
index,
|
|
379
|
+
itemId: `fc_${randomId()}`,
|
|
380
|
+
name: "",
|
|
381
|
+
outputIndex: nextOutputIndex++
|
|
382
|
+
};
|
|
383
|
+
existing.id = contentToText(toolCall.id) || existing.id;
|
|
384
|
+
existing.name += contentToText(fn.name);
|
|
385
|
+
tools.set(index, existing);
|
|
386
|
+
if (isNew) {
|
|
387
|
+
enqueue("response.output_item.added", {
|
|
388
|
+
item: functionCallItem(existing, "in_progress"),
|
|
389
|
+
output_index: existing.outputIndex ?? 0,
|
|
390
|
+
type: "response.output_item.added"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const argumentDelta = contentToText(fn.arguments);
|
|
394
|
+
if (argumentDelta) {
|
|
395
|
+
existing.arguments += argumentDelta;
|
|
396
|
+
enqueue("response.function_call_arguments.delta", {
|
|
397
|
+
delta: argumentDelta,
|
|
398
|
+
item_id: existing.itemId,
|
|
399
|
+
output_index: existing.outputIndex ?? 0,
|
|
400
|
+
type: "response.function_call_arguments.delta"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const reader = chatStream.getReader();
|
|
405
|
+
try {
|
|
406
|
+
while (true) {
|
|
407
|
+
const result = await reader.read();
|
|
408
|
+
if (result.done) {
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
412
|
+
const lines = buffer.split(/\r?\n/);
|
|
413
|
+
buffer = lines.pop() ?? "";
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
processChatSseLine(line, { appendText, appendToolCall });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (buffer) {
|
|
419
|
+
processChatSseLine(buffer, { appendText, appendToolCall });
|
|
420
|
+
}
|
|
421
|
+
const outputEntries = [];
|
|
422
|
+
if (messageOutputIndex !== void 0) {
|
|
423
|
+
const item = messageOutputItem(text, messageId);
|
|
424
|
+
outputEntries.push([messageOutputIndex, item]);
|
|
425
|
+
enqueue("response.output_text.done", {
|
|
426
|
+
content_index: 0,
|
|
427
|
+
item_id: messageId,
|
|
428
|
+
output_index: messageOutputIndex,
|
|
429
|
+
text,
|
|
430
|
+
type: "response.output_text.done"
|
|
431
|
+
});
|
|
432
|
+
enqueue("response.content_part.done", {
|
|
433
|
+
content_index: 0,
|
|
434
|
+
item_id: messageId,
|
|
435
|
+
output_index: messageOutputIndex,
|
|
436
|
+
part: {
|
|
437
|
+
annotations: [],
|
|
438
|
+
text,
|
|
439
|
+
type: "output_text"
|
|
440
|
+
},
|
|
441
|
+
type: "response.content_part.done"
|
|
442
|
+
});
|
|
443
|
+
enqueue("response.output_item.done", {
|
|
444
|
+
item,
|
|
445
|
+
output_index: messageOutputIndex,
|
|
446
|
+
type: "response.output_item.done"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
for (const tool of [...tools.values()].sort(
|
|
450
|
+
(a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
|
|
451
|
+
)) {
|
|
452
|
+
const item = functionCallItem(tool);
|
|
453
|
+
const outputIndex = tool.outputIndex ?? 0;
|
|
454
|
+
outputEntries.push([outputIndex, item]);
|
|
455
|
+
enqueue("response.function_call_arguments.done", {
|
|
456
|
+
arguments: tool.arguments,
|
|
457
|
+
item_id: item.id,
|
|
458
|
+
output_index: outputIndex,
|
|
459
|
+
type: "response.function_call_arguments.done"
|
|
460
|
+
});
|
|
461
|
+
enqueue("response.output_item.done", {
|
|
462
|
+
item,
|
|
463
|
+
output_index: outputIndex,
|
|
464
|
+
type: "response.output_item.done"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const output = outputEntries.sort(([left], [right]) => left - right).map(([, item]) => item);
|
|
468
|
+
enqueue("response.completed", {
|
|
469
|
+
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
470
|
+
type: "response.completed"
|
|
471
|
+
});
|
|
472
|
+
enqueue("done", "[DONE]");
|
|
473
|
+
controller.close();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
await reader.cancel(error).catch(() => {
|
|
476
|
+
});
|
|
477
|
+
controller.error(error);
|
|
478
|
+
} finally {
|
|
479
|
+
reader.releaseLock();
|
|
480
|
+
}
|
|
252
481
|
}
|
|
253
|
-
|
|
254
|
-
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
#cacheAccess(access) {
|
|
258
|
-
this.#cachedAccess = access;
|
|
259
|
-
return access;
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// src/copilot.ts
|
|
264
|
-
var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
265
|
-
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
266
|
-
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
267
|
-
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
268
|
-
function applyCopilotHeaders(headers, token) {
|
|
269
|
-
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
270
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
271
|
-
headers.set("copilot-integration-id", "vscode-chat");
|
|
272
|
-
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
273
|
-
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
274
|
-
headers.set("openai-intent", "conversation-panel");
|
|
275
|
-
headers.set("user-agent", "hoopilot/0.1.0");
|
|
276
|
-
headers.set("x-github-api-version", "2026-06-01");
|
|
277
|
-
return headers;
|
|
278
|
-
}
|
|
279
|
-
function applyGithubApiHeaders(headers, token) {
|
|
280
|
-
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
281
|
-
headers.set("authorization", `token ${token}`);
|
|
282
|
-
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
283
|
-
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
284
|
-
headers.set("user-agent", "hoopilot/0.1.0");
|
|
285
|
-
headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
|
|
286
|
-
return headers;
|
|
482
|
+
});
|
|
287
483
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
#fetch;
|
|
292
|
-
#githubApiBaseUrl;
|
|
293
|
-
constructor(options = {}) {
|
|
294
|
-
this.#auth = new CopilotAuth(options);
|
|
295
|
-
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
296
|
-
this.#fetch = options.fetch ?? fetch;
|
|
297
|
-
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
298
|
-
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Fetch the Copilot account's quota / premium-request usage from the GitHub
|
|
303
|
-
* REST `copilot_internal/user` endpoint. The stored device-flow OAuth token is
|
|
304
|
-
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
305
|
-
*/
|
|
306
|
-
async usage(signal) {
|
|
307
|
-
if (!isTrustedTokenBaseUrl(
|
|
308
|
-
this.#githubApiBaseUrl,
|
|
309
|
-
ALLOWED_GITHUB_API_HOSTS,
|
|
310
|
-
this.#allowUnsafeUpstream
|
|
311
|
-
)) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
const access = await this.#auth.getAccess();
|
|
317
|
-
const headers = applyGithubApiHeaders(new Headers(), access.token);
|
|
318
|
-
return this.#fetch(`${this.#githubApiBaseUrl}/copilot_internal/user`, {
|
|
319
|
-
headers,
|
|
320
|
-
method: "GET",
|
|
321
|
-
signal
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
async chatCompletions(body, signal) {
|
|
325
|
-
return this.fetchCopilot("/chat/completions", {
|
|
326
|
-
body: JSON.stringify(body),
|
|
327
|
-
headers: {
|
|
328
|
-
"content-type": "application/json"
|
|
329
|
-
},
|
|
330
|
-
method: "POST",
|
|
331
|
-
signal
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
async responses(body, signal) {
|
|
335
|
-
return this.fetchCopilot("/responses", {
|
|
336
|
-
body,
|
|
337
|
-
headers: {
|
|
338
|
-
"content-type": "application/json"
|
|
339
|
-
},
|
|
340
|
-
method: "POST",
|
|
341
|
-
signal
|
|
342
|
-
});
|
|
484
|
+
function inputToMessages(input) {
|
|
485
|
+
if (typeof input === "string") {
|
|
486
|
+
return [{ content: input, role: "user" }];
|
|
343
487
|
}
|
|
344
|
-
|
|
345
|
-
return
|
|
346
|
-
headers: {
|
|
347
|
-
accept: "application/json"
|
|
348
|
-
},
|
|
349
|
-
method: "GET",
|
|
350
|
-
signal
|
|
351
|
-
});
|
|
488
|
+
if (!Array.isArray(input)) {
|
|
489
|
+
return [];
|
|
352
490
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
);
|
|
491
|
+
const messages = [];
|
|
492
|
+
for (const item of input) {
|
|
493
|
+
const record = asRecord(item);
|
|
494
|
+
const type = contentToText(record.type);
|
|
495
|
+
if (type === "function_call_output") {
|
|
496
|
+
messages.push({
|
|
497
|
+
content: contentToText(record.output),
|
|
498
|
+
role: "tool",
|
|
499
|
+
tool_call_id: contentToText(record.call_id)
|
|
500
|
+
});
|
|
501
|
+
continue;
|
|
363
502
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
if (Object.keys(quotas).length === 0) {
|
|
379
|
-
const remaining = asRecord(record.limited_user_quotas);
|
|
380
|
-
const monthly = asRecord(record.monthly_quotas);
|
|
381
|
-
for (const category of /* @__PURE__ */ new Set([...Object.keys(remaining), ...Object.keys(monthly)])) {
|
|
382
|
-
const entitlement = numberOrUndefined(monthly[category]);
|
|
383
|
-
const left = numberOrUndefined(remaining[category]);
|
|
384
|
-
quotas[category] = removeUndefinedQuota({
|
|
385
|
-
entitlement,
|
|
386
|
-
percentRemaining: entitlement !== void 0 && entitlement > 0 && left !== void 0 ? left / entitlement * 100 : void 0,
|
|
387
|
-
remaining: left,
|
|
388
|
-
used: usedFrom(entitlement, left)
|
|
503
|
+
if (type === "function_call") {
|
|
504
|
+
messages.push({
|
|
505
|
+
role: "assistant",
|
|
506
|
+
tool_calls: [
|
|
507
|
+
{
|
|
508
|
+
function: {
|
|
509
|
+
arguments: contentToText(record.arguments),
|
|
510
|
+
name: contentToText(record.name)
|
|
511
|
+
},
|
|
512
|
+
id: contentToText(record.call_id) || contentToText(record.id),
|
|
513
|
+
type: "function"
|
|
514
|
+
}
|
|
515
|
+
]
|
|
389
516
|
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (type && type !== "message") {
|
|
520
|
+
unsupportedResponsesFeature(`input item type "${type}"`);
|
|
521
|
+
}
|
|
522
|
+
const role = responsesRoleToChatRole(contentToText(record.role));
|
|
523
|
+
const content = chatMessageContent(record.content);
|
|
524
|
+
if (role && content !== void 0) {
|
|
525
|
+
messages.push({ content, role });
|
|
390
526
|
}
|
|
391
527
|
}
|
|
392
|
-
return
|
|
393
|
-
accessTypeSku: stringOrUndefined(record.access_type_sku),
|
|
394
|
-
chatEnabled: typeof record.chat_enabled === "boolean" ? record.chat_enabled : void 0,
|
|
395
|
-
plan: stringOrUndefined(record.copilot_plan),
|
|
396
|
-
quotaResetDate: stringOrUndefined(record.quota_reset_date) ?? stringOrUndefined(record.quota_reset_date_utc) ?? stringOrUndefined(record.limited_user_reset_date),
|
|
397
|
-
quotas
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
function normalizeQuotaDetail(detail) {
|
|
401
|
-
const entitlement = numberOrUndefined(detail.entitlement);
|
|
402
|
-
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
403
|
-
return removeUndefinedQuota({
|
|
404
|
-
entitlement,
|
|
405
|
-
overageCount: numberOrUndefined(detail.overage_count),
|
|
406
|
-
overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
|
|
407
|
-
percentRemaining: numberOrUndefined(detail.percent_remaining),
|
|
408
|
-
remaining,
|
|
409
|
-
unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
|
|
410
|
-
used: usedFrom(entitlement, remaining)
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
function usedFrom(entitlement, remaining) {
|
|
414
|
-
if (entitlement === void 0 || remaining === void 0) {
|
|
415
|
-
return void 0;
|
|
416
|
-
}
|
|
417
|
-
return Math.max(0, entitlement - remaining);
|
|
418
|
-
}
|
|
419
|
-
function numberOrUndefined(value) {
|
|
420
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
421
|
-
}
|
|
422
|
-
function stringOrUndefined(value) {
|
|
423
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
424
|
-
}
|
|
425
|
-
function removeUndefinedQuota(quota) {
|
|
426
|
-
return Object.fromEntries(
|
|
427
|
-
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
function removeUndefinedUsage(usage) {
|
|
431
|
-
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
432
|
-
return Object.fromEntries(entries);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/github-device.ts
|
|
436
|
-
var import_promises = require("timers/promises");
|
|
437
|
-
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
438
|
-
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
439
|
-
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
440
|
-
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
441
|
-
var REQUEST_TIMEOUT_MS = 15e3;
|
|
442
|
-
async function githubCopilotDeviceLogin(options = {}) {
|
|
443
|
-
const env = options.env ?? process.env;
|
|
444
|
-
const fetcher = options.fetch ?? fetch;
|
|
445
|
-
const sleeper = options.sleep ?? import_promises.setTimeout;
|
|
446
|
-
const domain = normalizeDomain(
|
|
447
|
-
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
448
|
-
);
|
|
449
|
-
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
450
|
-
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
451
|
-
const verificationUrl = device.verification_uri;
|
|
452
|
-
const userCode = device.user_code;
|
|
453
|
-
const deviceCode = device.device_code;
|
|
454
|
-
if (!verificationUrl || !userCode || !deviceCode) {
|
|
455
|
-
throw new Error("GitHub device authorization response is missing required fields.");
|
|
456
|
-
}
|
|
457
|
-
options.logger?.info(`First copy your one-time code: ${userCode}`);
|
|
458
|
-
options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
|
|
459
|
-
await options.openBrowser?.(verificationUrl);
|
|
460
|
-
return {
|
|
461
|
-
domain,
|
|
462
|
-
token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
|
|
463
|
-
deviceCode,
|
|
464
|
-
expiresIn: positiveSeconds(device.expires_in, 900),
|
|
465
|
-
interval: positiveSeconds(device.interval, 5)
|
|
466
|
-
})
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
async function requestDeviceCode(fetcher, domain, clientId) {
|
|
470
|
-
const response = await fetcher(`https://${domain}/login/device/code`, {
|
|
471
|
-
body: JSON.stringify({
|
|
472
|
-
client_id: clientId,
|
|
473
|
-
scope: "read:user"
|
|
474
|
-
}),
|
|
475
|
-
headers: oauthHeaders(),
|
|
476
|
-
method: "POST",
|
|
477
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
478
|
-
});
|
|
479
|
-
if (!response.ok) {
|
|
480
|
-
throw new Error(
|
|
481
|
-
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
482
|
-
response
|
|
483
|
-
)}`
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
return parseJsonResponse(
|
|
487
|
-
response,
|
|
488
|
-
"GitHub device authorization response was not valid JSON"
|
|
489
|
-
);
|
|
528
|
+
return messages;
|
|
490
529
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
client_id: clientId,
|
|
499
|
-
device_code: device.deviceCode,
|
|
500
|
-
grant_type: DEVICE_GRANT_TYPE
|
|
501
|
-
}),
|
|
502
|
-
headers: oauthHeaders(),
|
|
503
|
-
method: "POST",
|
|
504
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
505
|
-
});
|
|
506
|
-
if (!response.ok) {
|
|
507
|
-
throw new Error(
|
|
508
|
-
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
509
|
-
response
|
|
510
|
-
)}`
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
const data = await parseJsonResponse(
|
|
514
|
-
response,
|
|
515
|
-
"GitHub device token response was not valid JSON"
|
|
516
|
-
);
|
|
517
|
-
if (data.access_token) {
|
|
518
|
-
return data.access_token;
|
|
530
|
+
function chatMessageContent(content) {
|
|
531
|
+
if (typeof content === "string") {
|
|
532
|
+
return content;
|
|
533
|
+
}
|
|
534
|
+
if (!Array.isArray(content)) {
|
|
535
|
+
if (content === void 0 || content === null) {
|
|
536
|
+
return void 0;
|
|
519
537
|
}
|
|
520
|
-
|
|
538
|
+
unsupportedResponsesFeature("non-array message content objects");
|
|
539
|
+
}
|
|
540
|
+
const parts = [];
|
|
541
|
+
for (const part of content) {
|
|
542
|
+
const record = asRecord(part);
|
|
543
|
+
const type = contentToText(record.type);
|
|
544
|
+
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
545
|
+
parts.push({ text: contentToText(record.text), type: "text" });
|
|
521
546
|
continue;
|
|
522
547
|
}
|
|
523
|
-
if (
|
|
524
|
-
|
|
548
|
+
if (type === "input_image") {
|
|
549
|
+
if (contentToText(record.file_id)) {
|
|
550
|
+
unsupportedResponsesFeature("input_image file_id parts");
|
|
551
|
+
}
|
|
552
|
+
const imageUrl = contentToText(record.image_url);
|
|
553
|
+
if (!imageUrl) {
|
|
554
|
+
unsupportedResponsesFeature("input_image parts without image_url");
|
|
555
|
+
}
|
|
556
|
+
const image = { url: imageUrl };
|
|
557
|
+
const detail = contentToText(record.detail);
|
|
558
|
+
if (detail) {
|
|
559
|
+
image.detail = detail;
|
|
560
|
+
}
|
|
561
|
+
parts.push({ image_url: image, type: "image_url" });
|
|
525
562
|
continue;
|
|
526
563
|
}
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
if (data.error === "access_denied") {
|
|
531
|
-
throw new Error("GitHub device login was cancelled.");
|
|
564
|
+
if (type === "input_file") {
|
|
565
|
+
unsupportedResponsesFeature("input_file parts");
|
|
532
566
|
}
|
|
533
|
-
if (
|
|
534
|
-
|
|
567
|
+
if (type === "input_audio") {
|
|
568
|
+
unsupportedResponsesFeature("input_audio parts");
|
|
535
569
|
}
|
|
570
|
+
unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
|
|
536
571
|
}
|
|
537
|
-
|
|
572
|
+
if (parts.length === 0) {
|
|
573
|
+
return void 0;
|
|
574
|
+
}
|
|
575
|
+
if (parts.every((part) => part.type === "text")) {
|
|
576
|
+
return parts.map((part) => contentToText(part.text)).join("\n");
|
|
577
|
+
}
|
|
578
|
+
return parts;
|
|
538
579
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
580
|
+
function legacyPromptToText(prompt) {
|
|
581
|
+
if (typeof prompt === "string") {
|
|
582
|
+
return prompt;
|
|
583
|
+
}
|
|
584
|
+
if (Array.isArray(prompt) && prompt.length === 1 && typeof prompt[0] === "string") {
|
|
585
|
+
return prompt[0];
|
|
586
|
+
}
|
|
587
|
+
throw new OpenAICompatibilityError(
|
|
588
|
+
"Hoopilot legacy completions compatibility supports exactly one string prompt per request."
|
|
589
|
+
);
|
|
545
590
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
591
|
+
function assertSupportedLegacyCompletionRequest(request) {
|
|
592
|
+
if (request.echo === true) {
|
|
593
|
+
throw new OpenAICompatibilityError(
|
|
594
|
+
"Hoopilot legacy completions compatibility does not support echo=true."
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (typeof request.best_of === "number" && request.best_of > 1) {
|
|
598
|
+
throw new OpenAICompatibilityError(
|
|
599
|
+
"Hoopilot legacy completions compatibility does not support best_of greater than 1."
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (typeof request.logprobs === "number" && request.logprobs > 0) {
|
|
603
|
+
throw new OpenAICompatibilityError(
|
|
604
|
+
"Hoopilot legacy completions compatibility does not support legacy logprobs."
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (contentToText(request.suffix)) {
|
|
608
|
+
throw new OpenAICompatibilityError(
|
|
609
|
+
"Hoopilot legacy completions compatibility does not support suffix."
|
|
610
|
+
);
|
|
611
|
+
}
|
|
548
612
|
}
|
|
549
|
-
function
|
|
550
|
-
|
|
613
|
+
function contentToText(content) {
|
|
614
|
+
if (typeof content === "string") {
|
|
615
|
+
return content;
|
|
616
|
+
}
|
|
617
|
+
if (typeof content === "number" || typeof content === "boolean") {
|
|
618
|
+
return String(content);
|
|
619
|
+
}
|
|
620
|
+
if (Array.isArray(content)) {
|
|
621
|
+
return content.map((item) => contentToText(item)).filter(Boolean).join("\n");
|
|
622
|
+
}
|
|
623
|
+
if (content && typeof content === "object") {
|
|
624
|
+
const record = content;
|
|
625
|
+
if (typeof record.text === "string") {
|
|
626
|
+
return record.text;
|
|
627
|
+
}
|
|
628
|
+
if (typeof record.output_text === "string") {
|
|
629
|
+
return record.output_text;
|
|
630
|
+
}
|
|
631
|
+
return JSON.stringify(content);
|
|
632
|
+
}
|
|
633
|
+
return "";
|
|
551
634
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return JSON.parse(text);
|
|
556
|
-
} catch {
|
|
557
|
-
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
635
|
+
function responsesRoleToChatRole(role) {
|
|
636
|
+
if (!role) {
|
|
637
|
+
return "user";
|
|
558
638
|
}
|
|
639
|
+
if (role === "assistant" || role === "developer" || role === "system" || role === "tool" || role === "user") {
|
|
640
|
+
return role === "developer" ? "system" : role;
|
|
641
|
+
}
|
|
642
|
+
unsupportedResponsesFeature(`message role "${role}"`);
|
|
559
643
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
var import_pino_pretty = __toESM(require("pino-pretty"), 1);
|
|
564
|
-
var DEFAULT_LOG_FORMAT = "pretty";
|
|
565
|
-
var DEFAULT_LOG_LEVEL = "info";
|
|
566
|
-
var LOG_FORMATS = ["json", "pretty"];
|
|
567
|
-
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
568
|
-
var REDACT_PATHS = [
|
|
569
|
-
"apiKey",
|
|
570
|
-
"authorization",
|
|
571
|
-
"cookie",
|
|
572
|
-
"headers.authorization",
|
|
573
|
-
"headers.Authorization",
|
|
574
|
-
"headers.cookie",
|
|
575
|
-
"headers.Cookie",
|
|
576
|
-
"headers.x-api-key",
|
|
577
|
-
"headers.X-Api-Key",
|
|
578
|
-
"token",
|
|
579
|
-
"*.apiKey",
|
|
580
|
-
"*.authorization",
|
|
581
|
-
"*.cookie",
|
|
582
|
-
"*.token",
|
|
583
|
-
"*.headers.authorization",
|
|
584
|
-
"*.headers.Authorization",
|
|
585
|
-
"*.headers.cookie",
|
|
586
|
-
"*.headers.Cookie",
|
|
587
|
-
"*.headers.x-api-key",
|
|
588
|
-
"*.headers.X-Api-Key"
|
|
589
|
-
];
|
|
590
|
-
var noopLogger = {
|
|
591
|
-
child: () => noopLogger,
|
|
592
|
-
debug: () => {
|
|
593
|
-
},
|
|
594
|
-
error: () => {
|
|
595
|
-
},
|
|
596
|
-
fatal: () => {
|
|
597
|
-
},
|
|
598
|
-
info: () => {
|
|
599
|
-
},
|
|
600
|
-
trace: () => {
|
|
601
|
-
},
|
|
602
|
-
warn: () => {
|
|
644
|
+
function chatTools(tools) {
|
|
645
|
+
if (!Array.isArray(tools)) {
|
|
646
|
+
return void 0;
|
|
603
647
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
colorize: options.colorize ?? process.stderr.isTTY,
|
|
626
|
-
destination: options.stream ?? 1,
|
|
627
|
-
ignore: "pid,hostname",
|
|
628
|
-
singleLine: true,
|
|
629
|
-
translateTime: "SYS:standard"
|
|
630
|
-
})
|
|
631
|
-
);
|
|
648
|
+
const converted = tools.map((tool) => {
|
|
649
|
+
const record = asRecord(tool);
|
|
650
|
+
const type = contentToText(record.type);
|
|
651
|
+
if (type !== "function") {
|
|
652
|
+
unsupportedResponsesFeature(`tool type "${type || "unknown"}"`);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
function: removeUndefined({
|
|
656
|
+
description: record.description,
|
|
657
|
+
name: record.name,
|
|
658
|
+
parameters: record.parameters,
|
|
659
|
+
strict: record.strict
|
|
660
|
+
}),
|
|
661
|
+
type: "function"
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
return converted.length > 0 ? converted : void 0;
|
|
665
|
+
}
|
|
666
|
+
function chatToolChoice(toolChoice) {
|
|
667
|
+
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
668
|
+
return toolChoice;
|
|
632
669
|
}
|
|
633
|
-
|
|
634
|
-
|
|
670
|
+
const record = asRecord(toolChoice);
|
|
671
|
+
const type = contentToText(record.type);
|
|
672
|
+
if (type === "function" && typeof record.name === "string") {
|
|
673
|
+
return { function: { name: record.name }, type: "function" };
|
|
635
674
|
}
|
|
636
|
-
|
|
675
|
+
unsupportedResponsesFeature(`tool_choice type "${type || "unknown"}"`);
|
|
637
676
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
677
|
+
function unsupportedResponsesFeature(feature) {
|
|
678
|
+
throw new OpenAICompatibilityError(
|
|
679
|
+
`Hoopilot Responses-to-chat compatibility does not support ${feature}.`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
function outputItemsFromMessage(message) {
|
|
683
|
+
const output = [];
|
|
684
|
+
const text = contentToText(message.content);
|
|
685
|
+
if (text) {
|
|
686
|
+
output.push(messageOutputItem(text));
|
|
641
687
|
}
|
|
642
|
-
|
|
643
|
-
|
|
688
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
689
|
+
for (const toolCall of toolCalls) {
|
|
690
|
+
const record = asRecord(toolCall);
|
|
691
|
+
const fn = asRecord(record.function);
|
|
692
|
+
output.push(
|
|
693
|
+
functionCallItem({
|
|
694
|
+
arguments: contentToText(fn.arguments),
|
|
695
|
+
id: contentToText(record.id) || `call_${randomId()}`,
|
|
696
|
+
index: output.length,
|
|
697
|
+
name: contentToText(fn.name)
|
|
698
|
+
})
|
|
699
|
+
);
|
|
644
700
|
}
|
|
645
|
-
|
|
701
|
+
return output;
|
|
646
702
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
703
|
+
function messageOutputItem(text, id = `msg_${randomId()}`) {
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{
|
|
707
|
+
annotations: [],
|
|
708
|
+
text,
|
|
709
|
+
type: "output_text"
|
|
710
|
+
}
|
|
711
|
+
],
|
|
712
|
+
id,
|
|
713
|
+
role: "assistant",
|
|
714
|
+
status: "completed",
|
|
715
|
+
type: "message"
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function functionCallItem(tool, status = "completed") {
|
|
719
|
+
return {
|
|
720
|
+
arguments: tool.arguments,
|
|
721
|
+
call_id: tool.id,
|
|
722
|
+
id: tool.itemId ?? `fc_${randomId()}`,
|
|
723
|
+
name: tool.name,
|
|
724
|
+
status,
|
|
725
|
+
type: "function_call"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function outputText(output) {
|
|
729
|
+
return output.flatMap((item) => {
|
|
730
|
+
const content = item.content;
|
|
731
|
+
return Array.isArray(content) ? content : [];
|
|
732
|
+
}).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
|
|
733
|
+
}
|
|
734
|
+
function responseUsage(usage) {
|
|
735
|
+
const record = asRecord(usage);
|
|
736
|
+
if (Object.keys(record).length === 0) {
|
|
737
|
+
return null;
|
|
650
738
|
}
|
|
651
|
-
|
|
652
|
-
|
|
739
|
+
const inputTokens = record.prompt_tokens;
|
|
740
|
+
const outputTokens = record.completion_tokens;
|
|
741
|
+
return removeUndefined({
|
|
742
|
+
input_tokens: inputTokens,
|
|
743
|
+
input_tokens_details: responseUsageDetails(record.prompt_tokens_details, inputTokens, {
|
|
744
|
+
cached_tokens: 0
|
|
745
|
+
}),
|
|
746
|
+
output_tokens: outputTokens,
|
|
747
|
+
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
748
|
+
reasoning_tokens: 0
|
|
749
|
+
}),
|
|
750
|
+
total_tokens: record.total_tokens
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function responseUsageDetails(value, tokenCount, fallback) {
|
|
754
|
+
const record = asRecord(value);
|
|
755
|
+
if (Object.keys(record).length > 0) {
|
|
756
|
+
return record;
|
|
653
757
|
}
|
|
654
|
-
|
|
758
|
+
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
655
759
|
}
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
760
|
+
function extractTokenUsage(usage) {
|
|
761
|
+
const record = asRecord(usage);
|
|
762
|
+
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
763
|
+
const completion = firstNumber(record.completion_tokens, record.output_tokens);
|
|
764
|
+
const total = firstNumber(record.total_tokens);
|
|
765
|
+
if (prompt === void 0 && completion === void 0 && total === void 0) {
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
const promptTokens = prompt ?? 0;
|
|
769
|
+
const completionTokens = completion ?? 0;
|
|
770
|
+
const reasoning = firstNumber(
|
|
771
|
+
asRecord(record.completion_tokens_details).reasoning_tokens,
|
|
772
|
+
asRecord(record.output_tokens_details).reasoning_tokens
|
|
773
|
+
);
|
|
774
|
+
const cached = firstNumber(
|
|
775
|
+
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
776
|
+
asRecord(record.input_tokens_details).cached_tokens
|
|
659
777
|
);
|
|
778
|
+
return removeUndefined({
|
|
779
|
+
cachedTokens: cached,
|
|
780
|
+
completionTokens,
|
|
781
|
+
promptTokens,
|
|
782
|
+
reasoningTokens: reasoning,
|
|
783
|
+
totalTokens: total ?? promptTokens + completionTokens
|
|
784
|
+
});
|
|
660
785
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
stack: error.stack
|
|
667
|
-
};
|
|
786
|
+
function firstNumber(...values) {
|
|
787
|
+
for (const value of values) {
|
|
788
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
668
791
|
}
|
|
669
|
-
return
|
|
792
|
+
return void 0;
|
|
670
793
|
}
|
|
671
|
-
function
|
|
672
|
-
return
|
|
794
|
+
function firstChoice(completion) {
|
|
795
|
+
return completionChoices(completion)[0] ?? {};
|
|
673
796
|
}
|
|
674
|
-
function
|
|
675
|
-
|
|
797
|
+
function completionChoices(completion) {
|
|
798
|
+
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
799
|
+
return choices.map((choice) => asRecord(choice));
|
|
676
800
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
801
|
+
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
802
|
+
let event = "message";
|
|
803
|
+
const dataLines = [];
|
|
804
|
+
for (const line of block.split(/\r?\n/)) {
|
|
805
|
+
const trimmed = line.trim();
|
|
806
|
+
if (trimmed.startsWith("event:")) {
|
|
807
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
808
|
+
} else if (trimmed.startsWith("data:")) {
|
|
809
|
+
dataLines.push(trimmed.slice("data:".length).trim());
|
|
810
|
+
}
|
|
684
811
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const instructions = contentToText(request.instructions);
|
|
689
|
-
if (instructions) {
|
|
690
|
-
messages.push({ content: instructions, role: "system" });
|
|
812
|
+
const data = dataLines.join("\n");
|
|
813
|
+
if (!data) {
|
|
814
|
+
return;
|
|
691
815
|
}
|
|
692
|
-
|
|
693
|
-
|
|
816
|
+
if (data === "[DONE]") {
|
|
817
|
+
markTerminal();
|
|
818
|
+
enqueue("[DONE]");
|
|
819
|
+
return;
|
|
694
820
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
user: request.user
|
|
735
|
-
});
|
|
821
|
+
const parsed = parseJson(data);
|
|
822
|
+
if (!parsed) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const error = completionStreamError(event, parsed);
|
|
826
|
+
if (error) {
|
|
827
|
+
markTerminal();
|
|
828
|
+
enqueue({ error });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const choices = completionChoices(parsed).map((choice, index) => {
|
|
832
|
+
const delta = asRecord(choice.delta);
|
|
833
|
+
const text = contentToText(delta.content);
|
|
834
|
+
const finishReason = choice.finish_reason ?? null;
|
|
835
|
+
if (!text && finishReason === null) {
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
finish_reason: finishReason,
|
|
840
|
+
index: typeof choice.index === "number" ? choice.index : index,
|
|
841
|
+
logprobs: choice.logprobs ?? null,
|
|
842
|
+
text
|
|
843
|
+
};
|
|
844
|
+
}).filter((choice) => choice !== void 0);
|
|
845
|
+
const usage = asRecord(parsed.usage);
|
|
846
|
+
const hasUsage = Object.keys(usage).length > 0;
|
|
847
|
+
if (choices.length === 0 && !hasUsage) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
enqueue(
|
|
851
|
+
removeUndefined({
|
|
852
|
+
choices,
|
|
853
|
+
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
854
|
+
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
855
|
+
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
856
|
+
object: "text_completion",
|
|
857
|
+
usage: hasUsage ? usage : void 0
|
|
858
|
+
})
|
|
859
|
+
);
|
|
736
860
|
}
|
|
737
|
-
function
|
|
738
|
-
const
|
|
739
|
-
|
|
861
|
+
function completionStreamError(event, parsed) {
|
|
862
|
+
const responseError = asRecord(asRecord(parsed.response).error);
|
|
863
|
+
const directError = asRecord(parsed.error);
|
|
864
|
+
const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
|
|
865
|
+
if (error) {
|
|
866
|
+
return error;
|
|
867
|
+
}
|
|
868
|
+
if (event === "error" || parsed.type === "response.failed") {
|
|
869
|
+
return removeUndefined({
|
|
870
|
+
code: contentToText(parsed.code) || void 0,
|
|
871
|
+
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
872
|
+
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
740
876
|
}
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
877
|
+
function processChatSseLine(line, handlers) {
|
|
878
|
+
const trimmed = line.trim();
|
|
879
|
+
if (!trimmed.startsWith("data:")) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const data = trimmed.slice("data:".length).trim();
|
|
883
|
+
if (!data || data === "[DONE]") {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const parsed = parseJson(data);
|
|
887
|
+
if (!parsed) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const choice = firstChoice(parsed);
|
|
891
|
+
const delta = asRecord(choice.delta);
|
|
892
|
+
const content = contentToText(delta.content);
|
|
893
|
+
if (content) {
|
|
894
|
+
handlers.appendText(content);
|
|
895
|
+
}
|
|
896
|
+
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
897
|
+
for (const toolCall of toolCalls) {
|
|
898
|
+
handlers.appendToolCall(asRecord(toolCall));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function baseStreamResponse(id, model, createdAt, status, output) {
|
|
902
|
+
return {
|
|
903
|
+
created_at: createdAt,
|
|
750
904
|
error: null,
|
|
751
905
|
id,
|
|
752
906
|
incomplete_details: null,
|
|
@@ -756,206 +910,106 @@ function chatCompletionToResponse(completion, responseId) {
|
|
|
756
910
|
model,
|
|
757
911
|
object: "response",
|
|
758
912
|
output,
|
|
759
|
-
output_text: outputText(output),
|
|
760
913
|
parallel_tool_calls: true,
|
|
761
|
-
status
|
|
914
|
+
status,
|
|
762
915
|
temperature: null,
|
|
763
916
|
tool_choice: "auto",
|
|
764
|
-
tools: [],
|
|
765
|
-
top_p: null
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
return
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const encoder = new TextEncoder();
|
|
859
|
-
const decoder = new TextDecoder();
|
|
860
|
-
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
861
|
-
const messageId = `msg_${randomId()}`;
|
|
862
|
-
const createdAt = epochSeconds();
|
|
863
|
-
let buffer = "";
|
|
864
|
-
let text = "";
|
|
865
|
-
let messageOutputIndex;
|
|
866
|
-
let nextOutputIndex = 0;
|
|
867
|
-
let sequenceNumber = 0;
|
|
868
|
-
const tools = /* @__PURE__ */ new Map();
|
|
869
|
-
return new ReadableStream({
|
|
870
|
-
async start(controller) {
|
|
871
|
-
const enqueue = (event, data) => {
|
|
872
|
-
controller.enqueue(
|
|
873
|
-
encoder.encode(
|
|
874
|
-
encodeSse(
|
|
875
|
-
event,
|
|
876
|
-
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
877
|
-
)
|
|
878
|
-
)
|
|
879
|
-
);
|
|
880
|
-
};
|
|
881
|
-
enqueue("response.created", {
|
|
882
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
883
|
-
type: "response.created"
|
|
884
|
-
});
|
|
885
|
-
const ensureMessageStarted = () => {
|
|
886
|
-
if (messageOutputIndex !== void 0) {
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
messageOutputIndex = nextOutputIndex++;
|
|
890
|
-
enqueue("response.output_item.added", {
|
|
891
|
-
item: {
|
|
892
|
-
content: [],
|
|
893
|
-
id: messageId,
|
|
894
|
-
role: "assistant",
|
|
895
|
-
status: "in_progress",
|
|
896
|
-
type: "message"
|
|
897
|
-
},
|
|
898
|
-
output_index: messageOutputIndex,
|
|
899
|
-
type: "response.output_item.added"
|
|
900
|
-
});
|
|
901
|
-
enqueue("response.content_part.added", {
|
|
902
|
-
content_index: 0,
|
|
903
|
-
item_id: messageId,
|
|
904
|
-
output_index: messageOutputIndex,
|
|
905
|
-
part: {
|
|
906
|
-
annotations: [],
|
|
907
|
-
text: "",
|
|
908
|
-
type: "output_text"
|
|
909
|
-
},
|
|
910
|
-
type: "response.content_part.added"
|
|
911
|
-
});
|
|
912
|
-
};
|
|
913
|
-
const appendText = (delta) => {
|
|
914
|
-
ensureMessageStarted();
|
|
915
|
-
text += delta;
|
|
916
|
-
enqueue("response.output_text.delta", {
|
|
917
|
-
content_index: 0,
|
|
918
|
-
delta,
|
|
919
|
-
item_id: messageId,
|
|
920
|
-
output_index: messageOutputIndex ?? 0,
|
|
921
|
-
type: "response.output_text.delta"
|
|
922
|
-
});
|
|
923
|
-
};
|
|
924
|
-
const appendToolCall = (toolCall) => {
|
|
925
|
-
const fn = asRecord(toolCall.function);
|
|
926
|
-
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
927
|
-
let existing = tools.get(index);
|
|
928
|
-
const isNew = !existing;
|
|
929
|
-
existing ??= {
|
|
930
|
-
arguments: "",
|
|
931
|
-
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
932
|
-
index,
|
|
933
|
-
itemId: `fc_${randomId()}`,
|
|
934
|
-
name: "",
|
|
935
|
-
outputIndex: nextOutputIndex++
|
|
936
|
-
};
|
|
937
|
-
existing.id = contentToText(toolCall.id) || existing.id;
|
|
938
|
-
existing.name += contentToText(fn.name);
|
|
939
|
-
tools.set(index, existing);
|
|
940
|
-
if (isNew) {
|
|
941
|
-
enqueue("response.output_item.added", {
|
|
942
|
-
item: functionCallItem(existing, "in_progress"),
|
|
943
|
-
output_index: existing.outputIndex ?? 0,
|
|
944
|
-
type: "response.output_item.added"
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
const argumentDelta = contentToText(fn.arguments);
|
|
948
|
-
if (argumentDelta) {
|
|
949
|
-
existing.arguments += argumentDelta;
|
|
950
|
-
enqueue("response.function_call_arguments.delta", {
|
|
951
|
-
delta: argumentDelta,
|
|
952
|
-
item_id: existing.itemId,
|
|
953
|
-
output_index: existing.outputIndex ?? 0,
|
|
954
|
-
type: "response.function_call_arguments.delta"
|
|
955
|
-
});
|
|
956
|
-
}
|
|
917
|
+
tools: [],
|
|
918
|
+
top_p: null
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function encodeSse(event, data) {
|
|
922
|
+
if (data === "[DONE]") {
|
|
923
|
+
return "data: [DONE]\n\n";
|
|
924
|
+
}
|
|
925
|
+
return `event: ${event}
|
|
926
|
+
data: ${JSON.stringify(data)}
|
|
927
|
+
|
|
928
|
+
`;
|
|
929
|
+
}
|
|
930
|
+
function encodeDataSse(data) {
|
|
931
|
+
if (data === "[DONE]") {
|
|
932
|
+
return "data: [DONE]\n\n";
|
|
933
|
+
}
|
|
934
|
+
return `data: ${JSON.stringify(data)}
|
|
935
|
+
|
|
936
|
+
`;
|
|
937
|
+
}
|
|
938
|
+
function parseJson(data) {
|
|
939
|
+
try {
|
|
940
|
+
return asRecord(JSON.parse(data));
|
|
941
|
+
} catch {
|
|
942
|
+
return void 0;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function removeUndefined(record) {
|
|
946
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
947
|
+
}
|
|
948
|
+
function randomId() {
|
|
949
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
950
|
+
}
|
|
951
|
+
function epochSeconds() {
|
|
952
|
+
return Math.floor(Date.now() / 1e3);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/anthropic.ts
|
|
956
|
+
var AnthropicCompatibilityError = class extends Error {
|
|
957
|
+
constructor(message) {
|
|
958
|
+
super(message);
|
|
959
|
+
this.name = "AnthropicCompatibilityError";
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
function anthropicMessagesToResponsesRequest(request) {
|
|
963
|
+
return removeUndefined2({
|
|
964
|
+
input: anthropicMessagesToResponsesInput(request.messages),
|
|
965
|
+
instructions: anthropicSystemToInstructions(request.system),
|
|
966
|
+
max_output_tokens: typeof request.max_tokens === "number" && Number.isFinite(request.max_tokens) ? request.max_tokens : void 0,
|
|
967
|
+
metadata: request.metadata,
|
|
968
|
+
model: normalizeRequestedModel(request.model),
|
|
969
|
+
parallel_tool_calls: true,
|
|
970
|
+
reasoning: anthropicThinkingToReasoning(request.thinking),
|
|
971
|
+
stop: anthropicStopSequences(request.stop_sequences),
|
|
972
|
+
stream: request.stream === true,
|
|
973
|
+
temperature: request.temperature,
|
|
974
|
+
tool_choice: anthropicToolChoice(request.tool_choice),
|
|
975
|
+
tools: anthropicTools(request.tools),
|
|
976
|
+
top_p: request.top_p
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function responsesResponseToAnthropicMessage(response, fallbackModel) {
|
|
980
|
+
const content = anthropicContentFromResponsesOutput(response);
|
|
981
|
+
const usage = anthropicUsage(response.usage);
|
|
982
|
+
return {
|
|
983
|
+
content,
|
|
984
|
+
id: textValue(response.id) || `msg_${randomId2()}`,
|
|
985
|
+
model: textValue(response.model) || fallbackModel,
|
|
986
|
+
role: "assistant",
|
|
987
|
+
stop_reason: anthropicStopReason(response, content),
|
|
988
|
+
stop_sequence: null,
|
|
989
|
+
type: "message",
|
|
990
|
+
usage
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function responsesStreamToAnthropicStream(stream, options) {
|
|
994
|
+
const decoder = new TextDecoder();
|
|
995
|
+
const encoder = new TextEncoder();
|
|
996
|
+
let buffer = "";
|
|
997
|
+
const state = {
|
|
998
|
+
blocks: /* @__PURE__ */ new Map(),
|
|
999
|
+
completed: false,
|
|
1000
|
+
messageId: options.messageId ?? `msg_${randomId2()}`,
|
|
1001
|
+
model: options.model,
|
|
1002
|
+
nextBlockIndex: 0,
|
|
1003
|
+
sawToolUse: false,
|
|
1004
|
+
started: false,
|
|
1005
|
+
usage: anthropicUsage(void 0)
|
|
1006
|
+
};
|
|
1007
|
+
return new ReadableStream({
|
|
1008
|
+
async start(controller) {
|
|
1009
|
+
const enqueue = (event, data) => {
|
|
1010
|
+
controller.enqueue(encoder.encode(encodeSse2(event, data)));
|
|
957
1011
|
};
|
|
958
|
-
const reader =
|
|
1012
|
+
const reader = stream.getReader();
|
|
959
1013
|
try {
|
|
960
1014
|
while (true) {
|
|
961
1015
|
const result = await reader.read();
|
|
@@ -963,67 +1017,17 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
963
1017
|
break;
|
|
964
1018
|
}
|
|
965
1019
|
buffer += decoder.decode(result.value, { stream: true });
|
|
966
|
-
const
|
|
967
|
-
buffer =
|
|
968
|
-
for (const
|
|
969
|
-
|
|
1020
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
1021
|
+
buffer = blocks.pop() ?? "";
|
|
1022
|
+
for (const block of blocks) {
|
|
1023
|
+
processResponsesSseBlock(block, state, enqueue);
|
|
970
1024
|
}
|
|
971
1025
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const outputEntries = [];
|
|
976
|
-
if (messageOutputIndex !== void 0) {
|
|
977
|
-
const item = messageOutputItem(text, messageId);
|
|
978
|
-
outputEntries.push([messageOutputIndex, item]);
|
|
979
|
-
enqueue("response.output_text.done", {
|
|
980
|
-
content_index: 0,
|
|
981
|
-
item_id: messageId,
|
|
982
|
-
output_index: messageOutputIndex,
|
|
983
|
-
text,
|
|
984
|
-
type: "response.output_text.done"
|
|
985
|
-
});
|
|
986
|
-
enqueue("response.content_part.done", {
|
|
987
|
-
content_index: 0,
|
|
988
|
-
item_id: messageId,
|
|
989
|
-
output_index: messageOutputIndex,
|
|
990
|
-
part: {
|
|
991
|
-
annotations: [],
|
|
992
|
-
text,
|
|
993
|
-
type: "output_text"
|
|
994
|
-
},
|
|
995
|
-
type: "response.content_part.done"
|
|
996
|
-
});
|
|
997
|
-
enqueue("response.output_item.done", {
|
|
998
|
-
item,
|
|
999
|
-
output_index: messageOutputIndex,
|
|
1000
|
-
type: "response.output_item.done"
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
for (const tool of [...tools.values()].sort(
|
|
1004
|
-
(a, b) => (a.outputIndex ?? 0) - (b.outputIndex ?? 0)
|
|
1005
|
-
)) {
|
|
1006
|
-
const item = functionCallItem(tool);
|
|
1007
|
-
const outputIndex = tool.outputIndex ?? 0;
|
|
1008
|
-
outputEntries.push([outputIndex, item]);
|
|
1009
|
-
enqueue("response.function_call_arguments.done", {
|
|
1010
|
-
arguments: tool.arguments,
|
|
1011
|
-
item_id: item.id,
|
|
1012
|
-
output_index: outputIndex,
|
|
1013
|
-
type: "response.function_call_arguments.done"
|
|
1014
|
-
});
|
|
1015
|
-
enqueue("response.output_item.done", {
|
|
1016
|
-
item,
|
|
1017
|
-
output_index: outputIndex,
|
|
1018
|
-
type: "response.output_item.done"
|
|
1019
|
-
});
|
|
1026
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
1027
|
+
if (tail.trim()) {
|
|
1028
|
+
processResponsesSseBlock(tail, state, enqueue);
|
|
1020
1029
|
}
|
|
1021
|
-
|
|
1022
|
-
enqueue("response.completed", {
|
|
1023
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
|
|
1024
|
-
type: "response.completed"
|
|
1025
|
-
});
|
|
1026
|
-
enqueue("done", "[DONE]");
|
|
1030
|
+
finishAnthropicStream(state, enqueue);
|
|
1027
1031
|
controller.close();
|
|
1028
1032
|
} catch (error) {
|
|
1029
1033
|
await reader.cancel(error).catch(() => {
|
|
@@ -1035,475 +1039,1129 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
1035
1039
|
}
|
|
1036
1040
|
});
|
|
1037
1041
|
}
|
|
1038
|
-
function
|
|
1039
|
-
|
|
1040
|
-
|
|
1042
|
+
function estimateAnthropicMessageTokens(request) {
|
|
1043
|
+
const chars = estimatedTextSize(request.system) + estimatedTextSize(request.messages) + estimatedTextSize(request.tools) + estimatedTextSize(request.tool_choice) + estimatedTextSize(request.thinking);
|
|
1044
|
+
const messageCount = Array.isArray(request.messages) ? request.messages.length : 1;
|
|
1045
|
+
const toolCount = Array.isArray(request.tools) ? request.tools.length : 0;
|
|
1046
|
+
const inputTokens = Math.max(1, Math.ceil(chars / 4) + messageCount * 4 + toolCount * 16);
|
|
1047
|
+
return {
|
|
1048
|
+
input_tokens: inputTokens,
|
|
1049
|
+
total_tokens: inputTokens
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function anthropicMessagesToResponsesInput(messages) {
|
|
1053
|
+
if (!Array.isArray(messages)) {
|
|
1054
|
+
throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
|
|
1055
|
+
}
|
|
1056
|
+
const input = [];
|
|
1057
|
+
for (const message of messages) {
|
|
1058
|
+
const record = asRecord(message);
|
|
1059
|
+
const role = anthropicRole(record.role);
|
|
1060
|
+
const parts = anthropicContentParts(record.content);
|
|
1061
|
+
const messageParts = [];
|
|
1062
|
+
const flushMessage = () => {
|
|
1063
|
+
if (messageParts.length === 0) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
input.push({
|
|
1067
|
+
content: [...messageParts],
|
|
1068
|
+
role,
|
|
1069
|
+
type: "message"
|
|
1070
|
+
});
|
|
1071
|
+
messageParts.length = 0;
|
|
1072
|
+
};
|
|
1073
|
+
for (const part of parts) {
|
|
1074
|
+
const type = textValue(part.type) || "text";
|
|
1075
|
+
if (type === "text") {
|
|
1076
|
+
const text = textValue(part.text);
|
|
1077
|
+
if (text) {
|
|
1078
|
+
messageParts.push({
|
|
1079
|
+
text,
|
|
1080
|
+
type: role === "assistant" ? "output_text" : "input_text"
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
if (type === "image") {
|
|
1086
|
+
if (role !== "user") {
|
|
1087
|
+
throw new AnthropicCompatibilityError(
|
|
1088
|
+
"Anthropic image content is only supported for user messages."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
messageParts.push(anthropicImageToResponsesPart(part));
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (type === "tool_use") {
|
|
1095
|
+
flushMessage();
|
|
1096
|
+
input.push({
|
|
1097
|
+
arguments: JSON.stringify(asRecord(part.input)),
|
|
1098
|
+
call_id: textValue(part.id) || `call_${randomId2()}`,
|
|
1099
|
+
name: textValue(part.name),
|
|
1100
|
+
type: "function_call"
|
|
1101
|
+
});
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (type === "tool_result") {
|
|
1105
|
+
flushMessage();
|
|
1106
|
+
input.push({
|
|
1107
|
+
call_id: textValue(part.tool_use_id),
|
|
1108
|
+
output: anthropicToolResultOutput(part.content),
|
|
1109
|
+
type: "function_call_output"
|
|
1110
|
+
});
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (type === "thinking" || type === "redacted_thinking") {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
throw new AnthropicCompatibilityError(
|
|
1117
|
+
`Anthropic content block type "${type}" is not supported.`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
flushMessage();
|
|
1041
1121
|
}
|
|
1042
|
-
|
|
1122
|
+
return input;
|
|
1123
|
+
}
|
|
1124
|
+
function anthropicRole(value) {
|
|
1125
|
+
const role = textValue(value);
|
|
1126
|
+
if (role === "assistant" || role === "user") {
|
|
1127
|
+
return role;
|
|
1128
|
+
}
|
|
1129
|
+
if (!role) {
|
|
1130
|
+
return "user";
|
|
1131
|
+
}
|
|
1132
|
+
throw new AnthropicCompatibilityError(`Anthropic message role "${role}" is not supported.`);
|
|
1133
|
+
}
|
|
1134
|
+
function anthropicContentParts(content) {
|
|
1135
|
+
if (typeof content === "string") {
|
|
1136
|
+
return [{ text: content, type: "text" }];
|
|
1137
|
+
}
|
|
1138
|
+
if (Array.isArray(content)) {
|
|
1139
|
+
return content.map(
|
|
1140
|
+
(part) => typeof part === "string" ? { text: part, type: "text" } : asRecord(part)
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (content === void 0 || content === null) {
|
|
1043
1144
|
return [];
|
|
1044
1145
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1146
|
+
return [asRecord(content)];
|
|
1147
|
+
}
|
|
1148
|
+
function anthropicImageToResponsesPart(part) {
|
|
1149
|
+
const source = asRecord(part.source);
|
|
1150
|
+
const sourceType = textValue(source.type);
|
|
1151
|
+
if (sourceType === "base64") {
|
|
1152
|
+
const mediaType = textValue(source.media_type) || "image/png";
|
|
1153
|
+
const data = textValue(source.data);
|
|
1154
|
+
if (!data) {
|
|
1155
|
+
throw new AnthropicCompatibilityError("Anthropic base64 image content requires source.data.");
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
detail: "auto",
|
|
1159
|
+
image_url: `data:${mediaType};base64,${data}`,
|
|
1160
|
+
type: "input_image"
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (sourceType === "url") {
|
|
1164
|
+
const url = textValue(source.url);
|
|
1165
|
+
if (!url) {
|
|
1166
|
+
throw new AnthropicCompatibilityError("Anthropic URL image content requires source.url.");
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
detail: "auto",
|
|
1170
|
+
image_url: url,
|
|
1171
|
+
type: "input_image"
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
throw new AnthropicCompatibilityError(
|
|
1175
|
+
`Anthropic image source type "${sourceType || "unknown"}" is not supported.`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
function anthropicToolResultOutput(content) {
|
|
1179
|
+
if (typeof content === "string") {
|
|
1180
|
+
return content;
|
|
1181
|
+
}
|
|
1182
|
+
if (Array.isArray(content)) {
|
|
1183
|
+
return content.map((part) => {
|
|
1184
|
+
const record = asRecord(part);
|
|
1185
|
+
return textValue(record.text) || textValue(record.content) || JSON.stringify(part);
|
|
1186
|
+
}).filter(Boolean).join("\n");
|
|
1187
|
+
}
|
|
1188
|
+
if (content === void 0 || content === null) {
|
|
1189
|
+
return "";
|
|
1190
|
+
}
|
|
1191
|
+
return typeof content === "object" ? JSON.stringify(content) : String(content);
|
|
1192
|
+
}
|
|
1193
|
+
function anthropicSystemToInstructions(system) {
|
|
1194
|
+
if (typeof system === "string") {
|
|
1195
|
+
return system || void 0;
|
|
1196
|
+
}
|
|
1197
|
+
if (!Array.isArray(system)) {
|
|
1198
|
+
return void 0;
|
|
1199
|
+
}
|
|
1200
|
+
const text = system.map((part) => textValue(asRecord(part).text) || textValue(part)).filter(Boolean).join("\n");
|
|
1201
|
+
return text || void 0;
|
|
1202
|
+
}
|
|
1203
|
+
function anthropicTools(tools) {
|
|
1204
|
+
if (!Array.isArray(tools)) {
|
|
1205
|
+
return void 0;
|
|
1206
|
+
}
|
|
1207
|
+
const converted = tools.map((tool) => {
|
|
1208
|
+
const record = asRecord(tool);
|
|
1209
|
+
return removeUndefined2({
|
|
1210
|
+
description: record.description,
|
|
1211
|
+
name: record.name,
|
|
1212
|
+
parameters: record.input_schema,
|
|
1213
|
+
strict: record.strict,
|
|
1214
|
+
type: "function"
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
return converted.length > 0 ? converted : void 0;
|
|
1218
|
+
}
|
|
1219
|
+
function anthropicToolChoice(toolChoice) {
|
|
1220
|
+
if (toolChoice === void 0 || toolChoice === null) {
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
const record = asRecord(toolChoice);
|
|
1224
|
+
const type = textValue(record.type);
|
|
1225
|
+
if (type === "auto") {
|
|
1226
|
+
return "auto";
|
|
1227
|
+
}
|
|
1228
|
+
if (type === "any") {
|
|
1229
|
+
return "required";
|
|
1230
|
+
}
|
|
1231
|
+
if (type === "none") {
|
|
1232
|
+
return "none";
|
|
1233
|
+
}
|
|
1234
|
+
if (type === "tool") {
|
|
1235
|
+
return { name: textValue(record.name), type: "function" };
|
|
1236
|
+
}
|
|
1237
|
+
throw new AnthropicCompatibilityError(
|
|
1238
|
+
`Anthropic tool_choice type "${type || "unknown"}" is not supported.`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
function anthropicThinkingToReasoning(thinking) {
|
|
1242
|
+
const record = asRecord(thinking);
|
|
1243
|
+
if (Object.keys(record).length === 0) {
|
|
1244
|
+
return void 0;
|
|
1245
|
+
}
|
|
1246
|
+
const type = textValue(record.type);
|
|
1247
|
+
if (type && type !== "enabled") {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
const budget = typeof record.budget_tokens === "number" ? record.budget_tokens : 0;
|
|
1251
|
+
return {
|
|
1252
|
+
effort: budget >= 16e3 ? "high" : budget >= 4e3 ? "medium" : "low"
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function anthropicStopSequences(stopSequences) {
|
|
1256
|
+
if (!Array.isArray(stopSequences) || stopSequences.length === 0) {
|
|
1257
|
+
return void 0;
|
|
1258
|
+
}
|
|
1259
|
+
return stopSequences.map((sequence) => textValue(sequence)).filter(Boolean);
|
|
1260
|
+
}
|
|
1261
|
+
function anthropicContentFromResponsesOutput(response) {
|
|
1262
|
+
const content = [];
|
|
1263
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
1264
|
+
for (const item of output) {
|
|
1047
1265
|
const record = asRecord(item);
|
|
1048
|
-
const type =
|
|
1049
|
-
if (type === "
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1266
|
+
const type = textValue(record.type);
|
|
1267
|
+
if (type === "message") {
|
|
1268
|
+
const parts = Array.isArray(record.content) ? record.content : [];
|
|
1269
|
+
for (const part of parts) {
|
|
1270
|
+
const partRecord = asRecord(part);
|
|
1271
|
+
const text = textValue(partRecord.text) || textValue(partRecord.output_text);
|
|
1272
|
+
if (text) {
|
|
1273
|
+
content.push({ text, type: "text" });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (type === "function_call") {
|
|
1279
|
+
content.push({
|
|
1280
|
+
id: textValue(record.call_id) || textValue(record.id) || `call_${randomId2()}`,
|
|
1281
|
+
input: parseToolInput(textValue(record.arguments)),
|
|
1282
|
+
name: textValue(record.name),
|
|
1283
|
+
type: "tool_use"
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (content.length === 0) {
|
|
1288
|
+
const outputText2 = textValue(response.output_text);
|
|
1289
|
+
if (outputText2) {
|
|
1290
|
+
content.push({ text: outputText2, type: "text" });
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return content;
|
|
1294
|
+
}
|
|
1295
|
+
function anthropicStopReason(response, content) {
|
|
1296
|
+
if (content.some((part) => part.type === "tool_use")) {
|
|
1297
|
+
return "tool_use";
|
|
1298
|
+
}
|
|
1299
|
+
const incompleteReason = textValue(asRecord(response.incomplete_details).reason);
|
|
1300
|
+
if (textValue(response.status) === "incomplete" || incompleteReason === "max_output_tokens") {
|
|
1301
|
+
return "max_tokens";
|
|
1302
|
+
}
|
|
1303
|
+
return "end_turn";
|
|
1304
|
+
}
|
|
1305
|
+
function anthropicUsage(usage) {
|
|
1306
|
+
const record = asRecord(usage);
|
|
1307
|
+
const inputTokens = firstNumber2(record.input_tokens, record.prompt_tokens) ?? 0;
|
|
1308
|
+
const outputTokens = firstNumber2(record.output_tokens, record.completion_tokens) ?? 0;
|
|
1309
|
+
const details = asRecord(record.input_tokens_details);
|
|
1310
|
+
return removeUndefined2({
|
|
1311
|
+
cache_creation_input_tokens: firstNumber2(record.cache_creation_input_tokens),
|
|
1312
|
+
cache_read_input_tokens: firstNumber2(record.cache_read_input_tokens, details.cached_tokens) ?? void 0,
|
|
1313
|
+
input_tokens: inputTokens,
|
|
1314
|
+
output_tokens: outputTokens
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
function processResponsesSseBlock(block, state, enqueue) {
|
|
1318
|
+
const { data, event } = parseSseBlock(block);
|
|
1319
|
+
if (!data || data === "[DONE]") {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const parsed = parseJsonObject(data);
|
|
1323
|
+
if (!parsed) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const type = textValue(parsed.type) || event;
|
|
1327
|
+
if (type === "response.created") {
|
|
1328
|
+
const response = asRecord(parsed.response);
|
|
1329
|
+
state.messageId = textValue(response.id) || state.messageId;
|
|
1330
|
+
state.model = textValue(response.model) || state.model;
|
|
1331
|
+
startAnthropicMessage(state, enqueue);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (type === "response.output_item.added") {
|
|
1335
|
+
const item = asRecord(parsed.item);
|
|
1336
|
+
if (textValue(item.type) === "function_call") {
|
|
1337
|
+
ensureToolBlock(state, parsed, item, enqueue);
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (type === "response.output_text.delta") {
|
|
1342
|
+
const blockState = ensureTextBlock(state, parsed, enqueue);
|
|
1343
|
+
const delta = textValue(parsed.delta);
|
|
1344
|
+
if (delta) {
|
|
1345
|
+
blockState.sentText += delta;
|
|
1346
|
+
enqueue("content_block_delta", {
|
|
1347
|
+
delta: { text: delta, type: "text_delta" },
|
|
1348
|
+
index: blockState.index,
|
|
1349
|
+
type: "content_block_delta"
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (type === "response.output_text.done" || type === "response.content_part.done") {
|
|
1355
|
+
const blockState = ensureTextBlock(state, parsed, enqueue);
|
|
1356
|
+
const text = textValue(parsed.text) || textValue(asRecord(parsed.part).text);
|
|
1357
|
+
if (text && !blockState.sentText) {
|
|
1358
|
+
blockState.sentText = text;
|
|
1359
|
+
enqueue("content_block_delta", {
|
|
1360
|
+
delta: { text, type: "text_delta" },
|
|
1361
|
+
index: blockState.index,
|
|
1362
|
+
type: "content_block_delta"
|
|
1054
1363
|
});
|
|
1055
|
-
continue;
|
|
1056
1364
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
]
|
|
1365
|
+
stopBlock(blockState, enqueue);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (type === "response.function_call_arguments.delta") {
|
|
1369
|
+
const blockState = ensureToolBlock(state, parsed, {}, enqueue);
|
|
1370
|
+
const delta = textValue(parsed.delta);
|
|
1371
|
+
if (delta) {
|
|
1372
|
+
blockState.sentText += delta;
|
|
1373
|
+
enqueue("content_block_delta", {
|
|
1374
|
+
delta: { partial_json: delta, type: "input_json_delta" },
|
|
1375
|
+
index: blockState.index,
|
|
1376
|
+
type: "content_block_delta"
|
|
1070
1377
|
});
|
|
1071
|
-
continue;
|
|
1072
1378
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (type === "response.function_call_arguments.done") {
|
|
1382
|
+
const blockState = ensureToolBlock(state, parsed, {}, enqueue);
|
|
1383
|
+
const args = textValue(parsed.arguments);
|
|
1384
|
+
if (args && !blockState.sentText) {
|
|
1385
|
+
blockState.sentText = args;
|
|
1386
|
+
enqueue("content_block_delta", {
|
|
1387
|
+
delta: { partial_json: args, type: "input_json_delta" },
|
|
1388
|
+
index: blockState.index,
|
|
1389
|
+
type: "content_block_delta"
|
|
1390
|
+
});
|
|
1075
1391
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1392
|
+
stopBlock(blockState, enqueue);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (type === "response.output_item.done") {
|
|
1396
|
+
const item = asRecord(parsed.item);
|
|
1397
|
+
if (textValue(item.type) === "function_call") {
|
|
1398
|
+
const blockState = ensureToolBlock(state, parsed, item, enqueue);
|
|
1399
|
+
const args = textValue(item.arguments);
|
|
1400
|
+
if (args && !blockState.sentText) {
|
|
1401
|
+
blockState.sentText = args;
|
|
1402
|
+
enqueue("content_block_delta", {
|
|
1403
|
+
delta: { partial_json: args, type: "input_json_delta" },
|
|
1404
|
+
index: blockState.index,
|
|
1405
|
+
type: "content_block_delta"
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
stopBlock(blockState, enqueue);
|
|
1080
1409
|
}
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (type === "response.completed") {
|
|
1413
|
+
const response = asRecord(parsed.response);
|
|
1414
|
+
state.model = textValue(response.model) || state.model;
|
|
1415
|
+
state.usage = anthropicUsage(response.usage);
|
|
1416
|
+
finishAnthropicStream(state, enqueue);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (type === "response.failed" || event === "error") {
|
|
1420
|
+
const error = asRecord(asRecord(parsed.response).error);
|
|
1421
|
+
enqueue("error", {
|
|
1422
|
+
error: {
|
|
1423
|
+
message: textValue(error.message) || textValue(parsed.message) || "Upstream stream failed.",
|
|
1424
|
+
type: textValue(error.type) || "api_error"
|
|
1425
|
+
},
|
|
1426
|
+
type: "error"
|
|
1427
|
+
});
|
|
1428
|
+
state.completed = true;
|
|
1081
1429
|
}
|
|
1082
|
-
return messages;
|
|
1083
1430
|
}
|
|
1084
|
-
function
|
|
1085
|
-
if (
|
|
1086
|
-
return
|
|
1431
|
+
function startAnthropicMessage(state, enqueue) {
|
|
1432
|
+
if (state.started) {
|
|
1433
|
+
return;
|
|
1087
1434
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1435
|
+
state.started = true;
|
|
1436
|
+
enqueue("message_start", {
|
|
1437
|
+
message: {
|
|
1438
|
+
content: [],
|
|
1439
|
+
id: state.messageId,
|
|
1440
|
+
model: state.model,
|
|
1441
|
+
role: "assistant",
|
|
1442
|
+
stop_reason: null,
|
|
1443
|
+
stop_sequence: null,
|
|
1444
|
+
type: "message",
|
|
1445
|
+
usage: anthropicUsage(void 0)
|
|
1446
|
+
},
|
|
1447
|
+
type: "message_start"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
function finishAnthropicStream(state, enqueue) {
|
|
1451
|
+
if (state.completed) {
|
|
1452
|
+
return;
|
|
1093
1453
|
}
|
|
1094
|
-
|
|
1095
|
-
for (const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1454
|
+
startAnthropicMessage(state, enqueue);
|
|
1455
|
+
for (const block of [...state.blocks.values()].sort((left, right) => left.index - right.index)) {
|
|
1456
|
+
stopBlock(block, enqueue);
|
|
1457
|
+
}
|
|
1458
|
+
enqueue("message_delta", {
|
|
1459
|
+
delta: {
|
|
1460
|
+
stop_reason: state.sawToolUse ? "tool_use" : "end_turn",
|
|
1461
|
+
stop_sequence: null
|
|
1462
|
+
},
|
|
1463
|
+
type: "message_delta",
|
|
1464
|
+
usage: state.usage
|
|
1465
|
+
});
|
|
1466
|
+
enqueue("message_stop", { type: "message_stop" });
|
|
1467
|
+
state.completed = true;
|
|
1468
|
+
}
|
|
1469
|
+
function ensureTextBlock(state, payload, enqueue) {
|
|
1470
|
+
startAnthropicMessage(state, enqueue);
|
|
1471
|
+
const key = `text:${indexValue(payload.output_index)}:${indexValue(payload.content_index)}`;
|
|
1472
|
+
let block = state.blocks.get(key);
|
|
1473
|
+
if (!block) {
|
|
1474
|
+
block = { index: state.nextBlockIndex++, sentText: "", stopped: false, type: "text" };
|
|
1475
|
+
state.blocks.set(key, block);
|
|
1476
|
+
enqueue("content_block_start", {
|
|
1477
|
+
content_block: { text: "", type: "text" },
|
|
1478
|
+
index: block.index,
|
|
1479
|
+
type: "content_block_start"
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
return block;
|
|
1483
|
+
}
|
|
1484
|
+
function ensureToolBlock(state, payload, item, enqueue) {
|
|
1485
|
+
startAnthropicMessage(state, enqueue);
|
|
1486
|
+
state.sawToolUse = true;
|
|
1487
|
+
const key = `tool:${indexValue(payload.output_index)}`;
|
|
1488
|
+
let block = state.blocks.get(key);
|
|
1489
|
+
if (!block) {
|
|
1490
|
+
block = { index: state.nextBlockIndex++, sentText: "", stopped: false, type: "tool_use" };
|
|
1491
|
+
state.blocks.set(key, block);
|
|
1492
|
+
enqueue("content_block_start", {
|
|
1493
|
+
content_block: {
|
|
1494
|
+
id: textValue(item.call_id) || textValue(item.id) || `call_${randomId2()}`,
|
|
1495
|
+
input: {},
|
|
1496
|
+
name: textValue(item.name),
|
|
1497
|
+
type: "tool_use"
|
|
1498
|
+
},
|
|
1499
|
+
index: block.index,
|
|
1500
|
+
type: "content_block_start"
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return block;
|
|
1504
|
+
}
|
|
1505
|
+
function stopBlock(block, enqueue) {
|
|
1506
|
+
if (block.stopped) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
block.stopped = true;
|
|
1510
|
+
enqueue("content_block_stop", {
|
|
1511
|
+
index: block.index,
|
|
1512
|
+
type: "content_block_stop"
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
function parseSseBlock(block) {
|
|
1516
|
+
let event = "message";
|
|
1517
|
+
const data = [];
|
|
1518
|
+
for (const line of block.split(/\r?\n/)) {
|
|
1519
|
+
const trimmed = line.trim();
|
|
1520
|
+
if (trimmed.startsWith("event:")) {
|
|
1521
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
1522
|
+
} else if (trimmed.startsWith("data:")) {
|
|
1523
|
+
data.push(trimmed.slice("data:".length).trim());
|
|
1123
1524
|
}
|
|
1124
|
-
unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
|
|
1125
1525
|
}
|
|
1126
|
-
|
|
1526
|
+
return { data: data.join("\n"), event };
|
|
1527
|
+
}
|
|
1528
|
+
function parseJsonObject(text) {
|
|
1529
|
+
try {
|
|
1530
|
+
return asRecord(JSON.parse(text));
|
|
1531
|
+
} catch {
|
|
1127
1532
|
return void 0;
|
|
1128
1533
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1534
|
+
}
|
|
1535
|
+
function parseToolInput(argumentsText) {
|
|
1536
|
+
const parsed = parseJsonObject(argumentsText);
|
|
1537
|
+
return parsed ?? {};
|
|
1538
|
+
}
|
|
1539
|
+
function estimatedTextSize(value) {
|
|
1540
|
+
if (value === void 0 || value === null) {
|
|
1541
|
+
return 0;
|
|
1131
1542
|
}
|
|
1132
|
-
|
|
1543
|
+
if (typeof value === "string") {
|
|
1544
|
+
return value.length;
|
|
1545
|
+
}
|
|
1546
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1547
|
+
return String(value).length;
|
|
1548
|
+
}
|
|
1549
|
+
if (Array.isArray(value)) {
|
|
1550
|
+
return value.reduce((sum, item) => sum + estimatedTextSize(item), 0);
|
|
1551
|
+
}
|
|
1552
|
+
if (typeof value === "object") {
|
|
1553
|
+
return Object.values(value).reduce((sum, item) => sum + estimatedTextSize(item), 0);
|
|
1554
|
+
}
|
|
1555
|
+
return 0;
|
|
1133
1556
|
}
|
|
1134
|
-
function
|
|
1135
|
-
if (typeof
|
|
1136
|
-
return
|
|
1557
|
+
function textValue(value) {
|
|
1558
|
+
if (typeof value === "string") {
|
|
1559
|
+
return value;
|
|
1137
1560
|
}
|
|
1138
|
-
if (
|
|
1139
|
-
return
|
|
1561
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1562
|
+
return String(value);
|
|
1140
1563
|
}
|
|
1141
|
-
|
|
1142
|
-
"Hoopilot legacy completions compatibility supports exactly one string prompt per request."
|
|
1143
|
-
);
|
|
1564
|
+
return "";
|
|
1144
1565
|
}
|
|
1145
|
-
function
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1566
|
+
function firstNumber2(...values) {
|
|
1567
|
+
for (const value of values) {
|
|
1568
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1569
|
+
return value;
|
|
1570
|
+
}
|
|
1150
1571
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1572
|
+
return void 0;
|
|
1573
|
+
}
|
|
1574
|
+
function indexValue(value) {
|
|
1575
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1576
|
+
}
|
|
1577
|
+
function removeUndefined2(record) {
|
|
1578
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
1579
|
+
}
|
|
1580
|
+
function encodeSse2(event, data) {
|
|
1581
|
+
return `event: ${event}
|
|
1582
|
+
data: ${JSON.stringify(data)}
|
|
1583
|
+
|
|
1584
|
+
`;
|
|
1585
|
+
}
|
|
1586
|
+
function randomId2() {
|
|
1587
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/auth-store.ts
|
|
1591
|
+
var import_node_fs = require("fs");
|
|
1592
|
+
var import_node_path = require("path");
|
|
1593
|
+
var StoredCopilotAuthError = class extends Error {
|
|
1594
|
+
constructor(message) {
|
|
1595
|
+
super(message);
|
|
1596
|
+
this.name = "StoredCopilotAuthError";
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
function authStorePath(env = process.env) {
|
|
1600
|
+
const explicit = envValue(env.HOOPILOT_AUTH_FILE);
|
|
1601
|
+
if (explicit) {
|
|
1602
|
+
return explicit;
|
|
1603
|
+
}
|
|
1604
|
+
const xdg = envValue(env.XDG_CONFIG_HOME);
|
|
1605
|
+
if (xdg) {
|
|
1606
|
+
return (0, import_node_path.join)(xdg, "hoopilot", "auth.json");
|
|
1607
|
+
}
|
|
1608
|
+
const appdata = envValue(env.APPDATA);
|
|
1609
|
+
if (appdata) {
|
|
1610
|
+
return (0, import_node_path.join)(appdata, "hoopilot", "auth.json");
|
|
1611
|
+
}
|
|
1612
|
+
const home = envValue(env.HOME);
|
|
1613
|
+
if (!home) {
|
|
1614
|
+
throw new StoredCopilotAuthError(
|
|
1615
|
+
"Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
|
|
1154
1616
|
);
|
|
1155
1617
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1618
|
+
const base = (0, import_node_path.join)(home, ".config");
|
|
1619
|
+
return (0, import_node_path.join)(base, "hoopilot", "auth.json");
|
|
1620
|
+
}
|
|
1621
|
+
function readStoredCopilotAuth(path = authStorePath()) {
|
|
1622
|
+
let text;
|
|
1623
|
+
try {
|
|
1624
|
+
text = (0, import_node_fs.readFileSync)(path, "utf8");
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (error.code === "ENOENT") {
|
|
1627
|
+
return void 0;
|
|
1628
|
+
}
|
|
1629
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
1630
|
+
}
|
|
1631
|
+
let parsed;
|
|
1632
|
+
try {
|
|
1633
|
+
parsed = JSON.parse(text);
|
|
1634
|
+
} catch {
|
|
1635
|
+
throw new StoredCopilotAuthError(
|
|
1636
|
+
`Hoopilot auth file at ${path} is not valid JSON. Run \`hoopilot login\` to replace it.`
|
|
1159
1637
|
);
|
|
1160
1638
|
}
|
|
1161
|
-
if (
|
|
1162
|
-
throw new
|
|
1163
|
-
|
|
1639
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1640
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
1641
|
+
}
|
|
1642
|
+
const record = parsed;
|
|
1643
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
1644
|
+
if (!token) {
|
|
1645
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
apiBaseUrl: typeof record.apiBaseUrl === "string" ? record.apiBaseUrl : void 0,
|
|
1649
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : void 0,
|
|
1650
|
+
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
1651
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
1652
|
+
token
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
1656
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
|
|
1657
|
+
const data = `${JSON.stringify(
|
|
1658
|
+
{
|
|
1659
|
+
...auth,
|
|
1660
|
+
createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1661
|
+
},
|
|
1662
|
+
null,
|
|
1663
|
+
2
|
|
1664
|
+
)}
|
|
1665
|
+
`;
|
|
1666
|
+
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
1667
|
+
(0, import_node_fs.writeFileSync)(tmpPath, data, { mode: 384 });
|
|
1668
|
+
(0, import_node_fs.renameSync)(tmpPath, path);
|
|
1669
|
+
try {
|
|
1670
|
+
(0, import_node_fs.chmodSync)(path, 384);
|
|
1671
|
+
} catch {
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/auth.ts
|
|
1676
|
+
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
1677
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
1678
|
+
var STORED_TOKEN_TTL_MS = 10 * 6e4;
|
|
1679
|
+
var CopilotAuthError = class extends Error {
|
|
1680
|
+
constructor(message) {
|
|
1681
|
+
super(message);
|
|
1682
|
+
this.name = "CopilotAuthError";
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
var CopilotAuth = class {
|
|
1686
|
+
#authStorePath;
|
|
1687
|
+
#copilotApiBaseUrl;
|
|
1688
|
+
#hasCopilotApiBaseUrlOverride;
|
|
1689
|
+
#cachedAccess;
|
|
1690
|
+
constructor(options = {}) {
|
|
1691
|
+
const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
|
|
1692
|
+
const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
|
|
1693
|
+
this.#authStorePath = options.authStorePath ?? envAuthStorePath;
|
|
1694
|
+
this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
|
|
1695
|
+
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
1696
|
+
options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
|
|
1164
1697
|
);
|
|
1165
1698
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
return content;
|
|
1170
|
-
}
|
|
1171
|
-
if (typeof content === "number" || typeof content === "boolean") {
|
|
1172
|
-
return String(content);
|
|
1173
|
-
}
|
|
1174
|
-
if (Array.isArray(content)) {
|
|
1175
|
-
return content.map((item) => contentToText(item)).filter(Boolean).join("\n");
|
|
1176
|
-
}
|
|
1177
|
-
if (content && typeof content === "object") {
|
|
1178
|
-
const record = content;
|
|
1179
|
-
if (typeof record.text === "string") {
|
|
1180
|
-
return record.text;
|
|
1699
|
+
async getAccess() {
|
|
1700
|
+
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
1701
|
+
return this.#cachedAccess;
|
|
1181
1702
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1703
|
+
let stored;
|
|
1704
|
+
try {
|
|
1705
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
1708
|
+
throw new CopilotAuthError(error.message);
|
|
1709
|
+
}
|
|
1710
|
+
throw error;
|
|
1184
1711
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1712
|
+
if (stored) {
|
|
1713
|
+
return this.#cacheAccess({
|
|
1714
|
+
apiBaseUrl: trimTrailingSlash(
|
|
1715
|
+
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
1716
|
+
),
|
|
1717
|
+
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
1718
|
+
source: "github-copilot-oauth",
|
|
1719
|
+
token: stored.token
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
throw new CopilotAuthError(
|
|
1723
|
+
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
1724
|
+
);
|
|
1192
1725
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1726
|
+
#cacheAccess(access) {
|
|
1727
|
+
this.#cachedAccess = access;
|
|
1728
|
+
return access;
|
|
1195
1729
|
}
|
|
1196
|
-
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// src/copilot.ts
|
|
1733
|
+
var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
1734
|
+
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
1735
|
+
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
1736
|
+
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
1737
|
+
function applyCopilotHeaders(headers, token) {
|
|
1738
|
+
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
1739
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
1740
|
+
headers.set("copilot-integration-id", "vscode-chat");
|
|
1741
|
+
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
1742
|
+
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
1743
|
+
headers.set("openai-intent", "conversation-panel");
|
|
1744
|
+
headers.set("user-agent", "hoopilot/0.1.0");
|
|
1745
|
+
headers.set("x-github-api-version", "2026-06-01");
|
|
1746
|
+
return headers;
|
|
1197
1747
|
}
|
|
1198
|
-
function
|
|
1199
|
-
|
|
1200
|
-
|
|
1748
|
+
function applyGithubApiHeaders(headers, token) {
|
|
1749
|
+
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
1750
|
+
headers.set("authorization", `token ${token}`);
|
|
1751
|
+
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
1752
|
+
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
1753
|
+
headers.set("user-agent", "hoopilot/0.1.0");
|
|
1754
|
+
headers.set("x-github-api-version", COPILOT_USAGE_API_VERSION);
|
|
1755
|
+
return headers;
|
|
1756
|
+
}
|
|
1757
|
+
var CopilotClient = class {
|
|
1758
|
+
#auth;
|
|
1759
|
+
#allowUnsafeUpstream;
|
|
1760
|
+
#fetch;
|
|
1761
|
+
#githubApiBaseUrl;
|
|
1762
|
+
constructor(options = {}) {
|
|
1763
|
+
this.#auth = new CopilotAuth(options);
|
|
1764
|
+
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
1765
|
+
this.#fetch = options.fetch ?? fetch;
|
|
1766
|
+
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
1767
|
+
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
1768
|
+
);
|
|
1201
1769
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1770
|
+
/**
|
|
1771
|
+
* Fetch the Copilot account's quota / premium-request usage from the GitHub
|
|
1772
|
+
* REST `copilot_internal/user` endpoint. The stored device-flow OAuth token is
|
|
1773
|
+
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
1774
|
+
*/
|
|
1775
|
+
async usage(signal) {
|
|
1776
|
+
if (!isTrustedTokenBaseUrl(
|
|
1777
|
+
this.#githubApiBaseUrl,
|
|
1778
|
+
ALLOWED_GITHUB_API_HOSTS,
|
|
1779
|
+
this.#allowUnsafeUpstream
|
|
1780
|
+
)) {
|
|
1781
|
+
throw new Error(
|
|
1782
|
+
`Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
|
|
1783
|
+
);
|
|
1207
1784
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
type: "function"
|
|
1216
|
-
};
|
|
1217
|
-
});
|
|
1218
|
-
return converted.length > 0 ? converted : void 0;
|
|
1219
|
-
}
|
|
1220
|
-
function chatToolChoice(toolChoice) {
|
|
1221
|
-
if (typeof toolChoice === "string" || toolChoice === void 0) {
|
|
1222
|
-
return toolChoice;
|
|
1785
|
+
const access = await this.#auth.getAccess();
|
|
1786
|
+
const headers = applyGithubApiHeaders(new Headers(), access.token);
|
|
1787
|
+
return this.#fetch(`${this.#githubApiBaseUrl}/copilot_internal/user`, {
|
|
1788
|
+
headers,
|
|
1789
|
+
method: "GET",
|
|
1790
|
+
signal
|
|
1791
|
+
});
|
|
1223
1792
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1793
|
+
async chatCompletions(body, signal) {
|
|
1794
|
+
return this.fetchCopilot("/chat/completions", {
|
|
1795
|
+
body: JSON.stringify(body),
|
|
1796
|
+
headers: {
|
|
1797
|
+
"content-type": "application/json"
|
|
1798
|
+
},
|
|
1799
|
+
method: "POST",
|
|
1800
|
+
signal
|
|
1801
|
+
});
|
|
1228
1802
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const text = contentToText(message.content);
|
|
1239
|
-
if (text) {
|
|
1240
|
-
output.push(messageOutputItem(text));
|
|
1803
|
+
async responses(body, signal) {
|
|
1804
|
+
return this.fetchCopilot("/responses", {
|
|
1805
|
+
body,
|
|
1806
|
+
headers: {
|
|
1807
|
+
"content-type": "application/json"
|
|
1808
|
+
},
|
|
1809
|
+
method: "POST",
|
|
1810
|
+
signal
|
|
1811
|
+
});
|
|
1241
1812
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
index: output.length,
|
|
1251
|
-
name: contentToText(fn.name)
|
|
1252
|
-
})
|
|
1253
|
-
);
|
|
1813
|
+
async models(signal) {
|
|
1814
|
+
return this.fetchCopilot("/models", {
|
|
1815
|
+
headers: {
|
|
1816
|
+
accept: "application/json"
|
|
1817
|
+
},
|
|
1818
|
+
method: "GET",
|
|
1819
|
+
signal
|
|
1820
|
+
});
|
|
1254
1821
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1822
|
+
async fetchCopilot(path, init) {
|
|
1823
|
+
const access = await this.#auth.getAccess();
|
|
1824
|
+
if (!isTrustedTokenBaseUrl(
|
|
1825
|
+
access.apiBaseUrl,
|
|
1826
|
+
ALLOWED_COPILOT_API_HOSTS,
|
|
1827
|
+
this.#allowUnsafeUpstream
|
|
1828
|
+
)) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
1834
|
+
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
1835
|
+
...init,
|
|
1836
|
+
headers
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
function normalizeCopilotUsage(body) {
|
|
1841
|
+
const record = asRecord(body);
|
|
1842
|
+
const quotas = {};
|
|
1843
|
+
const snapshots = asRecord(record.quota_snapshots);
|
|
1844
|
+
for (const [category, detail] of Object.entries(snapshots)) {
|
|
1845
|
+
quotas[category] = normalizeQuotaDetail(asRecord(detail));
|
|
1846
|
+
}
|
|
1847
|
+
if (Object.keys(quotas).length === 0) {
|
|
1848
|
+
const remaining = asRecord(record.limited_user_quotas);
|
|
1849
|
+
const monthly = asRecord(record.monthly_quotas);
|
|
1850
|
+
for (const category of /* @__PURE__ */ new Set([...Object.keys(remaining), ...Object.keys(monthly)])) {
|
|
1851
|
+
const entitlement = numberOrUndefined(monthly[category]);
|
|
1852
|
+
const left = numberOrUndefined(remaining[category]);
|
|
1853
|
+
quotas[category] = removeUndefinedQuota({
|
|
1854
|
+
entitlement,
|
|
1855
|
+
percentRemaining: entitlement !== void 0 && entitlement > 0 && left !== void 0 ? left / entitlement * 100 : void 0,
|
|
1856
|
+
remaining: left,
|
|
1857
|
+
used: usedFrom(entitlement, left)
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1292
1860
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}),
|
|
1300
|
-
output_tokens: outputTokens,
|
|
1301
|
-
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
1302
|
-
reasoning_tokens: 0
|
|
1303
|
-
}),
|
|
1304
|
-
total_tokens: record.total_tokens
|
|
1861
|
+
return removeUndefinedUsage({
|
|
1862
|
+
accessTypeSku: stringOrUndefined(record.access_type_sku),
|
|
1863
|
+
chatEnabled: typeof record.chat_enabled === "boolean" ? record.chat_enabled : void 0,
|
|
1864
|
+
plan: stringOrUndefined(record.copilot_plan),
|
|
1865
|
+
quotaResetDate: stringOrUndefined(record.quota_reset_date) ?? stringOrUndefined(record.quota_reset_date_utc) ?? stringOrUndefined(record.limited_user_reset_date),
|
|
1866
|
+
quotas
|
|
1305
1867
|
});
|
|
1306
1868
|
}
|
|
1307
|
-
function
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1869
|
+
function normalizeQuotaDetail(detail) {
|
|
1870
|
+
const entitlement = numberOrUndefined(detail.entitlement);
|
|
1871
|
+
const overageCount = numberOrUndefined(detail.overage_count);
|
|
1872
|
+
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
1873
|
+
return removeUndefinedQuota({
|
|
1874
|
+
entitlement,
|
|
1875
|
+
hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
|
|
1876
|
+
overageCount,
|
|
1877
|
+
overageEntitlement: numberOrUndefined(detail.overage_entitlement),
|
|
1878
|
+
overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
|
|
1879
|
+
percentRemaining: numberOrUndefined(detail.percent_remaining),
|
|
1880
|
+
quotaId: stringOrUndefined(detail.quota_id),
|
|
1881
|
+
quotaResetAt: stringOrUndefined(detail.quota_reset_at),
|
|
1882
|
+
remaining,
|
|
1883
|
+
timestampUtc: stringOrUndefined(detail.timestamp_utc),
|
|
1884
|
+
tokenBasedBilling: typeof detail.token_based_billing === "boolean" ? detail.token_based_billing : void 0,
|
|
1885
|
+
unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
|
|
1886
|
+
used: usedFrom(entitlement, remaining, overageCount)
|
|
1887
|
+
});
|
|
1313
1888
|
}
|
|
1314
|
-
function
|
|
1315
|
-
|
|
1316
|
-
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
1317
|
-
const completion = firstNumber(record.completion_tokens, record.output_tokens);
|
|
1318
|
-
const total = firstNumber(record.total_tokens);
|
|
1319
|
-
if (prompt === void 0 && completion === void 0 && total === void 0) {
|
|
1889
|
+
function usedFrom(entitlement, remaining, overageCount) {
|
|
1890
|
+
if (entitlement === void 0 || remaining === void 0) {
|
|
1320
1891
|
return void 0;
|
|
1321
1892
|
}
|
|
1322
|
-
const
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
asRecord(record.completion_tokens_details).reasoning_tokens,
|
|
1326
|
-
asRecord(record.output_tokens_details).reasoning_tokens
|
|
1327
|
-
);
|
|
1328
|
-
const cached = firstNumber(
|
|
1329
|
-
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
1330
|
-
asRecord(record.input_tokens_details).cached_tokens
|
|
1331
|
-
);
|
|
1332
|
-
return removeUndefined({
|
|
1333
|
-
cachedTokens: cached,
|
|
1334
|
-
completionTokens,
|
|
1335
|
-
promptTokens,
|
|
1336
|
-
reasoningTokens: reasoning,
|
|
1337
|
-
totalTokens: total ?? promptTokens + completionTokens
|
|
1338
|
-
});
|
|
1893
|
+
const base = entitlement - remaining;
|
|
1894
|
+
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
1895
|
+
return Math.max(0, base + overage);
|
|
1339
1896
|
}
|
|
1340
|
-
function
|
|
1341
|
-
|
|
1342
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1343
|
-
return value;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
return void 0;
|
|
1897
|
+
function numberOrUndefined(value) {
|
|
1898
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1347
1899
|
}
|
|
1348
|
-
function
|
|
1349
|
-
return
|
|
1900
|
+
function stringOrUndefined(value) {
|
|
1901
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1350
1902
|
}
|
|
1351
|
-
function
|
|
1352
|
-
|
|
1353
|
-
|
|
1903
|
+
function removeUndefinedQuota(quota) {
|
|
1904
|
+
return Object.fromEntries(
|
|
1905
|
+
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
1906
|
+
);
|
|
1354
1907
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
markTerminal();
|
|
1382
|
-
enqueue({ error });
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
const choices = completionChoices(parsed).map((choice, index) => {
|
|
1386
|
-
const delta = asRecord(choice.delta);
|
|
1387
|
-
const text = contentToText(delta.content);
|
|
1388
|
-
const finishReason = choice.finish_reason ?? null;
|
|
1389
|
-
if (!text && finishReason === null) {
|
|
1390
|
-
return void 0;
|
|
1391
|
-
}
|
|
1392
|
-
return {
|
|
1393
|
-
finish_reason: finishReason,
|
|
1394
|
-
index: typeof choice.index === "number" ? choice.index : index,
|
|
1395
|
-
logprobs: choice.logprobs ?? null,
|
|
1396
|
-
text
|
|
1397
|
-
};
|
|
1398
|
-
}).filter((choice) => choice !== void 0);
|
|
1399
|
-
const usage = asRecord(parsed.usage);
|
|
1400
|
-
const hasUsage = Object.keys(usage).length > 0;
|
|
1401
|
-
if (choices.length === 0 && !hasUsage) {
|
|
1402
|
-
return;
|
|
1908
|
+
function removeUndefinedUsage(usage) {
|
|
1909
|
+
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
1910
|
+
return Object.fromEntries(entries);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/github-device.ts
|
|
1914
|
+
var import_promises = require("timers/promises");
|
|
1915
|
+
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
1916
|
+
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
1917
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1918
|
+
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
1919
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
1920
|
+
async function githubCopilotDeviceLogin(options = {}) {
|
|
1921
|
+
const env = options.env ?? process.env;
|
|
1922
|
+
const fetcher = options.fetch ?? fetch;
|
|
1923
|
+
const sleeper = options.sleep ?? import_promises.setTimeout;
|
|
1924
|
+
const domain = normalizeDomain(
|
|
1925
|
+
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
1926
|
+
);
|
|
1927
|
+
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
1928
|
+
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
1929
|
+
const verificationUrl = device.verification_uri;
|
|
1930
|
+
const userCode = device.user_code;
|
|
1931
|
+
const deviceCode = device.device_code;
|
|
1932
|
+
if (!verificationUrl || !userCode || !deviceCode) {
|
|
1933
|
+
throw new Error("GitHub device authorization response is missing required fields.");
|
|
1403
1934
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1935
|
+
options.logger?.info(`First copy your one-time code: ${userCode}`);
|
|
1936
|
+
options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
|
|
1937
|
+
await options.openBrowser?.(verificationUrl);
|
|
1938
|
+
return {
|
|
1939
|
+
domain,
|
|
1940
|
+
token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
|
|
1941
|
+
deviceCode,
|
|
1942
|
+
expiresIn: positiveSeconds(device.expires_in, 900),
|
|
1943
|
+
interval: positiveSeconds(device.interval, 5)
|
|
1412
1944
|
})
|
|
1413
|
-
|
|
1945
|
+
};
|
|
1414
1946
|
}
|
|
1415
|
-
function
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1947
|
+
async function requestDeviceCode(fetcher, domain, clientId) {
|
|
1948
|
+
const response = await fetcher(`https://${domain}/login/device/code`, {
|
|
1949
|
+
body: JSON.stringify({
|
|
1950
|
+
client_id: clientId,
|
|
1951
|
+
scope: "read:user"
|
|
1952
|
+
}),
|
|
1953
|
+
headers: oauthHeaders(),
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1956
|
+
});
|
|
1957
|
+
if (!response.ok) {
|
|
1958
|
+
throw new Error(
|
|
1959
|
+
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
1960
|
+
response
|
|
1961
|
+
)}`
|
|
1962
|
+
);
|
|
1421
1963
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1964
|
+
return parseJsonResponse(
|
|
1965
|
+
response,
|
|
1966
|
+
"GitHub device authorization response was not valid JSON"
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
|
|
1970
|
+
let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
|
|
1971
|
+
const deadline = Date.now() + device.expiresIn * 1e3;
|
|
1972
|
+
while (Date.now() < deadline) {
|
|
1973
|
+
await sleeper(intervalMs);
|
|
1974
|
+
const response = await fetcher(`https://${domain}/login/oauth/access_token`, {
|
|
1975
|
+
body: JSON.stringify({
|
|
1976
|
+
client_id: clientId,
|
|
1977
|
+
device_code: device.deviceCode,
|
|
1978
|
+
grant_type: DEVICE_GRANT_TYPE
|
|
1979
|
+
}),
|
|
1980
|
+
headers: oauthHeaders(),
|
|
1981
|
+
method: "POST",
|
|
1982
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1427
1983
|
});
|
|
1984
|
+
if (!response.ok) {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
1987
|
+
response
|
|
1988
|
+
)}`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
const data = await parseJsonResponse(
|
|
1992
|
+
response,
|
|
1993
|
+
"GitHub device token response was not valid JSON"
|
|
1994
|
+
);
|
|
1995
|
+
if (data.access_token) {
|
|
1996
|
+
return data.access_token;
|
|
1997
|
+
}
|
|
1998
|
+
if (data.error === "authorization_pending") {
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
if (data.error === "slow_down") {
|
|
2002
|
+
intervalMs = positiveSeconds(data.interval, device.interval + 5) * 1e3 + POLLING_SAFETY_MARGIN_MS;
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
if (data.error === "expired_token") {
|
|
2006
|
+
throw new Error("GitHub device login expired. Run `hoopilot login` again.");
|
|
2007
|
+
}
|
|
2008
|
+
if (data.error === "access_denied") {
|
|
2009
|
+
throw new Error("GitHub device login was cancelled.");
|
|
2010
|
+
}
|
|
2011
|
+
if (data.error) {
|
|
2012
|
+
throw new Error(data.error_description || `GitHub device login failed: ${data.error}`);
|
|
2013
|
+
}
|
|
1428
2014
|
}
|
|
1429
|
-
|
|
2015
|
+
throw new Error("GitHub device login timed out. Run `hoopilot login` again.");
|
|
1430
2016
|
}
|
|
1431
|
-
function
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const content = contentToText(delta.content);
|
|
1447
|
-
if (content) {
|
|
1448
|
-
handlers.appendText(content);
|
|
2017
|
+
function oauthHeaders() {
|
|
2018
|
+
const headers = new Headers();
|
|
2019
|
+
headers.set("accept", "application/json");
|
|
2020
|
+
headers.set("content-type", "application/json");
|
|
2021
|
+
headers.set("user-agent", "hoopilot");
|
|
2022
|
+
return headers;
|
|
2023
|
+
}
|
|
2024
|
+
function normalizeDomain(value) {
|
|
2025
|
+
const raw = value.trim();
|
|
2026
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
2027
|
+
let url;
|
|
2028
|
+
try {
|
|
2029
|
+
url = new URL(withScheme);
|
|
2030
|
+
} catch {
|
|
2031
|
+
throw new Error(`Invalid GitHub domain: ${value}.`);
|
|
1449
2032
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
handlers.appendToolCall(asRecord(toolCall));
|
|
2033
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !url.hostname || url.pathname !== "" && url.pathname !== "/" || url.search || url.hash) {
|
|
2034
|
+
throw new Error(`Invalid GitHub domain: ${value}. Provide only a hostname.`);
|
|
1453
2035
|
}
|
|
2036
|
+
return url.host;
|
|
1454
2037
|
}
|
|
1455
|
-
function
|
|
1456
|
-
return
|
|
1457
|
-
created_at: createdAt,
|
|
1458
|
-
error: null,
|
|
1459
|
-
id,
|
|
1460
|
-
incomplete_details: null,
|
|
1461
|
-
instructions: null,
|
|
1462
|
-
max_output_tokens: null,
|
|
1463
|
-
metadata: {},
|
|
1464
|
-
model,
|
|
1465
|
-
object: "response",
|
|
1466
|
-
output,
|
|
1467
|
-
parallel_tool_calls: true,
|
|
1468
|
-
status,
|
|
1469
|
-
temperature: null,
|
|
1470
|
-
tool_choice: "auto",
|
|
1471
|
-
tools: [],
|
|
1472
|
-
top_p: null
|
|
1473
|
-
};
|
|
2038
|
+
function positiveSeconds(value, fallback) {
|
|
2039
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1474
2040
|
}
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
|
|
2041
|
+
async function parseJsonResponse(response, context) {
|
|
2042
|
+
const text = await response.text();
|
|
2043
|
+
try {
|
|
2044
|
+
return JSON.parse(text);
|
|
2045
|
+
} catch {
|
|
2046
|
+
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
1478
2047
|
}
|
|
1479
|
-
|
|
1480
|
-
data: ${JSON.stringify(data)}
|
|
2048
|
+
}
|
|
1481
2049
|
|
|
1482
|
-
|
|
2050
|
+
// src/logger.ts
|
|
2051
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
2052
|
+
var import_pino_pretty = __toESM(require("pino-pretty"), 1);
|
|
2053
|
+
var DEFAULT_LOG_FORMAT = "pretty";
|
|
2054
|
+
var DEFAULT_LOG_LEVEL = "info";
|
|
2055
|
+
var LOG_FORMATS = ["json", "pretty"];
|
|
2056
|
+
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
2057
|
+
var REDACT_PATHS = [
|
|
2058
|
+
"apiKey",
|
|
2059
|
+
"authorization",
|
|
2060
|
+
"cookie",
|
|
2061
|
+
"headers.authorization",
|
|
2062
|
+
"headers.Authorization",
|
|
2063
|
+
"headers.cookie",
|
|
2064
|
+
"headers.Cookie",
|
|
2065
|
+
"headers.x-api-key",
|
|
2066
|
+
"headers.X-Api-Key",
|
|
2067
|
+
"token",
|
|
2068
|
+
"*.apiKey",
|
|
2069
|
+
"*.authorization",
|
|
2070
|
+
"*.cookie",
|
|
2071
|
+
"*.token",
|
|
2072
|
+
"*.headers.authorization",
|
|
2073
|
+
"*.headers.Authorization",
|
|
2074
|
+
"*.headers.cookie",
|
|
2075
|
+
"*.headers.Cookie",
|
|
2076
|
+
"*.headers.x-api-key",
|
|
2077
|
+
"*.headers.X-Api-Key"
|
|
2078
|
+
];
|
|
2079
|
+
var noopLogger = {
|
|
2080
|
+
child: () => noopLogger,
|
|
2081
|
+
debug: () => {
|
|
2082
|
+
},
|
|
2083
|
+
error: () => {
|
|
2084
|
+
},
|
|
2085
|
+
fatal: () => {
|
|
2086
|
+
},
|
|
2087
|
+
info: () => {
|
|
2088
|
+
},
|
|
2089
|
+
trace: () => {
|
|
2090
|
+
},
|
|
2091
|
+
warn: () => {
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
function createHoopilotLogger(options = {}) {
|
|
2095
|
+
const env = options.env ?? process.env;
|
|
2096
|
+
const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
|
|
2097
|
+
const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
|
|
2098
|
+
const pinoOptions = {
|
|
2099
|
+
base: {
|
|
2100
|
+
service: "hoopilot",
|
|
2101
|
+
...options.base
|
|
2102
|
+
},
|
|
2103
|
+
level,
|
|
2104
|
+
redact: {
|
|
2105
|
+
censor: "[Redacted]",
|
|
2106
|
+
paths: REDACT_PATHS
|
|
2107
|
+
},
|
|
2108
|
+
timestamp: import_pino.default.stdTimeFunctions.isoTime
|
|
2109
|
+
};
|
|
2110
|
+
if (format === "pretty") {
|
|
2111
|
+
return (0, import_pino.default)(
|
|
2112
|
+
pinoOptions,
|
|
2113
|
+
(0, import_pino_pretty.default)({
|
|
2114
|
+
colorize: options.colorize ?? process.stderr.isTTY,
|
|
2115
|
+
destination: options.stream ?? 1,
|
|
2116
|
+
ignore: "pid,hostname",
|
|
2117
|
+
singleLine: true,
|
|
2118
|
+
translateTime: "SYS:standard"
|
|
2119
|
+
})
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
if (options.stream) {
|
|
2123
|
+
return (0, import_pino.default)(pinoOptions, options.stream);
|
|
2124
|
+
}
|
|
2125
|
+
return (0, import_pino.default)(pinoOptions);
|
|
1483
2126
|
}
|
|
1484
|
-
function
|
|
1485
|
-
if (
|
|
1486
|
-
return
|
|
2127
|
+
function parseLogFormat(value) {
|
|
2128
|
+
if (!value) {
|
|
2129
|
+
return DEFAULT_LOG_FORMAT;
|
|
1487
2130
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
2131
|
+
if (isLogFormat(value)) {
|
|
2132
|
+
return value;
|
|
2133
|
+
}
|
|
2134
|
+
throw new Error(`Invalid log format: ${value}. Expected one of: ${LOG_FORMATS.join(", ")}.`);
|
|
1491
2135
|
}
|
|
1492
|
-
function
|
|
1493
|
-
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
|
|
2136
|
+
function parseLogLevel(value) {
|
|
2137
|
+
if (!value) {
|
|
2138
|
+
return DEFAULT_LOG_LEVEL;
|
|
2139
|
+
}
|
|
2140
|
+
if (isLogLevel(value)) {
|
|
2141
|
+
return value;
|
|
1497
2142
|
}
|
|
2143
|
+
throw new Error(`Invalid log level: ${value}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
|
|
1498
2144
|
}
|
|
1499
|
-
function
|
|
1500
|
-
return
|
|
2145
|
+
function shouldCreateLogger(options) {
|
|
2146
|
+
return Boolean(
|
|
2147
|
+
options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
|
|
2148
|
+
);
|
|
1501
2149
|
}
|
|
1502
|
-
function
|
|
1503
|
-
|
|
2150
|
+
function errorDetails(error) {
|
|
2151
|
+
if (error instanceof Error) {
|
|
2152
|
+
return {
|
|
2153
|
+
message: error.message,
|
|
2154
|
+
name: error.name,
|
|
2155
|
+
stack: error.stack
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
return { message: String(error) };
|
|
1504
2159
|
}
|
|
1505
|
-
function
|
|
1506
|
-
return
|
|
2160
|
+
function isLogFormat(value) {
|
|
2161
|
+
return LOG_FORMATS.includes(value);
|
|
2162
|
+
}
|
|
2163
|
+
function isLogLevel(value) {
|
|
2164
|
+
return LOG_LEVELS.includes(value);
|
|
1507
2165
|
}
|
|
1508
2166
|
|
|
1509
2167
|
// src/metrics.ts
|
|
@@ -1712,11 +2370,43 @@ var MetricsRegistry = class {
|
|
|
1712
2370
|
gauge("remaining", "Remaining quota for the Copilot category.", (q) => q.remaining);
|
|
1713
2371
|
gauge("entitlement", "Quota entitlement for the Copilot category.", (q) => q.entitlement);
|
|
1714
2372
|
gauge("used", "Used quota (entitlement minus remaining) for the category.", (q) => q.used);
|
|
2373
|
+
gauge("overage_count", "Overage count for the Copilot category.", (q) => q.overageCount);
|
|
2374
|
+
gauge(
|
|
2375
|
+
"overage_entitlement",
|
|
2376
|
+
"Overage entitlement for the Copilot category.",
|
|
2377
|
+
(q) => q.overageEntitlement
|
|
2378
|
+
);
|
|
1715
2379
|
gauge(
|
|
1716
2380
|
"percent_remaining",
|
|
1717
2381
|
"Percent of quota remaining for the Copilot category.",
|
|
1718
2382
|
(q) => q.percentRemaining
|
|
1719
2383
|
);
|
|
2384
|
+
booleanGauge(
|
|
2385
|
+
"unlimited",
|
|
2386
|
+
"Whether the Copilot quota category is unlimited.",
|
|
2387
|
+
(q) => q.unlimited
|
|
2388
|
+
);
|
|
2389
|
+
booleanGauge(
|
|
2390
|
+
"overage_permitted",
|
|
2391
|
+
"Whether overage is permitted for the Copilot category.",
|
|
2392
|
+
(q) => q.overagePermitted
|
|
2393
|
+
);
|
|
2394
|
+
booleanGauge("has_quota", "Whether the Copilot quota category has a quota.", (q) => q.hasQuota);
|
|
2395
|
+
booleanGauge(
|
|
2396
|
+
"token_based_billing",
|
|
2397
|
+
"Whether the Copilot quota category uses token-based billing.",
|
|
2398
|
+
(q) => q.tokenBasedBilling
|
|
2399
|
+
);
|
|
2400
|
+
dateGauge(
|
|
2401
|
+
"category_reset_timestamp_seconds",
|
|
2402
|
+
"Unix epoch of the Copilot category-specific quota reset.",
|
|
2403
|
+
(q) => q.quotaResetAt
|
|
2404
|
+
);
|
|
2405
|
+
dateGauge(
|
|
2406
|
+
"category_snapshot_timestamp_seconds",
|
|
2407
|
+
"Unix epoch of the Copilot category quota snapshot.",
|
|
2408
|
+
(q) => q.timestampUtc
|
|
2409
|
+
);
|
|
1720
2410
|
const resetMs = usage.quotaResetDate ? Date.parse(usage.quotaResetDate) : Number.NaN;
|
|
1721
2411
|
if (Number.isFinite(resetMs)) {
|
|
1722
2412
|
lines.push(
|
|
@@ -1735,6 +2425,30 @@ var MetricsRegistry = class {
|
|
|
1735
2425
|
})} 1`
|
|
1736
2426
|
);
|
|
1737
2427
|
}
|
|
2428
|
+
function booleanGauge(suffix, help, pick) {
|
|
2429
|
+
const present = categories.filter(([, quota]) => pick(quota) !== void 0);
|
|
2430
|
+
if (present.length === 0) {
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
|
|
2434
|
+
lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
|
|
2435
|
+
for (const [category, quota] of present) {
|
|
2436
|
+
lines.push(
|
|
2437
|
+
`hoopilot_copilot_quota_${suffix}${labels({ category })} ${pick(quota) ? 1 : 0}`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
function dateGauge(suffix, help, pick) {
|
|
2442
|
+
const present = categories.map(([category, quota]) => [category, Date.parse(pick(quota) ?? "")]).filter(([, timestamp]) => Number.isFinite(timestamp));
|
|
2443
|
+
if (present.length === 0) {
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
lines.push(`# HELP hoopilot_copilot_quota_${suffix} ${help}`);
|
|
2447
|
+
lines.push(`# TYPE hoopilot_copilot_quota_${suffix} gauge`);
|
|
2448
|
+
for (const [category, timestamp] of present) {
|
|
2449
|
+
lines.push(`hoopilot_copilot_quota_${suffix}${labels({ category })} ${timestamp / 1e3}`);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
1738
2452
|
}
|
|
1739
2453
|
};
|
|
1740
2454
|
function observeResponseUsage(response, fallbackModel, onUsage, signal) {
|
|
@@ -1877,6 +2591,7 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
1877
2591
|
var DEFAULT_PORT = 4141;
|
|
1878
2592
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1879
2593
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
2594
|
+
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
1880
2595
|
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1881
2596
|
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1882
2597
|
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
@@ -1946,6 +2661,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1946
2661
|
if (request.method === "GET" && apiPath === "/v1/models") {
|
|
1947
2662
|
return finish(await handleModels(client, metrics, request.signal, requestLogger));
|
|
1948
2663
|
}
|
|
2664
|
+
if (request.method === "POST" && apiPath === "/v1/messages") {
|
|
2665
|
+
return finish(
|
|
2666
|
+
await handleAnthropicMessages(client, metrics, recordTokens, request, requestLogger)
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
|
|
2670
|
+
return finish(handleAnthropicCountTokens(await readJson(request)));
|
|
2671
|
+
}
|
|
1949
2672
|
if (request.method === "POST" && apiPath === "/v1/chat/completions") {
|
|
1950
2673
|
return finish(
|
|
1951
2674
|
await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
|
|
@@ -1969,16 +2692,16 @@ function createHoopilotHandler(options = {}) {
|
|
|
1969
2692
|
return finish(jsonError(401, "copilot_auth_error", error.message));
|
|
1970
2693
|
}
|
|
1971
2694
|
const message = errorMessage(error);
|
|
1972
|
-
if (message === INVALID_JSON_MESSAGE) {
|
|
2695
|
+
if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
|
|
1973
2696
|
requestLogger.warn(
|
|
1974
2697
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1975
|
-
"request body was
|
|
2698
|
+
"request body was not usable json"
|
|
1976
2699
|
);
|
|
1977
2700
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1978
|
-
} else if (error instanceof OpenAICompatibilityError) {
|
|
2701
|
+
} else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
1979
2702
|
requestLogger.warn(
|
|
1980
2703
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1981
|
-
"request body used unsupported
|
|
2704
|
+
"request body used unsupported compatibility fields"
|
|
1982
2705
|
);
|
|
1983
2706
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1984
2707
|
} else if (error instanceof RequestBodyTooLargeError) {
|
|
@@ -2022,6 +2745,40 @@ function startHoopilotServer(options = {}) {
|
|
|
2022
2745
|
url: `http://${urlHost(host)}:${server.port}`
|
|
2023
2746
|
};
|
|
2024
2747
|
}
|
|
2748
|
+
async function handleAnthropicMessages(client, metrics, recordTokens, request, logger) {
|
|
2749
|
+
const anthropicRequest = await readJson(request);
|
|
2750
|
+
const responsesRequest = anthropicMessagesToResponsesRequest(anthropicRequest);
|
|
2751
|
+
const upstream = await client.responses(JSON.stringify(responsesRequest), request.signal);
|
|
2752
|
+
metrics.recordUpstream("/responses", upstream.ok);
|
|
2753
|
+
if (!upstream.ok) {
|
|
2754
|
+
return proxyError(upstream, logger);
|
|
2755
|
+
}
|
|
2756
|
+
logUpstreamSuccess(logger, "/responses", upstream.status);
|
|
2757
|
+
const model = normalizeRequestedModel(responsesRequest.model);
|
|
2758
|
+
if (isStreamingResponse(upstream) && upstream.body) {
|
|
2759
|
+
const observed = observeResponseUsage(upstream, model, recordTokens, request.signal);
|
|
2760
|
+
if (!observed.body) {
|
|
2761
|
+
return proxyResponse(observed);
|
|
2762
|
+
}
|
|
2763
|
+
return proxyResponse(
|
|
2764
|
+
new Response(responsesStreamToAnthropicStream(observed.body, { model }), {
|
|
2765
|
+
headers: observed.headers,
|
|
2766
|
+
status: observed.status,
|
|
2767
|
+
statusText: observed.statusText
|
|
2768
|
+
})
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
const body = asRecord(await upstream.json());
|
|
2772
|
+
const usage = extractTokenUsage(body.usage);
|
|
2773
|
+
if (usage) {
|
|
2774
|
+
const responseModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
2775
|
+
recordTokens(responseModel || model, usage);
|
|
2776
|
+
}
|
|
2777
|
+
return jsonResponse(responsesResponseToAnthropicMessage(body, model));
|
|
2778
|
+
}
|
|
2779
|
+
function handleAnthropicCountTokens(body) {
|
|
2780
|
+
return jsonResponse(estimateAnthropicMessageTokens(body));
|
|
2781
|
+
}
|
|
2025
2782
|
async function handleModels(client, metrics, signal, logger) {
|
|
2026
2783
|
const upstream = await client.models(signal);
|
|
2027
2784
|
metrics.recordUpstream("/models", upstream.ok);
|
|
@@ -2129,20 +2886,24 @@ function proxyResponse(upstream) {
|
|
|
2129
2886
|
}
|
|
2130
2887
|
async function readJson(request) {
|
|
2131
2888
|
const text = await readRequestText(request);
|
|
2889
|
+
return parseJsonObject2(text);
|
|
2890
|
+
}
|
|
2891
|
+
function parseJsonObject2(text) {
|
|
2892
|
+
let parsed;
|
|
2132
2893
|
try {
|
|
2133
|
-
|
|
2894
|
+
parsed = JSON.parse(text);
|
|
2134
2895
|
} catch {
|
|
2135
2896
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
2136
2897
|
}
|
|
2898
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2899
|
+
throw new Error(JSON_OBJECT_MESSAGE);
|
|
2900
|
+
}
|
|
2901
|
+
return parsed;
|
|
2137
2902
|
}
|
|
2138
2903
|
async function readJsonText(request) {
|
|
2139
2904
|
const text = await readRequestText(request);
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
return text;
|
|
2143
|
-
} catch {
|
|
2144
|
-
throw new Error(INVALID_JSON_MESSAGE);
|
|
2145
|
-
}
|
|
2905
|
+
parseJsonObject2(text);
|
|
2906
|
+
return text;
|
|
2146
2907
|
}
|
|
2147
2908
|
async function readRequestText(request) {
|
|
2148
2909
|
const contentLength = request.headers.get("content-length");
|
|
@@ -2217,9 +2978,10 @@ function websocketUnsupportedResponse() {
|
|
|
2217
2978
|
}
|
|
2218
2979
|
function corsHeaders() {
|
|
2219
2980
|
return {
|
|
2220
|
-
"access-control-allow-headers": "authorization, content-type, x-api-key",
|
|
2981
|
+
"access-control-allow-headers": "anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, content-type, x-api-key, x-request-id",
|
|
2221
2982
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
2222
|
-
"access-control-allow-origin": "*"
|
|
2983
|
+
"access-control-allow-origin": "*",
|
|
2984
|
+
"access-control-expose-headers": "x-request-id"
|
|
2223
2985
|
};
|
|
2224
2986
|
}
|
|
2225
2987
|
function isAuthorized(request, apiKey) {
|
|
@@ -2371,6 +3133,10 @@ function canonicalApiPath(path) {
|
|
|
2371
3133
|
return "/v1/chat/completions";
|
|
2372
3134
|
case "/completions":
|
|
2373
3135
|
return "/v1/completions";
|
|
3136
|
+
case "/messages":
|
|
3137
|
+
return "/v1/messages";
|
|
3138
|
+
case "/messages/count_tokens":
|
|
3139
|
+
return "/v1/messages/count_tokens";
|
|
2374
3140
|
case "/responses":
|
|
2375
3141
|
return "/v1/responses";
|
|
2376
3142
|
case "/usage":
|
|
@@ -2395,6 +3161,12 @@ function routeFor(method, path) {
|
|
|
2395
3161
|
if (method === "GET" && path === "/v1/models") {
|
|
2396
3162
|
return "models";
|
|
2397
3163
|
}
|
|
3164
|
+
if (method === "POST" && path === "/v1/messages") {
|
|
3165
|
+
return "anthropic_messages";
|
|
3166
|
+
}
|
|
3167
|
+
if (method === "POST" && path === "/v1/messages/count_tokens") {
|
|
3168
|
+
return "anthropic_count_tokens";
|
|
3169
|
+
}
|
|
2398
3170
|
if (method === "POST" && path === "/v1/chat/completions") {
|
|
2399
3171
|
return "chat_completions";
|
|
2400
3172
|
}
|
|
@@ -2432,8 +3204,8 @@ function metricsResponse(metrics) {
|
|
|
2432
3204
|
});
|
|
2433
3205
|
}
|
|
2434
3206
|
async function handleUsage(metrics, readUsage, signal) {
|
|
2435
|
-
const proxy = metrics.snapshot();
|
|
2436
3207
|
const { copilot, error } = await readUsage(signal);
|
|
3208
|
+
const proxy = metrics.snapshot();
|
|
2437
3209
|
const body = { copilot: copilot ?? null, object: "usage", proxy };
|
|
2438
3210
|
if (error) {
|
|
2439
3211
|
body.copilot_error = error;
|
|
@@ -2458,10 +3230,10 @@ function createUsageReader(client, metrics, now = Date.now, ttlMs = USAGE_CACHE_
|
|
|
2458
3230
|
metrics.recordCopilotQuota(value);
|
|
2459
3231
|
return { copilot: value };
|
|
2460
3232
|
} catch (error) {
|
|
2461
|
-
metrics.recordUpstream(usagePath, false);
|
|
2462
3233
|
if (error instanceof CopilotAuthError) {
|
|
2463
3234
|
return { error: error.message };
|
|
2464
3235
|
}
|
|
3236
|
+
metrics.recordUpstream(usagePath, false);
|
|
2465
3237
|
return { error: errorMessage(error) };
|
|
2466
3238
|
}
|
|
2467
3239
|
};
|
|
@@ -2475,6 +3247,7 @@ function safeParseJson(text) {
|
|
|
2475
3247
|
}
|
|
2476
3248
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2477
3249
|
0 && (module.exports = {
|
|
3250
|
+
AnthropicCompatibilityError,
|
|
2478
3251
|
COPILOT_USAGE_API_VERSION,
|
|
2479
3252
|
CopilotAuth,
|
|
2480
3253
|
CopilotAuthError,
|
|
@@ -2485,6 +3258,7 @@ function safeParseJson(text) {
|
|
|
2485
3258
|
DEFAULT_MODEL,
|
|
2486
3259
|
MetricsRegistry,
|
|
2487
3260
|
PROMETHEUS_CONTENT_TYPE,
|
|
3261
|
+
anthropicMessagesToResponsesRequest,
|
|
2488
3262
|
applyCopilotHeaders,
|
|
2489
3263
|
applyGithubApiHeaders,
|
|
2490
3264
|
authStorePath,
|
|
@@ -2494,6 +3268,7 @@ function safeParseJson(text) {
|
|
|
2494
3268
|
completionsRequestToChatCompletion,
|
|
2495
3269
|
createHoopilotHandler,
|
|
2496
3270
|
createHoopilotLogger,
|
|
3271
|
+
estimateAnthropicMessageTokens,
|
|
2497
3272
|
extractTokenUsage,
|
|
2498
3273
|
fallbackModels,
|
|
2499
3274
|
githubCopilotDeviceLogin,
|
|
@@ -2507,7 +3282,9 @@ function safeParseJson(text) {
|
|
|
2507
3282
|
parseLogLevel,
|
|
2508
3283
|
readStoredCopilotAuth,
|
|
2509
3284
|
responsesRequestToChatCompletion,
|
|
3285
|
+
responsesResponseToAnthropicMessage,
|
|
2510
3286
|
responsesStreamFromChatStream,
|
|
3287
|
+
responsesStreamToAnthropicStream,
|
|
2511
3288
|
startHoopilotServer,
|
|
2512
3289
|
writeStoredCopilotAuth
|
|
2513
3290
|
});
|