@chenpu17/cc-gw 0.2.1

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.
@@ -0,0 +1,2152 @@
1
+ // index.ts
2
+ import Fastify from "fastify";
3
+ import fastifyCors from "@fastify/cors";
4
+ import fastifyStatic from "@fastify/static";
5
+ import fs3 from "fs";
6
+ import path3 from "path";
7
+ import process2 from "process";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // config/manager.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import os from "os";
14
+ import { EventEmitter } from "events";
15
+ var HOME_DIR = path.join(os.homedir(), ".cc-gw");
16
+ var CONFIG_PATH = path.join(HOME_DIR, "config.json");
17
+ var TypedEmitter = class extends EventEmitter {
18
+ on(event, listener) {
19
+ return super.on(event, listener);
20
+ }
21
+ off(event, listener) {
22
+ return super.off(event, listener);
23
+ }
24
+ emitTyped(event, ...args) {
25
+ return super.emit(event, ...args);
26
+ }
27
+ };
28
+ var emitter = new TypedEmitter();
29
+ var cachedConfig = null;
30
+ function parseConfig(raw) {
31
+ const data = JSON.parse(raw);
32
+ if (typeof data.port !== "number") {
33
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11\u6216\u9519\u8BEF\u7684 port \u5B57\u6BB5");
34
+ }
35
+ if (!Array.isArray(data.providers)) {
36
+ data.providers = [];
37
+ }
38
+ if (!data.defaults) {
39
+ data.defaults = {
40
+ completion: null,
41
+ reasoning: null,
42
+ background: null,
43
+ longContextThreshold: 6e4
44
+ };
45
+ } else {
46
+ data.defaults.longContextThreshold ??= 6e4;
47
+ }
48
+ if (typeof data.logRetentionDays !== "number") {
49
+ data.logRetentionDays = 30;
50
+ }
51
+ if (typeof data.storePayloads !== "boolean") {
52
+ data.storePayloads = true;
53
+ }
54
+ if (!data.modelRoutes || typeof data.modelRoutes !== "object") {
55
+ data.modelRoutes = {};
56
+ } else {
57
+ const sanitized = {};
58
+ for (const [key, value] of Object.entries(data.modelRoutes)) {
59
+ if (typeof value !== "string")
60
+ continue;
61
+ const trimmedKey = key.trim();
62
+ const trimmedValue = value.trim();
63
+ if (!trimmedKey || !trimmedValue)
64
+ continue;
65
+ sanitized[trimmedKey] = trimmedValue;
66
+ }
67
+ data.modelRoutes = sanitized;
68
+ }
69
+ return data;
70
+ }
71
+ function loadConfig() {
72
+ if (!fs.existsSync(CONFIG_PATH)) {
73
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${CONFIG_PATH}`);
74
+ }
75
+ const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
76
+ cachedConfig = parseConfig(raw);
77
+ return cachedConfig;
78
+ }
79
+ function getConfig() {
80
+ if (cachedConfig)
81
+ return cachedConfig;
82
+ return loadConfig();
83
+ }
84
+ function updateConfig(next) {
85
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
86
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2), "utf-8");
87
+ cachedConfig = next;
88
+ emitter.emitTyped("change", cachedConfig);
89
+ }
90
+ function onConfigChange(listener) {
91
+ emitter.on("change", listener);
92
+ if (cachedConfig)
93
+ listener(cachedConfig);
94
+ return () => emitter.off("change", listener);
95
+ }
96
+
97
+ // protocol/normalize.ts
98
+ function extractText(content) {
99
+ if (content == null)
100
+ return "";
101
+ if (typeof content === "string")
102
+ return content;
103
+ if (Array.isArray(content)) {
104
+ return content.map((item) => {
105
+ if (typeof item === "string")
106
+ return item;
107
+ if (item && typeof item === "object") {
108
+ if (item.type === "text" && item.text)
109
+ return item.text;
110
+ if (item.content)
111
+ return extractText(item.content);
112
+ }
113
+ return "";
114
+ }).filter(Boolean).join("\n");
115
+ }
116
+ if (typeof content === "object") {
117
+ if (content.text)
118
+ return content.text;
119
+ if (content.content)
120
+ return extractText(content.content);
121
+ }
122
+ return "";
123
+ }
124
+ function normalizeSystem(messages, systemField) {
125
+ const systemParts = [];
126
+ if (Array.isArray(systemField)) {
127
+ systemParts.push(...systemField.map((item) => extractText(item)));
128
+ } else if (typeof systemField === "string") {
129
+ systemParts.push(systemField);
130
+ } else if (systemField) {
131
+ systemParts.push(extractText(systemField));
132
+ }
133
+ const remaining = [];
134
+ for (const msg of messages) {
135
+ if (msg.role === "system" || msg.role === "developer") {
136
+ const text = extractText(msg.content);
137
+ if (text)
138
+ systemParts.push(text);
139
+ continue;
140
+ }
141
+ remaining.push(msg);
142
+ }
143
+ const system = systemParts.filter((part) => part && part.trim().length > 0).join("\n\n") || null;
144
+ return { system, remaining };
145
+ }
146
+ function normalizeClaudePayload(payload) {
147
+ const stream = Boolean(payload.stream);
148
+ const thinking = Boolean(payload.thinking);
149
+ const messages = Array.isArray(payload.messages) ? payload.messages : payload.messages ? [payload.messages] : [];
150
+ const { system, remaining } = normalizeSystem(messages, payload.system);
151
+ const normalizedMessages = remaining.map((msg) => {
152
+ if (msg.role === "user") {
153
+ if (Array.isArray(msg.content)) {
154
+ const textParts = [];
155
+ const toolResults = [];
156
+ for (const block of msg.content) {
157
+ if (block.type === "text" && block.text) {
158
+ textParts.push(block.text);
159
+ } else if (block.type === "tool_result") {
160
+ toolResults.push({
161
+ id: block.tool_use_id || block.id || "tool_result",
162
+ name: block.name,
163
+ content: block.content ?? block.text ?? null,
164
+ cacheControl: block.cache_control
165
+ });
166
+ }
167
+ }
168
+ return {
169
+ role: "user",
170
+ text: textParts.join("\n"),
171
+ toolResults
172
+ };
173
+ }
174
+ return {
175
+ role: "user",
176
+ text: extractText(msg.content)
177
+ };
178
+ }
179
+ if (msg.role === "assistant") {
180
+ const toolCalls = [];
181
+ let text = "";
182
+ if (Array.isArray(msg.content)) {
183
+ const textParts = [];
184
+ for (const block of msg.content) {
185
+ if (block.type === "text" && block.text) {
186
+ textParts.push(block.text);
187
+ } else if (block.type === "tool_use") {
188
+ toolCalls.push({
189
+ id: block.id || `call_${Math.random().toString(36).slice(2)}`,
190
+ name: block.name,
191
+ arguments: block.input,
192
+ cacheControl: block.cache_control
193
+ });
194
+ }
195
+ }
196
+ text = textParts.join("\n");
197
+ } else if (typeof msg.content === "string") {
198
+ text = msg.content;
199
+ }
200
+ return {
201
+ role: "assistant",
202
+ text,
203
+ toolCalls
204
+ };
205
+ }
206
+ return {
207
+ role: "user",
208
+ text: extractText(msg.content)
209
+ };
210
+ });
211
+ return {
212
+ original: payload,
213
+ system,
214
+ messages: normalizedMessages,
215
+ tools: Array.isArray(payload.tools) ? payload.tools : [],
216
+ stream,
217
+ thinking
218
+ };
219
+ }
220
+
221
+ // protocol/tokenizer.ts
222
+ import { encoding_for_model } from "tiktoken";
223
+ function getEncoder(model) {
224
+ try {
225
+ return encoding_for_model(model);
226
+ } catch {
227
+ return encoding_for_model("gpt-3.5-turbo");
228
+ }
229
+ }
230
+ function estimateTextTokens(text, model) {
231
+ if (!text)
232
+ return 0;
233
+ try {
234
+ const encoder2 = getEncoder(model);
235
+ return encoder2.encode(text).length;
236
+ } catch {
237
+ return Math.ceil(text.length / 4);
238
+ }
239
+ }
240
+ function estimateTokens(payload, model) {
241
+ try {
242
+ const encoder2 = getEncoder(model);
243
+ let total = 0;
244
+ if (payload.system) {
245
+ total += encoder2.encode(payload.system).length;
246
+ }
247
+ for (const message of payload.messages) {
248
+ if (message.text) {
249
+ total += encoder2.encode(message.text).length;
250
+ }
251
+ if (message.toolCalls) {
252
+ for (const call of message.toolCalls) {
253
+ total += encoder2.encode(JSON.stringify(call.arguments ?? {})).length;
254
+ }
255
+ }
256
+ if (message.toolResults) {
257
+ for (const result of message.toolResults) {
258
+ total += encoder2.encode(JSON.stringify(result.content ?? "")).length;
259
+ }
260
+ }
261
+ }
262
+ return total;
263
+ } catch {
264
+ const text = [payload.system ?? "", ...payload.messages.map((m) => m.text ?? "")].join("\n");
265
+ return Math.ceil(text.length / 4);
266
+ }
267
+ }
268
+
269
+ // router/index.ts
270
+ function resolveByIdentifier(identifier, providers) {
271
+ if (!identifier)
272
+ return null;
273
+ if (identifier.includes(":")) {
274
+ const [providerId, modelId] = identifier.split(":", 2);
275
+ const provider = providers.find((p) => p.id === providerId);
276
+ if (provider && (provider.defaultModel === modelId || provider.models?.some((m) => m.id === modelId))) {
277
+ return { providerId, modelId, provider, tokenEstimate: 0 };
278
+ }
279
+ } else {
280
+ for (const provider of providers) {
281
+ if (provider.defaultModel === identifier || provider.models?.some((m) => m.id === identifier)) {
282
+ return { providerId: provider.id, modelId: identifier, provider, tokenEstimate: 0 };
283
+ }
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ function resolveRoute(ctx) {
289
+ const config = getConfig();
290
+ const providers = config.providers;
291
+ if (!providers.length) {
292
+ throw new Error("\u672A\u914D\u7F6E\u4EFB\u4F55\u6A21\u578B\u63D0\u4F9B\u5546\uFF0C\u8BF7\u5148\u5728 Web UI \u4E2D\u6DFB\u52A0 Provider\u3002");
293
+ }
294
+ const requestedModel = ctx.requestedModel?.trim();
295
+ const mappedIdentifier = requestedModel ? config.modelRoutes?.[requestedModel] ?? null : null;
296
+ const fallbackModelId = providers[0].defaultModel ?? providers[0].models?.[0]?.id ?? "gpt-4o";
297
+ const tokenEstimate = estimateTokens(
298
+ ctx.payload,
299
+ mappedIdentifier ?? requestedModel ?? fallbackModelId
300
+ );
301
+ const strategy = ctx.payload;
302
+ const defaults = config.defaults;
303
+ if (mappedIdentifier) {
304
+ const mapped = resolveByIdentifier(mappedIdentifier, providers);
305
+ if (mapped) {
306
+ return { ...mapped, tokenEstimate };
307
+ }
308
+ console.warn(`modelRoutes \u6620\u5C04\u76EE\u6807\u65E0\u6548: ${mappedIdentifier}`);
309
+ }
310
+ const fromRequest = resolveByIdentifier(requestedModel, providers);
311
+ if (fromRequest) {
312
+ return { ...fromRequest, tokenEstimate };
313
+ }
314
+ if (strategy.thinking && defaults.reasoning) {
315
+ const target = resolveByIdentifier(defaults.reasoning, providers);
316
+ if (target)
317
+ return { ...target, tokenEstimate };
318
+ }
319
+ if (tokenEstimate > (defaults.longContextThreshold ?? 6e4) && defaults.background) {
320
+ const target = resolveByIdentifier(defaults.background, providers);
321
+ if (target)
322
+ return { ...target, tokenEstimate };
323
+ }
324
+ if (defaults.completion) {
325
+ const target = resolveByIdentifier(defaults.completion, providers);
326
+ if (target)
327
+ return { ...target, tokenEstimate };
328
+ }
329
+ const firstProvider = providers[0];
330
+ const modelId = firstProvider.defaultModel || firstProvider.models?.[0]?.id;
331
+ if (!modelId) {
332
+ throw new Error(`Provider ${firstProvider.id} \u672A\u914D\u7F6E\u4EFB\u4F55\u6A21\u578B`);
333
+ }
334
+ return {
335
+ providerId: firstProvider.id,
336
+ modelId,
337
+ provider: firstProvider,
338
+ tokenEstimate
339
+ };
340
+ }
341
+
342
+ // protocol/toProvider.ts
343
+ function buildMessages(payload) {
344
+ const messages = [];
345
+ if (payload.system) {
346
+ messages.push({ role: "system", content: payload.system });
347
+ }
348
+ for (const message of payload.messages) {
349
+ if (message.role === "user") {
350
+ if (message.toolResults?.length) {
351
+ for (const tool of message.toolResults) {
352
+ const serialized = typeof tool.content === "string" ? tool.content : JSON.stringify(tool.content ?? "");
353
+ messages.push({
354
+ role: "tool",
355
+ tool_call_id: tool.id,
356
+ name: tool.name ?? tool.id,
357
+ content: serialized ?? "",
358
+ cache_control: tool.cacheControl
359
+ });
360
+ }
361
+ }
362
+ const userContent = message.text ?? "";
363
+ const hasUserText = userContent.trim().length > 0;
364
+ if (hasUserText || !message.toolResults?.length) {
365
+ messages.push({ role: "user", content: hasUserText ? userContent : "" });
366
+ }
367
+ } else if (message.role === "assistant") {
368
+ const openAiMsg = {
369
+ role: "assistant",
370
+ content: message.text ?? ""
371
+ };
372
+ if (message.toolCalls?.length) {
373
+ openAiMsg.tool_calls = message.toolCalls.map((call) => ({
374
+ id: call.id,
375
+ type: "function",
376
+ function: {
377
+ name: call.name,
378
+ arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {})
379
+ },
380
+ cache_control: call.cacheControl
381
+ }));
382
+ if (!openAiMsg.content) {
383
+ openAiMsg.content = null;
384
+ }
385
+ }
386
+ messages.push(openAiMsg);
387
+ }
388
+ }
389
+ return messages;
390
+ }
391
+ function buildProviderBody(payload, options = {}) {
392
+ const body = {
393
+ messages: buildMessages(payload)
394
+ };
395
+ if (options.maxTokens) {
396
+ if (payload.thinking) {
397
+ body.max_completion_tokens = options.maxTokens;
398
+ } else {
399
+ body.max_tokens = options.maxTokens;
400
+ }
401
+ }
402
+ if (typeof options.temperature === "number") {
403
+ body.temperature = options.temperature;
404
+ }
405
+ const tools = options.overrideTools ?? payload.tools;
406
+ if (tools && tools.length > 0) {
407
+ body.tools = tools.map((tool) => ({
408
+ type: "function",
409
+ function: {
410
+ name: tool.name,
411
+ description: tool.description,
412
+ parameters: tool.input_schema ?? tool.parameters ?? {}
413
+ }
414
+ }));
415
+ }
416
+ if (options.toolChoice) {
417
+ body.tool_choice = options.toolChoice;
418
+ }
419
+ const passthroughKeys = [
420
+ "cache_control",
421
+ "metadata",
422
+ "response_format",
423
+ "parallel_tool_calls",
424
+ "frequency_penalty",
425
+ "presence_penalty",
426
+ "logit_bias",
427
+ "top_p",
428
+ "top_k",
429
+ "stop",
430
+ "stop_sequences",
431
+ "user",
432
+ "seed",
433
+ "n",
434
+ "options"
435
+ ];
436
+ const original = payload.original ?? {};
437
+ for (const key of passthroughKeys) {
438
+ if (Object.prototype.hasOwnProperty.call(original, key)) {
439
+ const value = original[key];
440
+ if (value !== void 0) {
441
+ ;
442
+ body[key] = value;
443
+ }
444
+ }
445
+ }
446
+ return body;
447
+ }
448
+ function buildAnthropicContentFromText(text) {
449
+ if (!text || text.length === 0) {
450
+ return [];
451
+ }
452
+ return [
453
+ {
454
+ type: "text",
455
+ text
456
+ }
457
+ ];
458
+ }
459
+ function buildAnthropicBody(payload, options = {}) {
460
+ const messages = [];
461
+ for (const message of payload.messages) {
462
+ const blocks = [];
463
+ if (message.text) {
464
+ blocks.push(...buildAnthropicContentFromText(message.text));
465
+ }
466
+ if (message.role === "user" && message.toolResults?.length) {
467
+ for (const result of message.toolResults) {
468
+ const content = typeof result.content === "string" ? [{ type: "text", text: result.content }] : [{ type: "text", text: JSON.stringify(result.content ?? "") }];
469
+ blocks.push({
470
+ type: "tool_result",
471
+ tool_use_id: result.id,
472
+ content,
473
+ cache_control: result.cacheControl
474
+ });
475
+ }
476
+ }
477
+ if (message.role === "assistant" && message.toolCalls?.length) {
478
+ for (const call of message.toolCalls) {
479
+ blocks.push({
480
+ type: "tool_use",
481
+ id: call.id,
482
+ name: call.name,
483
+ input: call.arguments ?? {},
484
+ cache_control: call.cacheControl
485
+ });
486
+ }
487
+ }
488
+ if (message.role === "assistant" || message.role === "user") {
489
+ if (blocks.length === 0) {
490
+ blocks.push({ type: "text", text: "" });
491
+ }
492
+ messages.push({
493
+ role: message.role,
494
+ content: blocks
495
+ });
496
+ }
497
+ }
498
+ const body = {
499
+ system: payload.system ?? void 0,
500
+ messages
501
+ };
502
+ if (options.maxTokens) {
503
+ body.max_tokens = options.maxTokens;
504
+ }
505
+ if (typeof options.temperature === "number") {
506
+ body.temperature = options.temperature;
507
+ }
508
+ const tools = options.overrideTools ?? payload.tools;
509
+ if (tools && tools.length > 0) {
510
+ body.tools = tools.map((tool) => ({
511
+ type: "tool",
512
+ name: tool.name,
513
+ description: tool.description,
514
+ input_schema: tool.input_schema ?? tool.parameters ?? {}
515
+ }));
516
+ }
517
+ if (options.toolChoice) {
518
+ body.tool_choice = options.toolChoice;
519
+ }
520
+ return body;
521
+ }
522
+
523
+ // providers/openai.ts
524
+ import { fetch } from "undici";
525
+ import { ReadableStream } from "stream/web";
526
+ var encoder = new TextEncoder();
527
+ function createJsonStream(payload) {
528
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
529
+ return new ReadableStream({
530
+ start(controller) {
531
+ controller.enqueue(encoder.encode(text));
532
+ controller.close();
533
+ }
534
+ });
535
+ }
536
+ function resolveEndpoint(config, options) {
537
+ if (options?.endpoint)
538
+ return options.endpoint;
539
+ const base = config.baseUrl.replace(/\/$/, "");
540
+ const defaultPath = options?.defaultPath ?? "v1/chat/completions";
541
+ if (base.endsWith("/chat/completions"))
542
+ return base;
543
+ let pathSegment = defaultPath;
544
+ const versionMatch = base.match(/\/v(\d+)$/);
545
+ if (versionMatch && defaultPath.startsWith("v1/")) {
546
+ pathSegment = defaultPath.slice(3);
547
+ }
548
+ if (pathSegment.startsWith("/")) {
549
+ pathSegment = pathSegment.slice(1);
550
+ }
551
+ return `${base}/${pathSegment}`;
552
+ }
553
+ function createOpenAIConnector(config, options) {
554
+ const url = resolveEndpoint(config, options);
555
+ const shouldLogEndpoint = process.env.CC_GW_DEBUG_ENDPOINTS === "1";
556
+ return {
557
+ id: config.id,
558
+ async send(request) {
559
+ const headers = {
560
+ "Content-Type": "application/json",
561
+ ...config.extraHeaders,
562
+ ...request.headers
563
+ };
564
+ if (config.apiKey) {
565
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
566
+ }
567
+ const body = {
568
+ ...request.body,
569
+ model: request.model,
570
+ stream: request.stream ?? false
571
+ };
572
+ const payload = options?.mutateBody ? options.mutateBody(body) : body;
573
+ if (shouldLogEndpoint) {
574
+ console.info(`[cc-gw] provider=${config.id} endpoint=${url}`);
575
+ }
576
+ const res = await fetch(url, {
577
+ method: "POST",
578
+ headers,
579
+ body: JSON.stringify(payload)
580
+ });
581
+ if (shouldLogEndpoint) {
582
+ console.info(`[cc-gw] provider=${config.id} status=${res.status}`);
583
+ }
584
+ if (res.status >= 400 && options?.mapErrorBody) {
585
+ let raw = null;
586
+ try {
587
+ raw = await res.json();
588
+ } catch {
589
+ raw = await res.text();
590
+ }
591
+ if (shouldLogEndpoint) {
592
+ console.warn(`[cc-gw] provider=${config.id} error_body=${typeof raw === "string" ? raw : JSON.stringify(raw)}`);
593
+ }
594
+ const mapped = options.mapErrorBody(raw);
595
+ return {
596
+ status: res.status,
597
+ headers: res.headers,
598
+ body: createJsonStream(mapped)
599
+ };
600
+ }
601
+ return {
602
+ status: res.status,
603
+ headers: res.headers,
604
+ body: res.body
605
+ };
606
+ }
607
+ };
608
+ }
609
+
610
+ // providers/kimi.ts
611
+ var codeMessageMap = {
612
+ invalid_api_key: "Kimi API Key \u65E0\u6548\uFF0C\u8BF7\u786E\u8BA4\u5728\u63A7\u5236\u53F0\u590D\u5236\u7684\u503C\u662F\u5426\u6B63\u786E",
613
+ permission_denied: "Kimi API \u6743\u9650\u4E0D\u8DB3\u6216\u8D26\u53F7\u72B6\u6001\u5F02\u5E38",
614
+ insufficient_quota: "Kimi \u914D\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u524D\u5F80\u6708\u4E4B\u6697\u9762\u63A7\u5236\u53F0\u5145\u503C",
615
+ rate_limit_exceeded: "Kimi \u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5"
616
+ };
617
+ function mapKimiError(payload) {
618
+ if (typeof payload === "string") {
619
+ try {
620
+ return mapKimiError(JSON.parse(payload));
621
+ } catch {
622
+ return { error: { message: payload, code: "unknown_error" } };
623
+ }
624
+ }
625
+ const data = payload;
626
+ const code = data?.error?.code ?? data?.error?.type ?? "unknown_error";
627
+ const message = codeMessageMap[code] ?? data?.error?.message ?? "Kimi \u8BF7\u6C42\u5931\u8D25";
628
+ return {
629
+ error: {
630
+ code,
631
+ message
632
+ }
633
+ };
634
+ }
635
+ function createKimiConnector(config) {
636
+ const base = config.baseUrl.replace(/\/$/, "");
637
+ const endpointBase = base.endsWith("/v1") ? base.slice(0, -3) : base;
638
+ const endpoint = `${endpointBase}/v1/chat/completions`;
639
+ if (process.env.CC_GW_DEBUG_ENDPOINTS === "1") {
640
+ console.info(`[cc-gw] kimi connector base=${config.baseUrl} resolved=${endpoint}`);
641
+ }
642
+ return createOpenAIConnector(config, {
643
+ endpoint,
644
+ mapErrorBody: mapKimiError
645
+ });
646
+ }
647
+
648
+ // providers/deepseek.ts
649
+ var codeMessageMap2 = {
650
+ authentication_error: "DeepSeek API Key \u65E0\u6548\u6216\u672A\u914D\u7F6E",
651
+ permission_denied: "DeepSeek API Key \u6743\u9650\u4E0D\u8DB3\uFF0C\u8BF7\u68C0\u67E5\u8BA2\u9605\u8BA1\u5212",
652
+ rate_limit_exceeded: "DeepSeek \u8BF7\u6C42\u9891\u7387\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
653
+ insufficient_quota: "DeepSeek \u8D26\u6237\u4F59\u989D\u4E0D\u8DB3\uFF0C\u8BF7\u5145\u503C\u540E\u7EE7\u7EED\u4F7F\u7528"
654
+ };
655
+ function mapDeepSeekError(payload) {
656
+ if (typeof payload === "string") {
657
+ try {
658
+ return mapDeepSeekError(JSON.parse(payload));
659
+ } catch {
660
+ return { error: { message: payload, code: "unknown_error" } };
661
+ }
662
+ }
663
+ const data = payload;
664
+ const code = data?.error?.code ?? data?.error?.type ?? "unknown_error";
665
+ const mappedMessage = codeMessageMap2[code] ?? data?.error?.message ?? "DeepSeek \u8BF7\u6C42\u5931\u8D25";
666
+ return {
667
+ error: {
668
+ code,
669
+ message: mappedMessage
670
+ }
671
+ };
672
+ }
673
+ function createDeepSeekConnector(config) {
674
+ return createOpenAIConnector(config, {
675
+ defaultPath: "v1/chat/completions",
676
+ mapErrorBody: mapDeepSeekError
677
+ });
678
+ }
679
+
680
+ // providers/anthropic.ts
681
+ import { fetch as fetch2 } from "undici";
682
+ var DEFAULT_VERSION = "2023-06-01";
683
+ function createAnthropicConnector(config) {
684
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
685
+ return {
686
+ id: config.id,
687
+ async send(request) {
688
+ const headers = {
689
+ "Content-Type": "application/json",
690
+ "anthropic-version": DEFAULT_VERSION,
691
+ ...config.extraHeaders,
692
+ ...request.headers
693
+ };
694
+ delete headers.Authorization;
695
+ if (config.apiKey) {
696
+ headers["x-api-key"] = config.apiKey;
697
+ }
698
+ if (!headers["anthropic-version"]) {
699
+ headers["anthropic-version"] = DEFAULT_VERSION;
700
+ }
701
+ const payload = {
702
+ ...request.body,
703
+ model: request.model,
704
+ stream: request.stream ?? false
705
+ };
706
+ const response = await fetch2(`${baseUrl}/messages`, {
707
+ method: "POST",
708
+ headers,
709
+ body: JSON.stringify(payload)
710
+ });
711
+ return {
712
+ status: response.status,
713
+ headers: response.headers,
714
+ body: response.body
715
+ };
716
+ }
717
+ };
718
+ }
719
+
720
+ // providers/registry.ts
721
+ var connectors = /* @__PURE__ */ new Map();
722
+ function buildConnector(config) {
723
+ switch (config.type) {
724
+ case "deepseek":
725
+ return createDeepSeekConnector(config);
726
+ case "kimi":
727
+ return createKimiConnector(config);
728
+ case "anthropic":
729
+ return createAnthropicConnector(config);
730
+ case "openai":
731
+ case "custom":
732
+ default:
733
+ return createOpenAIConnector(config);
734
+ }
735
+ }
736
+ function rebuildConnectors() {
737
+ const config = getConfig();
738
+ connectors = new Map(config.providers.map((provider) => [provider.id, buildConnector(provider)]));
739
+ }
740
+ rebuildConnectors();
741
+ onConfigChange(() => rebuildConnectors());
742
+ function getConnector(providerId) {
743
+ const connector = connectors.get(providerId);
744
+ if (!connector) {
745
+ throw new Error(`\u672A\u627E\u5230 provider: ${providerId}`);
746
+ }
747
+ return connector;
748
+ }
749
+
750
+ // logging/logger.ts
751
+ import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from "zlib";
752
+
753
+ // storage/index.ts
754
+ import fs2 from "fs";
755
+ import os2 from "os";
756
+ import path2 from "path";
757
+ import Database from "better-sqlite3";
758
+ var HOME_DIR2 = path2.join(os2.homedir(), ".cc-gw");
759
+ var DATA_DIR = path2.join(HOME_DIR2, "data");
760
+ var DB_PATH = path2.join(DATA_DIR, "gateway.db");
761
+ var db = null;
762
+ function ensureSchema(instance) {
763
+ instance.exec(`
764
+ CREATE TABLE IF NOT EXISTS request_logs (
765
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
766
+ timestamp INTEGER NOT NULL,
767
+ session_id TEXT,
768
+ provider TEXT NOT NULL,
769
+ model TEXT NOT NULL,
770
+ client_model TEXT,
771
+ latency_ms INTEGER,
772
+ status_code INTEGER,
773
+ input_tokens INTEGER,
774
+ output_tokens INTEGER,
775
+ cached_tokens INTEGER,
776
+ ttft_ms INTEGER,
777
+ tpot_ms REAL,
778
+ error TEXT
779
+ );
780
+
781
+ CREATE TABLE IF NOT EXISTS request_payloads (
782
+ request_id INTEGER PRIMARY KEY,
783
+ prompt TEXT,
784
+ response TEXT,
785
+ FOREIGN KEY(request_id) REFERENCES request_logs(id) ON DELETE CASCADE
786
+ );
787
+
788
+ CREATE TABLE IF NOT EXISTS daily_metrics (
789
+ date TEXT PRIMARY KEY,
790
+ request_count INTEGER DEFAULT 0,
791
+ total_input_tokens INTEGER DEFAULT 0,
792
+ total_output_tokens INTEGER DEFAULT 0,
793
+ total_latency_ms INTEGER DEFAULT 0
794
+ );
795
+ `);
796
+ }
797
+ function getDb() {
798
+ if (db)
799
+ return db;
800
+ fs2.mkdirSync(DATA_DIR, { recursive: true });
801
+ db = new Database(DB_PATH);
802
+ ensureSchema(db);
803
+ ensureColumns(db);
804
+ return db;
805
+ }
806
+ function ensureColumns(instance) {
807
+ const columns = instance.prepare("PRAGMA table_info(request_logs)").all();
808
+ const hasCachedTokens = columns.some((column) => column.name === "cached_tokens");
809
+ if (!hasCachedTokens) {
810
+ instance.exec("ALTER TABLE request_logs ADD COLUMN cached_tokens INTEGER");
811
+ }
812
+ const hasClientModel = columns.some((column) => column.name === "client_model");
813
+ if (!hasClientModel) {
814
+ instance.exec("ALTER TABLE request_logs ADD COLUMN client_model TEXT");
815
+ }
816
+ const hasTtft = columns.some((column) => column.name === "ttft_ms");
817
+ if (!hasTtft) {
818
+ instance.exec("ALTER TABLE request_logs ADD COLUMN ttft_ms INTEGER");
819
+ }
820
+ const hasTpot = columns.some((column) => column.name === "tpot_ms");
821
+ if (!hasTpot) {
822
+ instance.exec("ALTER TABLE request_logs ADD COLUMN tpot_ms REAL");
823
+ }
824
+ }
825
+
826
+ // 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
+ var BROTLI_OPTIONS = {
852
+ params: {
853
+ [zlibConstants.BROTLI_PARAM_QUALITY]: 1
854
+ }
855
+ };
856
+ function compressPayload(value) {
857
+ if (value === void 0 || value === null) {
858
+ return null;
859
+ }
860
+ if (value.length === 0) {
861
+ return Buffer.alloc(0);
862
+ }
863
+ return brotliCompressSync(Buffer.from(value, "utf8"), BROTLI_OPTIONS);
864
+ }
865
+ function decompressPayload(value) {
866
+ if (value === void 0 || value === null) {
867
+ return null;
868
+ }
869
+ if (typeof value === "string") {
870
+ return value;
871
+ }
872
+ if (Buffer.isBuffer(value)) {
873
+ if (value.length === 0) {
874
+ return "";
875
+ }
876
+ try {
877
+ const decompressed = brotliDecompressSync(value);
878
+ return decompressed.toString("utf8");
879
+ } catch {
880
+ return value.toString("utf8");
881
+ }
882
+ }
883
+ return null;
884
+ }
885
+ function updateLogTokens(requestId, values) {
886
+ const db2 = getDb();
887
+ const setters = ["input_tokens = ?", "output_tokens = ?", "cached_tokens = ?"];
888
+ const params = [
889
+ values.inputTokens,
890
+ values.outputTokens,
891
+ values.cachedTokens ?? null
892
+ ];
893
+ if (values.ttftMs !== void 0) {
894
+ setters.push("ttft_ms = ?");
895
+ params.push(values.ttftMs ?? null);
896
+ }
897
+ if (values.tpotMs !== void 0) {
898
+ setters.push("tpot_ms = ?");
899
+ params.push(values.tpotMs ?? null);
900
+ }
901
+ db2.prepare(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`).run(...params, requestId);
902
+ }
903
+ function finalizeLog(requestId, info) {
904
+ const db2 = getDb();
905
+ const setters = [];
906
+ const values = [];
907
+ if (info.latencyMs !== void 0) {
908
+ setters.push("latency_ms = ?");
909
+ values.push(info.latencyMs);
910
+ }
911
+ if (info.statusCode !== void 0) {
912
+ setters.push("status_code = ?");
913
+ values.push(info.statusCode ?? null);
914
+ }
915
+ if (info.error !== void 0) {
916
+ setters.push("error = ?");
917
+ values.push(info.error ?? null);
918
+ }
919
+ if (info.clientModel !== void 0) {
920
+ setters.push("client_model = ?");
921
+ values.push(info.clientModel ?? null);
922
+ }
923
+ if (setters.length === 0)
924
+ return;
925
+ const stmt = db2.prepare(`UPDATE request_logs SET ${setters.join(", ")} WHERE id = ?`);
926
+ stmt.run(...values, requestId);
927
+ }
928
+ function upsertLogPayload(requestId, payload) {
929
+ if (payload.prompt === void 0 && payload.response === void 0) {
930
+ return;
931
+ }
932
+ const db2 = getDb();
933
+ const promptData = payload.prompt === void 0 ? null : compressPayload(payload.prompt);
934
+ const responseData = payload.response === void 0 ? null : compressPayload(payload.response);
935
+ db2.prepare(`
936
+ INSERT INTO request_payloads (request_id, prompt, response)
937
+ VALUES (?, ?, ?)
938
+ ON CONFLICT(request_id) DO UPDATE SET
939
+ prompt = COALESCE(excluded.prompt, request_payloads.prompt),
940
+ response = COALESCE(excluded.response, request_payloads.response)
941
+ `).run(
942
+ requestId,
943
+ promptData,
944
+ responseData
945
+ );
946
+ }
947
+ function updateMetrics(date, delta) {
948
+ const db2 = getDb();
949
+ db2.prepare(`
950
+ INSERT INTO daily_metrics (date, request_count, total_input_tokens, total_output_tokens, total_latency_ms)
951
+ VALUES (@date, @requests, @inputTokens, @outputTokens, @latencyMs)
952
+ ON CONFLICT(date) DO UPDATE SET
953
+ request_count = daily_metrics.request_count + excluded.request_count,
954
+ total_input_tokens = daily_metrics.total_input_tokens + excluded.total_input_tokens,
955
+ total_output_tokens = daily_metrics.total_output_tokens + excluded.total_output_tokens,
956
+ total_latency_ms = daily_metrics.total_latency_ms + excluded.total_latency_ms
957
+ `).run({
958
+ date,
959
+ requests: delta.requests,
960
+ inputTokens: delta.inputTokens,
961
+ outputTokens: delta.outputTokens,
962
+ latencyMs: delta.latencyMs
963
+ });
964
+ }
965
+
966
+ // metrics/activity.ts
967
+ var activeRequests = 0;
968
+ function incrementActiveRequests() {
969
+ activeRequests += 1;
970
+ }
971
+ function decrementActiveRequests() {
972
+ if (activeRequests > 0) {
973
+ activeRequests -= 1;
974
+ }
975
+ }
976
+ function getActiveRequestCount() {
977
+ return activeRequests;
978
+ }
979
+
980
+ // routes/messages.ts
981
+ function mapStopReason(reason) {
982
+ switch (reason) {
983
+ case "stop":
984
+ return "end_turn";
985
+ case "tool_calls":
986
+ return "tool_use";
987
+ case "length":
988
+ return "max_tokens";
989
+ default:
990
+ return reason ?? null;
991
+ }
992
+ }
993
+ var roundTwoDecimals = (value) => Math.round(value * 100) / 100;
994
+ function computeTpot(totalLatencyMs, outputTokens, options) {
995
+ if (!Number.isFinite(outputTokens) || outputTokens <= 0) {
996
+ return null;
997
+ }
998
+ const streaming = options?.streaming ?? false;
999
+ const ttftMs = options?.ttftMs ?? null;
1000
+ if (streaming && (ttftMs === null || ttftMs === void 0)) {
1001
+ return null;
1002
+ }
1003
+ const effectiveLatency = streaming && ttftMs != null ? Math.max(totalLatencyMs - ttftMs, 0) : totalLatencyMs;
1004
+ const raw = effectiveLatency / outputTokens;
1005
+ return Number.isFinite(raw) ? roundTwoDecimals(raw) : null;
1006
+ }
1007
+ function resolveCachedTokens(usage) {
1008
+ if (!usage || typeof usage !== "object") {
1009
+ return null;
1010
+ }
1011
+ if (typeof usage.cached_tokens === "number") {
1012
+ return usage.cached_tokens;
1013
+ }
1014
+ const promptDetails = usage.prompt_tokens_details;
1015
+ if (promptDetails && typeof promptDetails.cached_tokens === "number") {
1016
+ return promptDetails.cached_tokens;
1017
+ }
1018
+ return null;
1019
+ }
1020
+ function buildClaudeResponse(openAI, model) {
1021
+ const choice = openAI.choices?.[0];
1022
+ const message = choice?.message ?? {};
1023
+ const contentBlocks = [];
1024
+ if (typeof message.content === "string" && message.content.length > 0) {
1025
+ contentBlocks.push({ type: "text", text: message.content });
1026
+ }
1027
+ if (Array.isArray(message.tool_calls)) {
1028
+ for (const call of message.tool_calls) {
1029
+ contentBlocks.push({
1030
+ type: "tool_use",
1031
+ id: call.id || `tool_${Math.random().toString(36).slice(2)}`,
1032
+ name: call.function?.name,
1033
+ input: (() => {
1034
+ try {
1035
+ return call.function?.arguments ? JSON.parse(call.function.arguments) : {};
1036
+ } catch {
1037
+ return {};
1038
+ }
1039
+ })()
1040
+ });
1041
+ }
1042
+ }
1043
+ return {
1044
+ id: openAI.id ? openAI.id.replace("chatcmpl", "msg") : `msg_${Math.random().toString(36).slice(2)}`,
1045
+ type: "message",
1046
+ role: "assistant",
1047
+ model,
1048
+ content: contentBlocks,
1049
+ stop_reason: mapStopReason(choice?.finish_reason),
1050
+ stop_sequence: null,
1051
+ usage: {
1052
+ input_tokens: openAI.usage?.prompt_tokens ?? 0,
1053
+ output_tokens: openAI.usage?.completion_tokens ?? 0
1054
+ }
1055
+ };
1056
+ }
1057
+ async function registerMessagesRoute(app) {
1058
+ app.post("/v1/messages", async (request, reply) => {
1059
+ const payload = request.body;
1060
+ if (!payload || typeof payload !== "object") {
1061
+ reply.code(400);
1062
+ return { error: "Invalid request body" };
1063
+ }
1064
+ const normalized = normalizeClaudePayload(payload);
1065
+ const requestedModel = typeof payload.model === "string" ? payload.model : void 0;
1066
+ const target = resolveRoute({
1067
+ payload: normalized,
1068
+ requestedModel
1069
+ });
1070
+ const providerType = target.provider.type ?? "custom";
1071
+ const providerBody = providerType === "anthropic" ? buildAnthropicBody(normalized, {
1072
+ maxTokens: payload.max_tokens ?? target.provider.models?.find((m) => m.id === target.modelId)?.maxTokens,
1073
+ temperature: payload.temperature,
1074
+ toolChoice: payload.tool_choice,
1075
+ overrideTools: payload.tools
1076
+ }) : buildProviderBody(normalized, {
1077
+ maxTokens: payload.max_tokens ?? target.provider.models?.find((m) => m.id === target.modelId)?.maxTokens,
1078
+ temperature: payload.temperature,
1079
+ toolChoice: payload.tool_choice,
1080
+ overrideTools: payload.tools
1081
+ });
1082
+ const connector = getConnector(target.providerId);
1083
+ const requestStart = Date.now();
1084
+ const storePayloads = getConfig().storePayloads !== false;
1085
+ const logId = recordLog({
1086
+ timestamp: requestStart,
1087
+ provider: target.providerId,
1088
+ model: target.modelId,
1089
+ clientModel: requestedModel,
1090
+ sessionId: payload.metadata?.user_id
1091
+ });
1092
+ incrementActiveRequests();
1093
+ if (storePayloads) {
1094
+ upsertLogPayload(logId, {
1095
+ prompt: (() => {
1096
+ try {
1097
+ return JSON.stringify(payload);
1098
+ } catch {
1099
+ return null;
1100
+ }
1101
+ })()
1102
+ });
1103
+ }
1104
+ let finalized = false;
1105
+ const finalize = (statusCode, error) => {
1106
+ if (finalized)
1107
+ return;
1108
+ finalizeLog(logId, {
1109
+ latencyMs: Date.now() - requestStart,
1110
+ statusCode,
1111
+ error,
1112
+ clientModel: requestedModel ?? null
1113
+ });
1114
+ finalized = true;
1115
+ };
1116
+ const logUsage = (stage, usage) => {
1117
+ request.log.info(
1118
+ {
1119
+ event: "usage.metrics",
1120
+ stage,
1121
+ provider: target.providerId,
1122
+ model: target.modelId,
1123
+ stream: normalized.stream,
1124
+ tokens: usage
1125
+ },
1126
+ "upstream usage summary"
1127
+ );
1128
+ console.info("[cc-gw][usage]", stage, {
1129
+ provider: target.providerId,
1130
+ model: target.modelId,
1131
+ stream: normalized.stream,
1132
+ tokens: usage
1133
+ });
1134
+ };
1135
+ try {
1136
+ const upstream = await connector.send({
1137
+ model: target.modelId,
1138
+ body: providerBody,
1139
+ stream: normalized.stream
1140
+ });
1141
+ if (upstream.status >= 400) {
1142
+ reply.code(upstream.status);
1143
+ const bodyText = upstream.body ? await new Response(upstream.body).text() : "";
1144
+ const errorText = bodyText || "Upstream provider error";
1145
+ if (storePayloads) {
1146
+ upsertLogPayload(logId, { response: bodyText || null });
1147
+ }
1148
+ finalize(upstream.status, errorText);
1149
+ return { error: errorText };
1150
+ }
1151
+ if (!normalized.stream) {
1152
+ const json = await new Response(upstream.body).json();
1153
+ if (providerType === "anthropic") {
1154
+ let inputTokens2 = json.usage?.input_tokens ?? 0;
1155
+ let outputTokens2 = json.usage?.output_tokens ?? 0;
1156
+ const cachedTokens2 = resolveCachedTokens(json.usage);
1157
+ if (!inputTokens2) {
1158
+ inputTokens2 = target.tokenEstimate || estimateTokens(normalized, target.modelId);
1159
+ }
1160
+ if (!outputTokens2) {
1161
+ const textBlocks = Array.isArray(json.content) ? json.content.filter((block) => block?.type === "text").map((block) => block.text ?? "").join("\n") : "";
1162
+ outputTokens2 = estimateTextTokens(textBlocks, target.modelId);
1163
+ }
1164
+ logUsage("non_stream.anthropic", {
1165
+ input: inputTokens2,
1166
+ output: outputTokens2,
1167
+ cached: cachedTokens2
1168
+ });
1169
+ const latencyMs2 = Date.now() - requestStart;
1170
+ updateLogTokens(logId, {
1171
+ inputTokens: inputTokens2,
1172
+ outputTokens: outputTokens2,
1173
+ cachedTokens: cachedTokens2,
1174
+ ttftMs: latencyMs2,
1175
+ tpotMs: computeTpot(latencyMs2, outputTokens2, { streaming: false })
1176
+ });
1177
+ updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1178
+ requests: 1,
1179
+ inputTokens: inputTokens2,
1180
+ outputTokens: outputTokens2,
1181
+ latencyMs: latencyMs2
1182
+ });
1183
+ if (storePayloads) {
1184
+ upsertLogPayload(logId, {
1185
+ response: (() => {
1186
+ try {
1187
+ return JSON.stringify(json);
1188
+ } catch {
1189
+ return null;
1190
+ }
1191
+ })()
1192
+ });
1193
+ }
1194
+ finalize(200, null);
1195
+ reply.header("content-type", "application/json");
1196
+ return json;
1197
+ }
1198
+ const claudeResponse = buildClaudeResponse(json, target.modelId);
1199
+ let inputTokens = json.usage?.prompt_tokens ?? 0;
1200
+ let outputTokens = json.usage?.completion_tokens ?? 0;
1201
+ const cachedTokens = resolveCachedTokens(json.usage);
1202
+ if (!inputTokens) {
1203
+ inputTokens = target.tokenEstimate || estimateTokens(normalized, target.modelId);
1204
+ }
1205
+ if (!outputTokens) {
1206
+ const text = claudeResponse.content.filter((block) => block?.type === "text").map((block) => block.text ?? "").join("\n");
1207
+ outputTokens = estimateTextTokens(text, target.modelId);
1208
+ }
1209
+ logUsage("non_stream.openai", {
1210
+ input: inputTokens,
1211
+ output: outputTokens,
1212
+ cached: cachedTokens
1213
+ });
1214
+ const latencyMs = Date.now() - requestStart;
1215
+ updateLogTokens(logId, {
1216
+ inputTokens,
1217
+ outputTokens,
1218
+ cachedTokens,
1219
+ ttftMs: latencyMs,
1220
+ tpotMs: computeTpot(latencyMs, outputTokens, { streaming: false })
1221
+ });
1222
+ updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1223
+ requests: 1,
1224
+ inputTokens,
1225
+ outputTokens,
1226
+ latencyMs
1227
+ });
1228
+ if (storePayloads) {
1229
+ upsertLogPayload(logId, {
1230
+ response: (() => {
1231
+ try {
1232
+ return JSON.stringify(claudeResponse);
1233
+ } catch {
1234
+ return null;
1235
+ }
1236
+ })()
1237
+ });
1238
+ }
1239
+ finalize(200, null);
1240
+ reply.header("content-type", "application/json");
1241
+ return claudeResponse;
1242
+ }
1243
+ if (!upstream.body) {
1244
+ reply.code(500);
1245
+ finalize(500, "Upstream returned empty body");
1246
+ return { error: "Upstream returned empty body" };
1247
+ }
1248
+ reply.header("content-type", "text/event-stream; charset=utf-8");
1249
+ reply.header("cache-control", "no-cache, no-store, must-revalidate");
1250
+ reply.header("connection", "keep-alive");
1251
+ reply.raw.writeHead(200);
1252
+ if (providerType === "anthropic") {
1253
+ const reader2 = upstream.body.getReader();
1254
+ const decoder2 = new TextDecoder();
1255
+ let buffer2 = "";
1256
+ let currentEvent = null;
1257
+ let usagePrompt2 = 0;
1258
+ let usageCompletion2 = 0;
1259
+ let usageCached2 = null;
1260
+ let accumulatedContent2 = "";
1261
+ while (true) {
1262
+ const { value, done } = await reader2.read();
1263
+ if (done)
1264
+ break;
1265
+ if (!value)
1266
+ continue;
1267
+ const chunk = decoder2.decode(value, { stream: true });
1268
+ buffer2 += chunk;
1269
+ let newlineIndex = buffer2.indexOf("\n");
1270
+ while (newlineIndex !== -1) {
1271
+ const line = buffer2.slice(0, newlineIndex + 1);
1272
+ buffer2 = buffer2.slice(newlineIndex + 1);
1273
+ const trimmed = line.trim();
1274
+ if (trimmed.startsWith("event:")) {
1275
+ currentEvent = trimmed.slice(6).trim();
1276
+ } else if (trimmed.startsWith("data:")) {
1277
+ if (currentEvent === "message_delta" || currentEvent === "message_stop") {
1278
+ try {
1279
+ const data = JSON.parse(trimmed.slice(5).trim());
1280
+ if (data?.usage) {
1281
+ usagePrompt2 = data.usage.input_tokens ?? usagePrompt2;
1282
+ usageCompletion2 = data.usage.output_tokens ?? usageCompletion2;
1283
+ if (typeof data.usage.cached_tokens === "number") {
1284
+ usageCached2 = data.usage.cached_tokens;
1285
+ }
1286
+ }
1287
+ const deltaText = data?.delta?.text;
1288
+ if (typeof deltaText === "string") {
1289
+ accumulatedContent2 += deltaText;
1290
+ }
1291
+ } catch (error) {
1292
+ request.log.warn({ error }, "Failed to parse Anthropic SSE data");
1293
+ }
1294
+ }
1295
+ }
1296
+ reply.raw.write(line);
1297
+ newlineIndex = buffer2.indexOf("\n");
1298
+ }
1299
+ }
1300
+ if (buffer2.length > 0) {
1301
+ reply.raw.write(buffer2);
1302
+ }
1303
+ reply.raw.end();
1304
+ if (!usagePrompt2) {
1305
+ usagePrompt2 = target.tokenEstimate || estimateTokens(normalized, target.modelId);
1306
+ }
1307
+ if (!usageCompletion2) {
1308
+ usageCompletion2 = accumulatedContent2 ? estimateTextTokens(accumulatedContent2, target.modelId) : estimateTextTokens("", target.modelId);
1309
+ }
1310
+ const totalLatencyMs = Date.now() - requestStart;
1311
+ const ttftMs = firstTokenAt ? firstTokenAt - requestStart : null;
1312
+ logUsage("stream.anthropic.final", {
1313
+ input: usagePrompt2,
1314
+ output: usageCompletion2,
1315
+ cached: usageCached2
1316
+ });
1317
+ updateLogTokens(logId, {
1318
+ inputTokens: usagePrompt2,
1319
+ outputTokens: usageCompletion2,
1320
+ cachedTokens: usageCached2,
1321
+ ttftMs,
1322
+ tpotMs: computeTpot(totalLatencyMs, usageCompletion2, {
1323
+ streaming: true,
1324
+ ttftMs
1325
+ })
1326
+ });
1327
+ updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1328
+ requests: 1,
1329
+ inputTokens: usagePrompt2,
1330
+ outputTokens: usageCompletion2,
1331
+ latencyMs: totalLatencyMs
1332
+ });
1333
+ if (storePayloads) {
1334
+ upsertLogPayload(logId, {
1335
+ response: (() => {
1336
+ try {
1337
+ return JSON.stringify({
1338
+ content: accumulatedContent2,
1339
+ usage: {
1340
+ input: usagePrompt2,
1341
+ output: usageCompletion2,
1342
+ cached: usageCached2
1343
+ }
1344
+ });
1345
+ } catch {
1346
+ return accumulatedContent2;
1347
+ }
1348
+ })()
1349
+ });
1350
+ }
1351
+ finalize(200, null);
1352
+ return reply;
1353
+ }
1354
+ const reader = upstream.body.getReader();
1355
+ const decoder = new TextDecoder();
1356
+ let buffer = "";
1357
+ let textBlockStarted = false;
1358
+ let encounteredToolCall = false;
1359
+ const toolAccum = {};
1360
+ let usagePrompt = 0;
1361
+ let usageCompletion = 0;
1362
+ let usageCached = null;
1363
+ let accumulatedContent = "";
1364
+ let completed = false;
1365
+ let firstTokenAt = null;
1366
+ const encode = (event, data) => {
1367
+ reply.raw.write(`event: ${event}
1368
+ data: ${JSON.stringify(data)}
1369
+
1370
+ `);
1371
+ };
1372
+ encode("message_start", {
1373
+ type: "message_start",
1374
+ message: {
1375
+ id: `msg_${Math.random().toString(36).slice(2)}`,
1376
+ type: "message",
1377
+ role: "assistant",
1378
+ model: target.modelId,
1379
+ content: [],
1380
+ stop_reason: null,
1381
+ stop_sequence: null,
1382
+ usage: { input_tokens: 0, output_tokens: 0 }
1383
+ }
1384
+ });
1385
+ while (true) {
1386
+ const { value, done } = await reader.read();
1387
+ if (done)
1388
+ break;
1389
+ if (!value)
1390
+ continue;
1391
+ const chunk = decoder.decode(value, { stream: true });
1392
+ buffer += chunk;
1393
+ const lines = buffer.split("\n");
1394
+ if (!buffer.endsWith("\n")) {
1395
+ buffer = lines.pop() ?? "";
1396
+ } else {
1397
+ buffer = "";
1398
+ }
1399
+ for (const line of lines) {
1400
+ const trimmed = line.trim();
1401
+ if (!trimmed.startsWith("data:"))
1402
+ continue;
1403
+ const dataStr = trimmed.slice(5).trim();
1404
+ if (dataStr === "[DONE]") {
1405
+ if (encounteredToolCall) {
1406
+ for (const idx of Object.keys(toolAccum)) {
1407
+ encode("content_block_stop", {
1408
+ type: "content_block_stop",
1409
+ index: Number(idx)
1410
+ });
1411
+ }
1412
+ } else if (textBlockStarted) {
1413
+ encode("content_block_stop", {
1414
+ type: "content_block_stop",
1415
+ index: 0
1416
+ });
1417
+ }
1418
+ const finalPromptTokens = usagePrompt || target.tokenEstimate || estimateTokens(normalized, target.modelId);
1419
+ const finalCompletionTokens = usageCompletion || estimateTextTokens(accumulatedContent, target.modelId);
1420
+ encode("message_delta", {
1421
+ type: "message_delta",
1422
+ delta: {
1423
+ stop_reason: encounteredToolCall ? "tool_use" : "end_turn",
1424
+ stop_sequence: null
1425
+ },
1426
+ usage: {
1427
+ input_tokens: finalPromptTokens,
1428
+ output_tokens: finalCompletionTokens
1429
+ }
1430
+ });
1431
+ encode("message_stop", { type: "message_stop" });
1432
+ reply.raw.write("\n");
1433
+ reply.raw.end();
1434
+ const totalLatencyMs = Date.now() - requestStart;
1435
+ const ttftMs = firstTokenAt ? firstTokenAt - requestStart : null;
1436
+ logUsage("stream.openai.final", {
1437
+ input: finalPromptTokens,
1438
+ output: finalCompletionTokens,
1439
+ cached: usageCached
1440
+ });
1441
+ updateLogTokens(logId, {
1442
+ inputTokens: finalPromptTokens,
1443
+ outputTokens: finalCompletionTokens,
1444
+ cachedTokens: usageCached,
1445
+ ttftMs,
1446
+ tpotMs: computeTpot(totalLatencyMs, finalCompletionTokens, {
1447
+ streaming: true,
1448
+ ttftMs
1449
+ })
1450
+ });
1451
+ updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1452
+ requests: 1,
1453
+ inputTokens: finalPromptTokens,
1454
+ outputTokens: finalCompletionTokens,
1455
+ latencyMs: totalLatencyMs
1456
+ });
1457
+ if (storePayloads) {
1458
+ upsertLogPayload(logId, {
1459
+ response: (() => {
1460
+ try {
1461
+ return JSON.stringify({
1462
+ content: accumulatedContent,
1463
+ toolCalls: Object.keys(toolAccum).length > 0 ? toolAccum : void 0,
1464
+ usage: {
1465
+ input: finalPromptTokens,
1466
+ output: finalCompletionTokens,
1467
+ cached: usageCached
1468
+ }
1469
+ });
1470
+ } catch {
1471
+ return accumulatedContent;
1472
+ }
1473
+ })()
1474
+ });
1475
+ }
1476
+ finalize(200, null);
1477
+ completed = true;
1478
+ return reply;
1479
+ }
1480
+ let parsed;
1481
+ try {
1482
+ parsed = JSON.parse(dataStr);
1483
+ } catch {
1484
+ continue;
1485
+ }
1486
+ const choice = parsed.choices?.[0];
1487
+ if (!choice)
1488
+ continue;
1489
+ const usagePayload = parsed.usage || choice.usage || choice.delta && choice.delta.usage || null;
1490
+ if (usagePayload) {
1491
+ usagePrompt = usagePayload.prompt_tokens ?? usagePrompt;
1492
+ usageCompletion = usagePayload.completion_tokens ?? usageCompletion;
1493
+ if (typeof usagePayload.cached_tokens === "number") {
1494
+ usageCached = usagePayload.cached_tokens;
1495
+ }
1496
+ }
1497
+ if (choice.delta?.tool_calls) {
1498
+ if (!firstTokenAt) {
1499
+ firstTokenAt = Date.now();
1500
+ }
1501
+ encounteredToolCall = true;
1502
+ for (const toolCall of choice.delta.tool_calls) {
1503
+ const idx = toolCall.index ?? 0;
1504
+ if (toolAccum[idx] === void 0) {
1505
+ toolAccum[idx] = "";
1506
+ encode("content_block_start", {
1507
+ type: "content_block_start",
1508
+ index: idx,
1509
+ content_block: {
1510
+ type: "tool_use",
1511
+ id: toolCall.id || `tool_${Date.now()}_${idx}`,
1512
+ name: toolCall.function?.name,
1513
+ input: {}
1514
+ }
1515
+ });
1516
+ }
1517
+ const deltaArgs = toolCall.function?.arguments || "";
1518
+ if (deltaArgs) {
1519
+ toolAccum[idx] += deltaArgs;
1520
+ encode("content_block_delta", {
1521
+ type: "content_block_delta",
1522
+ index: idx,
1523
+ delta: {
1524
+ type: "input_json_delta",
1525
+ partial_json: deltaArgs
1526
+ }
1527
+ });
1528
+ }
1529
+ }
1530
+ continue;
1531
+ }
1532
+ if (choice.delta?.content) {
1533
+ if (!firstTokenAt && choice.delta.content.length > 0) {
1534
+ firstTokenAt = Date.now();
1535
+ }
1536
+ if (!textBlockStarted) {
1537
+ textBlockStarted = true;
1538
+ encode("content_block_start", {
1539
+ type: "content_block_start",
1540
+ index: 0,
1541
+ content_block: {
1542
+ type: "text",
1543
+ text: ""
1544
+ }
1545
+ });
1546
+ }
1547
+ encode("content_block_delta", {
1548
+ type: "content_block_delta",
1549
+ index: 0,
1550
+ delta: {
1551
+ type: "text_delta",
1552
+ text: choice.delta.content
1553
+ }
1554
+ });
1555
+ accumulatedContent += choice.delta.content ?? "";
1556
+ }
1557
+ if (choice.delta?.reasoning) {
1558
+ if (!firstTokenAt) {
1559
+ firstTokenAt = Date.now();
1560
+ }
1561
+ if (!textBlockStarted) {
1562
+ textBlockStarted = true;
1563
+ encode("content_block_start", {
1564
+ type: "content_block_start",
1565
+ index: 0,
1566
+ content_block: {
1567
+ type: "text",
1568
+ text: ""
1569
+ }
1570
+ });
1571
+ }
1572
+ encode("content_block_delta", {
1573
+ type: "content_block_delta",
1574
+ index: 0,
1575
+ delta: {
1576
+ type: "thinking_delta",
1577
+ thinking: choice.delta.reasoning
1578
+ }
1579
+ });
1580
+ }
1581
+ }
1582
+ }
1583
+ if (!completed) {
1584
+ reply.raw.end();
1585
+ const totalLatencyMs = Date.now() - requestStart;
1586
+ const fallbackPrompt = usagePrompt || target.tokenEstimate || estimateTokens(normalized, target.modelId);
1587
+ const fallbackCompletion = usageCompletion || estimateTextTokens(accumulatedContent, target.modelId);
1588
+ const ttftMs = firstTokenAt ? firstTokenAt - requestStart : null;
1589
+ logUsage("stream.openai.fallback", {
1590
+ input: fallbackPrompt,
1591
+ output: fallbackCompletion,
1592
+ cached: usageCached
1593
+ });
1594
+ updateLogTokens(logId, {
1595
+ inputTokens: fallbackPrompt,
1596
+ outputTokens: fallbackCompletion,
1597
+ cachedTokens: usageCached,
1598
+ ttftMs,
1599
+ tpotMs: computeTpot(totalLatencyMs, fallbackCompletion, {
1600
+ streaming: true,
1601
+ ttftMs
1602
+ })
1603
+ });
1604
+ updateMetrics((/* @__PURE__ */ new Date()).toISOString().slice(0, 10), {
1605
+ requests: 1,
1606
+ inputTokens: fallbackPrompt,
1607
+ outputTokens: fallbackCompletion,
1608
+ latencyMs: totalLatencyMs
1609
+ });
1610
+ if (storePayloads) {
1611
+ upsertLogPayload(logId, {
1612
+ response: (() => {
1613
+ try {
1614
+ return JSON.stringify({
1615
+ content: accumulatedContent,
1616
+ usage: {
1617
+ input: fallbackPrompt,
1618
+ output: fallbackCompletion,
1619
+ cached: usageCached
1620
+ }
1621
+ });
1622
+ } catch {
1623
+ return accumulatedContent;
1624
+ }
1625
+ })()
1626
+ });
1627
+ }
1628
+ finalize(200, null);
1629
+ return reply;
1630
+ }
1631
+ } catch (err) {
1632
+ const message = err instanceof Error ? err.message : "Unexpected error";
1633
+ if (!reply.sent) {
1634
+ reply.code(500);
1635
+ }
1636
+ finalize(reply.statusCode >= 400 ? reply.statusCode : 500, message);
1637
+ return { error: message };
1638
+ } finally {
1639
+ decrementActiveRequests();
1640
+ if (!finalized && reply.sent) {
1641
+ finalize(reply.statusCode ?? 200, null);
1642
+ }
1643
+ }
1644
+ });
1645
+ }
1646
+
1647
+ // logging/queries.ts
1648
+ function queryLogs(options = {}) {
1649
+ const db2 = getDb();
1650
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
1651
+ const offset = Math.max(options.offset ?? 0, 0);
1652
+ const conditions = [];
1653
+ const params = {};
1654
+ if (options.provider) {
1655
+ conditions.push("provider = @provider");
1656
+ params.provider = options.provider;
1657
+ }
1658
+ if (options.model) {
1659
+ conditions.push("model = @model");
1660
+ params.model = options.model;
1661
+ }
1662
+ if (options.status === "success") {
1663
+ conditions.push("error IS NULL");
1664
+ } else if (options.status === "error") {
1665
+ conditions.push("error IS NOT NULL");
1666
+ }
1667
+ if (typeof options.from === "number") {
1668
+ conditions.push("timestamp >= @from");
1669
+ params.from = options.from;
1670
+ }
1671
+ if (typeof options.to === "number") {
1672
+ conditions.push("timestamp <= @to");
1673
+ params.to = options.to;
1674
+ }
1675
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1676
+ const totalRow = db2.prepare(`SELECT COUNT(*) AS count FROM request_logs ${whereClause}`).get(params);
1677
+ const items = db2.prepare(
1678
+ `SELECT id, timestamp, session_id, provider, model, client_model, latency_ms, status_code, input_tokens, output_tokens, cached_tokens, ttft_ms, tpot_ms, error
1679
+ FROM request_logs
1680
+ ${whereClause}
1681
+ ORDER BY timestamp DESC
1682
+ LIMIT @limit OFFSET @offset`
1683
+ ).all({ ...params, limit, offset });
1684
+ return {
1685
+ total: totalRow?.count ?? 0,
1686
+ items
1687
+ };
1688
+ }
1689
+ function getLogDetail(id) {
1690
+ const db2 = getDb();
1691
+ const record = db2.prepare(
1692
+ `SELECT id, timestamp, session_id, provider, model, client_model, latency_ms, status_code, input_tokens, output_tokens, cached_tokens, ttft_ms, tpot_ms, error
1693
+ FROM request_logs
1694
+ WHERE id = ?`
1695
+ ).get(id);
1696
+ return record ?? null;
1697
+ }
1698
+ function getLogPayload(id) {
1699
+ const db2 = getDb();
1700
+ const payload = db2.prepare(`SELECT prompt, response FROM request_payloads WHERE request_id = ?`).get(id);
1701
+ if (!payload) {
1702
+ return null;
1703
+ }
1704
+ return {
1705
+ prompt: decompressPayload(payload.prompt),
1706
+ response: decompressPayload(payload.response)
1707
+ };
1708
+ }
1709
+ function cleanupLogsBefore(timestamp) {
1710
+ const db2 = getDb();
1711
+ const stmt = db2.prepare(`DELETE FROM request_logs WHERE timestamp < ?`);
1712
+ const result = stmt.run(timestamp);
1713
+ return Number(result.changes ?? 0);
1714
+ }
1715
+ function getDailyMetrics(days = 7) {
1716
+ const db2 = getDb();
1717
+ const rows = db2.prepare(
1718
+ `SELECT date, request_count AS requestCount, total_input_tokens AS inputTokens,
1719
+ total_output_tokens AS outputTokens, total_latency_ms AS totalLatency
1720
+ FROM daily_metrics
1721
+ ORDER BY date DESC
1722
+ LIMIT ?`
1723
+ ).all(days);
1724
+ return rows.map((row) => ({
1725
+ date: row.date,
1726
+ requestCount: row.requestCount ?? 0,
1727
+ inputTokens: row.inputTokens ?? 0,
1728
+ outputTokens: row.outputTokens ?? 0,
1729
+ avgLatencyMs: row.requestCount ? Math.round((row.totalLatency ?? 0) / row.requestCount) : 0
1730
+ })).reverse();
1731
+ }
1732
+ function getMetricsOverview() {
1733
+ const db2 = getDb();
1734
+ const totalsRow = db2.prepare(
1735
+ `SELECT
1736
+ COALESCE(SUM(request_count), 0) AS requests,
1737
+ COALESCE(SUM(total_input_tokens), 0) AS inputTokens,
1738
+ COALESCE(SUM(total_output_tokens), 0) AS outputTokens,
1739
+ COALESCE(SUM(total_latency_ms), 0) AS totalLatency
1740
+ FROM daily_metrics`
1741
+ ).get();
1742
+ const todayKey = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1743
+ const todayRow = db2.prepare(
1744
+ `SELECT request_count AS requests,
1745
+ total_input_tokens AS inputTokens,
1746
+ total_output_tokens AS outputTokens,
1747
+ total_latency_ms AS totalLatency
1748
+ FROM daily_metrics WHERE date = ?`
1749
+ ).get(todayKey);
1750
+ const resolveAvg = (totalLatency, requests) => requests > 0 ? Math.round(totalLatency / requests) : 0;
1751
+ const totalsRequests = totalsRow.requests ?? 0;
1752
+ const totalsLatency = totalsRow.totalLatency ?? 0;
1753
+ const todayRequests = todayRow?.requests ?? 0;
1754
+ const todayLatency = todayRow?.totalLatency ?? 0;
1755
+ return {
1756
+ totals: {
1757
+ requests: totalsRequests,
1758
+ inputTokens: totalsRow.inputTokens ?? 0,
1759
+ outputTokens: totalsRow.outputTokens ?? 0,
1760
+ avgLatencyMs: resolveAvg(totalsLatency, totalsRequests)
1761
+ },
1762
+ today: {
1763
+ requests: todayRequests,
1764
+ inputTokens: todayRow?.inputTokens ?? 0,
1765
+ outputTokens: todayRow?.outputTokens ?? 0,
1766
+ avgLatencyMs: resolveAvg(todayLatency, todayRequests)
1767
+ }
1768
+ };
1769
+ }
1770
+ function getModelUsageMetrics(days = 7, limit = 10) {
1771
+ const db2 = getDb();
1772
+ const since = Date.now() - days * 24 * 60 * 60 * 1e3;
1773
+ const rows = db2.prepare(
1774
+ `SELECT
1775
+ model,
1776
+ provider,
1777
+ COUNT(*) AS requests,
1778
+ COALESCE(SUM(input_tokens), 0) AS inputTokens,
1779
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
1780
+ COALESCE(SUM(latency_ms), 0) AS totalLatency
1781
+ FROM request_logs
1782
+ WHERE timestamp >= ?
1783
+ GROUP BY provider, model
1784
+ ORDER BY requests DESC
1785
+ LIMIT ?`
1786
+ ).all(since, limit);
1787
+ return rows.map((row) => ({
1788
+ model: row.model,
1789
+ provider: row.provider,
1790
+ requests: row.requests ?? 0,
1791
+ inputTokens: row.inputTokens ?? 0,
1792
+ outputTokens: row.outputTokens ?? 0,
1793
+ avgLatencyMs: row.requests ? Math.round((row.totalLatency ?? 0) / row.requests) : 0
1794
+ }));
1795
+ }
1796
+
1797
+ // routes/admin.ts
1798
+ async function registerAdminRoutes(app) {
1799
+ app.get("/api/status", async () => {
1800
+ const config = getConfig();
1801
+ return {
1802
+ port: config.port,
1803
+ host: config.host,
1804
+ providers: config.providers.length,
1805
+ activeRequests: getActiveRequestCount()
1806
+ };
1807
+ });
1808
+ app.get("/api/providers", async () => {
1809
+ const config = getConfig();
1810
+ return config.providers;
1811
+ });
1812
+ app.get("/api/config", async () => {
1813
+ return getConfig();
1814
+ });
1815
+ app.get("/api/config/info", async () => {
1816
+ const config = getConfig();
1817
+ return {
1818
+ config,
1819
+ path: CONFIG_PATH
1820
+ };
1821
+ });
1822
+ app.put("/api/config", async (request, reply) => {
1823
+ const body = request.body;
1824
+ if (!body || typeof body.port !== "number") {
1825
+ reply.code(400);
1826
+ return { error: "Invalid config payload" };
1827
+ }
1828
+ updateConfig(body);
1829
+ return { success: true };
1830
+ });
1831
+ app.post("/api/providers/:id/test", async (request, reply) => {
1832
+ const id = String(request.params.id);
1833
+ const config = getConfig();
1834
+ const provider = config.providers.find((item) => item.id === id);
1835
+ if (!provider) {
1836
+ reply.code(404);
1837
+ return { error: "Provider not found" };
1838
+ }
1839
+ const startedAt = Date.now();
1840
+ const targetModel = provider.defaultModel || provider.models?.[0]?.id;
1841
+ if (!targetModel) {
1842
+ reply.code(400);
1843
+ return {
1844
+ ok: false,
1845
+ status: 0,
1846
+ statusText: "No model configured for provider"
1847
+ };
1848
+ }
1849
+ const testPayload = normalizeClaudePayload({
1850
+ model: targetModel,
1851
+ stream: false,
1852
+ temperature: 0,
1853
+ messages: [
1854
+ {
1855
+ role: "user",
1856
+ content: [
1857
+ {
1858
+ type: "text",
1859
+ text: "\u4F60\u597D\uFF0C\u8FD9\u662F\u4E00\u6B21\u8FDE\u63A5\u6D4B\u8BD5\u3002\u8BF7\u7B80\u77ED\u56DE\u5E94\u4EE5\u786E\u8BA4\u670D\u52A1\u53EF\u7528\u3002"
1860
+ }
1861
+ ]
1862
+ }
1863
+ ],
1864
+ system: "You are a connection diagnostic assistant."
1865
+ });
1866
+ const providerBody = provider.type === "anthropic" ? buildAnthropicBody(testPayload, {
1867
+ maxTokens: provider.models?.find((m) => m.id === targetModel)?.maxTokens ?? 256,
1868
+ temperature: 0,
1869
+ toolChoice: void 0,
1870
+ overrideTools: void 0
1871
+ }) : buildProviderBody(testPayload, {
1872
+ maxTokens: provider.models?.find((m) => m.id === targetModel)?.maxTokens ?? 256,
1873
+ temperature: 0,
1874
+ toolChoice: void 0,
1875
+ overrideTools: void 0
1876
+ });
1877
+ const connector = getConnector(provider.id);
1878
+ try {
1879
+ const upstream = await connector.send({
1880
+ model: targetModel,
1881
+ body: providerBody,
1882
+ stream: false
1883
+ });
1884
+ const duration = Date.now() - startedAt;
1885
+ if (upstream.status >= 400) {
1886
+ const errorText = upstream.body ? await new Response(upstream.body).text() : "";
1887
+ return {
1888
+ ok: false,
1889
+ status: upstream.status,
1890
+ statusText: errorText || "Upstream error",
1891
+ durationMs: duration
1892
+ };
1893
+ }
1894
+ const raw = upstream.body ? await new Response(upstream.body).text() : "";
1895
+ let parsed = null;
1896
+ try {
1897
+ parsed = raw ? JSON.parse(raw) : null;
1898
+ } catch {
1899
+ return {
1900
+ ok: false,
1901
+ status: upstream.status,
1902
+ statusText: "Invalid JSON response",
1903
+ durationMs: duration
1904
+ };
1905
+ }
1906
+ let sample = "";
1907
+ if (provider.type === "anthropic") {
1908
+ const contentBlocks = Array.isArray(parsed?.content) ? parsed.content : [];
1909
+ const textBlocks = contentBlocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
1910
+ sample = textBlocks.join("\n");
1911
+ } else {
1912
+ const choice = Array.isArray(parsed?.choices) ? parsed.choices[0] : null;
1913
+ if (choice) {
1914
+ if (Array.isArray(choice.message?.content)) {
1915
+ sample = choice.message.content.join("\n");
1916
+ } else {
1917
+ sample = choice.message?.content ?? choice.text ?? "";
1918
+ }
1919
+ }
1920
+ }
1921
+ sample = typeof sample === "string" ? sample.trim() : "";
1922
+ return {
1923
+ ok: Boolean(sample),
1924
+ status: upstream.status,
1925
+ statusText: sample ? "OK" : "Empty response",
1926
+ durationMs: duration,
1927
+ sample: sample ? sample.slice(0, 200) : null
1928
+ };
1929
+ } catch (error) {
1930
+ reply.code(502);
1931
+ return {
1932
+ ok: false,
1933
+ status: 0,
1934
+ statusText: error instanceof Error ? error.message : "Network error",
1935
+ durationMs: Date.now() - startedAt
1936
+ };
1937
+ }
1938
+ });
1939
+ app.get("/api/logs", async (request, reply) => {
1940
+ const query = request.query ?? {};
1941
+ const limitRaw = Number(query.limit ?? 50);
1942
+ const offsetRaw = Number(query.offset ?? 0);
1943
+ const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 200) : 50;
1944
+ const offset = Number.isFinite(offsetRaw) ? Math.max(offsetRaw, 0) : 0;
1945
+ const provider = typeof query.provider === "string" && query.provider.length > 0 ? query.provider : void 0;
1946
+ const model = typeof query.model === "string" && query.model.length > 0 ? query.model : void 0;
1947
+ const statusParam = typeof query.status === "string" ? query.status : void 0;
1948
+ const status = statusParam === "success" || statusParam === "error" ? statusParam : void 0;
1949
+ const parseTime = (value) => {
1950
+ if (!value)
1951
+ return void 0;
1952
+ const numeric = Number(value);
1953
+ if (Number.isFinite(numeric))
1954
+ return numeric;
1955
+ const parsed = Date.parse(value);
1956
+ return Number.isFinite(parsed) ? parsed : void 0;
1957
+ };
1958
+ const from = parseTime(query.from);
1959
+ const to = parseTime(query.to);
1960
+ const { items, total } = queryLogs({ limit, offset, provider, model, status, from, to });
1961
+ reply.header("x-total-count", String(total));
1962
+ return { total, items };
1963
+ });
1964
+ app.get("/api/logs/:id", async (request, reply) => {
1965
+ const id = Number(request.params.id);
1966
+ if (!Number.isFinite(id)) {
1967
+ reply.code(400);
1968
+ return { error: "Invalid id" };
1969
+ }
1970
+ const record = getLogDetail(id);
1971
+ if (!record) {
1972
+ reply.code(404);
1973
+ return { error: "Not found" };
1974
+ }
1975
+ const payload = getLogPayload(id);
1976
+ return { ...record, payload };
1977
+ });
1978
+ app.post("/api/logs/cleanup", async () => {
1979
+ const config = getConfig();
1980
+ const retentionDays = config.logRetentionDays ?? 30;
1981
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
1982
+ const deleted = cleanupLogsBefore(cutoff);
1983
+ return { success: true, deleted };
1984
+ });
1985
+ app.get("/api/db/info", async () => {
1986
+ const db2 = getDb();
1987
+ const pageCount = db2.pragma("page_count", { simple: true });
1988
+ const pageSize = db2.pragma("page_size", { simple: true });
1989
+ return {
1990
+ pageCount,
1991
+ pageSize,
1992
+ sizeBytes: pageCount * pageSize
1993
+ };
1994
+ });
1995
+ app.get("/api/stats/overview", async () => {
1996
+ return getMetricsOverview();
1997
+ });
1998
+ app.get("/api/stats/daily", async (request) => {
1999
+ const query = request.query ?? {};
2000
+ const daysRaw = Number(query.days ?? 7);
2001
+ const days = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 30)) : 7;
2002
+ return getDailyMetrics(days);
2003
+ });
2004
+ app.get("/api/stats/model", async (request) => {
2005
+ const query = request.query ?? {};
2006
+ const daysRaw = Number(query.days ?? 7);
2007
+ const limitRaw = Number(query.limit ?? 10);
2008
+ const days = Number.isFinite(daysRaw) ? Math.max(1, Math.min(daysRaw, 90)) : 7;
2009
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(limitRaw, 50)) : 10;
2010
+ return getModelUsageMetrics(days, limit);
2011
+ });
2012
+ }
2013
+
2014
+ // tasks/maintenance.ts
2015
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2016
+ var timersStarted = false;
2017
+ function startMaintenanceTimers() {
2018
+ if (timersStarted)
2019
+ return;
2020
+ timersStarted = true;
2021
+ scheduleCleanup();
2022
+ }
2023
+ function scheduleCleanup() {
2024
+ const run = () => {
2025
+ try {
2026
+ const retentionDays = getConfig().logRetentionDays ?? 30;
2027
+ const cutoff = Date.now() - retentionDays * DAY_MS;
2028
+ const deleted = cleanupLogsBefore(cutoff);
2029
+ if (deleted > 0) {
2030
+ console.info(`[maintenance] cleaned ${deleted} old log entries`);
2031
+ }
2032
+ } catch (err) {
2033
+ console.error("[maintenance] cleanup failed", err);
2034
+ }
2035
+ };
2036
+ setInterval(run, DAY_MS);
2037
+ }
2038
+
2039
+ // index.ts
2040
+ var DEFAULT_PORT = 3456;
2041
+ var DEFAULT_HOST = "127.0.0.1";
2042
+ var cachedConfig2 = loadConfig();
2043
+ onConfigChange((config) => {
2044
+ cachedConfig2 = config;
2045
+ });
2046
+ function resolveWebDist() {
2047
+ const __filename2 = fileURLToPath(import.meta.url);
2048
+ const __dirname = path3.dirname(__filename2);
2049
+ const candidates = [
2050
+ process2.env.CC_GW_UI_ROOT,
2051
+ path3.resolve(__dirname, "../web/public"),
2052
+ path3.resolve(__dirname, "../web/dist"),
2053
+ path3.resolve(__dirname, "../../web/dist"),
2054
+ path3.resolve(__dirname, "../../../src/web/dist"),
2055
+ path3.resolve(process2.cwd(), "src/web/dist")
2056
+ ].filter((item) => Boolean(item));
2057
+ for (const candidate of candidates) {
2058
+ if (fs3.existsSync(candidate)) {
2059
+ return candidate;
2060
+ }
2061
+ }
2062
+ return null;
2063
+ }
2064
+ async function createServer() {
2065
+ const app = Fastify({ logger: true });
2066
+ await app.register(fastifyCors, {
2067
+ origin: true,
2068
+ credentials: true
2069
+ });
2070
+ const webRoot = resolveWebDist();
2071
+ if (webRoot) {
2072
+ await app.register(fastifyStatic, {
2073
+ root: webRoot,
2074
+ prefix: "/ui/"
2075
+ });
2076
+ app.get("/", async (_, reply) => reply.redirect("/ui/"));
2077
+ app.get("/ui", async (_, reply) => reply.redirect("/ui/"));
2078
+ const assetHandler = async (request, reply) => {
2079
+ const params = request.params;
2080
+ const target = params["*"] ?? "";
2081
+ if (target.includes("..")) {
2082
+ reply.code(400);
2083
+ return { error: "Invalid asset path" };
2084
+ }
2085
+ return reply.sendFile(path3.join("assets", target));
2086
+ };
2087
+ app.get("/assets/*", assetHandler);
2088
+ app.head("/assets/*", assetHandler);
2089
+ const faviconHandler = async (_, reply) => reply.sendFile("favicon.ico");
2090
+ app.get("/favicon.ico", faviconHandler);
2091
+ app.head("/favicon.ico", faviconHandler);
2092
+ app.setNotFoundHandler((request, reply) => {
2093
+ const url = request.raw.url ?? "";
2094
+ if (url.startsWith("/ui/")) {
2095
+ reply.type("text/html");
2096
+ return reply.sendFile("index.html");
2097
+ }
2098
+ reply.code(404).send({ error: "Not Found" });
2099
+ });
2100
+ } else {
2101
+ app.log.warn("\u672A\u627E\u5230 Web UI \u6784\u5EFA\u4EA7\u7269\uFF0C/ui \u76EE\u5F55\u5C06\u4E0D\u53EF\u7528\u3002");
2102
+ }
2103
+ await registerMessagesRoute(app);
2104
+ await registerAdminRoutes(app);
2105
+ startMaintenanceTimers();
2106
+ app.get("/health", async () => {
2107
+ return {
2108
+ status: "ok",
2109
+ timestamp: Date.now(),
2110
+ providerCount: getConfig().providers.length
2111
+ };
2112
+ });
2113
+ return app;
2114
+ }
2115
+ async function startServer(options = {}) {
2116
+ const app = await createServer();
2117
+ const envPort = process2.env.PORT ? Number.parseInt(process2.env.PORT, 10) : void 0;
2118
+ const envHost = process2.env.HOST;
2119
+ const configPort = cachedConfig2?.port;
2120
+ const configHost = cachedConfig2?.host;
2121
+ const port = options.port ?? envPort ?? configPort ?? DEFAULT_PORT;
2122
+ const host = options.host ?? envHost ?? configHost ?? DEFAULT_HOST;
2123
+ await app.listen({ port, host });
2124
+ return app;
2125
+ }
2126
+ async function main() {
2127
+ try {
2128
+ const app = await startServer();
2129
+ const shutdown = async () => {
2130
+ try {
2131
+ await app.close();
2132
+ process2.exit(0);
2133
+ } catch (err) {
2134
+ app.log.error({ err }, "\u5173\u95ED\u670D\u52A1\u5931\u8D25");
2135
+ process2.exit(1);
2136
+ }
2137
+ };
2138
+ process2.on("SIGTERM", shutdown);
2139
+ process2.on("SIGINT", shutdown);
2140
+ } catch (err) {
2141
+ console.error("\u542F\u52A8\u670D\u52A1\u5931\u8D25", err);
2142
+ process2.exit(1);
2143
+ }
2144
+ }
2145
+ var __filename = fileURLToPath(import.meta.url);
2146
+ if (process2.argv[1] && path3.resolve(process2.argv[1]) === __filename) {
2147
+ main();
2148
+ }
2149
+ export {
2150
+ createServer,
2151
+ startServer
2152
+ };