@cartanova/qgrid-cli 2.10.3 → 2.10.5
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-image-generation.js +6 -2
- package/bundle/dist/application/qgrid/qgrid-response-format.js +4 -4
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +63 -140
- package/bundle/dist/application/qgrid/qgrid.frame.js +3 -6
- package/bundle/dist/application/qgrid/qgrid.types.js +4 -8
- package/bundle/dist/application/request-log/request-log.model.js +21 -15
- package/bundle/dist/application/setting/setting.model.js +2 -64
- package/bundle/dist/application/setting/setting.store.js +3 -4
- package/bundle/dist/application/setting/setting.types.js +2 -12
- package/bundle/dist/i18n/en.js +1 -3
- package/bundle/dist/i18n/ko.js +1 -3
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +19 -8
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +18 -0
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +14 -3
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +9 -0
- package/bundle/src/application/qgrid/qgrid-response-format.ts +10 -7
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +0 -111
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +82 -178
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +0 -38
- package/bundle/src/application/qgrid/qgrid.frame.ts +4 -6
- package/bundle/src/application/qgrid/qgrid.types.ts +14 -4
- package/bundle/src/application/request-log/request-log.model.test.ts +17 -12
- package/bundle/src/application/request-log/request-log.model.ts +8 -2
- package/bundle/src/application/setting/setting.model.test.ts +1 -101
- package/bundle/src/application/setting/setting.model.ts +0 -73
- package/bundle/src/application/setting/setting.store.ts +2 -3
- package/bundle/src/application/setting/setting.types.ts +0 -12
- package/bundle/src/application/sonamu.generated.http +2 -7
- package/bundle/src/i18n/en.ts +0 -3
- package/bundle/src/i18n/ko.ts +0 -3
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +19 -1
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +22 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +12 -0
- package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +1 -0
- package/bundle/web-dist/client/assets/index-CvG4UB9v.css +1 -0
- package/bundle/web-dist/client/assets/{index-BIC1sQxw.js → index-Dxw7DiXF.js} +4 -4
- package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +1 -0
- package/bundle/web-dist/client/assets/{monit-CraVV3MQ.js → monit-E8eGW1Sj.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-B3ZBoJ60.js → routes-7spLfIGZ.js} +2 -2
- package/bundle/web-dist/client/assets/services.generated-jnPHYMER.js +51 -0
- package/bundle/web-dist/client/assets/settings-BdosZFNC.js +1 -0
- package/bundle/web-dist/client/assets/{show-ItTExdAh.js → show-b2PFNI3D.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-BynKx8Vn.js → tokens-CBdHb2_U.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-8xw6on5P.js → use-oauth-login-flow-Cyn3YPqW.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-D-Y8OySK.js → use-update-token-mutation-DQCwCYfR.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{cost-CSH_t17I.js → cost-59jBdx-I.js} +8 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-Bj1cPq6x.js → lazyRouteComponent-CUR-kWzI.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-BSk9lOHe.js → logs-CaaWO9-Z.js} +9 -7
- package/bundle/web-dist/server/assets/{monit-D9W4ioUi.js → monit-CBil92fJ.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-CmvbWuDf.js → plus-Bvdj8Pwx.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BjR1Iaz8.js → react-dom-CyuShs9K.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-B8U9BL9-.js → refresh-cw-3QAiSWBd.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-C7lB2Q85.js → routes-DtVf9yqo.js} +7 -7
- package/bundle/web-dist/server/assets/{services.generated-BM4X3Ecj.js → services.generated-FPwsG9p3.js} +116 -131
- package/bundle/web-dist/server/assets/{settings-BFVuldch.js → settings-DeHNwoMv.js} +40 -251
- package/bundle/web-dist/server/assets/{show-Cswh_tqs.js → show-8NNdRxhM.js} +3 -3
- package/bundle/web-dist/server/assets/{tokens-DiTUwUwr.js → tokens-odgS2-9q.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DYKnG4A2.js → use-oauth-login-flow-DTQKdYBO.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-DqGMopII.js → use-update-token-mutation-xvXbPrCI.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +3 -2
- package/skills/qgrid/SKILL.md +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +12 -9
- package/skills/qgrid/references/decision-rationale.md +2 -2
- package/skills/qgrid/references/openai-codex-runtime.md +5 -1
- package/bundle/dist/utils/process-supervisor.js +0 -22
- package/bundle/dist/utils/server-restart.js +0 -61
- package/bundle/src/utils/process-supervisor.test.ts +0 -24
- package/bundle/src/utils/process-supervisor.ts +0 -17
- package/bundle/src/utils/server-restart.test.ts +0 -76
- package/bundle/src/utils/server-restart.ts +0 -68
- package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +0 -1
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +0 -1
- package/bundle/web-dist/client/assets/logs-zvdujZWn.js +0 -1
- package/bundle/web-dist/client/assets/services.generated-Cbvg2sGQ.js +0 -51
- package/bundle/web-dist/client/assets/settings-DwbzT5jo.js +0 -1
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
|
-
import { ServiceUnavailableException } from "sonamu";
|
|
3
2
|
|
|
4
|
-
import { SD } from "../../i18n/sd.generated";
|
|
5
|
-
import { isRestartPending } from "../../utils/server-restart";
|
|
6
3
|
import { RequestLogModel } from "../request-log/request-log.model";
|
|
7
4
|
import {
|
|
8
5
|
estimateImageGenerationCostMicroUsd,
|
|
@@ -20,44 +17,6 @@ const STALE_RUN_THRESHOLD_MS = 30 * 60 * 1000;
|
|
|
20
17
|
const STALE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
21
18
|
let staleCleanupInFlight: Promise<void> | undefined;
|
|
22
19
|
let lastStaleCleanupStartedAt = 0;
|
|
23
|
-
const activeNativeRunRefs = new Map<number, number>();
|
|
24
|
-
const interruptedNativeRunIds = new Set<number>();
|
|
25
|
-
const activeNativeFinalizers = new Map<number, Set<Promise<void>>>();
|
|
26
|
-
|
|
27
|
-
function restartUnavailable(): ServiceUnavailableException {
|
|
28
|
-
return new ServiceUnavailableException(SD("qgrid.restartPending")());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function registerNativeRun(requestLogId: number): void {
|
|
32
|
-
activeNativeRunRefs.set(requestLogId, (activeNativeRunRefs.get(requestLogId) ?? 0) + 1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function releaseNativeRun(requestLogId: number): void {
|
|
36
|
-
const refs = activeNativeRunRefs.get(requestLogId);
|
|
37
|
-
if (refs === undefined || refs <= 1) activeNativeRunRefs.delete(requestLogId);
|
|
38
|
-
else activeNativeRunRefs.set(requestLogId, refs - 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function trackNativeFinalizer<T>(requestLogId: number, finalize: () => Promise<T>): Promise<T> {
|
|
42
|
-
const operation = finalize();
|
|
43
|
-
const settled = operation.then(
|
|
44
|
-
() => undefined,
|
|
45
|
-
() => undefined,
|
|
46
|
-
);
|
|
47
|
-
const finalizers = activeNativeFinalizers.get(requestLogId) ?? new Set<Promise<void>>();
|
|
48
|
-
finalizers.add(settled);
|
|
49
|
-
activeNativeFinalizers.set(requestLogId, finalizers);
|
|
50
|
-
void settled.then(() => {
|
|
51
|
-
finalizers.delete(settled);
|
|
52
|
-
if (finalizers.size === 0) activeNativeFinalizers.delete(requestLogId);
|
|
53
|
-
});
|
|
54
|
-
return operation;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function assertNativeRunAdmission(): void {
|
|
58
|
-
if (isRestartPending()) throw restartUnavailable();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
20
|
function withProviderPrefix(
|
|
62
21
|
routeModel: string | undefined,
|
|
63
22
|
model: string | undefined,
|
|
@@ -110,8 +69,6 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
110
69
|
requestLogId: number;
|
|
111
70
|
stepIndex: number;
|
|
112
71
|
}> {
|
|
113
|
-
assertNativeRunAdmission();
|
|
114
|
-
|
|
115
72
|
let requestLogId: number;
|
|
116
73
|
let stepIndex = 0;
|
|
117
74
|
|
|
@@ -140,14 +97,6 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
140
97
|
});
|
|
141
98
|
}
|
|
142
99
|
|
|
143
|
-
// DB 준비 중에 재시작이 시작됐다면 provider 실행으로 넘어가지 않는다.
|
|
144
|
-
// 이 요청은 재시작 snapshot에 없었으므로 방금 준비한 run을 직접 마감한다.
|
|
145
|
-
if (isRestartPending()) {
|
|
146
|
-
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
147
|
-
throw restartUnavailable();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
registerNativeRun(requestLogId);
|
|
151
100
|
return { requestLogId, stepIndex };
|
|
152
101
|
}
|
|
153
102
|
|
|
@@ -157,102 +106,94 @@ export async function afterQuery(
|
|
|
157
106
|
args: QueryInput,
|
|
158
107
|
result: QueryOutput,
|
|
159
108
|
): Promise<RunLifecycleResult> {
|
|
160
|
-
|
|
109
|
+
// provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
|
|
110
|
+
const requestedModelName = withProviderPrefix(
|
|
111
|
+
args.model,
|
|
112
|
+
args.model ?? result.requestedModel ?? result.model,
|
|
113
|
+
);
|
|
114
|
+
const servedModelName = withProviderPrefix(args.model, result.model);
|
|
115
|
+
|
|
116
|
+
// generate step 기록
|
|
117
|
+
await RequestLogModel.appendStep(requestLogId, {
|
|
118
|
+
step_index: stepIndex,
|
|
119
|
+
type: "generate",
|
|
120
|
+
model_name: servedModelName,
|
|
121
|
+
requested_model_name: requestedModelName,
|
|
122
|
+
fallback_count: result.modelFallbacks?.length ?? 0,
|
|
123
|
+
input_tokens: result.usage.input_tokens,
|
|
124
|
+
output_tokens: result.usage.output_tokens,
|
|
125
|
+
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
126
|
+
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
127
|
+
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
128
|
+
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
129
|
+
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
130
|
+
cost_source: result.costSource,
|
|
131
|
+
duration_ms: result.durationMs,
|
|
132
|
+
ttft_ms: result.ttftMs,
|
|
133
|
+
finish_reason: result.finishReason,
|
|
134
|
+
});
|
|
161
135
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
136
|
+
const imageParts = getImageParts(result);
|
|
137
|
+
const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
|
|
138
|
+
result,
|
|
139
|
+
args.imageGenerationOptions,
|
|
140
|
+
);
|
|
141
|
+
for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
|
|
142
|
+
await RequestLogModel.appendStep(requestLogId, step);
|
|
143
|
+
}
|
|
170
144
|
|
|
171
|
-
|
|
145
|
+
if (result.finishReason === "tool-calls") {
|
|
146
|
+
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
147
|
+
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
148
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
149
|
+
const tc = toolCalls[i]!;
|
|
150
|
+
if (tc.type !== "tool-call") continue;
|
|
172
151
|
await RequestLogModel.appendStep(requestLogId, {
|
|
173
152
|
step_index: stepIndex,
|
|
174
|
-
type: "
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
output_tokens: result.usage.output_tokens,
|
|
180
|
-
cache_read_tokens: result.usage.cache_read_input_tokens,
|
|
181
|
-
cache_creation_tokens: result.usage.cache_creation_input_tokens,
|
|
182
|
-
cache_creation_5m_tokens: result.usage.cache_creation_5m_input_tokens,
|
|
183
|
-
cache_creation_1h_tokens: result.usage.cache_creation_1h_input_tokens,
|
|
184
|
-
cost_usd: Math.round(result.costUsd * 1_000_000),
|
|
185
|
-
cost_source: result.costSource,
|
|
186
|
-
duration_ms: result.durationMs,
|
|
187
|
-
ttft_ms: result.ttftMs,
|
|
188
|
-
finish_reason: result.finishReason,
|
|
153
|
+
type: "tool_call",
|
|
154
|
+
tool_call_index: imageParts.length + i,
|
|
155
|
+
tool_call_id: tc.toolCallId,
|
|
156
|
+
tool_name: tc.toolName,
|
|
157
|
+
tool_args: tc.input,
|
|
189
158
|
});
|
|
159
|
+
}
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
result,
|
|
194
|
-
args.imageGenerationOptions,
|
|
195
|
-
);
|
|
196
|
-
for (const step of buildImageGenerationToolSteps(args, imageParts, stepIndex)) {
|
|
197
|
-
await RequestLogModel.appendStep(requestLogId, step);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (result.finishReason === "tool-calls") {
|
|
201
|
-
// tool-call step을 즉시 기록하고, 다음 follow-up이 같은 row에 결과를 채운다.
|
|
202
|
-
const toolCalls = result.content.filter((c) => c.type === "tool-call");
|
|
203
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
204
|
-
const tc = toolCalls[i]!;
|
|
205
|
-
if (tc.type !== "tool-call") continue;
|
|
206
|
-
await RequestLogModel.appendStep(requestLogId, {
|
|
207
|
-
step_index: stepIndex,
|
|
208
|
-
type: "tool_call",
|
|
209
|
-
tool_call_index: imageParts.length + i,
|
|
210
|
-
tool_call_id: tc.toolCallId,
|
|
211
|
-
tool_name: tc.toolName,
|
|
212
|
-
tool_args: tc.input,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { runContext: { requestLogId } };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// finishReason === "stop" → run 종료 (전체 step usage 합산)
|
|
220
|
-
const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
221
|
-
const responseText = formatResponseForLog(result);
|
|
222
|
-
await RequestLogModel.finishRun(requestLogId, {
|
|
223
|
-
status: "succeeded",
|
|
224
|
-
response: responseText,
|
|
225
|
-
// structured 요청만 판정한다. deti 배치의 broken JSON 류를 목록에서 바로
|
|
226
|
-
// 구별하기 위한 기록이며, 비structured 요청은 null 로 남는다.
|
|
227
|
-
response_json_ok: args.jsonSchema ? isParseableJson(responseText) : null,
|
|
228
|
-
token_name: result.tokenName,
|
|
229
|
-
input_tokens: agg.input_tokens,
|
|
230
|
-
output_tokens: agg.output_tokens,
|
|
231
|
-
cache_read_tokens: agg.cache_read_tokens,
|
|
232
|
-
cache_creation_tokens: agg.cache_creation_tokens,
|
|
233
|
-
cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
|
|
234
|
-
cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
|
|
235
|
-
duration_ms: agg.duration_ms,
|
|
236
|
-
// multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
|
|
237
|
-
requested_model_name: requestedModelName,
|
|
238
|
-
model_name: servedModelName,
|
|
239
|
-
fallback_count: agg.fallback_count,
|
|
240
|
-
cost_usd: agg.cost_usd,
|
|
241
|
-
cost_source: agg.cost_source,
|
|
242
|
-
image_cost_usd: imageCostMicroUsd,
|
|
243
|
-
...(imageParts.length > 0 ? { is_image_generation: true } : {}),
|
|
244
|
-
image_cost_method:
|
|
245
|
-
imageCostMicroUsd !== null
|
|
246
|
-
? imageGenerationCostMethod(args.imageGenerationOptions, result)
|
|
247
|
-
: null,
|
|
248
|
-
history: filterHistoryForStorage(args.history),
|
|
249
|
-
});
|
|
161
|
+
return { runContext: { requestLogId } };
|
|
162
|
+
}
|
|
250
163
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
164
|
+
// finishReason === "stop" → run 종료 (전체 step usage 합산)
|
|
165
|
+
const agg = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
166
|
+
const responseText = formatResponseForLog(result);
|
|
167
|
+
await RequestLogModel.finishRun(requestLogId, {
|
|
168
|
+
status: "succeeded",
|
|
169
|
+
response: responseText,
|
|
170
|
+
// structured 요청만 판정한다. deti 배치의 broken JSON 류를 목록에서 바로
|
|
171
|
+
// 구별하기 위한 기록이며, 비structured 요청은 null 로 남는다.
|
|
172
|
+
response_json_ok: args.jsonSchema ? isParseableJson(responseText) : null,
|
|
173
|
+
token_name: result.tokenName,
|
|
174
|
+
input_tokens: agg.input_tokens,
|
|
175
|
+
output_tokens: agg.output_tokens,
|
|
176
|
+
cache_read_tokens: agg.cache_read_tokens,
|
|
177
|
+
cache_creation_tokens: agg.cache_creation_tokens,
|
|
178
|
+
cache_creation_5m_tokens: agg.cache_creation_5m_tokens,
|
|
179
|
+
cache_creation_1h_tokens: agg.cache_creation_1h_tokens,
|
|
180
|
+
duration_ms: agg.duration_ms,
|
|
181
|
+
// multi-step run 부모의 모델은 최종 turn을 서빙한 모델이다.
|
|
182
|
+
requested_model_name: requestedModelName,
|
|
183
|
+
model_name: servedModelName,
|
|
184
|
+
fallback_count: agg.fallback_count,
|
|
185
|
+
cost_usd: agg.cost_usd,
|
|
186
|
+
cost_source: agg.cost_source,
|
|
187
|
+
image_cost_usd: imageCostMicroUsd,
|
|
188
|
+
...(imageParts.length > 0 ? { is_image_generation: true } : {}),
|
|
189
|
+
image_cost_method:
|
|
190
|
+
imageCostMicroUsd !== null
|
|
191
|
+
? imageGenerationCostMethod(args.imageGenerationOptions, result)
|
|
192
|
+
: null,
|
|
193
|
+
history: filterHistoryForStorage(args.history),
|
|
255
194
|
});
|
|
195
|
+
|
|
196
|
+
return {};
|
|
256
197
|
}
|
|
257
198
|
|
|
258
199
|
export async function finishRunWithError(
|
|
@@ -260,48 +201,11 @@ export async function finishRunWithError(
|
|
|
260
201
|
errorMessage: string,
|
|
261
202
|
args?: QueryInput,
|
|
262
203
|
): Promise<void> {
|
|
263
|
-
|
|
264
|
-
return trackNativeFinalizer(requestLogId, async () => {
|
|
265
|
-
try {
|
|
266
|
-
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
267
|
-
} finally {
|
|
268
|
-
releaseNativeRun(requestLogId);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
204
|
+
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
271
205
|
}
|
|
272
206
|
|
|
273
207
|
export async function finishRunAborted(requestLogId: number, args?: QueryInput): Promise<void> {
|
|
274
|
-
|
|
275
|
-
return trackNativeFinalizer(requestLogId, async () => {
|
|
276
|
-
try {
|
|
277
|
-
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
|
|
278
|
-
} finally {
|
|
279
|
-
releaseNativeRun(requestLogId);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** 현재 이 프로세스가 provider를 실행 중인 native run만 재시작 오류로 마감한다. */
|
|
285
|
-
export async function finishActiveNativeRunsForRestart(): Promise<void> {
|
|
286
|
-
const requestLogIds = [...activeNativeRunRefs.keys()];
|
|
287
|
-
for (const requestLogId of requestLogIds) {
|
|
288
|
-
interruptedNativeRunIds.add(requestLogId);
|
|
289
|
-
activeNativeRunRefs.delete(requestLogId);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
await Promise.all(
|
|
293
|
-
requestLogIds.map(async (requestLogId) => {
|
|
294
|
-
await Promise.all(activeNativeFinalizers.get(requestLogId) ?? []);
|
|
295
|
-
await finishTerminalRun(requestLogId, "error", "server restarted");
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 프로세스 전역 registry를 격리해야 하는 단위 테스트 전용. */
|
|
301
|
-
export function resetNativeRunRegistryForTests(): void {
|
|
302
|
-
activeNativeRunRefs.clear();
|
|
303
|
-
interruptedNativeRunIds.clear();
|
|
304
|
-
activeNativeFinalizers.clear();
|
|
208
|
+
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
|
|
305
209
|
}
|
|
306
210
|
|
|
307
211
|
async function finishTerminalRun(
|
|
@@ -21,7 +21,6 @@ const {
|
|
|
21
21
|
dispatcherQueryStreamMock,
|
|
22
22
|
beforeQueryMock,
|
|
23
23
|
afterQueryMock,
|
|
24
|
-
assertNativeRunAdmissionMock,
|
|
25
24
|
finishRunWithErrorMock,
|
|
26
25
|
finishRunAbortedMock,
|
|
27
26
|
getRateLimitsByTokenIdMock,
|
|
@@ -45,7 +44,6 @@ const {
|
|
|
45
44
|
dispatcherQueryStreamMock: vi.fn(),
|
|
46
45
|
beforeQueryMock: vi.fn(),
|
|
47
46
|
afterQueryMock: vi.fn(),
|
|
48
|
-
assertNativeRunAdmissionMock: vi.fn(),
|
|
49
47
|
finishRunWithErrorMock: vi.fn(),
|
|
50
48
|
finishRunAbortedMock: vi.fn(),
|
|
51
49
|
getRateLimitsByTokenIdMock: vi.fn(),
|
|
@@ -95,7 +93,6 @@ vi.mock("./qgrid.dispatcher", async (importOriginal) => {
|
|
|
95
93
|
});
|
|
96
94
|
|
|
97
95
|
vi.mock("./qgrid-run-lifecycle", () => ({
|
|
98
|
-
assertNativeRunAdmission: assertNativeRunAdmissionMock,
|
|
99
96
|
beforeQuery: beforeQueryMock,
|
|
100
97
|
afterQuery: afterQueryMock,
|
|
101
98
|
finishRunWithError: finishRunWithErrorMock,
|
|
@@ -215,7 +212,6 @@ describe("QgridFrame.query request logging", () => {
|
|
|
215
212
|
beforeEach(() => {
|
|
216
213
|
beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 41, stepIndex: 0 });
|
|
217
214
|
afterQueryMock.mockReset().mockResolvedValue({});
|
|
218
|
-
assertNativeRunAdmissionMock.mockReset();
|
|
219
215
|
finishRunWithErrorMock.mockReset();
|
|
220
216
|
dispatcherQueryMock.mockReset();
|
|
221
217
|
findActiveByProviderAndNameMock.mockReset();
|
|
@@ -368,18 +364,6 @@ describe("QgridFrame.query request logging", () => {
|
|
|
368
364
|
expect(finishRunWithErrorMock).not.toHaveBeenCalled();
|
|
369
365
|
});
|
|
370
366
|
|
|
371
|
-
it("rejects logger-disabled native dispatch while restart admission is closed", async () => {
|
|
372
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
373
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await expect(
|
|
377
|
-
QgridFrame.query({ prompt: "hi", model: "openai/gpt-5-codex", logger: false }),
|
|
378
|
-
).rejects.toMatchObject({ statusCode: 503 });
|
|
379
|
-
expect(dispatcherQueryMock).not.toHaveBeenCalled();
|
|
380
|
-
expect(beforeQueryMock).not.toHaveBeenCalled();
|
|
381
|
-
});
|
|
382
|
-
|
|
383
367
|
it("merges a tool run id with the provider thread coordinate", async () => {
|
|
384
368
|
dispatcherQueryMock.mockResolvedValueOnce(queryOutput());
|
|
385
369
|
afterQueryMock.mockResolvedValueOnce({ runContext: { requestLogId: 41 } });
|
|
@@ -952,7 +936,6 @@ describe("QgridFrame.queryStream request logging", () => {
|
|
|
952
936
|
beforeEach(() => {
|
|
953
937
|
beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 52, stepIndex: 0 });
|
|
954
938
|
afterQueryMock.mockReset().mockResolvedValue({});
|
|
955
|
-
assertNativeRunAdmissionMock.mockReset();
|
|
956
939
|
finishRunWithErrorMock.mockReset();
|
|
957
940
|
finishRunAbortedMock.mockReset();
|
|
958
941
|
dispatcherQueryStreamMock.mockReset();
|
|
@@ -986,27 +969,6 @@ describe("QgridFrame.queryStream request logging", () => {
|
|
|
986
969
|
};
|
|
987
970
|
}
|
|
988
971
|
|
|
989
|
-
it("rejects stream preparation while restart admission is closed", async () => {
|
|
990
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
991
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
await expect(QgridFrame.prepareStream({ prompt: "hi" })).rejects.toMatchObject({
|
|
995
|
-
statusCode: 503,
|
|
996
|
-
});
|
|
997
|
-
expect(dispatcherQueryStreamMock).not.toHaveBeenCalled();
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it("rechecks admission before dispatching a previously prepared stream", async () => {
|
|
1001
|
-
const { streamId } = await QgridFrame.prepareStream({ prompt: "hi", logger: false });
|
|
1002
|
-
assertNativeRunAdmissionMock.mockImplementationOnce(() => {
|
|
1003
|
-
throw Object.assign(new Error("server restarting"), { statusCode: 503 });
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
await expect(QgridFrame.queryStream(streamId)).rejects.toMatchObject({ statusCode: 503 });
|
|
1007
|
-
expect(dispatcherQueryStreamMock).not.toHaveBeenCalled();
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
972
|
function installSseContext() {
|
|
1011
973
|
let closeHandler: (() => void) | undefined;
|
|
1012
974
|
const sse = {
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
RefreshFailedError,
|
|
39
39
|
} from "./oauth";
|
|
40
40
|
import {
|
|
41
|
-
assertNativeRunAdmission,
|
|
42
41
|
afterQuery,
|
|
43
42
|
beforeQuery,
|
|
44
43
|
finishRunAborted,
|
|
@@ -248,7 +247,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
248
247
|
|
|
249
248
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
250
249
|
async query(args: QueryInput): Promise<QueryOutput> {
|
|
251
|
-
assertNativeRunAdmission();
|
|
252
250
|
rejectInvalidCallerSchemas(args);
|
|
253
251
|
const resolvedArgs = await resolveTokenName(args);
|
|
254
252
|
const disconnect = createHttpDisconnectHandle();
|
|
@@ -299,7 +297,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
299
297
|
|
|
300
298
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
301
299
|
async prepareStream(args: QueryInput): Promise<{ streamId: string }> {
|
|
302
|
-
assertNativeRunAdmission();
|
|
303
300
|
rejectImageGenerationStream(args);
|
|
304
301
|
rejectInvalidCallerSchemas(args);
|
|
305
302
|
const resolvedArgs = await resolveTokenName(args);
|
|
@@ -315,7 +312,6 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
315
312
|
const args = pendingStreams.get(streamId);
|
|
316
313
|
pendingStreams.delete(streamId);
|
|
317
314
|
if (!args) throw new Error("invalid or expired streamId");
|
|
318
|
-
assertNativeRunAdmission();
|
|
319
315
|
rejectImageGenerationStream(args);
|
|
320
316
|
|
|
321
317
|
const ctx = Sonamu.getContext();
|
|
@@ -504,8 +500,10 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
504
500
|
// 화면의 목록 필터를 그대로 받아 같은 조건의 합계를 돌려준다 — 목록과 비용이
|
|
505
501
|
// 서로 다른 모수를 보면 사용자에게는 숫자가 어긋난 것으로 읽힌다.
|
|
506
502
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
507
|
-
async totalCost(
|
|
508
|
-
|
|
503
|
+
async totalCost(
|
|
504
|
+
params?: RequestLogListParams,
|
|
505
|
+
): Promise<{ usd: number; unknownImageCostCount: number }> {
|
|
506
|
+
return RequestLogModel.totalCost(params);
|
|
509
507
|
}
|
|
510
508
|
|
|
511
509
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
@@ -12,10 +12,10 @@ export const QgridTool = z.object({
|
|
|
12
12
|
});
|
|
13
13
|
export type QgridTool = z.infer<typeof QgridTool>;
|
|
14
14
|
|
|
15
|
-
// Observed
|
|
15
|
+
// Observed image response values, distinct from requested generation hints.
|
|
16
16
|
export const ImageGenerationMetadata = z.object({
|
|
17
|
-
route: z.
|
|
18
|
-
model: z.
|
|
17
|
+
route: z.enum(["codex-images", "codex-responses"]),
|
|
18
|
+
model: z.string().optional(),
|
|
19
19
|
background: z.string().optional(),
|
|
20
20
|
quality: z.string().optional(),
|
|
21
21
|
size: z.string().optional(),
|
|
@@ -79,7 +79,17 @@ export type ServiceTier = z.infer<typeof ServiceTier>;
|
|
|
79
79
|
export const ImageGenerationQuality = z.enum(["low", "medium", "high"]);
|
|
80
80
|
export type ImageGenerationQuality = z.infer<typeof ImageGenerationQuality>;
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
// Subscription-backend hints, not public Images API resolution guarantees.
|
|
83
|
+
export const ImageGenerationSize = z.union([
|
|
84
|
+
z.literal("auto"),
|
|
85
|
+
z
|
|
86
|
+
.string()
|
|
87
|
+
.regex(/^[1-9]\d*x[1-9]\d*$/)
|
|
88
|
+
.refine(
|
|
89
|
+
(size) => size.split("x").every((edge) => Number.isSafeInteger(Number(edge))),
|
|
90
|
+
"Image dimensions must be positive safe integers",
|
|
91
|
+
),
|
|
92
|
+
]);
|
|
83
93
|
export type ImageGenerationSize = z.infer<typeof ImageGenerationSize>;
|
|
84
94
|
|
|
85
95
|
export const ImageGenerationOptions = z
|
|
@@ -564,7 +564,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
564
564
|
* @param confirmedTotal SUM 쿼리가 돌려줄 micro-USD 합계
|
|
565
565
|
* @param legacyRows 재계산 대상 row 들
|
|
566
566
|
*/
|
|
567
|
-
function mockTotalCostQueries(confirmedTotal: number, legacyRows: unknown[]) {
|
|
567
|
+
function mockTotalCostQueries(confirmedTotal: number, legacyRows: unknown[], unknownImageCostCount = 0) {
|
|
568
568
|
// subset qb 는 SELECT 를 비우고(clear) 집계/축소된 컬럼만 다시 얹는다 — 구현과 같은 체인.
|
|
569
569
|
const sumChain = {
|
|
570
570
|
where: vi.fn(),
|
|
@@ -572,7 +572,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
572
572
|
whereRaw: vi.fn(),
|
|
573
573
|
clear: vi.fn(),
|
|
574
574
|
select: vi.fn(),
|
|
575
|
-
first: vi.fn(async () => ({ total: confirmedTotal })),
|
|
575
|
+
first: vi.fn(async () => ({ total: confirmedTotal, unknownImageCostCount })),
|
|
576
576
|
};
|
|
577
577
|
sumChain.where.mockReturnValue(sumChain);
|
|
578
578
|
sumChain.whereIn.mockReturnValue(sumChain);
|
|
@@ -606,7 +606,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
606
606
|
it("uses exact stored cost for rows with a cost source", async () => {
|
|
607
607
|
mockTotalCostQueries(3_000, []);
|
|
608
608
|
|
|
609
|
-
await expect(RequestLogModel.totalCost()).resolves.
|
|
609
|
+
await expect(RequestLogModel.totalCost()).resolves.toEqual({ usd: 0.003, unknownImageCostCount: 0 });
|
|
610
610
|
});
|
|
611
611
|
|
|
612
612
|
it("sums every row in SQL without a cost_source predicate", async () => {
|
|
@@ -637,7 +637,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
637
637
|
},
|
|
638
638
|
]);
|
|
639
639
|
|
|
640
|
-
await expect(RequestLogModel.totalCost()).resolves.toBeCloseTo(0.4725, 10);
|
|
640
|
+
await expect(RequestLogModel.totalCost().then((result) => result.usd)).resolves.toBeCloseTo(0.4725, 10);
|
|
641
641
|
});
|
|
642
642
|
|
|
643
643
|
it("adds only the delta for legacy rows already counted in the SQL sum", async () => {
|
|
@@ -656,7 +656,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
656
656
|
},
|
|
657
657
|
]);
|
|
658
658
|
|
|
659
|
-
await expect(RequestLogModel.totalCost()).resolves.toBeCloseTo(0.4725, 10);
|
|
659
|
+
await expect(RequestLogModel.totalCost().then((result) => result.usd)).resolves.toBeCloseTo(0.4725, 10);
|
|
660
660
|
});
|
|
661
661
|
|
|
662
662
|
it("leaves the stored value untouched for legacy rows without a model name", async () => {
|
|
@@ -675,7 +675,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
675
675
|
},
|
|
676
676
|
]);
|
|
677
677
|
|
|
678
|
-
await expect(RequestLogModel.totalCost()).resolves.toBeCloseTo(0.005, 10);
|
|
678
|
+
await expect(RequestLogModel.totalCost().then((result) => result.usd)).resolves.toBeCloseTo(0.005, 10);
|
|
679
679
|
});
|
|
680
680
|
|
|
681
681
|
it("applies list filters to both cost queries so totals match the filtered list", async () => {
|
|
@@ -721,7 +721,7 @@ describe("RequestLogModel cost provenance", () => {
|
|
|
721
721
|
},
|
|
722
722
|
]);
|
|
723
723
|
|
|
724
|
-
await expect(RequestLogModel.totalCost()).resolves.toBeCloseTo(0.4755, 10);
|
|
724
|
+
await expect(RequestLogModel.totalCost().then((result) => result.usd)).resolves.toBeCloseTo(0.4755, 10);
|
|
725
725
|
});
|
|
726
726
|
});
|
|
727
727
|
|
|
@@ -738,10 +738,13 @@ describe("RequestLogModel PostgreSQL", () => {
|
|
|
738
738
|
{ cost_usd: 5_000, image_cost_usd: 10_000, cost_source: "provider" },
|
|
739
739
|
{ cost_usd: 1_000, image_cost_usd: 20_000, cost_source: null },
|
|
740
740
|
{ cost_usd: 900_000, image_cost_usd: 700_000, cost_source: null },
|
|
741
|
+
{ cost_usd: 1_000, image_cost_usd: null, cost_source: "provider", is_image_generation: true },
|
|
742
|
+
{ cost_usd: 2_000, image_cost_usd: null, cost_source: "provider", is_image_generation: false },
|
|
743
|
+
{ cost_usd: 500_000, image_cost_usd: null, cost_source: "provider", is_image_generation: true },
|
|
741
744
|
].entries()) {
|
|
742
745
|
const id = await RequestLogModel.createRun({
|
|
743
746
|
user_prompt: "cost regression",
|
|
744
|
-
project_name: index
|
|
747
|
+
project_name: index === 2 || index === 5 ? `${projectName}-other` : projectName,
|
|
745
748
|
});
|
|
746
749
|
ids.push(id);
|
|
747
750
|
await RequestLogModel.getPuri("w").table("request_logs").where("id", id).update({
|
|
@@ -754,10 +757,12 @@ describe("RequestLogModel PostgreSQL", () => {
|
|
|
754
757
|
|
|
755
758
|
const params = { sonamuFilter: { project_name: projectName }, num: 0, page: 1 };
|
|
756
759
|
const { rows } = await RequestLogModel.findMany("C", params);
|
|
757
|
-
expect(rows.map((row) => row.id)).toEqual(expect.arrayContaining(ids
|
|
758
|
-
expect(rows).toHaveLength(
|
|
759
|
-
//
|
|
760
|
-
await
|
|
760
|
+
expect(rows.map((row) => row.id)).toEqual(expect.arrayContaining([ids[0], ids[1], ids[3], ids[4]]));
|
|
761
|
+
expect(rows).toHaveLength(4);
|
|
762
|
+
// .038 known images + .001 hosted driver + .002 text; other project excluded.
|
|
763
|
+
const total = await RequestLogModel.totalCost(params);
|
|
764
|
+
expect(total.usd).toBeCloseTo(0.041, 10);
|
|
765
|
+
expect(total.unknownImageCostCount).toBe(1);
|
|
761
766
|
} finally {
|
|
762
767
|
await RequestLogModel.del(ids);
|
|
763
768
|
}
|
|
@@ -820,7 +820,9 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
820
820
|
* 필터 조건을 여기서 다시 구현하지 않고 `applyListFilters` 를 공유하는 것이 핵심이다 —
|
|
821
821
|
* 두 쿼리가 각자 필터를 가지면 화면에서 필터를 걸었을 때 행 수와 비용이 조용히 어긋난다.
|
|
822
822
|
*/
|
|
823
|
-
async totalCost(
|
|
823
|
+
async totalCost(
|
|
824
|
+
rawParams: RequestLogListParams = { num: 0, page: 1 },
|
|
825
|
+
): Promise<{ usd: number; unknownImageCostCount: number }> {
|
|
824
826
|
const params = {
|
|
825
827
|
num: 0,
|
|
826
828
|
page: 1,
|
|
@@ -843,6 +845,9 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
843
845
|
total: Puri.rawNumber(
|
|
844
846
|
"COALESCE(SUM(GREATEST(request_logs.cost_usd, 0) + COALESCE(request_logs.image_cost_usd, 0)), 0)",
|
|
845
847
|
),
|
|
848
|
+
unknownImageCostCount: Puri.rawNumber(
|
|
849
|
+
"COUNT(*) FILTER (WHERE request_logs.is_image_generation = true AND request_logs.image_cost_usd IS NULL)",
|
|
850
|
+
),
|
|
846
851
|
})
|
|
847
852
|
.first();
|
|
848
853
|
|
|
@@ -867,7 +872,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
867
872
|
const storedTotal = Number(stored?.total ?? 0) / MICRO_USD;
|
|
868
873
|
|
|
869
874
|
// legacy row 는 위 SUM 에 저장값으로 이미 포함되어 있다. 재계산값과의 차액만 더한다.
|
|
870
|
-
|
|
875
|
+
const usd = legacyRows.reduce((sum, row) => {
|
|
871
876
|
const alreadySummed = Math.max(row.cost_usd ?? 0, 0) / MICRO_USD;
|
|
872
877
|
const usage = normalizedUsageForCost(row);
|
|
873
878
|
if (!usage.model) return sum;
|
|
@@ -884,6 +889,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
884
889
|
alreadySummed
|
|
885
890
|
);
|
|
886
891
|
}, storedTotal);
|
|
892
|
+
return { usd, unknownImageCostCount: Number(stored?.unknownImageCostCount ?? 0) };
|
|
887
893
|
}
|
|
888
894
|
|
|
889
895
|
async distinctProjectNames(): Promise<string[]> {
|