@cartanova/qgrid-cli 2.10.5 → 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 (79) 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 +7 -11
  5. package/bundle/dist/application/qgrid/qgrid-response-format.js +4 -4
  6. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +24 -7
  7. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +8 -3
  8. package/bundle/dist/application/qgrid/qgrid.frame.js +24 -10
  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 +2 -2
  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 +13 -2
  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 +19 -8
  23. package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -24
  24. package/bundle/src/application/qgrid/qgrid-response-format.test.ts +4 -2
  25. package/bundle/src/application/qgrid/qgrid-response-format.ts +7 -10
  26. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +87 -9
  27. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +45 -5
  28. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +136 -2
  29. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +11 -1
  30. package/bundle/src/application/qgrid/qgrid.frame.test.ts +73 -0
  31. package/bundle/src/application/qgrid/qgrid.frame.ts +39 -4
  32. package/bundle/src/application/request-log/request-log.entity.json +2 -2
  33. package/bundle/src/application/sonamu.generated.http +2 -2
  34. package/bundle/src/i18n/sd.generated.ts +2 -2
  35. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -1
  36. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +24 -2
  37. package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
  38. package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
  39. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +223 -32
  40. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
  41. package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
  42. package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
  43. package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
  44. package/bundle/web-dist/client/assets/{index-Dxw7DiXF.js → index-D9ryIyL-.js} +4 -4
  45. package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
  46. package/bundle/web-dist/client/assets/{monit-E8eGW1Sj.js → monit-DojxOLj6.js} +1 -1
  47. package/bundle/web-dist/client/assets/{routes-7spLfIGZ.js → routes-DXJhNvbI.js} +2 -2
  48. package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-DqFYgWsY.js} +1 -1
  49. package/bundle/web-dist/client/assets/{settings-BdosZFNC.js → settings-bopyz9pa.js} +1 -1
  50. package/bundle/web-dist/client/assets/{show-b2PFNI3D.js → show-DbLD3MzB.js} +1 -1
  51. package/bundle/web-dist/client/assets/{tokens-CBdHb2_U.js → tokens-2-AIi9sZ.js} +1 -1
  52. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cyn3YPqW.js → use-oauth-login-flow-CnqTWs_H.js} +1 -1
  53. package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-Cxb9K0fM.js} +1 -1
  54. package/bundle/web-dist/client/index.html +2 -2
  55. package/bundle/web-dist/server/assets/{cost-59jBdx-I.js → cost-eEHeTRT5.js} +4 -5
  56. package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
  57. package/bundle/web-dist/server/assets/{logs-CaaWO9-Z.js → logs-CIVFxfoi.js} +10 -9
  58. package/bundle/web-dist/server/assets/{monit-CBil92fJ.js → monit-DBXezg7R.js} +4 -4
  59. package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-BH-t4cm_.js} +1 -1
  60. package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-CZ2eynd8.js} +1 -1
  61. package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-CosAdcz0.js} +1 -1
  62. package/bundle/web-dist/server/assets/{routes-DtVf9yqo.js → routes-COWYIvA-.js} +6 -7
  63. package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-607iwhMI.js} +2 -2
  64. package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-C_6dpK5p.js} +3 -3
  65. package/bundle/web-dist/server/assets/{show-8NNdRxhM.js → show-DY396-LD.js} +8 -20
  66. package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-COvV7qUP.js} +4 -4
  67. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-VYCYf3uv.js} +1 -1
  68. package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-D8XYV5of.js} +1 -1
  69. package/bundle/web-dist/server/entry-server.generated.js +10 -10
  70. package/package.json +1 -1
  71. package/skills/qgrid/SKILL.md +9 -8
  72. package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -16
  73. package/skills/qgrid/references/decision-rationale.md +8 -8
  74. package/skills/qgrid/references/openai-codex-runtime.md +19 -11
  75. package/skills/qgrid/references/request-log-run-lifecycle.md +4 -2
  76. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
  77. package/skills/qgrid/references/verification-and-debugging.md +23 -9
  78. package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +0 -1
  79. package/bundle/web-dist/client/assets/logs-3n_U4qnI.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,7 +16,7 @@ function result(content: QgridContent[]): QueryOutput {
16
16
  };
