@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.
Files changed (81) hide show
  1. package/bundle/dist/application/monit/monit.frame.js +1 -1
  2. package/bundle/dist/application/qgrid/expired-token-reminder.js +1 -1
  3. package/bundle/dist/application/qgrid/oauth.js +1 -1
  4. package/bundle/dist/application/qgrid/qgrid-image-generation.js +6 -6
  5. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +24 -7
  6. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +8 -3
  7. package/bundle/dist/application/qgrid/qgrid.frame.js +25 -11
  8. package/bundle/dist/application/qgrid/qgrid.types.js +4 -8
  9. package/bundle/dist/application/qgrid/token-death.js +1 -1
  10. package/bundle/dist/application/qgrid/token-window-keepalive.js +1 -1
  11. package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
  12. package/bundle/dist/application/request-log/request-log.model.js +23 -17
  13. package/bundle/dist/i18n/sd.generated.js +3 -3
  14. package/bundle/dist/sonamu.config.js +2 -2
  15. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +1 -1
  16. package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
  17. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +31 -9
  18. package/bundle/dist/utils/providers/openai/openai-direct-client.js +5 -4
  19. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +111 -57
  20. package/bundle/dist/utils/providers/openai/openai-image-generation.js +25 -17
  21. package/bundle/dist/utils/providers/openai/openai-refresh.js +1 -1
  22. package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +34 -5
  23. package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -13
  24. package/bundle/src/application/qgrid/qgrid-response-format.test.ts +11 -0
  25. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +87 -9
  26. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +45 -5
  27. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +136 -2
  28. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +11 -1
  29. package/bundle/src/application/qgrid/qgrid.frame.test.ts +73 -0
  30. package/bundle/src/application/qgrid/qgrid.frame.ts +43 -6
  31. package/bundle/src/application/qgrid/qgrid.types.ts +14 -4
  32. package/bundle/src/application/request-log/request-log.entity.json +2 -2
  33. package/bundle/src/application/request-log/request-log.model.test.ts +17 -12
  34. package/bundle/src/application/request-log/request-log.model.ts +8 -2
  35. package/bundle/src/application/sonamu.generated.http +2 -2
  36. package/bundle/src/i18n/sd.generated.ts +2 -2
  37. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +42 -2
  38. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +46 -2
  39. package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
  40. package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
  41. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +235 -32
  42. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
  43. package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
  44. package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
  45. package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
  46. package/bundle/web-dist/client/assets/{index-reW8VvJF.js → index-D9ryIyL-.js} +4 -4
  47. package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
  48. package/bundle/web-dist/client/assets/{monit-DkABv7PG.js → monit-DojxOLj6.js} +1 -1
  49. package/bundle/web-dist/client/assets/{routes-BRmQ4M3-.js → routes-DXJhNvbI.js} +2 -2
  50. package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-DqFYgWsY.js} +1 -1
  51. package/bundle/web-dist/client/assets/{settings-BHvtycEZ.js → settings-bopyz9pa.js} +1 -1
  52. package/bundle/web-dist/client/assets/{show-qeTIzgDZ.js → show-DbLD3MzB.js} +1 -1
  53. package/bundle/web-dist/client/assets/{tokens-5Bep_G-w.js → tokens-2-AIi9sZ.js} +1 -1
  54. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cx-QO25M.js → use-oauth-login-flow-CnqTWs_H.js} +1 -1
  55. package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-Cxb9K0fM.js} +1 -1
  56. package/bundle/web-dist/client/index.html +2 -2
  57. package/bundle/web-dist/server/assets/{cost-CSH_t17I.js → cost-eEHeTRT5.js} +5 -1
  58. package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
  59. package/bundle/web-dist/server/assets/{logs-CagNGTBj.js → logs-CIVFxfoi.js} +11 -8
  60. package/bundle/web-dist/server/assets/{monit-DSxs4Jnl.js → monit-DBXezg7R.js} +4 -4
  61. package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-BH-t4cm_.js} +1 -1
  62. package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-CZ2eynd8.js} +1 -1
  63. package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-CosAdcz0.js} +1 -1
  64. package/bundle/web-dist/server/assets/{routes-i-sTqXMX.js → routes-COWYIvA-.js} +5 -6
  65. package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-607iwhMI.js} +2 -2
  66. package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-C_6dpK5p.js} +3 -3
  67. package/bundle/web-dist/server/assets/{show-C4jk1Hrp.js → show-DY396-LD.js} +8 -20
  68. package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-COvV7qUP.js} +4 -4
  69. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-VYCYf3uv.js} +1 -1
  70. package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-D8XYV5of.js} +1 -1
  71. package/bundle/web-dist/server/entry-server.generated.js +10 -10
  72. package/package.json +3 -2
  73. package/skills/qgrid/SKILL.md +9 -8
  74. package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -16
  75. package/skills/qgrid/references/decision-rationale.md +8 -8
  76. package/skills/qgrid/references/openai-codex-runtime.md +23 -11
  77. package/skills/qgrid/references/request-log-run-lifecycle.md +4 -2
  78. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
  79. package/skills/qgrid/references/verification-and-debugging.md +23 -9
  80. package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +0 -1
  81. 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("Transparent image generation does not support tools or structured output");
