@openhoo/hoopilot 0.7.4 → 0.8.0

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