@jeffreycao/copilot-api 2.7.36 → 2.7.38

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.
@@ -1,9 +1,9 @@
1
1
  import { D as state, E as isGitHubCopilotEnabled, O as HTTPError, S as PATHS, a as getAnthropicApiKey, c as getConfig, d as getUpstreamTransportConfig, g as isResponsesApiWebSocketEnabled, h as isResponsesApiWebSearchEnabled, i as getAlphaSearchModel, j as forwardError, l as getMessageApiWebSearchModel, m as isMessagesApiEnabled, o as getClaudeAutoModel, p as isAlphaSearchCodexPriorityEnabled, s as getClaudeTokenMultiplier, u as getOpencodeModelContextWindow, w as GITHUB_COPILOT_PROVIDER, y as reloadConfig } from "./config-store-CYG9aCTL.js";
2
- import { A as resolveMappedModel, C as getModelResponsesApiCompactThreshold$1, D as isContextManagementEnabledForMessages, E as getSmallModelForProvider, O as isContextManagementEnabledForResponses, S as getModelMappings, T as getSmallModel, _ as getModelsDevModelMaxOutputTokens, a as normalizeProviderBaseUrl, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getModelsDevModel, h as getBuiltinProviderModelRecords, i as listEnabledProviders, j as setModelMappings, k as isGpt56OrAbove, l as getLocalProviderModelRecords, m as builtinProviderModelRegistry, n as getRawProviderConfig, o as resolveEffectiveProviderType, p as usesModelsDevModelCatalog, s as resolveProviderConfigForModel, t as getProviderConfig, u as getProviderModelConfig, v as getModelsDevModelPricing, w as getReasoningEffortForModel, x as getExtraPromptForModel } from "./config-D8pUyhJp.js";
3
- import { $ as forwardCodexResponses, B as getXaiAccessToken, C as copilotHeaders, D as prepareForCompact, E as isOpencodeOauthApp, F as compactTextOnlyGuard, I as createAuthMiddleware, J as XAI_API_BASE_URL, K as readGitHubToken, L as getConfiguredAdminApiKeys, M as compactMessageSections, O as prepareInteractionHeaders, P as compactSystemPromptStarts, Q as buildCodexRequestHeaders, S as copilotBaseUrl, T as copilotWebSocketHeaders, Z as CODEX_API_BASE_URL, _ as isAsyncIterable, at as createResponsesSafeStream, b as parseUserIdMetadata, ct as createPooledWebSocketStream, d as generateRequestIdFromPayload, dt as stripInternalRequestHeaders, et as generateTraceId, f as getPromptCacheKey, g as getUUID, h as getSessionAffinityFromHeaders, it as createResponsesHttpEventStream, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as createWebSocketUrl, m as getRootSessionIdFromHeaders, n as invalidateCodexRuntime, nt as resolveTraceId$1, ot as encodePoolKeyPart, p as getRootSessionId, q as readGitHubTokenFromEnv, rt as fetchUpstreamWithLifecycle, s as setupCodexToken, st as getResponsesStreamTerminalDisposition, tt as requestContext, u as stopCopilotRefreshLoop, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-BL6FigKp.js";
2
+ import { A as resolveMappedModel, C as getModelResponsesApiCompactThreshold$1, D as isContextManagementEnabledForMessages, E as getSmallModelForProvider, O as isContextManagementEnabledForResponses, S as getModelMappings, T as getSmallModel, _ as getModelsDevModelMaxOutputTokens, a as normalizeProviderBaseUrl, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getModelsDevModel, h as getBuiltinProviderModelRecords, i as listEnabledProviders, j as setModelMappings, k as isGpt56OrAbove, l as getLocalProviderModelRecords, m as builtinProviderModelRegistry, n as getRawProviderConfig, o as resolveEffectiveProviderType, p as usesModelsDevModelCatalog, s as resolveProviderConfigForModel, t as getProviderConfig, u as getProviderModelConfig, v as getModelsDevModelPricing, w as getReasoningEffortForModel, x as getExtraPromptForModel } from "./config-BzhriV_y.js";
3
+ import { $ as forwardCodexResponses, B as getXaiAccessToken, C as copilotHeaders, D as prepareForCompact, E as isOpencodeOauthApp, F as compactTextOnlyGuard, I as createAuthMiddleware, J as XAI_API_BASE_URL, K as readGitHubToken, L as getConfiguredAdminApiKeys, M as compactMessageSections, O as prepareInteractionHeaders, P as compactSystemPromptStarts, Q as buildCodexRequestHeaders, S as copilotBaseUrl, T as copilotWebSocketHeaders, Z as CODEX_API_BASE_URL, _ as isAsyncIterable, at as createResponsesHttpEventStream, b as parseUserIdMetadata, ct as getResponsesStreamTerminalDisposition, d as generateRequestIdFromPayload, et as generateTraceId, f as getPromptCacheKey, ft as stripInternalRequestHeaders, g as getUUID, h as getSessionAffinityFromHeaders, it as fetchUpstreamWithLifecycle, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as createPooledWebSocketStream, m as getRootSessionIdFromHeaders, n as invalidateCodexRuntime, nt as resolveTraceId$1, ot as createResponsesSafeStream, p as getRootSessionId, q as readGitHubTokenFromEnv, rt as setPluginRequestActualModel, s as setupCodexToken, st as encodePoolKeyPart, tt as requestContext, u as stopCopilotRefreshLoop, ut as createWebSocketUrl, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-XNKmjmqH.js";
4
4
  import { a as isDeferredToolName, c as parseMcpToolSearchSentinel, d as shouldEnableResponsesToolSearch, i as isBridgeToolSearchName, l as resolveBridgeToolSearchName, o as listDeferredToolNames, r as formatToolSearchBridgeArguments, s as normalizeToolSearchBridgeArguments, t as BRIDGE_TOOL_SEARCH_NAME, u as selectDeferredToolsByNames } from "./tool-search-CcvaqM03.js";
5
5
  import { n as isProviderAgentModelVisible, t as getProviderAgentModels } from "./provider-management-B9lCOTka.js";
6
- import { n as stopModelsRefreshLoop, t as setupCopilotRuntime } from "./copilot-runtime-RW0tbgv3.js";
6
+ import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-8IpywGxw.js";
7
7
  import consola from "consola";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
9
  import fs, { readFileSync } from "node:fs";
@@ -35,605 +35,184 @@ const traceIdMiddleware = async (c, next) => {
35
35
  });
36
36
  };
37
37
  //#endregion
