@chenpu17/cc-gw 0.2.1 → 0.2.3

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.
@@ -12,6 +12,14 @@ import fs from "fs";
12
12
  import path from "path";
13
13
  import os from "os";
14
14
  import { EventEmitter } from "events";
15
+ var LOG_LEVELS = /* @__PURE__ */ new Set([
16
+ "fatal",
17
+ "error",
18
+ "warn",
19
+ "info",
20
+ "debug",
21
+ "trace"
22
+ ]);
15
23
  var HOME_DIR = path.join(os.homedir(), ".cc-gw");
16
24
  var CONFIG_PATH = path.join(HOME_DIR, "config.json");
17
25
  var TypedEmitter = class extends EventEmitter {
@@ -66,6 +74,12 @@ function parseConfig(raw) {
66
74
  }
67
75
  data.modelRoutes = sanitized;
68
76
  }
77
+ if (typeof data.logLevel !== "string" || !LOG_LEVELS.has(data.logLevel)) {
78
+ data.logLevel = "info";
79
+ }
80
+ if (typeof data.requestLogging !== "boolean") {
81
+ data.requestLogging = true;
82
+ }
69
83
  return data;
70
84
  }
71
85
  function loadConfig() {
@@ -723,6 +737,8 @@ function buildConnector(config) {
723
737
  switch (config.type) {
724
738
  case "deepseek":
725
739
  return createDeepSeekConnector(config);
740
+ case "huawei":
741
+ return createOpenAIConnector(config);
726
742
  case "kimi":
727
743
  return createKimiConnector(config);
728
744
  case "anthropic":
@@ -754,20 +770,93 @@ import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants }
754
770
  import fs2 from "fs";
755
771
  import os2 from "os";
756
772
  import path2 from "path";
757
- import Database from "better-sqlite3";
773
+ import sqlite3 from "sqlite3";
758
774
  var HOME_DIR2 = path2.join(os2.homedir(), ".cc-gw");
759
775
  var DATA_DIR = path2.join(HOME_DIR2, "data");
760
776
  var DB_PATH = path2.join(DATA_DIR, "gateway.db");
761
- var db = null;
762
- function ensureSchema(instance) {
763
- instance.exec(`
764
- CREATE TABLE IF NOT EXISTS request_logs (
777
+ sqlite3.verbose();
778
+ var dbPromise = null;
779
+ var dbInstance = null;
780
+ function exec(db, sql) {
781
+ return new Promise((resolve, reject) => {
782
+ db.exec(sql, (error) => {
783
+ if (error) {
784
+ reject(error);
785
+ return;
786
+ }
787
+ resolve();
788
+ });
789
+ });
790
+ }
791
+ function run(db, sql, params = []) {
792
+ return new Promise((resolve, reject) => {
793
+ const handler = function(error) {
794
+ if (error) {
795
+ reject(error);
796
+ return;
797
+ }
798
+ resolve({ lastID: this.lastID, changes: this.changes });
799
+ };
800
+ if (Array.isArray(params)) {
801
+ db.run(sql, params, handler);
802
+ } else {
803
+ db.run(sql, params, handler);
804
+ }
805
+ });
806
+ }
807
+ function all(db, sql, params = []) {
808
+ return new Promise((resolve, reject) => {
809
+ const callback = (error, rows) => {
810
+ if (error) {
811
+ reject(error);
812
+ return;
813
+ }
814
+ resolve(rows);
815
+ };
816
+ if (Array.isArray(params)) {
817
+ db.all(sql, params, callback);
818
+ } else {
819
+ db.all(sql, params, callback);
820
+ }
821
+ });
822
+ }
823
+ function get(db, sql, params = []) {
824
+ return new Promise((resolve, reject) => {
825
+ const callback = (error, row) => {
826
+ if (error) {
827
+ reject(error);
828
+ return;
829
+ }
830
+ resolve(row);
831
+ };
832
+ if (Array.isArray(params)) {
833
+ db.get(sql, params, callback);
834
+ } else {
835
+ db.get(sql, params, callback);
836
+ }
837
+ });
838
+ }
839
+ async function columnExists(db, table, column) {
840
+ const rows = await all(db, `PRAGMA table_info(${table})`);
841
+ return rows.some((row) => row.name === column);
842
+ }
843
+ async function maybeAddColumn(db, table, column, definition) {
844
+ const exists = await columnExists(db, table, column);
845
+ if (!exists) {
846
+ await run(db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
847
+ }
848
+ }
849
+ async function ensureSchema(db) {
850
+ await exec(
851
+ db,
852
+ `CREATE TABLE IF NOT EXISTS request_logs (
765
853
  id INTEGER PRIMARY KEY AUTOINCREMENT,
766
854
  timestamp INTEGER NOT NULL,
767
855
  session_id TEXT,
768
856
  provider TEXT NOT NULL,
769
857
  model TEXT NOT NULL,
770
858
  client_model TEXT,
859
+ stream INTEGER,
771
860
  latency_ms INTEGER,
772
861
  status_code INTEGER,
773
862
  input_tokens INTEGER,
@@ -791,63 +880,51 @@ function ensureSchema(instance) {
791
880
  total_input_tokens INTEGER DEFAULT 0,
792
881
  total_output_tokens INTEGER DEFAULT 0,
793
882
  total_latency_ms INTEGER DEFAULT 0
794
- );
795
- `);
796
- }
797
- function getDb() {
798
- if (db)
799
- return db;
800
- fs2.mkdirSync(DATA_DIR, { recursive: true });
801
- db = new Database(DB_PATH);
802
- ensureSchema(db);
803
- ensureColumns(db);
804
- return db;
883
+ );`
884
+ );
885
+ await maybeAddColumn(db, "request_logs", "client_model", "TEXT");
886
+ await maybeAddColumn(db, "request_logs", "cached_tokens", "INTEGER");
887
+ await maybeAddColumn(db, "request_logs", "ttft_ms", "INTEGER");
888
+ await maybeAddColumn(db, "request_logs", "tpot_ms", "REAL");
889
+ await maybeAddColumn(db, "request_logs", "stream", "INTEGER");
805
890
  }
806
- function ensureColumns(instance) {
807
- const columns = instance.prepare("PRAGMA table_info(request_logs)").all();
808
- const hasCachedTokens = columns.some((column) => column.name === "cached_tokens");
809
- if (!hasCachedTokens) {
810
- instance.exec("ALTER TABLE request_logs ADD COLUMN cached_tokens INTEGER");
811
- }
812
- const hasClientModel = columns.some((column) => column.name === "client_model");
813
- if (!hasClientModel) {
814
- instance.exec("ALTER TABLE request_logs ADD COLUMN client_model TEXT");
815
- }
816
- const hasTtft = columns.some((column) => column.name === "ttft_ms");
817
- if (!hasTtft) {
818
- instance.exec("ALTER TABLE request_logs ADD COLUMN ttft_ms INTEGER");
819
- }
820
- const hasTpot = columns.some((column) => column.name === "tpot_ms");
821
- if (!hasTpot) {
822
- instance.exec("ALTER TABLE request_logs ADD COLUMN tpot_ms REAL");
891
+ async function getDb() {
892
+ if (dbInstance) {
893
+ return dbInstance;
894
+ }
895
+ if (!dbPromise) {
896
+ fs2.mkdirSync(DATA_DIR, { recursive: true });
897
+ dbPromise = new Promise((resolve, reject) => {
898
+ const instance = new sqlite3.Database(DB_PATH, (error) => {
899
+ if (error) {
900
+ reject(error);
901
+ return;
902
+ }
903
+ ensureSchema(instance).then(() => {
904
+ dbInstance = instance;
905
+ resolve(instance);
906
+ }).catch((schemaError) => {
907
+ instance.close(() => reject(schemaError));
908
+ });
909
+ });
910
+ });
823
911
  }
912
+ return dbPromise;
913
+ }
914
+ async function runQuery(sql, params = []) {
915
+ const db = await getDb();
916
+ return run(db, sql, params);
917
+ }
918
+ async function getOne(sql, params = []) {
919
+ const db = await getDb();
920
+ return get(db, sql, params);
921
+ }
922
+ async function getAll(sql, params = []) {
923
+ const db = await getDb();
924
+ return all(db, sql, params);
824
925
  }
825
926
 
826
927
  // logging/logger.ts
827
- function recordLog(entry) {
828
- const db2 = getDb();
829
- const stmt = db2.prepare(`
830
- INSERT INTO request_logs (
831
- timestamp, session_id, provider, model, client_model,
832
- latency_ms, status_code, input_tokens, output_tokens, cached_tokens, error
833
- ) VALUES (@timestamp, @sessionId, @provider, @model, @clientModel, @latencyMs, @statusCode, @inputTokens, @outputTokens, @cachedTokens, @error)
834
- `);
835
- const result = stmt.run({
836
- timestamp: entry.timestamp,
837
- sessionId: entry.sessionId ?? null,
838
- provider: entry.provider,
839
- model: entry.model,
840
- clientModel: entry.clientModel ?? null,
841
- latencyMs: entry.latencyMs ?? null,
842
- statusCode: entry.statusCode ?? null,
843
- inputTokens: entry.inputTokens ?? null,
844
- outputTokens: entry.outputTokens ?? null,
845
- cachedTokens: entry.cachedTokens ?? null,
846
- error: entry.error ?? null
847
- });
848
- const requestId = Number(result.lastInsertRowid);
849
- return requestId;
850
- }
851
928
  var BROTLI_OPTIONS = {
852
929
  params: {
853
930
  [zlibConstants.BROTLI_PARAM_QUALITY]: 1
@@ -874,16 +951,37 @@ function decompressPayload(value) {
874
951
  return "";
875
952
  }
876
953
  try {
877
- const decompressed = brotliDecompressSync(value);
878
- return decompressed.toString("utf8");
954
+ return brotliDecompressSync(value).toString("utf8");
879
955
  } catch {
880
956
  return value.toString("utf8");
881
957
  }
882
958
  }
883
959
  return null;
884
960
  }
885
- function updateLogTokens(requestId, values) {
886
- const db2 = getDb();
961
+ async function recordLog(entry) {
962
+ const result = await runQuery(
963
+ `INSERT INTO request_logs (
964
+ timestamp, session_id, provider, model, client_model, stream,
965
+ latency_ms, status_code, input_tokens, output_tokens, cached_tokens, error
966
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
967
+ [
968
+ entry.timestamp,
969
+ entry.sessionId ?? null,
970
+ entry.provider,
971
+ entry.model,
972
+ entry.clientModel ?? null,
973
+ entry.stream ? 1 : 0,
974
+ entry.latencyMs ?? null,
975
+ entry.statusCode ?? null,
976
+ entry.inputTokens ?? null,
977
+ entry.outputTokens ?? null,
978
+ entry.cachedTokens ?? null,
979
+ entry.error ?? null
980
+ ]
981
+ );
982
+ return Number(result.lastID);
983
+ }
984
+ async function updateLogTokens(requestId, values) {
887
985
  const setters = ["input_tokens = ?", "output_tokens = ?", "cached_tokens = ?"];
888
986
  const params = [
889
987
  values.inputTokens,
@@ -898,10 +996,10 @@ function updateLogTokens(requestId, values) {
898
996
  setters.push("tpot_ms = ?");
899
997
  params.push(values.tpotMs ?? null);
900
998
  }
901
- db2.prepare(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`).run(...params, requestId);
999
+ params.push(requestId);
1000
+ await runQuery(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`, params);
902
1001
  }
903
- function finalizeLog(requestId, info) {
904
- const db2 = getDb();
1002
+ async function finalizeLog(requestId, info) {
905
1003
  const setters = [];
906
1004
  const values = [];
907
1005
  if (info.latencyMs !== void 0) {
@@ -922,45 +1020,41 @@ function finalizeLog(requestId, info) {
922
1020
  }
923
1021
  if (setters.length === 0)
924
1022
  return;
925
- const stmt = db2.prepare(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`);
926
- stmt.run(...values, requestId);
1023
+ values.push(requestId);
1024
+ await runQuery(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`, values);
927
1025
  }
928
- function upsertLogPayload(requestId, payload) {
1026
+ async function upsertLogPayload(requestId, payload) {
929
1027
  if (payload.prompt === void 0 && payload.response === void 0) {
930
1028
  return;
931
1029
  }
932
- const db2 = getDb();
933
1030
  const promptData = payload.prompt === void 0 ? null : compressPayload(payload.prompt);
934
1031
  const responseData = payload.response === void 0 ? null : compressPayload(payload.response);
935
- db2.prepare(`
936
- INSERT INTO request_payloads (request_id, prompt, response)
937
- VALUES (?, ?, ?)
938
- ON CONFLICT(request_id) DO UPDATE SET
939
- prompt = COALESCE(excluded.prompt, request_payloads.prompt),
940
- response = COALESCE(excluded.response, request_payloads.response)
941
- `).run(
942
- requestId,
943
- promptData,
944
- responseData
1032
+ await runQuery(
1033
+ `INSERT INTO request_payloads (request_id, prompt, response)
1034
+ VALUES (?, ?, ?)
1035
+ ON CONFLICT(request_id) DO UPDATE SET
1036
+ prompt = COALESCE(excluded.prompt, request_payloads.prompt),
1037
+ response = COALESCE(excluded.response, request_payloads.response)`,
1038
+ [requestId, promptData, responseData]
945
1039
  );
946
1040
  }
947
- function updateMetrics(date, delta) {
948
- const db2 = getDb();
949
- db2.prepare(`
950
- INSERT INTO daily_metrics (date, request_count, total_input_tokens, total_output_tokens, total_latency_ms)
951
- VALUES (@date, @requests, @inputTokens, @outputTokens, @latencyMs)
952
- ON CONFLICT(date) DO UPDATE SET
953
- request_count = daily_metrics.request_count + excluded.request_count,
954
- total_input_tokens = daily_metrics.total_input_tokens + excluded.total_input_tokens,
955
- total_output_tokens = daily_metrics.total_output_tokens + excluded.total_output_tokens,
956
- total_latency_ms = daily_metrics.total_latency_ms + excluded.total_latency_ms
957
- `).run({
958
- date,
959
- requests: delta.requests,
960
- inputTokens: delta.inputTokens,
961
- outputTokens: delta.outputTokens,
962
- latencyMs: delta.latencyMs
963
- });
1041
+ async function updateMetrics(date, delta) {
1042
+ await runQuery(
1043
+ `INSERT INTO daily_metrics (date, request_count, total_input_tokens, total_output_tokens, total_latency_ms)
1044
+ VALUES (?, ?, ?, ?, ?)
1045
+ ON CONFLICT(date) DO UPDATE SET
1046
+ request_count = daily_metrics.request_count + excluded.request_count,
1047
+ total_input_tokens = daily_metrics.total_input_tokens + excluded.total_input_tokens,
1048
+ total_output_tokens = daily_metrics.total_output_tokens + excluded.total_output_tokens,
1049
+ total_latency_ms = daily_metrics.total_latency_ms + excluded.total_latency_ms`,
1050
+ [
1051
+ date,
1052
+ delta.requests,
1053
+ delta.inputTokens,
1054
+ delta.outputTokens,
1055
+ delta.latencyMs
1056
+ ]
1057
+ );
964
1058
  }
965
1059
 
966
1060
  // metrics/activity.ts
@@ -990,6 +1084,79 @@ function mapStopReason(reason) {
990
1084
  return reason ?? null;
991
1085
  }
992
1086
  }
1087
+ function stringifyToolContent(value) {
1088
+ if (value === null || value === void 0) {
1089
+ return "";
1090
+ }
1091
+ if (typeof value === "string") {
1092
+ return value;
1093
+ }
1094
+ try {
1095
+ return JSON.stringify(value, null, 2);
1096
+ } catch {
1097
+ return String(value);
1098
+ }
1099
+ }
1100
+ function mergeText(base, extraParts) {
1101
+ const parts = [];
1102
+ if (base && base.trim().length > 0) {
1103
+ parts.push(base);
1104
+ }
1105
+ for (const part of extraParts) {
1106
+ if (part && part.trim().length > 0) {
1107
+ parts.push(part);
1108
+ }
1109
+ }
1110
+ return parts.join("\n\n");
1111
+ }
1112
+ function stripTooling(payload) {
1113
+ const messages = payload.messages.map((message) => {
1114
+ if (message.role === "user") {
1115
+ const extras = (message.toolResults ?? []).map((result) => {
1116
+ const label = result.name || result.id;
1117
+ const content = stringifyToolContent(result.content);
1118
+ return label ? `${label}${content ? `
1119
+ ${content}` : ""}` : content;
1120
+ });
1121
+ return {
1122
+ role: message.role,
1123
+ text: mergeText(message.text, extras)
1124
+ };
1125
+ }
1126
+ if (message.role === "assistant") {
1127
+ const extras = (message.toolCalls ?? []).map((call) => {
1128
+ const label = call.name || call.id;
1129
+ const args = stringifyToolContent(call.arguments);
1130
+ return label ? `Requested tool ${label}${args ? `
1131
+ ${args}` : ""}` : args;
1132
+ });
1133
+ return {
1134
+ role: message.role,
1135
+ text: mergeText(message.text, extras)
1136
+ };
1137
+ }
1138
+ return {
1139
+ role: message.role,
1140
+ text: message.text
1141
+ };
1142
+ });
1143
+ return {
1144
+ ...payload,
1145
+ messages,
1146
+ tools: []
1147
+ };
1148
+ }
1149
+ function stripMetadata(payload) {
1150
+ const original = payload.original;
1151
+ if (!original || typeof original !== "object") {
1152
+ return payload;
1153
+ }
1154
+ const { metadata, ...rest } = original;
1155
+ return {
1156
+ ...payload,
1157
+ original: rest
1158
+ };
1159
+ }
993
1160
  var roundTwoDecimals = (value) => Math.round(value * 100) / 100;
994
1161
  function computeTpot(totalLatencyMs, outputTokens, options) {
995
1162
  if (!Number.isFinite(outputTokens) || outputTokens <= 0) {
@@ -1015,8 +1182,21 @@ function resolveCachedTokens(usage) {
1015
1182
  if (promptDetails && typeof promptDetails.cached_tokens === "number") {
1016
1183
  return promptDetails.cached_tokens;
1017
1184
  }
1185
+ if (typeof usage.cache_read_input_tokens === "number") {
1186
+ return usage.cache_read_input_tokens;
1187
+ }
1188
+ if (typeof usage.cache_creation_input_tokens === "number") {
1189
+ return usage.cache_creation_input_tokens;
1190
+ }
1018
1191
  return null;
1019
1192
  }
1193
+ function cloneOriginalPayload(value) {
1194
+ const structuredCloneFn = globalThis.structuredClone;
1195
+ if (structuredCloneFn) {
1196
+ return structuredCloneFn(value);
1197
+ }
1198
+ return JSON.parse(JSON.stringify(value));
1199
+ }
1020
1200
  function buildClaudeResponse(openAI, model) {
1021
1201
  const choice = openAI.choices?.[0];
1022
1202
  const message = choice?.message ?? {};
@@ -1061,6 +1241,16 @@ async function registerMessagesRoute(app) {
1061
1241
  reply.code(400);
1062
1242
  return { error: "Invalid request body" };
1063
1243
  }
1244
+ const rawUrl = typeof request.raw?.url === "string" ? request.raw.url : request.url ?? "";
1245
+ let querySuffix = null;
1246
+ if (typeof rawUrl === "string" && rawUrl.includes("?")) {
1247
+ querySuffix = rawUrl.slice(rawUrl.indexOf("?"));
1248
+ } else if (typeof request.querystring === "string" && request.querystring.length > 0) {
1249
+ querySuffix = `?${request.querystring}`;
1250
+ }
1251
+ if (querySuffix) {
1252
+ console.info(`[cc-gw] inbound url ${rawUrl} query ${querySuffix}`);
1253
+ }
1064
1254
  const normalized = normalizeClaudePayload(payload);
1065
1255
  const requestedModel = typeof payload.model === "string" ? payload.model : void 0;
1066
1256
  const target = resolveRoute({
@@ -1068,30 +1258,69 @@ async function registerMessagesRoute(app) {
1068
1258
  requestedModel
1069
1259
  });
1070
1260
  const providerType = target.provider.type ?? "custom";
1071
- const providerBody = providerType === "anthropic" ? buildAnthropicBody(normalized, {
1072
- maxTokens: payload.max_tokens ?? target.provider.models?.find((m) => m.id === target.modelId)?.maxTokens,
1073
- temperature: payload.temperature,
1074
- toolChoice: payload.tool_choice,
1075
- overrideTools: payload.tools
1076
- }) : buildProviderBody(normalized, {
1077
- maxTokens: payload.max_tokens ?? target.provider.models?.find((m) => m.id === target.modelId)?.maxTokens,
1078
- temperature: payload.temperature,
1079
- toolChoice: payload.tool_choice,
1080
- overrideTools: payload.tools
1081
- });
1261
+ const modelDefinition = target.provider.models?.find((m) => m.id === target.modelId);
1262
+ const supportsTools = modelDefinition?.capabilities?.tools === true;
1263
+ const supportsMetadata = providerType !== "custom";
1264
+ let normalizedForProvider = supportsTools ? normalized : stripTooling(normalized);
1265
+ if (!supportsMetadata) {
1266
+ normalizedForProvider = stripMetadata(normalizedForProvider);
1267
+ }
1268
+ const maxTokensOverride = payload.max_tokens ?? modelDefinition?.maxTokens;
1269
+ const toolChoice = supportsTools ? payload.tool_choice : void 0;
1270
+ const overrideTools = supportsTools ? payload.tools : void 0;
1271
+ let providerBody;
1272
+ let providerHeaders;
1273
+ if (providerType === "anthropic") {
1274
+ providerBody = cloneOriginalPayload(payload);
1275
+ providerBody.model = target.modelId;
1276
+ if (normalized.stream !== void 0) {
1277
+ providerBody.stream = normalized.stream;
1278
+ }
1279
+ const collected = {};
1280
+ const skip = /* @__PURE__ */ new Set(["content-length", "host", "connection", "transfer-encoding"]);
1281
+ const sourceHeaders = request.raw?.headers ?? request.headers;
1282
+ for (const [headerKey, headerValue] of Object.entries(sourceHeaders)) {
1283
+ const lower = headerKey.toLowerCase();
1284
+ if (skip.has(lower))
1285
+ continue;
1286
+ let value;
1287
+ if (typeof headerValue === "string") {
1288
+ value = headerValue;
1289
+ } else if (Array.isArray(headerValue)) {
1290
+ value = headerValue.find((item) => typeof item === "string" && item.length > 0);
1291
+ }
1292
+ if (value && value.length > 0) {
1293
+ collected[lower] = value;
1294
+ }
1295
+ }
1296
+ if (!("content-type" in collected)) {
1297
+ collected["content-type"] = "application/json";
1298
+ }
1299
+ if (Object.keys(collected).length > 0) {
1300
+ providerHeaders = collected;
1301
+ }
1302
+ } else {
1303
+ providerBody = buildProviderBody(normalizedForProvider, {
1304
+ maxTokens: maxTokensOverride,
1305
+ temperature: payload.temperature,
1306
+ toolChoice,
1307
+ overrideTools
1308
+ });
1309
+ }
1082
1310
  const connector = getConnector(target.providerId);
1083
1311
  const requestStart = Date.now();
1084
1312
  const storePayloads = getConfig().storePayloads !== false;
1085
- const logId = recordLog({
1313
+ const logId = await recordLog({
1086
1314
  timestamp: requestStart,
1087
1315
  provider: target.providerId,
1088
1316
  model: target.modelId,
1089
1317
  clientModel: requestedModel,
1090
- sessionId: payload.metadata?.user_id
1318
+ sessionId: payload.metadata?.user_id,
1319
+ stream: normalized.stream
1091
1320
  });
1092
1321
  incrementActiveRequests();
1093
1322
  if (storePayloads) {
1094
- upsertLogPayload(logId, {
1323
+ await upsertLogPayload(logId, {
1095
1324
  prompt: (() => {
1096
1325
  try {
1097
1326
  return JSON.stringify(payload);
@@ -1102,10 +1331,10 @@ async function registerMessagesRoute(app) {
1102
1331
  });
1103
1332
  }
1104
1333
  let finalized = false;
1105
- const finalize = (statusCode, error) => {
1334
+ const finalize = async (statusCode, error) => {
1106
1335
  if (finalized)
1107
1336
  return;
1108
- finalizeLog(logId, {
1337
+ await finalizeLog(logId, {
1109
1338
  latencyMs: Date.now() - requestStart,
1110
1339
  statusCode,
1111
1340
  error,
@@ -1136,16 +1365,21 @@ async function registerMessagesRoute(app) {
1136
1365
  const upstream = await connector.send({
1137
1366
  model: target.modelId,
1138
1367
  body: providerBody,
1139
- stream: normalized.stream
1368
+ stream: normalized.stream,
1369
+ query: querySuffix,
1370
+ headers: providerHeaders
1140
1371
  });
1141
1372
  if (upstream.status >= 400) {
1142
1373
  reply.code(upstream.status);
1143
1374
  const bodyText = upstream.body ? await new Response(upstream.body).text() : "";
1144
1375
  const errorText = bodyText || "Upstream provider error";
1376
+ console.warn(
1377
+ `[cc-gw][provider:${target.providerId}] upstream error status=${upstream.status} body=${bodyText || "<empty>"}`
1378
+ );
1145
1379
  if (storePayloads) {
1146
- upsertLogPayload(logId, { response: bodyText || null });
1380
+ await upsertLogPayload(logId, { response: bodyText || null });
1147
1381
  }
1148
- finalize(upstream.status, errorText);
1382
+ await finalize(upstream.status, errorText);
1149
1383
  return { error: errorText };
1150
1384
  }
1151
1385
  if (!normalized.stream) {
@@ -1167,21 +1401,21 @@ async function registerMessagesRoute(app) {
1167
1401
  cached: cachedTokens2
1168
1402
  });
1169
1403
  const latencyMs2 = Date.now() - requestStart;
1170
- updateLogTokens(logId, {
1404
+ await updateLogTokens(logId, {
1171
1405
  inputTokens: inputTokens2,
1172
1406
  outputTokens: outputTokens2,
1173
1407
  cachedTokens: cachedTokens2,
1174
1408
  ttftMs: latencyMs2,
1175
1409
  tpotMs: computeTpot(latencyMs2, outputTokens2, { streaming: false })
1176
1410
  });
1177
- updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1411
+ await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1178
1412
  requests: 1,
1179
1413
  inputTokens: inputTokens2,
1180
1414
  outputTokens: outputTokens2,
1181
1415
  latencyMs: latencyMs2
1182
1416
  });
1183
1417
  if (storePayloads) {
1184
- upsertLogPayload(logId, {
1418
+ await upsertLogPayload(logId, {
1185
1419
  response: (() => {
1186
1420
  try {
1187
1421
  return JSON.stringify(json);
@@ -1191,7 +1425,7 @@ async function registerMessagesRoute(app) {
1191
1425
  })()
1192
1426
  });
1193
1427
  }
1194
- finalize(200, null);
1428
+ await finalize(200, null);
1195
1429
  reply.header("content-type", "application/json");
1196
1430
  return json;
1197
1431
  }
@@ -1212,21 +1446,21 @@ async function registerMessagesRoute(app) {
1212
1446
  cached: cachedTokens
1213
1447
  });
1214
1448
  const latencyMs = Date.now() - requestStart;
1215
- updateLogTokens(logId, {
1449
+ await updateLogTokens(logId, {
1216
1450
  inputTokens,
1217
1451
  outputTokens,
1218
1452
  cachedTokens,
1219
1453
  ttftMs: latencyMs,
1220
1454
  tpotMs: computeTpot(latencyMs, outputTokens, { streaming: false })
1221
1455
  });
1222
- updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1456
+ await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1223
1457
  requests: 1,
1224
1458
  inputTokens,
1225
1459
  outputTokens,
1226
1460
  latencyMs
1227
1461
  });
1228
1462
  if (storePayloads) {
1229
- upsertLogPayload(logId, {
1463
+ await upsertLogPayload(logId, {
1230
1464
  response: (() => {
1231
1465
  try {
1232
1466
  return JSON.stringify(claudeResponse);
@@ -1236,18 +1470,19 @@ async function registerMessagesRoute(app) {
1236
1470
  })()
1237
1471
  });
1238
1472
  }
1239
- finalize(200, null);
1473
+ await finalize(200, null);
1240
1474
  reply.header("content-type", "application/json");
1241
1475
  return claudeResponse;
1242
1476
  }
1243
1477
  if (!upstream.body) {
1244
1478
  reply.code(500);
1245
- finalize(500, "Upstream returned empty body");
1479
+ await finalize(500, "Upstream returned empty body");
1246
1480
  return { error: "Upstream returned empty body" };
1247
1481
  }
1248
1482
  reply.header("content-type", "text/event-stream; charset=utf-8");
1249
1483
  reply.header("cache-control", "no-cache, no-store, must-revalidate");
1250
1484
  reply.header("connection", "keep-alive");
1485
+ reply.hijack();
1251
1486
  reply.raw.writeHead(200);
1252
1487
  if (providerType === "anthropic") {
1253
1488
  const reader2 = upstream.body.getReader();
@@ -1258,6 +1493,8 @@ async function registerMessagesRoute(app) {
1258
1493
  let usageCompletion2 = 0;
1259
1494
  let usageCached2 = null;
1260
1495
  let accumulatedContent2 = "";
1496
+ let firstTokenAt2 = null;
1497
+ let lastUsagePayload = null;
1261
1498
  while (true) {
1262
1499
  const { value, done } = await reader2.read();
1263
1500
  if (done)
@@ -1280,12 +1517,17 @@ async function registerMessagesRoute(app) {
1280
1517
  if (data?.usage) {
1281
1518
  usagePrompt2 = data.usage.input_tokens ?? usagePrompt2;
1282
1519
  usageCompletion2 = data.usage.output_tokens ?? usageCompletion2;
1283
- if (typeof data.usage.cached_tokens === "number") {
1284
- usageCached2 = data.usage.cached_tokens;
1520
+ const maybeCached = resolveCachedTokens(data.usage);
1521
+ if (maybeCached !== null) {
1522
+ usageCached2 = maybeCached;
1285
1523
  }
1524
+ lastUsagePayload = data.usage;
1286
1525
  }
1287
1526
  const deltaText = data?.delta?.text;
1288
1527
  if (typeof deltaText === "string") {
1528
+ if (!firstTokenAt2 && deltaText.length > 0) {
1529
+ firstTokenAt2 = Date.now();
1530
+ }
1289
1531
  accumulatedContent2 += deltaText;
1290
1532
  }
1291
1533
  } catch (error) {
@@ -1307,14 +1549,20 @@ async function registerMessagesRoute(app) {
1307
1549
  if (!usageCompletion2) {
1308
1550
  usageCompletion2 = accumulatedContent2 ? estimateTextTokens(accumulatedContent2, target.modelId) : estimateTextTokens("", target.modelId);
1309
1551
  }
1552
+ if (!firstTokenAt2) {
1553
+ firstTokenAt2 = requestStart;
1554
+ }
1310
1555
  const totalLatencyMs = Date.now() - requestStart;
1311
- const ttftMs = firstTokenAt ? firstTokenAt - requestStart : null;
1556
+ const ttftMs = firstTokenAt2 ? firstTokenAt2 - requestStart : null;
1557
+ if (usageCached2 === null) {
1558
+ usageCached2 = resolveCachedTokens(lastUsagePayload);
1559
+ }
1312
1560
  logUsage("stream.anthropic.final", {
1313
1561
  input: usagePrompt2,
1314
1562
  output: usageCompletion2,
1315
1563
  cached: usageCached2
1316
1564
  });
1317
- updateLogTokens(logId, {
1565
+ await updateLogTokens(logId, {
1318
1566
  inputTokens: usagePrompt2,
1319
1567
  outputTokens: usageCompletion2,
1320
1568
  cachedTokens: usageCached2,
@@ -1324,14 +1572,14 @@ async function registerMessagesRoute(app) {
1324
1572
  ttftMs
1325
1573
  })
1326
1574
  });
1327
- updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1575
+ await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1328
1576
  requests: 1,
1329
1577
  inputTokens: usagePrompt2,
1330
1578
  outputTokens: usageCompletion2,
1331
1579
  latencyMs: totalLatencyMs
1332
1580
  });
1333
1581
  if (storePayloads) {
1334
- upsertLogPayload(logId, {
1582
+ await upsertLogPayload(logId, {
1335
1583
  response: (() => {
1336
1584
  try {
1337
1585
  return JSON.stringify({
@@ -1348,7 +1596,7 @@ async function registerMessagesRoute(app) {
1348
1596
  })()
1349
1597
  });
1350
1598
  }
1351
- finalize(200, null);
1599
+ await finalize(200, null);
1352
1600
  return reply;
1353
1601
  }
1354
1602
  const reader = upstream.body.getReader();
@@ -1438,7 +1686,7 @@ data: ${JSON.stringify(data)}
1438
1686
  output: finalCompletionTokens,
1439
1687
  cached: usageCached
1440
1688
  });
1441
- updateLogTokens(logId, {
1689
+ await updateLogTokens(logId, {
1442
1690
  inputTokens: finalPromptTokens,
1443
1691
  outputTokens: finalCompletionTokens,
1444
1692
  cachedTokens: usageCached,
@@ -1448,14 +1696,14 @@ data: ${JSON.stringify(data)}
1448
1696
  ttftMs
1449
1697
  })
1450
1698
  });
1451
- updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1699
+ await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1452
1700
  requests: 1,
1453
1701
  inputTokens: finalPromptTokens,
1454
1702
  outputTokens: finalCompletionTokens,
1455
1703
  latencyMs: totalLatencyMs
1456
1704
  });
1457
1705
  if (storePayloads) {
1458
- upsertLogPayload(logId, {
1706
+ await upsertLogPayload(logId, {
1459
1707
  response: (() => {
1460
1708
  try {
1461
1709
  return JSON.stringify({
@@ -1473,7 +1721,7 @@ data: ${JSON.stringify(data)}
1473
1721
  })()
1474
1722
  });
1475
1723
  }
1476
- finalize(200, null);
1724
+ await finalize(200, null);
1477
1725
  completed = true;
1478
1726
  return reply;
1479
1727
  }
@@ -1495,6 +1743,7 @@ data: ${JSON.stringify(data)}
1495
1743
  }
1496
1744
  }
1497
1745
  if (choice.delta?.tool_calls) {
1746
+ request.log.debug({ event: "debug.tool_call_delta", delta: choice.delta?.tool_calls }, "tool call delta received");
1498
1747
  if (!firstTokenAt) {
1499
1748
  firstTokenAt = Date.now();
1500
1749
  }
@@ -1582,6 +1831,9 @@ data: ${JSON.stringify(data)}
1582
1831
  }
1583
1832
  if (!completed) {
1584
1833
  reply.raw.end();
1834
+ if (!firstTokenAt) {
1835
+ firstTokenAt = requestStart;
1836
+ }
1585
1837
  const totalLatencyMs = Date.now() - requestStart;
1586
1838
  const fallbackPrompt = usagePrompt || target.tokenEstimate || estimateTokens(normalized, target.modelId);
1587
1839
  const fallbackCompletion = usageCompletion || estimateTextTokens(accumulatedContent, target.modelId);
@@ -1591,7 +1843,7 @@ data: ${JSON.stringify(data)}
1591
1843
  output: fallbackCompletion,
1592
1844
  cached: usageCached
1593
1845
  });
1594
- updateLogTokens(logId, {
1846
+ await updateLogTokens(logId, {
1595
1847
  inputTokens: fallbackPrompt,
1596
1848
  outputTokens: fallbackCompletion,
1597
1849
  cachedTokens: usageCached,
@@ -1601,14 +1853,14 @@ data: ${JSON.stringify(data)}
1601
1853
  ttftMs
1602
1854
  })
1603
1855
  });
1604
- updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1856
+ await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1605
1857
  requests: 1,
1606
1858
  inputTokens: fallbackPrompt,
1607
1859
  outputTokens: fallbackCompletion,
1608
1860
  latencyMs: totalLatencyMs
1609
1861
  });
1610
1862
  if (storePayloads) {
1611
- upsertLogPayload(logId, {
1863
+ await upsertLogPayload(logId, {
1612
1864
  response: (() => {
1613
1865
  try {
1614
1866
  return JSON.stringify({
@@ -1625,7 +1877,7 @@ data: ${JSON.stringify(data)}
1625
1877
  })()
1626
1878
  });
1627
1879
  }
1628
- finalize(200, null);
1880
+ await finalize(200, null);
1629
1881
  return reply;
1630
1882
  }
1631
1883
  } catch (err) {
@@ -1633,31 +1885,30 @@ data: ${JSON.stringify(data)}
1633
1885
  if (!reply.sent) {
1634
1886
  reply.code(500);
1635
1887
  }
1636
- finalize(reply.statusCode >= 400 ? reply.statusCode : 500, message);
1888
+ await finalize(reply.statusCode >= 400 ? reply.statusCode : 500, message);
1637
1889
  return { error: message };
1638
1890
  } finally {
1639
1891
  decrementActiveRequests();
1640
1892
  if (!finalized && reply.sent) {
1641
- finalize(reply.statusCode ?? 200, null);
1893
+ await finalize(reply.statusCode ?? 200, null);
1642
1894
  }
1643
1895
  }
1644
1896
  });
1645
1897
  }
1646
1898
 
1647
1899
  // logging/queries.ts
1648
- function queryLogs(options = {}) {
1649
- const db2 = getDb();
1900
+ async function queryLogs(options = {}) {
1650
1901
  const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
1651
1902
  const offset = Math.max(options.offset ?? 0, 0);
1652
1903
  const conditions = [];
1653
1904
  const params = {};
1654
1905
  if (options.provider) {
1655
- conditions.push("provider = @provider");
1656
- params.provider = options.provider;
1906
+ conditions.push("provider = $provider");
1907
+ params.$provider = options.provider;
1657
1908
  }
1658
1909
  if (options.model) {
1659
- conditions.push("model = @model");
1660
- params.model = options.model;
1910
+ conditions.push("model = $model");
1911
+ params.$model = options.model;
1661
1912
  }
1662
1913
  if (options.status === "success") {
1663
1914
  conditions.push("error IS NULL");
@@ -1665,39 +1916,49 @@ function queryLogs(options = {}) {
1665
1916
  conditions.push("error IS NOT NULL");
1666
1917
  }
1667
1918
  if (typeof options.from === "number") {
1668
- conditions.push("timestamp >= @from");
1669
- params.from = options.from;
1919
+ conditions.push("timestamp >= $from");
1920
+ params.$from = options.from;
1670
1921
  }
1671
1922
  if (typeof options.to === "number") {
1672
- conditions.push("timestamp <= @to");
1673
- params.to = options.to;
1923
+ conditions.push("timestamp <= $to");
1924
+ params.$to = options.to;
1674
1925
  }
1675
1926
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1676
- const totalRow = db2.prepare(`SELECT COUNT(*) AS count FROM request_logs ${whereClause}`).get(params);
1677
- const items = db2.prepare(
1678
- `SELECT id, timestamp, session_id, provider, model, client_model, latency_ms, status_code, input_tokens, output_tokens, cached_tokens, ttft_ms, tpot_ms, error
1927
+ const totalRow = await getOne(
1928
+ `SELECT COUNT(*) AS count FROM request_logs ${whereClause}`,
1929
+ params
1930
+ );
1931
+ const items = await getAll(
1932
+ `SELECT id, timestamp, session_id, provider, model, client_model,
1933
+ stream, latency_ms, status_code, input_tokens, output_tokens,
1934
+ cached_tokens, ttft_ms, tpot_ms, error
1679
1935
  FROM request_logs
1680
1936
  ${whereClause}
1681
1937
  ORDER BY timestamp DESC
1682
- LIMIT @limit OFFSET @offset`
1683
- ).all({ ...params, limit, offset });
1938
+ LIMIT $limit OFFSET $offset`,
1939
+ { ...params, $limit: limit, $offset: offset }
1940
+ );
1684
1941
  return {
1685
1942
  total: totalRow?.count ?? 0,
1686
1943
  items
1687
1944
  };
1688
1945
  }
1689
- function getLogDetail(id) {
1690
- const db2 = getDb();
1691
- const record = db2.prepare(
1692
- `SELECT id, timestamp, session_id, provider, model, client_model, latency_ms, status_code, input_tokens, output_tokens, cached_tokens, ttft_ms, tpot_ms, error
1946
+ async function getLogDetail(id) {
1947
+ const record = await getOne(
1948
+ `SELECT id, timestamp, session_id, provider, model, client_model,
1949
+ stream, latency_ms, status_code, input_tokens, output_tokens,
1950
+ cached_tokens, ttft_ms, tpot_ms, error
1693
1951
  FROM request_logs
1694
- WHERE id = ?`
1695
- ).get(id);
1952
+ WHERE id = ?`,
1953
+ [id]
1954
+ );
1696
1955
  return record ?? null;
1697
1956
  }
1698
- function getLogPayload(id) {
1699
- const db2 = getDb();
1700
- const payload = db2.prepare(`SELECT prompt, response FROM request_payloads WHERE request_id = ?`).get(id);
1957
+ async function getLogPayload(id) {
1958
+ const payload = await getOne(
1959
+ "SELECT prompt, response FROM request_payloads WHERE request_id = ?",
1960
+ [id]
1961
+ );
1701
1962
  if (!payload) {
1702
1963
  return null;
1703
1964
  }
@@ -1706,21 +1967,30 @@ function getLogPayload(id) {
1706
1967
  response: decompressPayload(payload.response)
1707
1968
  };
1708
1969
  }
1709
- function cleanupLogsBefore(timestamp) {
1710
- const db2 = getDb();
1711
- const stmt = db2.prepare(`DELETE FROM request_logs WHERE timestamp < ?`);
1712
- const result = stmt.run(timestamp);
1970
+ async function cleanupLogsBefore(timestamp) {
1971
+ const result = await runQuery("DELETE FROM request_logs WHERE timestamp < ?", [timestamp]);
1713
1972
  return Number(result.changes ?? 0);
1714
1973
  }
1715
- function getDailyMetrics(days = 7) {
1716
- const db2 = getDb();
1717
- const rows = db2.prepare(
1718
- `SELECT date, request_count AS requestCount, total_input_tokens AS inputTokens,
1719
- total_output_tokens AS outputTokens, total_latency_ms AS totalLatency
1720
- FROM daily_metrics
1721
- ORDER BY date DESC
1722
- LIMIT ?`
1723
- ).all(days);
1974
+ async function clearAllLogs() {
1975
+ const logsResult = await runQuery("DELETE FROM request_logs", []);
1976
+ const metricsResult = await runQuery("DELETE FROM daily_metrics", []);
1977
+ return {
1978
+ logs: Number(logsResult.changes ?? 0),
1979
+ metrics: Number(metricsResult.changes ?? 0)
1980
+ };
1981
+ }
1982
+ async function getDailyMetrics(days = 7) {
1983
+ const rows = await getAll(
1984
+ `SELECT date,
1985
+ request_count AS requestCount,
1986
+ total_input_tokens AS inputTokens,
1987
+ total_output_tokens AS outputTokens,
1988
+ total_latency_ms AS totalLatency
1989
+ FROM daily_metrics
1990
+ ORDER BY date DESC
1991
+ LIMIT ?`,
1992
+ [days]
1993
+ );
1724
1994
  return rows.map((row) => ({
1725
1995
  date: row.date,
1726
1996
  requestCount: row.requestCount ?? 0,
@@ -1729,34 +1999,35 @@ function getDailyMetrics(days = 7) {
1729
1999
  avgLatencyMs: row.requestCount ? Math.round((row.totalLatency ?? 0) / row.requestCount) : 0
1730
2000
  })).reverse();
1731
2001
  }
1732
- function getMetricsOverview() {
1733
- const db2 = getDb();
1734
- const totalsRow = db2.prepare(
2002
+ async function getMetricsOverview() {
2003
+ const totalsRow = await getOne(
1735
2004
  `SELECT
1736
- COALESCE(SUM(request_count), 0) AS requests,
1737
- COALESCE(SUM(total_input_tokens), 0) AS inputTokens,
1738
- COALESCE(SUM(total_output_tokens), 0) AS outputTokens,
1739
- COALESCE(SUM(total_latency_ms), 0) AS totalLatency
1740
- FROM daily_metrics`
1741
- ).get();
2005
+ COALESCE(SUM(request_count), 0) AS requests,
2006
+ COALESCE(SUM(total_input_tokens), 0) AS inputTokens,
2007
+ COALESCE(SUM(total_output_tokens), 0) AS outputTokens,
2008
+ COALESCE(SUM(total_latency_ms), 0) AS totalLatency
2009
+ FROM daily_metrics`
2010
+ );
1742
2011
  const todayKey = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1743
- const todayRow = db2.prepare(
2012
+ const todayRow = await getOne(
1744
2013
  `SELECT request_count AS requests,
1745
- total_input_tokens AS inputTokens,
1746
- total_output_tokens AS outputTokens,
1747
- total_latency_ms AS totalLatency
1748
- FROM daily_metrics WHERE date = ?`
1749
- ).get(todayKey);
2014
+ total_input_tokens AS inputTokens,
2015
+ total_output_tokens AS outputTokens,
2016
+ total_latency_ms AS totalLatency
2017
+ FROM daily_metrics
2018
+ WHERE date = ?`,
2019
+ [todayKey]
2020
+ );
1750
2021
  const resolveAvg = (totalLatency, requests) => requests > 0 ? Math.round(totalLatency / requests) : 0;
1751
- const totalsRequests = totalsRow.requests ?? 0;
1752
- const totalsLatency = totalsRow.totalLatency ?? 0;
2022
+ const totalsRequests = totalsRow?.requests ?? 0;
2023
+ const totalsLatency = totalsRow?.totalLatency ?? 0;
1753
2024
  const todayRequests = todayRow?.requests ?? 0;
1754
2025
  const todayLatency = todayRow?.totalLatency ?? 0;
1755
2026
  return {
1756
2027
  totals: {
1757
2028
  requests: totalsRequests,
1758
- inputTokens: totalsRow.inputTokens ?? 0,
1759
- outputTokens: totalsRow.outputTokens ?? 0,
2029
+ inputTokens: totalsRow?.inputTokens ?? 0,
2030
+ outputTokens: totalsRow?.outputTokens ?? 0,
1760
2031
  avgLatencyMs: resolveAvg(totalsLatency, totalsRequests)
1761
2032
  },
1762
2033
  today: {
@@ -1767,35 +2038,44 @@ function getMetricsOverview() {
1767
2038
  }
1768
2039
  };
1769
2040
  }
1770
- function getModelUsageMetrics(days = 7, limit = 10) {
1771
- const db2 = getDb();
2041
+ async function getModelUsageMetrics(days = 7, limit = 10) {
1772
2042
  const since = Date.now() - days * 24 * 60 * 60 * 1e3;
1773
- const rows = db2.prepare(
2043
+ const rows = await getAll(
1774
2044
  `SELECT
1775
- model,
1776
- provider,
1777
- COUNT(*) AS requests,
1778
- COALESCE(SUM(input_tokens), 0) AS inputTokens,
1779
- COALESCE(SUM(output_tokens), 0) AS outputTokens,
1780
- COALESCE(SUM(latency_ms), 0) AS totalLatency
1781
- FROM request_logs
1782
- WHERE timestamp >= ?
1783
- GROUP BY provider, model
1784
- ORDER BY requests DESC
1785
- LIMIT ?`
1786
- ).all(since, limit);
2045
+ model,
2046
+ provider,
2047
+ COUNT(*) AS requests,
2048
+ COALESCE(SUM(input_tokens), 0) AS inputTokens,
2049
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
2050
+ COALESCE(SUM(latency_ms), 0) AS totalLatency,
2051
+ AVG(CASE WHEN ttft_ms IS NULL THEN NULL ELSE ttft_ms END) AS avgTtftMs,
2052
+ AVG(CASE WHEN tpot_ms IS NULL THEN NULL ELSE tpot_ms END) AS avgTpotMs
2053
+ FROM request_logs
2054
+ WHERE timestamp >= ?
2055
+ GROUP BY provider, model
2056
+ ORDER BY requests DESC
2057
+ LIMIT ?`,
2058
+ [since, limit]
2059
+ );
2060
+ const roundValue = (value, fractionDigits = 0) => value == null ? null : Number(value.toFixed(fractionDigits));
1787
2061
  return rows.map((row) => ({
1788
2062
  model: row.model,
1789
2063
  provider: row.provider,
1790
2064
  requests: row.requests ?? 0,
1791
2065
  inputTokens: row.inputTokens ?? 0,
1792
2066
  outputTokens: row.outputTokens ?? 0,
1793
- avgLatencyMs: row.requests ? Math.round((row.totalLatency ?? 0) / row.requests) : 0
2067
+ avgLatencyMs: row.requests ? Math.round((row.totalLatency ?? 0) / row.requests) : 0,
2068
+ avgTtftMs: roundValue(row.avgTtftMs, 0),
2069
+ avgTpotMs: roundValue(row.avgTpotMs, 2)
1794
2070
  }));
1795
2071
  }
1796
2072
 
1797
2073
  // routes/admin.ts
1798
2074
  async function registerAdminRoutes(app) {
2075
+ const mapLogRecord = (record) => ({
2076
+ ...record,
2077
+ stream: Boolean(record?.stream)
2078
+ });
1799
2079
  app.get("/api/status", async () => {
1800
2080
  const config = getConfig();
1801
2081
  return {
@@ -1957,9 +2237,9 @@ async function registerAdminRoutes(app) {
1957
2237
  };
1958
2238
  const from = parseTime(query.from);
1959
2239
  const to = parseTime(query.to);
1960
- const { items, total } = queryLogs({ limit, offset, provider, model, status, from, to });
2240
+ const { items, total } = await queryLogs({ limit, offset, provider, model, status, from, to });
1961
2241
  reply.header("x-total-count", String(total));
1962
- return { total, items };
2242
+ return { total, items: items.map(mapLogRecord) };
1963
2243
  });
1964
2244
  app.get("/api/logs/:id", async (request, reply) => {
1965
2245
  const id = Number(request.params.id);
@@ -1967,25 +2247,30 @@ async function registerAdminRoutes(app) {
1967
2247
  reply.code(400);
1968
2248
  return { error: "Invalid id" };
1969
2249
  }
1970
- const record = getLogDetail(id);
2250
+ const record = await getLogDetail(id);
1971
2251
  if (!record) {
1972
2252
  reply.code(404);
1973
2253
  return { error: "Not found" };
1974
2254
  }
1975
- const payload = getLogPayload(id);
1976
- return { ...record, payload };
2255
+ const payload = await getLogPayload(id);
2256
+ return { ...mapLogRecord(record), payload };
1977
2257
  });
1978
2258
  app.post("/api/logs/cleanup", async () => {
1979
2259
  const config = getConfig();
1980
2260
  const retentionDays = config.logRetentionDays ?? 30;
1981
2261
  const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
1982
- const deleted = cleanupLogsBefore(cutoff);
2262
+ const deleted = await cleanupLogsBefore(cutoff);
1983
2263
  return { success: true, deleted };
1984
2264
  });
2265
+ app.post("/api/logs/clear", async () => {
2266
+ const { logs, metrics } = await clearAllLogs();
2267
+ return { success: true, deleted: logs, metricsCleared: metrics };
2268
+ });
1985
2269
  app.get("/api/db/info", async () => {
1986
- const db2 = getDb();
1987
- const pageCount = db2.pragma("page_count", { simple: true });
1988
- const pageSize = db2.pragma("page_size", { simple: true });
2270
+ const pageCountRow = await getOne("PRAGMA page_count");
2271
+ const pageSizeRow = await getOne("PRAGMA page_size");
2272
+ const pageCount = pageCountRow?.page_count ?? 0;
2273
+ const pageSize = pageSizeRow?.page_size ?? 0;
1989
2274
  return {
1990
2275
  pageCount,
1991
2276
  pageSize,
@@ -2021,7 +2306,7 @@ function startMaintenanceTimers() {
2021
2306
  scheduleCleanup();
2022
2307
  }
2023
2308
  function scheduleCleanup() {
2024
- const run = () => {
2309
+ const run2 = () => {
2025
2310
  try {
2026
2311
  const retentionDays = getConfig().logRetentionDays ?? 30;
2027
2312
  const cutoff = Date.now() - retentionDays * DAY_MS;
@@ -2033,7 +2318,7 @@ function scheduleCleanup() {
2033
2318
  console.error("[maintenance] cleanup failed", err);
2034
2319
  }
2035
2320
  };
2036
- setInterval(run, DAY_MS);
2321
+ setInterval(run2, DAY_MS);
2037
2322
  }
2038
2323
 
2039
2324
  // index.ts
@@ -2062,7 +2347,13 @@ function resolveWebDist() {
2062
2347
  return null;
2063
2348
  }
2064
2349
  async function createServer() {
2065
- const app = Fastify({ logger: true });
2350
+ const config = cachedConfig2 ?? loadConfig();
2351
+ const app = Fastify({
2352
+ logger: {
2353
+ level: config.logLevel ?? "info"
2354
+ },
2355
+ disableRequestLogging: config.requestLogging === false
2356
+ });
2066
2357
  await app.register(fastifyCors, {
2067
2358
  origin: true,
2068
2359
  credentials: true