@cartanova/qgrid-cli 2.10.5 → 2.10.7

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 (96) 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 +26 -7
  7. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +12 -7
  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 +5 -3
  13. package/bundle/dist/application/sonamu.generated.js +4 -1
  14. package/bundle/dist/application/sonamu.generated.sso.js +3 -1
  15. package/bundle/dist/i18n/sd.generated.js +4 -3
  16. package/bundle/dist/migrations/20261016155350_alter_request_logs_add1_alter1.js +15 -0
  17. package/bundle/dist/sonamu.config.js +2 -2
  18. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +1 -1
  19. package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
  20. package/bundle/dist/utils/providers/common/model-cost.js +14 -3
  21. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +13 -2
  22. package/bundle/dist/utils/providers/openai/openai-direct-client.js +5 -4
  23. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +111 -57
  24. package/bundle/dist/utils/providers/openai/openai-image-generation.js +25 -17
  25. package/bundle/dist/utils/providers/openai/openai-refresh.js +1 -1
  26. package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +19 -8
  27. package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -24
  28. package/bundle/src/application/qgrid/qgrid-response-format.test.ts +4 -2
  29. package/bundle/src/application/qgrid/qgrid-response-format.ts +7 -10
  30. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +105 -9
  31. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +47 -5
  32. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +160 -2
  33. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +40 -18
  34. package/bundle/src/application/qgrid/qgrid.frame.test.ts +74 -0
  35. package/bundle/src/application/qgrid/qgrid.frame.ts +39 -4
  36. package/bundle/src/application/request-log/request-log.entity.json +11 -2
  37. package/bundle/src/application/request-log/request-log.model.ts +4 -0
  38. package/bundle/src/application/sonamu.generated.http +2 -2
  39. package/bundle/src/application/sonamu.generated.sso.ts +2 -0
  40. package/bundle/src/application/sonamu.generated.ts +4 -0
  41. package/bundle/src/i18n/sd.generated.ts +3 -2
  42. package/bundle/src/migrations/20261016155350_alter_request_logs_add1_alter1.ts +15 -0
  43. package/bundle/src/utils/providers/common/model-cost.test.ts +28 -0
  44. package/bundle/src/utils/providers/common/model-cost.ts +19 -2
  45. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -1
  46. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +24 -2
  47. package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
  48. package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
  49. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +223 -32
  50. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
  51. package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
  52. package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
  53. package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
  54. package/bundle/web-dist/client/assets/index-Bw2s88Ot.css +1 -0
  55. package/bundle/web-dist/client/assets/{index-Dxw7DiXF.js → index-CwqqPF4F.js} +4 -4
  56. package/bundle/web-dist/client/assets/logs-CZgl1RSu.js +1 -0
  57. package/bundle/web-dist/client/assets/{monit-E8eGW1Sj.js → monit-TarvUWiE.js} +1 -1
  58. package/bundle/web-dist/client/assets/request-model-CojtinqK.js +1 -0
  59. package/bundle/web-dist/client/assets/{routes-7spLfIGZ.js → routes-BXR6OLah.js} +2 -2
  60. package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-BsieU37H.js} +1 -1
  61. package/bundle/web-dist/client/assets/{settings-BdosZFNC.js → settings-B1y5MNSU.js} +1 -1
  62. package/bundle/web-dist/client/assets/show-CG-BAdhS.js +2 -0
  63. package/bundle/web-dist/client/assets/{tokens-CBdHb2_U.js → tokens-D9tAWBEW.js} +1 -1
  64. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cyn3YPqW.js → use-oauth-login-flow-DxdOhz4a.js} +1 -1
  65. package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-DAq0F8p-.js} +1 -1
  66. package/bundle/web-dist/client/index.html +3 -3
  67. package/bundle/web-dist/server/assets/{cost-59jBdx-I.js → cost-eEHeTRT5.js} +4 -5
  68. package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Dtd0NR4r.js} +2 -2
  69. package/bundle/web-dist/server/assets/{logs-CaaWO9-Z.js → logs-l91avYif.js} +33 -21
  70. package/bundle/web-dist/server/assets/{monit-CBil92fJ.js → monit-DvzribSE.js} +4 -4
  71. package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-KZrECewT.js} +1 -1
  72. package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-B4a6-uuR.js} +1 -1
  73. package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-BND93JrF.js} +1 -1
  74. package/bundle/web-dist/server/assets/{request-model-CnCeaNLE.js → request-model-CbqfnQOJ.js} +19 -1
  75. package/bundle/web-dist/server/assets/{routes-DtVf9yqo.js → routes-BAPWTizT.js} +6 -7
  76. package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-CEbGjsI0.js} +3 -2
  77. package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-BZR5HSAL.js} +3 -3
  78. package/bundle/web-dist/server/assets/{show-8NNdRxhM.js → show-CxVZTAdv.js} +17 -21
  79. package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-D1hb4eKk.js} +4 -4
  80. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-Cvdy0zF8.js} +1 -1
  81. package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-CfYmk_4_.js} +1 -1
  82. package/bundle/web-dist/server/entry-server.generated.js +13 -10
  83. package/package.json +1 -1
  84. package/skills/qgrid/SKILL.md +9 -8
  85. package/skills/qgrid/references/ai-sdk-provider-contract.md +27 -17
  86. package/skills/qgrid/references/decision-rationale.md +8 -8
  87. package/skills/qgrid/references/openai-codex-runtime.md +19 -11
  88. package/skills/qgrid/references/prompt-cache-and-usage.md +3 -0
  89. package/skills/qgrid/references/request-log-run-lifecycle.md +14 -2
  90. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
  91. package/skills/qgrid/references/verification-and-debugging.md +23 -9
  92. package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +0 -1
  93. package/bundle/web-dist/client/assets/index-CvG4UB9v.css +0 -1
  94. package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +0 -1
  95. package/bundle/web-dist/client/assets/request-model-D4CZPmnG.js +0 -1
  96. package/bundle/web-dist/client/assets/show-b2PFNI3D.js +0 -2
