@cartanova/qgrid-cli 2.4.6 → 2.4.8
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/qgrid-run-lifecycle.js +15 -1
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -4
- package/bundle/dist/application/qgrid/qgrid.frame.js +20 -1
- package/bundle/dist/application/qgrid/qgrid.types.js +38 -2
- package/bundle/dist/application/request-log/request-log.model.js +49 -22
- package/bundle/dist/application/request-log-step/request-log-step.types.js +8 -1
- package/bundle/dist/application/sonamu.generated.js +37 -1
- package/bundle/dist/application/sonamu.generated.sso.js +25 -1
- package/bundle/dist/i18n/sd.generated.js +13 -1
- package/bundle/dist/migrations/20260718180824_alter_request_logs_add5_alter1.js +20 -0
- package/bundle/dist/migrations/20260718180825_alter_request_log_steps_add7_alter1.js +22 -0
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +8 -2
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +6 -3
- package/bundle/dist/utils/providers/anthropic/claude-session.js +11 -7
- package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +117 -7
- package/bundle/dist/utils/providers/common/model-cost.js +45 -98
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +68 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +14 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +81 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -9
- package/bundle/src/application/qgrid/qgrid.frame.ts +19 -0
- package/bundle/src/application/qgrid/qgrid.types.ts +36 -0
- package/bundle/src/application/qgrid/tool-emulation.test.ts +1 -0
- package/bundle/src/application/request-log/request-log.entity.json +42 -0
- package/bundle/src/application/request-log/request-log.model.test.ts +58 -0
- package/bundle/src/application/request-log/request-log.model.ts +107 -18
- package/bundle/src/application/request-log-step/request-log-step.entity.json +54 -0
- package/bundle/src/application/request-log-step/request-log-step.types.ts +7 -0
- package/bundle/src/application/sonamu.generated.http +26 -0
- package/bundle/src/application/sonamu.generated.sso.ts +24 -0
- package/bundle/src/application/sonamu.generated.ts +48 -0
- package/bundle/src/i18n/sd.generated.ts +12 -0
- package/bundle/src/migrations/20260718180824_alter_request_logs_add5_alter1.ts +25 -0
- package/bundle/src/migrations/20260718180825_alter_request_log_steps_add7_alter1.ts +29 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +15 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +11 -2
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +7 -2
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +26 -0
- package/bundle/src/utils/providers/anthropic/claude-session.ts +16 -4
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +131 -0
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +194 -11
- package/bundle/src/utils/providers/common/model-cost.test.ts +136 -2
- package/bundle/src/utils/providers/common/model-cost.ts +76 -99
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +18 -0
- package/bundle/web-dist/client/assets/{index-BOXhNo0k.js → index-CmuPjbCl.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-BShH_sut.js → logs-cXOsDli3.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-CoEAp5S0.js → monit-B5Edhvy-.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-Dr3cGnV3.js → routes-UPWScORY.js} +1 -1
- package/bundle/web-dist/client/assets/{sd.generated-D2Y9QeiN.js → sd.generated-DYuKzBY6.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-CmxXl220.js → services.generated-D_SkWOo-.js} +1 -1
- package/bundle/web-dist/client/assets/show-pRp-eZIK.js +30 -0
- package/bundle/web-dist/client/assets/{tokens-hP1NecIg.js → tokens-DJlttxCM.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-6J1T9U2-.js → use-update-token-mutation-CpFGLWUl.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BlfOh9S3.js → lazyRouteComponent-Cu6IkL8A.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-DYly4ipQ.js → logs-Dx2knN2T.js} +10 -7
- package/bundle/web-dist/server/assets/{monit-CYyY2S2f.js → monit-DU6PLatf.js} +2 -2
- package/bundle/web-dist/server/assets/{routes-Dqu3osXt.js → routes-CTco1oJZ.js} +3 -3
- package/bundle/web-dist/server/assets/{sd.generated-CE6D5clm.js → sd.generated-qzuRe35X.js} +12 -0
- package/bundle/web-dist/server/assets/{services.generated-DyZUzXiq.js → services.generated-C8q1M41a.js} +1 -1
- package/bundle/web-dist/server/assets/{show-BDul8xLb.js → show-B3q8_GDh.js} +47 -4
- package/bundle/web-dist/server/assets/{tokens-fzMW4G74.js → tokens-Bvjn15h9.js} +3 -3
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BejUW5sw.js → use-update-token-mutation-BlZo6swF.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +8 -8
- package/package.json +2 -2
- package/skills/qgrid/SKILL.md +4 -0
- package/skills/qgrid/references/ai-sdk-provider-contract.md +30 -2
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +43 -5
- package/skills/qgrid/references/decision-rationale.md +2 -0
- package/skills/qgrid/references/prompt-cache-and-usage.md +4 -1
- package/skills/qgrid/references/request-log-run-lifecycle.md +6 -2
- package/skills/qgrid/references/sonamu-api-web-flow.md +13 -0
- package/bundle/web-dist/client/assets/show-CXII6Iy7.js +0 -30
|
@@ -369,6 +369,87 @@ describe("QgridDispatcherClass", () => {
|
|
|
369
369
|
expect(result.usage.cache_creation_input_tokens).toBe(600);
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
+
it("provider cost 가 0이면 Anthropic 5m/1h cache breakdown 으로 fallback cost 를 계산한다", async () => {
|
|
373
|
+
const dispatcher = new QgridDispatcherClass();
|
|
374
|
+
const generate = vi.fn(
|
|
375
|
+
async (_req: GenerateRequest): Promise<GenerateResult> => ({
|
|
376
|
+
text: "ok",
|
|
377
|
+
tokenName: "anthropic/test",
|
|
378
|
+
usage: {
|
|
379
|
+
totalTokens: 100_000,
|
|
380
|
+
inputTokens: 100_000,
|
|
381
|
+
cachedInputTokens: 0,
|
|
382
|
+
cacheCreationInputTokens: 80_000,
|
|
383
|
+
cacheCreationInputTokens5m: 30_000,
|
|
384
|
+
cacheCreationInputTokens1h: 50_000,
|
|
385
|
+
outputTokens: 0,
|
|
386
|
+
reasoningOutputTokens: 0,
|
|
387
|
+
},
|
|
388
|
+
durationMs: 12,
|
|
389
|
+
costUsd: 0,
|
|
390
|
+
model: "claude-sonnet-4-6",
|
|
391
|
+
threadCoord: { workerId: 1, threadId: "sess-1", epoch: 0 },
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
dispatcher.anthropicDispatcher = { generate } as never;
|
|
395
|
+
|
|
396
|
+
const result = await dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" });
|
|
397
|
+
|
|
398
|
+
expect(result.costUsd).toBeCloseTo(0.4725, 10);
|
|
399
|
+
expect(result.usage.cache_creation_input_tokens).toBe(80_000);
|
|
400
|
+
expect(result.usage.cache_creation_5m_input_tokens).toBe(30_000);
|
|
401
|
+
expect(result.usage.cache_creation_1h_input_tokens).toBe(50_000);
|
|
402
|
+
expect(result.costSource).toBe("pricing_table");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("Fable refusal fallback은 실제 serving model과 requested model을 구분한다", async () => {
|
|
406
|
+
const dispatcher = new QgridDispatcherClass();
|
|
407
|
+
const generate = vi.fn(
|
|
408
|
+
async (_req: GenerateRequest): Promise<GenerateResult> => ({
|
|
409
|
+
text: "served by opus",
|
|
410
|
+
tokenName: "anthropic/test",
|
|
411
|
+
usage: {
|
|
412
|
+
totalTokens: 120,
|
|
413
|
+
inputTokens: 100,
|
|
414
|
+
cachedInputTokens: 0,
|
|
415
|
+
outputTokens: 20,
|
|
416
|
+
reasoningOutputTokens: 0,
|
|
417
|
+
},
|
|
418
|
+
durationMs: 12,
|
|
419
|
+
costUsd: 0.003,
|
|
420
|
+
requestedModel: "claude-fable-5",
|
|
421
|
+
model: "claude-opus-4-8",
|
|
422
|
+
modelFallbacks: [
|
|
423
|
+
{
|
|
424
|
+
trigger: "refusal",
|
|
425
|
+
fromModel: "claude-fable-5",
|
|
426
|
+
toModel: "claude-opus-4-8",
|
|
427
|
+
category: "cyber",
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
threadCoord: { workerId: 1, threadId: "sess-1", epoch: 0 },
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
dispatcher.anthropicDispatcher = { generate } as never;
|
|
434
|
+
|
|
435
|
+
const result = await dispatcher.query({ prompt: "hi", model: "anthropic/claude-fable-5" });
|
|
436
|
+
|
|
437
|
+
expect(result).toMatchObject({
|
|
438
|
+
model: "claude-opus-4-8",
|
|
439
|
+
requestedModel: "claude-fable-5",
|
|
440
|
+
costUsd: 0.003,
|
|
441
|
+
costSource: "provider",
|
|
442
|
+
modelFallbacks: [
|
|
443
|
+
{
|
|
444
|
+
trigger: "refusal",
|
|
445
|
+
fromModel: "claude-fable-5",
|
|
446
|
+
toModel: "claude-opus-4-8",
|
|
447
|
+
category: "cyber",
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
372
453
|
it("provider ttftMs 를 QueryOutput.ttftMs 로 매핑하고 누락 값은 0 으로 둔다", async () => {
|
|
373
454
|
const dispatcher = new QgridDispatcherClass();
|
|
374
455
|
const generate = vi
|
|
@@ -256,27 +256,34 @@ function buildRawOutputSchema(
|
|
|
256
256
|
function toEmulationResult(
|
|
257
257
|
result: GenerateResult,
|
|
258
258
|
): Omit<QueryOutput, "content" | "finishReason" | "runContext"> {
|
|
259
|
+
const hasProviderCost = result.costUsd !== undefined && result.costUsd > 0;
|
|
259
260
|
return {
|
|
260
261
|
text: result.text,
|
|
261
262
|
tokenName: result.tokenName,
|
|
262
263
|
model: result.model,
|
|
264
|
+
requestedModel: result.requestedModel,
|
|
265
|
+
modelFallbacks: result.modelFallbacks,
|
|
263
266
|
usage: {
|
|
264
267
|
input_tokens: result.usage.inputTokens,
|
|
265
268
|
output_tokens: result.usage.outputTokens,
|
|
266
269
|
cache_creation_input_tokens: result.usage.cacheCreationInputTokens ?? 0,
|
|
270
|
+
cache_creation_5m_input_tokens: result.usage.cacheCreationInputTokens5m,
|
|
271
|
+
cache_creation_1h_input_tokens: result.usage.cacheCreationInputTokens1h,
|
|
267
272
|
cache_read_input_tokens: result.usage.cachedInputTokens,
|
|
268
273
|
},
|
|
269
274
|
durationMs: result.durationMs,
|
|
270
275
|
ttftMs: result.ttftMs ?? 0,
|
|
271
|
-
costUsd:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
costUsd: hasProviderCost
|
|
277
|
+
? result.costUsd!
|
|
278
|
+
: calculateCostUsd(result.model, {
|
|
279
|
+
inputTokens: result.usage.inputTokens,
|
|
280
|
+
outputTokens: result.usage.outputTokens,
|
|
281
|
+
cachedInputTokens: result.usage.cachedInputTokens,
|
|
282
|
+
cacheCreationInputTokens: result.usage.cacheCreationInputTokens ?? 0,
|
|
283
|
+
cacheCreationInputTokens5m: result.usage.cacheCreationInputTokens5m,
|
|
284
|
+
cacheCreationInputTokens1h: result.usage.cacheCreationInputTokens1h,
|
|
285
|
+
}),
|
|
286
|
+
costSource: hasProviderCost ? "provider" : "pricing_table",
|
|
280
287
|
};
|
|
281
288
|
}
|
|
282
289
|
|
|
@@ -157,6 +157,8 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
157
157
|
token_name: result.tokenName,
|
|
158
158
|
project_name: args.projectName?.length ? args.projectName : null,
|
|
159
159
|
model_name: result.model ?? null,
|
|
160
|
+
requested_model_name: result.requestedModel ?? result.model ?? args.model ?? null,
|
|
161
|
+
fallback_count: result.modelFallbacks?.length ?? 0,
|
|
160
162
|
user_prompt: args.prompt,
|
|
161
163
|
system_prompt: args.system ?? null,
|
|
162
164
|
response: responseText,
|
|
@@ -164,9 +166,12 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
164
166
|
output_tokens: result.usage.output_tokens,
|
|
165
167
|
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
166
168
|
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
169
|
+
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens ?? null,
|
|
170
|
+
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens ?? null,
|
|
167
171
|
duration_ms: result.durationMs,
|
|
168
172
|
ttft_ms: result.ttftMs,
|
|
169
173
|
cost_usd: result.costUsd !== null ? Math.round(result.costUsd * MICRO_USD) : null,
|
|
174
|
+
cost_source: result.costSource,
|
|
170
175
|
image_cost_usd: imageCostMicroUsd,
|
|
171
176
|
image_cost_method:
|
|
172
177
|
imageCostMicroUsd !== null
|
|
@@ -332,10 +337,17 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
332
337
|
const stepId = await RequestLogModel.appendStep(input.requestLogId, {
|
|
333
338
|
step_index: input.stepIndex,
|
|
334
339
|
type: input.type,
|
|
340
|
+
model_name: input.modelName,
|
|
341
|
+
requested_model_name: input.requestedModelName,
|
|
342
|
+
fallback_count: input.fallbackCount,
|
|
335
343
|
input_tokens: input.inputTokens,
|
|
336
344
|
output_tokens: input.outputTokens,
|
|
337
345
|
cache_read_tokens: input.cacheReadTokens,
|
|
338
346
|
cache_creation_tokens: input.cacheCreationTokens,
|
|
347
|
+
cache_creation_5m_tokens: input.cacheCreation5mTokens,
|
|
348
|
+
cache_creation_1h_tokens: input.cacheCreation1hTokens,
|
|
349
|
+
cost_usd: input.costUsd === undefined ? undefined : Math.round(input.costUsd * MICRO_USD),
|
|
350
|
+
cost_source: input.costSource,
|
|
339
351
|
duration_ms: input.durationMs,
|
|
340
352
|
ttft_ms: input.ttftMs ?? null,
|
|
341
353
|
finish_reason: input.finishReason,
|
|
@@ -358,10 +370,17 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
358
370
|
status: input.status,
|
|
359
371
|
response: input.response,
|
|
360
372
|
token_name: input.tokenName,
|
|
373
|
+
model_name: input.modelName,
|
|
374
|
+
requested_model_name: input.requestedModelName,
|
|
375
|
+
fallback_count: input.fallbackCount,
|
|
361
376
|
input_tokens: input.totalInputTokens,
|
|
362
377
|
output_tokens: input.totalOutputTokens,
|
|
363
378
|
cache_read_tokens: input.totalCacheReadTokens,
|
|
364
379
|
cache_creation_tokens: input.totalCacheCreationTokens,
|
|
380
|
+
cache_creation_5m_tokens: input.totalCacheCreation5mTokens,
|
|
381
|
+
cache_creation_1h_tokens: input.totalCacheCreation1hTokens,
|
|
382
|
+
cost_usd: input.costUsd === undefined ? undefined : Math.round(input.costUsd * MICRO_USD),
|
|
383
|
+
cost_source: input.costSource,
|
|
365
384
|
duration_ms: input.totalDurationMs,
|
|
366
385
|
history: input.history ? JSON.parse(input.history) : undefined,
|
|
367
386
|
error_message: input.errorMessage,
|
|
@@ -125,21 +125,38 @@ export const QueryInput = z.object({
|
|
|
125
125
|
});
|
|
126
126
|
export type QueryInput = z.infer<typeof QueryInput>;
|
|
127
127
|
|
|
128
|
+
export const ModelFallback = z.object({
|
|
129
|
+
trigger: z.literal("refusal"),
|
|
130
|
+
fromModel: z.string(),
|
|
131
|
+
toModel: z.string(),
|
|
132
|
+
category: z.string().optional(),
|
|
133
|
+
explanation: z.string().optional(),
|
|
134
|
+
});
|
|
135
|
+
export type ModelFallback = z.infer<typeof ModelFallback>;
|
|
136
|
+
|
|
137
|
+
export const CostSource = z.enum(["provider", "pricing_table", "mixed"]);
|
|
138
|
+
export type CostSource = z.infer<typeof CostSource>;
|
|
139
|
+
|
|
128
140
|
export const QueryOutput = z.object({
|
|
129
141
|
text: z.string(),
|
|
130
142
|
content: z.array(QgridContent),
|
|
131
143
|
finishReason: FinishReason,
|
|
132
144
|
tokenName: z.string().optional(),
|
|
133
145
|
model: z.string().optional(),
|
|
146
|
+
requestedModel: z.string().optional(),
|
|
147
|
+
modelFallbacks: z.array(ModelFallback).optional(),
|
|
134
148
|
usage: z.object({
|
|
135
149
|
input_tokens: z.number(),
|
|
136
150
|
output_tokens: z.number(),
|
|
137
151
|
cache_creation_input_tokens: z.number(),
|
|
152
|
+
cache_creation_5m_input_tokens: z.number().optional(),
|
|
153
|
+
cache_creation_1h_input_tokens: z.number().optional(),
|
|
138
154
|
cache_read_input_tokens: z.number(),
|
|
139
155
|
}),
|
|
140
156
|
durationMs: z.number(),
|
|
141
157
|
ttftMs: z.number(),
|
|
142
158
|
costUsd: z.number(),
|
|
159
|
+
costSource: CostSource,
|
|
143
160
|
runContext: QgridRunContext.optional(),
|
|
144
161
|
});
|
|
145
162
|
export type QueryOutput = z.infer<typeof QueryOutput>;
|
|
@@ -156,17 +173,22 @@ export const StreamEvents = z.object({
|
|
|
156
173
|
done: z.object({
|
|
157
174
|
text: z.string(),
|
|
158
175
|
model: z.string().optional(),
|
|
176
|
+
requestedModel: z.string().optional(),
|
|
177
|
+
modelFallbacks: z.array(ModelFallback).optional(),
|
|
159
178
|
tokenName: z.string().optional(),
|
|
160
179
|
finishReason: FinishReason,
|
|
161
180
|
usage: z.object({
|
|
162
181
|
input_tokens: z.number(),
|
|
163
182
|
output_tokens: z.number(),
|
|
164
183
|
cache_creation_input_tokens: z.number(),
|
|
184
|
+
cache_creation_5m_input_tokens: z.number().optional(),
|
|
185
|
+
cache_creation_1h_input_tokens: z.number().optional(),
|
|
165
186
|
cache_read_input_tokens: z.number(),
|
|
166
187
|
}),
|
|
167
188
|
durationMs: z.number(),
|
|
168
189
|
ttftMs: z.number(),
|
|
169
190
|
costUsd: z.number(),
|
|
191
|
+
costSource: CostSource,
|
|
170
192
|
content: z.array(QgridContent),
|
|
171
193
|
runContext: QgridRunContext.optional(),
|
|
172
194
|
}),
|
|
@@ -190,10 +212,17 @@ export const AppendStepInput = z.object({
|
|
|
190
212
|
requestLogId: z.number(),
|
|
191
213
|
stepIndex: z.number(),
|
|
192
214
|
type: z.enum(["generate", "tool_call"]),
|
|
215
|
+
modelName: z.string().optional(),
|
|
216
|
+
requestedModelName: z.string().optional(),
|
|
217
|
+
fallbackCount: z.number().optional(),
|
|
193
218
|
inputTokens: z.number().optional(),
|
|
194
219
|
outputTokens: z.number().optional(),
|
|
195
220
|
cacheReadTokens: z.number().optional(),
|
|
196
221
|
cacheCreationTokens: z.number().optional(),
|
|
222
|
+
cacheCreation5mTokens: z.number().optional(),
|
|
223
|
+
cacheCreation1hTokens: z.number().optional(),
|
|
224
|
+
costUsd: z.number().optional(),
|
|
225
|
+
costSource: CostSource.optional(),
|
|
197
226
|
durationMs: z.number().optional(),
|
|
198
227
|
ttftMs: z.number().nullable().optional(),
|
|
199
228
|
finishReason: z.string().optional(),
|
|
@@ -214,10 +243,17 @@ export const FinishRunInput = z.object({
|
|
|
214
243
|
status: z.enum(["succeeded", "error", "aborted"]),
|
|
215
244
|
response: z.string().optional(),
|
|
216
245
|
tokenName: z.string().optional(),
|
|
246
|
+
modelName: z.string().optional(),
|
|
247
|
+
requestedModelName: z.string().optional(),
|
|
248
|
+
fallbackCount: z.number().optional(),
|
|
217
249
|
totalInputTokens: z.number().optional(),
|
|
218
250
|
totalOutputTokens: z.number().optional(),
|
|
219
251
|
totalCacheReadTokens: z.number().optional(),
|
|
220
252
|
totalCacheCreationTokens: z.number().optional(),
|
|
253
|
+
totalCacheCreation5mTokens: z.number().optional(),
|
|
254
|
+
totalCacheCreation1hTokens: z.number().optional(),
|
|
255
|
+
costUsd: z.number().optional(),
|
|
256
|
+
costSource: CostSource.optional(),
|
|
221
257
|
totalDurationMs: z.number().optional(),
|
|
222
258
|
history: z.string().optional(),
|
|
223
259
|
errorMessage: z.string().optional(),
|
|
@@ -29,6 +29,19 @@
|
|
|
29
29
|
"length": 50,
|
|
30
30
|
"nullable": true
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"name": "requested_model_name",
|
|
34
|
+
"desc": "요청 모델 이름 (fallback 전)",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"length": 50,
|
|
37
|
+
"nullable": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "fallback_count",
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"nullable": true,
|
|
43
|
+
"desc": "모델 fallback 횟수"
|
|
44
|
+
},
|
|
32
45
|
{ "name": "user_prompt", "type": "string", "nullable": true, "desc": "사용자 프롬프트" },
|
|
33
46
|
{ "name": "system_prompt", "type": "string", "nullable": true, "desc": "시스템 프롬프트" },
|
|
34
47
|
{ "name": "response", "desc": "llm 응답 전문", "type": "string", "dbDefault": "\"\"" },
|
|
@@ -41,9 +54,28 @@
|
|
|
41
54
|
"desc": "캐시 생성 토큰",
|
|
42
55
|
"dbDefault": "0"
|
|
43
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"name": "cache_creation_5m_tokens",
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"nullable": true,
|
|
61
|
+
"desc": "5분 TTL 캐시 생성 토큰"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "cache_creation_1h_tokens",
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"nullable": true,
|
|
67
|
+
"desc": "1시간 TTL 캐시 생성 토큰"
|
|
68
|
+
},
|
|
44
69
|
{ "name": "duration_ms", "type": "integer", "desc": "응답 소요시간(ms)", "dbDefault": "0" },
|
|
45
70
|
{ "name": "ttft_ms", "type": "integer", "desc": "first gen TTFT(ms)", "dbDefault": "0" },
|
|
46
71
|
{ "name": "cost_usd", "type": "integer", "nullable": true, "desc": "API 비용" },
|
|
72
|
+
{
|
|
73
|
+
"name": "cost_source",
|
|
74
|
+
"type": "string",
|
|
75
|
+
"length": 20,
|
|
76
|
+
"nullable": true,
|
|
77
|
+
"desc": "비용 출처(provider/pricing_table/mixed)"
|
|
78
|
+
},
|
|
47
79
|
{
|
|
48
80
|
"name": "image_cost_usd",
|
|
49
81
|
"type": "integer",
|
|
@@ -102,6 +134,8 @@
|
|
|
102
134
|
"token_name",
|
|
103
135
|
"project_name",
|
|
104
136
|
"model_name",
|
|
137
|
+
"requested_model_name",
|
|
138
|
+
"fallback_count",
|
|
105
139
|
"user_prompt",
|
|
106
140
|
"system_prompt",
|
|
107
141
|
"response",
|
|
@@ -109,9 +143,12 @@
|
|
|
109
143
|
"output_tokens",
|
|
110
144
|
"cache_read_tokens",
|
|
111
145
|
"cache_creation_tokens",
|
|
146
|
+
"cache_creation_5m_tokens",
|
|
147
|
+
"cache_creation_1h_tokens",
|
|
112
148
|
"duration_ms",
|
|
113
149
|
"ttft_ms",
|
|
114
150
|
"cost_usd",
|
|
151
|
+
"cost_source",
|
|
115
152
|
"image_cost_usd",
|
|
116
153
|
"image_cost_method",
|
|
117
154
|
"effort",
|
|
@@ -127,13 +164,18 @@
|
|
|
127
164
|
"token_name",
|
|
128
165
|
"project_name",
|
|
129
166
|
"model_name",
|
|
167
|
+
"requested_model_name",
|
|
168
|
+
"fallback_count",
|
|
130
169
|
"input_tokens",
|
|
131
170
|
"output_tokens",
|
|
132
171
|
"cache_read_tokens",
|
|
133
172
|
"cache_creation_tokens",
|
|
173
|
+
"cache_creation_5m_tokens",
|
|
174
|
+
"cache_creation_1h_tokens",
|
|
134
175
|
"duration_ms",
|
|
135
176
|
"ttft_ms",
|
|
136
177
|
"cost_usd",
|
|
178
|
+
"cost_source",
|
|
137
179
|
"image_cost_usd",
|
|
138
180
|
"image_cost_method",
|
|
139
181
|
"effort",
|
|
@@ -90,3 +90,61 @@ describe("RequestLogModel TTFT", () => {
|
|
|
90
90
|
);
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
describe("RequestLogModel cost provenance", () => {
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.restoreAllMocks();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("uses exact stored cost for rows with a cost source", async () => {
|
|
100
|
+
const chain = {
|
|
101
|
+
where: vi.fn(),
|
|
102
|
+
select: vi.fn(async () => [
|
|
103
|
+
{
|
|
104
|
+
model_name: "claude-opus-4-8",
|
|
105
|
+
input_tokens: 1_000_000,
|
|
106
|
+
output_tokens: 1_000_000,
|
|
107
|
+
cache_read_tokens: 0,
|
|
108
|
+
cache_creation_tokens: 0,
|
|
109
|
+
cache_creation_5m_tokens: null,
|
|
110
|
+
cache_creation_1h_tokens: null,
|
|
111
|
+
cost_usd: 3_000,
|
|
112
|
+
cost_source: "provider",
|
|
113
|
+
},
|
|
114
|
+
]),
|
|
115
|
+
};
|
|
116
|
+
chain.where.mockReturnValue(chain);
|
|
117
|
+
const db = vi.fn(() => chain);
|
|
118
|
+
vi.spyOn(RequestLogModel as unknown as { getDB: () => typeof db }, "getDB").mockReturnValue(
|
|
119
|
+
db,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await expect(RequestLogModel.totalCost()).resolves.toBe(0.003);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("keeps TTL-aware price-table recomputation for legacy rows", async () => {
|
|
126
|
+
const chain = {
|
|
127
|
+
where: vi.fn(),
|
|
128
|
+
select: vi.fn(async () => [
|
|
129
|
+
{
|
|
130
|
+
model_name: "claude-sonnet-4-6",
|
|
131
|
+
input_tokens: 100_000,
|
|
132
|
+
output_tokens: 0,
|
|
133
|
+
cache_read_tokens: 0,
|
|
134
|
+
cache_creation_tokens: 80_000,
|
|
135
|
+
cache_creation_5m_tokens: 30_000,
|
|
136
|
+
cache_creation_1h_tokens: 50_000,
|
|
137
|
+
cost_usd: null,
|
|
138
|
+
cost_source: null,
|
|
139
|
+
},
|
|
140
|
+
]),
|
|
141
|
+
};
|
|
142
|
+
chain.where.mockReturnValue(chain);
|
|
143
|
+
const db = vi.fn(() => chain);
|
|
144
|
+
vi.spyOn(RequestLogModel as unknown as { getDB: () => typeof db }, "getDB").mockReturnValue(
|
|
145
|
+
db,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await expect(RequestLogModel.totalCost()).resolves.toBeCloseTo(0.4725, 10);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -24,7 +24,10 @@ type RequestLogUsageRow = {
|
|
|
24
24
|
output_tokens: number;
|
|
25
25
|
cache_read_tokens: number;
|
|
26
26
|
cache_creation_tokens: number;
|
|
27
|
+
cache_creation_5m_tokens?: number | null;
|
|
28
|
+
cache_creation_1h_tokens?: number | null;
|
|
27
29
|
cost_usd?: number | null;
|
|
30
|
+
cost_source?: string | null;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function isAnthropicUsageRow(row: Pick<RequestLogUsageRow, "token_name" | "model_name">): boolean {
|
|
@@ -45,6 +48,8 @@ function normalizedUsageForCost(row: RequestLogUsageRow): {
|
|
|
45
48
|
outputTokens: number;
|
|
46
49
|
cachedInputTokens: number;
|
|
47
50
|
cacheCreationInputTokens: number;
|
|
51
|
+
cacheCreationInputTokens5m?: number;
|
|
52
|
+
cacheCreationInputTokens1h?: number;
|
|
48
53
|
} {
|
|
49
54
|
const cacheRead = row.cache_read_tokens;
|
|
50
55
|
const cacheCreation = row.cache_creation_tokens;
|
|
@@ -57,10 +62,18 @@ function normalizedUsageForCost(row: RequestLogUsageRow): {
|
|
|
57
62
|
outputTokens: row.output_tokens,
|
|
58
63
|
cachedInputTokens: cacheRead,
|
|
59
64
|
cacheCreationInputTokens: cacheCreation,
|
|
65
|
+
...(row.cache_creation_5m_tokens !== null && row.cache_creation_5m_tokens !== undefined
|
|
66
|
+
? { cacheCreationInputTokens5m: row.cache_creation_5m_tokens }
|
|
67
|
+
: {}),
|
|
68
|
+
...(row.cache_creation_1h_tokens !== null && row.cache_creation_1h_tokens !== undefined
|
|
69
|
+
? { cacheCreationInputTokens1h: row.cache_creation_1h_tokens }
|
|
70
|
+
: {}),
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
function normalizeLegacyAnthropicRow<T extends RequestLogUsageRow>(row: T): T {
|
|
75
|
+
// 새 row 는 당시의 provider/가격표 비용을 확정 저장한다. 현재 가격표로 덮어쓰지 않는다.
|
|
76
|
+
if (row.cost_source) return row;
|
|
64
77
|
const usage = normalizedUsageForCost(row);
|
|
65
78
|
if (usage.inputTokens === row.input_tokens) return row;
|
|
66
79
|
const normalized = { ...row, input_tokens: usage.inputTokens };
|
|
@@ -258,7 +271,14 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
258
271
|
output_tokens?: number;
|
|
259
272
|
cache_read_tokens?: number;
|
|
260
273
|
cache_creation_tokens?: number;
|
|
274
|
+
cache_creation_5m_tokens?: number | null;
|
|
275
|
+
cache_creation_1h_tokens?: number | null;
|
|
261
276
|
duration_ms?: number;
|
|
277
|
+
requested_model_name?: string | null;
|
|
278
|
+
model_name?: string | null;
|
|
279
|
+
fallback_count?: number | null;
|
|
280
|
+
cost_usd?: number | null;
|
|
281
|
+
cost_source?: string | null;
|
|
262
282
|
history?: unknown;
|
|
263
283
|
error_message?: string;
|
|
264
284
|
tool_call_count?: number;
|
|
@@ -277,7 +297,14 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
277
297
|
"output_tokens",
|
|
278
298
|
"cache_read_tokens",
|
|
279
299
|
"cache_creation_tokens",
|
|
300
|
+
"cache_creation_5m_tokens",
|
|
301
|
+
"cache_creation_1h_tokens",
|
|
280
302
|
"duration_ms",
|
|
303
|
+
"requested_model_name",
|
|
304
|
+
"model_name",
|
|
305
|
+
"fallback_count",
|
|
306
|
+
"cost_usd",
|
|
307
|
+
"cost_source",
|
|
281
308
|
"error_message",
|
|
282
309
|
"tool_call_count",
|
|
283
310
|
"image_cost_usd",
|
|
@@ -289,21 +316,30 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
289
316
|
if (params.history !== undefined) update.history = params.history;
|
|
290
317
|
update.ttft_ms = (await this.firstGenerateStepTtft(requestLogId)) ?? 0;
|
|
291
318
|
|
|
292
|
-
|
|
319
|
+
// 서버 run 경로는 step 에서 확정한 exact cost 를 넘긴다. 외부/legacy caller 가 비용을
|
|
320
|
+
// 생략한 경우에만 저장된 모델+usage 로 계산한다.
|
|
321
|
+
if (
|
|
322
|
+
params.cost_usd === undefined &&
|
|
323
|
+
params.input_tokens !== undefined &&
|
|
324
|
+
params.output_tokens !== undefined
|
|
325
|
+
) {
|
|
293
326
|
const db = this.getDB("r");
|
|
294
327
|
const [row] = await db("request_logs")
|
|
295
328
|
.select("model_name")
|
|
296
329
|
.where("id", requestLogId)
|
|
297
330
|
.limit(1);
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
const effectiveModelName = params.model_name ?? row?.model_name;
|
|
332
|
+
if (effectiveModelName) {
|
|
333
|
+
const model = canonicalModelName(effectiveModelName);
|
|
300
334
|
const usage = normalizedUsageForCost({
|
|
301
335
|
token_name: params.token_name,
|
|
302
|
-
model_name:
|
|
336
|
+
model_name: effectiveModelName,
|
|
303
337
|
input_tokens: params.input_tokens,
|
|
304
338
|
output_tokens: params.output_tokens,
|
|
305
339
|
cache_read_tokens: params.cache_read_tokens ?? 0,
|
|
306
340
|
cache_creation_tokens: params.cache_creation_tokens ?? 0,
|
|
341
|
+
cache_creation_5m_tokens: params.cache_creation_5m_tokens,
|
|
342
|
+
cache_creation_1h_tokens: params.cache_creation_1h_tokens,
|
|
307
343
|
});
|
|
308
344
|
update.cost_usd = Math.round(
|
|
309
345
|
calculateCostUsd(model, {
|
|
@@ -311,8 +347,11 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
311
347
|
outputTokens: usage.outputTokens,
|
|
312
348
|
cachedInputTokens: usage.cachedInputTokens,
|
|
313
349
|
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
350
|
+
cacheCreationInputTokens5m: usage.cacheCreationInputTokens5m,
|
|
351
|
+
cacheCreationInputTokens1h: usage.cacheCreationInputTokens1h,
|
|
314
352
|
}) * 1_000_000,
|
|
315
353
|
);
|
|
354
|
+
update.cost_source = "pricing_table";
|
|
316
355
|
}
|
|
317
356
|
}
|
|
318
357
|
|
|
@@ -341,25 +380,62 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
341
380
|
output_tokens: number;
|
|
342
381
|
cache_read_tokens: number;
|
|
343
382
|
cache_creation_tokens: number;
|
|
383
|
+
cache_creation_5m_tokens?: number;
|
|
384
|
+
cache_creation_1h_tokens?: number;
|
|
344
385
|
duration_ms: number;
|
|
386
|
+
requested_model_name?: string;
|
|
387
|
+
model_name?: string;
|
|
388
|
+
fallback_count: number;
|
|
389
|
+
cost_usd: number;
|
|
390
|
+
cost_source?: string;
|
|
345
391
|
}> {
|
|
346
392
|
const db = this.getDB("r");
|
|
347
|
-
const
|
|
393
|
+
const rows = (await db("request_log_steps")
|
|
394
|
+
.select(
|
|
395
|
+
"input_tokens",
|
|
396
|
+
"output_tokens",
|
|
397
|
+
"cache_read_tokens",
|
|
398
|
+
"cache_creation_tokens",
|
|
399
|
+
"cache_creation_5m_tokens",
|
|
400
|
+
"cache_creation_1h_tokens",
|
|
401
|
+
"duration_ms",
|
|
402
|
+
"requested_model_name",
|
|
403
|
+
"model_name",
|
|
404
|
+
"fallback_count",
|
|
405
|
+
"cost_usd",
|
|
406
|
+
"cost_source",
|
|
407
|
+
)
|
|
348
408
|
.where("request_log_id", requestLogId)
|
|
349
|
-
.where("type", "generate")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
409
|
+
.where("type", "generate")) as Array<Record<string, unknown>>;
|
|
410
|
+
|
|
411
|
+
const sum = (field: string): number =>
|
|
412
|
+
rows.reduce((total, row) => total + Number(row[field] ?? 0), 0);
|
|
413
|
+
const optionalSum = (field: string): number | undefined =>
|
|
414
|
+
rows.some((row) => row[field] !== null && row[field] !== undefined) ? sum(field) : undefined;
|
|
415
|
+
const collapse = (field: string): string | undefined => {
|
|
416
|
+
const values = [
|
|
417
|
+
...new Set(
|
|
418
|
+
rows
|
|
419
|
+
.map((row) => row[field])
|
|
420
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0),
|
|
421
|
+
),
|
|
422
|
+
];
|
|
423
|
+
if (values.length === 0) return undefined;
|
|
424
|
+
return values.length === 1 ? values[0] : "mixed";
|
|
425
|
+
};
|
|
357
426
|
return {
|
|
358
|
-
input_tokens:
|
|
359
|
-
output_tokens:
|
|
360
|
-
cache_read_tokens:
|
|
361
|
-
cache_creation_tokens:
|
|
362
|
-
|
|
427
|
+
input_tokens: sum("input_tokens"),
|
|
428
|
+
output_tokens: sum("output_tokens"),
|
|
429
|
+
cache_read_tokens: sum("cache_read_tokens"),
|
|
430
|
+
cache_creation_tokens: sum("cache_creation_tokens"),
|
|
431
|
+
cache_creation_5m_tokens: optionalSum("cache_creation_5m_tokens"),
|
|
432
|
+
cache_creation_1h_tokens: optionalSum("cache_creation_1h_tokens"),
|
|
433
|
+
duration_ms: sum("duration_ms"),
|
|
434
|
+
requested_model_name: collapse("requested_model_name"),
|
|
435
|
+
model_name: collapse("model_name"),
|
|
436
|
+
fallback_count: sum("fallback_count"),
|
|
437
|
+
cost_usd: sum("cost_usd"),
|
|
438
|
+
cost_source: collapse("cost_source"),
|
|
363
439
|
};
|
|
364
440
|
}
|
|
365
441
|
|
|
@@ -419,17 +495,28 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
419
495
|
"output_tokens",
|
|
420
496
|
"cache_read_tokens",
|
|
421
497
|
"cache_creation_tokens",
|
|
498
|
+
"cache_creation_5m_tokens",
|
|
499
|
+
"cache_creation_1h_tokens",
|
|
422
500
|
"cost_usd",
|
|
501
|
+
"cost_source",
|
|
423
502
|
)) as Array<{
|
|
424
503
|
model_name: string | null;
|
|
425
504
|
input_tokens: number;
|
|
426
505
|
output_tokens: number;
|
|
427
506
|
cache_read_tokens: number;
|
|
428
507
|
cache_creation_tokens: number;
|
|
508
|
+
cache_creation_5m_tokens: number | null;
|
|
509
|
+
cache_creation_1h_tokens: number | null;
|
|
429
510
|
cost_usd: number | null;
|
|
511
|
+
cost_source: string | null;
|
|
430
512
|
}>;
|
|
431
513
|
|
|
432
514
|
return rows.reduce((sum, row) => {
|
|
515
|
+
// cost_source 가 있으면 새 계약으로 확정 저장된 값이다. 프로모션/가격 변경 이후에도
|
|
516
|
+
// 당시 비용을 유지한다. NULL인 legacy row 만 현재 가격표로 보정한다.
|
|
517
|
+
if (row.cost_source && row.cost_usd !== null) {
|
|
518
|
+
return sum + Math.max(row.cost_usd, 0) / MICRO_USD;
|
|
519
|
+
}
|
|
433
520
|
const usage = normalizedUsageForCost(row);
|
|
434
521
|
if (!usage.model) return sum + Math.max(row.cost_usd ?? 0, 0) / MICRO_USD;
|
|
435
522
|
return (
|
|
@@ -439,6 +526,8 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
439
526
|
outputTokens: usage.outputTokens,
|
|
440
527
|
cachedInputTokens: usage.cachedInputTokens,
|
|
441
528
|
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
529
|
+
cacheCreationInputTokens5m: usage.cacheCreationInputTokens5m,
|
|
530
|
+
cacheCreationInputTokens1h: usage.cacheCreationInputTokens1h,
|
|
442
531
|
})
|
|
443
532
|
);
|
|
444
533
|
}, 0);
|