@cartanova/qgrid-cli 2.9.4 → 2.9.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 (59) hide show
  1. package/bundle/dist/application/qgrid/expired-token-reminder.js +6 -5
  2. package/bundle/dist/application/qgrid/qgrid-image-generation.js +12 -3
  3. package/bundle/dist/application/qgrid/qgrid-response-format.js +14 -12
  4. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
  5. package/bundle/dist/application/qgrid/qgrid.types.js +44 -4
  6. package/bundle/dist/application/qgrid/tool-emulation.js +4 -2
  7. package/bundle/dist/application/setting/setting.model.js +8 -2
  8. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +22 -3
  9. package/bundle/dist/utils/providers/openai/openai-direct-client.js +39 -1
  10. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +47 -21
  11. package/bundle/dist/utils/providers/openai/openai-image-generation.js +146 -0
  12. package/bundle/dist/utils/providers/openai/openai-quota.js +11 -3
  13. package/bundle/dist/utils/providers/openai/openai-refresh.js +7 -5
  14. package/bundle/dist/utils/slack-notify.js +8 -4
  15. package/bundle/src/application/qgrid/expired-token-reminder.test.ts +6 -1
  16. package/bundle/src/application/qgrid/expired-token-reminder.ts +10 -4
  17. package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +53 -0
  18. package/bundle/src/application/qgrid/qgrid-image-generation.ts +32 -2
  19. package/bundle/src/application/qgrid/qgrid-response-format.test.ts +32 -1
  20. package/bundle/src/application/qgrid/qgrid-response-format.ts +28 -18
  21. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +32 -0
  22. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +1 -1
  23. package/bundle/src/application/qgrid/qgrid.types.ts +47 -5
  24. package/bundle/src/application/qgrid/tool-emulation.test.ts +25 -0
  25. package/bundle/src/application/qgrid/tool-emulation.ts +6 -1
  26. package/bundle/src/application/setting/setting.model.test.ts +20 -0
  27. package/bundle/src/application/setting/setting.model.ts +10 -2
  28. package/bundle/src/application/sonamu.generated.http +4 -2
  29. package/bundle/src/utils/providers/common/provider-dispatcher.ts +8 -2
  30. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -0
  31. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +27 -2
  32. package/bundle/src/utils/providers/openai/openai-direct-client.ts +48 -1
  33. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +155 -2
  34. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +54 -19
  35. package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +152 -0
  36. package/bundle/src/utils/providers/openai/openai-image-generation.ts +183 -0
  37. package/bundle/src/utils/providers/openai/openai-quota.test.ts +32 -0
  38. package/bundle/src/utils/providers/openai/openai-quota.ts +15 -8
  39. package/bundle/src/utils/providers/openai/openai-refresh.test.ts +18 -0
  40. package/bundle/src/utils/providers/openai/openai-refresh.ts +11 -4
  41. package/bundle/src/utils/slack-notify.test.ts +14 -0
  42. package/bundle/src/utils/slack-notify.ts +19 -3
  43. package/bundle/web-dist/client/assets/{index-fogJvajr.js → index-BtVmANcA.js} +3 -3
  44. package/bundle/web-dist/client/assets/{logs-CsY2cqvt.js → logs-BCuaLfPL.js} +1 -1
  45. package/bundle/web-dist/client/assets/{monit-ChgaLEcD.js → monit-BkGkzl6m.js} +1 -1
  46. package/bundle/web-dist/client/assets/{routes-BvyiTnOZ.js → routes-DqY8SXcB.js} +1 -1
  47. package/bundle/web-dist/client/assets/{settings-ZR2J9rWb.js → settings-DbkDAJfh.js} +1 -1
  48. package/bundle/web-dist/client/assets/{show-CjwO4-So.js → show-Dx5JFzwy.js} +1 -1
  49. package/bundle/web-dist/client/assets/{tokens-Bx-hG1gm.js → tokens-DHahzDQJ.js} +1 -1
  50. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Bqax_icS.js → use-oauth-login-flow-Cjdnj_8D.js} +1 -1
  51. package/bundle/web-dist/client/index.html +1 -1
  52. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  53. package/package.json +2 -1
  54. package/skills/qgrid/SKILL.md +8 -1
  55. package/skills/qgrid/references/ai-sdk-provider-contract.md +4 -2
  56. package/skills/qgrid/references/decision-rationale.md +2 -2
  57. package/skills/qgrid/references/openai-codex-runtime.md +9 -1
  58. package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
  59. package/skills/qgrid/references/token-auth-quota-lifecycle.md +1 -1
