@cartanova/qgrid-cli 2.3.4 → 2.3.6
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/qgrid/oauth.js +15 -12
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -1
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +2 -1
- package/bundle/dist/application/qgrid/qgrid.frame.js +23 -10
- package/bundle/dist/application/qgrid/qgrid.types.js +8 -2
- package/bundle/dist/application/qgrid/token-subscriber.js +16 -8
- package/bundle/dist/application/qgrid/token-trigger-setup.js +3 -2
- package/bundle/dist/application/request-log/request-log.model.js +12 -7
- package/bundle/dist/application/sonamu.generated.js +11 -3
- package/bundle/dist/application/sonamu.generated.sso.js +7 -2
- package/bundle/dist/application/token/token.model.js +10 -2
- package/bundle/dist/application/token/token.types.js +5 -3
- package/bundle/dist/i18n/sd.generated.js +4 -1
- package/bundle/dist/migrations/20260630223950_alter_tokens_add1_alter1.js +16 -0
- package/bundle/dist/migrations/20260701105327_alter_request_logs_add1_alter1.js +16 -0
- package/bundle/dist/migrations/20260701105328_alter_request_log_steps_add1_alter1.js +16 -0
- package/bundle/dist/sonamu.config.js +13 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +99 -12
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +31 -0
- package/bundle/dist/utils/providers/anthropic/claude-session.js +37 -14
- package/bundle/dist/utils/providers/common/ttft.js +25 -0
- package/bundle/dist/utils/providers/openai/codex-worker.js +35 -7
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +237 -39
- package/bundle/dist/utils/providers/openai/openai-quota.js +30 -0
- package/bundle/src/application/qgrid/oauth.ts +17 -7
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +96 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +1 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +16 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +1 -0
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +128 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +41 -20
- package/bundle/src/application/qgrid/qgrid.types.ts +6 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +89 -0
- package/bundle/src/application/qgrid/token-subscriber.ts +40 -11
- package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -1
- package/bundle/src/application/qgrid/tool-emulation.test.ts +7 -3
- package/bundle/src/application/request-log/request-log.entity.json +3 -0
- package/bundle/src/application/request-log/request-log.model.test.ts +92 -0
- package/bundle/src/application/request-log/request-log.model.ts +20 -6
- package/bundle/src/application/request-log-step/request-log-step.entity.json +3 -0
- package/bundle/src/application/sonamu.generated.http +7 -2
- package/bundle/src/application/sonamu.generated.sso.ts +5 -0
- package/bundle/src/application/sonamu.generated.ts +11 -1
- package/bundle/src/application/token/token.entity.json +8 -2
- package/bundle/src/application/token/token.model.test.ts +69 -0
- package/bundle/src/application/token/token.model.ts +8 -1
- package/bundle/src/application/token/token.types.test.ts +34 -0
- package/bundle/src/application/token/token.types.ts +5 -0
- package/bundle/src/i18n/sd.generated.ts +3 -0
- package/bundle/src/migrations/20260630223950_alter_tokens_add1_alter1.ts +15 -0
- package/bundle/src/migrations/20260701105327_alter_request_logs_add1_alter1.ts +15 -0
- package/bundle/src/migrations/20260701105328_alter_request_log_steps_add1_alter1.ts +15 -0
- package/bundle/src/sonamu.config.ts +8 -1
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +183 -15
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +142 -12
- package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +89 -0
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +27 -0
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +40 -0
- package/bundle/src/utils/providers/anthropic/claude-session.ts +58 -6
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -0
- package/bundle/src/utils/providers/common/ttft.test.ts +37 -0
- package/bundle/src/utils/providers/common/ttft.ts +26 -0
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +148 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +50 -8
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +378 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +332 -37
- package/bundle/src/utils/providers/openai/openai-quota.test.ts +85 -0
- package/bundle/src/utils/providers/openai/openai-quota.ts +46 -0
- package/bundle/web-dist/client/assets/index-5WdFEqZT.css +1 -0
- package/bundle/web-dist/client/assets/{index-DeThumA3.js → index-jnUY7nJ-.js} +3 -3
- package/bundle/web-dist/client/assets/logs-B0pWpDrg.js +1 -0
- package/bundle/web-dist/client/assets/routes-7XhVuG7o.js +5 -0
- package/bundle/web-dist/client/assets/{sd.generated-7jzAQL2_.js → sd.generated-DXhjjL2f.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-Bx1Eh7ot.js → services.generated-B7u0U1wC.js} +1 -1
- package/bundle/web-dist/client/assets/{show-C6OofoAL.js → show-CcjZ9nr7.js} +3 -3
- package/bundle/web-dist/client/assets/tokens-DlwAm0v4.js +1 -0
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BEaI4mOq.js → lazyRouteComponent-B0LkbiYf.js} +1 -2
- package/bundle/web-dist/server/assets/{logs-CHM0fhfO.js → logs-dLx6gMXT.js} +17 -5
- package/bundle/web-dist/server/assets/{routes-C9xFMJ_r.js → routes-DD3Hb9xO.js} +201 -13
- package/bundle/web-dist/server/assets/{sd.generated-BeUHLWKH.js → sd.generated-DdfpD_iN.js} +131 -1
- package/bundle/web-dist/server/assets/{services.generated-C39hdSv0.js → services.generated-D3ZCvIkt.js} +5 -4
- package/bundle/web-dist/server/assets/{show-BZWqH09U.js → show-BQjeyv6A.js} +76 -25
- package/bundle/web-dist/server/assets/{tokens-IPwv3pNB.js → tokens-CSOsBpP7.js} +4 -3
- package/bundle/web-dist/server/entry-server.generated.js +6 -7
- package/dist/cli.js +67 -15
- package/package.json +1 -1
- package/bundle/web-dist/client/assets/index-B5DSo5uH.css +0 -1
- package/bundle/web-dist/client/assets/logs-CtmOVH58.js +0 -1
- package/bundle/web-dist/client/assets/routes-Cfoe9J0I.js +0 -5
- package/bundle/web-dist/client/assets/tokens-BAZq8sTo.js +0 -1
- package/bundle/web-dist/server/assets/shim-B9ZrFt3Z.js +0 -129
- /package/bundle/web-dist/server/assets/{cost-DzjfVtUr.js → cost-CSH_t17I.js} +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "../../../_virtual/_rolldown/runtime.js";
|
|
2
|
+
//#region src/utils/providers/openai/openai-quota.ts
|
|
3
|
+
async function readOpenAIQuotaUsage(readRateLimits) {
|
|
4
|
+
try {
|
|
5
|
+
const { data, cachedAt } = await readRateLimits();
|
|
6
|
+
const rateLimits = data.rateLimits;
|
|
7
|
+
const primary = rateLimits?.primary;
|
|
8
|
+
if (!primary || typeof primary.usedPercent !== "number" || !Number.isFinite(primary.usedPercent)) return {
|
|
9
|
+
kind: "lookup_failed",
|
|
10
|
+
reason: "primary usedPercent unavailable"
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
kind: "ok",
|
|
14
|
+
utilizationPct: primary.usedPercent,
|
|
15
|
+
cacheAgeMs: Math.max(Date.now() - cachedAt, 0),
|
|
16
|
+
windowDurationMins: primary.windowDurationMins,
|
|
17
|
+
resetsAt: primary.resetsAt,
|
|
18
|
+
limitId: rateLimits.limitId
|
|
19
|
+
};
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return {
|
|
22
|
+
kind: "lookup_failed",
|
|
23
|
+
reason: e.message
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { readOpenAIQuotaUsage };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXF1b3RhLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy91dGlscy9wcm92aWRlcnMvb3BlbmFpL29wZW5haS1xdW90YS50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyB0eXBlIEdldEFjY291bnRSYXRlTGltaXRzUmVzcG9uc2UgfSBmcm9tIFwiLi4vLi4vLi4vY29kZXgtcHJvdG9jb2wvdjIvR2V0QWNjb3VudFJhdGVMaW1pdHNSZXNwb25zZVwiO1xuXG5leHBvcnQgdHlwZSBPcGVuQUlSYXRlTGltaXRzV2l0aE1ldGEgPSB7XG4gIGRhdGE6IEdldEFjY291bnRSYXRlTGltaXRzUmVzcG9uc2U7XG4gIGNhY2hlZEF0OiBudW1iZXI7XG59O1xuXG5leHBvcnQgdHlwZSBPcGVuQUlRdW90YVVzYWdlUmVzdWx0ID1cbiAgfCB7XG4gICAgICBraW5kOiBcIm9rXCI7XG4gICAgICB1dGlsaXphdGlvblBjdDogbnVtYmVyO1xuICAgICAgY2FjaGVBZ2VNczogbnVtYmVyO1xuICAgICAgd2luZG93RHVyYXRpb25NaW5zOiBudW1iZXIgfCBudWxsO1xuICAgICAgcmVzZXRzQXQ6IG51bWJlciB8IG51bGw7XG4gICAgICBsaW1pdElkOiBzdHJpbmcgfCBudWxsO1xuICAgIH1cbiAgfCB7IGtpbmQ6IFwibG9va3VwX2ZhaWxlZFwiOyByZWFzb246IHN0cmluZyB9O1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVhZE9wZW5BSVF1b3RhVXNhZ2UoXG4gIHJlYWRSYXRlTGltaXRzOiAoKSA9PiBQcm9taXNlPE9wZW5BSVJhdGVMaW1pdHNXaXRoTWV0YT4sXG4pOiBQcm9taXNlPE9wZW5BSVF1b3RhVXNhZ2VSZXN1bHQ+IHtcbiAgdHJ5IHtcbiAgICBjb25zdCB7IGRhdGEsIGNhY2hlZEF0IH0gPSBhd2FpdCByZWFkUmF0ZUxpbWl0cygpO1xuICAgIGNvbnN0IHJhdGVMaW1pdHMgPSBkYXRhLnJhdGVMaW1pdHM7XG4gICAgY29uc3QgcHJpbWFyeSA9IHJhdGVMaW1pdHM/LnByaW1hcnk7XG5cbiAgICBpZiAoXG4gICAgICAhcHJpbWFyeSB8fFxuICAgICAgdHlwZW9mIHByaW1hcnkudXNlZFBlcmNlbnQgIT09IFwibnVtYmVyXCIgfHxcbiAgICAgICFOdW1iZXIuaXNGaW5pdGUocHJpbWFyeS51c2VkUGVyY2VudClcbiAgICApIHtcbiAgICAgIHJldHVybiB7IGtpbmQ6IFwibG9va3VwX2ZhaWxlZFwiLCByZWFzb246IFwicHJpbWFyeSB1c2VkUGVyY2VudCB1bmF2YWlsYWJsZVwiIH07XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIGtpbmQ6IFwib2tcIixcbiAgICAgIHV0aWxpemF0aW9uUGN0OiBwcmltYXJ5LnVzZWRQZXJjZW50LFxuICAgICAgY2FjaGVBZ2VNczogTWF0aC5tYXgoRGF0ZS5ub3coKSAtIGNhY2hlZEF0LCAwKSxcbiAgICAgIHdpbmRvd0R1cmF0aW9uTWluczogcHJpbWFyeS53aW5kb3dEdXJhdGlvbk1pbnMsXG4gICAgICByZXNldHNBdDogcHJpbWFyeS5yZXNldHNBdCxcbiAgICAgIGxpbWl0SWQ6IHJhdGVMaW1pdHMubGltaXRJZCxcbiAgICB9O1xuICB9IGNhdGNoIChlKSB7XG4gICAgcmV0dXJuIHsga2luZDogXCJsb29rdXBfZmFpbGVkXCIsIHJlYXNvbjogKGUgYXMgRXJyb3IpLm1lc3NhZ2UgfTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOztBQWtCQSxlQUFzQixxQkFDcEIsZ0JBQ2lDO0FBQ2pDLEtBQUk7RUFDRixNQUFNLEVBQUUsTUFBTSxhQUFhLE1BQU0sZ0JBQWdCO0VBQ2pELE1BQU0sYUFBYSxLQUFLO0VBQ3hCLE1BQU0sVUFBVSxZQUFZO0FBRTVCLE1BQ0UsQ0FBQyxXQUNELE9BQU8sUUFBUSxnQkFBZ0IsWUFDL0IsQ0FBQyxPQUFPLFNBQVMsUUFBUSxZQUFZLENBRXJDLFFBQU87R0FBRSxNQUFNO0dBQWlCLFFBQVE7R0FBbUM7QUFHN0UsU0FBTztHQUNMLE1BQU07R0FDTixnQkFBZ0IsUUFBUTtHQUN4QixZQUFZLEtBQUssSUFBSSxLQUFLLEtBQUssR0FBRyxVQUFVLEVBQUU7R0FDOUMsb0JBQW9CLFFBQVE7R0FDNUIsVUFBVSxRQUFRO0dBQ2xCLFNBQVMsV0FBVztHQUNyQjtVQUNNLEdBQUc7QUFDVixTQUFPO0dBQUUsTUFBTTtHQUFpQixRQUFTLEVBQVk7R0FBUyJ9
|
|
@@ -10,6 +10,10 @@ export type AnthropicUsageRaw = {
|
|
|
10
10
|
five_hour?: { utilization: number | null; resets_at: string | null } | null;
|
|
11
11
|
seven_day?: { utilization: number | null; resets_at: string | null } | null;
|
|
12
12
|
};
|
|
13
|
+
export type AnthropicUsageWithMeta = {
|
|
14
|
+
data: AnthropicUsageRaw;
|
|
15
|
+
cachedAt: number;
|
|
16
|
+
};
|
|
13
17
|
|
|
14
18
|
const logger = getLogger(["qgrid", "oauth"]);
|
|
15
19
|
|
|
@@ -140,14 +144,18 @@ export async function refreshAccessToken(refreshToken: string): Promise<OAuthTok
|
|
|
140
144
|
};
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
const usageCache: Record<string,
|
|
147
|
+
const usageCache: Record<string, AnthropicUsageWithMeta> = {};
|
|
144
148
|
const USAGE_API_CACHE_TTL = 60_000; // 1분
|
|
145
149
|
|
|
146
150
|
export async function fetchUsage(accessToken: string): Promise<AnthropicUsageRaw> {
|
|
151
|
+
return (await fetchUsageWithMeta(accessToken)).data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function fetchUsageWithMeta(accessToken: string): Promise<AnthropicUsageWithMeta> {
|
|
147
155
|
const cacheKey = accessToken.slice(-10);
|
|
148
156
|
const cached = usageCache[cacheKey];
|
|
149
157
|
if (cached && Date.now() - cached.cachedAt < USAGE_API_CACHE_TTL) {
|
|
150
|
-
return cached
|
|
158
|
+
return cached;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
@@ -173,16 +181,18 @@ export async function fetchUsage(accessToken: string): Promise<AnthropicUsageRaw
|
|
|
173
181
|
logger.warn(`${res.status}: ${errorMessage}`);
|
|
174
182
|
|
|
175
183
|
// 이전 성공 캐시 있으면 유지
|
|
176
|
-
if (cached?.data && !cached.data.error) return cached
|
|
184
|
+
if (cached?.data && !cached.data.error) return cached;
|
|
177
185
|
|
|
178
186
|
// 실패도 캐시 (반복 호출 방지)
|
|
179
187
|
const errorResult = { error: errorMessage };
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
const entry = { data: errorResult, cachedAt: Date.now() };
|
|
189
|
+
usageCache[cacheKey] = entry;
|
|
190
|
+
return entry;
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
const data = (await res.json()) as AnthropicUsageRaw;
|
|
185
194
|
// cache invalidate
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
const entry = { data, cachedAt: Date.now() };
|
|
196
|
+
usageCache[cacheKey] = entry;
|
|
197
|
+
return entry;
|
|
188
198
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { afterQuery } from "./qgrid-run-lifecycle";
|
|
4
|
+
import { type QueryOutput } from "./qgrid.types";
|
|
5
|
+
|
|
6
|
+
const { appendStepMock, aggregateStepUsageMock, finishRunMock } = vi.hoisted(() => ({
|
|
7
|
+
appendStepMock: vi.fn(),
|
|
8
|
+
aggregateStepUsageMock: vi.fn(),
|
|
9
|
+
finishRunMock: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../request-log/request-log.model", () => ({
|
|
13
|
+
RequestLogModel: {
|
|
14
|
+
appendStep: appendStepMock,
|
|
15
|
+
aggregateStepUsage: aggregateStepUsageMock,
|
|
16
|
+
finishRun: finishRunMock,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
function queryOutput(overrides: Partial<QueryOutput> = {}): QueryOutput {
|
|
21
|
+
return {
|
|
22
|
+
text: "hello",
|
|
23
|
+
content: [],
|
|
24
|
+
finishReason: "stop",
|
|
25
|
+
tokenName: "tok-A",
|
|
26
|
+
model: "gpt-5-codex",
|
|
27
|
+
usage: {
|
|
28
|
+
input_tokens: 5,
|
|
29
|
+
output_tokens: 7,
|
|
30
|
+
cache_creation_input_tokens: 0,
|
|
31
|
+
cache_read_input_tokens: 0,
|
|
32
|
+
},
|
|
33
|
+
durationMs: 120,
|
|
34
|
+
ttftMs: 39,
|
|
35
|
+
costUsd: 0.001,
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("qgrid run lifecycle TTFT", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
appendStepMock.mockReset();
|
|
43
|
+
aggregateStepUsageMock.mockReset();
|
|
44
|
+
aggregateStepUsageMock.mockResolvedValue({
|
|
45
|
+
input_tokens: 5,
|
|
46
|
+
output_tokens: 7,
|
|
47
|
+
cache_read_tokens: 0,
|
|
48
|
+
cache_creation_tokens: 0,
|
|
49
|
+
duration_ms: 120,
|
|
50
|
+
});
|
|
51
|
+
finishRunMock.mockReset();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("appends generate step ttft_ms from QueryOutput", async () => {
|
|
55
|
+
await afterQuery(10, 2, { prompt: "hi" }, queryOutput({ ttftMs: 44 }));
|
|
56
|
+
|
|
57
|
+
expect(appendStepMock).toHaveBeenCalledWith(
|
|
58
|
+
10,
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
step_index: 2,
|
|
61
|
+
type: "generate",
|
|
62
|
+
duration_ms: 120,
|
|
63
|
+
ttft_ms: 44,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("preserves generate step ttft_ms zero when no first-token timing is available", async () => {
|
|
69
|
+
await afterQuery(
|
|
70
|
+
10,
|
|
71
|
+
2,
|
|
72
|
+
{ prompt: "hi" },
|
|
73
|
+
queryOutput({ finishReason: "tool-calls", ttftMs: 0 }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(appendStepMock).toHaveBeenCalledWith(
|
|
77
|
+
10,
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
ttft_ms: 0,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
expect(finishRunMock).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not pass ttft into finishRun params because the model derives the run value", async () => {
|
|
86
|
+
await afterQuery(10, 2, { prompt: "hi" }, queryOutput({ ttftMs: 44 }));
|
|
87
|
+
|
|
88
|
+
expect(finishRunMock).toHaveBeenCalledWith(
|
|
89
|
+
10,
|
|
90
|
+
expect.not.objectContaining({
|
|
91
|
+
ttft_ms: expect.anything(),
|
|
92
|
+
ttftMs: expect.anything(),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -85,6 +85,7 @@ export async function afterQuery(
|
|
|
85
85
|
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
86
86
|
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
87
87
|
duration_ms: result.durationMs,
|
|
88
|
+
ttft_ms: result.ttftMs,
|
|
88
89
|
finish_reason: result.finishReason,
|
|
89
90
|
});
|
|
90
91
|
|
|
@@ -335,4 +335,20 @@ describe("QgridDispatcherClass", () => {
|
|
|
335
335
|
expect(result.costUsd).toBe(0.123456);
|
|
336
336
|
expect(result.usage.cache_creation_input_tokens).toBe(600);
|
|
337
337
|
});
|
|
338
|
+
|
|
339
|
+
it("provider ttftMs 를 QueryOutput.ttftMs 로 매핑하고 누락 값은 0 으로 둔다", async () => {
|
|
340
|
+
const dispatcher = new QgridDispatcherClass();
|
|
341
|
+
const generate = vi
|
|
342
|
+
.fn()
|
|
343
|
+
.mockResolvedValueOnce(providerResult({ ttftMs: 37 }))
|
|
344
|
+
.mockResolvedValueOnce(providerResult());
|
|
345
|
+
dispatcher.openaiDispatcher = { generate } as never;
|
|
346
|
+
|
|
347
|
+
await expect(
|
|
348
|
+
dispatcher.query({ prompt: "hi", model: "openai/gpt-5.5" }),
|
|
349
|
+
).resolves.toMatchObject({ ttftMs: 37 });
|
|
350
|
+
await expect(
|
|
351
|
+
dispatcher.query({ prompt: "hi", model: "openai/gpt-5.5" }),
|
|
352
|
+
).resolves.toMatchObject({ ttftMs: 0 });
|
|
353
|
+
});
|
|
338
354
|
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { QgridFrame } from "./qgrid.frame";
|
|
4
|
+
|
|
5
|
+
const { findOneMock, saveMock, requestLogSaveMock, dispatcherQueryMock } = vi.hoisted(() => ({
|
|
6
|
+
findOneMock: vi.fn(),
|
|
7
|
+
saveMock: vi.fn(),
|
|
8
|
+
requestLogSaveMock: vi.fn(),
|
|
9
|
+
dispatcherQueryMock: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../request-log/request-log.model", () => ({
|
|
13
|
+
MICRO_USD: 1_000_000,
|
|
14
|
+
RequestLogModel: {
|
|
15
|
+
save: requestLogSaveMock,
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../token/token.model", () => ({
|
|
20
|
+
TokenModel: {
|
|
21
|
+
findOne: findOneMock,
|
|
22
|
+
save: saveMock,
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("./qgrid.dispatcher", () => ({
|
|
27
|
+
QgridDispatcher: {
|
|
28
|
+
query: dispatcherQueryMock,
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const tokenEntry = {
|
|
33
|
+
id: 1,
|
|
34
|
+
created_at: new Date("2026-06-30T00:00:00.000Z"),
|
|
35
|
+
provider: "anthropic",
|
|
36
|
+
credentials: {
|
|
37
|
+
accessToken: "sk-ant-oat01-test",
|
|
38
|
+
refreshToken: "sk-ant-ort01-test",
|
|
39
|
+
expiresAt: Date.now() + 3_600_000,
|
|
40
|
+
accountUuid: "acc-1",
|
|
41
|
+
},
|
|
42
|
+
name: "tok-A",
|
|
43
|
+
active: true,
|
|
44
|
+
ord: 0,
|
|
45
|
+
quota_threshold: null,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("QgridFrame.updateToken", () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
findOneMock.mockReset();
|
|
51
|
+
saveMock.mockReset();
|
|
52
|
+
saveMock.mockResolvedValue([1]);
|
|
53
|
+
requestLogSaveMock.mockReset();
|
|
54
|
+
requestLogSaveMock.mockResolvedValue([1]);
|
|
55
|
+
dispatcherQueryMock.mockReset();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rejects quota thresholds outside TokenSaveParams bounds before saving", async () => {
|
|
59
|
+
findOneMock.mockResolvedValueOnce(tokenEntry);
|
|
60
|
+
|
|
61
|
+
await expect(QgridFrame.updateToken(1, "tok-A", 0)).rejects.toThrow(
|
|
62
|
+
"quotaThreshold must be an integer between 1 and 100, or null",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(saveMock).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("saves valid quota thresholds through the same schema used by token saves", async () => {
|
|
69
|
+
findOneMock.mockResolvedValueOnce(tokenEntry);
|
|
70
|
+
|
|
71
|
+
await expect(QgridFrame.updateToken(1, "tok-A", 80)).resolves.toEqual({ updated: true });
|
|
72
|
+
|
|
73
|
+
expect(saveMock).toHaveBeenCalledWith([
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
id: 1,
|
|
76
|
+
name: "tok-A",
|
|
77
|
+
quota_threshold: 80,
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("QgridFrame.query auto logging", () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
requestLogSaveMock.mockReset();
|
|
86
|
+
requestLogSaveMock.mockResolvedValue([1]);
|
|
87
|
+
dispatcherQueryMock.mockReset();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function queryOutput(ttftMs: number) {
|
|
91
|
+
return {
|
|
92
|
+
text: "hello",
|
|
93
|
+
content: [{ type: "text", text: "hello" }],
|
|
94
|
+
finishReason: "stop",
|
|
95
|
+
tokenName: "tok-A",
|
|
96
|
+
model: "gpt-5-codex",
|
|
97
|
+
usage: {
|
|
98
|
+
input_tokens: 5,
|
|
99
|
+
output_tokens: 7,
|
|
100
|
+
cache_creation_input_tokens: 0,
|
|
101
|
+
cache_read_input_tokens: 0,
|
|
102
|
+
},
|
|
103
|
+
durationMs: 120,
|
|
104
|
+
ttftMs,
|
|
105
|
+
costUsd: 0.001,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
it("persists auto request ttft_ms from QueryOutput", async () => {
|
|
110
|
+
dispatcherQueryMock.mockResolvedValueOnce(queryOutput(39));
|
|
111
|
+
|
|
112
|
+
await QgridFrame.query({ prompt: "hi", model: "openai/gpt-5-codex", logMode: "auto" });
|
|
113
|
+
|
|
114
|
+
expect(requestLogSaveMock).toHaveBeenCalledWith([
|
|
115
|
+
expect.objectContaining({ duration_ms: 120, ttft_ms: 39 }),
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("persists auto request ttft_ms zero when no first-token timing is available", async () => {
|
|
120
|
+
dispatcherQueryMock.mockResolvedValueOnce(queryOutput(0));
|
|
121
|
+
|
|
122
|
+
await QgridFrame.query({ prompt: "hi", model: "openai/gpt-5-codex", logMode: "auto" });
|
|
123
|
+
|
|
124
|
+
expect(requestLogSaveMock).toHaveBeenCalledWith([
|
|
125
|
+
expect.objectContaining({ ttft_ms: 0 }),
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -2,9 +2,10 @@ import crypto from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
4
|
import { type FastifyReply } from "fastify";
|
|
5
|
-
import { api, BaseFrameClass, Sonamu, stream } from "sonamu";
|
|
5
|
+
import { api, BadRequestException, BaseFrameClass, Sonamu, stream } from "sonamu";
|
|
6
6
|
import { getCacheManagerRef } from "sonamu/cache";
|
|
7
7
|
|
|
8
|
+
import { type LocalizedString } from "../../i18n/sd.generated";
|
|
8
9
|
import {
|
|
9
10
|
getAccessToken,
|
|
10
11
|
getExpiresAt,
|
|
@@ -13,7 +14,11 @@ import {
|
|
|
13
14
|
import { MICRO_USD, RequestLogModel } from "../request-log/request-log.model";
|
|
14
15
|
import { type TokenSubsetA } from "../sonamu.generated";
|
|
15
16
|
import { TokenModel } from "../token/token.model";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
TokenCredentials,
|
|
19
|
+
TokenSaveParams,
|
|
20
|
+
type TokenSaveParams as TokenSaveParamsType,
|
|
21
|
+
} from "../token/token.types";
|
|
17
22
|
import {
|
|
18
23
|
type AnthropicUsageRaw,
|
|
19
24
|
buildAuthUrl,
|
|
@@ -49,6 +54,10 @@ type PendingOAuth = { codeVerifier: string; name: string; redirectUri: string };
|
|
|
49
54
|
const OAUTH_STATE_PREFIX = "oauth:state:";
|
|
50
55
|
const OAUTH_STATE_TTL = "5m";
|
|
51
56
|
|
|
57
|
+
function unixSecondsToIso(seconds: number | null | undefined): string | null {
|
|
58
|
+
return typeof seconds === "number" ? new Date(seconds * 1000).toISOString() : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
async function setOAuthState(state: string, data: PendingOAuth): Promise<void> {
|
|
53
62
|
const cache = getCacheManagerRef();
|
|
54
63
|
if (!cache) throw new Error("CacheManager not initialized");
|
|
@@ -142,6 +151,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
142
151
|
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
143
152
|
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
144
153
|
duration_ms: result.durationMs,
|
|
154
|
+
ttft_ms: result.ttftMs,
|
|
145
155
|
cost_usd: result.costUsd !== null ? Math.round(result.costUsd * MICRO_USD) : null,
|
|
146
156
|
effort: args.effort ?? null,
|
|
147
157
|
// malformed history가 와도 성공한 턴(특히 stream sse.end())을 깨지 않도록 방어.
|
|
@@ -296,6 +306,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
296
306
|
cache_read_tokens: input.cacheReadTokens,
|
|
297
307
|
cache_creation_tokens: input.cacheCreationTokens,
|
|
298
308
|
duration_ms: input.durationMs,
|
|
309
|
+
ttft_ms: input.ttftMs ?? null,
|
|
299
310
|
finish_reason: input.finishReason,
|
|
300
311
|
reasoning_text: input.reasoningText,
|
|
301
312
|
reasoning_tokens: input.reasoningTokens,
|
|
@@ -359,18 +370,33 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
359
370
|
}
|
|
360
371
|
|
|
361
372
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
362
|
-
async updateToken(
|
|
373
|
+
async updateToken(
|
|
374
|
+
id: number,
|
|
375
|
+
name?: string,
|
|
376
|
+
quotaThreshold?: number | null,
|
|
377
|
+
): Promise<{ updated: boolean }> {
|
|
363
378
|
const entry = await TokenModel.findOne("A", { id });
|
|
364
379
|
if (!entry) return { updated: false };
|
|
365
380
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
381
|
+
const patch: TokenSaveParamsType = {
|
|
382
|
+
id: entry.id,
|
|
383
|
+
provider: entry.provider,
|
|
384
|
+
credentials: entry.credentials,
|
|
385
|
+
name: name !== undefined ? name : entry.name,
|
|
386
|
+
};
|
|
387
|
+
if (quotaThreshold !== undefined) {
|
|
388
|
+
patch.quota_threshold = quotaThreshold;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const parsed = TokenSaveParams.safeParse(patch);
|
|
392
|
+
if (!parsed.success) {
|
|
393
|
+
throw new BadRequestException(
|
|
394
|
+
"quotaThreshold must be an integer between 1 and 100, or null" as LocalizedString,
|
|
395
|
+
{ zodError: parsed.error },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await TokenModel.save([parsed.data]);
|
|
374
400
|
return { updated: true };
|
|
375
401
|
}
|
|
376
402
|
|
|
@@ -513,25 +539,20 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
513
539
|
|
|
514
540
|
if (entry.provider === "openai") {
|
|
515
541
|
try {
|
|
516
|
-
const raw =
|
|
517
|
-
|
|
518
|
-
primary?: { usedPercent: number; windowDurationMins: number; resetsAt: number };
|
|
519
|
-
secondary?: { usedPercent: number; windowDurationMins: number; resetsAt: number };
|
|
520
|
-
};
|
|
521
|
-
};
|
|
522
|
-
const rl = raw?.rateLimits;
|
|
542
|
+
const raw = await QgridDispatcher.openaiDispatcher?.getRateLimitsByTokenId(entry.id);
|
|
543
|
+
const rl = raw?.data.rateLimits;
|
|
523
544
|
return {
|
|
524
545
|
provider: "openai",
|
|
525
546
|
fiveHour: rl?.primary
|
|
526
547
|
? {
|
|
527
548
|
utilization: rl.primary.usedPercent,
|
|
528
|
-
resetsAt:
|
|
549
|
+
resetsAt: unixSecondsToIso(rl.primary.resetsAt),
|
|
529
550
|
}
|
|
530
551
|
: null,
|
|
531
552
|
sevenDay: rl?.secondary
|
|
532
553
|
? {
|
|
533
554
|
utilization: rl.secondary.usedPercent,
|
|
534
|
-
resetsAt:
|
|
555
|
+
resetsAt: unixSecondsToIso(rl.secondary.resetsAt),
|
|
535
556
|
}
|
|
536
557
|
: null,
|
|
537
558
|
};
|
|
@@ -102,6 +102,7 @@ export const QueryOutput = z.object({
|
|
|
102
102
|
cache_read_input_tokens: z.number(),
|
|
103
103
|
}),
|
|
104
104
|
durationMs: z.number(),
|
|
105
|
+
ttftMs: z.number(),
|
|
105
106
|
costUsd: z.number(),
|
|
106
107
|
runContext: QgridRunContext.optional(),
|
|
107
108
|
});
|
|
@@ -128,6 +129,7 @@ export const StreamEvents = z.object({
|
|
|
128
129
|
cache_read_input_tokens: z.number(),
|
|
129
130
|
}),
|
|
130
131
|
durationMs: z.number(),
|
|
132
|
+
ttftMs: z.number(),
|
|
131
133
|
costUsd: z.number(),
|
|
132
134
|
content: z.array(QgridContent),
|
|
133
135
|
runContext: QgridRunContext.optional(),
|
|
@@ -157,6 +159,7 @@ export const AppendStepInput = z.object({
|
|
|
157
159
|
cacheReadTokens: z.number().optional(),
|
|
158
160
|
cacheCreationTokens: z.number().optional(),
|
|
159
161
|
durationMs: z.number().optional(),
|
|
162
|
+
ttftMs: z.number().nullable().optional(),
|
|
160
163
|
finishReason: z.string().optional(),
|
|
161
164
|
reasoningText: z.string().optional(),
|
|
162
165
|
reasoningTokens: z.number().optional(),
|
|
@@ -250,6 +253,9 @@ export type HealthResponse = z.infer<typeof HealthResponse>;
|
|
|
250
253
|
export class QuotaError extends Error {
|
|
251
254
|
readonly code = "QUOTA_EXHAUSTED" as const;
|
|
252
255
|
}
|
|
256
|
+
export class QuotaThresholdExceededError extends Error {
|
|
257
|
+
readonly code = "QUOTA_THRESHOLD_EXCEEDED" as const;
|
|
258
|
+
}
|
|
253
259
|
export class TimeoutError extends Error {
|
|
254
260
|
readonly code = "TIMEOUT" as const;
|
|
255
261
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { TokenSubscriber } from "./token-subscriber";
|
|
4
|
+
|
|
5
|
+
const { findOneMock, findActiveMock } = vi.hoisted(() => ({
|
|
6
|
+
findOneMock: vi.fn(),
|
|
7
|
+
findActiveMock: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("../token/token.model", () => ({
|
|
11
|
+
TokenModel: {
|
|
12
|
+
findOne: findOneMock,
|
|
13
|
+
findActive: findActiveMock,
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function openaiToken(active = true) {
|
|
18
|
+
return {
|
|
19
|
+
id: 1,
|
|
20
|
+
provider: "openai",
|
|
21
|
+
active,
|
|
22
|
+
name: "tok-A",
|
|
23
|
+
credentials: { accessToken: "access", accountId: "account" },
|
|
24
|
+
quota_threshold: 80,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function subscriberWith(openaiDispatcher: Record<string, unknown>) {
|
|
29
|
+
return new TokenSubscriber(
|
|
30
|
+
{} as never,
|
|
31
|
+
{
|
|
32
|
+
removeCache: vi.fn(),
|
|
33
|
+
upsertCache: vi.fn(),
|
|
34
|
+
replaceCache: vi.fn(),
|
|
35
|
+
openaiDispatcher,
|
|
36
|
+
anthropicDispatcher: null,
|
|
37
|
+
} as never,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function flushPromises(): Promise<void> {
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("TokenSubscriber OpenAI notifications", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
findOneMock.mockReset();
|
|
48
|
+
findActiveMock.mockReset();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("updates OpenAI token metadata before activating workers", async () => {
|
|
52
|
+
const calls: string[] = [];
|
|
53
|
+
const openaiDispatcher = {
|
|
54
|
+
onTokenUpdated: vi.fn(async () => {
|
|
55
|
+
calls.push("updated");
|
|
56
|
+
}),
|
|
57
|
+
onTokenActivated: vi.fn(() => {
|
|
58
|
+
calls.push("activated");
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
const subscriber = subscriberWith(openaiDispatcher);
|
|
62
|
+
findOneMock.mockResolvedValueOnce(openaiToken(true));
|
|
63
|
+
|
|
64
|
+
await subscriber.handleNotification(JSON.stringify({ op: "UPDATE", id: 1 }));
|
|
65
|
+
await flushPromises();
|
|
66
|
+
|
|
67
|
+
expect(calls).toEqual(["updated", "activated"]);
|
|
68
|
+
expect(openaiDispatcher.onTokenUpdated).toHaveBeenCalledWith(
|
|
69
|
+
1,
|
|
70
|
+
"tok-A",
|
|
71
|
+
{ accessToken: "access", accountId: "account" },
|
|
72
|
+
80,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not spawn workers for inactive OpenAI inserts", async () => {
|
|
77
|
+
const openaiDispatcher = {
|
|
78
|
+
onTokenAdded: vi.fn(async () => {}),
|
|
79
|
+
onTokenDeactivated: vi.fn(),
|
|
80
|
+
};
|
|
81
|
+
const subscriber = subscriberWith(openaiDispatcher);
|
|
82
|
+
findOneMock.mockResolvedValueOnce(openaiToken(false));
|
|
83
|
+
|
|
84
|
+
await subscriber.handleNotification(JSON.stringify({ op: "INSERT", id: 1 }));
|
|
85
|
+
|
|
86
|
+
expect(openaiDispatcher.onTokenAdded).not.toHaveBeenCalled();
|
|
87
|
+
expect(openaiDispatcher.onTokenDeactivated).toHaveBeenCalledWith(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -177,17 +177,20 @@ export class TokenSubscriber {
|
|
|
177
177
|
|
|
178
178
|
if (row.provider === "openai") {
|
|
179
179
|
const creds = row.credentials as Record<string, unknown>;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
const openaiDispatcher = this.dispatcher.openaiDispatcher;
|
|
181
|
+
if (payload.op === "INSERT" && row.active) {
|
|
182
|
+
openaiDispatcher
|
|
183
|
+
?.onTokenAdded(payload.id, row.name, creds as OpenAICredentials, row.quota_threshold)
|
|
183
184
|
.catch((e) => logger.warn(`openai worker spawn failed: ${(e as Error).message}`));
|
|
184
185
|
} else if (row.active) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
if (openaiDispatcher) {
|
|
187
|
+
openaiDispatcher
|
|
188
|
+
.onTokenUpdated(payload.id, row.name, creds as OpenAICredentials, row.quota_threshold)
|
|
189
|
+
.then(() => openaiDispatcher.onTokenActivated(payload.id))
|
|
190
|
+
.catch((e) => logger.warn(`openai worker update failed: ${(e as Error).message}`));
|
|
191
|
+
}
|
|
189
192
|
} else {
|
|
190
|
-
|
|
193
|
+
openaiDispatcher?.onTokenDeactivated(payload.id);
|
|
191
194
|
}
|
|
192
195
|
} else if (row.provider === "anthropic") {
|
|
193
196
|
// anthropic 토큰 이벤트는 동기(void). worker 가 없고 풀(Map)만 관리하므로 단순:
|
|
@@ -195,9 +198,19 @@ export class TokenSubscriber {
|
|
|
195
198
|
// (별도 activate/deactivate 콜백이 없어 active 토글을 add/remove 로 매핑.)
|
|
196
199
|
const creds = row.credentials as AnthropicCredentials;
|
|
197
200
|
if (payload.op === "INSERT" && row.active) {
|
|
198
|
-
this.dispatcher.anthropicDispatcher?.onTokenAdded(
|
|
201
|
+
this.dispatcher.anthropicDispatcher?.onTokenAdded(
|
|
202
|
+
payload.id,
|
|
203
|
+
row.name,
|
|
204
|
+
creds,
|
|
205
|
+
row.quota_threshold,
|
|
206
|
+
);
|
|
199
207
|
} else if (row.active) {
|
|
200
|
-
this.dispatcher.anthropicDispatcher?.onTokenUpdated(
|
|
208
|
+
this.dispatcher.anthropicDispatcher?.onTokenUpdated(
|
|
209
|
+
payload.id,
|
|
210
|
+
row.name,
|
|
211
|
+
creds,
|
|
212
|
+
row.quota_threshold,
|
|
213
|
+
);
|
|
201
214
|
} else {
|
|
202
215
|
// inactive 면 INSERT 든 UPDATE 든 풀에 넣지 않는다.
|
|
203
216
|
this.dispatcher.anthropicDispatcher?.onTokenRemoved(payload.id);
|
|
@@ -213,8 +226,24 @@ export class TokenSubscriber {
|
|
|
213
226
|
// (rows 는 active 만 — inactive/삭제된 토큰은 여기 없으므로 replaceTokens 가 풀에서 제거한다.)
|
|
214
227
|
const anthropicRows = rows
|
|
215
228
|
.filter((r) => r.provider === "anthropic")
|
|
216
|
-
.map((r) => ({
|
|
229
|
+
.map((r) => ({
|
|
230
|
+
id: r.id,
|
|
231
|
+
name: r.name,
|
|
232
|
+
credentials: r.credentials as AnthropicCredentials,
|
|
233
|
+
quotaThreshold: r.quota_threshold,
|
|
234
|
+
}));
|
|
217
235
|
this.dispatcher.anthropicDispatcher?.replaceTokens(anthropicRows);
|
|
236
|
+
const openaiRows = rows
|
|
237
|
+
.filter((r) => r.provider === "openai")
|
|
238
|
+
.map((r) => ({
|
|
239
|
+
id: r.id,
|
|
240
|
+
name: r.name,
|
|
241
|
+
credentials: r.credentials as OpenAICredentials,
|
|
242
|
+
quotaThreshold: r.quota_threshold,
|
|
243
|
+
}));
|
|
244
|
+
await this.dispatcher.openaiDispatcher
|
|
245
|
+
?.replaceTokens(openaiRows)
|
|
246
|
+
.catch((e) => logger.warn(`openai reconcile failed: ${(e as Error).message}`));
|
|
218
247
|
this.lastReconcileAt = new Date();
|
|
219
248
|
}
|
|
220
249
|
}
|