@chenpu17/cc-gw 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/package.json +32 -20
- package/src/cli/dist/index.js +18 -6
- package/src/server/dist/index.js +1492 -90
- package/src/web/dist/assets/{About-CiTOjn8x.js → About-DifCcqF-.js} +1 -1
- package/src/web/dist/assets/ApiKeys-Cja_7EiV.js +21 -0
- package/src/web/dist/assets/Dashboard-BKODF_sa.js +1 -0
- package/src/web/dist/assets/Help-CVYbgGOi.js +1 -0
- package/src/web/dist/assets/Logs-ZqIQEkJH.js +1 -0
- package/src/web/dist/assets/ModelManagement-CWeSK3Hd.js +1 -0
- package/src/web/dist/assets/Settings-CaTqdHzh.js +1 -0
- package/src/web/dist/assets/index-CDJfhjXI.js +133 -0
- package/src/web/dist/assets/index-DG02tgGK.js +61 -0
- package/src/web/dist/assets/index-fgl6ZbLo.css +1 -0
- package/src/web/dist/assets/{useApiQuery-DMfHsHz0.js → useApiQuery-Bna2BImG.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/Dashboard-C0PbDn4x.js +0 -61
- package/src/web/dist/assets/Logs-BNTvOSt2.js +0 -1
- package/src/web/dist/assets/ModelManagement-jcVnkvGR.js +0 -1
- package/src/web/dist/assets/Settings-BVdJhIOL.js +0 -1
- package/src/web/dist/assets/index-Bym_WpRV.css +0 -1
- package/src/web/dist/assets/index-DSgvyziA.js +0 -123
package/src/server/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import Fastify from "fastify";
|
|
3
3
|
import fastifyCors from "@fastify/cors";
|
|
4
4
|
import fastifyStatic from "@fastify/static";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import fs4 from "fs";
|
|
6
|
+
import path4 from "path";
|
|
7
7
|
import process2 from "process";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
@@ -20,7 +20,8 @@ var LOG_LEVELS = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"debug",
|
|
21
21
|
"trace"
|
|
22
22
|
]);
|
|
23
|
-
var
|
|
23
|
+
var HOME_OVERRIDE = process.env.CC_GW_HOME;
|
|
24
|
+
var HOME_DIR = path.resolve(HOME_OVERRIDE ?? path.join(os.homedir(), ".cc-gw"));
|
|
24
25
|
var CONFIG_PATH = path.join(HOME_DIR, "config.json");
|
|
25
26
|
var TypedEmitter = class extends EventEmitter {
|
|
26
27
|
on(event, listener) {
|
|
@@ -34,7 +35,54 @@ var TypedEmitter = class extends EventEmitter {
|
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
var emitter = new TypedEmitter();
|
|
38
|
+
var KNOWN_ENDPOINTS = ["anthropic", "openai"];
|
|
37
39
|
var cachedConfig = null;
|
|
40
|
+
function sanitizeDefaults(input) {
|
|
41
|
+
const defaults = {
|
|
42
|
+
completion: null,
|
|
43
|
+
reasoning: null,
|
|
44
|
+
background: null,
|
|
45
|
+
longContextThreshold: 6e4
|
|
46
|
+
};
|
|
47
|
+
if (input) {
|
|
48
|
+
if (typeof input.completion === "string" || input.completion === null) {
|
|
49
|
+
defaults.completion = input.completion ?? null;
|
|
50
|
+
}
|
|
51
|
+
if (typeof input.reasoning === "string" || input.reasoning === null) {
|
|
52
|
+
defaults.reasoning = input.reasoning ?? null;
|
|
53
|
+
}
|
|
54
|
+
if (typeof input.background === "string" || input.background === null) {
|
|
55
|
+
defaults.background = input.background ?? null;
|
|
56
|
+
}
|
|
57
|
+
if (typeof input.longContextThreshold === "number" && Number.isFinite(input.longContextThreshold)) {
|
|
58
|
+
defaults.longContextThreshold = input.longContextThreshold;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return defaults;
|
|
62
|
+
}
|
|
63
|
+
function sanitizeModelRoutes(input) {
|
|
64
|
+
if (!input)
|
|
65
|
+
return {};
|
|
66
|
+
const sanitized = {};
|
|
67
|
+
for (const [key, value] of Object.entries(input)) {
|
|
68
|
+
if (typeof value !== "string")
|
|
69
|
+
continue;
|
|
70
|
+
const trimmedKey = key.trim();
|
|
71
|
+
const trimmedValue = value.trim();
|
|
72
|
+
if (!trimmedKey || !trimmedValue)
|
|
73
|
+
continue;
|
|
74
|
+
sanitized[trimmedKey] = trimmedValue;
|
|
75
|
+
}
|
|
76
|
+
return sanitized;
|
|
77
|
+
}
|
|
78
|
+
function resolveEndpointRouting(source, fallback) {
|
|
79
|
+
const defaultsRaw = typeof source === "object" && source !== null ? source.defaults : void 0;
|
|
80
|
+
const routesRaw = typeof source === "object" && source !== null ? source.modelRoutes : void 0;
|
|
81
|
+
return {
|
|
82
|
+
defaults: sanitizeDefaults(defaultsRaw ?? fallback.defaults),
|
|
83
|
+
modelRoutes: sanitizeModelRoutes(routesRaw ?? fallback.modelRoutes)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
38
86
|
function parseConfig(raw) {
|
|
39
87
|
const data = JSON.parse(raw);
|
|
40
88
|
if (typeof data.port !== "number") {
|
|
@@ -43,43 +91,43 @@ function parseConfig(raw) {
|
|
|
43
91
|
if (!Array.isArray(data.providers)) {
|
|
44
92
|
data.providers = [];
|
|
45
93
|
}
|
|
46
|
-
|
|
47
|
-
data.defaults = {
|
|
48
|
-
completion: null,
|
|
49
|
-
reasoning: null,
|
|
50
|
-
background: null,
|
|
51
|
-
longContextThreshold: 6e4
|
|
52
|
-
};
|
|
53
|
-
} else {
|
|
54
|
-
data.defaults.longContextThreshold ??= 6e4;
|
|
55
|
-
}
|
|
94
|
+
const legacyDefaults = sanitizeDefaults(data.defaults);
|
|
56
95
|
if (typeof data.logRetentionDays !== "number") {
|
|
57
96
|
data.logRetentionDays = 30;
|
|
58
97
|
}
|
|
59
98
|
if (typeof data.storePayloads !== "boolean") {
|
|
60
99
|
data.storePayloads = true;
|
|
61
100
|
}
|
|
62
|
-
|
|
63
|
-
data.modelRoutes = {};
|
|
64
|
-
} else {
|
|
65
|
-
const sanitized = {};
|
|
66
|
-
for (const [key, value] of Object.entries(data.modelRoutes)) {
|
|
67
|
-
if (typeof value !== "string")
|
|
68
|
-
continue;
|
|
69
|
-
const trimmedKey = key.trim();
|
|
70
|
-
const trimmedValue = value.trim();
|
|
71
|
-
if (!trimmedKey || !trimmedValue)
|
|
72
|
-
continue;
|
|
73
|
-
sanitized[trimmedKey] = trimmedValue;
|
|
74
|
-
}
|
|
75
|
-
data.modelRoutes = sanitized;
|
|
76
|
-
}
|
|
101
|
+
const legacyRoutes = sanitizeModelRoutes(data.modelRoutes);
|
|
77
102
|
if (typeof data.logLevel !== "string" || !LOG_LEVELS.has(data.logLevel)) {
|
|
78
103
|
data.logLevel = "info";
|
|
79
104
|
}
|
|
80
105
|
if (typeof data.requestLogging !== "boolean") {
|
|
81
106
|
data.requestLogging = true;
|
|
82
107
|
}
|
|
108
|
+
if (typeof data.responseLogging !== "boolean") {
|
|
109
|
+
data.responseLogging = data.requestLogging !== false;
|
|
110
|
+
}
|
|
111
|
+
const endpointRouting = {};
|
|
112
|
+
const sourceRouting = data.endpointRouting && typeof data.endpointRouting === "object" ? data.endpointRouting : {};
|
|
113
|
+
const fallbackAnthropic = {
|
|
114
|
+
defaults: legacyDefaults,
|
|
115
|
+
modelRoutes: legacyRoutes
|
|
116
|
+
};
|
|
117
|
+
const fallbackOpenAI = {
|
|
118
|
+
defaults: sanitizeDefaults(void 0),
|
|
119
|
+
modelRoutes: {}
|
|
120
|
+
};
|
|
121
|
+
for (const endpoint of KNOWN_ENDPOINTS) {
|
|
122
|
+
const fallback = endpoint === "anthropic" ? fallbackAnthropic : fallbackOpenAI;
|
|
123
|
+
endpointRouting[endpoint] = resolveEndpointRouting(
|
|
124
|
+
sourceRouting[endpoint],
|
|
125
|
+
fallback
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
data.endpointRouting = endpointRouting;
|
|
129
|
+
data.defaults = { ...endpointRouting.anthropic.defaults };
|
|
130
|
+
data.modelRoutes = { ...endpointRouting.anthropic.modelRoutes };
|
|
83
131
|
return data;
|
|
84
132
|
}
|
|
85
133
|
function loadConfig() {
|
|
@@ -97,8 +145,9 @@ function getConfig() {
|
|
|
97
145
|
}
|
|
98
146
|
function updateConfig(next) {
|
|
99
147
|
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
const normalized = parseConfig(JSON.stringify(next));
|
|
149
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2), "utf-8");
|
|
150
|
+
cachedConfig = normalized;
|
|
102
151
|
emitter.emitTyped("change", cachedConfig);
|
|
103
152
|
}
|
|
104
153
|
function onConfigChange(listener) {
|
|
@@ -301,19 +350,23 @@ function resolveByIdentifier(identifier, providers) {
|
|
|
301
350
|
}
|
|
302
351
|
function resolveRoute(ctx) {
|
|
303
352
|
const config = getConfig();
|
|
353
|
+
const endpointConfig = config.endpointRouting?.[ctx.endpoint] ?? config.endpointRouting?.anthropic;
|
|
354
|
+
if (!endpointConfig) {
|
|
355
|
+
throw new Error(`\u672A\u627E\u5230\u7AEF\u70B9 ${ctx.endpoint} \u7684\u8DEF\u7531\u914D\u7F6E`);
|
|
356
|
+
}
|
|
304
357
|
const providers = config.providers;
|
|
305
358
|
if (!providers.length) {
|
|
306
359
|
throw new Error("\u672A\u914D\u7F6E\u4EFB\u4F55\u6A21\u578B\u63D0\u4F9B\u5546\uFF0C\u8BF7\u5148\u5728 Web UI \u4E2D\u6DFB\u52A0 Provider\u3002");
|
|
307
360
|
}
|
|
308
361
|
const requestedModel = ctx.requestedModel?.trim();
|
|
309
|
-
const mappedIdentifier = requestedModel ?
|
|
362
|
+
const mappedIdentifier = requestedModel ? endpointConfig.modelRoutes?.[requestedModel] ?? null : null;
|
|
310
363
|
const fallbackModelId = providers[0].defaultModel ?? providers[0].models?.[0]?.id ?? "gpt-4o";
|
|
311
364
|
const tokenEstimate = estimateTokens(
|
|
312
365
|
ctx.payload,
|
|
313
366
|
mappedIdentifier ?? requestedModel ?? fallbackModelId
|
|
314
367
|
);
|
|
315
368
|
const strategy = ctx.payload;
|
|
316
|
-
const defaults =
|
|
369
|
+
const defaults = endpointConfig.defaults;
|
|
317
370
|
if (mappedIdentifier) {
|
|
318
371
|
const mapped = resolveByIdentifier(mappedIdentifier, providers);
|
|
319
372
|
if (mapped) {
|
|
@@ -771,7 +824,8 @@ import fs2 from "fs";
|
|
|
771
824
|
import os2 from "os";
|
|
772
825
|
import path2 from "path";
|
|
773
826
|
import sqlite3 from "sqlite3";
|
|
774
|
-
var
|
|
827
|
+
var HOME_OVERRIDE2 = process.env.CC_GW_HOME;
|
|
828
|
+
var HOME_DIR2 = path2.resolve(HOME_OVERRIDE2 ?? path2.join(os2.homedir(), ".cc-gw"));
|
|
775
829
|
var DATA_DIR = path2.join(HOME_DIR2, "data");
|
|
776
830
|
var DB_PATH = path2.join(DATA_DIR, "gateway.db");
|
|
777
831
|
sqlite3.verbose();
|
|
@@ -840,6 +894,45 @@ async function columnExists(db, table, column) {
|
|
|
840
894
|
const rows = await all(db, `PRAGMA table_info(${table})`);
|
|
841
895
|
return rows.some((row) => row.name === column);
|
|
842
896
|
}
|
|
897
|
+
async function migrateDailyMetricsTable(db) {
|
|
898
|
+
const columns = await all(db, "PRAGMA table_info(daily_metrics)");
|
|
899
|
+
if (columns.length === 0)
|
|
900
|
+
return;
|
|
901
|
+
const hasEndpointColumn = columns.some((column) => column.name === "endpoint");
|
|
902
|
+
const primaryKeyColumns = columns.filter((column) => column.pk > 0);
|
|
903
|
+
const hasCompositePrimaryKey = primaryKeyColumns.length > 1;
|
|
904
|
+
if (!hasEndpointColumn || !hasCompositePrimaryKey) {
|
|
905
|
+
const endpointSelector = hasEndpointColumn ? "COALESCE(endpoint, 'anthropic')" : "'anthropic'";
|
|
906
|
+
await exec(
|
|
907
|
+
db,
|
|
908
|
+
`ALTER TABLE daily_metrics RENAME TO daily_metrics_old;
|
|
909
|
+
CREATE TABLE daily_metrics (
|
|
910
|
+
date TEXT NOT NULL,
|
|
911
|
+
endpoint TEXT NOT NULL DEFAULT 'anthropic',
|
|
912
|
+
request_count INTEGER DEFAULT 0,
|
|
913
|
+
total_input_tokens INTEGER DEFAULT 0,
|
|
914
|
+
total_output_tokens INTEGER DEFAULT 0,
|
|
915
|
+
total_latency_ms INTEGER DEFAULT 0,
|
|
916
|
+
PRIMARY KEY (date, endpoint)
|
|
917
|
+
);
|
|
918
|
+
INSERT INTO daily_metrics (date, endpoint, request_count, total_input_tokens, total_output_tokens, total_latency_ms)
|
|
919
|
+
SELECT date,
|
|
920
|
+
${endpointSelector},
|
|
921
|
+
request_count,
|
|
922
|
+
total_input_tokens,
|
|
923
|
+
total_output_tokens,
|
|
924
|
+
total_latency_ms
|
|
925
|
+
FROM daily_metrics_old;
|
|
926
|
+
DROP TABLE daily_metrics_old;`
|
|
927
|
+
);
|
|
928
|
+
} else {
|
|
929
|
+
await run(db, "UPDATE daily_metrics SET endpoint = 'anthropic' WHERE endpoint IS NULL OR endpoint = ''");
|
|
930
|
+
}
|
|
931
|
+
await run(
|
|
932
|
+
db,
|
|
933
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_daily_metrics_date_endpoint ON daily_metrics(date, endpoint)"
|
|
934
|
+
);
|
|
935
|
+
}
|
|
843
936
|
async function maybeAddColumn(db, table, column, definition) {
|
|
844
937
|
const exists = await columnExists(db, table, column);
|
|
845
938
|
if (!exists) {
|
|
@@ -853,6 +946,7 @@ async function ensureSchema(db) {
|
|
|
853
946
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
854
947
|
timestamp INTEGER NOT NULL,
|
|
855
948
|
session_id TEXT,
|
|
949
|
+
endpoint TEXT NOT NULL DEFAULT 'anthropic',
|
|
856
950
|
provider TEXT NOT NULL,
|
|
857
951
|
model TEXT NOT NULL,
|
|
858
952
|
client_model TEXT,
|
|
@@ -864,7 +958,10 @@ async function ensureSchema(db) {
|
|
|
864
958
|
cached_tokens INTEGER,
|
|
865
959
|
ttft_ms INTEGER,
|
|
866
960
|
tpot_ms REAL,
|
|
867
|
-
error TEXT
|
|
961
|
+
error TEXT,
|
|
962
|
+
api_key_id INTEGER,
|
|
963
|
+
api_key_name TEXT,
|
|
964
|
+
api_key_value TEXT
|
|
868
965
|
);
|
|
869
966
|
|
|
870
967
|
CREATE TABLE IF NOT EXISTS request_payloads (
|
|
@@ -875,11 +972,43 @@ async function ensureSchema(db) {
|
|
|
875
972
|
);
|
|
876
973
|
|
|
877
974
|
CREATE TABLE IF NOT EXISTS daily_metrics (
|
|
878
|
-
date TEXT
|
|
975
|
+
date TEXT NOT NULL,
|
|
976
|
+
endpoint TEXT NOT NULL DEFAULT 'anthropic',
|
|
879
977
|
request_count INTEGER DEFAULT 0,
|
|
880
978
|
total_input_tokens INTEGER DEFAULT 0,
|
|
881
979
|
total_output_tokens INTEGER DEFAULT 0,
|
|
882
|
-
total_latency_ms INTEGER DEFAULT 0
|
|
980
|
+
total_latency_ms INTEGER DEFAULT 0,
|
|
981
|
+
PRIMARY KEY (date, endpoint)
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
985
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
986
|
+
name TEXT NOT NULL,
|
|
987
|
+
description TEXT,
|
|
988
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
989
|
+
key_ciphertext TEXT,
|
|
990
|
+
key_prefix TEXT,
|
|
991
|
+
key_suffix TEXT,
|
|
992
|
+
is_wildcard INTEGER DEFAULT 0,
|
|
993
|
+
enabled INTEGER DEFAULT 1,
|
|
994
|
+
created_at INTEGER NOT NULL,
|
|
995
|
+
updated_at INTEGER,
|
|
996
|
+
last_used_at INTEGER,
|
|
997
|
+
request_count INTEGER DEFAULT 0,
|
|
998
|
+
total_input_tokens INTEGER DEFAULT 0,
|
|
999
|
+
total_output_tokens INTEGER DEFAULT 0
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
CREATE TABLE IF NOT EXISTS api_key_audit_logs (
|
|
1003
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1004
|
+
api_key_id INTEGER,
|
|
1005
|
+
api_key_name TEXT,
|
|
1006
|
+
operation TEXT NOT NULL,
|
|
1007
|
+
operator TEXT,
|
|
1008
|
+
details TEXT,
|
|
1009
|
+
ip_address TEXT,
|
|
1010
|
+
created_at TEXT NOT NULL,
|
|
1011
|
+
FOREIGN KEY(api_key_id) REFERENCES api_keys(id) ON DELETE SET NULL
|
|
883
1012
|
);`
|
|
884
1013
|
);
|
|
885
1014
|
await maybeAddColumn(db, "request_logs", "client_model", "TEXT");
|
|
@@ -887,6 +1016,36 @@ async function ensureSchema(db) {
|
|
|
887
1016
|
await maybeAddColumn(db, "request_logs", "ttft_ms", "INTEGER");
|
|
888
1017
|
await maybeAddColumn(db, "request_logs", "tpot_ms", "REAL");
|
|
889
1018
|
await maybeAddColumn(db, "request_logs", "stream", "INTEGER");
|
|
1019
|
+
await maybeAddColumn(db, "request_logs", "endpoint", "TEXT DEFAULT 'anthropic'");
|
|
1020
|
+
await maybeAddColumn(db, "request_logs", "api_key_id", "INTEGER");
|
|
1021
|
+
await maybeAddColumn(db, "request_logs", "api_key_name", "TEXT");
|
|
1022
|
+
await maybeAddColumn(db, "request_logs", "api_key_value", "TEXT");
|
|
1023
|
+
const hasKeyHash = await columnExists(db, "api_keys", "key_hash");
|
|
1024
|
+
if (!hasKeyHash) {
|
|
1025
|
+
await run(db, "ALTER TABLE api_keys ADD COLUMN key_hash TEXT");
|
|
1026
|
+
}
|
|
1027
|
+
await maybeAddColumn(db, "api_keys", "key_ciphertext", "TEXT");
|
|
1028
|
+
await maybeAddColumn(db, "api_keys", "key_prefix", "TEXT");
|
|
1029
|
+
await maybeAddColumn(db, "api_keys", "key_suffix", "TEXT");
|
|
1030
|
+
await maybeAddColumn(db, "api_keys", "updated_at", "INTEGER");
|
|
1031
|
+
await maybeAddColumn(db, "api_keys", "last_used_at", "INTEGER");
|
|
1032
|
+
await maybeAddColumn(db, "api_keys", "description", "TEXT");
|
|
1033
|
+
await maybeAddColumn(db, "api_keys", "request_count", "INTEGER DEFAULT 0");
|
|
1034
|
+
await maybeAddColumn(db, "api_keys", "total_input_tokens", "INTEGER DEFAULT 0");
|
|
1035
|
+
await maybeAddColumn(db, "api_keys", "total_output_tokens", "INTEGER DEFAULT 0");
|
|
1036
|
+
await migrateDailyMetricsTable(db);
|
|
1037
|
+
await run(db, "CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash) WHERE key_hash IS NOT NULL");
|
|
1038
|
+
await run(db, "UPDATE api_keys SET key_hash = '*' WHERE is_wildcard = 1 AND (key_hash IS NULL OR key_hash = '')");
|
|
1039
|
+
await run(db, "UPDATE api_keys SET updated_at = created_at WHERE updated_at IS NULL");
|
|
1040
|
+
const wildcardRow = await get(db, "SELECT COUNT(*) as count FROM api_keys WHERE is_wildcard = 1");
|
|
1041
|
+
if (!wildcardRow || wildcardRow.count === 0) {
|
|
1042
|
+
const now = Date.now();
|
|
1043
|
+
await run(
|
|
1044
|
+
db,
|
|
1045
|
+
"INSERT INTO api_keys (name, description, key_hash, is_wildcard, enabled, created_at, updated_at) VALUES (?, ?, ?, 1, 1, ?, ?)",
|
|
1046
|
+
["Any Key", null, "*", now, now]
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
890
1049
|
}
|
|
891
1050
|
async function getDb() {
|
|
892
1051
|
if (dbInstance) {
|
|
@@ -961,12 +1120,14 @@ function decompressPayload(value) {
|
|
|
961
1120
|
async function recordLog(entry) {
|
|
962
1121
|
const result = await runQuery(
|
|
963
1122
|
`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
|
-
|
|
1123
|
+
timestamp, session_id, endpoint, provider, model, client_model, stream,
|
|
1124
|
+
latency_ms, status_code, input_tokens, output_tokens, cached_tokens, error,
|
|
1125
|
+
api_key_id, api_key_name, api_key_value
|
|
1126
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
967
1127
|
[
|
|
968
1128
|
entry.timestamp,
|
|
969
1129
|
entry.sessionId ?? null,
|
|
1130
|
+
entry.endpoint,
|
|
970
1131
|
entry.provider,
|
|
971
1132
|
entry.model,
|
|
972
1133
|
entry.clientModel ?? null,
|
|
@@ -976,7 +1137,10 @@ async function recordLog(entry) {
|
|
|
976
1137
|
entry.inputTokens ?? null,
|
|
977
1138
|
entry.outputTokens ?? null,
|
|
978
1139
|
entry.cachedTokens ?? null,
|
|
979
|
-
entry.error ?? null
|
|
1140
|
+
entry.error ?? null,
|
|
1141
|
+
entry.apiKeyId ?? null,
|
|
1142
|
+
entry.apiKeyName ?? null,
|
|
1143
|
+
entry.apiKeyValue ?? null
|
|
980
1144
|
]
|
|
981
1145
|
);
|
|
982
1146
|
return Number(result.lastID);
|
|
@@ -1038,17 +1202,18 @@ async function upsertLogPayload(requestId, payload) {
|
|
|
1038
1202
|
[requestId, promptData, responseData]
|
|
1039
1203
|
);
|
|
1040
1204
|
}
|
|
1041
|
-
async function updateMetrics(date, delta) {
|
|
1205
|
+
async function updateMetrics(date, endpoint, delta) {
|
|
1042
1206
|
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
|
|
1207
|
+
`INSERT INTO daily_metrics (date, endpoint, request_count, total_input_tokens, total_output_tokens, total_latency_ms)
|
|
1208
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1209
|
+
ON CONFLICT(date, endpoint) DO UPDATE SET
|
|
1046
1210
|
request_count = daily_metrics.request_count + excluded.request_count,
|
|
1047
1211
|
total_input_tokens = daily_metrics.total_input_tokens + excluded.total_input_tokens,
|
|
1048
1212
|
total_output_tokens = daily_metrics.total_output_tokens + excluded.total_output_tokens,
|
|
1049
1213
|
total_latency_ms = daily_metrics.total_latency_ms + excluded.total_latency_ms`,
|
|
1050
1214
|
[
|
|
1051
1215
|
date,
|
|
1216
|
+
endpoint,
|
|
1052
1217
|
delta.requests,
|
|
1053
1218
|
delta.inputTokens,
|
|
1054
1219
|
delta.outputTokens,
|
|
@@ -1071,6 +1236,358 @@ function getActiveRequestCount() {
|
|
|
1071
1236
|
return activeRequests;
|
|
1072
1237
|
}
|
|
1073
1238
|
|
|
1239
|
+
// api-keys/service.ts
|
|
1240
|
+
import { randomBytes as randomBytes2, createHash } from "crypto";
|
|
1241
|
+
|
|
1242
|
+
// security/encryption.ts
|
|
1243
|
+
import fs3 from "fs";
|
|
1244
|
+
import os3 from "os";
|
|
1245
|
+
import path3 from "path";
|
|
1246
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
1247
|
+
var HOME_OVERRIDE3 = process.env.CC_GW_HOME;
|
|
1248
|
+
var HOME_DIR3 = path3.resolve(HOME_OVERRIDE3 ?? path3.join(os3.homedir(), ".cc-gw"));
|
|
1249
|
+
var KEY_PATH = path3.join(HOME_DIR3, "encryption.key");
|
|
1250
|
+
var KEY_LENGTH = 32;
|
|
1251
|
+
var KEY_FILE_MODE = 384;
|
|
1252
|
+
var cachedKey = null;
|
|
1253
|
+
function writeKeyFile(buffer) {
|
|
1254
|
+
fs3.mkdirSync(HOME_DIR3, { recursive: true });
|
|
1255
|
+
fs3.writeFileSync(KEY_PATH, buffer.toString("base64"), { encoding: "utf8", mode: KEY_FILE_MODE });
|
|
1256
|
+
}
|
|
1257
|
+
function decodeKeyContent(content) {
|
|
1258
|
+
const trimmed = content.trim();
|
|
1259
|
+
if (!trimmed) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
const tryBase64 = (() => {
|
|
1263
|
+
try {
|
|
1264
|
+
const decoded = Buffer.from(trimmed, "base64");
|
|
1265
|
+
return decoded.length === KEY_LENGTH ? decoded : null;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
})();
|
|
1270
|
+
if (tryBase64)
|
|
1271
|
+
return tryBase64;
|
|
1272
|
+
if (/^[0-9a-fA-F]+$/.test(trimmed) && trimmed.length === KEY_LENGTH * 2) {
|
|
1273
|
+
const decoded = Buffer.from(trimmed, "hex");
|
|
1274
|
+
if (decoded.length === KEY_LENGTH) {
|
|
1275
|
+
return decoded;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (trimmed.length === KEY_LENGTH) {
|
|
1279
|
+
const ascii = Buffer.from(trimmed, "utf8");
|
|
1280
|
+
if (ascii.length === KEY_LENGTH) {
|
|
1281
|
+
return ascii;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
function ensureKeyMaterial() {
|
|
1287
|
+
if (cachedKey) {
|
|
1288
|
+
return cachedKey;
|
|
1289
|
+
}
|
|
1290
|
+
fs3.mkdirSync(HOME_DIR3, { recursive: true });
|
|
1291
|
+
if (!fs3.existsSync(KEY_PATH)) {
|
|
1292
|
+
const generated = randomBytes(KEY_LENGTH);
|
|
1293
|
+
writeKeyFile(generated);
|
|
1294
|
+
cachedKey = generated;
|
|
1295
|
+
return generated;
|
|
1296
|
+
}
|
|
1297
|
+
const content = fs3.readFileSync(KEY_PATH, "utf8");
|
|
1298
|
+
const decoded = decodeKeyContent(content);
|
|
1299
|
+
if (decoded) {
|
|
1300
|
+
cachedKey = decoded;
|
|
1301
|
+
return decoded;
|
|
1302
|
+
}
|
|
1303
|
+
const regenerated = randomBytes(KEY_LENGTH);
|
|
1304
|
+
writeKeyFile(regenerated);
|
|
1305
|
+
cachedKey = regenerated;
|
|
1306
|
+
console.info("[cc-gw][encryption] regenerated encryption key due to invalid file format");
|
|
1307
|
+
return regenerated;
|
|
1308
|
+
}
|
|
1309
|
+
function encryptSecret(value) {
|
|
1310
|
+
if (value === null || value === void 0) {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const key = ensureKeyMaterial();
|
|
1314
|
+
const iv = randomBytes(12);
|
|
1315
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
1316
|
+
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
1317
|
+
const tag = cipher.getAuthTag();
|
|
1318
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
1319
|
+
}
|
|
1320
|
+
function decryptSecret(payload) {
|
|
1321
|
+
if (!payload) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
const buffer = Buffer.from(payload, "base64");
|
|
1326
|
+
if (buffer.length <= 28) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
const iv = buffer.subarray(0, 12);
|
|
1330
|
+
const tag = buffer.subarray(12, 28);
|
|
1331
|
+
const ciphertext = buffer.subarray(28);
|
|
1332
|
+
const decipher = createDecipheriv("aes-256-gcm", ensureKeyMaterial(), iv);
|
|
1333
|
+
decipher.setAuthTag(tag);
|
|
1334
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1335
|
+
return decrypted.toString("utf8");
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.warn("[cc-gw][encryption] failed to decrypt payload", error);
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// api-keys/service.ts
|
|
1343
|
+
var apiKeysHasUpdatedAt = null;
|
|
1344
|
+
async function ensureApiKeysMetadataLoaded() {
|
|
1345
|
+
if (apiKeysHasUpdatedAt !== null) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const info = await getAll("PRAGMA table_info(api_keys)");
|
|
1349
|
+
apiKeysHasUpdatedAt = info.some((row) => row.name === "updated_at");
|
|
1350
|
+
}
|
|
1351
|
+
function toIsoOrNull(value) {
|
|
1352
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1353
|
+
try {
|
|
1354
|
+
return new Date(value).toISOString();
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
var ApiKeyError = class extends Error {
|
|
1361
|
+
constructor(message, code) {
|
|
1362
|
+
super(message);
|
|
1363
|
+
this.code = code;
|
|
1364
|
+
this.name = "ApiKeyError";
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
var KEY_PREFIX = "sk-ccgw-";
|
|
1368
|
+
function hashKey(value) {
|
|
1369
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1370
|
+
}
|
|
1371
|
+
function maskKey(prefix, suffix) {
|
|
1372
|
+
if (!prefix && !suffix) {
|
|
1373
|
+
return "********";
|
|
1374
|
+
}
|
|
1375
|
+
const safePrefix = prefix ?? "";
|
|
1376
|
+
const safeSuffix = suffix ?? "";
|
|
1377
|
+
return `${safePrefix}****${safeSuffix}`;
|
|
1378
|
+
}
|
|
1379
|
+
async function recordAuditLog(payload) {
|
|
1380
|
+
const { apiKeyId = null, apiKeyName = null, operation, operator = null, details = null, ipAddress = null } = payload;
|
|
1381
|
+
const serializedDetails = details ? JSON.stringify(details) : null;
|
|
1382
|
+
await runQuery(
|
|
1383
|
+
`INSERT INTO api_key_audit_logs (api_key_id, api_key_name, operation, operator, details, ip_address, created_at)
|
|
1384
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1385
|
+
[apiKeyId, apiKeyName, operation, operator, serializedDetails, ipAddress, (/* @__PURE__ */ new Date()).toISOString()]
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
function generateKey() {
|
|
1389
|
+
const randomPart = randomBytes2(24).toString("base64url");
|
|
1390
|
+
const key = `${KEY_PREFIX}${randomPart}`;
|
|
1391
|
+
return {
|
|
1392
|
+
key,
|
|
1393
|
+
prefix: key.slice(0, 6),
|
|
1394
|
+
suffix: key.slice(-4)
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
async function listApiKeys() {
|
|
1398
|
+
const rows = await getAll("SELECT id, name, description, key_prefix, key_suffix, is_wildcard, enabled, created_at, last_used_at, request_count, total_input_tokens, total_output_tokens FROM api_keys ORDER BY is_wildcard DESC, created_at DESC");
|
|
1399
|
+
return rows.map((row) => ({
|
|
1400
|
+
id: row.id,
|
|
1401
|
+
name: row.name,
|
|
1402
|
+
description: row.description ?? null,
|
|
1403
|
+
maskedKey: row.is_wildcard ? null : maskKey(row.key_prefix, row.key_suffix),
|
|
1404
|
+
isWildcard: Boolean(row.is_wildcard),
|
|
1405
|
+
enabled: Boolean(row.enabled),
|
|
1406
|
+
createdAt: toIsoOrNull(row.created_at),
|
|
1407
|
+
lastUsedAt: toIsoOrNull(row.last_used_at),
|
|
1408
|
+
requestCount: row.request_count ?? 0,
|
|
1409
|
+
totalInputTokens: row.total_input_tokens ?? 0,
|
|
1410
|
+
totalOutputTokens: row.total_output_tokens ?? 0
|
|
1411
|
+
}));
|
|
1412
|
+
}
|
|
1413
|
+
async function createApiKey(name, description, context) {
|
|
1414
|
+
const trimmed = name.trim();
|
|
1415
|
+
if (!trimmed) {
|
|
1416
|
+
throw new Error("Name is required");
|
|
1417
|
+
}
|
|
1418
|
+
if (trimmed.length > 100) {
|
|
1419
|
+
throw new Error("Name too long (max 100 characters)");
|
|
1420
|
+
}
|
|
1421
|
+
const trimmedDescription = typeof description === "string" ? description.trim() : "";
|
|
1422
|
+
if (trimmedDescription.length > 500) {
|
|
1423
|
+
throw new Error("Description too long (max 500 characters)");
|
|
1424
|
+
}
|
|
1425
|
+
await ensureApiKeysMetadataLoaded();
|
|
1426
|
+
const { key, prefix, suffix } = generateKey();
|
|
1427
|
+
const hashed = hashKey(key);
|
|
1428
|
+
const encrypted = encryptSecret(key);
|
|
1429
|
+
const now = Date.now();
|
|
1430
|
+
const columns = ["name", "description", "key_hash", "key_ciphertext", "key_prefix", "key_suffix", "is_wildcard", "enabled", "created_at"];
|
|
1431
|
+
const placeholders = ["?", "?", "?", "?", "?", "?", "?", "?", "?"];
|
|
1432
|
+
const values = [trimmed, trimmedDescription || null, hashed, encrypted, prefix, suffix, 0, 1, now];
|
|
1433
|
+
if (apiKeysHasUpdatedAt) {
|
|
1434
|
+
columns.push("updated_at");
|
|
1435
|
+
placeholders.push("?");
|
|
1436
|
+
values.push(now);
|
|
1437
|
+
}
|
|
1438
|
+
const result = await runQuery(
|
|
1439
|
+
`INSERT INTO api_keys (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`,
|
|
1440
|
+
values
|
|
1441
|
+
);
|
|
1442
|
+
await recordAuditLog({
|
|
1443
|
+
apiKeyId: result.lastID,
|
|
1444
|
+
apiKeyName: trimmed,
|
|
1445
|
+
operation: "create",
|
|
1446
|
+
operator: context?.operator ?? null,
|
|
1447
|
+
ipAddress: context?.ipAddress ?? null
|
|
1448
|
+
});
|
|
1449
|
+
return {
|
|
1450
|
+
id: result.lastID,
|
|
1451
|
+
key,
|
|
1452
|
+
name: trimmed,
|
|
1453
|
+
description: trimmedDescription || null,
|
|
1454
|
+
createdAt: new Date(now).toISOString()
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
async function setApiKeyEnabled(id, enabled, context) {
|
|
1458
|
+
await ensureApiKeysMetadataLoaded();
|
|
1459
|
+
const existing = await getOne("SELECT id, name, is_wildcard, enabled FROM api_keys WHERE id = ?", [id]);
|
|
1460
|
+
if (!existing) {
|
|
1461
|
+
throw new Error("API key not found");
|
|
1462
|
+
}
|
|
1463
|
+
if (apiKeysHasUpdatedAt) {
|
|
1464
|
+
await runQuery("UPDATE api_keys SET enabled = ?, updated_at = ? WHERE id = ?", [enabled ? 1 : 0, Date.now(), id]);
|
|
1465
|
+
} else {
|
|
1466
|
+
await runQuery("UPDATE api_keys SET enabled = ? WHERE id = ?", [enabled ? 1 : 0, id]);
|
|
1467
|
+
}
|
|
1468
|
+
await recordAuditLog({
|
|
1469
|
+
apiKeyId: existing.id,
|
|
1470
|
+
apiKeyName: existing.name,
|
|
1471
|
+
operation: enabled ? "enable" : "disable",
|
|
1472
|
+
operator: context?.operator ?? null,
|
|
1473
|
+
ipAddress: context?.ipAddress ?? null
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
async function deleteApiKey(id, context) {
|
|
1477
|
+
const existing = await getOne("SELECT id, name, is_wildcard FROM api_keys WHERE id = ?", [id]);
|
|
1478
|
+
if (!existing) {
|
|
1479
|
+
throw new Error("API key not found");
|
|
1480
|
+
}
|
|
1481
|
+
if (existing.is_wildcard) {
|
|
1482
|
+
throw new Error("Cannot delete wildcard key");
|
|
1483
|
+
}
|
|
1484
|
+
await runQuery("DELETE FROM api_keys WHERE id = ?", [id]);
|
|
1485
|
+
await recordAuditLog({
|
|
1486
|
+
apiKeyId: existing.id,
|
|
1487
|
+
apiKeyName: existing.name,
|
|
1488
|
+
operation: "delete",
|
|
1489
|
+
operator: context?.operator ?? null,
|
|
1490
|
+
ipAddress: context?.ipAddress ?? null
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
async function fetchWildcard() {
|
|
1494
|
+
const wildcard = await getOne("SELECT id, name, enabled FROM api_keys WHERE is_wildcard = 1 LIMIT 1");
|
|
1495
|
+
return wildcard ?? null;
|
|
1496
|
+
}
|
|
1497
|
+
async function resolveApiKey(providedRaw, context) {
|
|
1498
|
+
const provided = providedRaw?.trim() ?? "";
|
|
1499
|
+
const wildcard = await fetchWildcard();
|
|
1500
|
+
if (!provided) {
|
|
1501
|
+
if (wildcard && wildcard.enabled) {
|
|
1502
|
+
return {
|
|
1503
|
+
id: wildcard.id,
|
|
1504
|
+
name: wildcard.name,
|
|
1505
|
+
isWildcard: true,
|
|
1506
|
+
providedKey: ""
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
await recordAuditLog({
|
|
1510
|
+
operation: "auth_failure",
|
|
1511
|
+
details: { reason: "missing" },
|
|
1512
|
+
ipAddress: context?.ipAddress ?? null
|
|
1513
|
+
});
|
|
1514
|
+
throw new ApiKeyError("API key is required", "missing");
|
|
1515
|
+
}
|
|
1516
|
+
const hashed = hashKey(provided);
|
|
1517
|
+
const existing = await getOne("SELECT id, name, enabled, is_wildcard FROM api_keys WHERE key_hash = ?", [hashed]);
|
|
1518
|
+
if (existing) {
|
|
1519
|
+
if (!existing.enabled) {
|
|
1520
|
+
await recordAuditLog({
|
|
1521
|
+
apiKeyId: existing.id,
|
|
1522
|
+
apiKeyName: existing.name,
|
|
1523
|
+
operation: "auth_failure",
|
|
1524
|
+
details: { reason: "disabled" },
|
|
1525
|
+
ipAddress: context?.ipAddress ?? null
|
|
1526
|
+
});
|
|
1527
|
+
throw new ApiKeyError("API key is disabled", "disabled");
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
id: existing.id,
|
|
1531
|
+
name: existing.name,
|
|
1532
|
+
isWildcard: Boolean(existing.is_wildcard),
|
|
1533
|
+
providedKey: provided
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (wildcard && wildcard.enabled) {
|
|
1537
|
+
return {
|
|
1538
|
+
id: wildcard.id,
|
|
1539
|
+
name: wildcard.name,
|
|
1540
|
+
isWildcard: true,
|
|
1541
|
+
providedKey: provided
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
await recordAuditLog({
|
|
1545
|
+
operation: "auth_failure",
|
|
1546
|
+
details: { reason: "invalid", hash: hashed.slice(0, 16) },
|
|
1547
|
+
ipAddress: context?.ipAddress ?? null
|
|
1548
|
+
});
|
|
1549
|
+
throw new ApiKeyError("Invalid API key provided", "invalid");
|
|
1550
|
+
}
|
|
1551
|
+
async function recordApiKeyUsage(id, delta) {
|
|
1552
|
+
const now = Date.now();
|
|
1553
|
+
await ensureApiKeysMetadataLoaded();
|
|
1554
|
+
if (apiKeysHasUpdatedAt) {
|
|
1555
|
+
await runQuery(
|
|
1556
|
+
`UPDATE api_keys
|
|
1557
|
+
SET last_used_at = ?,
|
|
1558
|
+
request_count = COALESCE(request_count, 0) + 1,
|
|
1559
|
+
total_input_tokens = COALESCE(total_input_tokens, 0) + ?,
|
|
1560
|
+
total_output_tokens = COALESCE(total_output_tokens, 0) + ?,
|
|
1561
|
+
updated_at = ?
|
|
1562
|
+
WHERE id = ?`,
|
|
1563
|
+
[now, delta.inputTokens, delta.outputTokens, now, id]
|
|
1564
|
+
);
|
|
1565
|
+
} else {
|
|
1566
|
+
await runQuery(
|
|
1567
|
+
`UPDATE api_keys
|
|
1568
|
+
SET last_used_at = ?,
|
|
1569
|
+
request_count = COALESCE(request_count, 0) + 1,
|
|
1570
|
+
total_input_tokens = COALESCE(total_input_tokens, 0) + ?,
|
|
1571
|
+
total_output_tokens = COALESCE(total_output_tokens, 0) + ?
|
|
1572
|
+
WHERE id = ?`,
|
|
1573
|
+
[now, delta.inputTokens, delta.outputTokens, id]
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function decryptApiKeyValue(value) {
|
|
1578
|
+
return decryptSecret(value);
|
|
1579
|
+
}
|
|
1580
|
+
async function ensureWildcardMetadata() {
|
|
1581
|
+
const wildcard = await fetchWildcard();
|
|
1582
|
+
if (!wildcard) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
await runQuery(
|
|
1586
|
+
"UPDATE api_keys SET key_prefix = COALESCE(key_prefix, ?), key_suffix = COALESCE(key_suffix, ?) WHERE id = ?",
|
|
1587
|
+
["WILD", "KEY", wildcard.id]
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1074
1591
|
// routes/messages.ts
|
|
1075
1592
|
function mapStopReason(reason) {
|
|
1076
1593
|
switch (reason) {
|
|
@@ -1235,12 +1752,60 @@ function buildClaudeResponse(openAI, model) {
|
|
|
1235
1752
|
};
|
|
1236
1753
|
}
|
|
1237
1754
|
async function registerMessagesRoute(app) {
|
|
1238
|
-
|
|
1755
|
+
const handler = async (request, reply) => {
|
|
1239
1756
|
const payload = request.body;
|
|
1240
1757
|
if (!payload || typeof payload !== "object") {
|
|
1241
1758
|
reply.code(400);
|
|
1242
1759
|
return { error: "Invalid request body" };
|
|
1243
1760
|
}
|
|
1761
|
+
const resolveHeaderValue = (value) => {
|
|
1762
|
+
if (!value)
|
|
1763
|
+
return void 0;
|
|
1764
|
+
if (typeof value === "string")
|
|
1765
|
+
return value;
|
|
1766
|
+
if (Array.isArray(value)) {
|
|
1767
|
+
const found = value.find((item) => typeof item === "string" && item.trim().length > 0);
|
|
1768
|
+
return found;
|
|
1769
|
+
}
|
|
1770
|
+
return void 0;
|
|
1771
|
+
};
|
|
1772
|
+
let providedApiKey = resolveHeaderValue(request.headers["x-api-key"]);
|
|
1773
|
+
if (!providedApiKey) {
|
|
1774
|
+
const authHeader = resolveHeaderValue(request.headers["authorization"]);
|
|
1775
|
+
if (authHeader && authHeader.toLowerCase().startsWith("bearer ")) {
|
|
1776
|
+
providedApiKey = authHeader.slice(7);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
let apiKeyContext;
|
|
1780
|
+
try {
|
|
1781
|
+
apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (error instanceof ApiKeyError) {
|
|
1784
|
+
reply.code(401);
|
|
1785
|
+
return {
|
|
1786
|
+
error: {
|
|
1787
|
+
code: "invalid_api_key",
|
|
1788
|
+
message: error.message
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
throw error;
|
|
1793
|
+
}
|
|
1794
|
+
const encryptedApiKeyValue = apiKeyContext.providedKey ? encryptSecret(apiKeyContext.providedKey) : null;
|
|
1795
|
+
let usageRecorded = false;
|
|
1796
|
+
const commitUsage = async (inputTokens, outputTokens) => {
|
|
1797
|
+
if (usageRecorded)
|
|
1798
|
+
return;
|
|
1799
|
+
usageRecorded = true;
|
|
1800
|
+
if (apiKeyContext.id) {
|
|
1801
|
+
const safeInput = Number.isFinite(inputTokens) ? inputTokens : 0;
|
|
1802
|
+
const safeOutput = Number.isFinite(outputTokens) ? outputTokens : 0;
|
|
1803
|
+
await recordApiKeyUsage(apiKeyContext.id, {
|
|
1804
|
+
inputTokens: safeInput,
|
|
1805
|
+
outputTokens: safeOutput
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1244
1809
|
const rawUrl = typeof request.raw?.url === "string" ? request.raw.url : request.url ?? "";
|
|
1245
1810
|
let querySuffix = null;
|
|
1246
1811
|
if (typeof rawUrl === "string" && rawUrl.includes("?")) {
|
|
@@ -1258,14 +1823,13 @@ async function registerMessagesRoute(app) {
|
|
|
1258
1823
|
requestedModel
|
|
1259
1824
|
});
|
|
1260
1825
|
const providerType = target.provider.type ?? "custom";
|
|
1261
|
-
const
|
|
1262
|
-
const supportsTools = modelDefinition?.capabilities?.tools === true;
|
|
1826
|
+
const supportsTools = (target.provider.type ?? "custom") !== "custom";
|
|
1263
1827
|
const supportsMetadata = providerType !== "custom";
|
|
1264
1828
|
let normalizedForProvider = supportsTools ? normalized : stripTooling(normalized);
|
|
1265
1829
|
if (!supportsMetadata) {
|
|
1266
1830
|
normalizedForProvider = stripMetadata(normalizedForProvider);
|
|
1267
1831
|
}
|
|
1268
|
-
const maxTokensOverride = payload.max_tokens ??
|
|
1832
|
+
const maxTokensOverride = payload.max_tokens ?? void 0;
|
|
1269
1833
|
const toolChoice = supportsTools ? payload.tool_choice : void 0;
|
|
1270
1834
|
const overrideTools = supportsTools ? payload.tools : void 0;
|
|
1271
1835
|
let providerBody;
|
|
@@ -1312,11 +1876,15 @@ async function registerMessagesRoute(app) {
|
|
|
1312
1876
|
const storePayloads = getConfig().storePayloads !== false;
|
|
1313
1877
|
const logId = await recordLog({
|
|
1314
1878
|
timestamp: requestStart,
|
|
1879
|
+
endpoint: "anthropic",
|
|
1315
1880
|
provider: target.providerId,
|
|
1316
1881
|
model: target.modelId,
|
|
1317
1882
|
clientModel: requestedModel,
|
|
1318
1883
|
sessionId: payload.metadata?.user_id,
|
|
1319
|
-
stream: normalized.stream
|
|
1884
|
+
stream: normalized.stream,
|
|
1885
|
+
apiKeyId: apiKeyContext.id,
|
|
1886
|
+
apiKeyName: apiKeyContext.name,
|
|
1887
|
+
apiKeyValue: encryptedApiKeyValue
|
|
1320
1888
|
});
|
|
1321
1889
|
incrementActiveRequests();
|
|
1322
1890
|
if (storePayloads) {
|
|
@@ -1379,6 +1947,7 @@ async function registerMessagesRoute(app) {
|
|
|
1379
1947
|
if (storePayloads) {
|
|
1380
1948
|
await upsertLogPayload(logId, { response: bodyText || null });
|
|
1381
1949
|
}
|
|
1950
|
+
await commitUsage(0, 0);
|
|
1382
1951
|
await finalize(upstream.status, errorText);
|
|
1383
1952
|
return { error: errorText };
|
|
1384
1953
|
}
|
|
@@ -1408,7 +1977,8 @@ async function registerMessagesRoute(app) {
|
|
|
1408
1977
|
ttftMs: latencyMs2,
|
|
1409
1978
|
tpotMs: computeTpot(latencyMs2, outputTokens2, { streaming: false })
|
|
1410
1979
|
});
|
|
1411
|
-
await
|
|
1980
|
+
await commitUsage(inputTokens2, outputTokens2);
|
|
1981
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "anthropic", {
|
|
1412
1982
|
requests: 1,
|
|
1413
1983
|
inputTokens: inputTokens2,
|
|
1414
1984
|
outputTokens: outputTokens2,
|
|
@@ -1453,7 +2023,8 @@ async function registerMessagesRoute(app) {
|
|
|
1453
2023
|
ttftMs: latencyMs,
|
|
1454
2024
|
tpotMs: computeTpot(latencyMs, outputTokens, { streaming: false })
|
|
1455
2025
|
});
|
|
1456
|
-
await
|
|
2026
|
+
await commitUsage(inputTokens, outputTokens);
|
|
2027
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "anthropic", {
|
|
1457
2028
|
requests: 1,
|
|
1458
2029
|
inputTokens,
|
|
1459
2030
|
outputTokens,
|
|
@@ -1476,6 +2047,7 @@ async function registerMessagesRoute(app) {
|
|
|
1476
2047
|
}
|
|
1477
2048
|
if (!upstream.body) {
|
|
1478
2049
|
reply.code(500);
|
|
2050
|
+
await commitUsage(0, 0);
|
|
1479
2051
|
await finalize(500, "Upstream returned empty body");
|
|
1480
2052
|
return { error: "Upstream returned empty body" };
|
|
1481
2053
|
}
|
|
@@ -1572,7 +2144,8 @@ async function registerMessagesRoute(app) {
|
|
|
1572
2144
|
ttftMs
|
|
1573
2145
|
})
|
|
1574
2146
|
});
|
|
1575
|
-
await
|
|
2147
|
+
await commitUsage(usagePrompt2, usageCompletion2);
|
|
2148
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "anthropic", {
|
|
1576
2149
|
requests: 1,
|
|
1577
2150
|
inputTokens: usagePrompt2,
|
|
1578
2151
|
outputTokens: usageCompletion2,
|
|
@@ -1696,7 +2269,8 @@ data: ${JSON.stringify(data)}
|
|
|
1696
2269
|
ttftMs
|
|
1697
2270
|
})
|
|
1698
2271
|
});
|
|
1699
|
-
await
|
|
2272
|
+
await commitUsage(finalPromptTokens, finalCompletionTokens);
|
|
2273
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "anthropic", {
|
|
1700
2274
|
requests: 1,
|
|
1701
2275
|
inputTokens: finalPromptTokens,
|
|
1702
2276
|
outputTokens: finalCompletionTokens,
|
|
@@ -1853,7 +2427,8 @@ data: ${JSON.stringify(data)}
|
|
|
1853
2427
|
ttftMs
|
|
1854
2428
|
})
|
|
1855
2429
|
});
|
|
1856
|
-
await
|
|
2430
|
+
await commitUsage(fallbackPrompt, fallbackCompletion);
|
|
2431
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "anthropic", {
|
|
1857
2432
|
requests: 1,
|
|
1858
2433
|
inputTokens: fallbackPrompt,
|
|
1859
2434
|
outputTokens: fallbackCompletion,
|
|
@@ -1885,6 +2460,7 @@ data: ${JSON.stringify(data)}
|
|
|
1885
2460
|
if (!reply.sent) {
|
|
1886
2461
|
reply.code(500);
|
|
1887
2462
|
}
|
|
2463
|
+
await commitUsage(0, 0);
|
|
1888
2464
|
await finalize(reply.statusCode >= 400 ? reply.statusCode : 500, message);
|
|
1889
2465
|
return { error: message };
|
|
1890
2466
|
} finally {
|
|
@@ -1893,7 +2469,593 @@ data: ${JSON.stringify(data)}
|
|
|
1893
2469
|
await finalize(reply.statusCode ?? 200, null);
|
|
1894
2470
|
}
|
|
1895
2471
|
}
|
|
1896
|
-
}
|
|
2472
|
+
};
|
|
2473
|
+
app.post("/v1/messages", handler);
|
|
2474
|
+
app.post("/anthropic/v1/messages", handler);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// protocol/normalize-openai.ts
|
|
2478
|
+
function coerceArray(value) {
|
|
2479
|
+
if (value == null)
|
|
2480
|
+
return [];
|
|
2481
|
+
return Array.isArray(value) ? value : [value];
|
|
2482
|
+
}
|
|
2483
|
+
function extractTextFromContent(content) {
|
|
2484
|
+
const textParts = [];
|
|
2485
|
+
const toolResults = [];
|
|
2486
|
+
const toolCalls = [];
|
|
2487
|
+
const blocks = coerceArray(content);
|
|
2488
|
+
for (const block of blocks) {
|
|
2489
|
+
if (!block || typeof block !== "object") {
|
|
2490
|
+
if (typeof block === "string") {
|
|
2491
|
+
textParts.push(block);
|
|
2492
|
+
}
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const type = block.type ?? block.kind ?? block.role;
|
|
2496
|
+
const textValue = typeof block.text === "string" ? block.text : typeof block.value === "string" ? block.value : typeof block.content === "string" ? block.content : "";
|
|
2497
|
+
if (type === "text" || type === "input_text" || type === "output_text") {
|
|
2498
|
+
if (textValue) {
|
|
2499
|
+
textParts.push(textValue);
|
|
2500
|
+
}
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
if (type === "tool_result" || type === "function_result") {
|
|
2504
|
+
toolResults.push({
|
|
2505
|
+
id: typeof block.tool_call_id === "string" ? block.tool_call_id : typeof block.id === "string" ? block.id : `tool_result_${Math.random().toString(36).slice(2)}`,
|
|
2506
|
+
name: typeof block.name === "string" ? block.name : void 0,
|
|
2507
|
+
content: block.result ?? block.output ?? block.content ?? textValue ?? null,
|
|
2508
|
+
cacheControl: block.cache_control
|
|
2509
|
+
});
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
if (type === "tool_use" || type === "function_call") {
|
|
2513
|
+
toolCalls.push({
|
|
2514
|
+
id: typeof block.id === "string" ? block.id : `tool_call_${Math.random().toString(36).slice(2)}`,
|
|
2515
|
+
name: typeof block.name === "string" ? block.name : block.function?.name ? block.function.name : "tool",
|
|
2516
|
+
arguments: block.arguments ?? block.input ?? block.function?.arguments ?? {},
|
|
2517
|
+
cacheControl: block.cache_control
|
|
2518
|
+
});
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return {
|
|
2523
|
+
text: textParts.join("\n"),
|
|
2524
|
+
toolResults: toolResults.length > 0 ? toolResults : void 0,
|
|
2525
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function mapInputToMessages(payload) {
|
|
2529
|
+
const messages = [];
|
|
2530
|
+
const systemParts = [];
|
|
2531
|
+
const inputItems = coerceArray(payload?.input ?? payload?.messages);
|
|
2532
|
+
for (const item of inputItems) {
|
|
2533
|
+
if (item == null)
|
|
2534
|
+
continue;
|
|
2535
|
+
if (typeof item === "string") {
|
|
2536
|
+
messages.push({
|
|
2537
|
+
role: "user",
|
|
2538
|
+
text: item
|
|
2539
|
+
});
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
if (typeof item !== "object")
|
|
2543
|
+
continue;
|
|
2544
|
+
const role = item.role === "assistant" || item.role === "system" ? item.role : item.role === "developer" ? "system" : "user";
|
|
2545
|
+
if (role === "system") {
|
|
2546
|
+
const parts = extractTextFromContent(item.content);
|
|
2547
|
+
if (parts.text) {
|
|
2548
|
+
systemParts.push(parts.text);
|
|
2549
|
+
}
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const { text, toolResults, toolCalls } = extractTextFromContent(item.content);
|
|
2553
|
+
const normalized = {
|
|
2554
|
+
role: role === "assistant" ? "assistant" : "user",
|
|
2555
|
+
text
|
|
2556
|
+
};
|
|
2557
|
+
if (role === "user" && toolResults) {
|
|
2558
|
+
normalized.toolResults = toolResults;
|
|
2559
|
+
}
|
|
2560
|
+
if (role === "assistant") {
|
|
2561
|
+
const inlineToolCalls = coerceArray(item.tool_calls);
|
|
2562
|
+
if (inlineToolCalls.length > 0) {
|
|
2563
|
+
normalized.toolCalls = inlineToolCalls.map((call) => ({
|
|
2564
|
+
id: typeof call.id === "string" ? call.id : `tool_call_${Math.random().toString(36).slice(2)}`,
|
|
2565
|
+
name: call.function?.name ?? call.name ?? "tool",
|
|
2566
|
+
arguments: (() => {
|
|
2567
|
+
if (typeof call.function?.arguments === "string") {
|
|
2568
|
+
try {
|
|
2569
|
+
return JSON.parse(call.function.arguments);
|
|
2570
|
+
} catch {
|
|
2571
|
+
return call.function.arguments;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return call.arguments ?? call.input ?? {};
|
|
2575
|
+
})(),
|
|
2576
|
+
cacheControl: call.cache_control
|
|
2577
|
+
}));
|
|
2578
|
+
} else if (toolCalls) {
|
|
2579
|
+
normalized.toolCalls = toolCalls;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
messages.push(normalized);
|
|
2583
|
+
}
|
|
2584
|
+
const extraInstructions = coerceArray(payload?.instructions);
|
|
2585
|
+
for (const instruction of extraInstructions) {
|
|
2586
|
+
if (typeof instruction === "string") {
|
|
2587
|
+
if (instruction.trim().length > 0) {
|
|
2588
|
+
systemParts.push(instruction.trim());
|
|
2589
|
+
}
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (instruction && typeof instruction === "object") {
|
|
2593
|
+
const parts = extractTextFromContent(instruction);
|
|
2594
|
+
if (parts.text) {
|
|
2595
|
+
systemParts.push(parts.text);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
const system = systemParts.length > 0 ? systemParts.join("\n\n") : null;
|
|
2600
|
+
return { messages, system };
|
|
2601
|
+
}
|
|
2602
|
+
function normalizeOpenAIResponsesPayload(payload) {
|
|
2603
|
+
const stream = Boolean(payload?.stream);
|
|
2604
|
+
const thinking = Boolean(payload?.reasoning ?? payload?.thinking);
|
|
2605
|
+
const { messages, system } = mapInputToMessages(payload);
|
|
2606
|
+
const toolsArray = coerceArray(payload?.tools);
|
|
2607
|
+
return {
|
|
2608
|
+
original: payload,
|
|
2609
|
+
system,
|
|
2610
|
+
messages,
|
|
2611
|
+
tools: toolsArray,
|
|
2612
|
+
stream,
|
|
2613
|
+
thinking
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// routes/openai.ts
|
|
2618
|
+
var OPENAI_DEBUG = process.env.CC_GW_DEBUG_OPENAI === "1";
|
|
2619
|
+
var debugLog = (...args) => {
|
|
2620
|
+
if (OPENAI_DEBUG) {
|
|
2621
|
+
console.info("[cc-gw][openai]", ...args);
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
var roundTwoDecimals2 = (value) => Math.round(value * 100) / 100;
|
|
2625
|
+
function computeTpot2(totalLatencyMs, outputTokens, options) {
|
|
2626
|
+
if (!Number.isFinite(outputTokens) || outputTokens <= 0) {
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
const streaming = options?.streaming ?? false;
|
|
2630
|
+
const ttftMs = options?.ttftMs ?? null;
|
|
2631
|
+
const reasoningTokens = options?.reasoningTokens ?? 0;
|
|
2632
|
+
const totalTokensHint = options?.totalTokens ?? null;
|
|
2633
|
+
let effectiveLatency = totalLatencyMs;
|
|
2634
|
+
if (streaming && ttftMs != null && totalLatencyMs > 0) {
|
|
2635
|
+
const ttftRatio = ttftMs / totalLatencyMs;
|
|
2636
|
+
if (reasoningTokens > 0) {
|
|
2637
|
+
effectiveLatency = totalLatencyMs;
|
|
2638
|
+
} else if (ttftRatio <= 0.2) {
|
|
2639
|
+
effectiveLatency = Math.max(totalLatencyMs - ttftMs, totalLatencyMs * 0.2);
|
|
2640
|
+
} else {
|
|
2641
|
+
effectiveLatency = totalLatencyMs;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
const raw = effectiveLatency / outputTokens;
|
|
2645
|
+
return Number.isFinite(raw) ? roundTwoDecimals2(raw) : null;
|
|
2646
|
+
}
|
|
2647
|
+
function resolveCachedTokens2(usage) {
|
|
2648
|
+
if (!usage || typeof usage !== "object") {
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2651
|
+
if (typeof usage.cached_tokens === "number") {
|
|
2652
|
+
return usage.cached_tokens;
|
|
2653
|
+
}
|
|
2654
|
+
const promptDetails = usage.prompt_tokens_details;
|
|
2655
|
+
if (promptDetails && typeof promptDetails.cached_tokens === "number") {
|
|
2656
|
+
return promptDetails.cached_tokens;
|
|
2657
|
+
}
|
|
2658
|
+
const inputDetails = usage.input_tokens_details;
|
|
2659
|
+
if (inputDetails && typeof inputDetails.cached_tokens === "number") {
|
|
2660
|
+
return inputDetails.cached_tokens;
|
|
2661
|
+
}
|
|
2662
|
+
if (typeof usage.cache_read_input_tokens === "number") {
|
|
2663
|
+
return usage.cache_read_input_tokens;
|
|
2664
|
+
}
|
|
2665
|
+
if (typeof usage.cache_creation_input_tokens === "number") {
|
|
2666
|
+
return usage.cache_creation_input_tokens;
|
|
2667
|
+
}
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
async function registerOpenAiRoutes(app) {
|
|
2671
|
+
const handleResponses = async (request, reply) => {
|
|
2672
|
+
const payload = request.body;
|
|
2673
|
+
if (!payload || typeof payload !== "object") {
|
|
2674
|
+
reply.code(400);
|
|
2675
|
+
return { error: "Invalid request body" };
|
|
2676
|
+
}
|
|
2677
|
+
debugLog("incoming request", {
|
|
2678
|
+
stream: Boolean(payload.stream),
|
|
2679
|
+
model: payload.model,
|
|
2680
|
+
hasToolChoice: Boolean(payload.tool_choice),
|
|
2681
|
+
toolsCount: Array.isArray(payload.tools) ? payload.tools.length : 0
|
|
2682
|
+
});
|
|
2683
|
+
const resolveHeaderValue = (value) => {
|
|
2684
|
+
if (!value)
|
|
2685
|
+
return void 0;
|
|
2686
|
+
if (typeof value === "string")
|
|
2687
|
+
return value;
|
|
2688
|
+
if (Array.isArray(value)) {
|
|
2689
|
+
const found = value.find((item) => typeof item === "string" && item.trim().length > 0);
|
|
2690
|
+
return found;
|
|
2691
|
+
}
|
|
2692
|
+
return void 0;
|
|
2693
|
+
};
|
|
2694
|
+
let providedApiKey = resolveHeaderValue(request.headers["authorization"]);
|
|
2695
|
+
if (providedApiKey && providedApiKey.toLowerCase().startsWith("bearer ")) {
|
|
2696
|
+
providedApiKey = providedApiKey.slice(7);
|
|
2697
|
+
}
|
|
2698
|
+
if (!providedApiKey) {
|
|
2699
|
+
providedApiKey = resolveHeaderValue(request.headers["x-api-key"]);
|
|
2700
|
+
}
|
|
2701
|
+
let apiKeyContext;
|
|
2702
|
+
try {
|
|
2703
|
+
apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
if (error instanceof ApiKeyError) {
|
|
2706
|
+
reply.code(401);
|
|
2707
|
+
return {
|
|
2708
|
+
error: {
|
|
2709
|
+
code: "invalid_api_key",
|
|
2710
|
+
message: error.message
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
throw error;
|
|
2715
|
+
}
|
|
2716
|
+
const encryptedApiKeyValue = apiKeyContext.providedKey ? encryptSecret(apiKeyContext.providedKey) : null;
|
|
2717
|
+
let usageRecorded = false;
|
|
2718
|
+
const commitUsage = async (inputTokens, outputTokens) => {
|
|
2719
|
+
if (usageRecorded)
|
|
2720
|
+
return;
|
|
2721
|
+
usageRecorded = true;
|
|
2722
|
+
if (apiKeyContext.id) {
|
|
2723
|
+
const safeInput = Number.isFinite(inputTokens) ? inputTokens : 0;
|
|
2724
|
+
const safeOutput = Number.isFinite(outputTokens) ? outputTokens : 0;
|
|
2725
|
+
await recordApiKeyUsage(apiKeyContext.id, {
|
|
2726
|
+
inputTokens: safeInput,
|
|
2727
|
+
outputTokens: safeOutput
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
const normalized = normalizeOpenAIResponsesPayload(payload);
|
|
2732
|
+
const requestedModel = typeof payload.model === "string" ? payload.model : void 0;
|
|
2733
|
+
const target = resolveRoute({
|
|
2734
|
+
payload: normalized,
|
|
2735
|
+
requestedModel,
|
|
2736
|
+
endpoint: "openai"
|
|
2737
|
+
});
|
|
2738
|
+
let connector;
|
|
2739
|
+
const providerType = target.provider.type ?? "openai";
|
|
2740
|
+
if (providerType === "openai") {
|
|
2741
|
+
connector = createOpenAIConnector(target.provider, { defaultPath: "v1/responses" });
|
|
2742
|
+
} else {
|
|
2743
|
+
connector = getConnector(target.providerId);
|
|
2744
|
+
}
|
|
2745
|
+
const requestStart = Date.now();
|
|
2746
|
+
const storePayloads = getConfig().storePayloads !== false;
|
|
2747
|
+
const logId = await recordLog({
|
|
2748
|
+
timestamp: requestStart,
|
|
2749
|
+
endpoint: "openai",
|
|
2750
|
+
provider: target.providerId,
|
|
2751
|
+
model: target.modelId,
|
|
2752
|
+
clientModel: requestedModel,
|
|
2753
|
+
sessionId: payload.metadata?.user_id ?? payload.user,
|
|
2754
|
+
stream: normalized.stream,
|
|
2755
|
+
apiKeyId: apiKeyContext.id,
|
|
2756
|
+
apiKeyName: apiKeyContext.name,
|
|
2757
|
+
apiKeyValue: encryptedApiKeyValue
|
|
2758
|
+
});
|
|
2759
|
+
if (storePayloads) {
|
|
2760
|
+
await upsertLogPayload(logId, {
|
|
2761
|
+
prompt: (() => {
|
|
2762
|
+
try {
|
|
2763
|
+
return JSON.stringify(payload);
|
|
2764
|
+
} catch {
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
})()
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
incrementActiveRequests();
|
|
2771
|
+
let finalized = false;
|
|
2772
|
+
const finalize = async (statusCode, error) => {
|
|
2773
|
+
if (finalized)
|
|
2774
|
+
return;
|
|
2775
|
+
await finalizeLog(logId, {
|
|
2776
|
+
latencyMs: Date.now() - requestStart,
|
|
2777
|
+
statusCode,
|
|
2778
|
+
error,
|
|
2779
|
+
clientModel: requestedModel ?? null
|
|
2780
|
+
});
|
|
2781
|
+
finalized = true;
|
|
2782
|
+
};
|
|
2783
|
+
try {
|
|
2784
|
+
const providerBody = { ...payload };
|
|
2785
|
+
providerBody.model = target.modelId;
|
|
2786
|
+
providerBody.stream = normalized.stream;
|
|
2787
|
+
if (providerBody.max_output_tokens == null && typeof providerBody.max_tokens === "number") {
|
|
2788
|
+
providerBody.max_output_tokens = providerBody.max_tokens;
|
|
2789
|
+
}
|
|
2790
|
+
delete providerBody.max_tokens;
|
|
2791
|
+
if (typeof providerBody.thinking === "boolean") {
|
|
2792
|
+
delete providerBody.thinking;
|
|
2793
|
+
}
|
|
2794
|
+
if (typeof providerBody.reasoning === "boolean") {
|
|
2795
|
+
delete providerBody.reasoning;
|
|
2796
|
+
}
|
|
2797
|
+
if (providerBody.tool_choice === void 0) {
|
|
2798
|
+
delete providerBody.tool_choice;
|
|
2799
|
+
}
|
|
2800
|
+
if (providerBody.tools === void 0) {
|
|
2801
|
+
delete providerBody.tools;
|
|
2802
|
+
}
|
|
2803
|
+
if (providerBody.response_format === void 0) {
|
|
2804
|
+
delete providerBody.response_format;
|
|
2805
|
+
}
|
|
2806
|
+
const upstream = await connector.send({
|
|
2807
|
+
model: target.modelId,
|
|
2808
|
+
body: providerBody,
|
|
2809
|
+
stream: normalized.stream
|
|
2810
|
+
});
|
|
2811
|
+
if (upstream.status >= 400) {
|
|
2812
|
+
reply.code(upstream.status);
|
|
2813
|
+
const bodyText = upstream.body ? await new Response(upstream.body).text() : "";
|
|
2814
|
+
const errorText = bodyText || "Upstream provider error";
|
|
2815
|
+
debugLog("upstream error", upstream.status, errorText.slice(0, 200));
|
|
2816
|
+
if (storePayloads) {
|
|
2817
|
+
await upsertLogPayload(logId, { response: bodyText || null });
|
|
2818
|
+
}
|
|
2819
|
+
await commitUsage(0, 0);
|
|
2820
|
+
await finalize(upstream.status, errorText);
|
|
2821
|
+
return { error: errorText };
|
|
2822
|
+
}
|
|
2823
|
+
if (!normalized.stream) {
|
|
2824
|
+
const rawBody = upstream.body ? await new Response(upstream.body).text() : "";
|
|
2825
|
+
if (storePayloads) {
|
|
2826
|
+
await upsertLogPayload(logId, { response: rawBody });
|
|
2827
|
+
}
|
|
2828
|
+
let parsed = null;
|
|
2829
|
+
try {
|
|
2830
|
+
parsed = rawBody ? JSON.parse(rawBody) : {};
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
await commitUsage(0, 0);
|
|
2833
|
+
await finalize(200, null);
|
|
2834
|
+
reply.header("content-type", "application/json");
|
|
2835
|
+
return rawBody;
|
|
2836
|
+
}
|
|
2837
|
+
const usagePayload = parsed?.usage ?? null;
|
|
2838
|
+
const inputTokens2 = usagePayload?.input_tokens ?? usagePayload?.prompt_tokens ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
|
|
2839
|
+
const baseOutputTokens = usagePayload?.output_tokens ?? usagePayload?.completion_tokens ?? (typeof parsed?.content === "string" ? estimateTokens(normalized, target.modelId) : 0);
|
|
2840
|
+
const reasoningTokens2 = (() => {
|
|
2841
|
+
const details = usagePayload?.completion_tokens_details;
|
|
2842
|
+
if (details && typeof details.reasoning_tokens === "number") {
|
|
2843
|
+
return details.reasoning_tokens;
|
|
2844
|
+
}
|
|
2845
|
+
if (typeof usagePayload?.reasoning_tokens === "number") {
|
|
2846
|
+
return usagePayload.reasoning_tokens;
|
|
2847
|
+
}
|
|
2848
|
+
return 0;
|
|
2849
|
+
})();
|
|
2850
|
+
const outputTokens2 = baseOutputTokens + reasoningTokens2;
|
|
2851
|
+
const cachedTokens = resolveCachedTokens2(usagePayload);
|
|
2852
|
+
const latencyMs2 = Date.now() - requestStart;
|
|
2853
|
+
await updateLogTokens(logId, {
|
|
2854
|
+
inputTokens: inputTokens2,
|
|
2855
|
+
outputTokens: outputTokens2,
|
|
2856
|
+
cachedTokens,
|
|
2857
|
+
ttftMs: usagePayload?.first_token_latency_ms ?? latencyMs2,
|
|
2858
|
+
tpotMs: usagePayload?.tokens_per_second ? computeTpot2(latencyMs2, outputTokens2, { streaming: false, reasoningTokens: reasoningTokens2 }) : null
|
|
2859
|
+
});
|
|
2860
|
+
await commitUsage(inputTokens2, outputTokens2);
|
|
2861
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "openai", {
|
|
2862
|
+
requests: 1,
|
|
2863
|
+
inputTokens: inputTokens2,
|
|
2864
|
+
outputTokens: outputTokens2,
|
|
2865
|
+
latencyMs: latencyMs2
|
|
2866
|
+
});
|
|
2867
|
+
await finalize(200, null);
|
|
2868
|
+
reply.header("content-type", "application/json");
|
|
2869
|
+
return parsed ?? {};
|
|
2870
|
+
}
|
|
2871
|
+
if (!upstream.body) {
|
|
2872
|
+
reply.code(500);
|
|
2873
|
+
await commitUsage(0, 0);
|
|
2874
|
+
await finalize(500, "Upstream returned empty body");
|
|
2875
|
+
return { error: "Upstream returned empty body" };
|
|
2876
|
+
}
|
|
2877
|
+
reply.raw.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
2878
|
+
reply.raw.setHeader("cache-control", "no-cache, no-transform");
|
|
2879
|
+
reply.raw.setHeader("connection", "keep-alive");
|
|
2880
|
+
reply.raw.setHeader("x-accel-buffering", "no");
|
|
2881
|
+
if (typeof reply.raw.writeHead === "function") {
|
|
2882
|
+
reply.raw.writeHead(200);
|
|
2883
|
+
}
|
|
2884
|
+
if (typeof reply.raw.flushHeaders === "function") {
|
|
2885
|
+
;
|
|
2886
|
+
reply.raw.flushHeaders();
|
|
2887
|
+
}
|
|
2888
|
+
const reader = upstream.body.getReader();
|
|
2889
|
+
const decoder = new TextDecoder();
|
|
2890
|
+
let buffer = "";
|
|
2891
|
+
let usagePrompt = null;
|
|
2892
|
+
let usageCompletion = null;
|
|
2893
|
+
let usageReasoning = null;
|
|
2894
|
+
let usageCached = null;
|
|
2895
|
+
let firstTokenAt = null;
|
|
2896
|
+
let chunkCount = 0;
|
|
2897
|
+
const capturedResponseChunks = storePayloads ? [] : null;
|
|
2898
|
+
const replyClosed = () => {
|
|
2899
|
+
debugLog("client connection closed before completion");
|
|
2900
|
+
};
|
|
2901
|
+
reply.raw.once("close", replyClosed);
|
|
2902
|
+
try {
|
|
2903
|
+
const selectMax = (candidates, current) => candidates.reduce((max, value) => {
|
|
2904
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
2905
|
+
return max == null || value > max ? value : max;
|
|
2906
|
+
}
|
|
2907
|
+
return max;
|
|
2908
|
+
}, current);
|
|
2909
|
+
const applyUsagePayload = (usagePayload) => {
|
|
2910
|
+
usagePrompt = selectMax(
|
|
2911
|
+
[
|
|
2912
|
+
typeof usagePayload.prompt_tokens === "number" ? usagePayload.prompt_tokens : null,
|
|
2913
|
+
typeof usagePayload.input_tokens === "number" ? usagePayload.input_tokens : null,
|
|
2914
|
+
typeof usagePayload.total_tokens === "number" && typeof usagePayload.completion_tokens === "number" ? usagePayload.total_tokens - usagePayload.completion_tokens : null
|
|
2915
|
+
],
|
|
2916
|
+
usagePrompt
|
|
2917
|
+
);
|
|
2918
|
+
const reasoningTokens2 = typeof usagePayload?.completion_tokens_details?.reasoning_tokens === "number" ? usagePayload.completion_tokens_details.reasoning_tokens : typeof usagePayload.reasoning_tokens === "number" ? usagePayload.reasoning_tokens : null;
|
|
2919
|
+
usageCompletion = selectMax(
|
|
2920
|
+
[
|
|
2921
|
+
typeof usagePayload.output_tokens === "number" ? usagePayload.output_tokens : null,
|
|
2922
|
+
typeof usagePayload.completion_tokens === "number" ? usagePayload.completion_tokens : null,
|
|
2923
|
+
typeof usagePayload.response_tokens === "number" ? usagePayload.response_tokens : null,
|
|
2924
|
+
typeof usagePayload.total_tokens === "number" && typeof usagePrompt === "number" ? usagePayload.total_tokens - usagePrompt : null,
|
|
2925
|
+
reasoningTokens2
|
|
2926
|
+
],
|
|
2927
|
+
usageCompletion
|
|
2928
|
+
);
|
|
2929
|
+
usageReasoning = selectMax(
|
|
2930
|
+
[reasoningTokens2],
|
|
2931
|
+
usageReasoning
|
|
2932
|
+
);
|
|
2933
|
+
if (usageCached == null) {
|
|
2934
|
+
usageCached = resolveCachedTokens2(usagePayload);
|
|
2935
|
+
}
|
|
2936
|
+
if (OPENAI_DEBUG) {
|
|
2937
|
+
debugLog("usage payload received", usagePayload);
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
while (true) {
|
|
2941
|
+
const { value, done } = await reader.read();
|
|
2942
|
+
if (value && !firstTokenAt) {
|
|
2943
|
+
firstTokenAt = Date.now();
|
|
2944
|
+
}
|
|
2945
|
+
if (value) {
|
|
2946
|
+
const chunk = decoder.decode(value, { stream: !done });
|
|
2947
|
+
if (OPENAI_DEBUG) {
|
|
2948
|
+
debugLog("sse chunk", chunk.length > 200 ? `${chunk.slice(0, 200)}\u2026` : chunk);
|
|
2949
|
+
}
|
|
2950
|
+
buffer += chunk;
|
|
2951
|
+
chunkCount += 1;
|
|
2952
|
+
reply.raw.write(chunk);
|
|
2953
|
+
if (capturedResponseChunks) {
|
|
2954
|
+
capturedResponseChunks.push(chunk);
|
|
2955
|
+
}
|
|
2956
|
+
while (true) {
|
|
2957
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
2958
|
+
if (newlineIndex === -1)
|
|
2959
|
+
break;
|
|
2960
|
+
const line = buffer.slice(0, newlineIndex);
|
|
2961
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
2962
|
+
const trimmed = line.trim();
|
|
2963
|
+
if (!trimmed.startsWith("data:"))
|
|
2964
|
+
continue;
|
|
2965
|
+
const dataStr = trimmed.slice(5).trim();
|
|
2966
|
+
if (dataStr === "[DONE]") {
|
|
2967
|
+
if (OPENAI_DEBUG) {
|
|
2968
|
+
debugLog("done marker received");
|
|
2969
|
+
}
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
try {
|
|
2973
|
+
const parsed = JSON.parse(dataStr);
|
|
2974
|
+
const usagePayload = parsed?.usage || parsed?.response?.usage || null;
|
|
2975
|
+
if (usagePayload) {
|
|
2976
|
+
applyUsagePayload(usagePayload);
|
|
2977
|
+
}
|
|
2978
|
+
} catch (parseError) {
|
|
2979
|
+
if (OPENAI_DEBUG) {
|
|
2980
|
+
debugLog("failed to parse SSE data line (possibly incomplete):", dataStr.slice(0, 100));
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (done) {
|
|
2986
|
+
if (buffer.length > 0) {
|
|
2987
|
+
const trimmed = buffer.trim();
|
|
2988
|
+
if (trimmed.startsWith("data:")) {
|
|
2989
|
+
const dataStr = trimmed.slice(5).trim();
|
|
2990
|
+
if (dataStr !== "[DONE]") {
|
|
2991
|
+
try {
|
|
2992
|
+
const parsed = JSON.parse(dataStr);
|
|
2993
|
+
const usagePayload = parsed?.usage || parsed?.response?.usage || null;
|
|
2994
|
+
if (usagePayload) {
|
|
2995
|
+
applyUsagePayload(usagePayload);
|
|
2996
|
+
}
|
|
2997
|
+
} catch {
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
break;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
} finally {
|
|
3006
|
+
reply.raw.end();
|
|
3007
|
+
reply.raw.removeListener("close", replyClosed);
|
|
3008
|
+
debugLog("stream finished", { chunkCount, usagePrompt, usageCompletion, usageReasoning, usageCached });
|
|
3009
|
+
if (capturedResponseChunks && capturedResponseChunks.length > 0) {
|
|
3010
|
+
try {
|
|
3011
|
+
await upsertLogPayload(logId, { response: capturedResponseChunks.join("") });
|
|
3012
|
+
} catch (error) {
|
|
3013
|
+
debugLog("failed to persist streamed payload", error);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const latencyMs = Date.now() - requestStart;
|
|
3018
|
+
const inputTokens = usagePrompt ?? usageCompletion ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
|
|
3019
|
+
const textOutputTokens = usageCompletion ?? 0;
|
|
3020
|
+
const reasoningTokens = usageReasoning ?? 0;
|
|
3021
|
+
const outputTokens = textOutputTokens + reasoningTokens;
|
|
3022
|
+
await updateLogTokens(logId, {
|
|
3023
|
+
inputTokens,
|
|
3024
|
+
outputTokens,
|
|
3025
|
+
cachedTokens: usageCached,
|
|
3026
|
+
ttftMs: firstTokenAt ? firstTokenAt - requestStart : null,
|
|
3027
|
+
tpotMs: computeTpot2(latencyMs, outputTokens, {
|
|
3028
|
+
streaming: true,
|
|
3029
|
+
ttftMs: firstTokenAt ? firstTokenAt - requestStart : null,
|
|
3030
|
+
reasoningTokens
|
|
3031
|
+
})
|
|
3032
|
+
});
|
|
3033
|
+
await commitUsage(inputTokens, outputTokens);
|
|
3034
|
+
await updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "openai", {
|
|
3035
|
+
requests: 1,
|
|
3036
|
+
inputTokens,
|
|
3037
|
+
outputTokens,
|
|
3038
|
+
latencyMs
|
|
3039
|
+
});
|
|
3040
|
+
await finalize(200, null);
|
|
3041
|
+
return reply;
|
|
3042
|
+
} catch (error) {
|
|
3043
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
3044
|
+
if (!reply.sent) {
|
|
3045
|
+
reply.code(500);
|
|
3046
|
+
}
|
|
3047
|
+
await commitUsage(0, 0);
|
|
3048
|
+
await finalize(reply.statusCode >= 400 ? reply.statusCode : 500, message);
|
|
3049
|
+
return { error: message };
|
|
3050
|
+
} finally {
|
|
3051
|
+
decrementActiveRequests();
|
|
3052
|
+
if (!finalized && reply.sent) {
|
|
3053
|
+
await finalize(reply.statusCode ?? 200, null);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
app.post("/openai/v1/responses", handleResponses);
|
|
3058
|
+
app.post("/openai/responses", handleResponses);
|
|
1897
3059
|
}
|
|
1898
3060
|
|
|
1899
3061
|
// logging/queries.ts
|
|
@@ -1906,6 +3068,10 @@ async function queryLogs(options = {}) {
|
|
|
1906
3068
|
conditions.push("provider = $provider");
|
|
1907
3069
|
params.$provider = options.provider;
|
|
1908
3070
|
}
|
|
3071
|
+
if (options.endpoint) {
|
|
3072
|
+
conditions.push("endpoint = $endpoint");
|
|
3073
|
+
params.$endpoint = options.endpoint;
|
|
3074
|
+
}
|
|
1909
3075
|
if (options.model) {
|
|
1910
3076
|
conditions.push("model = $model");
|
|
1911
3077
|
params.$model = options.model;
|
|
@@ -1923,15 +3089,24 @@ async function queryLogs(options = {}) {
|
|
|
1923
3089
|
conditions.push("timestamp <= $to");
|
|
1924
3090
|
params.$to = options.to;
|
|
1925
3091
|
}
|
|
3092
|
+
if (options.apiKeyIds && options.apiKeyIds.length > 0) {
|
|
3093
|
+
const placeholders = [];
|
|
3094
|
+
options.apiKeyIds.forEach((id, index) => {
|
|
3095
|
+
const key = `$apiKey${index}`;
|
|
3096
|
+
placeholders.push(key);
|
|
3097
|
+
params[key] = id;
|
|
3098
|
+
});
|
|
3099
|
+
conditions.push(`(api_key_id IN (${placeholders.join(", ")}))`);
|
|
3100
|
+
}
|
|
1926
3101
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1927
3102
|
const totalRow = await getOne(
|
|
1928
3103
|
`SELECT COUNT(*) AS count FROM request_logs ${whereClause}`,
|
|
1929
3104
|
params
|
|
1930
3105
|
);
|
|
1931
3106
|
const items = await getAll(
|
|
1932
|
-
`SELECT id, timestamp, session_id, provider, model, client_model,
|
|
3107
|
+
`SELECT id, timestamp, session_id, endpoint, provider, model, client_model,
|
|
1933
3108
|
stream, latency_ms, status_code, input_tokens, output_tokens,
|
|
1934
|
-
cached_tokens, ttft_ms, tpot_ms, error
|
|
3109
|
+
cached_tokens, ttft_ms, tpot_ms, error, api_key_id, api_key_name, api_key_value
|
|
1935
3110
|
FROM request_logs
|
|
1936
3111
|
${whereClause}
|
|
1937
3112
|
ORDER BY timestamp DESC
|
|
@@ -1945,9 +3120,9 @@ async function queryLogs(options = {}) {
|
|
|
1945
3120
|
}
|
|
1946
3121
|
async function getLogDetail(id) {
|
|
1947
3122
|
const record = await getOne(
|
|
1948
|
-
`SELECT id, timestamp, session_id, provider, model, client_model,
|
|
3123
|
+
`SELECT id, timestamp, session_id, endpoint, provider, model, client_model,
|
|
1949
3124
|
stream, latency_ms, status_code, input_tokens, output_tokens,
|
|
1950
|
-
cached_tokens, ttft_ms, tpot_ms, error
|
|
3125
|
+
cached_tokens, ttft_ms, tpot_ms, error, api_key_id, api_key_name, api_key_value
|
|
1951
3126
|
FROM request_logs
|
|
1952
3127
|
WHERE id = ?`,
|
|
1953
3128
|
[id]
|
|
@@ -1979,7 +3154,12 @@ async function clearAllLogs() {
|
|
|
1979
3154
|
metrics: Number(metricsResult.changes ?? 0)
|
|
1980
3155
|
};
|
|
1981
3156
|
}
|
|
1982
|
-
async function getDailyMetrics(days = 7) {
|
|
3157
|
+
async function getDailyMetrics(days = 7, endpoint) {
|
|
3158
|
+
const params = [days];
|
|
3159
|
+
const whereClause = endpoint ? "WHERE endpoint = ?" : "";
|
|
3160
|
+
if (endpoint) {
|
|
3161
|
+
params.unshift(endpoint);
|
|
3162
|
+
}
|
|
1983
3163
|
const rows = await getAll(
|
|
1984
3164
|
`SELECT date,
|
|
1985
3165
|
request_count AS requestCount,
|
|
@@ -1987,9 +3167,10 @@ async function getDailyMetrics(days = 7) {
|
|
|
1987
3167
|
total_output_tokens AS outputTokens,
|
|
1988
3168
|
total_latency_ms AS totalLatency
|
|
1989
3169
|
FROM daily_metrics
|
|
3170
|
+
${whereClause}
|
|
1990
3171
|
ORDER BY date DESC
|
|
1991
3172
|
LIMIT ?`,
|
|
1992
|
-
|
|
3173
|
+
params
|
|
1993
3174
|
);
|
|
1994
3175
|
return rows.map((row) => ({
|
|
1995
3176
|
date: row.date,
|
|
@@ -1999,14 +3180,17 @@ async function getDailyMetrics(days = 7) {
|
|
|
1999
3180
|
avgLatencyMs: row.requestCount ? Math.round((row.totalLatency ?? 0) / row.requestCount) : 0
|
|
2000
3181
|
})).reverse();
|
|
2001
3182
|
}
|
|
2002
|
-
async function getMetricsOverview() {
|
|
3183
|
+
async function getMetricsOverview(endpoint) {
|
|
3184
|
+
const totalsWhere = endpoint ? "WHERE endpoint = ?" : "";
|
|
2003
3185
|
const totalsRow = await getOne(
|
|
2004
3186
|
`SELECT
|
|
2005
3187
|
COALESCE(SUM(request_count), 0) AS requests,
|
|
2006
3188
|
COALESCE(SUM(total_input_tokens), 0) AS inputTokens,
|
|
2007
3189
|
COALESCE(SUM(total_output_tokens), 0) AS outputTokens,
|
|
2008
3190
|
COALESCE(SUM(total_latency_ms), 0) AS totalLatency
|
|
2009
|
-
FROM daily_metrics
|
|
3191
|
+
FROM daily_metrics
|
|
3192
|
+
${totalsWhere}`,
|
|
3193
|
+
endpoint ? [endpoint] : []
|
|
2010
3194
|
);
|
|
2011
3195
|
const todayKey = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2012
3196
|
const todayRow = await getOne(
|
|
@@ -2015,8 +3199,9 @@ async function getMetricsOverview() {
|
|
|
2015
3199
|
total_output_tokens AS outputTokens,
|
|
2016
3200
|
total_latency_ms AS totalLatency
|
|
2017
3201
|
FROM daily_metrics
|
|
2018
|
-
WHERE date =
|
|
2019
|
-
|
|
3202
|
+
WHERE date = ?
|
|
3203
|
+
${endpoint ? "AND endpoint = ?" : ""}`,
|
|
3204
|
+
endpoint ? [todayKey, endpoint] : [todayKey]
|
|
2020
3205
|
);
|
|
2021
3206
|
const resolveAvg = (totalLatency, requests) => requests > 0 ? Math.round(totalLatency / requests) : 0;
|
|
2022
3207
|
const totalsRequests = totalsRow?.requests ?? 0;
|
|
@@ -2038,8 +3223,13 @@ async function getMetricsOverview() {
|
|
|
2038
3223
|
}
|
|
2039
3224
|
};
|
|
2040
3225
|
}
|
|
2041
|
-
async function getModelUsageMetrics(days = 7, limit = 10) {
|
|
3226
|
+
async function getModelUsageMetrics(days = 7, limit = 10, endpoint) {
|
|
2042
3227
|
const since = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
3228
|
+
const params = [since];
|
|
3229
|
+
const endpointClause = endpoint ? "AND endpoint = ?" : "";
|
|
3230
|
+
if (endpoint) {
|
|
3231
|
+
params.push(endpoint);
|
|
3232
|
+
}
|
|
2043
3233
|
const rows = await getAll(
|
|
2044
3234
|
`SELECT
|
|
2045
3235
|
model,
|
|
@@ -2052,10 +3242,11 @@ async function getModelUsageMetrics(days = 7, limit = 10) {
|
|
|
2052
3242
|
AVG(CASE WHEN tpot_ms IS NULL THEN NULL ELSE tpot_ms END) AS avgTpotMs
|
|
2053
3243
|
FROM request_logs
|
|
2054
3244
|
WHERE timestamp >= ?
|
|
3245
|
+
${endpointClause}
|
|
2055
3246
|
GROUP BY provider, model
|
|
2056
3247
|
ORDER BY requests DESC
|
|
2057
3248
|
LIMIT ?`,
|
|
2058
|
-
[
|
|
3249
|
+
[...params, limit]
|
|
2059
3250
|
);
|
|
2060
3251
|
const roundValue = (value, fractionDigits = 0) => value == null ? null : Number(value.toFixed(fractionDigits));
|
|
2061
3252
|
return rows.map((row) => ({
|
|
@@ -2069,13 +3260,81 @@ async function getModelUsageMetrics(days = 7, limit = 10) {
|
|
|
2069
3260
|
avgTpotMs: roundValue(row.avgTpotMs, 2)
|
|
2070
3261
|
}));
|
|
2071
3262
|
}
|
|
3263
|
+
async function getApiKeyOverviewMetrics(rangeDays = 7, endpoint) {
|
|
3264
|
+
const totals = await getOne("SELECT COUNT(*) AS total, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) AS enabled FROM api_keys");
|
|
3265
|
+
const since = Date.now() - rangeDays * 24 * 60 * 60 * 1e3;
|
|
3266
|
+
const params = [since];
|
|
3267
|
+
const endpointClause = endpoint ? "AND endpoint = ?" : "";
|
|
3268
|
+
if (endpoint) {
|
|
3269
|
+
params.push(endpoint);
|
|
3270
|
+
}
|
|
3271
|
+
const active = await getOne(
|
|
3272
|
+
`SELECT COUNT(DISTINCT api_key_id) AS count
|
|
3273
|
+
FROM request_logs
|
|
3274
|
+
WHERE api_key_id IS NOT NULL
|
|
3275
|
+
AND timestamp >= ?
|
|
3276
|
+
${endpointClause}`,
|
|
3277
|
+
params
|
|
3278
|
+
);
|
|
3279
|
+
return {
|
|
3280
|
+
totalKeys: totals?.total ?? 0,
|
|
3281
|
+
enabledKeys: totals?.enabled ?? 0,
|
|
3282
|
+
activeKeys: active?.count ?? 0,
|
|
3283
|
+
rangeDays
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
async function getApiKeyUsageMetrics(days = 7, limit = 10, endpoint) {
|
|
3287
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
3288
|
+
const params = [since];
|
|
3289
|
+
const endpointClause = endpoint ? "AND endpoint = ?" : "";
|
|
3290
|
+
if (endpoint) {
|
|
3291
|
+
params.push(endpoint);
|
|
3292
|
+
}
|
|
3293
|
+
const rows = await getAll(
|
|
3294
|
+
`SELECT
|
|
3295
|
+
api_key_id AS apiKeyId,
|
|
3296
|
+
api_key_name AS apiKeyName,
|
|
3297
|
+
COUNT(*) AS requests,
|
|
3298
|
+
COALESCE(SUM(input_tokens), 0) AS inputTokens,
|
|
3299
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
3300
|
+
MAX(timestamp) AS lastUsedAt
|
|
3301
|
+
FROM request_logs
|
|
3302
|
+
WHERE timestamp >= ?
|
|
3303
|
+
${endpointClause}
|
|
3304
|
+
GROUP BY api_key_id, api_key_name
|
|
3305
|
+
ORDER BY requests DESC
|
|
3306
|
+
LIMIT ?`,
|
|
3307
|
+
[...params, limit]
|
|
3308
|
+
);
|
|
3309
|
+
return rows.map((row) => ({
|
|
3310
|
+
apiKeyId: row.apiKeyId ?? null,
|
|
3311
|
+
apiKeyName: row.apiKeyName ?? null,
|
|
3312
|
+
requests: row.requests ?? 0,
|
|
3313
|
+
inputTokens: row.inputTokens ?? 0,
|
|
3314
|
+
outputTokens: row.outputTokens ?? 0,
|
|
3315
|
+
lastUsedAt: row.lastUsedAt ? new Date(row.lastUsedAt).toISOString() : null
|
|
3316
|
+
}));
|
|
3317
|
+
}
|
|
2072
3318
|
|
|
2073
3319
|
// routes/admin.ts
|
|
2074
3320
|
async function registerAdminRoutes(app) {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
3321
|
+
try {
|
|
3322
|
+
await ensureWildcardMetadata();
|
|
3323
|
+
} catch (error) {
|
|
3324
|
+
app.log.warn({ error }, "[api-keys] failed to ensure wildcard metadata");
|
|
3325
|
+
}
|
|
3326
|
+
const mapLogRecord = (record, options) => {
|
|
3327
|
+
const base = {
|
|
3328
|
+
...record,
|
|
3329
|
+
stream: Boolean(record?.stream)
|
|
3330
|
+
};
|
|
3331
|
+
if (options?.includeKeyValue) {
|
|
3332
|
+
base.api_key_value = options.decryptedKey ?? record?.api_key_value ?? null;
|
|
3333
|
+
} else {
|
|
3334
|
+
delete base.api_key_value;
|
|
3335
|
+
}
|
|
3336
|
+
return base;
|
|
3337
|
+
};
|
|
2079
3338
|
app.get("/api/status", async () => {
|
|
2080
3339
|
const config = getConfig();
|
|
2081
3340
|
return {
|
|
@@ -2144,12 +3403,12 @@ async function registerAdminRoutes(app) {
|
|
|
2144
3403
|
system: "You are a connection diagnostic assistant."
|
|
2145
3404
|
});
|
|
2146
3405
|
const providerBody = provider.type === "anthropic" ? buildAnthropicBody(testPayload, {
|
|
2147
|
-
maxTokens:
|
|
3406
|
+
maxTokens: 256,
|
|
2148
3407
|
temperature: 0,
|
|
2149
3408
|
toolChoice: void 0,
|
|
2150
3409
|
overrideTools: void 0
|
|
2151
3410
|
}) : buildProviderBody(testPayload, {
|
|
2152
|
-
maxTokens:
|
|
3411
|
+
maxTokens: 256,
|
|
2153
3412
|
temperature: 0,
|
|
2154
3413
|
toolChoice: void 0,
|
|
2155
3414
|
overrideTools: void 0
|
|
@@ -2176,6 +3435,16 @@ async function registerAdminRoutes(app) {
|
|
|
2176
3435
|
try {
|
|
2177
3436
|
parsed = raw ? JSON.parse(raw) : null;
|
|
2178
3437
|
} catch {
|
|
3438
|
+
const fallbackSample = raw?.trim() ?? "";
|
|
3439
|
+
if (provider.type && provider.type !== "anthropic") {
|
|
3440
|
+
return {
|
|
3441
|
+
ok: fallbackSample.length > 0,
|
|
3442
|
+
status: upstream.status,
|
|
3443
|
+
statusText: fallbackSample ? "OK (text response)" : "Empty response",
|
|
3444
|
+
durationMs: duration,
|
|
3445
|
+
sample: fallbackSample ? fallbackSample.slice(0, 200) : null
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
2179
3448
|
return {
|
|
2180
3449
|
ok: false,
|
|
2181
3450
|
status: upstream.status,
|
|
@@ -2226,6 +3495,7 @@ async function registerAdminRoutes(app) {
|
|
|
2226
3495
|
const model = typeof query.model === "string" && query.model.length > 0 ? query.model : void 0;
|
|
2227
3496
|
const statusParam = typeof query.status === "string" ? query.status : void 0;
|
|
2228
3497
|
const status = statusParam === "success" || statusParam === "error" ? statusParam : void 0;
|
|
3498
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
2229
3499
|
const parseTime = (value) => {
|
|
2230
3500
|
if (!value)
|
|
2231
3501
|
return void 0;
|
|
@@ -2237,9 +3507,22 @@ async function registerAdminRoutes(app) {
|
|
|
2237
3507
|
};
|
|
2238
3508
|
const from = parseTime(query.from);
|
|
2239
3509
|
const to = parseTime(query.to);
|
|
2240
|
-
const
|
|
3510
|
+
const collectApiKeyIds = (value) => {
|
|
3511
|
+
if (!value)
|
|
3512
|
+
return [];
|
|
3513
|
+
if (Array.isArray(value)) {
|
|
3514
|
+
return value.flatMap((item) => collectApiKeyIds(item));
|
|
3515
|
+
}
|
|
3516
|
+
if (typeof value === "string") {
|
|
3517
|
+
return value.split(",").map((part) => Number(part.trim())).filter((num) => Number.isFinite(num));
|
|
3518
|
+
}
|
|
3519
|
+
return [];
|
|
3520
|
+
};
|
|
3521
|
+
const apiKeyIdsRaw = collectApiKeyIds(query.apiKeys ?? query.apiKeyIds ?? query.apiKey);
|
|
3522
|
+
const apiKeyIds = apiKeyIdsRaw.length > 0 ? Array.from(new Set(apiKeyIdsRaw)) : void 0;
|
|
3523
|
+
const { items, total } = await queryLogs({ limit, offset, provider, model, status, from, to, apiKeyIds, endpoint });
|
|
2241
3524
|
reply.header("x-total-count", String(total));
|
|
2242
|
-
return { total, items: items.map(mapLogRecord) };
|
|
3525
|
+
return { total, items: items.map((item) => mapLogRecord(item)) };
|
|
2243
3526
|
});
|
|
2244
3527
|
app.get("/api/logs/:id", async (request, reply) => {
|
|
2245
3528
|
const id = Number(request.params.id);
|
|
@@ -2253,7 +3536,8 @@ async function registerAdminRoutes(app) {
|
|
|
2253
3536
|
return { error: "Not found" };
|
|
2254
3537
|
}
|
|
2255
3538
|
const payload = await getLogPayload(id);
|
|
2256
|
-
|
|
3539
|
+
const decryptedKey = await decryptApiKeyValue(record.api_key_value);
|
|
3540
|
+
return { ...mapLogRecord(record, { includeKeyValue: true, decryptedKey }), payload };
|
|
2257
3541
|
});
|
|
2258
3542
|
app.post("/api/logs/cleanup", async () => {
|
|
2259
3543
|
const config = getConfig();
|
|
@@ -2277,14 +3561,17 @@ async function registerAdminRoutes(app) {
|
|
|
2277
3561
|
sizeBytes: pageCount * pageSize
|
|
2278
3562
|
};
|
|
2279
3563
|
});
|
|
2280
|
-
app.get("/api/stats/overview", async () => {
|
|
2281
|
-
|
|
3564
|
+
app.get("/api/stats/overview", async (request) => {
|
|
3565
|
+
const query = request.query ?? {};
|
|
3566
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
3567
|
+
return getMetricsOverview(endpoint);
|
|
2282
3568
|
});
|
|
2283
3569
|
app.get("/api/stats/daily", async (request) => {
|
|
2284
3570
|
const query = request.query ?? {};
|
|
2285
3571
|
const daysRaw = Number(query.days ?? 7);
|
|
2286
3572
|
const days = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 30)) : 7;
|
|
2287
|
-
|
|
3573
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
3574
|
+
return getDailyMetrics(days, endpoint);
|
|
2288
3575
|
});
|
|
2289
3576
|
app.get("/api/stats/model", async (request) => {
|
|
2290
3577
|
const query = request.query ?? {};
|
|
@@ -2292,9 +3579,86 @@ async function registerAdminRoutes(app) {
|
|
|
2292
3579
|
const limitRaw = Number(query.limit ?? 10);
|
|
2293
3580
|
const days = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 90)) : 7;
|
|
2294
3581
|
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(limitRaw, 50)) : 10;
|
|
2295
|
-
|
|
3582
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
3583
|
+
return getModelUsageMetrics(days, limit, endpoint);
|
|
3584
|
+
});
|
|
3585
|
+
app.get("/api/stats/api-keys/overview", async (request) => {
|
|
3586
|
+
const query = request.query ?? {};
|
|
3587
|
+
const daysRaw = Number(query.days ?? 7);
|
|
3588
|
+
const rangeDays = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 90)) : 7;
|
|
3589
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
3590
|
+
return getApiKeyOverviewMetrics(rangeDays, endpoint);
|
|
3591
|
+
});
|
|
3592
|
+
app.get("/api/stats/api-keys/usage", async (request) => {
|
|
3593
|
+
const query = request.query ?? {};
|
|
3594
|
+
const daysRaw = Number(query.days ?? 7);
|
|
3595
|
+
const limitRaw = Number(query.limit ?? 10);
|
|
3596
|
+
const days = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 90)) : 7;
|
|
3597
|
+
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(limitRaw, 50)) : 10;
|
|
3598
|
+
const endpoint = isEndpoint(query.endpoint) ? query.endpoint : void 0;
|
|
3599
|
+
return getApiKeyUsageMetrics(days, limit, endpoint);
|
|
3600
|
+
});
|
|
3601
|
+
app.get("/api/keys", async () => {
|
|
3602
|
+
return listApiKeys();
|
|
3603
|
+
});
|
|
3604
|
+
app.post("/api/keys", async (request, reply) => {
|
|
3605
|
+
const body = request.body;
|
|
3606
|
+
if (!body?.name || typeof body.name !== "string") {
|
|
3607
|
+
reply.code(400);
|
|
3608
|
+
return { error: "Name is required" };
|
|
3609
|
+
}
|
|
3610
|
+
try {
|
|
3611
|
+
return await createApiKey(body.name, body.description, { ipAddress: request.ip });
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
reply.code(400);
|
|
3614
|
+
return { error: error instanceof Error ? error.message : "Failed to create API key" };
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
app.patch("/api/keys/:id", async (request, reply) => {
|
|
3618
|
+
const id = Number(request.params.id);
|
|
3619
|
+
if (!Number.isFinite(id)) {
|
|
3620
|
+
reply.code(400);
|
|
3621
|
+
return { error: "Invalid id" };
|
|
3622
|
+
}
|
|
3623
|
+
const body = request.body;
|
|
3624
|
+
if (typeof body?.enabled !== "boolean") {
|
|
3625
|
+
reply.code(400);
|
|
3626
|
+
return { error: "enabled field is required" };
|
|
3627
|
+
}
|
|
3628
|
+
try {
|
|
3629
|
+
await setApiKeyEnabled(id, body.enabled, { ipAddress: request.ip });
|
|
3630
|
+
return { success: true };
|
|
3631
|
+
} catch (error) {
|
|
3632
|
+
if (error instanceof Error && error.message === "API key not found") {
|
|
3633
|
+
reply.code(404);
|
|
3634
|
+
} else {
|
|
3635
|
+
reply.code(400);
|
|
3636
|
+
}
|
|
3637
|
+
return { error: error instanceof Error ? error.message : "Failed to update API key" };
|
|
3638
|
+
}
|
|
3639
|
+
});
|
|
3640
|
+
app.delete("/api/keys/:id", async (request, reply) => {
|
|
3641
|
+
const id = Number(request.params.id);
|
|
3642
|
+
if (!Number.isFinite(id)) {
|
|
3643
|
+
reply.code(400);
|
|
3644
|
+
return { error: "Invalid id" };
|
|
3645
|
+
}
|
|
3646
|
+
try {
|
|
3647
|
+
await deleteApiKey(id, { ipAddress: request.ip });
|
|
3648
|
+
return { success: true };
|
|
3649
|
+
} catch (error) {
|
|
3650
|
+
if (error instanceof Error && error.message === "API key not found") {
|
|
3651
|
+
reply.code(404);
|
|
3652
|
+
} else if (error instanceof Error && error.message === "Cannot delete wildcard key") {
|
|
3653
|
+
reply.code(403);
|
|
3654
|
+
} else {
|
|
3655
|
+
reply.code(400);
|
|
3656
|
+
}
|
|
3657
|
+
return { error: error instanceof Error ? error.message : "Failed to delete API key" };
|
|
3658
|
+
}
|
|
2296
3659
|
});
|
|
2297
3660
|
}
|
|
3661
|
+
var isEndpoint = (value) => value === "anthropic" || value === "openai";
|
|
2298
3662
|
|
|
2299
3663
|
// tasks/maintenance.ts
|
|
2300
3664
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -2330,17 +3694,17 @@ onConfigChange((config) => {
|
|
|
2330
3694
|
});
|
|
2331
3695
|
function resolveWebDist() {
|
|
2332
3696
|
const __filename2 = fileURLToPath(import.meta.url);
|
|
2333
|
-
const __dirname =
|
|
3697
|
+
const __dirname = path4.dirname(__filename2);
|
|
2334
3698
|
const candidates = [
|
|
2335
3699
|
process2.env.CC_GW_UI_ROOT,
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
3700
|
+
path4.resolve(__dirname, "../web/public"),
|
|
3701
|
+
path4.resolve(__dirname, "../web/dist"),
|
|
3702
|
+
path4.resolve(__dirname, "../../web/dist"),
|
|
3703
|
+
path4.resolve(__dirname, "../../../src/web/dist"),
|
|
3704
|
+
path4.resolve(process2.cwd(), "src/web/dist")
|
|
2341
3705
|
].filter((item) => Boolean(item));
|
|
2342
3706
|
for (const candidate of candidates) {
|
|
2343
|
-
if (
|
|
3707
|
+
if (fs4.existsSync(candidate)) {
|
|
2344
3708
|
return candidate;
|
|
2345
3709
|
}
|
|
2346
3710
|
}
|
|
@@ -2348,12 +3712,49 @@ function resolveWebDist() {
|
|
|
2348
3712
|
}
|
|
2349
3713
|
async function createServer() {
|
|
2350
3714
|
const config = cachedConfig2 ?? loadConfig();
|
|
3715
|
+
const requestLogEnabled = config.requestLogging !== false;
|
|
3716
|
+
const responseLogEnabled = config.responseLogging !== false;
|
|
2351
3717
|
const app = Fastify({
|
|
2352
3718
|
logger: {
|
|
2353
3719
|
level: config.logLevel ?? "info"
|
|
2354
3720
|
},
|
|
2355
|
-
disableRequestLogging:
|
|
3721
|
+
disableRequestLogging: true
|
|
2356
3722
|
});
|
|
3723
|
+
if (requestLogEnabled) {
|
|
3724
|
+
app.addHook("onRequest", (request, _reply, done) => {
|
|
3725
|
+
const socket = request.socket;
|
|
3726
|
+
const hostname = typeof request.hostname === "string" && request.hostname.length > 0 ? request.hostname : typeof request.headers.host === "string" ? request.headers.host : void 0;
|
|
3727
|
+
app.log.info(
|
|
3728
|
+
{
|
|
3729
|
+
reqId: request.id,
|
|
3730
|
+
req: {
|
|
3731
|
+
method: request.method,
|
|
3732
|
+
url: request.url,
|
|
3733
|
+
hostname,
|
|
3734
|
+
remoteAddress: request.ip,
|
|
3735
|
+
remotePort: socket && typeof socket.remotePort === "number" ? socket.remotePort : void 0
|
|
3736
|
+
}
|
|
3737
|
+
},
|
|
3738
|
+
"incoming request"
|
|
3739
|
+
);
|
|
3740
|
+
done();
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
if (responseLogEnabled) {
|
|
3744
|
+
app.addHook("onResponse", (request, reply, done) => {
|
|
3745
|
+
app.log.info(
|
|
3746
|
+
{
|
|
3747
|
+
reqId: request.id,
|
|
3748
|
+
res: {
|
|
3749
|
+
statusCode: reply.statusCode
|
|
3750
|
+
},
|
|
3751
|
+
responseTime: typeof reply.getResponseTime === "function" ? reply.getResponseTime() : void 0
|
|
3752
|
+
},
|
|
3753
|
+
"request completed"
|
|
3754
|
+
);
|
|
3755
|
+
done();
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
2357
3758
|
await app.register(fastifyCors, {
|
|
2358
3759
|
origin: true,
|
|
2359
3760
|
credentials: true
|
|
@@ -2373,7 +3774,7 @@ async function createServer() {
|
|
|
2373
3774
|
reply.code(400);
|
|
2374
3775
|
return { error: "Invalid asset path" };
|
|
2375
3776
|
}
|
|
2376
|
-
return reply.sendFile(
|
|
3777
|
+
return reply.sendFile(path4.join("assets", target));
|
|
2377
3778
|
};
|
|
2378
3779
|
app.get("/assets/*", assetHandler);
|
|
2379
3780
|
app.head("/assets/*", assetHandler);
|
|
@@ -2392,6 +3793,7 @@ async function createServer() {
|
|
|
2392
3793
|
app.log.warn("\u672A\u627E\u5230 Web UI \u6784\u5EFA\u4EA7\u7269\uFF0C/ui \u76EE\u5F55\u5C06\u4E0D\u53EF\u7528\u3002");
|
|
2393
3794
|
}
|
|
2394
3795
|
await registerMessagesRoute(app);
|
|
3796
|
+
await registerOpenAiRoutes(app);
|
|
2395
3797
|
await registerAdminRoutes(app);
|
|
2396
3798
|
startMaintenanceTimers();
|
|
2397
3799
|
app.get("/health", async () => {
|
|
@@ -2434,7 +3836,7 @@ async function main() {
|
|
|
2434
3836
|
}
|
|
2435
3837
|
}
|
|
2436
3838
|
var __filename = fileURLToPath(import.meta.url);
|
|
2437
|
-
if (process2.argv[1] &&
|
|
3839
|
+
if (process2.argv[1] && path4.resolve(process2.argv[1]) === __filename) {
|
|
2438
3840
|
main();
|
|
2439
3841
|
}
|
|
2440
3842
|
export {
|