@@ -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/IHsgdXNhZ2U6IHBhcnNlZC51c2FnZSB9IDoge30pLFxuICAgICAgfSxcbiAgICB9O1xuICB9KTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFZQSxNQUFNLGdCQUFnQixLQUFLLE9BQU87QUFDbEMsTUFBTSxtQkFBbUIsT0FBTztBQUNoQyxNQUFNLHFCQUFxQixLQUFLLE9BQU87QUFDdkMsTUFBTSxnQkFBZ0IsT0FBTyxLQUFLO0NBQUM7Q0FBSztDQUFJO0NBQUk7Q0FBSTtDQUFJO0NBQUk7Q0FBSTtBQUFFLENBQUM7QUFFbkUsU0FBZ0IsNEJBQTRCLFNBQWlDO0NBQzNFLElBQUksUUFBUSxnQkFBZ0IsUUFBUSxPQUFPLFFBQ3pDLE1BQU0sSUFBSSxvQkFDUiwwRUFDRjtDQUVGLE1BQU0sV0FBVyxRQUFRO0NBQ3pCLElBQUksQ0FBQyxZQUFZLGFBQWEsUUFBUSxTQUFTLGVBQWUsZUFDNUQsTUFBTSxJQUFJLG9CQUNSLHNFQUNGO0NBRUYsSUFBSSxTQUFTLGlCQUFpQixTQUFTLGtCQUFrQixPQUN2RCxNQUFNLElBQUksb0JBQW9CLHVEQUF1RDtDQUV2RixNQUFNLE9BQWlCLENBQUM7Q0FDeEIsTUFBTSxTQUF1QyxDQUFDO0NBQzlDLElBQUksUUFBUSxjQUFjLEtBQUssS0FBSyx5QkFBeUIsUUFBUSxjQUFjO0NBQ25GLEtBQUssTUFBTSxRQUFRLFFBQVEsU0FBUztFQUNsQyxJQUFJLEtBQUssU0FBUyxhQUFhO0VBQy9CLElBQUksS0FBSyxTQUFTLDJCQUEyQixPQUFPLEtBQUssV0FBVyxVQUFVO0dBQzVFLE9BQU8sS0FBSyxFQUNWLFdBQVcsUUFBUSxjQUFjLEtBQUssUUFBUSxLQUFLLGFBQWEsRUFBRSxVQUFVLEtBQUssU0FDbkYsQ0FBQztHQUNEO0VBQ0Y7RUFDQSxJQUFJLEtBQUssU0FBUyxhQUFhLE9BQU8sS0FBSyxTQUFTLFVBQ2xELE1BQU0sSUFBSSxvQkFDUixnRUFDRjtFQUVGLElBQUksT0FBTyxLQUFLLFlBQVksVUFBVTtHQUNwQyxLQUFLLEtBQUssR0FBRyxLQUFLLFFBQVEsT0FBTyxLQUFLLEtBQUssU0FBUztHQUNwRDtFQUNGO0VBQ0EsSUFBSSxDQUFDLE1BQU0sUUFBUSxLQUFLLE9BQU8sR0FDN0IsTUFBTSxJQUFJLG9CQUFvQiw4REFBOEQ7RUFFOUYsS0FBSyxNQUFNLFFBQVEsS0FBSyxTQUFTO0dBQy9CLElBQUksQ0FBQyxRQUFRLE9BQU8sU0FBUyxVQUMzQixNQUFNLElBQUksb0JBQW9CLDJEQUEyRDtHQUUzRixJQUNFO0lBQUM7SUFBYztJQUFlO0dBQU0sQ0FBQyxDQUFDLFNBQVMsS0FBSyxJQUFJLEtBQ3hELE9BQU8sS0FBSyxTQUFTLFVBRXJCLEtBQUssS0FBSyxHQUFHLEtBQUssUUFBUSxPQUFPLEtBQUssS0FBSyxNQUFNO1FBQzVDLElBQUksS0FBSyxTQUFTLGlCQUFpQixLQUFLLFNBQVMsYUFBYTtJQUNuRSxNQUFNLE1BQU0sT0FBTyxLQUFLLGNBQWMsV0FBVyxLQUFLLFlBQVksS0FBSyxXQUFXO0lBQ2xGLElBQUksT0FBTyxRQUFRLFlBQVksQ0FBQyw4QkFBOEIsS0FBSyxHQUFHLEdBQ3BFLE1BQU0sSUFBSSxvQkFBb0Isd0RBQXdEO0lBRXhGLE9BQU8sS0FBSyxFQUFFLFdBQVcsSUFBSSxDQUFDO0dBQ2hDLE9BQ0UsTUFBTSxJQUFJLG9CQUFvQiwyREFBMkQ7RUFFN0Y7Q0FDRjtDQUNBLElBQUksT0FBTyxTQUFTLEdBQ2xCLE1BQU0sSUFBSSxvQkFBb0IsK0RBQStEO0NBQy9GLElBQUksQ0FBQyxLQUFLLE1BQU0sU0FBUyxLQUFLLEtBQUssQ0FBQyxHQUFHLE1BQU0sSUFBSSxvQkFBb0IsdUJBQXVCO0NBQzVGLE9BQU87RUFDTCxLQUFLLDRCQUE0QixRQUMvQixnQkFDQSxXQUFXLE9BQU8sU0FBUyxVQUFVLGVBQ3ZDO0VBQ0EsTUFBTTtHQUNKLE9BQU87R0FDUCxRQUFRLEtBQUssS0FBSyxNQUFNO0dBQ3hCLFlBQVk7R0FDWixHQUFJLFNBQVMsVUFBVSxFQUFFLFNBQVMsU0FBUyxRQUFRLElBQUksQ0FBQztHQUN4RCxHQUFJLFNBQVMsT0FBTyxFQUFFLE1BQU0sU0FBUyxLQUFLLElBQUksQ0FBQztHQUMvQyxHQUFJLE9BQU8sU0FBUyxFQUFFLE9BQU8sSUFBSSxDQUFDO0VBQ3BDO0NBQ0Y7QUFDRjtBQUdBLFNBQWdCLHNCQUFzQixRQUFtRDtDQUN2RixJQUFJLE9BQU8sU0FBUyxLQUFLLEtBQUssZ0JBQWdCLENBQUMsSUFBSSxLQUFLLENBQUMseUJBQXlCLEtBQUssTUFBTSxHQUMzRixNQUFNLElBQUksb0JBQW9CLG9DQUFvQztDQUVwRSxNQUFNLFFBQVEsT0FBTyxLQUFLLFFBQVEsUUFBUTtDQUMxQyxJQUNFLE1BQU0sU0FBUyxNQUNmLENBQUMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxDQUFDLENBQUMsT0FBTyxhQUFhLEtBQzFDLE1BQU0sU0FBUyxTQUFTLElBQUksRUFBRSxNQUFNLFFBRXBDLE1BQU0sSUFBSSxvQkFBb0IsdUNBQXVDO0NBRXZFLE1BQU0sUUFBUSxNQUFNLGFBQWEsRUFBRTtDQUNuQyxNQUFNLFNBQVMsTUFBTSxhQUFhLEVBQUU7Q0FDcEMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxVQUFVLFFBQVEsU0FBUyxrQkFDeEMsTUFBTSxJQUFJLG9CQUFvQixvREFBb0Q7Q0FFcEYsSUFBSTtDQUNKLElBQUk7RUFDRixRQUFRLElBQUksS0FBSyxLQUFLLEtBQUs7Q0FDN0IsUUFBUTtFQUNOLE1BQU0sSUFBSSxvQkFBb0IsMENBQTBDO0NBQzFFO0NBQ0EsSUFBSSxjQUFjO0NBQ2xCLElBQUksVUFBVTtDQUNkLEtBQUssSUFBSSxJQUFJLEdBQUcsSUFBSSxNQUFNLEtBQUssUUFBUSxLQUFLLEdBQUc7RUFDN0MsZ0JBQWdCLE1BQU0sS0FBSyxPQUFPO0VBQ2xDLFlBQVksTUFBTSxLQUFLLEtBQU07RUFDN0IsSUFBSSxlQUFlLFNBQVMsT0FBTztHQUFFLE9BQU8sTUFBTTtHQUFPLFFBQVEsTUFBTTtFQUFPO0NBQ2hGO0NBQ0EsTUFBTSxJQUFJLG9CQUNSLDZFQUNGO0FBQ0Y7QUFFQSxlQUFzQiw0QkFDcEIsVUFDa0M7Q0FDbEMsSUFBSSxDQUFDLFNBQVMsTUFBTSxNQUFNLElBQUksb0JBQW9CLDRCQUE0QjtDQUM5RSxNQUFNLFNBQVMsU0FBUyxLQUFLLFVBQVU7Q0FDdkMsTUFBTSxTQUF1QixDQUFDO0NBQzlCLElBQUksT0FBTztDQUNYLElBQUk7RUFDRixPQUFPLE1BQU07R0FDWCxNQUFNLEVBQUUsTUFBTSxVQUFVLE1BQU0sT0FBTyxLQUFLO0dBQzFDLElBQUksTUFBTTtHQUNWLFFBQVEsTUFBTTtHQUNkLElBQUksT0FBTyxvQkFBb0I7SUFDN0IsTUFBTSxPQUFPLE9BQU87SUFDcEIsTUFBTSxJQUFJLG9CQUFvQix3Q0FBd0M7R0FDeEU7R0FDQSxPQUFPLEtBQUssS0FBSztFQUNuQjtDQUNGLFVBQVU7RUFDUixPQUFPLFlBQVk7Q0FDckI7Q0FDQSxNQUFNLFNBQVMsRUFBRSxPQUFPO0VBQ3RCLE1BQU0sRUFDSCxNQUFNLEVBQUUsT0FBTztHQUFFLFVBQVUsRUFBRSxPQUFPLENBQUMsQ0FBQyxJQUFJLENBQUM7R0FBRyxnQkFBZ0IsRUFBRSxPQUFPLENBQUMsQ0FBQyxRQUFRO0VBQUUsQ0FBQyxDQUFDLENBQUMsQ0FDdEYsSUFBSSxDQUFDLENBQUMsQ0FDTixJQUFJLEVBQUU7RUFDVCxTQUFTLEVBQUUsT0FBTyxDQUFDLENBQUMsU0FBUztFQUM3QixPQUFPLHdCQUF3QixNQUFNO0NBQ3ZDLENBQUM7Q0FDRCxJQUFJO0NBQ0osSUFBSTtFQUNGLFNBQVMsT0FBTyxNQUFNLEtBQUssTUFBTSxPQUFPLE9BQU8sTUFBTSxDQUFDLENBQUMsU0FBUyxNQUFNLENBQUMsQ0FBQztDQUMxRSxRQUFRO0VBQ04sTUFBTSxJQUFJLG9CQUFvQixtQ0FBbUM7Q0FDbkU7Q0FDQSxPQUFPLE9BQU8sS0FBSyxLQUFLLE1BQU0sVUFBVTtFQUN0QyxNQUFNLGFBQWEsc0JBQXNCLEtBQUssUUFBUTtFQUN0RCxPQUFPO0dBQ0wsTUFBTTtHQUNOLFFBQVEsS0FBSztHQUNiLFVBQVU7R0FDVixHQUFJLEtBQUssaUJBQWlCLEVBQUUsZUFBZSxLQUFLLGVBQWUsSUFBSSxDQUFDO0dBQ3BFLFlBQVk7SUFDVixPQUFPO0lBQ1AsT0FBTztJQUNQLFlBQVk7SUFDWixNQUFNLEdBQUcsV0FBVyxNQUFNLEdBQUcsV0FBVztJQUN4QyxHQUFJLE9BQU8sVUFBVSxFQUFFLFNBQVMsT0FBTyxRQUFRLElBQUksQ0FBQztJQUNwRCxHQUFJLFVBQVUsS0FBSyxPQUFPLFFBQVEsRUFBRSxPQUFPLE9BQU8sTUFBTSxJQUFJLENBQUM7R0FDL0Q7RUFDRjtDQUNGLENBQUM7QUFDSCJ9
154
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLWltYWdlLWdlbmVyYXRpb24uanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWltYWdlLWdlbmVyYXRpb24udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUE5HIH0gZnJvbSBcInBuZ2pzXCI7XG5pbXBvcnQgeyB6IH0gZnJvbSBcInpvZFwiO1xuXG5pbXBvcnQgeyBJbWFnZUdlbmVyYXRpb25NZXRhZGF0YSB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC50eXBlc1wiO1xuaW1wb3J0IHtcbiAgQ0hBVEdQVF9DT0RFWF9SRVNQT05TRVNfVVJMLFxuICBpbWFnZU1pbWVUeXBlLFxuICBPcGVuQUlQcm90b2NvbEVycm9yLFxuICB0eXBlIE9wZW5BSU5vcm1hbGl6ZWRFdmVudCxcbiAgdHlwZSBPcGVuQUlSZXNwb25zZXNPcHRpb25zLFxufSBmcm9tIFwiLi9vcGVuYWktYmFja2VuZC1wcm90b2NvbFwiO1xuXG5jb25zdCBNQVhfUE5HX0JZVEVTID0gMzIgKiAxMDI0ICogMTAyNDtcbmNvbnN0IE1BWF9JTUFHRV9QSVhFTFMgPSA0MDk2ICogNDA5NjtcbmNvbnN0IE1BWF9SRVNQT05TRV9CWVRFUyA9IDY0ICogMTAyNCAqIDEwMjQ7XG5jb25zdCBQTkdfU0lHTkFUVVJFID0gQnVmZmVyLmZyb20oWzEzNywgODAsIDc4LCA3MSwgMTMsIDEwLCAyNiwgMTBdKTtcblxuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkU3RhbmRhbG9uZUltYWdlUmVxdWVzdChvcHRpb25zOiBPcGVuQUlSZXNwb25zZXNPcHRpb25zKSB7XG4gIGlmIChvcHRpb25zLm91dHB1dFNjaGVtYSB8fCBvcHRpb25zLnRvb2xzPy5sZW5ndGgpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICAgIFwiU3RhbmRhbG9uZSBpbWFnZSBnZW5lcmF0aW9uIGRvZXMgbm90IHN1cHBvcnQgdG9vbHMgb3Igc3RydWN0dXJlZCBvdXRwdXRcIixcbiAgICApO1xuICB9XG4gIGlmICghb3B0aW9ucy5pbWFnZUdlbmVyYXRpb24pIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIlN0YW5kYWxvbmUgaW1hZ2VzIHJlcXVpcmUgaW1hZ2UgZ2VuZXJhdGlvbiB0byBiZSBlbmFibGVkXCIpO1xuICB9XG4gIGNvbnN0IGNvbnRyb2xzID0gdHlwZW9mIG9wdGlvbnMuaW1hZ2VHZW5lcmF0aW9uID09PSBcIm9iamVjdFwiID8gb3B0aW9ucy5pbWFnZUdlbmVyYXRpb24gOiB7fTtcbiAgaWYgKG9wdGlvbnMubW9kZWwgIT09IFwiZ3B0LWltYWdlLTJcIiAmJiBjb250cm9scy5iYWNrZ3JvdW5kICE9PSBcInRyYW5zcGFyZW50XCIpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICAgIFwiU3RhbmRhbG9uZSBpbWFnZXMgcmVxdWlyZSBhbiBleHBsaWNpdCB0cmFuc3BhcmVudCBiYWNrZ3JvdW5kIHJlcXVlc3RcIixcbiAgICApO1xuICB9XG4gIGlmIChjb250cm9scy5vdXRwdXRfZm9ybWF0ICYmIGNvbnRyb2xzLm91dHB1dF9mb3JtYXQgIT09IFwicG5nXCIpIHtcbiAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIlN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvbiBvbmx5IHN1cHBvcnRzIFBORyBvdXRwdXRcIik7XG4gIH1cbiAgY29uc3QgdGV4dDogc3RyaW5nW10gPSBbXTtcbiAgY29uc3QgaW1hZ2VzOiBBcnJheTx7IGltYWdlX3VybDogc3RyaW5nIH0+ID0gW107XG4gIGlmIChvcHRpb25zLmluc3RydWN0aW9ucykgdGV4dC5wdXNoKGBTeXN0ZW0gaW5zdHJ1Y3Rpb25zOlxcbiR7b3B0aW9ucy5pbnN0cnVjdGlvbnN9YCk7XG4gIGZvciAoY29uc3QgaXRlbSBvZiBvcHRpb25zLmhpc3RvcnkpIHtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcInJlYXNvbmluZ1wiKSBjb250aW51ZTtcbiAgICBpZiAoaXRlbS50eXBlID09PSBcImltYWdlX2dlbmVyYXRpb25fY2FsbFwiICYmIHR5cGVvZiBpdGVtLnJlc3VsdCA9PT0gXCJzdHJpbmdcIikge1xuICAgICAgaW1hZ2VzLnB1c2goe1xuICAgICAgICBpbWFnZV91cmw6IGBkYXRhOiR7aW1hZ2VNaW1lVHlwZShpdGVtLnJlc3VsdCwgaXRlbS5vdXRwdXRfZm9ybWF0KX07YmFzZTY0LCR7aXRlbS5yZXN1bHR9YCxcbiAgICAgIH0pO1xuICAgICAgY29udGludWU7XG4gICAgfVxuICAgIGlmIChpdGVtLnR5cGUgIT09IFwibWVzc2FnZVwiICYmIHR5cGVvZiBpdGVtLnJvbGUgIT09IFwic3RyaW5nXCIpIHtcbiAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFxuICAgICAgICBcIlVuc3VwcG9ydGVkIGNvbnZlcnNhdGlvbiBpdGVtIGZvciBzdGFuZGFsb25lIGltYWdlIGdlbmVyYXRpb25cIixcbiAgICAgICk7XG4gICAgfVxuICAgIGlmICh0eXBlb2YgaXRlbS5jb250ZW50ID09PSBcInN0cmluZ1wiKSB7XG4gICAgICB0ZXh0LnB1c2goYCR7aXRlbS5yb2xlID8/IFwidXNlclwifTpcXG4ke2l0ZW0uY29udGVudH1gKTtcbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBpZiAoIUFycmF5LmlzQXJyYXkoaXRlbS5jb250ZW50KSkge1xuICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIGNvbnRlbnQgZm9yIHN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvblwiKTtcbiAgICB9XG4gICAgZm9yIChjb25zdCBwYXJ0IG9mIGl0ZW0uY29udGVudCkge1xuICAgICAgaWYgKCFwYXJ0IHx8IHR5cGVvZiBwYXJ0ICE9PSBcIm9iamVjdFwiKSB7XG4gICAgICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiVW5zdXBwb3J0ZWQgbWVzc2FnZSBwYXJ0IGZvciBzdGFuZGFsb25lIGltYWdlIGdlbmVyYXRpb25cIik7XG4gICAgICB9XG4gICAgICBpZiAoXG4gICAgICAgIFtcImlucHV0X3RleHRcIiwgXCJvdXRwdXRfdGV4dFwiLCBcInRleHRcIl0uaW5jbHVkZXMocGFydC50eXBlKSAmJlxuICAgICAgICB0eXBlb2YgcGFydC50ZXh0ID09PSBcInN0cmluZ1wiXG4gICAgICApIHtcbiAgICAgICAgdGV4dC5wdXNoKGAke2l0ZW0ucm9sZSA/PyBcInVzZXJcIn06XFxuJHtwYXJ0LnRleHR9YCk7XG4gICAgICB9IGVsc2UgaWYgKHBhcnQudHlwZSA9PT0gXCJpbnB1dF9pbWFnZVwiIHx8IHBhcnQudHlwZSA9PT0gXCJpbWFnZV91cmxcIikge1xuICAgICAgICBjb25zdCB1cmwgPSB0eXBlb2YgcGFydC5pbWFnZV91cmwgPT09IFwic3RyaW5nXCIgPyBwYXJ0LmltYWdlX3VybCA6IHBhcnQuaW1hZ2VfdXJsPy51cmw7XG4gICAgICAgIGlmICh0eXBlb2YgdXJsICE9PSBcInN0cmluZ1wiIHx8ICEvXihodHRwczpcXC9cXC98ZGF0YTppbWFnZVxcLykvaS50ZXN0KHVybCkpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlZmVyZW5jZXMgbXVzdCBiZSBIVFRQUyBVUkxzIG9yIGltYWdlIGRhdGEgVVJMc1wiKTtcbiAgICAgICAgfVxuICAgICAgICBpbWFnZXMucHVzaCh7IGltYWdlX3VybDogdXJsIH0pO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJVbnN1cHBvcnRlZCBtZXNzYWdlIHBhcnQgZm9yIHN0YW5kYWxvbmUgaW1hZ2UgZ2VuZXJhdGlvblwiKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgaWYgKGltYWdlcy5sZW5ndGggPiA1KVxuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiU3RhbmRhbG9uZSBpbWFnZSBlZGl0aW5nIHN1cHBvcnRzIGF0IG1vc3QgNSByZWZlcmVuY2UgaW1hZ2VzXCIpO1xuICBpZiAoIXRleHQuc29tZSgocGFydCkgPT4gcGFydC50cmltKCkpKSB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHByb21wdCBpcyBlbXB0eVwiKTtcbiAgcmV0dXJuIHtcbiAgICB1cmw6IENIQVRHUFRfQ09ERVhfUkVTUE9OU0VTX1VSTC5yZXBsYWNlKFxuICAgICAgL1xcL3Jlc3BvbnNlcyQvLFxuICAgICAgYC9pbWFnZXMvJHtpbWFnZXMubGVuZ3RoID8gXCJlZGl0c1wiIDogXCJnZW5lcmF0aW9uc1wifWAsXG4gICAgKSxcbiAgICBib2R5OiB7XG4gICAgICBtb2RlbDogXCJncHQtaW1hZ2UtMlwiLFxuICAgICAgcHJvbXB0OiB0ZXh0LmpvaW4oXCJcXG5cXG5cIiksXG4gICAgICAuLi4oY29udHJvbHMuYmFja2dyb3VuZCA/IHsgYmFja2dyb3VuZDogY29udHJvbHMuYmFja2dyb3VuZCB9IDoge30pLFxuICAgICAgLi4uKGNvbnRyb2xzLnF1YWxpdHkgPyB7IHF1YWxpdHk6IGNvbnRyb2xzLnF1YWxpdHkgfSA6IHt9KSxcbiAgICAgIC4uLihjb250cm9scy5zaXplID8geyBzaXplOiBjb250cm9scy5zaXplIH0gOiB7fSksXG4gICAgICAuLi4oaW1hZ2VzLmxlbmd0aCA/IHsgaW1hZ2VzIH0gOiB7fSksXG4gICAgfSxcbiAgfTtcbn1cblxuZnVuY3Rpb24gaW5zcGVjdEdlbmVyYXRlZFBuZyhcbiAgYmFzZTY0OiBzdHJpbmcsXG4gIHJlcXVpcmVUcmFuc3BhcmVuY3k6IGJvb2xlYW4sXG4pOiB7IHdpZHRoOiBudW1iZXI7IGhlaWdodDogbnVtYmVyIH0ge1xuICBpZiAoYmFzZTY0Lmxlbmd0aCA+IE1hdGguY2VpbChNQVhfUE5HX0JZVEVTIC8gMykgKiA0IHx8ICEvXltBLVphLXowLTkrL10rPXswLDJ9JC8udGVzdChiYXNlNjQpKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbnZhbGlkIG9yIG92ZXJzaXplZCBnZW5lcmF0ZWQgUE5HXCIpO1xuICB9XG4gIGNvbnN0IGJ5dGVzID0gQnVmZmVyLmZyb20oYmFzZTY0LCBcImJhc2U2NFwiKTtcbiAgaWYgKFxuICAgIGJ5dGVzLmxlbmd0aCA8IDMzIHx8XG4gICAgIWJ5dGVzLnN1YmFycmF5KDAsIDgpLmVxdWFscyhQTkdfU0lHTkFUVVJFKSB8fFxuICAgIGJ5dGVzLnRvU3RyaW5nKFwiYXNjaWlcIiwgMTIsIDE2KSAhPT0gXCJJSERSXCJcbiAgKSB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIGRpZCBub3QgcmV0dXJuIGEgUE5HXCIpO1xuICB9XG4gIGNvbnN0IHdpZHRoID0gYnl0ZXMucmVhZFVJbnQzMkJFKDE2KTtcbiAgY29uc3QgaGVpZ2h0ID0gYnl0ZXMucmVhZFVJbnQzMkJFKDIwKTtcbiAgaWYgKCF3aWR0aCB8fCAhaGVpZ2h0IHx8IHdpZHRoICogaGVpZ2h0ID4gTUFYX0lNQUdFX1BJWEVMUykge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiR2VuZXJhdGVkIFBORyBkaW1lbnNpb25zIGV4Y2VlZCB0aGUgZGVjb2RpbmcgbGltaXRcIik7XG4gIH1cbiAgbGV0IGltYWdlOiBQTkc7XG4gIHRyeSB7XG4gICAgaW1hZ2UgPSBQTkcuc3luYy5yZWFkKGJ5dGVzKTtcbiAgfSBjYXRjaCB7XG4gICAgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSBnZW5lcmF0aW9uIHJldHVybmVkIGFuIGludmFsaWQgUE5HXCIpO1xuICB9XG4gIGlmICghcmVxdWlyZVRyYW5zcGFyZW5jeSkgcmV0dXJuIHsgd2lkdGg6IGltYWdlLndpZHRoLCBoZWlnaHQ6IGltYWdlLmhlaWdodCB9O1xuICAvLyBBbiBhbHBoYSBjaGFubmVsIGFsb25lIGNhbiBzdGlsbCBiZSBmdWxseSBvcGFxdWUgb3IgaGF2ZSBubyB2aXNpYmxlIGNvbnRlbnQuXG4gIGxldCB0cmFuc3BhcmVudCA9IGZhbHNlO1xuICBsZXQgdmlzaWJsZSA9IGZhbHNlO1xuICBmb3IgKGxldCBpID0gMzsgaSA8IGltYWdlLmRhdGEubGVuZ3RoOyBpICs9IDQpIHtcbiAgICB0cmFuc3BhcmVudCB8fD0gaW1hZ2UuZGF0YVtpXSA9PT0gMDtcbiAgICB2aXNpYmxlIHx8PSBpbWFnZS5kYXRhW2ldISA+IDA7XG4gICAgaWYgKHRyYW5zcGFyZW50ICYmIHZpc2libGUpIHJldHVybiB7IHdpZHRoOiBpbWFnZS53aWR0aCwgaGVpZ2h0OiBpbWFnZS5oZWlnaHQgfTtcbiAgfVxuICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcbiAgICBcIkltYWdlIGdlbmVyYXRpb24gZGlkIG5vdCByZXR1cm4gdmlzaWJsZSBjb250ZW50IG9uIGEgdHJhbnNwYXJlbnQgYmFja2dyb3VuZFwiLFxuICApO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVhZFN0YW5kYWxvbmVJbWFnZVJlc3BvbnNlKFxuICByZXNwb25zZTogUmVzcG9uc2UsXG4gIHJlcXVpcmVUcmFuc3BhcmVuY3k6IGJvb2xlYW4sXG4pOiBQcm9taXNlPE9wZW5BSU5vcm1hbGl6ZWRFdmVudFtdPiB7XG4gIGlmICghcmVzcG9uc2UuYm9keSkgdGhyb3cgbmV3IE9wZW5BSVByb3RvY29sRXJyb3IoXCJJbWFnZSByZXNwb25zZSBoYWQgbm8gYm9keVwiKTtcbiAgY29uc3QgcmVhZGVyID0gcmVzcG9uc2UuYm9keS5nZXRSZWFkZXIoKTtcbiAgY29uc3QgY2h1bmtzOiBVaW50OEFycmF5W10gPSBbXTtcbiAgbGV0IHNpemUgPSAwO1xuICB0cnkge1xuICAgIHdoaWxlICh0cnVlKSB7XG4gICAgICBjb25zdCB7IGRvbmUsIHZhbHVlIH0gPSBhd2FpdCByZWFkZXIucmVhZCgpO1xuICAgICAgaWYgKGRvbmUpIGJyZWFrO1xuICAgICAgc2l6ZSArPSB2YWx1ZS5ieXRlTGVuZ3RoO1xuICAgICAgaWYgKHNpemUgPiBNQVhfUkVTUE9OU0VfQllURVMpIHtcbiAgICAgICAgYXdhaXQgcmVhZGVyLmNhbmNlbCgpO1xuICAgICAgICB0aHJvdyBuZXcgT3BlbkFJUHJvdG9jb2xFcnJvcihcIkltYWdlIHJlc3BvbnNlIGV4Y2VlZGVkIHRoZSBzaXplIGxpbWl0XCIpO1xuICAgICAgfVxuICAgICAgY2h1bmtzLnB1c2godmFsdWUpO1xuICAgIH1cbiAgfSBmaW5hbGx5IHtcbiAgICByZWFkZXIucmVsZWFzZUxvY2soKTtcbiAgfVxuICBjb25zdCBzY2hlbWEgPSB6Lm9iamVjdCh7XG4gICAgZGF0YTogelxuICAgICAgLmFycmF5KHoub2JqZWN0KHsgYjY0X2pzb246IHouc3RyaW5nKCkubWluKDEpLCByZXZpc2VkX3Byb21wdDogei5zdHJpbmcoKS5udWxsaXNoKCkgfSkpXG4gICAgICAubWluKDEpXG4gICAgICAubWF4KDE2KSxcbiAgICBxdWFsaXR5OiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gICAgbW9kZWw6IHouc3RyaW5nKCkub3B0aW9uYWwoKSxcbiAgICBiYWNrZ3JvdW5kOiB6LnN0cmluZygpLm9wdGlvbmFsKCksXG4gICAgdXNhZ2U6IEltYWdlR2VuZXJhdGlvbk1ldGFkYXRhLnNoYXBlLnVzYWdlLFxuICB9KTtcbiAgbGV0IHBhcnNlZDogei5pbmZlcjx0eXBlb2Ygc2NoZW1hPjtcbiAgdHJ5IHtcbiAgICBwYXJzZWQgPSBzY2hlbWEucGFyc2UoSlNPTi5wYXJzZShCdWZmZXIuY29uY2F0KGNodW5rcykudG9TdHJpbmcoXCJ1dGY4XCIpKSk7XG4gIH0gY2F0Y2gge1xuICAgIHRocm93IG5ldyBPcGVuQUlQcm90b2NvbEVycm9yKFwiSW52YWxpZCBzdGFuZGFsb25lIGltYWdlIHJlc3BvbnNlXCIpO1xuICB9XG4gIGNvbnN0IGJhY2tncm91bmQgPSBwYXJzZWQuYmFja2dyb3VuZCA/PyAocmVxdWlyZVRyYW5zcGFyZW5jeSA/IFwidHJhbnNwYXJlbnRcIiA6IHVuZGVmaW5lZCk7XG4gIHJldHVybiBwYXJzZWQuZGF0YS5tYXAoKGl0ZW0sIGluZGV4KSA9PiB7XG4gICAgY29uc3QgZGltZW5zaW9ucyA9IGluc3BlY3RHZW5lcmF0ZWRQbmcoaXRlbS5iNjRfanNvbiwgcmVxdWlyZVRyYW5zcGFyZW5jeSk7XG4gICAgcmV0dXJuIHtcbiAgICAgIHR5cGU6IFwiaW1hZ2VcIixcbiAgICAgIGJhc2U2NDogaXRlbS5iNjRfanNvbixcbiAgICAgIG1pbWVUeXBlOiBcImltYWdlL3BuZ1wiLFxuICAgICAgLi4uKGl0ZW0ucmV2aXNlZF9wcm9tcHQgPyB7IHJldmlzZWRQcm9tcHQ6IGl0ZW0ucmV2aXNlZF9wcm9tcHQgfSA6IHt9KSxcbiAgICAgIGdlbmVyYXRpb246IHtcbiAgICAgICAgcm91dGU6IFwiY29kZXgtaW1hZ2VzXCIsXG4gICAgICAgIG1vZGVsOiBwYXJzZWQubW9kZWwgPz8gXCJncHQtaW1hZ2UtMlwiLFxuICAgICAgICAuLi4oYmFja2dyb3VuZCA/IHsgYmFja2dyb3VuZCB9IDoge30pLFxuICAgICAgICBzaXplOiBgJHtkaW1lbnNpb25zLndpZHRofXgke2RpbWVuc2lvbnMuaGVpZ2h0fWAsXG4gICAgICAgIC4uLihwYXJzZWQucXVhbGl0eSA/IHsgcXVhbGl0eTogcGFyc2VkLnF1YWxpdHkgfSA6IHt9KSxcbiAgICAgICAgLi4uKGluZGV4ID09PSAwICYmIHBhcnNlZC51c2FnZSA/IHsgdXNhZ2U6IHBhcnNlZC51c2FnZSB9IDoge30pLFxuICAgICAgfSxcbiAgICB9O1xuICB9KTtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFZQSxNQUFNLGdCQUFnQixLQUFLLE9BQU87QUFDbEMsTUFBTSxtQkFBbUIsT0FBTztBQUNoQyxNQUFNLHFCQUFxQixLQUFLLE9BQU87QUFDdkMsTUFBTSxnQkFBZ0IsT0FBTyxLQUFLO0NBQUM7Q0FBSztDQUFJO0NBQUk7Q0FBSTtDQUFJO0NBQUk7Q0FBSTtBQUFFLENBQUM7QUFFbkUsU0FBZ0IsNEJBQTRCLFNBQWlDO0NB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
@@ -1,6 +1,6 @@
1
1
  import { TokenModel } from "../../../application/token/token.model.js";