@@ -6,7 +6,7 @@ import { getLogger } from "@logtape/logtape";
6
6
  /**
7
7
  * OpenAI chatgptAuthTokens refresh handler.
8
8
  *
9
- * - direct HTTPS/WS transport가 401 refresh를 소유하고 저장된 refresh token으로 갱신함
9
+ * - direct transport와 quota 조회의 401에서 같은 refresh 경로를 사용함
10
10
  * - qgrid 가 DB 의 refresh_token 으로 새 access_token 을 발급받아 응답
11
11
  * - per-token inflight promise 로 concurrent refresh dedup
12
12
  * - rotation 감지: 새 refresh_token 있으면 DB 즉시 업데이트
@@ -70,16 +70,18 @@ async function doRefresh(tokenId) {
70
70
  const body = await resp.text().catch(() => "");
71
71
  let errorCode;
72
72
  try {
73
- errorCode = JSON.parse(body).error;
73
+ const error = JSON.parse(body).error;
74
+ const code = typeof error === "string" ? error : error && typeof error === "object" && "code" in error ? error.code : void 0;
75
+ if (typeof code === "string" && /^[a-z_]{1,80}$/.test(code)) errorCode = code;
74
76
  } catch {}
75
- logger.error(`OpenAI refresh FAILED token=${token.name}(id=${tokenId}) status=${resp.status} code=${errorCode ?? "?"} body=${body}`);
77
+ logger.error(`OpenAI refresh FAILED token=${token.name}(id=${tokenId}) status=${resp.status} code=${errorCode ?? "unknown"}`);
76
78
  if (errorCode && PERMANENT_FAILURE_CODES.has(errorCode)) await deactivateAuthDeadToken({
77
79
  id: tokenId,
78
80
  name: token.name,
79
81
  provider: "openai",
80
82
  credentials: creds
81
83
  }, `openai:${errorCode}`);
82
- throw new Error(`OpenAI refresh failed: ${resp.status} ${body}`);
84
+ throw new Error(`OpenAI refresh failed: ${resp.status} ${errorCode ?? "unknown"}`);
83
85
  }
84
86
  const data = await resp.json();
85
87
  const newRefreshToken = data.refresh_token ?? creds.refreshToken;
@@ -114,4 +116,4 @@ async function doRefresh(tokenId) {
114
116
  //#endregion
115
117
  export { handleChatgptAuthTokensRefresh };
116
118
 
117
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXJlZnJlc2guanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLXJlZnJlc2gudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBPcGVuQUkgY2hhdGdwdEF1dGhUb2tlbnMgcmVmcmVzaCBoYW5kbGVyLlxuICpcbiAqIC0gZGlyZWN0IEhUVFBTL1dTIHRyYW5zcG9ydOqwgCA0MDEgcmVmcmVzaOulvCDshozsnKDtlZjqs6Ag7KCA7J6l65CcIHJlZnJlc2ggdG9rZW7snLzroZwg6rCx7Iug7ZWoXG4gKiAtIHFncmlkIOqwgCBEQiDsnZggcmVmcmVzaF90b2tlbiDsnLzroZwg7IOIIGFjY2Vzc190b2tlbiDsnYQg67Cc6riJ67Cb7JWEIOydkeuLtVxuICogLSBwZXItdG9rZW4gaW5mbGlnaHQgcHJvbWlzZSDroZwgY29uY3VycmVudCByZWZyZXNoIGRlZHVwXG4gKiAtIHJvdGF0aW9uIOqwkOyngDog7IOIIHJlZnJlc2hfdG9rZW4g7J6I7Jy866m0IERCIOymieyLnCDsl4XrjbDsnbTtirhcbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZGVhY3RpdmF0ZUF1dGhEZWFkVG9rZW4gfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vcWdyaWQvdG9rZW4tZGVhdGhcIjtcbmltcG9ydCB7IFRva2VuTW9kZWwgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4ubW9kZWxcIjtcbmltcG9ydCB7IHR5cGUgT3BlbkFJQ3JlZGVudGlhbHMgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4udHlwZXNcIjtcblxuY29uc3QgbG9nZ2VyID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwib3BlbmFpLXJlZnJlc2hcIl0pO1xuXG5pbXBvcnQgeyBPUEVOQUlfQ0xJRU5UX0lELCBPUEVOQUlfVE9LRU5fVVJMIH0gZnJvbSBcIi4vb3BlbmFpLWNvbnN0YW50c1wiO1xuXG4vLyDsnqzroZzqt7jsnbgg7Jm4IOuzteq1rCDrtojqsIDtlZwgcmVmcmVzaCDsi6TtjKgg7L2U65OcLiDslrTripAg6rKD7J2065OgIO2GoO2BsOydtCDso73sl4jri6TripQg7ZmV7KCVIOyLoO2YuOuLpC5cbmNvbnN0IFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTID0gbmV3IFNldChbXG4gIFwicmVmcmVzaF90b2tlbl9leHBpcmVkXCIsXG4gIFwicmVmcmVzaF90b2tlbl9yZXVzZWRcIixcbiAgXCJyZWZyZXNoX3Rva2VuX2ludmFsaWRhdGVkXCIsXG5dKTtcblxuLy8gcGVyLXRva2VuIGluZmxpZ2h0IHByb21pc2UgZGVkdXAgKyBtaW5pbXVtIGludGVydmFsXG5jb25zdCBpbmZsaWdodFJlZnJlc2ggPSBuZXcgTWFwPG51bWJlciwgUHJvbWlzZTxSZWZyZXNoUmVzdWx0Pj4oKTtcbmNvbnN0IGxhc3RSZWZyZXNoVGltZSA9IG5ldyBNYXA8bnVtYmVyLCBudW1iZXI+KCk7XG5jb25zdCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUyA9IDVfMDAwO1xuXG5pbnRlcmZhY2UgUmVmcmVzaFJlc3VsdCB7XG4gIGFjY2Vzc1Rva2VuOiBzdHJpbmc7XG4gIGNoYXRncHRBY2NvdW50SWQ6IHN0cmluZztcbiAgY2hhdGdwdFBsYW5UeXBlPzogc3RyaW5nO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gaGFuZGxlQ2hhdGdwdEF1dGhUb2tlbnNSZWZyZXNoKHRva2VuSWQ6IG51bWJlcik6IFByb21pc2U8UmVmcmVzaFJlc3VsdD4ge1xuICBsb2dnZXIuaW5mbyhgcmVmcmVzaCByZXF1ZXN0ZWQgZm9yIHRva2VuICR7dG9rZW5JZH0gKHBpZD0ke3Byb2Nlc3MucGlkfSlgKTtcbiAgY29uc3QgZXhpc3RpbmcgPSBpbmZsaWdodFJlZnJlc2guZ2V0KHRva2VuSWQpO1xuICBpZiAoZXhpc3RpbmcpIHtcbiAgICBsb2dnZXIuaW5mbyhgcmVmcmVzaCBkZWR1cCBmb3IgdG9rZW4gJHt0b2tlbklkfWApO1xuICAgIHJldHVybiBleGlzdGluZztcbiAgfVxuXG4gIGNvbnN0IGxhc3RBdCA9IGxhc3RSZWZyZXNoVGltZS5nZXQodG9rZW5JZCkgPz8gMDtcbiAgaWYgKERhdGUubm93KCkgLSBsYXN0QXQgPCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUykge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoIHRvbyBzb29uIGZvciB0b2tlbiAke3Rva2VuSWR9LCByZWFkaW5nIGN1cnJlbnQgY3JlZHMgZnJvbSBEQmApO1xuICAgIHJldHVybiByZWFkQ3VycmVudENyZWRzKHRva2VuSWQpO1xuICB9XG5cbiAgbGFzdFJlZnJlc2hUaW1lLnNldCh0b2tlbklkLCBEYXRlLm5vdygpKTtcbiAgY29uc3QgcHJvbWlzZSA9IGRvUmVmcmVzaCh0b2tlbklkKTtcbiAgaW5mbGlnaHRSZWZyZXNoLnNldCh0b2tlbklkLCBwcm9taXNlKTtcblxuICB0cnkge1xuICAgIHJldHVybiBhd2FpdCBwcm9taXNlO1xuICB9IGZpbmFsbHkge1xuICAgIGluZmxpZ2h0UmVmcmVzaC5kZWxldGUodG9rZW5JZCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZEN1cnJlbnRDcmVkcyh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHRocm93IG5ldyBFcnJvcihgdG9rZW4gJHt0b2tlbklkfSBub3QgZm91bmRgKTtcbiAgY29uc3QgY3JlZHMgPSB0b2tlbi5jcmVkZW50aWFscyBhcyBPcGVuQUlDcmVkZW50aWFscztcbiAgcmV0dXJuIHtcbiAgICBhY2Nlc3NUb2tlbjogY3JlZHMuYWNjZXNzVG9rZW4sXG4gICAgY2hhdGdwdEFjY291bnRJZDogY3JlZHMuYWNjb3VudElkLFxuICAgIGNoYXRncHRQbGFuVHlwZTogY3JlZHMucGxhblR5cGUsXG4gIH07XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGRvUmVmcmVzaCh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYHRva2VuICR7dG9rZW5JZH0gbm90IGZvdW5kIG9yIG5vdCBvcGVuYWlgKTtcbiAgfVxuXG4gIGNvbnN0IGNyZWRzID0gdG9rZW4uY3JlZGVudGlhbHMgYXMgT3BlbkFJQ3JlZGVudGlhbHM7XG4gIGlmICghY3JlZHMucmVmcmVzaFRva2VuKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGB0b2tlbiAke3Rva2VuSWR9IGhhcyBubyByZWZyZXNoVG9rZW5gKTtcbiAgfVxuXG4gIGxvZ2dlci5pbmZvKGByZWZyZXNoaW5nIHRva2VuICR7dG9rZW4ubmFtZX0gKGlkPSR7dG9rZW5JZH0pYCk7XG5cbiAgY29uc3QgcmVzcCA9IGF3YWl0IGZldGNoKE9QRU5BSV9UT0tFTl9VUkwsIHtcbiAgICBtZXRob2Q6IFwiUE9TVFwiLFxuICAgIC8vIO2GoO2BsCDsl5Trk5ztj6zsnbjtirjripQgT0F1dGgg7ZGc7KSAIGZvcm0g7J247L2U65SpIOqzhOyVveydhCDqsJXsoJztlZzri6QuIEpTT04g7Jy866GcIOuztOuCtOuptFxuICAgIC8vIGFjY2VzcyB0b2tlbiDrp4zro4wg7ZuE7J2YIOuqqOuToCByZWZyZXNoIOqwgCDqsbDrtoDrkJjslrQg7Yag7YGw7J20IO2GteynuOuhnCDso73ripTri6QuXG4gICAgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFwiIH0sXG4gICAgYm9keTogbmV3IFVSTFNlYXJjaFBhcmFtcyh7XG4gICAgICBncmFudF90eXBlOiBcInJlZnJlc2hfdG9rZW5cIixcbiAgICAgIGNsaWVudF9pZDogT1BFTkFJX0NMSUVOVF9JRCxcbiAgICAgIHJlZnJlc2hfdG9rZW46IGNyZWRzLnJlZnJlc2hUb2tlbixcbiAgICB9KSxcbiAgfSk7XG5cbiAgaWYgKCFyZXNwLm9rKSB7XG4gICAgY29uc3QgYm9keSA9IGF3YWl0IHJlc3AudGV4dCgpLmNhdGNoKCgpID0+IFwiXCIpO1xuICAgIC8vIDQwMSDrs7jrrLjsnZggZXJyb3Ig7L2U65Oc6rCAIO2GoO2BsCDsgqzrp53snZgg6rKw7KCV7KCBIOyCrOyduOydtOuLpC5cbiAgICAvLyAgIHJlZnJlc2hfdG9rZW5fZXhwaXJlZCAgICAg4oaSIHJlZnJlc2hfdG9rZW4g7J6Q7LK0IOunjOujjFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9yZXVzZWQgICAgICDihpIg7J2066+4IOyCrOyaqeuQnCjtmozsoIQg7Y+Q6riw65CcKSByZWZyZXNoX3Rva2VuIOyerOyCrOyaqSDihpIg7Yyo67CA66asIOustO2aqO2ZlFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9pbnZhbGlkYXRlZCDihpIg7ISc67KE7LihIHJldm9rZVxuICAgIC8vIOychCAz6rCc64qUIOuqqOuRkCDsmIHqtawg7Iuk7YyoLiDsnqzrk7HroZ0g7Jm4IOuzteq1rCDrtojqsIAuXG4gICAgbGV0IGVycm9yQ29kZTogc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIHRyeSB7XG4gICAgICBlcnJvckNvZGUgPSAoSlNPTi5wYXJzZShib2R5KSBhcyB7IGVycm9yPzogc3RyaW5nIH0pLmVycm9yO1xuICAgIH0gY2F0Y2gge31cbiAgICBsb2dnZXIuZXJyb3IoXG4gICAgICBgT3BlbkFJIHJlZnJlc2ggRkFJTEVEIHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkgc3RhdHVzPSR7cmVzcC5zdGF0dXN9IGAgK1xuICAgICAgICBgY29kZT0ke2Vycm9yQ29kZSA/PyBcIj9cIn0gYm9keT0ke2JvZHl9YCxcbiAgICApO1xuICAgIC8vIOyYgeq1rCDsi6TtjKjripQgZXJyb3JDb2RlIOuhnOunjCDtjJDsoJXtlZzri6Qg4oCUIHN0YXR1cyDrpbwg7ZWo6ruYIOyalOq1rO2VmOuptCBwcm92aWRlciDqsIBcbiAgICAvLyDsg4Htg5zsvZTrk5zrpbwg67CU6r+AIOuVjCDtjJDsoJXsnbQg7KGw7Jqp7Z6IIOupjuuKlOuLpC5cbiAgICBpZiAoZXJyb3JDb2RlICYmIFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTLmhhcyhlcnJvckNvZGUpKSB7XG4gICAgICBhd2FpdCBkZWFjdGl2YXRlQXV0aERlYWRUb2tlbihcbiAgICAgICAgeyBpZDogdG9rZW5JZCwgbmFtZTogdG9rZW4ubmFtZSwgcHJvdmlkZXI6IFwib3BlbmFpXCIsIGNyZWRlbnRpYWxzOiBjcmVkcyB9LFxuICAgICAgICBgb3BlbmFpOiR7ZXJyb3JDb2RlfWAsXG4gICAgICApO1xuICAgIH1cbiAgICB0aHJvdyBuZXcgRXJyb3IoYE9wZW5BSSByZWZyZXNoIGZhaWxlZDogJHtyZXNwLnN0YXR1c30gJHtib2R5fWApO1xuICB9XG5cbiAgY29uc3QgZGF0YSA9IChhd2FpdCByZXNwLmpzb24oKSkgYXMge1xuICAgIGFjY2Vzc190b2tlbjogc3RyaW5nO1xuICAgIHJlZnJlc2hfdG9rZW4/OiBzdHJpbmc7XG4gICAgZXhwaXJlc19pbj86IG51bWJlcjtcbiAgICBpZF90b2tlbj86IHN0cmluZztcbiAgfTtcblxuICBjb25zdCBuZXdSZWZyZXNoVG9rZW4gPSBkYXRhLnJlZnJlc2hfdG9rZW4gPz8gY3JlZHMucmVmcmVzaFRva2VuO1xuICBjb25zdCByb3RhdGVkID1cbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IG51bGwgJiZcbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IHVuZGVmaW5lZCAmJlxuICAgIGRhdGEucmVmcmVzaF90b2tlbiAhPT0gY3JlZHMucmVmcmVzaFRva2VuO1xuICBpZiAocm90YXRlZCkge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoX3Rva2VuIHJvdGF0ZWQgZm9yIHRva2VuICR7dG9rZW4ubmFtZX1gKTtcbiAgfVxuXG4gIGNvbnN0IHVwZGF0ZWRDcmVkczogT3BlbkFJQ3JlZGVudGlhbHMgPSB7XG4gICAgLi4uY3JlZHMsXG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIHJlZnJlc2hUb2tlbjogbmV3UmVmcmVzaFRva2VuLFxuICAgIGFjY2Vzc1Rva2VuRXhwaXJlc0F0OiBkYXRhLmV4cGlyZXNfaW5cbiAgICAgID8gRGF0ZS5ub3coKSArIGRhdGEuZXhwaXJlc19pbiAqIDEwMDBcbiAgICAgIDogY3JlZHMuYWNjZXNzVG9rZW5FeHBpcmVzQXQsXG4gICAgLi4uKGRhdGEuaWRfdG9rZW4gPyB7IGlkVG9rZW46IGRhdGEuaWRfdG9rZW4gfSA6IHt9KSxcbiAgfTtcblxuICAvLyDtmozsoIQocm90YXRlZCnsnbQg7J287Ja064Ks64qU642wIOydtCBzYXZlIOqwgCDsi6TtjKjtlZjrqbQsIE9wZW5BSSDsqr0g7JibIHJlZnJlc2hfdG9rZW4g7J2AIOydtOuvuFxuICAvLyDtj5DquLDrkJwg7IOB7YOc7JeQ7IScIERCIOyXkOuKlCDsmJsg7Yag7YGw7J20IOuCqOuKlOuLpCDihpIg64uk7J2MIHJlZnJlc2gg6rCAIHJlZnJlc2hfdG9rZW5fcmV1c2VkIOuhnFxuICAvLyDsmIHqtawg7IKs66edLiDsnbQg6rWs6rCEIOyLpO2MqOuKlCDthqDtgbAg7IKs66ed7J2YIOyngeygkSDsm5DsnbjsnbTrr4DroZwg67CY65Oc7IucIO2BrOqyjCDrgqjquLTri6QuXG4gIHRyeSB7XG4gICAgYXdhaXQgVG9rZW5Nb2RlbC5zYXZlKFtcbiAgICAgIHtcbiAgICAgICAgaWQ6IHRva2VuSWQsXG4gICAgICAgIHByb3ZpZGVyOiBcIm9wZW5haVwiLFxuICAgICAgICBjcmVkZW50aWFsczogdXBkYXRlZENyZWRzLFxuICAgICAgICBuYW1lOiB0b2tlbi5uYW1lLFxuICAgICAgICByZWF1dGhfcmVxdWlyZWQ6IGZhbHNlLFxuICAgICAgfSxcbiAgICBdKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGxvZ2dlci5lcnJvcihcbiAgICAgIGBPcGVuQUkgcmVmcmVzaDogREIgc2F2ZSBGQUlMRUQgYWZ0ZXIgcm90YXRpb24ocm90YXRlZD0ke3JvdGF0ZWR9KSBgICtcbiAgICAgICAgYHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkg4oCUIOyYmyByZWZyZXNoX3Rva2VuIO2PkOq4sOuQqCwgREIg66+467CY7JiBIOKGkiDthqDtgbAg7IKs66edIOychO2XmC4gZXJyPSR7U3RyaW5nKGUpfWAsXG4gICAgKTtcbiAgICB0aHJvdyBlO1xuICB9XG5cbiAgbG9nZ2VyLmluZm8oYHRva2VuICR7dG9rZW4ubmFtZX0gcmVmcmVzaGVkIHN1Y2Nlc3NmdWxseSAocm90YXRlZD0ke3JvdGF0ZWR9KWApO1xuXG4gIHJldHVybiB7XG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIGNoYXRncHRBY2NvdW50SWQ6IGNyZWRzLmFjY291bnRJZCxcbiAgICBjaGF0Z3B0UGxhblR5cGU6IGNyZWRzLnBsYW5UeXBlLFxuICB9O1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7O0FBY0EsTUFBTSxTQUFTLFVBQVUsQ0FBQyxTQUFTLGdCQUFnQixDQUFDO0FBS3BELE1BQU0sMENBQTBCLElBQUksSUFBSTtDQUN0QztDQUNBO0NBQ0E7QUFDRixDQUFDO0FBR0QsTUFBTSxrQ0FBa0IsSUFBSSxJQUFvQztBQUNoRSxNQUFNLGtDQUFrQixJQUFJLElBQW9CO0FBQ2hELE1BQU0sMEJBQTBCO0FBUWhDLGVBQXNCLCtCQUErQixTQUF5QztDQUM1RixPQUFPLEtBQUssK0JBQStCLFFBQVEsUUFBUSxRQUFRLElBQUksRUFBRTtDQUN6RSxNQUFNLFdBQVcsZ0JBQWdCLElBQUksT0FBTztDQUM1QyxJQUFJLFVBQVU7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
119
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXJlZnJlc2guanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLXJlZnJlc2gudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBPcGVuQUkgY2hhdGdwdEF1dGhUb2tlbnMgcmVmcmVzaCBoYW5kbGVyLlxuICpcbiAqIC0gZGlyZWN0IHRyYW5zcG9ydOyZgCBxdW90YSDsobDtmozsnZggNDAx7JeQ7IScIOqwmeydgCByZWZyZXNoIOqyveuhnOulvCDsgqzsmqntlahcbiAqIC0gcWdyaWQg6rCAIERCIOydmCByZWZyZXNoX3Rva2VuIOycvOuhnCDsg4ggYWNjZXNzX3Rva2VuIOydhCDrsJzquInrsJvslYQg7J2R64u1XG4gKiAtIHBlci10b2tlbiBpbmZsaWdodCBwcm9taXNlIOuhnCBjb25jdXJyZW50IHJlZnJlc2ggZGVkdXBcbiAqIC0gcm90YXRpb24g6rCQ7KeAOiDsg4ggcmVmcmVzaF90b2tlbiDsnojsnLzrqbQgREIg7KaJ7IucIOyXheuNsOydtO2KuFxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBkZWFjdGl2YXRlQXV0aERlYWRUb2tlbiB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi1kZWF0aFwiO1xuaW1wb3J0IHsgVG9rZW5Nb2RlbCB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi90b2tlbi90b2tlbi5tb2RlbFwiO1xuaW1wb3J0IHsgdHlwZSBPcGVuQUlDcmVkZW50aWFscyB9IGZyb20gXCIuLi8uLi8uLi9hcHBsaWNhdGlvbi90b2tlbi90b2tlbi50eXBlc1wiO1xuXG5jb25zdCBsb2dnZXIgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJvcGVuYWktcmVmcmVzaFwiXSk7XG5cbmltcG9ydCB7IE9QRU5BSV9DTElFTlRfSUQsIE9QRU5BSV9UT0tFTl9VUkwgfSBmcm9tIFwiLi9vcGVuYWktY29uc3RhbnRzXCI7XG5cbi8vIOyerOuhnOq3uOyduCDsmbgg67O16rWsIOu2iOqwgO2VnCByZWZyZXNoIOyLpO2MqCDsvZTrk5wuIOyWtOuKkCDqsoPsnbTrk6Ag7Yag7YGw7J20IOyjveyXiOuLpOuKlCDtmZXsoJUg7Iug7Zi464ukLlxuY29uc3QgUEVSTUFORU5UX0ZBSUxVUkVfQ09ERVMgPSBuZXcgU2V0KFtcbiAgXCJyZWZyZXNoX3Rva2VuX2V4cGlyZWRcIixcbiAgXCJyZWZyZXNoX3Rva2VuX3JldXNlZFwiLFxuICBcInJlZnJlc2hfdG9rZW5faW52YWxpZGF0ZWRcIixcbl0pO1xuXG4vLyBwZXItdG9rZW4gaW5mbGlnaHQgcHJvbWlzZSBkZWR1cCArIG1pbmltdW0gaW50ZXJ2YWxcbmNvbnN0IGluZmxpZ2h0UmVmcmVzaCA9IG5ldyBNYXA8bnVtYmVyLCBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+PigpO1xuY29uc3QgbGFzdFJlZnJlc2hUaW1lID0gbmV3IE1hcDxudW1iZXIsIG51bWJlcj4oKTtcbmNvbnN0IFJFRlJFU0hfTUlOX0lOVEVSVkFMX01TID0gNV8wMDA7XG5cbmludGVyZmFjZSBSZWZyZXNoUmVzdWx0IHtcbiAgYWNjZXNzVG9rZW46IHN0cmluZztcbiAgY2hhdGdwdEFjY291bnRJZDogc3RyaW5nO1xuICBjaGF0Z3B0UGxhblR5cGU/OiBzdHJpbmc7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBoYW5kbGVDaGF0Z3B0QXV0aFRva2Vuc1JlZnJlc2godG9rZW5JZDogbnVtYmVyKTogUHJvbWlzZTxSZWZyZXNoUmVzdWx0PiB7XG4gIGxvZ2dlci5pbmZvKGByZWZyZXNoIHJlcXVlc3RlZCBmb3IgdG9rZW4gJHt0b2tlbklkfSAocGlkPSR7cHJvY2Vzcy5waWR9KWApO1xuICBjb25zdCBleGlzdGluZyA9IGluZmxpZ2h0UmVmcmVzaC5nZXQodG9rZW5JZCk7XG4gIGlmIChleGlzdGluZykge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoIGRlZHVwIGZvciB0b2tlbiAke3Rva2VuSWR9YCk7XG4gICAgcmV0dXJuIGV4aXN0aW5nO1xuICB9XG5cbiAgY29uc3QgbGFzdEF0ID0gbGFzdFJlZnJlc2hUaW1lLmdldCh0b2tlbklkKSA/PyAwO1xuICBpZiAoRGF0ZS5ub3coKSAtIGxhc3RBdCA8IFJFRlJFU0hfTUlOX0lOVEVSVkFMX01TKSB7XG4gICAgbG9nZ2VyLmluZm8oYHJlZnJlc2ggdG9vIHNvb24gZm9yIHRva2VuICR7dG9rZW5JZH0sIHJlYWRpbmcgY3VycmVudCBjcmVkcyBmcm9tIERCYCk7XG4gICAgcmV0dXJuIHJlYWRDdXJyZW50Q3JlZHModG9rZW5JZCk7XG4gIH1cblxuICBsYXN0UmVmcmVzaFRpbWUuc2V0KHRva2VuSWQsIERhdGUubm93KCkpO1xuICBjb25zdCBwcm9taXNlID0gZG9SZWZyZXNoKHRva2VuSWQpO1xuICBpbmZsaWdodFJlZnJlc2guc2V0KHRva2VuSWQsIHByb21pc2UpO1xuXG4gIHRyeSB7XG4gICAgcmV0dXJuIGF3YWl0IHByb21pc2U7XG4gIH0gZmluYWxseSB7XG4gICAgaW5mbGlnaHRSZWZyZXNoLmRlbGV0ZSh0b2tlbklkKTtcbiAgfVxufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkQ3VycmVudENyZWRzKHRva2VuSWQ6IG51bWJlcik6IFByb21pc2U8UmVmcmVzaFJlc3VsdD4ge1xuICBjb25zdCB0b2tlbiA9IGF3YWl0IFRva2VuTW9kZWwuZmluZE9uZShcIkFcIiwgeyBpZDogdG9rZW5JZCB9KTtcbiAgaWYgKCF0b2tlbiB8fCB0b2tlbi5wcm92aWRlciAhPT0gXCJvcGVuYWlcIikgdGhyb3cgbmV3IEVycm9yKGB0b2tlbiAke3Rva2VuSWR9IG5vdCBmb3VuZGApO1xuICBjb25zdCBjcmVkcyA9IHRva2VuLmNyZWRlbnRpYWxzIGFzIE9wZW5BSUNyZWRlbnRpYWxzO1xuICByZXR1cm4ge1xuICAgIGFjY2Vzc1Rva2VuOiBjcmVkcy5hY2Nlc3NUb2tlbixcbiAgICBjaGF0Z3B0QWNjb3VudElkOiBjcmVkcy5hY2NvdW50SWQsXG4gICAgY2hhdGdwdFBsYW5UeXBlOiBjcmVkcy5wbGFuVHlwZSxcbiAgfTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZG9SZWZyZXNoKHRva2VuSWQ6IG51bWJlcik6IFByb21pc2U8UmVmcmVzaFJlc3VsdD4ge1xuICBjb25zdCB0b2tlbiA9IGF3YWl0IFRva2VuTW9kZWwuZmluZE9uZShcIkFcIiwgeyBpZDogdG9rZW5JZCB9KTtcbiAgaWYgKCF0b2tlbiB8fCB0b2tlbi5wcm92aWRlciAhPT0gXCJvcGVuYWlcIikge1xuICAgIHRocm93IG5ldyBFcnJvcihgdG9rZW4gJHt0b2tlbklkfSBub3QgZm91bmQgb3Igbm90IG9wZW5haWApO1xuICB9XG5cbiAgY29uc3QgY3JlZHMgPSB0b2tlbi5jcmVkZW50aWFscyBhcyBPcGVuQUlDcmVkZW50aWFscztcbiAgaWYgKCFjcmVkcy5yZWZyZXNoVG9rZW4pIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYHRva2VuICR7dG9rZW5JZH0gaGFzIG5vIHJlZnJlc2hUb2tlbmApO1xuICB9XG5cbiAgbG9nZ2VyLmluZm8oYHJlZnJlc2hpbmcgdG9rZW4gJHt0b2tlbi5uYW1lfSAoaWQ9JHt0b2tlbklkfSlgKTtcblxuICBjb25zdCByZXNwID0gYXdhaXQgZmV0Y2goT1BFTkFJX1RPS0VOX1VSTCwge1xuICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgLy8g7Yag7YGwIOyXlOuTnO2PrOyduO2KuOuKlCBPQXV0aCDtkZzspIAgZm9ybSDsnbjsvZTrlKkg6rOE7JW97J2EIOqwleygnO2VnOuLpC4gSlNPTiDsnLzroZwg67O064K066m0XG4gICAgLy8gYWNjZXNzIHRva2VuIOunjOujjCDtm4TsnZgg66qo65OgIHJlZnJlc2gg6rCAIOqxsOu2gOuQmOyWtCDthqDtgbDsnbQg7Ya17Ke466GcIOyjveuKlOuLpC5cbiAgICBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkXCIgfSxcbiAgICBib2R5OiBuZXcgVVJMU2VhcmNoUGFyYW1zKHtcbiAgICAgIGdyYW50X3R5cGU6IFwicmVmcmVzaF90b2tlblwiLFxuICAgICAgY2xpZW50X2lkOiBPUEVOQUlfQ0xJRU5UX0lELFxuICAgICAgcmVmcmVzaF90b2tlbjogY3JlZHMucmVmcmVzaFRva2VuLFxuICAgIH0pLFxuICB9KTtcblxuICBpZiAoIXJlc3Aub2spIHtcbiAgICBjb25zdCBib2R5ID0gYXdhaXQgcmVzcC50ZXh0KCkuY2F0Y2goKCkgPT4gXCJcIik7XG4gICAgLy8gNDAxIOuzuOusuOydmCBlcnJvciDsvZTrk5zqsIAg7Yag7YGwIOyCrOunneydmCDqsrDsoJXsoIEg7IKs7J247J2064ukLlxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9leHBpcmVkICAgICDihpIgcmVmcmVzaF90b2tlbiDsnpDssrQg66eM66OMXG4gICAgLy8gICByZWZyZXNoX3Rva2VuX3JldXNlZCAgICAgIOKGkiDsnbTrr7gg7IKs7Jqp65CcKO2ajOyghCDtj5DquLDrkJwpIHJlZnJlc2hfdG9rZW4g7J6s7IKs7JqpIOKGkiDtjKjrsIDrpqwg66y07Zqo7ZmUXG4gICAgLy8gICByZWZyZXNoX3Rva2VuX2ludmFsaWRhdGVkIOKGkiDshJzrsoTsuKEgcmV2b2tlXG4gICAgLy8g7JyEIDPqsJzripQg66qo65GQIOyYgeq1rCDsi6TtjKguIOyerOuTseuhnSDsmbgg67O16rWsIOu2iOqwgC5cbiAgICBsZXQgZXJyb3JDb2RlOiBzdHJpbmcgfCB1bmRlZmluZWQ7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IGVycm9yID0gKEpTT04ucGFyc2UoYm9keSkgYXMgeyBlcnJvcj86IHVua25vd24gfSkuZXJyb3I7XG4gICAgICBjb25zdCBjb2RlID1cbiAgICAgICAgdHlwZW9mIGVycm9yID09PSBcInN0cmluZ1wiXG4gICAgICAgICAgPyBlcnJvclxuICAgICAgICAgIDogZXJyb3IgJiYgdHlwZW9mIGVycm9yID09PSBcIm9iamVjdFwiICYmIFwiY29kZVwiIGluIGVycm9yXG4gICAgICAgICAgICA/IGVycm9yLmNvZGVcbiAgICAgICAgICAgIDogdW5kZWZpbmVkO1xuICAgICAgaWYgKHR5cGVvZiBjb2RlID09PSBcInN0cmluZ1wiICYmIC9eW2Etel9dezEsODB9JC8udGVzdChjb2RlKSkgZXJyb3JDb2RlID0gY29kZTtcbiAgICB9IGNhdGNoIHt9XG4gICAgbG9nZ2VyLmVycm9yKFxuICAgICAgYE9wZW5BSSByZWZyZXNoIEZBSUxFRCB0b2tlbj0ke3Rva2VuLm5hbWV9KGlkPSR7dG9rZW5JZH0pIHN0YXR1cz0ke3Jlc3Auc3RhdHVzfSBgICtcbiAgICAgICAgYGNvZGU9JHtlcnJvckNvZGUgPz8gXCJ1bmtub3duXCJ9YCxcbiAgICApO1xuICAgIC8vIOyYgeq1rCDsi6TtjKjripQgZXJyb3JDb2RlIOuhnOunjCDtjJDsoJXtlZzri6Qg4oCUIHN0YXR1cyDrpbwg7ZWo6ruYIOyalOq1rO2VmOuptCBwcm92aWRlciDqsIBcbiAgICAvLyDsg4Htg5zsvZTrk5zrpbwg67CU6r+AIOuVjCDtjJDsoJXsnbQg7KGw7Jqp7Z6IIOupjuuKlOuLpC5cbiAgICBpZiAoZXJyb3JDb2RlICYmIFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTLmhhcyhlcnJvckNvZGUpKSB7XG4gICAgICBhd2FpdCBkZWFjdGl2YXRlQXV0aERlYWRUb2tlbihcbiAgICAgICAgeyBpZDogdG9rZW5JZCwgbmFtZTogdG9rZW4ubmFtZSwgcHJvdmlkZXI6IFwib3BlbmFpXCIsIGNyZWRlbnRpYWxzOiBjcmVkcyB9LFxuICAgICAgICBgb3BlbmFpOiR7ZXJyb3JDb2RlfWAsXG4gICAgICApO1xuICAgIH1cbiAgICB0aHJvdyBuZXcgRXJyb3IoYE9wZW5BSSByZWZyZXNoIGZhaWxlZDogJHtyZXNwLnN0YXR1c30gJHtlcnJvckNvZGUgPz8gXCJ1bmtub3duXCJ9YCk7XG4gIH1cblxuICBjb25zdCBkYXRhID0gKGF3YWl0IHJlc3AuanNvbigpKSBhcyB7XG4gICAgYWNjZXNzX3Rva2VuOiBzdHJpbmc7XG4gICAgcmVmcmVzaF90b2tlbj86IHN0cmluZztcbiAgICBleHBpcmVzX2luPzogbnVtYmVyO1xuICAgIGlkX3Rva2VuPzogc3RyaW5nO1xuICB9O1xuXG4gIGNvbnN0IG5ld1JlZnJlc2hUb2tlbiA9IGRhdGEucmVmcmVzaF90b2tlbiA/PyBjcmVkcy5yZWZyZXNoVG9rZW47XG4gIGNvbnN0IHJvdGF0ZWQgPVxuICAgIGRhdGEucmVmcmVzaF90b2tlbiAhPT0gbnVsbCAmJlxuICAgIGRhdGEucmVmcmVzaF90b2tlbiAhPT0gdW5kZWZpbmVkICYmXG4gICAgZGF0YS5yZWZyZXNoX3Rva2VuICE9PSBjcmVkcy5yZWZyZXNoVG9rZW47XG4gIGlmIChyb3RhdGVkKSB7XG4gICAgbG9nZ2VyLmluZm8oYHJlZnJlc2hfdG9rZW4gcm90YXRlZCBmb3IgdG9rZW4gJHt0b2tlbi5uYW1lfWApO1xuICB9XG5cbiAgY29uc3QgdXBkYXRlZENyZWRzOiBPcGVuQUlDcmVkZW50aWFscyA9IHtcbiAgICAuLi5jcmVkcyxcbiAgICBhY2Nlc3NUb2tlbjogZGF0YS5hY2Nlc3NfdG9rZW4sXG4gICAgcmVmcmVzaFRva2VuOiBuZXdSZWZyZXNoVG9rZW4sXG4gICAgYWNjZXNzVG9rZW5FeHBpcmVzQXQ6IGRhdGEuZXhwaXJlc19pblxuICAgICAgPyBEYXRlLm5vdygpICsgZGF0YS5leHBpcmVzX2luICogMTAwMFxuICAgICAgOiBjcmVkcy5hY2Nlc3NUb2tlbkV4cGlyZXNBdCxcbiAgICAuLi4oZGF0YS5pZF90b2tlbiA/IHsgaWRUb2tlbjogZGF0YS5pZF90b2tlbiB9IDoge30pLFxuICB9O1xuXG4gIC8vIO2ajOyghChyb3RhdGVkKeydtCDsnbzslrTrgqzripTrjbAg7J20IHNhdmUg6rCAIOyLpO2MqO2VmOuptCwgT3BlbkFJIOyqvSDsmJsgcmVmcmVzaF90b2tlbiDsnYAg7J2066+4XG4gIC8vIO2PkOq4sOuQnCDsg4Htg5zsl5DshJwgREIg7JeQ64qUIOyYmyDthqDtgbDsnbQg64Ko64qU64ukIOKGkiDri6TsnYwgcmVmcmVzaCDqsIAgcmVmcmVzaF90b2tlbl9yZXVzZWQg66GcXG4gIC8vIOyYgeq1rCDsgqzrp50uIOydtCDqtazqsIQg7Iuk7Yyo64qUIO2GoO2BsCDsgqzrp53snZgg7KeB7KCRIOybkOyduOydtOuvgOuhnCDrsJjrk5zsi5wg7YGs6rKMIOuCqOq4tOuLpC5cbiAgdHJ5IHtcbiAgICBhd2FpdCBUb2tlbk1vZGVsLnNhdmUoW1xuICAgICAge1xuICAgICAgICBpZDogdG9rZW5JZCxcbiAgICAgICAgcHJvdmlkZXI6IFwib3BlbmFpXCIsXG4gICAgICAgIGNyZWRlbnRpYWxzOiB1cGRhdGVkQ3JlZHMsXG4gICAgICAgIG5hbWU6IHRva2VuLm5hbWUsXG4gICAgICAgIHJlYXV0aF9yZXF1aXJlZDogZmFsc2UsXG4gICAgICB9LFxuICAgIF0pO1xuICB9IGNhdGNoIChlKSB7XG4gICAgbG9nZ2VyLmVycm9yKFxuICAgICAgYE9wZW5BSSByZWZyZXNoOiBEQiBzYXZlIEZBSUxFRCBhZnRlciByb3RhdGlvbihyb3RhdGVkPSR7cm90YXRlZH0pIGAgK1xuICAgICAgICBgdG9rZW49JHt0b2tlbi5uYW1lfShpZD0ke3Rva2VuSWR9KSDigJQg7JibIHJlZnJlc2hfdG9rZW4g7Y+Q6riw65CoLCBEQiDrr7jrsJjsmIEg4oaSIO2GoO2BsCDsgqzrp50g7JyE7ZeYLiBlcnI9JHtTdHJpbmcoZSl9YCxcbiAgICApO1xuICAgIHRocm93IGU7XG4gIH1cblxuICBsb2dnZXIuaW5mbyhgdG9rZW4gJHt0b2tlbi5uYW1lfSByZWZyZXNoZWQgc3VjY2Vzc2Z1bGx5IChyb3RhdGVkPSR7cm90YXRlZH0pYCk7XG5cbiAgcmV0dXJuIHtcbiAgICBhY2Nlc3NUb2tlbjogZGF0YS5hY2Nlc3NfdG9rZW4sXG4gICAgY2hhdGdwdEFjY291bnRJZDogY3JlZHMuYWNjb3VudElkLFxuICAgIGNoYXRncHRQbGFuVHlwZTogY3JlZHMucGxhblR5cGUsXG4gIH07XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7QUFjQSxN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
@@ -12,6 +12,7 @@ import { getLogger } from "@logtape/logtape";
12
12
  const logger = getLogger(["qgrid", "slack"]);
13
13
  const SLACK_POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage";
14
14
  const SLACK_TIMEOUT_MS = 5e3;
15
+ var SlackNotificationError = class extends Error {};
15
16
  /** 상태를 색으로 먼저 읽히게 하는 attachment 색상 바. */
