@cartanova/qgrid-cli 2.10.10 → 2.11.1

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 (302) hide show
  1. package/bundle/dist/_virtual/_rolldown/runtime.js +1 -10
  2. package/bundle/dist/application/monit/log-buffer.js +12 -6
  3. package/bundle/dist/application/monit/monit.frame.js +11 -3
  4. package/bundle/dist/application/monit/monit.types.js +1 -1
  5. package/bundle/dist/application/qgrid/conv-routing.js +9 -6
  6. package/bundle/dist/application/qgrid/expired-token-reminder.js +2 -2
  7. package/bundle/dist/application/qgrid/oauth.js +31 -4
  8. package/bundle/dist/application/qgrid/qgrid-image-generation.js +1 -1
  9. package/bundle/dist/application/qgrid/qgrid-response-format.js +21 -19
  10. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +40 -33
  11. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +128 -93
  12. package/bundle/dist/application/qgrid/qgrid.frame.js +930 -49
  13. package/bundle/dist/application/qgrid/qgrid.types.js +212 -5
  14. package/bundle/dist/application/qgrid/schema-prompt.js +75 -48
  15. package/bundle/dist/application/qgrid/slack-user-map.js +1 -1
  16. package/bundle/dist/application/qgrid/token-death.js +1 -1
  17. package/bundle/dist/application/qgrid/token-subscriber.js +83 -30
  18. package/bundle/dist/application/qgrid/token-trigger-setup.js +1 -1
  19. package/bundle/dist/application/qgrid/token-window-keepalive.js +8 -6
  20. package/bundle/dist/application/qgrid/tool-emulation-schema.js +13 -13
  21. package/bundle/dist/application/qgrid/tool-emulation.js +19 -15
  22. package/bundle/dist/application/request-log/json-schema-type-text.js +29 -12
  23. package/bundle/dist/application/request-log/request-log.model.js +83 -70
  24. package/bundle/dist/application/request-log/request-log.types.js +1 -1
  25. package/bundle/dist/application/request-log/zod-code-format.js +1 -1
  26. package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
  27. package/bundle/dist/application/request-log-step/request-log-step.types.js +1 -1
  28. package/bundle/dist/application/setting/setting.constant.js +1 -1
  29. package/bundle/dist/application/setting/setting.model.js +2 -2
  30. package/bundle/dist/application/setting/setting.store.js +2 -2
  31. package/bundle/dist/application/setting/setting.types.js +1 -1
  32. package/bundle/dist/application/sonamu.generated.js +32 -16
  33. package/bundle/dist/application/sonamu.generated.sso.js +1 -1
  34. package/bundle/dist/application/token/token.model.js +26 -9
  35. package/bundle/dist/application/token/token.types.js +20 -3
  36. package/bundle/dist/i18n/en.js +15 -1
  37. package/bundle/dist/i18n/ko.js +15 -1
  38. package/bundle/dist/i18n/sd.generated.js +1 -1
  39. package/bundle/dist/migrations/20260807133553_manual__backfill_request_log_cost.js +8 -6
  40. package/bundle/dist/server-bootstrap.js +4 -2
  41. package/bundle/dist/server-error-handler.js +1 -1
  42. package/bundle/dist/sonamu.config.js +37 -8
  43. package/bundle/dist/startup-migrations.js +17 -7
  44. package/bundle/dist/testing/global.js +1 -1
  45. package/bundle/dist/testing/schema-bench.js +0 -0
  46. package/bundle/dist/testing/test-database-name.js +1 -1
  47. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
  48. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +11 -5
  49. package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +4 -4
  50. package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
  51. package/bundle/dist/utils/providers/anthropic/claude-session.js +20 -19
  52. package/bundle/dist/utils/providers/anthropic/fence-strip.js +1 -1
  53. package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +49 -23
  54. package/bundle/dist/utils/providers/common/credentials.js +5 -1
  55. package/bundle/dist/utils/providers/common/effort.js +3 -3
  56. package/bundle/dist/utils/providers/common/model-cost.js +69 -14
  57. package/bundle/dist/utils/providers/common/provider-types.js +29 -1
  58. package/bundle/dist/utils/providers/common/schema-validation.js +78 -40
  59. package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +1 -1
  60. package/bundle/dist/utils/providers/common/strictifier.js +62 -43
  61. package/bundle/dist/utils/providers/common/thinking.js +3 -2
  62. package/bundle/dist/utils/providers/common/ttft.js +1 -1
  63. package/bundle/dist/utils/providers/grok/grok-backend-protocol.js +427 -0
  64. package/bundle/dist/utils/providers/grok/grok-constants.js +55 -0
  65. package/bundle/dist/utils/providers/grok/grok-direct-client.js +196 -0
  66. package/bundle/dist/utils/providers/grok/grok-dispatcher.js +1313 -0
  67. package/bundle/dist/utils/providers/grok/grok-headers.js +118 -0
  68. package/bundle/dist/utils/providers/grok/grok-media-client.js +420 -0
  69. package/bundle/dist/utils/providers/grok/grok-models.js +114 -0
  70. package/bundle/dist/utils/providers/grok/grok-oauth.js +455 -0
  71. package/bundle/dist/utils/providers/grok/grok-quota.js +119 -0
  72. package/bundle/dist/utils/providers/grok/grok-refresh.js +192 -0
  73. package/bundle/dist/utils/providers/grok/grok-types.js +186 -0
  74. package/bundle/dist/utils/providers/grok/grok-voice-client.js +199 -0
  75. package/bundle/dist/utils/providers/grok/index.js +11 -0
  76. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +111 -83
  77. package/bundle/dist/utils/providers/openai/openai-callback-relay.js +3 -2
  78. package/bundle/dist/utils/providers/openai/openai-constants.js +1 -1
  79. package/bundle/dist/utils/providers/openai/openai-direct-client.js +23 -7
  80. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +143 -106
  81. package/bundle/dist/utils/providers/openai/openai-image-generation.js +48 -33
  82. package/bundle/dist/utils/providers/openai/openai-oauth.js +28 -13
  83. package/bundle/dist/utils/providers/openai/openai-quota.js +38 -27
  84. package/bundle/dist/utils/providers/openai/openai-refresh.js +17 -9
  85. package/bundle/dist/utils/providers/openai/openai-sse.js +1 -1
  86. package/bundle/dist/utils/providers/openai/openai-transport-config.js +1 -1
  87. package/bundle/dist/utils/quiet-hours.js +1 -1
  88. package/bundle/dist/utils/slack-notify.js +14 -8
  89. package/bundle/dist/utils/subset-loaders.js +4 -5
  90. package/bundle/src/application/monit/log-buffer.ts +23 -9
  91. package/bundle/src/application/monit/monit.frame.ts +22 -5
  92. package/bundle/src/application/monit/monit.types.ts +6 -0
  93. package/bundle/src/application/qgrid/conv-routing.test.ts +76 -4
  94. package/bundle/src/application/qgrid/conv-routing.ts +35 -9
  95. package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -1
  96. package/bundle/src/application/qgrid/oauth.ts +51 -15
  97. package/bundle/src/application/qgrid/qgrid-image-generation.ts +9 -0
  98. package/bundle/src/application/qgrid/qgrid-response-format.ts +50 -25
  99. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +19 -0
  100. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +84 -40
  101. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +97 -0
  102. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +274 -126
  103. package/bundle/src/application/qgrid/qgrid.frame.test.ts +1013 -3
  104. package/bundle/src/application/qgrid/qgrid.frame.ts +1415 -60
  105. package/bundle/src/application/qgrid/qgrid.types.ts +335 -0
  106. package/bundle/src/application/qgrid/schema-prompt.ts +105 -41
  107. package/bundle/src/application/qgrid/slack-user-map.ts +5 -0
  108. package/bundle/src/application/qgrid/token-death.ts +4 -0
  109. package/bundle/src/application/qgrid/token-subscriber.test.ts +21 -19
  110. package/bundle/src/application/qgrid/token-subscriber.ts +186 -53
  111. package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -0
  112. package/bundle/src/application/qgrid/token-window-keepalive.ts +49 -6
  113. package/bundle/src/application/qgrid/tool-emulation-schema.ts +33 -29
  114. package/bundle/src/application/qgrid/tool-emulation.ts +52 -27
  115. package/bundle/src/application/request-log/json-schema-type-text.test.ts +5 -5
  116. package/bundle/src/application/request-log/json-schema-type-text.ts +81 -26
  117. package/bundle/src/application/request-log/request-log.model.test.ts +111 -4
  118. package/bundle/src/application/request-log/request-log.model.ts +277 -146
  119. package/bundle/src/application/request-log/request-log.types.ts +6 -0
  120. package/bundle/src/application/request-log/zod-code-format.ts +11 -0
  121. package/bundle/src/application/request-log-step/request-log-step.model.ts +1 -0
  122. package/bundle/src/application/request-log-step/request-log-step.types.ts +2 -0
  123. package/bundle/src/application/setting/setting.constant.ts +9 -0
  124. package/bundle/src/application/setting/setting.model.ts +12 -2
  125. package/bundle/src/application/setting/setting.store.ts +5 -1
  126. package/bundle/src/application/setting/setting.types.ts +8 -0
  127. package/bundle/src/application/sonamu.generated.http +133 -0
  128. package/bundle/src/application/sonamu.generated.sso.ts +2 -0
  129. package/bundle/src/application/sonamu.generated.ts +15 -0
  130. package/bundle/src/application/token/token.identity.test.ts +20 -0
  131. package/bundle/src/application/token/token.model.ts +66 -7
  132. package/bundle/src/application/token/token.types.ts +24 -1
  133. package/bundle/src/i18n/en.ts +19 -0
  134. package/bundle/src/i18n/ko.ts +21 -0
  135. package/bundle/src/i18n/sd.generated.ts +23 -2
  136. package/bundle/src/migrations/20260807133553_manual__backfill_request_log_cost.ts +16 -8
  137. package/bundle/src/server-bootstrap.ts +4 -2
  138. package/bundle/src/server-error-handler.ts +1 -0
  139. package/bundle/src/sonamu.config.ts +69 -7
  140. package/bundle/src/startup-migrations.ts +27 -9
  141. package/bundle/src/testing/global.ts +20 -0
  142. package/bundle/src/testing/schema-bench.ts +105 -30
  143. package/bundle/src/testing/test-database-name.ts +2 -0
  144. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +8 -0
  145. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +41 -6
  146. package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +7 -3
  147. package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +6 -0
  148. package/bundle/src/utils/providers/anthropic/claude-session.ts +58 -24
  149. package/bundle/src/utils/providers/anthropic/fence-strip.ts +11 -0
  150. package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +110 -45
  151. package/bundle/src/utils/providers/common/credentials.ts +8 -0
  152. package/bundle/src/utils/providers/common/effort.ts +9 -2
  153. package/bundle/src/utils/providers/common/model-cost.test.ts +48 -1
  154. package/bundle/src/utils/providers/common/model-cost.ts +259 -158
  155. package/bundle/src/utils/providers/common/provider-dispatcher.ts +98 -2
  156. package/bundle/src/utils/providers/common/provider-types.ts +46 -3
  157. package/bundle/src/utils/providers/common/schema-validation.test.ts +5 -1
  158. package/bundle/src/utils/providers/common/schema-validation.ts +124 -56
  159. package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -0
  160. package/bundle/src/utils/providers/common/strictifier.ts +185 -59
  161. package/bundle/src/utils/providers/common/thinking.ts +2 -2
  162. package/bundle/src/utils/providers/common/ttft.ts +4 -2
  163. package/bundle/src/utils/providers/grok/__fixtures__/golden-responses-1.0.60.json +164 -0
  164. package/bundle/src/utils/providers/grok/grok-backend-protocol.test.ts +325 -0
  165. package/bundle/src/utils/providers/grok/grok-backend-protocol.ts +721 -0
  166. package/bundle/src/utils/providers/grok/grok-constants.ts +105 -0
  167. package/bundle/src/utils/providers/grok/grok-direct-client.test.ts +179 -0
  168. package/bundle/src/utils/providers/grok/grok-direct-client.ts +313 -0
  169. package/bundle/src/utils/providers/grok/grok-dispatcher.test.ts +1395 -0
  170. package/bundle/src/utils/providers/grok/grok-dispatcher.ts +2105 -0
  171. package/bundle/src/utils/providers/grok/grok-headers.test.ts +149 -0
  172. package/bundle/src/utils/providers/grok/grok-headers.ts +199 -0
  173. package/bundle/src/utils/providers/grok/grok-media-client.test.ts +446 -0
  174. package/bundle/src/utils/providers/grok/grok-media-client.ts +703 -0
  175. package/bundle/src/utils/providers/grok/grok-models.test.ts +96 -0
  176. package/bundle/src/utils/providers/grok/grok-models.ts +161 -0
  177. package/bundle/src/utils/providers/grok/grok-oauth.test.ts +406 -0
  178. package/bundle/src/utils/providers/grok/grok-oauth.ts +771 -0
  179. package/bundle/src/utils/providers/grok/grok-quota.test.ts +142 -0
  180. package/bundle/src/utils/providers/grok/grok-quota.ts +198 -0
  181. package/bundle/src/utils/providers/grok/grok-refresh.test.ts +239 -0
  182. package/bundle/src/utils/providers/grok/grok-refresh.ts +325 -0
  183. package/bundle/src/utils/providers/grok/grok-types.test.ts +252 -0
  184. package/bundle/src/utils/providers/grok/grok-types.ts +374 -0
  185. package/bundle/src/utils/providers/grok/grok-voice-client.test.ts +321 -0
  186. package/bundle/src/utils/providers/grok/grok-voice-client.ts +284 -0
  187. package/bundle/src/utils/providers/grok/index.ts +20 -0
  188. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +221 -137
  189. package/bundle/src/utils/providers/openai/openai-callback-relay.ts +12 -1
  190. package/bundle/src/utils/providers/openai/openai-constants.ts +2 -0
  191. package/bundle/src/utils/providers/openai/openai-direct-client.ts +111 -21
  192. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +322 -169
  193. package/bundle/src/utils/providers/openai/openai-image-generation.ts +116 -36
  194. package/bundle/src/utils/providers/openai/openai-oauth.ts +48 -20
  195. package/bundle/src/utils/providers/openai/openai-quota.ts +77 -46
  196. package/bundle/src/utils/providers/openai/openai-refresh.ts +42 -18
  197. package/bundle/src/utils/providers/openai/openai-sse.ts +27 -3
  198. package/bundle/src/utils/providers/openai/openai-transport-config.ts +2 -0
  199. package/bundle/src/utils/quiet-hours.ts +12 -1
  200. package/bundle/src/utils/slack-notify.ts +44 -13
  201. package/bundle/src/utils/subset-loaders.ts +7 -7
  202. package/bundle/web-dist/client/assets/clsx-5XM_pHvN.js +1 -0
  203. package/bundle/web-dist/client/assets/index-Be2qCZmI.css +1 -0
  204. package/bundle/web-dist/client/assets/index-C96h3feE.js +32 -0
  205. package/bundle/web-dist/client/assets/logs-DBM6U0QD.js +1 -0
  206. package/bundle/web-dist/client/assets/monit-BBjXa7vB.js +1 -0
  207. package/bundle/web-dist/client/assets/routes-Crz_PQzi.js +5 -0
  208. package/bundle/web-dist/client/assets/{services.generated-NP-ValHd.js → services.generated-B5lc3aIC.js} +5 -5
  209. package/bundle/web-dist/client/assets/settings-Cdwm3OBB.js +1 -0
  210. package/bundle/web-dist/client/assets/show-DBUhALT1.js +2 -0
  211. package/bundle/web-dist/client/assets/tokens-CVQAVJTo.js +1 -0
  212. package/bundle/web-dist/client/assets/total-cost-MnfKU8IN.js +1 -0
  213. package/bundle/web-dist/client/assets/use-oauth-login-flow-BfxYI4Oh.js +1 -0
  214. package/bundle/web-dist/client/assets/use-update-token-mutation-qKBC2sn7.js +1 -0
  215. package/bundle/web-dist/client/assets/{vendor-tanstack-DJRQAbrY.js → vendor-tanstack-DRPs09ai.js} +1 -1
  216. package/bundle/web-dist/client/index.html +5 -5
  217. package/bundle/web-dist/server/assets/clsx-D_N5PN_2.js +16 -0
  218. package/bundle/web-dist/server/assets/{lazyRouteComponent-CgBdabgd.js → lazyRouteComponent-AKiHfZWp.js} +129 -2
  219. package/bundle/web-dist/server/assets/{logs-BMyR98rU.js → logs-B7QmGJq3.js} +18 -14
  220. package/bundle/web-dist/server/assets/{monit-ZPt66W5M.js → monit-DFt7x3bh.js} +13 -12
  221. package/bundle/web-dist/server/assets/{plus-BZ9I0VgC.js → plus-DaYQRpBt.js} +1 -1
  222. package/bundle/web-dist/server/assets/{react-dom-Ds75e4Jl.js → react-dom-Cv23mm1K.js} +2 -129
  223. package/bundle/web-dist/server/assets/{refresh-cw-CztCFis-.js → refresh-cw-BpXNsAy-.js} +1 -1
  224. package/bundle/web-dist/server/assets/{request-model-DjGldb-h.js → request-model-eHE6YbaF.js} +1 -1
  225. package/bundle/web-dist/server/assets/{routes-Ct-k7nBA.js → routes-_Y-PJfgO.js} +110 -39
  226. package/bundle/web-dist/server/assets/{services.generated-BA4AcAf5.js → services.generated-CMJ8AHbO.js} +582 -172
  227. package/bundle/web-dist/server/assets/{settings-CBUZ47Xx.js → settings-JwJKxn2e.js} +14 -14
  228. package/bundle/web-dist/server/assets/{show-LSFIzvO2.js → show-BEv6z1v_.js} +90 -60
  229. package/bundle/web-dist/server/assets/{tokens-DxrBW512.js → tokens-CNT4mI-f.js} +162 -50
  230. package/bundle/web-dist/server/assets/total-cost-fTE9Xy82.js +18 -0
  231. package/bundle/web-dist/server/assets/use-oauth-login-flow-jkAkT3X6.js +2345 -0
  232. package/bundle/web-dist/server/assets/{use-update-token-mutation-pkArIzCP.js → use-update-token-mutation-Sm6kh02m.js} +1 -1
  233. package/bundle/web-dist/server/entry-server.generated.js +1644 -21655
  234. package/dist/cli.js +9 -9
  235. package/package.json +4 -11
  236. package/postinstall.mjs +12 -0
  237. package/skills/qgrid/SKILL.md +1 -0
  238. package/skills/qgrid/references/cli-env-and-server-boot.md +4 -1
  239. package/skills/qgrid/references/decision-rationale.md +23 -0
  240. package/skills/qgrid/references/grok-runtime.md +89 -0
  241. package/skills/qgrid/references/provider-runtime-differences.md +30 -29
  242. package/skills/qgrid/references/repo-map.md +10 -0
  243. package/skills/qgrid/references/token-auth-quota-lifecycle.md +56 -3
  244. package/bundle/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +0 -406
  245. package/bundle/dist/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.js +0 -1507
  246. package/bundle/dist/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.js +0 -887
  247. package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.js +0 -2756
  248. package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/index.js +0 -8
  249. package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/utils.js +0 -8
  250. package/bundle/dist/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.js +0 -922
  251. package/bundle/dist/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.js +0 -386
  252. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +0 -121
  253. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/constants.js +0 -51
  254. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.js +0 -1317
  255. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.js +0 -561
  256. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/error.js +0 -40
  257. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.js +0 -272
  258. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.js +0 -27
  259. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.js +0 -77
  260. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.js +0 -381
  261. package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/timers.js +0 -51
  262. package/bundle/dist/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.js +0 -2986
  263. package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.js +0 -10
  264. package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.js +0 -109
  265. package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.js +0 -10
  266. package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.js +0 -10
  267. package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.js +0 -10
  268. package/bundle/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +0 -1126
  269. package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/index.js +0 -21
  270. package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.js +0 -432
  271. package/bundle/dist/node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.js +0 -517
  272. package/bundle/dist/node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.js +0 -101
  273. package/bundle/dist/node_modules/.pnpm/vite@8.0.3_@emnapi_core@1.11.2_@emnapi_runtime@1.11.2_@types_node@25.0.7_esbuild@0.27.4_5b35fc1fb85d2af4d105e0d74f940f50/node_modules/vite/dist/node/module-runner.js +0 -1265
  274. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js +0 -9
  275. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/benchmark.CX_oY03V.js +0 -41
  276. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/evaluatedModules.Dg1zASAC.js +0 -19
  277. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.Chj8NDwU.js +0 -202
  278. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.DdgEv5B1.js +0 -46
  279. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js +0 -119
  280. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js +0 -3044
  281. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js +0 -59
  282. package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/index.js +0 -28
  283. package/bundle/dist/testing/setup-mocks.js +0 -16
  284. package/bundle/src/testing/setup-mocks.ts +0 -44
  285. package/bundle/web-dist/client/assets/index-Bw2s88Ot.css +0 -1
  286. package/bundle/web-dist/client/assets/index-DZwzUfI-.js +0 -106
  287. package/bundle/web-dist/client/assets/logs-Cog79DU5.js +0 -1
  288. package/bundle/web-dist/client/assets/monit-2auznBys.js +0 -1
  289. package/bundle/web-dist/client/assets/routes-B0lTeLK-.js +0 -5
  290. package/bundle/web-dist/client/assets/settings-D8ZxZGSJ.js +0 -1
  291. package/bundle/web-dist/client/assets/show-CFSERlnM.js +0 -2
  292. package/bundle/web-dist/client/assets/tokens-CZXUyI7O.js +0 -1
  293. package/bundle/web-dist/client/assets/use-oauth-login-flow-hboOs3wG.js +0 -1
  294. package/bundle/web-dist/client/assets/use-update-token-mutation-BQHS0byG.js +0 -1
  295. package/bundle/web-dist/server/assets/use-oauth-login-flow-Gn8flMK5.js +0 -178
  296. package/dist/cli.d.ts +0 -10
  297. /package/bundle/web-dist/client/assets/{cost-DH2jZbrw.js → cost-DB3HbU7T.js} +0 -0
  298. /package/bundle/web-dist/client/assets/{plus-B62CuqT8.js → plus-BsQdY7PS.js} +0 -0
  299. /package/bundle/web-dist/client/assets/{preload-helper-ca-nBW7U.js → preload-helper-DWTEM3RW.js} +0 -0
  300. /package/bundle/web-dist/client/assets/{refresh-cw-NNcU0Cmq.js → refresh-cw-D9IUmT23.js} +0 -0
  301. /package/bundle/web-dist/client/assets/{request-model-CojtinqK.js → request-model-Bpp_rkK8.js} +0 -0
  302. /package/bundle/web-dist/server/assets/{cost-eEHeTRT5.js → cost-BC9yzyA9.js} +0 -0
