@hsupu/copilot-api 0.7.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/main.js ADDED
@@ -0,0 +1,3271 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from "citty";
3
+ import consola from "consola";
4
+ import fs from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ import clipboard from "clipboardy";
9
+ import { serve } from "srvx";
10
+ import invariant from "tiny-invariant";
11
+ import { getProxyForUrl } from "proxy-from-env";
12
+ import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
13
+ import { execSync } from "node:child_process";
14
+ import process$1 from "node:process";
15
+ import { Hono } from "hono";
16
+ import { cors } from "hono/cors";
17
+ import { logger } from "hono/logger";
18
+ import { streamSSE } from "hono/streaming";
19
+ import { events } from "fetch-event-stream";
20
+
21
+ //#region src/lib/paths.ts
22
+ const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
23
+ const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
24
+ const PATHS = {
25
+ APP_DIR,
26
+ GITHUB_TOKEN_PATH
27
+ };
28
+ async function ensurePaths() {
29
+ await fs.mkdir(PATHS.APP_DIR, { recursive: true });
30
+ await ensureFile(PATHS.GITHUB_TOKEN_PATH);
31
+ }
32
+ async function ensureFile(filePath) {
33
+ try {
34
+ await fs.access(filePath, fs.constants.W_OK);
35
+ if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
36
+ } catch {
37
+ await fs.writeFile(filePath, "");
38
+ await fs.chmod(filePath, 384);
39
+ }
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/lib/state.ts
44
+ const state = {
45
+ accountType: "individual",
46
+ manualApprove: false,
47
+ rateLimitWait: false,
48
+ showToken: false
49
+ };
50
+
51
+ //#endregion
52
+ //#region src/lib/api-config.ts
53
+ const standardHeaders = () => ({
54
+ "content-type": "application/json",
55
+ accept: "application/json"
56
+ });
57
+ const COPILOT_VERSION = "0.26.7";
58
+ const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
59
+ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
60
+ const API_VERSION = "2025-04-01";
61
+ const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
62
+ const copilotHeaders = (state$1, vision = false) => {
63
+ const headers = {
64
+ Authorization: `Bearer ${state$1.copilotToken}`,
65
+ "content-type": standardHeaders()["content-type"],
66
+ "copilot-integration-id": "vscode-chat",
67
+ "editor-version": `vscode/${state$1.vsCodeVersion}`,
68
+ "editor-plugin-version": EDITOR_PLUGIN_VERSION,
69
+ "user-agent": USER_AGENT,
70
+ "openai-intent": "conversation-panel",
71
+ "x-github-api-version": API_VERSION,
72
+ "x-request-id": randomUUID(),
73
+ "x-vscode-user-agent-library-version": "electron-fetch"
74
+ };
75
+ if (vision) headers["copilot-vision-request"] = "true";
76
+ return headers;
77
+ };
78
+ const GITHUB_API_BASE_URL = "https://api.github.com";
79
+ const githubHeaders = (state$1) => ({
80
+ ...standardHeaders(),
81
+ authorization: `token ${state$1.githubToken}`,
82
+ "editor-version": `vscode/${state$1.vsCodeVersion}`,
83
+ "editor-plugin-version": EDITOR_PLUGIN_VERSION,
84
+ "user-agent": USER_AGENT,
85
+ "x-github-api-version": API_VERSION,
86
+ "x-vscode-user-agent-library-version": "electron-fetch"
87
+ });
88
+ const GITHUB_BASE_URL = "https://github.com";
89
+ const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
90
+ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
91
+
92
+ //#endregion
93
+ //#region src/lib/error.ts
94
+ var HTTPError = class HTTPError extends Error {
95
+ status;
96
+ responseText;
97
+ constructor(message, status, responseText) {
98
+ super(message);
99
+ this.status = status;
100
+ this.responseText = responseText;
101
+ }
102
+ static async fromResponse(message, response) {
103
+ const text = await response.text();
104
+ return new HTTPError(message, response.status, text);
105
+ }
106
+ };
107
+ async function forwardError(c, error) {
108
+ consola.error("Error occurred:", error);
109
+ if (error instanceof HTTPError) {
110
+ let errorJson;
111
+ try {
112
+ errorJson = JSON.parse(error.responseText);
113
+ } catch {
114
+ errorJson = error.responseText;
115
+ }
116
+ consola.error("HTTP error:", errorJson);
117
+ return c.json({ error: {
118
+ message: error.responseText,
119
+ type: "error"
120
+ } }, error.status);
121
+ }
122
+ return c.json({ error: {
123
+ message: error.message,
124
+ type: "error"
125
+ } }, 500);
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/services/github/get-copilot-token.ts
130
+ const getCopilotToken = async () => {
131
+ const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) });
132
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot token", response);
133
+ return await response.json();
134
+ };
135
+
136
+ //#endregion
137
+ //#region src/services/github/get-device-code.ts
138
+ async function getDeviceCode() {
139
+ const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
140
+ method: "POST",
141
+ headers: standardHeaders(),
142
+ body: JSON.stringify({
143
+ client_id: GITHUB_CLIENT_ID,
144
+ scope: GITHUB_APP_SCOPES
145
+ })
146
+ });
147
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to get device code", response);
148
+ return await response.json();
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/services/github/get-user.ts
153
+ async function getGitHubUser() {
154
+ const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
155
+ authorization: `token ${state.githubToken}`,
156
+ ...standardHeaders()
157
+ } });
158
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to get GitHub user", response);
159
+ return await response.json();
160
+ }
161
+
162
+ //#endregion
163
+ //#region src/services/copilot/get-models.ts
164
+ const getModels = async () => {
165
+ const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) });
166
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to get models", response);
167
+ return await response.json();
168
+ };
169
+
170
+ //#endregion
171
+ //#region src/services/get-vscode-version.ts
172
+ const FALLBACK = "1.104.3";
173
+ const GITHUB_API_URL = "https://api.github.com/repos/microsoft/vscode/releases/latest";
174
+ async function getVSCodeVersion() {
175
+ const controller = new AbortController();
176
+ const timeout = setTimeout(() => {
177
+ controller.abort();
178
+ }, 5e3);
179
+ try {
180
+ const response = await fetch(GITHUB_API_URL, {
181
+ signal: controller.signal,
182
+ headers: {
183
+ Accept: "application/vnd.github.v3+json",
184
+ "User-Agent": "copilot-api"
185
+ }
186
+ });
187
+ if (!response.ok) return FALLBACK;
188
+ const version = (await response.json()).tag_name;
189
+ if (version && /^\d+\.\d+\.\d+$/.test(version)) return version;
190
+ return FALLBACK;
191
+ } catch {
192
+ return FALLBACK;
193
+ } finally {
194
+ clearTimeout(timeout);
195
+ }
196
+ }
197
+
198
+ //#endregion
199
+ //#region src/lib/utils.ts
200
+ const sleep = (ms) => new Promise((resolve) => {
201
+ setTimeout(resolve, ms);
202
+ });
203
+ const isNullish = (value) => value === null || value === void 0;
204
+ async function cacheModels() {
205
+ state.models = await getModels();
206
+ }
207
+ const cacheVSCodeVersion = async () => {
208
+ const response = await getVSCodeVersion();
209
+ state.vsCodeVersion = response;
210
+ consola.info(`Using VSCode version: ${response}`);
211
+ };
212
+
213
+ //#endregion
214
+ //#region src/services/github/poll-access-token.ts
215
+ async function pollAccessToken(deviceCode) {
216
+ const sleepDuration = (deviceCode.interval + 1) * 1e3;
217
+ consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
218
+ const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
219
+ while (Date.now() < expiresAt) {
220
+ const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
221
+ method: "POST",
222
+ headers: standardHeaders(),
223
+ body: JSON.stringify({
224
+ client_id: GITHUB_CLIENT_ID,
225
+ device_code: deviceCode.device_code,
226
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
227
+ })
228
+ });
229
+ if (!response.ok) {
230
+ await sleep(sleepDuration);
231
+ consola.error("Failed to poll access token:", await response.text());
232
+ continue;
233
+ }
234
+ const json = await response.json();
235
+ consola.debug("Polling access token response:", json);
236
+ const { access_token } = json;
237
+ if (access_token) return access_token;
238
+ else await sleep(sleepDuration);
239
+ }
240
+ throw new Error("Device code expired. Please run the authentication flow again.");
241
+ }
242
+
243
+ //#endregion
244
+ //#region src/lib/token.ts
245
+ const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
246
+ const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
247
+ const setupCopilotToken = async () => {
248
+ const { token, refresh_in } = await getCopilotToken();
249
+ state.copilotToken = token;
250
+ consola.debug("GitHub Copilot Token fetched successfully!");
251
+ if (state.showToken) consola.info("Copilot token:", token);
252
+ const refreshInterval = (refresh_in - 60) * 1e3;
253
+ setInterval(async () => {
254
+ consola.debug("Refreshing Copilot token");
255
+ try {
256
+ const { token: token$1 } = await getCopilotToken();
257
+ state.copilotToken = token$1;
258
+ consola.debug("Copilot token refreshed");
259
+ if (state.showToken) consola.info("Refreshed Copilot token:", token$1);
260
+ } catch (error) {
261
+ consola.error("Failed to refresh Copilot token (will retry on next interval):", error);
262
+ }
263
+ }, refreshInterval);
264
+ };
265
+ async function setupGitHubToken(options) {
266
+ try {
267
+ const githubToken = await readGithubToken();
268
+ if (githubToken && !options?.force) {
269
+ state.githubToken = githubToken;
270
+ if (state.showToken) consola.info("GitHub token:", githubToken);
271
+ await logUser();
272
+ return;
273
+ }
274
+ consola.info("Not logged in, getting new access token");
275
+ const response = await getDeviceCode();
276
+ consola.debug("Device code response:", response);
277
+ consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
278
+ const token = await pollAccessToken(response);
279
+ await writeGithubToken(token);
280
+ state.githubToken = token;
281
+ if (state.showToken) consola.info("GitHub token:", token);
282
+ await logUser();
283
+ } catch (error) {
284
+ if (error instanceof HTTPError) {
285
+ consola.error("Failed to get GitHub token:", error.responseText);
286
+ throw error;
287
+ }
288
+ consola.error("Failed to get GitHub token:", error);
289
+ throw error;
290
+ }
291
+ }
292
+ async function logUser() {
293
+ const user = await getGitHubUser();
294
+ consola.info(`Logged in as ${user.login}`);
295
+ }
296
+
297
+ //#endregion
298
+ //#region src/auth.ts
299
+ async function runAuth(options) {
300
+ if (options.verbose) {
301
+ consola.level = 5;
302
+ consola.info("Verbose logging enabled");
303
+ }
304
+ state.showToken = options.showToken;
305
+ await ensurePaths();
306
+ await setupGitHubToken({ force: true });
307
+ consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
308
+ }
309
+ const auth = defineCommand({
310
+ meta: {
311
+ name: "auth",
312
+ description: "Run GitHub auth flow without running the server"
313
+ },
314
+ args: {
315
+ verbose: {
316
+ alias: "v",
317
+ type: "boolean",
318
+ default: false,
319
+ description: "Enable verbose logging"
320
+ },
321
+ "show-token": {
322
+ type: "boolean",
323
+ default: false,
324
+ description: "Show GitHub token on auth"
325
+ }
326
+ },
327
+ run({ args }) {
328
+ return runAuth({
329
+ verbose: args.verbose,
330
+ showToken: args["show-token"]
331
+ });
332
+ }
333
+ });
334
+
335
+ //#endregion
336
+ //#region src/services/github/get-copilot-usage.ts
337
+ const getCopilotUsage = async () => {
338
+ const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) });
339
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot usage", response);
340
+ return await response.json();
341
+ };
342
+
343
+ //#endregion
344
+ //#region src/check-usage.ts
345
+ const checkUsage = defineCommand({
346
+ meta: {
347
+ name: "check-usage",
348
+ description: "Show current GitHub Copilot usage/quota information"
349
+ },
350
+ async run() {
351
+ await ensurePaths();
352
+ await setupGitHubToken();
353
+ try {
354
+ const usage = await getCopilotUsage();
355
+ const premium = usage.quota_snapshots.premium_interactions;
356
+ const premiumTotal = premium.entitlement;
357
+ const premiumUsed = premiumTotal - premium.remaining;
358
+ const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
359
+ const premiumPercentRemaining = premium.percent_remaining;
360
+ function summarizeQuota(name, snap) {
361
+ if (!snap) return `${name}: N/A`;
362
+ const total = snap.entitlement;
363
+ const used = total - snap.remaining;
364
+ const percentUsed = total > 0 ? used / total * 100 : 0;
365
+ const percentRemaining = snap.percent_remaining;
366
+ return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
367
+ }
368
+ const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
369
+ const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
370
+ const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
371
+ consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
372
+ } catch (err) {
373
+ consola.error("Failed to fetch Copilot usage:", err);
374
+ process.exit(1);
375
+ }
376
+ }
377
+ });
378
+
379
+ //#endregion
380
+ //#region src/debug.ts
381
+ async function getPackageVersion() {
382
+ try {
383
+ const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
384
+ return JSON.parse(await fs.readFile(packageJsonPath)).version;
385
+ } catch {
386
+ return "unknown";
387
+ }
388
+ }
389
+ function getRuntimeInfo() {
390
+ const isBun = typeof Bun !== "undefined";
391
+ return {
392
+ name: isBun ? "bun" : "node",
393
+ version: isBun ? Bun.version : process.version.slice(1),
394
+ platform: os.platform(),
395
+ arch: os.arch()
396
+ };
397
+ }
398
+ async function checkTokenExists() {
399
+ try {
400
+ if (!(await fs.stat(PATHS.GITHUB_TOKEN_PATH)).isFile()) return false;
401
+ return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim().length > 0;
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
406
+ async function getDebugInfo() {
407
+ const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
408
+ return {
409
+ version,
410
+ runtime: getRuntimeInfo(),
411
+ paths: {
412
+ APP_DIR: PATHS.APP_DIR,
413
+ GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH
414
+ },
415
+ tokenExists
416
+ };
417
+ }
418
+ function printDebugInfoPlain(info) {
419
+ consola.info(`copilot-api debug
420
+
421
+ Version: ${info.version}
422
+ Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
423
+
424
+ Paths:
425
+ - APP_DIR: ${info.paths.APP_DIR}
426
+ - GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}
427
+
428
+ Token exists: ${info.tokenExists ? "Yes" : "No"}`);
429
+ }
430
+ function printDebugInfoJson(info) {
431
+ console.log(JSON.stringify(info, null, 2));
432
+ }
433
+ async function runDebug(options) {
434
+ const debugInfo = await getDebugInfo();
435
+ if (options.json) printDebugInfoJson(debugInfo);
436
+ else printDebugInfoPlain(debugInfo);
437
+ }
438
+ const debug = defineCommand({
439
+ meta: {
440
+ name: "debug",
441
+ description: "Print debug information about the application"
442
+ },
443
+ args: { json: {
444
+ type: "boolean",
445
+ default: false,
446
+ description: "Output debug information as JSON"
447
+ } },
448
+ run({ args }) {
449
+ return runDebug({ json: args.json });
450
+ }
451
+ });
452
+
453
+ //#endregion
454
+ //#region src/logout.ts
455
+ async function runLogout() {
456
+ try {
457
+ await fs.unlink(PATHS.GITHUB_TOKEN_PATH);
458
+ consola.success("Logged out successfully. GitHub token removed.");
459
+ } catch (error) {
460
+ if (error.code === "ENOENT") consola.info("No token found. Already logged out.");
461
+ else {
462
+ consola.error("Failed to remove token:", error);
463
+ throw error;
464
+ }
465
+ }
466
+ }
467
+ const logout = defineCommand({
468
+ meta: {
469
+ name: "logout",
470
+ description: "Remove stored GitHub token and log out"
471
+ },
472
+ run() {
473
+ return runLogout();
474
+ }
475
+ });
476
+
477
+ //#endregion
478
+ //#region src/lib/history.ts
479
+ function generateId() {
480
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
481
+ }
482
+ const historyState = {
483
+ enabled: false,
484
+ entries: [],
485
+ sessions: /* @__PURE__ */ new Map(),
486
+ currentSessionId: "",
487
+ maxEntries: 1e3,
488
+ sessionTimeoutMs: 1800 * 1e3
489
+ };
490
+ function initHistory(enabled, maxEntries) {
491
+ historyState.enabled = enabled;
492
+ historyState.maxEntries = maxEntries;
493
+ historyState.entries = [];
494
+ historyState.sessions = /* @__PURE__ */ new Map();
495
+ historyState.currentSessionId = enabled ? generateId() : "";
496
+ }
497
+ function isHistoryEnabled() {
498
+ return historyState.enabled;
499
+ }
500
+ function getCurrentSession(endpoint) {
501
+ const now = Date.now();
502
+ if (historyState.currentSessionId) {
503
+ const session = historyState.sessions.get(historyState.currentSessionId);
504
+ if (session && now - session.lastActivity < historyState.sessionTimeoutMs) {
505
+ session.lastActivity = now;
506
+ return historyState.currentSessionId;
507
+ }
508
+ }
509
+ const sessionId = generateId();
510
+ historyState.currentSessionId = sessionId;
511
+ historyState.sessions.set(sessionId, {
512
+ id: sessionId,
513
+ startTime: now,
514
+ lastActivity: now,
515
+ requestCount: 0,
516
+ totalInputTokens: 0,
517
+ totalOutputTokens: 0,
518
+ models: [],
519
+ endpoint
520
+ });
521
+ return sessionId;
522
+ }
523
+ function recordRequest(endpoint, request) {
524
+ if (!historyState.enabled) return "";
525
+ const sessionId = getCurrentSession(endpoint);
526
+ const session = historyState.sessions.get(sessionId);
527
+ if (!session) return "";
528
+ const entry = {
529
+ id: generateId(),
530
+ sessionId,
531
+ timestamp: Date.now(),
532
+ endpoint,
533
+ request: {
534
+ model: request.model,
535
+ messages: request.messages,
536
+ stream: request.stream,
537
+ tools: request.tools,
538
+ max_tokens: request.max_tokens,
539
+ temperature: request.temperature,
540
+ system: request.system
541
+ }
542
+ };
543
+ historyState.entries.push(entry);
544
+ session.requestCount++;
545
+ if (!session.models.includes(request.model)) session.models.push(request.model);
546
+ while (historyState.entries.length > historyState.maxEntries) {
547
+ const removed = historyState.entries.shift();
548
+ if (removed) {
549
+ if (historyState.entries.filter((e) => e.sessionId === removed.sessionId).length === 0) historyState.sessions.delete(removed.sessionId);
550
+ }
551
+ }
552
+ return entry.id;
553
+ }
554
+ function recordResponse(id, response, durationMs) {
555
+ if (!historyState.enabled || !id) return;
556
+ const entry = historyState.entries.find((e) => e.id === id);
557
+ if (entry) {
558
+ entry.response = response;
559
+ entry.durationMs = durationMs;
560
+ const session = historyState.sessions.get(entry.sessionId);
561
+ if (session) {
562
+ session.totalInputTokens += response.usage.input_tokens;
563
+ session.totalOutputTokens += response.usage.output_tokens;
564
+ session.lastActivity = Date.now();
565
+ }
566
+ }
567
+ }
568
+ function getHistory(options = {}) {
569
+ const { page = 1, limit = 50, model, endpoint, success, from, to, search, sessionId } = options;
570
+ let filtered = [...historyState.entries];
571
+ if (sessionId) filtered = filtered.filter((e) => e.sessionId === sessionId);
572
+ if (model) {
573
+ const modelLower = model.toLowerCase();
574
+ filtered = filtered.filter((e) => e.request.model.toLowerCase().includes(modelLower) || e.response?.model.toLowerCase().includes(modelLower));
575
+ }
576
+ if (endpoint) filtered = filtered.filter((e) => e.endpoint === endpoint);
577
+ if (success !== void 0) filtered = filtered.filter((e) => e.response?.success === success);
578
+ if (from) filtered = filtered.filter((e) => e.timestamp >= from);
579
+ if (to) filtered = filtered.filter((e) => e.timestamp <= to);
580
+ if (search) {
581
+ const searchLower = search.toLowerCase();
582
+ filtered = filtered.filter((e) => {
583
+ const msgMatch = e.request.messages.some((m) => {
584
+ if (typeof m.content === "string") return m.content.toLowerCase().includes(searchLower);
585
+ if (Array.isArray(m.content)) return m.content.some((c) => c.text && c.text.toLowerCase().includes(searchLower));
586
+ return false;
587
+ });
588
+ const respMatch = e.response?.content && typeof e.response.content.content === "string" && e.response.content.content.toLowerCase().includes(searchLower);
589
+ const toolMatch = e.response?.toolCalls?.some((t) => t.name.toLowerCase().includes(searchLower));
590
+ const sysMatch = e.request.system?.toLowerCase().includes(searchLower);
591
+ return msgMatch || respMatch || toolMatch || sysMatch;
592
+ });
593
+ }
594
+ filtered.sort((a, b) => b.timestamp - a.timestamp);
595
+ const total = filtered.length;
596
+ const totalPages = Math.ceil(total / limit);
597
+ const start$1 = (page - 1) * limit;
598
+ return {
599
+ entries: filtered.slice(start$1, start$1 + limit),
600
+ total,
601
+ page,
602
+ limit,
603
+ totalPages
604
+ };
605
+ }
606
+ function getEntry(id) {
607
+ return historyState.entries.find((e) => e.id === id);
608
+ }
609
+ function getSessions() {
610
+ const sessions = Array.from(historyState.sessions.values()).sort((a, b) => b.lastActivity - a.lastActivity);
611
+ return {
612
+ sessions,
613
+ total: sessions.length
614
+ };
615
+ }
616
+ function getSession(id) {
617
+ return historyState.sessions.get(id);
618
+ }
619
+ function getSessionEntries(sessionId) {
620
+ return historyState.entries.filter((e) => e.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
621
+ }
622
+ function clearHistory() {
623
+ historyState.entries = [];
624
+ historyState.sessions = /* @__PURE__ */ new Map();
625
+ historyState.currentSessionId = generateId();
626
+ }
627
+ function deleteSession(sessionId) {
628
+ if (!historyState.sessions.has(sessionId)) return false;
629
+ historyState.entries = historyState.entries.filter((e) => e.sessionId !== sessionId);
630
+ historyState.sessions.delete(sessionId);
631
+ if (historyState.currentSessionId === sessionId) historyState.currentSessionId = generateId();
632
+ return true;
633
+ }
634
+ function getStats() {
635
+ const entries = historyState.entries;
636
+ const modelDist = {};
637
+ const endpointDist = {};
638
+ const hourlyActivity = {};
639
+ let totalInput = 0;
640
+ let totalOutput = 0;
641
+ let totalDuration = 0;
642
+ let durationCount = 0;
643
+ let successCount = 0;
644
+ let failCount = 0;
645
+ for (const entry of entries) {
646
+ const model = entry.response?.model || entry.request.model;
647
+ modelDist[model] = (modelDist[model] || 0) + 1;
648
+ endpointDist[entry.endpoint] = (endpointDist[entry.endpoint] || 0) + 1;
649
+ const hour = new Date(entry.timestamp).toISOString().slice(0, 13);
650
+ hourlyActivity[hour] = (hourlyActivity[hour] || 0) + 1;
651
+ if (entry.response) {
652
+ if (entry.response.success) successCount++;
653
+ else failCount++;
654
+ totalInput += entry.response.usage.input_tokens;
655
+ totalOutput += entry.response.usage.output_tokens;
656
+ }
657
+ if (entry.durationMs) {
658
+ totalDuration += entry.durationMs;
659
+ durationCount++;
660
+ }
661
+ }
662
+ const recentActivity = Object.entries(hourlyActivity).sort(([a], [b]) => a.localeCompare(b)).slice(-24).map(([hour, count]) => ({
663
+ hour,
664
+ count
665
+ }));
666
+ const now = Date.now();
667
+ let activeSessions = 0;
668
+ for (const session of historyState.sessions.values()) if (now - session.lastActivity < historyState.sessionTimeoutMs) activeSessions++;
669
+ return {
670
+ totalRequests: entries.length,
671
+ successfulRequests: successCount,
672
+ failedRequests: failCount,
673
+ totalInputTokens: totalInput,
674
+ totalOutputTokens: totalOutput,
675
+ averageDurationMs: durationCount > 0 ? totalDuration / durationCount : 0,
676
+ modelDistribution: modelDist,
677
+ endpointDistribution: endpointDist,
678
+ recentActivity,
679
+ activeSessions
680
+ };
681
+ }
682
+ function exportHistory(format = "json") {
683
+ if (format === "json") return JSON.stringify({
684
+ sessions: Array.from(historyState.sessions.values()),
685
+ entries: historyState.entries
686
+ }, null, 2);
687
+ const headers = [
688
+ "id",
689
+ "session_id",
690
+ "timestamp",
691
+ "endpoint",
692
+ "request_model",
693
+ "message_count",
694
+ "stream",
695
+ "success",
696
+ "response_model",
697
+ "input_tokens",
698
+ "output_tokens",
699
+ "duration_ms",
700
+ "stop_reason",
701
+ "error"
702
+ ];
703
+ const rows = historyState.entries.map((e) => [
704
+ e.id,
705
+ e.sessionId,
706
+ new Date(e.timestamp).toISOString(),
707
+ e.endpoint,
708
+ e.request.model,
709
+ e.request.messages.length,
710
+ e.request.stream,
711
+ e.response?.success ?? "",
712
+ e.response?.model ?? "",
713
+ e.response?.usage.input_tokens ?? "",
714
+ e.response?.usage.output_tokens ?? "",
715
+ e.durationMs ?? "",
716
+ e.response?.stop_reason ?? "",
717
+ e.response?.error ?? ""
718
+ ]);
719
+ return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
720
+ }
721
+
722
+ //#endregion
723
+ //#region src/lib/proxy.ts
724
+ function initProxyFromEnv() {
725
+ if (typeof Bun !== "undefined") return;
726
+ try {
727
+ const direct = new Agent();
728
+ const proxies = /* @__PURE__ */ new Map();
729
+ setGlobalDispatcher({
730
+ dispatch(options, handler) {
731
+ try {
732
+ const origin = typeof options.origin === "string" ? new URL(options.origin) : options.origin;
733
+ const raw = getProxyForUrl(origin.toString());
734
+ const proxyUrl = raw && raw.length > 0 ? raw : void 0;
735
+ if (!proxyUrl) {
736
+ consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
737
+ return direct.dispatch(options, handler);
738
+ }
739
+ let agent = proxies.get(proxyUrl);
740
+ if (!agent) {
741
+ agent = new ProxyAgent(proxyUrl);
742
+ proxies.set(proxyUrl, agent);
743
+ }
744
+ let label = proxyUrl;
745
+ try {
746
+ const u = new URL(proxyUrl);
747
+ label = `${u.protocol}//${u.host}`;
748
+ } catch {}
749
+ consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`);
750
+ return agent.dispatch(options, handler);
751
+ } catch {
752
+ return direct.dispatch(options, handler);
753
+ }
754
+ },
755
+ close() {
756
+ return direct.close();
757
+ },
758
+ destroy() {
759
+ return direct.destroy();
760
+ }
761
+ });
762
+ consola.debug("HTTP proxy configured from environment (per-URL)");
763
+ } catch (err) {
764
+ consola.debug("Proxy setup skipped:", err);
765
+ }
766
+ }
767
+
768
+ //#endregion
769
+ //#region src/lib/shell.ts
770
+ function getShell() {
771
+ const { platform, ppid, env } = process$1;
772
+ if (platform === "win32") {
773
+ try {
774
+ const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
775
+ if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
776
+ } catch {
777
+ return "cmd";
778
+ }
779
+ return "cmd";
780
+ } else {
781
+ const shellPath = env.SHELL;
782
+ if (shellPath) {
783
+ if (shellPath.endsWith("zsh")) return "zsh";
784
+ if (shellPath.endsWith("fish")) return "fish";
785
+ if (shellPath.endsWith("bash")) return "bash";
786
+ }
787
+ return "sh";
788
+ }
789
+ }
790
+ /**
791
+ * Generates a copy-pasteable script to set multiple environment variables
792
+ * and run a subsequent command.
793
+ * @param {EnvVars} envVars - An object of environment variables to set.
794
+ * @param {string} commandToRun - The command to run after setting the variables.
795
+ * @returns {string} The formatted script string.
796
+ */
797
+ function generateEnvScript(envVars, commandToRun = "") {
798
+ const shell = getShell();
799
+ const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
800
+ let commandBlock;
801
+ switch (shell) {
802
+ case "powershell":
803
+ commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = "${value.replace(/"/g, "`\"")}"`).join("; ");
804
+ break;
805
+ case "cmd":
806
+ commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
807
+ break;
808
+ case "fish":
809
+ commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} "${value.replace(/"/g, "\\\"")}"`).join("; ");
810
+ break;
811
+ default: {
812
+ const assignments = filteredEnvVars.map(([key, value]) => `${key}="${value.replace(/"/g, "\\\"")}"`).join(" ");
813
+ commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
814
+ break;
815
+ }
816
+ }
817
+ if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : " && "}${commandToRun}`;
818
+ return commandBlock || commandToRun;
819
+ }
820
+
821
+ //#endregion
822
+ //#region src/lib/approval.ts
823
+ const awaitApproval = async () => {
824
+ if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
825
+ };
826
+
827
+ //#endregion
828
+ //#region src/lib/queue.ts
829
+ var RequestQueue = class {
830
+ queue = [];
831
+ processing = false;
832
+ lastRequestTime = 0;
833
+ async enqueue(execute, rateLimitSeconds) {
834
+ return new Promise((resolve, reject) => {
835
+ this.queue.push({
836
+ execute,
837
+ resolve,
838
+ reject
839
+ });
840
+ if (this.queue.length > 1) {
841
+ const waitTime = Math.ceil((this.queue.length - 1) * rateLimitSeconds);
842
+ consola.info(`Request queued. Position: ${this.queue.length}, estimated wait: ${waitTime}s`);
843
+ }
844
+ this.processQueue(rateLimitSeconds);
845
+ });
846
+ }
847
+ async processQueue(rateLimitSeconds) {
848
+ if (this.processing) return;
849
+ this.processing = true;
850
+ while (this.queue.length > 0) {
851
+ const elapsedMs = Date.now() - this.lastRequestTime;
852
+ const requiredMs = rateLimitSeconds * 1e3;
853
+ if (this.lastRequestTime > 0 && elapsedMs < requiredMs) {
854
+ const waitMs = requiredMs - elapsedMs;
855
+ consola.debug(`Rate limit: waiting ${Math.ceil(waitMs / 1e3)}s`);
856
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
857
+ }
858
+ const request = this.queue.shift();
859
+ if (!request) break;
860
+ this.lastRequestTime = Date.now();
861
+ try {
862
+ const result = await request.execute();
863
+ request.resolve(result);
864
+ } catch (error) {
865
+ request.reject(error);
866
+ }
867
+ }
868
+ this.processing = false;
869
+ }
870
+ get length() {
871
+ return this.queue.length;
872
+ }
873
+ };
874
+ const requestQueue = new RequestQueue();
875
+ /**
876
+ * Execute a request with rate limiting via queue.
877
+ * Requests are queued and processed sequentially at the configured rate.
878
+ */
879
+ async function executeWithRateLimit(state$1, execute) {
880
+ if (state$1.rateLimitSeconds === void 0) return execute();
881
+ return requestQueue.enqueue(execute, state$1.rateLimitSeconds);
882
+ }
883
+
884
+ //#endregion
885
+ //#region src/lib/tokenizer.ts
886
+ const ENCODING_MAP = {
887
+ o200k_base: () => import("gpt-tokenizer/encoding/o200k_base"),
888
+ cl100k_base: () => import("gpt-tokenizer/encoding/cl100k_base"),
889
+ p50k_base: () => import("gpt-tokenizer/encoding/p50k_base"),
890
+ p50k_edit: () => import("gpt-tokenizer/encoding/p50k_edit"),
891
+ r50k_base: () => import("gpt-tokenizer/encoding/r50k_base")
892
+ };
893
+ const encodingCache = /* @__PURE__ */ new Map();
894
+ /**
895
+ * Calculate tokens for tool calls
896
+ */
897
+ const calculateToolCallsTokens = (toolCalls, encoder, constants) => {
898
+ let tokens = 0;
899
+ for (const toolCall of toolCalls) {
900
+ tokens += constants.funcInit;
901
+ tokens += encoder.encode(JSON.stringify(toolCall)).length;
902
+ }
903
+ tokens += constants.funcEnd;
904
+ return tokens;
905
+ };
906
+ /**
907
+ * Calculate tokens for content parts
908
+ */
909
+ const calculateContentPartsTokens = (contentParts, encoder) => {
910
+ let tokens = 0;
911
+ for (const part of contentParts) if (part.type === "image_url") tokens += encoder.encode(part.image_url.url).length + 85;
912
+ else if (part.text) tokens += encoder.encode(part.text).length;
913
+ return tokens;
914
+ };
915
+ /**
916
+ * Calculate tokens for a single message
917
+ */
918
+ const calculateMessageTokens = (message, encoder, constants) => {
919
+ const tokensPerMessage = 3;
920
+ const tokensPerName = 1;
921
+ let tokens = tokensPerMessage;
922
+ for (const [key, value] of Object.entries(message)) {
923
+ if (typeof value === "string") tokens += encoder.encode(value).length;
924
+ if (key === "name") tokens += tokensPerName;
925
+ if (key === "tool_calls") tokens += calculateToolCallsTokens(value, encoder, constants);
926
+ if (key === "content" && Array.isArray(value)) tokens += calculateContentPartsTokens(value, encoder);
927
+ }
928
+ return tokens;
929
+ };
930
+ /**
931
+ * Calculate tokens using custom algorithm
932
+ */
933
+ const calculateTokens = (messages, encoder, constants) => {
934
+ if (messages.length === 0) return 0;
935
+ let numTokens = 0;
936
+ for (const message of messages) numTokens += calculateMessageTokens(message, encoder, constants);
937
+ numTokens += 3;
938
+ return numTokens;
939
+ };
940
+ /**
941
+ * Get the corresponding encoder module based on encoding type
942
+ */
943
+ const getEncodeChatFunction = async (encoding) => {
944
+ if (encodingCache.has(encoding)) {
945
+ const cached = encodingCache.get(encoding);
946
+ if (cached) return cached;
947
+ }
948
+ const supportedEncoding = encoding;
949
+ if (!(supportedEncoding in ENCODING_MAP)) {
950
+ const fallbackModule = await ENCODING_MAP.o200k_base();
951
+ encodingCache.set(encoding, fallbackModule);
952
+ return fallbackModule;
953
+ }
954
+ const encodingModule = await ENCODING_MAP[supportedEncoding]();
955
+ encodingCache.set(encoding, encodingModule);
956
+ return encodingModule;
957
+ };
958
+ /**
959
+ * Get tokenizer type from model information
960
+ */
961
+ const getTokenizerFromModel = (model) => {
962
+ return model.capabilities.tokenizer || "o200k_base";
963
+ };
964
+ /**
965
+ * Get model-specific constants for token calculation.
966
+ * These values are empirically determined based on OpenAI's function calling token overhead.
967
+ * - funcInit: Tokens for initializing a function definition
968
+ * - propInit: Tokens for initializing the properties section
969
+ * - propKey: Tokens per property key
970
+ * - enumInit: Token adjustment when enum is present (negative because type info is replaced)
971
+ * - enumItem: Tokens per enum value
972
+ * - funcEnd: Tokens for closing the function definition
973
+ */
974
+ const getModelConstants = (model) => {
975
+ return model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
976
+ funcInit: 10,
977
+ propInit: 3,
978
+ propKey: 3,
979
+ enumInit: -3,
980
+ enumItem: 3,
981
+ funcEnd: 12
982
+ } : {
983
+ funcInit: 7,
984
+ propInit: 3,
985
+ propKey: 3,
986
+ enumInit: -3,
987
+ enumItem: 3,
988
+ funcEnd: 12
989
+ };
990
+ };
991
+ /**
992
+ * Calculate tokens for a single parameter
993
+ */
994
+ const calculateParameterTokens = (key, prop, context) => {
995
+ const { encoder, constants } = context;
996
+ let tokens = constants.propKey;
997
+ if (typeof prop !== "object" || prop === null) return tokens;
998
+ const param = prop;
999
+ const paramName = key;
1000
+ const paramType = param.type || "string";
1001
+ let paramDesc = param.description || "";
1002
+ if (param.enum && Array.isArray(param.enum)) {
1003
+ tokens += constants.enumInit;
1004
+ for (const item of param.enum) {
1005
+ tokens += constants.enumItem;
1006
+ tokens += encoder.encode(String(item)).length;
1007
+ }
1008
+ }
1009
+ if (paramDesc.endsWith(".")) paramDesc = paramDesc.slice(0, -1);
1010
+ const line = `${paramName}:${paramType}:${paramDesc}`;
1011
+ tokens += encoder.encode(line).length;
1012
+ const excludedKeys = new Set([
1013
+ "type",
1014
+ "description",
1015
+ "enum"
1016
+ ]);
1017
+ for (const propertyName of Object.keys(param)) if (!excludedKeys.has(propertyName)) {
1018
+ const propertyValue = param[propertyName];
1019
+ const propertyText = typeof propertyValue === "string" ? propertyValue : JSON.stringify(propertyValue);
1020
+ tokens += encoder.encode(`${propertyName}:${propertyText}`).length;
1021
+ }
1022
+ return tokens;
1023
+ };
1024
+ /**
1025
+ * Calculate tokens for function parameters
1026
+ */
1027
+ const calculateParametersTokens = (parameters, encoder, constants) => {
1028
+ if (!parameters || typeof parameters !== "object") return 0;
1029
+ const params = parameters;
1030
+ let tokens = 0;
1031
+ for (const [key, value] of Object.entries(params)) if (key === "properties") {
1032
+ const properties = value;
1033
+ if (Object.keys(properties).length > 0) {
1034
+ tokens += constants.propInit;
1035
+ for (const propKey of Object.keys(properties)) tokens += calculateParameterTokens(propKey, properties[propKey], {
1036
+ encoder,
1037
+ constants
1038
+ });
1039
+ }
1040
+ } else {
1041
+ const paramText = typeof value === "string" ? value : JSON.stringify(value);
1042
+ tokens += encoder.encode(`${key}:${paramText}`).length;
1043
+ }
1044
+ return tokens;
1045
+ };
1046
+ /**
1047
+ * Calculate tokens for a single tool
1048
+ */
1049
+ const calculateToolTokens = (tool, encoder, constants) => {
1050
+ let tokens = constants.funcInit;
1051
+ const func = tool.function;
1052
+ const fName = func.name;
1053
+ let fDesc = func.description || "";
1054
+ if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
1055
+ const line = fName + ":" + fDesc;
1056
+ tokens += encoder.encode(line).length;
1057
+ if (typeof func.parameters === "object" && func.parameters !== null) tokens += calculateParametersTokens(func.parameters, encoder, constants);
1058
+ return tokens;
1059
+ };
1060
+ /**
1061
+ * Calculate token count for tools based on model
1062
+ */
1063
+ const numTokensForTools = (tools, encoder, constants) => {
1064
+ let funcTokenCount = 0;
1065
+ for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
1066
+ funcTokenCount += constants.funcEnd;
1067
+ return funcTokenCount;
1068
+ };
1069
+ /**
1070
+ * Calculate the token count of messages, supporting multiple GPT encoders
1071
+ */
1072
+ const getTokenCount = async (payload, model) => {
1073
+ const tokenizer = getTokenizerFromModel(model);
1074
+ const encoder = await getEncodeChatFunction(tokenizer);
1075
+ const simplifiedMessages = payload.messages;
1076
+ const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
1077
+ const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
1078
+ const constants = getModelConstants(model);
1079
+ let inputTokens = calculateTokens(inputMessages, encoder, constants);
1080
+ if (payload.tools && payload.tools.length > 0) inputTokens += numTokensForTools(payload.tools, encoder, constants);
1081
+ const outputTokens = calculateTokens(outputMessages, encoder, constants);
1082
+ return {
1083
+ input: inputTokens,
1084
+ output: outputTokens
1085
+ };
1086
+ };
1087
+
1088
+ //#endregion
1089
+ //#region src/services/copilot/create-chat-completions.ts
1090
+ const createChatCompletions = async (payload) => {
1091
+ if (!state.copilotToken) throw new Error("Copilot token not found");
1092
+ const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x$1) => x$1.type === "image_url"));
1093
+ const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
1094
+ const headers = {
1095
+ ...copilotHeaders(state, enableVision),
1096
+ "X-Initiator": isAgentCall ? "agent" : "user"
1097
+ };
1098
+ const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
1099
+ method: "POST",
1100
+ headers,
1101
+ body: JSON.stringify(payload)
1102
+ });
1103
+ if (!response.ok) {
1104
+ consola.error("Failed to create chat completions", response);
1105
+ throw await HTTPError.fromResponse("Failed to create chat completions", response);
1106
+ }
1107
+ if (payload.stream) return events(response);
1108
+ return await response.json();
1109
+ };
1110
+
1111
+ //#endregion
1112
+ //#region src/routes/chat-completions/handler.ts
1113
+ async function handleCompletion$1(c) {
1114
+ const startTime = Date.now();
1115
+ let payload = await c.req.json();
1116
+ consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
1117
+ const historyId = recordRequest("openai", {
1118
+ model: payload.model,
1119
+ messages: convertOpenAIMessages(payload.messages),
1120
+ stream: payload.stream ?? false,
1121
+ tools: payload.tools?.map((t) => ({
1122
+ name: t.function.name,
1123
+ description: t.function.description
1124
+ })),
1125
+ max_tokens: payload.max_tokens ?? void 0,
1126
+ temperature: payload.temperature ?? void 0
1127
+ });
1128
+ const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1129
+ try {
1130
+ if (selectedModel) {
1131
+ const tokenCount = await getTokenCount(payload, selectedModel);
1132
+ consola.info("Current token count:", tokenCount);
1133
+ } else consola.warn("No model selected, skipping token count calculation");
1134
+ } catch (error) {
1135
+ consola.warn("Failed to calculate token count:", error);
1136
+ }
1137
+ if (state.manualApprove) await awaitApproval();
1138
+ if (isNullish(payload.max_tokens)) {
1139
+ payload = {
1140
+ ...payload,
1141
+ max_tokens: selectedModel?.capabilities.limits.max_output_tokens
1142
+ };
1143
+ consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
1144
+ }
1145
+ try {
1146
+ const response = await executeWithRateLimit(state, () => createChatCompletions(payload));
1147
+ if (isNonStreaming$1(response)) {
1148
+ consola.debug("Non-streaming response:", JSON.stringify(response));
1149
+ const choice = response.choices[0];
1150
+ recordResponse(historyId, {
1151
+ success: true,
1152
+ model: response.model,
1153
+ usage: {
1154
+ input_tokens: response.usage?.prompt_tokens ?? 0,
1155
+ output_tokens: response.usage?.completion_tokens ?? 0
1156
+ },
1157
+ stop_reason: choice?.finish_reason ?? void 0,
1158
+ content: choice?.message ? {
1159
+ role: choice.message.role,
1160
+ content: typeof choice.message.content === "string" ? choice.message.content : JSON.stringify(choice.message.content),
1161
+ tool_calls: choice.message.tool_calls?.map((tc) => ({
1162
+ id: tc.id,
1163
+ type: tc.type,
1164
+ function: {
1165
+ name: tc.function.name,
1166
+ arguments: tc.function.arguments
1167
+ }
1168
+ }))
1169
+ } : null,
1170
+ toolCalls: choice?.message?.tool_calls?.map((tc) => ({
1171
+ id: tc.id,
1172
+ name: tc.function.name,
1173
+ input: tc.function.arguments
1174
+ }))
1175
+ }, Date.now() - startTime);
1176
+ return c.json(response);
1177
+ }
1178
+ consola.debug("Streaming response");
1179
+ return streamSSE(c, async (stream) => {
1180
+ let streamModel = "";
1181
+ let streamInputTokens = 0;
1182
+ let streamOutputTokens = 0;
1183
+ let streamFinishReason = "";
1184
+ let streamContent = "";
1185
+ const streamToolCalls = [];
1186
+ const toolCallAccumulators = /* @__PURE__ */ new Map();
1187
+ try {
1188
+ for await (const chunk of response) {
1189
+ consola.debug("Streaming chunk:", JSON.stringify(chunk));
1190
+ if (chunk.data && chunk.data !== "[DONE]") try {
1191
+ const parsed = JSON.parse(chunk.data);
1192
+ if (parsed.model && !streamModel) streamModel = parsed.model;
1193
+ if (parsed.usage) {
1194
+ streamInputTokens = parsed.usage.prompt_tokens;
1195
+ streamOutputTokens = parsed.usage.completion_tokens;
1196
+ }
1197
+ const choice = parsed.choices[0];
1198
+ if (choice?.delta?.content) streamContent += choice.delta.content;
1199
+ if (choice?.delta?.tool_calls) for (const tc of choice.delta.tool_calls) {
1200
+ const idx = tc.index;
1201
+ if (!toolCallAccumulators.has(idx)) toolCallAccumulators.set(idx, {
1202
+ id: tc.id || "",
1203
+ name: tc.function?.name || "",
1204
+ arguments: ""
1205
+ });
1206
+ const acc = toolCallAccumulators.get(idx);
1207
+ if (acc) {
1208
+ if (tc.id) acc.id = tc.id;
1209
+ if (tc.function?.name) acc.name = tc.function.name;
1210
+ if (tc.function?.arguments) acc.arguments += tc.function.arguments;
1211
+ }
1212
+ }
1213
+ if (choice?.finish_reason) streamFinishReason = choice.finish_reason;
1214
+ } catch {}
1215
+ await stream.writeSSE(chunk);
1216
+ }
1217
+ for (const tc of toolCallAccumulators.values()) if (tc.id && tc.name) streamToolCalls.push({
1218
+ id: tc.id,
1219
+ name: tc.name,
1220
+ arguments: tc.arguments
1221
+ });
1222
+ const toolCallsForContent = streamToolCalls.map((tc) => ({
1223
+ id: tc.id,
1224
+ type: "function",
1225
+ function: {
1226
+ name: tc.name,
1227
+ arguments: tc.arguments
1228
+ }
1229
+ }));
1230
+ recordResponse(historyId, {
1231
+ success: true,
1232
+ model: streamModel || payload.model,
1233
+ usage: {
1234
+ input_tokens: streamInputTokens,
1235
+ output_tokens: streamOutputTokens
1236
+ },
1237
+ stop_reason: streamFinishReason || void 0,
1238
+ content: {
1239
+ role: "assistant",
1240
+ content: streamContent || void 0,
1241
+ tool_calls: toolCallsForContent.length > 0 ? toolCallsForContent : void 0
1242
+ },
1243
+ toolCalls: streamToolCalls.length > 0 ? streamToolCalls.map((tc) => ({
1244
+ id: tc.id,
1245
+ name: tc.name,
1246
+ input: tc.arguments
1247
+ })) : void 0
1248
+ }, Date.now() - startTime);
1249
+ } catch (error) {
1250
+ recordResponse(historyId, {
1251
+ success: false,
1252
+ model: streamModel || payload.model,
1253
+ usage: {
1254
+ input_tokens: 0,
1255
+ output_tokens: 0
1256
+ },
1257
+ error: error instanceof Error ? error.message : "Stream error",
1258
+ content: null
1259
+ }, Date.now() - startTime);
1260
+ throw error;
1261
+ }
1262
+ });
1263
+ } catch (error) {
1264
+ recordResponse(historyId, {
1265
+ success: false,
1266
+ model: payload.model,
1267
+ usage: {
1268
+ input_tokens: 0,
1269
+ output_tokens: 0
1270
+ },
1271
+ error: error instanceof Error ? error.message : "Unknown error",
1272
+ content: null
1273
+ }, Date.now() - startTime);
1274
+ throw error;
1275
+ }
1276
+ }
1277
+ const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
1278
+ function convertOpenAIMessages(messages) {
1279
+ return messages.map((msg) => {
1280
+ const result = {
1281
+ role: msg.role,
1282
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
1283
+ };
1284
+ if ("tool_calls" in msg && msg.tool_calls) result.tool_calls = msg.tool_calls.map((tc) => ({
1285
+ id: tc.id,
1286
+ type: tc.type,
1287
+ function: {
1288
+ name: tc.function.name,
1289
+ arguments: tc.function.arguments
1290
+ }
1291
+ }));
1292
+ if ("tool_call_id" in msg && msg.tool_call_id) result.tool_call_id = msg.tool_call_id;
1293
+ if ("name" in msg && msg.name) result.name = msg.name;
1294
+ return result;
1295
+ });
1296
+ }
1297
+
1298
+ //#endregion
1299
+ //#region src/routes/chat-completions/route.ts
1300
+ const completionRoutes = new Hono();
1301
+ completionRoutes.post("/", async (c) => {
1302
+ try {
1303
+ return await handleCompletion$1(c);
1304
+ } catch (error) {
1305
+ return await forwardError(c, error);
1306
+ }
1307
+ });
1308
+
1309
+ //#endregion
1310
+ //#region src/services/copilot/create-embeddings.ts
1311
+ const createEmbeddings = async (payload) => {
1312
+ if (!state.copilotToken) throw new Error("Copilot token not found");
1313
+ const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
1314
+ method: "POST",
1315
+ headers: copilotHeaders(state),
1316
+ body: JSON.stringify(payload)
1317
+ });
1318
+ if (!response.ok) throw await HTTPError.fromResponse("Failed to create embeddings", response);
1319
+ return await response.json();
1320
+ };
1321
+
1322
+ //#endregion
1323
+ //#region src/routes/embeddings/route.ts
1324
+ const embeddingRoutes = new Hono();
1325
+ embeddingRoutes.post("/", async (c) => {
1326
+ try {
1327
+ const payload = await c.req.json();
1328
+ const response = await createEmbeddings(payload);
1329
+ return c.json(response);
1330
+ } catch (error) {
1331
+ return await forwardError(c, error);
1332
+ }
1333
+ });
1334
+
1335
+ //#endregion
1336
+ //#region src/routes/event-logging/route.ts
1337
+ const eventLoggingRoutes = new Hono();
1338
+ eventLoggingRoutes.post("/batch", (c) => {
1339
+ return c.text("OK", 200);
1340
+ });
1341
+
1342
+ //#endregion
1343
+ //#region src/routes/history/api.ts
1344
+ function handleGetEntries(c) {
1345
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1346
+ const query = c.req.query();
1347
+ const options = {
1348
+ page: query.page ? Number.parseInt(query.page, 10) : void 0,
1349
+ limit: query.limit ? Number.parseInt(query.limit, 10) : void 0,
1350
+ model: query.model || void 0,
1351
+ endpoint: query.endpoint,
1352
+ success: query.success ? query.success === "true" : void 0,
1353
+ from: query.from ? Number.parseInt(query.from, 10) : void 0,
1354
+ to: query.to ? Number.parseInt(query.to, 10) : void 0,
1355
+ search: query.search || void 0,
1356
+ sessionId: query.sessionId || void 0
1357
+ };
1358
+ const result = getHistory(options);
1359
+ return c.json(result);
1360
+ }
1361
+ function handleGetEntry(c) {
1362
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1363
+ const id = c.req.param("id");
1364
+ const entry = getEntry(id);
1365
+ if (!entry) return c.json({ error: "Entry not found" }, 404);
1366
+ return c.json(entry);
1367
+ }
1368
+ function handleDeleteEntries(c) {
1369
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1370
+ clearHistory();
1371
+ return c.json({
1372
+ success: true,
1373
+ message: "History cleared"
1374
+ });
1375
+ }
1376
+ function handleGetStats(c) {
1377
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1378
+ const stats = getStats();
1379
+ return c.json(stats);
1380
+ }
1381
+ function handleExport(c) {
1382
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1383
+ const format = c.req.query("format") || "json";
1384
+ const data = exportHistory(format);
1385
+ if (format === "csv") {
1386
+ c.header("Content-Type", "text/csv");
1387
+ c.header("Content-Disposition", "attachment; filename=history.csv");
1388
+ } else {
1389
+ c.header("Content-Type", "application/json");
1390
+ c.header("Content-Disposition", "attachment; filename=history.json");
1391
+ }
1392
+ return c.body(data);
1393
+ }
1394
+ function handleGetSessions(c) {
1395
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1396
+ const result = getSessions();
1397
+ return c.json(result);
1398
+ }
1399
+ function handleGetSession(c) {
1400
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1401
+ const id = c.req.param("id");
1402
+ const session = getSession(id);
1403
+ if (!session) return c.json({ error: "Session not found" }, 404);
1404
+ const entries = getSessionEntries(id);
1405
+ return c.json({
1406
+ ...session,
1407
+ entries
1408
+ });
1409
+ }
1410
+ function handleDeleteSession(c) {
1411
+ if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
1412
+ const id = c.req.param("id");
1413
+ if (!deleteSession(id)) return c.json({ error: "Session not found" }, 404);
1414
+ return c.json({
1415
+ success: true,
1416
+ message: "Session deleted"
1417
+ });
1418
+ }
1419
+
1420
+ //#endregion
1421
+ //#region src/routes/history/ui/script.ts
1422
+ const script = `
1423
+ let currentSessionId = null;
1424
+ let currentEntryId = null;
1425
+ let debounceTimer = null;
1426
+
1427
+ function formatTime(ts) {
1428
+ const d = new Date(ts);
1429
+ return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
1430
+ }
1431
+
1432
+ function formatDate(ts) {
1433
+ const d = new Date(ts);
1434
+ return d.toLocaleDateString([], {month:'short',day:'numeric'}) + ' ' + formatTime(ts);
1435
+ }
1436
+
1437
+ function formatNumber(n) {
1438
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1439
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
1440
+ return n.toString();
1441
+ }
1442
+
1443
+ function formatDuration(ms) {
1444
+ if (!ms) return '-';
1445
+ if (ms < 1000) return ms + 'ms';
1446
+ return (ms / 1000).toFixed(1) + 's';
1447
+ }
1448
+
1449
+ function getContentText(content) {
1450
+ if (!content) return '';
1451
+ if (typeof content === 'string') return content;
1452
+ if (Array.isArray(content)) {
1453
+ return content.map(c => {
1454
+ if (c.type === 'text') return c.text || '';
1455
+ if (c.type === 'tool_use') return '[tool_use: ' + c.name + ']';
1456
+ if (c.type === 'tool_result') return '[tool_result: ' + (c.tool_use_id || '').slice(0,8) + ']';
1457
+ if (c.type === 'image' || c.type === 'image_url') return '[image]';
1458
+ return c.text || '[' + (c.type || 'unknown') + ']';
1459
+ }).join('\\n');
1460
+ }
1461
+ return JSON.stringify(content, null, 2);
1462
+ }
1463
+
1464
+ function formatContentForDisplay(content) {
1465
+ if (!content) return { summary: '', raw: 'null' };
1466
+ if (typeof content === 'string') return { summary: content, raw: JSON.stringify(content) };
1467
+ if (Array.isArray(content)) {
1468
+ const parts = [];
1469
+ for (const c of content) {
1470
+ if (c.type === 'text') {
1471
+ parts.push(c.text || '');
1472
+ } else if (c.type === 'tool_use') {
1473
+ parts.push('--- tool_use: ' + c.name + ' [' + (c.id || '').slice(0,8) + '] ---\\n' + JSON.stringify(c.input, null, 2));
1474
+ } else if (c.type === 'tool_result') {
1475
+ const resultContent = typeof c.content === 'string' ? c.content : JSON.stringify(c.content, null, 2);
1476
+ parts.push('--- tool_result [' + (c.tool_use_id || '').slice(0,8) + '] ---\\n' + resultContent);
1477
+ } else if (c.type === 'image' || c.type === 'image_url') {
1478
+ parts.push('[image data]');
1479
+ } else {
1480
+ parts.push(JSON.stringify(c, null, 2));
1481
+ }
1482
+ }
1483
+ return { summary: parts.join('\\n\\n'), raw: JSON.stringify(content, null, 2) };
1484
+ }
1485
+ const raw = JSON.stringify(content, null, 2);
1486
+ return { summary: raw, raw };
1487
+ }
1488
+
1489
+ async function loadStats() {
1490
+ try {
1491
+ const res = await fetch('/history/api/stats');
1492
+ const data = await res.json();
1493
+ if (data.error) return;
1494
+ document.getElementById('stat-total').textContent = formatNumber(data.totalRequests);
1495
+ document.getElementById('stat-success').textContent = formatNumber(data.successfulRequests);
1496
+ document.getElementById('stat-failed').textContent = formatNumber(data.failedRequests);
1497
+ document.getElementById('stat-input').textContent = formatNumber(data.totalInputTokens);
1498
+ document.getElementById('stat-output').textContent = formatNumber(data.totalOutputTokens);
1499
+ document.getElementById('stat-sessions').textContent = data.activeSessions;
1500
+ } catch (e) {
1501
+ console.error('Failed to load stats', e);
1502
+ }
1503
+ }
1504
+
1505
+ async function loadSessions() {
1506
+ try {
1507
+ const res = await fetch('/history/api/sessions');
1508
+ const data = await res.json();
1509
+ if (data.error) {
1510
+ document.getElementById('sessions-list').innerHTML = '<div class="empty-state">Not enabled</div>';
1511
+ return;
1512
+ }
1513
+
1514
+ let html = '<div class="session-item all' + (currentSessionId === null ? ' active' : '') + '" onclick="selectSession(null)">All Requests</div>';
1515
+
1516
+ for (const s of data.sessions) {
1517
+ const isActive = currentSessionId === s.id;
1518
+ const shortId = s.id.slice(0, 8);
1519
+ html += \`
1520
+ <div class="session-item\${isActive ? ' active' : ''}" onclick="selectSession('\${s.id}')">
1521
+ <div class="session-meta">
1522
+ <span>\${s.models[0] || 'Unknown'}</span>
1523
+ <span class="session-time">\${formatDate(s.startTime)}</span>
1524
+ </div>
1525
+ <div class="session-stats">
1526
+ <span style="color:var(--text-dim);font-family:monospace;font-size:10px;">\${shortId}</span>
1527
+ <span>\${s.requestCount} req</span>
1528
+ <span>\${formatNumber(s.totalInputTokens + s.totalOutputTokens)} tok</span>
1529
+ <span class="badge \${s.endpoint}">\${s.endpoint}</span>
1530
+ </div>
1531
+ </div>
1532
+ \`;
1533
+ }
1534
+
1535
+ document.getElementById('sessions-list').innerHTML = html || '<div class="empty-state">No sessions</div>';
1536
+ } catch (e) {
1537
+ document.getElementById('sessions-list').innerHTML = '<div class="empty-state">Error loading</div>';
1538
+ }
1539
+ }
1540
+
1541
+ function selectSession(id) {
1542
+ currentSessionId = id;
1543
+ loadSessions();
1544
+ loadEntries();
1545
+ closeDetail();
1546
+ }
1547
+
1548
+ async function loadEntries() {
1549
+ const container = document.getElementById('entries-container');
1550
+ container.innerHTML = '<div class="loading">Loading...</div>';
1551
+
1552
+ const params = new URLSearchParams();
1553
+ params.set('limit', '100');
1554
+
1555
+ if (currentSessionId) params.set('sessionId', currentSessionId);
1556
+
1557
+ const endpoint = document.getElementById('filter-endpoint').value;
1558
+ const success = document.getElementById('filter-success').value;
1559
+ const search = document.getElementById('filter-search').value;
1560
+
1561
+ if (endpoint) params.set('endpoint', endpoint);
1562
+ if (success) params.set('success', success);
1563
+ if (search) params.set('search', search);
1564
+
1565
+ try {
1566
+ const res = await fetch('/history/api/entries?' + params.toString());
1567
+ const data = await res.json();
1568
+
1569
+ if (data.error) {
1570
+ container.innerHTML = '<div class="empty-state"><h3>History Not Enabled</h3><p>Start server with --history</p></div>';
1571
+ return;
1572
+ }
1573
+
1574
+ if (data.entries.length === 0) {
1575
+ container.innerHTML = '<div class="empty-state"><h3>No entries</h3><p>Make some API requests</p></div>';
1576
+ return;
1577
+ }
1578
+
1579
+ let html = '';
1580
+ for (const e of data.entries) {
1581
+ const isSelected = currentEntryId === e.id;
1582
+ const status = !e.response ? 'pending' : (e.response.success ? 'success' : 'error');
1583
+ const statusLabel = !e.response ? 'pending' : (e.response.success ? 'success' : 'error');
1584
+ const tokens = e.response ? formatNumber(e.response.usage.input_tokens) + '/' + formatNumber(e.response.usage.output_tokens) : '-';
1585
+ const shortId = e.id.slice(0, 8);
1586
+
1587
+ html += \`
1588
+ <div class="entry-item\${isSelected ? ' selected' : ''}" onclick="showDetail('\${e.id}')">
1589
+ <div class="entry-header">
1590
+ <span class="entry-time">\${formatTime(e.timestamp)}</span>
1591
+ <span style="color:var(--text-dim);font-family:monospace;font-size:10px;">\${shortId}</span>
1592
+ <span class="badge \${e.endpoint}">\${e.endpoint}</span>
1593
+ <span class="badge \${status}">\${statusLabel}</span>
1594
+ \${e.request.stream ? '<span class="badge stream">stream</span>' : ''}
1595
+ <span class="entry-model">\${e.response?.model || e.request.model}</span>
1596
+ <span class="entry-tokens">\${tokens}</span>
1597
+ <span class="entry-duration">\${formatDuration(e.durationMs)}</span>
1598
+ </div>
1599
+ </div>
1600
+ \`;
1601
+ }
1602
+
1603
+ container.innerHTML = html;
1604
+ } catch (e) {
1605
+ container.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
1606
+ }
1607
+ }
1608
+
1609
+ async function showDetail(id) {
1610
+ // Update selected state without reloading
1611
+ const prevSelected = document.querySelector('.entry-item.selected');
1612
+ if (prevSelected) prevSelected.classList.remove('selected');
1613
+ const newSelected = document.querySelector(\`.entry-item[onclick*="'\${id}'"]\`);
1614
+ if (newSelected) newSelected.classList.add('selected');
1615
+ currentEntryId = id;
1616
+
1617
+ const panel = document.getElementById('detail-panel');
1618
+ const content = document.getElementById('detail-content');
1619
+ panel.classList.add('open');
1620
+ content.innerHTML = '<div class="loading">Loading...</div>';
1621
+
1622
+ try {
1623
+ const res = await fetch('/history/api/entries/' + id);
1624
+ const entry = await res.json();
1625
+ if (entry.error) {
1626
+ content.innerHTML = '<div class="empty-state">Not found</div>';
1627
+ return;
1628
+ }
1629
+
1630
+ let html = '';
1631
+
1632
+ // Entry metadata (IDs)
1633
+ html += \`
1634
+ <div class="detail-section">
1635
+ <h4>Entry Info</h4>
1636
+ <div class="response-info">
1637
+ <div class="info-item"><div class="info-label">Entry ID</div><div class="info-value" style="font-family:monospace;font-size:11px;">\${entry.id}</div></div>
1638
+ <div class="info-item"><div class="info-label">Session ID</div><div class="info-value" style="font-family:monospace;font-size:11px;">\${entry.sessionId || '-'}</div></div>
1639
+ <div class="info-item"><div class="info-label">Timestamp</div><div class="info-value">\${formatDate(entry.timestamp)}</div></div>
1640
+ <div class="info-item"><div class="info-label">Endpoint</div><div class="info-value"><span class="badge \${entry.endpoint}">\${entry.endpoint}</span></div></div>
1641
+ </div>
1642
+ </div>
1643
+ \`;
1644
+
1645
+ // Response info
1646
+ if (entry.response) {
1647
+ html += \`
1648
+ <div class="detail-section">
1649
+ <h4>Response</h4>
1650
+ <div class="response-info">
1651
+ <div class="info-item"><div class="info-label">Status</div><div class="info-value"><span class="badge \${entry.response.success ? 'success' : 'error'}">\${entry.response.success ? 'Success' : 'Error'}</span></div></div>
1652
+ <div class="info-item"><div class="info-label">Model</div><div class="info-value">\${entry.response.model}</div></div>
1653
+ <div class="info-item"><div class="info-label">Input Tokens</div><div class="info-value">\${formatNumber(entry.response.usage.input_tokens)}</div></div>
1654
+ <div class="info-item"><div class="info-label">Output Tokens</div><div class="info-value">\${formatNumber(entry.response.usage.output_tokens)}</div></div>
1655
+ <div class="info-item"><div class="info-label">Duration</div><div class="info-value">\${formatDuration(entry.durationMs)}</div></div>
1656
+ <div class="info-item"><div class="info-label">Stop Reason</div><div class="info-value">\${entry.response.stop_reason || '-'}</div></div>
1657
+ </div>
1658
+ \${entry.response.error ? '<div style="color:var(--error);margin-top:8px;">Error: ' + entry.response.error + '</div>' : ''}
1659
+ </div>
1660
+ \`;
1661
+ }
1662
+
1663
+ // System prompt
1664
+ if (entry.request.system) {
1665
+ html += \`
1666
+ <div class="detail-section">
1667
+ <h4>System Prompt</h4>
1668
+ <div class="message system">
1669
+ <div class="message-content">\${escapeHtml(entry.request.system)}</div>
1670
+ </div>
1671
+ </div>
1672
+ \`;
1673
+ }
1674
+
1675
+ // Messages
1676
+ html += '<div class="detail-section"><h4>Messages</h4><div class="messages-list">';
1677
+ for (const msg of entry.request.messages) {
1678
+ const roleClass = msg.role === 'user' ? 'user' : (msg.role === 'assistant' ? 'assistant' : (msg.role === 'system' ? 'system' : 'tool'));
1679
+ const formatted = formatContentForDisplay(msg.content);
1680
+ const isLong = formatted.summary.length > 500;
1681
+ const rawContent = JSON.stringify(msg, null, 2);
1682
+
1683
+ html += \`
1684
+ <div class="message \${roleClass}">
1685
+ <button class="raw-btn small" onclick="showRawJson(event, \${escapeAttr(rawContent)})">Raw</button>
1686
+ <button class="copy-btn small" onclick="copyText(event, this)" data-content="\${escapeAttr(formatted.summary)}">Copy</button>
1687
+ <div class="message-role">\${msg.role}\${msg.name ? ' (' + msg.name + ')' : ''}\${msg.tool_call_id ? ' [' + (msg.tool_call_id || '').slice(0,8) + ']' : ''}</div>
1688
+ <div class="message-content\${isLong ? ' collapsed' : ''}" id="msg-\${Math.random().toString(36).slice(2)}">\${escapeHtml(formatted.summary)}</div>
1689
+ \${isLong ? '<span class="expand-btn" onclick="toggleExpand(this)">Show more</span>' : ''}
1690
+ \`;
1691
+
1692
+ // Tool calls
1693
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1694
+ for (const tc of msg.tool_calls) {
1695
+ html += \`
1696
+ <div class="tool-call">
1697
+ <span class="tool-name">\${tc.function.name}</span>
1698
+ <div class="tool-args">\${escapeHtml(tc.function.arguments)}</div>
1699
+ </div>
1700
+ \`;
1701
+ }
1702
+ }
1703
+
1704
+ html += '</div>';
1705
+ }
1706
+ html += '</div></div>';
1707
+
1708
+ // Response content
1709
+ if (entry.response?.content) {
1710
+ const formatted = formatContentForDisplay(entry.response.content.content);
1711
+ const rawContent = JSON.stringify(entry.response.content, null, 2);
1712
+ html += \`
1713
+ <div class="detail-section">
1714
+ <h4>Response Content</h4>
1715
+ <div class="message assistant">
1716
+ <button class="raw-btn small" onclick="showRawJson(event, \${escapeAttr(rawContent)})">Raw</button>
1717
+ <button class="copy-btn small" onclick="copyText(event, this)" data-content="\${escapeAttr(formatted.summary)}">Copy</button>
1718
+ <div class="message-content">\${escapeHtml(formatted.summary)}</div>
1719
+ </div>
1720
+ </div>
1721
+ \`;
1722
+ }
1723
+
1724
+ // Response tool calls
1725
+ if (entry.response?.toolCalls && entry.response.toolCalls.length > 0) {
1726
+ html += '<div class="detail-section"><h4>Tool Calls</h4>';
1727
+ for (const tc of entry.response.toolCalls) {
1728
+ const tcRaw = JSON.stringify(tc, null, 2);
1729
+ html += \`
1730
+ <div class="tool-call" style="position:relative;">
1731
+ <button class="raw-btn small" style="position:absolute;top:4px;right:4px;opacity:1;" onclick="showRawJson(event, \${escapeAttr(tcRaw)})">Raw</button>
1732
+ <span class="tool-name">\${tc.name}</span> <span style="color:var(--text-muted);font-size:11px;">[\${(tc.id || '').slice(0,8)}]</span>
1733
+ <div class="tool-args">\${escapeHtml(tc.input)}</div>
1734
+ </div>
1735
+ \`;
1736
+ }
1737
+ html += '</div>';
1738
+ }
1739
+
1740
+ // Tools defined
1741
+ if (entry.request.tools && entry.request.tools.length > 0) {
1742
+ html += '<div class="detail-section"><h4>Available Tools (' + entry.request.tools.length + ')</h4>';
1743
+ html += '<div style="font-size:11px;color:var(--text-muted)">' + entry.request.tools.map(t => t.name).join(', ') + '</div>';
1744
+ html += '</div>';
1745
+ }
1746
+
1747
+ content.innerHTML = html;
1748
+ } catch (e) {
1749
+ content.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
1750
+ }
1751
+ }
1752
+
1753
+ function closeDetail() {
1754
+ currentEntryId = null;
1755
+ document.getElementById('detail-panel').classList.remove('open');
1756
+ loadEntries();
1757
+ }
1758
+
1759
+ function toggleExpand(btn) {
1760
+ const content = btn.previousElementSibling;
1761
+ const isCollapsed = content.classList.contains('collapsed');
1762
+ content.classList.toggle('collapsed');
1763
+ btn.textContent = isCollapsed ? 'Show less' : 'Show more';
1764
+ }
1765
+
1766
+ function copyText(event, btn) {
1767
+ event.stopPropagation();
1768
+ const text = btn.getAttribute('data-content');
1769
+ navigator.clipboard.writeText(text);
1770
+ const orig = btn.textContent;
1771
+ btn.textContent = 'Copied!';
1772
+ setTimeout(() => btn.textContent = orig, 1000);
1773
+ }
1774
+
1775
+ function escapeHtml(str) {
1776
+ if (!str) return '';
1777
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1778
+ }
1779
+
1780
+ function escapeAttr(str) {
1781
+ if (!str) return '';
1782
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
1783
+ }
1784
+
1785
+ let currentRawContent = '';
1786
+
1787
+ function showRawJson(event, content) {
1788
+ event.stopPropagation();
1789
+ currentRawContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
1790
+ document.getElementById('raw-content').textContent = currentRawContent;
1791
+ document.getElementById('raw-modal').classList.add('open');
1792
+ }
1793
+
1794
+ function closeRawModal(event) {
1795
+ if (event && event.target !== event.currentTarget) return;
1796
+ document.getElementById('raw-modal').classList.remove('open');
1797
+ }
1798
+
1799
+ function copyRawContent() {
1800
+ navigator.clipboard.writeText(currentRawContent);
1801
+ const btns = document.querySelectorAll('.modal-header button');
1802
+ const copyBtn = btns[0];
1803
+ const orig = copyBtn.textContent;
1804
+ copyBtn.textContent = 'Copied!';
1805
+ setTimeout(() => copyBtn.textContent = orig, 1000);
1806
+ }
1807
+
1808
+ function debounceFilter() {
1809
+ clearTimeout(debounceTimer);
1810
+ debounceTimer = setTimeout(loadEntries, 300);
1811
+ }
1812
+
1813
+ function refresh() {
1814
+ loadStats();
1815
+ loadSessions();
1816
+ loadEntries();
1817
+ }
1818
+
1819
+ function exportData(format) {
1820
+ window.open('/history/api/export?format=' + format, '_blank');
1821
+ }
1822
+
1823
+ async function clearAll() {
1824
+ if (!confirm('Clear all history? This cannot be undone.')) return;
1825
+ try {
1826
+ await fetch('/history/api/entries', { method: 'DELETE' });
1827
+ currentSessionId = null;
1828
+ currentEntryId = null;
1829
+ closeDetail();
1830
+ refresh();
1831
+ } catch (e) {
1832
+ alert('Failed: ' + e.message);
1833
+ }
1834
+ }
1835
+
1836
+ // Initial load
1837
+ loadStats();
1838
+ loadSessions();
1839
+ loadEntries();
1840
+
1841
+ // Keyboard shortcuts
1842
+ document.addEventListener('keydown', (e) => {
1843
+ if (e.key === 'Escape') {
1844
+ if (document.getElementById('raw-modal').classList.contains('open')) {
1845
+ closeRawModal();
1846
+ } else {
1847
+ closeDetail();
1848
+ }
1849
+ }
1850
+ if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
1851
+ e.preventDefault();
1852
+ refresh();
1853
+ }
1854
+ });
1855
+
1856
+ // Auto-refresh every 10 seconds
1857
+ setInterval(() => {
1858
+ loadStats();
1859
+ loadSessions();
1860
+ }, 10000);
1861
+ `;
1862
+
1863
+ //#endregion
1864
+ //#region src/routes/history/ui/styles.ts
1865
+ const styles = `
1866
+ :root {
1867
+ --bg: #0d1117;
1868
+ --bg-secondary: #161b22;
1869
+ --bg-tertiary: #21262d;
1870
+ --bg-hover: #30363d;
1871
+ --text: #e6edf3;
1872
+ --text-muted: #8b949e;
1873
+ --text-dim: #6e7681;
1874
+ --border: #30363d;
1875
+ --primary: #58a6ff;
1876
+ --success: #3fb950;
1877
+ --error: #f85149;
1878
+ --warning: #d29922;
1879
+ --purple: #a371f7;
1880
+ --cyan: #39c5cf;
1881
+ }
1882
+ @media (prefers-color-scheme: light) {
1883
+ :root {
1884
+ --bg: #ffffff;
1885
+ --bg-secondary: #f6f8fa;
1886
+ --bg-tertiary: #eaeef2;
1887
+ --bg-hover: #d0d7de;
1888
+ --text: #1f2328;
1889
+ --text-muted: #656d76;
1890
+ --text-dim: #8c959f;
1891
+ --border: #d0d7de;
1892
+ }
1893
+ }
1894
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1895
+ body {
1896
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1897
+ background: var(--bg);
1898
+ color: var(--text);
1899
+ line-height: 1.4;
1900
+ font-size: 13px;
1901
+ }
1902
+
1903
+ /* Layout */
1904
+ .layout { display: flex; height: 100vh; }
1905
+ .sidebar {
1906
+ width: 280px;
1907
+ border-right: 1px solid var(--border);
1908
+ display: flex;
1909
+ flex-direction: column;
1910
+ background: var(--bg-secondary);
1911
+ }
1912
+ .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
1913
+
1914
+ /* Header */
1915
+ .header {
1916
+ padding: 12px 16px;
1917
+ border-bottom: 1px solid var(--border);
1918
+ display: flex;
1919
+ align-items: center;
1920
+ justify-content: space-between;
1921
+ gap: 12px;
1922
+ background: var(--bg-secondary);
1923
+ }
1924
+ .header h1 { font-size: 16px; font-weight: 600; }
1925
+ .header-actions { display: flex; gap: 8px; }
1926
+
1927
+ /* Stats bar */
1928
+ .stats-bar {
1929
+ display: flex;
1930
+ gap: 16px;
1931
+ padding: 8px 16px;
1932
+ border-bottom: 1px solid var(--border);
1933
+ background: var(--bg-tertiary);
1934
+ font-size: 12px;
1935
+ }
1936
+ .stat { display: flex; align-items: center; gap: 4px; }
1937
+ .stat-value { font-weight: 600; }
1938
+ .stat-label { color: var(--text-muted); }
1939
+
1940
+ /* Sessions sidebar */
1941
+ .sidebar-header {
1942
+ padding: 12px;
1943
+ border-bottom: 1px solid var(--border);
1944
+ font-weight: 600;
1945
+ display: flex;
1946
+ justify-content: space-between;
1947
+ align-items: center;
1948
+ }
1949
+ .sessions-list {
1950
+ flex: 1;
1951
+ overflow-y: auto;
1952
+ }
1953
+ .session-item {
1954
+ padding: 10px 12px;
1955
+ border-bottom: 1px solid var(--border);
1956
+ cursor: pointer;
1957
+ transition: background 0.15s;
1958
+ }
1959
+ .session-item:hover { background: var(--bg-hover); }
1960
+ .session-item.active { background: var(--bg-tertiary); border-left: 3px solid var(--primary); }
1961
+ .session-item.all { font-weight: 600; color: var(--primary); }
1962
+ .session-meta { display: flex; justify-content: space-between; margin-bottom: 4px; }
1963
+ .session-time { color: var(--text-muted); font-size: 11px; }
1964
+ .session-stats { display: flex; gap: 8px; font-size: 11px; color: var(--text-dim); }
1965
+
1966
+ /* Buttons */
1967
+ button {
1968
+ background: var(--bg-tertiary);
1969
+ border: 1px solid var(--border);
1970
+ color: var(--text);
1971
+ padding: 5px 10px;
1972
+ border-radius: 6px;
1973
+ cursor: pointer;
1974
+ font-size: 12px;
1975
+ transition: all 0.15s;
1976
+ display: inline-flex;
1977
+ align-items: center;
1978
+ gap: 4px;
1979
+ }
1980
+ button:hover { background: var(--bg-hover); }
1981
+ button.primary { background: var(--primary); color: #fff; border-color: var(--primary); }
1982
+ button.danger { color: var(--error); }
1983
+ button.danger:hover { background: rgba(248,81,73,0.1); }
1984
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
1985
+ button.small { padding: 3px 6px; font-size: 11px; }
1986
+ button.icon-only { padding: 5px 6px; }
1987
+
1988
+ /* Filters */
1989
+ .filters {
1990
+ display: flex;
1991
+ gap: 8px;
1992
+ padding: 8px 16px;
1993
+ border-bottom: 1px solid var(--border);
1994
+ flex-wrap: wrap;
1995
+ }
1996
+ input, select {
1997
+ background: var(--bg);
1998
+ border: 1px solid var(--border);
1999
+ color: var(--text);
2000
+ padding: 5px 8px;
2001
+ border-radius: 6px;
2002
+ font-size: 12px;
2003
+ }
2004
+ input:focus, select:focus { outline: none; border-color: var(--primary); }
2005
+ input::placeholder { color: var(--text-dim); }
2006
+
2007
+ /* Entries list */
2008
+ .entries-container { flex: 1; overflow-y: auto; }
2009
+ .entry-item {
2010
+ border-bottom: 1px solid var(--border);
2011
+ cursor: pointer;
2012
+ transition: background 0.15s;
2013
+ }
2014
+ .entry-item:hover { background: var(--bg-secondary); }
2015
+ .entry-item.selected { background: var(--bg-tertiary); }
2016
+ .entry-header {
2017
+ display: flex;
2018
+ align-items: center;
2019
+ gap: 8px;
2020
+ padding: 8px 16px;
2021
+ }
2022
+ .entry-time { color: var(--text-muted); font-size: 11px; min-width: 70px; }
2023
+ .entry-model { font-weight: 500; flex: 1; }
2024
+ .entry-tokens { font-size: 11px; color: var(--text-dim); }
2025
+ .entry-duration { font-size: 11px; color: var(--text-dim); min-width: 50px; text-align: right; }
2026
+
2027
+ /* Badges */
2028
+ .badge {
2029
+ display: inline-block;
2030
+ padding: 1px 6px;
2031
+ border-radius: 10px;
2032
+ font-size: 10px;
2033
+ font-weight: 500;
2034
+ }
2035
+ .badge.success { background: rgba(63, 185, 80, 0.15); color: var(--success); }
2036
+ .badge.error { background: rgba(248, 81, 73, 0.15); color: var(--error); }
2037
+ .badge.pending { background: rgba(136, 136, 136, 0.15); color: var(--text-muted); }
2038
+ .badge.anthropic { background: rgba(163, 113, 247, 0.15); color: var(--purple); }
2039
+ .badge.openai { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
2040
+ .badge.stream { background: rgba(57, 197, 207, 0.15); color: var(--cyan); }
2041
+
2042
+ /* Detail panel */
2043
+ .detail-panel {
2044
+ width: 0;
2045
+ border-left: 1px solid var(--border);
2046
+ background: var(--bg-secondary);
2047
+ transition: width 0.2s;
2048
+ overflow: hidden;
2049
+ display: flex;
2050
+ flex-direction: column;
2051
+ }
2052
+ .detail-panel.open { width: 50%; min-width: 400px; }
2053
+ .detail-header {
2054
+ padding: 12px 16px;
2055
+ border-bottom: 1px solid var(--border);
2056
+ display: flex;
2057
+ justify-content: space-between;
2058
+ align-items: center;
2059
+ }
2060
+ .detail-content { flex: 1; overflow-y: auto; padding: 16px; }
2061
+ .detail-section { margin-bottom: 16px; }
2062
+ .detail-section h4 {
2063
+ font-size: 11px;
2064
+ text-transform: uppercase;
2065
+ color: var(--text-muted);
2066
+ margin-bottom: 8px;
2067
+ letter-spacing: 0.5px;
2068
+ }
2069
+
2070
+ /* Messages display */
2071
+ .messages-list { display: flex; flex-direction: column; gap: 8px; }
2072
+ .message {
2073
+ padding: 10px 12px;
2074
+ border-radius: 8px;
2075
+ background: var(--bg);
2076
+ border: 1px solid var(--border);
2077
+ position: relative;
2078
+ }
2079
+ .message.user { border-left: 3px solid var(--primary); }
2080
+ .message.assistant { border-left: 3px solid var(--success); }
2081
+ .message.system { border-left: 3px solid var(--warning); background: var(--bg-tertiary); }
2082
+ .message.tool { border-left: 3px solid var(--purple); }
2083
+ .message-role {
2084
+ font-size: 10px;
2085
+ text-transform: uppercase;
2086
+ color: var(--text-muted);
2087
+ margin-bottom: 4px;
2088
+ font-weight: 600;
2089
+ }
2090
+ .message-content {
2091
+ white-space: pre-wrap;
2092
+ word-break: break-word;
2093
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
2094
+ font-size: 12px;
2095
+ max-height: 300px;
2096
+ overflow-y: auto;
2097
+ }
2098
+ .message-content.collapsed { max-height: 100px; }
2099
+ .expand-btn {
2100
+ color: var(--primary);
2101
+ cursor: pointer;
2102
+ font-size: 11px;
2103
+ margin-top: 4px;
2104
+ display: inline-block;
2105
+ }
2106
+
2107
+ /* Tool calls */
2108
+ .tool-call {
2109
+ background: var(--bg-tertiary);
2110
+ padding: 8px;
2111
+ border-radius: 6px;
2112
+ margin-top: 8px;
2113
+ font-size: 12px;
2114
+ }
2115
+ .tool-name { color: var(--purple); font-weight: 600; }
2116
+ .tool-args {
2117
+ font-family: monospace;
2118
+ font-size: 11px;
2119
+ color: var(--text-muted);
2120
+ margin-top: 4px;
2121
+ white-space: pre-wrap;
2122
+ max-height: 150px;
2123
+ overflow-y: auto;
2124
+ }
2125
+
2126
+ /* Response info */
2127
+ .response-info {
2128
+ display: grid;
2129
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
2130
+ gap: 12px;
2131
+ }
2132
+ .info-item { }
2133
+ .info-label { font-size: 11px; color: var(--text-muted); }
2134
+ .info-value { font-weight: 500; }
2135
+
2136
+ /* Empty state */
2137
+ .empty-state {
2138
+ text-align: center;
2139
+ padding: 40px 20px;
2140
+ color: var(--text-muted);
2141
+ }
2142
+ .empty-state h3 { margin-bottom: 8px; color: var(--text); }
2143
+
2144
+ /* Loading */
2145
+ .loading { text-align: center; padding: 20px; color: var(--text-muted); }
2146
+
2147
+ /* Scrollbar */
2148
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
2149
+ ::-webkit-scrollbar-track { background: var(--bg); }
2150
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
2151
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
2152
+
2153
+ /* Copy/Raw buttons */
2154
+ .copy-btn, .raw-btn {
2155
+ position: absolute;
2156
+ top: 4px;
2157
+ opacity: 0;
2158
+ transition: opacity 0.15s;
2159
+ }
2160
+ .copy-btn { right: 4px; }
2161
+ .raw-btn { right: 50px; }
2162
+ .message:hover .copy-btn, .message:hover .raw-btn { opacity: 1; }
2163
+
2164
+ /* Raw JSON modal */
2165
+ .modal-overlay {
2166
+ position: fixed;
2167
+ top: 0; left: 0; right: 0; bottom: 0;
2168
+ background: rgba(0,0,0,0.6);
2169
+ display: none;
2170
+ justify-content: center;
2171
+ align-items: center;
2172
+ z-index: 1000;
2173
+ }
2174
+ .modal-overlay.open { display: flex; }
2175
+ .modal {
2176
+ background: var(--bg-secondary);
2177
+ border: 1px solid var(--border);
2178
+ border-radius: 8px;
2179
+ width: 80%;
2180
+ max-width: 800px;
2181
+ max-height: 80vh;
2182
+ display: flex;
2183
+ flex-direction: column;
2184
+ }
2185
+ .modal-header {
2186
+ padding: 12px 16px;
2187
+ border-bottom: 1px solid var(--border);
2188
+ display: flex;
2189
+ justify-content: space-between;
2190
+ align-items: center;
2191
+ }
2192
+ .modal-body {
2193
+ flex: 1;
2194
+ overflow: auto;
2195
+ padding: 16px;
2196
+ }
2197
+ .modal-body pre {
2198
+ margin: 0;
2199
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
2200
+ font-size: 12px;
2201
+ white-space: pre-wrap;
2202
+ word-break: break-word;
2203
+ }
2204
+ `;
2205
+
2206
+ //#endregion
2207
+ //#region src/routes/history/ui/template.ts
2208
+ const template = `
2209
+ <div class="layout">
2210
+ <!-- Sidebar: Sessions -->
2211
+ <div class="sidebar">
2212
+ <div class="sidebar-header">
2213
+ <span>Sessions</span>
2214
+ <button class="small danger" onclick="clearAll()" title="Clear all">Clear</button>
2215
+ </div>
2216
+ <div class="sessions-list" id="sessions-list">
2217
+ <div class="loading">Loading...</div>
2218
+ </div>
2219
+ </div>
2220
+
2221
+ <!-- Main content -->
2222
+ <div class="main">
2223
+ <div class="header">
2224
+ <h1>Request History</h1>
2225
+ <div class="header-actions">
2226
+ <button onclick="refresh()">Refresh</button>
2227
+ <button onclick="exportData('json')">Export JSON</button>
2228
+ <button onclick="exportData('csv')">Export CSV</button>
2229
+ </div>
2230
+ </div>
2231
+
2232
+ <div class="stats-bar" id="stats-bar">
2233
+ <div class="stat"><span class="stat-value" id="stat-total">-</span><span class="stat-label">requests</span></div>
2234
+ <div class="stat"><span class="stat-value" id="stat-success">-</span><span class="stat-label">success</span></div>
2235
+ <div class="stat"><span class="stat-value" id="stat-failed">-</span><span class="stat-label">failed</span></div>
2236
+ <div class="stat"><span class="stat-value" id="stat-input">-</span><span class="stat-label">in tokens</span></div>
2237
+ <div class="stat"><span class="stat-value" id="stat-output">-</span><span class="stat-label">out tokens</span></div>
2238
+ <div class="stat"><span class="stat-value" id="stat-sessions">-</span><span class="stat-label">sessions</span></div>
2239
+ </div>
2240
+
2241
+ <div class="filters">
2242
+ <input type="text" id="filter-search" placeholder="Search messages..." style="flex:1;min-width:150px" onkeyup="debounceFilter()">
2243
+ <select id="filter-endpoint" onchange="loadEntries()">
2244
+ <option value="">All Endpoints</option>
2245
+ <option value="anthropic">Anthropic</option>
2246
+ <option value="openai">OpenAI</option>
2247
+ </select>
2248
+ <select id="filter-success" onchange="loadEntries()">
2249
+ <option value="">All Status</option>
2250
+ <option value="true">Success</option>
2251
+ <option value="false">Failed</option>
2252
+ </select>
2253
+ </div>
2254
+
2255
+ <div style="display:flex;flex:1;overflow:hidden;">
2256
+ <div class="entries-container" id="entries-container">
2257
+ <div class="loading">Loading...</div>
2258
+ </div>
2259
+
2260
+ <!-- Detail panel -->
2261
+ <div class="detail-panel" id="detail-panel">
2262
+ <div class="detail-header">
2263
+ <span>Request Details</span>
2264
+ <button class="icon-only" onclick="closeDetail()">&times;</button>
2265
+ </div>
2266
+ <div class="detail-content" id="detail-content"></div>
2267
+ </div>
2268
+ </div>
2269
+ </div>
2270
+ </div>
2271
+
2272
+ <!-- Raw JSON Modal -->
2273
+ <div class="modal-overlay" id="raw-modal" onclick="closeRawModal(event)">
2274
+ <div class="modal" onclick="event.stopPropagation()">
2275
+ <div class="modal-header">
2276
+ <span>Raw JSON</span>
2277
+ <div>
2278
+ <button class="small" onclick="copyRawContent()">Copy</button>
2279
+ <button class="icon-only" onclick="closeRawModal()">&times;</button>
2280
+ </div>
2281
+ </div>
2282
+ <div class="modal-body">
2283
+ <pre id="raw-content"></pre>
2284
+ </div>
2285
+ </div>
2286
+ </div>
2287
+ `;
2288
+
2289
+ //#endregion
2290
+ //#region src/routes/history/ui.ts
2291
+ function getHistoryUI() {
2292
+ return `<!DOCTYPE html>
2293
+ <html lang="en">
2294
+ <head>
2295
+ <meta charset="UTF-8">
2296
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2297
+ <title>Copilot API - Request History</title>
2298
+ <style>${styles}</style>
2299
+ </head>
2300
+ <body>
2301
+ ${template}
2302
+ <script>${script}<\/script>
2303
+ </body>
2304
+ </html>`;
2305
+ }
2306
+
2307
+ //#endregion
2308
+ //#region src/routes/history/route.ts
2309
+ const historyRoutes = new Hono();
2310
+ historyRoutes.get("/api/entries", handleGetEntries);
2311
+ historyRoutes.get("/api/entries/:id", handleGetEntry);
2312
+ historyRoutes.delete("/api/entries", handleDeleteEntries);
2313
+ historyRoutes.get("/api/stats", handleGetStats);
2314
+ historyRoutes.get("/api/export", handleExport);
2315
+ historyRoutes.get("/api/sessions", handleGetSessions);
2316
+ historyRoutes.get("/api/sessions/:id", handleGetSession);
2317
+ historyRoutes.delete("/api/sessions/:id", handleDeleteSession);
2318
+ historyRoutes.get("/", (c) => {
2319
+ return c.html(getHistoryUI());
2320
+ });
2321
+
2322
+ //#endregion
2323
+ //#region src/routes/messages/utils.ts
2324
+ function mapOpenAIStopReasonToAnthropic(finishReason) {
2325
+ if (finishReason === null) return null;
2326
+ return {
2327
+ stop: "end_turn",
2328
+ length: "max_tokens",
2329
+ tool_calls: "tool_use",
2330
+ content_filter: "end_turn"
2331
+ }[finishReason];
2332
+ }
2333
+
2334
+ //#endregion
2335
+ //#region src/routes/messages/non-stream-translation.ts
2336
+ const OPENAI_TOOL_NAME_LIMIT = 64;
2337
+ function fixMessageSequence(messages) {
2338
+ const fixedMessages = [];
2339
+ for (let i = 0; i < messages.length; i++) {
2340
+ const message = messages[i];
2341
+ fixedMessages.push(message);
2342
+ if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
2343
+ const foundToolResponses = /* @__PURE__ */ new Set();
2344
+ let j = i + 1;
2345
+ while (j < messages.length && messages[j].role === "tool") {
2346
+ const toolMessage = messages[j];
2347
+ if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
2348
+ j++;
2349
+ }
2350
+ for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
2351
+ consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
2352
+ fixedMessages.push({
2353
+ role: "tool",
2354
+ tool_call_id: toolCall.id,
2355
+ content: "Tool execution was interrupted or failed."
2356
+ });
2357
+ }
2358
+ }
2359
+ }
2360
+ return fixedMessages;
2361
+ }
2362
+ function translateToOpenAI(payload) {
2363
+ const toolNameMapping = {
2364
+ truncatedToOriginal: /* @__PURE__ */ new Map(),
2365
+ originalToTruncated: /* @__PURE__ */ new Map()
2366
+ };
2367
+ const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
2368
+ return {
2369
+ payload: {
2370
+ model: translateModelName(payload.model),
2371
+ messages: fixMessageSequence(messages),
2372
+ max_tokens: payload.max_tokens,
2373
+ stop: payload.stop_sequences,
2374
+ stream: payload.stream,
2375
+ temperature: payload.temperature,
2376
+ top_p: payload.top_p,
2377
+ user: payload.metadata?.user_id,
2378
+ tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
2379
+ tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
2380
+ },
2381
+ toolNameMapping
2382
+ };
2383
+ }
2384
+ function translateModelName(model) {
2385
+ const shortNameMap = {
2386
+ opus: "claude-opus-4.5",
2387
+ sonnet: "claude-sonnet-4.5",
2388
+ haiku: "claude-haiku-4.5"
2389
+ };
2390
+ if (shortNameMap[model]) return shortNameMap[model];
2391
+ if (model.match(/^claude-sonnet-4-5-\d+$/)) return "claude-sonnet-4.5";
2392
+ if (model.match(/^claude-sonnet-4-\d+$/)) return "claude-sonnet-4";
2393
+ if (model.match(/^claude-opus-4-5-\d+$/)) return "claude-opus-4.5";
2394
+ if (model.match(/^claude-opus-4-\d+$/)) return "claude-opus-4.5";
2395
+ if (model.match(/^claude-haiku-4-5-\d+$/)) return "claude-haiku-4.5";
2396
+ if (model.match(/^claude-haiku-3-5-\d+$/)) return "claude-haiku-4.5";
2397
+ return model;
2398
+ }
2399
+ function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
2400
+ const systemMessages = handleSystemPrompt(system);
2401
+ const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
2402
+ return [...systemMessages, ...otherMessages];
2403
+ }
2404
+ function handleSystemPrompt(system) {
2405
+ if (!system) return [];
2406
+ if (typeof system === "string") return [{
2407
+ role: "system",
2408
+ content: system
2409
+ }];
2410
+ else return [{
2411
+ role: "system",
2412
+ content: system.map((block) => block.text).join("\n\n")
2413
+ }];
2414
+ }
2415
+ function handleUserMessage(message) {
2416
+ const newMessages = [];
2417
+ if (Array.isArray(message.content)) {
2418
+ const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
2419
+ const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
2420
+ for (const block of toolResultBlocks) newMessages.push({
2421
+ role: "tool",
2422
+ tool_call_id: block.tool_use_id,
2423
+ content: mapContent(block.content)
2424
+ });
2425
+ if (otherBlocks.length > 0) newMessages.push({
2426
+ role: "user",
2427
+ content: mapContent(otherBlocks)
2428
+ });
2429
+ } else newMessages.push({
2430
+ role: "user",
2431
+ content: mapContent(message.content)
2432
+ });
2433
+ return newMessages;
2434
+ }
2435
+ function handleAssistantMessage(message, toolNameMapping) {
2436
+ if (!Array.isArray(message.content)) return [{
2437
+ role: "assistant",
2438
+ content: mapContent(message.content)
2439
+ }];
2440
+ const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
2441
+ const textBlocks = message.content.filter((block) => block.type === "text");
2442
+ const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
2443
+ const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
2444
+ return toolUseBlocks.length > 0 ? [{
2445
+ role: "assistant",
2446
+ content: allTextContent || null,
2447
+ tool_calls: toolUseBlocks.map((toolUse) => ({
2448
+ id: toolUse.id,
2449
+ type: "function",
2450
+ function: {
2451
+ name: getTruncatedToolName(toolUse.name, toolNameMapping),
2452
+ arguments: JSON.stringify(toolUse.input)
2453
+ }
2454
+ }))
2455
+ }] : [{
2456
+ role: "assistant",
2457
+ content: mapContent(message.content)
2458
+ }];
2459
+ }
2460
+ function mapContent(content) {
2461
+ if (typeof content === "string") return content;
2462
+ if (!Array.isArray(content)) return null;
2463
+ if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
2464
+ const contentParts = [];
2465
+ for (const block of content) switch (block.type) {
2466
+ case "text":
2467
+ contentParts.push({
2468
+ type: "text",
2469
+ text: block.text
2470
+ });
2471
+ break;
2472
+ case "thinking":
2473
+ contentParts.push({
2474
+ type: "text",
2475
+ text: block.thinking
2476
+ });
2477
+ break;
2478
+ case "image":
2479
+ contentParts.push({
2480
+ type: "image_url",
2481
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
2482
+ });
2483
+ break;
2484
+ }
2485
+ return contentParts;
2486
+ }
2487
+ function getTruncatedToolName(originalName, toolNameMapping) {
2488
+ if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
2489
+ const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
2490
+ if (existingTruncated) return existingTruncated;
2491
+ let hash = 0;
2492
+ for (let i = 0; i < originalName.length; i++) {
2493
+ const char = originalName.charCodeAt(i);
2494
+ hash = (hash << 5) - hash + char;
2495
+ hash = hash & hash;
2496
+ }
2497
+ const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
2498
+ const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
2499
+ toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
2500
+ toolNameMapping.originalToTruncated.set(originalName, truncatedName);
2501
+ consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
2502
+ return truncatedName;
2503
+ }
2504
+ function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
2505
+ if (!anthropicTools) return;
2506
+ return anthropicTools.map((tool) => ({
2507
+ type: "function",
2508
+ function: {
2509
+ name: getTruncatedToolName(tool.name, toolNameMapping),
2510
+ description: tool.description,
2511
+ parameters: tool.input_schema
2512
+ }
2513
+ }));
2514
+ }
2515
+ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
2516
+ if (!anthropicToolChoice) return;
2517
+ switch (anthropicToolChoice.type) {
2518
+ case "auto": return "auto";
2519
+ case "any": return "required";
2520
+ case "tool":
2521
+ if (anthropicToolChoice.name) return {
2522
+ type: "function",
2523
+ function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
2524
+ };
2525
+ return;
2526
+ case "none": return "none";
2527
+ default: return;
2528
+ }
2529
+ }
2530
+ function translateToAnthropic(response, toolNameMapping) {
2531
+ if (response.choices.length === 0) return {
2532
+ id: response.id,
2533
+ type: "message",
2534
+ role: "assistant",
2535
+ model: response.model,
2536
+ content: [],
2537
+ stop_reason: "end_turn",
2538
+ stop_sequence: null,
2539
+ usage: {
2540
+ input_tokens: response.usage?.prompt_tokens ?? 0,
2541
+ output_tokens: response.usage?.completion_tokens ?? 0
2542
+ }
2543
+ };
2544
+ const allTextBlocks = [];
2545
+ const allToolUseBlocks = [];
2546
+ let stopReason = null;
2547
+ stopReason = response.choices[0]?.finish_reason ?? stopReason;
2548
+ for (const choice of response.choices) {
2549
+ const textBlocks = getAnthropicTextBlocks(choice.message.content);
2550
+ const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
2551
+ allTextBlocks.push(...textBlocks);
2552
+ allToolUseBlocks.push(...toolUseBlocks);
2553
+ if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
2554
+ }
2555
+ return {
2556
+ id: response.id,
2557
+ type: "message",
2558
+ role: "assistant",
2559
+ model: response.model,
2560
+ content: [...allTextBlocks, ...allToolUseBlocks],
2561
+ stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
2562
+ stop_sequence: null,
2563
+ usage: {
2564
+ input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
2565
+ output_tokens: response.usage?.completion_tokens ?? 0,
2566
+ ...response.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.prompt_tokens_details.cached_tokens }
2567
+ }
2568
+ };
2569
+ }
2570
+ function getAnthropicTextBlocks(messageContent) {
2571
+ if (typeof messageContent === "string") return [{
2572
+ type: "text",
2573
+ text: messageContent
2574
+ }];
2575
+ if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
2576
+ type: "text",
2577
+ text: part.text
2578
+ }));
2579
+ return [];
2580
+ }
2581
+ function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
2582
+ if (!toolCalls) return [];
2583
+ return toolCalls.map((toolCall) => {
2584
+ let input = {};
2585
+ try {
2586
+ input = JSON.parse(toolCall.function.arguments);
2587
+ } catch (error) {
2588
+ consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
2589
+ }
2590
+ const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
2591
+ return {
2592
+ type: "tool_use",
2593
+ id: toolCall.id,
2594
+ name: originalName,
2595
+ input
2596
+ };
2597
+ });
2598
+ }
2599
+
2600
+ //#endregion
2601
+ //#region src/routes/messages/count-tokens-handler.ts
2602
+ /**
2603
+ * Handles token counting for Anthropic messages
2604
+ */
2605
+ async function handleCountTokens(c) {
2606
+ try {
2607
+ const anthropicBeta = c.req.header("anthropic-beta");
2608
+ const anthropicPayload = await c.req.json();
2609
+ const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
2610
+ const selectedModel = state.models?.data.find((model) => model.id === anthropicPayload.model);
2611
+ if (!selectedModel) {
2612
+ consola.warn("Model not found, returning default token count");
2613
+ return c.json({ input_tokens: 1 });
2614
+ }
2615
+ const tokenCount = await getTokenCount(openAIPayload, selectedModel);
2616
+ if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
2617
+ let mcpToolExist = false;
2618
+ if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
2619
+ if (!mcpToolExist) {
2620
+ if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
2621
+ else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
2622
+ }
2623
+ }
2624
+ let finalTokenCount = tokenCount.input + tokenCount.output;
2625
+ if (anthropicPayload.model.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
2626
+ else if (anthropicPayload.model.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
2627
+ consola.info("Token count:", finalTokenCount);
2628
+ return c.json({ input_tokens: finalTokenCount });
2629
+ } catch (error) {
2630
+ consola.error("Error counting tokens:", error);
2631
+ return c.json({ input_tokens: 1 });
2632
+ }
2633
+ }
2634
+
2635
+ //#endregion
2636
+ //#region src/routes/messages/stream-translation.ts
2637
+ function isToolBlockOpen(state$1) {
2638
+ if (!state$1.contentBlockOpen) return false;
2639
+ return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
2640
+ }
2641
+ function translateChunkToAnthropicEvents(chunk, state$1, toolNameMapping) {
2642
+ const events$1 = [];
2643
+ if (chunk.choices.length === 0) {
2644
+ if (chunk.model && !state$1.model) state$1.model = chunk.model;
2645
+ return events$1;
2646
+ }
2647
+ const choice = chunk.choices[0];
2648
+ const { delta } = choice;
2649
+ if (!state$1.messageStartSent) {
2650
+ const model = chunk.model || state$1.model || "unknown";
2651
+ events$1.push({
2652
+ type: "message_start",
2653
+ message: {
2654
+ id: chunk.id || `msg_${Date.now()}`,
2655
+ type: "message",
2656
+ role: "assistant",
2657
+ content: [],
2658
+ model,
2659
+ stop_reason: null,
2660
+ stop_sequence: null,
2661
+ usage: {
2662
+ input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
2663
+ output_tokens: 0,
2664
+ ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
2665
+ }
2666
+ }
2667
+ });
2668
+ state$1.messageStartSent = true;
2669
+ }
2670
+ if (delta.content) {
2671
+ if (isToolBlockOpen(state$1)) {
2672
+ events$1.push({
2673
+ type: "content_block_stop",
2674
+ index: state$1.contentBlockIndex
2675
+ });
2676
+ state$1.contentBlockIndex++;
2677
+ state$1.contentBlockOpen = false;
2678
+ }
2679
+ if (!state$1.contentBlockOpen) {
2680
+ events$1.push({
2681
+ type: "content_block_start",
2682
+ index: state$1.contentBlockIndex,
2683
+ content_block: {
2684
+ type: "text",
2685
+ text: ""
2686
+ }
2687
+ });
2688
+ state$1.contentBlockOpen = true;
2689
+ }
2690
+ events$1.push({
2691
+ type: "content_block_delta",
2692
+ index: state$1.contentBlockIndex,
2693
+ delta: {
2694
+ type: "text_delta",
2695
+ text: delta.content
2696
+ }
2697
+ });
2698
+ }
2699
+ if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
2700
+ if (toolCall.id && toolCall.function?.name) {
2701
+ if (state$1.contentBlockOpen) {
2702
+ events$1.push({
2703
+ type: "content_block_stop",
2704
+ index: state$1.contentBlockIndex
2705
+ });
2706
+ state$1.contentBlockIndex++;
2707
+ state$1.contentBlockOpen = false;
2708
+ }
2709
+ const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
2710
+ const anthropicBlockIndex = state$1.contentBlockIndex;
2711
+ state$1.toolCalls[toolCall.index] = {
2712
+ id: toolCall.id,
2713
+ name: originalName,
2714
+ anthropicBlockIndex
2715
+ };
2716
+ events$1.push({
2717
+ type: "content_block_start",
2718
+ index: anthropicBlockIndex,
2719
+ content_block: {
2720
+ type: "tool_use",
2721
+ id: toolCall.id,
2722
+ name: originalName,
2723
+ input: {}
2724
+ }
2725
+ });
2726
+ state$1.contentBlockOpen = true;
2727
+ }
2728
+ if (toolCall.function?.arguments) {
2729
+ const toolCallInfo = state$1.toolCalls[toolCall.index];
2730
+ if (toolCallInfo) events$1.push({
2731
+ type: "content_block_delta",
2732
+ index: toolCallInfo.anthropicBlockIndex,
2733
+ delta: {
2734
+ type: "input_json_delta",
2735
+ partial_json: toolCall.function.arguments
2736
+ }
2737
+ });
2738
+ }
2739
+ }
2740
+ if (choice.finish_reason) {
2741
+ if (state$1.contentBlockOpen) {
2742
+ events$1.push({
2743
+ type: "content_block_stop",
2744
+ index: state$1.contentBlockIndex
2745
+ });
2746
+ state$1.contentBlockOpen = false;
2747
+ }
2748
+ events$1.push({
2749
+ type: "message_delta",
2750
+ delta: {
2751
+ stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
2752
+ stop_sequence: null
2753
+ },
2754
+ usage: {
2755
+ input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
2756
+ output_tokens: chunk.usage?.completion_tokens ?? 0,
2757
+ ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
2758
+ }
2759
+ }, { type: "message_stop" });
2760
+ }
2761
+ return events$1;
2762
+ }
2763
+ function translateErrorToAnthropicErrorEvent() {
2764
+ return {
2765
+ type: "error",
2766
+ error: {
2767
+ type: "api_error",
2768
+ message: "An unexpected error occurred during streaming."
2769
+ }
2770
+ };
2771
+ }
2772
+
2773
+ //#endregion
2774
+ //#region src/routes/messages/handler.ts
2775
+ async function handleCompletion(c) {
2776
+ const startTime = Date.now();
2777
+ const anthropicPayload = await c.req.json();
2778
+ consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
2779
+ const historyId = recordRequest("anthropic", {
2780
+ model: anthropicPayload.model,
2781
+ messages: convertAnthropicMessages(anthropicPayload.messages),
2782
+ stream: anthropicPayload.stream ?? false,
2783
+ tools: anthropicPayload.tools?.map((t) => ({
2784
+ name: t.name,
2785
+ description: t.description
2786
+ })),
2787
+ max_tokens: anthropicPayload.max_tokens,
2788
+ temperature: anthropicPayload.temperature,
2789
+ system: extractSystemPrompt(anthropicPayload.system)
2790
+ });
2791
+ const { payload: openAIPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
2792
+ consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
2793
+ if (state.manualApprove) await awaitApproval();
2794
+ try {
2795
+ const response = await executeWithRateLimit(state, () => createChatCompletions(openAIPayload));
2796
+ if (isNonStreaming(response)) {
2797
+ consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
2798
+ const anthropicResponse = translateToAnthropic(response, toolNameMapping);
2799
+ consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
2800
+ recordResponse(historyId, {
2801
+ success: true,
2802
+ model: anthropicResponse.model,
2803
+ usage: anthropicResponse.usage,
2804
+ stop_reason: anthropicResponse.stop_reason ?? void 0,
2805
+ content: {
2806
+ role: "assistant",
2807
+ content: anthropicResponse.content.map((block) => {
2808
+ if (block.type === "text") return {
2809
+ type: "text",
2810
+ text: block.text
2811
+ };
2812
+ if (block.type === "tool_use") return {
2813
+ type: "tool_use",
2814
+ id: block.id,
2815
+ name: block.name,
2816
+ input: JSON.stringify(block.input)
2817
+ };
2818
+ return { type: block.type };
2819
+ })
2820
+ },
2821
+ toolCalls: extractToolCallsFromContent(anthropicResponse.content)
2822
+ }, Date.now() - startTime);
2823
+ return c.json(anthropicResponse);
2824
+ }
2825
+ consola.debug("Streaming response from Copilot");
2826
+ return streamSSE(c, async (stream) => {
2827
+ const streamState = {
2828
+ messageStartSent: false,
2829
+ contentBlockIndex: 0,
2830
+ contentBlockOpen: false,
2831
+ toolCalls: {}
2832
+ };
2833
+ let streamModel = "";
2834
+ let streamInputTokens = 0;
2835
+ let streamOutputTokens = 0;
2836
+ let streamStopReason = "";
2837
+ let streamContent = "";
2838
+ const streamToolCalls = [];
2839
+ let currentToolCall = null;
2840
+ try {
2841
+ for await (const rawEvent of response) {
2842
+ consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
2843
+ if (rawEvent.data === "[DONE]") break;
2844
+ if (!rawEvent.data) continue;
2845
+ let chunk;
2846
+ try {
2847
+ chunk = JSON.parse(rawEvent.data);
2848
+ } catch (parseError) {
2849
+ consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
2850
+ continue;
2851
+ }
2852
+ if (chunk.model && !streamModel) streamModel = chunk.model;
2853
+ const events$1 = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
2854
+ for (const event of events$1) {
2855
+ consola.debug("Translated Anthropic event:", JSON.stringify(event));
2856
+ switch (event.type) {
2857
+ case "content_block_delta":
2858
+ if ("text" in event.delta) streamContent += event.delta.text;
2859
+ else if ("partial_json" in event.delta && currentToolCall) currentToolCall.input += event.delta.partial_json;
2860
+ break;
2861
+ case "content_block_start":
2862
+ if (event.content_block.type === "tool_use") currentToolCall = {
2863
+ id: event.content_block.id,
2864
+ name: event.content_block.name,
2865
+ input: ""
2866
+ };
2867
+ break;
2868
+ case "content_block_stop":
2869
+ if (currentToolCall) {
2870
+ streamToolCalls.push(currentToolCall);
2871
+ currentToolCall = null;
2872
+ }
2873
+ break;
2874
+ case "message_delta":
2875
+ if (event.delta.stop_reason) streamStopReason = event.delta.stop_reason;
2876
+ if (event.usage) {
2877
+ streamInputTokens = event.usage.input_tokens ?? 0;
2878
+ streamOutputTokens = event.usage.output_tokens;
2879
+ }
2880
+ break;
2881
+ }
2882
+ await stream.writeSSE({
2883
+ event: event.type,
2884
+ data: JSON.stringify(event)
2885
+ });
2886
+ }
2887
+ }
2888
+ const contentBlocks = [];
2889
+ if (streamContent) contentBlocks.push({
2890
+ type: "text",
2891
+ text: streamContent
2892
+ });
2893
+ for (const tc of streamToolCalls) contentBlocks.push({
2894
+ type: "tool_use",
2895
+ ...tc
2896
+ });
2897
+ recordResponse(historyId, {
2898
+ success: true,
2899
+ model: streamModel || anthropicPayload.model,
2900
+ usage: {
2901
+ input_tokens: streamInputTokens,
2902
+ output_tokens: streamOutputTokens
2903
+ },
2904
+ stop_reason: streamStopReason || void 0,
2905
+ content: contentBlocks.length > 0 ? {
2906
+ role: "assistant",
2907
+ content: contentBlocks
2908
+ } : null,
2909
+ toolCalls: streamToolCalls.length > 0 ? streamToolCalls.map((tc) => ({
2910
+ id: tc.id,
2911
+ name: tc.name,
2912
+ input: tc.input
2913
+ })) : void 0
2914
+ }, Date.now() - startTime);
2915
+ } catch (error) {
2916
+ consola.error("Stream error:", error);
2917
+ recordResponse(historyId, {
2918
+ success: false,
2919
+ model: streamModel || anthropicPayload.model,
2920
+ usage: {
2921
+ input_tokens: 0,
2922
+ output_tokens: 0
2923
+ },
2924
+ error: error instanceof Error ? error.message : "Stream error",
2925
+ content: null
2926
+ }, Date.now() - startTime);
2927
+ const errorEvent = translateErrorToAnthropicErrorEvent();
2928
+ await stream.writeSSE({
2929
+ event: errorEvent.type,
2930
+ data: JSON.stringify(errorEvent)
2931
+ });
2932
+ }
2933
+ });
2934
+ } catch (error) {
2935
+ recordResponse(historyId, {
2936
+ success: false,
2937
+ model: anthropicPayload.model,
2938
+ usage: {
2939
+ input_tokens: 0,
2940
+ output_tokens: 0
2941
+ },
2942
+ error: error instanceof Error ? error.message : "Unknown error",
2943
+ content: null
2944
+ }, Date.now() - startTime);
2945
+ throw error;
2946
+ }
2947
+ }
2948
+ function convertAnthropicMessages(messages) {
2949
+ return messages.map((msg) => {
2950
+ if (typeof msg.content === "string") return {
2951
+ role: msg.role,
2952
+ content: msg.content
2953
+ };
2954
+ const content = msg.content.map((block) => {
2955
+ if (block.type === "text") return {
2956
+ type: "text",
2957
+ text: block.text
2958
+ };
2959
+ if (block.type === "tool_use") return {
2960
+ type: "tool_use",
2961
+ id: block.id,
2962
+ name: block.name,
2963
+ input: JSON.stringify(block.input)
2964
+ };
2965
+ if (block.type === "tool_result") {
2966
+ const resultContent = typeof block.content === "string" ? block.content : block.content.map((c) => c.type === "text" ? c.text : `[${c.type}]`).join("\n");
2967
+ return {
2968
+ type: "tool_result",
2969
+ tool_use_id: block.tool_use_id,
2970
+ content: resultContent
2971
+ };
2972
+ }
2973
+ return { type: block.type };
2974
+ });
2975
+ return {
2976
+ role: msg.role,
2977
+ content
2978
+ };
2979
+ });
2980
+ }
2981
+ function extractSystemPrompt(system) {
2982
+ if (!system) return void 0;
2983
+ if (typeof system === "string") return system;
2984
+ return system.map((block) => block.text).join("\n");
2985
+ }
2986
+ function extractToolCallsFromContent(content) {
2987
+ const tools = [];
2988
+ for (const block of content) if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_use" && "id" in block && "name" in block && "input" in block) tools.push({
2989
+ id: String(block.id),
2990
+ name: String(block.name),
2991
+ input: JSON.stringify(block.input)
2992
+ });
2993
+ return tools.length > 0 ? tools : void 0;
2994
+ }
2995
+ const isNonStreaming = (response) => Object.hasOwn(response, "choices");
2996
+
2997
+ //#endregion
2998
+ //#region src/routes/messages/route.ts
2999
+ const messageRoutes = new Hono();
3000
+ messageRoutes.post("/", async (c) => {
3001
+ try {
3002
+ return await handleCompletion(c);
3003
+ } catch (error) {
3004
+ return await forwardError(c, error);
3005
+ }
3006
+ });
3007
+ messageRoutes.post("/count_tokens", async (c) => {
3008
+ try {
3009
+ return await handleCountTokens(c);
3010
+ } catch (error) {
3011
+ return await forwardError(c, error);
3012
+ }
3013
+ });
3014
+
3015
+ //#endregion
3016
+ //#region src/routes/models/route.ts
3017
+ const modelRoutes = new Hono();
3018
+ modelRoutes.get("/", async (c) => {
3019
+ try {
3020
+ if (!state.models) await cacheModels();
3021
+ const models = state.models?.data.map((model) => ({
3022
+ id: model.id,
3023
+ object: "model",
3024
+ type: "model",
3025
+ created: 0,
3026
+ created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
3027
+ owned_by: model.vendor,
3028
+ display_name: model.name
3029
+ }));
3030
+ return c.json({
3031
+ object: "list",
3032
+ data: models,
3033
+ has_more: false
3034
+ });
3035
+ } catch (error) {
3036
+ return await forwardError(c, error);
3037
+ }
3038
+ });
3039
+
3040
+ //#endregion
3041
+ //#region src/routes/token/route.ts
3042
+ const tokenRoute = new Hono();
3043
+ tokenRoute.get("/", async (c) => {
3044
+ try {
3045
+ return c.json({ token: state.copilotToken });
3046
+ } catch (error) {
3047
+ return await forwardError(c, error);
3048
+ }
3049
+ });
3050
+
3051
+ //#endregion
3052
+ //#region src/routes/usage/route.ts
3053
+ const usageRoute = new Hono();
3054
+ usageRoute.get("/", async (c) => {
3055
+ try {
3056
+ const usage = await getCopilotUsage();
3057
+ return c.json(usage);
3058
+ } catch (error) {
3059
+ return await forwardError(c, error);
3060
+ }
3061
+ });
3062
+
3063
+ //#endregion
3064
+ //#region src/server.ts
3065
+ const server = new Hono();
3066
+ server.use(logger());
3067
+ server.use(cors());
3068
+ server.get("/", (c) => c.text("Server running"));
3069
+ server.get("/health", (c) => {
3070
+ const healthy = Boolean(state.copilotToken && state.githubToken);
3071
+ return c.json({
3072
+ status: healthy ? "healthy" : "unhealthy",
3073
+ checks: {
3074
+ copilotToken: Boolean(state.copilotToken),
3075
+ githubToken: Boolean(state.githubToken),
3076
+ models: Boolean(state.models)
3077
+ }
3078
+ }, healthy ? 200 : 503);
3079
+ });
3080
+ server.route("/chat/completions", completionRoutes);
3081
+ server.route("/models", modelRoutes);
3082
+ server.route("/embeddings", embeddingRoutes);
3083
+ server.route("/usage", usageRoute);
3084
+ server.route("/token", tokenRoute);
3085
+ server.route("/v1/chat/completions", completionRoutes);
3086
+ server.route("/v1/models", modelRoutes);
3087
+ server.route("/v1/embeddings", embeddingRoutes);
3088
+ server.route("/v1/messages", messageRoutes);
3089
+ server.route("/api/event_logging", eventLoggingRoutes);
3090
+ server.route("/history", historyRoutes);
3091
+
3092
+ //#endregion
3093
+ //#region src/start.ts
3094
+ async function runServer(options) {
3095
+ if (options.proxyEnv) initProxyFromEnv();
3096
+ if (options.verbose) {
3097
+ consola.level = 5;
3098
+ consola.info("Verbose logging enabled");
3099
+ }
3100
+ state.accountType = options.accountType;
3101
+ if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
3102
+ state.manualApprove = options.manual;
3103
+ state.rateLimitSeconds = options.rateLimit;
3104
+ state.rateLimitWait = options.rateLimitWait;
3105
+ state.showToken = options.showToken;
3106
+ initHistory(options.history, options.historyLimit);
3107
+ if (options.history) consola.info(`History recording enabled (max ${options.historyLimit} entries)`);
3108
+ await ensurePaths();
3109
+ await cacheVSCodeVersion();
3110
+ if (options.githubToken) {
3111
+ state.githubToken = options.githubToken;
3112
+ consola.info("Using provided GitHub token");
3113
+ } else await setupGitHubToken();
3114
+ await setupCopilotToken();
3115
+ await cacheModels();
3116
+ consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
3117
+ const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
3118
+ if (options.claudeCode) {
3119
+ invariant(state.models, "Models should be loaded by now");
3120
+ const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
3121
+ type: "select",
3122
+ options: state.models.data.map((model) => model.id)
3123
+ });
3124
+ const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
3125
+ type: "select",
3126
+ options: state.models.data.map((model) => model.id)
3127
+ });
3128
+ const command = generateEnvScript({
3129
+ ANTHROPIC_BASE_URL: serverUrl,
3130
+ ANTHROPIC_AUTH_TOKEN: "dummy",
3131
+ ANTHROPIC_MODEL: selectedModel,
3132
+ ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
3133
+ ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
3134
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
3135
+ DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
3136
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
3137
+ }, "claude");
3138
+ try {
3139
+ clipboard.writeSync(command);
3140
+ consola.success("Copied Claude Code command to clipboard!");
3141
+ } catch {
3142
+ consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
3143
+ consola.log(command);
3144
+ }
3145
+ }
3146
+ consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage${options.history ? `\nšŸ“œ History UI: ${serverUrl}/history` : ""}`);
3147
+ serve({
3148
+ fetch: server.fetch,
3149
+ port: options.port,
3150
+ hostname: options.host
3151
+ });
3152
+ }
3153
+ const start = defineCommand({
3154
+ meta: {
3155
+ name: "start",
3156
+ description: "Start the Copilot API server"
3157
+ },
3158
+ args: {
3159
+ port: {
3160
+ alias: "p",
3161
+ type: "string",
3162
+ default: "4141",
3163
+ description: "Port to listen on"
3164
+ },
3165
+ host: {
3166
+ alias: "H",
3167
+ type: "string",
3168
+ description: "Host/interface to bind to (e.g., 127.0.0.1 for localhost only, 0.0.0.0 for all interfaces)"
3169
+ },
3170
+ verbose: {
3171
+ alias: "v",
3172
+ type: "boolean",
3173
+ default: false,
3174
+ description: "Enable verbose logging"
3175
+ },
3176
+ "account-type": {
3177
+ alias: "a",
3178
+ type: "string",
3179
+ default: "individual",
3180
+ description: "Account type to use (individual, business, enterprise)"
3181
+ },
3182
+ manual: {
3183
+ type: "boolean",
3184
+ default: false,
3185
+ description: "Enable manual request approval"
3186
+ },
3187
+ "rate-limit": {
3188
+ alias: "r",
3189
+ type: "string",
3190
+ description: "Rate limit in seconds between requests"
3191
+ },
3192
+ wait: {
3193
+ alias: "w",
3194
+ type: "boolean",
3195
+ default: false,
3196
+ description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
3197
+ },
3198
+ "github-token": {
3199
+ alias: "g",
3200
+ type: "string",
3201
+ description: "Provide GitHub token directly (must be generated using the `auth` subcommand)"
3202
+ },
3203
+ "claude-code": {
3204
+ alias: "c",
3205
+ type: "boolean",
3206
+ default: false,
3207
+ description: "Generate a command to launch Claude Code with Copilot API config"
3208
+ },
3209
+ "show-token": {
3210
+ type: "boolean",
3211
+ default: false,
3212
+ description: "Show GitHub and Copilot tokens on fetch and refresh"
3213
+ },
3214
+ "proxy-env": {
3215
+ type: "boolean",
3216
+ default: false,
3217
+ description: "Initialize proxy from environment variables"
3218
+ },
3219
+ history: {
3220
+ type: "boolean",
3221
+ default: false,
3222
+ description: "Enable request history recording and Web UI at /history"
3223
+ },
3224
+ "history-limit": {
3225
+ type: "string",
3226
+ default: "1000",
3227
+ description: "Maximum number of history entries to keep in memory"
3228
+ }
3229
+ },
3230
+ run({ args }) {
3231
+ const rateLimitRaw = args["rate-limit"];
3232
+ const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
3233
+ return runServer({
3234
+ port: Number.parseInt(args.port, 10),
3235
+ host: args.host,
3236
+ verbose: args.verbose,
3237
+ accountType: args["account-type"],
3238
+ manual: args.manual,
3239
+ rateLimit,
3240
+ rateLimitWait: args.wait,
3241
+ githubToken: args["github-token"],
3242
+ claudeCode: args["claude-code"],
3243
+ showToken: args["show-token"],
3244
+ proxyEnv: args["proxy-env"],
3245
+ history: args.history,
3246
+ historyLimit: Number.parseInt(args["history-limit"], 10)
3247
+ });
3248
+ }
3249
+ });
3250
+
3251
+ //#endregion
3252
+ //#region src/main.ts
3253
+ consola.options.formatOptions.date = true;
3254
+ const main = defineCommand({
3255
+ meta: {
3256
+ name: "copilot-api",
3257
+ description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
3258
+ },
3259
+ subCommands: {
3260
+ auth,
3261
+ logout,
3262
+ start,
3263
+ "check-usage": checkUsage,
3264
+ debug
3265
+ }
3266
+ });
3267
+ await runMain(main);
3268
+
3269
+ //#endregion
3270
+ export { };
3271
+ //# sourceMappingURL=main.js.map