16
17
  const SLACK_COLOR = {
17
18
  good: "#2eb886",
@@ -23,6 +24,7 @@ async function notifySlack(notification, readSetting = getSetting) {
23
24
  const { title, subject, context, color, urgent, now } = notification;
24
25
  if (!botToken || !channel) {
25
26
  logger.debug(`slack not configured, skipping notification: ${title} ${subject ?? ""}`);
27
+ if (notification.throwOnFailure) throw new SlackNotificationError("Slack 봇 토큰과 채널 ID를 설정해 주세요");
26
28
  return;
27
29
  }
28
30
  if (!urgent && readSetting("slack.enabled", "SLACK_ENABLED") === "false") {
@@ -65,12 +67,14 @@ async function notifySlack(notification, readSetting = getSetting) {
65
67
  signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
66
68
  });
67
69
  const body = await res.json().catch(() => null);
68
- if (!body?.ok) logger.warn(`slack notification rejected: ${body?.error ?? `http ${res.status}`}`);
70
+ if (!res.ok || body?.ok !== true) throw new SlackNotificationError(`Slack 알림 발송 실패: ${typeof body?.error === "string" && /^[a-z_]{1,80}$/.test(body.error) ? body.error : `HTTP ${res.status}`}`);
69
71
  } catch (e) {
70
- logger.warn(`slack notification failed: ${e.message}`);
72
+ const error = e instanceof SlackNotificationError ? e : new SlackNotificationError("Slack 알림 발송 실패: 네트워크 오류 또는 응답 시간 초과");
73
+ logger.warn(error.message);
74
+ if (notification.throwOnFailure) throw error;
71
75
  }
72
76
  }
