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