@hsupu/copilot-api 0.8.0 → 0.8.1-beta.1

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 DELETED
@@ -1,4571 +0,0 @@
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 { Box, Text, render, useInput, useStdout } from "ink";
16
- import React, { useEffect, useState } from "react";
17
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
18
- import { Hono } from "hono";
19
- import { cors } from "hono/cors";
20
- import { streamSSE } from "hono/streaming";
21
- import { events } from "fetch-event-stream";
22
-
23
- //#region src/lib/paths.ts
24
- const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
25
- const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
26
- const PATHS = {
27
- APP_DIR,
28
- GITHUB_TOKEN_PATH
29
- };
30
- async function ensurePaths() {
31
- await fs.mkdir(PATHS.APP_DIR, { recursive: true });
32
- await ensureFile(PATHS.GITHUB_TOKEN_PATH);
33
- }
34
- async function ensureFile(filePath) {
35
- try {
36
- await fs.access(filePath, fs.constants.W_OK);
37
- if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
38
- } catch {
39
- await fs.writeFile(filePath, "");
40
- await fs.chmod(filePath, 384);
41
- }
42
- }
43
-
44
- //#endregion
45
- //#region src/lib/state.ts
46
- const state = {
47
- accountType: "individual",
48
- manualApprove: false,
49
- rateLimitWait: false,
50
- showToken: false,
51
- autoCompact: false
52
- };
53
-
54
- //#endregion
55
- //#region src/lib/api-config.ts
56
- const standardHeaders = () => ({
57
- "content-type": "application/json",
58
- accept: "application/json"
59
- });
60
- const COPILOT_VERSION = "0.26.7";
61
- const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
62
- const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
63
- const API_VERSION = "2025-04-01";
64
- const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
65
- const copilotHeaders = (state$1, vision = false) => {
66
- const headers = {
67
- Authorization: `Bearer ${state$1.copilotToken}`,
68
- "content-type": standardHeaders()["content-type"],
69
- "copilot-integration-id": "vscode-chat",
70
- "editor-version": `vscode/${state$1.vsCodeVersion}`,
71
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
72
- "user-agent": USER_AGENT,
73
- "openai-intent": "conversation-panel",
74
- "x-github-api-version": API_VERSION,
75
- "x-request-id": randomUUID(),
76
- "x-vscode-user-agent-library-version": "electron-fetch"
77
- };
78
- if (vision) headers["copilot-vision-request"] = "true";
79
- return headers;
80
- };
81
- const GITHUB_API_BASE_URL = "https://api.github.com";
82
- const githubHeaders = (state$1) => ({
83
- ...standardHeaders(),
84
- authorization: `token ${state$1.githubToken}`,
85
- "editor-version": `vscode/${state$1.vsCodeVersion}`,
86
- "editor-plugin-version": EDITOR_PLUGIN_VERSION,
87
- "user-agent": USER_AGENT,
88
- "x-github-api-version": API_VERSION,
89
- "x-vscode-user-agent-library-version": "electron-fetch"
90
- });
91
- const GITHUB_BASE_URL = "https://github.com";
92
- const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
93
- const GITHUB_APP_SCOPES = ["read:user"].join(" ");
94
-
95
- //#endregion
96
- //#region src/lib/error.ts
97
- var HTTPError = class HTTPError extends Error {
98
- status;
99
- responseText;
100
- constructor(message, status, responseText) {
101
- super(message);
102
- this.status = status;
103
- this.responseText = responseText;
104
- }
105
- static async fromResponse(message, response) {
106
- const text = await response.text();
107
- return new HTTPError(message, response.status, text);
108
- }
109
- };
110
- /** Parse token limit info from error message */
111
- function parseTokenLimitError(message) {
112
- const match = message.match(/prompt token count of (\d+) exceeds the limit of (\d+)/);
113
- if (match) return {
114
- current: Number.parseInt(match[1], 10),
115
- limit: Number.parseInt(match[2], 10)
116
- };
117
- return null;
118
- }
119
- /** Format Anthropic-compatible error for token limit exceeded */
120
- function formatTokenLimitError(current, limit) {
121
- const excess = current - limit;
122
- const percentage = Math.round(excess / limit * 100);
123
- return {
124
- type: "error",
125
- error: {
126
- type: "invalid_request_error",
127
- message: `prompt is too long: ${current} tokens > ${limit} maximum (${excess} tokens over, ${percentage}% excess)`
128
- }
129
- };
130
- }
131
- async function forwardError(c, error) {
132
- consola.error("Error occurred:", error);
133
- if (error instanceof HTTPError) {
134
- let errorJson;
135
- try {
136
- errorJson = JSON.parse(error.responseText);
137
- } catch {
138
- errorJson = error.responseText;
139
- }
140
- consola.error("HTTP error:", errorJson);
141
- const copilotError = errorJson;
142
- if (copilotError.error?.code === "model_max_prompt_tokens_exceeded") {
143
- const tokenInfo = parseTokenLimitError(copilotError.error.message ?? "");
144
- if (tokenInfo) {
145
- const formattedError = formatTokenLimitError(tokenInfo.current, tokenInfo.limit);
146
- consola.info("Returning formatted token limit error:", formattedError);
147
- return c.json(formattedError, 400);
148
- }
149
- }
150
- return c.json({ error: {
151
- message: error.responseText,
152
- type: "error"
153
- } }, error.status);
154
- }
155
- return c.json({ error: {
156
- message: error.message,
157
- type: "error"
158
- } }, 500);
159
- }
160
-
161
- //#endregion
162
- //#region src/services/github/get-copilot-token.ts
163
- const getCopilotToken = async () => {
164
- const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, { headers: githubHeaders(state) });
165
- if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot token", response);
166
- return await response.json();
167
- };
168
-
169
- //#endregion
170
- //#region src/services/github/get-device-code.ts
171
- async function getDeviceCode() {
172
- const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, {
173
- method: "POST",
174
- headers: standardHeaders(),
175
- body: JSON.stringify({
176
- client_id: GITHUB_CLIENT_ID,
177
- scope: GITHUB_APP_SCOPES
178
- })
179
- });
180
- if (!response.ok) throw await HTTPError.fromResponse("Failed to get device code", response);
181
- return await response.json();
182
- }
183
-
184
- //#endregion
185
- //#region src/services/github/get-user.ts
186
- async function getGitHubUser() {
187
- const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { headers: {
188
- authorization: `token ${state.githubToken}`,
189
- ...standardHeaders()
190
- } });
191
- if (!response.ok) throw await HTTPError.fromResponse("Failed to get GitHub user", response);
192
- return await response.json();
193
- }
194
-
195
- //#endregion
196
- //#region src/services/copilot/get-models.ts
197
- const getModels = async () => {
198
- const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) });
199
- if (!response.ok) throw await HTTPError.fromResponse("Failed to get models", response);
200
- return await response.json();
201
- };
202
-
203
- //#endregion
204
- //#region src/services/get-vscode-version.ts
205
- const FALLBACK = "1.104.3";
206
- const GITHUB_API_URL = "https://api.github.com/repos/microsoft/vscode/releases/latest";
207
- async function getVSCodeVersion() {
208
- const controller = new AbortController();
209
- const timeout = setTimeout(() => {
210
- controller.abort();
211
- }, 5e3);
212
- try {
213
- const response = await fetch(GITHUB_API_URL, {
214
- signal: controller.signal,
215
- headers: {
216
- Accept: "application/vnd.github.v3+json",
217
- "User-Agent": "copilot-api"
218
- }
219
- });
220
- if (!response.ok) return FALLBACK;
221
- const version = (await response.json()).tag_name;
222
- if (version && /^\d+\.\d+\.\d+$/.test(version)) return version;
223
- return FALLBACK;
224
- } catch {
225
- return FALLBACK;
226
- } finally {
227
- clearTimeout(timeout);
228
- }
229
- }
230
-
231
- //#endregion
232
- //#region src/lib/utils.ts
233
- const sleep = (ms) => new Promise((resolve) => {
234
- setTimeout(resolve, ms);
235
- });
236
- const isNullish = (value) => value === null || value === void 0;
237
- async function cacheModels() {
238
- state.models = await getModels();
239
- }
240
- const cacheVSCodeVersion = async () => {
241
- const response = await getVSCodeVersion();
242
- state.vsCodeVersion = response;
243
- consola.info(`Using VSCode version: ${response}`);
244
- };
245
-
246
- //#endregion
247
- //#region src/services/github/poll-access-token.ts
248
- async function pollAccessToken(deviceCode) {
249
- const sleepDuration = (deviceCode.interval + 1) * 1e3;
250
- consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
251
- const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
252
- while (Date.now() < expiresAt) {
253
- const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
254
- method: "POST",
255
- headers: standardHeaders(),
256
- body: JSON.stringify({
257
- client_id: GITHUB_CLIENT_ID,
258
- device_code: deviceCode.device_code,
259
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
260
- })
261
- });
262
- if (!response.ok) {
263
- await sleep(sleepDuration);
264
- consola.error("Failed to poll access token:", await response.text());
265
- continue;
266
- }
267
- const json = await response.json();
268
- consola.debug("Polling access token response:", json);
269
- const { access_token } = json;
270
- if (access_token) return access_token;
271
- else await sleep(sleepDuration);
272
- }
273
- throw new Error("Device code expired. Please run the authentication flow again.");
274
- }
275
-
276
- //#endregion
277
- //#region src/lib/token.ts
278
- const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8");
279
- const writeGithubToken = (token) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token);
280
- const setupCopilotToken = async () => {
281
- const { token, refresh_in } = await getCopilotToken();
282
- state.copilotToken = token;
283
- consola.debug("GitHub Copilot Token fetched successfully!");
284
- if (state.showToken) consola.info("Copilot token:", token);
285
- const refreshInterval = (refresh_in - 60) * 1e3;
286
- setInterval(async () => {
287
- consola.debug("Refreshing Copilot token");
288
- try {
289
- const { token: token$1 } = await getCopilotToken();
290
- state.copilotToken = token$1;
291
- consola.debug("Copilot token refreshed");
292
- if (state.showToken) consola.info("Refreshed Copilot token:", token$1);
293
- } catch (error) {
294
- consola.error("Failed to refresh Copilot token (will retry on next interval):", error);
295
- }
296
- }, refreshInterval);
297
- };
298
- async function setupGitHubToken(options) {
299
- try {
300
- const githubToken = await readGithubToken();
301
- if (githubToken && !options?.force) {
302
- state.githubToken = githubToken;
303
- if (state.showToken) consola.info("GitHub token:", githubToken);
304
- await logUser();
305
- return;
306
- }
307
- consola.info("Not logged in, getting new access token");
308
- const response = await getDeviceCode();
309
- consola.debug("Device code response:", response);
310
- consola.info(`Please enter the code "${response.user_code}" in ${response.verification_uri}`);
311
- const token = await pollAccessToken(response);
312
- await writeGithubToken(token);
313
- state.githubToken = token;
314
- if (state.showToken) consola.info("GitHub token:", token);
315
- await logUser();
316
- } catch (error) {
317
- if (error instanceof HTTPError) {
318
- consola.error("Failed to get GitHub token:", error.responseText);
319
- throw error;
320
- }
321
- consola.error("Failed to get GitHub token:", error);
322
- throw error;
323
- }
324
- }
325
- async function logUser() {
326
- const user = await getGitHubUser();
327
- consola.info(`Logged in as ${user.login}`);
328
- }
329
-
330
- //#endregion
331
- //#region src/auth.ts
332
- async function runAuth(options) {
333
- if (options.verbose) {
334
- consola.level = 5;
335
- consola.info("Verbose logging enabled");
336
- }
337
- state.showToken = options.showToken;
338
- await ensurePaths();
339
- await setupGitHubToken({ force: true });
340
- consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
341
- }
342
- const auth = defineCommand({
343
- meta: {
344
- name: "auth",
345
- description: "Run GitHub auth flow without running the server"
346
- },
347
- args: {
348
- verbose: {
349
- alias: "v",
350
- type: "boolean",
351
- default: false,
352
- description: "Enable verbose logging"
353
- },
354
- "show-token": {
355
- type: "boolean",
356
- default: false,
357
- description: "Show GitHub token on auth"
358
- }
359
- },
360
- run({ args }) {
361
- return runAuth({
362
- verbose: args.verbose,
363
- showToken: args["show-token"]
364
- });
365
- }
366
- });
367
-
368
- //#endregion
369
- //#region src/services/github/get-copilot-usage.ts
370
- const getCopilotUsage = async () => {
371
- const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, { headers: githubHeaders(state) });
372
- if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot usage", response);
373
- return await response.json();
374
- };
375
-
376
- //#endregion
377
- //#region src/check-usage.ts
378
- const checkUsage = defineCommand({
379
- meta: {
380
- name: "check-usage",
381
- description: "Show current GitHub Copilot usage/quota information"
382
- },
383
- async run() {
384
- await ensurePaths();
385
- await setupGitHubToken();
386
- try {
387
- const usage = await getCopilotUsage();
388
- const premium = usage.quota_snapshots.premium_interactions;
389
- const premiumTotal = premium.entitlement;
390
- const premiumUsed = premiumTotal - premium.remaining;
391
- const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
392
- const premiumPercentRemaining = premium.percent_remaining;
393
- function summarizeQuota(name, snap) {
394
- if (!snap) return `${name}: N/A`;
395
- const total = snap.entitlement;
396
- const used = total - snap.remaining;
397
- const percentUsed = total > 0 ? used / total * 100 : 0;
398
- const percentRemaining = snap.percent_remaining;
399
- return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
400
- }
401
- const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
402
- const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
403
- const completionsLine = summarizeQuota("Completions", usage.quota_snapshots.completions);
404
- consola.box(`Copilot Usage (plan: ${usage.copilot_plan})\nQuota resets: ${usage.quota_reset_date}\n\nQuotas:\n ${premiumLine}\n ${chatLine}\n ${completionsLine}`);
405
- } catch (err) {
406
- consola.error("Failed to fetch Copilot usage:", err);
407
- process.exit(1);
408
- }
409
- }
410
- });
411
-
412
- //#endregion
413
- //#region src/debug.ts
414
- async function getPackageVersion() {
415
- try {
416
- const packageJsonPath = new URL("../package.json", import.meta.url).pathname;
417
- return JSON.parse(await fs.readFile(packageJsonPath)).version;
418
- } catch {
419
- return "unknown";
420
- }
421
- }
422
- function getRuntimeInfo() {
423
- const isBun = typeof Bun !== "undefined";
424
- return {
425
- name: isBun ? "bun" : "node",
426
- version: isBun ? Bun.version : process.version.slice(1),
427
- platform: os.platform(),
428
- arch: os.arch()
429
- };
430
- }
431
- async function checkTokenExists() {
432
- try {
433
- if (!(await fs.stat(PATHS.GITHUB_TOKEN_PATH)).isFile()) return false;
434
- return (await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")).trim().length > 0;
435
- } catch {
436
- return false;
437
- }
438
- }
439
- async function getDebugInfo() {
440
- const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
441
- return {
442
- version,
443
- runtime: getRuntimeInfo(),
444
- paths: {
445
- APP_DIR: PATHS.APP_DIR,
446
- GITHUB_TOKEN_PATH: PATHS.GITHUB_TOKEN_PATH
447
- },
448
- tokenExists
449
- };
450
- }
451
- function printDebugInfoPlain(info) {
452
- consola.info(`copilot-api debug
453
-
454
- Version: ${info.version}
455
- Runtime: ${info.runtime.name} ${info.runtime.version} (${info.runtime.platform} ${info.runtime.arch})
456
-
457
- Paths:
458
- - APP_DIR: ${info.paths.APP_DIR}
459
- - GITHUB_TOKEN_PATH: ${info.paths.GITHUB_TOKEN_PATH}
460
-
461
- Token exists: ${info.tokenExists ? "Yes" : "No"}`);
462
- }
463
- function printDebugInfoJson(info) {
464
- console.log(JSON.stringify(info, null, 2));
465
- }
466
- async function runDebug(options) {
467
- const debugInfo = await getDebugInfo();
468
- if (options.json) printDebugInfoJson(debugInfo);
469
- else printDebugInfoPlain(debugInfo);
470
- }
471
- const debug = defineCommand({
472
- meta: {
473
- name: "debug",
474
- description: "Print debug information about the application"
475
- },
476
- args: { json: {
477
- type: "boolean",
478
- default: false,
479
- description: "Output debug information as JSON"
480
- } },
481
- run({ args }) {
482
- return runDebug({ json: args.json });
483
- }
484
- });
485
-
486
- //#endregion
487
- //#region src/logout.ts
488
- async function runLogout() {
489
- try {
490
- await fs.unlink(PATHS.GITHUB_TOKEN_PATH);
491
- consola.success("Logged out successfully. GitHub token removed.");
492
- } catch (error) {
493
- if (error.code === "ENOENT") consola.info("No token found. Already logged out.");
494
- else {
495
- consola.error("Failed to remove token:", error);
496
- throw error;
497
- }
498
- }
499
- }
500
- const logout = defineCommand({
501
- meta: {
502
- name: "logout",
503
- description: "Remove stored GitHub token and log out"
504
- },
505
- run() {
506
- return runLogout();
507
- }
508
- });
509
-
510
- //#endregion
511
- //#region src/lib/history.ts
512
- function generateId$1() {
513
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
514
- }
515
- const historyState = {
516
- enabled: false,
517
- entries: [],
518
- sessions: /* @__PURE__ */ new Map(),
519
- currentSessionId: "",
520
- maxEntries: 1e3,
521
- sessionTimeoutMs: 1800 * 1e3
522
- };
523
- function initHistory(enabled, maxEntries) {
524
- historyState.enabled = enabled;
525
- historyState.maxEntries = maxEntries;
526
- historyState.entries = [];
527
- historyState.sessions = /* @__PURE__ */ new Map();
528
- historyState.currentSessionId = enabled ? generateId$1() : "";
529
- }
530
- function isHistoryEnabled() {
531
- return historyState.enabled;
532
- }
533
- function getCurrentSession(endpoint) {
534
- const now = Date.now();
535
- if (historyState.currentSessionId) {
536
- const session = historyState.sessions.get(historyState.currentSessionId);
537
- if (session && now - session.lastActivity < historyState.sessionTimeoutMs) {
538
- session.lastActivity = now;
539
- return historyState.currentSessionId;
540
- }
541
- }
542
- const sessionId = generateId$1();
543
- historyState.currentSessionId = sessionId;
544
- historyState.sessions.set(sessionId, {
545
- id: sessionId,
546
- startTime: now,
547
- lastActivity: now,
548
- requestCount: 0,
549
- totalInputTokens: 0,
550
- totalOutputTokens: 0,
551
- models: [],
552
- endpoint
553
- });
554
- return sessionId;
555
- }
556
- function recordRequest(endpoint, request) {
557
- if (!historyState.enabled) return "";
558
- const sessionId = getCurrentSession(endpoint);
559
- const session = historyState.sessions.get(sessionId);
560
- if (!session) return "";
561
- const entry = {
562
- id: generateId$1(),
563
- sessionId,
564
- timestamp: Date.now(),
565
- endpoint,
566
- request: {
567
- model: request.model,
568
- messages: request.messages,
569
- stream: request.stream,
570
- tools: request.tools,
571
- max_tokens: request.max_tokens,
572
- temperature: request.temperature,
573
- system: request.system
574
- }
575
- };
576
- historyState.entries.push(entry);
577
- session.requestCount++;
578
- if (!session.models.includes(request.model)) session.models.push(request.model);
579
- if (request.tools && request.tools.length > 0) {
580
- if (!session.toolsUsed) session.toolsUsed = [];
581
- for (const tool of request.tools) if (!session.toolsUsed.includes(tool.name)) session.toolsUsed.push(tool.name);
582
- }
583
- while (historyState.maxEntries > 0 && historyState.entries.length > historyState.maxEntries) {
584
- const removed = historyState.entries.shift();
585
- if (removed) {
586
- if (historyState.entries.filter((e) => e.sessionId === removed.sessionId).length === 0) historyState.sessions.delete(removed.sessionId);
587
- }
588
- }
589
- return entry.id;
590
- }
591
- function recordResponse(id, response, durationMs) {
592
- if (!historyState.enabled || !id) return;
593
- const entry = historyState.entries.find((e) => e.id === id);
594
- if (entry) {
595
- entry.response = response;
596
- entry.durationMs = durationMs;
597
- const session = historyState.sessions.get(entry.sessionId);
598
- if (session) {
599
- session.totalInputTokens += response.usage.input_tokens;
600
- session.totalOutputTokens += response.usage.output_tokens;
601
- session.lastActivity = Date.now();
602
- }
603
- }
604
- }
605
- function getHistory(options = {}) {
606
- const { page = 1, limit = 50, model, endpoint, success, from, to, search, sessionId } = options;
607
- let filtered = [...historyState.entries];
608
- if (sessionId) filtered = filtered.filter((e) => e.sessionId === sessionId);
609
- if (model) {
610
- const modelLower = model.toLowerCase();
611
- filtered = filtered.filter((e) => e.request.model.toLowerCase().includes(modelLower) || e.response?.model.toLowerCase().includes(modelLower));
612
- }
613
- if (endpoint) filtered = filtered.filter((e) => e.endpoint === endpoint);
614
- if (success !== void 0) filtered = filtered.filter((e) => e.response?.success === success);
615
- if (from) filtered = filtered.filter((e) => e.timestamp >= from);
616
- if (to) filtered = filtered.filter((e) => e.timestamp <= to);
617
- if (search) {
618
- const searchLower = search.toLowerCase();
619
- filtered = filtered.filter((e) => {
620
- const msgMatch = e.request.messages.some((m) => {
621
- if (typeof m.content === "string") return m.content.toLowerCase().includes(searchLower);
622
- if (Array.isArray(m.content)) return m.content.some((c) => c.text && c.text.toLowerCase().includes(searchLower));
623
- return false;
624
- });
625
- const respMatch = e.response?.content && typeof e.response.content.content === "string" && e.response.content.content.toLowerCase().includes(searchLower);
626
- const toolMatch = e.response?.toolCalls?.some((t) => t.name.toLowerCase().includes(searchLower));
627
- const sysMatch = e.request.system?.toLowerCase().includes(searchLower);
628
- return msgMatch || respMatch || toolMatch || sysMatch;
629
- });
630
- }
631
- filtered.sort((a, b) => b.timestamp - a.timestamp);
632
- const total = filtered.length;
633
- const totalPages = Math.ceil(total / limit);
634
- const start$1 = (page - 1) * limit;
635
- return {
636
- entries: filtered.slice(start$1, start$1 + limit),
637
- total,
638
- page,
639
- limit,
640
- totalPages
641
- };
642
- }
643
- function getEntry(id) {
644
- return historyState.entries.find((e) => e.id === id);
645
- }
646
- function getSessions() {
647
- const sessions = Array.from(historyState.sessions.values()).sort((a, b) => b.lastActivity - a.lastActivity);
648
- return {
649
- sessions,
650
- total: sessions.length
651
- };
652
- }
653
- function getSession(id) {
654
- return historyState.sessions.get(id);
655
- }
656
- function getSessionEntries(sessionId) {
657
- return historyState.entries.filter((e) => e.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
658
- }
659
- function clearHistory() {
660
- historyState.entries = [];
661
- historyState.sessions = /* @__PURE__ */ new Map();
662
- historyState.currentSessionId = generateId$1();
663
- }
664
- function deleteSession(sessionId) {
665
- if (!historyState.sessions.has(sessionId)) return false;
666
- historyState.entries = historyState.entries.filter((e) => e.sessionId !== sessionId);
667
- historyState.sessions.delete(sessionId);
668
- if (historyState.currentSessionId === sessionId) historyState.currentSessionId = generateId$1();
669
- return true;
670
- }
671
- function getStats() {
672
- const entries = historyState.entries;
673
- const modelDist = {};
674
- const endpointDist = {};
675
- const hourlyActivity = {};
676
- let totalInput = 0;
677
- let totalOutput = 0;
678
- let totalDuration = 0;
679
- let durationCount = 0;
680
- let successCount = 0;
681
- let failCount = 0;
682
- for (const entry of entries) {
683
- const model = entry.response?.model || entry.request.model;
684
- modelDist[model] = (modelDist[model] || 0) + 1;
685
- endpointDist[entry.endpoint] = (endpointDist[entry.endpoint] || 0) + 1;
686
- const hour = new Date(entry.timestamp).toISOString().slice(0, 13);
687
- hourlyActivity[hour] = (hourlyActivity[hour] || 0) + 1;
688
- if (entry.response) {
689
- if (entry.response.success) successCount++;
690
- else failCount++;
691
- totalInput += entry.response.usage.input_tokens;
692
- totalOutput += entry.response.usage.output_tokens;
693
- }
694
- if (entry.durationMs) {
695
- totalDuration += entry.durationMs;
696
- durationCount++;
697
- }
698
- }
699
- const recentActivity = Object.entries(hourlyActivity).sort(([a], [b]) => a.localeCompare(b)).slice(-24).map(([hour, count]) => ({
700
- hour,
701
- count
702
- }));
703
- const now = Date.now();
704
- let activeSessions = 0;
705
- for (const session of historyState.sessions.values()) if (now - session.lastActivity < historyState.sessionTimeoutMs) activeSessions++;
706
- return {
707
- totalRequests: entries.length,
708
- successfulRequests: successCount,
709
- failedRequests: failCount,
710
- totalInputTokens: totalInput,
711
- totalOutputTokens: totalOutput,
712
- averageDurationMs: durationCount > 0 ? totalDuration / durationCount : 0,
713
- modelDistribution: modelDist,
714
- endpointDistribution: endpointDist,
715
- recentActivity,
716
- activeSessions
717
- };
718
- }
719
- function exportHistory(format = "json") {
720
- if (format === "json") return JSON.stringify({
721
- sessions: Array.from(historyState.sessions.values()),
722
- entries: historyState.entries
723
- }, null, 2);
724
- const headers = [
725
- "id",
726
- "session_id",
727
- "timestamp",
728
- "endpoint",
729
- "request_model",
730
- "message_count",
731
- "stream",
732
- "success",
733
- "response_model",
734
- "input_tokens",
735
- "output_tokens",
736
- "duration_ms",
737
- "stop_reason",
738
- "error"
739
- ];
740
- const rows = historyState.entries.map((e) => [
741
- e.id,
742
- e.sessionId,
743
- new Date(e.timestamp).toISOString(),
744
- e.endpoint,
745
- e.request.model,
746
- e.request.messages.length,
747
- e.request.stream,
748
- e.response?.success ?? "",
749
- e.response?.model ?? "",
750
- e.response?.usage.input_tokens ?? "",
751
- e.response?.usage.output_tokens ?? "",
752
- e.durationMs ?? "",
753
- e.response?.stop_reason ?? "",
754
- e.response?.error ?? ""
755
- ]);
756
- return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
757
- }
758
-
759
- //#endregion
760
- //#region src/lib/proxy.ts
761
- function initProxyFromEnv() {
762
- if (typeof Bun !== "undefined") return;
763
- try {
764
- const direct = new Agent();
765
- const proxies = /* @__PURE__ */ new Map();
766
- setGlobalDispatcher({
767
- dispatch(options, handler) {
768
- try {
769
- const origin = typeof options.origin === "string" ? new URL(options.origin) : options.origin;
770
- const raw = getProxyForUrl(origin.toString());
771
- const proxyUrl = raw && raw.length > 0 ? raw : void 0;
772
- if (!proxyUrl) {
773
- consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
774
- return direct.dispatch(options, handler);
775
- }
776
- let agent = proxies.get(proxyUrl);
777
- if (!agent) {
778
- agent = new ProxyAgent(proxyUrl);
779
- proxies.set(proxyUrl, agent);
780
- }
781
- let label = proxyUrl;
782
- try {
783
- const u = new URL(proxyUrl);
784
- label = `${u.protocol}//${u.host}`;
785
- } catch {}
786
- consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`);
787
- return agent.dispatch(options, handler);
788
- } catch {
789
- return direct.dispatch(options, handler);
790
- }
791
- },
792
- close() {
793
- return direct.close();
794
- },
795
- destroy() {
796
- return direct.destroy();
797
- }
798
- });
799
- consola.debug("HTTP proxy configured from environment (per-URL)");
800
- } catch (err) {
801
- consola.debug("Proxy setup skipped:", err);
802
- }
803
- }
804
-
805
- //#endregion
806
- //#region src/lib/shell.ts
807
- function getShell() {
808
- const { platform, ppid, env } = process$1;
809
- if (platform === "win32") {
810
- try {
811
- const command = `wmic process get ParentProcessId,Name | findstr "${ppid}"`;
812
- if (execSync(command, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
813
- } catch {
814
- return "cmd";
815
- }
816
- return "cmd";
817
- } else {
818
- const shellPath = env.SHELL;
819
- if (shellPath) {
820
- if (shellPath.endsWith("zsh")) return "zsh";
821
- if (shellPath.endsWith("fish")) return "fish";
822
- if (shellPath.endsWith("bash")) return "bash";
823
- }
824
- return "sh";
825
- }
826
- }
827
- /**
828
- * Generates a copy-pasteable script to set multiple environment variables
829
- * and run a subsequent command.
830
- * @param {EnvVars} envVars - An object of environment variables to set.
831
- * @param {string} commandToRun - The command to run after setting the variables.
832
- * @returns {string} The formatted script string.
833
- */
834
- function generateEnvScript(envVars, commandToRun = "") {
835
- const shell = getShell();
836
- const filteredEnvVars = Object.entries(envVars).filter(([, value]) => value !== void 0);
837
- let commandBlock;
838
- switch (shell) {
839
- case "powershell":
840
- commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = "${value.replaceAll("\"", "`\"")}"`).join("; ");
841
- break;
842
- case "cmd":
843
- commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
844
- break;
845
- case "fish":
846
- commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} "${value.replaceAll("\"", String.raw`\"`)}"`).join("; ");
847
- break;
848
- default: {
849
- const assignments = filteredEnvVars.map(([key, value]) => `${key}="${value.replaceAll("\"", String.raw`\"`)}"`).join(" ");
850
- commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
851
- break;
852
- }
853
- }
854
- if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : " && "}${commandToRun}`;
855
- return commandBlock || commandToRun;
856
- }
857
-
858
- //#endregion
859
- //#region src/lib/tui/console-renderer.ts
860
- function formatDuration$1(ms) {
861
- if (ms < 1e3) return `${ms}ms`;
862
- return `${(ms / 1e3).toFixed(1)}s`;
863
- }
864
- function formatNumber$1(n) {
865
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
866
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
867
- return String(n);
868
- }
869
- function formatTokens$1(input, output) {
870
- if (input === void 0 || output === void 0) return "-";
871
- return `${formatNumber$1(input)}/${formatNumber$1(output)}`;
872
- }
873
- /**
874
- * Console renderer that shows request lifecycle
875
- * Start: METHOD /path model-name
876
- * Complete: METHOD /path 200 1.2s 1.5K/500 model-name
877
- */
878
- var ConsoleRenderer = class {
879
- activeRequests = /* @__PURE__ */ new Map();
880
- showActive;
881
- constructor(options) {
882
- this.showActive = options?.showActive ?? true;
883
- }
884
- onRequestStart(request) {
885
- this.activeRequests.set(request.id, request);
886
- if (this.showActive) {
887
- const modelInfo = request.model ? ` ${request.model}` : "";
888
- const queueInfo = request.queuePosition !== void 0 && request.queuePosition > 0 ? ` [q#${request.queuePosition}]` : "";
889
- consola.log(`[....] ${request.method} ${request.path}${modelInfo}${queueInfo}`);
890
- }
891
- }
892
- onRequestUpdate(id, update) {
893
- const request = this.activeRequests.get(id);
894
- if (!request) return;
895
- Object.assign(request, update);
896
- if (this.showActive && update.status === "streaming") {
897
- const modelInfo = request.model ? ` ${request.model}` : "";
898
- consola.log(`[<-->] ${request.method} ${request.path}${modelInfo} streaming...`);
899
- }
900
- }
901
- onRequestComplete(request) {
902
- this.activeRequests.delete(request.id);
903
- const status = request.statusCode ?? 0;
904
- const duration = formatDuration$1(request.durationMs ?? 0);
905
- const tokens = request.model ? formatTokens$1(request.inputTokens, request.outputTokens) : "";
906
- const modelInfo = request.model ? ` ${request.model}` : "";
907
- const isError = request.status === "error" || status >= 400;
908
- const prefix = isError ? "[FAIL]" : "[ OK ]";
909
- const tokensPart = tokens ? ` ${tokens}` : "";
910
- const content = `${prefix} ${request.method} ${request.path} ${status} ${duration}${tokensPart}${modelInfo}`;
911
- if (isError) {
912
- const errorInfo = request.error ? `: ${request.error}` : "";
913
- consola.log(content + errorInfo);
914
- } else consola.log(content);
915
- }
916
- destroy() {
917
- this.activeRequests.clear();
918
- }
919
- };
920
-
921
- //#endregion
922
- //#region src/lib/tui/fullscreen-renderer.tsx
923
- const tuiState = {
924
- activeRequests: /* @__PURE__ */ new Map(),
925
- completedRequests: [],
926
- errorRequests: []
927
- };
928
- const listeners = [];
929
- function notifyListeners() {
930
- for (const listener of listeners) listener();
931
- }
932
- function formatDuration(ms) {
933
- if (ms < 1e3) return `${ms}ms`;
934
- return `${(ms / 1e3).toFixed(1)}s`;
935
- }
936
- function formatNumber(n) {
937
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
938
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
939
- return String(n);
940
- }
941
- function formatTokens(input, output) {
942
- if (input === void 0 || output === void 0) return "-";
943
- return `${formatNumber(input)}/${formatNumber(output)}`;
944
- }
945
- function getElapsedTime(startTime) {
946
- return formatDuration(Date.now() - startTime);
947
- }
948
- function TabHeader({ currentTab, counts }) {
949
- const tabs = [
950
- {
951
- key: "active",
952
- label: "Active",
953
- count: counts.active
954
- },
955
- {
956
- key: "completed",
957
- label: "Completed",
958
- count: counts.completed
959
- },
960
- {
961
- key: "errors",
962
- label: "Errors",
963
- count: counts.errors
964
- }
965
- ];
966
- return /* @__PURE__ */ jsxs(Box, {
967
- borderStyle: "single",
968
- paddingX: 1,
969
- children: [tabs.map((tab, idx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [idx > 0 && /* @__PURE__ */ jsx(Text, { children: " │ " }), /* @__PURE__ */ jsxs(Text, {
970
- bold: currentTab === tab.key,
971
- color: currentTab === tab.key ? "cyan" : void 0,
972
- inverse: currentTab === tab.key,
973
- children: [
974
- " ",
975
- "[",
976
- idx + 1,
977
- "] ",
978
- tab.label,
979
- " (",
980
- tab.count,
981
- ")",
982
- " "
983
- ]
984
- })] }, tab.key)), /* @__PURE__ */ jsx(Text, {
985
- dimColor: true,
986
- children: " │ Press 1/2/3 to switch tabs, q to quit"
987
- })]
988
- });
989
- }
990
- function getStatusColor(status) {
991
- if (status === "streaming") return "yellow";
992
- if (status === "queued") return "gray";
993
- return "blue";
994
- }
995
- function getStatusIcon(status) {
996
- if (status === "streaming") return "⟳";
997
- if (status === "queued") return "ā—·";
998
- return "ā—";
999
- }
1000
- function ActiveRequestRow({ request }) {
1001
- const [, setTick] = useState(0);
1002
- useEffect(() => {
1003
- const interval = setInterval(() => setTick((t) => t + 1), 1e3);
1004
- return () => clearInterval(interval);
1005
- }, []);
1006
- const statusColor = getStatusColor(request.status);
1007
- const statusIcon = getStatusIcon(request.status);
1008
- return /* @__PURE__ */ jsxs(Box, { children: [
1009
- /* @__PURE__ */ jsxs(Text, {
1010
- color: statusColor,
1011
- children: [statusIcon, " "]
1012
- }),
1013
- /* @__PURE__ */ jsx(Text, {
1014
- bold: true,
1015
- children: request.method
1016
- }),
1017
- /* @__PURE__ */ jsxs(Text, { children: [
1018
- " ",
1019
- request.path,
1020
- " "
1021
- ] }),
1022
- /* @__PURE__ */ jsxs(Text, {
1023
- dimColor: true,
1024
- children: [getElapsedTime(request.startTime), " "]
1025
- }),
1026
- request.queuePosition !== void 0 && request.queuePosition > 0 && /* @__PURE__ */ jsxs(Text, {
1027
- color: "gray",
1028
- children: [
1029
- "[queue #",
1030
- request.queuePosition,
1031
- "] "
1032
- ]
1033
- }),
1034
- /* @__PURE__ */ jsx(Text, {
1035
- color: "magenta",
1036
- children: request.model
1037
- })
1038
- ] });
1039
- }
1040
- function CompletedRequestRow({ request }) {
1041
- const isError = request.status === "error" || (request.statusCode ?? 0) >= 400;
1042
- return /* @__PURE__ */ jsxs(Box, { children: [
1043
- /* @__PURE__ */ jsxs(Text, {
1044
- color: isError ? "red" : "green",
1045
- children: [isError ? "āœ—" : "āœ“", " "]
1046
- }),
1047
- /* @__PURE__ */ jsx(Text, {
1048
- bold: true,
1049
- children: request.method
1050
- }),
1051
- /* @__PURE__ */ jsxs(Text, { children: [
1052
- " ",
1053
- request.path,
1054
- " "
1055
- ] }),
1056
- /* @__PURE__ */ jsxs(Text, {
1057
- color: isError ? "red" : "green",
1058
- children: [request.statusCode ?? "-", " "]
1059
- }),
1060
- /* @__PURE__ */ jsxs(Text, {
1061
- dimColor: true,
1062
- children: [formatDuration(request.durationMs ?? 0), " "]
1063
- }),
1064
- /* @__PURE__ */ jsxs(Text, { children: [formatTokens(request.inputTokens, request.outputTokens), " "] }),
1065
- /* @__PURE__ */ jsx(Text, {
1066
- color: "magenta",
1067
- children: request.model
1068
- })
1069
- ] });
1070
- }
1071
- function ErrorRequestRow({ request }) {
1072
- return /* @__PURE__ */ jsxs(Box, {
1073
- flexDirection: "column",
1074
- children: [/* @__PURE__ */ jsxs(Box, { children: [
1075
- /* @__PURE__ */ jsx(Text, {
1076
- color: "red",
1077
- children: "āœ— "
1078
- }),
1079
- /* @__PURE__ */ jsx(Text, {
1080
- bold: true,
1081
- children: request.method
1082
- }),
1083
- /* @__PURE__ */ jsxs(Text, { children: [
1084
- " ",
1085
- request.path,
1086
- " "
1087
- ] }),
1088
- /* @__PURE__ */ jsxs(Text, {
1089
- color: "red",
1090
- children: [request.statusCode ?? "-", " "]
1091
- }),
1092
- /* @__PURE__ */ jsxs(Text, {
1093
- dimColor: true,
1094
- children: [formatDuration(request.durationMs ?? 0), " "]
1095
- }),
1096
- /* @__PURE__ */ jsx(Text, {
1097
- color: "magenta",
1098
- children: request.model
1099
- })
1100
- ] }), request.error && /* @__PURE__ */ jsx(Box, {
1101
- marginLeft: 2,
1102
- children: /* @__PURE__ */ jsxs(Text, {
1103
- color: "red",
1104
- dimColor: true,
1105
- children: ["└─ ", request.error]
1106
- })
1107
- })]
1108
- });
1109
- }
1110
- function ContentPanel({ currentTab, activeList, completedList, errorList, contentHeight }) {
1111
- if (currentTab === "active") {
1112
- if (activeList.length === 0) return /* @__PURE__ */ jsx(Text, {
1113
- dimColor: true,
1114
- children: "No active requests"
1115
- });
1116
- return /* @__PURE__ */ jsx(Fragment, { children: activeList.slice(0, contentHeight).map((req) => /* @__PURE__ */ jsx(ActiveRequestRow, { request: req }, req.id)) });
1117
- }
1118
- if (currentTab === "completed") {
1119
- if (completedList.length === 0) return /* @__PURE__ */ jsx(Text, {
1120
- dimColor: true,
1121
- children: "No completed requests"
1122
- });
1123
- return /* @__PURE__ */ jsx(Fragment, { children: completedList.slice(-contentHeight).reverse().map((req) => /* @__PURE__ */ jsx(CompletedRequestRow, { request: req }, req.id)) });
1124
- }
1125
- if (errorList.length === 0) return /* @__PURE__ */ jsx(Text, {
1126
- dimColor: true,
1127
- children: "No errors"
1128
- });
1129
- return /* @__PURE__ */ jsx(Fragment, { children: errorList.slice(-contentHeight).reverse().map((req) => /* @__PURE__ */ jsx(ErrorRequestRow, { request: req }, req.id)) });
1130
- }
1131
- function TuiApp() {
1132
- const [currentTab, setCurrentTab] = useState("active");
1133
- const [, forceUpdate] = useState(0);
1134
- const { stdout } = useStdout();
1135
- useEffect(() => {
1136
- const listener = () => forceUpdate((n) => n + 1);
1137
- listeners.push(listener);
1138
- return () => {
1139
- const idx = listeners.indexOf(listener);
1140
- if (idx !== -1) listeners.splice(idx, 1);
1141
- };
1142
- }, []);
1143
- useInput((input, key) => {
1144
- switch (input) {
1145
- case "1":
1146
- setCurrentTab("active");
1147
- break;
1148
- case "2":
1149
- setCurrentTab("completed");
1150
- break;
1151
- case "3":
1152
- setCurrentTab("errors");
1153
- break;
1154
- default: if (input === "q" || key.ctrl && input === "c") process.exit(0);
1155
- }
1156
- });
1157
- const activeList = Array.from(tuiState.activeRequests.values());
1158
- const completedList = tuiState.completedRequests;
1159
- const errorList = tuiState.errorRequests;
1160
- const counts = {
1161
- active: activeList.length,
1162
- completed: completedList.length,
1163
- errors: errorList.length
1164
- };
1165
- const terminalHeight = stdout.rows || 24;
1166
- const contentHeight = terminalHeight - 3 - 1 - 2;
1167
- return /* @__PURE__ */ jsxs(Box, {
1168
- flexDirection: "column",
1169
- height: terminalHeight,
1170
- children: [
1171
- /* @__PURE__ */ jsx(TabHeader, {
1172
- currentTab,
1173
- counts
1174
- }),
1175
- /* @__PURE__ */ jsx(Box, {
1176
- flexDirection: "column",
1177
- height: contentHeight,
1178
- borderStyle: "single",
1179
- paddingX: 1,
1180
- overflow: "hidden",
1181
- children: /* @__PURE__ */ jsx(ContentPanel, {
1182
- currentTab,
1183
- activeList,
1184
- completedList,
1185
- errorList,
1186
- contentHeight
1187
- })
1188
- }),
1189
- /* @__PURE__ */ jsx(Box, {
1190
- paddingX: 1,
1191
- children: /* @__PURE__ */ jsxs(Text, {
1192
- dimColor: true,
1193
- children: [
1194
- "copilot-api │ Active: ",
1195
- counts.active,
1196
- " │ Completed: ",
1197
- counts.completed,
1198
- " ",
1199
- "│ Errors: ",
1200
- counts.errors
1201
- ]
1202
- })
1203
- })
1204
- ]
1205
- });
1206
- }
1207
- /**
1208
- * Fullscreen TUI renderer using Ink
1209
- * Provides interactive terminal interface with tabs
1210
- */
1211
- var FullscreenRenderer = class {
1212
- inkInstance = null;
1213
- maxHistory = 100;
1214
- constructor(options) {
1215
- if (options?.maxHistory !== void 0) this.maxHistory = options.maxHistory;
1216
- }
1217
- start() {
1218
- if (this.inkInstance) return;
1219
- this.inkInstance = render(/* @__PURE__ */ jsx(TuiApp, {}), {});
1220
- }
1221
- onRequestStart(request) {
1222
- tuiState.activeRequests.set(request.id, { ...request });
1223
- notifyListeners();
1224
- }
1225
- onRequestUpdate(id, update) {
1226
- const request = tuiState.activeRequests.get(id);
1227
- if (!request) return;
1228
- Object.assign(request, update);
1229
- notifyListeners();
1230
- }
1231
- onRequestComplete(request) {
1232
- tuiState.activeRequests.delete(request.id);
1233
- if (request.status === "error" || (request.statusCode ?? 0) >= 400) {
1234
- tuiState.errorRequests.push({ ...request });
1235
- while (tuiState.errorRequests.length > this.maxHistory) tuiState.errorRequests.shift();
1236
- }
1237
- tuiState.completedRequests.push({ ...request });
1238
- while (tuiState.completedRequests.length > this.maxHistory) tuiState.completedRequests.shift();
1239
- notifyListeners();
1240
- }
1241
- destroy() {
1242
- if (this.inkInstance) {
1243
- this.inkInstance.unmount();
1244
- this.inkInstance = null;
1245
- }
1246
- tuiState.activeRequests.clear();
1247
- tuiState.completedRequests = [];
1248
- tuiState.errorRequests = [];
1249
- }
1250
- };
1251
-
1252
- //#endregion
1253
- //#region src/lib/tui/tracker.ts
1254
- function generateId() {
1255
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
1256
- }
1257
- var RequestTracker = class {
1258
- requests = /* @__PURE__ */ new Map();
1259
- renderer = null;
1260
- completedQueue = [];
1261
- historySize = 5;
1262
- completedDisplayMs = 2e3;
1263
- setRenderer(renderer) {
1264
- this.renderer = renderer;
1265
- }
1266
- setOptions(options) {
1267
- if (options.historySize !== void 0) this.historySize = options.historySize;
1268
- if (options.completedDisplayMs !== void 0) this.completedDisplayMs = options.completedDisplayMs;
1269
- }
1270
- /**
1271
- * Start tracking a new request
1272
- * Returns the tracking ID
1273
- */
1274
- startRequest(method, path$1, model) {
1275
- const id = generateId();
1276
- const request = {
1277
- id,
1278
- method,
1279
- path: path$1,
1280
- model,
1281
- startTime: Date.now(),
1282
- status: "executing"
1283
- };
1284
- this.requests.set(id, request);
1285
- this.renderer?.onRequestStart(request);
1286
- return id;
1287
- }
1288
- /**
1289
- * Update request status
1290
- */
1291
- updateRequest(id, update) {
1292
- const request = this.requests.get(id);
1293
- if (!request) return;
1294
- if (update.status !== void 0) request.status = update.status;
1295
- if (update.statusCode !== void 0) request.statusCode = update.statusCode;
1296
- if (update.durationMs !== void 0) request.durationMs = update.durationMs;
1297
- if (update.inputTokens !== void 0) request.inputTokens = update.inputTokens;
1298
- if (update.outputTokens !== void 0) request.outputTokens = update.outputTokens;
1299
- if (update.error !== void 0) request.error = update.error;
1300
- if (update.queuePosition !== void 0) request.queuePosition = update.queuePosition;
1301
- this.renderer?.onRequestUpdate(id, update);
1302
- }
1303
- /**
1304
- * Mark request as completed
1305
- */
1306
- completeRequest(id, statusCode, usage) {
1307
- const request = this.requests.get(id);
1308
- if (!request) return;
1309
- request.status = statusCode >= 200 && statusCode < 400 ? "completed" : "error";
1310
- request.statusCode = statusCode;
1311
- request.durationMs = Date.now() - request.startTime;
1312
- if (usage) {
1313
- request.inputTokens = usage.inputTokens;
1314
- request.outputTokens = usage.outputTokens;
1315
- }
1316
- this.renderer?.onRequestComplete(request);
1317
- this.requests.delete(id);
1318
- this.completedQueue.push(request);
1319
- while (this.completedQueue.length > this.historySize) this.completedQueue.shift();
1320
- setTimeout(() => {
1321
- const idx = this.completedQueue.indexOf(request);
1322
- if (idx !== -1) this.completedQueue.splice(idx, 1);
1323
- }, this.completedDisplayMs);
1324
- }
1325
- /**
1326
- * Mark request as failed with error
1327
- */
1328
- failRequest(id, error) {
1329
- const request = this.requests.get(id);
1330
- if (!request) return;
1331
- request.status = "error";
1332
- request.error = error;
1333
- request.durationMs = Date.now() - request.startTime;
1334
- this.renderer?.onRequestComplete(request);
1335
- this.requests.delete(id);
1336
- this.completedQueue.push(request);
1337
- while (this.completedQueue.length > this.historySize) this.completedQueue.shift();
1338
- }
1339
- /**
1340
- * Get all active requests
1341
- */
1342
- getActiveRequests() {
1343
- return Array.from(this.requests.values());
1344
- }
1345
- /**
1346
- * Get recently completed requests
1347
- */
1348
- getCompletedRequests() {
1349
- return [...this.completedQueue];
1350
- }
1351
- /**
1352
- * Get request by ID
1353
- */
1354
- getRequest(id) {
1355
- return this.requests.get(id);
1356
- }
1357
- /**
1358
- * Clear all tracked requests
1359
- */
1360
- clear() {
1361
- this.requests.clear();
1362
- this.completedQueue = [];
1363
- }
1364
- };
1365
- const requestTracker = new RequestTracker();
1366
-
1367
- //#endregion
1368
- //#region src/lib/tui/middleware.ts
1369
- /**
1370
- * Custom logger middleware that tracks requests through the TUI system
1371
- * Shows single-line output: METHOD /path 200 1.2s 1.5K/500 model-name
1372
- *
1373
- * For streaming responses (SSE), the handler is responsible for calling
1374
- * completeRequest after the stream finishes.
1375
- */
1376
- function tuiLogger() {
1377
- return async (c, next) => {
1378
- const method = c.req.method;
1379
- const path$1 = c.req.path;
1380
- const trackingId = requestTracker.startRequest(method, path$1, "");
1381
- c.set("trackingId", trackingId);
1382
- try {
1383
- await next();
1384
- if ((c.res.headers.get("content-type") ?? "").includes("text/event-stream")) return;
1385
- const status = c.res.status;
1386
- const inputTokens = c.res.headers.get("x-input-tokens");
1387
- const outputTokens = c.res.headers.get("x-output-tokens");
1388
- const model = c.res.headers.get("x-model");
1389
- if (model) {
1390
- const request = requestTracker.getRequest(trackingId);
1391
- if (request) request.model = model;
1392
- }
1393
- requestTracker.completeRequest(trackingId, status, inputTokens && outputTokens ? {
1394
- inputTokens: Number.parseInt(inputTokens, 10),
1395
- outputTokens: Number.parseInt(outputTokens, 10)
1396
- } : void 0);
1397
- } catch (error) {
1398
- requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Unknown error");
1399
- throw error;
1400
- }
1401
- };
1402
- }
1403
-
1404
- //#endregion
1405
- //#region src/lib/tui/index.ts
1406
- /**
1407
- * Initialize the TUI system
1408
- * @param options.mode - "console" for simple log output (default), "fullscreen" for interactive TUI
1409
- */
1410
- function initTui(options) {
1411
- const enabled = options?.enabled ?? process.stdout.isTTY;
1412
- const mode = options?.mode ?? "console";
1413
- if (enabled) if (mode === "fullscreen") {
1414
- const renderer = new FullscreenRenderer({ maxHistory: options?.historySize ?? 100 });
1415
- requestTracker.setRenderer(renderer);
1416
- renderer.start();
1417
- } else {
1418
- const renderer = new ConsoleRenderer();
1419
- requestTracker.setRenderer(renderer);
1420
- }
1421
- if (options?.historySize !== void 0 || options?.completedDisplayMs !== void 0) requestTracker.setOptions({
1422
- historySize: options.historySize,
1423
- completedDisplayMs: options.completedDisplayMs
1424
- });
1425
- }
1426
-
1427
- //#endregion
1428
- //#region src/lib/approval.ts
1429
- const awaitApproval = async () => {
1430
- if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
1431
- };
1432
-
1433
- //#endregion
1434
- //#region src/lib/tokenizer.ts
1435
- const ENCODING_MAP = {
1436
- o200k_base: () => import("gpt-tokenizer/encoding/o200k_base"),
1437
- cl100k_base: () => import("gpt-tokenizer/encoding/cl100k_base"),
1438
- p50k_base: () => import("gpt-tokenizer/encoding/p50k_base"),
1439
- p50k_edit: () => import("gpt-tokenizer/encoding/p50k_edit"),
1440
- r50k_base: () => import("gpt-tokenizer/encoding/r50k_base")
1441
- };
1442
- const encodingCache = /* @__PURE__ */ new Map();
1443
- /**
1444
- * Calculate tokens for tool calls
1445
- */
1446
- const calculateToolCallsTokens = (toolCalls, encoder, constants) => {
1447
- let tokens = 0;
1448
- for (const toolCall of toolCalls) {
1449
- tokens += constants.funcInit;
1450
- tokens += encoder.encode(JSON.stringify(toolCall)).length;
1451
- }
1452
- tokens += constants.funcEnd;
1453
- return tokens;
1454
- };
1455
- /**
1456
- * Calculate tokens for content parts
1457
- */
1458
- const calculateContentPartsTokens = (contentParts, encoder) => {
1459
- let tokens = 0;
1460
- for (const part of contentParts) if (part.type === "image_url") tokens += encoder.encode(part.image_url.url).length + 85;
1461
- else if (part.text) tokens += encoder.encode(part.text).length;
1462
- return tokens;
1463
- };
1464
- /**
1465
- * Calculate tokens for a single message
1466
- */
1467
- const calculateMessageTokens = (message, encoder, constants) => {
1468
- const tokensPerMessage = 3;
1469
- const tokensPerName = 1;
1470
- let tokens = tokensPerMessage;
1471
- for (const [key, value] of Object.entries(message)) {
1472
- if (typeof value === "string") tokens += encoder.encode(value).length;
1473
- if (key === "name") tokens += tokensPerName;
1474
- if (key === "tool_calls") tokens += calculateToolCallsTokens(value, encoder, constants);
1475
- if (key === "content" && Array.isArray(value)) tokens += calculateContentPartsTokens(value, encoder);
1476
- }
1477
- return tokens;
1478
- };
1479
- /**
1480
- * Calculate tokens using custom algorithm
1481
- */
1482
- const calculateTokens = (messages, encoder, constants) => {
1483
- if (messages.length === 0) return 0;
1484
- let numTokens = 0;
1485
- for (const message of messages) numTokens += calculateMessageTokens(message, encoder, constants);
1486
- numTokens += 3;
1487
- return numTokens;
1488
- };
1489
- /**
1490
- * Get the corresponding encoder module based on encoding type
1491
- */
1492
- const getEncodeChatFunction = async (encoding) => {
1493
- if (encodingCache.has(encoding)) {
1494
- const cached = encodingCache.get(encoding);
1495
- if (cached) return cached;
1496
- }
1497
- const supportedEncoding = encoding;
1498
- if (!(supportedEncoding in ENCODING_MAP)) {
1499
- const fallbackModule = await ENCODING_MAP.o200k_base();
1500
- encodingCache.set(encoding, fallbackModule);
1501
- return fallbackModule;
1502
- }
1503
- const encodingModule = await ENCODING_MAP[supportedEncoding]();
1504
- encodingCache.set(encoding, encodingModule);
1505
- return encodingModule;
1506
- };
1507
- /**
1508
- * Get tokenizer type from model information
1509
- */
1510
- const getTokenizerFromModel = (model) => {
1511
- return model.capabilities.tokenizer || "o200k_base";
1512
- };
1513
- /**
1514
- * Get model-specific constants for token calculation.
1515
- * These values are empirically determined based on OpenAI's function calling token overhead.
1516
- * - funcInit: Tokens for initializing a function definition
1517
- * - propInit: Tokens for initializing the properties section
1518
- * - propKey: Tokens per property key
1519
- * - enumInit: Token adjustment when enum is present (negative because type info is replaced)
1520
- * - enumItem: Tokens per enum value
1521
- * - funcEnd: Tokens for closing the function definition
1522
- */
1523
- const getModelConstants = (model) => {
1524
- return model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
1525
- funcInit: 10,
1526
- propInit: 3,
1527
- propKey: 3,
1528
- enumInit: -3,
1529
- enumItem: 3,
1530
- funcEnd: 12
1531
- } : {
1532
- funcInit: 7,
1533
- propInit: 3,
1534
- propKey: 3,
1535
- enumInit: -3,
1536
- enumItem: 3,
1537
- funcEnd: 12
1538
- };
1539
- };
1540
- /**
1541
- * Calculate tokens for a single parameter
1542
- */
1543
- const calculateParameterTokens = (key, prop, context) => {
1544
- const { encoder, constants } = context;
1545
- let tokens = constants.propKey;
1546
- if (typeof prop !== "object" || prop === null) return tokens;
1547
- const param = prop;
1548
- const paramName = key;
1549
- const paramType = param.type || "string";
1550
- let paramDesc = param.description || "";
1551
- if (param.enum && Array.isArray(param.enum)) {
1552
- tokens += constants.enumInit;
1553
- for (const item of param.enum) {
1554
- tokens += constants.enumItem;
1555
- tokens += encoder.encode(String(item)).length;
1556
- }
1557
- }
1558
- if (paramDesc.endsWith(".")) paramDesc = paramDesc.slice(0, -1);
1559
- const line = `${paramName}:${paramType}:${paramDesc}`;
1560
- tokens += encoder.encode(line).length;
1561
- const excludedKeys = new Set([
1562
- "type",
1563
- "description",
1564
- "enum"
1565
- ]);
1566
- for (const propertyName of Object.keys(param)) if (!excludedKeys.has(propertyName)) {
1567
- const propertyValue = param[propertyName];
1568
- const propertyText = typeof propertyValue === "string" ? propertyValue : JSON.stringify(propertyValue);
1569
- tokens += encoder.encode(`${propertyName}:${propertyText}`).length;
1570
- }
1571
- return tokens;
1572
- };
1573
- /**
1574
- * Calculate tokens for function parameters
1575
- */
1576
- const calculateParametersTokens = (parameters, encoder, constants) => {
1577
- if (!parameters || typeof parameters !== "object") return 0;
1578
- const params = parameters;
1579
- let tokens = 0;
1580
- for (const [key, value] of Object.entries(params)) if (key === "properties") {
1581
- const properties = value;
1582
- if (Object.keys(properties).length > 0) {
1583
- tokens += constants.propInit;
1584
- for (const propKey of Object.keys(properties)) tokens += calculateParameterTokens(propKey, properties[propKey], {
1585
- encoder,
1586
- constants
1587
- });
1588
- }
1589
- } else {
1590
- const paramText = typeof value === "string" ? value : JSON.stringify(value);
1591
- tokens += encoder.encode(`${key}:${paramText}`).length;
1592
- }
1593
- return tokens;
1594
- };
1595
- /**
1596
- * Calculate tokens for a single tool
1597
- */
1598
- const calculateToolTokens = (tool, encoder, constants) => {
1599
- let tokens = constants.funcInit;
1600
- const func = tool.function;
1601
- const fName = func.name;
1602
- let fDesc = func.description || "";
1603
- if (fDesc.endsWith(".")) fDesc = fDesc.slice(0, -1);
1604
- const line = fName + ":" + fDesc;
1605
- tokens += encoder.encode(line).length;
1606
- if (typeof func.parameters === "object" && func.parameters !== null) tokens += calculateParametersTokens(func.parameters, encoder, constants);
1607
- return tokens;
1608
- };
1609
- /**
1610
- * Calculate token count for tools based on model
1611
- */
1612
- const numTokensForTools = (tools, encoder, constants) => {
1613
- let funcTokenCount = 0;
1614
- for (const tool of tools) funcTokenCount += calculateToolTokens(tool, encoder, constants);
1615
- funcTokenCount += constants.funcEnd;
1616
- return funcTokenCount;
1617
- };
1618
- /**
1619
- * Calculate the token count of messages, supporting multiple GPT encoders
1620
- */
1621
- const getTokenCount = async (payload, model) => {
1622
- const tokenizer = getTokenizerFromModel(model);
1623
- const encoder = await getEncodeChatFunction(tokenizer);
1624
- const simplifiedMessages = payload.messages;
1625
- const inputMessages = simplifiedMessages.filter((msg) => msg.role !== "assistant");
1626
- const outputMessages = simplifiedMessages.filter((msg) => msg.role === "assistant");
1627
- const constants = getModelConstants(model);
1628
- let inputTokens = calculateTokens(inputMessages, encoder, constants);
1629
- if (payload.tools && payload.tools.length > 0) inputTokens += numTokensForTools(payload.tools, encoder, constants);
1630
- const outputTokens = calculateTokens(outputMessages, encoder, constants);
1631
- return {
1632
- input: inputTokens,
1633
- output: outputTokens
1634
- };
1635
- };
1636
-
1637
- //#endregion
1638
- //#region src/lib/auto-compact.ts
1639
- const DEFAULT_CONFIG = {
1640
- targetTokens: 1e5,
1641
- safetyMarginPercent: 10
1642
- };
1643
- /**
1644
- * Check if payload needs compaction based on model limits.
1645
- * Uses a safety margin to account for token counting differences.
1646
- */
1647
- async function checkNeedsCompaction(payload, model, safetyMarginPercent = 10) {
1648
- const currentTokens = (await getTokenCount(payload, model)).input;
1649
- const rawLimit = model.capabilities.limits.max_prompt_tokens ?? 128e3;
1650
- const limit = Math.floor(rawLimit * (1 - safetyMarginPercent / 100));
1651
- return {
1652
- needed: currentTokens > limit,
1653
- currentTokens,
1654
- limit
1655
- };
1656
- }
1657
- /**
1658
- * Calculate approximate token count for a single message.
1659
- * This is a fast estimation for splitting decisions.
1660
- */
1661
- function estimateMessageTokens(message) {
1662
- let text = "";
1663
- if (typeof message.content === "string") text = message.content;
1664
- else if (Array.isArray(message.content)) {
1665
- for (const part of message.content) if (part.type === "text") text += part.text;
1666
- else if ("image_url" in part) text += part.image_url.url;
1667
- }
1668
- if (message.tool_calls) text += JSON.stringify(message.tool_calls);
1669
- return Math.ceil(text.length / 4) + 10;
1670
- }
1671
- /**
1672
- * Extract system messages from the beginning of the message list.
1673
- */
1674
- function extractSystemMessages(messages) {
1675
- const systemMessages = [];
1676
- let i = 0;
1677
- while (i < messages.length) {
1678
- const msg = messages[i];
1679
- if (msg.role === "system" || msg.role === "developer") {
1680
- systemMessages.push(msg);
1681
- i++;
1682
- } else break;
1683
- }
1684
- return {
1685
- systemMessages,
1686
- remainingMessages: messages.slice(i)
1687
- };
1688
- }
1689
- /**
1690
- * Find messages to keep from the end to stay under target tokens.
1691
- * Returns the starting index of messages to preserve.
1692
- */
1693
- function findPreserveIndex(messages, targetTokens, systemTokens) {
1694
- const availableTokens = targetTokens - systemTokens - 500;
1695
- let accumulatedTokens = 0;
1696
- for (let i = messages.length - 1; i >= 0; i--) {
1697
- const msgTokens = estimateMessageTokens(messages[i]);
1698
- if (accumulatedTokens + msgTokens > availableTokens) return i + 1;
1699
- accumulatedTokens += msgTokens;
1700
- }
1701
- return 0;
1702
- }
1703
- /**
1704
- * Calculate estimated tokens for system messages.
1705
- */
1706
- function estimateSystemTokens(systemMessages) {
1707
- return systemMessages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
1708
- }
1709
- /**
1710
- * Create a truncation marker message.
1711
- */
1712
- function createTruncationMarker(removedCount) {
1713
- return {
1714
- role: "user",
1715
- content: `[CONTEXT TRUNCATED: ${removedCount} earlier messages were removed to fit context limits. The conversation continues below.]`
1716
- };
1717
- }
1718
- /**
1719
- * Perform auto-compaction on a payload that exceeds token limits.
1720
- * This uses simple truncation - no LLM calls required.
1721
- */
1722
- async function autoCompact(payload, model, config = {}) {
1723
- const cfg = {
1724
- ...DEFAULT_CONFIG,
1725
- ...config
1726
- };
1727
- const originalTokens = (await getTokenCount(payload, model)).input;
1728
- const rawLimit = model.capabilities.limits.max_prompt_tokens ?? 128e3;
1729
- const limit = Math.floor(rawLimit * (1 - cfg.safetyMarginPercent / 100));
1730
- if (originalTokens <= limit) return {
1731
- payload,
1732
- wasCompacted: false,
1733
- originalTokens,
1734
- compactedTokens: originalTokens,
1735
- removedMessageCount: 0
1736
- };
1737
- consola.info(`Auto-compact: ${originalTokens} tokens exceeds limit of ${limit}, truncating...`);
1738
- const { systemMessages, remainingMessages } = extractSystemMessages(payload.messages);
1739
- const systemTokens = estimateSystemTokens(systemMessages);
1740
- consola.debug(`Auto-compact: ${systemMessages.length} system messages (~${systemTokens} tokens)`);
1741
- const effectiveTarget = Math.min(cfg.targetTokens, limit);
1742
- const preserveIndex = findPreserveIndex(remainingMessages, effectiveTarget, systemTokens);
1743
- if (preserveIndex === 0) {
1744
- consola.warn("Auto-compact: Cannot truncate further without losing all conversation history");
1745
- return {
1746
- payload,
1747
- wasCompacted: false,
1748
- originalTokens,
1749
- compactedTokens: originalTokens,
1750
- removedMessageCount: 0
1751
- };
1752
- }
1753
- const removedMessages = remainingMessages.slice(0, preserveIndex);
1754
- const preservedMessages = remainingMessages.slice(preserveIndex);
1755
- consola.info(`Auto-compact: Removing ${removedMessages.length} messages, keeping ${preservedMessages.length}`);
1756
- const truncationMarker = createTruncationMarker(removedMessages.length);
1757
- const newPayload = {
1758
- ...payload,
1759
- messages: [
1760
- ...systemMessages,
1761
- truncationMarker,
1762
- ...preservedMessages
1763
- ]
1764
- };
1765
- const newTokenCount = await getTokenCount(newPayload, model);
1766
- consola.info(`Auto-compact: Reduced from ${originalTokens} to ${newTokenCount.input} tokens`);
1767
- if (newTokenCount.input > limit) {
1768
- consola.warn(`Auto-compact: Still over limit (${newTokenCount.input} > ${limit}), trying more aggressive truncation`);
1769
- const aggressiveTarget = Math.floor(effectiveTarget * .7);
1770
- if (aggressiveTarget < 2e4) {
1771
- consola.error("Auto-compact: Cannot reduce further, target too low");
1772
- return {
1773
- payload: newPayload,
1774
- wasCompacted: true,
1775
- originalTokens,
1776
- compactedTokens: newTokenCount.input,
1777
- removedMessageCount: removedMessages.length
1778
- };
1779
- }
1780
- return autoCompact(payload, model, {
1781
- ...cfg,
1782
- targetTokens: aggressiveTarget
1783
- });
1784
- }
1785
- return {
1786
- payload: newPayload,
1787
- wasCompacted: true,
1788
- originalTokens,
1789
- compactedTokens: newTokenCount.input,
1790
- removedMessageCount: removedMessages.length
1791
- };
1792
- }
1793
- /**
1794
- * Create a marker to append to responses indicating auto-compaction occurred.
1795
- */
1796
- function createCompactionMarker(result) {
1797
- if (!result.wasCompacted) return "";
1798
- const reduction = result.originalTokens - result.compactedTokens;
1799
- const percentage = Math.round(reduction / result.originalTokens * 100);
1800
- return `\n\n---\n[Auto-compacted: ${result.removedMessageCount} messages removed, ${result.originalTokens} → ${result.compactedTokens} tokens (${percentage}% reduction)]`;
1801
- }
1802
-
1803
- //#endregion
1804
- //#region src/lib/queue.ts
1805
- var RequestQueue = class {
1806
- queue = [];
1807
- processing = false;
1808
- lastRequestTime = 0;
1809
- async enqueue(execute, rateLimitSeconds) {
1810
- return new Promise((resolve, reject) => {
1811
- this.queue.push({
1812
- execute,
1813
- resolve,
1814
- reject
1815
- });
1816
- if (this.queue.length > 1) {
1817
- const waitTime = Math.ceil((this.queue.length - 1) * rateLimitSeconds);
1818
- consola.info(`Request queued. Position: ${this.queue.length}, estimated wait: ${waitTime}s`);
1819
- }
1820
- this.processQueue(rateLimitSeconds);
1821
- });
1822
- }
1823
- async processQueue(rateLimitSeconds) {
1824
- if (this.processing) return;
1825
- this.processing = true;
1826
- while (this.queue.length > 0) {
1827
- const elapsedMs = Date.now() - this.lastRequestTime;
1828
- const requiredMs = rateLimitSeconds * 1e3;
1829
- if (this.lastRequestTime > 0 && elapsedMs < requiredMs) {
1830
- const waitMs = requiredMs - elapsedMs;
1831
- consola.debug(`Rate limit: waiting ${Math.ceil(waitMs / 1e3)}s`);
1832
- await new Promise((resolve) => setTimeout(resolve, waitMs));
1833
- }
1834
- const request = this.queue.shift();
1835
- if (!request) break;
1836
- this.lastRequestTime = Date.now();
1837
- try {
1838
- const result = await request.execute();
1839
- request.resolve(result);
1840
- } catch (error) {
1841
- request.reject(error);
1842
- }
1843
- }
1844
- this.processing = false;
1845
- }
1846
- get length() {
1847
- return this.queue.length;
1848
- }
1849
- };
1850
- const requestQueue = new RequestQueue();
1851
- /**
1852
- * Execute a request with rate limiting via queue.
1853
- * Requests are queued and processed sequentially at the configured rate.
1854
- */
1855
- async function executeWithRateLimit(state$1, execute) {
1856
- if (state$1.rateLimitSeconds === void 0) return execute();
1857
- return requestQueue.enqueue(execute, state$1.rateLimitSeconds);
1858
- }
1859
-
1860
- //#endregion
1861
- //#region src/services/copilot/create-chat-completions.ts
1862
- const createChatCompletions = async (payload) => {
1863
- if (!state.copilotToken) throw new Error("Copilot token not found");
1864
- const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x$1) => x$1.type === "image_url"));
1865
- const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
1866
- const headers = {
1867
- ...copilotHeaders(state, enableVision),
1868
- "X-Initiator": isAgentCall ? "agent" : "user"
1869
- };
1870
- const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
1871
- method: "POST",
1872
- headers,
1873
- body: JSON.stringify(payload)
1874
- });
1875
- if (!response.ok) {
1876
- consola.error("Failed to create chat completions", response);
1877
- throw await HTTPError.fromResponse("Failed to create chat completions", response);
1878
- }
1879
- if (payload.stream) return events(response);
1880
- return await response.json();
1881
- };
1882
-
1883
- //#endregion
1884
- //#region src/routes/chat-completions/handler.ts
1885
- async function handleCompletion$1(c) {
1886
- const startTime = Date.now();
1887
- const originalPayload = await c.req.json();
1888
- consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
1889
- const trackingId = c.get("trackingId");
1890
- updateTrackerModel$1(trackingId, originalPayload.model);
1891
- const ctx = {
1892
- historyId: recordRequest("openai", {
1893
- model: originalPayload.model,
1894
- messages: convertOpenAIMessages(originalPayload.messages),
1895
- stream: originalPayload.stream ?? false,
1896
- tools: originalPayload.tools?.map((t) => ({
1897
- name: t.function.name,
1898
- description: t.function.description
1899
- })),
1900
- max_tokens: originalPayload.max_tokens ?? void 0,
1901
- temperature: originalPayload.temperature ?? void 0
1902
- }),
1903
- trackingId,
1904
- startTime
1905
- };
1906
- const selectedModel = state.models?.data.find((model) => model.id === originalPayload.model);
1907
- await logTokenCount(originalPayload, selectedModel);
1908
- const { finalPayload, compactResult } = await buildFinalPayload$1(originalPayload, selectedModel);
1909
- if (compactResult) ctx.compactResult = compactResult;
1910
- const payload = isNullish(finalPayload.max_tokens) ? {
1911
- ...finalPayload,
1912
- max_tokens: selectedModel?.capabilities.limits.max_output_tokens
1913
- } : finalPayload;
1914
- if (isNullish(originalPayload.max_tokens)) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
1915
- if (state.manualApprove) await awaitApproval();
1916
- try {
1917
- const response = await executeWithRateLimit(state, () => createChatCompletions(payload));
1918
- if (isNonStreaming$1(response)) return handleNonStreamingResponse$1(c, response, ctx);
1919
- consola.debug("Streaming response");
1920
- updateTrackerStatus$1(trackingId, "streaming");
1921
- return streamSSE(c, async (stream) => {
1922
- await handleStreamingResponse$1({
1923
- stream,
1924
- response,
1925
- payload,
1926
- ctx
1927
- });
1928
- });
1929
- } catch (error) {
1930
- recordErrorResponse$1(ctx, payload.model, error);
1931
- throw error;
1932
- }
1933
- }
1934
- async function buildFinalPayload$1(payload, model) {
1935
- if (!state.autoCompact || !model) {
1936
- if (state.autoCompact && !model) consola.warn(`Auto-compact: Model '${payload.model}' not found in cached models, skipping`);
1937
- return {
1938
- finalPayload: payload,
1939
- compactResult: null
1940
- };
1941
- }
1942
- try {
1943
- const check = await checkNeedsCompaction(payload, model);
1944
- consola.info(`Auto-compact check: ${check.currentTokens} tokens, limit ${check.limit}, needed: ${check.needed}`);
1945
- if (!check.needed) return {
1946
- finalPayload: payload,
1947
- compactResult: null
1948
- };
1949
- consola.info(`Auto-compact triggered: ${check.currentTokens} tokens > ${check.limit} limit`);
1950
- const compactResult = await autoCompact(payload, model);
1951
- return {
1952
- finalPayload: compactResult.payload,
1953
- compactResult
1954
- };
1955
- } catch (error) {
1956
- consola.warn("Auto-compact failed, proceeding with original payload:", error);
1957
- return {
1958
- finalPayload: payload,
1959
- compactResult: null
1960
- };
1961
- }
1962
- }
1963
- async function logTokenCount(payload, selectedModel) {
1964
- try {
1965
- if (selectedModel) {
1966
- const tokenCount = await getTokenCount(payload, selectedModel);
1967
- consola.info("Current token count:", tokenCount);
1968
- } else consola.warn("No model selected, skipping token count calculation");
1969
- } catch (error) {
1970
- consola.warn("Failed to calculate token count:", error);
1971
- }
1972
- }
1973
- function updateTrackerModel$1(trackingId, model) {
1974
- if (!trackingId) return;
1975
- const request = requestTracker.getRequest(trackingId);
1976
- if (request) request.model = model;
1977
- }
1978
- function updateTrackerStatus$1(trackingId, status) {
1979
- if (!trackingId) return;
1980
- requestTracker.updateRequest(trackingId, { status });
1981
- }
1982
- function recordErrorResponse$1(ctx, model, error) {
1983
- recordResponse(ctx.historyId, {
1984
- success: false,
1985
- model,
1986
- usage: {
1987
- input_tokens: 0,
1988
- output_tokens: 0
1989
- },
1990
- error: error instanceof Error ? error.message : "Unknown error",
1991
- content: null
1992
- }, Date.now() - ctx.startTime);
1993
- }
1994
- function handleNonStreamingResponse$1(c, originalResponse, ctx) {
1995
- consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
1996
- let response = originalResponse;
1997
- if (ctx.compactResult?.wasCompacted && response.choices[0]?.message.content) {
1998
- const marker = createCompactionMarker(ctx.compactResult);
1999
- response = {
2000
- ...response,
2001
- choices: response.choices.map((choice$1, i) => i === 0 ? {
2002
- ...choice$1,
2003
- message: {
2004
- ...choice$1.message,
2005
- content: (choice$1.message.content ?? "") + marker
2006
- }
2007
- } : choice$1)
2008
- };
2009
- }
2010
- const choice = response.choices[0];
2011
- const usage = response.usage;
2012
- recordResponse(ctx.historyId, {
2013
- success: true,
2014
- model: response.model,
2015
- usage: {
2016
- input_tokens: usage?.prompt_tokens ?? 0,
2017
- output_tokens: usage?.completion_tokens ?? 0
2018
- },
2019
- stop_reason: choice.finish_reason,
2020
- content: buildResponseContent(choice),
2021
- toolCalls: extractToolCalls(choice)
2022
- }, Date.now() - ctx.startTime);
2023
- if (ctx.trackingId && usage) requestTracker.updateRequest(ctx.trackingId, {
2024
- inputTokens: usage.prompt_tokens,
2025
- outputTokens: usage.completion_tokens
2026
- });
2027
- return c.json(response);
2028
- }
2029
- function buildResponseContent(choice) {
2030
- return {
2031
- role: choice.message.role,
2032
- content: typeof choice.message.content === "string" ? choice.message.content : JSON.stringify(choice.message.content),
2033
- tool_calls: choice.message.tool_calls?.map((tc) => ({
2034
- id: tc.id,
2035
- type: tc.type,
2036
- function: {
2037
- name: tc.function.name,
2038
- arguments: tc.function.arguments
2039
- }
2040
- }))
2041
- };
2042
- }
2043
- function extractToolCalls(choice) {
2044
- return choice.message.tool_calls?.map((tc) => ({
2045
- id: tc.id,
2046
- name: tc.function.name,
2047
- input: tc.function.arguments
2048
- }));
2049
- }
2050
- function createStreamAccumulator() {
2051
- return {
2052
- model: "",
2053
- inputTokens: 0,
2054
- outputTokens: 0,
2055
- finishReason: "",
2056
- content: "",
2057
- toolCalls: [],
2058
- toolCallMap: /* @__PURE__ */ new Map()
2059
- };
2060
- }
2061
- async function handleStreamingResponse$1(opts) {
2062
- const { stream, response, payload, ctx } = opts;
2063
- const acc = createStreamAccumulator();
2064
- try {
2065
- for await (const chunk of response) {
2066
- consola.debug("Streaming chunk:", JSON.stringify(chunk));
2067
- parseStreamChunk(chunk, acc);
2068
- await stream.writeSSE(chunk);
2069
- }
2070
- if (ctx.compactResult?.wasCompacted) {
2071
- const marker = createCompactionMarker(ctx.compactResult);
2072
- const markerChunk = {
2073
- id: `compact-marker-${Date.now()}`,
2074
- object: "chat.completion.chunk",
2075
- created: Math.floor(Date.now() / 1e3),
2076
- model: acc.model || payload.model,
2077
- choices: [{
2078
- index: 0,
2079
- delta: { content: marker },
2080
- finish_reason: null,
2081
- logprobs: null
2082
- }]
2083
- };
2084
- await stream.writeSSE({
2085
- data: JSON.stringify(markerChunk),
2086
- event: "message"
2087
- });
2088
- acc.content += marker;
2089
- }
2090
- recordStreamSuccess(acc, payload.model, ctx);
2091
- completeTracking$1(ctx.trackingId, acc.inputTokens, acc.outputTokens);
2092
- } catch (error) {
2093
- recordStreamError({
2094
- acc,
2095
- fallbackModel: payload.model,
2096
- ctx,
2097
- error
2098
- });
2099
- failTracking$1(ctx.trackingId, error);
2100
- throw error;
2101
- }
2102
- }
2103
- function parseStreamChunk(chunk, acc) {
2104
- if (!chunk.data || chunk.data === "[DONE]") return;
2105
- try {
2106
- const parsed = JSON.parse(chunk.data);
2107
- accumulateModel(parsed, acc);
2108
- accumulateUsage(parsed, acc);
2109
- accumulateChoice(parsed.choices[0], acc);
2110
- } catch {}
2111
- }
2112
- function accumulateModel(parsed, acc) {
2113
- if (parsed.model && !acc.model) acc.model = parsed.model;
2114
- }
2115
- function accumulateUsage(parsed, acc) {
2116
- if (parsed.usage) {
2117
- acc.inputTokens = parsed.usage.prompt_tokens;
2118
- acc.outputTokens = parsed.usage.completion_tokens;
2119
- }
2120
- }
2121
- function accumulateChoice(choice, acc) {
2122
- if (!choice) return;
2123
- if (choice.delta.content) acc.content += choice.delta.content;
2124
- if (choice.delta.tool_calls) accumulateToolCalls(choice.delta.tool_calls, acc);
2125
- if (choice.finish_reason) acc.finishReason = choice.finish_reason;
2126
- }
2127
- function accumulateToolCalls(toolCalls, acc) {
2128
- if (!toolCalls) return;
2129
- for (const tc of toolCalls) {
2130
- const idx = tc.index;
2131
- if (!acc.toolCallMap.has(idx)) acc.toolCallMap.set(idx, {
2132
- id: tc.id ?? "",
2133
- name: tc.function?.name ?? "",
2134
- arguments: ""
2135
- });
2136
- const item = acc.toolCallMap.get(idx);
2137
- if (item) {
2138
- if (tc.id) item.id = tc.id;
2139
- if (tc.function?.name) item.name = tc.function.name;
2140
- if (tc.function?.arguments) item.arguments += tc.function.arguments;
2141
- }
2142
- }
2143
- }
2144
- function recordStreamSuccess(acc, fallbackModel, ctx) {
2145
- for (const tc of acc.toolCallMap.values()) if (tc.id && tc.name) acc.toolCalls.push(tc);
2146
- const toolCalls = acc.toolCalls.map((tc) => ({
2147
- id: tc.id,
2148
- type: "function",
2149
- function: {
2150
- name: tc.name,
2151
- arguments: tc.arguments
2152
- }
2153
- }));
2154
- recordResponse(ctx.historyId, {
2155
- success: true,
2156
- model: acc.model || fallbackModel,
2157
- usage: {
2158
- input_tokens: acc.inputTokens,
2159
- output_tokens: acc.outputTokens
2160
- },
2161
- stop_reason: acc.finishReason || void 0,
2162
- content: {
2163
- role: "assistant",
2164
- content: acc.content,
2165
- tool_calls: toolCalls.length > 0 ? toolCalls : void 0
2166
- },
2167
- toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls.map((tc) => ({
2168
- id: tc.id,
2169
- name: tc.name,
2170
- input: tc.arguments
2171
- })) : void 0
2172
- }, Date.now() - ctx.startTime);
2173
- }
2174
- function recordStreamError(opts) {
2175
- const { acc, fallbackModel, ctx, error } = opts;
2176
- recordResponse(ctx.historyId, {
2177
- success: false,
2178
- model: acc.model || fallbackModel,
2179
- usage: {
2180
- input_tokens: 0,
2181
- output_tokens: 0
2182
- },
2183
- error: error instanceof Error ? error.message : "Stream error",
2184
- content: null
2185
- }, Date.now() - ctx.startTime);
2186
- }
2187
- function completeTracking$1(trackingId, inputTokens, outputTokens) {
2188
- if (!trackingId) return;
2189
- requestTracker.updateRequest(trackingId, {
2190
- inputTokens,
2191
- outputTokens
2192
- });
2193
- requestTracker.completeRequest(trackingId, 200, {
2194
- inputTokens,
2195
- outputTokens
2196
- });
2197
- }
2198
- function failTracking$1(trackingId, error) {
2199
- if (!trackingId) return;
2200
- requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Stream error");
2201
- }
2202
- const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
2203
- function convertOpenAIMessages(messages) {
2204
- return messages.map((msg) => {
2205
- const result = {
2206
- role: msg.role,
2207
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
2208
- };
2209
- if ("tool_calls" in msg && msg.tool_calls) result.tool_calls = msg.tool_calls.map((tc) => ({
2210
- id: tc.id,
2211
- type: tc.type,
2212
- function: {
2213
- name: tc.function.name,
2214
- arguments: tc.function.arguments
2215
- }
2216
- }));
2217
- if ("tool_call_id" in msg && msg.tool_call_id) result.tool_call_id = msg.tool_call_id;
2218
- if ("name" in msg && msg.name) result.name = msg.name;
2219
- return result;
2220
- });
2221
- }
2222
-
2223
- //#endregion
2224
- //#region src/routes/chat-completions/route.ts
2225
- const completionRoutes = new Hono();
2226
- completionRoutes.post("/", async (c) => {
2227
- try {
2228
- return await handleCompletion$1(c);
2229
- } catch (error) {
2230
- return await forwardError(c, error);
2231
- }
2232
- });
2233
-
2234
- //#endregion
2235
- //#region src/services/copilot/create-embeddings.ts
2236
- const createEmbeddings = async (payload) => {
2237
- if (!state.copilotToken) throw new Error("Copilot token not found");
2238
- const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
2239
- method: "POST",
2240
- headers: copilotHeaders(state),
2241
- body: JSON.stringify(payload)
2242
- });
2243
- if (!response.ok) throw await HTTPError.fromResponse("Failed to create embeddings", response);
2244
- return await response.json();
2245
- };
2246
-
2247
- //#endregion
2248
- //#region src/routes/embeddings/route.ts
2249
- const embeddingRoutes = new Hono();
2250
- embeddingRoutes.post("/", async (c) => {
2251
- try {
2252
- const payload = await c.req.json();
2253
- const response = await createEmbeddings(payload);
2254
- return c.json(response);
2255
- } catch (error) {
2256
- return await forwardError(c, error);
2257
- }
2258
- });
2259
-
2260
- //#endregion
2261
- //#region src/routes/event-logging/route.ts
2262
- const eventLoggingRoutes = new Hono();
2263
- eventLoggingRoutes.post("/batch", (c) => {
2264
- return c.text("OK", 200);
2265
- });
2266
-
2267
- //#endregion
2268
- //#region src/routes/history/api.ts
2269
- function handleGetEntries(c) {
2270
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2271
- const query = c.req.query();
2272
- const options = {
2273
- page: query.page ? Number.parseInt(query.page, 10) : void 0,
2274
- limit: query.limit ? Number.parseInt(query.limit, 10) : void 0,
2275
- model: query.model || void 0,
2276
- endpoint: query.endpoint,
2277
- success: query.success ? query.success === "true" : void 0,
2278
- from: query.from ? Number.parseInt(query.from, 10) : void 0,
2279
- to: query.to ? Number.parseInt(query.to, 10) : void 0,
2280
- search: query.search || void 0,
2281
- sessionId: query.sessionId || void 0
2282
- };
2283
- const result = getHistory(options);
2284
- return c.json(result);
2285
- }
2286
- function handleGetEntry(c) {
2287
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2288
- const id = c.req.param("id");
2289
- const entry = getEntry(id);
2290
- if (!entry) return c.json({ error: "Entry not found" }, 404);
2291
- return c.json(entry);
2292
- }
2293
- function handleDeleteEntries(c) {
2294
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2295
- clearHistory();
2296
- return c.json({
2297
- success: true,
2298
- message: "History cleared"
2299
- });
2300
- }
2301
- function handleGetStats(c) {
2302
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2303
- const stats = getStats();
2304
- return c.json(stats);
2305
- }
2306
- function handleExport(c) {
2307
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2308
- const format = c.req.query("format") || "json";
2309
- const data = exportHistory(format);
2310
- if (format === "csv") {
2311
- c.header("Content-Type", "text/csv");
2312
- c.header("Content-Disposition", "attachment; filename=history.csv");
2313
- } else {
2314
- c.header("Content-Type", "application/json");
2315
- c.header("Content-Disposition", "attachment; filename=history.json");
2316
- }
2317
- return c.body(data);
2318
- }
2319
- function handleGetSessions(c) {
2320
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2321
- const result = getSessions();
2322
- return c.json(result);
2323
- }
2324
- function handleGetSession(c) {
2325
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2326
- const id = c.req.param("id");
2327
- const session = getSession(id);
2328
- if (!session) return c.json({ error: "Session not found" }, 404);
2329
- const entries = getSessionEntries(id);
2330
- return c.json({
2331
- ...session,
2332
- entries
2333
- });
2334
- }
2335
- function handleDeleteSession(c) {
2336
- if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
2337
- const id = c.req.param("id");
2338
- if (!deleteSession(id)) return c.json({ error: "Session not found" }, 404);
2339
- return c.json({
2340
- success: true,
2341
- message: "Session deleted"
2342
- });
2343
- }
2344
-
2345
- //#endregion
2346
- //#region src/routes/history/ui/script.ts
2347
- const script = `
2348
- let currentSessionId = null;
2349
- let currentEntryId = null;
2350
- let debounceTimer = null;
2351
-
2352
- function formatTime(ts) {
2353
- const d = new Date(ts);
2354
- return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
2355
- }
2356
-
2357
- function formatDate(ts) {
2358
- const d = new Date(ts);
2359
- return d.toLocaleDateString([], {month:'short',day:'numeric'}) + ' ' + formatTime(ts);
2360
- }
2361
-
2362
- function formatNumber(n) {
2363
- if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
2364
- if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
2365
- return n.toString();
2366
- }
2367
-
2368
- function formatDuration(ms) {
2369
- if (!ms) return '-';
2370
- if (ms < 1000) return ms + 'ms';
2371
- return (ms / 1000).toFixed(1) + 's';
2372
- }
2373
-
2374
- function getContentText(content) {
2375
- if (!content) return '';
2376
- if (typeof content === 'string') return content;
2377
- if (Array.isArray(content)) {
2378
- return content.map(c => {
2379
- if (c.type === 'text') return c.text || '';
2380
- if (c.type === 'tool_use') return '[tool_use: ' + c.name + ']';
2381
- if (c.type === 'tool_result') return '[tool_result: ' + (c.tool_use_id || '').slice(0,8) + ']';
2382
- if (c.type === 'image' || c.type === 'image_url') return '[image]';
2383
- return c.text || '[' + (c.type || 'unknown') + ']';
2384
- }).join('\\n');
2385
- }
2386
- return JSON.stringify(content, null, 2);
2387
- }
2388
-
2389
- // Extract real user text, skipping system tags like <system-reminder>, <ide_opened_file>, etc.
2390
- function extractRealUserText(content) {
2391
- if (!content) return '';
2392
- let text = '';
2393
- if (typeof content === 'string') {
2394
- text = content;
2395
- } else if (Array.isArray(content)) {
2396
- text = content
2397
- .filter(c => c.type === 'text' && c.text)
2398
- .map(c => c.text)
2399
- .join('\\n');
2400
- }
2401
- if (!text) return '';
2402
-
2403
- // Remove system tags and their content
2404
- const systemTags = [
2405
- 'system-reminder',
2406
- 'ide_opened_file',
2407
- 'ide_selection',
2408
- 'ide_visible_files',
2409
- 'ide_diagnostics',
2410
- 'ide_cursor_position',
2411
- 'user-prompt-submit-hook',
2412
- 'antml:function_calls',
2413
- 'antml:invoke',
2414
- 'antml:parameter'
2415
- ];
2416
-
2417
- let cleaned = text;
2418
- for (const tag of systemTags) {
2419
- // Remove <tag>...</tag> blocks (including multiline)
2420
- const regex = new RegExp('<' + tag + '[^>]*>[\\\\s\\\\S]*?</' + tag + '>', 'gi');
2421
- cleaned = cleaned.replace(regex, '');
2422
- // Remove self-closing <tag ... /> or <tag ...>content without closing
2423
- const selfClosingRegex = new RegExp('<' + tag + '[^>]*/>', 'gi');
2424
- cleaned = cleaned.replace(selfClosingRegex, '');
2425
- }
2426
-
2427
- // Trim whitespace and return
2428
- return cleaned.trim();
2429
- }
2430
-
2431
- // Get preview text from assistant message content
2432
- function getAssistantPreview(content) {
2433
- if (!content) return '';
2434
- if (typeof content === 'string') {
2435
- const text = content.trim();
2436
- if (text.length > 0) {
2437
- return text.length > 80 ? text.slice(0, 80) + '...' : text;
2438
- }
2439
- return '';
2440
- }
2441
- if (Array.isArray(content)) {
2442
- // First try to get text content
2443
- const textParts = content.filter(c => c.type === 'text' && c.text).map(c => c.text);
2444
- if (textParts.length > 0) {
2445
- const text = textParts.join('\\n').trim();
2446
- if (text.length > 0) {
2447
- return text.length > 80 ? text.slice(0, 80) + '...' : text;
2448
- }
2449
- }
2450
- // If no text, show tool_use info
2451
- const toolUses = content.filter(c => c.type === 'tool_use');
2452
- if (toolUses.length === 1) {
2453
- return '[tool_use: ' + toolUses[0].name + ']';
2454
- } else if (toolUses.length > 1) {
2455
- return '[' + toolUses.length + ' tool_uses]';
2456
- }
2457
- }
2458
- return '';
2459
- }
2460
-
2461
- function formatContentForDisplay(content) {
2462
- if (!content) return { summary: '', raw: 'null' };
2463
- if (typeof content === 'string') return { summary: content, raw: JSON.stringify(content) };
2464
- if (Array.isArray(content)) {
2465
- const parts = [];
2466
- for (const c of content) {
2467
- if (c.type === 'text') {
2468
- parts.push(c.text || '');
2469
- } else if (c.type === 'tool_use') {
2470
- parts.push('--- tool_use: ' + c.name + ' [' + (c.id || '').slice(0,8) + '] ---\\n' + JSON.stringify(c.input, null, 2));
2471
- } else if (c.type === 'tool_result') {
2472
- const resultContent = typeof c.content === 'string' ? c.content : JSON.stringify(c.content, null, 2);
2473
- parts.push('--- tool_result [' + (c.tool_use_id || '').slice(0,8) + '] ---\\n' + resultContent);
2474
- } else if (c.type === 'image' || c.type === 'image_url') {
2475
- parts.push('[image data]');
2476
- } else {
2477
- parts.push(JSON.stringify(c, null, 2));
2478
- }
2479
- }
2480
- return { summary: parts.join('\\n\\n'), raw: JSON.stringify(content, null, 2) };
2481
- }
2482
- const raw = JSON.stringify(content, null, 2);
2483
- return { summary: raw, raw };
2484
- }
2485
-
2486
- async function loadStats() {
2487
- try {
2488
- const res = await fetch('/history/api/stats');
2489
- const data = await res.json();
2490
- if (data.error) return;
2491
- document.getElementById('stat-total').textContent = formatNumber(data.totalRequests);
2492
- document.getElementById('stat-success').textContent = formatNumber(data.successfulRequests);
2493
- document.getElementById('stat-failed').textContent = formatNumber(data.failedRequests);
2494
- document.getElementById('stat-input').textContent = formatNumber(data.totalInputTokens);
2495
- document.getElementById('stat-output').textContent = formatNumber(data.totalOutputTokens);
2496
- document.getElementById('stat-sessions').textContent = data.activeSessions;
2497
- } catch (e) {
2498
- console.error('Failed to load stats', e);
2499
- }
2500
- }
2501
-
2502
- async function loadSessions() {
2503
- try {
2504
- const res = await fetch('/history/api/sessions');
2505
- const data = await res.json();
2506
- if (data.error) {
2507
- document.getElementById('sessions-list').innerHTML = '<div class="empty-state">Not enabled</div>';
2508
- return;
2509
- }
2510
-
2511
- let html = '<div class="session-item all' + (currentSessionId === null ? ' active' : '') + '" onclick="selectSession(null)">All Requests</div>';
2512
-
2513
- for (const s of data.sessions) {
2514
- const isActive = currentSessionId === s.id;
2515
- const shortId = s.id.slice(0, 8);
2516
- const toolCount = s.toolsUsed ? s.toolsUsed.length : 0;
2517
- html += \`
2518
- <div class="session-item\${isActive ? ' active' : ''}" onclick="selectSession('\${s.id}')">
2519
- <div class="session-meta">
2520
- <span>\${s.models[0] || 'Unknown'}</span>
2521
- <span class="session-time">\${formatDate(s.startTime)}</span>
2522
- </div>
2523
- <div class="session-stats">
2524
- <span style="color:var(--text-dim);font-family:monospace;font-size:10px;">\${shortId}</span>
2525
- <span>\${s.requestCount} req</span>
2526
- <span>\${formatNumber(s.totalInputTokens + s.totalOutputTokens)} tok</span>
2527
- \${toolCount > 0 ? '<span class="badge tool">' + toolCount + ' tools</span>' : ''}
2528
- <span class="badge \${s.endpoint}">\${s.endpoint}</span>
2529
- </div>
2530
- </div>
2531
- \`;
2532
- }
2533
-
2534
- document.getElementById('sessions-list').innerHTML = html || '<div class="empty-state">No sessions</div>';
2535
- } catch (e) {
2536
- document.getElementById('sessions-list').innerHTML = '<div class="empty-state">Error loading</div>';
2537
- }
2538
- }
2539
-
2540
- function selectSession(id) {
2541
- currentSessionId = id;
2542
- loadSessions();
2543
- loadEntries();
2544
- closeDetail();
2545
- }
2546
-
2547
- async function loadEntries() {
2548
- const container = document.getElementById('entries-container');
2549
- container.innerHTML = '<div class="loading">Loading...</div>';
2550
-
2551
- const params = new URLSearchParams();
2552
- params.set('limit', '100');
2553
-
2554
- if (currentSessionId) params.set('sessionId', currentSessionId);
2555
-
2556
- const endpoint = document.getElementById('filter-endpoint').value;
2557
- const success = document.getElementById('filter-success').value;
2558
- const search = document.getElementById('filter-search').value;
2559
-
2560
- if (endpoint) params.set('endpoint', endpoint);
2561
- if (success) params.set('success', success);
2562
- if (search) params.set('search', search);
2563
-
2564
- try {
2565
- const res = await fetch('/history/api/entries?' + params.toString());
2566
- const data = await res.json();
2567
-
2568
- if (data.error) {
2569
- container.innerHTML = '<div class="empty-state"><h3>History Not Enabled</h3><p>Start server with --history</p></div>';
2570
- return;
2571
- }
2572
-
2573
- if (data.entries.length === 0) {
2574
- container.innerHTML = '<div class="empty-state"><h3>No entries</h3><p>Make some API requests</p></div>';
2575
- return;
2576
- }
2577
-
2578
- let html = '';
2579
- for (const e of data.entries) {
2580
- const isSelected = currentEntryId === e.id;
2581
- const status = !e.response ? 'pending' : (e.response.success ? 'success' : 'error');
2582
- const statusLabel = !e.response ? 'pending' : (e.response.success ? 'success' : 'error');
2583
- const tokens = e.response ? formatNumber(e.response.usage.input_tokens) + '/' + formatNumber(e.response.usage.output_tokens) : '-';
2584
- const shortId = e.id.slice(0, 8);
2585
-
2586
- // Get preview: show meaningful context about the request
2587
- let lastUserMsg = '';
2588
- const messages = e.request.messages;
2589
- const lastMsg = messages[messages.length - 1];
2590
-
2591
- // If last message is tool_result, look at the previous assistant message for context
2592
- if (lastMsg && lastMsg.role === 'user') {
2593
- const content = lastMsg.content;
2594
- if (Array.isArray(content) && content.length > 0 && content[0].type === 'tool_result') {
2595
- // This is a tool_result response - look for previous assistant message
2596
- const prevMsg = messages.length >= 2 ? messages[messages.length - 2] : null;
2597
- if (prevMsg && prevMsg.role === 'assistant') {
2598
- lastUserMsg = getAssistantPreview(prevMsg.content);
2599
- }
2600
- // If no meaningful preview from assistant, show tool_result count
2601
- if (!lastUserMsg) {
2602
- const toolResults = content.filter(c => c.type === 'tool_result');
2603
- lastUserMsg = '[' + toolResults.length + ' tool_result' + (toolResults.length > 1 ? 's' : '') + ']';
2604
- }
2605
- } else {
2606
- // Regular user message, extract real text
2607
- const realText = extractRealUserText(lastMsg.content);
2608
- if (realText.length > 0) {
2609
- lastUserMsg = realText.slice(0, 80);
2610
- if (realText.length > 80) lastUserMsg += '...';
2611
- }
2612
- }
2613
- } else if (lastMsg && lastMsg.role === 'assistant') {
2614
- lastUserMsg = getAssistantPreview(lastMsg.content);
2615
- }
2616
-
2617
- html += \`
2618
- <div class="entry-item\${isSelected ? ' selected' : ''}" onclick="showDetail('\${e.id}')">
2619
- <div class="entry-header">
2620
- <span class="entry-time">\${formatTime(e.timestamp)}</span>
2621
- <span style="color:var(--text-dim);font-family:monospace;font-size:10px;">\${shortId}</span>
2622
- <span class="badge \${e.endpoint}">\${e.endpoint}</span>
2623
- <span class="badge \${status}">\${statusLabel}</span>
2624
- \${e.request.stream ? '<span class="badge stream">stream</span>' : ''}
2625
- <span class="entry-model">\${e.response?.model || e.request.model}</span>
2626
- <span class="entry-tokens">\${tokens}</span>
2627
- <span class="entry-duration">\${formatDuration(e.durationMs)}</span>
2628
- </div>
2629
- \${lastUserMsg ? '<div class="entry-preview">' + escapeHtml(lastUserMsg) + '</div>' : ''}
2630
- </div>
2631
- \`;
2632
- }
2633
-
2634
- container.innerHTML = html;
2635
- } catch (e) {
2636
- container.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
2637
- }
2638
- }
2639
-
2640
- async function showDetail(id) {
2641
- // Update selected state without reloading
2642
- const prevSelected = document.querySelector('.entry-item.selected');
2643
- if (prevSelected) prevSelected.classList.remove('selected');
2644
- const newSelected = document.querySelector(\`.entry-item[onclick*="'\${id}'"]\`);
2645
- if (newSelected) newSelected.classList.add('selected');
2646
- currentEntryId = id;
2647
-
2648
- const panel = document.getElementById('detail-panel');
2649
- const content = document.getElementById('detail-content');
2650
- panel.classList.add('open');
2651
- content.innerHTML = '<div class="loading">Loading...</div>';
2652
-
2653
- try {
2654
- const res = await fetch('/history/api/entries/' + id);
2655
- const entry = await res.json();
2656
- if (entry.error) {
2657
- content.innerHTML = '<div class="empty-state">Not found</div>';
2658
- return;
2659
- }
2660
-
2661
- let html = '';
2662
-
2663
- // Entry metadata (IDs)
2664
- html += \`
2665
- <div class="detail-section">
2666
- <h4>Entry Info</h4>
2667
- <div class="response-info">
2668
- <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>
2669
- <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>
2670
- <div class="info-item"><div class="info-label">Timestamp</div><div class="info-value">\${formatDate(entry.timestamp)}</div></div>
2671
- <div class="info-item"><div class="info-label">Endpoint</div><div class="info-value"><span class="badge \${entry.endpoint}">\${entry.endpoint}</span></div></div>
2672
- </div>
2673
- </div>
2674
- \`;
2675
-
2676
- // Response info
2677
- if (entry.response) {
2678
- html += \`
2679
- <div class="detail-section">
2680
- <h4>Response</h4>
2681
- <div class="response-info">
2682
- <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>
2683
- <div class="info-item"><div class="info-label">Model</div><div class="info-value">\${entry.response.model}</div></div>
2684
- <div class="info-item"><div class="info-label">Input Tokens</div><div class="info-value">\${formatNumber(entry.response.usage.input_tokens)}</div></div>
2685
- <div class="info-item"><div class="info-label">Output Tokens</div><div class="info-value">\${formatNumber(entry.response.usage.output_tokens)}</div></div>
2686
- <div class="info-item"><div class="info-label">Duration</div><div class="info-value">\${formatDuration(entry.durationMs)}</div></div>
2687
- <div class="info-item"><div class="info-label">Stop Reason</div><div class="info-value">\${entry.response.stop_reason || '-'}</div></div>
2688
- </div>
2689
- \${entry.response.error ? '<div class="error-detail"><div class="error-label">Error Details</div><pre class="error-content">' + escapeHtml(entry.response.error) + '</pre></div>' : ''}
2690
- </div>
2691
- \`;
2692
- }
2693
-
2694
- // System prompt
2695
- if (entry.request.system) {
2696
- html += \`
2697
- <div class="detail-section">
2698
- <h4>System Prompt</h4>
2699
- <div class="message system">
2700
- <div class="message-content">\${escapeHtml(entry.request.system)}</div>
2701
- </div>
2702
- </div>
2703
- \`;
2704
- }
2705
-
2706
- // Messages
2707
- html += '<div class="detail-section"><h4>Messages</h4><div class="messages-list">';
2708
- for (const msg of entry.request.messages) {
2709
- const roleClass = msg.role === 'user' ? 'user' : (msg.role === 'assistant' ? 'assistant' : (msg.role === 'system' ? 'system' : 'tool'));
2710
- const formatted = formatContentForDisplay(msg.content);
2711
- const isLong = formatted.summary.length > 500;
2712
- const rawContent = JSON.stringify(msg, null, 2);
2713
-
2714
- html += \`
2715
- <div class="message \${roleClass}">
2716
- <button class="raw-btn small" onclick="showRawJson(event, \${escapeAttr(rawContent)})">Raw</button>
2717
- <button class="copy-btn small" onclick="copyText(event, this)" data-content="\${escapeAttr(formatted.summary)}">Copy</button>
2718
- <div class="message-role">\${msg.role}\${msg.name ? ' (' + msg.name + ')' : ''}\${msg.tool_call_id ? ' [' + (msg.tool_call_id || '').slice(0,8) + ']' : ''}</div>
2719
- <div class="message-content\${isLong ? ' collapsed' : ''}" id="msg-\${Math.random().toString(36).slice(2)}">\${escapeHtml(formatted.summary)}</div>
2720
- \${isLong ? '<span class="expand-btn" onclick="toggleExpand(this)">Show more</span>' : ''}
2721
- \`;
2722
-
2723
- // Tool calls
2724
- if (msg.tool_calls && msg.tool_calls.length > 0) {
2725
- for (const tc of msg.tool_calls) {
2726
- html += \`
2727
- <div class="tool-call">
2728
- <span class="tool-name">\${tc.function.name}</span>
2729
- <div class="tool-args">\${escapeHtml(tc.function.arguments)}</div>
2730
- </div>
2731
- \`;
2732
- }
2733
- }
2734
-
2735
- html += '</div>';
2736
- }
2737
- html += '</div></div>';
2738
-
2739
- // Response content
2740
- if (entry.response?.content) {
2741
- const formatted = formatContentForDisplay(entry.response.content.content);
2742
- const rawContent = JSON.stringify(entry.response.content, null, 2);
2743
- html += \`
2744
- <div class="detail-section">
2745
- <h4>Response Content</h4>
2746
- <div class="message assistant">
2747
- <button class="raw-btn small" onclick="showRawJson(event, \${escapeAttr(rawContent)})">Raw</button>
2748
- <button class="copy-btn small" onclick="copyText(event, this)" data-content="\${escapeAttr(formatted.summary)}">Copy</button>
2749
- <div class="message-content">\${escapeHtml(formatted.summary)}</div>
2750
- </div>
2751
- </div>
2752
- \`;
2753
- }
2754
-
2755
- // Response tool calls
2756
- if (entry.response?.toolCalls && entry.response.toolCalls.length > 0) {
2757
- html += '<div class="detail-section"><h4>Tool Calls</h4>';
2758
- for (const tc of entry.response.toolCalls) {
2759
- const tcRaw = JSON.stringify(tc, null, 2);
2760
- html += \`
2761
- <div class="tool-call" style="position:relative;">
2762
- <button class="raw-btn small" style="position:absolute;top:4px;right:4px;opacity:1;" onclick="showRawJson(event, \${escapeAttr(tcRaw)})">Raw</button>
2763
- <span class="tool-name">\${tc.name}</span> <span style="color:var(--text-muted);font-size:11px;">[\${(tc.id || '').slice(0,8)}]</span>
2764
- <div class="tool-args">\${escapeHtml(tc.input)}</div>
2765
- </div>
2766
- \`;
2767
- }
2768
- html += '</div>';
2769
- }
2770
-
2771
- // Tools defined
2772
- if (entry.request.tools && entry.request.tools.length > 0) {
2773
- html += '<div class="detail-section"><h4>Available Tools (' + entry.request.tools.length + ')</h4>';
2774
- html += '<div style="font-size:11px;color:var(--text-muted)">' + entry.request.tools.map(t => t.name).join(', ') + '</div>';
2775
- html += '</div>';
2776
- }
2777
-
2778
- content.innerHTML = html;
2779
- } catch (e) {
2780
- content.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
2781
- }
2782
- }
2783
-
2784
- function closeDetail() {
2785
- currentEntryId = null;
2786
- document.getElementById('detail-panel').classList.remove('open');
2787
- loadEntries();
2788
- }
2789
-
2790
- function toggleExpand(btn) {
2791
- const content = btn.previousElementSibling;
2792
- const isCollapsed = content.classList.contains('collapsed');
2793
- content.classList.toggle('collapsed');
2794
- btn.textContent = isCollapsed ? 'Show less' : 'Show more';
2795
- }
2796
-
2797
- function copyText(event, btn) {
2798
- event.stopPropagation();
2799
- const text = btn.getAttribute('data-content');
2800
- navigator.clipboard.writeText(text);
2801
- const orig = btn.textContent;
2802
- btn.textContent = 'Copied!';
2803
- setTimeout(() => btn.textContent = orig, 1000);
2804
- }
2805
-
2806
- function escapeHtml(str) {
2807
- if (!str) return '';
2808
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2809
- }
2810
-
2811
- function escapeAttr(str) {
2812
- if (!str) return '';
2813
- return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
2814
- }
2815
-
2816
- let currentRawContent = '';
2817
-
2818
- function showRawJson(event, content) {
2819
- event.stopPropagation();
2820
- currentRawContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
2821
- document.getElementById('raw-content').textContent = currentRawContent;
2822
- document.getElementById('raw-modal').classList.add('open');
2823
- }
2824
-
2825
- function closeRawModal(event) {
2826
- if (event && event.target !== event.currentTarget) return;
2827
- document.getElementById('raw-modal').classList.remove('open');
2828
- }
2829
-
2830
- function copyRawContent() {
2831
- navigator.clipboard.writeText(currentRawContent);
2832
- const btns = document.querySelectorAll('.modal-header button');
2833
- const copyBtn = btns[0];
2834
- const orig = copyBtn.textContent;
2835
- copyBtn.textContent = 'Copied!';
2836
- setTimeout(() => copyBtn.textContent = orig, 1000);
2837
- }
2838
-
2839
- function debounceFilter() {
2840
- clearTimeout(debounceTimer);
2841
- debounceTimer = setTimeout(loadEntries, 300);
2842
- }
2843
-
2844
- function refresh() {
2845
- loadStats();
2846
- loadSessions();
2847
- loadEntries();
2848
- }
2849
-
2850
- function exportData(format) {
2851
- window.open('/history/api/export?format=' + format, '_blank');
2852
- }
2853
-
2854
- async function clearAll() {
2855
- if (!confirm('Clear all history? This cannot be undone.')) return;
2856
- try {
2857
- await fetch('/history/api/entries', { method: 'DELETE' });
2858
- currentSessionId = null;
2859
- currentEntryId = null;
2860
- closeDetail();
2861
- refresh();
2862
- } catch (e) {
2863
- alert('Failed: ' + e.message);
2864
- }
2865
- }
2866
-
2867
- // Initial load
2868
- loadStats();
2869
- loadSessions();
2870
- loadEntries();
2871
-
2872
- // Keyboard shortcuts
2873
- document.addEventListener('keydown', (e) => {
2874
- if (e.key === 'Escape') {
2875
- if (document.getElementById('raw-modal').classList.contains('open')) {
2876
- closeRawModal();
2877
- } else {
2878
- closeDetail();
2879
- }
2880
- }
2881
- if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
2882
- e.preventDefault();
2883
- refresh();
2884
- }
2885
- });
2886
-
2887
- // Auto-refresh every 10 seconds
2888
- setInterval(() => {
2889
- loadStats();
2890
- loadSessions();
2891
- }, 10000);
2892
- `;
2893
-
2894
- //#endregion
2895
- //#region src/routes/history/ui/styles.ts
2896
- const styles = `
2897
- :root {
2898
- --bg: #0d1117;
2899
- --bg-secondary: #161b22;
2900
- --bg-tertiary: #21262d;
2901
- --bg-hover: #30363d;
2902
- --text: #e6edf3;
2903
- --text-muted: #8b949e;
2904
- --text-dim: #6e7681;
2905
- --border: #30363d;
2906
- --primary: #58a6ff;
2907
- --success: #3fb950;
2908
- --error: #f85149;
2909
- --warning: #d29922;
2910
- --purple: #a371f7;
2911
- --cyan: #39c5cf;
2912
- }
2913
- @media (prefers-color-scheme: light) {
2914
- :root {
2915
- --bg: #ffffff;
2916
- --bg-secondary: #f6f8fa;
2917
- --bg-tertiary: #eaeef2;
2918
- --bg-hover: #d0d7de;
2919
- --text: #1f2328;
2920
- --text-muted: #656d76;
2921
- --text-dim: #8c959f;
2922
- --border: #d0d7de;
2923
- }
2924
- }
2925
- * { box-sizing: border-box; margin: 0; padding: 0; }
2926
- body {
2927
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
2928
- background: var(--bg);
2929
- color: var(--text);
2930
- line-height: 1.4;
2931
- font-size: 13px;
2932
- }
2933
-
2934
- /* Layout */
2935
- .layout { display: flex; height: 100vh; }
2936
- .sidebar {
2937
- width: 280px;
2938
- border-right: 1px solid var(--border);
2939
- display: flex;
2940
- flex-direction: column;
2941
- background: var(--bg-secondary);
2942
- }
2943
- .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
2944
-
2945
- /* Header */
2946
- .header {
2947
- padding: 12px 16px;
2948
- border-bottom: 1px solid var(--border);
2949
- display: flex;
2950
- align-items: center;
2951
- justify-content: space-between;
2952
- gap: 12px;
2953
- background: var(--bg-secondary);
2954
- }
2955
- .header h1 { font-size: 16px; font-weight: 600; }
2956
- .header-actions { display: flex; gap: 8px; }
2957
-
2958
- /* Stats bar */
2959
- .stats-bar {
2960
- display: flex;
2961
- gap: 16px;
2962
- padding: 8px 16px;
2963
- border-bottom: 1px solid var(--border);
2964
- background: var(--bg-tertiary);
2965
- font-size: 12px;
2966
- }
2967
- .stat { display: flex; align-items: center; gap: 4px; }
2968
- .stat-value { font-weight: 600; }
2969
- .stat-label { color: var(--text-muted); }
2970
-
2971
- /* Sessions sidebar */
2972
- .sidebar-header {
2973
- padding: 12px;
2974
- border-bottom: 1px solid var(--border);
2975
- font-weight: 600;
2976
- display: flex;
2977
- justify-content: space-between;
2978
- align-items: center;
2979
- }
2980
- .sessions-list {
2981
- flex: 1;
2982
- overflow-y: auto;
2983
- }
2984
- .session-item {
2985
- padding: 10px 12px;
2986
- border-bottom: 1px solid var(--border);
2987
- cursor: pointer;
2988
- transition: background 0.15s;
2989
- }
2990
- .session-item:hover { background: var(--bg-hover); }
2991
- .session-item.active { background: var(--bg-tertiary); border-left: 3px solid var(--primary); }
2992
- .session-item.all { font-weight: 600; color: var(--primary); }
2993
- .session-meta { display: flex; justify-content: space-between; margin-bottom: 4px; }
2994
- .session-time { color: var(--text-muted); font-size: 11px; }
2995
- .session-stats { display: flex; gap: 8px; font-size: 11px; color: var(--text-dim); }
2996
-
2997
- /* Buttons */
2998
- button {
2999
- background: var(--bg-tertiary);
3000
- border: 1px solid var(--border);
3001
- color: var(--text);
3002
- padding: 5px 10px;
3003
- border-radius: 6px;
3004
- cursor: pointer;
3005
- font-size: 12px;
3006
- transition: all 0.15s;
3007
- display: inline-flex;
3008
- align-items: center;
3009
- gap: 4px;
3010
- }
3011
- button:hover { background: var(--bg-hover); }
3012
- button.primary { background: var(--primary); color: #fff; border-color: var(--primary); }
3013
- button.danger { color: var(--error); }
3014
- button.danger:hover { background: rgba(248,81,73,0.1); }
3015
- button:disabled { opacity: 0.5; cursor: not-allowed; }
3016
- button.small { padding: 3px 6px; font-size: 11px; }
3017
- button.icon-only { padding: 5px 6px; }
3018
-
3019
- /* Filters */
3020
- .filters {
3021
- display: flex;
3022
- gap: 8px;
3023
- padding: 8px 16px;
3024
- border-bottom: 1px solid var(--border);
3025
- flex-wrap: wrap;
3026
- }
3027
- input, select {
3028
- background: var(--bg);
3029
- border: 1px solid var(--border);
3030
- color: var(--text);
3031
- padding: 5px 8px;
3032
- border-radius: 6px;
3033
- font-size: 12px;
3034
- }
3035
- input:focus, select:focus { outline: none; border-color: var(--primary); }
3036
- input::placeholder { color: var(--text-dim); }
3037
-
3038
- /* Entries list */
3039
- .entries-container { flex: 1; overflow-y: auto; }
3040
- .entry-item {
3041
- border-bottom: 1px solid var(--border);
3042
- cursor: pointer;
3043
- transition: background 0.15s;
3044
- }
3045
- .entry-item:hover { background: var(--bg-secondary); }
3046
- .entry-item.selected { background: var(--bg-tertiary); }
3047
- .entry-header {
3048
- display: flex;
3049
- align-items: center;
3050
- gap: 8px;
3051
- padding: 8px 16px;
3052
- }
3053
- .entry-time { color: var(--text-muted); font-size: 11px; min-width: 70px; }
3054
- .entry-model { font-weight: 500; flex: 1; }
3055
- .entry-tokens { font-size: 11px; color: var(--text-dim); }
3056
- .entry-duration { font-size: 11px; color: var(--text-dim); min-width: 50px; text-align: right; }
3057
- .entry-preview {
3058
- padding: 0 16px 8px 16px;
3059
- font-size: 11px;
3060
- color: var(--text-muted);
3061
- overflow: hidden;
3062
- text-overflow: ellipsis;
3063
- white-space: nowrap;
3064
- }
3065
-
3066
- /* Badges */
3067
- .badge {
3068
- display: inline-block;
3069
- padding: 1px 6px;
3070
- border-radius: 10px;
3071
- font-size: 10px;
3072
- font-weight: 500;
3073
- }
3074
- .badge.success { background: rgba(63, 185, 80, 0.15); color: var(--success); }
3075
- .badge.error { background: rgba(248, 81, 73, 0.15); color: var(--error); }
3076
- .badge.pending { background: rgba(136, 136, 136, 0.15); color: var(--text-muted); }
3077
- .badge.anthropic { background: rgba(163, 113, 247, 0.15); color: var(--purple); }
3078
- .badge.openai { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
3079
- .badge.stream { background: rgba(57, 197, 207, 0.15); color: var(--cyan); }
3080
- .badge.tool { background: rgba(88, 166, 255, 0.15); color: var(--primary); }
3081
-
3082
- /* Detail panel */
3083
- .detail-panel {
3084
- width: 0;
3085
- border-left: 1px solid var(--border);
3086
- background: var(--bg-secondary);
3087
- transition: width 0.2s;
3088
- overflow: hidden;
3089
- display: flex;
3090
- flex-direction: column;
3091
- }
3092
- .detail-panel.open { width: 50%; min-width: 400px; }
3093
- .detail-header {
3094
- padding: 12px 16px;
3095
- border-bottom: 1px solid var(--border);
3096
- display: flex;
3097
- justify-content: space-between;
3098
- align-items: center;
3099
- }
3100
- .detail-content { flex: 1; overflow-y: auto; padding: 16px; }
3101
- .detail-section { margin-bottom: 16px; }
3102
- .detail-section h4 {
3103
- font-size: 11px;
3104
- text-transform: uppercase;
3105
- color: var(--text-muted);
3106
- margin-bottom: 8px;
3107
- letter-spacing: 0.5px;
3108
- }
3109
-
3110
- /* Messages display */
3111
- .messages-list { display: flex; flex-direction: column; gap: 8px; }
3112
- .message {
3113
- padding: 10px 12px;
3114
- border-radius: 8px;
3115
- background: var(--bg);
3116
- border: 1px solid var(--border);
3117
- position: relative;
3118
- }
3119
- .message.user { border-left: 3px solid var(--primary); }
3120
- .message.assistant { border-left: 3px solid var(--success); }
3121
- .message.system { border-left: 3px solid var(--warning); background: var(--bg-tertiary); }
3122
- .message.tool { border-left: 3px solid var(--purple); }
3123
- .message-role {
3124
- font-size: 10px;
3125
- text-transform: uppercase;
3126
- color: var(--text-muted);
3127
- margin-bottom: 4px;
3128
- font-weight: 600;
3129
- }
3130
- .message-content {
3131
- white-space: pre-wrap;
3132
- word-break: break-word;
3133
- font-family: 'SF Mono', Monaco, 'Courier New', monospace;
3134
- font-size: 12px;
3135
- max-height: 300px;
3136
- overflow-y: auto;
3137
- }
3138
- .message-content.collapsed { max-height: 100px; }
3139
- .expand-btn {
3140
- color: var(--primary);
3141
- cursor: pointer;
3142
- font-size: 11px;
3143
- margin-top: 4px;
3144
- display: inline-block;
3145
- }
3146
-
3147
- /* Tool calls */
3148
- .tool-call {
3149
- background: var(--bg-tertiary);
3150
- padding: 8px;
3151
- border-radius: 6px;
3152
- margin-top: 8px;
3153
- font-size: 12px;
3154
- }
3155
- .tool-name { color: var(--purple); font-weight: 600; }
3156
- .tool-args {
3157
- font-family: monospace;
3158
- font-size: 11px;
3159
- color: var(--text-muted);
3160
- margin-top: 4px;
3161
- white-space: pre-wrap;
3162
- max-height: 150px;
3163
- overflow-y: auto;
3164
- }
3165
-
3166
- /* Response info */
3167
- .response-info {
3168
- display: grid;
3169
- grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
3170
- gap: 12px;
3171
- }
3172
- .info-item { }
3173
- .info-label { font-size: 11px; color: var(--text-muted); }
3174
- .info-value { font-weight: 500; }
3175
-
3176
- /* Error detail display */
3177
- .error-detail {
3178
- margin-top: 12px;
3179
- padding: 12px;
3180
- background: rgba(248, 81, 73, 0.1);
3181
- border: 1px solid rgba(248, 81, 73, 0.3);
3182
- border-radius: 6px;
3183
- }
3184
- .error-label {
3185
- font-size: 11px;
3186
- color: var(--error);
3187
- font-weight: 600;
3188
- margin-bottom: 8px;
3189
- text-transform: uppercase;
3190
- }
3191
- .error-content {
3192
- margin: 0;
3193
- font-family: 'SF Mono', Monaco, 'Courier New', monospace;
3194
- font-size: 12px;
3195
- color: var(--error);
3196
- white-space: pre-wrap;
3197
- word-break: break-word;
3198
- max-height: 300px;
3199
- overflow-y: auto;
3200
- }
3201
-
3202
- /* Empty state */
3203
- .empty-state {
3204
- text-align: center;
3205
- padding: 40px 20px;
3206
- color: var(--text-muted);
3207
- }
3208
- .empty-state h3 { margin-bottom: 8px; color: var(--text); }
3209
-
3210
- /* Loading */
3211
- .loading { text-align: center; padding: 20px; color: var(--text-muted); }
3212
-
3213
- /* Scrollbar */
3214
- ::-webkit-scrollbar { width: 8px; height: 8px; }
3215
- ::-webkit-scrollbar-track { background: var(--bg); }
3216
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
3217
- ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
3218
-
3219
- /* Copy/Raw buttons */
3220
- .copy-btn, .raw-btn {
3221
- position: absolute;
3222
- top: 4px;
3223
- opacity: 0;
3224
- transition: opacity 0.15s;
3225
- }
3226
- .copy-btn { right: 4px; }
3227
- .raw-btn { right: 50px; }
3228
- .message:hover .copy-btn, .message:hover .raw-btn { opacity: 1; }
3229
-
3230
- /* Raw JSON modal */
3231
- .modal-overlay {
3232
- position: fixed;
3233
- top: 0; left: 0; right: 0; bottom: 0;
3234
- background: rgba(0,0,0,0.6);
3235
- display: none;
3236
- justify-content: center;
3237
- align-items: center;
3238
- z-index: 1000;
3239
- }
3240
- .modal-overlay.open { display: flex; }
3241
- .modal {
3242
- background: var(--bg-secondary);
3243
- border: 1px solid var(--border);
3244
- border-radius: 8px;
3245
- width: 80%;
3246
- max-width: 800px;
3247
- max-height: 80vh;
3248
- display: flex;
3249
- flex-direction: column;
3250
- }
3251
- .modal-header {
3252
- padding: 12px 16px;
3253
- border-bottom: 1px solid var(--border);
3254
- display: flex;
3255
- justify-content: space-between;
3256
- align-items: center;
3257
- }
3258
- .modal-body {
3259
- flex: 1;
3260
- overflow: auto;
3261
- padding: 16px;
3262
- }
3263
- .modal-body pre {
3264
- margin: 0;
3265
- font-family: 'SF Mono', Monaco, 'Courier New', monospace;
3266
- font-size: 12px;
3267
- white-space: pre-wrap;
3268
- word-break: break-word;
3269
- }
3270
- `;
3271
-
3272
- //#endregion
3273
- //#region src/routes/history/ui/template.ts
3274
- const template = `
3275
- <div class="layout">
3276
- <!-- Sidebar: Sessions -->
3277
- <div class="sidebar">
3278
- <div class="sidebar-header">
3279
- <span>Sessions</span>
3280
- <button class="small danger" onclick="clearAll()" title="Clear all">Clear</button>
3281
- </div>
3282
- <div class="sessions-list" id="sessions-list">
3283
- <div class="loading">Loading...</div>
3284
- </div>
3285
- </div>
3286
-
3287
- <!-- Main content -->
3288
- <div class="main">
3289
- <div class="header">
3290
- <h1>Request History</h1>
3291
- <div class="header-actions">
3292
- <button onclick="refresh()">Refresh</button>
3293
- <button onclick="exportData('json')">Export JSON</button>
3294
- <button onclick="exportData('csv')">Export CSV</button>
3295
- </div>
3296
- </div>
3297
-
3298
- <div class="stats-bar" id="stats-bar">
3299
- <div class="stat"><span class="stat-value" id="stat-total">-</span><span class="stat-label">requests</span></div>
3300
- <div class="stat"><span class="stat-value" id="stat-success">-</span><span class="stat-label">success</span></div>
3301
- <div class="stat"><span class="stat-value" id="stat-failed">-</span><span class="stat-label">failed</span></div>
3302
- <div class="stat"><span class="stat-value" id="stat-input">-</span><span class="stat-label">in tokens</span></div>
3303
- <div class="stat"><span class="stat-value" id="stat-output">-</span><span class="stat-label">out tokens</span></div>
3304
- <div class="stat"><span class="stat-value" id="stat-sessions">-</span><span class="stat-label">sessions</span></div>
3305
- </div>
3306
-
3307
- <div class="filters">
3308
- <input type="text" id="filter-search" placeholder="Search messages..." style="flex:1;min-width:150px" onkeyup="debounceFilter()">
3309
- <select id="filter-endpoint" onchange="loadEntries()">
3310
- <option value="">All Endpoints</option>
3311
- <option value="anthropic">Anthropic</option>
3312
- <option value="openai">OpenAI</option>
3313
- </select>
3314
- <select id="filter-success" onchange="loadEntries()">
3315
- <option value="">All Status</option>
3316
- <option value="true">Success</option>
3317
- <option value="false">Failed</option>
3318
- </select>
3319
- </div>
3320
-
3321
- <div style="display:flex;flex:1;overflow:hidden;">
3322
- <div class="entries-container" id="entries-container">
3323
- <div class="loading">Loading...</div>
3324
- </div>
3325
-
3326
- <!-- Detail panel -->
3327
- <div class="detail-panel" id="detail-panel">
3328
- <div class="detail-header">
3329
- <span>Request Details</span>
3330
- <button class="icon-only" onclick="closeDetail()">&times;</button>
3331
- </div>
3332
- <div class="detail-content" id="detail-content"></div>
3333
- </div>
3334
- </div>
3335
- </div>
3336
- </div>
3337
-
3338
- <!-- Raw JSON Modal -->
3339
- <div class="modal-overlay" id="raw-modal" onclick="closeRawModal(event)">
3340
- <div class="modal" onclick="event.stopPropagation()">
3341
- <div class="modal-header">
3342
- <span>Raw JSON</span>
3343
- <div>
3344
- <button class="small" onclick="copyRawContent()">Copy</button>
3345
- <button class="icon-only" onclick="closeRawModal()">&times;</button>
3346
- </div>
3347
- </div>
3348
- <div class="modal-body">
3349
- <pre id="raw-content"></pre>
3350
- </div>
3351
- </div>
3352
- </div>
3353
- `;
3354
-
3355
- //#endregion
3356
- //#region src/routes/history/ui.ts
3357
- function getHistoryUI() {
3358
- return `<!DOCTYPE html>
3359
- <html lang="en">
3360
- <head>
3361
- <meta charset="UTF-8">
3362
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
3363
- <title>Copilot API - Request History</title>
3364
- <style>${styles}</style>
3365
- </head>
3366
- <body>
3367
- ${template}
3368
- <script>${script}<\/script>
3369
- </body>
3370
- </html>`;
3371
- }
3372
-
3373
- //#endregion
3374
- //#region src/routes/history/route.ts
3375
- const historyRoutes = new Hono();
3376
- historyRoutes.get("/api/entries", handleGetEntries);
3377
- historyRoutes.get("/api/entries/:id", handleGetEntry);
3378
- historyRoutes.delete("/api/entries", handleDeleteEntries);
3379
- historyRoutes.get("/api/stats", handleGetStats);
3380
- historyRoutes.get("/api/export", handleExport);
3381
- historyRoutes.get("/api/sessions", handleGetSessions);
3382
- historyRoutes.get("/api/sessions/:id", handleGetSession);
3383
- historyRoutes.delete("/api/sessions/:id", handleDeleteSession);
3384
- historyRoutes.get("/", (c) => {
3385
- return c.html(getHistoryUI());
3386
- });
3387
-
3388
- //#endregion
3389
- //#region src/routes/messages/utils.ts
3390
- function mapOpenAIStopReasonToAnthropic(finishReason) {
3391
- if (finishReason === null) return null;
3392
- return {
3393
- stop: "end_turn",
3394
- length: "max_tokens",
3395
- tool_calls: "tool_use",
3396
- content_filter: "end_turn"
3397
- }[finishReason];
3398
- }
3399
-
3400
- //#endregion
3401
- //#region src/routes/messages/non-stream-translation.ts
3402
- const OPENAI_TOOL_NAME_LIMIT = 64;
3403
- function fixMessageSequence(messages) {
3404
- const fixedMessages = [];
3405
- for (let i = 0; i < messages.length; i++) {
3406
- const message = messages[i];
3407
- fixedMessages.push(message);
3408
- if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
3409
- const foundToolResponses = /* @__PURE__ */ new Set();
3410
- let j = i + 1;
3411
- while (j < messages.length && messages[j].role === "tool") {
3412
- const toolMessage = messages[j];
3413
- if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
3414
- j++;
3415
- }
3416
- for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
3417
- consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
3418
- fixedMessages.push({
3419
- role: "tool",
3420
- tool_call_id: toolCall.id,
3421
- content: "Tool execution was interrupted or failed."
3422
- });
3423
- }
3424
- }
3425
- }
3426
- return fixedMessages;
3427
- }
3428
- function translateToOpenAI(payload) {
3429
- const toolNameMapping = {
3430
- truncatedToOriginal: /* @__PURE__ */ new Map(),
3431
- originalToTruncated: /* @__PURE__ */ new Map()
3432
- };
3433
- const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
3434
- return {
3435
- payload: {
3436
- model: translateModelName(payload.model),
3437
- messages: fixMessageSequence(messages),
3438
- max_tokens: payload.max_tokens,
3439
- stop: payload.stop_sequences,
3440
- stream: payload.stream,
3441
- temperature: payload.temperature,
3442
- top_p: payload.top_p,
3443
- user: payload.metadata?.user_id,
3444
- tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
3445
- tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
3446
- },
3447
- toolNameMapping
3448
- };
3449
- }
3450
- function translateModelName(model) {
3451
- const shortNameMap = {
3452
- opus: "claude-opus-4.5",
3453
- sonnet: "claude-sonnet-4.5",
3454
- haiku: "claude-haiku-4.5"
3455
- };
3456
- if (shortNameMap[model]) return shortNameMap[model];
3457
- if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
3458
- if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
3459
- if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
3460
- if (/^claude-opus-4-\d+$/.test(model)) return "claude-opus-4.5";
3461
- if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
3462
- if (/^claude-haiku-3-5-\d+$/.test(model)) return "claude-haiku-4.5";
3463
- return model;
3464
- }
3465
- function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
3466
- const systemMessages = handleSystemPrompt(system);
3467
- const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
3468
- return [...systemMessages, ...otherMessages];
3469
- }
3470
- function handleSystemPrompt(system) {
3471
- if (!system) return [];
3472
- if (typeof system === "string") return [{
3473
- role: "system",
3474
- content: system
3475
- }];
3476
- else return [{
3477
- role: "system",
3478
- content: system.map((block) => block.text).join("\n\n")
3479
- }];
3480
- }
3481
- function handleUserMessage(message) {
3482
- const newMessages = [];
3483
- if (Array.isArray(message.content)) {
3484
- const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
3485
- const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
3486
- for (const block of toolResultBlocks) newMessages.push({
3487
- role: "tool",
3488
- tool_call_id: block.tool_use_id,
3489
- content: mapContent(block.content)
3490
- });
3491
- if (otherBlocks.length > 0) newMessages.push({
3492
- role: "user",
3493
- content: mapContent(otherBlocks)
3494
- });
3495
- } else newMessages.push({
3496
- role: "user",
3497
- content: mapContent(message.content)
3498
- });
3499
- return newMessages;
3500
- }
3501
- function handleAssistantMessage(message, toolNameMapping) {
3502
- if (!Array.isArray(message.content)) return [{
3503
- role: "assistant",
3504
- content: mapContent(message.content)
3505
- }];
3506
- const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
3507
- const textBlocks = message.content.filter((block) => block.type === "text");
3508
- const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
3509
- const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
3510
- return toolUseBlocks.length > 0 ? [{
3511
- role: "assistant",
3512
- content: allTextContent || null,
3513
- tool_calls: toolUseBlocks.map((toolUse) => ({
3514
- id: toolUse.id,
3515
- type: "function",
3516
- function: {
3517
- name: getTruncatedToolName(toolUse.name, toolNameMapping),
3518
- arguments: JSON.stringify(toolUse.input)
3519
- }
3520
- }))
3521
- }] : [{
3522
- role: "assistant",
3523
- content: mapContent(message.content)
3524
- }];
3525
- }
3526
- function mapContent(content) {
3527
- if (typeof content === "string") return content;
3528
- if (!Array.isArray(content)) return null;
3529
- 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");
3530
- const contentParts = [];
3531
- for (const block of content) switch (block.type) {
3532
- case "text":
3533
- contentParts.push({
3534
- type: "text",
3535
- text: block.text
3536
- });
3537
- break;
3538
- case "thinking":
3539
- contentParts.push({
3540
- type: "text",
3541
- text: block.thinking
3542
- });
3543
- break;
3544
- case "image":
3545
- contentParts.push({
3546
- type: "image_url",
3547
- image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
3548
- });
3549
- break;
3550
- }
3551
- return contentParts;
3552
- }
3553
- function getTruncatedToolName(originalName, toolNameMapping) {
3554
- if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
3555
- const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
3556
- if (existingTruncated) return existingTruncated;
3557
- let hash = 0;
3558
- for (let i = 0; i < originalName.length; i++) {
3559
- const char = originalName.codePointAt(i) ?? 0;
3560
- hash = (hash << 5) - hash + char;
3561
- hash = hash & hash;
3562
- }
3563
- const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
3564
- const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
3565
- toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
3566
- toolNameMapping.originalToTruncated.set(originalName, truncatedName);
3567
- consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
3568
- return truncatedName;
3569
- }
3570
- function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
3571
- if (!anthropicTools) return;
3572
- return anthropicTools.map((tool) => ({
3573
- type: "function",
3574
- function: {
3575
- name: getTruncatedToolName(tool.name, toolNameMapping),
3576
- description: tool.description,
3577
- parameters: tool.input_schema
3578
- }
3579
- }));
3580
- }
3581
- function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
3582
- if (!anthropicToolChoice) return;
3583
- switch (anthropicToolChoice.type) {
3584
- case "auto": return "auto";
3585
- case "any": return "required";
3586
- case "tool":
3587
- if (anthropicToolChoice.name) return {
3588
- type: "function",
3589
- function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
3590
- };
3591
- return;
3592
- case "none": return "none";
3593
- default: return;
3594
- }
3595
- }
3596
- /** Create empty response for edge case of no choices */
3597
- function createEmptyResponse(response) {
3598
- return {
3599
- id: response.id,
3600
- type: "message",
3601
- role: "assistant",
3602
- model: response.model,
3603
- content: [],
3604
- stop_reason: "end_turn",
3605
- stop_sequence: null,
3606
- usage: {
3607
- input_tokens: response.usage?.prompt_tokens ?? 0,
3608
- output_tokens: response.usage?.completion_tokens ?? 0
3609
- }
3610
- };
3611
- }
3612
- /** Build usage object from response */
3613
- function buildUsageObject(response) {
3614
- const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
3615
- return {
3616
- input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
3617
- output_tokens: response.usage?.completion_tokens ?? 0,
3618
- ...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
3619
- };
3620
- }
3621
- function translateToAnthropic(response, toolNameMapping) {
3622
- if (response.choices.length === 0) return createEmptyResponse(response);
3623
- const allTextBlocks = [];
3624
- const allToolUseBlocks = [];
3625
- let stopReason = null;
3626
- stopReason = response.choices[0]?.finish_reason ?? stopReason;
3627
- for (const choice of response.choices) {
3628
- const textBlocks = getAnthropicTextBlocks(choice.message.content);
3629
- const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
3630
- allTextBlocks.push(...textBlocks);
3631
- allToolUseBlocks.push(...toolUseBlocks);
3632
- if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
3633
- }
3634
- return {
3635
- id: response.id,
3636
- type: "message",
3637
- role: "assistant",
3638
- model: response.model,
3639
- content: [...allTextBlocks, ...allToolUseBlocks],
3640
- stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
3641
- stop_sequence: null,
3642
- usage: buildUsageObject(response)
3643
- };
3644
- }
3645
- function getAnthropicTextBlocks(messageContent) {
3646
- if (typeof messageContent === "string") return [{
3647
- type: "text",
3648
- text: messageContent
3649
- }];
3650
- if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
3651
- type: "text",
3652
- text: part.text
3653
- }));
3654
- return [];
3655
- }
3656
- function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
3657
- if (!toolCalls) return [];
3658
- return toolCalls.map((toolCall) => {
3659
- let input = {};
3660
- try {
3661
- input = JSON.parse(toolCall.function.arguments);
3662
- } catch (error) {
3663
- consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
3664
- }
3665
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
3666
- return {
3667
- type: "tool_use",
3668
- id: toolCall.id,
3669
- name: originalName,
3670
- input
3671
- };
3672
- });
3673
- }
3674
-
3675
- //#endregion
3676
- //#region src/routes/messages/count-tokens-handler.ts
3677
- /**
3678
- * Handles token counting for Anthropic messages
3679
- */
3680
- async function handleCountTokens(c) {
3681
- try {
3682
- const anthropicBeta = c.req.header("anthropic-beta");
3683
- const anthropicPayload = await c.req.json();
3684
- const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
3685
- const selectedModel = state.models?.data.find((model) => model.id === anthropicPayload.model);
3686
- if (!selectedModel) {
3687
- consola.warn("Model not found, returning default token count");
3688
- return c.json({ input_tokens: 1 });
3689
- }
3690
- const tokenCount = await getTokenCount(openAIPayload, selectedModel);
3691
- if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
3692
- let mcpToolExist = false;
3693
- if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
3694
- if (!mcpToolExist) {
3695
- if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
3696
- else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
3697
- }
3698
- }
3699
- let finalTokenCount = tokenCount.input + tokenCount.output;
3700
- if (anthropicPayload.model.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
3701
- else if (anthropicPayload.model.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
3702
- consola.info("Token count:", finalTokenCount);
3703
- return c.json({ input_tokens: finalTokenCount });
3704
- } catch (error) {
3705
- consola.error("Error counting tokens:", error);
3706
- return c.json({ input_tokens: 1 });
3707
- }
3708
- }
3709
-
3710
- //#endregion
3711
- //#region src/routes/messages/stream-translation.ts
3712
- function isToolBlockOpen(state$1) {
3713
- if (!state$1.contentBlockOpen) return false;
3714
- return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
3715
- }
3716
- function translateChunkToAnthropicEvents(chunk, state$1, toolNameMapping) {
3717
- const events$1 = [];
3718
- if (chunk.choices.length === 0) {
3719
- if (chunk.model && !state$1.model) state$1.model = chunk.model;
3720
- return events$1;
3721
- }
3722
- const choice = chunk.choices[0];
3723
- const { delta } = choice;
3724
- if (!state$1.messageStartSent) {
3725
- const model = chunk.model || state$1.model || "unknown";
3726
- events$1.push({
3727
- type: "message_start",
3728
- message: {
3729
- id: chunk.id || `msg_${Date.now()}`,
3730
- type: "message",
3731
- role: "assistant",
3732
- content: [],
3733
- model,
3734
- stop_reason: null,
3735
- stop_sequence: null,
3736
- usage: {
3737
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
3738
- output_tokens: 0,
3739
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
3740
- }
3741
- }
3742
- });
3743
- state$1.messageStartSent = true;
3744
- }
3745
- if (delta.content) {
3746
- if (isToolBlockOpen(state$1)) {
3747
- events$1.push({
3748
- type: "content_block_stop",
3749
- index: state$1.contentBlockIndex
3750
- });
3751
- state$1.contentBlockIndex++;
3752
- state$1.contentBlockOpen = false;
3753
- }
3754
- if (!state$1.contentBlockOpen) {
3755
- events$1.push({
3756
- type: "content_block_start",
3757
- index: state$1.contentBlockIndex,
3758
- content_block: {
3759
- type: "text",
3760
- text: ""
3761
- }
3762
- });
3763
- state$1.contentBlockOpen = true;
3764
- }
3765
- events$1.push({
3766
- type: "content_block_delta",
3767
- index: state$1.contentBlockIndex,
3768
- delta: {
3769
- type: "text_delta",
3770
- text: delta.content
3771
- }
3772
- });
3773
- }
3774
- if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
3775
- if (toolCall.id && toolCall.function?.name) {
3776
- if (state$1.contentBlockOpen) {
3777
- events$1.push({
3778
- type: "content_block_stop",
3779
- index: state$1.contentBlockIndex
3780
- });
3781
- state$1.contentBlockIndex++;
3782
- state$1.contentBlockOpen = false;
3783
- }
3784
- const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
3785
- const anthropicBlockIndex = state$1.contentBlockIndex;
3786
- state$1.toolCalls[toolCall.index] = {
3787
- id: toolCall.id,
3788
- name: originalName,
3789
- anthropicBlockIndex
3790
- };
3791
- events$1.push({
3792
- type: "content_block_start",
3793
- index: anthropicBlockIndex,
3794
- content_block: {
3795
- type: "tool_use",
3796
- id: toolCall.id,
3797
- name: originalName,
3798
- input: {}
3799
- }
3800
- });
3801
- state$1.contentBlockOpen = true;
3802
- }
3803
- if (toolCall.function?.arguments) {
3804
- const toolCallInfo = state$1.toolCalls[toolCall.index];
3805
- if (toolCallInfo) events$1.push({
3806
- type: "content_block_delta",
3807
- index: toolCallInfo.anthropicBlockIndex,
3808
- delta: {
3809
- type: "input_json_delta",
3810
- partial_json: toolCall.function.arguments
3811
- }
3812
- });
3813
- }
3814
- }
3815
- if (choice.finish_reason) {
3816
- if (state$1.contentBlockOpen) {
3817
- events$1.push({
3818
- type: "content_block_stop",
3819
- index: state$1.contentBlockIndex
3820
- });
3821
- state$1.contentBlockOpen = false;
3822
- }
3823
- events$1.push({
3824
- type: "message_delta",
3825
- delta: {
3826
- stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
3827
- stop_sequence: null
3828
- },
3829
- usage: {
3830
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
3831
- output_tokens: chunk.usage?.completion_tokens ?? 0,
3832
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
3833
- }
3834
- }, { type: "message_stop" });
3835
- }
3836
- return events$1;
3837
- }
3838
- function translateErrorToAnthropicErrorEvent() {
3839
- return {
3840
- type: "error",
3841
- error: {
3842
- type: "api_error",
3843
- message: "An unexpected error occurred during streaming."
3844
- }
3845
- };
3846
- }
3847
-
3848
- //#endregion
3849
- //#region src/routes/messages/handler.ts
3850
- async function handleCompletion(c) {
3851
- const startTime = Date.now();
3852
- const anthropicPayload = await c.req.json();
3853
- consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
3854
- const trackingId = c.get("trackingId");
3855
- updateTrackerModel(trackingId, anthropicPayload.model);
3856
- const ctx = {
3857
- historyId: recordRequest("anthropic", {
3858
- model: anthropicPayload.model,
3859
- messages: convertAnthropicMessages(anthropicPayload.messages),
3860
- stream: anthropicPayload.stream ?? false,
3861
- tools: anthropicPayload.tools?.map((t) => ({
3862
- name: t.name,
3863
- description: t.description
3864
- })),
3865
- max_tokens: anthropicPayload.max_tokens,
3866
- temperature: anthropicPayload.temperature,
3867
- system: extractSystemPrompt(anthropicPayload.system)
3868
- }),
3869
- trackingId,
3870
- startTime
3871
- };
3872
- const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
3873
- consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
3874
- const selectedModel = state.models?.data.find((model) => model.id === translatedPayload.model);
3875
- const { finalPayload: openAIPayload, compactResult } = await buildFinalPayload(translatedPayload, selectedModel);
3876
- if (compactResult) ctx.compactResult = compactResult;
3877
- if (state.manualApprove) await awaitApproval();
3878
- try {
3879
- const response = await executeWithRateLimit(state, () => createChatCompletions(openAIPayload));
3880
- if (isNonStreaming(response)) return handleNonStreamingResponse({
3881
- c,
3882
- response,
3883
- toolNameMapping,
3884
- ctx
3885
- });
3886
- consola.debug("Streaming response from Copilot");
3887
- updateTrackerStatus(trackingId, "streaming");
3888
- return streamSSE(c, async (stream) => {
3889
- await handleStreamingResponse({
3890
- stream,
3891
- response,
3892
- toolNameMapping,
3893
- anthropicPayload,
3894
- ctx
3895
- });
3896
- });
3897
- } catch (error) {
3898
- recordErrorResponse(ctx, anthropicPayload.model, error);
3899
- throw error;
3900
- }
3901
- }
3902
- function updateTrackerModel(trackingId, model) {
3903
- if (!trackingId) return;
3904
- const request = requestTracker.getRequest(trackingId);
3905
- if (request) request.model = model;
3906
- }
3907
- async function buildFinalPayload(payload, model) {
3908
- if (!state.autoCompact || !model) {
3909
- if (state.autoCompact && !model) consola.warn(`Auto-compact: Model '${payload.model}' not found in cached models, skipping`);
3910
- return {
3911
- finalPayload: payload,
3912
- compactResult: null
3913
- };
3914
- }
3915
- try {
3916
- const check = await checkNeedsCompaction(payload, model);
3917
- consola.info(`Auto-compact check: ${check.currentTokens} tokens, limit ${check.limit}, needed: ${check.needed}`);
3918
- if (!check.needed) return {
3919
- finalPayload: payload,
3920
- compactResult: null
3921
- };
3922
- consola.info(`Auto-compact triggered: ${check.currentTokens} tokens > ${check.limit} limit`);
3923
- const compactResult = await autoCompact(payload, model);
3924
- return {
3925
- finalPayload: compactResult.payload,
3926
- compactResult
3927
- };
3928
- } catch (error) {
3929
- consola.warn("Auto-compact failed, proceeding with original payload:", error);
3930
- return {
3931
- finalPayload: payload,
3932
- compactResult: null
3933
- };
3934
- }
3935
- }
3936
- function updateTrackerStatus(trackingId, status) {
3937
- if (!trackingId) return;
3938
- requestTracker.updateRequest(trackingId, { status });
3939
- }
3940
- function recordErrorResponse(ctx, model, error) {
3941
- recordResponse(ctx.historyId, {
3942
- success: false,
3943
- model,
3944
- usage: {
3945
- input_tokens: 0,
3946
- output_tokens: 0
3947
- },
3948
- error: error instanceof Error ? error.message : "Unknown error",
3949
- content: null
3950
- }, Date.now() - ctx.startTime);
3951
- }
3952
- function handleNonStreamingResponse(opts) {
3953
- const { c, response, toolNameMapping, ctx } = opts;
3954
- consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
3955
- let anthropicResponse = translateToAnthropic(response, toolNameMapping);
3956
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
3957
- if (ctx.compactResult?.wasCompacted) {
3958
- const marker = createCompactionMarker(ctx.compactResult);
3959
- anthropicResponse = appendMarkerToAnthropicResponse(anthropicResponse, marker);
3960
- }
3961
- recordResponse(ctx.historyId, {
3962
- success: true,
3963
- model: anthropicResponse.model,
3964
- usage: anthropicResponse.usage,
3965
- stop_reason: anthropicResponse.stop_reason ?? void 0,
3966
- content: {
3967
- role: "assistant",
3968
- content: anthropicResponse.content.map((block) => {
3969
- if (block.type === "text") return {
3970
- type: "text",
3971
- text: block.text
3972
- };
3973
- if (block.type === "tool_use") return {
3974
- type: "tool_use",
3975
- id: block.id,
3976
- name: block.name,
3977
- input: JSON.stringify(block.input)
3978
- };
3979
- return { type: block.type };
3980
- })
3981
- },
3982
- toolCalls: extractToolCallsFromContent(anthropicResponse.content)
3983
- }, Date.now() - ctx.startTime);
3984
- if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
3985
- inputTokens: anthropicResponse.usage.input_tokens,
3986
- outputTokens: anthropicResponse.usage.output_tokens
3987
- });
3988
- return c.json(anthropicResponse);
3989
- }
3990
- function appendMarkerToAnthropicResponse(response, marker) {
3991
- const content = [...response.content];
3992
- const lastTextIndex = content.findLastIndex((block) => block.type === "text");
3993
- if (lastTextIndex !== -1) {
3994
- const textBlock = content[lastTextIndex];
3995
- if (textBlock.type === "text") content[lastTextIndex] = {
3996
- ...textBlock,
3997
- text: textBlock.text + marker
3998
- };
3999
- } else content.push({
4000
- type: "text",
4001
- text: marker
4002
- });
4003
- return {
4004
- ...response,
4005
- content
4006
- };
4007
- }
4008
- function createAnthropicStreamAccumulator() {
4009
- return {
4010
- model: "",
4011
- inputTokens: 0,
4012
- outputTokens: 0,
4013
- stopReason: "",
4014
- content: "",
4015
- toolCalls: [],
4016
- currentToolCall: null
4017
- };
4018
- }
4019
- async function handleStreamingResponse(opts) {
4020
- const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
4021
- const streamState = {
4022
- messageStartSent: false,
4023
- contentBlockIndex: 0,
4024
- contentBlockOpen: false,
4025
- toolCalls: {}
4026
- };
4027
- const acc = createAnthropicStreamAccumulator();
4028
- try {
4029
- await processStreamChunks({
4030
- stream,
4031
- response,
4032
- toolNameMapping,
4033
- streamState,
4034
- acc
4035
- });
4036
- if (ctx.compactResult?.wasCompacted) {
4037
- const marker = createCompactionMarker(ctx.compactResult);
4038
- await sendCompactionMarkerEvent(stream, streamState, marker);
4039
- acc.content += marker;
4040
- }
4041
- recordStreamingResponse(acc, anthropicPayload.model, ctx);
4042
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens);
4043
- } catch (error) {
4044
- consola.error("Stream error:", error);
4045
- recordStreamingError({
4046
- acc,
4047
- fallbackModel: anthropicPayload.model,
4048
- ctx,
4049
- error
4050
- });
4051
- failTracking(ctx.trackingId, error);
4052
- const errorEvent = translateErrorToAnthropicErrorEvent();
4053
- await stream.writeSSE({
4054
- event: errorEvent.type,
4055
- data: JSON.stringify(errorEvent)
4056
- });
4057
- }
4058
- }
4059
- async function sendCompactionMarkerEvent(stream, streamState, marker) {
4060
- const blockStartEvent = {
4061
- type: "content_block_start",
4062
- index: streamState.contentBlockIndex,
4063
- content_block: {
4064
- type: "text",
4065
- text: ""
4066
- }
4067
- };
4068
- await stream.writeSSE({
4069
- event: "content_block_start",
4070
- data: JSON.stringify(blockStartEvent)
4071
- });
4072
- const deltaEvent = {
4073
- type: "content_block_delta",
4074
- index: streamState.contentBlockIndex,
4075
- delta: {
4076
- type: "text_delta",
4077
- text: marker
4078
- }
4079
- };
4080
- await stream.writeSSE({
4081
- event: "content_block_delta",
4082
- data: JSON.stringify(deltaEvent)
4083
- });
4084
- const blockStopEvent = {
4085
- type: "content_block_stop",
4086
- index: streamState.contentBlockIndex
4087
- };
4088
- await stream.writeSSE({
4089
- event: "content_block_stop",
4090
- data: JSON.stringify(blockStopEvent)
4091
- });
4092
- streamState.contentBlockIndex++;
4093
- }
4094
- async function processStreamChunks(opts) {
4095
- const { stream, response, toolNameMapping, streamState, acc } = opts;
4096
- for await (const rawEvent of response) {
4097
- consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
4098
- if (rawEvent.data === "[DONE]") break;
4099
- if (!rawEvent.data) continue;
4100
- let chunk;
4101
- try {
4102
- chunk = JSON.parse(rawEvent.data);
4103
- } catch (parseError) {
4104
- consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
4105
- continue;
4106
- }
4107
- if (chunk.model && !acc.model) acc.model = chunk.model;
4108
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
4109
- for (const event of events$1) {
4110
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
4111
- processAnthropicEvent(event, acc);
4112
- await stream.writeSSE({
4113
- event: event.type,
4114
- data: JSON.stringify(event)
4115
- });
4116
- }
4117
- }
4118
- }
4119
- function processAnthropicEvent(event, acc) {
4120
- switch (event.type) {
4121
- case "content_block_delta":
4122
- handleContentBlockDelta(event.delta, acc);
4123
- break;
4124
- case "content_block_start":
4125
- handleContentBlockStart(event.content_block, acc);
4126
- break;
4127
- case "content_block_stop":
4128
- handleContentBlockStop(acc);
4129
- break;
4130
- case "message_delta":
4131
- handleMessageDelta(event.delta, event.usage, acc);
4132
- break;
4133
- default: break;
4134
- }
4135
- }
4136
- function handleContentBlockDelta(delta, acc) {
4137
- if (delta.type === "text_delta") acc.content += delta.text;
4138
- else if (delta.type === "input_json_delta" && acc.currentToolCall) acc.currentToolCall.input += delta.partial_json;
4139
- }
4140
- function handleContentBlockStart(block, acc) {
4141
- if (block.type === "tool_use") acc.currentToolCall = {
4142
- id: block.id,
4143
- name: block.name,
4144
- input: ""
4145
- };
4146
- }
4147
- function handleContentBlockStop(acc) {
4148
- if (acc.currentToolCall) {
4149
- acc.toolCalls.push(acc.currentToolCall);
4150
- acc.currentToolCall = null;
4151
- }
4152
- }
4153
- function handleMessageDelta(delta, usage, acc) {
4154
- if (delta.stop_reason) acc.stopReason = delta.stop_reason;
4155
- if (usage) {
4156
- acc.inputTokens = usage.input_tokens ?? 0;
4157
- acc.outputTokens = usage.output_tokens;
4158
- }
4159
- }
4160
- function recordStreamingResponse(acc, fallbackModel, ctx) {
4161
- const contentBlocks = [];
4162
- if (acc.content) contentBlocks.push({
4163
- type: "text",
4164
- text: acc.content
4165
- });
4166
- for (const tc of acc.toolCalls) contentBlocks.push({
4167
- type: "tool_use",
4168
- ...tc
4169
- });
4170
- recordResponse(ctx.historyId, {
4171
- success: true,
4172
- model: acc.model || fallbackModel,
4173
- usage: {
4174
- input_tokens: acc.inputTokens,
4175
- output_tokens: acc.outputTokens
4176
- },
4177
- stop_reason: acc.stopReason || void 0,
4178
- content: contentBlocks.length > 0 ? {
4179
- role: "assistant",
4180
- content: contentBlocks
4181
- } : null,
4182
- toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls : void 0
4183
- }, Date.now() - ctx.startTime);
4184
- }
4185
- function recordStreamingError(opts) {
4186
- const { acc, fallbackModel, ctx, error } = opts;
4187
- recordResponse(ctx.historyId, {
4188
- success: false,
4189
- model: acc.model || fallbackModel,
4190
- usage: {
4191
- input_tokens: 0,
4192
- output_tokens: 0
4193
- },
4194
- error: error instanceof Error ? error.message : "Stream error",
4195
- content: null
4196
- }, Date.now() - ctx.startTime);
4197
- }
4198
- function completeTracking(trackingId, inputTokens, outputTokens) {
4199
- if (!trackingId) return;
4200
- requestTracker.updateRequest(trackingId, {
4201
- inputTokens,
4202
- outputTokens
4203
- });
4204
- requestTracker.completeRequest(trackingId, 200, {
4205
- inputTokens,
4206
- outputTokens
4207
- });
4208
- }
4209
- function failTracking(trackingId, error) {
4210
- if (!trackingId) return;
4211
- requestTracker.failRequest(trackingId, error instanceof Error ? error.message : "Stream error");
4212
- }
4213
- function convertAnthropicMessages(messages) {
4214
- return messages.map((msg) => {
4215
- if (typeof msg.content === "string") return {
4216
- role: msg.role,
4217
- content: msg.content
4218
- };
4219
- const content = msg.content.map((block) => {
4220
- if (block.type === "text") return {
4221
- type: "text",
4222
- text: block.text
4223
- };
4224
- if (block.type === "tool_use") return {
4225
- type: "tool_use",
4226
- id: block.id,
4227
- name: block.name,
4228
- input: JSON.stringify(block.input)
4229
- };
4230
- if (block.type === "tool_result") {
4231
- const resultContent = typeof block.content === "string" ? block.content : block.content.map((c) => c.type === "text" ? c.text : `[${c.type}]`).join("\n");
4232
- return {
4233
- type: "tool_result",
4234
- tool_use_id: block.tool_use_id,
4235
- content: resultContent
4236
- };
4237
- }
4238
- return { type: block.type };
4239
- });
4240
- return {
4241
- role: msg.role,
4242
- content
4243
- };
4244
- });
4245
- }
4246
- function extractSystemPrompt(system) {
4247
- if (!system) return void 0;
4248
- if (typeof system === "string") return system;
4249
- return system.map((block) => block.text).join("\n");
4250
- }
4251
- function extractToolCallsFromContent(content) {
4252
- const tools = [];
4253
- 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({
4254
- id: String(block.id),
4255
- name: String(block.name),
4256
- input: JSON.stringify(block.input)
4257
- });
4258
- return tools.length > 0 ? tools : void 0;
4259
- }
4260
- const isNonStreaming = (response) => Object.hasOwn(response, "choices");
4261
-
4262
- //#endregion
4263
- //#region src/routes/messages/route.ts
4264
- const messageRoutes = new Hono();
4265
- messageRoutes.post("/", async (c) => {
4266
- try {
4267
- return await handleCompletion(c);
4268
- } catch (error) {
4269
- return await forwardError(c, error);
4270
- }
4271
- });
4272
- messageRoutes.post("/count_tokens", async (c) => {
4273
- try {
4274
- return await handleCountTokens(c);
4275
- } catch (error) {
4276
- return await forwardError(c, error);
4277
- }
4278
- });
4279
-
4280
- //#endregion
4281
- //#region src/routes/models/route.ts
4282
- const modelRoutes = new Hono();
4283
- modelRoutes.get("/", async (c) => {
4284
- try {
4285
- if (!state.models) await cacheModels();
4286
- const models = state.models?.data.map((model) => ({
4287
- id: model.id,
4288
- object: "model",
4289
- type: "model",
4290
- created: 0,
4291
- created_at: (/* @__PURE__ */ new Date(0)).toISOString(),
4292
- owned_by: model.vendor,
4293
- display_name: model.name,
4294
- capabilities: {
4295
- family: model.capabilities.family,
4296
- type: model.capabilities.type,
4297
- tokenizer: model.capabilities.tokenizer,
4298
- limits: {
4299
- max_context_window_tokens: model.capabilities.limits.max_context_window_tokens,
4300
- max_output_tokens: model.capabilities.limits.max_output_tokens,
4301
- max_prompt_tokens: model.capabilities.limits.max_prompt_tokens
4302
- },
4303
- supports: {
4304
- tool_calls: model.capabilities.supports.tool_calls,
4305
- parallel_tool_calls: model.capabilities.supports.parallel_tool_calls
4306
- }
4307
- }
4308
- }));
4309
- return c.json({
4310
- object: "list",
4311
- data: models,
4312
- has_more: false
4313
- });
4314
- } catch (error) {
4315
- return await forwardError(c, error);
4316
- }
4317
- });
4318
-
4319
- //#endregion
4320
- //#region src/routes/token/route.ts
4321
- const tokenRoute = new Hono();
4322
- tokenRoute.get("/", async (c) => {
4323
- try {
4324
- return c.json({ token: state.copilotToken });
4325
- } catch (error) {
4326
- return await forwardError(c, error);
4327
- }
4328
- });
4329
-
4330
- //#endregion
4331
- //#region src/routes/usage/route.ts
4332
- const usageRoute = new Hono();
4333
- usageRoute.get("/", async (c) => {
4334
- try {
4335
- const usage = await getCopilotUsage();
4336
- return c.json(usage);
4337
- } catch (error) {
4338
- return await forwardError(c, error);
4339
- }
4340
- });
4341
-
4342
- //#endregion
4343
- //#region src/server.ts
4344
- const server = new Hono();
4345
- server.use(tuiLogger());
4346
- server.use(cors());
4347
- server.get("/", (c) => c.text("Server running"));
4348
- server.get("/health", (c) => {
4349
- const healthy = Boolean(state.copilotToken && state.githubToken);
4350
- return c.json({
4351
- status: healthy ? "healthy" : "unhealthy",
4352
- checks: {
4353
- copilotToken: Boolean(state.copilotToken),
4354
- githubToken: Boolean(state.githubToken),
4355
- models: Boolean(state.models)
4356
- }
4357
- }, healthy ? 200 : 503);
4358
- });
4359
- server.route("/chat/completions", completionRoutes);
4360
- server.route("/models", modelRoutes);
4361
- server.route("/embeddings", embeddingRoutes);
4362
- server.route("/usage", usageRoute);
4363
- server.route("/token", tokenRoute);
4364
- server.route("/v1/chat/completions", completionRoutes);
4365
- server.route("/v1/models", modelRoutes);
4366
- server.route("/v1/embeddings", embeddingRoutes);
4367
- server.route("/v1/messages", messageRoutes);
4368
- server.route("/api/event_logging", eventLoggingRoutes);
4369
- server.route("/history", historyRoutes);
4370
-
4371
- //#endregion
4372
- //#region src/start.ts
4373
- async function runServer(options) {
4374
- if (options.proxyEnv) initProxyFromEnv();
4375
- if (options.verbose) {
4376
- consola.level = 5;
4377
- consola.info("Verbose logging enabled");
4378
- }
4379
- state.accountType = options.accountType;
4380
- if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
4381
- state.manualApprove = options.manual;
4382
- state.rateLimitSeconds = options.rateLimit;
4383
- state.rateLimitWait = options.rateLimitWait;
4384
- state.showToken = options.showToken;
4385
- state.autoCompact = options.autoCompact;
4386
- if (options.autoCompact) consola.info("Auto-compact enabled: will compress context when exceeding token limits");
4387
- initHistory(options.history, options.historyLimit);
4388
- if (options.history) {
4389
- const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
4390
- consola.info(`History recording enabled (${limitText} entries)`);
4391
- }
4392
- initTui({
4393
- enabled: true,
4394
- mode: options.tui
4395
- });
4396
- await ensurePaths();
4397
- await cacheVSCodeVersion();
4398
- if (options.githubToken) {
4399
- state.githubToken = options.githubToken;
4400
- consola.info("Using provided GitHub token");
4401
- } else await setupGitHubToken();
4402
- await setupCopilotToken();
4403
- await cacheModels();
4404
- consola.info(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
4405
- const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
4406
- if (options.claudeCode) {
4407
- invariant(state.models, "Models should be loaded by now");
4408
- const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
4409
- type: "select",
4410
- options: state.models.data.map((model) => model.id)
4411
- });
4412
- const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
4413
- type: "select",
4414
- options: state.models.data.map((model) => model.id)
4415
- });
4416
- const command = generateEnvScript({
4417
- ANTHROPIC_BASE_URL: serverUrl,
4418
- ANTHROPIC_AUTH_TOKEN: "dummy",
4419
- ANTHROPIC_MODEL: selectedModel,
4420
- ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
4421
- ANTHROPIC_SMALL_FAST_MODEL: selectedSmallModel,
4422
- ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedSmallModel,
4423
- DISABLE_NON_ESSENTIAL_MODEL_CALLS: "1",
4424
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
4425
- }, "claude");
4426
- try {
4427
- clipboard.writeSync(command);
4428
- consola.success("Copied Claude Code command to clipboard!");
4429
- } catch {
4430
- consola.warn("Failed to copy to clipboard. Here is the Claude Code command:");
4431
- consola.log(command);
4432
- }
4433
- }
4434
- consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage${options.history ? `\nšŸ“œ History UI: ${serverUrl}/history` : ""}`);
4435
- serve({
4436
- fetch: server.fetch,
4437
- port: options.port,
4438
- hostname: options.host
4439
- });
4440
- }
4441
- const start = defineCommand({
4442
- meta: {
4443
- name: "start",
4444
- description: "Start the Copilot API server"
4445
- },
4446
- args: {
4447
- port: {
4448
- alias: "p",
4449
- type: "string",
4450
- default: "4141",
4451
- description: "Port to listen on"
4452
- },
4453
- host: {
4454
- alias: "H",
4455
- type: "string",
4456
- description: "Host/interface to bind to (e.g., 127.0.0.1 for localhost only, 0.0.0.0 for all interfaces)"
4457
- },
4458
- verbose: {
4459
- alias: "v",
4460
- type: "boolean",
4461
- default: false,
4462
- description: "Enable verbose logging"
4463
- },
4464
- "account-type": {
4465
- alias: "a",
4466
- type: "string",
4467
- default: "individual",
4468
- description: "Account type to use (individual, business, enterprise)"
4469
- },
4470
- manual: {
4471
- type: "boolean",
4472
- default: false,
4473
- description: "Enable manual request approval"
4474
- },
4475
- "rate-limit": {
4476
- alias: "r",
4477
- type: "string",
4478
- description: "Rate limit in seconds between requests"
4479
- },
4480
- wait: {
4481
- alias: "w",
4482
- type: "boolean",
4483
- default: false,
4484
- description: "Wait instead of error when rate limit is hit. Has no effect if rate limit is not set"
4485
- },
4486
- "github-token": {
4487
- alias: "g",
4488
- type: "string",
4489
- description: "Provide GitHub token directly (must be generated using the `auth` subcommand)"
4490
- },
4491
- "claude-code": {
4492
- alias: "c",
4493
- type: "boolean",
4494
- default: false,
4495
- description: "Generate a command to launch Claude Code with Copilot API config"
4496
- },
4497
- "show-token": {
4498
- type: "boolean",
4499
- default: false,
4500
- description: "Show GitHub and Copilot tokens on fetch and refresh"
4501
- },
4502
- "proxy-env": {
4503
- type: "boolean",
4504
- default: false,
4505
- description: "Initialize proxy from environment variables"
4506
- },
4507
- history: {
4508
- type: "boolean",
4509
- default: false,
4510
- description: "Enable request history recording and Web UI at /history"
4511
- },
4512
- "history-limit": {
4513
- type: "string",
4514
- default: "1000",
4515
- description: "Maximum number of history entries to keep in memory (0 = unlimited)"
4516
- },
4517
- tui: {
4518
- type: "string",
4519
- default: "console",
4520
- description: "TUI mode: 'console' for simple log output, 'fullscreen' for interactive terminal UI with tabs"
4521
- },
4522
- "auto-compact": {
4523
- type: "boolean",
4524
- default: false,
4525
- description: "Automatically compress conversation history when exceeding model token limits"
4526
- }
4527
- },
4528
- run({ args }) {
4529
- const rateLimitRaw = args["rate-limit"];
4530
- const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
4531
- return runServer({
4532
- port: Number.parseInt(args.port, 10),
4533
- host: args.host,
4534
- verbose: args.verbose,
4535
- accountType: args["account-type"],
4536
- manual: args.manual,
4537
- rateLimit,
4538
- rateLimitWait: args.wait,
4539
- githubToken: args["github-token"],
4540
- claudeCode: args["claude-code"],
4541
- showToken: args["show-token"],
4542
- proxyEnv: args["proxy-env"],
4543
- history: args.history,
4544
- historyLimit: Number.parseInt(args["history-limit"], 10),
4545
- tui: args.tui,
4546
- autoCompact: args["auto-compact"]
4547
- });
4548
- }
4549
- });
4550
-
4551
- //#endregion
4552
- //#region src/main.ts
4553
- consola.options.formatOptions.date = true;
4554
- const main = defineCommand({
4555
- meta: {
4556
- name: "copilot-api",
4557
- description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
4558
- },
4559
- subCommands: {
4560
- auth,
4561
- logout,
4562
- start,
4563
- "check-usage": checkUsage,
4564
- debug
4565
- }
4566
- });
4567
- await runMain(main);
4568
-
4569
- //#endregion
4570
- export { };
4571
- //# sourceMappingURL=main.js.map