@cartanova/qgrid-cli 2.3.6 → 2.3.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-image-generation.js +42 -0
- package/bundle/dist/application/qgrid/qgrid-response-format.js +44 -0
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +23 -4
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +15 -6
- package/bundle/dist/application/qgrid/qgrid.frame.js +52 -17
- package/bundle/dist/application/qgrid/qgrid.types.js +36 -12
- package/bundle/dist/application/qgrid/tool-emulation.js +16 -8
- package/bundle/dist/application/request-log/request-log.model.js +5 -2
- package/bundle/dist/application/sonamu.generated.js +13 -4
- package/bundle/dist/application/sonamu.generated.sso.js +9 -3
- package/bundle/dist/i18n/sd.generated.js +4 -1
- package/bundle/dist/migrations/20260705140614_alter_request_logs_add1_alter1.js +16 -0
- package/bundle/dist/migrations/20260705140615_alter_request_log_steps_alter1.js +14 -0
- package/bundle/dist/migrations/20260706120000_alter_request_logs_add_image_cost.js +17 -0
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +2 -1
- package/bundle/dist/utils/providers/openai/codex-rpc.js +1 -1
- package/bundle/dist/utils/providers/openai/codex-worker.js +78 -6
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +21 -4
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +62 -0
- package/bundle/src/application/qgrid/qgrid-response-format.ts +59 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +105 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +34 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +33 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -5
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +123 -1
- package/bundle/src/application/qgrid/qgrid.frame.ts +52 -5
- package/bundle/src/application/qgrid/qgrid.types.ts +22 -0
- package/bundle/src/application/qgrid/tool-emulation.test.ts +44 -1
- package/bundle/src/application/qgrid/tool-emulation.ts +27 -4
- package/bundle/src/application/request-log/request-log.entity.json +73 -47
- package/bundle/src/application/request-log/request-log.model.ts +7 -0
- package/bundle/src/application/sonamu.generated.http +16 -3
- package/bundle/src/application/sonamu.generated.sso.ts +6 -0
- package/bundle/src/application/sonamu.generated.ts +12 -0
- package/bundle/src/i18n/sd.generated.ts +3 -0
- package/bundle/src/migrations/20260705140614_alter_request_logs_add1_alter1.ts +15 -0
- package/bundle/src/migrations/20260705140615_alter_request_log_steps_alter1.ts +13 -0
- package/bundle/src/migrations/20260706120000_alter_request_logs_add_image_cost.ts +14 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +5 -0
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +14 -0
- package/bundle/src/utils/providers/openai/codex-rpc.ts +2 -0
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +219 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +154 -5
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +95 -1
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +42 -0
- package/bundle/web-dist/client/assets/index-OQd6CclQ.js +78 -0
- package/bundle/web-dist/client/assets/index-VE_24EJf.css +1 -0
- package/bundle/web-dist/client/assets/{logs-B0pWpDrg.js → logs-8T91eoLY.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-7XhVuG7o.js → routes--GqHRT2A.js} +1 -1
- package/bundle/web-dist/client/assets/{sd.generated-DXhjjL2f.js → sd.generated-BrUnRt0B.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-B7u0U1wC.js → services.generated-AsEvHbJ7.js} +1 -1
- package/bundle/web-dist/client/assets/{show-CcjZ9nr7.js → show-cHMucIDC.js} +3 -3
- package/bundle/web-dist/client/assets/{tokens-DlwAm0v4.js → tokens-BXAJUeCJ.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-B0LkbiYf.js → lazyRouteComponent-BswulcOx.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-dLx6gMXT.js → logs-B_ZLzpwY.js} +4 -4
- package/bundle/web-dist/server/assets/{routes-DD3Hb9xO.js → routes-D9OiKoqa.js} +2 -2
- package/bundle/web-dist/server/assets/{sd.generated-DdfpD_iN.js → sd.generated-DRuQSIKo.js} +3 -0
- package/bundle/web-dist/server/assets/{services.generated-D3ZCvIkt.js → services.generated-Dp0uKgpV.js} +1 -1
- package/bundle/web-dist/server/assets/{show-BQjeyv6A.js → show-CGlX9OYt.js} +104 -13
- package/bundle/web-dist/server/assets/{tokens-CSOsBpP7.js → tokens-Cjw6cQxZ.js} +2 -2
- package/bundle/web-dist/server/entry-server.generated.js +9 -8
- package/package.json +1 -1
- package/bundle/web-dist/client/assets/index-5WdFEqZT.css +0 -1
- package/bundle/web-dist/client/assets/index-jnUY7nJ-.js +0 -78
|
@@ -44,6 +44,19 @@
|
|
|
44
44
|
{ "name": "duration_ms", "type": "integer", "desc": "응답 소요시간(ms)", "dbDefault": "0" },
|
|
45
45
|
{ "name": "ttft_ms", "type": "integer", "desc": "first gen TTFT(ms)", "dbDefault": "0" },
|
|
46
46
|
{ "name": "cost_usd", "type": "integer", "nullable": true, "desc": "API 비용" },
|
|
47
|
+
{
|
|
48
|
+
"name": "image_cost_usd",
|
|
49
|
+
"type": "integer",
|
|
50
|
+
"nullable": true,
|
|
51
|
+
"desc": "이미지 생성 추정 비용"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "image_cost_method",
|
|
55
|
+
"type": "string",
|
|
56
|
+
"length": 100,
|
|
57
|
+
"nullable": true,
|
|
58
|
+
"desc": "이미지 비용 추정 방식"
|
|
59
|
+
},
|
|
47
60
|
{ "name": "effort", "type": "string", "length": 10, "nullable": true, "desc": "effort level" },
|
|
48
61
|
{
|
|
49
62
|
"name": "history",
|
|
@@ -60,7 +73,13 @@
|
|
|
60
73
|
"dbDefault": "\"succeeded\""
|
|
61
74
|
},
|
|
62
75
|
{ "name": "error_message", "type": "string", "nullable": true, "desc": "에러 메시지" },
|
|
63
|
-
{ "name": "tool_call_count", "type": "integer", "desc": "tool call 횟수", "dbDefault": "0" }
|
|
76
|
+
{ "name": "tool_call_count", "type": "integer", "desc": "tool call 횟수", "dbDefault": "0" },
|
|
77
|
+
{
|
|
78
|
+
"name": "is_image_generation",
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"desc": "이미지 생성 turn 여부 (quota 귀속용)",
|
|
81
|
+
"dbDefault": "false"
|
|
82
|
+
}
|
|
64
83
|
],
|
|
65
84
|
"indexes": [
|
|
66
85
|
{
|
|
@@ -77,57 +96,64 @@
|
|
|
77
96
|
}
|
|
78
97
|
],
|
|
79
98
|
"subsets": {
|
|
80
|
-
"A":
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
"A": [
|
|
100
|
+
"id",
|
|
101
|
+
"created_at",
|
|
102
|
+
"token_name",
|
|
103
|
+
"project_name",
|
|
104
|
+
"model_name",
|
|
105
|
+
"user_prompt",
|
|
106
|
+
"system_prompt",
|
|
107
|
+
"response",
|
|
108
|
+
"input_tokens",
|
|
109
|
+
"output_tokens",
|
|
110
|
+
"cache_read_tokens",
|
|
111
|
+
"cache_creation_tokens",
|
|
112
|
+
"duration_ms",
|
|
113
|
+
"ttft_ms",
|
|
114
|
+
"cost_usd",
|
|
115
|
+
"image_cost_usd",
|
|
116
|
+
"image_cost_method",
|
|
117
|
+
"effort",
|
|
118
|
+
"history",
|
|
119
|
+
"status",
|
|
120
|
+
"error_message",
|
|
121
|
+
"tool_call_count",
|
|
122
|
+
"is_image_generation"
|
|
123
|
+
],
|
|
124
|
+
"P": [
|
|
125
|
+
"id",
|
|
126
|
+
"created_at",
|
|
127
|
+
"token_name",
|
|
128
|
+
"project_name",
|
|
129
|
+
"model_name",
|
|
130
|
+
"input_tokens",
|
|
131
|
+
"output_tokens",
|
|
132
|
+
"cache_read_tokens",
|
|
133
|
+
"cache_creation_tokens",
|
|
134
|
+
"duration_ms",
|
|
135
|
+
"ttft_ms",
|
|
136
|
+
"cost_usd",
|
|
137
|
+
"image_cost_usd",
|
|
138
|
+
"image_cost_method",
|
|
139
|
+
"effort",
|
|
140
|
+
"status",
|
|
141
|
+
"tool_call_count",
|
|
142
|
+
"is_image_generation"
|
|
143
|
+
]
|
|
123
144
|
},
|
|
124
145
|
"enums": {
|
|
125
|
-
"RequestLogOrderBy": { "
|
|
146
|
+
"RequestLogOrderBy": { "id-desc": "ID최신순" },
|
|
126
147
|
"RequestLogSearchField": {
|
|
127
|
-
"
|
|
148
|
+
"id": "ID",
|
|
149
|
+
"token_name": "토큰이름",
|
|
150
|
+
"user_prompt": "사용자 프롬프트"
|
|
128
151
|
},
|
|
129
152
|
"RequestLogStatus": {
|
|
130
|
-
"
|
|
153
|
+
"running": "실행 중",
|
|
154
|
+
"succeeded": "성공",
|
|
155
|
+
"error": "에러",
|
|
156
|
+
"aborted": "중단"
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
159
|
}
|
|
@@ -209,6 +209,7 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
209
209
|
effort?: string | null;
|
|
210
210
|
project_name?: string | null;
|
|
211
211
|
history?: unknown;
|
|
212
|
+
is_image_generation?: boolean;
|
|
212
213
|
}): Promise<number> {
|
|
213
214
|
const wdb = this.getPuri("w");
|
|
214
215
|
wdb.ubRegister("request_logs", {
|
|
@@ -225,6 +226,8 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
225
226
|
cache_creation_tokens: 0,
|
|
226
227
|
duration_ms: 0,
|
|
227
228
|
tool_call_count: 0,
|
|
229
|
+
// 이미지 turn 식별(R13). run 경로(tools+image 조합)도 auto 경로와 동일하게 마킹.
|
|
230
|
+
is_image_generation: params.is_image_generation ?? false,
|
|
228
231
|
...(params.history !== undefined ? { history: params.history as { type: string }[] } : {}),
|
|
229
232
|
});
|
|
230
233
|
return wdb.transaction(async (trx) => {
|
|
@@ -259,6 +262,8 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
259
262
|
history?: unknown;
|
|
260
263
|
error_message?: string;
|
|
261
264
|
tool_call_count?: number;
|
|
265
|
+
image_cost_usd?: number | null;
|
|
266
|
+
image_cost_method?: string | null;
|
|
262
267
|
},
|
|
263
268
|
): Promise<void> {
|
|
264
269
|
if (params.status === "succeeded" && !params.token_name) {
|
|
@@ -275,6 +280,8 @@ class RequestLogModelClass extends BaseModelClass<
|
|
|
275
280
|
"duration_ms",
|
|
276
281
|
"error_message",
|
|
277
282
|
"tool_call_count",
|
|
283
|
+
"image_cost_usd",
|
|
284
|
+
"image_cost_method",
|
|
278
285
|
] as const;
|
|
279
286
|
for (const key of fields) {
|
|
280
287
|
if (params[key] !== undefined) update[key] = params[key];
|
|
@@ -182,11 +182,14 @@ Content-Type: application/json
|
|
|
182
182
|
"duration_ms": 0,
|
|
183
183
|
"ttft_ms": 0,
|
|
184
184
|
"cost_usd": null,
|
|
185
|
+
"image_cost_usd": null,
|
|
186
|
+
"image_cost_method": null,
|
|
185
187
|
"effort": null,
|
|
186
188
|
"history": null,
|
|
187
189
|
"status": "running",
|
|
188
190
|
"error_message": null,
|
|
189
|
-
"tool_call_count": 0
|
|
191
|
+
"tool_call_count": 0,
|
|
192
|
+
"is_image_generation": false
|
|
190
193
|
}
|
|
191
194
|
]
|
|
192
195
|
}
|
|
@@ -245,7 +248,12 @@ Content-Type: application/json
|
|
|
245
248
|
"output": "OUTPUT",
|
|
246
249
|
"isError": false
|
|
247
250
|
}
|
|
248
|
-
]
|
|
251
|
+
],
|
|
252
|
+
"imageGeneration": false,
|
|
253
|
+
"imageGenerationOptions": {
|
|
254
|
+
"quality": "low",
|
|
255
|
+
"size": "1024x1024"
|
|
256
|
+
}
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -292,7 +300,12 @@ Content-Type: application/json
|
|
|
292
300
|
"output": "OUTPUT",
|
|
293
301
|
"isError": false
|
|
294
302
|
}
|
|
295
|
-
]
|
|
303
|
+
],
|
|
304
|
+
"imageGeneration": false,
|
|
305
|
+
"imageGenerationOptions": {
|
|
306
|
+
"quality": "low",
|
|
307
|
+
"size": "1024x1024"
|
|
308
|
+
}
|
|
296
309
|
}
|
|
297
310
|
}
|
|
298
311
|
|
|
@@ -33,11 +33,14 @@ export const requestLogSubsetQueries = {
|
|
|
33
33
|
duration_ms: "request_logs.duration_ms",
|
|
34
34
|
ttft_ms: "request_logs.ttft_ms",
|
|
35
35
|
cost_usd: "request_logs.cost_usd",
|
|
36
|
+
image_cost_usd: "request_logs.image_cost_usd",
|
|
37
|
+
image_cost_method: "request_logs.image_cost_method",
|
|
36
38
|
effort: "request_logs.effort",
|
|
37
39
|
history: "request_logs.history",
|
|
38
40
|
status: "request_logs.status",
|
|
39
41
|
error_message: "request_logs.error_message",
|
|
40
42
|
tool_call_count: "request_logs.tool_call_count",
|
|
43
|
+
is_image_generation: "request_logs.is_image_generation",
|
|
41
44
|
});
|
|
42
45
|
},
|
|
43
46
|
P: (qbWrapper: PuriWrapper<DatabaseSchemaExtend>) => {
|
|
@@ -54,9 +57,12 @@ export const requestLogSubsetQueries = {
|
|
|
54
57
|
duration_ms: "request_logs.duration_ms",
|
|
55
58
|
ttft_ms: "request_logs.ttft_ms",
|
|
56
59
|
cost_usd: "request_logs.cost_usd",
|
|
60
|
+
image_cost_usd: "request_logs.image_cost_usd",
|
|
61
|
+
image_cost_method: "request_logs.image_cost_method",
|
|
57
62
|
effort: "request_logs.effort",
|
|
58
63
|
status: "request_logs.status",
|
|
59
64
|
tool_call_count: "request_logs.tool_call_count",
|
|
65
|
+
is_image_generation: "request_logs.is_image_generation",
|
|
60
66
|
});
|
|
61
67
|
},
|
|
62
68
|
};
|
|
@@ -102,11 +102,14 @@ export const RequestLogBaseSchema = z.object({
|
|
|
102
102
|
duration_ms: z.int(),
|
|
103
103
|
ttft_ms: z.int(),
|
|
104
104
|
cost_usd: z.int().nullable(),
|
|
105
|
+
image_cost_usd: z.int().nullable(),
|
|
106
|
+
image_cost_method: z.string().max(100).nullable(),
|
|
105
107
|
effort: z.string().max(10).nullable(),
|
|
106
108
|
history: HistoryItems.nullable(),
|
|
107
109
|
status: RequestLogStatus,
|
|
108
110
|
error_message: z.string().nullable(),
|
|
109
111
|
tool_call_count: z.int(),
|
|
112
|
+
is_image_generation: z.boolean(),
|
|
110
113
|
});
|
|
111
114
|
export type RequestLogBaseSchema = z.infer<typeof RequestLogBaseSchema> & {
|
|
112
115
|
readonly __hasDefault__: readonly [
|
|
@@ -124,11 +127,14 @@ export type RequestLogBaseSchema = z.infer<typeof RequestLogBaseSchema> & {
|
|
|
124
127
|
"duration_ms",
|
|
125
128
|
"ttft_ms",
|
|
126
129
|
"cost_usd",
|
|
130
|
+
"image_cost_usd",
|
|
131
|
+
"image_cost_method",
|
|
127
132
|
"effort",
|
|
128
133
|
"history",
|
|
129
134
|
"status",
|
|
130
135
|
"error_message",
|
|
131
136
|
"tool_call_count",
|
|
137
|
+
"is_image_generation",
|
|
132
138
|
"id",
|
|
133
139
|
];
|
|
134
140
|
};
|
|
@@ -261,11 +267,14 @@ export const RequestLogSubsetA = z.object({
|
|
|
261
267
|
duration_ms: z.int(),
|
|
262
268
|
ttft_ms: z.int(),
|
|
263
269
|
cost_usd: z.int().nullable(),
|
|
270
|
+
image_cost_usd: z.int().nullable(),
|
|
271
|
+
image_cost_method: z.string().max(100).nullable(),
|
|
264
272
|
effort: z.string().max(10).nullable(),
|
|
265
273
|
history: HistoryItems.nullable(),
|
|
266
274
|
status: RequestLogStatus,
|
|
267
275
|
error_message: z.string().nullable(),
|
|
268
276
|
tool_call_count: z.int(),
|
|
277
|
+
is_image_generation: z.boolean(),
|
|
269
278
|
});
|
|
270
279
|
export type RequestLogSubsetA = z.infer<typeof RequestLogSubsetA>;
|
|
271
280
|
export const RequestLogSubsetP = z.object({
|
|
@@ -281,9 +290,12 @@ export const RequestLogSubsetP = z.object({
|
|
|
281
290
|
duration_ms: z.int(),
|
|
282
291
|
ttft_ms: z.int(),
|
|
283
292
|
cost_usd: z.int().nullable(),
|
|
293
|
+
image_cost_usd: z.int().nullable(),
|
|
294
|
+
image_cost_method: z.string().max(100).nullable(),
|
|
284
295
|
effort: z.string().max(10).nullable(),
|
|
285
296
|
status: RequestLogStatus,
|
|
286
297
|
tool_call_count: z.int(),
|
|
298
|
+
is_image_generation: z.boolean(),
|
|
287
299
|
});
|
|
288
300
|
export type RequestLogSubsetP = z.infer<typeof RequestLogSubsetP>;
|
|
289
301
|
export type RequestLogSubsetMapping = {
|
|
@@ -225,11 +225,14 @@ const entityLabels = {
|
|
|
225
225
|
"entity.RequestLog.duration_ms": "응답 소요시간(ms)",
|
|
226
226
|
"entity.RequestLog.ttft_ms": "first gen TTFT(ms)",
|
|
227
227
|
"entity.RequestLog.cost_usd": "API 비용",
|
|
228
|
+
"entity.RequestLog.image_cost_usd": "이미지 생성 추정 비용",
|
|
229
|
+
"entity.RequestLog.image_cost_method": "이미지 비용 추정 방식",
|
|
228
230
|
"entity.RequestLog.effort": "effort level",
|
|
229
231
|
"entity.RequestLog.history": "inject_items 히스토리",
|
|
230
232
|
"entity.RequestLog.status": "실행 상태",
|
|
231
233
|
"entity.RequestLog.error_message": "에러 메시지",
|
|
232
234
|
"entity.RequestLog.tool_call_count": "tool call 횟수",
|
|
235
|
+
"entity.RequestLog.is_image_generation": "이미지 생성 turn 여부 (quota 귀속용)",
|
|
233
236
|
"enum.RequestLogOrderBy.id-desc": "ID최신순",
|
|
234
237
|
"enum.RequestLogSearchField.id": "ID",
|
|
235
238
|
"enum.RequestLogSearchField.token_name": "토큰이름",
|
|
@@ -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.boolean("is_image_generation").notNullable().defaultTo(knex.raw("false"));
|
|
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("is_image_generation");
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.raw(
|
|
5
|
+
`CREATE INDEX request_log_steps_request_log_id_index ON request_log_steps USING btree(request_log_id ASC NULLS LAST);`,
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function down(knex: Knex): Promise<void> {
|
|
10
|
+
await knex.schema.alterTable("request_log_steps", (table) => {
|
|
11
|
+
table.dropIndex(["request_log_id"], "request_log_steps_request_log_id_index");
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
table.integer("image_cost_usd").nullable();
|
|
6
|
+
table.string("image_cost_method", 100).nullable();
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function down(knex: Knex): Promise<void> {
|
|
11
|
+
await knex.schema.alterTable("request_logs", (table) => {
|
|
12
|
+
table.dropColumns("image_cost_usd", "image_cost_method");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -278,6 +278,11 @@ export class AnthropicDispatcher implements ProviderDispatcher {
|
|
|
278
278
|
onDelta: (t: string) => void,
|
|
279
279
|
opts?: { includePartialMessages?: boolean },
|
|
280
280
|
): Promise<GenerateResult> {
|
|
281
|
+
// 이미지 생성은 OpenAI(codex) 경로 전용. Anthropic 라우팅에 이미지 플래그가
|
|
282
|
+
// 도달하면 조용히 텍스트로 폴백하지 않고 명시적으로 거부한다.
|
|
283
|
+
if (req.imageGeneration) {
|
|
284
|
+
throw new Error("image generation is not supported on the Anthropic route");
|
|
285
|
+
}
|
|
281
286
|
// model 정규화를 dispatcher 진입점에서 한 번 한다: result.model / runClaudeSession 전부
|
|
282
287
|
// canonical(prefix 없는 cost 키) 을 쓰게 통일. 미지정이면 ANTHROPIC_DEFAULT_MODEL — qgrid.dispatcher
|
|
283
288
|
// 의 "sonnet" 별칭 우회 차단. 정규화 규칙은 fallback 경로와 공유(canonicalAnthropicModel).
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { type JsonValue } from "../../../codex-protocol/serde_json/JsonValue";
|
|
8
8
|
import { type TokenUsageBreakdown } from "../../../codex-protocol/v2/TokenUsageBreakdown";
|
|
9
9
|
import { type UserInput } from "../../../codex-protocol/v2/UserInput";
|
|
10
|
+
import { type ImageGenerationOptions } from "../../../application/qgrid/qgrid.types";
|
|
10
11
|
|
|
11
12
|
// qgrid provider 내부 표준 usage. codex-protocol 의 TokenUsageBreakdown 은 생성 파일이므로
|
|
12
13
|
// provider 전용 cache write 세부 필드는 여기서 확장해 보존한다.
|
|
@@ -22,6 +23,13 @@ export interface ReuseThreadCoord {
|
|
|
22
23
|
epoch: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// provider-무관 이미지 결과. OpenAI(codex) 경로에서만 채워지며, 상위(qgrid.dispatcher)가
|
|
27
|
+
// content 파트로 전파한다. Anthropic 경로는 이 필드를 채우지 않는다.
|
|
28
|
+
export interface GeneratedImage {
|
|
29
|
+
data: string; // base64 PNG
|
|
30
|
+
revisedPrompt: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export interface GenerateRequest {
|
|
26
34
|
// 미지정이면 dispatcher 가 provider 별 default 를 적용한다(Anthropic: ANTHROPIC_DEFAULT_MODEL).
|
|
27
35
|
// OpenAI 경로는 항상 prefix split 후 canonical model 을 넘기므로 영향 없음.
|
|
@@ -42,6 +50,10 @@ export interface GenerateRequest {
|
|
|
42
50
|
// coldInput + coldHistory 로 폴백한다(전체 history 로 문맥 복구).
|
|
43
51
|
reuse?: ReuseThreadCoord;
|
|
44
52
|
reuseInput?: Array<UserInput>;
|
|
53
|
+
// codex 내장 image_generation tool 을 켠다(OpenAI 경로 전용, opt-in).
|
|
54
|
+
// 이 플래그가 있으면 dispatcher 는 항상 cold thread 로 실행하고 재사용 라우팅을 건너뛴다.
|
|
55
|
+
imageGeneration?: boolean;
|
|
56
|
+
imageGenerationOptions?: ImageGenerationOptions;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
export interface GenerateResult {
|
|
@@ -56,6 +68,8 @@ export interface GenerateResult {
|
|
|
56
68
|
model: string;
|
|
57
69
|
// 이번 turn 이 사용한 thread 좌표. 상위가 conv 핸들을 발급/갱신하는 데 쓴다.
|
|
58
70
|
threadCoord: ReuseThreadCoord;
|
|
71
|
+
// 이미지 turn 에서만 채워짐(OpenAI 경로). 완성 이미지가 없으면 undefined.
|
|
72
|
+
images?: GeneratedImage[];
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
// 스트림 콜백 컨테이너. 계층별로 onComplete payload 만 다르고, 스트림 이벤트 shape 는 동일하다.
|
|
@@ -15,6 +15,7 @@ import { type AccountLoginCompletedNotification } from "../../../codex-protocol/
|
|
|
15
15
|
import { type AgentMessageDeltaNotification } from "../../../codex-protocol/v2/AgentMessageDeltaNotification";
|
|
16
16
|
import { type ErrorNotification } from "../../../codex-protocol/v2/ErrorNotification";
|
|
17
17
|
import { type ItemCompletedNotification } from "../../../codex-protocol/v2/ItemCompletedNotification";
|
|
18
|
+
import { type ItemStartedNotification } from "../../../codex-protocol/v2/ItemStartedNotification";
|
|
18
19
|
import { type ThreadTokenUsageUpdatedNotification } from "../../../codex-protocol/v2/ThreadTokenUsageUpdatedNotification";
|
|
19
20
|
import { type TurnCompletedNotification } from "../../../codex-protocol/v2/TurnCompletedNotification";
|
|
20
21
|
|
|
@@ -26,6 +27,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
26
27
|
|
|
27
28
|
export type NotificationMap = {
|
|
28
29
|
"turn/completed": TurnCompletedNotification;
|
|
30
|
+
"item/started": ItemStartedNotification;
|
|
29
31
|
"item/completed": ItemCompletedNotification;
|
|
30
32
|
"item/agentMessage/delta": AgentMessageDeltaNotification;
|
|
31
33
|
"thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification;
|
|
@@ -309,3 +309,222 @@ describe("CodexAppServerWorker prompt prefix", () => {
|
|
|
309
309
|
expect(schemaTurnStart).toEqual(expect.objectContaining({ outputSchema }));
|
|
310
310
|
});
|
|
311
311
|
});
|
|
312
|
+
|
|
313
|
+
describe("CodexAppServerWorker image generation capture", () => {
|
|
314
|
+
const IMAGE_B64 = "iVBORw0KGgoBAgMEBQYHCA==";
|
|
315
|
+
|
|
316
|
+
function imageItem(id: string, result: string, revisedPrompt: string | null = null) {
|
|
317
|
+
return { type: "imageGeneration", id, status: "generating", result, revisedPrompt };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
it("captures multiple completed image items into the images array in order", async () => {
|
|
321
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
322
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
323
|
+
if (method === "turn/start") {
|
|
324
|
+
handlers.get("item/completed")?.({
|
|
325
|
+
threadId: "t1",
|
|
326
|
+
item: imageItem("img-a", IMAGE_B64, "a red circle"),
|
|
327
|
+
} as never);
|
|
328
|
+
handlers.get("item/completed")?.({
|
|
329
|
+
threadId: "t1",
|
|
330
|
+
item: imageItem("img-b", IMAGE_B64, "a blue square"),
|
|
331
|
+
} as never);
|
|
332
|
+
handlers.get("turn/completed")?.({
|
|
333
|
+
threadId: "t1",
|
|
334
|
+
turn: { status: "completed", durationMs: 100 },
|
|
335
|
+
} as never);
|
|
336
|
+
return { turn: { id: "turn-1" } };
|
|
337
|
+
}
|
|
338
|
+
throw new Error(`unexpected request: ${method}`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const result = await worker.executeTurn({
|
|
342
|
+
input: [{ type: "text", text: "two images", text_elements: [] }],
|
|
343
|
+
imageGeneration: true,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result.images).toHaveLength(2);
|
|
347
|
+
expect(result.images?.map((i) => i.revisedPrompt)).toEqual(["a red circle", "a blue square"]);
|
|
348
|
+
expect(result.images?.[0]?.data).toBe(IMAGE_B64);
|
|
349
|
+
expect(result.imageAttempted).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("treats status='generating' completed item with valid base64 as a finished image", async () => {
|
|
353
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
354
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
355
|
+
if (method === "turn/start") {
|
|
356
|
+
// 실측: 완료 item 의 status 가 "generating" 으로 와도 result 는 완성 base64.
|
|
357
|
+
handlers.get("item/completed")?.({
|
|
358
|
+
threadId: "t1",
|
|
359
|
+
item: imageItem("img-a", IMAGE_B64),
|
|
360
|
+
} as never);
|
|
361
|
+
handlers.get("turn/completed")?.({
|
|
362
|
+
threadId: "t1",
|
|
363
|
+
turn: { status: "completed", durationMs: 50 },
|
|
364
|
+
} as never);
|
|
365
|
+
return { turn: { id: "turn-1" } };
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`unexpected request: ${method}`);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = await worker.executeTurn({
|
|
371
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
372
|
+
imageGeneration: true,
|
|
373
|
+
});
|
|
374
|
+
expect(result.images).toHaveLength(1);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("dedups repeated completed events for the same item id", async () => {
|
|
378
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
379
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
380
|
+
if (method === "turn/start") {
|
|
381
|
+
handlers.get("item/completed")?.({ threadId: "t1", item: imageItem("dup", IMAGE_B64) } as never);
|
|
382
|
+
handlers.get("item/completed")?.({ threadId: "t1", item: imageItem("dup", IMAGE_B64) } as never);
|
|
383
|
+
handlers.get("turn/completed")?.({
|
|
384
|
+
threadId: "t1",
|
|
385
|
+
turn: { status: "completed", durationMs: 50 },
|
|
386
|
+
} as never);
|
|
387
|
+
return { turn: { id: "turn-1" } };
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`unexpected request: ${method}`);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const result = await worker.executeTurn({
|
|
393
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
394
|
+
imageGeneration: true,
|
|
395
|
+
});
|
|
396
|
+
expect(result.images).toHaveLength(1);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("marks imageAttempted but returns no images when only item/started fires (tool called, not finished)", async () => {
|
|
400
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
401
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
402
|
+
if (method === "turn/start") {
|
|
403
|
+
handlers.get("item/started")?.({
|
|
404
|
+
threadId: "t1",
|
|
405
|
+
item: { type: "imageGeneration", id: "img-a", status: "in_progress", result: "", revisedPrompt: null },
|
|
406
|
+
} as never);
|
|
407
|
+
handlers.get("turn/completed")?.({
|
|
408
|
+
threadId: "t1",
|
|
409
|
+
turn: { status: "completed", durationMs: 50 },
|
|
410
|
+
} as never);
|
|
411
|
+
return { turn: { id: "turn-1" } };
|
|
412
|
+
}
|
|
413
|
+
throw new Error(`unexpected request: ${method}`);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const result = await worker.executeTurn({
|
|
417
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
418
|
+
imageGeneration: true,
|
|
419
|
+
});
|
|
420
|
+
expect(result.images).toBeUndefined();
|
|
421
|
+
expect(result.imageAttempted).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("does not count an empty or invalid base64 result as a finished image", async () => {
|
|
425
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
426
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
427
|
+
if (method === "turn/start") {
|
|
428
|
+
handlers.get("item/completed")?.({ threadId: "t1", item: imageItem("empty", "") } as never);
|
|
429
|
+
handlers.get("item/completed")?.({ threadId: "t1", item: imageItem("junk", "not-base64") } as never);
|
|
430
|
+
handlers.get("turn/completed")?.({
|
|
431
|
+
threadId: "t1",
|
|
432
|
+
turn: { status: "completed", durationMs: 50 },
|
|
433
|
+
} as never);
|
|
434
|
+
return { turn: { id: "turn-1" } };
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`unexpected request: ${method}`);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const result = await worker.executeTurn({
|
|
440
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
441
|
+
imageGeneration: true,
|
|
442
|
+
});
|
|
443
|
+
expect(result.images).toBeUndefined();
|
|
444
|
+
expect(result.imageAttempted).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("discards partial images when the turn fails (no promotion to success)", async () => {
|
|
448
|
+
const { worker } = createWorkerWithNotificationRpc(async (method, _params, handlers) => {
|
|
449
|
+
if (method === "thread/start") return { thread: { id: "t1" }, model: "gpt-test" };
|
|
450
|
+
if (method === "turn/start") {
|
|
451
|
+
handlers.get("item/completed")?.({ threadId: "t1", item: imageItem("img-a", IMAGE_B64) } as never);
|
|
452
|
+
handlers.get("turn/completed")?.({
|
|
453
|
+
threadId: "t1",
|
|
454
|
+
turn: { status: "failed", error: { message: "boom" } },
|
|
455
|
+
} as never);
|
|
456
|
+
return { turn: { id: "turn-1" } };
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`unexpected request: ${method}`);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
await expect(
|
|
462
|
+
worker.executeTurn({
|
|
463
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
464
|
+
imageGeneration: true,
|
|
465
|
+
}),
|
|
466
|
+
).rejects.toThrow(/turn failed/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("does not register an image turn thread for reuse (threadMeta excluded)", async () => {
|
|
470
|
+
const spy = createWorkerWithRequestSpy();
|
|
471
|
+
type WorkerInternals = { createThread(req: unknown): Promise<{ threadId: string; model: string }> };
|
|
472
|
+
const { threadId } = await (spy.worker as unknown as WorkerInternals).createThread({
|
|
473
|
+
input: [{ type: "text", text: "img", text_elements: [] }],
|
|
474
|
+
imageGeneration: true,
|
|
475
|
+
});
|
|
476
|
+
const threadMeta = (spy.worker as unknown as { threadMeta: Map<string, unknown> }).threadMeta;
|
|
477
|
+
expect(threadMeta.has(threadId)).toBe(false);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("still registers a normal text thread for reuse (regression guard)", async () => {
|
|
481
|
+
const spy = createWorkerWithRequestSpy();
|
|
482
|
+
type WorkerInternals = { createThread(req: unknown): Promise<{ threadId: string; model: string }> };
|
|
483
|
+
const { threadId } = await (spy.worker as unknown as WorkerInternals).createThread({
|
|
484
|
+
input: [{ type: "text", text: "hi", text_elements: [] }],
|
|
485
|
+
});
|
|
486
|
+
const threadMeta = (spy.worker as unknown as { threadMeta: Map<string, unknown> }).threadMeta;
|
|
487
|
+
expect(threadMeta.has(threadId)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("CodexAppServerWorker image generation thread", () => {
|
|
492
|
+
type WorkerInternals = {
|
|
493
|
+
createThread(req: unknown): Promise<{ threadId: string; model: string }>;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
function threadStartParams(spy: ReturnType<typeof createWorkerWithRequestSpy>) {
|
|
497
|
+
return spy.request.mock.calls.find(([method]) => method === "thread/start")?.[1] as
|
|
498
|
+
| { baseInstructions?: string; config?: Record<string, unknown> }
|
|
499
|
+
| undefined;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
it("enables image_generation feature and swaps base instructions for image requests", async () => {
|
|
503
|
+
const spy = createWorkerWithRequestSpy();
|
|
504
|
+
await (spy.worker as unknown as WorkerInternals).createThread({
|
|
505
|
+
input: [{ type: "text", text: "draw a red circle", text_elements: [] }],
|
|
506
|
+
imageGeneration: true,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const params = threadStartParams(spy);
|
|
510
|
+
expect(params?.config).toEqual(
|
|
511
|
+
expect.objectContaining({ "features.image_generation": true }),
|
|
512
|
+
);
|
|
513
|
+
// "text only" 억제기가 이미지 허용 instruction 으로 교체돼야 tool 이 실제로 호출된다.
|
|
514
|
+
expect(params?.baseInstructions).toContain("image_generation tool");
|
|
515
|
+
expect(params?.baseInstructions).not.toContain("Respond with text only");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("leaves tool config and instructions untouched when the flag is absent", async () => {
|
|
519
|
+
const spy = createWorkerWithRequestSpy();
|
|
520
|
+
await (spy.worker as unknown as WorkerInternals).createThread({
|
|
521
|
+
input: [{ type: "text", text: "hello", text_elements: [] }],
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const params = threadStartParams(spy);
|
|
525
|
+
// 회귀 가드: 이미지 플래그 없는 요청은 image_generation override 를 싣지 않고,
|
|
526
|
+
// 텍스트 전용 instruction 을 유지한다(플래그 없는 경로 무변화).
|
|
527
|
+
expect(params?.config).not.toHaveProperty("features.image_generation");
|
|
528
|
+
expect(params?.baseInstructions).toContain("Respond with text only");
|
|
529
|
+
});
|
|
530
|
+
});
|