@openhoo/hoopilot 0.7.5 → 0.8.0

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