73
77
  //#endregion
74
- export { SLACK_COLOR, notifySlack };
78
+ export { SLACK_COLOR, SlackNotificationError, notifySlack };
75
79
 
76
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG4gIC8qKlxuICAgKiDsobDsmqkg7Iuc6rCEKOyjvOunkMK3MjB+OOyLnCnsl5Drj4Qg67O064K464ukLiDshJzruYTsiqTqsIAg66mI7LaYIOyDge2DnOyymOufvCDsp4DquIgg7JWM7KeAIOyViuycvOuptCDshpDtlbTqsIBcbiAgICog7Luk7KeA64qUIOyCrOqxtOyXkOunjCDsk7Tri6Qg4oCUIOuCqOyaqe2VmOuptCDsobDsmqkg7Iuc6rCEIOyekOyytOqwgCDrrLTsnZjrr7jtlbTsp4Tri6QuXG4gICAqL1xuICB1cmdlbnQ/OiBib29sZWFuO1xuICAvKiog7KGw7JqpIOyLnOqwhCDtjJDsoJUg6riw7KSAIOyLnOqwgS4g7YWM7Iqk7Yq47JeQ7IScIOyLnOqzhOulvCDqs6DsoJXtlaAg65WM66eMIOuEmOq4tOuLpC4gKi9cbiAgbm93PzogRGF0ZTtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBub3RpZnlTbGFjayhcbiAgbm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbixcbiAgcmVhZFNldHRpbmc6IHR5cGVvZiBnZXRTZXR0aW5nID0gZ2V0U2V0dGluZyxcbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHJlYWRTZXR0aW5nKFwic2xhY2suYm90VG9rZW5cIiwgXCJTTEFDS19CT1RfVE9LRU5cIik7XG4gIGNvbnN0IGNoYW5uZWwgPSByZWFkU2V0dGluZyhcInNsYWNrLmNoYW5uZWxJZFwiLCBcIlNMQUNLX0NIQU5ORUxfSURcIik7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yLCB1cmdlbnQsIG5vdyB9ID0gbm90aWZpY2F0aW9uO1xuICBpZiAoIWJvdFRva2VuIHx8ICFjaGFubmVsKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBzbGFjayBub3QgY29uZmlndXJlZCwgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyDsl7DtnLTsspjrn7wg6rec7LmZ7Jy866GcIOyeoeydhCDsiJgg7JeG64qUIOq4sOqwhOyXkCDqtIDrpqzsnpDqsIAg64K066as64qUIOyKpOychOy5mC4gdXJnZW50IOuKlCDthrXqs7zsi5ztgqjri6Qg4oCUXG4gIC8vIOuBiCDsg4Htg5zroZwgcHJvdmlkZXIg6rCAIOyghOu2gCDso73snLzrqbQg7Jew7Zy0IOuCtOuCtCDshJzruYTsiqTqsIAg66mI7LaYIOykhCDslYTrrLTrj4Qg66qo66W464ukLlxuICBpZiAoIXVyZ2VudCAmJiByZWFkU2V0dGluZyhcInNsYWNrLmVuYWJsZWRcIiwgXCJTTEFDS19FTkFCTEVEXCIpID09PSBcImZhbHNlXCIpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIGRpc2FibGVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIC8vIOunjOujjCDslYzrprzsnYAg7KO86riw7KCB7Jy866GcIOuwmOuzteuQmOuvgOuhnCDsl6zquLDshJwg67KE66Ck64+EIOuLpOydjCDqt7zrrLQg7Iuc6rCEIOyyqyDso7zquLDsl5Ag64uk7IucIOyYqOuLpC5cbiAgaWYgKCF1cmdlbnQgJiYgaXNRdWlldEhvdXJzKG5vdyA/PyBuZXcgRGF0ZSgpLCByZWFkU2V0dGluZykpIHtcbiAgICBsb2dnZXIuZGVidWcoYHF1aWV0IGhvdXJzLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8gZmFsbGJhY2sg7J2AIGF0dGFjaG1lbnQg7JWI7JeQIOuRlOuLpC4g7LWc7IOB7JyEIHRleHQg66GcIOuztOuCtOuptCBibG9ja3Mg6rCAIOygleyDgSDroIzrjZTrkJjripRcbiAgICAgICAgLy8g7ZmU66m07JeQ7ISc64+EIO2VqOq7mCDstpzroKXrj7wg7KCc66qp7J20IOuRkCDrsogg67O07J2464ukLiBhdHRhY2htZW50IGZhbGxiYWNrIOydgCBibG9ja3Mg66W8XG4gICAgICAgIC8vIOuquyDqt7jrpqzripQg7YG065287J207Ja47Yq47JmAIO2RuOyLnCDslYzrprzsl5DshJzrp4wg7JOw7J2464ukLlxuICAgICAgICBhdHRhY2htZW50czogW1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZhbGxiYWNrOiBzdWJqZWN0ID8gYCR7dGl0bGV9IOKAlCAke3N1YmplY3R9YCA6IHRpdGxlLFxuICAgICAgICAgICAgLi4uKGNvbG9yID8geyBjb2xvciB9IDoge30pLFxuICAgICAgICAgICAgYmxvY2tzLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dChTTEFDS19USU1FT1VUX01TKSxcbiAgICB9KTtcblxuICAgIC8vIGJvdCDrr7jstIjrjIAobm90X2luX2NoYW5uZWwpwrfsnpjrqrvrkJwg7LGE64SQIOuTsSDqsIDsnqUg7Z2U7ZWcIOyYpOyEpOygleydgCBIVFRQIDIwMCArIG9rOmZhbHNlIOuhnCDsmKjri6QuXG4gICAgLy8gc3RhdHVzIOunjCDrs7TrqbQg7KGw7Jqp7Z6IIOyEseqztSDsspjrpqzrj7wg7JWM66a87J20IO2GteynuOuhnCDsgqzrnbzsp4Tri6QuXG4gICAgY29uc3QgYm9keSA9IChhd2FpdCByZXMuanNvbigpLmNhdGNoKCgpID0+IG51bGwpKSBhcyB7IG9rPzogYm9vbGVhbjsgZXJyb3I/OiBzdHJpbmcgfSB8IG51bGw7XG4gICAgaWYgKCFib2R5Py5vaykge1xuICAgICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiByZWplY3RlZDogJHtib2R5Py5lcnJvciA/PyBgaHR0cCAke3Jlcy5zdGF0dXN9YH1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dnZXIud2Fybihgc2xhY2sgbm90aWZpY2F0aW9uIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQVlBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7O0FBR3pCLE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBbUJBLGVBQXNCLFlBQ3BCLGNBQ0EsY0FBaUMsWUFDbEI7Q0FDZixNQUFNLFdBQVcsWUFBWSxrQkFBa0IsaUJBQWlCO0NBQ2hFLE1BQU0sVUFBVSxZQUFZLG1CQUFtQixrQkFBa0I7Q0FDakUsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLE9BQU8sUUFBUSxRQUFRO0NBQ3hELElBQUksQ0FBQyxZQUFZLENBQUMsU0FBUztFQUN6QixPQUFPLE1BQU0sZ0RBQWdELE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDckY7Q0FDRjtDQUlBLElBQUksQ0FBQyxVQUFVLFlBQVksaUJBQWlCLGVBQWUsTUFBTSxTQUFTO0VBQ3hFLE9BQU8sTUFBTSwwQ0FBMEMsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUMvRTtDQUNGO0NBR0EsSUFBSSxDQUFDLFVBQVUsYUFBYSx1QkFBTyxJQUFJLEtBQUssR0FBRyxXQUFXLEdBQUc7RUFDM0QsT0FBTyxNQUFNLHVDQUF1QyxNQUFNLEdBQUcsV0FBVyxJQUFJO0VBQzVFO0NBQ0Y7Q0FFQSxNQUFNLFNBQW9CLENBQ3hCO0VBQ0UsTUFBTTtFQUNOLE1BQU07R0FBRSxNQUFNO0dBQVUsTUFBTSxVQUFVLElBQUksTUFBTSxPQUFPLFFBQVEsTUFBTSxJQUFJLE1BQU07RUFBRztDQUN0RixDQUNGO0NBQ0EsSUFBSSxTQUNGLE9BQU8sS0FBSztFQUFFLE1BQU07RUFBVyxVQUFVLENBQUM7R0FBRSxNQUFNO0dBQVUsTUFBTTtFQUFRLENBQUM7Q0FBRSxDQUFDO0NBR2hGLElBQUk7RUFDRixNQUFNLE1BQU0sTUFBTSxNQUFNLHdCQUF3QjtHQUM5QyxRQUFRO0dBQ1IsU0FBUztJQUNQLGdCQUFnQjtJQUNoQixlQUFlLFVBQVU7R0FDM0I7R0FDQSxNQUFNLEtBQUssVUFBVTtJQUNuQjtJQUlBLGFBQWEsQ0FDWDtLQUNFLFVBQVUsVUFBVSxHQUFHLE1BQU0sS0FBSyxZQUFZO0tBQzlDLEdBQUksUUFBUSxFQUFFLE1BQU0sSUFBSSxDQUFDO0tBQ3pCO0lBQ0YsQ0FDRjtHQUNGLENBQUM7R0FDRCxRQUFRLFlBQVksUUFBUSxnQkFBZ0I7RUFDOUMsQ0FBQztFQUlELE1BQU0sT0FBUSxNQUFNLElBQUksS0FBSyxDQUFDLENBQUMsWUFBWSxJQUFJO0VBQy9DLElBQUksQ0FBQyxNQUFNLElBQ1QsT0FBTyxLQUFLLGdDQUFnQyxNQUFNLFNBQVMsUUFBUSxJQUFJLFVBQVU7Q0FFckYsU0FBUyxHQUFHO0VBQ1YsT0FBTyxLQUFLLDhCQUErQixFQUFZLFNBQVM7Q0FDbEU7QUFDRiJ9
80
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuKlCBgdXJnZW50YCDqsIAg7JWE64uMIOyVjOumvOydhCDrs7TrgrTsp4Ag7JWK64qU64ukLlxuICog7JWM66a8IOqyveuhnOqwgCDthqDtgbAg65287Jqw7YyF7J2064KYIOyYpOulmCDsoITtjIzrpbwg7KeA7JewwrfssKjri6jtlbTshJzripQg7JWIIOuQnOuLpC5cbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZ2V0U2V0dGluZyB9IGZyb20gXCIuLi9hcHBsaWNhdGlvbi9zZXR0aW5nL3NldHRpbmcuc3RvcmVcIjtcbmltcG9ydCB7IGlzUXVpZXRIb3VycyB9IGZyb20gXCIuL3F1aWV0LWhvdXJzXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuZXhwb3J0IGNsYXNzIFNsYWNrTm90aWZpY2F0aW9uRXJyb3IgZXh0ZW5kcyBFcnJvciB7fVxuXG4vKiog7IOB7YOc66W8IOyDieycvOuhnCDrqLzsoIAg7J297Z6I6rKMIO2VmOuKlCBhdHRhY2htZW50IOyDieyDgSDrsJQuICovXG5leHBvcnQgY29uc3QgU0xBQ0tfQ09MT1IgPSB7XG4gIGdvb2Q6IFwiIzJlYjg4NlwiLFxuICBiYWQ6IFwiI2UwMWU1YVwiLFxufSBhcyBjb25zdDtcblxuZXhwb3J0IHR5cGUgU2xhY2tOb3RpZmljYXRpb24gPSB7XG4gIC8qKiDtlZwg7KSEIOygnOuqqS4g66y07IqoIOydvOydtCDsnbzslrTrgqzripTsp4Drp4wg64u064qU64ukLiAqL1xuICB0aXRsZTogc3RyaW5nO1xuICAvKiog7KCc66qpIOyYhiDsvZTrk5wg7Iqk7YOA7J2866GcIOu2meuKlCDrjIDsg4Eg7Iud67OE7J6QLiAqL1xuICBzdWJqZWN0Pzogc3RyaW5nO1xuICAvKiog7KCc66qpIOyVhOuemCDsnpHsnYAg6riA7JSo66GcIOuCtOugpOqwgOuKlCDrtoDqsIAg7KCV67O0IOKAlCDsnb3sp4Ag7JWK7JWE64+EIOuQmOuKlCDqsoPrk6QuICovXG4gIGNvbnRleHQ/OiBzdHJpbmc7XG4gIGNvbG9yPzogKHR5cGVvZiBTTEFDS19DT0xPUilba2V5b2YgdHlwZW9mIFNMQUNLX0NPTE9SXTtcbiAgLyoqXG4gICAqIOyhsOyaqSDsi5zqsIQo7KO866eQwrcyMH447IucKeyXkOuPhCDrs7Trgrjri6QuIOyEnOu5hOyKpOqwgCDrqYjstpgg7IOB7YOc7LKY65+8IOyngOq4iCDslYzsp4Ag7JWK7Jy866m0IOyGkO2VtOqwgFxuICAgKiDsu6Tsp4DripQg7IKs6rG07JeQ66eMIOyTtOuLpCDigJQg64Ko7Jqp7ZWY66m0IOyhsOyaqSDsi5zqsIQg7J6Q7LK06rCAIOustOydmOuvuO2VtOynhOuLpC5cbiAgICovXG4gIHVyZ2VudD86IGJvb2xlYW47XG4gIC8qKiBNYW51YWwgc2VuZHMgbXVzdCByZXBvcnQgZGVsaXZlcnkgZmFpbHVyZXM7IGJhY2tncm91bmQgbm90aWZpY2F0aW9ucyByZW1haW4gZmFpbC1vcGVuLiAqL1xuICB0aHJvd09uRmFpbHVyZT86IGJvb2xlYW47XG4gIC8qKiDsobDsmqkg7Iuc6rCEIO2MkOyglSDquLDspIAg7Iuc6rCBLiDthYzsiqTtirjsl5DshJwg7Iuc6rOE66W8IOqzoOygle2VoCDrlYzrp4wg64SY6ri064ukLiAqL1xuICBub3c/OiBEYXRlO1xufTtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIG5vdGlmeVNsYWNrKFxuICBub3RpZmljYXRpb246IFNsYWNrTm90aWZpY2F0aW9uLFxuICByZWFkU2V0dGluZzogdHlwZW9mIGdldFNldHRpbmcgPSBnZXRTZXR0aW5nLFxuKTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IGJvdFRva2VuID0gcmVhZFNldHRpbmcoXCJzbGFjay5ib3RUb2tlblwiLCBcIlNMQUNLX0JPVF9UT0tFTlwiKTtcbiAgY29uc3QgY2hhbm5lbCA9IHJlYWRTZXR0aW5nKFwic2xhY2suY2hhbm5lbElkXCIsIFwiU0xBQ0tfQ0hBTk5FTF9JRFwiKTtcbiAgY29uc3QgeyB0aXRsZSwgc3ViamVjdCwgY29udGV4dCwgY29sb3IsIHVyZ2VudCwgbm93IH0gPSBub3RpZmljYXRpb247XG4gIGlmICghYm90VG9rZW4gfHwgIWNoYW5uZWwpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIG5vdCBjb25maWd1cmVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIGlmIChub3RpZmljYXRpb24udGhyb3dPbkZhaWx1cmUpIHtcbiAgICAgIHRocm93IG5ldyBTbGFja05vdGlmaWNhdGlvbkVycm9yKFwiU2xhY2sg67SHIO2GoO2BsOqzvCDssYTrhJAgSUTrpbwg7ISk7KCV7ZW0IOyjvOyEuOyalFwiKTtcbiAgICB9XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgLy8g7Jew7Zy07LKY65+8IOq3nOy5meycvOuhnCDsnqHsnYQg7IiYIOyXhuuKlCDquLDqsITsl5Ag6rSA66as7J6Q6rCAIOuCtOumrOuKlCDsiqTsnITsuZguIHVyZ2VudCDripQg7Ya16rO87Iuc7YKo64ukIOKAlFxuICAvLyDrgYgg7IOB7YOc66GcIHByb3ZpZGVyIOqwgCDsoITrtoAg7KO97Jy866m0IOyXsO2ctCDrgrTrgrQg7ISc67mE7Iqk6rCAIOupiOy2mCDspIQg7JWE66y064+EIOuqqOuluOuLpC5cbiAgaWYgKCF1cmdlbnQgJiYgcmVhZFNldHRpbmcoXCJzbGFjay5lbmFibGVkXCIsIFwiU0xBQ0tfRU5BQkxFRFwiKSA9PT0gXCJmYWxzZVwiKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBzbGFjayBkaXNhYmxlZCwgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyDrp4zro4wg7JWM66a87J2AIOyjvOq4sOyggeycvOuhnCDrsJjrs7XrkJjrr4DroZwg7Jes6riw7IScIOuyhOugpOuPhCDri6TsnYwg6re866y0IOyLnOqwhCDssqsg7KO86riw7JeQIOuLpOyLnCDsmKjri6QuXG4gIGlmICghdXJnZW50ICYmIGlzUXVpZXRIb3Vycyhub3cgPz8gbmV3IERhdGUoKSwgcmVhZFNldHRpbmcpKSB7XG4gICAgbG9nZ2VyLmRlYnVnKGBxdWlldCBob3Vycywgc2tpcHBpbmcgbm90aWZpY2F0aW9uOiAke3RpdGxlfSAke3N1YmplY3QgPz8gXCJcIn1gKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBjb25zdCBibG9ja3M6IHVua25vd25bXSA9IFtcbiAgICB7XG4gICAgICB0eXBlOiBcInNlY3Rpb25cIixcbiAgICAgIHRleHQ6IHsgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogc3ViamVjdCA/IGAqJHt0aXRsZX0qICBcXGAke3N1YmplY3R9XFxgYCA6IGAqJHt0aXRsZX0qYCB9LFxuICAgIH0sXG4gIF07XG4gIGlmIChjb250ZXh0KSB7XG4gICAgYmxvY2tzLnB1c2goeyB0eXBlOiBcImNvbnRleHRcIiwgZWxlbWVudHM6IFt7IHR5cGU6IFwibXJrZHduXCIsIHRleHQ6IGNvbnRleHQgfV0gfSk7XG4gIH1cblxuICB0cnkge1xuICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKFNMQUNLX1BPU1RfTUVTU0FHRV9VUkwsIHtcbiAgICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgICBoZWFkZXJzOiB7XG4gICAgICAgIFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOFwiLFxuICAgICAgICBBdXRob3JpemF0aW9uOiBgQmVhcmVyICR7Ym90VG9rZW59YCxcbiAgICAgIH0sXG4gICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICAgIGNoYW5uZWwsXG4gICAgICAgIC8vIGZhbGxiYWNrIOydgCBhdHRhY2htZW50IOyViOyXkCDrkZTri6QuIOy1nOyDgeychCB0ZXh0IOuhnCDrs7TrgrTrqbQgYmxvY2tzIOqwgCDsoJXsg4Eg66CM642U65CY64qUXG4gICAgICAgIC8vIO2ZlOuptOyXkOyEnOuPhCDtlajqu5gg7Lac66Cl64+8IOygnOuqqeydtCDrkZAg67KIIOuztOyduOuLpC4gYXR0YWNobWVudCBmYWxsYmFjayDsnYAgYmxvY2tzIOulvFxuICAgICAgICAvLyDrqrsg6re466as64qUIO2BtOudvOydtOyWuO2KuOyZgCDtkbjsi5wg7JWM66a87JeQ7ISc66eMIOyTsOyduOuLpC5cbiAgICAgICAgYXR0YWNobWVudHM6IFtcbiAgICAgICAgICB7XG4gICAgICAgICAgICBmYWxsYmFjazogc3ViamVjdCA/IGAke3RpdGxlfSDigJQgJHtzdWJqZWN0fWAgOiB0aXRsZSxcbiAgICAgICAgICAgIC4uLihjb2xvciA/IHsgY29sb3IgfSA6IHt9KSxcbiAgICAgICAgICAgIGJsb2NrcyxcbiAgICAgICAgICB9LFxuICAgICAgICBdLFxuICAgICAgfSksXG4gICAgICBzaWduYWw6IEFib3J0U2lnbmFsLnRpbWVvdXQoU0xBQ0tfVElNRU9VVF9NUyksXG4gICAgfSk7XG5cbiAgICAvLyBib3Qg66+47LSI64yAKG5vdF9pbl9jaGFubmVsKcK37J6Y66q765CcIOyxhOuEkCDrk7Eg6rCA7J6lIO2dlO2VnCDsmKTshKTsoJXsnYAgSFRUUCAyMDAgKyBvazpmYWxzZSDroZwg7Jio64ukLlxuICAgIC8vIHN0YXR1cyDrp4wg67O066m0IOyhsOyaqe2eiCDshLHqs7Ug7LKY66as64+8IOyVjOumvOydtCDthrXsp7jroZwg7IKs65287KeE64ukLlxuICAgIGNvbnN0IGJvZHkgPSAoYXdhaXQgcmVzLmpzb24oKS5jYXRjaCgoKSA9PiBudWxsKSkgYXMgeyBvaz86IGJvb2xlYW47IGVycm9yPzogc3RyaW5nIH0gfCBudWxsO1xuICAgIGlmICghcmVzLm9rIHx8IGJvZHk/Lm9rICE9PSB0cnVlKSB7XG4gICAgICBjb25zdCBjb2RlID1cbiAgICAgICAgdHlwZW9mIGJvZHk/LmVycm9yID09PSBcInN0cmluZ1wiICYmIC9eW2Etel9dezEsODB9JC8udGVzdChib2R5LmVycm9yKVxuICAgICAgICAgID8gYm9keS5lcnJvclxuICAgICAgICAgIDogYEhUVFAgJHtyZXMuc3RhdHVzfWA7XG4gICAgICB0aHJvdyBuZXcgU2xhY2tOb3RpZmljYXRpb25FcnJvcihgU2xhY2sg7JWM66a8IOuwnOyGoSDsi6TtjKg6ICR7Y29kZX1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zdCBlcnJvciA9XG4gICAgICBlIGluc3RhbmNlb2YgU2xhY2tOb3RpZmljYXRpb25FcnJvclxuICAgICAgICA/IGVcbiAgICAgICAgOiBuZXcgU2xhY2tOb3RpZmljYXRpb25FcnJvcihcIlNsYWNrIOyVjOumvCDrsJzshqEg7Iuk7YyoOiDrhKTtirjsm4ztgawg7Jik66WYIOuYkOuKlCDsnZHri7Ug7Iuc6rCEIOy0iOqzvFwiKTtcbiAgICBsb2dnZXIud2FybihlcnJvci5tZXNzYWdlKTtcbiAgICBpZiAobm90aWZpY2F0aW9uLnRocm93T25GYWlsdXJlKSB0aHJvdyBlcnJvcjtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7OztBQVlBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7QUFFekIsSUFBYSx5QkFBYixjQUE0QyxNQUFNLENBQUM7O0FBR25ELE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBcUJBLGVBQXNCLFlBQ3BCLGNBQ0EsY0FBaUMsWUFDbEI7Q0FDZixNQUFNLFdBQVcsWUFBWSxrQkFBa0IsaUJBQWlCO0NBQ2hFLE1BQU0sVUFBVSxZQUFZLG1CQUFtQixrQkFBa0I7Q0FDakUsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLE9BQU8sUUFBUSxRQUFRO0NBQ3hELElBQUksQ0FBQyxZQUFZLENBQUMsU0FBUztFQUN6QixPQUFPLE1BQU0sZ0RBQWdELE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDckYsSUFBSSxhQUFhLGdCQUNmLE1BQU0sSUFBSSx1QkFBdUIsNEJBQTRCO0VBRS9EO0NBQ0Y7Q0FJQSxJQUFJLENBQUMsVUFBVSxZQUFZLGlCQUFpQixlQUFlLE1BQU0sU0FBUztFQUN4RSxPQUFPLE1BQU0sMENBQTBDLE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDL0U7Q0FDRjtDQUdBLElBQUksQ0FBQyxVQUFVLGFBQWEsdUJBQU8sSUFBSSxLQUFLLEdBQUcsV0FBVyxHQUFHO0VBQzNELE9BQU8sTUFBTSx1Q0FBdUMsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUM1RTtDQUNGO0NBRUEsTUFBTSxTQUFvQixDQUN4QjtFQUNFLE1BQU07RUFDTixNQUFNO0dBQUUsTUFBTTtHQUFVLE1BQU0sVUFBVSxJQUFJLE1BQU0sT0FBTyxRQUFRLE1BQU0sSUFBSSxNQUFNO0VBQUc7Q0FDdEYsQ0FDRjtDQUNBLElBQUksU0FDRixPQUFPLEtBQUs7RUFBRSxNQUFNO0VBQVcsVUFBVSxDQUFDO0dBQUUsTUFBTTtHQUFVLE1BQU07RUFBUSxDQUFDO0NBQUUsQ0FBQztDQUdoRixJQUFJO0VBQ0YsTUFBTSxNQUFNLE1BQU0sTUFBTSx3QkFBd0I7R0FDOUMsUUFBUTtHQUNSLFNBQVM7SUFDUCxnQkFBZ0I7SUFDaEIsZUFBZSxVQUFVO0dBQzNCO0dBQ0EsTUFBTSxLQUFLLFVBQVU7SUFDbkI7SUFJQSxhQUFhLENBQ1g7S0FDRSxVQUFVLFVBQVUsR0FBRyxNQUFNLEtBQUssWUFBWTtLQUM5QyxHQUFJLFFBQVEsRUFBRSxNQUFNLElBQUksQ0FBQztLQUN6QjtJQUNGLENBQ0Y7R0FDRixDQUFDO0dBQ0QsUUFBUSxZQUFZLFFBQVEsZ0JBQWdCO0VBQzlDLENBQUM7RUFJRCxNQUFNLE9BQVEsTUFBTSxJQUFJLEtBQUssQ0FBQyxDQUFDLFlBQVksSUFBSTtFQUMvQyxJQUFJLENBQUMsSUFBSSxNQUFNLE1BQU0sT0FBTyxNQUsxQixNQUFNLElBQUksdUJBQXVCLG1CQUgvQixPQUFPLE1BQU0sVUFBVSxZQUFZLGlCQUFpQixLQUFLLEtBQUssS0FBSyxJQUMvRCxLQUFLLFFBQ0wsUUFBUSxJQUFJLFVBQ3dDO0NBRTlELFNBQVMsR0FBRztFQUNWLE1BQU0sUUFDSixhQUFhLHlCQUNULElBQ0EsSUFBSSx1QkFBdUIscUNBQXFDO0VBQ3RFLE9BQU8sS0FBSyxNQUFNLE9BQU87RUFDekIsSUFBSSxhQUFhLGdCQUFnQixNQUFNO0NBQ3pDO0FBQ0YifQ==
@@ -157,7 +157,12 @@ describe("expired token reminder scheduling", () => {
157
157
  const sent = await sendExpiredTokenReminderNow(deps);
158
158
 
159
159
  expect(sent).toBe(1);
160
- expect(notifySlackMock).toHaveBeenCalledWith(expect.objectContaining({ urgent: true }));
160
+ expect(notifySlackMock).toHaveBeenCalledWith(expect.objectContaining({ urgent: true, throwOnFailure: true }));
161
+ });
162
+
163
+ it("never reports a sent count when manual Slack delivery fails", async () => {
164
+ notifySlackMock.mockRejectedValue(new Error("Slack delivery failed"));
165
+ await expect(sendExpiredTokenReminderNow(deps)).rejects.toThrow("Slack delivery failed");
161
166
  });
