@cartanova/qgrid-cli 2.9.4 → 2.9.5

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 (58) 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 +8 -6
  4. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
  5. package/bundle/dist/application/qgrid/qgrid.types.js +31 -4
  6. package/bundle/dist/application/qgrid/tool-emulation.js +3 -2
  7. package/bundle/dist/application/setting/setting.model.js +8 -2
  8. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +1 -1
  9. package/bundle/dist/utils/providers/openai/openai-direct-client.js +39 -1
  10. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +41 -20
  11. package/bundle/dist/utils/providers/openai/openai-image-generation.js +145 -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 +26 -0
  20. package/bundle/src/application/qgrid/qgrid-response-format.ts +21 -13
  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 +32 -0
  24. package/bundle/src/application/qgrid/tool-emulation.test.ts +18 -0
  25. package/bundle/src/application/qgrid/tool-emulation.ts +3 -0
  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 +5 -1
  30. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +9 -1
  31. package/bundle/src/utils/providers/openai/openai-direct-client.ts +48 -1
  32. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +136 -2
  33. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +44 -18
  34. package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +135 -0
  35. package/bundle/src/utils/providers/openai/openai-image-generation.ts +177 -0
  36. package/bundle/src/utils/providers/openai/openai-quota.test.ts +32 -0
  37. package/bundle/src/utils/providers/openai/openai-quota.ts +15 -8
  38. package/bundle/src/utils/providers/openai/openai-refresh.test.ts +18 -0
  39. package/bundle/src/utils/providers/openai/openai-refresh.ts +11 -4
  40. package/bundle/src/utils/slack-notify.test.ts +14 -0
  41. package/bundle/src/utils/slack-notify.ts +19 -3
  42. package/bundle/web-dist/client/assets/{index-fogJvajr.js → index-BcCytvFf.js} +3 -3
  43. package/bundle/web-dist/client/assets/{logs-CsY2cqvt.js → logs-BkOilk5x.js} +1 -1
  44. package/bundle/web-dist/client/assets/{monit-ChgaLEcD.js → monit-8oCSLCdL.js} +1 -1
  45. package/bundle/web-dist/client/assets/{routes-BvyiTnOZ.js → routes-DFIF4IoW.js} +1 -1
  46. package/bundle/web-dist/client/assets/{settings-ZR2J9rWb.js → settings-nUI-DR3U.js} +1 -1
  47. package/bundle/web-dist/client/assets/{show-CjwO4-So.js → show-Bzx6Xkoc.js} +1 -1
  48. package/bundle/web-dist/client/assets/{tokens-Bx-hG1gm.js → tokens-DFWCL7Ts.js} +1 -1
  49. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Bqax_icS.js → use-oauth-login-flow-B5MAjpVc.js} +1 -1
  50. package/bundle/web-dist/client/index.html +1 -1
  51. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  52. package/package.json +2 -1
  53. package/skills/qgrid/SKILL.md +1 -1
  54. package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
  55. package/skills/qgrid/references/decision-rationale.md +2 -2
  56. package/skills/qgrid/references/openai-codex-runtime.md +8 -0
  57. package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
  58. 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
  }
