@cartanova/qgrid-cli 2.3.5 → 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 +8 -3
- 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.ts +7 -2
- 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
|
@@ -39,7 +39,8 @@ const SETUP_SQL = `
|
|
|
39
39
|
OLD.active IS DISTINCT FROM NEW.active OR
|
|
40
40
|
OLD.credentials IS DISTINCT FROM NEW.credentials OR
|
|
41
41
|
OLD.provider IS DISTINCT FROM NEW.provider OR
|
|
42
|
-
OLD.name IS DISTINCT FROM NEW.name
|
|
42
|
+
OLD.name IS DISTINCT FROM NEW.name OR
|
|
43
|
+
OLD.quota_threshold IS DISTINCT FROM NEW.quota_threshold
|
|
43
44
|
)
|
|
44
45
|
EXECUTE FUNCTION public.tokens_notify();
|
|
45
46
|
`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { buildToolCallSchema } from "./tool-emulation";
|
|
4
3
|
import { type QgridTool } from "./qgrid.types";
|
|
4
|
+
import { buildToolCallSchema } from "./tool-emulation";
|
|
5
5
|
|
|
6
6
|
describe("buildToolCallSchema", () => {
|
|
7
7
|
it("documents that emulated tools must be requested through StructuredOutput", () => {
|
|
@@ -36,9 +36,13 @@ describe("buildToolCallSchema", () => {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
expect(schema.description).toContain("StructuredOutput");
|
|
39
|
-
expect(schema.description).toContain(
|
|
39
|
+
expect(schema.description).toContain(
|
|
40
|
+
"Do not invoke listed client tools as native Claude Code tools",
|
|
41
|
+
);
|
|
40
42
|
expect(schema.properties?.action?.description).toContain("tool_call");
|
|
41
|
-
expect(schema.properties?.toolCalls?.description).toContain(
|
|
43
|
+
expect(schema.properties?.toolCalls?.description).toContain(
|
|
44
|
+
"Do not call these tools as native",
|
|
45
|
+
);
|
|
42
46
|
expect(
|
|
43
47
|
schema.properties?.toolCalls?.anyOf?.[0]?.items?.properties?.toolName?.description,
|
|
44
48
|
).toContain("getWeather");
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"dbDefault": "0"
|
|
43
43
|
},
|
|
44
44
|
{ "name": "duration_ms", "type": "integer", "desc": "응답 소요시간(ms)", "dbDefault": "0" },
|
|
45
|
+
{ "name": "ttft_ms", "type": "integer", "desc": "first gen TTFT(ms)", "dbDefault": "0" },
|
|
45
46
|
{ "name": "cost_usd", "type": "integer", "nullable": true, "desc": "API 비용" },
|
|
46
47
|
{ "name": "effort", "type": "string", "length": 10, "nullable": true, "desc": "effort level" },
|
|
47
48
|
{
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
"cache_read_tokens",
|
|
92
93
|
"cache_creation_tokens",
|
|
93
94
|
"duration_ms",
|
|
95
|
+
"ttft_ms",
|
|
94
96
|
"cost_usd",
|
|
95
97
|
"effort",
|
|
96
98
|
"history",
|
|
@@ -111,6 +113,7 @@
|
|
|
111
113
|
"cache_read_tokens",
|
|
112
114
|
"cache_creation_tokens",
|
|
113
115
|
"duration_ms",
|
|
116
|
+
"ttft_ms",
|
|
114
117
|
"cost_usd",
|
|
115
118
|
"effort",
|
|
116
119
|
"status",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { RequestLogModel } from "./request-log.model";
|
|
4
|
+
|
|
5
|
+
function mockQueryBuilder(rows: unknown[]) {
|
|
6
|
+
const chain = {
|
|
7
|
+
select: vi.fn(() => chain),
|
|
8
|
+
where: vi.fn(() => chain),
|
|
9
|
+
whereNotNull: vi.fn(() => chain),
|
|
10
|
+
orderBy: vi.fn(() => chain),
|
|
11
|
+
limit: vi.fn(async () => rows),
|
|
12
|
+
};
|
|
13
|
+
return chain;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("RequestLogModel TTFT", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("reads the first non-null generate-step ttft", async () => {
|
|
22
|
+
const chain = mockQueryBuilder([{ ttft_ms: 42 }]);
|
|
23
|
+
const db = vi.fn(() => chain);
|
|
24
|
+
vi.spyOn(RequestLogModel as unknown as { getDB: () => typeof db }, "getDB").mockReturnValue(
|
|
25
|
+
db,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await expect(RequestLogModel.firstGenerateStepTtft(7)).resolves.toBe(42);
|
|
29
|
+
|
|
30
|
+
expect(db).toHaveBeenCalledWith("request_log_steps");
|
|
31
|
+
expect(chain.where).toHaveBeenCalledWith("request_log_id", 7);
|
|
32
|
+
expect(chain.where).toHaveBeenCalledWith("type", "generate");
|
|
33
|
+
expect(chain.whereNotNull).toHaveBeenCalledWith("ttft_ms");
|
|
34
|
+
expect(chain.orderBy).toHaveBeenNthCalledWith(1, "step_index", "asc");
|
|
35
|
+
expect(chain.orderBy).toHaveBeenNthCalledWith(2, "id", "asc");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns null when no generate step has ttft", async () => {
|
|
39
|
+
const db = vi.fn(() => mockQueryBuilder([]));
|
|
40
|
+
vi.spyOn(RequestLogModel as unknown as { getDB: () => typeof db }, "getDB").mockReturnValue(
|
|
41
|
+
db,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await expect(RequestLogModel.firstGenerateStepTtft(7)).resolves.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("stores server-derived ttft_ms during finishRun", async () => {
|
|
48
|
+
const ubRegister = vi.fn();
|
|
49
|
+
const transaction = vi.fn(async (cb: (trx: { ubUpsert: () => Promise<number[]> }) => void) =>
|
|
50
|
+
cb({ ubUpsert: vi.fn(async () => [7]) }),
|
|
51
|
+
);
|
|
52
|
+
vi.spyOn(RequestLogModel, "firstGenerateStepTtft").mockResolvedValue(55);
|
|
53
|
+
vi.spyOn(
|
|
54
|
+
RequestLogModel as unknown as { getPuri: () => { ubRegister: typeof ubRegister; transaction: typeof transaction } },
|
|
55
|
+
"getPuri",
|
|
56
|
+
).mockReturnValue({ ubRegister, transaction });
|
|
57
|
+
|
|
58
|
+
await RequestLogModel.finishRun(7, { status: "error", error_message: "boom" });
|
|
59
|
+
|
|
60
|
+
expect(ubRegister).toHaveBeenCalledWith(
|
|
61
|
+
"request_logs",
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
id: 7,
|
|
64
|
+
status: "error",
|
|
65
|
+
error_message: "boom",
|
|
66
|
+
ttft_ms: 55,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("stores zero during finishRun when no generate step has ttft", async () => {
|
|
72
|
+
const ubRegister = vi.fn();
|
|
73
|
+
const transaction = vi.fn(async (cb: (trx: { ubUpsert: () => Promise<number[]> }) => void) =>
|
|
74
|
+
cb({ ubUpsert: vi.fn(async () => [7]) }),
|
|
75
|
+
);
|
|
76
|
+
vi.spyOn(RequestLogModel, "firstGenerateStepTtft").mockResolvedValue(null);
|
|
77
|
+
vi.spyOn(
|
|
78
|
+
RequestLogModel as unknown as { getPuri: () => { ubRegister: typeof ubRegister; transaction: typeof transaction } },
|
|
79
|
+
"getPuri",
|
|
80
|
+
).mockReturnValue({ ubRegister, transaction });
|
|
81
|
+
|
|
82
|
+
await RequestLogModel.finishRun(7, { status: "error", error_message: "boom" });
|
|
83
|
+
|
|
84
|
+
expect(ubRegister).toHaveBeenCalledWith(
|
|
85
|
+
"request_logs",
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
id: 7,
|
|
88
|
+
ttft_ms: 0,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -46,15 +46,15 @@ function normalizedUsageForCost(row: RequestLogUsageRow): {
|
|
|
46
46
|
cachedInputTokens: number;
|
|
47
47
|
cacheCreationInputTokens: number;
|
|
48
48
|
} {
|
|
49
|
-
const cacheRead =
|
|
50
|
-
const cacheCreation =
|
|
51
|
-
const storedInput =
|
|
49
|
+
const cacheRead = row.cache_read_tokens;
|
|
50
|
+
const cacheCreation = row.cache_creation_tokens;
|
|
51
|
+
const storedInput = row.input_tokens;
|
|
52
52
|
const legacyAnthropicSplitInput =
|
|
53
53
|
isAnthropicUsageRow(row) && storedInput < cacheRead + cacheCreation;
|
|
54
54
|
return {
|
|
55
55
|
model: row.model_name ? canonicalModelName(row.model_name) : null,
|
|
56
56
|
inputTokens: legacyAnthropicSplitInput ? storedInput + cacheRead + cacheCreation : storedInput,
|
|
57
|
-
outputTokens:
|
|
57
|
+
outputTokens: row.output_tokens,
|
|
58
58
|
cachedInputTokens: cacheRead,
|
|
59
59
|
cacheCreationInputTokens: cacheCreation,
|
|
60
60
|
};
|
|
@@ -62,7 +62,7 @@ function normalizedUsageForCost(row: RequestLogUsageRow): {
|
|
|
62
62
|
|
|
63
63
|
function normalizeLegacyAnthropicRow<T extends RequestLogUsageRow>(row: T): T {
|
|
64
64
|
const usage = normalizedUsageForCost(row);
|
|
65
|
-
if (usage.inputTokens ===
|
|
65
|
+
if (usage.inputTokens === row.input_tokens) return row;
|
|
66
66
|
const normalized = { ...row, input_tokens: usage.inputTokens };
|
|
67
67
|
if (usage.model) {
|
|
68
68
|
normalized.cost_usd = Math.round(calculateCostUsd(usage.model, usage) * MICRO_USD);
|
|
@@ -280,6 +280,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
280
280
|
if (params[key] !== undefined) update[key] = params[key];
|
|
281
281
|
}
|
|
282
282
|
if (params.history !== undefined) update.history = params.history;
|
|
283
|
+
update.ttft_ms = (await this.firstGenerateStepTtft(requestLogId)) ?? 0;
|
|
283
284
|
|
|
284
285
|
if (params.input_tokens !== undefined && params.output_tokens !== undefined) {
|
|
285
286
|
const db = this.getDB("r");
|
|
@@ -315,6 +316,19 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
315
316
|
});
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
async firstGenerateStepTtft(requestLogId: number): Promise<number | null> {
|
|
320
|
+
const db = this.getDB("r");
|
|
321
|
+
const [row] = await db("request_log_steps")
|
|
322
|
+
.select("ttft_ms")
|
|
323
|
+
.where("request_log_id", requestLogId)
|
|
324
|
+
.where("type", "generate")
|
|
325
|
+
.whereNotNull("ttft_ms")
|
|
326
|
+
.orderBy("step_index", "asc")
|
|
327
|
+
.orderBy("id", "asc")
|
|
328
|
+
.limit(1);
|
|
329
|
+
return row?.ttft_ms === null || row?.ttft_ms === undefined ? null : Number(row.ttft_ms);
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
async aggregateStepUsage(requestLogId: number): Promise<{
|
|
319
333
|
input_tokens: number;
|
|
320
334
|
output_tokens: number;
|
|
@@ -410,7 +424,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
410
424
|
|
|
411
425
|
return rows.reduce((sum, row) => {
|
|
412
426
|
const usage = normalizedUsageForCost(row);
|
|
413
|
-
if (!usage.model) return sum + Math.max(
|
|
427
|
+
if (!usage.model) return sum + Math.max(row.cost_usd ?? 0, 0) / MICRO_USD;
|
|
414
428
|
return (
|
|
415
429
|
sum +
|
|
416
430
|
calculateCostUsd(usage.model, {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"desc": "캐시 생성 토큰"
|
|
33
33
|
},
|
|
34
34
|
{ "name": "duration_ms", "type": "integer", "nullable": true, "desc": "소요시간(ms)" },
|
|
35
|
+
{ "name": "ttft_ms", "type": "integer", "nullable": true, "desc": "Gen TTFT(ms)" },
|
|
35
36
|
{
|
|
36
37
|
"name": "finish_reason",
|
|
37
38
|
"type": "string",
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"cache_read_tokens",
|
|
100
101
|
"cache_creation_tokens",
|
|
101
102
|
"duration_ms",
|
|
103
|
+
"ttft_ms",
|
|
102
104
|
"finish_reason",
|
|
103
105
|
"reasoning_text",
|
|
104
106
|
"reasoning_tokens",
|
|
@@ -120,6 +122,7 @@
|
|
|
120
122
|
"cache_read_tokens",
|
|
121
123
|
"cache_creation_tokens",
|
|
122
124
|
"duration_ms",
|
|
125
|
+
"ttft_ms",
|
|
123
126
|
"finish_reason",
|
|
124
127
|
"reasoning_tokens",
|
|
125
128
|
"tool_call_index",
|
|
@@ -39,7 +39,8 @@ Content-Type: application/json
|
|
|
39
39
|
},
|
|
40
40
|
"name": "NAME",
|
|
41
41
|
"active": false,
|
|
42
|
-
"ord": 0
|
|
42
|
+
"ord": 0,
|
|
43
|
+
"quota_threshold": null
|
|
43
44
|
}
|
|
44
45
|
]
|
|
45
46
|
}
|
|
@@ -106,6 +107,7 @@ Content-Type: application/json
|
|
|
106
107
|
"cache_read_tokens": null,
|
|
107
108
|
"cache_creation_tokens": null,
|
|
108
109
|
"duration_ms": null,
|
|
110
|
+
"ttft_ms": null,
|
|
109
111
|
"finish_reason": null,
|
|
110
112
|
"reasoning_text": null,
|
|
111
113
|
"reasoning_tokens": null,
|
|
@@ -178,6 +180,7 @@ Content-Type: application/json
|
|
|
178
180
|
"cache_read_tokens": 0,
|
|
179
181
|
"cache_creation_tokens": 0,
|
|
180
182
|
"duration_ms": 0,
|
|
183
|
+
"ttft_ms": 0,
|
|
181
184
|
"cost_usd": null,
|
|
182
185
|
"effort": null,
|
|
183
186
|
"history": null,
|
|
@@ -330,6 +333,7 @@ Content-Type: application/json
|
|
|
330
333
|
"cacheReadTokens": 0,
|
|
331
334
|
"cacheCreationTokens": 0,
|
|
332
335
|
"durationMs": 0,
|
|
336
|
+
"ttftMs": null,
|
|
333
337
|
"finishReason": "FINISHREASON",
|
|
334
338
|
"reasoningText": "REASONINGTEXT",
|
|
335
339
|
"reasoningTokens": 0,
|
|
@@ -405,7 +409,8 @@ Content-Type: application/json
|
|
|
405
409
|
|
|
406
410
|
{
|
|
407
411
|
"id": 0,
|
|
408
|
-
"name": "NAME"
|
|
412
|
+
"name": "NAME",
|
|
413
|
+
"quotaThreshold": null
|
|
409
414
|
}
|
|
410
415
|
|
|
411
416
|
###
|
|
@@ -31,6 +31,7 @@ export const requestLogSubsetQueries = {
|
|
|
31
31
|
cache_read_tokens: "request_logs.cache_read_tokens",
|
|
32
32
|
cache_creation_tokens: "request_logs.cache_creation_tokens",
|
|
33
33
|
duration_ms: "request_logs.duration_ms",
|
|
34
|
+
ttft_ms: "request_logs.ttft_ms",
|
|
34
35
|
cost_usd: "request_logs.cost_usd",
|
|
35
36
|
effort: "request_logs.effort",
|
|
36
37
|
history: "request_logs.history",
|
|
@@ -51,6 +52,7 @@ export const requestLogSubsetQueries = {
|
|
|
51
52
|
cache_read_tokens: "request_logs.cache_read_tokens",
|
|
52
53
|
cache_creation_tokens: "request_logs.cache_creation_tokens",
|
|
53
54
|
duration_ms: "request_logs.duration_ms",
|
|
55
|
+
ttft_ms: "request_logs.ttft_ms",
|
|
54
56
|
cost_usd: "request_logs.cost_usd",
|
|
55
57
|
effort: "request_logs.effort",
|
|
56
58
|
status: "request_logs.status",
|
|
@@ -78,6 +80,7 @@ export const requestLogStepSubsetQueries = {
|
|
|
78
80
|
cache_read_tokens: "request_log_steps.cache_read_tokens",
|
|
79
81
|
cache_creation_tokens: "request_log_steps.cache_creation_tokens",
|
|
80
82
|
duration_ms: "request_log_steps.duration_ms",
|
|
83
|
+
ttft_ms: "request_log_steps.ttft_ms",
|
|
81
84
|
finish_reason: "request_log_steps.finish_reason",
|
|
82
85
|
reasoning_text: "request_log_steps.reasoning_text",
|
|
83
86
|
reasoning_tokens: "request_log_steps.reasoning_tokens",
|
|
@@ -101,6 +104,7 @@ export const requestLogStepSubsetQueries = {
|
|
|
101
104
|
cache_read_tokens: "request_log_steps.cache_read_tokens",
|
|
102
105
|
cache_creation_tokens: "request_log_steps.cache_creation_tokens",
|
|
103
106
|
duration_ms: "request_log_steps.duration_ms",
|
|
107
|
+
ttft_ms: "request_log_steps.ttft_ms",
|
|
104
108
|
finish_reason: "request_log_steps.finish_reason",
|
|
105
109
|
reasoning_tokens: "request_log_steps.reasoning_tokens",
|
|
106
110
|
tool_call_index: "request_log_steps.tool_call_index",
|
|
@@ -129,6 +133,7 @@ export const tokenSubsetQueries = {
|
|
|
129
133
|
name: "tokens.name",
|
|
130
134
|
active: "tokens.active",
|
|
131
135
|
ord: "tokens.ord",
|
|
136
|
+
quota_threshold: "tokens.quota_threshold",
|
|
132
137
|
});
|
|
133
138
|
},
|
|
134
139
|
};
|
|
@@ -100,6 +100,7 @@ export const RequestLogBaseSchema = z.object({
|
|
|
100
100
|
cache_read_tokens: z.int(),
|
|
101
101
|
cache_creation_tokens: z.int(),
|
|
102
102
|
duration_ms: z.int(),
|
|
103
|
+
ttft_ms: z.int(),
|
|
103
104
|
cost_usd: z.int().nullable(),
|
|
104
105
|
effort: z.string().max(10).nullable(),
|
|
105
106
|
history: HistoryItems.nullable(),
|
|
@@ -121,6 +122,7 @@ export type RequestLogBaseSchema = z.infer<typeof RequestLogBaseSchema> & {
|
|
|
121
122
|
"cache_read_tokens",
|
|
122
123
|
"cache_creation_tokens",
|
|
123
124
|
"duration_ms",
|
|
125
|
+
"ttft_ms",
|
|
124
126
|
"cost_usd",
|
|
125
127
|
"effort",
|
|
126
128
|
"history",
|
|
@@ -143,6 +145,7 @@ export const RequestLogStepBaseSchema = z.object({
|
|
|
143
145
|
cache_read_tokens: z.int().nullable(),
|
|
144
146
|
cache_creation_tokens: z.int().nullable(),
|
|
145
147
|
duration_ms: z.int().nullable(),
|
|
148
|
+
ttft_ms: z.int().nullable(),
|
|
146
149
|
finish_reason: z.string().max(20).nullable(),
|
|
147
150
|
reasoning_text: z.string().nullable(),
|
|
148
151
|
reasoning_tokens: z.int().nullable(),
|
|
@@ -162,6 +165,7 @@ export type RequestLogStepBaseSchema = z.infer<typeof RequestLogStepBaseSchema>
|
|
|
162
165
|
"cache_read_tokens",
|
|
163
166
|
"cache_creation_tokens",
|
|
164
167
|
"duration_ms",
|
|
168
|
+
"ttft_ms",
|
|
165
169
|
"finish_reason",
|
|
166
170
|
"reasoning_text",
|
|
167
171
|
"reasoning_tokens",
|
|
@@ -185,9 +189,10 @@ export const TokenBaseSchema = z.object({
|
|
|
185
189
|
name: z.string(),
|
|
186
190
|
active: z.boolean(),
|
|
187
191
|
ord: z.int(),
|
|
192
|
+
quota_threshold: z.int().nullable(),
|
|
188
193
|
});
|
|
189
194
|
export type TokenBaseSchema = z.infer<typeof TokenBaseSchema> & {
|
|
190
|
-
readonly __hasDefault__: readonly ["created_at", "active", "ord", "id"];
|
|
195
|
+
readonly __hasDefault__: readonly ["created_at", "active", "ord", "quota_threshold", "id"];
|
|
191
196
|
};
|
|
192
197
|
|
|
193
198
|
// BaseListParams: RequestLog
|
|
@@ -254,6 +259,7 @@ export const RequestLogSubsetA = z.object({
|
|
|
254
259
|
cache_read_tokens: z.int(),
|
|
255
260
|
cache_creation_tokens: z.int(),
|
|
256
261
|
duration_ms: z.int(),
|
|
262
|
+
ttft_ms: z.int(),
|
|
257
263
|
cost_usd: z.int().nullable(),
|
|
258
264
|
effort: z.string().max(10).nullable(),
|
|
259
265
|
history: HistoryItems.nullable(),
|
|
@@ -273,6 +279,7 @@ export const RequestLogSubsetP = z.object({
|
|
|
273
279
|
cache_read_tokens: z.int(),
|
|
274
280
|
cache_creation_tokens: z.int(),
|
|
275
281
|
duration_ms: z.int(),
|
|
282
|
+
ttft_ms: z.int(),
|
|
276
283
|
cost_usd: z.int().nullable(),
|
|
277
284
|
effort: z.string().max(10).nullable(),
|
|
278
285
|
status: RequestLogStatus,
|
|
@@ -297,6 +304,7 @@ export const RequestLogStepSubsetA = z.object({
|
|
|
297
304
|
cache_read_tokens: z.int().nullable(),
|
|
298
305
|
cache_creation_tokens: z.int().nullable(),
|
|
299
306
|
duration_ms: z.int().nullable(),
|
|
307
|
+
ttft_ms: z.int().nullable(),
|
|
300
308
|
finish_reason: z.string().max(20).nullable(),
|
|
301
309
|
reasoning_text: z.string().nullable(),
|
|
302
310
|
reasoning_tokens: z.int().nullable(),
|
|
@@ -319,6 +327,7 @@ export const RequestLogStepSubsetT = z.object({
|
|
|
319
327
|
cache_read_tokens: z.int().nullable(),
|
|
320
328
|
cache_creation_tokens: z.int().nullable(),
|
|
321
329
|
duration_ms: z.int().nullable(),
|
|
330
|
+
ttft_ms: z.int().nullable(),
|
|
322
331
|
finish_reason: z.string().max(20).nullable(),
|
|
323
332
|
reasoning_tokens: z.int().nullable(),
|
|
324
333
|
tool_call_index: z.int().nullable(),
|
|
@@ -344,6 +353,7 @@ export const TokenSubsetA = z.object({
|
|
|
344
353
|
name: z.string(),
|
|
345
354
|
active: z.boolean(),
|
|
346
355
|
ord: z.int(),
|
|
356
|
+
quota_threshold: z.int().nullable(),
|
|
347
357
|
});
|
|
348
358
|
export type TokenSubsetA = z.infer<typeof TokenSubsetA>;
|
|
349
359
|
export type TokenSubsetMapping = {
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
},
|
|
15
15
|
{ "name": "name", "desc": "토큰 이름", "type": "string" },
|
|
16
16
|
{ "name": "active", "type": "boolean", "desc": "활성 여부", "dbDefault": "true" },
|
|
17
|
-
{ "name": "ord", "type": "integer", "desc": "정렬 순서", "dbDefault": "0" }
|
|
17
|
+
{ "name": "ord", "type": "integer", "desc": "정렬 순서", "dbDefault": "0" },
|
|
18
|
+
{
|
|
19
|
+
"name": "quota_threshold",
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"nullable": true,
|
|
22
|
+
"desc": "quota 사용률 임계치(%). null이면 미설정"
|
|
23
|
+
}
|
|
18
24
|
],
|
|
19
25
|
"indexes": [],
|
|
20
26
|
"subsets": {
|
|
21
|
-
"A": ["id", "created_at", "provider", "credentials", "name", "active", "ord"]
|
|
27
|
+
"A": ["id", "created_at", "provider", "credentials", "name", "active", "ord", "quota_threshold"]
|
|
22
28
|
},
|
|
23
29
|
"enums": {
|
|
24
30
|
"TokenOrderBy": { "id-desc": "ID최신순", "ord-asc": "순서순" },
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { TokenModel } from "./token.model";
|
|
4
|
+
|
|
5
|
+
const baseToken = {
|
|
6
|
+
provider: "anthropic",
|
|
7
|
+
credentials: {
|
|
8
|
+
accessToken: "sk-ant-oat01-test",
|
|
9
|
+
refreshToken: "sk-ant-ort01-test",
|
|
10
|
+
expiresAt: Date.now() + 3_600_000,
|
|
11
|
+
accountUuid: "acc-1",
|
|
12
|
+
},
|
|
13
|
+
name: "tok-A",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MockPuri = {
|
|
17
|
+
ubRegister: ReturnType<typeof vi.fn>;
|
|
18
|
+
transaction: ReturnType<typeof vi.fn>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function mockWritePuri() {
|
|
22
|
+
const ubRegister = vi.fn();
|
|
23
|
+
const ubUpsert = vi.fn(async () => [1]);
|
|
24
|
+
const transaction = vi.fn(
|
|
25
|
+
async (cb: (trx: { ubUpsert: typeof ubUpsert }) => Promise<number[]>) =>
|
|
26
|
+
cb({ ubUpsert }),
|
|
27
|
+
);
|
|
28
|
+
vi.spyOn(TokenModel as unknown as { getPuri: (mode: "w") => MockPuri }, "getPuri")
|
|
29
|
+
.mockReturnValue({ ubRegister, transaction });
|
|
30
|
+
|
|
31
|
+
return { ubRegister, ubUpsert, transaction };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("TokenModel.save", () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("applies 80% quota threshold to newly created tokens by default", async () => {
|
|
40
|
+
const { ubRegister } = mockWritePuri();
|
|
41
|
+
|
|
42
|
+
await TokenModel.save([baseToken]);
|
|
43
|
+
|
|
44
|
+
expect(ubRegister).toHaveBeenCalledWith(
|
|
45
|
+
"tokens",
|
|
46
|
+
expect.objectContaining({ quota_threshold: 80 }),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("preserves explicit null quota threshold on newly created tokens", async () => {
|
|
51
|
+
const { ubRegister } = mockWritePuri();
|
|
52
|
+
|
|
53
|
+
await TokenModel.save([{ ...baseToken, quota_threshold: null }]);
|
|
54
|
+
|
|
55
|
+
expect(ubRegister).toHaveBeenCalledWith(
|
|
56
|
+
"tokens",
|
|
57
|
+
expect.objectContaining({ quota_threshold: null }),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not inject a quota threshold into existing token updates", async () => {
|
|
62
|
+
const { ubRegister } = mockWritePuri();
|
|
63
|
+
|
|
64
|
+
await TokenModel.save([{ ...baseToken, id: 1 }]);
|
|
65
|
+
|
|
66
|
+
const saved = ubRegister.mock.calls[0]?.[1];
|
|
67
|
+
expect(saved).not.toHaveProperty("quota_threshold");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -13,6 +13,13 @@ import { type TokenSubsetKey, type TokenSubsetMapping } from "../sonamu.generate
|
|
|
13
13
|
import { tokenLoaderQueries, tokenSubsetQueries } from "../sonamu.generated.sso";
|
|
14
14
|
import { type TokenListParams, type TokenSaveParams } from "./token.types";
|
|
15
15
|
|
|
16
|
+
const DEFAULT_QUOTA_THRESHOLD = 80;
|
|
17
|
+
|
|
18
|
+
function applyCreateDefaults(sp: TokenSaveParams): TokenSaveParams {
|
|
19
|
+
if (sp.id !== undefined || sp.quota_threshold !== undefined) return sp;
|
|
20
|
+
return { ...sp, quota_threshold: DEFAULT_QUOTA_THRESHOLD };
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
class TokenModelClass extends BaseModelClass<
|
|
17
24
|
TokenSubsetKey,
|
|
18
25
|
TokenSubsetMapping,
|
|
@@ -143,7 +150,7 @@ class TokenModelClass extends BaseModelClass<
|
|
|
143
150
|
async save(spa: TokenSaveParams[]): Promise<number[]> {
|
|
144
151
|
const wdb = this.getPuri("w");
|
|
145
152
|
spa.forEach((sp) => {
|
|
146
|
-
wdb.ubRegister("tokens", sp);
|
|
153
|
+
wdb.ubRegister("tokens", applyCreateDefaults(sp));
|
|
147
154
|
});
|
|
148
155
|
return wdb.transaction(async (trx) => {
|
|
149
156
|
return trx.ubUpsert("tokens");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { TokenSaveParams } from "./token.types";
|
|
4
|
+
|
|
5
|
+
const baseToken = {
|
|
6
|
+
provider: "anthropic",
|
|
7
|
+
credentials: {
|
|
8
|
+
accessToken: "sk-ant-oat01-test",
|
|
9
|
+
refreshToken: "sk-ant-ort01-test",
|
|
10
|
+
expiresAt: Date.now() + 3_600_000,
|
|
11
|
+
accountUuid: "acc-1",
|
|
12
|
+
},
|
|
13
|
+
name: "tok-A",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("TokenSaveParams", () => {
|
|
17
|
+
it("keeps quota_threshold optional for existing token saves", () => {
|
|
18
|
+
const parsed = TokenSaveParams.parse(baseToken);
|
|
19
|
+
|
|
20
|
+
expect(parsed).not.toHaveProperty("quota_threshold");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("accepts valid nullable quota_threshold values", () => {
|
|
24
|
+
expect(TokenSaveParams.parse({ ...baseToken, quota_threshold: 80 }).quota_threshold).toBe(80);
|
|
25
|
+
expect(
|
|
26
|
+
TokenSaveParams.parse({ ...baseToken, quota_threshold: null }).quota_threshold,
|
|
27
|
+
).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects quota_threshold values outside 1..100", () => {
|
|
31
|
+
expect(() => TokenSaveParams.parse({ ...baseToken, quota_threshold: 0 })).toThrow();
|
|
32
|
+
expect(() => TokenSaveParams.parse({ ...baseToken, quota_threshold: 101 })).toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -30,11 +30,16 @@ export type TokenCredentials = z.infer<typeof TokenCredentials>;
|
|
|
30
30
|
export const TokenListParams = TokenBaseListParams;
|
|
31
31
|
export type TokenListParams = z.infer<typeof TokenListParams>;
|
|
32
32
|
|
|
33
|
+
const TokenQuotaThreshold = z.int().min(1).max(100).nullable();
|
|
34
|
+
|
|
33
35
|
// Token - SaveParams
|
|
34
36
|
export const TokenSaveParams = TokenBaseSchema.partial({
|
|
35
37
|
id: true,
|
|
36
38
|
created_at: true,
|
|
37
39
|
active: true,
|
|
38
40
|
ord: true,
|
|
41
|
+
quota_threshold: true,
|
|
42
|
+
}).extend({
|
|
43
|
+
quota_threshold: TokenQuotaThreshold.optional(),
|
|
39
44
|
});
|
|
40
45
|
export type TokenSaveParams = z.infer<typeof TokenSaveParams>;
|
|
@@ -223,6 +223,7 @@ const entityLabels = {
|
|
|
223
223
|
"entity.RequestLog.cache_read_tokens": "캐시 읽기 토큰",
|
|
224
224
|
"entity.RequestLog.cache_creation_tokens": "캐시 생성 토큰",
|
|
225
225
|
"entity.RequestLog.duration_ms": "응답 소요시간(ms)",
|
|
226
|
+
"entity.RequestLog.ttft_ms": "first gen TTFT(ms)",
|
|
226
227
|
"entity.RequestLog.cost_usd": "API 비용",
|
|
227
228
|
"entity.RequestLog.effort": "effort level",
|
|
228
229
|
"entity.RequestLog.history": "inject_items 히스토리",
|
|
@@ -248,6 +249,7 @@ const entityLabels = {
|
|
|
248
249
|
"entity.RequestLogStep.cache_read_tokens": "캐시 읽기 토큰",
|
|
249
250
|
"entity.RequestLogStep.cache_creation_tokens": "캐시 생성 토큰",
|
|
250
251
|
"entity.RequestLogStep.duration_ms": "소요시간(ms)",
|
|
252
|
+
"entity.RequestLogStep.ttft_ms": "Gen TTFT(ms)",
|
|
251
253
|
"entity.RequestLogStep.finish_reason": "완료 사유",
|
|
252
254
|
"entity.RequestLogStep.reasoning_text": "reasoning 텍스트",
|
|
253
255
|
"entity.RequestLogStep.reasoning_tokens": "reasoning 토큰 수",
|
|
@@ -270,6 +272,7 @@ const entityLabels = {
|
|
|
270
272
|
"entity.Token.name": "토큰 이름",
|
|
271
273
|
"entity.Token.active": "활성 여부",
|
|
272
274
|
"entity.Token.ord": "정렬 순서",
|
|
275
|
+
"entity.Token.quota_threshold": "quota 사용률 임계치(%). null이면 미설정",
|
|
273
276
|
"enum.TokenOrderBy.id-desc": "ID최신순",
|
|
274
277
|
"enum.TokenOrderBy.ord-asc": "순서순",
|
|
275
278
|
"enum.TokenSearchField.id": "ID",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.alterTable("tokens", (table) => {
|
|
5
|
+
// add
|
|
6
|
+
table.integer("quota_threshold").nullable();
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
await knex.schema.alterTable("tokens", (table) => {
|
|
12
|
+
// rollback - add
|
|
13
|
+
table.dropColumns("quota_threshold");
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.alterTable("request_logs", (table) => {
|
|
5
|
+
// add
|
|
6
|
+
table.integer("ttft_ms").notNullable().defaultTo(knex.raw("0"));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
await knex.schema.alterTable("request_logs", (table) => {
|
|
12
|
+
// rollback - add
|
|
13
|
+
table.dropColumns("ttft_ms");
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.alterTable("request_log_steps", (table) => {
|
|
5
|
+
// add
|
|
6
|
+
table.integer("ttft_ms").nullable();
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
await knex.schema.alterTable("request_log_steps", (table) => {
|
|
12
|
+
// rollback - add
|
|
13
|
+
table.dropColumns("ttft_ms");
|
|
14
|
+
});
|
|
15
|
+
}
|