162
167
 
163
168
  it("보낼 대상이 없으면 0을 돌려주고 아무것도 보내지 않는다", async () => {
@@ -54,7 +54,10 @@ export function buildReminderContext(
54
54
  return [...lines, "재로그인이 필요합니다"].filter(Boolean).join("\n");
55
55
  }
56
56
 
57
- async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Promise<number> {
57
+ async function sendReminder(
58
+ deps: ExpiredTokenReminderDeps,
59
+ mode: "scheduled" | "manual",
60
+ ): Promise<number> {
58
61
  const expired = await deps.findReauthRequired();
59
62
  if (expired.length === 0) return 0;
60
63
 
@@ -66,7 +69,8 @@ async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Pro
66
69
  subject: expired.length > 1 ? `${expired.length}건` : (expired[0]!.name ?? "unnamed"),
67
70
  context: buildReminderContext(expired, userMap),
68
71
  color: SLACK_COLOR.bad,
69
- urgent,
72
+ urgent: mode === "manual",
73
+ throwOnFailure: mode === "manual",
70
74
  });
71
75
  return expired.length;
72
76
  }
@@ -80,7 +84,7 @@ async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Pro
80
84
  export function sendExpiredTokenReminderNow(
81
85
  deps: ExpiredTokenReminderDeps = defaultDeps,
82
86
  ): Promise<number> {
83
- return sendReminder(deps, true);
87
+ return sendReminder(deps, "manual");
84
88
  }
85
89
 
86
90
  let timer: ReturnType<typeof setInterval> | null = null;
@@ -104,7 +108,9 @@ function scheduleExpiredTokenReminder(runNow: boolean, deps: ExpiredTokenReminde
104
108
  );
105
109
 
106
110
  const run = () =>
107
- sendReminder(deps).catch((e) => logger.warn(`reminder failed: ${(e as Error).message}`));
111
+ sendReminder(deps, "scheduled").catch((e) =>
112
+ logger.warn(`reminder failed: ${(e as Error).message}`),
113
+ );
108
114
 
109
115
  if (runNow) void run();
110
116
  timer = setInterval(run, minutes * MINUTE_MS);
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { estimateImageGenerationCostMicroUsd, imageGenerationCostMethod } from "./qgrid-image-generation";
4
+ import { type ImageGenerationMetadata, type QgridContent, type QueryOutput } from "./qgrid.types";
5
+
6
+ const options = { quality: "high" as const, size: "1024x1024" as const, background: "transparent" as const };
7
+ const generation: ImageGenerationMetadata = {
8
+ route: "codex-images", model: "gpt-image-2", background: "transparent",
9
+ size: "1254x1254", quality: "medium",
10
+ };
11
+ function result(content: QgridContent[]): QueryOutput {
12
+ return {
13
+ text: "", content, finishReason: "stop", tokenName: "token", model: "gpt-image-2",
14
+ usage: { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
15
+ durationMs: 10, ttftMs: 0, costUsd: 0, costSource: "pricing_table",
16
+ };
17
+ }
18
+
19
+ describe("standalone image cost estimates", () => {
20
+ it("uses response usage once for multiple outputs without pricing requested settings or driver tokens", () => {
21
+ const output = result([
22
+ { type: "image", data: "first", generation: { ...generation,
23
+ usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300,
24
+ input_tokens_details: { text_tokens: 40, image_tokens: 60, cached_tokens: 50 } },
25
+ } },
26
+ { type: "image", data: "second", generation },
27
+ ]);
28
+ expect(estimateImageGenerationCostMicroUsd(output, options)).toBe(6_680);
29
+ expect(imageGenerationCostMethod(options, output)).toBe(
30
+ "estimated:gpt-image-2:reported-usage:public-prices:conservative",
31
+ );
32
+ });
33
+
34
+ it("does not substitute requested size pricing when standalone usage is missing", () => {
35
+ expect(estimateImageGenerationCostMicroUsd(result([{ type: "image", data: "png", generation }]), options)).toBeNull();
36
+ });
37
+
38
+ it("prices unknown input remainder at the image rate and ignores inconsistent splits", () => {
39
+ for (const [textTokens, imageTokens, expected] of [[40, 10, 6_680], [100, 60, 6_800]]) {
40
+ const output = result([{ type: "image", data: "png", generation: { ...generation, usage: {
41
+ input_tokens: 100, output_tokens: 200, total_tokens: 300,
42
+ input_tokens_details: { text_tokens: textTokens!, image_tokens: imageTokens! },
43
+ } } }]);
44
+ expect(estimateImageGenerationCostMicroUsd(output, options)).toBe(expected);
45
+ }
46
+ });
47
+
48
+ it("preserves legacy per-image price assumptions when response metadata is absent", () => {
49
+ const output = result([{ type: "image", data: "first" }, { type: "image", data: "second" }]);
50
+ expect(estimateImageGenerationCostMicroUsd(output, options)).toBe(422_000);
51
+ expect(imageGenerationCostMethod(options, output)).toBe("assumed:gpt-image-2:high:1024x1024:png");
52
+ });
53
+ });
@@ -46,7 +46,17 @@ export function resolveImageGenerationOptions(
46
46
  };
47
47
  }
