@openhoo/hoopilot 0.7.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/cli.js +734 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1967 -1255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1962 -1255
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.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,642 +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 overageCount = numberOrUndefined(detail.overage_count);
|
|
333
|
-
const remaining = numberOrUndefined(detail.remaining) ?? numberOrUndefined(detail.quota_remaining);
|
|
334
|
-
return removeUndefinedQuota({
|
|
335
|
-
entitlement,
|
|
336
|
-
hasQuota: typeof detail.has_quota === "boolean" ? detail.has_quota : void 0,
|
|
337
|
-
overageCount,
|
|
338
|
-
overageEntitlement: numberOrUndefined(detail.overage_entitlement),
|
|
339
|
-
overagePermitted: typeof detail.overage_permitted === "boolean" ? detail.overage_permitted : void 0,
|
|
340
|
-
percentRemaining: numberOrUndefined(detail.percent_remaining),
|
|
341
|
-
quotaId: stringOrUndefined(detail.quota_id),
|
|
342
|
-
quotaResetAt: stringOrUndefined(detail.quota_reset_at),
|
|
343
|
-
remaining,
|
|
344
|
-
timestampUtc: stringOrUndefined(detail.timestamp_utc),
|
|
345
|
-
tokenBasedBilling: typeof detail.token_based_billing === "boolean" ? detail.token_based_billing : void 0,
|
|
346
|
-
unlimited: typeof detail.unlimited === "boolean" ? detail.unlimited : void 0,
|
|
347
|
-
used: usedFrom(entitlement, remaining, overageCount)
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
function usedFrom(entitlement, remaining, overageCount) {
|
|
351
|
-
if (entitlement === void 0 || remaining === void 0) {
|
|
352
|
-
return void 0;
|
|
353
|
-
}
|
|
354
|
-
const base = entitlement - remaining;
|
|
355
|
-
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
356
|
-
return Math.max(0, base + overage);
|
|
357
|
-
}
|
|
358
|
-
function numberOrUndefined(value) {
|
|
359
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
360
|
-
}
|
|
361
|
-
function stringOrUndefined(value) {
|
|
362
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
363
|
-
}
|
|
364
|
-
function removeUndefinedQuota(quota) {
|
|
365
|
-
return Object.fromEntries(
|
|
366
|
-
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
function removeUndefinedUsage(usage) {
|
|
370
|
-
const entries = Object.entries(usage).filter(([, value]) => value !== void 0);
|
|
371
|
-
return Object.fromEntries(entries);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// src/github-device.ts
|
|
375
|
-
import { setTimeout as sleep } from "timers/promises";
|
|
376
|
-
var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
377
|
-
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
378
|
-
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
379
|
-
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
380
|
-
var REQUEST_TIMEOUT_MS = 15e3;
|
|
381
|
-
async function githubCopilotDeviceLogin(options = {}) {
|
|
382
|
-
const env = options.env ?? process.env;
|
|
383
|
-
const fetcher = options.fetch ?? fetch;
|
|
384
|
-
const sleeper = options.sleep ?? sleep;
|
|
385
|
-
const domain = normalizeDomain(
|
|
386
|
-
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
387
|
-
);
|
|
388
|
-
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
389
|
-
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
390
|
-
const verificationUrl = device.verification_uri;
|
|
391
|
-
const userCode = device.user_code;
|
|
392
|
-
const deviceCode = device.device_code;
|
|
393
|
-
if (!verificationUrl || !userCode || !deviceCode) {
|
|
394
|
-
throw new Error("GitHub device authorization response is missing required fields.");
|
|
395
|
-
}
|
|
396
|
-
options.logger?.info(`First copy your one-time code: ${userCode}`);
|
|
397
|
-
options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
|
|
398
|
-
await options.openBrowser?.(verificationUrl);
|
|
399
|
-
return {
|
|
400
|
-
domain,
|
|
401
|
-
token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
|
|
402
|
-
deviceCode,
|
|
403
|
-
expiresIn: positiveSeconds(device.expires_in, 900),
|
|
404
|
-
interval: positiveSeconds(device.interval, 5)
|
|
405
|
-
})
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
async function requestDeviceCode(fetcher, domain, clientId) {
|
|
409
|
-
const response = await fetcher(`https://${domain}/login/device/code`, {
|
|
410
|
-
body: JSON.stringify({
|
|
411
|
-
client_id: clientId,
|
|
412
|
-
scope: "read:user"
|
|
413
|
-
}),
|
|
414
|
-
headers: oauthHeaders(),
|
|
415
|
-
method: "POST",
|
|
416
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
417
|
-
});
|
|
418
|
-
if (!response.ok) {
|
|
419
|
-
throw new Error(
|
|
420
|
-
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
421
|
-
response
|
|
422
|
-
)}`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
return parseJsonResponse(
|
|
426
|
-
response,
|
|
427
|
-
"GitHub device authorization response was not valid JSON"
|
|
428
|
-
);
|
|
453
|
+
return messages;
|
|
429
454
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
client_id: clientId,
|
|
438
|
-
device_code: device.deviceCode,
|
|
439
|
-
grant_type: DEVICE_GRANT_TYPE
|
|
440
|
-
}),
|
|
441
|
-
headers: oauthHeaders(),
|
|
442
|
-
method: "POST",
|
|
443
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
444
|
-
});
|
|
445
|
-
if (!response.ok) {
|
|
446
|
-
throw new Error(
|
|
447
|
-
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
448
|
-
response
|
|
449
|
-
)}`
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
const data = await parseJsonResponse(
|
|
453
|
-
response,
|
|
454
|
-
"GitHub device token response was not valid JSON"
|
|
455
|
-
);
|
|
456
|
-
if (data.access_token) {
|
|
457
|
-
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;
|
|
458
462
|
}
|
|
459
|
-
|
|
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" });
|
|
460
471
|
continue;
|
|
461
472
|
}
|
|
462
|
-
if (
|
|
463
|
-
|
|
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" });
|
|
464
487
|
continue;
|
|
465
488
|
}
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
if (data.error === "access_denied") {
|
|
470
|
-
throw new Error("GitHub device login was cancelled.");
|
|
489
|
+
if (type === "input_file") {
|
|
490
|
+
unsupportedResponsesFeature("input_file parts");
|
|
471
491
|
}
|
|
472
|
-
if (
|
|
473
|
-
|
|
492
|
+
if (type === "input_audio") {
|
|
493
|
+
unsupportedResponsesFeature("input_audio parts");
|
|
474
494
|
}
|
|
495
|
+
unsupportedResponsesFeature(`content part type "${type || "unknown"}"`);
|
|
475
496
|
}
|
|
476
|
-
|
|
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;
|
|
477
504
|
}
|
|
478
|
-
function
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
+
);
|
|
484
515
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
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
|
+
}
|
|
487
537
|
}
|
|
488
|
-
function
|
|
489
|
-
|
|
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 "";
|
|
490
559
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return JSON.parse(text);
|
|
495
|
-
} catch {
|
|
496
|
-
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
560
|
+
function responsesRoleToChatRole(role) {
|
|
561
|
+
if (!role) {
|
|
562
|
+
return "user";
|
|
497
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}"`);
|
|
498
568
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
import pretty from "pino-pretty";
|
|
503
|
-
var DEFAULT_LOG_FORMAT = "pretty";
|
|
504
|
-
var DEFAULT_LOG_LEVEL = "info";
|
|
505
|
-
var LOG_FORMATS = ["json", "pretty"];
|
|
506
|
-
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
507
|
-
var REDACT_PATHS = [
|
|
508
|
-
"apiKey",
|
|
509
|
-
"authorization",
|
|
510
|
-
"cookie",
|
|
511
|
-
"headers.authorization",
|
|
512
|
-
"headers.Authorization",
|
|
513
|
-
"headers.cookie",
|
|
514
|
-
"headers.Cookie",
|
|
515
|
-
"headers.x-api-key",
|
|
516
|
-
"headers.X-Api-Key",
|
|
517
|
-
"token",
|
|
518
|
-
"*.apiKey",
|
|
519
|
-
"*.authorization",
|
|
520
|
-
"*.cookie",
|
|
521
|
-
"*.token",
|
|
522
|
-
"*.headers.authorization",
|
|
523
|
-
"*.headers.Authorization",
|
|
524
|
-
"*.headers.cookie",
|
|
525
|
-
"*.headers.Cookie",
|
|
526
|
-
"*.headers.x-api-key",
|
|
527
|
-
"*.headers.X-Api-Key"
|
|
528
|
-
];
|
|
529
|
-
var noopLogger = {
|
|
530
|
-
child: () => noopLogger,
|
|
531
|
-
debug: () => {
|
|
532
|
-
},
|
|
533
|
-
error: () => {
|
|
534
|
-
},
|
|
535
|
-
fatal: () => {
|
|
536
|
-
},
|
|
537
|
-
info: () => {
|
|
538
|
-
},
|
|
539
|
-
trace: () => {
|
|
540
|
-
},
|
|
541
|
-
warn: () => {
|
|
569
|
+
function chatTools(tools) {
|
|
570
|
+
if (!Array.isArray(tools)) {
|
|
571
|
+
return void 0;
|
|
542
572
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
colorize: options.colorize ?? process.stderr.isTTY,
|
|
565
|
-
destination: options.stream ?? 1,
|
|
566
|
-
ignore: "pid,hostname",
|
|
567
|
-
singleLine: true,
|
|
568
|
-
translateTime: "SYS:standard"
|
|
569
|
-
})
|
|
570
|
-
);
|
|
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;
|
|
571
594
|
}
|
|
572
|
-
|
|
573
|
-
|
|
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" };
|
|
574
599
|
}
|
|
575
|
-
|
|
600
|
+
unsupportedResponsesFeature(`tool_choice type "${type || "unknown"}"`);
|
|
576
601
|
}
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
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));
|
|
580
612
|
}
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
);
|
|
583
625
|
}
|
|
584
|
-
|
|
626
|
+
return output;
|
|
585
627
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
|
|
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;
|
|
589
663
|
}
|
|
590
|
-
|
|
591
|
-
|
|
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;
|
|
592
682
|
}
|
|
593
|
-
|
|
683
|
+
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
594
684
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
598
702
|
);
|
|
703
|
+
return removeUndefined({
|
|
704
|
+
cachedTokens: cached,
|
|
705
|
+
completionTokens,
|
|
706
|
+
promptTokens,
|
|
707
|
+
reasoningTokens: reasoning,
|
|
708
|
+
totalTokens: total ?? promptTokens + completionTokens
|
|
709
|
+
});
|
|
599
710
|
}
|
|
600
|
-
function
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
stack: error.stack
|
|
606
|
-
};
|
|
711
|
+
function firstNumber(...values) {
|
|
712
|
+
for (const value of values) {
|
|
713
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
607
716
|
}
|
|
608
|
-
return
|
|
717
|
+
return void 0;
|
|
609
718
|
}
|
|
610
|
-
function
|
|
611
|
-
return
|
|
719
|
+
function firstChoice(completion) {
|
|
720
|
+
return completionChoices(completion)[0] ?? {};
|
|
612
721
|
}
|
|
613
|
-
function
|
|
614
|
-
|
|
722
|
+
function completionChoices(completion) {
|
|
723
|
+
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
724
|
+
return choices.map((choice) => asRecord(choice));
|
|
615
725
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
}
|
|
623
736
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const instructions = contentToText(request.instructions);
|
|
628
|
-
if (instructions) {
|
|
629
|
-
messages.push({ content: instructions, role: "system" });
|
|
737
|
+
const data = dataLines.join("\n");
|
|
738
|
+
if (!data) {
|
|
739
|
+
return;
|
|
630
740
|
}
|
|
631
|
-
|
|
632
|
-
|
|
741
|
+
if (data === "[DONE]") {
|
|
742
|
+
markTerminal();
|
|
743
|
+
enqueue("[DONE]");
|
|
744
|
+
return;
|
|
633
745
|
}
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
user: request.user
|
|
674
|
-
});
|
|
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
|
+
);
|
|
675
785
|
}
|
|
676
|
-
function
|
|
677
|
-
const
|
|
678
|
-
|
|
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;
|
|
679
801
|
}
|
|
680
|
-
function
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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,
|
|
689
829
|
error: null,
|
|
690
830
|
id,
|
|
691
831
|
incomplete_details: null,
|
|
@@ -695,206 +835,106 @@ function chatCompletionToResponse(completion, responseId) {
|
|
|
695
835
|
model,
|
|
696
836
|
object: "response",
|
|
697
837
|
output,
|
|
698
|
-
output_text: outputText(output),
|
|
699
838
|
parallel_tool_calls: true,
|
|
700
|
-
status
|
|
839
|
+
status,
|
|
701
840
|
temperature: null,
|
|
702
841
|
tool_choice: "auto",
|
|
703
|
-
tools: [],
|
|
704
|
-
top_p: null
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const encoder = new TextEncoder();
|
|
798
|
-
const decoder = new TextDecoder();
|
|
799
|
-
const responseId = options.responseId ?? `resp_${randomId()}`;
|
|
800
|
-
const messageId = `msg_${randomId()}`;
|
|
801
|
-
const createdAt = epochSeconds();
|
|
802
|
-
let buffer = "";
|
|
803
|
-
let text = "";
|
|
804
|
-
let messageOutputIndex;
|
|
805
|
-
let nextOutputIndex = 0;
|
|
806
|
-
let sequenceNumber = 0;
|
|
807
|
-
const tools = /* @__PURE__ */ new Map();
|
|
808
|
-
return new ReadableStream({
|
|
809
|
-
async start(controller) {
|
|
810
|
-
const enqueue = (event, data) => {
|
|
811
|
-
controller.enqueue(
|
|
812
|
-
encoder.encode(
|
|
813
|
-
encodeSse(
|
|
814
|
-
event,
|
|
815
|
-
data === "[DONE]" ? data : { ...data, sequence_number: sequenceNumber++ }
|
|
816
|
-
)
|
|
817
|
-
)
|
|
818
|
-
);
|
|
819
|
-
};
|
|
820
|
-
enqueue("response.created", {
|
|
821
|
-
response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
|
|
822
|
-
type: "response.created"
|
|
823
|
-
});
|
|
824
|
-
const ensureMessageStarted = () => {
|
|
825
|
-
if (messageOutputIndex !== void 0) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
messageOutputIndex = nextOutputIndex++;
|
|
829
|
-
enqueue("response.output_item.added", {
|
|
830
|
-
item: {
|
|
831
|
-
content: [],
|
|
832
|
-
id: messageId,
|
|
833
|
-
role: "assistant",
|
|
834
|
-
status: "in_progress",
|
|
835
|
-
type: "message"
|
|
836
|
-
},
|
|
837
|
-
output_index: messageOutputIndex,
|
|
838
|
-
type: "response.output_item.added"
|
|
839
|
-
});
|
|
840
|
-
enqueue("response.content_part.added", {
|
|
841
|
-
content_index: 0,
|
|
842
|
-
item_id: messageId,
|
|
843
|
-
output_index: messageOutputIndex,
|
|
844
|
-
part: {
|
|
845
|
-
annotations: [],
|
|
846
|
-
text: "",
|
|
847
|
-
type: "output_text"
|
|
848
|
-
},
|
|
849
|
-
type: "response.content_part.added"
|
|
850
|
-
});
|
|
851
|
-
};
|
|
852
|
-
const appendText = (delta) => {
|
|
853
|
-
ensureMessageStarted();
|
|
854
|
-
text += delta;
|
|
855
|
-
enqueue("response.output_text.delta", {
|
|
856
|
-
content_index: 0,
|
|
857
|
-
delta,
|
|
858
|
-
item_id: messageId,
|
|
859
|
-
output_index: messageOutputIndex ?? 0,
|
|
860
|
-
type: "response.output_text.delta"
|
|
861
|
-
});
|
|
862
|
-
};
|
|
863
|
-
const appendToolCall = (toolCall) => {
|
|
864
|
-
const fn = asRecord(toolCall.function);
|
|
865
|
-
const index = typeof toolCall.index === "number" ? toolCall.index : tools.size;
|
|
866
|
-
let existing = tools.get(index);
|
|
867
|
-
const isNew = !existing;
|
|
868
|
-
existing ??= {
|
|
869
|
-
arguments: "",
|
|
870
|
-
id: contentToText(toolCall.id) || `call_${randomId()}`,
|
|
871
|
-
index,
|
|
872
|
-
itemId: `fc_${randomId()}`,
|
|
873
|
-
name: "",
|
|
874
|
-
outputIndex: nextOutputIndex++
|
|
875
|
-
};
|
|
876
|
-
existing.id = contentToText(toolCall.id) || existing.id;
|
|
877
|
-
existing.name += contentToText(fn.name);
|
|
878
|
-
tools.set(index, existing);
|
|
879
|
-
if (isNew) {
|
|
880
|
-
enqueue("response.output_item.added", {
|
|
881
|
-
item: functionCallItem(existing, "in_progress"),
|
|
882
|
-
output_index: existing.outputIndex ?? 0,
|
|
883
|
-
type: "response.output_item.added"
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
const argumentDelta = contentToText(fn.arguments);
|
|
887
|
-
if (argumentDelta) {
|
|
888
|
-
existing.arguments += argumentDelta;
|
|
889
|
-
enqueue("response.function_call_arguments.delta", {
|
|
890
|
-
delta: argumentDelta,
|
|
891
|
-
item_id: existing.itemId,
|
|
892
|
-
output_index: existing.outputIndex ?? 0,
|
|
893
|
-
type: "response.function_call_arguments.delta"
|
|
894
|
-
});
|
|
895
|
-
}
|
|
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)));
|
|
896
936
|
};
|
|
897
|
-
const reader =
|
|
937
|
+
const reader = stream.getReader();
|
|
898
938
|
try {
|
|
899
939
|
while (true) {
|
|
900
940
|
const result = await reader.read();
|
|
@@ -902,547 +942,1151 @@ function responsesStreamFromChatStream(chatStream, options) {
|
|
|
902
942
|
break;
|
|
903
943
|
}
|
|
904
944
|
buffer += decoder.decode(result.value, { stream: true });
|
|
905
|
-
const
|
|
906
|
-
buffer =
|
|
907
|
-
for (const
|
|
908
|
-
|
|
945
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
946
|
+
buffer = blocks.pop() ?? "";
|
|
947
|
+
for (const block of blocks) {
|
|
948
|
+
processResponsesSseBlock(block, state, enqueue);
|
|
909
949
|
}
|
|
910
950
|
}
|
|
911
|
-
|
|
912
|
-
|
|
951
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
952
|
+
if (tail.trim()) {
|
|
953
|
+
processResponsesSseBlock(tail, state, enqueue);
|
|
913
954
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
955
|
+
finishAnthropicStream(state, enqueue);
|
|
956
|
+
controller.close();
|
|
957
|
+
} catch (error) {
|
|
958
|
+
await reader.cancel(error).catch(() => {
|
|
959
|
+
});
|
|
960
|
+
controller.error(error);
|
|
961
|
+
} finally {
|
|
962
|
+
reader.releaseLock();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
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({
|
|
922
1004
|
text,
|
|
923
|
-
type: "
|
|
924
|
-
});
|
|
925
|
-
enqueue("response.content_part.done", {
|
|
926
|
-
content_index: 0,
|
|
927
|
-
item_id: messageId,
|
|
928
|
-
output_index: messageOutputIndex,
|
|
929
|
-
part: {
|
|
930
|
-
annotations: [],
|
|
931
|
-
text,
|
|
932
|
-
type: "output_text"
|
|
933
|
-
},
|
|
934
|
-
type: "response.content_part.done"
|
|
935
|
-
});
|
|
936
|
-
enqueue("response.output_item.done", {
|
|
937
|
-
item,
|
|
938
|
-
output_index: messageOutputIndex,
|
|
939
|
-
type: "response.output_item.done"
|
|
1005
|
+
type: role === "assistant" ? "output_text" : "input_text"
|
|
940
1006
|
});
|
|
941
1007
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
arguments: tool.arguments,
|
|
950
|
-
item_id: item.id,
|
|
951
|
-
output_index: outputIndex,
|
|
952
|
-
type: "response.function_call_arguments.done"
|
|
953
|
-
});
|
|
954
|
-
enqueue("response.output_item.done", {
|
|
955
|
-
item,
|
|
956
|
-
output_index: outputIndex,
|
|
957
|
-
type: "response.output_item.done"
|
|
958
|
-
});
|
|
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
|
+
);
|
|
959
1015
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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"
|
|
964
1026
|
});
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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();
|
|
1046
|
+
}
|
|
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) {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
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) {
|
|
1190
|
+
const record = asRecord(item);
|
|
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"
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
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"
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
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
|
+
});
|
|
1316
|
+
}
|
|
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"
|
|
969
1331
|
});
|
|
970
|
-
controller.error(error);
|
|
971
|
-
} finally {
|
|
972
|
-
reader.releaseLock();
|
|
973
1332
|
}
|
|
1333
|
+
stopBlock(blockState, enqueue);
|
|
974
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;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function startAnthropicMessage(state, enqueue) {
|
|
1357
|
+
if (state.started) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
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"
|
|
975
1373
|
});
|
|
976
1374
|
}
|
|
977
|
-
function
|
|
978
|
-
if (
|
|
979
|
-
return
|
|
1375
|
+
function finishAnthropicStream(state, enqueue) {
|
|
1376
|
+
if (state.completed) {
|
|
1377
|
+
return;
|
|
980
1378
|
}
|
|
981
|
-
|
|
982
|
-
|
|
1379
|
+
startAnthropicMessage(state, enqueue);
|
|
1380
|
+
for (const block of [...state.blocks.values()].sort((left, right) => left.index - right.index)) {
|
|
1381
|
+
stopBlock(block, enqueue);
|
|
983
1382
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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());
|
|
1014
1449
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1450
|
+
}
|
|
1451
|
+
return { data: data.join("\n"), event };
|
|
1452
|
+
}
|
|
1453
|
+
function parseJsonObject(text) {
|
|
1454
|
+
try {
|
|
1455
|
+
return asRecord(JSON.parse(text));
|
|
1456
|
+
} catch {
|
|
1457
|
+
return void 0;
|
|
1458
|
+
}
|
|
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;
|
|
1467
|
+
}
|
|
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;
|
|
1481
|
+
}
|
|
1482
|
+
function textValue(value) {
|
|
1483
|
+
if (typeof value === "string") {
|
|
1484
|
+
return value;
|
|
1485
|
+
}
|
|
1486
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1487
|
+
return String(value);
|
|
1488
|
+
}
|
|
1489
|
+
return "";
|
|
1490
|
+
}
|
|
1491
|
+
function firstNumber2(...values) {
|
|
1492
|
+
for (const value of values) {
|
|
1493
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1494
|
+
return value;
|
|
1019
1495
|
}
|
|
1020
1496
|
}
|
|
1021
|
-
return
|
|
1497
|
+
return void 0;
|
|
1022
1498
|
}
|
|
1023
|
-
function
|
|
1024
|
-
|
|
1025
|
-
|
|
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";
|
|
1026
1522
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
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."
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
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") {
|
|
1029
1552
|
return void 0;
|
|
1030
1553
|
}
|
|
1031
|
-
|
|
1554
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
1032
1555
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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.`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
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
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
async getAccess() {
|
|
1625
|
+
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
1626
|
+
return this.#cachedAccess;
|
|
1040
1627
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
unsupportedResponsesFeature("input_image parts without image_url");
|
|
1048
|
-
}
|
|
1049
|
-
const image = { url: imageUrl };
|
|
1050
|
-
const detail = contentToText(record.detail);
|
|
1051
|
-
if (detail) {
|
|
1052
|
-
image.detail = detail;
|
|
1628
|
+
let stored;
|
|
1629
|
+
try {
|
|
1630
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
1633
|
+
throw new CopilotAuthError(error.message);
|
|
1053
1634
|
}
|
|
1054
|
-
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
if (type === "input_file") {
|
|
1058
|
-
unsupportedResponsesFeature("input_file parts");
|
|
1635
|
+
throw error;
|
|
1059
1636
|
}
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
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
|
+
});
|
|
1062
1646
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
return void 0;
|
|
1647
|
+
throw new CopilotAuthError(
|
|
1648
|
+
"No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
|
|
1649
|
+
);
|
|
1067
1650
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1651
|
+
#cacheAccess(access) {
|
|
1652
|
+
this.#cachedAccess = access;
|
|
1653
|
+
return access;
|
|
1070
1654
|
}
|
|
1071
|
-
|
|
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;
|
|
1072
1672
|
}
|
|
1073
|
-
function
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
"Hoopilot legacy completions compatibility supports exactly one string prompt per request."
|
|
1082
|
-
);
|
|
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;
|
|
1083
1681
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (typeof request.logprobs === "number" && request.logprobs > 0) {
|
|
1096
|
-
throw new OpenAICompatibilityError(
|
|
1097
|
-
"Hoopilot legacy completions compatibility does not support legacy logprobs."
|
|
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
|
|
1098
1693
|
);
|
|
1099
1694
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
+
);
|
|
1709
|
+
}
|
|
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
|
+
});
|
|
1104
1717
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
+
});
|
|
1109
1727
|
}
|
|
1110
|
-
|
|
1111
|
-
return
|
|
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
|
+
});
|
|
1112
1737
|
}
|
|
1113
|
-
|
|
1114
|
-
return
|
|
1738
|
+
async models(signal) {
|
|
1739
|
+
return this.fetchCopilot("/models", {
|
|
1740
|
+
headers: {
|
|
1741
|
+
accept: "application/json"
|
|
1742
|
+
},
|
|
1743
|
+
method: "GET",
|
|
1744
|
+
signal
|
|
1745
|
+
});
|
|
1115
1746
|
}
|
|
1116
|
-
|
|
1117
|
-
const
|
|
1118
|
-
if (
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
+
);
|
|
1123
1757
|
}
|
|
1124
|
-
|
|
1758
|
+
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
1759
|
+
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
1760
|
+
...init,
|
|
1761
|
+
headers
|
|
1762
|
+
});
|
|
1125
1763
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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));
|
|
1131
1771
|
}
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
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
|
+
}
|
|
1134
1785
|
}
|
|
1135
|
-
|
|
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
|
|
1792
|
+
});
|
|
1136
1793
|
}
|
|
1137
|
-
function
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
};
|
|
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)
|
|
1156
1812
|
});
|
|
1157
|
-
return converted.length > 0 ? converted : void 0;
|
|
1158
1813
|
}
|
|
1159
|
-
function
|
|
1160
|
-
if (
|
|
1161
|
-
return
|
|
1162
|
-
}
|
|
1163
|
-
const record = asRecord(toolChoice);
|
|
1164
|
-
const type = contentToText(record.type);
|
|
1165
|
-
if (type === "function" && typeof record.name === "string") {
|
|
1166
|
-
return { function: { name: record.name }, type: "function" };
|
|
1814
|
+
function usedFrom(entitlement, remaining, overageCount) {
|
|
1815
|
+
if (entitlement === void 0 || remaining === void 0) {
|
|
1816
|
+
return void 0;
|
|
1167
1817
|
}
|
|
1168
|
-
|
|
1818
|
+
const base = entitlement - remaining;
|
|
1819
|
+
const overage = remaining === 0 ? overageCount ?? 0 : 0;
|
|
1820
|
+
return Math.max(0, base + overage);
|
|
1169
1821
|
}
|
|
1170
|
-
function
|
|
1171
|
-
|
|
1172
|
-
`Hoopilot Responses-to-chat compatibility does not support ${feature}.`
|
|
1173
|
-
);
|
|
1822
|
+
function numberOrUndefined(value) {
|
|
1823
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1174
1824
|
}
|
|
1175
|
-
function
|
|
1176
|
-
|
|
1177
|
-
const text = contentToText(message.content);
|
|
1178
|
-
if (text) {
|
|
1179
|
-
output.push(messageOutputItem(text));
|
|
1180
|
-
}
|
|
1181
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
1182
|
-
for (const toolCall of toolCalls) {
|
|
1183
|
-
const record = asRecord(toolCall);
|
|
1184
|
-
const fn = asRecord(record.function);
|
|
1185
|
-
output.push(
|
|
1186
|
-
functionCallItem({
|
|
1187
|
-
arguments: contentToText(fn.arguments),
|
|
1188
|
-
id: contentToText(record.id) || `call_${randomId()}`,
|
|
1189
|
-
index: output.length,
|
|
1190
|
-
name: contentToText(fn.name)
|
|
1191
|
-
})
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
return output;
|
|
1825
|
+
function stringOrUndefined(value) {
|
|
1826
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1195
1827
|
}
|
|
1196
|
-
function
|
|
1197
|
-
return
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
annotations: [],
|
|
1201
|
-
text,
|
|
1202
|
-
type: "output_text"
|
|
1203
|
-
}
|
|
1204
|
-
],
|
|
1205
|
-
id,
|
|
1206
|
-
role: "assistant",
|
|
1207
|
-
status: "completed",
|
|
1208
|
-
type: "message"
|
|
1209
|
-
};
|
|
1828
|
+
function removeUndefinedQuota(quota) {
|
|
1829
|
+
return Object.fromEntries(
|
|
1830
|
+
Object.entries(quota).filter(([, value]) => value !== void 0)
|
|
1831
|
+
);
|
|
1210
1832
|
}
|
|
1211
|
-
function
|
|
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.");
|
|
1859
|
+
}
|
|
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);
|
|
1212
1863
|
return {
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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)
|
|
1869
|
+
})
|
|
1219
1870
|
};
|
|
1220
1871
|
}
|
|
1221
|
-
function
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
function responseUsage(usage) {
|
|
1228
|
-
const record = asRecord(usage);
|
|
1229
|
-
if (Object.keys(record).length === 0) {
|
|
1230
|
-
return null;
|
|
1231
|
-
}
|
|
1232
|
-
const inputTokens = record.prompt_tokens;
|
|
1233
|
-
const outputTokens = record.completion_tokens;
|
|
1234
|
-
return removeUndefined({
|
|
1235
|
-
input_tokens: inputTokens,
|
|
1236
|
-
input_tokens_details: responseUsageDetails(record.prompt_tokens_details, inputTokens, {
|
|
1237
|
-
cached_tokens: 0
|
|
1238
|
-
}),
|
|
1239
|
-
output_tokens: outputTokens,
|
|
1240
|
-
output_tokens_details: responseUsageDetails(record.completion_tokens_details, outputTokens, {
|
|
1241
|
-
reasoning_tokens: 0
|
|
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"
|
|
1242
1877
|
}),
|
|
1243
|
-
|
|
1878
|
+
headers: oauthHeaders(),
|
|
1879
|
+
method: "POST",
|
|
1880
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1244
1881
|
});
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
return typeof tokenCount === "number" && Number.isFinite(tokenCount) ? fallback : void 0;
|
|
1252
|
-
}
|
|
1253
|
-
function extractTokenUsage(usage) {
|
|
1254
|
-
const record = asRecord(usage);
|
|
1255
|
-
const prompt = firstNumber(record.prompt_tokens, record.input_tokens);
|
|
1256
|
-
const completion = firstNumber(record.completion_tokens, record.output_tokens);
|
|
1257
|
-
const total = firstNumber(record.total_tokens);
|
|
1258
|
-
if (prompt === void 0 && completion === void 0 && total === void 0) {
|
|
1259
|
-
return void 0;
|
|
1882
|
+
if (!response.ok) {
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
1885
|
+
response
|
|
1886
|
+
)}`
|
|
1887
|
+
);
|
|
1260
1888
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
asRecord(record.completion_tokens_details).reasoning_tokens,
|
|
1265
|
-
asRecord(record.output_tokens_details).reasoning_tokens
|
|
1266
|
-
);
|
|
1267
|
-
const cached = firstNumber(
|
|
1268
|
-
asRecord(record.prompt_tokens_details).cached_tokens,
|
|
1269
|
-
asRecord(record.input_tokens_details).cached_tokens
|
|
1889
|
+
return parseJsonResponse(
|
|
1890
|
+
response,
|
|
1891
|
+
"GitHub device authorization response was not valid JSON"
|
|
1270
1892
|
);
|
|
1271
|
-
return removeUndefined({
|
|
1272
|
-
cachedTokens: cached,
|
|
1273
|
-
completionTokens,
|
|
1274
|
-
promptTokens,
|
|
1275
|
-
reasoningTokens: reasoning,
|
|
1276
|
-
totalTokens: total ?? promptTokens + completionTokens
|
|
1277
|
-
});
|
|
1278
1893
|
}
|
|
1279
|
-
function
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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)
|
|
1908
|
+
});
|
|
1909
|
+
if (!response.ok) {
|
|
1910
|
+
throw new Error(
|
|
1911
|
+
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
1912
|
+
response
|
|
1913
|
+
)}`
|
|
1914
|
+
);
|
|
1283
1915
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
function completionChoices(completion) {
|
|
1291
|
-
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
1292
|
-
return choices.map((choice) => asRecord(choice));
|
|
1293
|
-
}
|
|
1294
|
-
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
1295
|
-
let event = "message";
|
|
1296
|
-
const dataLines = [];
|
|
1297
|
-
for (const line of block.split(/\r?\n/)) {
|
|
1298
|
-
const trimmed = line.trim();
|
|
1299
|
-
if (trimmed.startsWith("event:")) {
|
|
1300
|
-
event = trimmed.slice("event:".length).trim() || event;
|
|
1301
|
-
} else if (trimmed.startsWith("data:")) {
|
|
1302
|
-
dataLines.push(trimmed.slice("data:".length).trim());
|
|
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;
|
|
1303
1922
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (error) {
|
|
1320
|
-
markTerminal();
|
|
1321
|
-
enqueue({ error });
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const choices = completionChoices(parsed).map((choice, index) => {
|
|
1325
|
-
const delta = asRecord(choice.delta);
|
|
1326
|
-
const text = contentToText(delta.content);
|
|
1327
|
-
const finishReason = choice.finish_reason ?? null;
|
|
1328
|
-
if (!text && finishReason === null) {
|
|
1329
|
-
return void 0;
|
|
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}`);
|
|
1330
1938
|
}
|
|
1331
|
-
return {
|
|
1332
|
-
finish_reason: finishReason,
|
|
1333
|
-
index: typeof choice.index === "number" ? choice.index : index,
|
|
1334
|
-
logprobs: choice.logprobs ?? null,
|
|
1335
|
-
text
|
|
1336
|
-
};
|
|
1337
|
-
}).filter((choice) => choice !== void 0);
|
|
1338
|
-
const usage = asRecord(parsed.usage);
|
|
1339
|
-
const hasUsage = Object.keys(usage).length > 0;
|
|
1340
|
-
if (choices.length === 0 && !hasUsage) {
|
|
1341
|
-
return;
|
|
1342
1939
|
}
|
|
1343
|
-
|
|
1344
|
-
removeUndefined({
|
|
1345
|
-
choices,
|
|
1346
|
-
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
1347
|
-
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
1348
|
-
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
1349
|
-
object: "text_completion",
|
|
1350
|
-
usage: hasUsage ? usage : void 0
|
|
1351
|
-
})
|
|
1352
|
-
);
|
|
1940
|
+
throw new Error("GitHub device login timed out. Run `hoopilot login` again.");
|
|
1353
1941
|
}
|
|
1354
|
-
function
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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}.`);
|
|
1360
1957
|
}
|
|
1361
|
-
if (
|
|
1362
|
-
|
|
1363
|
-
code: contentToText(parsed.code) || void 0,
|
|
1364
|
-
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
1365
|
-
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
1366
|
-
});
|
|
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.`);
|
|
1367
1960
|
}
|
|
1368
|
-
return
|
|
1961
|
+
return url.host;
|
|
1369
1962
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1963
|
+
function positiveSeconds(value, fallback) {
|
|
1964
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1965
|
+
}
|
|
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)}`);
|
|
1378
1972
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1973
|
+
}
|
|
1974
|
+
|
|
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: () => {
|
|
1382
2017
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
+
);
|
|
1388
2046
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
handlers.appendToolCall(asRecord(toolCall));
|
|
2047
|
+
if (options.stream) {
|
|
2048
|
+
return pino(pinoOptions, options.stream);
|
|
1392
2049
|
}
|
|
2050
|
+
return pino(pinoOptions);
|
|
1393
2051
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
error: null,
|
|
1398
|
-
id,
|
|
1399
|
-
incomplete_details: null,
|
|
1400
|
-
instructions: null,
|
|
1401
|
-
max_output_tokens: null,
|
|
1402
|
-
metadata: {},
|
|
1403
|
-
model,
|
|
1404
|
-
object: "response",
|
|
1405
|
-
output,
|
|
1406
|
-
parallel_tool_calls: true,
|
|
1407
|
-
status,
|
|
1408
|
-
temperature: null,
|
|
1409
|
-
tool_choice: "auto",
|
|
1410
|
-
tools: [],
|
|
1411
|
-
top_p: null
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
function encodeSse(event, data) {
|
|
1415
|
-
if (data === "[DONE]") {
|
|
1416
|
-
return "data: [DONE]\n\n";
|
|
2052
|
+
function parseLogFormat(value) {
|
|
2053
|
+
if (!value) {
|
|
2054
|
+
return DEFAULT_LOG_FORMAT;
|
|
1417
2055
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
`;
|
|
1422
|
-
}
|
|
1423
|
-
function encodeDataSse(data) {
|
|
1424
|
-
if (data === "[DONE]") {
|
|
1425
|
-
return "data: [DONE]\n\n";
|
|
2056
|
+
if (isLogFormat(value)) {
|
|
2057
|
+
return value;
|
|
1426
2058
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
`;
|
|
2059
|
+
throw new Error(`Invalid log format: ${value}. Expected one of: ${LOG_FORMATS.join(", ")}.`);
|
|
1430
2060
|
}
|
|
1431
|
-
function
|
|
1432
|
-
|
|
1433
|
-
return
|
|
1434
|
-
} catch {
|
|
1435
|
-
return void 0;
|
|
2061
|
+
function parseLogLevel(value) {
|
|
2062
|
+
if (!value) {
|
|
2063
|
+
return DEFAULT_LOG_LEVEL;
|
|
1436
2064
|
}
|
|
2065
|
+
if (isLogLevel(value)) {
|
|
2066
|
+
return value;
|
|
2067
|
+
}
|
|
2068
|
+
throw new Error(`Invalid log level: ${value}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
|
|
1437
2069
|
}
|
|
1438
|
-
function
|
|
1439
|
-
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
|
+
);
|
|
1440
2074
|
}
|
|
1441
|
-
function
|
|
1442
|
-
|
|
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) };
|
|
1443
2084
|
}
|
|
1444
|
-
function
|
|
1445
|
-
return
|
|
2085
|
+
function isLogFormat(value) {
|
|
2086
|
+
return LOG_FORMATS.includes(value);
|
|
2087
|
+
}
|
|
2088
|
+
function isLogLevel(value) {
|
|
2089
|
+
return LOG_LEVELS.includes(value);
|
|
1446
2090
|
}
|
|
1447
2091
|
|
|
1448
2092
|
// src/metrics.ts
|
|
@@ -1872,6 +2516,7 @@ var DEFAULT_HOST = "127.0.0.1";
|
|
|
1872
2516
|
var DEFAULT_PORT = 4141;
|
|
1873
2517
|
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1874
2518
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
2519
|
+
var JSON_OBJECT_MESSAGE = "Request body must be a JSON object.";
|
|
1875
2520
|
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1876
2521
|
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1877
2522
|
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
@@ -1941,6 +2586,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1941
2586
|
if (request.method === "GET" && apiPath === "/v1/models") {
|
|
1942
2587
|
return finish(await handleModels(client, metrics, request.signal, requestLogger));
|
|
1943
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
|
+
}
|
|
1944
2597
|
if (request.method === "POST" && apiPath === "/v1/chat/completions") {
|
|
1945
2598
|
return finish(
|
|
1946
2599
|
await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
|
|
@@ -1964,16 +2617,16 @@ function createHoopilotHandler(options = {}) {
|
|
|
1964
2617
|
return finish(jsonError(401, "copilot_auth_error", error.message));
|
|
1965
2618
|
}
|
|
1966
2619
|
const message = errorMessage(error);
|
|
1967
|
-
if (message === INVALID_JSON_MESSAGE) {
|
|
2620
|
+
if (message === INVALID_JSON_MESSAGE || message === JSON_OBJECT_MESSAGE) {
|
|
1968
2621
|
requestLogger.warn(
|
|
1969
2622
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1970
|
-
"request body was
|
|
2623
|
+
"request body was not usable json"
|
|
1971
2624
|
);
|
|
1972
2625
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1973
|
-
} else if (error instanceof OpenAICompatibilityError) {
|
|
2626
|
+
} else if (error instanceof OpenAICompatibilityError || error instanceof AnthropicCompatibilityError) {
|
|
1974
2627
|
requestLogger.warn(
|
|
1975
2628
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1976
|
-
"request body used unsupported
|
|
2629
|
+
"request body used unsupported compatibility fields"
|
|
1977
2630
|
);
|
|
1978
2631
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1979
2632
|
} else if (error instanceof RequestBodyTooLargeError) {
|
|
@@ -2017,6 +2670,40 @@ function startHoopilotServer(options = {}) {
|
|
|
2017
2670
|
url: `http://${urlHost(host)}:${server.port}`
|
|
2018
2671
|
};
|
|
2019
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
|
+
}
|
|
2020
2707
|
async function handleModels(client, metrics, signal, logger) {
|
|
2021
2708
|
const upstream = await client.models(signal);
|
|
2022
2709
|
metrics.recordUpstream("/models", upstream.ok);
|
|
@@ -2124,20 +2811,24 @@ function proxyResponse(upstream) {
|
|
|
2124
2811
|
}
|
|
2125
2812
|
async function readJson(request) {
|
|
2126
2813
|
const text = await readRequestText(request);
|
|
2814
|
+
return parseJsonObject2(text);
|
|
2815
|
+
}
|
|
2816
|
+
function parseJsonObject2(text) {
|
|
2817
|
+
let parsed;
|
|
2127
2818
|
try {
|
|
2128
|
-
|
|
2819
|
+
parsed = JSON.parse(text);
|
|
2129
2820
|
} catch {
|
|
2130
2821
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
2131
2822
|
}
|
|
2823
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2824
|
+
throw new Error(JSON_OBJECT_MESSAGE);
|
|
2825
|
+
}
|
|
2826
|
+
return parsed;
|
|
2132
2827
|
}
|
|
2133
2828
|
async function readJsonText(request) {
|
|
2134
2829
|
const text = await readRequestText(request);
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
return text;
|
|
2138
|
-
} catch {
|
|
2139
|
-
throw new Error(INVALID_JSON_MESSAGE);
|
|
2140
|
-
}
|
|
2830
|
+
parseJsonObject2(text);
|
|
2831
|
+
return text;
|
|
2141
2832
|
}
|
|
2142
2833
|
async function readRequestText(request) {
|
|
2143
2834
|
const contentLength = request.headers.get("content-length");
|
|
@@ -2212,9 +2903,10 @@ function websocketUnsupportedResponse() {
|
|
|
2212
2903
|
}
|
|
2213
2904
|
function corsHeaders() {
|
|
2214
2905
|
return {
|
|
2215
|
-
"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",
|
|
2216
2907
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
2217
|
-
"access-control-allow-origin": "*"
|
|
2908
|
+
"access-control-allow-origin": "*",
|
|
2909
|
+
"access-control-expose-headers": "x-request-id"
|
|
2218
2910
|
};
|
|
2219
2911
|
}
|
|
2220
2912
|
function isAuthorized(request, apiKey) {
|
|
@@ -2366,6 +3058,10 @@ function canonicalApiPath(path) {
|
|
|
2366
3058
|
return "/v1/chat/completions";
|
|
2367
3059
|
case "/completions":
|
|
2368
3060
|
return "/v1/completions";
|
|
3061
|
+
case "/messages":
|
|
3062
|
+
return "/v1/messages";
|
|
3063
|
+
case "/messages/count_tokens":
|
|
3064
|
+
return "/v1/messages/count_tokens";
|
|
2369
3065
|
case "/responses":
|
|
2370
3066
|
return "/v1/responses";
|
|
2371
3067
|
case "/usage":
|
|
@@ -2390,6 +3086,12 @@ function routeFor(method, path) {
|
|
|
2390
3086
|
if (method === "GET" && path === "/v1/models") {
|
|
2391
3087
|
return "models";
|
|
2392
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
|
+
}
|
|
2393
3095
|
if (method === "POST" && path === "/v1/chat/completions") {
|
|
2394
3096
|
return "chat_completions";
|
|
2395
3097
|
}
|
|
@@ -2469,6 +3171,7 @@ function safeParseJson(text) {
|
|
|
2469
3171
|
}
|
|
2470
3172
|
}
|
|
2471
3173
|
export {
|
|
3174
|
+
AnthropicCompatibilityError,
|
|
2472
3175
|
COPILOT_USAGE_API_VERSION,
|
|
2473
3176
|
CopilotAuth,
|
|
2474
3177
|
CopilotAuthError,
|
|
@@ -2479,6 +3182,7 @@ export {
|
|
|
2479
3182
|
DEFAULT_MODEL,
|
|
2480
3183
|
MetricsRegistry,
|
|
2481
3184
|
PROMETHEUS_CONTENT_TYPE,
|
|
3185
|
+
anthropicMessagesToResponsesRequest,
|
|
2482
3186
|
applyCopilotHeaders,
|
|
2483
3187
|
applyGithubApiHeaders,
|
|
2484
3188
|
authStorePath,
|
|
@@ -2488,6 +3192,7 @@ export {
|
|
|
2488
3192
|
completionsRequestToChatCompletion,
|
|
2489
3193
|
createHoopilotHandler,
|
|
2490
3194
|
createHoopilotLogger,
|
|
3195
|
+
estimateAnthropicMessageTokens,
|
|
2491
3196
|
extractTokenUsage,
|
|
2492
3197
|
fallbackModels,
|
|
2493
3198
|
githubCopilotDeviceLogin,
|
|
@@ -2501,7 +3206,9 @@ export {
|
|
|
2501
3206
|
parseLogLevel,
|
|
2502
3207
|
readStoredCopilotAuth,
|
|
2503
3208
|
responsesRequestToChatCompletion,
|
|
3209
|
+
responsesResponseToAnthropicMessage,
|
|
2504
3210
|
responsesStreamFromChatStream,
|
|
3211
|
+
responsesStreamToAnthropicStream,
|
|
2505
3212
|
startHoopilotServer,
|
|
2506
3213
|
writeStoredCopilotAuth
|
|
2507
3214
|
};
|