llmapi-v2 2.1.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/.env.example +40 -0
- package/Dockerfile +17 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/converter/request.d.ts +6 -0
- package/dist/converter/request.js +184 -0
- package/dist/converter/request.js.map +1 -0
- package/dist/converter/response.d.ts +6 -0
- package/dist/converter/response.js +76 -0
- package/dist/converter/response.js.map +1 -0
- package/dist/converter/stream.d.ts +54 -0
- package/dist/converter/stream.js +318 -0
- package/dist/converter/stream.js.map +1 -0
- package/dist/converter/types.d.ts +239 -0
- package/dist/converter/types.js +6 -0
- package/dist/converter/types.js.map +1 -0
- package/dist/data/posts.d.ts +19 -0
- package/dist/data/posts.js +462 -0
- package/dist/data/posts.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +6 -0
- package/dist/middleware/api-key-auth.js +76 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/quota-guard.d.ts +10 -0
- package/dist/middleware/quota-guard.js +27 -0
- package/dist/middleware/quota-guard.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +50 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +6 -0
- package/dist/middleware/request-logger.js +37 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/session-auth.d.ts +19 -0
- package/dist/middleware/session-auth.js +99 -0
- package/dist/middleware/session-auth.js.map +1 -0
- package/dist/providers/aliyun.d.ts +13 -0
- package/dist/providers/aliyun.js +20 -0
- package/dist/providers/aliyun.js.map +1 -0
- package/dist/providers/base-provider.d.ts +36 -0
- package/dist/providers/base-provider.js +133 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/deepseek.d.ts +11 -0
- package/dist/providers/deepseek.js +18 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +98 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/admin.d.ts +1 -0
- package/dist/routes/admin.js +153 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +318 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blog.d.ts +1 -0
- package/dist/routes/blog.js +29 -0
- package/dist/routes/blog.js.map +1 -0
- package/dist/routes/dashboard.d.ts +1 -0
- package/dist/routes/dashboard.js +184 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +309 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/models.d.ts +1 -0
- package/dist/routes/models.js +39 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/payment.d.ts +1 -0
- package/dist/routes/payment.js +150 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/sitemap.d.ts +1 -0
- package/dist/routes/sitemap.js +38 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/services/alipay.d.ts +27 -0
- package/dist/services/alipay.js +106 -0
- package/dist/services/alipay.js.map +1 -0
- package/dist/services/database.d.ts +4 -0
- package/dist/services/database.js +170 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/health-checker.d.ts +13 -0
- package/dist/services/health-checker.js +95 -0
- package/dist/services/health-checker.js.map +1 -0
- package/dist/services/mailer.d.ts +3 -0
- package/dist/services/mailer.js +91 -0
- package/dist/services/mailer.js.map +1 -0
- package/dist/services/metrics.d.ts +56 -0
- package/dist/services/metrics.js +94 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/remote-control.d.ts +20 -0
- package/dist/services/remote-control.js +209 -0
- package/dist/services/remote-control.js.map +1 -0
- package/dist/services/remote-ws.d.ts +5 -0
- package/dist/services/remote-ws.js +143 -0
- package/dist/services/remote-ws.js.map +1 -0
- package/dist/services/usage.d.ts +13 -0
- package/dist/services/usage.js +39 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +39 -0
- package/public/robots.txt +8 -0
- package/src/config.ts +140 -0
- package/src/converter/request.ts +207 -0
- package/src/converter/response.ts +85 -0
- package/src/converter/stream.ts +373 -0
- package/src/converter/types.ts +257 -0
- package/src/data/posts.ts +474 -0
- package/src/index.ts +219 -0
- package/src/middleware/api-key-auth.ts +82 -0
- package/src/middleware/quota-guard.ts +28 -0
- package/src/middleware/rate-limiter.ts +61 -0
- package/src/middleware/request-logger.ts +36 -0
- package/src/middleware/session-auth.ts +91 -0
- package/src/providers/aliyun.ts +16 -0
- package/src/providers/base-provider.ts +148 -0
- package/src/providers/deepseek.ts +14 -0
- package/src/providers/registry.ts +111 -0
- package/src/providers/types.ts +26 -0
- package/src/routes/admin.ts +169 -0
- package/src/routes/auth.ts +369 -0
- package/src/routes/blog.ts +28 -0
- package/src/routes/dashboard.ts +208 -0
- package/src/routes/messages.ts +346 -0
- package/src/routes/models.ts +37 -0
- package/src/routes/payment.ts +189 -0
- package/src/routes/sitemap.ts +40 -0
- package/src/services/alipay.ts +116 -0
- package/src/services/database.ts +187 -0
- package/src/services/health-checker.ts +115 -0
- package/src/services/mailer.ts +90 -0
- package/src/services/metrics.ts +104 -0
- package/src/services/remote-control.ts +226 -0
- package/src/services/remote-ws.ts +145 -0
- package/src/services/usage.ts +57 -0
- package/src/types/express.d.ts +46 -0
- package/src/utils/errors.ts +44 -0
- package/src/utils/logger.ts +8 -0
- package/tsconfig.json +17 -0
- package/views/pages/404.ejs +14 -0
- package/views/pages/admin.ejs +307 -0
- package/views/pages/blog-post.ejs +378 -0
- package/views/pages/blog.ejs +148 -0
- package/views/pages/dashboard.ejs +441 -0
- package/views/pages/docs.ejs +807 -0
- package/views/pages/index.ejs +416 -0
- package/views/pages/login.ejs +170 -0
- package/views/pages/orders.ejs +111 -0
- package/views/pages/pricing.ejs +379 -0
- package/views/pages/register.ejs +397 -0
- package/views/pages/remote.ejs +334 -0
- package/views/pages/settings.ejs +373 -0
- package/views/partials/header.ejs +70 -0
- package/views/partials/nav.ejs +140 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AliyunProvider = void 0;
|
|
4
|
+
const base_provider_1 = require("./base-provider");
|
|
5
|
+
/**
|
|
6
|
+
* Alibaba Cloud Bailian (DashScope) provider.
|
|
7
|
+
* Native Anthropic endpoint: https://dashscope-intl.aliyuncs.com/apps/anthropic
|
|
8
|
+
*
|
|
9
|
+
* Supports Qwen series models with full Anthropic API compatibility:
|
|
10
|
+
* - Streaming SSE
|
|
11
|
+
* - Tool calling (96.5% accuracy)
|
|
12
|
+
* - Context caching (automatic, reduces cost for repeated prefixes)
|
|
13
|
+
*/
|
|
14
|
+
class AliyunProvider extends base_provider_1.BaseProvider {
|
|
15
|
+
constructor(anthropicBaseUrl, apiKey, timeout, openaiBaseUrl) {
|
|
16
|
+
super('aliyun', anthropicBaseUrl, apiKey, timeout, openaiBaseUrl);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.AliyunProvider = AliyunProvider;
|
|
20
|
+
//# sourceMappingURL=aliyun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aliyun.js","sourceRoot":"","sources":["../../src/providers/aliyun.ts"],"names":[],"mappings":";;;AAAA,mDAA+C;AAE/C;;;;;;;;GAQG;AACH,MAAa,cAAe,SAAQ,4BAAY;IAC9C,YAAY,gBAAwB,EAAE,MAAc,EAAE,OAAe,EAAE,aAAsB;QAC3F,KAAK,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;IACpE,CAAC;CACF;AAJD,wCAIC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
2
|
+
import type { LLMProvider } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Transparent Anthropic proxy provider.
|
|
5
|
+
*
|
|
6
|
+
* Forwards requests directly to the provider's native Anthropic-compatible
|
|
7
|
+
* endpoint WITHOUT format conversion. The provider handles all Anthropic
|
|
8
|
+
* protocol details (streaming SSE, tool calling, etc.).
|
|
9
|
+
*
|
|
10
|
+
* This is the highest-quality approach: we rely on the provider's own
|
|
11
|
+
* Anthropic compatibility layer rather than building our own.
|
|
12
|
+
*/
|
|
13
|
+
export declare class BaseProvider implements LLMProvider {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly anthropicBaseUrl: string;
|
|
16
|
+
readonly openaiBaseUrl: string | undefined;
|
|
17
|
+
private readonly apiKey;
|
|
18
|
+
private readonly timeout;
|
|
19
|
+
private healthy;
|
|
20
|
+
private consecutiveFailures;
|
|
21
|
+
private lastFailureTime;
|
|
22
|
+
private static readonly FAILURE_THRESHOLD;
|
|
23
|
+
private static readonly RECOVERY_TIME_MS;
|
|
24
|
+
constructor(name: string, anthropicBaseUrl: string, apiKey: string, timeout: number, openaiBaseUrl?: string);
|
|
25
|
+
isHealthy(): boolean;
|
|
26
|
+
markUnhealthy(reason: string): void;
|
|
27
|
+
markHealthy(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Transparent proxy: forward an Anthropic-format request body directly
|
|
30
|
+
* to the provider's native Anthropic endpoint.
|
|
31
|
+
*
|
|
32
|
+
* Returns the raw IncomingMessage so the caller can pipe it directly
|
|
33
|
+
* back to the client (for streaming) or buffer it (for non-streaming).
|
|
34
|
+
*/
|
|
35
|
+
proxy(path: string, body: Buffer | string, headers: Record<string, string>, isStream: boolean): Promise<IncomingMessage>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BaseProvider = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
9
|
+
const url_1 = require("url");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
/**
|
|
12
|
+
* Transparent Anthropic proxy provider.
|
|
13
|
+
*
|
|
14
|
+
* Forwards requests directly to the provider's native Anthropic-compatible
|
|
15
|
+
* endpoint WITHOUT format conversion. The provider handles all Anthropic
|
|
16
|
+
* protocol details (streaming SSE, tool calling, etc.).
|
|
17
|
+
*
|
|
18
|
+
* This is the highest-quality approach: we rely on the provider's own
|
|
19
|
+
* Anthropic compatibility layer rather than building our own.
|
|
20
|
+
*/
|
|
21
|
+
class BaseProvider {
|
|
22
|
+
name;
|
|
23
|
+
anthropicBaseUrl;
|
|
24
|
+
openaiBaseUrl;
|
|
25
|
+
apiKey;
|
|
26
|
+
timeout;
|
|
27
|
+
// Circuit breaker
|
|
28
|
+
healthy = true;
|
|
29
|
+
consecutiveFailures = 0;
|
|
30
|
+
lastFailureTime = 0;
|
|
31
|
+
static FAILURE_THRESHOLD = 3;
|
|
32
|
+
static RECOVERY_TIME_MS = 30_000;
|
|
33
|
+
constructor(name, anthropicBaseUrl, apiKey, timeout, openaiBaseUrl) {
|
|
34
|
+
this.name = name;
|
|
35
|
+
this.anthropicBaseUrl = anthropicBaseUrl;
|
|
36
|
+
this.openaiBaseUrl = openaiBaseUrl;
|
|
37
|
+
this.apiKey = apiKey;
|
|
38
|
+
this.timeout = timeout;
|
|
39
|
+
}
|
|
40
|
+
isHealthy() {
|
|
41
|
+
if (this.healthy)
|
|
42
|
+
return true;
|
|
43
|
+
if (Date.now() - this.lastFailureTime > BaseProvider.RECOVERY_TIME_MS) {
|
|
44
|
+
this.healthy = true;
|
|
45
|
+
this.consecutiveFailures = 0;
|
|
46
|
+
logger_1.logger.info({ provider: this.name }, 'Provider auto-recovered');
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
markUnhealthy(reason) {
|
|
52
|
+
this.consecutiveFailures++;
|
|
53
|
+
this.lastFailureTime = Date.now();
|
|
54
|
+
if (this.consecutiveFailures >= BaseProvider.FAILURE_THRESHOLD) {
|
|
55
|
+
this.healthy = false;
|
|
56
|
+
logger_1.logger.warn({ provider: this.name, reason, failures: this.consecutiveFailures }, 'Provider marked unhealthy');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
markHealthy() {
|
|
60
|
+
if (!this.healthy) {
|
|
61
|
+
logger_1.logger.info({ provider: this.name }, 'Provider recovered');
|
|
62
|
+
}
|
|
63
|
+
this.healthy = true;
|
|
64
|
+
this.consecutiveFailures = 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Transparent proxy: forward an Anthropic-format request body directly
|
|
68
|
+
* to the provider's native Anthropic endpoint.
|
|
69
|
+
*
|
|
70
|
+
* Returns the raw IncomingMessage so the caller can pipe it directly
|
|
71
|
+
* back to the client (for streaming) or buffer it (for non-streaming).
|
|
72
|
+
*/
|
|
73
|
+
proxy(path, body, headers, isStream) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const parsed = new url_1.URL(this.anthropicBaseUrl);
|
|
76
|
+
const isHttps = parsed.protocol === 'https:';
|
|
77
|
+
const transport = isHttps ? https_1.default : http_1.default;
|
|
78
|
+
const payload = typeof body === 'string' ? Buffer.from(body) : body;
|
|
79
|
+
const fullPath = parsed.pathname.replace(/\/+$/, '') + path;
|
|
80
|
+
// Forward most headers, but override auth
|
|
81
|
+
const proxyHeaders = {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'Content-Length': payload.length,
|
|
84
|
+
'x-api-key': this.apiKey,
|
|
85
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
86
|
+
};
|
|
87
|
+
// Pass through anthropic-specific headers
|
|
88
|
+
if (headers['anthropic-version'])
|
|
89
|
+
proxyHeaders['anthropic-version'] = headers['anthropic-version'];
|
|
90
|
+
if (headers['anthropic-beta'])
|
|
91
|
+
proxyHeaders['anthropic-beta'] = headers['anthropic-beta'];
|
|
92
|
+
const options = {
|
|
93
|
+
hostname: parsed.hostname,
|
|
94
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
95
|
+
path: fullPath,
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: proxyHeaders,
|
|
98
|
+
timeout: this.timeout,
|
|
99
|
+
};
|
|
100
|
+
const req = transport.request(options, (res) => {
|
|
101
|
+
if (isStream || (res.statusCode && res.statusCode < 400)) {
|
|
102
|
+
resolve(res);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Non-stream error: buffer the error response to log it
|
|
106
|
+
const chunks = [];
|
|
107
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
108
|
+
res.on('end', () => {
|
|
109
|
+
const errBody = Buffer.concat(chunks).toString();
|
|
110
|
+
let message = `Provider ${this.name} returned ${res.statusCode}`;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(errBody);
|
|
113
|
+
message = parsed.error?.message || message;
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
const err = new Error(message);
|
|
117
|
+
err.statusCode = res.statusCode || 500;
|
|
118
|
+
err.rawBody = errBody;
|
|
119
|
+
reject(err);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
req.on('timeout', () => {
|
|
123
|
+
req.destroy();
|
|
124
|
+
reject(new Error(`Request to ${this.name} timed out after ${this.timeout}ms`));
|
|
125
|
+
});
|
|
126
|
+
req.on('error', reject);
|
|
127
|
+
req.write(payload);
|
|
128
|
+
req.end();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.BaseProvider = BaseProvider;
|
|
133
|
+
//# sourceMappingURL=base-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-provider.js","sourceRoot":"","sources":["../../src/providers/base-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseProvider } from './base-provider';
|
|
2
|
+
/**
|
|
3
|
+
* DeepSeek provider.
|
|
4
|
+
* Native Anthropic endpoint: https://api.deepseek.com/anthropic
|
|
5
|
+
*
|
|
6
|
+
* Best for: simple conversations, light tasks, cost-sensitive routing.
|
|
7
|
+
* Note: tool calling accuracy (~81.5%) is lower than Qwen.
|
|
8
|
+
*/
|
|
9
|
+
export declare class DeepSeekProvider extends BaseProvider {
|
|
10
|
+
constructor(anthropicBaseUrl: string, apiKey: string, timeout: number, openaiBaseUrl?: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeepSeekProvider = void 0;
|
|
4
|
+
const base_provider_1 = require("./base-provider");
|
|
5
|
+
/**
|
|
6
|
+
* DeepSeek provider.
|
|
7
|
+
* Native Anthropic endpoint: https://api.deepseek.com/anthropic
|
|
8
|
+
*
|
|
9
|
+
* Best for: simple conversations, light tasks, cost-sensitive routing.
|
|
10
|
+
* Note: tool calling accuracy (~81.5%) is lower than Qwen.
|
|
11
|
+
*/
|
|
12
|
+
class DeepSeekProvider extends base_provider_1.BaseProvider {
|
|
13
|
+
constructor(anthropicBaseUrl, apiKey, timeout, openaiBaseUrl) {
|
|
14
|
+
super('deepseek', anthropicBaseUrl, apiKey, timeout, openaiBaseUrl);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.DeepSeekProvider = DeepSeekProvider;
|
|
18
|
+
//# sourceMappingURL=deepseek.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../../src/providers/deepseek.ts"],"names":[],"mappings":";;;AAAA,mDAA+C;AAE/C;;;;;;GAMG;AACH,MAAa,gBAAiB,SAAQ,4BAAY;IAChD,YAAY,gBAAwB,EAAE,MAAc,EAAE,OAAe,EAAE,aAAsB;QAC3F,KAAK,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;IACtE,CAAC;CACF;AAJD,4CAIC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AppConfig } from '../config';
|
|
2
|
+
import type { LLMProvider, ResolvedRoute } from './types';
|
|
3
|
+
export declare function initRegistry(config: AppConfig): void;
|
|
4
|
+
/**
|
|
5
|
+
* Smart routing: decide which provider/model based on request characteristics.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - If request has tools -> coding task -> use primary coding model (Qwen)
|
|
9
|
+
* - If request is simple (no tools, few messages) -> use light model (DeepSeek)
|
|
10
|
+
* - Model name mapping: claude-opus -> best available, claude-haiku -> cheapest
|
|
11
|
+
* - Fallback: try providers in priority order
|
|
12
|
+
*/
|
|
13
|
+
export declare function smartResolve(claudeModel: string, hasTools: boolean, messageCount: number): ResolvedRoute | null;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve with failover: yield providers to try in priority order.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveWithFailover(claudeModel: string): AsyncGenerator<ResolvedRoute>;
|
|
18
|
+
export declare function getProviders(): Map<string, LLMProvider>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initRegistry = initRegistry;
|
|
4
|
+
exports.smartResolve = smartResolve;
|
|
5
|
+
exports.resolveWithFailover = resolveWithFailover;
|
|
6
|
+
exports.getProviders = getProviders;
|
|
7
|
+
const aliyun_1 = require("./aliyun");
|
|
8
|
+
const deepseek_1 = require("./deepseek");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const providers = new Map();
|
|
11
|
+
let modelRouting = {};
|
|
12
|
+
let smartRouting;
|
|
13
|
+
function initRegistry(config) {
|
|
14
|
+
providers.clear();
|
|
15
|
+
for (const pc of config.providers) {
|
|
16
|
+
const provider = createProvider(pc);
|
|
17
|
+
if (provider) {
|
|
18
|
+
providers.set(pc.name, provider);
|
|
19
|
+
logger_1.logger.info({ provider: pc.name, endpoint: pc.anthropicBaseUrl }, 'Provider registered (native Anthropic endpoint)');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
modelRouting = config.modelRouting;
|
|
23
|
+
smartRouting = config.smartRouting;
|
|
24
|
+
if (providers.size === 0) {
|
|
25
|
+
logger_1.logger.warn('No providers configured! API requests will fail.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createProvider(pc) {
|
|
29
|
+
if (!pc.enabled || !pc.apiKey)
|
|
30
|
+
return null;
|
|
31
|
+
switch (pc.name) {
|
|
32
|
+
case 'aliyun':
|
|
33
|
+
return new aliyun_1.AliyunProvider(pc.anthropicBaseUrl, pc.apiKey, pc.timeout, pc.openaiBaseUrl);
|
|
34
|
+
case 'deepseek':
|
|
35
|
+
return new deepseek_1.DeepSeekProvider(pc.anthropicBaseUrl, pc.apiKey, pc.timeout, pc.openaiBaseUrl);
|
|
36
|
+
default:
|
|
37
|
+
logger_1.logger.warn({ provider: pc.name }, 'Unknown provider type');
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Smart routing: decide which provider/model based on request characteristics.
|
|
43
|
+
*
|
|
44
|
+
* Rules:
|
|
45
|
+
* - If request has tools -> coding task -> use primary coding model (Qwen)
|
|
46
|
+
* - If request is simple (no tools, few messages) -> use light model (DeepSeek)
|
|
47
|
+
* - Model name mapping: claude-opus -> best available, claude-haiku -> cheapest
|
|
48
|
+
* - Fallback: try providers in priority order
|
|
49
|
+
*/
|
|
50
|
+
function smartResolve(claudeModel, hasTools, messageCount) {
|
|
51
|
+
// Claude-haiku always goes to the cheap model
|
|
52
|
+
if (claudeModel.includes('haiku')) {
|
|
53
|
+
const p = providers.get(smartRouting.lightModel.provider);
|
|
54
|
+
if (p?.isHealthy()) {
|
|
55
|
+
return { provider: p, backendModel: smartRouting.lightModel.model };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// No tools + short conversation = light task
|
|
59
|
+
if (!hasTools && messageCount <= smartRouting.lightTaskMaxMessages) {
|
|
60
|
+
const p = providers.get(smartRouting.lightModel.provider);
|
|
61
|
+
if (p?.isHealthy()) {
|
|
62
|
+
return { provider: p, backendModel: smartRouting.lightModel.model };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Tools present or complex conversation = coding task
|
|
66
|
+
if (hasTools) {
|
|
67
|
+
const p = providers.get(smartRouting.codingModel.provider);
|
|
68
|
+
if (p?.isHealthy()) {
|
|
69
|
+
return { provider: p, backendModel: smartRouting.codingModel.model };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Fallback to model-based routing
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve with failover: yield providers to try in priority order.
|
|
77
|
+
*/
|
|
78
|
+
async function* resolveWithFailover(claudeModel) {
|
|
79
|
+
const routes = modelRouting[claudeModel];
|
|
80
|
+
if (!routes || routes.length === 0) {
|
|
81
|
+
logger_1.logger.error({ model: claudeModel }, 'No routes configured for model');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
for (const route of routes) {
|
|
85
|
+
const provider = providers.get(route.provider);
|
|
86
|
+
if (!provider)
|
|
87
|
+
continue;
|
|
88
|
+
if (!provider.isHealthy()) {
|
|
89
|
+
logger_1.logger.debug({ provider: route.provider }, 'Skipping unhealthy provider');
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
yield { provider, backendModel: route.model };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function getProviders() {
|
|
96
|
+
return providers;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
2
|
+
export interface LLMProvider {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly anthropicBaseUrl: string;
|
|
5
|
+
isHealthy(): boolean;
|
|
6
|
+
markUnhealthy(reason: string): void;
|
|
7
|
+
markHealthy(): void;
|
|
8
|
+
/**
|
|
9
|
+
* Transparent proxy: forward raw Anthropic request to provider's native endpoint.
|
|
10
|
+
* Returns the raw HTTP response for direct piping back to the client.
|
|
11
|
+
*/
|
|
12
|
+
proxy(path: string, body: Buffer | string, headers: Record<string, string>, isStream: boolean): Promise<IncomingMessage>;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedRoute {
|
|
15
|
+
provider: LLMProvider;
|
|
16
|
+
backendModel: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const adminRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adminRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const database_1 = require("../services/database");
|
|
6
|
+
const session_auth_1 = require("../middleware/session-auth");
|
|
7
|
+
const health_checker_1 = require("../services/health-checker");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
exports.adminRouter = (0, express_1.Router)();
|
|
10
|
+
// All routes require admin auth
|
|
11
|
+
exports.adminRouter.use(session_auth_1.adminAuth);
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/admin/stats
|
|
14
|
+
* Platform-wide statistics.
|
|
15
|
+
*/
|
|
16
|
+
exports.adminRouter.get('/stats', async (_req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const pool = (0, database_1.getPool)();
|
|
19
|
+
const [users] = await pool.execute(`
|
|
20
|
+
SELECT
|
|
21
|
+
COUNT(*) as total,
|
|
22
|
+
SUM(CASE WHEN DATE(created_at) = CURRENT_DATE THEN 1 ELSE 0 END) as today_new
|
|
23
|
+
FROM users
|
|
24
|
+
`);
|
|
25
|
+
const [activeUsers] = await pool.execute(`
|
|
26
|
+
SELECT COUNT(DISTINCT user_id) as cnt FROM usage_logs
|
|
27
|
+
WHERE created_at >= NOW() - INTERVAL '7 days'
|
|
28
|
+
`);
|
|
29
|
+
const [requests] = await pool.execute(`
|
|
30
|
+
SELECT
|
|
31
|
+
COUNT(*) as total,
|
|
32
|
+
SUM(CASE WHEN DATE(created_at) = CURRENT_DATE THEN 1 ELSE 0 END) as today
|
|
33
|
+
FROM usage_logs
|
|
34
|
+
`);
|
|
35
|
+
const [tokens] = await pool.execute(`
|
|
36
|
+
SELECT
|
|
37
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
|
38
|
+
COALESCE(SUM(provider_cost), 0) as total_cost
|
|
39
|
+
FROM usage_logs
|
|
40
|
+
`);
|
|
41
|
+
const [monthCost] = await pool.execute(`
|
|
42
|
+
SELECT COALESCE(SUM(provider_cost), 0) as cost
|
|
43
|
+
FROM usage_logs WHERE created_at >= date_trunc('month', NOW())
|
|
44
|
+
`);
|
|
45
|
+
const [revenue] = await pool.execute(`
|
|
46
|
+
SELECT COALESCE(SUM(amount), 0) as total FROM orders WHERE status = 'paid'
|
|
47
|
+
`);
|
|
48
|
+
const [monthRevenue] = await pool.execute(`
|
|
49
|
+
SELECT COALESCE(SUM(amount), 0) as total FROM orders
|
|
50
|
+
WHERE status = 'paid' AND paid_at >= date_trunc('month', NOW())
|
|
51
|
+
`);
|
|
52
|
+
// Provider health
|
|
53
|
+
const providerHealth = (0, health_checker_1.getHealthStatus)();
|
|
54
|
+
res.json({
|
|
55
|
+
success: true,
|
|
56
|
+
stats: {
|
|
57
|
+
users: users[0],
|
|
58
|
+
activeUsers7d: activeUsers[0].cnt,
|
|
59
|
+
requests: requests[0],
|
|
60
|
+
tokens: tokens[0],
|
|
61
|
+
monthCost: parseFloat(monthCost[0].cost),
|
|
62
|
+
revenue: parseFloat(revenue[0].total),
|
|
63
|
+
monthRevenue: parseFloat(monthRevenue[0].total),
|
|
64
|
+
providerHealth,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
logger_1.logger.error({ err }, 'admin stats error');
|
|
70
|
+
res.status(500).json({ success: false, error: 'Internal error' });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* GET /api/admin/users
|
|
75
|
+
* Full user list with plan and usage info.
|
|
76
|
+
*/
|
|
77
|
+
exports.adminRouter.get('/users', async (_req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const pool = (0, database_1.getPool)();
|
|
80
|
+
const [rows] = await pool.execute(`
|
|
81
|
+
SELECT
|
|
82
|
+
u.id, u.email, u.name, u.role, u.status, u.created_at,
|
|
83
|
+
p.name as plan_name, p.display_name as plan_display,
|
|
84
|
+
COALESCE(s.tokens_used, 0) as tokens_used,
|
|
85
|
+
p.token_limit_monthly,
|
|
86
|
+
(SELECT COUNT(*) FROM usage_logs WHERE user_id = u.id) as total_requests,
|
|
87
|
+
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id AND status = 'active') as api_key_count
|
|
88
|
+
FROM users u
|
|
89
|
+
LEFT JOIN subscriptions s ON s.user_id = u.id
|
|
90
|
+
LEFT JOIN plans p ON s.plan_id = p.id
|
|
91
|
+
ORDER BY u.created_at DESC
|
|
92
|
+
`);
|
|
93
|
+
res.json({ success: true, users: rows });
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger_1.logger.error({ err }, 'admin users error');
|
|
97
|
+
res.status(500).json({ success: false, error: 'Internal error' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* PUT /api/admin/users/:id
|
|
102
|
+
* Update user status or plan.
|
|
103
|
+
*/
|
|
104
|
+
exports.adminRouter.put('/users/:id', async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const pool = (0, database_1.getPool)();
|
|
107
|
+
const userId = req.params.id;
|
|
108
|
+
const { status, plan } = req.body;
|
|
109
|
+
if (status) {
|
|
110
|
+
await pool.execute('UPDATE users SET status = ? WHERE id = ?', [status, userId]);
|
|
111
|
+
}
|
|
112
|
+
if (plan) {
|
|
113
|
+
const [plans] = await pool.execute('SELECT id FROM plans WHERE name = ?', [plan]);
|
|
114
|
+
const planId = plans[0]?.id;
|
|
115
|
+
if (planId) {
|
|
116
|
+
await pool.execute('UPDATE subscriptions SET plan_id = ?, tokens_used = 0 WHERE id = (SELECT id FROM subscriptions WHERE user_id = ? ORDER BY period_start DESC LIMIT 1)', [planId, userId]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
res.json({ success: true });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
logger_1.logger.error({ err }, 'admin update user error');
|
|
123
|
+
res.status(500).json({ success: false, error: 'Internal error' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* GET /api/admin/cost-report
|
|
128
|
+
* Provider cost breakdown.
|
|
129
|
+
*/
|
|
130
|
+
exports.adminRouter.get('/cost-report', async (_req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const pool = (0, database_1.getPool)();
|
|
133
|
+
const [daily] = await pool.execute(`
|
|
134
|
+
SELECT
|
|
135
|
+
DATE(created_at) as date,
|
|
136
|
+
provider_name,
|
|
137
|
+
COUNT(*) as requests,
|
|
138
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
139
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
140
|
+
COALESCE(SUM(provider_cost), 0) as cost
|
|
141
|
+
FROM usage_logs
|
|
142
|
+
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
|
|
143
|
+
GROUP BY DATE(created_at), provider_name
|
|
144
|
+
ORDER BY date DESC, provider_name
|
|
145
|
+
`);
|
|
146
|
+
res.json({ success: true, daily });
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
logger_1.logger.error({ err }, 'cost report error');
|
|
150
|
+
res.status(500).json({ success: false, error: 'Internal error' });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":";;;AAAA,qCAAiC;AAEjC,mDAA+C;AAC/C,6DAAuD;AACvD,+DAA6D;AAC7D,4CAAyC;AAE5B,QAAA,WAAW,GAAG,IAAA,gBAAM,GAAE,CAAC;AAEpC,gCAAgC;AAChC,mBAAW,CAAC,GAAG,CAAC,wBAAS,CAAC,CAAC;AAE3B;;;GAGG;AACH,mBAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAA,kBAAO,GAAE,CAAC;QAEvB,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;;;KAKlC,CAAC,CAAC;QAEH,MAAM,CAAC,WAAW,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;KAGxC,CAAC,CAAC;QAEH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;;;KAKrC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;;;KAKnC,CAAC,CAAC;QAEH,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;KAGtC,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;KAEpC,CAAC,CAAC;QAEH,MAAM,CAAC,YAAY,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;KAGzC,CAAC,CAAC;QAEH,kBAAkB;QAClB,MAAM,cAAc,GAAG,IAAA,gCAAe,GAAE,CAAC;QAEzC,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,KAAK,EAAE;gBACL,KAAK,EAAG,KAAe,CAAC,CAAC,CAAC;gBAC1B,aAAa,EAAG,WAAqB,CAAC,CAAC,CAAC,CAAC,GAAG;gBAC5C,QAAQ,EAAG,QAAkB,CAAC,CAAC,CAAC;gBAChC,MAAM,EAAG,MAAgB,CAAC,CAAC,CAAC;gBAC5B,SAAS,EAAE,UAAU,CAAE,SAAmB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACnD,OAAO,EAAE,UAAU,CAAE,OAAiB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;gBAChD,YAAY,EAAE,UAAU,CAAE,YAAsB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;gBAC1D,cAAc;aACf;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH;;;GAGG;AACH,mBAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAA,kBAAO,GAAE,CAAC;QACvB,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;;;;;;;;;;KAYjC,CAAC,CAAC;QAEH,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH;;;GAGG;AACH,mBAAW,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IAClE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAA,kBAAO,GAAE,CAAC;QACvB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QAElC,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,IAAI,CAAC,OAAO,CAAC,0CAA0C,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACnF,CAAC;QAED,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,qCAAqC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAClF,MAAM,MAAM,GAAI,KAAe,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACvC,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,IAAI,CAAC,OAAO,CAChB,sJAAsJ,EACtJ,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB,CAAC;YACJ,CAAC;QACH,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,yBAAyB,CAAC,CAAC;QACjD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH;;;GAGG;AACH,mBAAW,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,EAAE;IACrE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAA,kBAAO,GAAE,CAAC;QACvB,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;;;;;;;;;;;;KAYlC,CAAC,CAAC;QAEH,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,eAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;QAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpE,CAAC;AACH,CAAC,CAAC,CAAC"}
|