@cartanova/qgrid-cli 2.10.4 → 2.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/application/monit/monit.frame.js +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +1 -1
- package/bundle/dist/application/qgrid/oauth.js +1 -1
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +6 -6
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +24 -7
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +8 -3
- package/bundle/dist/application/qgrid/qgrid.frame.js +25 -11
- package/bundle/dist/application/qgrid/qgrid.types.js +4 -8
- package/bundle/dist/application/qgrid/token-death.js +1 -1
- package/bundle/dist/application/qgrid/token-window-keepalive.js +1 -1
- package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
- package/bundle/dist/application/request-log/request-log.model.js +23 -17
- package/bundle/dist/i18n/sd.generated.js +3 -3
- package/bundle/dist/sonamu.config.js +2 -2
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +31 -9
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +5 -4
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +111 -57
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +25 -17
- package/bundle/dist/utils/providers/openai/openai-refresh.js +1 -1
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +34 -5
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -13
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +11 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +87 -9
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +45 -5
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +136 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +11 -1
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +73 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +43 -6
- package/bundle/src/application/qgrid/qgrid.types.ts +14 -4
- package/bundle/src/application/request-log/request-log.entity.json +2 -2
- 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/sonamu.generated.http +2 -2
- package/bundle/src/i18n/sd.generated.ts +2 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +42 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +46 -2
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +235 -32
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
- package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
- package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
- package/bundle/web-dist/client/assets/{index-reW8VvJF.js → index-D9ryIyL-.js} +4 -4
- package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
- package/bundle/web-dist/client/assets/{monit-DkABv7PG.js → monit-DojxOLj6.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-BRmQ4M3-.js → routes-DXJhNvbI.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-DqFYgWsY.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-BHvtycEZ.js → settings-bopyz9pa.js} +1 -1
- package/bundle/web-dist/client/assets/{show-qeTIzgDZ.js → show-DbLD3MzB.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-5Bep_G-w.js → tokens-2-AIi9sZ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cx-QO25M.js → use-oauth-login-flow-CnqTWs_H.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-Cxb9K0fM.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{cost-CSH_t17I.js → cost-eEHeTRT5.js} +5 -1
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CagNGTBj.js → logs-CIVFxfoi.js} +11 -8
- package/bundle/web-dist/server/assets/{monit-DSxs4Jnl.js → monit-DBXezg7R.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-BH-t4cm_.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-CZ2eynd8.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-CosAdcz0.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-i-sTqXMX.js → routes-COWYIvA-.js} +5 -6
- package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-607iwhMI.js} +2 -2
- package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-C_6dpK5p.js} +3 -3
- package/bundle/web-dist/server/assets/{show-C4jk1Hrp.js → show-DY396-LD.js} +8 -20
- package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-COvV7qUP.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-VYCYf3uv.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-D8XYV5of.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +3 -2
- package/skills/qgrid/SKILL.md +9 -8
- package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -16
- package/skills/qgrid/references/decision-rationale.md +8 -8
- package/skills/qgrid/references/openai-codex-runtime.md +23 -11
- package/skills/qgrid/references/request-log-run-lifecycle.md +4 -2
- package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
- package/skills/qgrid/references/verification-and-debugging.md +23 -9
- package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +0 -1
- package/bundle/web-dist/client/assets/logs-DDIt97D1.js +0 -1
|
@@ -17,10 +17,11 @@ const PNG_SIGNATURE = Buffer.from([
|
|
|
17
17
|
10
|
|
18
18
|
]);
|
|
19
19
|
function buildStandaloneImageRequest(options) {
|
|
20
|
-
if (options.outputSchema || options.tools?.length) throw new OpenAIProtocolError("
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
20
|
+
if (options.outputSchema || options.tools?.length) throw new OpenAIProtocolError("Standalone image generation does not support tools or structured output");
|
|
21
|
+
if (!options.imageGeneration) throw new OpenAIProtocolError("Standalone images require image generation to be enabled");
|
|
22
|
+
const controls = typeof options.imageGeneration === "object" ? options.imageGeneration : {};
|
|
23
|
+
if (options.model !== "gpt-image-2" && controls.background !== "transparent") throw new OpenAIProtocolError("Standalone images require an explicit transparent background request");
|
|
24
|
+
if (controls.output_format && controls.output_format !== "png") throw new OpenAIProtocolError("Standalone image generation only supports PNG output");
|
|
24
25
|
const text = [];
|
|
25
26
|
const images = [];
|
|
26
27
|
if (options.instructions) text.push(`System instructions:\n${options.instructions}`);
|
|
@@ -30,14 +31,14 @@ function buildStandaloneImageRequest(options) {
|
|
|
30
31
|
images.push({ image_url: `data:${imageMimeType(item.result, item.output_format)};base64,${item.result}` });
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
|
-
if (item.type !== "message" && typeof item.role !== "string") throw new OpenAIProtocolError("Unsupported conversation item for
|
|
34
|
+
if (item.type !== "message" && typeof item.role !== "string") throw new OpenAIProtocolError("Unsupported conversation item for standalone image generation");
|
|
34
35
|
if (typeof item.content === "string") {
|
|
35
36
|
text.push(`${item.role ?? "user"}:\n${item.content}`);
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
|
-
if (!Array.isArray(item.content)) throw new OpenAIProtocolError("Unsupported message content for
|
|
39
|
+
if (!Array.isArray(item.content)) throw new OpenAIProtocolError("Unsupported message content for standalone image generation");
|
|
39
40
|
for (const part of item.content) {
|
|
40
|
-
if (!part || typeof part !== "object") throw new OpenAIProtocolError("Unsupported message part for
|
|
41
|
+
if (!part || typeof part !== "object") throw new OpenAIProtocolError("Unsupported message part for standalone image generation");
|
|
41
42
|
if ([
|
|
42
43
|
"input_text",
|
|
43
44
|
"output_text",
|
|
@@ -47,24 +48,24 @@ function buildStandaloneImageRequest(options) {
|
|
|
47
48
|
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
48
49
|
if (typeof url !== "string" || !/^(https:\/\/|data:image\/)/i.test(url)) throw new OpenAIProtocolError("Image references must be HTTPS URLs or image data URLs");
|
|
49
50
|
images.push({ image_url: url });
|
|
50
|
-
} else throw new OpenAIProtocolError("Unsupported message part for
|
|
51
|
+
} else throw new OpenAIProtocolError("Unsupported message part for standalone image generation");
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
if (images.length > 5) throw new OpenAIProtocolError("
|
|
54
|
+
if (images.length > 5) throw new OpenAIProtocolError("Standalone image editing supports at most 5 reference images");
|
|
54
55
|
if (!text.some((part) => part.trim())) throw new OpenAIProtocolError("Image prompt is empty");
|
|
55
56
|
return {
|
|
56
57
|
url: CHATGPT_CODEX_RESPONSES_URL.replace(/\/responses$/, `/images/${images.length ? "edits" : "generations"}`),
|
|
57
58
|
body: {
|
|
58
59
|
model: "gpt-image-2",
|
|
59
60
|
prompt: text.join("\n\n"),
|
|
60
|
-
background:
|
|
61
|
+
...controls.background ? { background: controls.background } : {},
|
|
61
62
|
...controls.quality ? { quality: controls.quality } : {},
|
|
62
63
|
...controls.size ? { size: controls.size } : {},
|
|
63
64
|
...images.length ? { images } : {}
|
|
64
65
|
}
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
|
-
function
|
|
68
|
+
function inspectGeneratedPng(base64, requireTransparency) {
|
|
68
69
|
if (base64.length > Math.ceil(MAX_PNG_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) throw new OpenAIProtocolError("Invalid or oversized generated PNG");
|
|
69
70
|
const bytes = Buffer.from(base64, "base64");
|
|
70
71
|
if (bytes.length < 33 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE) || bytes.toString("ascii", 12, 16) !== "IHDR") throw new OpenAIProtocolError("Image generation did not return a PNG");
|
|
@@ -77,6 +78,10 @@ function inspectTransparentPng(base64) {
|
|
|
77
78
|
} catch {
|
|
78
79
|
throw new OpenAIProtocolError("Image generation returned an invalid PNG");
|
|
79
80
|
}
|
|
81
|
+
if (!requireTransparency) return {
|
|
82
|
+
width: image.width,
|
|
83
|
+
height: image.height
|
|
84
|
+
};
|
|
80
85
|
let transparent = false;
|
|
81
86
|
let visible = false;
|
|
82
87
|
for (let i = 3; i < image.data.length; i += 4) {
|
|
@@ -89,7 +94,7 @@ function inspectTransparentPng(base64) {
|
|
|
89
94
|
}
|
|
90
95
|
throw new OpenAIProtocolError("Image generation did not return visible content on a transparent background");
|
|
91
96
|
}
|
|
92
|
-
async function readStandaloneImageResponse(response) {
|
|
97
|
+
async function readStandaloneImageResponse(response, requireTransparency) {
|
|
93
98
|
if (!response.body) throw new OpenAIProtocolError("Image response had no body");
|
|
94
99
|
const reader = response.body.getReader();
|
|
95
100
|
const chunks = [];
|
|
@@ -114,6 +119,8 @@ async function readStandaloneImageResponse(response) {
|
|
|
114
119
|
revised_prompt: z.string().nullish()
|
|
115
120
|
})).min(1).max(16),
|
|
116
121
|
quality: z.string().optional(),
|
|
122
|
+
model: z.string().optional(),
|
|
123
|
+
background: z.string().optional(),
|
|
117
124
|
usage: ImageGenerationMetadata.shape.usage
|
|
118
125
|
});
|
|
119
126
|
let parsed;
|
|
@@ -122,8 +129,9 @@ async function readStandaloneImageResponse(response) {
|
|
|
122
129
|
} catch {
|
|
123
130
|
throw new OpenAIProtocolError("Invalid standalone image response");
|
|
124
131
|
}
|
|
132
|
+
const background = parsed.background ?? (requireTransparency ? "transparent" : void 0);
|
|
125
133
|
return parsed.data.map((item, index) => {
|
|
126
|
-
const dimensions =
|
|
134
|
+
const dimensions = inspectGeneratedPng(item.b64_json, requireTransparency);
|
|
127
135
|
return {
|
|
128
136
|
type: "image",
|
|
129
137
|
base64: item.b64_json,
|
|
@@ -131,8 +139,8 @@ async function readStandaloneImageResponse(response) {
|
|
|
131
139
|
...item.revised_prompt ? { revisedPrompt: item.revised_prompt } : {},
|
|
132
140
|
generation: {
|
|
133
141
|
route: "codex-images",
|
|
134
|
-
model: "gpt-image-2",
|
|
135
|
-
background:
|
|
142
|
+
model: parsed.model ?? "gpt-image-2",
|
|
143
|
+
...background ? { background } : {},
|
|
136
144
|
size: `${dimensions.width}x${dimensions.height}`,
|
|
137
145
|
...parsed.quality ? { quality: parsed.quality } : {},
|
|
138
146
|
...index === 0 && parsed.usage ? { usage: parsed.usage } : {}
|
|
@@ -141,6 +149,6 @@ async function readStandaloneImageResponse(response) {
|
|
|
141
149
|
});
|
|
142
150
|
}
|
|
143
151
|
//#endregion
|
|
144
|
-
export { buildStandaloneImageRequest,
|
|
152
|
+
export { buildStandaloneImageRequest, readStandaloneImageResponse };
|
|
145
153
|
|
|
146
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLWltYWdlLWdlbmVyYXRpb24uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWltYWdlLWdlbmVyYXRpb24udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUE5HIH0gZnJvbSBcInBuZ2pzXCI7XG5pbXBvcnQgeyB6IH0gZnJvbSBcInpvZFwiO1xuXG5pbXBvcnQgeyBJbWFnZUdlbmVyYXRpb25NZXRhZGF0YSB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC50eXBlc1wiO1xuaW1wb3J0IHtcbiAgQ0hBVEdQVF9DT0RFWF9SRVNQT05TRVNfVVJMLFxuICBpbWFnZU1pbWVUeXBlLFxuICBPcGVuQUlQcm90b2NvbEVycm9yLFxuICB0eXBlIE9wZW5BSU5vcm1hbGl6ZWRFdmVudCxcbiAgdHlwZSBPcGVuQUlSZXNwb25zZXNPcHRpb25zLFxufSBmcm9tIFwiLi9vcGVuYWktYmFja2VuZC1wcm90b2NvbFwiO1xuXG5jb25zdCBNQVhfUE5HX0JZVEVTID0gMzIgKiAxMDI0ICogMTAyNDtcbmNvbnN0IE1BWF9JTUFHRV9QSVhFTFMgPSA0MDk2ICogNDA5NjtcbmNvbnN0IE1BWF9SRVNQT05TRV9CWVRFUyA9IDY0ICogMTAyNCAqIDEwMjQ7XG5jb25zdCBQTkdfU0lHTkFUVVJFID0gQnVmZmVyLmZyb20oWzEzNywgODAsIDc4LCA3MSwgMTMsIDEwLCAyNiwgMTBdKTtcblxuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkU3RhbmRhbG9uZUltYWdlUmVxdWVzdChvcHRpb25zOiBPcGVuQUlSZXNwb25zZXNPcHRpb25zKSB7XG4gIGlmIChvcHRpb25zLm91dHB1dFNjaGVtYSB8fCBvcHRpb25zLnRvb2xzPy5sZW5ndGgpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICAgIFwiVHJhbnNwYXJlbnQgaW1hZ2UgZ2VuZXJhdGlvbiBkb2VzIG5vdCBzdXBwb3J0IHRvb2xzIG9yIHN0cnVjdHVyZWQgb3V0cHV0XCIsXG4gICAgKTtcbiAgfVxuICBjb25zdCBjb250cm9scyA9IG9wdGlvbnMuaW1hZ2VHZW5lcmF0aW9uO1xuICBpZiAoIWNvbnRyb2xzIHx8IGNvbnRyb2xzID09PSB0cnVlIHx8IGNvbnRyb2xzLmJhY2tncm91bmQgIT09IFwidHJhbnNwYXJlbnRcIikge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFxuICAgICAgXCJTdGFuZGFsb25lIGltYWdlcyByZXF1aXJlIGFuIGV4cGxpY2l0IHRyYW5zcGFyZW50IGJhY2tncm91bmQgcmVxdWVzdFwiLFxuICAgICk7XG4gIH1cbiAgaWYgKGNvbnRyb2xzLm91dHB1dF9mb3JtYXQgJiYgY29udHJvbHMub3V0cHV0X2Zvcm1hdCAhPT0gXCJwbmdcIikge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiVHJhbnNwYXJlbnQgaW1hZ2UgZ2VuZXJhdGlvbiBvbmx5IHN1cHBvcnRzIFBORyBvdXRwdXRcIik7XG4gIH1cbiAgY29uc3QgdGV4dDogc3RyaW5nW10gPSBbXTtcbiAgY29uc3QgaW1hZ2VzOiBBcnJheTx7IGltYWdlX3VybDogc3RyaW5nIH0+ID0gW107XG4gIGlmIChvcHRpb25zLmluc3RydWN0aW9ucykgdGV4dC5wdXNoKGBTeXN0ZW0gaW5zdHJ1Y3Rpb25zOlxcbiR7b3B0aW9ucy5pbnN0cnVjdGlvbnN9YCk7XG4gIGZvciAoY29uc3QgaXRlbSBvZiBvcHRpb25zLmhpc3RvcnkpIHtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcInJlYXNvbmluZ1wiKSBjb250aW51ZTtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcImltYWdlX2dlbmVyYXRpb25fY2FsbFwiICYmIHR5cGVvZiBpdGVtLnJlc3VsdCA9PT0gXCJzdHJpbmdcIikge1xuICAgICAgaW1hZ2VzLnB1c2goe1xuICAgICAgICBpbWFnZV91cmw6IGBkYXRhOiR7aW1hZ2VNaW1lVHlwZShpdGVtLnJlc3VsdCwgaXRlbS5vdXRwdXRfZm9ybWF0KX07YmFzZTY0LCR7aXRlbS5yZXN1bHR9YCxcbiAgICAgIH0pO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGlmIChpdGVtLnR5cGUgIT09IFwibWVzc2FnZVwiICYmIHR5cGVvZiBpdGVtLnJvbGUgIT09IFwic3RyaW5nXCIpIHtcbiAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFxuICAgICAgICBcIlVuc3VwcG9ydGVkIGNvbnZlcnNhdGlvbiBpdGVtIGZvciB0cmFuc3BhcmVudCBpbWFnZSBnZW5lcmF0aW9uXCIsXG4gICAgICApO1xuICAgIH1cbiAgICBpZiAodHlwZW9mIGl0ZW0uY29udGVudCA9PT0gXCJzdHJpbmdcIikge1xuICAgICAgdGV4dC5wdXNoKGAke2l0ZW0ucm9sZSA/PyBcInVzZXJcIn06XFxuJHtpdGVtLmNvbnRlbnR9YCk7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG4gICAgaWYgKCFBcnJheS5pc0FycmF5KGl0ZW0uY29udGVudCkpIHtcbiAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiVW5zdXBwb3J0ZWQgbWVzc2FnZSBjb250ZW50IGZvciB0cmFuc3BhcmVudCBpbWFnZSBnZW5lcmF0aW9uXCIpO1xuICAgIH1cbiAgICBmb3IgKGNvbnN0IHBhcnQgb2YgaXRlbS5jb250ZW50KSB7XG4gICAgICBpZiAoIXBhcnQgfHwgdHlwZW9mIHBhcnQgIT09IFwib2JqZWN0XCIpIHtcbiAgICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIHBhcnQgZm9yIHRyYW5zcGFyZW50IGltYWdlIGdlbmVyYXRpb25cIik7XG4gICAgICB9XG4gICAgICBpZiAoXG4gICAgICAgIFtcImlucHV0X3RleHRcIiwgXCJvdXRwdXRfdGV4dFwiLCBcInRleHRcIl0uaW5jbHVkZXMocGFydC50eXBlKSAmJlxuICAgICAgICB0eXBlb2YgcGFydC50ZXh0ID09PSBcInN0cmluZ1wiXG4gICAgICApIHtcbiAgICAgICAgdGV4dC5wdXNoKGAke2l0ZW0ucm9sZSA/PyBcInVzZXJcIn06XFxuJHtwYXJ0LnRleHR9YCk7XG4gICAgICB9IGVsc2UgaWYgKHBhcnQudHlwZSA9PT0gXCJpbnB1dF9pbWFnZVwiIHx8IHBhcnQudHlwZSA9PT0gXCJpbWFnZV91cmxcIikge1xuICAgICAgICBjb25zdCB1cmwgPSB0eXBlb2YgcGFydC5pbWFnZV91cmwgPT09IFwic3RyaW5nXCIgPyBwYXJ0LmltYWdlX3VybCA6IHBhcnQuaW1hZ2VfdXJsPy51cmw7XG4gICAgICAgIGlmICh0eXBlb2YgdXJsICE9PSBcInN0cmluZ1wiIHx8ICEvXihodHRwczpcXC9cXC98ZGF0YTppbWFnZVxcLykvaS50ZXN0KHVybCkpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlZmVyZW5jZXMgbXVzdCBiZSBIVFRQUyBVUkxzIG9yIGltYWdlIGRhdGEgVVJMc1wiKTtcbiAgICAgICAgfVxuICAgICAgICBpbWFnZXMucHVzaCh7IGltYWdlX3VybDogdXJsIH0pO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIHBhcnQgZm9yIHRyYW5zcGFyZW50IGltYWdlIGdlbmVyYXRpb25cIik7XG4gICAgICB9XG4gICAgfVxuICB9XG4gIGlmIChpbWFnZXMubGVuZ3RoID4gNSlcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIlRyYW5zcGFyZW50IGltYWdlIGVkaXRpbmcgc3VwcG9ydHMgYXQgbW9zdCA1IHJlZmVyZW5jZSBpbWFnZXNcIik7XG4gIGlmICghdGV4dC5zb21lKChwYXJ0KSA9PiBwYXJ0LnRyaW0oKSkpIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiSW1hZ2UgcHJvbXB0IGlzIGVtcHR5XCIpO1xuICByZXR1cm4ge1xuICAgIHVybDogQ0hBVEdQVF9DT0RFWF9SRVNQT05TRVNfVVJMLnJlcGxhY2UoXG4gICAgICAvXFwvcmVzcG9uc2VzJC8sXG4gICAgICBgL2ltYWdlcy8ke2ltYWdlcy5sZW5ndGggPyBcImVkaXRzXCIgOiBcImdlbmVyYXRpb25zXCJ9YCxcbiAgICApLFxuICAgIGJvZHk6IHtcbiAgICAgIG1vZGVsOiBcImdwdC1pbWFnZS0yXCIsXG4gICAgICBwcm9tcHQ6IHRleHQuam9pbihcIlxcblxcblwiKSxcbiAgICAgIGJhY2tncm91bmQ6IFwidHJhbnNwYXJlbnRcIixcbiAgICAgIC4uLihjb250cm9scy5xdWFsaXR5ID8geyBxdWFsaXR5OiBjb250cm9scy5xdWFsaXR5IH0gOiB7fSksXG4gICAgICAuLi4oY29udHJvbHMuc2l6ZSA/IHsgc2l6ZTogY29udHJvbHMuc2l6ZSB9IDoge30pLFxuICAgICAgLi4uKGltYWdlcy5sZW5ndGggPyB7IGltYWdlcyB9IDoge30pLFxuICAgIH0sXG4gIH07XG59XG5cbi8vIERlY29kZSBiZWZvcmUgYWNjZXB0aW5nIHRoZSByZXN1bHQ6IGFuIGFscGhhIGNoYW5uZWwgYWxvbmUgY2FuIHN0aWxsIGJlIGZ1bGx5IG9wYXF1ZS5cbmV4cG9ydCBmdW5jdGlvbiBpbnNwZWN0VHJhbnNwYXJlbnRQbmcoYmFzZTY0OiBzdHJpbmcpOiB7IHdpZHRoOiBudW1iZXI7IGhlaWdodDogbnVtYmVyIH0ge1xuICBpZiAoYmFzZTY0Lmxlbmd0aCA+IE1hdGguY2VpbChNQVhfUE5HX0JZVEVTIC8gMykgKiA0IHx8ICEvXltBLVphLXowLTkrL10rPXswLDJ9JC8udGVzdChiYXNlNjQpKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbnZhbGlkIG9yIG92ZXJzaXplZCBnZW5lcmF0ZWQgUE5HXCIpO1xuICB9XG4gIGNvbnN0IGJ5dGVzID0gQnVmZmVyLmZyb20oYmFzZTY0LCBcImJhc2U2NFwiKTtcbiAgaWYgKFxuICAgIGJ5dGVzLmxlbmd0aCA8IDMzIHx8XG4gICAgIWJ5dGVzLnN1YmFycmF5KDAsIDgpLmVxdWFscyhQTkdfU0lHTkFUVVJFKSB8fFxuICAgIGJ5dGVzLnRvU3RyaW5nKFwiYXNjaWlcIiwgMTIsIDE2KSAhPT0gXCJJSERSXCJcbiAgKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIGRpZCBub3QgcmV0dXJuIGEgUE5HXCIpO1xuICB9XG4gIGNvbnN0IHdpZHRoID0gYnl0ZXMucmVhZFVJbnQzMkJFKDE2KTtcbiAgY29uc3QgaGVpZ2h0ID0gYnl0ZXMucmVhZFVJbnQzMkJFKDIwKTtcbiAgaWYgKCF3aWR0aCB8fCAhaGVpZ2h0IHx8IHdpZHRoICogaGVpZ2h0ID4gTUFYX0lNQUdFX1BJWEVMUykge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiR2VuZXJhdGVkIFBORyBkaW1lbnNpb25zIGV4Y2VlZCB0aGUgZGVjb2RpbmcgbGltaXRcIik7XG4gIH1cbiAgbGV0IGltYWdlOiBQTkc7XG4gIHRyeSB7XG4gICAgaW1hZ2UgPSBQTkcuc3luYy5yZWFkKGJ5dGVzKTtcbiAgfSBjYXRjaCB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIHJldHVybmVkIGFuIGludmFsaWQgUE5HXCIpO1xuICB9XG4gIGxldCB0cmFuc3BhcmVudCA9IGZhbHNlO1xuICBsZXQgdmlzaWJsZSA9IGZhbHNlO1xuICBmb3IgKGxldCBpID0gMzsgaSA8IGltYWdlLmRhdGEubGVuZ3RoOyBpICs9IDQpIHtcbiAgICB0cmFuc3BhcmVudCB8fD0gaW1hZ2UuZGF0YVtpXSA9PT0gMDtcbiAgICB2aXNpYmxlIHx8PSBpbWFnZS5kYXRhW2ldISA+IDA7XG4gICAgaWYgKHRyYW5zcGFyZW50ICYmIHZpc2libGUpIHJldHVybiB7IHdpZHRoOiBpbWFnZS53aWR0aCwgaGVpZ2h0OiBpbWFnZS5oZWlnaHQgfTtcbiAgfVxuICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICBcIkltYWdlIGdlbmVyYXRpb24gZGlkIG5vdCByZXR1cm4gdmlzaWJsZSBjb250ZW50IG9uIGEgdHJhbnNwYXJlbnQgYmFja2dyb3VuZFwiLFxuICApO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVhZFN0YW5kYWxvbmVJbWFnZVJlc3BvbnNlKFxuICByZXNwb25zZTogUmVzcG9uc2UsXG4pOiBQcm9taXNlPE9wZW5BSU5vcm1hbGl6ZWRFdmVudFtdPiB7XG4gIGlmICghcmVzcG9uc2UuYm9keSkgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSByZXNwb25zZSBoYWQgbm8gYm9keVwiKTtcbiAgY29uc3QgcmVhZGVyID0gcmVzcG9uc2UuYm9keS5nZXRSZWFkZXIoKTtcbiAgY29uc3QgY2h1bmtzOiBVaW50OEFycmF5W10gPSBbXTtcbiAgbGV0IHNpemUgPSAwO1xuICB0cnkge1xuICAgIHdoaWxlICh0cnVlKSB7XG4gICAgICBjb25zdCB7IGRvbmUsIHZhbHVlIH0gPSBhd2FpdCByZWFkZXIucmVhZCgpO1xuICAgICAgaWYgKGRvbmUpIGJyZWFrO1xuICAgICAgc2l6ZSArPSB2YWx1ZS5ieXRlTGVuZ3RoO1xuICAgICAgaWYgKHNpemUgPiBNQVhfUkVTUE9OU0VfQllURVMpIHtcbiAgICAgICAgYXdhaXQgcmVhZGVyLmNhbmNlbCgpO1xuICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlc3BvbnNlIGV4Y2VlZGVkIHRoZSBzaXplIGxpbWl0XCIpO1xuICAgICAgfVxuICAgICAgY2h1bmtzLnB1c2godmFsdWUpO1xuICAgIH1cbiAgfSBmaW5hbGx5IHtcbiAgICByZWFkZXIucmVsZWFzZUxvY2soKTtcbiAgfVxuICBjb25zdCBzY2hlbWEgPSB6Lm9iamVjdCh7XG4gICAgZGF0YTogelxuICAgICAgLmFycmF5KHoub2JqZWN0KHsgYjY0X2pzb246IHouc3RyaW5nKCkubWluKDEpLCByZXZpc2VkX3Byb21wdDogei5zdHJpbmcoKS5udWxsaXNoKCkgfSkpXG4gICAgICAubWluKDEpXG4gICAgICAubWF4KDE2KSxcbiAgICBxdWFsaXR5OiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gICAgdXNhZ2U6IEltYWdlR2VuZXJhdGlvbk1ldGFkYXRhLnNoYXBlLnVzYWdlLFxuICB9KTtcbiAgbGV0IHBhcnNlZDogei5pbmZlcjx0eXBlb2Ygc2NoZW1hPjtcbiAgdHJ5IHtcbiAgICBwYXJzZWQgPSBzY2hlbWEucGFyc2UoSlNPTi5wYXJzZShCdWZmZXIuY29uY2F0KGNodW5rcykudG9TdHJpbmcoXCJ1dGY4XCIpKSk7XG4gIH0gY2F0Y2gge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiSW52YWxpZCBzdGFuZGFsb25lIGltYWdlIHJlc3BvbnNlXCIpO1xuICB9XG4gIHJldHVybiBwYXJzZWQuZGF0YS5tYXAoKGl0ZW0sIGluZGV4KSA9PiB7XG4gICAgY29uc3QgZGltZW5zaW9ucyA9IGluc3BlY3RUcmFuc3BhcmVudFBuZyhpdGVtLmI2NF9qc29uKTtcbiAgICByZXR1cm4ge1xuICAgICAgdHlwZTogXCJpbWFnZVwiLFxuICAgICAgYmFzZTY0OiBpdGVtLmI2NF9qc29uLFxuICAgICAgbWltZVR5cGU6IFwiaW1hZ2UvcG5nXCIsXG4gICAgICAuLi4oaXRlbS5yZXZpc2VkX3Byb21wdCA/IHsgcmV2aXNlZFByb21wdDogaXRlbS5yZXZpc2VkX3Byb21wdCB9IDoge30pLFxuICAgICAgZ2VuZXJhdGlvbjoge1xuICAgICAgICByb3V0ZTogXCJjb2RleC1pbWFnZXNcIixcbiAgICAgICAgbW9kZWw6IFwiZ3B0LWltYWdlLTJcIixcbiAgICAgICAgYmFja2dyb3VuZDogXCJ0cmFuc3BhcmVudFwiLFxuICAgICAgICBzaXplOiBgJHtkaW1lbnNpb25zLndpZHRofXgke2RpbWVuc2lvbnMuaGVpZ2h0fWAsXG4gICAgICAgIC4uLihwYXJzZWQucXVhbGl0eSA/IHsgcXVhbGl0eTogcGFyc2VkLnF1YWxpdHkgfSA6IHt9KSxcbiAgICAgICAgLi4uKGluZGV4ID09PSAwICYmIHBhcnNlZC51c2FnZSA/IHsgdXNhZ2U6IHBhcnNlZC51c2FnZSB9IDoge30pLFxuICAgICAgfSxcbiAgICB9O1xuICB9KTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFZQSxNQUFNLGdCQUFnQixLQUFLLE9BQU87QUFDbEMsTUFBTSxtQkFBbUIsT0FBTztBQUNoQyxNQUFNLHFCQUFxQixLQUFLLE9BQU87QUFDdkMsTUFBTSxnQkFBZ0IsT0FBTyxLQUFLO0NBQUM7Q0FBSztDQUFJO0NBQUk7Q0FBSTtDQUFJO0NBQUk7Q0FBSTtBQUFFLENBQUM7QUFFbkUsU0FBZ0IsNEJBQTRCLFNBQWlDO0NBQzNFLElBQUksUUFBUSxnQkFBZ0IsUUFBUSxPQUFPLFFBQ3pDLE1BQU0sSUFBSSxvQkFDUiwwRUFDRjtDQUVGLE1BQU0sV0FBVyxRQUFRO0NBQ3pCLElBQUksQ0FBQyxZQUFZLGFBQWEsUUFBUSxTQUFTLGVBQWUsZUFDNUQsTUFBTSxJQUFJLG9CQUNSLHNFQUNGO0NBRUYsSUFBSSxTQUFTLGlCQUFpQixTQUFTLGtCQUFrQixPQUN2RCxNQUFNLElBQUksb0JBQW9CLHVEQUF1RDtDQUV2RixNQUFNLE9BQWlCLENBQUM7Q0FDeEIsTUFBTSxTQUF1QyxDQUFDO0NBQzlDLElBQUksUUFBUSxjQUFjLEtBQUssS0FBSyx5QkFBeUIsUUFBUSxjQUFjO0NBQ25GLEtBQUssTUFBTSxRQUFRLFFBQVEsU0FBUztFQUNsQyxJQUFJLEtBQUssU0FBUyxhQUFhO0VBQy9CLElBQUksS0FBSyxTQUFTLDJCQUEyQixPQUFPLEtBQUssV0FBVyxVQUFVO0dBQzVFLE9BQU8sS0FBSyxFQUNWLFdBQVcsUUFBUSxjQUFjLEtBQUssUUFBUSxLQUFLLGFBQWEsRUFBRSxVQUFVLEtBQUssU0FDbkYsQ0FBQztHQUNEO0VBQ0Y7RUFDQSxJQUFJLEtBQUssU0FBUyxhQUFhLE9BQU8sS0FBSyxTQUFTLFVBQ2xELE1BQU0sSUFBSSxvQkFDUixnRUFDRjtFQUVGLElBQUksT0FB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
|
|
154
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLWltYWdlLWdlbmVyYXRpb24uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWltYWdlLWdlbmVyYXRpb24udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUE5HIH0gZnJvbSBcInBuZ2pzXCI7XG5pbXBvcnQgeyB6IH0gZnJvbSBcInpvZFwiO1xuXG5pbXBvcnQgeyBJbWFnZUdlbmVyYXRpb25NZXRhZGF0YSB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC50eXBlc1wiO1xuaW1wb3J0IHtcbiAgQ0hBVEdQVF9DT0RFWF9SRVNQT05TRVNfVVJMLFxuICBpbWFnZU1pbWVUeXBlLFxuICBPcGVuQUlQcm90b2NvbEVycm9yLFxuICB0eXBlIE9wZW5BSU5vcm1hbGl6ZWRFdmVudCxcbiAgdHlwZSBPcGVuQUlSZXNwb25zZXNPcHRpb25zLFxufSBmcm9tIFwiLi9vcGVuYWktYmFja2VuZC1wcm90b2NvbFwiO1xuXG5jb25zdCBNQVhfUE5HX0JZVEVTID0gMzIgKiAxMDI0ICogMTAyNDtcbmNvbnN0IE1BWF9JTUFHRV9QSVhFTFMgPSA0MDk2ICogNDA5NjtcbmNvbnN0IE1BWF9SRVNQT05TRV9CWVRFUyA9IDY0ICogMTAyNCAqIDEwMjQ7XG5jb25zdCBQTkdfU0lHTkFUVVJFID0gQnVmZmVyLmZyb20oWzEzNywgODAsIDc4LCA3MSwgMTMsIDEwLCAyNiwgMTBdKTtcblxuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkU3RhbmRhbG9uZUltYWdlUmVxdWVzdChvcHRpb25zOiBPcGVuQUlSZXNwb25zZXNPcHRpb25zKSB7XG4gIGlmIChvcHRpb25zLm91dHB1dFNjaGVtYSB8fCBvcHRpb25zLnRvb2xzPy5sZW5ndGgpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICAgIFwiU3RhbmRhbG9uZSBpbWFnZSBnZW5lcmF0aW9uIGRvZXMgbm90IHN1cHBvcnQgdG9vbHMgb3Igc3RydWN0dXJlZCBvdXRwdXRcIixcbiAgICApO1xuICB9XG4gIGlmICghb3B0aW9ucy5pbWFnZUdlbmVyYXRpb24pIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIlN0YW5kYWxvbmUgaW1hZ2VzIHJlcXVpcmUgaW1hZ2UgZ2VuZXJhdGlvbiB0byBiZSBlbmFibGVkXCIpO1xuICB9XG4gIGNvbnN0IGNvbnRyb2xzID0gdHlwZW9mIG9wdGlvbnMuaW1hZ2VHZW5lcmF0aW9uID09PSBcIm9iamVjdFwiID8gb3B0aW9ucy5pbWFnZUdlbmVyYXRpb24gOiB7fTtcbiAgaWYgKG9wdGlvbnMubW9kZWwgIT09IFwiZ3B0LWltYWdlLTJcIiAmJiBjb250cm9scy5iYWNrZ3JvdW5kICE9PSBcInRyYW5zcGFyZW50XCIpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICAgIFwiU3RhbmRhbG9uZSBpbWFnZXMgcmVxdWlyZSBhbiBleHBsaWNpdCB0cmFuc3BhcmVudCBiYWNrZ3JvdW5kIHJlcXVlc3RcIixcbiAgICApO1xuICB9XG4gIGlmIChjb250cm9scy5vdXRwdXRfZm9ybWF0ICYmIGNvbnRyb2xzLm91dHB1dF9mb3JtYXQgIT09IFwicG5nXCIpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIlN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvbiBvbmx5IHN1cHBvcnRzIFBORyBvdXRwdXRcIik7XG4gIH1cbiAgY29uc3QgdGV4dDogc3RyaW5nW10gPSBbXTtcbiAgY29uc3QgaW1hZ2VzOiBBcnJheTx7IGltYWdlX3VybDogc3RyaW5nIH0+ID0gW107XG4gIGlmIChvcHRpb25zLmluc3RydWN0aW9ucykgdGV4dC5wdXNoKGBTeXN0ZW0gaW5zdHJ1Y3Rpb25zOlxcbiR7b3B0aW9ucy5pbnN0cnVjdGlvbnN9YCk7XG4gIGZvciAoY29uc3QgaXRlbSBvZiBvcHRpb25zLmhpc3RvcnkpIHtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcInJlYXNvbmluZ1wiKSBjb250aW51ZTtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcImltYWdlX2dlbmVyYXRpb25fY2FsbFwiICYmIHR5cGVvZiBpdGVtLnJlc3VsdCA9PT0gXCJzdHJpbmdcIikge1xuICAgICAgaW1hZ2VzLnB1c2goe1xuICAgICAgICBpbWFnZV91cmw6IGBkYXRhOiR7aW1hZ2VNaW1lVHlwZShpdGVtLnJlc3VsdCwgaXRlbS5vdXRwdXRfZm9ybWF0KX07YmFzZTY0LCR7aXRlbS5yZXN1bHR9YCxcbiAgICAgIH0pO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGlmIChpdGVtLnR5cGUgIT09IFwibWVzc2FnZVwiICYmIHR5cGVvZiBpdGVtLnJvbGUgIT09IFwic3RyaW5nXCIpIHtcbiAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFxuICAgICAgICBcIlVuc3VwcG9ydGVkIGNvbnZlcnNhdGlvbiBpdGVtIGZvciBzdGFuZGFsb25lIGltYWdlIGdlbmVyYXRpb25cIixcbiAgICAgICk7XG4gICAgfVxuICAgIGlmICh0eXBlb2YgaXRlbS5jb250ZW50ID09PSBcInN0cmluZ1wiKSB7XG4gICAgICB0ZXh0LnB1c2goYCR7aXRlbS5yb2xlID8/IFwidXNlclwifTpcXG4ke2l0ZW0uY29udGVudH1gKTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBpZiAoIUFycmF5LmlzQXJyYXkoaXRlbS5jb250ZW50KSkge1xuICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIGNvbnRlbnQgZm9yIHN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvblwiKTtcbiAgICB9XG4gICAgZm9yIChjb25zdCBwYXJ0IG9mIGl0ZW0uY29udGVudCkge1xuICAgICAgaWYgKCFwYXJ0IHx8IHR5cGVvZiBwYXJ0ICE9PSBcIm9iamVjdFwiKSB7XG4gICAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiVW5zdXBwb3J0ZWQgbWVzc2FnZSBwYXJ0IGZvciBzdGFuZGFsb25lIGltYWdlIGdlbmVyYXRpb25cIik7XG4gICAgICB9XG4gICAgICBpZiAoXG4gICAgICAgIFtcImlucHV0X3RleHRcIiwgXCJvdXRwdXRfdGV4dFwiLCBcInRleHRcIl0uaW5jbHVkZXMocGFydC50eXBlKSAmJlxuICAgICAgICB0eXBlb2YgcGFydC50ZXh0ID09PSBcInN0cmluZ1wiXG4gICAgICApIHtcbiAgICAgICAgdGV4dC5wdXNoKGAke2l0ZW0ucm9sZSA/PyBcInVzZXJcIn06XFxuJHtwYXJ0LnRleHR9YCk7XG4gICAgICB9IGVsc2UgaWYgKHBhcnQudHlwZSA9PT0gXCJpbnB1dF9pbWFnZVwiIHx8IHBhcnQudHlwZSA9PT0gXCJpbWFnZV91cmxcIikge1xuICAgICAgICBjb25zdCB1cmwgPSB0eXBlb2YgcGFydC5pbWFnZV91cmwgPT09IFwic3RyaW5nXCIgPyBwYXJ0LmltYWdlX3VybCA6IHBhcnQuaW1hZ2VfdXJsPy51cmw7XG4gICAgICAgIGlmICh0eXBlb2YgdXJsICE9PSBcInN0cmluZ1wiIHx8ICEvXihodHRwczpcXC9cXC98ZGF0YTppbWFnZVxcLykvaS50ZXN0KHVybCkpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlZmVyZW5jZXMgbXVzdCBiZSBIVFRQUyBVUkxzIG9yIGltYWdlIGRhdGEgVVJMc1wiKTtcbiAgICAgICAgfVxuICAgICAgICBpbWFnZXMucHVzaCh7IGltYWdlX3VybDogdXJsIH0pO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIHBhcnQgZm9yIHN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvblwiKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgaWYgKGltYWdlcy5sZW5ndGggPiA1KVxuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiU3RhbmRhbG9uZSBpbWFnZSBlZGl0aW5nIHN1cHBvcnRzIGF0IG1vc3QgNSByZWZlcmVuY2UgaW1hZ2VzXCIpO1xuICBpZiAoIXRleHQuc29tZSgocGFydCkgPT4gcGFydC50cmltKCkpKSB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHByb21wdCBpcyBlbXB0eVwiKTtcbiAgcmV0dXJuIHtcbiAgICB1cmw6IENIQVRHUFRfQ09ERVhfUkVTUE9OU0VTX1VSTC5yZXBsYWNlKFxuICAgICAgL1xcL3Jlc3BvbnNlcyQvLFxuICAgICAgYC9pbWFnZXMvJHtpbWFnZXMubGVuZ3RoID8gXCJlZGl0c1wiIDogXCJnZW5lcmF0aW9uc1wifWAsXG4gICAgKSxcbiAgICBib2R5OiB7XG4gICAgICBtb2RlbDogXCJncHQtaW1hZ2UtMlwiLFxuICAgICAgcHJvbXB0OiB0ZXh0LmpvaW4oXCJcXG5cXG5cIiksXG4gICAgICAuLi4oY29udHJvbHMuYmFja2dyb3VuZCA/IHsgYmFja2dyb3VuZDogY29udHJvbHMuYmFja2dyb3VuZCB9IDoge30pLFxuICAgICAgLi4uKGNvbnRyb2xzLnF1YWxpdHkgPyB7IHF1YWxpdHk6IGNvbnRyb2xzLnF1YWxpdHkgfSA6IHt9KSxcbiAgICAgIC4uLihjb250cm9scy5zaXplID8geyBzaXplOiBjb250cm9scy5zaXplIH0gOiB7fSksXG4gICAgICAuLi4oaW1hZ2VzLmxlbmd0aCA/IHsgaW1hZ2VzIH0gOiB7fSksXG4gICAgfSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gaW5zcGVjdEdlbmVyYXRlZFBuZyhcbiAgYmFzZTY0OiBzdHJpbmcsXG4gIHJlcXVpcmVUcmFuc3BhcmVuY3k6IGJvb2xlYW4sXG4pOiB7IHdpZHRoOiBudW1iZXI7IGhlaWdodDogbnVtYmVyIH0ge1xuICBpZiAoYmFzZTY0Lmxlbmd0aCA+IE1hdGguY2VpbChNQVhfUE5HX0JZVEVTIC8gMykgKiA0IHx8ICEvXltBLVphLXowLTkrL10rPXswLDJ9JC8udGVzdChiYXNlNjQpKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbnZhbGlkIG9yIG92ZXJzaXplZCBnZW5lcmF0ZWQgUE5HXCIpO1xuICB9XG4gIGNvbnN0IGJ5dGVzID0gQnVmZmVyLmZyb20oYmFzZTY0LCBcImJhc2U2NFwiKTtcbiAgaWYgKFxuICAgIGJ5dGVzLmxlbmd0aCA8IDMzIHx8XG4gICAgIWJ5dGVzLnN1YmFycmF5KDAsIDgpLmVxdWFscyhQTkdfU0lHTkFUVVJFKSB8fFxuICAgIGJ5dGVzLnRvU3RyaW5nKFwiYXNjaWlcIiwgMTIsIDE2KSAhPT0gXCJJSERSXCJcbiAgKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIGRpZCBub3QgcmV0dXJuIGEgUE5HXCIpO1xuICB9XG4gIGNvbnN0IHdpZHRoID0gYnl0ZXMucmVhZFVJbnQzMkJFKDE2KTtcbiAgY29uc3QgaGVpZ2h0ID0gYnl0ZXMucmVhZFVJbnQzMkJFKDIwKTtcbiAgaWYgKCF3aWR0aCB8fCAhaGVpZ2h0IHx8IHdpZHRoICogaGVpZ2h0ID4gTUFYX0lNQUdFX1BJWEVMUykge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiR2VuZXJhdGVkIFBORyBkaW1lbnNpb25zIGV4Y2VlZCB0aGUgZGVjb2RpbmcgbGltaXRcIik7XG4gIH1cbiAgbGV0IGltYWdlOiBQTkc7XG4gIHRyeSB7XG4gICAgaW1hZ2UgPSBQTkcuc3luYy5yZWFkKGJ5dGVzKTtcbiAgfSBjYXRjaCB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIHJldHVybmVkIGFuIGludmFsaWQgUE5HXCIpO1xuICB9XG4gIGlmICghcmVxdWlyZVRyYW5zcGFyZW5jeSkgcmV0dXJuIHsgd2lkdGg6IGltYWdlLndpZHRoLCBoZWlnaHQ6IGltYWdlLmhlaWdodCB9O1xuICAvLyBBbiBhbHBoYSBjaGFubmVsIGFsb25lIGNhbiBzdGlsbCBiZSBmdWxseSBvcGFxdWUgb3IgaGF2ZSBubyB2aXNpYmxlIGNvbnRlbnQuXG4gIGxldCB0cmFuc3BhcmVudCA9IGZhbHNlO1xuICBsZXQgdmlzaWJsZSA9IGZhbHNlO1xuICBmb3IgKGxldCBpID0gMzsgaSA8IGltYWdlLmRhdGEubGVuZ3RoOyBpICs9IDQpIHtcbiAgICB0cmFuc3BhcmVudCB8fD0gaW1hZ2UuZGF0YVtpXSA9PT0gMDtcbiAgICB2aXNpYmxlIHx8PSBpbWFnZS5kYXRhW2ldISA+IDA7XG4gICAgaWYgKHRyYW5zcGFyZW50ICYmIHZpc2libGUpIHJldHVybiB7IHdpZHRoOiBpbWFnZS53aWR0aCwgaGVpZ2h0OiBpbWFnZS5oZWlnaHQgfTtcbiAgfVxuICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICBcIkltYWdlIGdlbmVyYXRpb24gZGlkIG5vdCByZXR1cm4gdmlzaWJsZSBjb250ZW50IG9uIGEgdHJhbnNwYXJlbnQgYmFja2dyb3VuZFwiLFxuICApO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVhZFN0YW5kYWxvbmVJbWFnZVJlc3BvbnNlKFxuICByZXNwb25zZTogUmVzcG9uc2UsXG4gIHJlcXVpcmVUcmFuc3BhcmVuY3k6IGJvb2xlYW4sXG4pOiBQcm9taXNlPE9wZW5BSU5vcm1hbGl6ZWRFdmVudFtdPiB7XG4gIGlmICghcmVzcG9uc2UuYm9keSkgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSByZXNwb25zZSBoYWQgbm8gYm9keVwiKTtcbiAgY29uc3QgcmVhZGVyID0gcmVzcG9uc2UuYm9keS5nZXRSZWFkZXIoKTtcbiAgY29uc3QgY2h1bmtzOiBVaW50OEFycmF5W10gPSBbXTtcbiAgbGV0IHNpemUgPSAwO1xuICB0cnkge1xuICAgIHdoaWxlICh0cnVlKSB7XG4gICAgICBjb25zdCB7IGRvbmUsIHZhbHVlIH0gPSBhd2FpdCByZWFkZXIucmVhZCgpO1xuICAgICAgaWYgKGRvbmUpIGJyZWFrO1xuICAgICAgc2l6ZSArPSB2YWx1ZS5ieXRlTGVuZ3RoO1xuICAgICAgaWYgKHNpemUgPiBNQVhfUkVTUE9OU0VfQllURVMpIHtcbiAgICAgICAgYXdhaXQgcmVhZGVyLmNhbmNlbCgpO1xuICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlc3BvbnNlIGV4Y2VlZGVkIHRoZSBzaXplIGxpbWl0XCIpO1xuICAgICAgfVxuICAgICAgY2h1bmtzLnB1c2godmFsdWUpO1xuICAgIH1cbiAgfSBmaW5hbGx5IHtcbiAgICByZWFkZXIucmVsZWFzZUxvY2soKTtcbiAgfVxuICBjb25zdCBzY2hlbWEgPSB6Lm9iamVjdCh7XG4gICAgZGF0YTogelxuICAgICAgLmFycmF5KHoub2JqZWN0KHsgYjY0X2pzb246IHouc3RyaW5nKCkubWluKDEpLCByZXZpc2VkX3Byb21wdDogei5zdHJpbmcoKS5udWxsaXNoKCkgfSkpXG4gICAgICAubWluKDEpXG4gICAgICAubWF4KDE2KSxcbiAgICBxdWFsaXR5OiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gICAgbW9kZWw6IHouc3RyaW5nKCkub3B0aW9uYWwoKSxcbiAgICBiYWNrZ3JvdW5kOiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gICAgdXNhZ2U6IEltYWdlR2VuZXJhdGlvbk1ldGFkYXRhLnNoYXBlLnVzYWdlLFxuICB9KTtcbiAgbGV0IHBhcnNlZDogei5pbmZlcjx0eXBlb2Ygc2NoZW1hPjtcbiAgdHJ5IHtcbiAgICBwYXJzZWQgPSBzY2hlbWEucGFyc2UoSlNPTi5wYXJzZShCdWZmZXIuY29uY2F0KGNodW5rcykudG9TdHJpbmcoXCJ1dGY4XCIpKSk7XG4gIH0gY2F0Y2gge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiSW52YWxpZCBzdGFuZGFsb25lIGltYWdlIHJlc3BvbnNlXCIpO1xuICB9XG4gIGNvbnN0IGJhY2tncm91bmQgPSBwYXJzZWQuYmFja2dyb3VuZCA/PyAocmVxdWlyZVRyYW5zcGFyZW5jeSA/IFwidHJhbnNwYXJlbnRcIiA6IHVuZGVmaW5lZCk7XG4gIHJldHVybiBwYXJzZWQuZGF0YS5tYXAoKGl0ZW0sIGluZGV4KSA9PiB7XG4gICAgY29uc3QgZGltZW5zaW9ucyA9IGluc3BlY3RHZW5lcmF0ZWRQbmcoaXRlbS5iNjRfanNvbiwgcmVxdWlyZVRyYW5zcGFyZW5jeSk7XG4gICAgcmV0dXJuIHtcbiAgICAgIHR5cGU6IFwiaW1hZ2VcIixcbiAgICAgIGJhc2U2NDogaXRlbS5iNjRfanNvbixcbiAgICAgIG1pbWVUeXBlOiBcImltYWdlL3BuZ1wiLFxuICAgICAgLi4uKGl0ZW0ucmV2aXNlZF9wcm9tcHQgPyB7IHJldmlzZWRQcm9tcHQ6IGl0ZW0ucmV2aXNlZF9wcm9tcHQgfSA6IHt9KSxcbiAgICAgIGdlbmVyYXRpb246IHtcbiAgICAgICAgcm91dGU6IFwiY29kZXgtaW1hZ2VzXCIsXG4gICAgICAgIG1vZGVsOiBwYXJzZWQubW9kZWwgPz8gXCJncHQtaW1hZ2UtMlwiLFxuICAgICAgICAuLi4oYmFja2dyb3VuZCA/IHsgYmFja2dyb3VuZCB9IDoge30pLFxuICAgICAgICBzaXplOiBgJHtkaW1lbnNpb25zLndpZHRofXgke2RpbWVuc2lvbnMuaGVpZ2h0fWAsXG4gICAgICAgIC4uLihwYXJzZWQucXVhbGl0eSA/IHsgcXVhbGl0eTogcGFyc2VkLnF1YWxpdHkgfSA6IHt9KSxcbiAgICAgICAgLi4uKGluZGV4ID09PSAwICYmIHBhcnNlZC51c2FnZSA/IHsgdXNhZ2U6IHBhcnNlZC51c2FnZSB9IDoge30pLFxuICAgICAgfSxcbiAgICB9O1xuICB9KTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFZQSxNQUFNLGdCQUFnQixLQUFLLE9BQU87QUFDbEMsTUFBTSxtQkFBbUIsT0FBTztBQUNoQyxNQUFNLHFCQUFxQixLQUFLLE9BQU87QUFDdkMsTUFBTSxnQkFBZ0IsT0FBTyxLQUFLO0NBQUM7Q0FBSztDQUFJO0NBQUk7Q0FBSTtDQUFJO0NBQUk7Q0FBSTtBQUFFLENBQUM7QUFFbkUsU0FBZ0IsNEJBQTRCLFNBQWlDO0NB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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TokenModel } from "../../../application/token/token.model.js";
|
|
2
|
-
import { OPENAI_CLIENT_ID, OPENAI_TOKEN_URL } from "./openai-constants.js";
|
|
3
2
|
import { deactivateAuthDeadToken } from "../../../application/qgrid/token-death.js";
|
|
3
|
+
import { OPENAI_CLIENT_ID, OPENAI_TOKEN_URL } from "./openai-constants.js";
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
//#region src/utils/providers/openai/openai-refresh.ts
|
|
6
6
|
/**
|
|
@@ -16,16 +16,35 @@ function result(content: QgridContent[]): QueryOutput {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
describe("
|
|
19
|
+
describe("image cost estimates", () => {
|
|
20
|
+
it("accepts size hints without promising public API resolution limits", () => {
|
|
21
|
+
for (const size of ["auto", "1152x2560", "841x1870", "2160x3840"]) {
|
|
22
|
+
expect(QueryInput.parse({ prompt: "draw", imageGenerationOptions: { size } }).imageGenerationOptions?.size).toBe(size);
|
|
23
|
+
}
|
|
24
|
+
for (const size of ["9:20", "0x1024", "1.5x1024", "-1x1024", "9007199254740992x1"]) {
|
|
25
|
+
expect(QueryInput.safeParse({ prompt: "draw", imageGenerationOptions: { size } }).success).toBe(false);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("uses the default size price for custom/auto hints and prices hosted output without usage", () => {
|
|
30
|
+
const output = result([{ type: "image", data: "png" }]);
|
|
31
|
+
for (const size of ["auto", "1152x2560"] as const) {
|
|
32
|
+
expect(estimateImageGenerationCostMicroUsd(output, { size })).toBe(41_000);
|
|
33
|
+
expect(imageGenerationCostMethod({ size }, output)).toBe("assumed:gpt-image-2:medium:1536x1024:png");
|
|
34
|
+
}
|
|
35
|
+
expect(estimateImageGenerationCostMicroUsd(result([{ type: "image", data: "png", generation: {
|
|
36
|
+
route: "codex-responses", size: "841x1870", quality: "low",
|
|
37
|
+
} }]), options)).toBe(211_000);
|
|
38
|
+
});
|
|
20
39
|
it("accepts automatic image mode while preserving boolean requests", () => {
|
|
21
40
|
for (const imageGeneration of [true, false, "auto"] as const) {
|
|
22
41
|
expect(QueryInput.parse({ prompt: "chat", imageGeneration }).imageGeneration).toBe(imageGeneration);
|
|
23
42
|
}
|
|
24
43
|
expect(QueryInput.safeParse({ prompt: "chat", imageGeneration: "required" }).success).toBe(false);
|
|
25
44
|
});
|
|
26
|
-
it("uses
|
|
45
|
+
it.each(["codex-images", "codex-responses"] as const)("uses %s usage once for multiple outputs instead of size or driver tokens", (route) => {
|
|
27
46
|
const output = result([
|
|
28
|
-
{ type: "image", data: "first", generation: { ...generation,
|
|
47
|
+
{ type: "image", data: "first", generation: { ...generation, route,
|
|
29
48
|
usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300,
|
|
30
49
|
input_tokens_details: { text_tokens: 40, image_tokens: 60, cached_tokens: 50 } },
|
|
31
50
|
} },
|
|
@@ -37,8 +56,18 @@ describe("standalone image cost estimates", () => {
|
|
|
37
56
|
);
|
|
38
57
|
});
|
|
39
58
|
|
|
40
|
-
it("
|
|
41
|
-
|
|
59
|
+
it("falls back to per-image pricing when standalone usage is missing", () => {
|
|
60
|
+
const output = result([{ type: "image", data: "png", generation }]);
|
|
61
|
+
expect(estimateImageGenerationCostMicroUsd(output, options)).toBe(211_000);
|
|
62
|
+
expect(imageGenerationCostMethod(options, output)).toBe("assumed:gpt-image-2:high:1024x1024:png");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("respects reported zero usage instead of applying a per-image price", () => {
|
|
66
|
+
const output = result([{ type: "image", data: "png", generation: { ...generation,
|
|
67
|
+
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
|
68
|
+
} }]);
|
|
69
|
+
expect(estimateImageGenerationCostMicroUsd(output, options)).toBe(0);
|
|
70
|
+
expect(imageGenerationCostMethod(options, output)).toBe("estimated:gpt-image-2:reported-usage:public-prices:conservative");
|
|
42
71
|
});
|
|
43
72
|
|
|
44
73
|
it("prices unknown input remainder at the image rate and ignores inconsistent splits", () => {
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ImageGenerationOptions,
|
|
3
3
|
type ImageGenerationQuality,
|
|
4
|
-
type ImageGenerationSize,
|
|
5
4
|
type QueryOutput,
|
|
6
5
|
} from "./qgrid.types";
|
|
7
6
|
|
|
8
7
|
export const CODEX_IMAGE_GENERATION_MODEL = "gpt-image-2";
|
|
9
8
|
export const DEFAULT_IMAGE_GENERATION_QUALITY: ImageGenerationQuality = "medium";
|
|
10
|
-
export const DEFAULT_IMAGE_GENERATION_SIZE
|
|
9
|
+
export const DEFAULT_IMAGE_GENERATION_SIZE = "1536x1024";
|
|
10
|
+
|
|
11
|
+
type PricedImageSize = "1024x1024" | "1024x1536" | "1536x1024";
|
|
11
12
|
|
|
12
13
|
export type ResolvedImageGenerationOptions = {
|
|
13
14
|
quality: ImageGenerationQuality;
|
|
14
|
-
size:
|
|
15
|
+
size: PricedImageSize;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
// OpenAI gpt-image-2 image output cost estimates by quality/size:
|
|
18
19
|
// https://developers.openai.com/api/docs/guides/image-generation#calculating-costs
|
|
19
20
|
const IMAGE_OUTPUT_COST_MICRO_USD: Record<
|
|
20
21
|
ImageGenerationQuality,
|
|
21
|
-
Record<
|
|
22
|
+
Record<PricedImageSize, number>
|
|
22
23
|
> = {
|
|
23
24
|
low: {
|
|
24
25
|
"1024x1024": 6_000,
|
|
@@ -40,9 +41,14 @@ const IMAGE_OUTPUT_COST_MICRO_USD: Record<
|
|
|
40
41
|
export function resolveImageGenerationOptions(
|
|
41
42
|
options: ImageGenerationOptions | undefined,
|
|
42
43
|
): ResolvedImageGenerationOptions {
|
|
44
|
+
const size = options?.size;
|
|
43
45
|
return {
|
|
44
46
|
quality: options?.quality ?? DEFAULT_IMAGE_GENERATION_QUALITY,
|
|
45
|
-
|
|
47
|
+
// Unknown/custom sizes use the default price, without changing output hints.
|
|
48
|
+
size:
|
|
49
|
+
size === "1024x1024" || size === "1024x1536" || size === "1536x1024"
|
|
50
|
+
? size
|
|
51
|
+
: DEFAULT_IMAGE_GENERATION_SIZE,
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -51,9 +57,7 @@ export function imageGenerationCostMethod(
|
|
|
51
57
|
result?: QueryOutput,
|
|
52
58
|
): string {
|
|
53
59
|
if (
|
|
54
|
-
result?.content.some(
|
|
55
|
-
(item) => item.type === "image" && item.generation?.route === "codex-images",
|
|
56
|
-
)
|
|
60
|
+
result?.content.some((item) => item.type === "image" && item.generation?.usage !== undefined)
|
|
57
61
|
) {
|
|
58
62
|
return "estimated:gpt-image-2:reported-usage:public-prices:conservative";
|
|
59
63
|
}
|
|
@@ -68,11 +72,10 @@ export function estimateImageGenerationCostMicroUsd(
|
|
|
68
72
|
const images = result.content.filter((item) => item.type === "image");
|
|
69
73
|
const imageCount = images.length;
|
|
70
74
|
if (imageCount === 0) return null;
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
const usage = images.find((image) => image.generation?.usage)?.generation?.usage;
|
|
76
|
+
if (usage) {
|
|
77
|
+
// Response-level image usage is carried by its first image only.
|
|
73
78
|
// Do not multiply it by output count or price it as the requested text driver.
|
|
74
|
-
const usage = images.find((image) => image.generation?.usage)?.generation?.usage;
|
|
75
|
-
if (!usage) return null;
|
|
76
79
|
// Public GPT Image 2 rates verified 2026-09-08: text input $5/M, image input
|
|
77
80
|
// $8/M, output $30/M. Amounts below are microUSD, so no /1M conversion.
|
|
78
81
|
// https://developers.openai.com/api/docs/pricing#image-generation
|
|
@@ -88,5 +91,6 @@ export function estimateImageGenerationCostMicroUsd(
|
|
|
88
91
|
);
|
|
89
92
|
}
|
|
90
93
|
const resolved = resolveImageGenerationOptions(options);
|
|
91
|
-
|
|
94
|
+
const unitCost = IMAGE_OUTPUT_COST_MICRO_USD[resolved.quality][resolved.size];
|
|
95
|
+
return imageCount * unitCost;
|
|
92
96
|
}
|
|
@@ -19,6 +19,17 @@ it("logs observed standalone settings separately from requested controls without
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
describe("qgrid response log formatting", () => {
|
|
22
|
+
it("keeps custom requests and observations separate from fallback price assumptions", () => {
|
|
23
|
+
const args = { prompt: "draw in 9:20", imageGenerationOptions: { size: "1152x2560" } };
|
|
24
|
+
const generation = { route: "codex-responses" as const, size: "841x1870", quality: "low" };
|
|
25
|
+
const observed = JSON.parse(imageGenerationToolArgs(args, { generation }));
|
|
26
|
+
expect(observed.requestedOptions.size).toBe("1152x2560");
|
|
27
|
+
expect(observed.observedGeneration).toEqual(generation);
|
|
28
|
+
expect(observed).not.toHaveProperty("pricingAssumption");
|
|
29
|
+
expect(JSON.parse(imageGenerationToolArgs(args)).pricingAssumption).toEqual({
|
|
30
|
+
model: "gpt-image-2", quality: "medium", size: "1536x1024",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
22
33
|
it("logs the returned MIME and requested output format", () => {
|
|
23
34
|
expect(formatImagePartForLog({ type: "image", data: "webp", mediaType: "image/webp" })).toContain("data:image/webp;base64,webp");
|
|
24
35
|
expect(JSON.parse(imageGenerationToolArgs({ prompt: "draw", imageGenerationOptions: { outputFormat: "webp" } })).tool.outputFormat).toBe("webp");
|
|
@@ -436,13 +436,15 @@ describe("qgrid run lifecycle TTFT", () => {
|
|
|
436
436
|
);
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
-
it("
|
|
439
|
+
it("stores the hosted per-image estimate when image usage is absent", async () => {
|
|
440
440
|
await afterQuery(
|
|
441
441
|
10,
|
|
442
442
|
2,
|
|
443
443
|
{ prompt: "draw", imageGeneration: true },
|
|
444
444
|
queryOutput({
|
|
445
|
-
content: [{ type: "image", data: "iVBORw0KGgoBAgM", revisedPrompt: "green triangle"
|
|
445
|
+
content: [{ type: "image", data: "iVBORw0KGgoBAgM", revisedPrompt: "green triangle",
|
|
446
|
+
generation: { route: "codex-responses", size: "841x1870" },
|
|
447
|
+
}],
|
|
446
448
|
}),
|
|
447
449
|
);
|
|
448
450
|
|
|
@@ -456,36 +458,67 @@ describe("qgrid run lifecycle TTFT", () => {
|
|
|
456
458
|
);
|
|
457
459
|
});
|
|
458
460
|
|
|
459
|
-
it(
|
|
461
|
+
it.each([
|
|
462
|
+
["openai/gpt-5.5", "transparent"],
|
|
463
|
+
["openai/gpt-image-2", "opaque"],
|
|
464
|
+
] as const)("keeps standalone %s accounting out of text usage and records image usage once", async (model, background) => {
|
|
460
465
|
const generation = {
|
|
461
466
|
route: "codex-images" as const, model: "gpt-image-2" as const,
|
|
462
|
-
size: "1254x1254", quality: "medium", background
|
|
467
|
+
size: "1254x1254", quality: "medium", background,
|
|
463
468
|
};
|
|
464
469
|
aggregateStepUsageMock.mockResolvedValueOnce({
|
|
465
470
|
input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0,
|
|
466
471
|
duration_ms: 120, fallback_count: 0, cost_usd: 0, cost_source: "pricing_table",
|
|
467
472
|
});
|
|
468
473
|
await afterQuery(10, 0, {
|
|
469
|
-
prompt: "draw", model
|
|
470
|
-
imageGenerationOptions: { quality: "high", size: "1024x1024", background
|
|
474
|
+
prompt: "draw", model, imageGeneration: true,
|
|
475
|
+
imageGenerationOptions: { quality: "high", size: "1024x1024", background },
|
|
471
476
|
}, queryOutput({
|
|
472
|
-
model: "gpt-image-2",
|
|
477
|
+
model: "gpt-image-2", costUsd: 0,
|
|
473
478
|
usage: { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
474
479
|
content: [
|
|
475
480
|
{ type: "image", data: "first", generation: { ...generation, usage: {
|
|
476
481
|
input_tokens: 10, output_tokens: 100, total_tokens: 110,
|
|
482
|
+
input_tokens_details: { text_tokens: 4, image_tokens: 6 },
|
|
477
483
|
} } },
|
|
478
484
|
{ type: "image", data: "second", generation },
|
|
479
485
|
],
|
|
480
486
|
}));
|
|
481
487
|
expect(appendStepMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
482
|
-
type: "generate", model_name: "openai/gpt-image-2", requested_model_name:
|
|
488
|
+
type: "generate", model_name: "openai/gpt-image-2", requested_model_name: model,
|
|
483
489
|
input_tokens: 0, output_tokens: 0, cost_usd: 0,
|
|
484
490
|
}));
|
|
485
491
|
expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
486
|
-
input_tokens: 0, output_tokens: 0, cost_usd: 0, image_cost_usd:
|
|
492
|
+
input_tokens: 0, output_tokens: 0, cost_usd: 0, image_cost_usd: 3_068,
|
|
493
|
+
image_cost_method: "estimated:gpt-image-2:reported-usage:public-prices:conservative",
|
|
494
|
+
}));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("records auto chat driver cost and standalone image usage cost separately", async () => {
|
|
498
|
+
aggregateStepUsageMock.mockResolvedValueOnce({
|
|
499
|
+
input_tokens: 100, output_tokens: 20, cache_read_tokens: 0, cache_creation_tokens: 0,
|
|
500
|
+
duration_ms: 120, fallback_count: 0, cost_usd: 20, cost_source: "pricing_table",
|
|
501
|
+
});
|
|
502
|
+
await afterQuery(10, 0, {
|
|
503
|
+
prompt: "draw", model: "openai/gpt-6-luna", imageGeneration: "auto",
|
|
504
|
+
}, queryOutput({
|
|
505
|
+
model: "gpt-6-luna", costUsd: 0.00002,
|
|
506
|
+
usage: { input_tokens: 100, output_tokens: 20, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
|
|
507
|
+
content: [{ type: "image", data: "image", generation: {
|
|
508
|
+
route: "codex-images", model: "gpt-image-2", background: "opaque",
|
|
509
|
+
usage: {
|
|
510
|
+
input_tokens: 10, output_tokens: 100, total_tokens: 110,
|
|
511
|
+
input_tokens_details: { text_tokens: 4, image_tokens: 6 },
|
|
512
|
+
},
|
|
513
|
+
} }],
|
|
514
|
+
}));
|
|
515
|
+
expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
516
|
+
model_name: "openai/gpt-6-luna", requested_model_name: "openai/gpt-6-luna",
|
|
517
|
+
input_tokens: 100, output_tokens: 20, cost_usd: 20, image_cost_usd: 3_068,
|
|
518
|
+
is_image_generation: true,
|
|
487
519
|
image_cost_method: "estimated:gpt-image-2:reported-usage:public-prices:conservative",
|
|
488
520
|
}));
|
|
521
|
+
expect(appendStepMock.mock.calls.filter(([, step]) => step.type === "generate")).toHaveLength(1);
|
|
489
522
|
});
|
|
490
523
|
|
|
491
524
|
it("uses requested image quality and size for cost estimate", async () => {
|
|
@@ -609,6 +642,7 @@ describe("qgrid run lifecycle TTFT", () => {
|
|
|
609
642
|
|
|
610
643
|
describe("qgrid run lifecycle terminal rollup", () => {
|
|
611
644
|
beforeEach(() => {
|
|
645
|
+
appendStepMock.mockReset().mockResolvedValue(1);
|
|
612
646
|
aggregateStepUsageMock.mockReset().mockResolvedValue({
|
|
613
647
|
input_tokens: 120,
|
|
614
648
|
output_tokens: 30,
|
|
@@ -624,6 +658,50 @@ describe("qgrid run lifecycle terminal rollup", () => {
|
|
|
624
658
|
finishRunMock.mockReset();
|
|
625
659
|
});
|
|
626
660
|
|
|
661
|
+
it.each(["error", "aborted"] as const)("records a completed chat stage once before %s rollup", async (status) => {
|
|
662
|
+
const args = { prompt: "draw", model: "openai/gpt-6-astra", imageGeneration: "auto" as const };
|
|
663
|
+
const result = queryOutput({ model: "gpt-6-luna", requestedModel: "gpt-6-astra" });
|
|
664
|
+
const completedStep = { stepIndex: 2, result };
|
|
665
|
+
if (status === "error") await finishRunWithError(10, "image failed", args, completedStep);
|
|
666
|
+
else await finishRunAborted(10, args, completedStep);
|
|
667
|
+
expect(appendStepMock).toHaveBeenCalledOnce();
|
|
668
|
+
expect(appendStepMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
669
|
+
step_index: 2, type: "generate", model_name: "openai/gpt-6-luna",
|
|
670
|
+
requested_model_name: "openai/gpt-6-astra", input_tokens: 5, output_tokens: 7,
|
|
671
|
+
cost_usd: 1_000, cost_source: "pricing_table", ttft_ms: 39,
|
|
672
|
+
}));
|
|
673
|
+
expect(appendStepMock.mock.invocationCallOrder[0]).toBeLessThan(aggregateStepUsageMock.mock.invocationCallOrder[0]!);
|
|
674
|
+
expect(finishRunMock).toHaveBeenCalledOnce();
|
|
675
|
+
expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
676
|
+
status, error_message: status === "error" ? "image failed" : "client disconnected",
|
|
677
|
+
token_name: "tok-A", model_name: "openai/gpt-6-luna", requested_model_name: "openai/gpt-6-astra",
|
|
678
|
+
// Existing steps and the final completed driver are rolled up together.
|
|
679
|
+
input_tokens: 120, output_tokens: 30, cost_usd: 7_500, cost_source: "mixed",
|
|
680
|
+
}));
|
|
681
|
+
const finish = finishRunMock.mock.calls[0]![1];
|
|
682
|
+
expect(finish.image_cost_usd).toBeUndefined();
|
|
683
|
+
expect(finish.image_cost_method).toBeUndefined();
|
|
684
|
+
expect(finish.is_image_generation).toBeUndefined();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("still attempts aggregation and terminal status when the completed driver step cannot be logged", async () => {
|
|
688
|
+
appendStepMock.mockRejectedValueOnce(new Error("step write failed"));
|
|
689
|
+
await expect(finishRunWithError(10, "image failed", {
|
|
690
|
+
prompt: "draw", model: "openai/gpt-6-luna",
|
|
691
|
+
}, { stepIndex: 0, result: queryOutput({ model: "gpt-6-luna" }) })).resolves.toBeUndefined();
|
|
692
|
+
expect(aggregateStepUsageMock).toHaveBeenCalledWith(10);
|
|
693
|
+
expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
|
|
694
|
+
status: "error", error_message: "image failed", model_name: "openai/gpt-6-luna", token_name: "tok-A",
|
|
695
|
+
}));
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("does not replace the original failure when terminal persistence also fails", async () => {
|
|
699
|
+
finishRunMock.mockRejectedValueOnce(new Error("terminal write failed"));
|
|
700
|
+
await expect(finishRunWithError(10, "image failed", { prompt: "draw" }, {
|
|
701
|
+
stepIndex: 0, result: queryOutput(),
|
|
702
|
+
})).resolves.toBeUndefined();
|
|
703
|
+
});
|
|
704
|
+
|
|
627
705
|
it("keeps incurred step usage and cost when a run ends in error", async () => {
|
|
628
706
|
await finishRunWithError(10, "provider failed", {
|
|
629
707
|
prompt: "continue",
|
|
@@ -100,12 +100,12 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
100
100
|
return { requestLogId, stepIndex };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
async function appendGenerateStep(
|
|
104
104
|
requestLogId: number,
|
|
105
105
|
stepIndex: number,
|
|
106
106
|
args: QueryInput,
|
|
107
107
|
result: QueryOutput,
|
|
108
|
-
)
|
|
108
|
+
) {
|
|
109
109
|
// provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
|
|
110
110
|
const requestedModelName = withProviderPrefix(
|
|
111
111
|
args.model,
|
|
@@ -132,6 +132,21 @@ export async function afterQuery(
|
|
|
132
132
|
ttft_ms: result.ttftMs,
|
|
133
133
|
finish_reason: result.finishReason,
|
|
134
134
|
});
|
|
135
|
+
return { requestedModelName, servedModelName };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function afterQuery(
|
|
139
|
+
requestLogId: number,
|
|
140
|
+
stepIndex: number,
|
|
141
|
+
args: QueryInput,
|
|
142
|
+
result: QueryOutput,
|
|
143
|
+
): Promise<RunLifecycleResult> {
|
|
144
|
+
const { requestedModelName, servedModelName } = await appendGenerateStep(
|
|
145
|
+
requestLogId,
|
|
146
|
+
stepIndex,
|
|
147
|
+
args,
|
|
148
|
+
result,
|
|
149
|
+
);
|
|
135
150
|
|
|
136
151
|
const imageParts = getImageParts(result);
|
|
137
152
|
const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
|
|
@@ -200,12 +215,17 @@ export async function finishRunWithError(
|
|
|
200
215
|
requestLogId: number,
|
|
201
216
|
errorMessage: string,
|
|
202
217
|
args?: QueryInput,
|
|
218
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
203
219
|
): Promise<void> {
|
|
204
|
-
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
220
|
+
await finishTerminalRun(requestLogId, "error", errorMessage, args, completedStep);
|
|
205
221
|
}
|
|
206
222
|
|
|
207
|
-
export async function finishRunAborted(
|
|
208
|
-
|
|
223
|
+
export async function finishRunAborted(
|
|
224
|
+
requestLogId: number,
|
|
225
|
+
args?: QueryInput,
|
|
226
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args, completedStep);
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
async function finishTerminalRun(
|
|
@@ -213,8 +233,18 @@ async function finishTerminalRun(
|
|
|
213
233
|
status: "error" | "aborted",
|
|
214
234
|
errorMessage: string,
|
|
215
235
|
args?: QueryInput,
|
|
236
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
216
237
|
): Promise<void> {
|
|
217
238
|
try {
|
|
239
|
+
if (completedStep && args) {
|
|
240
|
+
try {
|
|
241
|
+
await appendGenerateStep(requestLogId, completedStep.stepIndex, args, completedStep.result);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
logger.error(
|
|
244
|
+
`appendGenerateStep failed while finishing ${status}: ${(e as Error).message}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
218
248
|
let aggregate: Awaited<ReturnType<typeof RequestLogModel.aggregateStepUsage>> | undefined;
|
|
219
249
|
try {
|
|
220
250
|
aggregate = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
@@ -226,6 +256,16 @@ async function finishTerminalRun(
|
|
|
226
256
|
status,
|
|
227
257
|
error_message: errorMessage,
|
|
228
258
|
history: args ? filterHistoryForStorage(args.history) : undefined,
|
|
259
|
+
...(completedStep
|
|
260
|
+
? {
|
|
261
|
+
token_name: completedStep.result.tokenName,
|
|
262
|
+
requested_model_name: withProviderPrefix(
|
|
263
|
+
args?.model,
|
|
264
|
+
args?.model ?? completedStep.result.requestedModel ?? completedStep.result.model,
|
|
265
|
+
),
|
|
266
|
+
model_name: withProviderPrefix(args?.model, completedStep.result.model),
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
229
269
|
...(aggregate
|
|
230
270
|
? {
|
|
231
271
|
input_tokens: aggregate.input_tokens,
|