@openhoo/hoopilot 0.7.4 → 0.8.0

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