48
48
 
49
- export function imageGenerationCostMethod(options: ImageGenerationOptions | undefined): string {
49
+ export function imageGenerationCostMethod(
50
+ options: ImageGenerationOptions | undefined,
51
+ result?: QueryOutput,
52
+ ): string {
53
+ if (
54
+ result?.content.some(
55
+ (item) => item.type === "image" && item.generation?.route === "codex-images",
56
+ )
57
+ ) {
58
+ return "estimated:gpt-image-2:reported-usage:public-prices:conservative";
59
+ }
50
60
  const resolved = resolveImageGenerationOptions(options);
51
61
  return `assumed:${CODEX_IMAGE_GENERATION_MODEL}:${resolved.quality}:${resolved.size}:png`;
52
62
  }
@@ -55,8 +65,28 @@ export function estimateImageGenerationCostMicroUsd(
55
65
  result: QueryOutput,
56
66
  options: ImageGenerationOptions | undefined,
57
67
  ): number | null {
58
- const imageCount = result.content.filter((item) => item.type === "image").length;
68
+ const images = result.content.filter((item) => item.type === "image");
69
+ const imageCount = images.length;
59
70
  if (imageCount === 0) return null;
71
+ if (images.some((image) => image.generation?.route === "codex-images")) {
72
+ // The standalone response's aggregate usage is carried by its first image only.
73
+ // Do not multiply it by output count or price it as the requested text driver.
74
+ const usage = images.find((image) => image.generation?.usage)?.generation?.usage;
75
+ if (!usage) return null;
76
+ // Public GPT Image 2 rates verified 2026-09-08: text input $5/M, image input
77
+ // $8/M, output $30/M. Amounts below are microUSD, so no /1M conversion.
78
+ // https://developers.openai.com/api/docs/pricing#image-generation
79
+ // Use a consistent reported split; unknown input is conservatively priced as
80
+ // images. Cache discounts and private subscription billing remain unknown.
81
+ const details = usage.input_tokens_details;
82
+ const textTokens =
83
+ details && details.text_tokens + details.image_tokens <= usage.input_tokens
84
+ ? details.text_tokens
85
+ : 0;
86
+ return Math.round(
87
+ textTokens * 5 + (usage.input_tokens - textTokens) * 8 + usage.output_tokens * 30,
88
+ );
89
+ }
60
90
  const resolved = resolveImageGenerationOptions(options);
61
91
  return imageCount * IMAGE_OUTPUT_COST_MICRO_USD[resolved.quality][resolved.size];
62
92
  }
@@ -1,8 +1,39 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { buildImageGenerationToolSteps, imageGenerationToolArgs } from "./qgrid-response-format";
3
+ import { buildImageGenerationToolSteps, formatImagePartForLog, imageGenerationToolArgs } from "./qgrid-response-format";
4
+
5
+ it("logs observed standalone settings separately from requested controls without a requested-price assumption", () => {
6
+ const requestedOptions = { quality: "high" as const, size: "1024x1024" as const, background: "transparent" as const };
7
+ const generation = {
8
+ route: "codex-images" as const, model: "gpt-image-2" as const,
9
+ quality: "medium", size: "1254x1254", background: "transparent",
10
+ usage: { input_tokens: 10, output_tokens: 100, total_tokens: 110 },
11
+ };
12
+ const [step] = buildImageGenerationToolSteps({
13
+ prompt: "draw", model: "openai/gpt-5.5", imageGenerationOptions: requestedOptions,
14
+ }, [{ type: "image", data: "png", generation }], 0);
15
+ const args = JSON.parse(step!.tool_args);
16
+ expect(args.requestedOptions).toEqual(requestedOptions);
17
+ expect(args.observedGeneration).toEqual(generation);
18
+ expect(args).not.toHaveProperty("pricingAssumption");
19
+ });
4
20
 
5
21
  describe("qgrid response log formatting", () => {
22
+ it("logs the returned MIME and requested output format", () => {
23
+ expect(formatImagePartForLog({ type: "image", data: "webp", mediaType: "image/webp" })).toContain("data:image/webp;base64,webp");
24
+ expect(JSON.parse(imageGenerationToolArgs({ prompt: "draw", imageGenerationOptions: { outputFormat: "webp" } })).tool.outputFormat).toBe("webp");
25
+ });
26
+
27
+ it("records requested image controls separately from pricing assumptions", () => {
28
+ const requestedOptions = { background: "transparent", size: "1024x1024", quality: "high" } as const;
29
+ const args = JSON.parse(imageGenerationToolArgs({
30
+ prompt: "an isolated circle", imageGeneration: true,
31
+ imageGenerationOptions: requestedOptions,
32
+ }));
33
+ expect(args.requestedOptions).toEqual(requestedOptions);
34
+ expect(args.pricingAssumption).not.toHaveProperty("background");
35
+ });
36
+
6
37
  it("includes input images in image-generation tool args", () => {
7
38
  expect(
8
39
  JSON.parse(
@@ -2,7 +2,12 @@ import {
2
2
  CODEX_IMAGE_GENERATION_MODEL,
3
3
  resolveImageGenerationOptions,
4
4
  } from "./qgrid-image-generation";
5
- import { type QgridContent, type QueryInput, type QueryOutput } from "./qgrid.types";
5
+ import {
6
+ type ImageGenerationMetadata,
7
+ type QgridContent,
8
+ type QueryInput,
9
+ type QueryOutput,
10
+ } from "./qgrid.types";
6
11
 
7
12
  export const CODEX_IMAGE_GENERATION_TOOL_CONFIG = {
8
13
  type: "image_generation",
@@ -21,10 +26,7 @@ export function formatResponseForLog(result: QueryOutput): string {
21
26
  return result.content
22
27
  .flatMap((item) => {
23
28
  if (item.type === "text") return [item.text];
24
- if (item.type === "image") {
25
- const alt = escapeHtmlAttribute(item.revisedPrompt ?? "generated image");
26
- return [`<img src="data:image/png;base64,${item.data}" alt="${alt}" />`];
27
- }
29
+ if (item.type === "image") return [formatImagePartForLog(item)];
28
30
  return [];
29
31
  })
30
32
  .filter((part) => part.length > 0)
@@ -37,12 +39,12 @@ export function getImageParts(result: QueryOutput): Extract<QgridContent, { type
37
39
 
38
40
  export function formatImagePartForLog(image: Extract<QgridContent, { type: "image" }>): string {
39
41
  const alt = escapeHtmlAttribute(image.revisedPrompt ?? "generated image");
40
- return `<img src="data:image/png;base64,${image.data}" alt="${alt}" />`;
42
+ return `<img src="data:${image.mediaType ?? "image/png"};base64,${image.data}" alt="${alt}" />`;
41
43
  }
42
44
 
43
45
  export function imageGenerationToolArgs(
44
46
  args: QueryInput,
45
- options: { includeInputImages?: boolean } = {},
47
+ options: { includeInputImages?: boolean; generation?: ImageGenerationMetadata } = {},
46
48
  ): string {
47
49
  const resolved = resolveImageGenerationOptions(args.imageGenerationOptions);
48
50
  const inputImages =
@@ -51,12 +53,22 @@ export function imageGenerationToolArgs(
51
53
  prompt: args.prompt,
52
54
  ...(inputImages.length > 0 ? { inputImages } : {}),
53
55
  driverModel: args.model ?? null,
54
- tool: CODEX_IMAGE_GENERATION_TOOL_CONFIG,
55
- pricingAssumption: {
56
- model: CODEX_IMAGE_GENERATION_MODEL,
57
- quality: resolved.quality,
58
- size: resolved.size,
56
+ tool: {
57
+ ...CODEX_IMAGE_GENERATION_TOOL_CONFIG,
58
+ ...(args.imageGenerationOptions?.outputFormat
59
+ ? { outputFormat: args.imageGenerationOptions.outputFormat }
60
+ : {}),
59
61
  },
62
+ ...(args.imageGenerationOptions ? { requestedOptions: args.imageGenerationOptions } : {}),
63
+ ...(options.generation
64
+ ? { observedGeneration: options.generation }
65
+ : {
66
+ pricingAssumption: {
67
+ model: CODEX_IMAGE_GENERATION_MODEL,
68
+ quality: resolved.quality,
69
+ size: resolved.size,
70
+ },
71
+ }),
60
72
  });
61
73
  }
62
74
 
@@ -75,18 +87,16 @@ export function buildImageGenerationToolSteps(
75
87
  }> {
76
88
  if (images.length === 0) return [];
77
89
 
78
- const firstToolArgs = imageGenerationToolArgs(args, { includeInputImages: true });
79
- const laterToolArgs =
80
- images.length > 1
81
- ? imageGenerationToolArgs(args, { includeInputImages: false })
82
- : firstToolArgs;
83
90
  return images.map((image, index) => ({
84
91
  step_index: stepIndex,
85
92
  type: "tool_call",
86
93
  tool_call_index: index,
87
94
  tool_call_id: `image_generation:${stepIndex}:${index}`,
88
95
  tool_name: "image_generation",
89
- tool_args: index === 0 ? firstToolArgs : laterToolArgs,
96
+ tool_args: imageGenerationToolArgs(args, {
97
+ includeInputImages: index === 0,
98
+ generation: image.generation,
99
+ }),
90
100
  tool_result: formatImagePartForLog(image),
91
101
  }));
92
102
  }
@@ -547,6 +547,38 @@ describe("qgrid run lifecycle TTFT", () => {
547
547
  );
548
548
  });
549
549
 
550
+ it("keeps standalone image accounting out of text usage and records response usage once", async () => {
551
+ const generation = {
552
+ route: "codex-images" as const, model: "gpt-image-2" as const,
553
+ size: "1254x1254", quality: "medium", background: "transparent",
554
+ };
555
+ aggregateStepUsageMock.mockResolvedValueOnce({
556
+ input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0,
557
+ duration_ms: 120, fallback_count: 0, cost_usd: 0, cost_source: "pricing_table",
558
+ });
559
+ await afterQuery(10, 0, {
560
+ prompt: "draw", model: "openai/gpt-5.5", imageGeneration: true,
561
+ imageGenerationOptions: { quality: "high", size: "1024x1024", background: "transparent" },
562
+ }, queryOutput({
563
+ model: "gpt-image-2", requestedModel: "gpt-5.5", costUsd: 0,
564
+ usage: { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
565
+ content: [
566
+ { type: "image", data: "first", generation: { ...generation, usage: {
567
+ input_tokens: 10, output_tokens: 100, total_tokens: 110,
568
+ } } },
569
+ { type: "image", data: "second", generation },
570
+ ],
571
+ }));
572
+ expect(appendStepMock).toHaveBeenCalledWith(10, expect.objectContaining({
573
+ type: "generate", model_name: "openai/gpt-image-2", requested_model_name: "openai/gpt-5.5",
574
+ input_tokens: 0, output_tokens: 0, cost_usd: 0,
575
+ }));
576
+ expect(finishRunMock).toHaveBeenCalledWith(10, expect.objectContaining({
577
+ input_tokens: 0, output_tokens: 0, cost_usd: 0, image_cost_usd: 3_080,
578
+ image_cost_method: "estimated:gpt-image-2:reported-usage:public-prices:conservative",
579
+ }));
580
+ });
581
+
550
582
  it("uses requested image quality and size for cost estimate", async () => {
551
583
  await afterQuery(
552
584
  10,
@@ -242,7 +242,7 @@ export async function afterQuery(
242
242
  image_cost_usd: imageCostMicroUsd,
243
243
  image_cost_method:
244
244
  imageCostMicroUsd !== null
245
- ? imageGenerationCostMethod(args.imageGenerationOptions)
245
+ ? imageGenerationCostMethod(args.imageGenerationOptions, result)
246
246
  : null,
247
247
  history: filterHistoryForStorage(args.history),
248
248
  });
@@ -12,6 +12,36 @@ export const QgridTool = z.object({
12
12
  });
13
13
  export type QgridTool = z.infer<typeof QgridTool>;
14
14
 
15
+ // Observed standalone Images response values, distinct from requested generation hints.
16
+ export const ImageGenerationMetadata = z.object({
17
+ route: z.literal("codex-images"),
18
+ model: z.literal("gpt-image-2"),
19
+ background: z.string().optional(),
20
+ quality: z.string().optional(),
21
+ size: z.string().optional(),
22
+ usage: z
23
+ .object({
24
+ input_tokens: z.number().nonnegative(),
25
+ output_tokens: z.number().nonnegative(),
26
+ total_tokens: z.number().nonnegative(),
27
+ input_tokens_details: z
28
+ .object({
29
+ image_tokens: z.number().nonnegative(),
30
+ text_tokens: z.number().nonnegative(),
31
+ cached_tokens: z.number().nonnegative().optional(),
32
+ })
33
+ .optional(),
34
+ output_tokens_details: z
35
+ .object({
36
+ image_tokens: z.number().nonnegative(),
37
+ text_tokens: z.number().nonnegative(),
38
+ })
39
+ .optional(),
40
+ })
41
+ .optional(),
42
+ });
43
+ export type ImageGenerationMetadata = z.infer<typeof ImageGenerationMetadata>;
44
+
15
45
  export const QgridContent = z.discriminatedUnion("type", [
16
46
  z.object({
17
47
  type: z.literal("text"),
@@ -25,9 +55,11 @@ export const QgridContent = z.discriminatedUnion("type", [
25
55
  }),
26
56
  z.object({
27
57
  type: z.literal("image"),
28
- // Codex image_generation result base64. qgrid 는 포맷/확장자 책임을 지지 않는다.
58
+ // Base64 image bytes; older responses without mediaType are PNG.
29
59
  data: z.string(),
60
+ mediaType: z.enum(["image/png", "image/jpeg", "image/webp"]).optional(),
30
61
  revisedPrompt: z.string().nullish(),
62
+ generation: ImageGenerationMetadata.optional(),
31
63
  }),
32
64
  ]);
33
65
  export type QgridContent = z.infer<typeof QgridContent>;
@@ -50,10 +82,20 @@ export type ImageGenerationQuality = z.infer<typeof ImageGenerationQuality>;
50
82
  export const ImageGenerationSize = z.enum(["1024x1024", "1024x1536", "1536x1024"]);
51
83
  export type ImageGenerationSize = z.infer<typeof ImageGenerationSize>;
52
84
 
53
- export const ImageGenerationOptions = z.object({
54
- quality: ImageGenerationQuality.optional(),
55
- size: ImageGenerationSize.optional(),
56
- });
85
+ export const ImageGenerationOptions = z
86
+ .object({
87
+ quality: ImageGenerationQuality.optional(),
88
+ size: ImageGenerationSize.optional(),
89
+ background: z.enum(["auto", "opaque", "transparent"]).optional(),
90
+ outputFormat: z.enum(["png", "jpeg", "webp"]).optional(),
91
+ })
92
+ .refine(
93
+ (options) =>
94
+ options.background !== "transparent" ||
95
+ !options.outputFormat ||
96
+ options.outputFormat === "png",
97
+ { message: "Transparent image generation only supports PNG output", path: ["outputFormat"] },
98
+ );
57
99
  export type ImageGenerationOptions = z.infer<typeof ImageGenerationOptions>;
58
100
 
59
101
  export const QgridInputPart = z.discriminatedUnion("type", [
@@ -22,6 +22,13 @@ const baseResult = {
22
22
  };
23
23
 
24
24
  describe("applyToolCallEmulation image parts", () => {
25
+ it("preserves non-PNG MIME types in response content", () => {
26
+ const out = applyToolCallEmulation(baseResult, undefined, {
27
+ images: [{ data: "webp", mediaType: "image/webp", revisedPrompt: null }], answerKind: "text",
28
+ });
29
+ expect(out.content).toContainEqual({ type: "image", data: "webp", mediaType: "image/webp", revisedPrompt: null });
30
+ });
31
+
25
32
  const img = { data: "iVBORw0KGgoBAgM", revisedPrompt: "a red circle" };
26
33
 
27
34
  it("appends an image part after text when images are present (no tools)", () => {
@@ -56,6 +63,24 @@ describe("applyToolCallEmulation image parts", () => {
56
63
  });
57
64
  expect(out.content).toEqual([{ type: "text", text: "here is your image" }]);
58
65
  });
66
+
67
+ it("preserves observed image settings and attaches response usage only once", () => {
68
+ const generation = {
69
+ route: "codex-images" as const, model: "gpt-image-2" as const,
70
+ size: "1254x1254", quality: "medium", background: "transparent",
71
+ };
72
+ const usage = { input_tokens: 10, output_tokens: 100, total_tokens: 110 };
73
+ const out = applyToolCallEmulation(baseResult, undefined, {
74
+ answerKind: "text",
75
+ images: [
76
+ { ...img, generation: { ...generation, usage } },
77
+ { ...img, generation },
78
+ ],
79
+ });
80
+ expect(out.content.filter((item) => item.type === "image").map((item) => item.generation))
81
+ .toEqual([{ ...generation, usage }, generation]);
82
+ expect(out.usage).toEqual(baseResult.usage);
83
+ });
59
84
  });
60
85
 
61
86
  describe("applyToolCallEmulation envelope validation", () => {