2
- import { OPENAI_CLIENT_ID, OPENAI_TOKEN_URL } from "./openai-constants.js";
3
2
  import { deactivateAuthDeadToken } from "../../../application/qgrid/token-death.js";
3
+ import { OPENAI_CLIENT_ID, OPENAI_TOKEN_URL } from "./openai-constants.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  //#region src/utils/providers/openai/openai-refresh.ts
6
6
  /**
@@ -16,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
 
@@ -104,6 +104,7 @@ describe("qgrid run lifecycle start", () => {
104
104
  system_prompt: "system",
105
105
  requested_model_name: "openai/gpt-5-codex",
106
106
  effort: undefined,
107
+ service_tier: undefined,
107
108
  project_name: "project",
108
109
  history: [{ type: "message", role: "user", content: "earlier" }],
109
110
  is_image_generation: undefined,
@@ -111,6 +112,16 @@ describe("qgrid run lifecycle start", () => {
111
112
  });
112
113
  });
113
114
 
115
+ it.each([
116
+ ["openai/gpt-6-sol", "fast", "fast"],
117
+ ["openai/gpt-6-sol", "flex", "flex"],
118
+ ["openai/gpt-6-sol", undefined, undefined],
119
+ ["anthropic/claude-sonnet-5-5", "fast", undefined],
120
+ ] as const)("logs service tier for %s with %s", async (model, serviceTier, expected) => {
121
+ await beforeQuery({ prompt: "hi", model, serviceTier });
122
+ expect(createRunMock).toHaveBeenCalledWith(expect.objectContaining({ service_tier: expected }));
123
+ });
124
+
114
125
  it.each([false, true, undefined])("does not log ignored effort when thinking is %s", async (thinking) => {
115
126
  await beforeQuery({ prompt: "hi", model: "openai/gpt-6-sol", thinking, effort: "high" });
116
127
  expect(createRunMock).toHaveBeenCalledWith(expect.objectContaining({
@@ -270,6 +281,13 @@ describe("qgrid run lifecycle TTFT", () => {
270
281
  );
271
282
  });
272
283
 
284
+ it.each(["fast", "flex", undefined] as const)("records the final turn service tier: %s", async (serviceTier) => {
285
+ await afterQuery(10, 2, { prompt: "hi", model: "openai/gpt-6-sol", serviceTier }, queryOutput());
286
+ expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
287
+ service_tier: serviceTier ?? null,
288
+ }));
289
+ });
290
+
273
291
  it("persists an immediate structured final answer as JSON text", async () => {
274
292
  await afterQuery(
275
293
  10,
@@ -436,13 +454,15 @@ describe("qgrid run lifecycle TTFT", () => {
436
454
  );
437
455
  });
438
456
 
439
- it("passes configured image generation cost estimate into finishRun", async () => {
457
+ it("stores the hosted per-image estimate when image usage is absent", async () => {
440
458
  await afterQuery(
441
459
  10,
442
460
  2,
443
461
  { prompt: "draw", imageGeneration: true },
444
462
  queryOutput({
445
- content: [{ type: "image", data: "iVBORw0KGgoBAgM", revisedPrompt: "green triangle" }],
463
+ content: [{ type: "image", data: "iVBORw0KGgoBAgM", revisedPrompt: "green triangle",
464
+ generation: { route: "codex-responses", size: "841x1870" },
465
+ }],
446
466
  }),
447
467
  );
448
468
 
@@ -456,38 +476,69 @@ describe("qgrid run lifecycle TTFT", () => {
456
476
  );
457
477
  });
458
478
 
459
- it("keeps standalone image accounting out of text usage and records response usage once", async () => {
479
+ it.each([
480
+ ["openai/gpt-5.5", "transparent"],
481
+ ["openai/gpt-image-2", "opaque"],
482
+ ] as const)("keeps standalone %s accounting out of text usage and records image usage once", async (model, background) => {
460
483
  const generation = {
461
484
  route: "codex-images" as const, model: "gpt-image-2" as const,
462
- size: "1254x1254", quality: "medium", background: "transparent",
485
+ size: "1254x1254", quality: "medium", background,
463
486
  };
464
487
  aggregateStepUsageMock.mockResolvedValueOnce({
465
488
  input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0,
466
489
  duration_ms: 120, fallback_count: 0, cost_usd: 0, cost_source: "pricing_table",
467
490
  });
468
491
  await afterQuery(10, 0, {
469
- prompt: "draw", model: "openai/gpt-5.5", imageGeneration: true,
470
- imageGenerationOptions: { quality: "high", size: "1024x1024", background: "transparent" },
492
+ prompt: "draw", model, imageGeneration: true,
493
+ imageGenerationOptions: { quality: "high", size: "1024x1024", background },
471
494
  }, queryOutput({
472
- model: "gpt-image-2", requestedModel: "gpt-5.5", costUsd: 0,
495
+ model: "gpt-image-2", costUsd: 0,
473
496
  usage: { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
474
497
  content: [
475
498
  { type: "image", data: "first", generation: { ...generation, usage: {
476
499
  input_tokens: 10, output_tokens: 100, total_tokens: 110,
500
+ input_tokens_details: { text_tokens: 4, image_tokens: 6 },
477
501
  } } },
478
502
  { type: "image", data: "second", generation },
479
503
  ],
480
504
  }));
481
505
  expect(appendStepMock).toHaveBeenCalledWith(10, expect.objectContaining({
482
- type: "generate", model_name: "openai/gpt-image-2", requested_model_name: "openai/gpt-5.5",
506
+ type: "generate", model_name: "openai/gpt-image-2", requested_model_name: model,
483
507
  input_tokens: 0, output_tokens: 0, cost_usd: 0,
484
508
  }));
485
509
  expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
486
- input_tokens: 0, output_tokens: 0, cost_usd: 0, image_cost_usd: 3_080,
510
+ input_tokens: 0, output_tokens: 0, cost_usd: 0, image_cost_usd: 3_068,
487
511
  image_cost_method: "estimated:gpt-image-2:reported-usage:public-prices:conservative",
488
512
  }));
489
513
  });
490
514
 
515
+ it("records auto chat driver cost and standalone image usage cost separately", async () => {
516
+ aggregateStepUsageMock.mockResolvedValueOnce({
517
+ input_tokens: 100, output_tokens: 20, cache_read_tokens: 0, cache_creation_tokens: 0,
518
+ duration_ms: 120, fallback_count: 0, cost_usd: 20, cost_source: "pricing_table",
519
+ });
520
+ await afterQuery(10, 0, {
521
+ prompt: "draw", model: "openai/gpt-6-luna", imageGeneration: "auto",
522
+ }, queryOutput({
523
+ model: "gpt-6-luna", costUsd: 0.00002,
524
+ usage: { input_tokens: 100, output_tokens: 20, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
525
+ content: [{ type: "image", data: "image", generation: {
526
+ route: "codex-images", model: "gpt-image-2", background: "opaque",
527
+ usage: {
528
+ input_tokens: 10, output_tokens: 100, total_tokens: 110,
529
+ input_tokens_details: { text_tokens: 4, image_tokens: 6 },
530
+ },
531
+ } }],
532
+ }));
533
+ expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
534
+ model_name: "openai/gpt-6-luna", requested_model_name: "openai/gpt-6-luna",
535
+ input_tokens: 100, output_tokens: 20, cost_usd: 20, image_cost_usd: 3_068,
536
+ is_image_generation: true,
537
+ image_cost_method: "estimated:gpt-image-2:reported-usage:public-prices:conservative",
538
+ }));
539
+ expect(appendStepMock.mock.calls.filter(([, step]) => step.type === "generate")).toHaveLength(1);
540
+ });
541
+
491
542
  it("uses requested image quality and size for cost estimate", async () => {
492
543
  await afterQuery(
493
544
  10,
@@ -609,6 +660,7 @@ describe("qgrid run lifecycle TTFT", () => {
609
660
 
610
661
  describe("qgrid run lifecycle terminal rollup", () => {
611
662
  beforeEach(() => {
663
+ appendStepMock.mockReset().mockResolvedValue(1);
612
664
  aggregateStepUsageMock.mockReset().mockResolvedValue({
613
665
  input_tokens: 120,
614
666
  output_tokens: 30,
@@ -624,6 +676,50 @@ describe("qgrid run lifecycle terminal rollup", () => {
624
676
  finishRunMock.mockReset();
625
677
  });
626
678
 
679
+ it.each(["error", "aborted"] as const)("records a completed chat stage once before %s rollup", async (status) => {
680
+ const args = { prompt: "draw", model: "openai/gpt-6-astra", imageGeneration: "auto" as const };
681
+ const result = queryOutput({ model: "gpt-6-luna", requestedModel: "gpt-6-astra" });
682
+ const completedStep = { stepIndex: 2, result };
683
+ if (status === "error") await finishRunWithError(10, "image failed", args, completedStep);
684
+ else await finishRunAborted(10, args, completedStep);
685
+ expect(appendStepMock).toHaveBeenCalledOnce();
686
+ expect(appendStepMock).toHaveBeenCalledWith(10, expect.objectContaining({
687
+ step_index: 2, type: "generate", model_name: "openai/gpt-6-luna",
688
+ requested_model_name: "openai/gpt-6-astra", input_tokens: 5, output_tokens: 7,
689
+ cost_usd: 1_000, cost_source: "pricing_table", ttft_ms: 39,
690
+ }));
691
+ expect(appendStepMock.mock.invocationCallOrder[0]).toBeLessThan(aggregateStepUsageMock.mock.invocationCallOrder[0]!);
692
+ expect(finishRunMock).toHaveBeenCalledOnce();
693
+ expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
694
+ status, error_message: status === "error" ? "image failed" : "client disconnected",
695
+ token_name: "tok-A", model_name: "openai/gpt-6-luna", requested_model_name: "openai/gpt-6-astra",
696
+ // Existing steps and the final completed driver are rolled up together.
697
+ input_tokens: 120, output_tokens: 30, cost_usd: 7_500, cost_source: "mixed",
698
+ }));
699
+ const finish = finishRunMock.mock.calls[0]![1];
700
+ expect(finish.image_cost_usd).toBeUndefined();
701
+ expect(finish.image_cost_method).toBeUndefined();
702
+ expect(finish.is_image_generation).toBeUndefined();
703
+ });
704
+
705
+ it("still attempts aggregation and terminal status when the completed driver step cannot be logged", async () => {
706
+ appendStepMock.mockRejectedValueOnce(new Error("step write failed"));
707
+ await expect(finishRunWithError(10, "image failed", {
708
+ prompt: "draw", model: "openai/gpt-6-luna",
709
+ }, { stepIndex: 0, result: queryOutput({ model: "gpt-6-luna" }) })).resolves.toBeUndefined();
710
+ expect(aggregateStepUsageMock).toHaveBeenCalledWith(10);
711
+ expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
712
+ status: "error", error_message: "image failed", model_name: "openai/gpt-6-luna", token_name: "tok-A",
713
+ }));
714
+ });
715
+
716
+ it("does not replace the original failure when terminal persistence also fails", async () => {
717
+ finishRunMock.mockRejectedValueOnce(new Error("terminal write failed"));
718
+ await expect(finishRunWithError(10, "image failed", { prompt: "draw" }, {
719
+ stepIndex: 0, result: queryOutput(),
720
+ })).resolves.toBeUndefined();
721
+ });
722
+
627
723
  it("keeps incurred step usage and cost when a run ends in error", async () => {
628
724
  await finishRunWithError(10, "provider failed", {
629
725
  prompt: "continue",