38
- //#region src/lib/process-cleanup.ts
39
- const cleanupHandlers = /* @__PURE__ */ new Set();
40
- let cleanupPromise = null;
41
- let cleanupState = "idle";
42
- let runtimeInitialized$1 = false;
43
- function initializeProcessCleanupRuntime() {
44
- if (runtimeInitialized$1) return;
45
- runtimeInitialized$1 = true;
46
- process.once("beforeExit", () => {
47
- runProcessCleanups();
48
- });
49
- process.once("exit", runProcessCleanupsSync);
50
- process.once("SIGINT", () => {
51
- shutdownProcess(0);
52
- });
53
- process.once("SIGTERM", () => {
54
- shutdownProcess(0);
55
- });
56
- }
57
- function runProcessCleanupsSync() {
58
- if (cleanupState !== "idle") return;
59
- cleanupState = "done";
60
- for (const handler of Array.from(cleanupHandlers)) try {
61
- handler();
62
- } catch {}
63
- }
64
- async function runProcessCleanups() {
65
- if (cleanupPromise) return cleanupPromise;
66
- if (cleanupState === "done") return;
67
- cleanupState = "running";
68
- cleanupPromise = (async () => {
69
- for (const handler of Array.from(cleanupHandlers)) await handler();
70
- cleanupState = "done";
71
- })();
72
- return cleanupPromise;
38
+ //#region src/lib/sqlite.ts
39
+ const MINIMUM_NODE_SQLITE_VERSION = "22.13.0";
40
+ const isBunRuntime = () => Boolean(globalThis.Bun);
41
+ function parseNodeVersion(version) {
42
+ return version.split(".", 3).map((part) => {
43
+ const parsed = Number.parseInt(part, 10);
44
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
45
+ });
73
46
  }
74
- async function shutdownProcess(exitCode) {
75
- try {
76
- await runProcessCleanups();
77
- } finally {
78
- process.exit(exitCode);
47
+ function isNodeSqliteSupportedVersion(version) {
48
+ const current = parseNodeVersion(version);
49
+ const minimum = parseNodeVersion(MINIMUM_NODE_SQLITE_VERSION);
50
+ for (const [index, minimumPart] of minimum.entries()) {
51
+ const currentPart = current[index] ?? 0;
52
+ if (currentPart > minimumPart) return true;
53
+ if (currentPart < minimumPart) return false;
79
54
  }
55
+ return true;
80
56
  }
81
- function registerProcessCleanup(handler) {
82
- initializeProcessCleanupRuntime();
83
- cleanupHandlers.add(handler);
84
- return () => {
85
- cleanupHandlers.delete(handler);
86
- };
57
+ function isSqliteRuntimeSupported(input = {}) {
58
+ if (input.isBun ?? isBunRuntime()) return true;
59
+ return isNodeSqliteSupportedVersion(input.nodeVersion ?? process.versions.node);
87
60
  }
88
- //#endregion
89
- //#region src/lib/logger.ts
90
- const LOG_RETENTION_MS = 6048e5;
91
- const CLEANUP_INTERVAL_MS = 864e5;
92
- const LOG_DIR_ENV = "COPILOT_API_LOG_DIR";
93
- const DEFAULT_LOG_DIR = path.join(PATHS.APP_DIR, "logs");
94
- const FLUSH_INTERVAL_MS = 1e3;
95
- const MAX_BUFFER_SIZE = 100;
96
- const getLogDir = () => process.env[LOG_DIR_ENV]?.trim() || DEFAULT_LOG_DIR;
97
- const logStreams = /* @__PURE__ */ new Map();
98
- const logBuffers = /* @__PURE__ */ new Map();
99
- let runtimeInitialized = false;
100
- let flushInterval;
101
- let cleanupInterval;
102
- let currentLogDateKey;
103
- const ensureLogDirectory = () => {
104
- const logDir = getLogDir();
105
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
106
- };
107
- const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : util.inspect(arg, {
108
- depth: null,
109
- colors: false
110
- })).join(" ");
111
- const sanitizeName = (name) => {
112
- const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
113
- return normalized === "" ? "handler" : normalized;
114
- };
115
- const maybeUnref = (timer) => {
116
- timer.unref();
117
- };
118
- const flushBuffer = (filePath) => {
119
- const buffer = logBuffers.get(filePath);
120
- if (!buffer) return;
121
- logBuffers.delete(filePath);
122
- if (buffer.length === 0) return;
123
- const stream = getLogStream(filePath);
124
- const content = buffer.join("\n") + "\n";
125
- stream.write(content, (error) => {
126
- if (error) console.warn("Failed to write handler log", error);
127
- });
128
- };
129
- const flushAllBuffers = () => {
130
- for (const filePath of logBuffers.keys()) flushBuffer(filePath);
131
- };
132
- const closeLogFile = async (filePath) => {
133
- flushBuffer(filePath);
134
- const stream = logStreams.get(filePath);
135
- logStreams.delete(filePath);
136
- logBuffers.delete(filePath);
137
- if (!stream || stream.closed) return;
138
- await new Promise((resolve) => {
139
- stream.once("close", resolve);
140
- stream.end();
141
- });
142
- };
143
- const closePreviousDateLogFiles = (dateKey) => {
144
- if (currentLogDateKey === void 0) {
145
- currentLogDateKey = dateKey;
146
- return;
61
+ function getUnsupportedNodeSqliteMessage(nodeVersion) {
62
+ return `SQLite-backed token usage requires Bun or Node.js >= ${MINIMUM_NODE_SQLITE_VERSION}. Detected Node.js ${nodeVersion}. Upgrade Node.js or run the CLI with Bun, for example \`bunx --bun @jeffreycao/copilot-api@latest start\` or \`bun run start start\`.`;
63
+ }
64
+ var UnsupportedNodeSqliteRuntimeError = class extends Error {
65
+ constructor(nodeVersion, cause) {
66
+ super(getUnsupportedNodeSqliteMessage(nodeVersion), { cause });
67
+ this.name = "UnsupportedNodeSqliteRuntimeError";
147
68
  }
148
- if (currentLogDateKey === dateKey) return;
149
- const filePaths = /* @__PURE__ */ new Set([...logBuffers.keys(), ...logStreams.keys()]);
150
- currentLogDateKey = dateKey;
151
- for (const filePath of filePaths) closeLogFile(filePath).catch((error) => {
152
- console.warn("Failed to rotate handler log", error);
153
- });
154
69
  };
155
- const cleanupOldLogs = async () => {
156
- const logDir = getLogDir();
157
- if (!fs.existsSync(logDir)) return;
158
- const now = Date.now();
159
- for (const entry of fs.readdirSync(logDir)) {
160
- const filePath = path.join(logDir, entry);
161
- let stats;
162
- try {
163
- stats = fs.statSync(filePath);
164
- } catch {
165
- continue;
166
- }
167
- if (!stats.isFile() || now - stats.mtimeMs <= LOG_RETENTION_MS) continue;
168
- try {
169
- await closeLogFile(filePath);
170
- } catch (error) {
171
- console.warn(`Failed to close old handler log: ${filePath}`, error);
172
- continue;
173
- }
174
- try {
175
- fs.rmSync(filePath);
176
- } catch (error) {
177
- console.warn(`Failed to remove old handler log: ${filePath}`, error);
178
- }
70
+ async function openBunDatabase(dbPath) {
71
+ return new (await (import(["bun", "sqlite"].join(":")))).Database(dbPath);
72
+ }
73
+ async function loadNodeSqliteModule() {
74
+ const nodeVersion = process.versions.node;
75
+ if (!isNodeSqliteSupportedVersion(nodeVersion)) throw new UnsupportedNodeSqliteRuntimeError(nodeVersion);
76
+ const emitWarning = process.emitWarning.bind(process);
77
+ process.emitWarning = (warning, ...args) => {
78
+ if (typeof warning === "string" && warning.includes("SQLite is an experimental feature")) return;
79
+ return emitWarning(warning, ...args);
80
+ };
81
+ const specifier = ["node", "sqlite"].join(":");
82
+ try {
83
+ return await import(specifier);
84
+ } catch (error) {
85
+ throw new UnsupportedNodeSqliteRuntimeError(nodeVersion, error);
86
+ } finally {
87
+ process.emitWarning = emitWarning;
179
88
  }
180
- };
181
- const scheduleLogCleanup = () => {
182
- cleanupOldLogs().catch((error) => {
183
- console.warn("Failed to clean up handler logs", error);
184
- });
185
- };
186
- const flushAndCloseLogStreams = () => {
187
- flushAllBuffers();
188
- for (const stream of logStreams.values()) stream.end();
189
- logStreams.clear();
190
- logBuffers.clear();
191
- currentLogDateKey = void 0;
192
- };
193
- const shutdownLoggerRuntime = () => {
194
- if (flushInterval) {
195
- clearInterval(flushInterval);
196
- flushInterval = void 0;
89
+ }
90
+ async function openNodeDatabase(dbPath) {
91
+ return new (await (loadNodeSqliteModule())).DatabaseSync(dbPath);
92
+ }
93
+ async function openSqliteDatabase(dbPath) {
94
+ const dir = path.dirname(dbPath);
95
+ if (dbPath !== ":memory:" && dir !== ".") await fs$1.mkdir(dir, { recursive: true });
96
+ return isBunRuntime() ? openBunDatabase(dbPath) : openNodeDatabase(dbPath);
97
+ }
98
+ var SqliteDbStore = class {
99
+ dbPromise = null;
100
+ options;
101
+ constructor(options) {
102
+ this.options = options;
197
103
  }
198
- if (cleanupInterval) {
199
- clearInterval(cleanupInterval);
200
- cleanupInterval = void 0;
104
+ getDb() {
105
+ this.dbPromise ??= this.open();
106
+ return this.dbPromise;
201
107
  }
202
- flushAndCloseLogStreams();
203
- runtimeInitialized = false;
204
- };
205
- const initializeLoggerRuntime = () => {
206
- if (runtimeInitialized) return;
207
- runtimeInitialized = true;
208
- ensureLogDirectory();
209
- scheduleLogCleanup();
210
- flushInterval = setInterval(flushAllBuffers, FLUSH_INTERVAL_MS);
211
- maybeUnref(flushInterval);
212
- cleanupInterval = setInterval(scheduleLogCleanup, CLEANUP_INTERVAL_MS);
213
- maybeUnref(cleanupInterval);
214
- registerProcessCleanup(shutdownLoggerRuntime);
215
- };
216
- const getLogStream = (filePath) => {
217
- initializeLoggerRuntime();
218
- let stream = logStreams.get(filePath);
219
- if (!stream || stream.destroyed) {
220
- const createdStream = fs.createWriteStream(filePath, { flags: "a" });
221
- stream = createdStream;
222
- logStreams.set(filePath, createdStream);
223
- createdStream.on("error", (error) => {
224
- console.warn("Log stream error", error);
225
- if (logStreams.get(filePath) === createdStream) logStreams.delete(filePath);
226
- });
108
+ async close(input) {
109
+ const currentDbPromise = this.dbPromise;
110
+ this.dbPromise = null;
111
+ if (!currentDbPromise) return;
112
+ const db = await currentDbPromise;
113
+ input?.beforeClose?.(db);
114
+ db.close?.();
227
115
  }
228
- return stream;
229
- };
230
- const appendLine = (filePath, line) => {
231
- let buffer = logBuffers.get(filePath);
232
- if (!buffer) {
233
- buffer = [];
234
- logBuffers.set(filePath, buffer);
116
+ async open() {
117
+ const db = await openSqliteDatabase(this.options.getPath());
118
+ this.options.initialize?.(db);
119
+ return db;
235
120
  }
236
- buffer.push(line);
237
- if (buffer.length >= MAX_BUFFER_SIZE) flushBuffer(filePath);
238
- };
239
- const debugLazy = (logger, factory) => {
240
- if (!state.verbose) return;
241
- logger.debug(...factory());
242
- };
243
- const debugJson = (logger, label, value) => {
244
- debugLazy(logger, () => [label, JSON.stringify(value)]);
245
- };
246
- const debugJsonAsync = async (logger, label, factory) => {
247
- if (!state.verbose) return;
248
- logger.debug(label, JSON.stringify(await factory()));
249
- };
250
- const debugJsonTail = (logger, label, { value, tailLength = 400 }) => {
251
- debugLazy(logger, () => [label, JSON.stringify(value).slice(-tailLength)]);
252
- };
253
- const createHandlerLogger = (name) => {
254
- const sanitizedName = sanitizeName(name);
255
- const instance = consola.withTag(name);
256
- if (state.verbose) instance.level = 5;
257
- instance.setReporters([]);
258
- instance.addReporter({ log(logObj) {
259
- initializeLoggerRuntime();
260
- const traceId = requestContext.getStore()?.traceId;
261
- const date = logObj.date;
262
- const dateKey = date.toLocaleDateString("sv-SE");
263
- const timestamp = date.toLocaleString("sv-SE", { hour12: false });
264
- const filePath = path.join(getLogDir(), `${sanitizedName}-${dateKey}.log`);
265
- const message = formatArgs(logObj.args);
266
- const traceIdStr = traceId ? ` [${traceId}]` : "";
267
- const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name}]${traceIdStr}${message ? ` ${message}` : ""}`;
268
- closePreviousDateLogFiles(dateKey);
269
- appendLine(filePath, line);
270
- } });
271
- return instance;
272
121
  };
273
122
  //#endregion
274
- //#region src/lib/models.ts
275
- /**
276
- * Converts a Copilot upstream model ID to a client-friendly ID that Claude Code
277
- * and Claude Desktop recognize (dots in version replaced with hyphens).
278
- * e.g. "claude-sonnet-4.6" -> "claude-sonnet-4-6"
279
- * Non-Claude models are returned unchanged.
280
- */
281
- const toClientModelId = (modelId) => {
282
- const normalized = normalizeSdkModelId(modelId);
283
- if (!normalized) return modelId;
284
- const versionHyphenated = normalized.version.replaceAll(".", "-");
285
- return `claude-${normalized.family}-${versionHyphenated}`;
286
- };
287
- const findEndpointModel = (sdkModelId) => {
288
- const models = state.models?.data ?? [];
289
- const exactMatch = models.find((m) => m.id === sdkModelId);
290
- if (exactMatch) return exactMatch;
291
- const normalized = normalizeSdkModelId(sdkModelId);
292
- if (!normalized) return;
293
- const modelName = `claude-${normalized.family}-${normalized.version}`;
294
- const model = models.find((m) => m.id === modelName);
295
- if (model) return model;
296
- };
297
- /**
298
- * Normalizes an SDK model ID to extract the model family and version.
299
- * this method from github copilot extension
300
- * Examples:
301
- * - "claude-opus-4-5-20251101" -> { family: "opus", version: "4.5" }
302
- * - "claude-3-5-sonnet-20241022" -> { family: "sonnet", version: "3.5" }
303
- * - "claude-sonnet-4-20250514" -> { family: "sonnet", version: "4" }
304
- * - "claude-haiku-3-5-20250514" -> { family: "haiku", version: "3.5" }
305
- * - "claude-haiku-4.5" -> { family: "haiku", version: "4.5" }
306
- */
307
- const normalizeSdkModelId = (sdkModelId) => {
308
- const withoutDate = sdkModelId.toLowerCase().replace(/-\d{8}$/, "");
309
- const pattern1 = withoutDate.match(/^claude-(\w+)-(\d+)\.(\d+)$/);
310
- if (pattern1) return {
311
- family: pattern1[1],
312
- version: `${pattern1[2]}.${pattern1[3]}`
313
- };
314
- const pattern2 = withoutDate.match(/^claude-(\w+)-(\d+)-(\d+)$/);
315
- if (pattern2) return {
316
- family: pattern2[1],
317
- version: `${pattern2[2]}.${pattern2[3]}`
318
- };
319
- const pattern3 = withoutDate.match(/^claude-(\d+)-(\d+)-(\w+)$/);
320
- if (pattern3) return {
321
- family: pattern3[3],
322
- version: `${pattern3[1]}.${pattern3[2]}`
323
- };
324
- const pattern4 = withoutDate.match(/^claude-(\w+)-(\d+)$/);
325
- if (pattern4) return {
326
- family: pattern4[1],
327
- version: pattern4[2]
328
- };
329
- const pattern5 = withoutDate.match(/^claude-(\d+)-(\w+)$/);
330
- if (pattern5) return {
331
- family: pattern5[2],
332
- version: pattern5[1]
333
- };
334
- };
335
- //#endregion
336
- //#region src/lib/provider-model.ts
337
- const parseProviderModelAlias = (model) => {
338
- const separatorIndex = model.indexOf("/");
339
- if (separatorIndex <= 0 || separatorIndex === model.length - 1) return null;
340
- const provider = model.slice(0, separatorIndex).trim();
341
- const providerModel = model.slice(separatorIndex + 1).trim();
342
- if (!provider || !providerModel) return null;
343
- return {
344
- model: providerModel,
345
- provider
346
- };
347
- };
348
- const createFallbackModel = (modelId) => ({
349
- capabilities: {
350
- family: "provider",
351
- limits: {},
352
- object: "model_capabilities",
353
- supports: {},
354
- tokenizer: "o200k_base",
355
- type: "chat"
356
- },
357
- id: modelId,
358
- model_picker_enabled: false,
359
- name: modelId,
360
- object: "model",
361
- preview: false,
362
- vendor: "provider",
363
- version: "unknown"
123
+ //#region src/lib/token-usage/store.ts
124
+ const DB_PATH_ENV = "COPILOT_API_SQLITE_DB_PATH";
125
+ const DEFAULT_DB_FILENAME = "copilot-api.sqlite";
126
+ const COST_NANOS_PER_UNIT$1 = 1e9;
127
+ let writeQueue = Promise.resolve();
128
+ function getDbPath() {
129
+ return process.env[DB_PATH_ENV] ?? path.join(PATHS.APP_DIR, DEFAULT_DB_FILENAME);
130
+ }
131
+ const tokenUsageDbStore = new SqliteDbStore({
132
+ getPath: getDbPath,
133
+ initialize: initializeTokenUsageDb
364
134
  });
365
- //#endregion
366
- //#region src/lib/provider-resolver.ts
367
- const AZURE_OPENAI_SCOPE = "https://cognitiveservices.azure.com/.default";
368
- let cachedAzureAccessTokenProvider = null;
369
- /**
370
- * Lazily creates the Entra token provider on first use and reuses it
371
- * afterwards. Importing @azure/identity eagerly adds ~0.5s to every process
372
- * startup, and constructing a new DefaultAzureCredential per call would drop
373
- * the token cache, so both the import and the credential are singletons.
374
- */
375
- async function getAzureAccessTokenProvider() {
376
- cachedAzureAccessTokenProvider ??= (async () => {
377
- try {
378
- const { DefaultAzureCredential, getBearerTokenProvider } = await import("@azure/identity");
379
- return getBearerTokenProvider(new DefaultAzureCredential(), AZURE_OPENAI_SCOPE);
380
- } catch (error) {
381
- cachedAzureAccessTokenProvider = null;
382
- throw error;
383
- }
384
- })();
385
- return cachedAzureAccessTokenProvider;
135
+ function getDb() {
136
+ return tokenUsageDbStore.getDb();
386
137
  }
387
- async function getDefaultAzureAccessToken() {
388
- return (await getAzureAccessTokenProvider())();
138
+ function isTokenUsageStorageEnabled() {
139
+ return isSqliteRuntimeSupported();
389
140
  }
390
- function isMissingCodexCredentialsError(error) {
391
- return error instanceof Error && error.message === "Codex credentials not found. Run `copilot-api auth login --provider codex` first.";
141
+ function initializeTokenUsageDb(db) {
142
+ db.exec("PRAGMA journal_mode = WAL");
143
+ db.exec("PRAGMA busy_timeout = 5000");
144
+ db.exec(`
145
+ CREATE TABLE IF NOT EXISTS token_usage_events (
146
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
147
+ created_at_ms INTEGER NOT NULL,
148
+ created_at_utc TEXT NOT NULL,
149
+ trace_id TEXT NOT NULL,
150
+ session_id TEXT NOT NULL DEFAULT '',
151
+ user_id TEXT NOT NULL DEFAULT '',
152
+ user_agent TEXT NOT NULL DEFAULT '',
153
+ source TEXT NOT NULL,
154
+ endpoint TEXT NOT NULL,
155
+ provider_name TEXT,
156
+ model TEXT NOT NULL,
157
+ input_tokens INTEGER NOT NULL DEFAULT 0,
158
+ output_tokens INTEGER NOT NULL DEFAULT 0,
159
+ cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
160
+ cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
161
+ total_tokens INTEGER NOT NULL DEFAULT 0,
162
+ total_nano_aiu INTEGER,
163
+ cost_currency TEXT,
164
+ total_cost_nanos INTEGER,
165
+ cost_source TEXT
166
+ )
167
+ `);
168
+ ensureColumn(db, "user_id", "TEXT NOT NULL DEFAULT ''");
169
+ ensureColumn(db, "user_agent", "TEXT NOT NULL DEFAULT ''");
170
+ ensureColumn(db, "total_tokens", "INTEGER NOT NULL DEFAULT 0");
171
+ ensureColumn(db, "total_nano_aiu", "INTEGER");
172
+ ensureColumn(db, "cost_currency", "TEXT");
173
+ ensureColumn(db, "total_cost_nanos", "INTEGER");
174
+ ensureColumn(db, "cost_source", "TEXT");
175
+ ensureColumn(db, "request_id", "TEXT");
176
+ ensureColumn(db, "first_token_ms", "REAL");
177
+ ensureColumn(db, "duration_ms", "REAL");
178
+ ensureColumn(db, "request_outcome", "TEXT");
179
+ db.exec(`
180
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_request_id
181
+ ON token_usage_events(request_id)
182
+ `);
183
+ db.exec(`
184
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_created_at_ms
185
+ ON token_usage_events(created_at_ms)
186
+ `);
187
+ db.exec(`
188
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_model
189
+ ON token_usage_events(model)
190
+ `);
191
+ db.exec(`
192
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_trace_id
193
+ ON token_usage_events(trace_id)
194
+ `);
195
+ db.exec(`
196
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_session_id
197
+ ON token_usage_events(session_id)
198
+ `);
199
+ db.exec(`
200
+ CREATE INDEX IF NOT EXISTS idx_token_usage_events_user_id
201
+ ON token_usage_events(user_id)
202
+ `);
392
203
  }
393
- async function resolveProviderConfig(providerName, getAzureAccessToken = getDefaultAzureAccessToken) {
394
- const normalizedProviderName = providerName.trim();
395
- if (!normalizedProviderName) return null;
396
- if (normalizedProviderName === "codex") {
397
- if (getRawProviderConfig(normalizedProviderName)?.enabled === false) return null;
398
- try {
399
- await setupCodexToken();
400
- } catch (error) {
401
- if (isMissingCodexCredentialsError(error)) return null;
402
- throw error;
403
- }
404
- const providerConfig = getProviderConfig(normalizedProviderName);
405
- if (!providerConfig) return null;
406
- return {
407
- ...providerConfig,
408
- apiKey: state.codexAccessToken ?? providerConfig.apiKey
409
- };
410
- }
411
- const providerConfig = getProviderConfig(normalizedProviderName);
412
- if (providerConfig?.name === "xai" && providerConfig.authType === "oauth2") {
413
- const accessToken = await getXaiAccessToken();
414
- return accessToken ? {
415
- ...providerConfig,
416
- baseUrl: providerConfig.baseUrl === "https://api.x.ai" ? XAI_API_BASE_URL : providerConfig.baseUrl,
417
- apiKey: accessToken
418
- } : null;
419
- }
420
- if (providerConfig?.authType !== "azure-entra") return providerConfig;
421
- try {
422
- return {
423
- ...providerConfig,
424
- apiKey: await getAzureAccessToken()
425
- };
426
- } catch (error) {
427
- throw new Error(`Failed to acquire Azure Entra token for provider '${normalizedProviderName}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
428
- }
204
+ function ensureColumn(db, name, definition) {
205
+ if (!db.prepare("PRAGMA table_info(token_usage_events)").all().some((row) => row.name === name)) db.exec(`ALTER TABLE token_usage_events ADD COLUMN ${name} ${definition}`);
429
206
  }
430
- /**
431
- * Returns the parsed "provider/model" alias only when that provider is
432
- * actually configured, otherwise null so the caller falls through to its
433
- * default flow. GitHub Copilot enterprise models can ship with namespaced ids
434
- * such as "org/family/model": without this check the first segment is
435
- * misrouted to a non-existent provider and surfaced as a 400/404.
436
- */
437
- async function ensureConfiguredProviderModelAlias(alias, resolveConfig = resolveProviderConfig) {
438
- if (!alias) return null;
439
- return await resolveConfig(alias.provider) ? alias : null;
207
+ function normalizeToken(value) {
208
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
209
+ return Math.max(0, Math.floor(value));
440
210
  }
441
- /** parseProviderModelAlias + ensureConfiguredProviderModelAlias. */
442
- async function resolveConfiguredProviderModelAlias(model, resolveConfig = resolveProviderConfig) {
443
- return ensureConfiguredProviderModelAlias(parseProviderModelAlias(model), resolveConfig);
211
+ function normalizeOptionalToken(value) {
212
+ return value === null || value === void 0 ? void 0 : normalizeToken(value);
444
213
  }
445
- //#endregion
446
- //#region src/lib/event-bus.ts
447
- var EventBus = class {
448
- handlers = /* @__PURE__ */ new Map();
449
- publish(name, event) {
450
- const handlers = this.handlers.get(name);
451
- if (!handlers) return;
452
- for (const handler of Array.from(handlers)) handler(event);
453
- }
454
- subscribe(name, handler) {
455
- let handlers = this.handlers.get(name);
456
- if (!handlers) {
457
- handlers = /* @__PURE__ */ new Set();
458
- this.handlers.set(name, handlers);
459
- }
460
- const registeredHandler = handler;
461
- handlers.add(registeredHandler);
462
- return () => {
463
- handlers.delete(registeredHandler);
464
- if (handlers.size === 0) this.handlers.delete(name);
465
- };
466
- }
467
- };
468
- //#endregion
469
- //#region src/lib/sqlite.ts
470
- const MINIMUM_NODE_SQLITE_VERSION = "22.13.0";
471
- const isBunRuntime = () => Boolean(globalThis.Bun);
472
- function parseNodeVersion(version) {
473
- return version.split(".", 3).map((part) => {
474
- const parsed = Number.parseInt(part, 10);
475
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
476
- });
477
- }
478
- function isNodeSqliteSupportedVersion(version) {
479
- const current = parseNodeVersion(version);
480
- const minimum = parseNodeVersion(MINIMUM_NODE_SQLITE_VERSION);
481
- for (const [index, minimumPart] of minimum.entries()) {
482
- const currentPart = current[index] ?? 0;
483
- if (currentPart > minimumPart) return true;
484
- if (currentPart < minimumPart) return false;
485
- }
486
- return true;
487
- }
488
- function isSqliteRuntimeSupported(input = {}) {
489
- if (input.isBun ?? isBunRuntime()) return true;
490
- return isNodeSqliteSupportedVersion(input.nodeVersion ?? process.versions.node);
491
- }
492
- function getUnsupportedNodeSqliteMessage(nodeVersion) {
493
- return `SQLite-backed token usage requires Bun or Node.js >= ${MINIMUM_NODE_SQLITE_VERSION}. Detected Node.js ${nodeVersion}. Upgrade Node.js or run the CLI with Bun, for example \`bunx --bun @jeffreycao/copilot-api@latest start\` or \`bun run start start\`.`;
494
- }
495
- var UnsupportedNodeSqliteRuntimeError = class extends Error {
496
- constructor(nodeVersion, cause) {
497
- super(getUnsupportedNodeSqliteMessage(nodeVersion), { cause });
498
- this.name = "UnsupportedNodeSqliteRuntimeError";
499
- }
500
- };
501
- async function openBunDatabase(dbPath) {
502
- return new (await (import(["bun", "sqlite"].join(":")))).Database(dbPath);
503
- }
504
- async function loadNodeSqliteModule() {
505
- const nodeVersion = process.versions.node;
506
- if (!isNodeSqliteSupportedVersion(nodeVersion)) throw new UnsupportedNodeSqliteRuntimeError(nodeVersion);
507
- const emitWarning = process.emitWarning.bind(process);
508
- process.emitWarning = (warning, ...args) => {
509
- if (typeof warning === "string" && warning.includes("SQLite is an experimental feature")) return;
510
- return emitWarning(warning, ...args);
511
- };
512
- const specifier = ["node", "sqlite"].join(":");
513
- try {
514
- return await import(specifier);
515
- } catch (error) {
516
- throw new UnsupportedNodeSqliteRuntimeError(nodeVersion, error);
517
- } finally {
518
- process.emitWarning = emitWarning;
519
- }
520
- }
521
- async function openNodeDatabase(dbPath) {
522
- return new (await (loadNodeSqliteModule())).DatabaseSync(dbPath);
523
- }
524
- async function openSqliteDatabase(dbPath) {
525
- const dir = path.dirname(dbPath);
526
- if (dbPath !== ":memory:" && dir !== ".") await fs$1.mkdir(dir, { recursive: true });
527
- return isBunRuntime() ? openBunDatabase(dbPath) : openNodeDatabase(dbPath);
528
- }
529
- var SqliteDbStore = class {
530
- dbPromise = null;
531
- options;
532
- constructor(options) {
533
- this.options = options;
534
- }
535
- getDb() {
536
- this.dbPromise ??= this.open();
537
- return this.dbPromise;
538
- }
539
- async close(input) {
540
- const currentDbPromise = this.dbPromise;
541
- this.dbPromise = null;
542
- if (!currentDbPromise) return;
543
- const db = await currentDbPromise;
544
- input?.beforeClose?.(db);
545
- db.close?.();
546
- }
547
- async open() {
548
- const db = await openSqliteDatabase(this.options.getPath());
549
- this.options.initialize?.(db);
550
- return db;
551
- }
552
- };
553
- //#endregion
554
- //#region src/lib/token-usage/store.ts
555
- const DB_PATH_ENV = "COPILOT_API_SQLITE_DB_PATH";
556
- const DEFAULT_DB_FILENAME = "copilot-api.sqlite";
557
- const COST_NANOS_PER_UNIT$1 = 1e9;
558
- let writeQueue = Promise.resolve();
559
- function getDbPath() {
560
- return process.env[DB_PATH_ENV] ?? path.join(PATHS.APP_DIR, DEFAULT_DB_FILENAME);
561
- }
562
- const tokenUsageDbStore = new SqliteDbStore({
563
- getPath: getDbPath,
564
- initialize: initializeTokenUsageDb
565
- });
566
- function getDb() {
567
- return tokenUsageDbStore.getDb();
568
- }
569
- function isTokenUsageStorageEnabled() {
570
- return isSqliteRuntimeSupported();
571
- }
572
- function initializeTokenUsageDb(db) {
573
- db.exec("PRAGMA journal_mode = WAL");
574
- db.exec("PRAGMA busy_timeout = 5000");
575
- db.exec(`
576
- CREATE TABLE IF NOT EXISTS token_usage_events (
577
- id INTEGER PRIMARY KEY AUTOINCREMENT,
578
- created_at_ms INTEGER NOT NULL,
579
- created_at_utc TEXT NOT NULL,
580
- trace_id TEXT NOT NULL,
581
- session_id TEXT NOT NULL DEFAULT '',
582
- user_id TEXT NOT NULL DEFAULT '',
583
- source TEXT NOT NULL,
584
- endpoint TEXT NOT NULL,
585
- provider_name TEXT,
586
- model TEXT NOT NULL,
587
- input_tokens INTEGER NOT NULL DEFAULT 0,
588
- output_tokens INTEGER NOT NULL DEFAULT 0,
589
- cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
590
- cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
591
- total_tokens INTEGER NOT NULL DEFAULT 0,
592
- total_nano_aiu INTEGER,
593
- cost_currency TEXT,
594
- total_cost_nanos INTEGER,
595
- cost_source TEXT
596
- )
597
- `);
598
- ensureColumn(db, "user_id", "TEXT NOT NULL DEFAULT ''");
599
- ensureColumn(db, "total_tokens", "INTEGER NOT NULL DEFAULT 0");
600
- ensureColumn(db, "total_nano_aiu", "INTEGER");
601
- ensureColumn(db, "cost_currency", "TEXT");
602
- ensureColumn(db, "total_cost_nanos", "INTEGER");
603
- ensureColumn(db, "cost_source", "TEXT");
604
- db.exec(`
605
- CREATE INDEX IF NOT EXISTS idx_token_usage_events_created_at_ms
606
- ON token_usage_events(created_at_ms)
607
- `);
608
- db.exec(`
609
- CREATE INDEX IF NOT EXISTS idx_token_usage_events_model
610
- ON token_usage_events(model)
611
- `);
612
- db.exec(`
613
- CREATE INDEX IF NOT EXISTS idx_token_usage_events_trace_id
614
- ON token_usage_events(trace_id)
615
- `);
616
- db.exec(`
617
- CREATE INDEX IF NOT EXISTS idx_token_usage_events_session_id
618
- ON token_usage_events(session_id)
619
- `);
620
- db.exec(`
621
- CREATE INDEX IF NOT EXISTS idx_token_usage_events_user_id
622
- ON token_usage_events(user_id)
623
- `);
624
- }
625
- function ensureColumn(db, name, definition) {
626
- if (!db.prepare("PRAGMA table_info(token_usage_events)").all().some((row) => row.name === name)) db.exec(`ALTER TABLE token_usage_events ADD COLUMN ${name} ${definition}`);
627
- }
628
- function normalizeToken(value) {
629
- if (typeof value !== "number" || !Number.isFinite(value)) return 0;
630
- return Math.max(0, Math.floor(value));
631
- }
632
- function normalizeOptionalToken(value) {
633
- return value === null || value === void 0 ? void 0 : normalizeToken(value);
634
- }
635
- function hasAnyToken(tokens) {
636
- return normalizeToken(tokens.input_tokens) > 0 || normalizeToken(tokens.output_tokens) > 0 || normalizeToken(tokens.cache_read_input_tokens) > 0 || normalizeToken(tokens.cache_creation_input_tokens) > 0 || normalizeToken(tokens.total_tokens) > 0 || normalizeToken(tokens.total_nano_aiu) > 0;
214
+ function hasAnyToken(tokens) {
215
+ return normalizeToken(tokens.input_tokens) > 0 || normalizeToken(tokens.output_tokens) > 0 || normalizeToken(tokens.cache_read_input_tokens) > 0 || normalizeToken(tokens.cache_creation_input_tokens) > 0 || normalizeToken(tokens.total_tokens) > 0 || normalizeToken(tokens.total_nano_aiu) > 0;
637
216
  }
638
217
  function resolveTotalTokens(input) {
639
218
  const explicitTotal = normalizeOptionalToken(input.total_tokens);
@@ -660,9 +239,14 @@ async function writeTokenUsageEvent(event) {
660
239
  total_nano_aiu,
661
240
  cost_currency,
662
241
  total_cost_nanos,
663
- cost_source
664
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
665
- `).run(event.created_at_ms, event.created_at_utc, event.trace_id, event.session_id, event.user_id, event.source, event.endpoint, event.provider_name, event.model, event.input_tokens, event.output_tokens, event.cache_read_input_tokens, event.cache_creation_input_tokens, event.total_tokens, event.total_nano_aiu, event.cost_currency, event.total_cost_nanos, event.cost_source);
242
+ cost_source,
243
+ request_id,
244
+ first_token_ms,
245
+ duration_ms,
246
+ request_outcome,
247
+ user_agent
248
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
249
+ `).run(event.created_at_ms, event.created_at_utc, event.trace_id, event.session_id, event.user_id, event.source, event.endpoint, event.provider_name, event.model, event.input_tokens, event.output_tokens, event.cache_read_input_tokens, event.cache_creation_input_tokens, event.total_tokens, event.total_nano_aiu, event.cost_currency, event.total_cost_nanos, event.cost_source, event.request_id, event.first_token_ms, event.duration_ms, event.request_outcome, event.user_agent);
666
250
  }
667
251
  function enqueueTokenUsageWrite(event) {
668
252
  if (!isTokenUsageStorageEnabled()) return;
@@ -670,6 +254,19 @@ function enqueueTokenUsageWrite(event) {
670
254
  consola.warn("Failed to record token usage", error);
671
255
  });
672
256
  }
257
+ function enqueueTokenUsageTiming(timing) {
258
+ if (!isTokenUsageStorageEnabled()) return;
259
+ const { id, firstTokenMs, durationMs, outcome } = timing;
260
+ writeQueue = writeQueue.then(async () => {
261
+ (await getDb()).prepare(`
262
+ UPDATE token_usage_events
263
+ SET first_token_ms = ?, duration_ms = ?, request_outcome = ?
264
+ WHERE request_id = ?
265
+ `).run(firstTokenMs, durationMs, outcome, id);
266
+ }).catch((error) => {
267
+ consola.warn("Failed to record token usage timing", error);
268
+ });
269
+ }
673
270
  async function flushTokenUsageEvents() {
674
271
  let currentQueue = writeQueue;
675
272
  while (true) {
@@ -878,6 +475,8 @@ function totalsFromRow(row, costs = []) {
878
475
  function modelSummaryFromRow(row, costs = []) {
879
476
  return {
880
477
  ...totalsFromRow(row, costs),
478
+ average_first_token_ms: nullableNumberFromRow(row, "average_first_token_ms"),
479
+ output_tokens_per_second: nullableNumberFromRow(row, "output_tokens_per_second"),
881
480
  model: typeof row.model === "string" ? row.model : "unknown"
882
481
  };
883
482
  }
@@ -889,8 +488,21 @@ function nullableStringFromRow(row, key) {
889
488
  const value = row[key];
890
489
  return typeof value === "string" ? value : null;
891
490
  }
491
+ function outputTokensPerSecond(row) {
492
+ const first = nullableNumberFromRow(row, "first_token_ms");
493
+ const duration = nullableNumberFromRow(row, "duration_ms");
494
+ const output = numberFromRow(row, "output_tokens");
495
+ if (row.request_outcome !== "completed" || row.usage_event_count !== 1 || first === null || first < 0 || duration === null || duration <= first || output <= 0) return null;
496
+ const rate = output * 1e3 / (duration - first);
497
+ return Number.isFinite(rate) ? rate : null;
498
+ }
892
499
  function usageEventFromRow(row) {
893
500
  return {
501
+ request_id: nullableStringFromRow(row, "request_id"),
502
+ first_token_ms: nullableNumberFromRow(row, "first_token_ms"),
503
+ duration_ms: nullableNumberFromRow(row, "duration_ms"),
504
+ request_outcome: nullableStringFromRow(row, "request_outcome"),
505
+ output_tokens_per_second: outputTokensPerSecond(row),
894
506
  cache_creation_input_tokens: numberFromRow(row, "cache_creation_input_tokens"),
895
507
  cache_read_input_tokens: numberFromRow(row, "cache_read_input_tokens"),
896
508
  cost: eventCostFromRow(row),
@@ -907,7 +519,8 @@ function usageEventFromRow(row) {
907
519
  total_nano_aiu: nullableNumberFromRow(row, "total_nano_aiu"),
908
520
  total_tokens: numberFromRow(row, "total_tokens"),
909
521
  trace_id: stringFromRow(row, "trace_id"),
910
- user_id: stringFromRow(row, "user_id")
522
+ user_id: stringFromRow(row, "user_id"),
523
+ user_agent: stringFromRow(row, "user_agent")
911
524
  };
912
525
  }
913
526
  function getTotalsRow(db, range) {
@@ -1034,11 +647,56 @@ function getSessionSummaries(db, range, limit, offset) {
1034
647
  };
1035
648
  });
1036
649
  }
650
+ function getModelTimingRows(db, intervals) {
651
+ if (intervals.length === 0) return [];
652
+ return db.prepare(`
653
+ WITH selected_requests AS (
654
+ SELECT DISTINCT request_id FROM token_usage_events
655
+ WHERE created_at_ms >= ? AND created_at_ms < ? AND request_id IS NOT NULL
656
+ ), requests AS (
657
+ SELECT e.request_id, MIN(e.model) AS model,
658
+ MIN(e.created_at_ms) AS first_usage_at_ms,
659
+ MAX(e.created_at_ms) AS last_usage_at_ms,
660
+ SUM(e.output_tokens) AS output_tokens,
661
+ MAX(e.first_token_ms) AS first_token_ms,
662
+ MAX(e.duration_ms) AS duration_ms
663
+ FROM selected_requests r JOIN token_usage_events e USING (request_id)
664
+ GROUP BY e.request_id
665
+ HAVING COUNT(DISTINCT e.model) = 1
666
+ AND COUNT(e.first_token_ms) = COUNT(*)
667
+ AND COUNT(e.duration_ms) = COUNT(*)
668
+ AND COUNT(e.request_outcome) = COUNT(*)
669
+ AND MIN(e.request_outcome) = 'completed'
670
+ AND MAX(e.request_outcome) = 'completed'
671
+ AND MIN(e.first_token_ms) >= 0
672
+ AND MIN(e.duration_ms) >= MAX(e.first_token_ms)
673
+ ), buckets(usage_date, start_ms, end_ms) AS (VALUES ${intervals.map(() => "(?, ?, ?)").join(", ")})
674
+ SELECT buckets.usage_date, model,
675
+ AVG(first_token_ms) AS average_first_token_ms,
676
+ 1000.0 * SUM(CASE WHEN duration_ms > first_token_ms AND output_tokens > 0 THEN output_tokens END)
677
+ / SUM(CASE WHEN duration_ms > first_token_ms AND output_tokens > 0 THEN duration_ms - first_token_ms END)
678
+ AS output_tokens_per_second
679
+ FROM requests JOIN buckets
680
+ ON first_usage_at_ms >= start_ms AND last_usage_at_ms < end_ms
681
+ GROUP BY buckets.usage_date, model
682
+ `).all(intervals[0].startMs, intervals[intervals.length - 1].endMs, ...intervals.flatMap(({ date, startMs, endMs }) => [
683
+ date,
684
+ startMs,
685
+ endMs
686
+ ]));
687
+ }
1037
688
  function getModelSummaries(db, range) {
1038
689
  const costMap = getModelCostMap(db, range);
690
+ const timings = new Map(getModelTimingRows(db, [{
691
+ date: "",
692
+ ...range
693
+ }]).map((row) => [stringFromRow(row, "model"), row]));
1039
694
  return getModelRows(db, range).map((row) => {
1040
695
  const model = stringFromRow(row, "model") || "unknown";
1041
- return modelSummaryFromRow(row, costMap.get(model) ?? []);
696
+ return modelSummaryFromRow({
697
+ ...row,
698
+ ...timings.get(model)
699
+ }, costMap.get(model) ?? []);
1042
700
  });
1043
701
  }
1044
702
  function getDailyModelSummaries(db, intervals) {
@@ -1093,13 +751,17 @@ function getDailyModelSummaries(db, intervals) {
1093
751
  const key = `${date}\u0000${model}`;
1094
752
  costsByDateAndModel.set(key, [...costsByDateAndModel.get(key) ?? [], cost]);
1095
753
  }
754
+ const timings = new Map(getModelTimingRows(db, intervals).map((row) => [`${stringFromRow(row, "usage_date")}\u0000${stringFromRow(row, "model")}`, row]));
1096
755
  const summariesByDate = /* @__PURE__ */ new Map();
1097
756
  for (const row of rows) {
1098
757
  const date = stringFromRow(row, "usage_date");
1099
758
  if (!date) continue;
1100
759
  const model = stringFromRow(row, "model") || "unknown";
1101
760
  const summaries = summariesByDate.get(date) ?? [];
1102
- summaries.push(modelSummaryFromRow(row, costsByDateAndModel.get(`${date}\u0000${model}`) ?? []));
761
+ summaries.push(modelSummaryFromRow({
762
+ ...row,
763
+ ...timings.get(`${date}\u0000${model}`)
764
+ }, costsByDateAndModel.get(`${date}\u0000${model}`) ?? []));
1103
765
  summariesByDate.set(date, summaries);
1104
766
  }
1105
767
  return summariesByDate;
@@ -1114,125 +776,622 @@ function createDailyBucket(interval, byModel) {
1114
776
  start_ms: interval.startMs,
1115
777
  totals
1116
778
  };
1117
- }
1118
- async function getTokenUsageSummary(period) {
1119
- if (!isTokenUsageStorageEnabled()) return createEmptySummary(period);
1120
- await flushTokenUsageEvents();
1121
- const db = await getDb();
1122
- const range = getPeriodRangeFromDb(db, period);
1123
- const totalsRow = getTotalsRow(db, range);
1124
- return {
1125
- byModel: getModelSummaries(db, range),
1126
- period,
1127
- range: rangePayload(range),
1128
- totals: totalsFromRow(totalsRow, getCostRows(db, range))
779
+ }
780
+ async function getTokenUsageSummary(period) {
781
+ if (!isTokenUsageStorageEnabled()) return createEmptySummary(period);
782
+ await flushTokenUsageEvents();
783
+ const db = await getDb();
784
+ const range = getPeriodRangeFromDb(db, period);
785
+ const totalsRow = getTotalsRow(db, range);
786
+ return {
787
+ byModel: getModelSummaries(db, range),
788
+ period,
789
+ range: rangePayload(range),
790
+ totals: totalsFromRow(totalsRow, getCostRows(db, range))
791
+ };
792
+ }
793
+ async function getTokenUsageDailySummary(period) {
794
+ if (!isTokenUsageStorageEnabled()) return createEmptyDailySummary(period);
795
+ await flushTokenUsageEvents();
796
+ const db = await getDb();
797
+ const range = getPeriodRangeFromDb(db, period);
798
+ const intervals = createDailyIntervals(range);
799
+ const dailySummaries = getDailyModelSummaries(db, intervals);
800
+ return {
801
+ byModel: getModelSummaries(db, range),
802
+ days: intervals.map((interval) => createDailyBucket(interval, dailySummaries.get(interval.date) ?? [])),
803
+ period,
804
+ range: rangePayload(range),
805
+ totals: totalsFromRow(getTotalsRow(db, range), getCostRows(db, range))
806
+ };
807
+ }
808
+ async function getTokenUsageEventsPage(input) {
809
+ if (!isTokenUsageStorageEnabled()) return createEmptyEventsPage(input);
810
+ await flushTokenUsageEvents();
811
+ const page = Math.max(1, Math.floor(input.page));
812
+ const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
813
+ const offset = (page - 1) * pageSize;
814
+ const db = await getDb();
815
+ const range = getPeriodRangeFromDb(db, input.period);
816
+ const totalRow = db.prepare(`
817
+ SELECT COUNT(*) AS total
818
+ FROM token_usage_events
819
+ WHERE created_at_ms >= ? AND created_at_ms < ?
820
+ `).get(range.startMs, range.endMs);
821
+ const rows = db.prepare(`
822
+ SELECT
823
+ id,
824
+ created_at_ms,
825
+ created_at_utc,
826
+ trace_id,
827
+ session_id,
828
+ user_id,
829
+ source,
830
+ endpoint,
831
+ provider_name,
832
+ model,
833
+ input_tokens,
834
+ output_tokens,
835
+ cache_read_input_tokens,
836
+ cache_creation_input_tokens,
837
+ total_nano_aiu,
838
+ total_tokens,
839
+ cost_currency,
840
+ total_cost_nanos,
841
+ cost_source,
842
+ request_id,
843
+ first_token_ms,
844
+ duration_ms,
845
+ request_outcome,
846
+ user_agent,
847
+ (SELECT COUNT(*) FROM token_usage_events sibling
848
+ WHERE sibling.request_id = token_usage_events.request_id) AS usage_event_count
849
+ FROM token_usage_events
850
+ WHERE created_at_ms >= ? AND created_at_ms < ?
851
+ ORDER BY created_at_ms DESC, id DESC
852
+ LIMIT ? OFFSET ?
853
+ `).all(range.startMs, range.endMs, pageSize, offset);
854
+ const total = numberFromRow(totalRow, "total");
855
+ return {
856
+ items: rows.map((row) => usageEventFromRow(row)),
857
+ page,
858
+ page_size: pageSize,
859
+ period: input.period,
860
+ range: {
861
+ end_ms: range.endMs,
862
+ end_utc: new Date(range.endMs).toISOString(),
863
+ start_ms: range.startMs,
864
+ start_utc: new Date(range.startMs).toISOString()
865
+ },
866
+ total,
867
+ total_pages: Math.max(1, Math.ceil(total / pageSize))
868
+ };
869
+ }
870
+ async function getTokenUsageSessionsPage(input) {
871
+ const page = Math.max(1, Math.floor(input.page));
872
+ const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
873
+ if (!isTokenUsageStorageEnabled()) return {
874
+ ...createEmptyEventsPage(input),
875
+ items: []
876
+ };
877
+ await flushTokenUsageEvents();
878
+ const db = await getDb();
879
+ const range = getPeriodRangeFromDb(db, input.period);
880
+ const total = numberFromRow(db.prepare(`
881
+ SELECT COUNT(DISTINCT session_id) AS total
882
+ FROM token_usage_events
883
+ WHERE created_at_ms >= ? AND created_at_ms < ?
884
+ `).get(range.startMs, range.endMs), "total");
885
+ return {
886
+ items: getSessionSummaries(db, range, pageSize, (page - 1) * pageSize),
887
+ page,
888
+ page_size: pageSize,
889
+ period: input.period,
890
+ range: rangePayload(range),
891
+ total,
892
+ total_pages: Math.max(1, Math.ceil(total / pageSize))
893
+ };
894
+ }
895
+ async function closeUsageStore() {
896
+ await flushTokenUsageEvents();
897
+ await tokenUsageDbStore.close({ beforeClose: (db) => {
898
+ try {
899
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
900
+ } catch {}
901
+ } });
902
+ writeQueue = Promise.resolve();
903
+ }
904
+ registerProcessCleanup(closeUsageStore);
905
+ //#endregion
906
+ //#region src/lib/request-events.ts
907
+ const TRACKED = /\/(chat\/completions|responses|messages|alpha\/search)$/;
908
+ function setPluginRequestModel(model) {
909
+ requestContext.getStore()?.pluginRequest?.setModel(model);
910
+ }
911
+ function requestEventsMiddleware(plugins, port) {
912
+ return async (c, next) => {
913
+ if (c.req.method !== "POST" || !TRACKED.test(c.req.path)) return await next();
914
+ const store = requestContext.getStore();
915
+ if (!store) return await next();
916
+ const started = performance.now();
917
+ const info = {
918
+ id: randomUUID(),
919
+ traceId: store.traceId,
920
+ userAgent: store.userAgent,
921
+ sessionId: store.sessionAffinity?.trim() || null,
922
+ path: c.req.path,
923
+ port,
924
+ startedAt: Date.now(),
925
+ model: null,
926
+ actualModel: null
927
+ };
928
+ let finished = false;
929
+ const signal = c.req.raw.signal;
930
+ const timing = {
931
+ id: info.id,
932
+ firstTokenMs: null,
933
+ durationMs: null,
934
+ outcome: null,
935
+ hasUsage: false,
936
+ recordFirstToken: () => {
937
+ if (finished || timing.firstTokenMs !== null) return;
938
+ timing.firstTokenMs = performance.now() - started;
939
+ plugins?.publish("request.first_token", {
940
+ ...info,
941
+ firstTokenMs: timing.firstTokenMs
942
+ });
943
+ }
944
+ };
945
+ store.requestTiming = timing;
946
+ let status = 0;
947
+ const finish = (outcome) => {
948
+ if (finished) return;
949
+ finished = true;
950
+ timing.durationMs = performance.now() - started;
951
+ timing.outcome = outcome;
952
+ if (timing.hasUsage) enqueueTokenUsageTiming(timing);
953
+ plugins?.publish("request.end", {
954
+ ...info,
955
+ durationMs: timing.durationMs,
956
+ firstTokenMs: timing.firstTokenMs,
957
+ status,
958
+ outcome
959
+ });
960
+ signal.removeEventListener("abort", abort);
961
+ };
962
+ const abort = () => finish("cancelled");
963
+ if (plugins?.enabled) store.pluginRequest = {
964
+ id: info.id,
965
+ setModel: (model) => {
966
+ if (finished || typeof model !== "string" || !model || info.model === model) return;
967
+ info.model = model;
968
+ plugins.publish("request.model", info);
969
+ },
970
+ setActualModel: (model) => {
971
+ if (finished || typeof model !== "string" || !model || info.actualModel === model) return;
972
+ info.actualModel = model;
973
+ plugins.publish("request.actual_model", info);
974
+ },
975
+ setSessionId: (sessionId) => {
976
+ if (!sessionId || info.sessionId === sessionId) return;
977
+ info.sessionId = sessionId;
978
+ if (!finished) plugins.publish("request.session", info);
979
+ },
980
+ recordFirstToken: timing.recordFirstToken,
981
+ recordUsage: (event) => plugins.publish("usage.recorded", {
982
+ ...event,
983
+ requestId: info.id,
984
+ userAgent: info.userAgent,
985
+ sessionId: event.session_id || null,
986
+ actualModel: event.model
987
+ })
988
+ };
989
+ plugins?.publish("request.start", info);
990
+ signal.addEventListener("abort", abort, { once: true });
991
+ if (signal.aborted) abort();
992
+ try {
993
+ await next();
994
+ const response = c.res;
995
+ status = response.status;
996
+ if (!response.body) {
997
+ finish(status >= 400 ? "error" : "completed");
998
+ return;
999
+ }
1000
+ const reader = response.body.getReader();
1001
+ const body = new ReadableStream({
1002
+ async pull(controller) {
1003
+ try {
1004
+ const result = await reader.read();
1005
+ if (result.done) {
1006
+ finish(status >= 400 ? "error" : "completed");
1007
+ controller.close();
1008
+ reader.releaseLock();
1009
+ return;
1010
+ }
1011
+ controller.enqueue(result.value);
1012
+ } catch (error) {
1013
+ finish("error");
1014
+ controller.error(error);
1015
+ reader.releaseLock();
1016
+ }
1017
+ },
1018
+ async cancel(reason) {
1019
+ finish("cancelled");
1020
+ try {
1021
+ await reader.cancel(reason);
1022
+ } finally {
1023
+ reader.releaseLock();
1024
+ }
1025
+ }
1026
+ });
1027
+ c.res = new Response(body, {
1028
+ status: response.status,
1029
+ statusText: response.statusText,
1030
+ headers: response.headers
1031
+ });
1032
+ } catch (error) {
1033
+ finish("error");
1034
+ throw error;
1035
+ }
1036
+ };
1037
+ }
1038
+ //#endregion
1039
+ //#region src/lib/logger.ts
1040
+ const LOG_RETENTION_MS = 6048e5;
1041
+ const CLEANUP_INTERVAL_MS = 864e5;
1042
+ const LOG_DIR_ENV = "COPILOT_API_LOG_DIR";
1043
+ const DEFAULT_LOG_DIR = path.join(PATHS.APP_DIR, "logs");
1044
+ const FLUSH_INTERVAL_MS = 1e3;
1045
+ const MAX_BUFFER_SIZE = 100;
1046
+ const getLogDir = () => process.env[LOG_DIR_ENV]?.trim() || DEFAULT_LOG_DIR;
1047
+ const logStreams = /* @__PURE__ */ new Map();
1048
+ const logBuffers = /* @__PURE__ */ new Map();
1049
+ let runtimeInitialized = false;
1050
+ let flushInterval;
1051
+ let cleanupInterval;
1052
+ let currentLogDateKey;
1053
+ const ensureLogDirectory = () => {
1054
+ const logDir = getLogDir();
1055
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
1056
+ };
1057
+ const formatArgs = (args) => args.map((arg) => typeof arg === "string" ? arg : util.inspect(arg, {
1058
+ depth: null,
1059
+ colors: false
1060
+ })).join(" ");
1061
+ const sanitizeName = (name) => {
1062
+ const normalized = name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
1063
+ return normalized === "" ? "handler" : normalized;
1064
+ };
1065
+ const maybeUnref = (timer) => {
1066
+ timer.unref();
1067
+ };
1068
+ const flushBuffer = (filePath) => {
1069
+ const buffer = logBuffers.get(filePath);
1070
+ if (!buffer) return;
1071
+ logBuffers.delete(filePath);
1072
+ if (buffer.length === 0) return;
1073
+ const stream = getLogStream(filePath);
1074
+ const content = buffer.join("\n") + "\n";
1075
+ stream.write(content, (error) => {
1076
+ if (error) console.warn("Failed to write handler log", error);
1077
+ });
1078
+ };
1079
+ const flushAllBuffers = () => {
1080
+ for (const filePath of logBuffers.keys()) flushBuffer(filePath);
1081
+ };
1082
+ const closeLogFile = async (filePath) => {
1083
+ flushBuffer(filePath);
1084
+ const stream = logStreams.get(filePath);
1085
+ logStreams.delete(filePath);
1086
+ logBuffers.delete(filePath);
1087
+ if (!stream || stream.closed) return;
1088
+ await new Promise((resolve) => {
1089
+ stream.once("close", resolve);
1090
+ stream.end();
1091
+ });
1092
+ };
1093
+ const closePreviousDateLogFiles = (dateKey) => {
1094
+ if (currentLogDateKey === void 0) {
1095
+ currentLogDateKey = dateKey;
1096
+ return;
1097
+ }
1098
+ if (currentLogDateKey === dateKey) return;
1099
+ const filePaths = /* @__PURE__ */ new Set([...logBuffers.keys(), ...logStreams.keys()]);
1100
+ currentLogDateKey = dateKey;
1101
+ for (const filePath of filePaths) closeLogFile(filePath).catch((error) => {
1102
+ console.warn("Failed to rotate handler log", error);
1103
+ });
1104
+ };
1105
+ const cleanupOldLogs = async () => {
1106
+ const logDir = getLogDir();
1107
+ if (!fs.existsSync(logDir)) return;
1108
+ const now = Date.now();
1109
+ for (const entry of fs.readdirSync(logDir)) {
1110
+ const filePath = path.join(logDir, entry);
1111
+ let stats;
1112
+ try {
1113
+ stats = fs.statSync(filePath);
1114
+ } catch {
1115
+ continue;
1116
+ }
1117
+ if (!stats.isFile() || now - stats.mtimeMs <= LOG_RETENTION_MS) continue;
1118
+ try {
1119
+ await closeLogFile(filePath);
1120
+ } catch (error) {
1121
+ console.warn(`Failed to close old handler log: ${filePath}`, error);
1122
+ continue;
1123
+ }
1124
+ try {
1125
+ fs.rmSync(filePath);
1126
+ } catch (error) {
1127
+ console.warn(`Failed to remove old handler log: ${filePath}`, error);
1128
+ }
1129
+ }
1130
+ };
1131
+ const scheduleLogCleanup = () => {
1132
+ cleanupOldLogs().catch((error) => {
1133
+ console.warn("Failed to clean up handler logs", error);
1134
+ });
1135
+ };
1136
+ const flushAndCloseLogStreams = () => {
1137
+ flushAllBuffers();
1138
+ for (const stream of logStreams.values()) stream.end();
1139
+ logStreams.clear();
1140
+ logBuffers.clear();
1141
+ currentLogDateKey = void 0;
1142
+ };
1143
+ const shutdownLoggerRuntime = () => {
1144
+ if (flushInterval) {
1145
+ clearInterval(flushInterval);
1146
+ flushInterval = void 0;
1147
+ }
1148
+ if (cleanupInterval) {
1149
+ clearInterval(cleanupInterval);
1150
+ cleanupInterval = void 0;
1151
+ }
1152
+ flushAndCloseLogStreams();
1153
+ runtimeInitialized = false;
1154
+ };
1155
+ const initializeLoggerRuntime = () => {
1156
+ if (runtimeInitialized) return;
1157
+ runtimeInitialized = true;
1158
+ ensureLogDirectory();
1159
+ scheduleLogCleanup();
1160
+ flushInterval = setInterval(flushAllBuffers, FLUSH_INTERVAL_MS);
1161
+ maybeUnref(flushInterval);
1162
+ cleanupInterval = setInterval(scheduleLogCleanup, CLEANUP_INTERVAL_MS);
1163
+ maybeUnref(cleanupInterval);
1164
+ registerProcessCleanup(shutdownLoggerRuntime);
1165
+ };
1166
+ const getLogStream = (filePath) => {
1167
+ initializeLoggerRuntime();
1168
+ let stream = logStreams.get(filePath);
1169
+ if (!stream || stream.destroyed) {
1170
+ const createdStream = fs.createWriteStream(filePath, { flags: "a" });
1171
+ stream = createdStream;
1172
+ logStreams.set(filePath, createdStream);
1173
+ createdStream.on("error", (error) => {
1174
+ console.warn("Log stream error", error);
1175
+ if (logStreams.get(filePath) === createdStream) logStreams.delete(filePath);
1176
+ });
1177
+ }
1178
+ return stream;
1179
+ };
1180
+ const appendLine = (filePath, line) => {
1181
+ let buffer = logBuffers.get(filePath);
1182
+ if (!buffer) {
1183
+ buffer = [];
1184
+ logBuffers.set(filePath, buffer);
1185
+ }
1186
+ buffer.push(line);
1187
+ if (buffer.length >= MAX_BUFFER_SIZE) flushBuffer(filePath);
1188
+ };
1189
+ const debugLazy = (logger, factory) => {
1190
+ if (!state.verbose) return;
1191
+ logger.debug(...factory());
1192
+ };
1193
+ const debugJson = (logger, label, value) => {
1194
+ debugLazy(logger, () => [label, JSON.stringify(value)]);
1195
+ };
1196
+ const debugJsonAsync = async (logger, label, factory) => {
1197
+ if (!state.verbose) return;
1198
+ logger.debug(label, JSON.stringify(await factory()));
1199
+ };
1200
+ const debugJsonTail = (logger, label, { value, tailLength = 400 }) => {
1201
+ debugLazy(logger, () => [label, JSON.stringify(value).slice(-tailLength)]);
1202
+ };
1203
+ const createHandlerLogger = (name) => {
1204
+ const sanitizedName = sanitizeName(name);
1205
+ const instance = consola.withTag(name);
1206
+ if (state.verbose) instance.level = 5;
1207
+ instance.setReporters([]);
1208
+ instance.addReporter({ log(logObj) {
1209
+ initializeLoggerRuntime();
1210
+ const traceId = requestContext.getStore()?.traceId;
1211
+ const date = logObj.date;
1212
+ const dateKey = date.toLocaleDateString("sv-SE");
1213
+ const timestamp = date.toLocaleString("sv-SE", { hour12: false });
1214
+ const filePath = path.join(getLogDir(), `${sanitizedName}-${dateKey}.log`);
1215
+ const message = formatArgs(logObj.args);
1216
+ const traceIdStr = traceId ? ` [${traceId}]` : "";
1217
+ const line = `[${timestamp}] [${logObj.type}] [${logObj.tag || name}]${traceIdStr}${message ? ` ${message}` : ""}`;
1218
+ closePreviousDateLogFiles(dateKey);
1219
+ appendLine(filePath, line);
1220
+ } });
1221
+ return instance;
1222
+ };
1223
+ //#endregion
1224
+ //#region src/lib/models.ts
1225
+ /**
1226
+ * Converts a Copilot upstream model ID to a client-friendly ID that Claude Code
1227
+ * and Claude Desktop recognize (dots in version replaced with hyphens).
1228
+ * e.g. "claude-sonnet-4.6" -> "claude-sonnet-4-6"
1229
+ * Non-Claude models are returned unchanged.
1230
+ */
1231
+ const toClientModelId = (modelId) => {
1232
+ const normalized = normalizeSdkModelId(modelId);
1233
+ if (!normalized) return modelId;
1234
+ const versionHyphenated = normalized.version.replaceAll(".", "-");
1235
+ return `claude-${normalized.family}-${versionHyphenated}`;
1236
+ };
1237
+ const findEndpointModel = (sdkModelId) => {
1238
+ const models = state.models?.data ?? [];
1239
+ const exactMatch = models.find((m) => m.id === sdkModelId);
1240
+ if (exactMatch) return exactMatch;
1241
+ const normalized = normalizeSdkModelId(sdkModelId);
1242
+ if (!normalized) return;
1243
+ const modelName = `claude-${normalized.family}-${normalized.version}`;
1244
+ const model = models.find((m) => m.id === modelName);
1245
+ if (model) return model;
1246
+ };
1247
+ /**
1248
+ * Normalizes an SDK model ID to extract the model family and version.
1249
+ * this method from github copilot extension
1250
+ * Examples:
1251
+ * - "claude-opus-4-5-20251101" -> { family: "opus", version: "4.5" }
1252
+ * - "claude-3-5-sonnet-20241022" -> { family: "sonnet", version: "3.5" }
1253
+ * - "claude-sonnet-4-20250514" -> { family: "sonnet", version: "4" }
1254
+ * - "claude-haiku-3-5-20250514" -> { family: "haiku", version: "3.5" }
1255
+ * - "claude-haiku-4.5" -> { family: "haiku", version: "4.5" }
1256
+ */
1257
+ const normalizeSdkModelId = (sdkModelId) => {
1258
+ const withoutDate = sdkModelId.toLowerCase().replace(/-\d{8}$/, "");
1259
+ const pattern1 = withoutDate.match(/^claude-(\w+)-(\d+)\.(\d+)$/);
1260
+ if (pattern1) return {
1261
+ family: pattern1[1],
1262
+ version: `${pattern1[2]}.${pattern1[3]}`
1263
+ };
1264
+ const pattern2 = withoutDate.match(/^claude-(\w+)-(\d+)-(\d+)$/);
1265
+ if (pattern2) return {
1266
+ family: pattern2[1],
1267
+ version: `${pattern2[2]}.${pattern2[3]}`
1129
1268
  };
1130
- }
1131
- async function getTokenUsageDailySummary(period) {
1132
- if (!isTokenUsageStorageEnabled()) return createEmptyDailySummary(period);
1133
- await flushTokenUsageEvents();
1134
- const db = await getDb();
1135
- const range = getPeriodRangeFromDb(db, period);
1136
- const intervals = createDailyIntervals(range);
1137
- const dailySummaries = getDailyModelSummaries(db, intervals);
1138
- return {
1139
- byModel: getModelSummaries(db, range),
1140
- days: intervals.map((interval) => createDailyBucket(interval, dailySummaries.get(interval.date) ?? [])),
1141
- period,
1142
- range: rangePayload(range),
1143
- totals: totalsFromRow(getTotalsRow(db, range), getCostRows(db, range))
1269
+ const pattern3 = withoutDate.match(/^claude-(\d+)-(\d+)-(\w+)$/);
1270
+ if (pattern3) return {
1271
+ family: pattern3[3],
1272
+ version: `${pattern3[1]}.${pattern3[2]}`
1144
1273
  };
1145
- }
1146
- async function getTokenUsageEventsPage(input) {
1147
- if (!isTokenUsageStorageEnabled()) return createEmptyEventsPage(input);
1148
- await flushTokenUsageEvents();
1149
- const page = Math.max(1, Math.floor(input.page));
1150
- const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
1151
- const offset = (page - 1) * pageSize;
1152
- const db = await getDb();
1153
- const range = getPeriodRangeFromDb(db, input.period);
1154
- const totalRow = db.prepare(`
1155
- SELECT COUNT(*) AS total
1156
- FROM token_usage_events
1157
- WHERE created_at_ms >= ? AND created_at_ms < ?
1158
- `).get(range.startMs, range.endMs);
1159
- const rows = db.prepare(`
1160
- SELECT
1161
- id,
1162
- created_at_ms,
1163
- created_at_utc,
1164
- trace_id,
1165
- session_id,
1166
- user_id,
1167
- source,
1168
- endpoint,
1169
- provider_name,
1170
- model,
1171
- input_tokens,
1172
- output_tokens,
1173
- cache_read_input_tokens,
1174
- cache_creation_input_tokens,
1175
- total_nano_aiu,
1176
- total_tokens,
1177
- cost_currency,
1178
- total_cost_nanos,
1179
- cost_source
1180
- FROM token_usage_events
1181
- WHERE created_at_ms >= ? AND created_at_ms < ?
1182
- ORDER BY created_at_ms DESC, id DESC
1183
- LIMIT ? OFFSET ?
1184
- `).all(range.startMs, range.endMs, pageSize, offset);
1185
- const total = numberFromRow(totalRow, "total");
1186
- return {
1187
- items: rows.map((row) => usageEventFromRow(row)),
1188
- page,
1189
- page_size: pageSize,
1190
- period: input.period,
1191
- range: {
1192
- end_ms: range.endMs,
1193
- end_utc: new Date(range.endMs).toISOString(),
1194
- start_ms: range.startMs,
1195
- start_utc: new Date(range.startMs).toISOString()
1196
- },
1197
- total,
1198
- total_pages: Math.max(1, Math.ceil(total / pageSize))
1274
+ const pattern4 = withoutDate.match(/^claude-(\w+)-(\d+)$/);
1275
+ if (pattern4) return {
1276
+ family: pattern4[1],
1277
+ version: pattern4[2]
1199
1278
  };
1200
- }
1201
- async function getTokenUsageSessionsPage(input) {
1202
- const page = Math.max(1, Math.floor(input.page));
1203
- const pageSize = Math.min(100, Math.max(1, Math.floor(input.pageSize)));
1204
- if (!isTokenUsageStorageEnabled()) return {
1205
- ...createEmptyEventsPage(input),
1206
- items: []
1279
+ const pattern5 = withoutDate.match(/^claude-(\d+)-(\w+)$/);
1280
+ if (pattern5) return {
1281
+ family: pattern5[2],
1282
+ version: pattern5[1]
1207
1283
  };
1208
- await flushTokenUsageEvents();
1209
- const db = await getDb();
1210
- const range = getPeriodRangeFromDb(db, input.period);
1211
- const total = numberFromRow(db.prepare(`
1212
- SELECT COUNT(DISTINCT session_id) AS total
1213
- FROM token_usage_events
1214
- WHERE created_at_ms >= ? AND created_at_ms < ?
1215
- `).get(range.startMs, range.endMs), "total");
1284
+ };
1285
+ //#endregion
1286
+ //#region src/lib/provider-model.ts
1287
+ const parseProviderModelAlias = (model) => {
1288
+ const separatorIndex = model.indexOf("/");
1289
+ if (separatorIndex <= 0 || separatorIndex === model.length - 1) return null;
1290
+ const provider = model.slice(0, separatorIndex).trim();
1291
+ const providerModel = model.slice(separatorIndex + 1).trim();
1292
+ if (!provider || !providerModel) return null;
1216
1293
  return {
1217
- items: getSessionSummaries(db, range, pageSize, (page - 1) * pageSize),
1218
- page,
1219
- page_size: pageSize,
1220
- period: input.period,
1221
- range: rangePayload(range),
1222
- total,
1223
- total_pages: Math.max(1, Math.ceil(total / pageSize))
1294
+ model: providerModel,
1295
+ provider
1224
1296
  };
1297
+ };
1298
+ const createFallbackModel = (modelId) => ({
1299
+ capabilities: {
1300
+ family: "provider",
1301
+ limits: {},
1302
+ object: "model_capabilities",
1303
+ supports: {},
1304
+ tokenizer: "o200k_base",
1305
+ type: "chat"
1306
+ },
1307
+ id: modelId,
1308
+ model_picker_enabled: false,
1309
+ name: modelId,
1310
+ object: "model",
1311
+ preview: false,
1312
+ vendor: "provider",
1313
+ version: "unknown"
1314
+ });
1315
+ //#endregion
1316
+ //#region src/lib/provider-resolver.ts
1317
+ const AZURE_OPENAI_SCOPE = "https://cognitiveservices.azure.com/.default";
1318
+ let cachedAzureAccessTokenProvider = null;
1319
+ /**
1320
+ * Lazily creates the Entra token provider on first use and reuses it
1321
+ * afterwards. Importing @azure/identity eagerly adds ~0.5s to every process
1322
+ * startup, and constructing a new DefaultAzureCredential per call would drop
1323
+ * the token cache, so both the import and the credential are singletons.
1324
+ */
1325
+ async function getAzureAccessTokenProvider() {
1326
+ cachedAzureAccessTokenProvider ??= (async () => {
1327
+ try {
1328
+ const { DefaultAzureCredential, getBearerTokenProvider } = await import("@azure/identity");
1329
+ return getBearerTokenProvider(new DefaultAzureCredential(), AZURE_OPENAI_SCOPE);
1330
+ } catch (error) {
1331
+ cachedAzureAccessTokenProvider = null;
1332
+ throw error;
1333
+ }
1334
+ })();
1335
+ return cachedAzureAccessTokenProvider;
1225
1336
  }
1226
- async function closeUsageStore() {
1227
- await flushTokenUsageEvents();
1228
- await tokenUsageDbStore.close({ beforeClose: (db) => {
1337
+ async function getDefaultAzureAccessToken() {
1338
+ return (await getAzureAccessTokenProvider())();
1339
+ }
1340
+ function isMissingCodexCredentialsError(error) {
1341
+ return error instanceof Error && error.message === "Codex credentials not found. Run `copilot-api auth login --provider codex` first.";
1342
+ }
1343
+ async function resolveProviderConfig(providerName, getAzureAccessToken = getDefaultAzureAccessToken) {
1344
+ const normalizedProviderName = providerName.trim();
1345
+ if (!normalizedProviderName) return null;
1346
+ if (normalizedProviderName === "codex") {
1347
+ if (getRawProviderConfig(normalizedProviderName)?.enabled === false) return null;
1229
1348
  try {
1230
- db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
1231
- } catch {}
1232
- } });
1233
- writeQueue = Promise.resolve();
1349
+ await setupCodexToken();
1350
+ } catch (error) {
1351
+ if (isMissingCodexCredentialsError(error)) return null;
1352
+ throw error;
1353
+ }
1354
+ const providerConfig = getProviderConfig(normalizedProviderName);
1355
+ if (!providerConfig) return null;
1356
+ return {
1357
+ ...providerConfig,
1358
+ apiKey: state.codexAccessToken ?? providerConfig.apiKey
1359
+ };
1360
+ }
1361
+ const providerConfig = getProviderConfig(normalizedProviderName);
1362
+ if (providerConfig?.name === "xai" && providerConfig.authType === "oauth2") {
1363
+ const accessToken = await getXaiAccessToken();
1364
+ return accessToken ? {
1365
+ ...providerConfig,
1366
+ baseUrl: providerConfig.baseUrl === "https://api.x.ai" ? XAI_API_BASE_URL : providerConfig.baseUrl,
1367
+ apiKey: accessToken
1368
+ } : null;
1369
+ }
1370
+ if (providerConfig?.authType !== "azure-entra") return providerConfig;
1371
+ try {
1372
+ return {
1373
+ ...providerConfig,
1374
+ apiKey: await getAzureAccessToken()
1375
+ };
1376
+ } catch (error) {
1377
+ throw new Error(`Failed to acquire Azure Entra token for provider '${normalizedProviderName}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1378
+ }
1379
+ }
1380
+ /**
1381
+ * Returns the parsed "provider/model" alias only when that provider is
1382
+ * actually configured, otherwise null so the caller falls through to its
1383
+ * default flow. GitHub Copilot enterprise models can ship with namespaced ids
1384
+ * such as "org/family/model": without this check the first segment is
1385
+ * misrouted to a non-existent provider and surfaced as a 400/404.
1386
+ */
1387
+ async function ensureConfiguredProviderModelAlias(alias, resolveConfig = resolveProviderConfig) {
1388
+ if (!alias) return null;
1389
+ return await resolveConfig(alias.provider) ? alias : null;
1390
+ }
1391
+ /** parseProviderModelAlias + ensureConfiguredProviderModelAlias. */
1392
+ async function resolveConfiguredProviderModelAlias(model, resolveConfig = resolveProviderConfig) {
1393
+ return ensureConfiguredProviderModelAlias(parseProviderModelAlias(model), resolveConfig);
1234
1394
  }
1235
- registerProcessCleanup(closeUsageStore);
1236
1395
  //#endregion
1237
1396
  //#region src/lib/token-usage/pricing.ts
1238
1397
  const COST_NANOS_PER_UNIT = 1e9;
@@ -1385,6 +1544,7 @@ function costNanosForTokens(tokens, pricePerMillionTokens) {
1385
1544
  //#endregion
1386
1545
  //#region src/lib/token-usage/index.ts
1387
1546
  const tokenUsageEventBus = new EventBus();
1547
+ const USER_AGENT_MAX_LENGTH = 64;
1388
1548
  function resolveTraceId(traceId) {
1389
1549
  return traceId?.trim() || requestContext.getStore()?.traceId || generateTraceId();
1390
1550
  }
@@ -1398,11 +1558,16 @@ function resolveUserId(input) {
1398
1558
  function toPersistedEvent(input) {
1399
1559
  if (!hasAnyToken(input)) return null;
1400
1560
  const now = /* @__PURE__ */ new Date();
1561
+ const timing = requestContext.getStore()?.requestTiming;
1401
1562
  const cost = resolveTokenUsageCost({
1402
1563
  ...input,
1403
1564
  at: now
1404
1565
  });
1405
1566
  return {
1567
+ request_id: timing?.id ?? null,
1568
+ first_token_ms: timing?.firstTokenMs ?? null,
1569
+ duration_ms: timing?.durationMs ?? null,
1570
+ request_outcome: timing?.outcome ?? null,
1406
1571
  cache_creation_input_tokens: normalizeToken(input.cache_creation_input_tokens),
1407
1572
  cache_read_input_tokens: normalizeToken(input.cache_read_input_tokens),
1408
1573
  cost_currency: cost?.currency ?? null,
@@ -1420,6 +1585,7 @@ function toPersistedEvent(input) {
1420
1585
  total_cost_nanos: cost?.total_cost_nanos ?? null,
1421
1586
  total_tokens: resolveTotalTokens(input),
1422
1587
  trace_id: resolveTraceId(input.traceId),
1588
+ user_agent: (requestContext.getStore()?.userAgent ?? "").slice(0, USER_AGENT_MAX_LENGTH),
1423
1589
  user_id: resolveUserId(input)
1424
1590
  };
1425
1591
  }
@@ -1427,9 +1593,15 @@ tokenUsageEventBus.subscribe("token_usage.recorded", enqueueTokenUsageWrite);
1427
1593
  function recordTokenUsageEvent(input) {
1428
1594
  const event = toPersistedEvent(input);
1429
1595
  if (!event) return;
1596
+ const observer = requestContext.getStore()?.pluginRequest;
1597
+ const timing = requestContext.getStore()?.requestTiming;
1598
+ if (timing) timing.hasUsage = true;
1599
+ observer?.setSessionId(event.session_id);
1600
+ observer?.recordUsage(event);
1430
1601
  tokenUsageEventBus.publish("token_usage.recorded", event);
1431
1602
  }
1432
1603
  function createTokenUsageRecorder(options) {
1604
+ requestContext.getStore()?.pluginRequest?.setSessionId(resolveTokenUsageSessionId(options.sessionId, options.fallbackSessionId));
1433
1605
  return (usage) => {
1434
1606
  recordTokenUsageEvent({
1435
1607
  ...usage,
@@ -1805,6 +1977,7 @@ const createResponses = async (payload, { vision, initiator, subagentMarker, req
1805
1977
  payload.service_tier = void 0;
1806
1978
  consola.log(`<-- model: ${payload.model}`);
1807
1979
  const effectiveTransport = compactType === 1 ? "http" : transport;
1980
+ setPluginRequestActualModel(payload.model);
1808
1981
  if (payload.stream === true && effectiveTransport === "websocket") {
1809
1982
  clientSignal?.throwIfAborted();
1810
1983
  const websocketRequest = prepareResponsesWebSocketRequest(payload, headers, {
@@ -2058,6 +2231,7 @@ async function forwardProviderMessages(providerConfig, payload, requestHeaders,
2058
2231
  const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
2059
2232
  applyOpencodeSessionHeader(providerConfig, headers, resolveOpencodeMessagesSession(payload));
2060
2233
  const transportConfig = getUpstreamTransportConfig();
2234
+ setPluginRequestActualModel(payload.model);
2061
2235
  return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "messages"), {
2062
2236
  method: "POST",
2063
2237
  headers,
@@ -2073,6 +2247,7 @@ async function forwardProviderChatCompletions(providerConfig, payload, requestHe
2073
2247
  const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
2074
2248
  applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
2075
2249
  const transportConfig = getUpstreamTransportConfig();
2250
+ setPluginRequestActualModel(payload.model);
2076
2251
  return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "chat/completions"), {
2077
2252
  method: "POST",
2078
2253
  headers,
@@ -2089,6 +2264,7 @@ async function forwardProviderResponses(providerConfig, payload, requestHeaders,
2089
2264
  const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
2090
2265
  applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
2091
2266
  await applyXaiGrokBuildHeaders(providerConfig, headers, payload);
2267
+ setPluginRequestActualModel(payload.model);
2092
2268
  return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "responses"), {
2093
2269
  method: "POST",
2094
2270
  headers,
@@ -2713,8 +2889,9 @@ function parseDebugBody(body) {
2713
2889
  return body;
2714
2890
  }
2715
2891
  }
2716
- async function forwardCodexAlphaSearchRequest(request) {
2892
+ async function forwardCodexAlphaSearchRequest(request, model) {
2717
2893
  await debugJsonAsync(logger$18, "alpha_search.codex.request", async () => ({ body: parseDebugBody(await request.clone().text()) }));
2894
+ if (model) setPluginRequestActualModel(model);
2718
2895
  const upstreamResponse = await alphaSearchRouteDependencies.forwardCodexAlphaSearch(request);
2719
2896
  await debugJsonAsync(logger$18, "alpha_search.codex.response", async () => ({
2720
2897
  body: parseDebugBody(await upstreamResponse.clone().text()),
@@ -2730,12 +2907,12 @@ function createAlphaSearchRequest(request, payload) {
2730
2907
  signal: request.signal
2731
2908
  });
2732
2909
  }
2733
- async function handleCodexRequest(c, request, resolvedProviderConfig) {
2910
+ async function handleCodexRequest(c, request, resolvedProviderConfig, model) {
2734
2911
  if (!(resolvedProviderConfig ?? await alphaSearchRouteDependencies.resolveProviderConfig("codex"))) return c.json({ error: {
2735
2912
  message: "Provider 'codex' not found or disabled",
2736
2913
  type: "invalid_request_error"
2737
2914
  } }, 404);
2738
- return await forwardCodexAlphaSearchRequest(request);
2915
+ return await forwardCodexAlphaSearchRequest(request, model);
2739
2916
  }
2740
2917
  function invalidRequest$1(c, message) {
2741
2918
  return c.json({ error: {
@@ -2750,7 +2927,9 @@ async function parseAlphaSearchBody(c) {
2750
2927
  } catch {
2751
2928
  return invalidRequest$1(c, "Invalid alpha search request: expected JSON body");
2752
2929
  }
2753
- if (typeof body?.model !== "string") return invalidRequest$1(c, "Invalid alpha search request: model must be a string");
2930
+ const model = body?.model;
2931
+ if (typeof model !== "string") return invalidRequest$1(c, "Invalid alpha search request: model must be a string");
2932
+ setPluginRequestModel(model);
2754
2933
  return body;
2755
2934
  }
2756
2935
  /**
@@ -2769,12 +2948,12 @@ async function handleAlphaSearchRequest(c, resolvedProviderConfig) {
2769
2948
  let providerModelAlias = parseProviderModelAlias(payload.model);
2770
2949
  if (providerModelAlias) {
2771
2950
  payload.model = providerModelAlias.model;
2772
- if (providerModelAlias.provider === "codex") return await handleCodexRequest(c, createAlphaSearchRequest(c.req.raw, payload));
2951
+ if (providerModelAlias.provider === "codex") return await handleCodexRequest(c, createAlphaSearchRequest(c.req.raw, payload), void 0, payload.model);
2773
2952
  }
2774
2953
  if (alphaSearchRouteDependencies.isAlphaSearchCodexPriorityEnabled()) {
2775
2954
  if (await alphaSearchRouteDependencies.resolveProviderConfig("codex")) {
2776
2955
  if (!payload.model.startsWith("gpt")) payload.model = "gpt-5.6-luna";
2777
- return await forwardCodexAlphaSearchRequest(createAlphaSearchRequest(c.req.raw, payload));
2956
+ return await forwardCodexAlphaSearchRequest(createAlphaSearchRequest(c.req.raw, payload), payload.model);
2778
2957
  }
2779
2958
  }
2780
2959
  const messagesBackedModel = await isMessagesBackedModel(resolvedRequestedModel);
@@ -2788,7 +2967,7 @@ async function handleAlphaSearchRequest(c, resolvedProviderConfig) {
2788
2967
  providerModelAlias = parseProviderModelAlias(payload.model);
2789
2968
  if (providerModelAlias) {
2790
2969
  payload.model = providerModelAlias.model;
2791
- if (providerModelAlias.provider === "codex") return await handleCodexRequest(c, createAlphaSearchRequest(c.req.raw, payload));
2970
+ if (providerModelAlias.provider === "codex") return await handleCodexRequest(c, createAlphaSearchRequest(c.req.raw, payload), void 0, payload.model);
2792
2971
  }
2793
2972
  }
2794
2973
  const fallbackRequestedModel = messagesBackedModel ? payload.model : requestedModel;
@@ -2832,6 +3011,46 @@ alphaSearchRoutes.post("/", async (c) => {
2832
3011
  }
2833
3012
  });
2834
3013
  //#endregion
3014
+ //#region src/lib/first-token.ts
3015
+ const nonempty = (value) => typeof value === "string" && value.length > 0;
3016
+ function recordChatCompletionFirstToken(chunk) {
3017
+ const observer = requestContext.getStore()?.requestTiming ?? requestContext.getStore()?.pluginRequest;
3018
+ if (!observer || !chunk) return;
3019
+ if (chunk.choices?.some(({ delta }) => delta && (nonempty(delta.content) || nonempty(delta.reasoning_text) || nonempty(delta.reasoning_content) || nonempty(delta.reasoning) || delta.tool_calls?.some((tool) => nonempty(tool.function?.arguments))))) observer.recordFirstToken();
3020
+ }
3021
+ function recordResponsesFirstToken(event) {
3022
+ const observer = requestContext.getStore()?.requestTiming ?? requestContext.getStore()?.pluginRequest;
3023
+ if (!observer || !event) return;
3024
+ switch (event.type) {
3025
+ case "response.output_item.added":
3026
+ if (event.item?.type === "reasoning") observer.recordFirstToken();
3027
+ break;
3028
+ case "response.reasoning_summary_part.added":
3029
+ observer.recordFirstToken();
3030
+ break;
3031
+ case "response.output_text.delta":
3032
+ case "response.reasoning_summary_text.delta":
3033
+ case "response.function_call_arguments.delta":
3034
+ case "response.custom_tool_call_input.delta": if (nonempty(event.delta)) observer.recordFirstToken();
3035
+ }
3036
+ }
3037
+ function recordAnthropicFirstToken(event) {
3038
+ const observer = requestContext.getStore()?.requestTiming ?? requestContext.getStore()?.pluginRequest;
3039
+ if (!observer || !event) return;
3040
+ if (event.type === "content_block_delta") {
3041
+ const { delta } = event;
3042
+ if (delta.type === "text_delta" && nonempty(delta.text) || delta.type === "thinking_delta" && nonempty(delta.thinking) || delta.type === "input_json_delta" && nonempty(delta.partial_json)) observer.recordFirstToken();
3043
+ } else if (event.type === "content_block_start") {
3044
+ const block = event.content_block;
3045
+ if (block.type === "text" && nonempty(block.text) || block.type === "thinking" || (block.type === "tool_use" || block.type === "server_tool_use") && block.input && Object.keys(block.input).length > 0) observer.recordFirstToken();
3046
+ }
3047
+ }
3048
+ function recordGoogleFirstToken(response) {
3049
+ const observer = requestContext.getStore()?.requestTiming ?? requestContext.getStore()?.pluginRequest;
3050
+ if (!observer) return;
3051
+ if (response.candidates?.some((candidate) => candidate.content?.parts.some((part) => part.thought === true || nonempty(part.text) || nonempty(part.functionCall?.name)))) observer.recordFirstToken();
3052
+ }
3053
+ //#endregion
2835
3054
  //#region src/lib/sse.ts
2836
3055
  async function writeSSEIfConnected(stream, message) {
2837
3056
  if (stream.aborted) return false;
@@ -3039,6 +3258,7 @@ const streamProviderChatCompletions = (c, upstreamResponse, options) => {
3039
3258
  debugJson(logger$17, "provider.chat_completions.stream_chunk", chunk);
3040
3259
  if (chunk.data && chunk.data !== "[DONE]") {
3041
3260
  const parsedChunk = parseChatCompletionChunkData(chunk.data);
3261
+ recordChatCompletionFirstToken(parsedChunk);
3042
3262
  if (parsedChunk?.usage) usage = normalizeOpenAIUsage(parsedChunk.usage);
3043
3263
  }
3044
3264
  await writeSSEIfConnected(stream, {
@@ -3076,6 +3296,7 @@ const createChatCompletions = async (payload, options) => {
3076
3296
  prepareForCompact(headers, options.compactType);
3077
3297
  consola.log(`<-- model: ${payload.model}`);
3078
3298
  const transportConfig = getUpstreamTransportConfig();
3299
+ setPluginRequestActualModel(payload.model);
3079
3300
  const response = await fetchUpstreamWithLifecycle(`${copilotBaseUrl(state)}/chat/completions`, {
3080
3301
  method: "POST",
3081
3302
  headers,
@@ -3098,6 +3319,7 @@ const createChatCompletions = async (payload, options) => {
3098
3319
  const logger$16 = createHandlerLogger("chat-completions-handler");
3099
3320
  async function handleCompletion$1(c) {
3100
3321
  let payload = await c.req.json();
3322
+ setPluginRequestModel(payload.model);
3101
3323
  const requestedModel = payload.model;
3102
3324
  payload.model = resolveMappedModel(payload.model);
3103
3325
  if (payload.model !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${payload.model}`);
@@ -3151,6 +3373,7 @@ async function handleCompletion$1(c) {
3151
3373
  for await (const chunk of response) {
3152
3374
  debugJson(logger$16, "Streaming chunk:", chunk);
3153
3375
  const parsedChunk = parseChatCompletionChunk(chunk);
3376
+ recordChatCompletionFirstToken(parsedChunk);
3154
3377
  if (parsedChunk?.usage || parsedChunk?.copilot_usage) usage = {
3155
3378
  ...normalizeOpenAIUsage(parsedChunk.usage),
3156
3379
  total_nano_aiu: normalizeOptionalToken(parsedChunk.copilot_usage?.total_nano_aiu)
@@ -8627,6 +8850,7 @@ async function forwardGoogleGenerateContent(provider, model, payload, requestHea
8627
8850
  applyOpencodeSessionHeader(provider, headers, options.sessionId);
8628
8851
  headers.accept = options.stream ? "text/event-stream" : "application/json";
8629
8852
  const transport = getUpstreamTransportConfig();
8853
+ setPluginRequestActualModel(model.replace(/^models\//u, ""));
8630
8854
  return await fetchUpstreamWithLifecycle(url, {
8631
8855
  method: "POST",
8632
8856
  headers,
@@ -8689,6 +8913,7 @@ async function handleGoogleProviderMessages(c, payload, provider, usageEndpoint)
8689
8913
  if (response.headers.get("content-type")?.includes("text/event-stream")) for await (const event of events(response, signal)) {
8690
8914
  if (!event.data || event.data === "[DONE]") continue;
8691
8915
  const response = JSON.parse(event.data);
8916
+ recordGoogleFirstToken(response);
8692
8917
  debugJson(logger$13, "provider.messages.google.raw_stream_event:", response);
8693
8918
  await writeEvents(translator.push(response));
8694
8919
  }
@@ -8718,6 +8943,7 @@ const providerMessagesHandlerDependencies = { resolveProviderConfig };
8718
8943
  async function handleProviderMessages(c) {
8719
8944
  const provider = c.req.param("provider");
8720
8945
  const payload = await c.req.json();
8946
+ setPluginRequestModel(payload.model);
8721
8947
  payload.model = fromClaudeDiscoveryModelId(payload.model);
8722
8948
  applyClaudeNoToolsEffort(payload, c.req.header("user-agent"));
8723
8949
  const claudeAutoModel = getClaudeAutoModel();
@@ -9160,6 +9386,7 @@ const streamOpenAICompatibleProviderMessages = ({ c, modelConfig, payload, prici
9160
9386
  continue;
9161
9387
  }
9162
9388
  const parsed = parseOpenAICompatibleStreamChunk(chunk.data);
9389
+ recordChatCompletionFirstToken(parsed);
9163
9390
  if (!parsed) continue;
9164
9391
  if (parsed.usage) usage = normalizeOpenAIUsage(parsed.usage);
9165
9392
  const events = translateChunkToAnthropicEvents(parsed, streamState);
@@ -9217,6 +9444,7 @@ const streamResponsesProviderMessages = ({ c, modelConfig, payload, pricingCurre
9217
9444
  continue;
9218
9445
  }
9219
9446
  const parsed = parseResponsesProviderStreamChunk(chunk.data, providerConfig);
9447
+ recordResponsesFirstToken(parsed);
9220
9448
  if (!parsed) continue;
9221
9449
  if (parsed.type === "response.completed" || parsed.type === "response.failed" || parsed.type === "response.incomplete") usage = normalizeResponsesUsage(parsed.response.usage);
9222
9450
  const events = translateResponsesStreamEvent(parsed, streamState);
@@ -9267,6 +9495,7 @@ const parseResponsesProviderStreamChunk = (data, providerConfig) => {
9267
9495
  const parseProviderStreamEvent = (data) => {
9268
9496
  try {
9269
9497
  const parsed = JSON.parse(data);
9498
+ recordAnthropicFirstToken(parsed);
9270
9499
  if (parsed.type === "message_start") return {
9271
9500
  data: JSON.stringify(parsed),
9272
9501
  model: parsed.message.model,
@@ -9433,6 +9662,7 @@ const createMessages = async (payload, anthropicBetaHeader, options) => {
9433
9662
  if (anthropicBeta) headers["anthropic-beta"] = anthropicBeta;
9434
9663
  consola.log(`<-- model: ${payload.model}`);
9435
9664
  const transportConfig = getUpstreamTransportConfig();
9665
+ setPluginRequestActualModel(payload.model);
9436
9666
  const response = await fetchUpstreamWithLifecycle(`${copilotBaseUrl(state)}/v1/messages`, {
9437
9667
  method: "POST",
9438
9668
  headers,
@@ -9512,6 +9742,7 @@ const handleWithChatCompletions = async (c, anthropicPayload, options) => {
9512
9742
  if (rawEvent.data === "[DONE]") break;
9513
9743
  if (!rawEvent.data) continue;
9514
9744
  const chunk = JSON.parse(rawEvent.data);
9745
+ recordChatCompletionFirstToken(chunk);
9515
9746
  if (chunk.usage || chunk.copilot_usage) usage = {
9516
9747
  ...normalizeOpenAIUsage(chunk.usage),
9517
9748
  total_nano_aiu: normalizeOptionalToken(chunk.copilot_usage?.total_nano_aiu)
@@ -9585,6 +9816,7 @@ const handleWithResponsesApi = async (c, anthropicPayload, options) => {
9585
9816
  if (!data) continue;
9586
9817
  debugLazy(logger, () => ["Responses raw stream event:", data]);
9587
9818
  const responseEvent = JSON.parse(data);
9819
+ recordResponsesFirstToken(responseEvent);
9588
9820
  if (responseEvent.type === "response.completed" || responseEvent.type === "response.failed" || responseEvent.type === "response.incomplete") usage = {
9589
9821
  ...normalizeResponsesUsage(responseEvent.response.usage),
9590
9822
  total_nano_aiu: normalizeOptionalToken(responseEvent.copilot_usage?.total_nano_aiu)
@@ -9656,6 +9888,7 @@ const handleWithMessagesApi = async (c, anthropicPayload, options) => {
9656
9888
  if (!data) continue;
9657
9889
  debugLazy(logger, () => ["Messages raw stream event:", data]);
9658
9890
  const parsedEvent = parseAnthropicStreamEvent(data);
9891
+ recordAnthropicFirstToken(parsedEvent);
9659
9892
  if (parsedEvent?.type === "message_start") usage = mergeAnthropicUsage(usage, {
9660
9893
  ...normalizeAnthropicUsage(parsedEvent.message.usage),
9661
9894
  ...normalizeCopilotUsage(parsedEvent.message.copilot_usage)
@@ -9787,6 +10020,7 @@ const messagesFlowHandlers = {
9787
10020
  };
9788
10021
  async function handleCompletion(c) {
9789
10022
  const anthropicPayload = await c.req.json();
10023
+ setPluginRequestModel(anthropicPayload.model);
9790
10024
  anthropicPayload.model = fromClaudeDiscoveryModelId(anthropicPayload.model);
9791
10025
  applyClaudeNoToolsEffort(anthropicPayload, c.req.header("user-agent"));
9792
10026
  return await handleCompletionPayload(c, anthropicPayload);
@@ -13452,6 +13686,7 @@ const streamProviderResponses = async (c, upstreamResponse, options) => {
13452
13686
  event: event.type
13453
13687
  };
13454
13688
  }
13689
+ recordResponsesFirstToken(event);
13455
13690
  if (event) {
13456
13691
  const nextUsage = getResponsesStreamEventUsage(event);
13457
13692
  if (nextUsage) usage = nextUsage;
@@ -13496,8 +13731,10 @@ providerResponsesRoutes.use(zstdDecompressionMiddleware);
13496
13731
  providerResponsesRoutes.post("/", async (c) => {
13497
13732
  try {
13498
13733
  const provider = c.req.param("provider") ?? "";
13734
+ const payload = await c.req.json();
13735
+ setPluginRequestModel(payload.model);
13499
13736
  return await handleProviderResponsesForProvider(c, {
13500
- payload: await c.req.json(),
13737
+ payload,
13501
13738
  provider
13502
13739
  });
13503
13740
  } catch (error) {
@@ -13551,6 +13788,7 @@ const responsesHandlerDependencies = {
13551
13788
  };
13552
13789
  const handleResponses = async (c) => {
13553
13790
  const payload = await c.req.json();
13791
+ setPluginRequestModel(payload.model);
13554
13792
  const requestedModel = payload.model;
13555
13793
  payload.model = responsesHandlerDependencies.resolveMappedModel(payload.model);
13556
13794
  if (payload.model !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${payload.model}`);
@@ -13644,6 +13882,7 @@ const handleResponses = async (c) => {
13644
13882
  for await (const chunk of { [Symbol.asyncIterator]: () => iterator }) {
13645
13883
  debugJson(logger$2, "Responses stream chunk:", chunk);
13646
13884
  const parsedEvent = parseResponsesStreamEvent(chunk);
13885
+ recordResponsesFirstToken(parsedEvent);
13647
13886
  if (parsedEvent?.type === "response.completed" || parsedEvent?.type === "response.failed" || parsedEvent?.type === "response.incomplete") usage = {
13648
13887
  ...normalizeResponsesUsage(parsedEvent.response.usage),
13649
13888
  total_nano_aiu: normalizeOptionalToken(parsedEvent.copilot_usage?.total_nano_aiu)
@@ -13927,6 +14166,7 @@ function createServer(options = {}) {
13927
14166
  allowUnauthenticatedPaths: [],
13928
14167
  allowWhenNoApiKeys: false
13929
14168
  }));
14169
+ server.use("*", requestEventsMiddleware(options.plugins, options.port ?? 4141));
13930
14170
  server.get("/", (c) => c.text("Server running"));
13931
14171
  server.get("/usage-viewer", (c) => {
13932
14172
  const usageViewerFileUrl = new URL("../pages/index.html", import.meta.url);
@@ -13966,4 +14206,4 @@ createServer();
13966
14206
  //#endregion
13967
14207
  export { createServer };
13968
14208
 
13969
- //# sourceMappingURL=server-BogSoFE5.js.map
14209
+ //# sourceMappingURL=server-CEF4Sh-g.js.map