@openhoo/hoopilot 0.7.5 → 0.8.1
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 +16 -2
- package/dist/cli.js +734 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1967 -1255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1962 -1255
- 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,642 +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 overageCount = numberOrUndefined(detail.overage_count);
|
|
403
|
-
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
404
|
-
return removeUndefinedQuota({
|
|
405
|
-
entitlement,
|
|
406
|
-
hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
|
|
407
|
-
overageCount,
|
|
408
|
-
overageEntitlement: numberOrUndefined(detail.overage_entitlement),
|
|
409
|
-
overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
|
|
410
|
-
percentRemaining: numberOrUndefined(detail.percent_remaining),
|
|
411
|
-
quotaId: stringOrUndefined(detail.quota_id),
|
|
412
|
-
quotaResetAt: stringOrUndefined(detail.quota_reset_at),
|
|
413
|
-
remaining,
|
|
414
|
-
timestampUtc: stringOrUndefined(detail.timestamp_utc),
|
|
415
|
-
tokenBasedBilling: typeof detail.token_based_billing === "boolean" ? detail.token_based_billing : void 0,
|
|
416
|
-
unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
|
|
417
|
-
used: usedFrom(entitlement, remaining, overageCount)
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
function usedFrom(entitlement, remaining, overageCount) {
|
|
421
|
-
if (entitlement === void 0 || remaining === void 0) {
|
|
422
|
-
return void 0;
|
|
423
|
-
}
|
|
424
|
-
const base = entitlement - remaining;
|
|
425
|
-
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
426
|
-
return Math.max(0, base + overage);
|
|
427
|
-
}
|
|
428
|
-
function numberOrUndefined(value) {
|
|
429
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
430
|
-
}
|
|
431
|
-
function stringOrUndefined(value) {
|
|
432
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
433
|
-
}
|
|
434
|
-
function removeUndefinedQuota(quota) {
|
|
435
|
-
return Object.fromEntries(
|
|
436
|
-
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
function removeUndefinedUsage(usage) {
|
|
440
|
-
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
441
|
-
return Object.fromEntries(entries);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// src/github-device.ts
|
|
445
|
-
var import_promises = require("timers/promises");
|
|
446
|
-
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
447
|
-
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
448
|
-
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
449
|
-
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
450
|
-
var REQUEST_TIMEOUT_MS = 15e3;
|
|
451
|
-
async function githubCopilotDeviceLogin(options = {}) {
|
|
452
|
-
const env = options.env ?? process.env;
|
|
453
|
-
const fetcher = options.fetch ?? fetch;
|
|
454
|
-
const sleeper = options.sleep ?? import_promises.setTimeout;
|
|
455
|
-
const domain = normalizeDomain(
|
|
456
|
-
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
457
|
-
);
|
|
458
|
-
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
459
|
-
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
460
|
-
const verificationUrl = device.verification_uri;
|
|
461
|
-
const userCode = device.user_code;
|
|
462
|
-
const deviceCode = device.device_code;
|
|
463
|
-
if (!verificationUrl || !userCode || !deviceCode) {
|
|
464
|
-
throw new Error("GitHub device authorization response is missing required fields.");
|
|
465
|
-
}
|
|
466
|
-
options.logger?.info(`First copy your one-time code: ${userCode}`);
|
|
467
|
-
options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
|
|
468
|
-
await options.openBrowser?.(verificationUrl);
|
|
469
|
-
return {
|
|
470
|
-
domain,
|
|
471
|
-
token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
|
|
472
|
-
deviceCode,
|
|
473
|
-
expiresIn: positiveSeconds(device.expires_in, 900),
|
|
474
|
-
interval: positiveSeconds(device.interval, 5)
|
|
475
|
-
})
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
async function requestDeviceCode(fetcher, domain, clientId) {
|
|
479
|
-
const response = await fetcher(`https://${domain}/login/device/code`, {
|
|
480
|
-
body: JSON.stringify({
|
|
481
|
-
client_id: clientId,
|
|
482
|
-
scope: "read:user"
|
|
483
|
-
}),
|
|
484
|
-
headers: oauthHeaders(),
|
|
485
|
-
method: "POST",
|
|
486
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
487
|
-
});
|
|
488
|
-
if (!response.ok) {
|
|
489
|
-
throw new Error(
|
|
490
|
-
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
491
|
-
response
|
|
492
|
-
)}`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
return parseJsonResponse(
|
|
496
|
-
response,
|
|
497
|
-
"GitHub device authorization response was not valid JSON"
|
|
498
|
-
);
|
|
528
|
+
return messages;
|
|
499
529
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
client_id: clientId,
|
|
508
|
-
device_code: device.deviceCode,
|
|
509
|
-
grant_type: DEVICE_GRANT_TYPE
|
|
510
|
-
}),
|
|
511
|
-
headers: oauthHeaders(),
|
|
512
|
-
method: "POST",
|
|
513
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
514
|
-
});
|
|
515
|
-
if (!response.ok) {
|
|
516
|
-
throw new Error(
|
|
517
|
-
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
518
|
-
response
|
|
519
|
-
)}`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
const data = await parseJsonResponse(
|
|
523
|
-
response,
|
|
524
|
-
"GitHub device token response was not valid JSON"
|
|
525
|
-
);
|
|
526
|
-
if (data.access_token) {
|
|
527
|
-
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;
|
|
528
537
|
}
|
|
529
|
-
|
|
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" });
|
|
530
546
|
continue;
|
|
531
547
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
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" });
|
|
534
562
|
continue;
|
|
535
563
|
}
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
if (data.error === "access_denied") {
|
|
540
|
-
throw new Error("GitHub device login was cancelled.");
|
|
564
|
+
if (type === "input_file") {
|
|
565
|
+
unsupportedResponsesFeature("input_file parts");
|
|
541
566
|
}
|
|
542
|
-
if (
|
|
543
|
-
|
|
567
|
+
if (type === "input_audio") {
|
|
568
|
+
unsupportedResponsesFeature("input_audio parts");
|
|
544
569
|
}
|
|
570
|
+
unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
|
|
545
571
|
}
|
|
546
|
-
|
|
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;
|
|
547
579
|
}
|
|
548
|
-
function
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
+
);
|
|
554
590
|
}
|
|
555
|
-
function
|
|
556
|
-
|
|
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
|
+
}
|
|
557
612
|
}
|
|
558
|
-
function
|
|
559
|
-
|
|
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 "";
|
|
560
634
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return JSON.parse(text);
|
|
565
|
-
} catch {
|
|
566
|
-
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
635
|
+
function responsesRoleToChatRole(role) {
|
|
636
|
+
if (!role) {
|
|
637
|
+
return "user";
|
|
567
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}"`);
|
|
568
643
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
var import_pino_pretty = __toESM(require("pino-pretty"), 1);
|
|
573
|
-
var DEFAULT_LOG_FORMAT = "pretty";
|
|
574
|
-
var DEFAULT_LOG_LEVEL = "info";
|
|
575
|
-
var LOG_FORMATS = ["json", "pretty"];
|
|
576
|
-
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
577
|
-
var REDACT_PATHS = [
|
|
578
|
-
"apiKey",
|
|
579
|
-
"authorization",
|
|
580
|
-
"cookie",
|
|
581
|
-
"headers.authorization",
|
|
582
|
-
"headers.Authorization",
|
|
583
|
-
"headers.cookie",
|
|
584
|
-
"headers.Cookie",
|
|
585
|
-
"headers.x-api-key",
|
|
586
|
-
"headers.X-Api-Key",
|
|
587
|
-
"token",
|
|
588
|
-
"*.apiKey",
|
|
589
|
-
"*.authorization",
|
|
590
|
-
"*.cookie",
|
|
591
|
-
"*.token",
|
|
592
|
-
"*.headers.authorization",
|
|
593
|
-
"*.headers.Authorization",
|
|
594
|
-
"*.headers.cookie",
|
|
595
|
-
"*.headers.Cookie",
|
|
596
|
-
"*.headers.x-api-key",
|
|
597
|
-
"*.headers.X-Api-Key"
|
|
598
|
-
];
|
|
599
|
-
var noopLogger = {
|
|
600
|
-
child: () => noopLogger,
|
|
601
|
-
debug: () => {
|
|
602
|
-
},
|
|
603
|
-
error: () => {
|
|
604
|
-
},
|
|
605
|
-
fatal: () => {
|
|
606
|
-
},
|
|
607
|
-
info: () => {
|
|
608
|
-
},
|
|
609
|
-
trace: () => {
|
|
610
|
-
},
|
|
611
|
-
warn: () => {
|
|
644
|
+
function chatTools(tools) {
|
|
645
|
+
if (!Array.isArray(tools)) {
|
|
646
|
+
return void 0;
|
|
612
647
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
colorize: options.colorize ?? process.stderr.isTTY,
|
|
635
|
-
destination: options.stream ?? 1,
|
|
636
|
-
ignore: "pid,hostname",
|
|
637
|
-
singleLine: true,
|
|
638
|
-
translateTime: "SYS:standard"
|
|
639
|
-
})
|
|
640
|
-
);
|
|
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;
|
|
641
669
|
}
|
|
642
|
-
|
|
643
|
-
|
|
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" };
|
|
644
674
|
}
|
|
645
|
-
|
|
675
|
+
unsupportedResponsesFeature(`tool_choice type "${type || "unknown"}"`);
|
|
646
676
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
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));
|
|
650
687
|
}
|
|
651
|
-
|
|
652
|
-
|
|
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
|
+
);
|
|
653
700
|
}
|
|
654
|
-
|
|
701
|
+
return output;
|
|
655
702
|
}
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
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;
|
|
659
738
|
}
|
|
660
|
-
|
|
661
|
-
|
|
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;
|
|
662
757
|
}
|
|
663
|
-
|
|
758
|
+
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
664
759
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
668
777
|
);
|
|
778
|
+
return removeUndefined({
|
|
779
|
+
cachedTokens: cached,
|
|
780
|
+
completionTokens,
|
|
781
|
+
promptTokens,
|
|
782
|
+
reasoningTokens: reasoning,
|
|
783
|
+
totalTokens: total ?? promptTokens + completionTokens
|
|
784
|
+
});
|
|
669
785
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
stack: error.stack
|
|
676
|
-
};
|
|
786
|
+
function firstNumber(...values) {
|
|
787
|
+
for (const value of values) {
|
|
788
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
677
791
|
}
|
|
678
|
-
return
|
|
792
|
+
return void 0;
|
|
679
793
|
}
|
|
680
|
-
function
|
|
681
|
-
return
|
|
794
|
+
function firstChoice(completion) {
|
|
795
|
+
return completionChoices(completion)[0] ?? {};
|
|
682
796
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
797
|
+
function completionChoices(completion) {
|
|
798
|
+
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
799
|
+
return choices.map((choice) => asRecord(choice));
|
|
685
800
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
+
}
|
|
693
811
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const instructions = contentToText(request.instructions);
|
|
698
|
-
if (instructions) {
|
|
699
|
-
messages.push({ content: instructions, role: "system" });
|
|
812
|
+
const data = dataLines.join("\n");
|
|
813
|
+
if (!data) {
|
|
814
|
+
return;
|
|
700
815
|
}
|
|
701
|
-
|
|
702
|
-
|
|
816
|
+
if (data === "[DONE]") {
|
|
817
|
+
markTerminal();
|
|
818
|
+
enqueue("[DONE]");
|
|
819
|
+
return;
|
|
703
820
|
}
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
user: request.user
|
|
744
|
-
});
|
|
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
|
+
);
|
|
745
860
|
}
|
|
746
|
-
function
|
|
747
|
-
const
|
|
748
|
-
|
|
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;
|
|
749
876
|
}
|
|
750
|
-
function
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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,
|
|
759
904
|
error: null,
|
|
760
905
|
id,
|
|
761
906
|
incomplete_details: null,
|
|
@@ -765,206 +910,106 @@ function chatCompletionToResponse(completion, responseId) {
|
|
|
765
910
|
model,
|
|
766
911
|
object: "response",
|
|
767
912
|
output,
|
|
768
|
-
output_text: outputText(output),
|
|
769
913
|
parallel_tool_calls: true,
|
|
770
|
-
status
|
|
914
|
+
status,
|
|
771
915
|
temperature: null,
|
|
772
916
|
tool_choice: "auto",
|
|
773
|
-
tools: [],
|
|
774
|
-
top_p: null
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
return
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const encoder = new TextEncoder();
|
|
868
|
-
const decoder = new TextDecoder();
|
|
869
|
-
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
870
|
-
const messageId = `msg_${randomId()}`;
|
|
871
|
-
const createdAt = epochSeconds();
|
|
872
|
-
let buffer = "";
|
|
873
|
-
let text = "";
|
|
874
|
-
let messageOutputIndex;
|
|
875
|
-
let nextOutputIndex = 0;
|
|
876
|
-
let sequenceNumber = 0;
|
|
877
|
-
const tools = /* @__PURE__ */ new Map();
|
|
878
|
-
return new ReadableStream({
|
|
879
|
-
async start(controller) {
|
|
880
|
-
const enqueue = (event, data) => {
|
|
881
|
-
controller.enqueue(
|
|
882
|
-
encoder.encode(
|
|
883
|
-
encodeSse(
|
|
884
|
-
event,
|
|
885
|
-
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
886
|
-
)
|
|
887
|
-
)
|
|
888
|
-
);
|
|
889
|
-
};
|
|
890
|
-
enqueue("response.created", {
|
|
891
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
892
|
-
type: "response.created"
|
|
893
|
-
});
|
|
894
|
-
const ensureMessageStarted = () => {
|
|
895
|
-
if (messageOutputIndex !== void 0) {
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
messageOutputIndex = nextOutputIndex++;
|
|
899
|
-
enqueue("response.output_item.added", {
|
|
900
|
-
item: {
|
|
901
|
-
content: [],
|
|
902
|
-
id: messageId,
|
|
903
|
-
role: "assistant",
|
|
904
|
-
status: "in_progress",
|
|
905
|
-
type: "message"
|
|
906
|
-
},
|
|
907
|
-
output_index: messageOutputIndex,
|
|
908
|
-
type: "response.output_item.added"
|
|
909
|
-
});
|
|
910
|
-
enqueue("response.content_part.added", {
|
|
911
|
-
content_index: 0,
|
|
912
|
-
item_id: messageId,
|
|
913
|
-
output_index: messageOutputIndex,
|
|
914
|
-
part: {
|
|
915
|
-
annotations: [],
|
|
916
|
-
text: "",
|
|
917
|
-
type: "output_text"
|
|
918
|
-
},
|
|
919
|
-
type: "response.content_part.added"
|
|
920
|
-
});
|
|
921
|
-
};
|
|
922
|
-
const appendText = (delta) => {
|
|
923
|
-
ensureMessageStarted();
|
|
924
|
-
text += delta;
|
|
925
|
-
enqueue("response.output_text.delta", {
|
|
926
|
-
content_index: 0,
|
|
927
|
-
delta,
|
|
928
|
-
item_id: messageId,
|
|
929
|
-
output_index: messageOutputIndex ?? 0,
|
|
930
|
-
type: "response.output_text.delta"
|
|
931
|
-
});
|
|
932
|
-
};
|
|
933
|
-
const appendToolCall = (toolCall) => {
|
|
934
|
-
const fn = asRecord(toolCall.function);
|
|
935
|
-
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
936
|
-
let existing = tools.get(index);
|
|
937
|
-
const isNew = !existing;
|
|
938
|
-
existing ??= {
|
|
939
|
-
arguments: "",
|
|
940
|
-
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
941
|
-
index,
|
|
942
|
-
itemId: `fc_${randomId()}`,
|
|
943
|
-
name: "",
|
|
944
|
-
outputIndex: nextOutputIndex++
|
|
945
|
-
};
|
|
946
|
-
existing.id = contentToText(toolCall.id) || existing.id;
|
|
947
|
-
existing.name += contentToText(fn.name);
|
|
948
|
-
tools.set(index, existing);
|
|
949
|
-
if (isNew) {
|
|
950
|
-
enqueue("response.output_item.added", {
|
|
951
|
-
item: functionCallItem(existing, "in_progress"),
|
|
952
|
-
output_index: existing.outputIndex ?? 0,
|
|
953
|
-
type: "response.output_item.added"
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
const argumentDelta = contentToText(fn.arguments);
|
|
957
|
-
if (argumentDelta) {
|
|
958
|
-
existing.arguments += argumentDelta;
|
|
959
|
-
enqueue("response.function_call_arguments.delta", {
|
|
960
|
-
delta: argumentDelta,
|
|
961
|
-
item_id: existing.itemId,
|
|
962
|
-
output_index: existing.outputIndex ?? 0,
|
|
963
|
-
type: "response.function_call_arguments.delta"
|
|
964
|
-
});
|
|
965
|
-
}
|
|
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)));
|
|
966
1011
|
};
|
|
967
|
-
const reader =
|
|
1012
|
+
const reader = stream.getReader();
|
|
968
1013
|
try {
|
|
969
1014
|
while (true) {
|
|
970
1015
|
const result = await reader.read();
|
|
@@ -972,547 +1017,1151 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
972
1017
|
break;
|
|
973
1018
|
}
|
|
974
1019
|
buffer += decoder.decode(result.value, { stream: true });
|
|
975
|
-
const
|
|
976
|
-
buffer =
|
|
977
|
-
for (const
|
|
978
|
-
|
|
1020
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
1021
|
+
buffer = blocks.pop() ?? "";
|
|
1022
|
+
for (const block of blocks) {
|
|
1023
|
+
processResponsesSseBlock(block, state, enqueue);
|
|
979
1024
|
}
|
|
980
1025
|
}
|
|
981
|
-
|
|
982
|
-
|
|
1026
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
1027
|
+
if (tail.trim()) {
|
|
1028
|
+
processResponsesSseBlock(tail, state, enqueue);
|
|
983
1029
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1030
|
+
finishAnthropicStream(state, enqueue);
|
|
1031
|
+
controller.close();
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
await reader.cancel(error).catch(() => {
|
|
1034
|
+
});
|
|
1035
|
+
controller.error(error);
|
|
1036
|
+
} finally {
|
|
1037
|
+
reader.releaseLock();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
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({
|
|
992
1079
|
text,
|
|
993
|
-
type: "
|
|
994
|
-
});
|
|
995
|
-
enqueue("response.content_part.done", {
|
|
996
|
-
content_index: 0,
|
|
997
|
-
item_id: messageId,
|
|
998
|
-
output_index: messageOutputIndex,
|
|
999
|
-
part: {
|
|
1000
|
-
annotations: [],
|
|
1001
|
-
text,
|
|
1002
|
-
type: "output_text"
|
|
1003
|
-
},
|
|
1004
|
-
type: "response.content_part.done"
|
|
1005
|
-
});
|
|
1006
|
-
enqueue("response.output_item.done", {
|
|
1007
|
-
item,
|
|
1008
|
-
output_index: messageOutputIndex,
|
|
1009
|
-
type: "response.output_item.done"
|
|
1080
|
+
type: role === "assistant" ? "output_text" : "input_text"
|
|
1010
1081
|
});
|
|
1011
1082
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
arguments: tool.arguments,
|
|
1020
|
-
item_id: item.id,
|
|
1021
|
-
output_index: outputIndex,
|
|
1022
|
-
type: "response.function_call_arguments.done"
|
|
1023
|
-
});
|
|
1024
|
-
enqueue("response.output_item.done", {
|
|
1025
|
-
item,
|
|
1026
|
-
output_index: outputIndex,
|
|
1027
|
-
type: "response.output_item.done"
|
|
1028
|
-
});
|
|
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
|
+
);
|
|
1029
1090
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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"
|
|
1034
1101
|
});
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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();
|
|
1121
|
+
}
|
|
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) {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
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) {
|
|
1265
|
+
const record = asRecord(item);
|
|
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"
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
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"
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
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
|
+
});
|
|
1391
|
+
}
|
|
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"
|
|
1039
1406
|
});
|
|
1040
|
-
controller.error(error);
|
|
1041
|
-
} finally {
|
|
1042
|
-
reader.releaseLock();
|
|
1043
1407
|
}
|
|
1408
|
+
stopBlock(blockState, enqueue);
|
|
1044
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;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function startAnthropicMessage(state, enqueue) {
|
|
1432
|
+
if (state.started) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
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"
|
|
1045
1448
|
});
|
|
1046
1449
|
}
|
|
1047
|
-
function
|
|
1048
|
-
if (
|
|
1049
|
-
return
|
|
1450
|
+
function finishAnthropicStream(state, enqueue) {
|
|
1451
|
+
if (state.completed) {
|
|
1452
|
+
return;
|
|
1050
1453
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1454
|
+
startAnthropicMessage(state, enqueue);
|
|
1455
|
+
for (const block of [...state.blocks.values()].sort((left, right) => left.index - right.index)) {
|
|
1456
|
+
stopBlock(block, enqueue);
|
|
1053
1457
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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());
|
|
1084
1524
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1525
|
+
}
|
|
1526
|
+
return { data: data.join("\n"), event };
|
|
1527
|
+
}
|
|
1528
|
+
function parseJsonObject(text) {
|
|
1529
|
+
try {
|
|
1530
|
+
return asRecord(JSON.parse(text));
|
|
1531
|
+
} catch {
|
|
1532
|
+
return void 0;
|
|
1533
|
+
}
|
|
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;
|
|
1542
|
+
}
|
|
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;
|
|
1556
|
+
}
|
|
1557
|
+
function textValue(value) {
|
|
1558
|
+
if (typeof value === "string") {
|
|
1559
|
+
return value;
|
|
1560
|
+
}
|
|
1561
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1562
|
+
return String(value);
|
|
1563
|
+
}
|
|
1564
|
+
return "";
|
|
1565
|
+
}
|
|
1566
|
+
function firstNumber2(...values) {
|
|
1567
|
+
for (const value of values) {
|
|
1568
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1569
|
+
return value;
|
|
1089
1570
|
}
|
|
1090
1571
|
}
|
|
1091
|
-
return
|
|
1572
|
+
return void 0;
|
|
1092
1573
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1095
|
-
|
|
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";
|
|
1096
1597
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
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."
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
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") {
|
|
1099
1627
|
return void 0;
|
|
1100
1628
|
}
|
|
1101
|
-
|
|
1629
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
1102
1630
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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.`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
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
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
async getAccess() {
|
|
1700
|
+
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
1701
|
+
return this.#cachedAccess;
|
|
1110
1702
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
unsupportedResponsesFeature("input_image parts without image_url");
|
|
1118
|
-
}
|
|
1119
|
-
const image = { url: imageUrl };
|
|
1120
|
-
const detail = contentToText(record.detail);
|
|
1121
|
-
if (detail) {
|
|
1122
|
-
image.detail = detail;
|
|
1703
|
+
let stored;
|
|
1704
|
+
try {
|
|
1705
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
1708
|
+
throw new CopilotAuthError(error.message);
|
|
1123
1709
|
}
|
|
1124
|
-
|
|
1125
|
-
continue;
|
|
1126
|
-
}
|
|
1127
|
-
if (type === "input_file") {
|
|
1128
|
-
unsupportedResponsesFeature("input_file parts");
|
|
1710
|
+
throw error;
|
|
1129
1711
|
}
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
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
|
+
});
|
|
1132
1721
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
return void 0;
|
|
1722
|
+
throw new CopilotAuthError(
|
|
1723
|
+
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
1724
|
+
);
|
|
1137
1725
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1726
|
+
#cacheAccess(access) {
|
|
1727
|
+
this.#cachedAccess = access;
|
|
1728
|
+
return access;
|
|
1140
1729
|
}
|
|
1141
|
-
|
|
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;
|
|
1142
1747
|
}
|
|
1143
|
-
function
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
"Hoopilot legacy completions compatibility supports exactly one string prompt per request."
|
|
1152
|
-
);
|
|
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;
|
|
1153
1756
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (typeof request.logprobs === "number" && request.logprobs > 0) {
|
|
1166
|
-
throw new OpenAICompatibilityError(
|
|
1167
|
-
"Hoopilot legacy completions compatibility does not support legacy logprobs."
|
|
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
|
|
1168
1768
|
);
|
|
1169
1769
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
+
);
|
|
1784
|
+
}
|
|
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
|
+
});
|
|
1174
1792
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
+
});
|
|
1179
1802
|
}
|
|
1180
|
-
|
|
1181
|
-
return
|
|
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
|
+
});
|
|
1182
1812
|
}
|
|
1183
|
-
|
|
1184
|
-
return
|
|
1813
|
+
async models(signal) {
|
|
1814
|
+
return this.fetchCopilot("/models", {
|
|
1815
|
+
headers: {
|
|
1816
|
+
accept: "application/json"
|
|
1817
|
+
},
|
|
1818
|
+
method: "GET",
|
|
1819
|
+
signal
|
|
1820
|
+
});
|
|
1185
1821
|
}
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
+
);
|
|
1193
1832
|
}
|
|
1194
|
-
|
|
1833
|
+
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
1834
|
+
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
1835
|
+
...init,
|
|
1836
|
+
headers
|
|
1837
|
+
});
|
|
1195
1838
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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));
|
|
1201
1846
|
}
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
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
|
+
}
|
|
1204
1860
|
}
|
|
1205
|
-
|
|
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
|
|
1867
|
+
});
|
|
1206
1868
|
}
|
|
1207
|
-
function
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
};
|
|
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)
|
|
1226
1887
|
});
|
|
1227
|
-
return converted.length > 0 ? converted : void 0;
|
|
1228
1888
|
}
|
|
1229
|
-
function
|
|
1230
|
-
if (
|
|
1231
|
-
return
|
|
1232
|
-
}
|
|
1233
|
-
const record = asRecord(toolChoice);
|
|
1234
|
-
const type = contentToText(record.type);
|
|
1235
|
-
if (type === "function" && typeof record.name === "string") {
|
|
1236
|
-
return { function: { name: record.name }, type: "function" };
|
|
1889
|
+
function usedFrom(entitlement, remaining, overageCount) {
|
|
1890
|
+
if (entitlement === void 0 || remaining === void 0) {
|
|
1891
|
+
return void 0;
|
|
1237
1892
|
}
|
|
1238
|
-
|
|
1893
|
+
const base = entitlement - remaining;
|
|
1894
|
+
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
1895
|
+
return Math.max(0, base + overage);
|
|
1239
1896
|
}
|
|
1240
|
-
function
|
|
1241
|
-
|
|
1242
|
-
`Hoopilot Responses-to-chat compatibility does not support ${feature}.`
|
|
1243
|
-
);
|
|
1897
|
+
function numberOrUndefined(value) {
|
|
1898
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1244
1899
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1247
|
-
const text = contentToText(message.content);
|
|
1248
|
-
if (text) {
|
|
1249
|
-
output.push(messageOutputItem(text));
|
|
1250
|
-
}
|
|
1251
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
1252
|
-
for (const toolCall of toolCalls) {
|
|
1253
|
-
const record = asRecord(toolCall);
|
|
1254
|
-
const fn = asRecord(record.function);
|
|
1255
|
-
output.push(
|
|
1256
|
-
functionCallItem({
|
|
1257
|
-
arguments: contentToText(fn.arguments),
|
|
1258
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
1259
|
-
index: output.length,
|
|
1260
|
-
name: contentToText(fn.name)
|
|
1261
|
-
})
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
1264
|
-
return output;
|
|
1900
|
+
function stringOrUndefined(value) {
|
|
1901
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1265
1902
|
}
|
|
1266
|
-
function
|
|
1267
|
-
return
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
annotations: [],
|
|
1271
|
-
text,
|
|
1272
|
-
type: "output_text"
|
|
1273
|
-
}
|
|
1274
|
-
],
|
|
1275
|
-
id,
|
|
1276
|
-
role: "assistant",
|
|
1277
|
-
status: "completed",
|
|
1278
|
-
type: "message"
|
|
1279
|
-
};
|
|
1903
|
+
function removeUndefinedQuota(quota) {
|
|
1904
|
+
return Object.fromEntries(
|
|
1905
|
+
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
1906
|
+
);
|
|
1280
1907
|
}
|
|
1281
|
-
function
|
|
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.");
|
|
1934
|
+
}
|
|
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);
|
|
1282
1938
|
return {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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)
|
|
1944
|
+
})
|
|
1289
1945
|
};
|
|
1290
1946
|
}
|
|
1291
|
-
function
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
function responseUsage(usage) {
|
|
1298
|
-
const record = asRecord(usage);
|
|
1299
|
-
if (Object.keys(record).length === 0) {
|
|
1300
|
-
return null;
|
|
1301
|
-
}
|
|
1302
|
-
const inputTokens = record.prompt_tokens;
|
|
1303
|
-
const outputTokens = record.completion_tokens;
|
|
1304
|
-
return removeUndefined({
|
|
1305
|
-
input_tokens: inputTokens,
|
|
1306
|
-
input_tokens_details: responseUsageDetails(record.prompt_tokens_details, inputTokens, {
|
|
1307
|
-
cached_tokens: 0
|
|
1308
|
-
}),
|
|
1309
|
-
output_tokens: outputTokens,
|
|
1310
|
-
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
1311
|
-
reasoning_tokens: 0
|
|
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"
|
|
1312
1952
|
}),
|
|
1313
|
-
|
|
1953
|
+
headers: oauthHeaders(),
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1314
1956
|
});
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
1322
|
-
}
|
|
1323
|
-
function extractTokenUsage(usage) {
|
|
1324
|
-
const record = asRecord(usage);
|
|
1325
|
-
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
1326
|
-
const completion = firstNumber(record.completion_tokens, record.output_tokens);
|
|
1327
|
-
const total = firstNumber(record.total_tokens);
|
|
1328
|
-
if (prompt === void 0 && completion === void 0 && total === void 0) {
|
|
1329
|
-
return void 0;
|
|
1957
|
+
if (!response.ok) {
|
|
1958
|
+
throw new Error(
|
|
1959
|
+
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
1960
|
+
response
|
|
1961
|
+
)}`
|
|
1962
|
+
);
|
|
1330
1963
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
asRecord(record.completion_tokens_details).reasoning_tokens,
|
|
1335
|
-
asRecord(record.output_tokens_details).reasoning_tokens
|
|
1336
|
-
);
|
|
1337
|
-
const cached = firstNumber(
|
|
1338
|
-
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
1339
|
-
asRecord(record.input_tokens_details).cached_tokens
|
|
1964
|
+
return parseJsonResponse(
|
|
1965
|
+
response,
|
|
1966
|
+
"GitHub device authorization response was not valid JSON"
|
|
1340
1967
|
);
|
|
1341
|
-
return removeUndefined({
|
|
1342
|
-
cachedTokens: cached,
|
|
1343
|
-
completionTokens,
|
|
1344
|
-
promptTokens,
|
|
1345
|
-
reasoningTokens: reasoning,
|
|
1346
|
-
totalTokens: total ?? promptTokens + completionTokens
|
|
1347
|
-
});
|
|
1348
1968
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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)
|
|
1983
|
+
});
|
|
1984
|
+
if (!response.ok) {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
1987
|
+
response
|
|
1988
|
+
)}`
|
|
1989
|
+
);
|
|
1353
1990
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
function completionChoices(completion) {
|
|
1361
|
-
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
1362
|
-
return choices.map((choice) => asRecord(choice));
|
|
1363
|
-
}
|
|
1364
|
-
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
1365
|
-
let event = "message";
|
|
1366
|
-
const dataLines = [];
|
|
1367
|
-
for (const line of block.split(/\r?\n/)) {
|
|
1368
|
-
const trimmed = line.trim();
|
|
1369
|
-
if (trimmed.startsWith("event:")) {
|
|
1370
|
-
event = trimmed.slice("event:".length).trim() || event;
|
|
1371
|
-
} else if (trimmed.startsWith("data:")) {
|
|
1372
|
-
dataLines.push(trimmed.slice("data:".length).trim());
|
|
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;
|
|
1373
1997
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (error) {
|
|
1390
|
-
markTerminal();
|
|
1391
|
-
enqueue({ error });
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
const choices = completionChoices(parsed).map((choice, index) => {
|
|
1395
|
-
const delta = asRecord(choice.delta);
|
|
1396
|
-
const text = contentToText(delta.content);
|
|
1397
|
-
const finishReason = choice.finish_reason ?? null;
|
|
1398
|
-
if (!text && finishReason === null) {
|
|
1399
|
-
return void 0;
|
|
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}`);
|
|
1400
2013
|
}
|
|
1401
|
-
return {
|
|
1402
|
-
finish_reason: finishReason,
|
|
1403
|
-
index: typeof choice.index === "number" ? choice.index : index,
|
|
1404
|
-
logprobs: choice.logprobs ?? null,
|
|
1405
|
-
text
|
|
1406
|
-
};
|
|
1407
|
-
}).filter((choice) => choice !== void 0);
|
|
1408
|
-
const usage = asRecord(parsed.usage);
|
|
1409
|
-
const hasUsage = Object.keys(usage).length > 0;
|
|
1410
|
-
if (choices.length === 0 && !hasUsage) {
|
|
1411
|
-
return;
|
|
1412
2014
|
}
|
|
1413
|
-
|
|
1414
|
-
removeUndefined({
|
|
1415
|
-
choices,
|
|
1416
|
-
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
1417
|
-
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
1418
|
-
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
1419
|
-
object: "text_completion",
|
|
1420
|
-
usage: hasUsage ? usage : void 0
|
|
1421
|
-
})
|
|
1422
|
-
);
|
|
2015
|
+
throw new Error("GitHub device login timed out. Run `hoopilot login` again.");
|
|
1423
2016
|
}
|
|
1424
|
-
function
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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}.`);
|
|
1430
2032
|
}
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
code: contentToText(parsed.code) || void 0,
|
|
1434
|
-
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
1435
|
-
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
1436
|
-
});
|
|
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.`);
|
|
1437
2035
|
}
|
|
1438
|
-
return
|
|
2036
|
+
return url.host;
|
|
1439
2037
|
}
|
|
1440
|
-
function
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2038
|
+
function positiveSeconds(value, fallback) {
|
|
2039
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
2040
|
+
}
|
|
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)}`);
|
|
1448
2047
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
2048
|
+
}
|
|
2049
|
+
|
|
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: () => {
|
|
1452
2092
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
+
);
|
|
1458
2121
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
handlers.appendToolCall(asRecord(toolCall));
|
|
2122
|
+
if (options.stream) {
|
|
2123
|
+
return (0, import_pino.default)(pinoOptions, options.stream);
|
|
1462
2124
|
}
|
|
2125
|
+
return (0, import_pino.default)(pinoOptions);
|
|
1463
2126
|
}
|
|
1464
|
-
function
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
error: null,
|
|
1468
|
-
id,
|
|
1469
|
-
incomplete_details: null,
|
|
1470
|
-
instructions: null,
|
|
1471
|
-
max_output_tokens: null,
|
|
1472
|
-
metadata: {},
|
|
1473
|
-
model,
|
|
1474
|
-
object: "response",
|
|
1475
|
-
output,
|
|
1476
|
-
parallel_tool_calls: true,
|
|
1477
|
-
status,
|
|
1478
|
-
temperature: null,
|
|
1479
|
-
tool_choice: "auto",
|
|
1480
|
-
tools: [],
|
|
1481
|
-
top_p: null
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
function encodeSse(event, data) {
|
|
1485
|
-
if (data === "[DONE]") {
|
|
1486
|
-
return "data: [DONE]\n\n";
|
|
2127
|
+
function parseLogFormat(value) {
|
|
2128
|
+
if (!value) {
|
|
2129
|
+
return DEFAULT_LOG_FORMAT;
|
|
1487
2130
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
`;
|
|
1492
|
-
}
|
|
1493
|
-
function encodeDataSse(data) {
|
|
1494
|
-
if (data === "[DONE]") {
|
|
1495
|
-
return "data: [DONE]\n\n";
|
|
2131
|
+
if (isLogFormat(value)) {
|
|
2132
|
+
return value;
|
|
1496
2133
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
`;
|
|
2134
|
+
throw new Error(`Invalid log format: ${value}. Expected one of: ${LOG_FORMATS.join(", ")}.`);
|
|
1500
2135
|
}
|
|
1501
|
-
function
|
|
1502
|
-
|
|
1503
|
-
return
|
|
1504
|
-
} catch {
|
|
1505
|
-
return void 0;
|
|
2136
|
+
function parseLogLevel(value) {
|
|
2137
|
+
if (!value) {
|
|
2138
|
+
return DEFAULT_LOG_LEVEL;
|
|
1506
2139
|
}
|
|
2140
|
+
if (isLogLevel(value)) {
|
|
2141
|
+
return value;
|
|
2142
|
+
}
|
|
2143
|
+
throw new Error(`Invalid log level: ${value}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
|
|
1507
2144
|
}
|
|
1508
|
-
function
|
|
1509
|
-
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
|
+
);
|
|
1510
2149
|
}
|
|
1511
|
-
function
|
|
1512
|
-
|
|
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) };
|
|
1513
2159
|
}
|
|
1514
|
-
function
|
|
1515
|
-
return
|
|
2160
|
+
function isLogFormat(value) {
|
|
2161
|
+
return LOG_FORMATS.includes(value);
|
|
2162
|
+
}
|
|
2163
|
+
function isLogLevel(value) {
|
|
2164
|
+
return LOG_LEVELS.includes(value);
|
|
1516
2165
|
}
|
|
1517
2166
|
|
|
1518
2167
|
// src/metrics.ts
|
|
@@ -1942,6 +2591,7 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
1942
2591
|
var DEFAULT_PORT = 4141;
|
|
1943
2592
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1944
2593
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
2594
|
+
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
1945
2595
|
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1946
2596
|
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1947
2597
|
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
@@ -2011,6 +2661,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
2011
2661
|
if (request.method === "GET" && apiPath === "/v1/models") {
|
|
2012
2662
|
return finish(await handleModels(client, metrics, request.signal, requestLogger));
|
|
2013
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
|
+
}
|
|
2014
2672
|
if (request.method === "POST" && apiPath === "/v1/chat/completions") {
|
|
2015
2673
|
return finish(
|
|
2016
2674
|
await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
|
|
@@ -2034,16 +2692,16 @@ function createHoopilotHandler(options = {}) {
|
|
|
2034
2692
|
return finish(jsonError(401, "copilot_auth_error", error.message));
|
|
2035
2693
|
}
|
|
2036
2694
|
const message = errorMessage(error);
|
|
2037
|
-
if (message === INVALID_JSON_MESSAGE) {
|
|
2695
|
+
if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
|
|
2038
2696
|
requestLogger.warn(
|
|
2039
2697
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
2040
|
-
"request body was
|
|
2698
|
+
"request body was not usable json"
|
|
2041
2699
|
);
|
|
2042
2700
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
2043
|
-
} else if (error instanceof OpenAICompatibilityError) {
|
|
2701
|
+
} else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
2044
2702
|
requestLogger.warn(
|
|
2045
2703
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
2046
|
-
"request body used unsupported
|
|
2704
|
+
"request body used unsupported compatibility fields"
|
|
2047
2705
|
);
|
|
2048
2706
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
2049
2707
|
} else if (error instanceof RequestBodyTooLargeError) {
|
|
@@ -2087,6 +2745,40 @@ function startHoopilotServer(options = {}) {
|
|
|
2087
2745
|
url: `http://${urlHost(host)}:${server.port}`
|
|
2088
2746
|
};
|
|
2089
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
|
+
}
|
|
2090
2782
|
async function handleModels(client, metrics, signal, logger) {
|
|
2091
2783
|
const upstream = await client.models(signal);
|
|
2092
2784
|
metrics.recordUpstream("/models", upstream.ok);
|
|
@@ -2194,20 +2886,24 @@ function proxyResponse(upstream) {
|
|
|
2194
2886
|
}
|
|
2195
2887
|
async function readJson(request) {
|
|
2196
2888
|
const text = await readRequestText(request);
|
|
2889
|
+
return parseJsonObject2(text);
|
|
2890
|
+
}
|
|
2891
|
+
function parseJsonObject2(text) {
|
|
2892
|
+
let parsed;
|
|
2197
2893
|
try {
|
|
2198
|
-
|
|
2894
|
+
parsed = JSON.parse(text);
|
|
2199
2895
|
} catch {
|
|
2200
2896
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
2201
2897
|
}
|
|
2898
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2899
|
+
throw new Error(JSON_OBJECT_MESSAGE);
|
|
2900
|
+
}
|
|
2901
|
+
return parsed;
|
|
2202
2902
|
}
|
|
2203
2903
|
async function readJsonText(request) {
|
|
2204
2904
|
const text = await readRequestText(request);
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
return text;
|
|
2208
|
-
} catch {
|
|
2209
|
-
throw new Error(INVALID_JSON_MESSAGE);
|
|
2210
|
-
}
|
|
2905
|
+
parseJsonObject2(text);
|
|
2906
|
+
return text;
|
|
2211
2907
|
}
|
|
2212
2908
|
async function readRequestText(request) {
|
|
2213
2909
|
const contentLength = request.headers.get("content-length");
|
|
@@ -2282,9 +2978,10 @@ function websocketUnsupportedResponse() {
|
|
|
2282
2978
|
}
|
|
2283
2979
|
function corsHeaders() {
|
|
2284
2980
|
return {
|
|
2285
|
-
"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",
|
|
2286
2982
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
2287
|
-
"access-control-allow-origin": "*"
|
|
2983
|
+
"access-control-allow-origin": "*",
|
|
2984
|
+
"access-control-expose-headers": "x-request-id"
|
|
2288
2985
|
};
|
|
2289
2986
|
}
|
|
2290
2987
|
function isAuthorized(request, apiKey) {
|
|
@@ -2436,6 +3133,10 @@ function canonicalApiPath(path) {
|
|
|
2436
3133
|
return "/v1/chat/completions";
|
|
2437
3134
|
case "/completions":
|
|
2438
3135
|
return "/v1/completions";
|
|
3136
|
+
case "/messages":
|
|
3137
|
+
return "/v1/messages";
|
|
3138
|
+
case "/messages/count_tokens":
|
|
3139
|
+
return "/v1/messages/count_tokens";
|
|
2439
3140
|
case "/responses":
|
|
2440
3141
|
return "/v1/responses";
|
|
2441
3142
|
case "/usage":
|
|
@@ -2460,6 +3161,12 @@ function routeFor(method, path) {
|
|
|
2460
3161
|
if (method === "GET" && path === "/v1/models") {
|
|
2461
3162
|
return "models";
|
|
2462
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
|
+
}
|
|
2463
3170
|
if (method === "POST" && path === "/v1/chat/completions") {
|
|
2464
3171
|
return "chat_completions";
|
|
2465
3172
|
}
|
|
@@ -2540,6 +3247,7 @@ function safeParseJson(text) {
|
|
|
2540
3247
|
}
|
|
2541
3248
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2542
3249
|
0 && (module.exports = {
|
|
3250
|
+
AnthropicCompatibilityError,
|
|
2543
3251
|
COPILOT_USAGE_API_VERSION,
|
|
2544
3252
|
CopilotAuth,
|
|
2545
3253
|
CopilotAuthError,
|
|
@@ -2550,6 +3258,7 @@ function safeParseJson(text) {
|
|
|
2550
3258
|
DEFAULT_MODEL,
|
|
2551
3259
|
MetricsRegistry,
|
|
2552
3260
|
PROMETHEUS_CONTENT_TYPE,
|
|
3261
|
+
anthropicMessagesToResponsesRequest,
|
|
2553
3262
|
applyCopilotHeaders,
|
|
2554
3263
|
applyGithubApiHeaders,
|
|
2555
3264
|
authStorePath,
|
|
@@ -2559,6 +3268,7 @@ function safeParseJson(text) {
|
|
|
2559
3268
|
completionsRequestToChatCompletion,
|
|
2560
3269
|
createHoopilotHandler,
|
|
2561
3270
|
createHoopilotLogger,
|
|
3271
|
+
estimateAnthropicMessageTokens,
|
|
2562
3272
|
extractTokenUsage,
|
|
2563
3273
|
fallbackModels,
|
|
2564
3274
|
githubCopilotDeviceLogin,
|
|
@@ -2572,7 +3282,9 @@ function safeParseJson(text) {
|
|
|
2572
3282
|
parseLogLevel,
|
|
2573
3283
|
readStoredCopilotAuth,
|
|
2574
3284
|
responsesRequestToChatCompletion,
|
|
3285
|
+
responsesResponseToAnthropicMessage,
|
|
2575
3286
|
responsesStreamFromChatStream,
|
|
3287
|
+
responsesStreamToAnthropicStream,
|
|
2576
3288
|
startHoopilotServer,
|
|
2577
3289
|
writeStoredCopilotAuth
|
|
2578
3290
|
});
|