@cartanova/qgrid-cli 2.10.2 → 2.10.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/application/monit/log-buffer.js +7 -11
- package/bundle/dist/application/monit/monit.frame.js +6 -11
- package/bundle/dist/application/qgrid/oauth.js +2 -2
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -36
- package/bundle/dist/application/qgrid/qgrid.frame.js +2 -2
- package/bundle/dist/application/qgrid/qgrid.types.js +2 -1
- package/bundle/dist/application/qgrid/token-subscriber.js +4 -7
- package/bundle/dist/application/qgrid/token-window-keepalive.js +3 -2
- package/bundle/dist/application/qgrid/tool-emulation-schema.js +4 -4
- package/bundle/dist/application/qgrid/tool-emulation.js +14 -2
- package/bundle/dist/application/request-log/request-log.model.js +7 -5
- package/bundle/dist/application/setting/setting.model.js +1 -1
- package/bundle/dist/sonamu.config.js +6 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +53 -84
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +55 -0
- package/bundle/dist/utils/providers/anthropic/claude-session.js +11 -13
- package/bundle/dist/utils/providers/common/effort.js +1 -1
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +3 -3
- package/bundle/dist/utils/providers/common/thinking.js +12 -0
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +6 -16
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +89 -78
- package/bundle/src/application/monit/log-buffer.test.ts +3 -4
- package/bundle/src/application/monit/log-buffer.ts +13 -27
- package/bundle/src/application/monit/monit.frame.test.ts +7 -8
- package/bundle/src/application/monit/monit.frame.ts +9 -14
- package/bundle/src/application/qgrid/oauth.ts +1 -1
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +7 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +1 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +62 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +25 -71
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -8
- package/bundle/src/application/qgrid/qgrid.types.ts +2 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +8 -18
- package/bundle/src/application/qgrid/token-subscriber.ts +10 -20
- package/bundle/src/application/qgrid/token-window-keepalive.test.ts +2 -2
- package/bundle/src/application/qgrid/token-window-keepalive.ts +2 -1
- package/bundle/src/application/qgrid/tool-emulation-schema.test.ts +6 -3
- package/bundle/src/application/qgrid/tool-emulation-schema.ts +3 -3
- package/bundle/src/application/qgrid/tool-emulation.test.ts +8 -1
- package/bundle/src/application/qgrid/tool-emulation.ts +29 -0
- package/bundle/src/application/request-log/request-log.model.ts +5 -5
- package/bundle/src/application/setting/setting.model.ts +1 -1
- package/bundle/src/sonamu.config.ts +5 -5
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +3 -6
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +26 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +70 -148
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.test.ts +36 -0
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +84 -0
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +74 -13
- package/bundle/src/utils/providers/anthropic/claude-session.ts +11 -20
- package/bundle/src/utils/providers/common/effort.ts +1 -1
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +4 -0
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -4
- package/bundle/src/utils/providers/common/thinking.ts +10 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +7 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +3 -3
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +20 -33
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +97 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +113 -77
- package/bundle/web-dist/client/assets/{index-DtRTSTes.js → index-BIC1sQxw.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-BhVYDEzC.js → logs-zvdujZWn.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-BxN4JWqK.js → monit-CraVV3MQ.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CBCqRp2o.js → routes-B3ZBoJ60.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-hBTEC7Ki.js → settings-DwbzT5jo.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BzYft5N4.js → show-ItTExdAh.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-PMybtI8X.js → tokens-BynKx8Vn.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-C51_d6-6.js → use-oauth-login-flow-8xw6on5P.js} +1 -1
- package/bundle/web-dist/client/index.html +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +1 -1
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +1 -0
- package/skills/qgrid/references/ai-sdk-provider-contract.md +2 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +7 -4
- package/skills/qgrid/references/decision-rationale.md +9 -0
- package/skills/qgrid/references/openai-codex-runtime.md +6 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
- package/skills/qgrid/references/tool-calling-and-multiturn.md +18 -1
- package/skills/qgrid/references/verification-and-debugging.md +1 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ProviderDispatcher,
|
|
13
13
|
} from "../common/provider-dispatcher";
|
|
14
14
|
import { SmoothWeightedRoundRobin } from "../common/smooth-weighted-round-robin";
|
|
15
|
+
import { ThinkingValidationError } from "../common/thinking";
|
|
15
16
|
import {
|
|
16
17
|
type JsonValue,
|
|
17
18
|
type OpenAIResponseItem,
|
|
@@ -23,8 +24,15 @@ import { handleChatgptAuthTokensRefresh } from "./openai-refresh";
|
|
|
23
24
|
import { type OpenAITransportKind, resolveOpenAITransportKind } from "./openai-transport-config";
|
|
24
25
|
|
|
25
26
|
const logger = getLogger(["qgrid", "openai-dispatcher"]);
|
|
26
|
-
//
|
|
27
|
-
|
|
27
|
+
// Verified with the subscription Responses backend; public API support is not sufficient.
|
|
28
|
+
const THINKING_OFF_MODELS = new Set([
|
|
29
|
+
"gpt-6-sol",
|
|
30
|
+
"gpt-6-luna",
|
|
31
|
+
"gpt-5.6-sol",
|
|
32
|
+
"gpt-5.6-terra",
|
|
33
|
+
"gpt-5.6-luna",
|
|
34
|
+
"gpt-5.5",
|
|
35
|
+
]);
|
|
28
36
|
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
|
|
29
37
|
const TRANSPARENT_IMAGE_TOOL = "generate_transparent_image";
|
|
30
38
|
|
|
@@ -97,10 +105,11 @@ function inputItems(req: GenerateRequest): OpenAIResponseItem[] {
|
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
function asReasoning(req: GenerateRequest): OpenAIResponsesOptions["reasoning"] | undefined {
|
|
108
|
+
if (req.thinking === false) return { effort: "none" };
|
|
100
109
|
const summary =
|
|
101
110
|
req.reasoningSummary && req.reasoningSummary !== "none" ? req.reasoningSummary : undefined;
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
const effort =
|
|
112
|
+
resolveOpenAIEffort(req.model ?? "", req.effort) ?? (req.thinking === true ? "low" : undefined);
|
|
104
113
|
if (!effort && !summary) return undefined;
|
|
105
114
|
return {
|
|
106
115
|
...(effort ? { effort } : {}),
|
|
@@ -178,31 +187,24 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
178
187
|
};
|
|
179
188
|
}
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
* Direct OpenAI dispatcher — Anthropic 과 동일한 stateless 실행 모델.
|
|
183
|
-
*
|
|
184
|
-
* 요청 = 토큰 선택(quota gate + weighted round-robin + cache affinity 선호) → HTTPS/WS
|
|
185
|
-
* 전송. permit/큐 같은 동시성 상한은 두지 않는다: 전송은 fetch 한 번이라 로컬 자원이
|
|
186
|
-
* 희소하지 않고, 상류 제한은 백엔드의 응답(429 등)이 진실이다. worker pool 시절의
|
|
187
|
-
* 큐 의미론(SERVER_BUSY, 큐 타임아웃)은 이 전환으로 제거됐다.
|
|
188
|
-
*/
|
|
190
|
+
// quota와 cache affinity로 토큰을 선택해 HTTPS/WS로 전송한다. 로컬 동시성 제한은 없다.
|
|
189
191
|
export class OpenAIDispatcher implements ProviderDispatcher {
|
|
190
192
|
readonly tokenMetadata = new Map<number, TokenMetadata>();
|
|
191
193
|
readonly transportKind: OpenAITransportKind;
|
|
192
194
|
readonly rateLimitsCache = new Map<number, OpenAIRateLimitsWithMeta & { generation: number }>();
|
|
193
|
-
|
|
195
|
+
readonly pendingRateLimits = new Map<
|
|
194
196
|
number,
|
|
195
197
|
{ token: TokenMetadata; generation: number; promise: Promise<OpenAIRateLimitsWithMeta> }
|
|
196
198
|
>();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
readonly clients = new Map<number, ClientEntry>();
|
|
200
|
+
readonly retiredClients = new Set<ClientEntry>();
|
|
199
201
|
static readonly RATE_LIMITS_CACHE_TTL = 60_000;
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
readonly selector = new SmoothWeightedRoundRobin();
|
|
204
|
+
readonly clientFactory: OpenAIDirectClientFactory;
|
|
205
|
+
readonly fetchImpl: typeof fetch;
|
|
206
|
+
readonly quotaBlocked = new Set<number>();
|
|
207
|
+
inFlight = 0;
|
|
206
208
|
|
|
207
209
|
constructor(
|
|
208
210
|
transportKind: OpenAITransportKind = resolveOpenAITransportKind(),
|
|
@@ -216,24 +218,25 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
216
218
|
|
|
217
219
|
async start(): Promise<void> {
|
|
218
220
|
const { rows } = await TokenModel.findMany("A");
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
rows
|
|
222
|
+
.filter((row) => row.provider === "openai")
|
|
223
|
+
.forEach((row) => {
|
|
224
|
+
this.setToken(
|
|
225
|
+
row.id,
|
|
226
|
+
row.name,
|
|
227
|
+
row.credentials as OpenAICredentials,
|
|
228
|
+
row.quota_threshold,
|
|
229
|
+
row.weight,
|
|
230
|
+
row.active,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
230
233
|
logger.info(`started direct OpenAI runtime with ${this.tokenMetadata.size} tokens`);
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
async stop(): Promise<void> {
|
|
234
|
-
|
|
237
|
+
this.clients.forEach(({ client }) => client.close?.());
|
|
235
238
|
this.clients.clear();
|
|
236
|
-
|
|
239
|
+
this.retiredClients.forEach((entry) => entry.client.close?.());
|
|
237
240
|
this.retiredClients.clear();
|
|
238
241
|
this.tokenMetadata.clear();
|
|
239
242
|
this.rateLimitsCache.clear();
|
|
@@ -315,11 +318,19 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
async run(req: GenerateRequest, onDelta?: (text: string) => void): Promise<GenerateResult> {
|
|
322
|
+
if (req.thinking === false) {
|
|
323
|
+
const model = req.model ?? "";
|
|
324
|
+
if (!THINKING_OFF_MODELS.has(model)) {
|
|
325
|
+
const reason =
|
|
326
|
+
model === "gpt-6-astra" || model === "gpt-6.1-sol"
|
|
327
|
+
? "the subscription backend requires thinking"
|
|
328
|
+
: "thinking-off support has not been verified on the subscription backend";
|
|
329
|
+
throw new ThinkingValidationError(
|
|
330
|
+
`thinking:false is unavailable for OpenAI model ${model || "<default>"}: ${reason}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
323
334
|
const signal = activeSignal(req.abortSignal, req.timeoutMs);
|
|
324
335
|
if (signal.aborted) throw abortError(signal);
|
|
325
336
|
const selection = await this.selectToken(
|
|
@@ -327,16 +338,16 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
327
338
|
req.requirePreferredToken ?? false,
|
|
328
339
|
signal,
|
|
329
340
|
);
|
|
330
|
-
this.
|
|
341
|
+
this.inFlight++;
|
|
331
342
|
try {
|
|
332
343
|
if (signal.aborted) throw abortError(signal);
|
|
333
344
|
return await this.runDirect(selection, { ...req, abortSignal: signal }, onDelta);
|
|
334
345
|
} finally {
|
|
335
|
-
this.
|
|
346
|
+
this.inFlight--;
|
|
336
347
|
}
|
|
337
348
|
}
|
|
338
349
|
|
|
339
|
-
|
|
350
|
+
async runDirect(
|
|
340
351
|
selection: TokenSelection,
|
|
341
352
|
req: GenerateRequest,
|
|
342
353
|
onDelta?: (text: string) => void,
|
|
@@ -344,6 +355,9 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
344
355
|
const startedAt = Date.now();
|
|
345
356
|
let firstDeltaAt: number | undefined;
|
|
346
357
|
let text = "";
|
|
358
|
+
let outputMessageAccepted = false;
|
|
359
|
+
const messagePhases = new Map<string, string>();
|
|
360
|
+
const streamedMessageIds = new Set<string>();
|
|
347
361
|
let usage = {
|
|
348
362
|
totalTokens: 0,
|
|
349
363
|
inputTokens: 0,
|
|
@@ -366,10 +380,53 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
366
380
|
requestOptions(req),
|
|
367
381
|
req.abortSignal,
|
|
368
382
|
)) {
|
|
383
|
+
if (
|
|
384
|
+
req.stopAfterOutputMessage &&
|
|
385
|
+
event.type === "output-item" &&
|
|
386
|
+
event.item.type === "message" &&
|
|
387
|
+
typeof event.item.id === "string" &&
|
|
388
|
+
typeof event.item.phase === "string"
|
|
389
|
+
) {
|
|
390
|
+
messagePhases.set(event.item.id, event.item.phase);
|
|
391
|
+
}
|
|
369
392
|
if (event.type === "text-delta") {
|
|
393
|
+
if (outputMessageAccepted) continue;
|
|
394
|
+
if (req.stopAfterOutputMessage && event.itemId) {
|
|
395
|
+
// Buffer commentary/unclassified messages until their completed item.
|
|
396
|
+
// Known final answers retain incremental streaming.
|
|
397
|
+
if (messagePhases.get(event.itemId) !== "final_answer") continue;
|
|
398
|
+
streamedMessageIds.add(event.itemId);
|
|
399
|
+
}
|
|
370
400
|
firstDeltaAt ??= Date.now();
|
|
371
401
|
text += event.text;
|
|
372
402
|
onDelta?.(event.text);
|
|
403
|
+
} else if (
|
|
404
|
+
event.type === "output-item" &&
|
|
405
|
+
event.completed &&
|
|
406
|
+
event.item.type === "message" &&
|
|
407
|
+
!outputMessageAccepted &&
|
|
408
|
+
req.stopAfterOutputMessage &&
|
|
409
|
+
Array.isArray(event.item.content)
|
|
410
|
+
) {
|
|
411
|
+
const messageText = event.item.content
|
|
412
|
+
.filter((part) => part?.type === "output_text" && typeof part.text === "string")
|
|
413
|
+
.map((part) => part.text)
|
|
414
|
+
.join("");
|
|
415
|
+
// A valid client tool request ends this application turn. Later answers cannot
|
|
416
|
+
// know its result yet. Never split/repair malformed JSON within a message.
|
|
417
|
+
const phase = typeof event.item.phase === "string" ? event.item.phase : undefined;
|
|
418
|
+
if (messageText) outputMessageAccepted = req.stopAfterOutputMessage(messageText, phase);
|
|
419
|
+
if (
|
|
420
|
+
typeof event.item.id === "string" &&
|
|
421
|
+
!streamedMessageIds.has(event.item.id) &&
|
|
422
|
+
(phase !== "commentary" || outputMessageAccepted) &&
|
|
423
|
+
messageText
|
|
424
|
+
) {
|
|
425
|
+
firstDeltaAt ??= Date.now();
|
|
426
|
+
text += messageText;
|
|
427
|
+
onDelta?.(messageText);
|
|
428
|
+
streamedMessageIds.add(event.item.id);
|
|
429
|
+
}
|
|
373
430
|
} else if (event.type === "image") {
|
|
374
431
|
imageAttempted = true;
|
|
375
432
|
images.push({
|
|
@@ -511,7 +568,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
511
568
|
}
|
|
512
569
|
|
|
513
570
|
/** 세대가 바뀐 client 는 사용 중인 요청이 끝난 뒤에 닫는다. */
|
|
514
|
-
|
|
571
|
+
acquireClient(selection: TokenSelection): ClientEntry {
|
|
515
572
|
const metadata = selection.metadata;
|
|
516
573
|
let entry = this.clients.get(selection.tokenId);
|
|
517
574
|
if (entry && entry.generation !== metadata.generation) {
|
|
@@ -538,7 +595,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
538
595
|
return entry;
|
|
539
596
|
}
|
|
540
597
|
|
|
541
|
-
|
|
598
|
+
releaseClient(entry: ClientEntry): void {
|
|
542
599
|
entry.inFlight = Math.max(0, entry.inFlight - 1);
|
|
543
600
|
if (entry.retired && entry.inFlight === 0) {
|
|
544
601
|
this.retiredClients.delete(entry);
|
|
@@ -546,8 +603,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
546
603
|
}
|
|
547
604
|
}
|
|
548
605
|
|
|
549
|
-
|
|
550
|
-
private retireClient(tokenId: number): void {
|
|
606
|
+
retireClient(tokenId: number): void {
|
|
551
607
|
const entry = this.clients.get(tokenId);
|
|
552
608
|
if (!entry) return;
|
|
553
609
|
this.clients.delete(tokenId);
|
|
@@ -556,13 +612,8 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
556
612
|
else this.retiredClients.add(entry);
|
|
557
613
|
}
|
|
558
614
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
* 토큰이 eligible 하면 그 판정만으로 즉시 결정하고(전체 sweep 생략, weighted 상태
|
|
562
|
-
* 비변경), 아니면 병렬 quota 판정 후 smooth weighted round-robin. 동시성 상한이
|
|
563
|
-
* 없으므로 "빈 자리" 개념도 없다 — 선택 즉시 실행이다.
|
|
564
|
-
*/
|
|
565
|
-
private async selectToken(
|
|
615
|
+
// 선호 토큰이 quota를 통과하면 다른 토큰 조회와 weighted 상태 변경을 생략한다.
|
|
616
|
+
async selectToken(
|
|
566
617
|
preferredTokenId: number | undefined,
|
|
567
618
|
requirePreferredToken: boolean,
|
|
568
619
|
signal: AbortSignal,
|
|
@@ -585,8 +636,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
585
636
|
}
|
|
586
637
|
}
|
|
587
638
|
|
|
588
|
-
//
|
|
589
|
-
// 토큰 수 × 조회 지연만큼 요청 시작을 늦춘다.
|
|
639
|
+
// 콜드 캐시에서 토큰 수만큼 조회 지연이 쌓이지 않도록 병렬 판정한다.
|
|
590
640
|
const checks = await Promise.all(
|
|
591
641
|
[...this.tokenMetadata.entries()]
|
|
592
642
|
.filter(([, token]) => token.active)
|
|
@@ -619,7 +669,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
619
669
|
return { tokenId: selected, metadata };
|
|
620
670
|
}
|
|
621
671
|
|
|
622
|
-
|
|
672
|
+
setToken(
|
|
623
673
|
id: number,
|
|
624
674
|
name: string,
|
|
625
675
|
credentials: OpenAICredentials,
|
|
@@ -667,9 +717,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
667
717
|
)
|
|
668
718
|
return cached;
|
|
669
719
|
|
|
670
|
-
//
|
|
671
|
-
// 조회가 몰릴 수 있다 — 진행 중인 fetch 를 공유한다. 최초 호출자의 abort 로
|
|
672
|
-
// 공유 조회가 실패해도 소비처(isQuotaEligible)가 fail-open 으로 처리한다.
|
|
720
|
+
// TTL 만료 시 조회를 공유한다. 최초 호출자의 abort로 실패해도 isQuotaEligible은 fail-open한다.
|
|
673
721
|
const pending = this.pendingRateLimits.get(tokenId);
|
|
674
722
|
if (pending?.token === token && pending.generation === generation) return pending.promise;
|
|
675
723
|
const fetchPromise = (async () => {
|
|
@@ -695,7 +743,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
695
743
|
return fetchPromise;
|
|
696
744
|
}
|
|
697
745
|
|
|
698
|
-
|
|
746
|
+
async refreshCredentials(tokenId: number) {
|
|
699
747
|
const previous = this.tokenMetadata.get(tokenId);
|
|
700
748
|
const generation = previous?.generation;
|
|
701
749
|
const refreshed = await handleChatgptAuthTokensRefresh(tokenId);
|
|
@@ -718,11 +766,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
718
766
|
};
|
|
719
767
|
}
|
|
720
768
|
|
|
721
|
-
|
|
722
|
-
id: number,
|
|
723
|
-
token: TokenMetadata,
|
|
724
|
-
signal?: AbortSignal,
|
|
725
|
-
): Promise<boolean> {
|
|
769
|
+
async isQuotaEligible(id: number, token: TokenMetadata, signal?: AbortSignal): Promise<boolean> {
|
|
726
770
|
if (token.quotaThreshold === null || token.quotaThreshold === undefined) return true;
|
|
727
771
|
const generation = token.generation;
|
|
728
772
|
const result = await readOpenAIQuotaUsage(async () => this.getRateLimitsByTokenId(id, signal));
|
|
@@ -751,23 +795,16 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
751
795
|
return true;
|
|
752
796
|
}
|
|
753
797
|
|
|
754
|
-
|
|
798
|
+
invalidateRateLimitsCache(id: number): void {
|
|
755
799
|
this.rateLimitsCache.delete(id);
|
|
756
800
|
}
|
|
757
801
|
|
|
758
|
-
|
|
802
|
+
countActiveTokens(): number {
|
|
759
803
|
return [...this.tokenMetadata.values()].filter((t) => t.active).length;
|
|
760
804
|
}
|
|
761
|
-
get inFlight(): number {
|
|
762
|
-
return this.inFlightCount;
|
|
763
|
-
}
|
|
764
805
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
* monit vitals 는 로그 폴링에 편승하는 값이라 여기서 네트워크를 타면 안 된다.
|
|
768
|
-
* threshold 미설정 토큰은 쿼터 판정을 돌지 않아 usedPercent 가 null 로 남을 수 있다.
|
|
769
|
-
*/
|
|
770
|
-
get quotaByToken(): Array<{
|
|
806
|
+
// 모니터링 폴링은 TTL 내 캐시만 읽고 fetch하지 않는다. 미조회 토큰은 usedPercent가 null이다.
|
|
807
|
+
getQuotaSnapshot(): Array<{
|
|
771
808
|
name: string;
|
|
772
809
|
usedPercent: number | null;
|
|
773
810
|
threshold: number | null;
|
|
@@ -788,8 +825,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
788
825
|
usedPercent: primary?.usedPercent ?? null,
|
|
789
826
|
threshold: t.quotaThreshold ?? null,
|
|
790
827
|
blocked: this.quotaBlocked.has(id),
|
|
791
|
-
// wham
|
|
792
|
-
// 소비처가 단위를 추측하지 않도록 여기서 ms epoch 으로 정규화한다.
|
|
828
|
+
// wham의 Unix 초를 epoch ms로 변환한다.
|
|
793
829
|
resetsAt:
|
|
794
830
|
primary?.resetsAt !== null && primary?.resetsAt !== undefined
|
|
795
831
|
? primary.resetsAt * 1000
|