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