17
17
  }
18
18
 
19
- describe("standalone image cost estimates", () => {
19
+ describe("image cost estimates", () => {
20
20
  it("accepts size hints without promising public API resolution limits", () => {
21
21
  for (const size of ["auto", "1152x2560", "841x1870", "2160x3840"]) {
22
22
  expect(QueryInput.parse({ prompt: "draw", imageGenerationOptions: { size } }).imageGenerationOptions?.size).toBe(size);
@@ -26,14 +26,15 @@ describe("standalone image cost estimates", () => {
26
26
  }
27
27
  });
28
28
 
29
- it("leaves custom/auto size and observed hosted image costs unknown", () => {
29
+ it("uses the default size price for custom/auto hints and prices hosted output without usage", () => {
30
30
  const output = result([{ type: "image", data: "png" }]);
31
31
  for (const size of ["auto", "1152x2560"] as const) {
32
- expect(estimateImageGenerationCostMicroUsd(output, { size })).toBeNull();
32
+ expect(estimateImageGenerationCostMicroUsd(output, { size })).toBe(41_000);
33
+ expect(imageGenerationCostMethod({ size }, output)).toBe("assumed:gpt-image-2:medium:1536x1024:png");
33
34
  }
34
35
  expect(estimateImageGenerationCostMicroUsd(result([{ type: "image", data: "png", generation: {
35
36
  route: "codex-responses", size: "841x1870", quality: "low",
36
- } }]), options)).toBeNull();
37
+ } }]), options)).toBe(211_000);
37
38
  });
38
39
  it("accepts automatic image mode while preserving boolean requests", () => {
39
40
  for (const imageGeneration of [true, false, "auto"] as const) {
@@ -41,9 +42,9 @@ describe("standalone image cost estimates", () => {
41
42
  }
42
43
  expect(QueryInput.safeParse({ prompt: "chat", imageGeneration: "required" }).success).toBe(false);
43
44
  });
44
- 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) => {
45
46
  const output = result([
46
- { type: "image", data: "first", generation: { ...generation,
47
+ { type: "image", data: "first", generation: { ...generation, route,
47
48
  usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300,
48
49
  input_tokens_details: { text_tokens: 40, image_tokens: 60, cached_tokens: 50 } },
49
50
  } },
@@ -55,8 +56,18 @@ describe("standalone image cost estimates", () => {
55
56
  );
56
57
  });
57
58
 
58
- it("does not substitute requested size pricing when standalone usage is missing", () => {
59
- 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");
60
71
  });
61
72
 
62
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
- Partial<Record<ImageGenerationSize, number>>
22
+ Record<PricedImageSize, number>
22
23
  > = {
23
24
  low: {
24
25
  "1024x1024": 6_000,
@@ -40,31 +41,27 @@ 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
 
49
55
  export function imageGenerationCostMethod(
50
56
  options: ImageGenerationOptions | undefined,
51
57
  result?: QueryOutput,
52
- ): string | null {
53
- if (
54
- result?.content.some(
55
- (item) => item.type === "image" && item.generation?.route === "codex-responses",
56
- )
57
- )
58
- return null;
58
+ ): string {
59
59
  if (
60
- result?.content.some(
61
- (item) => item.type === "image" && item.generation?.route === "codex-images",
62
- )
60
+ result?.content.some((item) => item.type === "image" && item.generation?.usage !== undefined)
63
61
  ) {
64
62
  return "estimated:gpt-image-2:reported-usage:public-prices:conservative";
65
63
  }
66
64
  const resolved = resolveImageGenerationOptions(options);
67
- if (IMAGE_OUTPUT_COST_MICRO_USD[resolved.quality][resolved.size] === undefined) return null;
68
65
  return `assumed:${CODEX_IMAGE_GENERATION_MODEL}:${resolved.quality}:${resolved.size}:png`;
69
66
  }
70
67
 
@@ -75,14 +72,10 @@ export function estimateImageGenerationCostMicroUsd(
75
72
  const images = result.content.filter((item) => item.type === "image");
76
73
  const imageCount = images.length;
77
74
  if (imageCount === 0) return null;
78
- // Hosted Responses reports observed settings but no trustworthy image usage
79
- // or guaranteed image model. Requested size/quality cannot price that output.
80
- if (images.some((image) => image.generation?.route === "codex-responses")) return null;
81
- if (images.some((image) => image.generation?.route === "codex-images")) {
82
- // 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.
83
78
  // Do not multiply it by output count or price it as the requested text driver.
84
- const usage = images.find((image) => image.generation?.usage)?.generation?.usage;
85
- if (!usage) return null;
86
79
  // Public GPT Image 2 rates verified 2026-09-08: text input $5/M, image input
87
80
  // $8/M, output $30/M. Amounts below are microUSD, so no /1M conversion.
88
81
  // https://developers.openai.com/api/docs/pricing#image-generation
@@ -99,5 +92,5 @@ export function estimateImageGenerationCostMicroUsd(
99
92
  }
100
93
  const resolved = resolveImageGenerationOptions(options);
101
94
  const unitCost = IMAGE_OUTPUT_COST_MICRO_USD[resolved.quality][resolved.size];
102
- return unitCost === undefined ? null : imageCount * unitCost;
95
+ return imageCount * unitCost;
103
96
  }
@@ -19,14 +19,16 @@ 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 hosted observations separate without inventing a price assumption", () => {
22
+ it("keeps custom requests and observations separate from fallback price assumptions", () => {
23
23
  const args = { prompt: "draw in 9:20", imageGenerationOptions: { size: "1152x2560" } };
24
24
  const generation = { route: "codex-responses" as const, size: "841x1870", quality: "low" };
25
25
  const observed = JSON.parse(imageGenerationToolArgs(args, { generation }));
26
26
  expect(observed.requestedOptions.size).toBe("1152x2560");
27
27
  expect(observed.observedGeneration).toEqual(generation);
28
28
  expect(observed).not.toHaveProperty("pricingAssumption");
29
- expect(JSON.parse(imageGenerationToolArgs(args))).not.toHaveProperty("pricingAssumption");
29
+ expect(JSON.parse(imageGenerationToolArgs(args)).pricingAssumption).toEqual({
30
+ model: "gpt-image-2", quality: "medium", size: "1536x1024",
31
+ });
30
32
  });
31
33
  it("logs the returned MIME and requested output format", () => {
32
34
  expect(formatImagePartForLog({ type: "image", data: "webp", mediaType: "image/webp" })).toContain("data:image/webp;base64,webp");
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  CODEX_IMAGE_GENERATION_MODEL,
3
- imageGenerationCostMethod,
4
3
  resolveImageGenerationOptions,
5
4
  } from "./qgrid-image-generation";
6
5
  import {
@@ -63,15 +62,13 @@ export function imageGenerationToolArgs(
63
62
  ...(args.imageGenerationOptions ? { requestedOptions: args.imageGenerationOptions } : {}),
64
63
  ...(options.generation
65
64
  ? { observedGeneration: options.generation }
66
- : imageGenerationCostMethod(args.imageGenerationOptions)
67
- ? {
68
- pricingAssumption: {
69
- model: CODEX_IMAGE_GENERATION_MODEL,
70
- quality: resolved.quality,
71
- size: resolved.size,
72
- },
73
- }
74
- : {}),
65
+ : {
66
+ pricingAssumption: {
67
+ model: CODEX_IMAGE_GENERATION_MODEL,
68
+ quality: resolved.quality,
69
+ size: resolved.size,
70
+ },
71
+ }),
75
72
  });
76
73
  }
77
74
 
@@ -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",