@cartanova/qgrid-cli 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js +9 -0
- package/bundle/dist/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/decorateMetadata.js +6 -0
- package/bundle/dist/dist/_virtual/_rolldown/runtime.js +15 -0
- package/bundle/dist/dist/application/qgrid/oauth.js +140 -0
- package/bundle/dist/dist/application/qgrid/pool.js +100 -0
- package/bundle/dist/dist/application/qgrid/qgrid.frame.js +280 -0
- package/bundle/dist/dist/application/qgrid/qgrid.types.js +79 -0
- package/bundle/dist/dist/application/qgrid/worker.js +183 -0
- package/bundle/dist/dist/application/queries.generated.js +31 -0
- package/bundle/dist/dist/application/request-log/request-log.model.js +113 -0
- package/bundle/dist/dist/application/request-log/request-log.types.js +13 -0
- package/bundle/dist/dist/application/sonamu.generated.js +99 -0
- package/bundle/dist/dist/application/sonamu.generated.sso.js +34 -0
- package/bundle/dist/dist/application/token/token.model.js +157 -0
- package/bundle/dist/dist/application/token/token.types.js +22 -0
- package/bundle/dist/dist/i18n/en.js +63 -0
- package/bundle/dist/dist/i18n/ko.js +62 -0
- package/bundle/dist/dist/i18n/sd.generated.js +272 -0
- package/bundle/dist/dist/index.js +11 -0
- package/bundle/dist/dist/migrations/20260410171405_create__request_logs.js +26 -0
- package/bundle/dist/dist/migrations/20260410171406_create__tokens.js +24 -0
- package/bundle/dist/dist/migrations/20260411120624_alter_tokens_alter2.js +16 -0
- package/bundle/dist/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +155 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_expect@4.1.2/node_modules/@vitest/expect/dist/index.js +1484 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_pretty-format@4.1.2/node_modules/@vitest/pretty-format/dist/index.js +887 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_runner@4.1.2/node_modules/@vitest/runner/dist/chunk-artifact.js +2746 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_runner@4.1.2/node_modules/@vitest/runner/dist/index.js +8 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_runner@4.1.2/node_modules/@vitest/runner/dist/utils.js +8 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_snapshot@4.1.2/node_modules/@vitest/snapshot/dist/index.js +662 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_spy@4.1.2/node_modules/@vitest/spy/dist/index.js +386 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +82 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/constants.js +48 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/diff.js +1307 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/display.js +558 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/error.js +33 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/helpers.js +226 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/offset.js +27 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/serialize.js +77 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/source-map.js +374 -0
- package/bundle/dist/dist/node_modules/.pnpm/@vitest_utils@4.1.2/node_modules/@vitest/utils/dist/timers.js +37 -0
- package/bundle/dist/dist/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.js +2886 -0
- package/bundle/dist/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.js +10 -0
- package/bundle/dist/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.js +109 -0
- package/bundle/dist/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.js +10 -0
- package/bundle/dist/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.js +10 -0
- package/bundle/dist/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.js +10 -0
- package/bundle/dist/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +1127 -0
- package/bundle/dist/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/index.js +21 -0
- package/bundle/dist/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.js +432 -0
- package/bundle/dist/dist/node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.js +506 -0
- package/bundle/dist/dist/node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.js +86 -0
- package/bundle/dist/dist/node_modules/.pnpm/vite@8.0.3_@emnapi_core@1.9.2_@emnapi_runtime@1.9.2_@types_node@25.0.7_esbuild@0.27.4_jiti@2.6.1_sass@1.98.0_tsx@4.21.0/node_modules/vite/dist/node/module-runner.js +426 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js +6 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/benchmark.CX_oY03V.js +41 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/evaluatedModules.Dg1zASAC.js +19 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/index.Chj8NDwU.js +202 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/index.DOa3dzoN.js +16 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js +59 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/test.p_J6dB8a.js +2910 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js +44 -0
- package/bundle/dist/dist/node_modules/.pnpm/vitest@4.1.2_@opentelemetry_api@1.9.0_@types_node@25.0.7_vite@8.0.3_@emnapi_core@1.9.2__bb31087278f97a39b31b92010beab980/node_modules/vitest/dist/index.js +28 -0
- package/bundle/dist/dist/sonamu.config.js +196 -0
- package/bundle/dist/dist/testing/fixture.js +8 -0
- package/bundle/dist/dist/testing/global.js +9 -0
- package/bundle/dist/dist/testing/setup-mocks.js +17 -0
- package/bundle/dist/dist/utils/subset-loaders.js +12 -0
- package/bundle/src/application/qgrid/oauth.ts +181 -0
- package/bundle/src/application/qgrid/pool.ts +132 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +275 -0
- package/bundle/src/application/qgrid/qgrid.types.ts +117 -0
- package/bundle/src/application/qgrid/worker.ts +234 -0
- package/bundle/src/application/queries.generated.ts +59 -0
- package/bundle/src/application/request-log/request-log.model.ts +155 -0
- package/bundle/src/application/request-log/request-log.types.ts +15 -0
- package/bundle/src/application/sonamu.generated.http +187 -0
- package/bundle/src/application/sonamu.generated.sso.ts +66 -0
- package/bundle/src/application/sonamu.generated.ts +135 -0
- package/bundle/src/application/token/token.model.ts +160 -0
- package/bundle/src/application/token/token.types.ts +26 -0
- package/bundle/src/i18n/en.ts +59 -0
- package/bundle/src/i18n/ko.ts +59 -0
- package/bundle/src/i18n/sd.generated.ts +384 -0
- package/bundle/src/index.ts +6 -0
- package/bundle/src/sonamu.config.ts +248 -0
- package/bundle/src/src/application/qgrid/oauth.ts +181 -0
- package/bundle/src/src/application/qgrid/pool.ts +132 -0
- package/bundle/src/src/application/qgrid/qgrid.frame.ts +275 -0
- package/bundle/src/src/application/qgrid/qgrid.types.ts +117 -0
- package/bundle/src/src/application/qgrid/worker.ts +234 -0
- package/bundle/src/src/application/queries.generated.ts +59 -0
- package/bundle/src/src/application/request-log/request-log.entity.json +75 -0
- package/bundle/src/src/application/request-log/request-log.model.ts +155 -0
- package/bundle/src/src/application/request-log/request-log.types.ts +15 -0
- package/bundle/src/src/application/sonamu.generated.http +187 -0
- package/bundle/src/src/application/sonamu.generated.sso.ts +66 -0
- package/bundle/src/src/application/sonamu.generated.ts +135 -0
- package/bundle/src/src/application/token/token.entity.json +37 -0
- package/bundle/src/src/application/token/token.model.ts +160 -0
- package/bundle/src/src/application/token/token.types.ts +26 -0
- package/bundle/src/src/i18n/en.ts +59 -0
- package/bundle/src/src/i18n/ko.ts +59 -0
- package/bundle/src/src/i18n/sd.generated.ts +384 -0
- package/bundle/src/src/index.ts +6 -0
- package/bundle/src/src/migrations/20260410171405_create__request_logs.ts +23 -0
- package/bundle/src/src/migrations/20260410171406_create__tokens.ts +21 -0
- package/bundle/src/src/migrations/20260411120624_alter_tokens_alter2.ts +15 -0
- package/bundle/src/src/sonamu.config.ts +248 -0
- package/bundle/src/src/testing/fixture.ts +6 -0
- package/bundle/src/src/testing/global.ts +6 -0
- package/bundle/src/src/testing/setup-mocks.ts +44 -0
- package/bundle/src/src/typings/fastify.d.ts +8 -0
- package/bundle/src/src/typings/sonamu.d.ts +17 -0
- package/bundle/src/src/utils/subset-loaders.ts +11 -0
- package/bundle/src/testing/fixture.ts +6 -0
- package/bundle/src/testing/global.ts +6 -0
- package/bundle/src/testing/setup-mocks.ts +44 -0
- package/bundle/src/typings/fastify.d.ts +8 -0
- package/bundle/src/typings/sonamu.d.ts +17 -0
- package/bundle/src/utils/subset-loaders.ts +11 -0
- package/bundle/web-dist/web-dist/client/assets/index-DlPchHBY.js +116 -0
- package/bundle/web-dist/web-dist/client/assets/index-vZsvyuFn.css +1 -0
- package/bundle/web-dist/web-dist/client/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/bundle/web-dist/web-dist/client/assets/routes-CagDz23B.js +1 -0
- package/bundle/web-dist/web-dist/client/assets/sd.generated-BrQoQXvv.js +47 -0
- package/bundle/web-dist/web-dist/client/assets/services.generated-BspO2L0j.js +1 -0
- package/bundle/web-dist/web-dist/client/assets/show-aXUGa0gm.js +29 -0
- package/bundle/web-dist/web-dist/client/assets/tokens-DRkXtZ3Q.js +1 -0
- package/bundle/web-dist/web-dist/client/assets/vendor-react-DH4kUfK2.js +9 -0
- package/bundle/web-dist/web-dist/client/assets/vendor-tanstack-CUxlo6YN.js +1 -0
- package/bundle/web-dist/web-dist/client/index.html +18 -0
- package/bundle/web-dist/web-dist/server/assets/react-dom-CGIzOSuj.js +335 -0
- package/bundle/web-dist/web-dist/server/assets/routes-ClzReRep.js +490 -0
- package/bundle/web-dist/web-dist/server/assets/sd.generated-BnonqqKr.js +21694 -0
- package/bundle/web-dist/web-dist/server/assets/services.generated-D5ABD5fr.js +836 -0
- package/bundle/web-dist/web-dist/server/assets/show-C9pcdSxc.js +23364 -0
- package/bundle/web-dist/web-dist/server/assets/tokens-JH6FsnWZ.js +860 -0
- package/bundle/web-dist/web-dist/server/entry-server.generated.js +44646 -0
- package/package.json +11 -10
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudePool — 멀티 토큰 프로세스 풀.
|
|
3
|
+
*
|
|
4
|
+
* 2-layer 아키텍처: ClaudePool → Worker (flat)
|
|
5
|
+
* - Map<token, Worker[]>로 토큰별 워커 관리 (원본 토큰이 키)
|
|
6
|
+
* - least-queue-depth 라우팅
|
|
7
|
+
* - 투명한 쿼터 failover (QuotaError 시 다른 토큰으로 자동 재시도)
|
|
8
|
+
* - 워커 관리만 담당 (DB 의존 없음)
|
|
9
|
+
*/
|
|
10
|
+
import type { CliResult, PoolConfig, QueryInput, TokenStats } from "./qgrid.types";
|
|
11
|
+
import { QuotaError } from "./qgrid.types";
|
|
12
|
+
import { Worker, type WorkerConfig } from "./worker";
|
|
13
|
+
|
|
14
|
+
export class ClaudePool {
|
|
15
|
+
workers = new Map<string, Worker[]>();
|
|
16
|
+
quotaExhausted = new Set<string>();
|
|
17
|
+
requestCounts = new Map<string, number>();
|
|
18
|
+
lastUsedToken = "";
|
|
19
|
+
size: number;
|
|
20
|
+
model: string;
|
|
21
|
+
timeout: number;
|
|
22
|
+
cwd: string;
|
|
23
|
+
maxCalls: number;
|
|
24
|
+
|
|
25
|
+
constructor(config: PoolConfig) {
|
|
26
|
+
this.size = config.size ?? 3;
|
|
27
|
+
this.model = config.model ?? "sonnet";
|
|
28
|
+
this.timeout = config.timeout ?? 300_000;
|
|
29
|
+
this.cwd = config.cwd ?? "/tmp/qgrid";
|
|
30
|
+
this.maxCalls = config.maxCalls ?? 500;
|
|
31
|
+
|
|
32
|
+
config.tokens.forEach((token) => {
|
|
33
|
+
this.createWorkers(token);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getStats(tokenNames?: Map<string, string>): TokenStats[] {
|
|
38
|
+
return [...this.workers.keys()].map((token) => ({
|
|
39
|
+
token,
|
|
40
|
+
name: tokenNames?.get(token) ?? "Unknown Key",
|
|
41
|
+
requests: this.requestCounts.get(token) ?? 0,
|
|
42
|
+
active: !this.quotaExhausted.has(token),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
selectWorker(): Worker | null {
|
|
47
|
+
const candidates = [...this.workers.entries()]
|
|
48
|
+
.filter(([token]) => !this.quotaExhausted.has(token))
|
|
49
|
+
.flatMap(([, workers]) => workers);
|
|
50
|
+
|
|
51
|
+
if (candidates.length === 0) return null;
|
|
52
|
+
|
|
53
|
+
return candidates.reduce((best, w) => (w.getQueueDepth() < best.getQueueDepth() ? w : best));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async query(input: QueryInput, timeoutMs?: number): Promise<CliResult> {
|
|
57
|
+
const triedTokens = new Set<string>();
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
const worker = this.selectWorker();
|
|
61
|
+
if (!worker) {
|
|
62
|
+
throw new QuotaError("All tokens exhausted");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (triedTokens.has(worker.tokenId)) {
|
|
66
|
+
throw new QuotaError("All tokens exhausted");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await worker.query(input, timeoutMs);
|
|
71
|
+
this.lastUsedToken = worker.tokenId;
|
|
72
|
+
this.requestCounts.set(worker.tokenId, (this.requestCounts.get(worker.tokenId) ?? 0) + 1);
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof QuotaError) {
|
|
77
|
+
this.quotaExhausted.add(worker.tokenId);
|
|
78
|
+
triedTokens.add(worker.tokenId);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
kill(): void {
|
|
87
|
+
[...this.workers.values()].flat().forEach((w) => {
|
|
88
|
+
w.kill();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
createWorkers(token: string): void {
|
|
93
|
+
if (this.workers.has(token)) return;
|
|
94
|
+
|
|
95
|
+
const workerConfig: WorkerConfig = {
|
|
96
|
+
token,
|
|
97
|
+
model: this.model,
|
|
98
|
+
timeout: this.timeout,
|
|
99
|
+
cwd: this.cwd,
|
|
100
|
+
maxCalls: this.maxCalls,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const workers = Array.from({ length: this.size }, () => new Worker(workerConfig));
|
|
104
|
+
this.workers.set(token, workers);
|
|
105
|
+
this.requestCounts.set(token, 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
destroyWorkers(token: string): void {
|
|
109
|
+
const workers = this.workers.get(token);
|
|
110
|
+
if (!workers) return;
|
|
111
|
+
|
|
112
|
+
workers.forEach((w) => {
|
|
113
|
+
w.kill();
|
|
114
|
+
});
|
|
115
|
+
this.workers.delete(token);
|
|
116
|
+
this.quotaExhausted.delete(token);
|
|
117
|
+
this.requestCounts.delete(token);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Pool 싱글턴 관리
|
|
122
|
+
let pool: ClaudePool | null = null;
|
|
123
|
+
|
|
124
|
+
export function initPool(tokens: string[]): ClaudePool {
|
|
125
|
+
pool = new ClaudePool({ tokens });
|
|
126
|
+
return pool;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getPool(): ClaudePool {
|
|
130
|
+
if (!pool) throw new Error("Pool not initialized. Call initPool() first.");
|
|
131
|
+
return pool;
|
|
132
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { FastifyReply } from "fastify";
|
|
2
|
+
import { api, BaseFrameClass } from "sonamu";
|
|
3
|
+
|
|
4
|
+
import { RequestLogModel } from "../request-log/request-log.model";
|
|
5
|
+
import { TokenModel } from "../token/token.model";
|
|
6
|
+
import type { RefreshTokenParams } from "../token/token.types";
|
|
7
|
+
import {
|
|
8
|
+
buildAuthUrl,
|
|
9
|
+
exchangeCodeForTokens,
|
|
10
|
+
fetchUsage,
|
|
11
|
+
generatePKCE,
|
|
12
|
+
refreshAccessToken,
|
|
13
|
+
} from "./oauth";
|
|
14
|
+
import { type ClaudePool, getPool } from "./pool";
|
|
15
|
+
import type {
|
|
16
|
+
CliResult,
|
|
17
|
+
HealthResponse,
|
|
18
|
+
OAuthStartResult,
|
|
19
|
+
TokenStats,
|
|
20
|
+
UsageResponse,
|
|
21
|
+
} from "./qgrid.types";
|
|
22
|
+
|
|
23
|
+
// PKCE 세션 메모리 저장 (state → { codeVerifier, name, redirectUri })
|
|
24
|
+
const pendingOAuth = new Map<string, { codeVerifier: string; name: string; redirectUri: string }>();
|
|
25
|
+
|
|
26
|
+
class QgridFrameClass extends BaseFrameClass {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("Qgrid");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
32
|
+
async query(prompt: string, system?: string, timeout?: number): Promise<CliResult> {
|
|
33
|
+
const pool = getPool();
|
|
34
|
+
const result = await pool.query({ system, prompt }, timeout);
|
|
35
|
+
|
|
36
|
+
// 로그 기록 실패해도 쿼리 결과는 반환
|
|
37
|
+
TokenModel.findByToken("A", pool.lastUsedToken)
|
|
38
|
+
.then((tokenEntry) => {
|
|
39
|
+
RequestLogModel.save([
|
|
40
|
+
{
|
|
41
|
+
token_name: tokenEntry?.name ?? "Unknown Key",
|
|
42
|
+
query: system ? `[System]\n${system}\n\n[User]\n${prompt}` : prompt,
|
|
43
|
+
response: result.text,
|
|
44
|
+
input_tokens: result.usage.input_tokens,
|
|
45
|
+
output_tokens: result.usage.output_tokens,
|
|
46
|
+
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
47
|
+
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
48
|
+
duration_ms: result.durationMs,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
})
|
|
52
|
+
.catch((e) => console.error("requestLog save failed:", e));
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
58
|
+
async stats(): Promise<TokenStats[]> {
|
|
59
|
+
const pool = getPool();
|
|
60
|
+
const entries = await TokenModel.findActive("A");
|
|
61
|
+
const tokenNames = new Map(entries.map((e) => [e.token, e.name]));
|
|
62
|
+
return pool.getStats(tokenNames);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
66
|
+
async addToken(token: string, name: string, refreshToken?: string): Promise<{ added: boolean }> {
|
|
67
|
+
await TokenModel.save([
|
|
68
|
+
{
|
|
69
|
+
token,
|
|
70
|
+
name,
|
|
71
|
+
...(refreshToken && refreshToken.length > 0 ? { refresh_token: refreshToken } : {}),
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
getPool().createWorkers(token);
|
|
75
|
+
return { added: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
79
|
+
async updateToken(
|
|
80
|
+
token: string,
|
|
81
|
+
name?: string,
|
|
82
|
+
newToken?: string,
|
|
83
|
+
refreshToken?: string,
|
|
84
|
+
): Promise<{ updated: boolean }> {
|
|
85
|
+
const pool = getPool();
|
|
86
|
+
const entry = await TokenModel.findByToken("A", token);
|
|
87
|
+
if (!entry) return { updated: false };
|
|
88
|
+
|
|
89
|
+
const hasNewToken = newToken !== undefined && newToken.length > 0;
|
|
90
|
+
const hasRefreshToken = refreshToken !== undefined && refreshToken.length > 0;
|
|
91
|
+
await TokenModel.save([
|
|
92
|
+
{
|
|
93
|
+
id: entry.id,
|
|
94
|
+
token: hasNewToken ? newToken : entry.token,
|
|
95
|
+
name: name !== undefined ? name : entry.name,
|
|
96
|
+
...(hasRefreshToken ? { refresh_token: refreshToken } : {}),
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
if (hasNewToken) {
|
|
101
|
+
pool.destroyWorkers(token);
|
|
102
|
+
pool.createWorkers(newToken);
|
|
103
|
+
}
|
|
104
|
+
return { updated: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
108
|
+
async removeToken(token: string): Promise<{ removed: boolean }> {
|
|
109
|
+
const pool = getPool();
|
|
110
|
+
if (!pool.workers.has(token)) return { removed: false };
|
|
111
|
+
|
|
112
|
+
const entry = await TokenModel.findByToken("A", token);
|
|
113
|
+
if (entry) await TokenModel.del([entry.id]);
|
|
114
|
+
pool.destroyWorkers(token);
|
|
115
|
+
return { removed: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
*
|
|
120
|
+
* @param id token_id
|
|
121
|
+
* 토큰 활성화/비활성화 토글 (pool 워커 생성/제거 & DB의 active 필드 업데이트)
|
|
122
|
+
*
|
|
123
|
+
* @returns 토큰 활성화 여부
|
|
124
|
+
*/
|
|
125
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
126
|
+
async toggleToken(id: number): Promise<{ active: boolean }> {
|
|
127
|
+
const pool = getPool();
|
|
128
|
+
const entry = await TokenModel.findOne("A", { id });
|
|
129
|
+
if (!entry) return { active: false };
|
|
130
|
+
|
|
131
|
+
const newActive = !entry.active;
|
|
132
|
+
await TokenModel.save([{ id, token: entry.token, active: newActive, name: entry.name }]);
|
|
133
|
+
|
|
134
|
+
// 토큰의 새로운 상태가 활성화면/비활성화면
|
|
135
|
+
if (newActive) {
|
|
136
|
+
// 워커 생성
|
|
137
|
+
pool.createWorkers(entry.token);
|
|
138
|
+
} else {
|
|
139
|
+
// 워커 제거
|
|
140
|
+
pool.destroyWorkers(entry.token);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { active: newActive };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// OAuth 로그인: authUrl 생성 (프론트에서 이 API 호출 후 authUrl로 리다이렉트)
|
|
147
|
+
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
148
|
+
async oauthStart(name: string): Promise<OAuthStartResult> {
|
|
149
|
+
const { codeVerifier, codeChallenge, state } = generatePKCE();
|
|
150
|
+
|
|
151
|
+
const serverPort = process.env.PORT ?? "44900";
|
|
152
|
+
const redirectUri = `http://localhost:${serverPort}/callback`;
|
|
153
|
+
const authUrl = buildAuthUrl(codeChallenge, state, redirectUri);
|
|
154
|
+
|
|
155
|
+
// PKCE를 메모리에 저장 (5분 TTL)
|
|
156
|
+
pendingOAuth.set(state, { codeVerifier, name, redirectUri });
|
|
157
|
+
setTimeout(() => pendingOAuth.delete(state), 300_000);
|
|
158
|
+
|
|
159
|
+
return { authUrl };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// OAuth 콜백 처리 — sonamu.config.ts의 custom 라우트에서 호출됨
|
|
163
|
+
async handleOAuthCallback(code: string, state: string, reply: FastifyReply): Promise<void> {
|
|
164
|
+
const pool = getPool();
|
|
165
|
+
const pending = pendingOAuth.get(state);
|
|
166
|
+
if (!pending) {
|
|
167
|
+
return reply.redirect("/?oauth=error&reason=invalid_state");
|
|
168
|
+
}
|
|
169
|
+
pendingOAuth.delete(state);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const tokens = await exchangeCodeForTokens(
|
|
173
|
+
code,
|
|
174
|
+
pending.codeVerifier,
|
|
175
|
+
state,
|
|
176
|
+
pending.redirectUri,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// 같은 계정의 이전 토큰이 있으면 교체
|
|
180
|
+
if (tokens.accountUuid) {
|
|
181
|
+
const oldEntries = await TokenModel.findByAccountUuid("A", tokens.accountUuid);
|
|
182
|
+
for (const old of oldEntries) {
|
|
183
|
+
pool.destroyWorkers(old.token);
|
|
184
|
+
await TokenModel.del([old.id]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 새 토큰 저장 + pool에 등록
|
|
189
|
+
await TokenModel.save([
|
|
190
|
+
{
|
|
191
|
+
token: tokens.accessToken,
|
|
192
|
+
name: pending.name,
|
|
193
|
+
refresh_token: tokens.refreshToken,
|
|
194
|
+
expires_at: tokens.expiresAt ? BigInt(tokens.expiresAt) : null,
|
|
195
|
+
account_uuid: tokens.accountUuid,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
pool.createWorkers(tokens.accessToken);
|
|
199
|
+
|
|
200
|
+
return reply.redirect(`/?oauth=success&name=${encodeURIComponent(pending.name)}`);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return reply.redirect(`/?oauth=error&reason=${encodeURIComponent((e as Error).message)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 토큰 사용량 조회 (OAuth usage API)
|
|
207
|
+
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
208
|
+
async usage(tokenName?: string): Promise<UsageResponse> {
|
|
209
|
+
const pool = getPool();
|
|
210
|
+
const { rows: allTokens } = await TokenModel.findMany("A");
|
|
211
|
+
const entry = tokenName
|
|
212
|
+
? allTokens.find((e) => e.name === tokenName)
|
|
213
|
+
: allTokens.findLast((e) => e.active);
|
|
214
|
+
|
|
215
|
+
if (!entry) return { error: "NOT_FOUND" };
|
|
216
|
+
|
|
217
|
+
// 만료 확인 + refresh 시도
|
|
218
|
+
let accessToken = entry.token;
|
|
219
|
+
const isExpired = entry.expires_at && Number(entry.expires_at) < Date.now();
|
|
220
|
+
|
|
221
|
+
if (isExpired && entry.refresh_token) {
|
|
222
|
+
try {
|
|
223
|
+
accessToken = await this.refreshToken(pool, entry);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.warn(`[usage] refresh failed for ${entry.name}: ${(e as Error).message}`);
|
|
226
|
+
return { error: "re-login required" };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// usage 호출
|
|
231
|
+
const result = await fetchUsage(accessToken);
|
|
232
|
+
|
|
233
|
+
// 인증 에러면 refresh_token으로 재시도
|
|
234
|
+
if (result.error && entry.refresh_token) {
|
|
235
|
+
try {
|
|
236
|
+
accessToken = await this.refreshToken(pool, entry);
|
|
237
|
+
return await fetchUsage(accessToken);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.warn(`[usage] refresh failed for ${entry.name}: ${(e as Error).message}`);
|
|
240
|
+
return { error: "re-login required" };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async refreshToken(pool: ClaudePool, token: RefreshTokenParams): Promise<string> {
|
|
248
|
+
if (!token.refresh_token) throw new Error("No refresh token");
|
|
249
|
+
const refreshed = await refreshAccessToken(token.refresh_token);
|
|
250
|
+
await TokenModel.save([
|
|
251
|
+
{
|
|
252
|
+
id: token.id,
|
|
253
|
+
token: refreshed.accessToken,
|
|
254
|
+
refresh_token: refreshed.refreshToken,
|
|
255
|
+
expires_at: BigInt(refreshed.expiresAt),
|
|
256
|
+
name: token.name ?? "",
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
pool.destroyWorkers(token.token);
|
|
260
|
+
pool.createWorkers(refreshed.accessToken);
|
|
261
|
+
return refreshed.accessToken;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
265
|
+
async health(): Promise<HealthResponse> {
|
|
266
|
+
const pool = getPool();
|
|
267
|
+
return {
|
|
268
|
+
status: "ok",
|
|
269
|
+
workers: [...pool.workers.values()].flat().length,
|
|
270
|
+
activeTokens: pool.workers.size - pool.quotaExhausted.size,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const QgridFrame = new QgridFrameClass();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ─── Query ───
|
|
4
|
+
|
|
5
|
+
export const QueryInput = z.object({
|
|
6
|
+
system: z.string().optional(),
|
|
7
|
+
prompt: z.string(),
|
|
8
|
+
timeout: z.number().optional(),
|
|
9
|
+
});
|
|
10
|
+
export type QueryInput = z.infer<typeof QueryInput>;
|
|
11
|
+
|
|
12
|
+
export const CliResult = z.object({
|
|
13
|
+
text: z.string(),
|
|
14
|
+
usage: z.object({
|
|
15
|
+
input_tokens: z.number(),
|
|
16
|
+
output_tokens: z.number(),
|
|
17
|
+
cache_creation_input_tokens: z.number(),
|
|
18
|
+
cache_read_input_tokens: z.number(),
|
|
19
|
+
}),
|
|
20
|
+
durationMs: z.number(),
|
|
21
|
+
costUsd: z.number(),
|
|
22
|
+
});
|
|
23
|
+
export type CliResult = z.infer<typeof CliResult>;
|
|
24
|
+
|
|
25
|
+
// ─── Pool Config ───
|
|
26
|
+
|
|
27
|
+
export const PoolConfig = z.object({
|
|
28
|
+
tokens: z.array(z.string()),
|
|
29
|
+
size: z.number().optional(),
|
|
30
|
+
model: z.string().optional(),
|
|
31
|
+
timeout: z.number().optional(),
|
|
32
|
+
cwd: z.string().optional(),
|
|
33
|
+
maxCalls: z.number().optional(),
|
|
34
|
+
});
|
|
35
|
+
export type PoolConfig = z.infer<typeof PoolConfig>;
|
|
36
|
+
|
|
37
|
+
// ─── Token Management ───
|
|
38
|
+
|
|
39
|
+
export const AddTokenInput = z.object({
|
|
40
|
+
token: z.string(),
|
|
41
|
+
name: z.string(),
|
|
42
|
+
});
|
|
43
|
+
export type AddTokenInput = z.infer<typeof AddTokenInput>;
|
|
44
|
+
|
|
45
|
+
export const RemoveTokenInput = z.object({
|
|
46
|
+
token: z.string(),
|
|
47
|
+
});
|
|
48
|
+
export type RemoveTokenInput = z.infer<typeof RemoveTokenInput>;
|
|
49
|
+
|
|
50
|
+
export const TokenStats = z.object({
|
|
51
|
+
token: z.string(),
|
|
52
|
+
name: z.string(),
|
|
53
|
+
requests: z.number(),
|
|
54
|
+
active: z.boolean(),
|
|
55
|
+
});
|
|
56
|
+
export type TokenStats = z.infer<typeof TokenStats>;
|
|
57
|
+
|
|
58
|
+
// ─── OAuth ───
|
|
59
|
+
|
|
60
|
+
export const OAuthStartResult = z.object({
|
|
61
|
+
authUrl: z.string(),
|
|
62
|
+
});
|
|
63
|
+
export type OAuthStartResult = z.infer<typeof OAuthStartResult>;
|
|
64
|
+
|
|
65
|
+
const RateLimit = z
|
|
66
|
+
.object({
|
|
67
|
+
utilization: z.number().nullable(),
|
|
68
|
+
resets_at: z.string().nullable(),
|
|
69
|
+
})
|
|
70
|
+
.nullable();
|
|
71
|
+
|
|
72
|
+
export const UsageResponse = z.object({
|
|
73
|
+
error: z.string().optional(),
|
|
74
|
+
five_hour: RateLimit.optional(),
|
|
75
|
+
seven_day: RateLimit.optional(),
|
|
76
|
+
seven_day_opus: RateLimit.optional(),
|
|
77
|
+
seven_day_sonnet: RateLimit.optional(),
|
|
78
|
+
seven_day_oauth_apps: RateLimit.optional(),
|
|
79
|
+
seven_day_cowork: RateLimit.optional(),
|
|
80
|
+
extra_usage: z
|
|
81
|
+
.object({
|
|
82
|
+
is_enabled: z.boolean(),
|
|
83
|
+
monthly_limit: z.number().nullable(),
|
|
84
|
+
used_credits: z.number().nullable(),
|
|
85
|
+
utilization: z.number().nullable(),
|
|
86
|
+
})
|
|
87
|
+
.nullable()
|
|
88
|
+
.optional(),
|
|
89
|
+
});
|
|
90
|
+
export type UsageResponse = z.infer<typeof UsageResponse>;
|
|
91
|
+
|
|
92
|
+
// ─── Health ───
|
|
93
|
+
|
|
94
|
+
export const HealthResponse = z.object({
|
|
95
|
+
status: z.string(),
|
|
96
|
+
workers: z.number(),
|
|
97
|
+
activeTokens: z.number(),
|
|
98
|
+
});
|
|
99
|
+
export type HealthResponse = z.infer<typeof HealthResponse>;
|
|
100
|
+
|
|
101
|
+
// ─── Errors ───
|
|
102
|
+
|
|
103
|
+
export class QuotaError extends Error {
|
|
104
|
+
readonly code = "QUOTA_EXHAUSTED" as const;
|
|
105
|
+
}
|
|
106
|
+
export class TimeoutError extends Error {
|
|
107
|
+
readonly code = "TIMEOUT" as const;
|
|
108
|
+
}
|
|
109
|
+
export class ProcessError extends Error {
|
|
110
|
+
readonly code = "PROCESS_ERROR" as const;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Utils ───
|
|
114
|
+
|
|
115
|
+
export function maskToken(token: string): string {
|
|
116
|
+
return token.length > 4 ? `...${token.slice(-4)}` : token;
|
|
117
|
+
}
|