@@ -2,7 +2,33 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { buildImageGenerationToolSteps, imageGenerationToolArgs } from "./qgrid-response-format";
4
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
+ });
20
+
5
21
  describe("qgrid response log formatting", () => {
22
+ it("records requested image controls separately from pricing assumptions", () => {
23
+ const requestedOptions = { background: "transparent", size: "1024x1024", quality: "high" } as const;
24
+ const args = JSON.parse(imageGenerationToolArgs({
25
+ prompt: "an isolated circle", imageGeneration: true,
26
+ imageGenerationOptions: requestedOptions,
27
+ }));
28
+ expect(args.requestedOptions).toEqual(requestedOptions);
29
+ expect(args.pricingAssumption).not.toHaveProperty("background");
30
+ });
31
+
6
32
  it("includes input images in image-generation tool args", () => {
7
33
  expect(
8
34
  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",
@@ -42,7 +47,7 @@ export function formatImagePartForLog(image: Extract<QgridContent, { type: "imag
42
47
 
43
48
  export function imageGenerationToolArgs(
44
49
  args: QueryInput,
45
- options: { includeInputImages?: boolean } = {},
50
+ options: { includeInputImages?: boolean; generation?: ImageGenerationMetadata } = {},
46
51
  ): string {
47
52
  const resolved = resolveImageGenerationOptions(args.imageGenerationOptions);
48
53
  const inputImages =
@@ -52,11 +57,16 @@ export function imageGenerationToolArgs(
52
57
  ...(inputImages.length > 0 ? { inputImages } : {}),
53
58
  driverModel: args.model ?? null,
54
59
  tool: CODEX_IMAGE_GENERATION_TOOL_CONFIG,
55
- pricingAssumption: {
56
- model: CODEX_IMAGE_GENERATION_MODEL,
57
- quality: resolved.quality,
58
- size: resolved.size,
59
- },
60
+ ...(args.imageGenerationOptions ? { requestedOptions: args.imageGenerationOptions } : {}),
61
+ ...(options.generation
62
+ ? { observedGeneration: options.generation }
63
+ : {
64
+ pricingAssumption: {
65
+ model: CODEX_IMAGE_GENERATION_MODEL,
66
+ quality: resolved.quality,
67
+ size: resolved.size,
68
+ },
69
+ }),
60
70
  });
61
71
  }
62
72
 
@@ -75,18 +85,16 @@ export function buildImageGenerationToolSteps(
75
85
  }> {
76
86
  if (images.length === 0) return [];
77
87
 
78
- const firstToolArgs = imageGenerationToolArgs(args, { includeInputImages: true });
79
- const laterToolArgs =
80
- images.length > 1
81
- ? imageGenerationToolArgs(args, { includeInputImages: false })
82
- : firstToolArgs;
83
88
  return images.map((image, index) => ({
84
89
  step_index: stepIndex,
85
90
  type: "tool_call",
86
91
  tool_call_index: index,
87
92
  tool_call_id: `image_generation:${stepIndex}:${index}`,
88
93
  tool_name: "image_generation",
89
- tool_args: index === 0 ? firstToolArgs : laterToolArgs,
94
+ tool_args: imageGenerationToolArgs(args, {
95
+ includeInputImages: index === 0,
96
+ generation: image.generation,
97
+ }),
90
98
  tool_result: formatImagePartForLog(image),
91
99
  }));
92
100
  }
@@ -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"),
@@ -28,6 +58,7 @@ export const QgridContent = z.discriminatedUnion("type", [
28
58
  // Codex image_generation result base64. qgrid 는 포맷/확장자 책임을 지지 않는다.
29
59
  data: z.string(),
30
60
  revisedPrompt: z.string().nullish(),
61
+ generation: ImageGenerationMetadata.optional(),
31
62
  }),
32
63
  ]);
33
64
  export type QgridContent = z.infer<typeof QgridContent>;
@@ -53,6 +84,7 @@ export type ImageGenerationSize = z.infer<typeof ImageGenerationSize>;
53
84
  export const ImageGenerationOptions = z.object({
54
85
  quality: ImageGenerationQuality.optional(),
55
86
  size: ImageGenerationSize.optional(),
87
+ background: z.enum(["auto", "opaque", "transparent"]).optional(),
56
88
  });
57
89
  export type ImageGenerationOptions = z.infer<typeof ImageGenerationOptions>;
58
90
 
@@ -56,6 +56,24 @@ describe("applyToolCallEmulation image parts", () => {
56
56
  });
57
57
  expect(out.content).toEqual([{ type: "text", text: "here is your image" }]);
58
58
  });
59
+
60
+ it("preserves observed image settings and attaches response usage only once", () => {
61
+ const generation = {
62
+ route: "codex-images" as const, model: "gpt-image-2" as const,
63
+ size: "1254x1254", quality: "medium", background: "transparent",
64
+ };
65
+ const usage = { input_tokens: 10, output_tokens: 100, total_tokens: 110 };
66
+ const out = applyToolCallEmulation(baseResult, undefined, {
67
+ answerKind: "text",
68
+ images: [
69
+ { ...img, generation: { ...generation, usage } },
70
+ { ...img, generation },
71
+ ],
72
+ });
73
+ expect(out.content.filter((item) => item.type === "image").map((item) => item.generation))
74
+ .toEqual([{ ...generation, usage }, generation]);
75
+ expect(out.usage).toEqual(baseResult.usage);
76
+ });
59
77
  });
