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