21
- const controls = options.imageGeneration;
22
- if (!controls || controls === true || controls.background !== "transparent") throw new OpenAIProtocolError("Standalone images require an explicit transparent background request");
23
- if (controls.output_format && controls.output_format !== "png") throw new OpenAIProtocolError("Transparent image generation only supports PNG output");
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 transparent image generation");
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 transparent image generation");
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 transparent image generation");
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 transparent image generation");
51
+ } else throw new OpenAIProtocolError("Unsupported message part for standalone image generation");
51
52
  }
52
53
  }
53
- if (images.length > 5) throw new OpenAIProtocolError("Transparent image editing supports at most 5 reference images");
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: "transparent",
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 inspectTransparentPng(base64) {
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 = inspectTransparentPng(item.b64_json);
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: "transparent",
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, inspectTransparentPng, readStandaloneImageResponse };
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/IHsgdXNhZ2U6IHBhcnNlZC51c2FnZSB9IDoge30pLFxuICAgICAgfSxcbiAgICB9O1xuICB9KTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFZQSxNQUFNLGdCQUFnQixLQUFLLE9BQU87QUFDbEMsTUFBTSxtQkFBbUIsT0FBTztBQUNoQyxNQUFNLHFCQUFxQixLQUFLLE9BQU87QUFDdkMsTUFBTSxnQkFBZ0IsT0FBTyxLQUFLO0NBQUM7Q0FBSztDQUFJO0NBQUk7Q0FBSTtDQUFJO0NBQUk7Q0FBSTtBQUFFLENBQUM7QUFFbkUsU0FBZ0IsNEJBQTRCLFNBQWlDO0NBQzNFLElBQUksUUFBUSxnQkFBZ0IsUUFBUSxPQUFPLFFBQ3pDLE1BQU0sSUFBSSxvQkFDUix5RUFDRjtDQUVGLElBQUksQ0FBQyxRQUFRLGlCQUNYLE1BQU0sSUFBSSxvQkFBb0IsMERBQTBEO0NBRTFGLE1BQU0sV0FBVyxPQUFPLFFBQVEsb0JBQW9CLFdBQVcsUUFBUSxrQkFBa0IsQ0FBQztDQUMxRixJQUFJLFFBQVEsVUFBVSxpQkFBaUIsU0FBUyxlQUFlLGVBQzdELE1BQU0sSUFBSSxvQkFDUixzRUFDRjtDQUVGLElBQUksU0FBUyxpQkFBaUIsU0FBUyxrQkFBa0IsT0FDdkQsTUFBTSxJQUFJLG9CQUFvQixzREFBc0Q7Q0FFdEYsTUFBTSxPQUFpQixDQUFDO0NBQ3hCLE1BQU0sU0FBdUMsQ0FBQztDQUM5QyxJQUFJLFFBQVEsY0FBYyxLQUFLLEtBQUsseUJBQXlCLFFBQVEsY0FBYztDQUNuRixLQUFLLE1BQU0sUUFBUSxRQUFRLFNBQVM7RUFDbEMsSUFBSSxLQUFLLFNBQVMsYUFBYTtFQUMvQixJQUFJLEtBQUssU0FBUywyQkFBMkIsT0FBTyxLQUFLLFdBQVcsVUFBVTtHQUM1RSxPQUFPLEtBQUssRUFDVixXQUFXLFFBQVEsY0FBYyxLQUFLLFFBQVEsS0FBSyxhQUFhLEVBQUUsVUFBVSxLQUFLLFNBQ25GLENBQUM7R0FDRDtFQUNGO0VBQ0EsSUFBSSxLQUFLLFNBQVMsYUFBYSxPQUFPLEtBQUssU0FBUyxVQUNsRCxNQUFNLElBQUksb0JBQ1IsK0RBQ0Y7RUFFRixJQUFJLE9BQU8sS0FBSyxZQUFZLFVBQVU7R0FDcEMsS0FBSyxLQUFLLEdBQUcsS0FBSyxRQUFRLE9BQU8sS0FBSyxLQUFLLFNBQVM7R0FDcEQ7RUFDRjtFQUNBLElBQUksQ0FBQyxNQUFNLFFBQVEsS0FBSyxPQUFPLEdBQzdCLE1BQU0sSUFBSSxvQkFBb0IsNkRBQTZEO0VBRTdGLEtBQUssTUFBTSxRQUFRLEtBQUssU0FBUztHQUMvQixJQUFJLENBQUMsUUFBUSxPQUFPLFNBQVMsVUFDM0IsTUFBTSxJQUFJLG9CQUFvQiwwREFBMEQ7R0FFMUYsSUFDRTtJQUFDO0lBQWM7SUFBZTtHQUFNLENBQUMsQ0FBQyxTQUFTLEtBQUssSUFBSSxLQUN4RCxPQUFPLEtBQUssU0FBUyxVQUVyQixLQUFLLEtBQUssR0FBRyxLQUFLLFFBQVEsT0FBTyxLQUFLLEtBQUssTUFBTTtRQUM1QyxJQUFJLEtBQUssU0FBUyxpQkFBaUIsS0FBSyxTQUFTLGFBQWE7SUFDbkUsTUFBTSxNQUFNLE9BQU8sS0FBSyxjQUFjLFdBQVcsS0FBSyxZQUFZLEtBQUssV0FBVztJQUNsRixJQUFJLE9BQU8sUUFBUSxZQUFZLENBQUMsOEJBQThCLEtBQUssR0FBRyxHQUNwRSxNQUFNLElBQUksb0JBQW9CLHdEQUF3RDtJQUV4RixPQUFPLEtBQUssRUFBRSxXQUFXLElBQUksQ0FBQztHQUNoQyxPQUNFLE1BQU0sSUFBSSxvQkFBb0IsMERBQTBEO0VBRTVGO0NBQ0Y7Q0FDQSxJQUFJLE9BQU8sU0FBUyxHQUNsQixNQUFNLElBQUksb0JBQW9CLDhEQUE4RDtDQUM5RixJQUFJLENBQUMsS0FBSyxNQUFNLFNBQVMsS0FBSyxLQUFLLENBQUMsR0FBRyxNQUFNLElBQUksb0JBQW9CLHVCQUF1QjtDQUM1RixPQUFPO0VBQ0wsS0FBSyw0QkFBNEIsUUFDL0IsZ0JBQ0EsV0FBVyxPQUFPLFNBQVMsVUFBVSxlQUN2QztFQUNBLE1BQU07R0FDSixPQUFPO0dBQ1AsUUFBUSxLQUFLLEtBQUssTUFBTTtHQUN4QixHQUFJLFNBQVMsYUFBYSxFQUFFLFlBQVksU0FBUyxXQUFXLElBQUksQ0FBQztHQUNqRSxHQUFJLFNBQVMsVUFBVSxFQUFFLFNBQVMsU0FBUyxRQUFRLElBQUksQ0FBQztHQUN4RCxHQUFJLFNBQVMsT0FBTyxFQUFFLE1BQU0sU0FBUyxLQUFLLElBQUksQ0FBQztHQUMvQyxHQUFJLE9BQU8sU0FBUyxFQUFFLE9BQU8sSUFBSSxDQUFDO0VBQ3BDO0NBQ0Y7QUFDRjtBQUVBLFNBQVMsb0JBQ1AsUUFDQSxxQkFDbUM7Q0FDbkMsSUFBSSxPQUFPLFNBQVMsS0FBSyxLQUFLLGdCQUFnQixDQUFDLElBQUksS0FBSyxDQUFDLHlCQUF5QixLQUFLLE1BQU0sR0FDM0YsTUFBTSxJQUFJLG9CQUFvQixvQ0FBb0M7Q0FFcEUsTUFBTSxRQUFRLE9BQU8sS0FBSyxRQUFRLFFBQVE7Q0FDMUMsSUFDRSxNQUFNLFNBQVMsTUFDZixDQUFDLE1BQU0sU0FBUyxHQUFHLENBQUMsQ0FBQyxDQUFDLE9BQU8sYUFBYSxLQUMxQyxNQUFNLFNBQVMsU0FBUyxJQUFJLEVBQUUsTUFBTSxRQUVwQyxNQUFNLElBQUksb0JBQW9CLHVDQUF1QztDQUV2RSxNQUFNLFFBQVEsTUFBTSxhQUFhLEVBQUU7Q0FDbkMsTUFBTSxTQUFTLE1BQU0sYUFBYSxFQUFFO0NBQ3BDLElBQUksQ0FBQyxTQUFTLENBQUMsVUFBVSxRQUFRLFNBQVMsa0JBQ3hDLE1BQU0sSUFBSSxvQkFBb0Isb0RBQW9EO0NBRXBGLElBQUk7Q0FDSixJQUFJO0VBQ0YsUUFBUSxJQUFJLEtBQUssS0FBSyxLQUFLO0NBQzdCLFFBQVE7RUFDTixNQUFNLElBQUksb0JBQW9CLDBDQUEwQztDQUMxRTtDQUNBLElBQUksQ0FBQyxxQkFBcUIsT0FBTztFQUFFLE9BQU8sTUFBTTtFQUFPLFFBQVEsTUFBTTtDQUFPO0NBRTVFLElBQUksY0FBYztDQUNsQixJQUFJLFVBQVU7Q0FDZCxLQUFLLElBQUksSUFBSSxHQUFHLElBQUksTUFBTSxLQUFLLFFBQVEsS0FBSyxHQUFHO0VBQzdDLGdCQUFnQixNQUFNLEtBQUssT0FBTztFQUNsQyxZQUFZLE1BQU0sS0FBSyxLQUFNO0VBQzdCLElBQUksZUFBZSxTQUFTLE9BQU87R0FBRSxPQUFPLE1BQU07R0FBTyxRQUFRLE1BQU07RUFBTztDQUNoRjtDQUNBLE1BQU0sSUFBSSxvQkFDUiw2RUFDRjtBQUNGO0FBRUEsZUFBc0IsNEJBQ3BCLFVBQ0EscUJBQ2tDO0NBQ2xDLElBQUksQ0FBQyxTQUFTLE1BQU0sTUFBTSxJQUFJLG9CQUFvQiw0QkFBNEI7Q0FDOUUsTUFBTSxTQUFTLFNBQVMsS0FBSyxVQUFVO0NBQ3ZDLE1BQU0sU0FBdUIsQ0FBQztDQUM5QixJQUFJLE9BQU87Q0FDWCxJQUFJO0VBQ0YsT0FBTyxNQUFNO0dBQ1gsTUFBTSxFQUFFLE1BQU0sVUFBVSxNQUFNLE9BQU8sS0FBSztHQUMxQyxJQUFJLE1BQU07R0FDVixRQUFRLE1BQU07R0FDZCxJQUFJLE9BQU8sb0JBQW9CO0lBQzdCLE1BQU0sT0FBTyxPQUFPO0lBQ3BCLE1BQU0sSUFBSSxvQkFBb0Isd0NBQXdDO0dBQ3hFO0dBQ0EsT0FBTyxLQUFLLEtBQUs7RUFDbkI7Q0FDRixVQUFVO0VBQ1IsT0FBTyxZQUFZO0NBQ3JCO0NBQ0EsTUFBTSxTQUFTLEVBQUUsT0FBTztFQUN0QixNQUFNLEVBQ0gsTUFBTSxFQUFFLE9BQU87R0FBRSxVQUFVLEVBQUUsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDO0dBQUcsZ0JBQWdCLEVBQUUsT0FBTyxDQUFDLENBQUMsUUFBUTtFQUFFLENBQUMsQ0FBQyxDQUFDLENBQ3RGLElBQUksQ0FBQyxDQUFDLENBQ04sSUFBSSxFQUFFO0VBQ1QsU0FBUyxFQUFFLE9BQU8sQ0FBQyxDQUFDLFNBQVM7RUFDN0IsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDLFNBQVM7RUFDM0IsWUFBWSxFQUFFLE9BQU8sQ0FBQyxDQUFDLFNBQVM7RUFDaEMsT0FBTyx3QkFBd0IsTUFBTTtDQUN2QyxDQUFDO0NBQ0QsSUFBSTtDQUNKLElBQUk7RUFDRixTQUFTLE9BQU8sTUFBTSxLQUFLLE1BQU0sT0FBTyxPQUFPLE1BQU0sQ0FBQyxDQUFDLFNBQVMsTUFBTSxDQUFDLENBQUM7Q0FDMUUsUUFBUTtFQUNOLE1BQU0sSUFBSSxvQkFBb0IsbUNBQW1DO0NBQ25FO0NBQ0EsTUFBTSxhQUFhLE9BQU8sZUFBZSxzQkFBc0IsZ0JBQWdCO0NBQy9FLE9BQU8sT0FBTyxLQUFLLEtBQUssTUFBTSxVQUFVO0VBQ3RDLE1BQU0sYUFBYSxvQkFBb0IsS0FBSyxVQUFVLG1CQUFtQjtFQUN6RSxPQUFPO0dBQ0wsTUFBTTtHQUNOLFFBQVEsS0FBSztHQUNiLFVBQVU7R0FDVixHQUFJLEtBQUssaUJBQWlCLEVBQUUsZUFBZSxLQUFLLGVBQWUsSUFBSSxDQUFDO0dBQ3BFLFlBQVk7SUFDVixPQUFPO0lBQ1AsT0FBTyxPQUFPLFNBQVM7SUFDdkIsR0FBSSxhQUFhLEVBQUUsV0FBVyxJQUFJLENBQUM7SUFDbkMsTUFBTSxHQUFHLFdBQVcsTUFBTSxHQUFHLFdBQVc7SUFDeEMsR0FBSSxPQUFPLFVBQVUsRUFBRSxTQUFTLE9BQU8sUUFBUSxJQUFJLENBQUM7SUFDcEQsR0FBSSxVQUFVLEtBQUssT0FBTyxRQUFRLEVBQUUsT0FBTyxPQUFPLE1BQU0sSUFBSSxDQUFDO0dBQy9EO0VBQ0Y7Q0FDRixDQUFDO0FBQ0gifQ==
@@ -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("standalone image cost estimates", () => {
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 response usage once for multiple outputs without pricing requested settings or driver tokens", () => {
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("does not substitute requested size pricing when standalone usage is missing", () => {
41
- expect(estimateImageGenerationCostMicroUsd(result([{ type: "image", data: "png", generation }]), options)).toBeNull();
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: ImageGenerationSize = "1536x1024";
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: ImageGenerationSize;
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<ImageGenerationSize, number>
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
- size: options?.size ?? DEFAULT_IMAGE_GENERATION_SIZE,
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
- if (images.some((image) => image.generation?.route === "codex-images")) {
72
- // The standalone response's aggregate usage is carried by its first image only.
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
- return imageCount * IMAGE_OUTPUT_COST_MICRO_USD[resolved.quality][resolved.size];
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("passes configured image generation cost estimate into finishRun", async () => {
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("keeps standalone image accounting out of text usage and records response usage once", async () => {
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: "transparent",
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: "openai/gpt-5.5", imageGeneration: true,
470
- imageGenerationOptions: { quality: "high", size: "1024x1024", background: "transparent" },
474
+ prompt: "draw", model, imageGeneration: true,
475
+ imageGenerationOptions: { quality: "high", size: "1024x1024", background },
471
476
  }, queryOutput({
472
- model: "gpt-image-2", requestedModel: "gpt-5.5", costUsd: 0,
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: "openai/gpt-5.5",
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: 3_080,
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
- export async function afterQuery(
103
+ async function appendGenerateStep(
104
104
  requestLogId: number,
105
105
  stepIndex: number,
106
106
  args: QueryInput,
107
107
  result: QueryOutput,
108
- ): Promise<RunLifecycleResult> {
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(requestLogId: number, args?: QueryInput): Promise<void> {
208
- await finishTerminalRun(requestLogId, "aborted", "client disconnected", args);
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,