60
78
 
61
79
  describe("applyToolCallEmulation envelope validation", () => {
@@ -12,6 +12,7 @@ const STRUCTURED_ENVELOPE_LIMITS = {
12
12
  maxDepth: 132,
13
13
  } as const;
14
14
  import {
15
+ type ImageGenerationMetadata,
15
16
  type QgridContent,
16
17
  type QgridThreadCoord,
17
18
  type QgridTool,
@@ -70,6 +71,7 @@ export class ToolCallEmulationError extends Error {
70
71
  interface EmulationImage {
71
72
  data: string;
72
73
  revisedPrompt?: string | null;
74
+ generation?: ImageGenerationMetadata;
73
75
  }
74
76
 
75
77
  // answer 인코딩 종류. envelope 디코더는 한 벌이고 이 값은 answer 브랜치의 타입과
@@ -212,6 +214,7 @@ function appendImages(content: QgridContent[], images?: EmulationImage[]): Qgrid
212
214
  type: "image",
213
215
  data: image.data,
214
216
  revisedPrompt: image.revisedPrompt ?? null,
217
+ ...(image.generation ? { generation: image.generation } : {}),
215
218
  }),
216
219
  ),
217
220
  ];
@@ -6,6 +6,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { resetNativeRunRegistryForTests } from "../qgrid/qgrid-run-lifecycle";
7
7
  import { resetServerRestartForTests } from "../../utils/server-restart";
8
8
  import { SettingModel } from "./setting.model";
9
+ import * as reminders from "../qgrid/expired-token-reminder";
10
+ import { SlackNotificationError } from "../../utils/slack-notify";
11
+
12
+ describe("SettingModel.triggerExpiryReminder", () => {
13
+ afterEach(() => vi.restoreAllMocks());
14
+
15
+ it("returns the sent count after successful delivery", async () => {
16
+ vi.spyOn(reminders, "sendExpiredTokenReminderNow").mockResolvedValue(3);
17
+ await expect(SettingModel.triggerExpiryReminder()).resolves.toEqual({ sent: 3 });
18
+ });
19
+
20
+ it("returns a readable API error when Slack rejects a manual send", async () => {
21
+ vi.spyOn(reminders, "sendExpiredTokenReminderNow").mockRejectedValue(
22
+ new SlackNotificationError("Slack 알림 발송 실패: not_in_channel"),
23
+ );
24
+ await expect(SettingModel.triggerExpiryReminder()).rejects.toMatchObject({
25
+ statusCode: 400, message: "Slack 알림 발송 실패: not_in_channel",
26
+ });
27
+ });
28
+ });
9
29
 
10
30
  function installContext(headers: Record<string, string>) {
11
31
  const raw = Object.assign(new EventEmitter(), {
@@ -11,9 +11,10 @@ import {
11
11
  exhaustive,
12
12
  } from "sonamu";
13
13
 
14
- import { SD } from "../../i18n/sd.generated";
14
+ import { SD, type LocalizedString } from "../../i18n/sd.generated";
15
15
  import { detectSupervisor } from "../../utils/process-supervisor";
16
16
  import { armServerRestartExit, beginServerRestart } from "../../utils/server-restart";
17
+ import { SlackNotificationError } from "../../utils/slack-notify";
17
18
  import { type SettingSubsetKey, type SettingSubsetMapping } from "../sonamu.generated";
18
19
  import { settingSubsetQueries, settingLoaderQueries } from "../sonamu.generated.sso";
19
20
  import {
@@ -315,7 +316,14 @@ class SettingModelClass extends BaseModelClass<
315
316
  // setting.model → expired-token-reminder → token.model 경로가 생겨,
316
317
  // token.model 을 mock 하는 테스트가 실제 모듈을 먼저 로드하게 된다.
317
318
  const { sendExpiredTokenReminderNow } = await import("../qgrid/expired-token-reminder");
318
- return { sent: await sendExpiredTokenReminderNow() };
319
+ try {
320
+ return { sent: await sendExpiredTokenReminderNow() };
321
+ } catch (error) {
322
+ if (error instanceof SlackNotificationError) {
323
+ throw new BadRequestException(error.message as LocalizedString);
324
+ }
325
+ throw error;
326
+ }
319
327
  }
320
328
 
321
329
  /**