@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.
- package/README.md +22 -5
- package/package.json +19 -28
- package/src/cli/dist/index.js +33 -5
- package/src/server/dist/index.js +515 -224
- package/src/web/dist/assets/{About-DwsCrDAG.js → About-ChfDc2NI.js} +1 -1
- package/src/web/dist/assets/{Dashboard-CX6rHITi.js → Dashboard-CDJyyn7X.js} +23 -23
- package/src/web/dist/assets/Logs-D05vFMv1.js +1 -0
- package/src/web/dist/assets/ModelManagement-B3YFArK5.js +1 -0
- package/src/web/dist/assets/Settings-DAYDEvz0.js +1 -0
- package/src/web/dist/assets/{index-tMD4UuQh.js → index-B82tYC06.js} +12 -12
- package/src/web/dist/assets/index-Bym_WpRV.css +1 -0
- package/src/web/dist/assets/{useApiQuery-BG_l-7WN.js → useApiQuery-BKzEW-RR.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/Logs-0wlHxVhg.js +0 -1
- package/src/web/dist/assets/ModelManagement-Ckc_KEXy.js +0 -1
- package/src/web/dist/assets/Settings-CeIWDWYw.js +0 -1
- package/src/web/dist/assets/index-BXlilpwV.css +0 -1
package/src/server/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
|
886
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
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
|
-
|
|
936
|
-
INSERT INTO request_payloads (request_id, prompt, response)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1284
|
-
|
|
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 =
|
|
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 =
|
|
1656
|
-
params
|
|
1906
|
+
conditions.push("provider = $provider");
|
|
1907
|
+
params.$provider = options.provider;
|
|
1657
1908
|
}
|
|
1658
1909
|
if (options.model) {
|
|
1659
|
-
conditions.push("model =
|
|
1660
|
-
params
|
|
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 >=
|
|
1669
|
-
params
|
|
1919
|
+
conditions.push("timestamp >= $from");
|
|
1920
|
+
params.$from = options.from;
|
|
1670
1921
|
}
|
|
1671
1922
|
if (typeof options.to === "number") {
|
|
1672
|
-
conditions.push("timestamp <=
|
|
1673
|
-
params
|
|
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 =
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
|
1683
|
-
|
|
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
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
-
|
|
1952
|
+
WHERE id = ?`,
|
|
1953
|
+
[id]
|
|
1954
|
+
);
|
|
1696
1955
|
return record ?? null;
|
|
1697
1956
|
}
|
|
1698
|
-
function getLogPayload(id) {
|
|
1699
|
-
const
|
|
1700
|
-
|
|
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
|
|
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
|
|
1716
|
-
const
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
|
1734
|
-
const totalsRow = db2.prepare(
|
|
2002
|
+
async function getMetricsOverview() {
|
|
2003
|
+
const totalsRow = await getOne(
|
|
1735
2004
|
`SELECT
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
)
|
|
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 =
|
|
2012
|
+
const todayRow = await getOne(
|
|
1744
2013
|
`SELECT request_count AS requests,
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
|
1752
|
-
const totalsLatency = totalsRow
|
|
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
|
|
1759
|
-
outputTokens: totalsRow
|
|
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 =
|
|
2043
|
+
const rows = await getAll(
|
|
1774
2044
|
`SELECT
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
|
1987
|
-
const
|
|
1988
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
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
|