@triflux/core 10.0.0-alpha.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.
- package/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Gemini 쿼터 API / 세션 토큰 / RPM 트래커
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
GEMINI_OAUTH_PATH, GEMINI_QUOTA_CACHE_PATH, GEMINI_PROJECT_CACHE_PATH,
|
|
11
|
+
GEMINI_SESSION_CACHE_PATH, GEMINI_RPM_TRACKER_PATH,
|
|
12
|
+
LEGACY_GEMINI_QUOTA_CACHE, LEGACY_GEMINI_PROJECT_CACHE,
|
|
13
|
+
LEGACY_GEMINI_SESSION_CACHE, LEGACY_GEMINI_RPM_TRACKER,
|
|
14
|
+
GEMINI_RPM_WINDOW_MS, GEMINI_QUOTA_STALE_MS, GEMINI_SESSION_STALE_MS,
|
|
15
|
+
GEMINI_API_TIMEOUT_MS,
|
|
16
|
+
GEMINI_REFRESH_FLAG, GEMINI_SESSION_REFRESH_FLAG,
|
|
17
|
+
} from "../constants.mjs";
|
|
18
|
+
import {
|
|
19
|
+
readJson, writeJsonSafe, readJsonMigrate, makeHash, clampPercent,
|
|
20
|
+
decodeJwtEmail, createHttpsPost,
|
|
21
|
+
} from "../utils.mjs";
|
|
22
|
+
|
|
23
|
+
const httpsPost = createHttpsPost(https, GEMINI_API_TIMEOUT_MS);
|
|
24
|
+
|
|
25
|
+
// Gemini 모델별 RPM 한도 (실측 기반: Pro 25, Flash 300)
|
|
26
|
+
export function getGeminiRpmLimit(model) {
|
|
27
|
+
if (model && model.includes("pro")) return 25;
|
|
28
|
+
return 300; // Flash 기본
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Gemini 모델 ID → HUD 표시 라벨 (동적 매핑)
|
|
32
|
+
export function getGeminiModelLabel(model) {
|
|
33
|
+
if (!model) return "";
|
|
34
|
+
// 버전 + 티어 추출: gemini-3.1-pro-preview → [3.1Pro], gemini-2.5-flash → [2.5Flash]
|
|
35
|
+
const m = model.match(/gemini-(\d+(?:\.\d+)?)-(\w+)/);
|
|
36
|
+
if (!m) return "";
|
|
37
|
+
const ver = m[1];
|
|
38
|
+
const tier = m[2].charAt(0).toUpperCase() + m[2].slice(1);
|
|
39
|
+
return `[${ver}${tier}]`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// remainingFraction → 사용 퍼센트 변환 (remainingAmount가 있으면 절대값도 제공)
|
|
43
|
+
export function deriveGeminiLimits(bucket) {
|
|
44
|
+
if (!bucket || bucket.remainingFraction == null) return null;
|
|
45
|
+
const fraction = bucket.remainingFraction;
|
|
46
|
+
const usedPct = clampPercent(Math.round((1 - fraction) * 100));
|
|
47
|
+
// remainingAmount가 API에서 오면 절대값 역산 (Gemini CLI 방식)
|
|
48
|
+
if (bucket.remainingAmount != null) {
|
|
49
|
+
const remaining = parseInt(bucket.remainingAmount, 10);
|
|
50
|
+
const limit = fraction > 0 ? Math.round(remaining / fraction) : 0;
|
|
51
|
+
return { usedPct, remaining, limit, resetTime: bucket.resetTime, modelId: bucket.modelId };
|
|
52
|
+
}
|
|
53
|
+
return { usedPct, remaining: null, limit: null, resetTime: bucket.resetTime, modelId: bucket.modelId };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGeminiEmail() {
|
|
57
|
+
try {
|
|
58
|
+
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
59
|
+
return decodeJwtEmail(oauth?.id_token);
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildGeminiAuthContext(accountId) {
|
|
64
|
+
const oauth = readJson(GEMINI_OAUTH_PATH, null);
|
|
65
|
+
const tokenSource = oauth?.refresh_token || oauth?.id_token || oauth?.access_token || "";
|
|
66
|
+
const tokenFingerprint = tokenSource ? makeHash(tokenSource) : "none";
|
|
67
|
+
const cacheKey = `${accountId || "gemini-main"}::${tokenFingerprint}`;
|
|
68
|
+
return { oauth, tokenFingerprint, cacheKey };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Gemini 쿼터 API 호출 (5분 캐시)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
export async function fetchGeminiQuota(accountId, options = {}) {
|
|
75
|
+
const authContext = options.authContext || buildGeminiAuthContext(accountId);
|
|
76
|
+
const { oauth, tokenFingerprint, cacheKey } = authContext;
|
|
77
|
+
const forceRefresh = options.forceRefresh === true;
|
|
78
|
+
|
|
79
|
+
// 1. 캐시 확인 (계정/토큰별)
|
|
80
|
+
const cache = readJsonMigrate(GEMINI_QUOTA_CACHE_PATH, LEGACY_GEMINI_QUOTA_CACHE, null);
|
|
81
|
+
if (!forceRefresh
|
|
82
|
+
&& cache?.cacheKey === cacheKey
|
|
83
|
+
&& cache?.timestamp
|
|
84
|
+
&& (Date.now() - cache.timestamp < GEMINI_QUOTA_STALE_MS)) {
|
|
85
|
+
return cache;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!oauth?.access_token) {
|
|
89
|
+
// access_token 없음: 에러 힌트를 캐시에 기록하고 stale 캐시 반환
|
|
90
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
91
|
+
...(cache || {}),
|
|
92
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
93
|
+
error: true,
|
|
94
|
+
errorType: "auth",
|
|
95
|
+
errorHint: "no access_token in oauth_creds.json",
|
|
96
|
+
});
|
|
97
|
+
return cache;
|
|
98
|
+
}
|
|
99
|
+
if (oauth.expiry_date && oauth.expiry_date < Date.now()) {
|
|
100
|
+
// OAuth 토큰 만료: 에러 힌트를 캐시에 기록 (refresh_token 갱신은 Gemini CLI 담당)
|
|
101
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
102
|
+
...(cache || {}),
|
|
103
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
104
|
+
error: true,
|
|
105
|
+
errorType: "auth",
|
|
106
|
+
errorHint: `token expired at ${new Date(oauth.expiry_date).toISOString()}`,
|
|
107
|
+
});
|
|
108
|
+
return cache;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. projectId (캐시 or API)
|
|
112
|
+
const fetchProjectId = async () => {
|
|
113
|
+
const loadRes = await httpsPost(
|
|
114
|
+
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
|
115
|
+
{ metadata: { pluginType: "GEMINI" } },
|
|
116
|
+
oauth.access_token,
|
|
117
|
+
);
|
|
118
|
+
const id = loadRes?.cloudaicompanionProject;
|
|
119
|
+
if (id) writeJsonSafe(GEMINI_PROJECT_CACHE_PATH, { cacheKey, projectId: id, timestamp: Date.now() });
|
|
120
|
+
return id || null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const projCache = readJsonMigrate(GEMINI_PROJECT_CACHE_PATH, LEGACY_GEMINI_PROJECT_CACHE, null);
|
|
124
|
+
let projectId = projCache?.cacheKey === cacheKey ? projCache?.projectId : null;
|
|
125
|
+
if (!projectId) projectId = await fetchProjectId();
|
|
126
|
+
if (!projectId) return cache;
|
|
127
|
+
|
|
128
|
+
// 4. retrieveUserQuota 호출
|
|
129
|
+
let quotaRes = await httpsPost(
|
|
130
|
+
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
|
|
131
|
+
{ project: projectId },
|
|
132
|
+
oauth.access_token,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// projectId 캐시가 만료/변경된 경우 1회 재시도
|
|
136
|
+
if (!quotaRes?.buckets && projCache?.projectId) {
|
|
137
|
+
projectId = await fetchProjectId();
|
|
138
|
+
if (!projectId) return cache;
|
|
139
|
+
quotaRes = await httpsPost(
|
|
140
|
+
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
|
|
141
|
+
{ project: projectId },
|
|
142
|
+
oauth.access_token,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!quotaRes?.buckets) {
|
|
147
|
+
// API 응답에 buckets 없음: 에러 코드 또는 응답 내용을 캐시에 기록
|
|
148
|
+
const apiError = quotaRes?.error?.message || quotaRes?.error?.code || quotaRes?.error || "no buckets in response";
|
|
149
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
150
|
+
...(cache || {}),
|
|
151
|
+
timestamp: cache?.timestamp || Date.now(),
|
|
152
|
+
error: true,
|
|
153
|
+
errorType: "api",
|
|
154
|
+
errorHint: String(apiError),
|
|
155
|
+
});
|
|
156
|
+
return cache;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 5. 캐시 저장
|
|
160
|
+
const result = {
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
cacheKey,
|
|
163
|
+
accountId: accountId || "gemini-main",
|
|
164
|
+
tokenFingerprint,
|
|
165
|
+
buckets: quotaRes.buckets,
|
|
166
|
+
};
|
|
167
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, result);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gemini RPM 트래커에서 최근 60초 내 요청 수를 읽는다.
|
|
173
|
+
* @returns {{ count: number, percent: number, remainingSec: number }}
|
|
174
|
+
*/
|
|
175
|
+
export function readGeminiRpm(model) {
|
|
176
|
+
try {
|
|
177
|
+
// 새 경로 → 레거시 경로 fallback
|
|
178
|
+
const rpmPath = existsSync(GEMINI_RPM_TRACKER_PATH) ? GEMINI_RPM_TRACKER_PATH
|
|
179
|
+
: existsSync(LEGACY_GEMINI_RPM_TRACKER) ? LEGACY_GEMINI_RPM_TRACKER : null;
|
|
180
|
+
if (!rpmPath) return { count: 0, percent: 0, remainingSec: 60 };
|
|
181
|
+
const raw = readFileSync(rpmPath, "utf-8");
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
const timestamps = Array.isArray(parsed.timestamps) ? parsed.timestamps : [];
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const recent = timestamps.filter((t) => now - t < GEMINI_RPM_WINDOW_MS);
|
|
186
|
+
const count = recent.length;
|
|
187
|
+
const rpmLimit = getGeminiRpmLimit(model);
|
|
188
|
+
const percent = clampPercent(Math.round((count / rpmLimit) * 100));
|
|
189
|
+
// 가장 오래된 엔트리가 윈도우에서 빠지기까지 남은 초 (0건이면 0s)
|
|
190
|
+
// 5초 단위 반올림으로 HUD 깜빡임 감소
|
|
191
|
+
const rawRemainingSec = recent.length > 0
|
|
192
|
+
? Math.max(0, Math.ceil((GEMINI_RPM_WINDOW_MS - (now - Math.min(...recent))) / 1000))
|
|
193
|
+
: 0;
|
|
194
|
+
const remainingSec = Math.ceil(rawRemainingSec / 5) * 5;
|
|
195
|
+
return { count, percent, remainingSec };
|
|
196
|
+
} catch {
|
|
197
|
+
return { count: 0, percent: 0, remainingSec: 60 };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function readGeminiQuotaSnapshot(accountId, authContext) {
|
|
202
|
+
const cache = readJsonMigrate(GEMINI_QUOTA_CACHE_PATH, LEGACY_GEMINI_QUOTA_CACHE, null);
|
|
203
|
+
if (!cache?.buckets) {
|
|
204
|
+
return { quota: null, shouldRefresh: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const cacheKey = authContext.cacheKey;
|
|
208
|
+
const isLegacyCache = !cache.cacheKey;
|
|
209
|
+
const keyMatched = cache.cacheKey === cacheKey;
|
|
210
|
+
const cacheTs = Number(cache.timestamp);
|
|
211
|
+
const ageMs = Number.isFinite(cacheTs) ? Date.now() - cacheTs : Number.MAX_SAFE_INTEGER;
|
|
212
|
+
const isFresh = ageMs < GEMINI_QUOTA_STALE_MS;
|
|
213
|
+
|
|
214
|
+
if (keyMatched) {
|
|
215
|
+
// resetTime이 지난 버킷의 remainingFraction을 1로 보정 (stale 캐시 방지)
|
|
216
|
+
if (Array.isArray(cache.buckets)) {
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const patchedBuckets = cache.buckets.map(b =>
|
|
219
|
+
b?.resetTime && new Date(b.resetTime).getTime() <= now
|
|
220
|
+
? { ...b, remainingFraction: 1 }
|
|
221
|
+
: b
|
|
222
|
+
);
|
|
223
|
+
return { quota: { ...cache, buckets: patchedBuckets }, shouldRefresh: !isFresh };
|
|
224
|
+
}
|
|
225
|
+
return { quota: cache, shouldRefresh: !isFresh };
|
|
226
|
+
}
|
|
227
|
+
if (isLegacyCache) {
|
|
228
|
+
return { quota: cache, shouldRefresh: true };
|
|
229
|
+
}
|
|
230
|
+
return { quota: null, shouldRefresh: true };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function scheduleGeminiQuotaRefresh(accountId) {
|
|
234
|
+
const scriptPath = process.argv[1];
|
|
235
|
+
if (!scriptPath) return;
|
|
236
|
+
try {
|
|
237
|
+
const child = spawn(
|
|
238
|
+
process.execPath,
|
|
239
|
+
[scriptPath, GEMINI_REFRESH_FLAG, "--account", accountId || "gemini-main"],
|
|
240
|
+
{
|
|
241
|
+
detached: true,
|
|
242
|
+
stdio: "ignore",
|
|
243
|
+
windowsHide: true,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
child.unref();
|
|
247
|
+
} catch (spawnErr) {
|
|
248
|
+
// spawn 실패 시 캐시에 에러 힌트 기록 (다음 HUD 렌더에서 원인 확인 가능)
|
|
249
|
+
writeJsonSafe(GEMINI_QUOTA_CACHE_PATH, {
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
error: true,
|
|
252
|
+
errorHint: String(spawnErr?.message || spawnErr),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function readGeminiSessionSnapshot() {
|
|
258
|
+
const cache = readJsonMigrate(GEMINI_SESSION_CACHE_PATH, LEGACY_GEMINI_SESSION_CACHE, null);
|
|
259
|
+
if (!cache?.session) {
|
|
260
|
+
return { session: null, shouldRefresh: true };
|
|
261
|
+
}
|
|
262
|
+
const ts = Number(cache.timestamp);
|
|
263
|
+
const ageMs = Number.isFinite(ts) ? Date.now() - ts : Number.MAX_SAFE_INTEGER;
|
|
264
|
+
const isFresh = ageMs < GEMINI_SESSION_STALE_MS;
|
|
265
|
+
return { session: cache.session, shouldRefresh: !isFresh };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function refreshGeminiSessionCache() {
|
|
269
|
+
const session = scanGeminiSessionTokens();
|
|
270
|
+
if (!session) return null;
|
|
271
|
+
writeJsonSafe(GEMINI_SESSION_CACHE_PATH, { timestamp: Date.now(), session });
|
|
272
|
+
return session;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function scheduleGeminiSessionRefresh() {
|
|
276
|
+
const scriptPath = process.argv[1];
|
|
277
|
+
if (!scriptPath) return;
|
|
278
|
+
try {
|
|
279
|
+
const child = spawn(process.execPath, [scriptPath, GEMINI_SESSION_REFRESH_FLAG], {
|
|
280
|
+
detached: true,
|
|
281
|
+
stdio: "ignore",
|
|
282
|
+
windowsHide: true,
|
|
283
|
+
});
|
|
284
|
+
child.unref();
|
|
285
|
+
} catch { /* 백그라운드 실행 실패 무시 */ }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Gemini 세션 JSON에서 토큰 사용량 추출
|
|
290
|
+
// ============================================================================
|
|
291
|
+
export function scanGeminiSessionTokens() {
|
|
292
|
+
const tmpDir = join(homedir(), ".gemini", "tmp");
|
|
293
|
+
if (!existsSync(tmpDir)) return null;
|
|
294
|
+
let best = null;
|
|
295
|
+
let bestTime = 0;
|
|
296
|
+
try {
|
|
297
|
+
const dirs = readdirSync(tmpDir).filter((d) => existsSync(join(tmpDir, d, "chats")));
|
|
298
|
+
for (const dir of dirs) {
|
|
299
|
+
const chatsDir = join(tmpDir, dir, "chats");
|
|
300
|
+
let files;
|
|
301
|
+
try { files = readdirSync(chatsDir).filter((f) => f.endsWith(".json")); } catch { continue; }
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
try {
|
|
304
|
+
const data = JSON.parse(readFileSync(join(chatsDir, file), "utf-8"));
|
|
305
|
+
const updatedAt = new Date(data.lastUpdated || 0).getTime();
|
|
306
|
+
if (updatedAt <= bestTime) continue;
|
|
307
|
+
let input = 0, output = 0;
|
|
308
|
+
let model = "unknown";
|
|
309
|
+
for (const msg of data.messages || []) {
|
|
310
|
+
if (msg.tokens) { input += msg.tokens.input || 0; output += msg.tokens.output || 0; }
|
|
311
|
+
if (msg.model) model = msg.model;
|
|
312
|
+
}
|
|
313
|
+
bestTime = updatedAt;
|
|
314
|
+
best = { input, output, total: input + output, model, lastUpdated: data.lastUpdated };
|
|
315
|
+
} catch { /* 무시 */ }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch { /* 무시 */ }
|
|
319
|
+
return best;
|
|
320
|
+
}
|