@@ -1,8 +1,13 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
+ import { z } from "zod";
2
3
 
3
- import { QuotaThresholdExceededError } from "../../../application/qgrid/qgrid.types";
4
+ import {
5
+ type ImageGenerationMetadata,
6
+ QuotaThresholdExceededError,
7
+ } from "../../../application/qgrid/qgrid.types";
4
8
  import { TokenModel } from "../../../application/token/token.model";
5
9
  import { type OpenAICredentials } from "../../../application/token/token.types";
10
+ import { isOpenAICredentials } from "../common/credentials";
6
11
  import { resolveOpenAIEffort } from "../common/effort";
7
12
  import {
8
13
  type GenerateRequest,
@@ -11,11 +16,13 @@ import {
11
16
  type GeneratedImage,
12
17
  type ProviderDispatcher,
13
18
  } from "../common/provider-dispatcher";
19
+ import { isJsonObject, jsonString } from "../common/provider-types";
14
20
  import { SmoothWeightedRoundRobin } from "../common/smooth-weighted-round-robin";
15
21
  import { ThinkingValidationError } from "../common/thinking";
16
22
  import {
17
23
  imageMimeType,
18
24
  type JsonValue,
25
+ type OpenAINormalizedEvent,
19
26
  type OpenAIResponseItem,
20
27
  type OpenAIResponsesOptions,
21
28
  } from "./openai-backend-protocol";
@@ -25,6 +32,7 @@ import { handleChatgptAuthTokensRefresh } from "./openai-refresh";
25
32
  import { type OpenAITransportKind, resolveOpenAITransportKind } from "./openai-transport-config";
26
33
 
27
34
  const logger = getLogger(["qgrid", "openai-dispatcher"]);
35
+
28
36
  // Verified with the subscription Responses backend; public API support is not sufficient.
29
37
  const THINKING_OFF_MODELS = new Set([
30
38
  "gpt-6-sol",
@@ -34,6 +42,7 @@ const THINKING_OFF_MODELS = new Set([
34
42
  "gpt-5.6-luna",
35
43
  "gpt-5.5",
36
44
  ]);
45
+
37
46
  const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
38
47
  const IMAGE_TOOL = "generate_image";
39
48
 
@@ -43,6 +52,27 @@ type ImageToolArguments = {
43
52
  num_last_images_to_include: number;
44
53
  };
45
54
 
55
+ // Exactly these three keys; the backend rejects strict tools, so arguments are validated here.
56
+ const ImageToolArgumentsSchema = z.strictObject({
57
+ prompt: z.string().refine((prompt) => prompt.trim().length > 0),
58
+ transparent_background: z.boolean(),
59
+ num_last_images_to_include: z.number().int().min(0).max(5),
60
+ });
61
+
62
+ function generatedImageFrom(
63
+ event: Extract<OpenAINormalizedEvent, { type: "image" }>,
64
+ ): GeneratedImage {
65
+ const image: GeneratedImage = {
66
+ data: event.base64,
67
+ mediaType: event.mimeType,
68
+ revisedPrompt: event.revisedPrompt ?? null,
69
+ };
70
+
71
+ if (event.generation) image.generation = event.generation;
72
+
73
+ return image;
74
+ }
75
+
46
76
  export type ImageFailureKind = "gate" | "not_called" | "incomplete";
47
77
 
48
78
  export class ImageGenerationError extends Error {
@@ -88,139 +118,161 @@ export interface OpenAIDispatcherDependencies {
88
118
 
89
119
  function abortError(signal: AbortSignal): Error {
90
120
  const reason = signal.reason;
121
+
91
122
  return reason instanceof Error ? reason : new DOMException("Aborted", "AbortError");
92
123
  }
93
124
 
94
125
  function activeSignal(signal?: AbortSignal, timeoutMs?: number): AbortSignal {
95
126
  const timeout = AbortSignal.timeout(timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
127
+
96
128
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
97
129
  }
98
130
 
99
131
  function quotaMessage(entries: Array<{ name: string; threshold: number }>): string {
100
132
  const detail = entries.map((e) => `${e.name} (threshold ${e.threshold}%)`).join(", ");
133
+
101
134
  return detail
102
135
  ? `All openai tokens exceeded quota threshold: ${detail}`
103
136
  : "All openai tokens exceeded quota threshold";
104
137
  }
105
138
 
106
- function inputItems(req: GenerateRequest): OpenAIResponseItem[] {
107
- const history = (req.coldHistory ?? []) as OpenAIResponseItem[];
139
+ function inputItems(req: GenerateRequest): JsonValue[] {
140
+ const history = req.coldHistory ?? [];
141
+
108
142
  const content: OpenAIResponseItem[] = req.coldInput.map((input) => {
109
143
  if (input.type === "text") return { type: "input_text", text: input.text };
144
+
110
145
  if (input.type === "image") return { type: "input_image", image_url: input.url };
146
+
111
147
  if (input.type === "localImage") return { type: "input_image", image_url: input.path };
148
+
112
149
  return { type: "input_text", text: `${input.name}: ${input.path}` };
113
150
  });
151
+
114
152
  return [...history, { type: "message", role: "user", content }];
115
153
  }
116
154
 
117
155
  function imageReferences(req: GenerateRequest): OpenAIResponseItem[] {
118
156
  return inputItems(req).flatMap((item) => {
119
- if (item.type === "image_generation_call" && typeof item.result === "string") {
157
+ if (!isJsonObject(item)) return [];
158
+ const result = jsonString(item.result);
159
+
160
+ if (item.type === "image_generation_call" && result !== undefined) {
120
161
  return [
121
162
  {
122
163
  type: "input_image",
123
- image_url: `data:${imageMimeType(item.result, item.output_format)};base64,${item.result}`,
164
+ image_url: `data:${imageMimeType(result, item.output_format)};base64,${result}`,
124
165
  },
125
166
  ];
126
167
  }
168
+
127
169
  if (!Array.isArray(item.content)) return [];
128
- return item.content.filter(
129
- (part): part is OpenAIResponseItem =>
130
- part !== null &&
131
- typeof part === "object" &&
132
- (part.type === "input_image" || part.type === "image_url"),
133
- );
170
+
171
+ return item.content
172
+ .filter(isJsonObject)
173
+ .filter((part) => part.type === "input_image" || part.type === "image_url");
134
174
  });
135
175
  }
136
176
 
137
- function asReasoning(req: GenerateRequest): OpenAIResponsesOptions["reasoning"] | undefined {
177
+ type OpenAIReasoning = NonNullable<OpenAIResponsesOptions["reasoning"]>;
178
+
179
+ function asReasoning(req: GenerateRequest): OpenAIReasoning | undefined {
138
180
  if (req.thinking === false) return { effort: "none" };
181
+
139
182
  const summary =
140
183
  req.reasoningSummary && req.reasoningSummary !== "none" ? req.reasoningSummary : undefined;
184
+
141
185
  const effort =
142
186
  resolveOpenAIEffort(req.model ?? "", req.effort) ?? (req.thinking === true ? "low" : undefined);
187
+
143
188
  if (!effort && !summary) return undefined;
144
- return {
145
- ...(effort ? { effort } : {}),
146
- ...(summary
147
- ? {
148
- summary: summary as NonNullable<OpenAIResponsesOptions["reasoning"]>["summary"],
149
- }
150
- : {}),
151
- };
189
+
190
+ const reasoning: OpenAIReasoning = {};
191
+
192
+ if (effort) reasoning.effort = effort;
193
+
194
+ if (summary) reasoning.summary = summary;
195
+
196
+ return reasoning;
152
197
  }
153
198
 
199
+ const AUTO_IMAGE_INSTRUCTIONS =
200
+ "Use generate_image when image generation or editing is needed; otherwise reply with text. Call it at most once per response. Supply a complete, self-contained prompt with all relevant conversation instructions. Set transparent_background to true only when a transparent background is requested. Set num_last_images_to_include to 0 for a new image without references. For editing or reference-based generation, choose the smallest number of recent conversation images that includes all needed references, at most 5. References retain their chronological order. Do not select images for an unrelated new request.";
201
+
202
+ const AUTO_IMAGE_TOOL: OpenAIResponseItem = {
203
+ type: "function",
204
+ name: IMAGE_TOOL,
205
+ description:
206
+ "Generate or edit an image, optionally with a transparent background or recent conversation images as references.",
207
+ // Astra's subscription backend rejects strict tools; validate arguments locally.
208
+ strict: false,
209
+ parameters: {
210
+ type: "object",
211
+ properties: {
212
+ prompt: {
213
+ type: "string",
214
+ minLength: 1,
215
+ description: "Complete, self-contained instructions for the image generation or edit.",
216
+ },
217
+ transparent_background: { type: "boolean" },
218
+ num_last_images_to_include: { type: "integer", minimum: 0, maximum: 5 },
219
+ },
220
+ required: ["prompt", "transparent_background", "num_last_images_to_include"],
221
+ additionalProperties: false,
222
+ },
223
+ };
224
+
154
225
  function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
155
226
  const { outputFormat, ...imageControls } = req.imageGenerationOptions ?? {};
156
227
  const auto = req.imageGeneration === "auto";
228
+
157
229
  if (auto && req.outputSchema) {
158
230
  throw new ImageGenerationError(
159
231
  "gate",
160
232
  "Automatic image generation does not support structured output or external tools",
161
233
  );
162
234
  }
235
+
163
236
  if (auto && outputFormat && outputFormat !== "png") {
164
237
  throw new ImageGenerationError("gate", "Automatic image generation only supports PNG output");
165
238
  }
166
- return {
167
- model: req.model ?? "",
168
- ...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
169
- ...(auto
170
- ? {
171
- instructions: [
172
- req.systemPrompt,
173
- "Use generate_image when image generation or editing is needed; otherwise reply with text. Call it at most once per response. Supply a complete, self-contained prompt with all relevant conversation instructions. Set transparent_background to true only when a transparent background is requested. Set num_last_images_to_include to 0 for a new image without references. For editing or reference-based generation, choose the smallest number of recent conversation images that includes all needed references, at most 5. References retain their chronological order. Do not select images for an unrelated new request.",
174
- ]
175
- .filter(Boolean)
176
- .join("\n\n"),
177
- parallelToolCalls: false,
178
- tools: [
179
- {
180
- type: "function",
181
- name: IMAGE_TOOL,
182
- description:
183
- "Generate or edit an image, optionally with a transparent background or recent conversation images as references.",
184
- // Astra's subscription backend rejects strict tools; validate arguments locally.
185
- strict: false,
186
- parameters: {
187
- type: "object",
188
- properties: {
189
- prompt: {
190
- type: "string",
191
- minLength: 1,
192
- description:
193
- "Complete, self-contained instructions for the image generation or edit.",
194
- },
195
- transparent_background: { type: "boolean" },
196
- num_last_images_to_include: { type: "integer", minimum: 0, maximum: 5 },
197
- },
198
- required: ["prompt", "transparent_background", "num_last_images_to_include"],
199
- additionalProperties: false,
200
- },
201
- },
202
- ],
203
- }
204
- : {}),
205
- history: inputItems(req),
206
- ...(asReasoning(req) ? { reasoning: asReasoning(req) } : {}),
207
- ...(req.verbosity ? { verbosity: req.verbosity as OpenAIResponsesOptions["verbosity"] } : {}),
208
- ...(req.serviceTier
209
- ? { serviceTier: req.serviceTier === "fast" ? "priority" : req.serviceTier }
210
- : {}),
211
- ...(req.promptCacheKey ? { promptCacheKey: req.promptCacheKey } : {}),
212
- ...(req.outputSchema ? { outputSchema: { schema: req.outputSchema as JsonValue } } : {}),
213
- ...(req.imageGeneration && !auto
214
- ? {
215
- imageGeneration: req.imageGenerationOptions
216
- ? {
217
- ...imageControls,
218
- ...(outputFormat ? { output_format: outputFormat } : {}),
219
- }
220
- : true,
221
- }
222
- : {}),
223
- };
239
+
240
+ const options: OpenAIResponsesOptions = { model: req.model ?? "", history: inputItems(req) };
241
+
242
+ if (req.systemPrompt) options.instructions = req.systemPrompt;
243
+
244
+ if (auto) {
245
+ options.instructions = [req.systemPrompt, AUTO_IMAGE_INSTRUCTIONS].filter(Boolean).join("\n\n");
246
+ options.parallelToolCalls = false;
247
+ options.tools = [AUTO_IMAGE_TOOL];
248
+ }
249
+
250
+ const reasoning = asReasoning(req);
251
+
252
+ if (reasoning) options.reasoning = reasoning;
253
+
254
+ if (req.verbosity) options.verbosity = req.verbosity;
255
+
256
+ if (req.serviceTier) {
257
+ options.serviceTier = req.serviceTier === "fast" ? "priority" : req.serviceTier;
258
+ }
259
+
260
+ if (req.promptCacheKey) options.promptCacheKey = req.promptCacheKey;
261
+
262
+ if (req.outputSchema) options.outputSchema = { schema: req.outputSchema };
263
+
264
+ if (req.imageGeneration && !auto) {
265
+ if (req.imageGenerationOptions) {
266
+ const controls: OpenAIResponseItem = { ...imageControls };
267
+
268
+ if (outputFormat) controls.output_format = outputFormat;
269
+ options.imageGeneration = controls;
270
+ } else {
271
+ options.imageGeneration = true;
272
+ }
273
+ }
274
+
275
+ return options;
224
276
  }
225
277
 
226
278
  // quota와 cache affinity로 토큰을 선택해 HTTPS/WS로 전송한다. 로컬 동시성 제한은 없다.
@@ -254,18 +306,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
254
306
 
255
307
  async start(): Promise<void> {
256
308
  const { rows } = await TokenModel.findMany("A");
257
- rows
258
- .filter((row) => row.provider === "openai")
259
- .forEach((row) => {
260
- this.setToken(
261
- row.id,
262
- row.name,
263
- row.credentials as OpenAICredentials,
264
- row.quota_threshold,
265
- row.weight,
266
- row.active,
267
- );
268
- });
309
+
310
+ for (const row of rows) {
311
+ if (row.provider !== "openai") continue;
312
+
313
+ if (!isOpenAICredentials(row.credentials)) {
314
+ logger.warn(`skipping token ${row.name}: credentials are not OpenAI OAuth credentials`);
315
+ continue;
316
+ }
317
+
318
+ this.setToken(row.id, row.name, row.credentials, row.quota_threshold, row.weight, row.active);
319
+ }
320
+
269
321
  logger.info(`started direct OpenAI runtime with ${this.tokenMetadata.size} tokens`);
270
322
  }
271
323
 
@@ -311,12 +363,14 @@ export class OpenAIDispatcher implements ProviderDispatcher {
311
363
 
312
364
  onTokenDeactivated(id: number): void {
313
365
  const token = this.tokenMetadata.get(id);
366
+
314
367
  if (token) token.active = false;
315
368
  this.selector.resetScores();
316
369
  }
317
370
 
318
371
  onTokenActivated(id: number): void {
319
372
  const token = this.tokenMetadata.get(id);
373
+
320
374
  if (token) token.active = true;
321
375
  this.selector.resetScores();
322
376
  }
@@ -331,7 +385,9 @@ export class OpenAIDispatcher implements ProviderDispatcher {
331
385
  }>,
332
386
  ): Promise<void> {
333
387
  const ids = new Set(rows.map((r) => r.id));
388
+
334
389
  for (const id of this.tokenMetadata.keys()) if (!ids.has(id)) await this.onTokenRemoved(id);
390
+
335
391
  for (const row of rows)
336
392
  await this.onTokenUpdated(row.id, row.name, row.credentials, row.quotaThreshold, row.weight);
337
393
  }
@@ -346,10 +402,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
346
402
  "gate",
347
403
  "image generation is not supported on the streaming path",
348
404
  );
405
+
349
406
  try {
350
407
  cb.onComplete(await this.run(req, cb.onDelta));
351
408
  } catch (error) {
352
- cb.onError(error as Error);
409
+ cb.onError(error instanceof Error ? error : new Error(String(error)));
353
410
  throw error;
354
411
  }
355
412
  }
@@ -357,26 +414,34 @@ export class OpenAIDispatcher implements ProviderDispatcher {
357
414
  async run(req: GenerateRequest, onDelta?: (text: string) => void): Promise<GenerateResult> {
358
415
  if (req.thinking === false) {
359
416
  const model = req.model ?? "";
417
+
360
418
  if (!THINKING_OFF_MODELS.has(model)) {
361
419
  const reason =
362
420
  model === "gpt-6-astra" || model === "gpt-6.1-sol"
363
421
  ? "the subscription backend requires thinking"
364
422
  : "thinking-off support has not been verified on the subscription backend";
423
+
365
424
  throw new ThinkingValidationError(
366
425
  `thinking:false is unavailable for OpenAI model ${model || "<default>"}: ${reason}`,
367
426
  );
368
427
  }
369
428
  }
429
+
370
430
  const signal = activeSignal(req.abortSignal, req.timeoutMs);
431
+
371
432
  if (signal.aborted) throw abortError(signal);
433
+
372
434
  const selection = await this.selectToken(
373
435
  req.preferredTokenId,
374
436
  req.requirePreferredToken ?? false,
375
437
  signal,
376
438
  );
439
+
377
440
  this.inFlight++;
441
+
378
442
  try {
379
443
  if (signal.aborted) throw abortError(signal);
444
+
380
445
  return await this.runDirect(selection, { ...req, abortSignal: signal }, onDelta);
381
446
  } finally {
382
447
  this.inFlight--;
@@ -394,6 +459,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
394
459
  let outputMessageAccepted = false;
395
460
  const messagePhases = new Map<string, string>();
396
461
  const streamedMessageIds = new Set<string>();
462
+
397
463
  let usage = {
398
464
  totalTokens: 0,
399
465
  inputTokens: 0,
@@ -401,48 +467,67 @@ export class OpenAIDispatcher implements ProviderDispatcher {
401
467
  cachedInputTokens: 0,
402
468
  reasoningOutputTokens: 0,
403
469
  };
470
+
404
471
  const images: GeneratedImage[] = [];
405
472
  let imageAttempted = false;
406
473
  const auto = req.imageGeneration === "auto";
407
474
  const imageCalls = new Map<string, ImageToolArguments>();
408
- const imageCallIds = new Set<unknown>();
475
+ const imageCallIds = new Set<JsonValue | undefined>();
409
476
  let imageToolAttempted = false;
410
477
  let driverCompleted = false;
411
478
  let servingModel = req.model ?? "";
412
479
  const metadata = selection.metadata;
413
- const driverResult = (): GenerateResult => ({
414
- text,
415
- tokenName: metadata.name,
416
- usage,
417
- durationMs: Date.now() - startedAt,
418
- ttftMs: firstDeltaAt === undefined ? null : firstDeltaAt - startedAt,
419
- model: servingModel,
420
- ...(servingModel !== req.model ? { requestedModel: req.model } : {}),
421
- threadCoord: { workerId: selection.tokenId, threadId: req.promptCacheKey ?? "", epoch: -1 },
422
- });
480
+
481
+ const driverResult = (): GenerateResult => {
482
+ const result: GenerateResult = {
483
+ text,
484
+ tokenName: metadata.name,
485
+ usage,
486
+ durationMs: Date.now() - startedAt,
487
+ ttftMs: firstDeltaAt === undefined ? null : firstDeltaAt - startedAt,
488
+ model: servingModel,
489
+ threadCoord: {
490
+ workerId: selection.tokenId,
491
+ threadId: req.promptCacheKey ?? "",
492
+ epoch: -1,
493
+ },
494
+ };
495
+
496
+ if (servingModel !== req.model) result.requestedModel = req.model;
497
+
498
+ return result;
499
+ };
500
+
423
501
  const clientEntry = this.acquireClient(selection);
502
+
424
503
  try {
425
504
  for await (const event of clientEntry.client.responses(
426
505
  requestOptions(req),
427
506
  req.abortSignal,
428
507
  )) {
508
+ const itemId = event.type === "output-item" ? jsonString(event.item.id) : undefined;
509
+ const itemPhase = event.type === "output-item" ? jsonString(event.item.phase) : undefined;
510
+
429
511
  if (
430
512
  req.stopAfterOutputMessage &&
431
513
  event.type === "output-item" &&
432
514
  event.item.type === "message" &&
433
- typeof event.item.id === "string" &&
434
- typeof event.item.phase === "string"
515
+ itemId !== undefined &&
516
+ itemPhase !== undefined
435
517
  ) {
436
- messagePhases.set(event.item.id, event.item.phase);
518
+ messagePhases.set(itemId, itemPhase);
437
519
  }
520
+
438
521
  if (event.type === "text-delta") {
439
522
  if (outputMessageAccepted) continue;
523
+
440
524
  if (req.stopAfterOutputMessage && event.itemId) {
441
525
  // Buffer commentary/unclassified messages until their completed item.
442
526
  // Known final answers retain incremental streaming.
443
527
  if (messagePhases.get(event.itemId) !== "final_answer") continue;
444
528
  streamedMessageIds.add(event.itemId);
445
529
  }
530
+
446
531
  firstDeltaAt ??= Date.now();
447
532
  text += event.text;
448
533
  onDelta?.(event.text);
@@ -455,34 +540,38 @@ export class OpenAIDispatcher implements ProviderDispatcher {
455
540
  Array.isArray(event.item.content)
456
541
  ) {
457
542
  const messageText = event.item.content
458
- .filter((part) => part?.type === "output_text" && typeof part.text === "string")
459
- .map((part) => part.text)
543
+ .flatMap((part) => {
544
+ const partText =
545
+ isJsonObject(part) && part.type === "output_text"
546
+ ? jsonString(part.text)
547
+ : undefined;
548
+
549
+ return partText === undefined ? [] : [partText];
550
+ })
460
551
  .join("");
552
+
461
553
  // A valid client tool request ends this application turn. Later answers cannot
462
554
  // know its result yet. Never split/repair malformed JSON within a message.
463
- const phase = typeof event.item.phase === "string" ? event.item.phase : undefined;
555
+ const phase = itemPhase;
556
+
464
557
  if (messageText) outputMessageAccepted = req.stopAfterOutputMessage(messageText, phase);
558
+
465
559
  if (
466
- typeof event.item.id === "string" &&
467
- !streamedMessageIds.has(event.item.id) &&
560
+ itemId !== undefined &&
561
+ !streamedMessageIds.has(itemId) &&
468
562
  (phase !== "commentary" || outputMessageAccepted) &&
469
563
  messageText
470
564
  ) {
471
565
  firstDeltaAt ??= Date.now();
472
566
  text += messageText;
473
567
  onDelta?.(messageText);
474
- streamedMessageIds.add(event.item.id);
568
+ streamedMessageIds.add(itemId);
475
569
  }
476
570
  } else if (event.type === "image") {
477
571
  if (auto)
478
572
  throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
479
573
  imageAttempted = true;
480
- images.push({
481
- data: event.base64,
482
- mediaType: event.mimeType,
483
- revisedPrompt: event.revisedPrompt ?? null,
484
- ...(event.generation ? { generation: event.generation } : {}),
485
- });
574
+ images.push(generatedImageFrom(event));
486
575
  } else if (event.type === "output-item" && event.item.type === "image_generation_call") {
487
576
  if (auto)
488
577
  throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
@@ -491,37 +580,33 @@ export class OpenAIDispatcher implements ProviderDispatcher {
491
580
  if (event.item.name !== IMAGE_TOOL) {
492
581
  throw new ImageGenerationError("incomplete", "Unexpected automatic image tool call");
493
582
  }
583
+
494
584
  imageToolAttempted = true;
495
585
  imageCallIds.add(event.item.call_id);
586
+
496
587
  if (!event.completed) continue;
497
- const id = event.item.call_id;
498
- let args: unknown;
588
+ const id = jsonString(event.item.call_id);
589
+ const rawArguments = jsonString(event.item.arguments);
590
+ let decoded: JsonValue;
591
+
499
592
  try {
500
- args = JSON.parse(String(event.item.arguments));
593
+ if (rawArguments === undefined) throw new Error("missing arguments");
594
+ // JSON.parse without a reviver yields only JSON values.
595
+ decoded = JSON.parse(rawArguments);
501
596
  } catch {
502
597
  throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
503
598
  }
504
- if (
505
- typeof id !== "string" ||
506
- !id ||
507
- !args ||
508
- typeof args !== "object" ||
509
- Array.isArray(args) ||
510
- Object.keys(args).length !== 3 ||
511
- !("prompt" in args) ||
512
- typeof args.prompt !== "string" ||
513
- !args.prompt.trim() ||
514
- !("transparent_background" in args) ||
515
- typeof args.transparent_background !== "boolean" ||
516
- !("num_last_images_to_include" in args) ||
517
- typeof args.num_last_images_to_include !== "number" ||
518
- !Number.isInteger(args.num_last_images_to_include) ||
519
- args.num_last_images_to_include < 0 ||
520
- args.num_last_images_to_include > 5
521
- ) {
599
+
600
+ const parsedArgs = ImageToolArgumentsSchema.safeParse(decoded);
601
+
602
+ if (!id || !parsedArgs.success) {
522
603
  throw new ImageGenerationError("incomplete", "Invalid image tool arguments");
523
604
  }
605
+
606
+ const args = parsedArgs.data;
607
+
524
608
  const previous = imageCalls.get(id);
609
+
525
610
  if (
526
611
  previous &&
527
612
  (previous.prompt !== args.prompt ||
@@ -530,6 +615,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
530
615
  ) {
531
616
  throw new ImageGenerationError("incomplete", "Conflicting image tool calls");
532
617
  }
618
+
533
619
  imageCalls.set(id, {
534
620
  prompt: args.prompt,
535
621
  transparent_background: args.transparent_background,
@@ -537,6 +623,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
537
623
  });
538
624
  } else if (event.type === "completed") {
539
625
  const imageUsage = event.imageGeneration?.usage;
626
+
540
627
  // The backend also reports all-zero image usage for text-only responses.
541
628
  if (
542
629
  auto &&
@@ -548,6 +635,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
548
635
  throw new ImageGenerationError("incomplete", "Unexpected hosted image generation");
549
636
  driverCompleted = true;
550
637
  servingModel = event.model ?? servingModel;
638
+
551
639
  if (event.usage) {
552
640
  usage = {
553
641
  totalTokens: event.usage.totalTokens,
@@ -557,20 +645,29 @@ export class OpenAIDispatcher implements ProviderDispatcher {
557
645
  reasoningOutputTokens: event.usage.reasoningTokens,
558
646
  };
559
647
  }
648
+
560
649
  if (event.imageGeneration) {
561
650
  const hostedImages = images.filter(
562
651
  (image) => image.generation?.route === "codex-responses",
563
652
  );
653
+
564
654
  for (const [index, image] of hostedImages.entries()) {
565
- image.generation = {
655
+ const generation: ImageGenerationMetadata = {
566
656
  route: "codex-responses",
567
- ...(event.imageGeneration.model ? { model: event.imageGeneration.model } : {}),
568
657
  ...image.generation,
569
- // Tool usage covers the whole response, not each returned image.
570
- ...(index === 0 && event.imageGeneration.usage
571
- ? { usage: event.imageGeneration.usage }
572
- : {}),
573
658
  };
659
+
660
+ // The image's own model wins; the response-level model only fills a gap.
661
+ if (generation.model === undefined && event.imageGeneration.model) {
662
+ generation.model = event.imageGeneration.model;
663
+ }
664
+
665
+ // Tool usage covers the whole response, not each returned image.
666
+ if (index === 0 && event.imageGeneration.usage) {
667
+ generation.usage = event.imageGeneration.usage;
668
+ }
669
+
670
+ image.generation = generation;
574
671
  }
575
672
  }
576
673
  } else if (event.type === "error") {
@@ -579,21 +676,26 @@ export class OpenAIDispatcher implements ProviderDispatcher {
579
676
  throw event.error;
580
677
  }
581
678
  }
679
+
582
680
  if (auto && imageToolAttempted) {
583
681
  if (!driverCompleted || imageCalls.size !== 1 || imageCallIds.size !== 1) {
584
682
  throw new ImageGenerationError("incomplete", "Expected one completed image tool call");
585
683
  }
684
+
586
685
  if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
587
686
  const args = [...imageCalls.values()][0]!;
588
687
  const referenceCount = args.num_last_images_to_include;
589
688
  const references = referenceCount ? imageReferences(req) : [];
689
+
590
690
  if (referenceCount > references.length) {
591
691
  throw new ImageGenerationError(
592
692
  "incomplete",
593
693
  "Requested image references are unavailable",
594
694
  );
595
695
  }
696
+
596
697
  let imageCompleted = false;
698
+
597
699
  for await (const event of clientEntry.client.responses(
598
700
  {
599
701
  model: "gpt-image-2",
@@ -616,26 +718,25 @@ export class OpenAIDispatcher implements ProviderDispatcher {
616
718
  },
617
719
  req.abortSignal,
618
720
  )) {
619
- if (event.type === "image")
620
- images.push({
621
- data: event.base64,
622
- mediaType: event.mimeType,
623
- revisedPrompt: event.revisedPrompt ?? null,
624
- ...(event.generation ? { generation: event.generation } : {}),
625
- });
721
+ if (event.type === "image") images.push(generatedImageFrom(event));
626
722
  else if (event.type === "completed") imageCompleted = true;
627
723
  else if (event.type === "error")
628
724
  throw new ImageGenerationError("incomplete", event.error.message, {
629
725
  cause: event.error,
630
726
  });
631
727
  }
728
+
632
729
  imageAttempted = true;
730
+
633
731
  if (!imageCompleted)
634
732
  throw new ImageGenerationError("incomplete", "Image generation did not complete");
635
733
  }
734
+
636
735
  if (auto && imageAttempted && !driverCompleted)
637
736
  throw new ImageGenerationError("incomplete", "Image generation response did not complete");
737
+
638
738
  if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
739
+
639
740
  if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
640
741
  throw new ImageGenerationError(
641
742
  imageAttempted ? "incomplete" : "not_called",
@@ -654,29 +755,35 @@ export class OpenAIDispatcher implements ProviderDispatcher {
654
755
  error instanceof Error ? error.message : String(error),
655
756
  { cause: error },
656
757
  );
758
+
657
759
  if (error instanceof Error) failure.name = error.name;
658
760
  // Only the completed chat stage is chargeable here; discard partial image output.
659
761
  failure.completedDriver = driverResult();
660
762
  throw failure;
661
763
  }
764
+
662
765
  throw error;
663
766
  } finally {
664
767
  this.releaseClient(clientEntry);
665
768
  }
666
- return {
667
- ...driverResult(),
668
- ...(images.length ? { images } : {}),
669
- };
769
+
770
+ const result = driverResult();
771
+
772
+ if (images.length) result.images = images;
773
+
774
+ return result;
670
775
  }
671
776
 
672
777
  /** 세대가 바뀐 client 는 사용 중인 요청이 끝난 뒤에 닫는다. */
673
778
  acquireClient(selection: TokenSelection): ClientEntry {
674
779
  const metadata = selection.metadata;
675
780
  let entry = this.clients.get(selection.tokenId);
781
+
676
782
  if (entry && entry.generation !== metadata.generation) {
677
783
  this.retireClient(selection.tokenId);
678
784
  entry = undefined;
679
785
  }
786
+
680
787
  if (!entry) {
681
788
  const client = this.clientFactory(
682
789
  {
@@ -690,15 +797,19 @@ export class OpenAIDispatcher implements ProviderDispatcher {
690
797
  },
691
798
  selection.tokenId,
692
799
  );
800
+
693
801
  entry = { generation: metadata.generation, client, inFlight: 0, retired: false };
694
802
  this.clients.set(selection.tokenId, entry);
695
803
  }
804
+
696
805
  entry.inFlight++;
806
+
697
807
  return entry;
698
808
  }
699
809
 
700
810
  releaseClient(entry: ClientEntry): void {
701
811
  entry.inFlight = Math.max(0, entry.inFlight - 1);
812
+
702
813
  if (entry.retired && entry.inFlight === 0) {
703
814
  this.retiredClients.delete(entry);
704
815
  entry.client.close?.();
@@ -707,9 +818,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
707
818
 
708
819
  retireClient(tokenId: number): void {
709
820
  const entry = this.clients.get(tokenId);
821
+
710
822
  if (!entry) return;
711
823
  this.clients.delete(tokenId);
712
824
  entry.retired = true;
825
+
713
826
  if (entry.inFlight === 0) entry.client.close?.();
714
827
  else this.retiredClients.add(entry);
715
828
  }
@@ -722,14 +835,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
722
835
  ): Promise<TokenSelection> {
723
836
  if (preferredTokenId !== undefined) {
724
837
  const preferred = this.tokenMetadata.get(preferredTokenId);
838
+
725
839
  if (preferred?.active && (await this.isQuotaEligible(preferredTokenId, preferred, signal))) {
726
840
  return { tokenId: preferredTokenId, metadata: preferred };
727
841
  }
842
+
728
843
  if (requirePreferredToken) {
729
844
  if (!preferred?.active) {
730
845
  throw new Error(`Preferred openai token ${preferredTokenId} is not available`);
731
846
  }
847
+
732
848
  const threshold = preferred.quotaThreshold;
849
+
733
850
  if (threshold !== null && threshold !== undefined) {
734
851
  throw new QuotaThresholdExceededError(
735
852
  quotaMessage([{ name: preferred.name, threshold }]),
@@ -739,23 +856,27 @@ export class OpenAIDispatcher implements ProviderDispatcher {
739
856
  }
740
857
 
741
858
  // 콜드 캐시에서 토큰 수만큼 조회 지연이 쌓이지 않도록 병렬 판정한다.
859
+ const activeTokens = [...this.tokenMetadata.entries()].filter(([, token]) => token.active);
860
+
742
861
  const checks = await Promise.all(
743
- [...this.tokenMetadata.entries()]
744
- .filter(([, token]) => token.active)
745
- .map(async ([id, token]) => ({
746
- id,
747
- token,
748
- eligible: await this.isQuotaEligible(id, token, signal),
749
- })),
862
+ activeTokens.map(async ([id, token]) => ({
863
+ id,
864
+ token,
865
+ eligible: await this.isQuotaEligible(id, token, signal),
866
+ })),
750
867
  );
868
+
751
869
  const eligible = new Set(checks.filter((c) => c.eligible).map((c) => c.id));
870
+
752
871
  if (eligible.size === 0) {
753
- const over = checks
754
- .filter(
755
- (c) =>
756
- !c.eligible && c.token.quotaThreshold !== null && c.token.quotaThreshold !== undefined,
757
- )
758
- .map((c) => ({ name: c.token.name, threshold: c.token.quotaThreshold as number }));
872
+ const over = checks.flatMap((c) => {
873
+ const threshold = c.token.quotaThreshold;
874
+
875
+ return c.eligible || threshold === null || threshold === undefined
876
+ ? []
877
+ : [{ name: c.token.name, threshold }];
878
+ });
879
+
759
880
  if (over.length) {
760
881
  logger.warn("quota_threshold gate: all_exceeded", {
761
882
  provider: "openai",
@@ -763,11 +884,15 @@ export class OpenAIDispatcher implements ProviderDispatcher {
763
884
  });
764
885
  throw new QuotaThresholdExceededError(quotaMessage(over));
765
886
  }
887
+
766
888
  throw new Error("NO_OPENAI_WORKERS");
767
889
  }
890
+
768
891
  const selected = this.selector.select(eligible);
769
892
  const metadata = selected !== null ? this.tokenMetadata.get(selected) : undefined;
893
+
770
894
  if (selected === null || !metadata) throw new Error("NO_OPENAI_WORKERS");
895
+
771
896
  return { tokenId: selected, metadata };
772
897
  }
773
898
 
@@ -780,6 +905,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
780
905
  active: boolean,
781
906
  ): void {
782
907
  const old = this.tokenMetadata.get(id);
908
+
783
909
  if (old) {
784
910
  Object.assign(old, {
785
911
  name,
@@ -799,6 +925,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
799
925
  generation: 1,
800
926
  });
801
927
  }
928
+
802
929
  this.selector.setToken(id, weight);
803
930
  this.invalidateRateLimitsCache(id);
804
931
  this.quotaBlocked.delete(id);
@@ -809,9 +936,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
809
936
  signal?: AbortSignal,
810
937
  ): Promise<OpenAIRateLimitsWithMeta> {
811
938
  const token = this.tokenMetadata.get(tokenId);
939
+
812
940
  if (!token) throw new Error(`openai token ${tokenId} not found`);
813
941
  const generation = token.generation;
814
942
  const cached = this.rateLimitsCache.get(tokenId);
943
+
815
944
  if (
816
945
  cached &&
817
946
  cached.generation === token.generation &&
@@ -821,27 +950,35 @@ export class OpenAIDispatcher implements ProviderDispatcher {
821
950
 
822
951
  // TTL 만료 시 조회를 공유한다. 최초 호출자의 abort로 실패해도 isQuotaEligible은 fail-open한다.
823
952
  const pending = this.pendingRateLimits.get(tokenId);
953
+
824
954
  if (pending?.token === token && pending.generation === generation) return pending.promise;
955
+
825
956
  const fetchPromise = (async () => {
826
957
  const result = await readOpenAIQuotaUsage({
827
958
  credentials: token.credentials,
828
959
  refreshCredentials: () => this.refreshCredentials(tokenId),
829
960
  fetch: this.fetchImpl,
830
- ...(signal ? { signal } : {}),
961
+ signal,
831
962
  });
963
+
832
964
  if (result.kind === "lookup_failed") throw new Error(result.reason);
965
+
833
966
  if (!result.raw) throw new Error("OpenAI quota response missing raw rate limits");
834
967
  const entry = { data: result.raw, cachedAt: Date.now(), generation };
968
+
835
969
  if (this.tokenMetadata.get(tokenId) === token && token.generation === generation) {
836
970
  this.rateLimitsCache.set(tokenId, entry);
837
971
  }
972
+
838
973
  return entry;
839
974
  })().finally(() => {
840
975
  if (this.pendingRateLimits.get(tokenId)?.promise === fetchPromise) {
841
976
  this.pendingRateLimits.delete(tokenId);
842
977
  }
843
978
  });
979
+
844
980
  this.pendingRateLimits.set(tokenId, { token, generation, promise: fetchPromise });
981
+
845
982
  return fetchPromise;
846
983
  }
847
984
 
@@ -849,12 +986,16 @@ export class OpenAIDispatcher implements ProviderDispatcher {
849
986
  const previous = this.tokenMetadata.get(tokenId);
850
987
  const generation = previous?.generation;
851
988
  const refreshed = await handleChatgptAuthTokensRefresh(tokenId);
989
+
852
990
  const credentials = {
853
991
  accessToken: refreshed.accessToken,
854
992
  accountId: refreshed.chatgptAccountId,
855
993
  };
994
+
856
995
  const current = this.tokenMetadata.get(tokenId);
996
+
857
997
  if (!current?.active) throw new Error(`openai token ${tokenId} is no longer active`);
998
+
858
999
  // A subscriber update may already contain newer credentials. Never overwrite it
859
1000
  // with the result of a refresh that started against an older generation.
860
1001
  if (current === previous && current.generation === generation) {
@@ -862,6 +1003,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
862
1003
  this.retireClient(tokenId);
863
1004
  this.invalidateRateLimitsCache(tokenId);
864
1005
  }
1006
+
865
1007
  return {
866
1008
  accessToken: current.credentials.accessToken,
867
1009
  accountId: current.credentials.accountId,
@@ -872,7 +1014,9 @@ export class OpenAIDispatcher implements ProviderDispatcher {
872
1014
  if (token.quotaThreshold === null || token.quotaThreshold === undefined) return true;
873
1015
  const generation = token.generation;
874
1016
  const result = await readOpenAIQuotaUsage(async () => this.getRateLimitsByTokenId(id, signal));
1017
+
875
1018
  if (this.tokenMetadata.get(id) !== token || token.generation !== generation) return false;
1019
+
876
1020
  if (result.kind === "lookup_failed") {
877
1021
  this.quotaBlocked.delete(id);
878
1022
  logger.warn("quota_threshold gate: lookup_fail_open", {
@@ -880,20 +1024,25 @@ export class OpenAIDispatcher implements ProviderDispatcher {
880
1024
  tokenName: token.name,
881
1025
  lookupReason: result.reason,
882
1026
  });
1027
+
883
1028
  return true;
884
1029
  }
1030
+
885
1031
  if (result.utilizationPct >= token.quotaThreshold) {
886
1032
  if (!this.quotaBlocked.has(id))
887
1033
  logger.info(
888
1034
  `quota_threshold gate: over_threshold ${token.name}[${id}] (${result.utilizationPct}% >= ${token.quotaThreshold}%)`,
889
1035
  );
890
1036
  this.quotaBlocked.add(id);
1037
+
891
1038
  return false;
892
1039
  }
1040
+
893
1041
  if (this.quotaBlocked.delete(id))
894
1042
  logger.info(
895
1043
  `quota_threshold gate: recovered ${token.name}[${id}] (${result.utilizationPct}% < ${token.quotaThreshold}%)`,
896
1044
  );
1045
+
897
1046
  return true;
898
1047
  }
899
1048
 
@@ -914,14 +1063,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
914
1063
  resetsAt: number | null;
915
1064
  }> {
916
1065
  const now = Date.now();
1066
+
917
1067
  return [...this.tokenMetadata.entries()]
918
1068
  .map(([id, t]) => {
919
1069
  const cached = this.rateLimitsCache.get(id);
1070
+
920
1071
  const fresh =
921
1072
  cached !== undefined &&
922
1073
  cached.generation === t.generation &&
923
1074
  now - cached.cachedAt < OpenAIDispatcher.RATE_LIMITS_CACHE_TTL;
1075
+
924
1076
  const primary = fresh ? cached.data.rateLimits?.primary : undefined;
1077
+
925
1078
  return {
926
1079
  name: t.name,
927
1080
  usedPercent: primary?.usedPercent ?? null,