@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.
- package/dist/auth-i0XN57BZ.js +2 -0
- package/dist/{auth-BnQ_KOxT.js → auth-jHSWJs1b.js} +3 -3
- package/dist/{auth-BnQ_KOxT.js.map → auth-jHSWJs1b.js.map} +1 -1
- package/dist/{config-D8pUyhJp.js → config-BzhriV_y.js} +1 -11
- package/dist/config-BzhriV_y.js.map +1 -0
- package/dist/{copilot-runtime-RW0tbgv3.js → copilot-runtime-8IpywGxw.js} +77 -3
- package/dist/copilot-runtime-8IpywGxw.js.map +1 -0
- package/dist/{debug-BXvLeaet.js → debug-q7d6HJLV.js} +2 -2
- package/dist/{debug-BXvLeaet.js.map → debug-q7d6HJLV.js.map} +1 -1
- package/dist/main.js +3 -3
- package/dist/{server-BogSoFE5.js → server-CEF4Sh-g.js} +947 -707
- package/dist/server-CEF4Sh-g.js.map +1 -0
- package/dist/{start-CH9AVcLK.js → start-Bawh7LU_.js} +141 -8
- package/dist/start-Bawh7LU_.js.map +1 -0
- package/dist/{token-BL6FigKp.js → token-XNKmjmqH.js} +7 -3
- package/dist/token-XNKmjmqH.js.map +1 -0
- package/package.json +1 -1
- package/pages/index.html +24 -4
- package/dist/auth-CmJpA6m3.js +0 -2
- package/dist/config-D8pUyhJp.js.map +0 -1
- package/dist/copilot-runtime-RW0tbgv3.js.map +0 -1
- package/dist/server-BogSoFE5.js.map +0 -1
- package/dist/start-CH9AVcLK.js.map +0 -1
- package/dist/token-BL6FigKp.js.map +0 -1
|
@@ -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-
|
|
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
|
|
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-
|
|
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/
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
104
|
+
getDb() {
|
|
105
|
+
this.dbPromise ??= this.open();
|
|
106
|
+
return this.dbPromise;
|
|
201
107
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
388
|
-
return (
|
|
138
|
+
function isTokenUsageStorageEnabled() {
|
|
139
|
+
return isSqliteRuntimeSupported();
|
|
389
140
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
return ensureConfiguredProviderModelAlias(parseProviderModelAlias(model), resolveConfig);
|
|
211
|
+
function normalizeOptionalToken(value) {
|
|
212
|
+
return value === null || value === void 0 ? void 0 : normalizeToken(value);
|
|
444
213
|
}
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
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
|
|
1227
|
-
await
|
|
1228
|
-
|
|
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
|
-
|
|
1231
|
-
} catch {
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
14209
|
+
//# sourceMappingURL=server-CEF4Sh-g.js.map
|