@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.
- package/bundle/dist/application/qgrid/expired-token-reminder.js +6 -5
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +12 -3
- package/bundle/dist/application/qgrid/qgrid-response-format.js +14 -12
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
- package/bundle/dist/application/qgrid/qgrid.types.js +44 -4
- package/bundle/dist/application/qgrid/tool-emulation.js +4 -2
- package/bundle/dist/application/setting/setting.model.js +8 -2
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +22 -3
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +39 -1
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +47 -21
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +146 -0
- package/bundle/dist/utils/providers/openai/openai-quota.js +11 -3
- package/bundle/dist/utils/providers/openai/openai-refresh.js +7 -5
- package/bundle/dist/utils/slack-notify.js +8 -4
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +6 -1
- package/bundle/src/application/qgrid/expired-token-reminder.ts +10 -4
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +53 -0
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +32 -2
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +32 -1
- package/bundle/src/application/qgrid/qgrid-response-format.ts +28 -18
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +32 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +1 -1
- package/bundle/src/application/qgrid/qgrid.types.ts +47 -5
- package/bundle/src/application/qgrid/tool-emulation.test.ts +25 -0
- package/bundle/src/application/qgrid/tool-emulation.ts +6 -1
- package/bundle/src/application/setting/setting.model.test.ts +20 -0
- package/bundle/src/application/setting/setting.model.ts +10 -2
- package/bundle/src/application/sonamu.generated.http +4 -2
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +8 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +27 -2
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +48 -1
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +155 -2
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +54 -19
- package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +152 -0
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +183 -0
- package/bundle/src/utils/providers/openai/openai-quota.test.ts +32 -0
- package/bundle/src/utils/providers/openai/openai-quota.ts +15 -8
- package/bundle/src/utils/providers/openai/openai-refresh.test.ts +18 -0
- package/bundle/src/utils/providers/openai/openai-refresh.ts +11 -4
- package/bundle/src/utils/slack-notify.test.ts +14 -0
- package/bundle/src/utils/slack-notify.ts +19 -3
- package/bundle/web-dist/client/assets/{index-fogJvajr.js → index-BtVmANcA.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-CsY2cqvt.js → logs-BCuaLfPL.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-ChgaLEcD.js → monit-BkGkzl6m.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-BvyiTnOZ.js → routes-DqY8SXcB.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-ZR2J9rWb.js → settings-DbkDAJfh.js} +1 -1
- package/bundle/web-dist/client/assets/{show-CjwO4-So.js → show-Dx5JFzwy.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-Bx-hG1gm.js → tokens-DHahzDQJ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Bqax_icS.js → use-oauth-login-flow-Cjdnj_8D.js} +1 -1
- package/bundle/web-dist/client/index.html +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +1 -1
- package/package.json +2 -1
- package/skills/qgrid/SKILL.md +8 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +4 -2
- package/skills/qgrid/references/decision-rationale.md +2 -2
- package/skills/qgrid/references/openai-codex-runtime.md +9 -1
- package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
- 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
|
|
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
|
-
|
|
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 ?? "
|
|
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} ${
|
|
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)
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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) =>
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
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:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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", () => {
|