@openhoo/hoopilot 0.2.2

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 ADDED
@@ -0,0 +1,1038 @@
1
+ // src/auth.ts
2
+ import { execFileSync } from "child_process";
3
+ var DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
4
+ var DEFAULT_TOKEN_EXCHANGE_URL = "https://api.github.com/copilot_internal/v2/token";
5
+ var REFRESH_SKEW_MS = 6e4;
6
+ var defaultLogger = {
7
+ info: () => void 0,
8
+ warn: () => void 0,
9
+ error: () => void 0
10
+ };
11
+ var CopilotAuthError = class extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "CopilotAuthError";
15
+ }
16
+ };
17
+ var CopilotAuth = class {
18
+ #authMode;
19
+ #copilotApiBaseUrl;
20
+ #copilotToken;
21
+ #env;
22
+ #fetch;
23
+ #githubToken;
24
+ #githubTokenCommand;
25
+ #logger;
26
+ #tokenExchangeUrl;
27
+ #cachedAccess;
28
+ constructor(options = {}) {
29
+ this.#authMode = options.authMode ?? "auto";
30
+ this.#copilotApiBaseUrl = trimTrailingSlash(
31
+ options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
32
+ );
33
+ this.#copilotToken = options.copilotToken;
34
+ this.#env = options.env ?? process.env;
35
+ this.#fetch = options.fetch ?? fetch;
36
+ this.#githubToken = options.githubToken;
37
+ this.#githubTokenCommand = options.githubTokenCommand ?? "gh auth token";
38
+ this.#logger = options.logger ?? defaultLogger;
39
+ this.#tokenExchangeUrl = options.tokenExchangeUrl ?? options.env?.COPILOT_TOKEN_EXCHANGE_URL ?? DEFAULT_TOKEN_EXCHANGE_URL;
40
+ }
41
+ async getAccess() {
42
+ if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
43
+ return this.#cachedAccess;
44
+ }
45
+ const directCopilotToken = this.#resolveDirectCopilotToken();
46
+ if (this.#authMode === "copilot-token") {
47
+ if (!directCopilotToken) {
48
+ throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
49
+ }
50
+ return this.#cacheAccess({
51
+ apiBaseUrl: this.#copilotApiBaseUrl,
52
+ expiresAtMs: Date.now() + 10 * 6e4,
53
+ source: "copilot-token",
54
+ token: directCopilotToken
55
+ });
56
+ }
57
+ if (directCopilotToken) {
58
+ return this.#cacheAccess({
59
+ apiBaseUrl: this.#copilotApiBaseUrl,
60
+ expiresAtMs: Date.now() + 10 * 6e4,
61
+ source: "copilot-token",
62
+ token: directCopilotToken
63
+ });
64
+ }
65
+ const githubToken = this.#resolveGithubToken();
66
+ if (!githubToken) {
67
+ throw new CopilotAuthError(
68
+ "No Copilot credential found. Set COPILOT_GITHUB_TOKEN, GITHUB_TOKEN, or sign in with gh auth login."
69
+ );
70
+ }
71
+ if (this.#authMode === "direct-github-token") {
72
+ return this.#cacheAccess({
73
+ apiBaseUrl: this.#copilotApiBaseUrl,
74
+ expiresAtMs: Date.now() + 10 * 6e4,
75
+ source: "direct-github-token",
76
+ token: githubToken
77
+ });
78
+ }
79
+ try {
80
+ const exchanged = await this.#exchangeGithubToken(githubToken);
81
+ return this.#cacheAccess(exchanged);
82
+ } catch (error) {
83
+ if (this.#authMode === "github-token") {
84
+ throw error;
85
+ }
86
+ this.#logger.warn(
87
+ `Copilot token exchange failed; falling back to direct GitHub token mode: ${errorMessage(error)}`
88
+ );
89
+ return this.#cacheAccess({
90
+ apiBaseUrl: this.#copilotApiBaseUrl,
91
+ expiresAtMs: Date.now() + 10 * 6e4,
92
+ source: "direct-github-token",
93
+ token: githubToken
94
+ });
95
+ }
96
+ }
97
+ #cacheAccess(access) {
98
+ this.#cachedAccess = access;
99
+ return access;
100
+ }
101
+ async #exchangeGithubToken(githubToken) {
102
+ const response = await this.#fetch(this.#tokenExchangeUrl, {
103
+ headers: {
104
+ accept: "application/vnd.github+json",
105
+ authorization: `token ${githubToken}`,
106
+ "editor-plugin-version": "hoopilot/0.1.0",
107
+ "editor-version": "Hoopilot/0.1.0",
108
+ "user-agent": "hoopilot/0.1.0"
109
+ },
110
+ method: "GET"
111
+ });
112
+ if (!response.ok) {
113
+ throw new CopilotAuthError(
114
+ `GitHub Copilot token exchange failed with ${response.status}: ${await safeResponseText(
115
+ response
116
+ )}`
117
+ );
118
+ }
119
+ const body = asRecord(await response.json());
120
+ const token = getString(body, "token");
121
+ if (!token) {
122
+ throw new CopilotAuthError("GitHub Copilot token exchange response did not include a token.");
123
+ }
124
+ return {
125
+ apiBaseUrl: endpointFromResponse(body) ?? this.#copilotApiBaseUrl,
126
+ expiresAtMs: expiresAtFromResponse(body),
127
+ source: "github-token",
128
+ token
129
+ };
130
+ }
131
+ #resolveDirectCopilotToken() {
132
+ return firstNonEmpty(
133
+ this.#copilotToken,
134
+ this.#env.COPILOT_API_TOKEN,
135
+ this.#env.GITHUB_COPILOT_API_TOKEN,
136
+ this.#env.GITHUB_COPILOT_TOKEN
137
+ );
138
+ }
139
+ #resolveGithubToken() {
140
+ return firstNonEmpty(
141
+ this.#githubToken,
142
+ this.#env.COPILOT_GITHUB_TOKEN,
143
+ this.#env.GITHUB_COPILOT_GITHUB_TOKEN,
144
+ this.#env.GH_TOKEN,
145
+ this.#env.GITHUB_TOKEN,
146
+ this.#readGithubTokenCommand()
147
+ );
148
+ }
149
+ #readGithubTokenCommand() {
150
+ if (this.#githubTokenCommand === false) {
151
+ return void 0;
152
+ }
153
+ const parts = splitCommand(this.#githubTokenCommand);
154
+ const [command, ...args] = parts;
155
+ if (!command) {
156
+ return void 0;
157
+ }
158
+ try {
159
+ const output = execFileSync(command, args, {
160
+ encoding: "utf8",
161
+ stdio: ["ignore", "pipe", "ignore"],
162
+ timeout: 5e3
163
+ });
164
+ return output.trim() || void 0;
165
+ } catch {
166
+ return void 0;
167
+ }
168
+ }
169
+ };
170
+ function splitCommand(command) {
171
+ const parts = [];
172
+ let current = "";
173
+ let quote;
174
+ let escaping = false;
175
+ for (const character of command.trim()) {
176
+ if (escaping) {
177
+ current += character;
178
+ escaping = false;
179
+ continue;
180
+ }
181
+ if (character === "\\") {
182
+ escaping = true;
183
+ continue;
184
+ }
185
+ if (quote) {
186
+ if (character === quote) {
187
+ quote = void 0;
188
+ } else {
189
+ current += character;
190
+ }
191
+ continue;
192
+ }
193
+ if (character === "'" || character === '"') {
194
+ quote = character;
195
+ continue;
196
+ }
197
+ if (/\s/.test(character)) {
198
+ if (current) {
199
+ parts.push(current);
200
+ current = "";
201
+ }
202
+ continue;
203
+ }
204
+ current += character;
205
+ }
206
+ if (current) {
207
+ parts.push(current);
208
+ }
209
+ return parts;
210
+ }
211
+ function endpointFromResponse(body) {
212
+ const endpoints = asRecord(body.endpoints);
213
+ const apiUrl = getString(endpoints, "api") ?? getString(endpoints, "proxy");
214
+ return apiUrl ? trimTrailingSlash(apiUrl) : void 0;
215
+ }
216
+ function expiresAtFromResponse(body) {
217
+ const expiresAt = body.expires_at;
218
+ if (typeof expiresAt === "number") {
219
+ return expiresAt < 1e10 ? expiresAt * 1e3 : expiresAt;
220
+ }
221
+ if (typeof expiresAt === "string") {
222
+ const asNumber = Number(expiresAt);
223
+ if (Number.isFinite(asNumber)) {
224
+ return asNumber < 1e10 ? asNumber * 1e3 : asNumber;
225
+ }
226
+ const parsed = Date.parse(expiresAt);
227
+ if (Number.isFinite(parsed)) {
228
+ return parsed;
229
+ }
230
+ }
231
+ const refreshIn = body.refresh_in;
232
+ if (typeof refreshIn === "number" && Number.isFinite(refreshIn)) {
233
+ return Date.now() + refreshIn * 1e3;
234
+ }
235
+ return Date.now() + 10 * 6e4;
236
+ }
237
+ function firstNonEmpty(...values) {
238
+ for (const value of values) {
239
+ const trimmed = value?.trim();
240
+ if (trimmed) {
241
+ return trimmed;
242
+ }
243
+ }
244
+ return void 0;
245
+ }
246
+ function asRecord(value) {
247
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
248
+ }
249
+ function getString(record, key) {
250
+ const value = record[key];
251
+ return typeof value === "string" && value ? value : void 0;
252
+ }
253
+ function trimTrailingSlash(value) {
254
+ return value.replace(/\/+$/, "");
255
+ }
256
+ async function safeResponseText(response) {
257
+ const text = await response.text();
258
+ return text.slice(0, 500);
259
+ }
260
+ function errorMessage(error) {
261
+ return error instanceof Error ? error.message : String(error);
262
+ }
263
+
264
+ // src/copilot.ts
265
+ var CopilotClient = class {
266
+ #auth;
267
+ #fetch;
268
+ constructor(options = {}) {
269
+ this.#auth = new CopilotAuth(options);
270
+ this.#fetch = options.fetch ?? fetch;
271
+ }
272
+ async chatCompletions(body, signal) {
273
+ return this.fetchCopilot("/chat/completions", {
274
+ body: JSON.stringify(body),
275
+ headers: {
276
+ "content-type": "application/json"
277
+ },
278
+ method: "POST",
279
+ signal
280
+ });
281
+ }
282
+ async forwardChatCompletions(body, signal) {
283
+ return this.fetchCopilot("/chat/completions", {
284
+ body,
285
+ headers: {
286
+ "content-type": "application/json"
287
+ },
288
+ method: "POST",
289
+ signal
290
+ });
291
+ }
292
+ async models(signal) {
293
+ return this.fetchCopilot("/models", {
294
+ headers: {
295
+ accept: "application/json"
296
+ },
297
+ method: "GET",
298
+ signal
299
+ });
300
+ }
301
+ async fetchCopilot(path, init) {
302
+ const access = await this.#auth.getAccess();
303
+ const headers = new Headers(init.headers);
304
+ headers.set("accept", headers.get("accept") ?? "application/json");
305
+ headers.set("authorization", `Bearer ${access.token}`);
306
+ headers.set("copilot-integration-id", "vscode-chat");
307
+ headers.set("editor-plugin-version", "hoopilot/0.1.0");
308
+ headers.set("editor-version", "Hoopilot/0.1.0");
309
+ headers.set("openai-intent", "conversation-panel");
310
+ headers.set("user-agent", "hoopilot/0.1.0");
311
+ return this.#fetch(`${access.apiBaseUrl}${path}`, {
312
+ ...init,
313
+ headers
314
+ });
315
+ }
316
+ };
317
+
318
+ // src/openai.ts
319
+ var DEFAULT_MODEL = "gpt-4.1";
320
+ function responsesRequestToChatCompletion(request) {
321
+ const messages = [];
322
+ const instructions = contentToText(request.instructions);
323
+ if (instructions) {
324
+ messages.push({ content: instructions, role: "system" });
325
+ }
326
+ for (const message of inputToMessages(request.input)) {
327
+ messages.push(message);
328
+ }
329
+ return removeUndefined({
330
+ frequency_penalty: request.frequency_penalty,
331
+ max_tokens: request.max_output_tokens ?? request.max_tokens,
332
+ messages,
333
+ metadata: request.metadata,
334
+ model: contentToText(request.model) || DEFAULT_MODEL,
335
+ presence_penalty: request.presence_penalty,
336
+ reasoning_effort: asRecord2(request.reasoning).effort,
337
+ response_format: asRecord2(request.text).format,
338
+ seed: request.seed,
339
+ stream: request.stream === true,
340
+ temperature: request.temperature,
341
+ tool_choice: chatToolChoice(request.tool_choice),
342
+ tools: chatTools(request.tools),
343
+ top_p: request.top_p
344
+ });
345
+ }
346
+ function completionsRequestToChatCompletion(request) {
347
+ return removeUndefined({
348
+ max_tokens: request.max_tokens,
349
+ messages: [{ content: promptToText(request.prompt), role: "user" }],
350
+ model: contentToText(request.model) || DEFAULT_MODEL,
351
+ stream: request.stream === true,
352
+ temperature: request.temperature,
353
+ top_p: request.top_p
354
+ });
355
+ }
356
+ function chatCompletionToResponse(completion, responseId) {
357
+ const id = responseId ?? `resp_${randomId()}`;
358
+ const choice = firstChoice(completion);
359
+ const message = asRecord2(choice.message);
360
+ const model = contentToText(completion.model) || DEFAULT_MODEL;
361
+ const output = outputItemsFromMessage(message);
362
+ const usage = responseUsage(completion.usage);
363
+ return removeUndefined({
364
+ created_at: epochSeconds(),
365
+ error: null,
366
+ id,
367
+ incomplete_details: null,
368
+ instructions: null,
369
+ max_output_tokens: null,
370
+ metadata: {},
371
+ model,
372
+ object: "response",
373
+ output,
374
+ output_text: outputText(output),
375
+ parallel_tool_calls: true,
376
+ status: "completed",
377
+ temperature: null,
378
+ tool_choice: "auto",
379
+ tools: [],
380
+ top_p: null,
381
+ usage
382
+ });
383
+ }
384
+ function chatCompletionToCompletion(completion) {
385
+ const choice = firstChoice(completion);
386
+ const message = asRecord2(choice.message);
387
+ return removeUndefined({
388
+ choices: [
389
+ {
390
+ finish_reason: choice.finish_reason ?? "stop",
391
+ index: 0,
392
+ logprobs: null,
393
+ text: contentToText(message.content)
394
+ }
395
+ ],
396
+ created: completion.created ?? epochSeconds(),
397
+ id: completion.id ?? `cmpl_${randomId()}`,
398
+ model: completion.model ?? DEFAULT_MODEL,
399
+ object: "text_completion",
400
+ usage: completion.usage
401
+ });
402
+ }
403
+ function normalizeModelsResponse(upstream) {
404
+ const record = asRecord2(upstream);
405
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
406
+ const models = data.map((model) => asRecord2(model)).filter((model) => typeof model.id === "string").map((model) => ({
407
+ created: model.created ?? 0,
408
+ id: model.id,
409
+ object: "model",
410
+ owned_by: model.owned_by ?? "github-copilot"
411
+ }));
412
+ return {
413
+ data: models.length > 0 ? models : fallbackModels(),
414
+ object: "list"
415
+ };
416
+ }
417
+ function fallbackModels() {
418
+ return [
419
+ {
420
+ created: 0,
421
+ id: DEFAULT_MODEL,
422
+ object: "model",
423
+ owned_by: "github-copilot"
424
+ }
425
+ ];
426
+ }
427
+ function responsesStreamFromChatStream(chatStream, options) {
428
+ const encoder = new TextEncoder();
429
+ const decoder = new TextDecoder();
430
+ const responseId = options.responseId ?? `resp_${randomId()}`;
431
+ const messageId = `msg_${randomId()}`;
432
+ const createdAt = epochSeconds();
433
+ let buffer = "";
434
+ let text = "";
435
+ const tools = /* @__PURE__ */ new Map();
436
+ return new ReadableStream({
437
+ async start(controller) {
438
+ const enqueue = (event, data) => {
439
+ controller.enqueue(encoder.encode(encodeSse(event, data)));
440
+ };
441
+ enqueue("response.created", {
442
+ response: baseStreamResponse(responseId, options.model, createdAt, "in_progress", []),
443
+ type: "response.created"
444
+ });
445
+ enqueue("response.output_item.added", {
446
+ item: {
447
+ content: [],
448
+ id: messageId,
449
+ role: "assistant",
450
+ status: "in_progress",
451
+ type: "message"
452
+ },
453
+ output_index: 0,
454
+ type: "response.output_item.added"
455
+ });
456
+ enqueue("response.content_part.added", {
457
+ content_index: 0,
458
+ item_id: messageId,
459
+ output_index: 0,
460
+ part: {
461
+ annotations: [],
462
+ text: "",
463
+ type: "output_text"
464
+ },
465
+ type: "response.content_part.added"
466
+ });
467
+ const reader = chatStream.getReader();
468
+ try {
469
+ while (true) {
470
+ const result = await reader.read();
471
+ if (result.done) {
472
+ break;
473
+ }
474
+ buffer += decoder.decode(result.value, { stream: true });
475
+ const lines = buffer.split(/\r?\n/);
476
+ buffer = lines.pop() ?? "";
477
+ for (const line of lines) {
478
+ processChatSseLine(line, enqueue, tools, (delta) => {
479
+ text += delta;
480
+ });
481
+ }
482
+ }
483
+ if (buffer) {
484
+ processChatSseLine(buffer, enqueue, tools, (delta) => {
485
+ text += delta;
486
+ });
487
+ }
488
+ const output = streamOutputItems(messageId, text, [...tools.values()]);
489
+ enqueue("response.output_text.done", {
490
+ content_index: 0,
491
+ item_id: messageId,
492
+ output_index: 0,
493
+ text,
494
+ type: "response.output_text.done"
495
+ });
496
+ enqueue("response.content_part.done", {
497
+ content_index: 0,
498
+ item_id: messageId,
499
+ output_index: 0,
500
+ part: {
501
+ annotations: [],
502
+ text,
503
+ type: "output_text"
504
+ },
505
+ type: "response.content_part.done"
506
+ });
507
+ enqueue("response.output_item.done", {
508
+ item: output[0],
509
+ output_index: 0,
510
+ type: "response.output_item.done"
511
+ });
512
+ tools.forEach((tool, index) => {
513
+ const item = functionCallItem(tool);
514
+ const outputIndex = index + 1;
515
+ enqueue("response.output_item.added", {
516
+ item,
517
+ output_index: outputIndex,
518
+ type: "response.output_item.added"
519
+ });
520
+ enqueue("response.function_call_arguments.done", {
521
+ arguments: tool.arguments,
522
+ item_id: item.id,
523
+ output_index: outputIndex,
524
+ type: "response.function_call_arguments.done"
525
+ });
526
+ enqueue("response.output_item.done", {
527
+ item,
528
+ output_index: outputIndex,
529
+ type: "response.output_item.done"
530
+ });
531
+ });
532
+ enqueue("response.completed", {
533
+ response: baseStreamResponse(responseId, options.model, createdAt, "completed", output),
534
+ type: "response.completed"
535
+ });
536
+ enqueue("done", "[DONE]");
537
+ controller.close();
538
+ } catch (error) {
539
+ controller.error(error);
540
+ } finally {
541
+ reader.releaseLock();
542
+ }
543
+ }
544
+ });
545
+ }
546
+ function inputToMessages(input) {
547
+ if (typeof input === "string") {
548
+ return [{ content: input, role: "user" }];
549
+ }
550
+ if (!Array.isArray(input)) {
551
+ return [];
552
+ }
553
+ const messages = [];
554
+ for (const item of input) {
555
+ const record = asRecord2(item);
556
+ if (record.type === "function_call_output") {
557
+ messages.push({
558
+ content: contentToText(record.output),
559
+ role: "tool",
560
+ tool_call_id: contentToText(record.call_id)
561
+ });
562
+ continue;
563
+ }
564
+ if (record.type === "function_call") {
565
+ messages.push({
566
+ role: "assistant",
567
+ tool_calls: [
568
+ {
569
+ function: {
570
+ arguments: contentToText(record.arguments),
571
+ name: contentToText(record.name)
572
+ },
573
+ id: contentToText(record.call_id) || contentToText(record.id),
574
+ type: "function"
575
+ }
576
+ ]
577
+ });
578
+ continue;
579
+ }
580
+ const role = roleToChatRole(contentToText(record.role));
581
+ const content = chatMessageContent(record.content);
582
+ if (role && content !== void 0) {
583
+ messages.push({ content, role });
584
+ }
585
+ }
586
+ return messages;
587
+ }
588
+ function chatMessageContent(content) {
589
+ if (typeof content === "string") {
590
+ return content;
591
+ }
592
+ if (!Array.isArray(content)) {
593
+ return contentToText(content) || void 0;
594
+ }
595
+ const parts = [];
596
+ for (const part of content) {
597
+ const record = asRecord2(part);
598
+ const type = contentToText(record.type);
599
+ if (type === "input_text" || type === "output_text" || type === "text") {
600
+ parts.push({ text: contentToText(record.text), type: "text" });
601
+ }
602
+ if (type === "input_image") {
603
+ const imageUrl = contentToText(record.image_url);
604
+ if (imageUrl) {
605
+ parts.push({ image_url: { url: imageUrl }, type: "image_url" });
606
+ }
607
+ }
608
+ }
609
+ if (parts.length === 0) {
610
+ return void 0;
611
+ }
612
+ if (parts.every((part) => part.type === "text")) {
613
+ return parts.map((part) => contentToText(part.text)).join("\n");
614
+ }
615
+ return parts;
616
+ }
617
+ function promptToText(prompt) {
618
+ if (Array.isArray(prompt)) {
619
+ return prompt.map((item) => contentToText(item)).join("\n");
620
+ }
621
+ return contentToText(prompt);
622
+ }
623
+ function contentToText(content) {
624
+ if (typeof content === "string") {
625
+ return content;
626
+ }
627
+ if (typeof content === "number" || typeof content === "boolean") {
628
+ return String(content);
629
+ }
630
+ if (Array.isArray(content)) {
631
+ return content.map((item) => contentToText(item)).filter(Boolean).join("\n");
632
+ }
633
+ if (content && typeof content === "object") {
634
+ const record = content;
635
+ if (typeof record.text === "string") {
636
+ return record.text;
637
+ }
638
+ if (typeof record.output_text === "string") {
639
+ return record.output_text;
640
+ }
641
+ return JSON.stringify(content);
642
+ }
643
+ return "";
644
+ }
645
+ function roleToChatRole(role) {
646
+ if (role === "assistant" || role === "developer" || role === "system" || role === "tool") {
647
+ return role === "developer" ? "system" : role;
648
+ }
649
+ return "user";
650
+ }
651
+ function chatTools(tools) {
652
+ if (!Array.isArray(tools)) {
653
+ return void 0;
654
+ }
655
+ const converted = tools.map((tool) => asRecord2(tool)).filter((tool) => tool.type === "function").map((tool) => ({
656
+ function: removeUndefined({
657
+ description: tool.description,
658
+ name: tool.name,
659
+ parameters: tool.parameters,
660
+ strict: tool.strict
661
+ }),
662
+ type: "function"
663
+ }));
664
+ return converted.length > 0 ? converted : void 0;
665
+ }
666
+ function chatToolChoice(toolChoice) {
667
+ if (typeof toolChoice === "string" || toolChoice === void 0) {
668
+ return toolChoice;
669
+ }
670
+ const record = asRecord2(toolChoice);
671
+ if (record.type === "function" && typeof record.name === "string") {
672
+ return { function: { name: record.name }, type: "function" };
673
+ }
674
+ return toolChoice;
675
+ }
676
+ function outputItemsFromMessage(message) {
677
+ const output = [];
678
+ const text = contentToText(message.content);
679
+ if (text) {
680
+ output.push(messageOutputItem(text));
681
+ }
682
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
683
+ for (const toolCall of toolCalls) {
684
+ const record = asRecord2(toolCall);
685
+ const fn = asRecord2(record.function);
686
+ output.push(
687
+ functionCallItem({
688
+ arguments: contentToText(fn.arguments),
689
+ id: contentToText(record.id) || `call_${randomId()}`,
690
+ index: output.length,
691
+ name: contentToText(fn.name)
692
+ })
693
+ );
694
+ }
695
+ return output;
696
+ }
697
+ function messageOutputItem(text, id = `msg_${randomId()}`) {
698
+ return {
699
+ content: [
700
+ {
701
+ annotations: [],
702
+ text,
703
+ type: "output_text"
704
+ }
705
+ ],
706
+ id,
707
+ role: "assistant",
708
+ status: "completed",
709
+ type: "message"
710
+ };
711
+ }
712
+ function functionCallItem(tool) {
713
+ return {
714
+ arguments: tool.arguments,
715
+ call_id: tool.id,
716
+ id: `fc_${randomId()}`,
717
+ name: tool.name,
718
+ status: "completed",
719
+ type: "function_call"
720
+ };
721
+ }
722
+ function outputText(output) {
723
+ return output.flatMap((item) => {
724
+ const content = item.content;
725
+ return Array.isArray(content) ? content : [];
726
+ }).map((part) => contentToText(asRecord2(part).text)).filter(Boolean).join("");
727
+ }
728
+ function responseUsage(usage) {
729
+ const record = asRecord2(usage);
730
+ if (Object.keys(record).length === 0) {
731
+ return null;
732
+ }
733
+ return removeUndefined({
734
+ input_tokens: record.prompt_tokens,
735
+ input_tokens_details: record.prompt_tokens_details,
736
+ output_tokens: record.completion_tokens,
737
+ output_tokens_details: record.completion_tokens_details,
738
+ total_tokens: record.total_tokens
739
+ });
740
+ }
741
+ function firstChoice(completion) {
742
+ const choices = Array.isArray(completion.choices) ? completion.choices : [];
743
+ return asRecord2(choices[0]);
744
+ }
745
+ function processChatSseLine(line, enqueue, tools, appendText) {
746
+ const trimmed = line.trim();
747
+ if (!trimmed.startsWith("data:")) {
748
+ return;
749
+ }
750
+ const data = trimmed.slice("data:".length).trim();
751
+ if (!data || data === "[DONE]") {
752
+ return;
753
+ }
754
+ const parsed = parseJson(data);
755
+ if (!parsed) {
756
+ return;
757
+ }
758
+ const choice = firstChoice(parsed);
759
+ const delta = asRecord2(choice.delta);
760
+ const content = contentToText(delta.content);
761
+ if (content) {
762
+ appendText(content);
763
+ enqueue("response.output_text.delta", {
764
+ content_index: 0,
765
+ delta: content,
766
+ item_id: "",
767
+ output_index: 0,
768
+ type: "response.output_text.delta"
769
+ });
770
+ }
771
+ const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
772
+ for (const toolCall of toolCalls) {
773
+ const record = asRecord2(toolCall);
774
+ const fn = asRecord2(record.function);
775
+ const index = typeof record.index === "number" ? record.index : tools.size;
776
+ const existing = tools.get(index) ?? {
777
+ arguments: "",
778
+ id: contentToText(record.id) || `call_${randomId()}`,
779
+ index,
780
+ name: ""
781
+ };
782
+ existing.id = contentToText(record.id) || existing.id;
783
+ existing.name += contentToText(fn.name);
784
+ existing.arguments += contentToText(fn.arguments);
785
+ tools.set(index, existing);
786
+ }
787
+ }
788
+ function streamOutputItems(messageId, text, tools) {
789
+ return [messageOutputItem(text, messageId), ...tools.map((tool) => functionCallItem(tool))];
790
+ }
791
+ function baseStreamResponse(id, model, createdAt, status, output) {
792
+ return {
793
+ created_at: createdAt,
794
+ error: null,
795
+ id,
796
+ incomplete_details: null,
797
+ instructions: null,
798
+ max_output_tokens: null,
799
+ metadata: {},
800
+ model,
801
+ object: "response",
802
+ output,
803
+ parallel_tool_calls: true,
804
+ status,
805
+ temperature: null,
806
+ tool_choice: "auto",
807
+ tools: [],
808
+ top_p: null
809
+ };
810
+ }
811
+ function encodeSse(event, data) {
812
+ if (data === "[DONE]") {
813
+ return "data: [DONE]\n\n";
814
+ }
815
+ return `event: ${event}
816
+ data: ${JSON.stringify(data)}
817
+
818
+ `;
819
+ }
820
+ function parseJson(data) {
821
+ try {
822
+ return asRecord2(JSON.parse(data));
823
+ } catch {
824
+ return void 0;
825
+ }
826
+ }
827
+ function removeUndefined(record) {
828
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
829
+ }
830
+ function asRecord2(value) {
831
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
832
+ }
833
+ function randomId() {
834
+ return crypto.randomUUID().replaceAll("-", "");
835
+ }
836
+ function epochSeconds() {
837
+ return Math.floor(Date.now() / 1e3);
838
+ }
839
+
840
+ // src/server.ts
841
+ var DEFAULT_HOST = "127.0.0.1";
842
+ var DEFAULT_PORT = 4141;
843
+ function createHoopilotHandler(options = {}) {
844
+ const client = new CopilotClient(options);
845
+ const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
846
+ return async (request) => {
847
+ const url = new URL(request.url);
848
+ if (request.method === "OPTIONS") {
849
+ return new Response(null, { headers: corsHeaders() });
850
+ }
851
+ if (!isAuthorized(request, apiKey)) {
852
+ return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
853
+ }
854
+ try {
855
+ if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/healthz")) {
856
+ return jsonResponse({
857
+ name: "hoopilot",
858
+ object: "health",
859
+ status: "ok"
860
+ });
861
+ }
862
+ if (request.method === "GET" && url.pathname === "/v1/models") {
863
+ return await handleModels(client, request.signal);
864
+ }
865
+ if (request.method === "POST" && url.pathname === "/v1/chat/completions") {
866
+ return await handleChatCompletions(client, request);
867
+ }
868
+ if (request.method === "POST" && url.pathname === "/v1/completions") {
869
+ return await handleCompletions(client, request);
870
+ }
871
+ if (request.method === "POST" && url.pathname === "/v1/responses") {
872
+ return await handleResponses(client, request);
873
+ }
874
+ return jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`);
875
+ } catch (error) {
876
+ if (error instanceof CopilotAuthError) {
877
+ return jsonError(401, "copilot_auth_error", error.message);
878
+ }
879
+ return jsonError(500, "internal_error", errorMessage2(error));
880
+ }
881
+ };
882
+ }
883
+ function startHoopilotServer(options = {}) {
884
+ const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
885
+ const port = Number(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
886
+ const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
887
+ const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
888
+ if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
889
+ throw new Error(
890
+ "Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
891
+ );
892
+ }
893
+ const server = Bun.serve({
894
+ fetch: createHoopilotHandler({
895
+ ...options,
896
+ apiKey,
897
+ host,
898
+ port
899
+ }),
900
+ hostname: host,
901
+ port
902
+ });
903
+ return {
904
+ server,
905
+ url: `http://${host}:${server.port}`
906
+ };
907
+ }
908
+ async function handleModels(client, signal) {
909
+ const upstream = await client.models(signal);
910
+ if (!upstream.ok) {
911
+ return jsonResponse({ data: fallbackModels(), object: "list" });
912
+ }
913
+ return jsonResponse(normalizeModelsResponse(await upstream.json()));
914
+ }
915
+ async function handleChatCompletions(client, request) {
916
+ const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
917
+ return proxyResponse(upstream);
918
+ }
919
+ async function handleCompletions(client, request) {
920
+ const body = await readJson(request);
921
+ const upstream = await client.chatCompletions(
922
+ completionsRequestToChatCompletion(body),
923
+ request.signal
924
+ );
925
+ if (!upstream.ok) {
926
+ return proxyError(upstream);
927
+ }
928
+ return jsonResponse(chatCompletionToCompletion(await upstream.json()));
929
+ }
930
+ async function handleResponses(client, request) {
931
+ const body = await readJson(request);
932
+ const chatRequest = responsesRequestToChatCompletion(body);
933
+ const upstream = await client.chatCompletions(chatRequest, request.signal);
934
+ if (!upstream.ok) {
935
+ return proxyError(upstream);
936
+ }
937
+ if (body.stream === true && upstream.body) {
938
+ return new Response(
939
+ responsesStreamFromChatStream(upstream.body, {
940
+ model: typeof chatRequest.model === "string" ? chatRequest.model : "gpt-4.1"
941
+ }),
942
+ {
943
+ headers: {
944
+ ...corsHeaders(),
945
+ "cache-control": "no-cache",
946
+ connection: "keep-alive",
947
+ "content-type": "text/event-stream; charset=utf-8"
948
+ }
949
+ }
950
+ );
951
+ }
952
+ return jsonResponse(chatCompletionToResponse(await upstream.json()));
953
+ }
954
+ async function proxyError(upstream) {
955
+ const text = await upstream.text();
956
+ return jsonError(upstream.status, "copilot_error", text || upstream.statusText);
957
+ }
958
+ function proxyResponse(upstream) {
959
+ const headers = new Headers(upstream.headers);
960
+ headers.delete("content-encoding");
961
+ headers.delete("content-length");
962
+ headers.delete("transfer-encoding");
963
+ for (const [key, value] of Object.entries(corsHeaders())) {
964
+ headers.set(key, value);
965
+ }
966
+ return new Response(upstream.body, {
967
+ headers,
968
+ status: upstream.status,
969
+ statusText: upstream.statusText
970
+ });
971
+ }
972
+ async function readJson(request) {
973
+ try {
974
+ const value = await request.json();
975
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
976
+ } catch {
977
+ throw new Error("Request body must be valid JSON.");
978
+ }
979
+ }
980
+ function jsonResponse(body, status = 200) {
981
+ return new Response(JSON.stringify(body), {
982
+ headers: {
983
+ ...corsHeaders(),
984
+ "content-type": "application/json; charset=utf-8"
985
+ },
986
+ status
987
+ });
988
+ }
989
+ function jsonError(status, code, message) {
990
+ return jsonResponse(
991
+ {
992
+ error: {
993
+ code,
994
+ message,
995
+ type: code
996
+ }
997
+ },
998
+ status
999
+ );
1000
+ }
1001
+ function corsHeaders() {
1002
+ return {
1003
+ "access-control-allow-headers": "authorization, content-type, x-api-key",
1004
+ "access-control-allow-methods": "GET, POST, OPTIONS",
1005
+ "access-control-allow-origin": "*"
1006
+ };
1007
+ }
1008
+ function isAuthorized(request, apiKey) {
1009
+ if (!apiKey) {
1010
+ return true;
1011
+ }
1012
+ const authorization = request.headers.get("authorization") ?? "";
1013
+ const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
1014
+ return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
1015
+ }
1016
+ function isLoopbackHost(host) {
1017
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
1018
+ }
1019
+ function errorMessage2(error) {
1020
+ return error instanceof Error ? error.message : String(error);
1021
+ }
1022
+ export {
1023
+ CopilotAuth,
1024
+ CopilotAuthError,
1025
+ CopilotClient,
1026
+ DEFAULT_MODEL,
1027
+ chatCompletionToCompletion,
1028
+ chatCompletionToResponse,
1029
+ completionsRequestToChatCompletion,
1030
+ createHoopilotHandler,
1031
+ fallbackModels,
1032
+ normalizeModelsResponse,
1033
+ responsesRequestToChatCompletion,
1034
+ responsesStreamFromChatStream,
1035
+ splitCommand,
1036
+ startHoopilotServer
1037
+ };
1038
+ //# sourceMappingURL=index.js.map