@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,11 +1,39 @@
1
1
  import { EventEmitter } from "node:events";
2
2
 
3
- import { Sonamu } from "sonamu";
3
+ import Fastify from "fastify";
4
+ import { BadRequestException, ServiceUnavailableException, Sonamu } from "sonamu";
4
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
6
 
6
- import { QgridFrame } from "./qgrid.frame";
7
- import { QueryInput } from "./qgrid.types";
7
+ import { SD } from "../../i18n/sd.generated";
8
+ import { handleServerError } from "../../server-error-handler";
9
+ import { mapRequestBodyTooLarge, QgridFrame } from "./qgrid.frame";
10
+ import { QgridDispatcher, QgridDispatcherClass } from "./qgrid.dispatcher";
11
+ import {
12
+ GenerateImagesInput,
13
+ MEDIA_MAX_INLINE_BYTES,
14
+ QueryInput,
15
+ REQUEST_BODY_LIMIT_BYTES,
16
+ TranscribeInput,
17
+ VideoStartInput,
18
+ } from "./qgrid.types";
19
+ import {
20
+ GROK_OAUTH_CLIENT_ID,
21
+ GROK_OAUTH_SCOPES,
22
+ type GrokCredentials,
23
+ GrokCapacityError,
24
+ GrokInvalidRequestError,
25
+ GrokNoWorkersError,
26
+ GrokOAuthError,
27
+ GrokOnDemandExcludedError,
28
+ GrokRateLimitedError,
29
+ GrokZdrVideoUnavailableError,
30
+ } from "../../utils/providers/grok";
8
31
  import { ImageGenerationError } from "../../utils/providers/openai/openai-dispatcher";
32
+ import type {
33
+ GenerateRequest,
34
+ GenerateResult,
35
+ GenerateStreamCallbacks,
36
+ } from "../../utils/providers/common/provider-dispatcher";
9
37
 
10
38
  const {
11
39
  findOneMock,
@@ -18,6 +46,7 @@ const {
18
46
  requestLogCreateRunMock,
19
47
  requestLogFinishRunMock,
20
48
  appendStepMock,
49
+ continueToolRunMock,
21
50
  dispatcherQueryMock,
22
51
  dispatcherQueryStreamMock,
23
52
  beforeQueryMock,
@@ -25,6 +54,10 @@ const {
25
54
  finishRunWithErrorMock,
26
55
  finishRunAbortedMock,
27
56
  getRateLimitsByTokenIdMock,
57
+ replaceByAccountMock,
58
+ completeGrokOAuthMock,
59
+ startGrokDeviceCodeMock,
60
+ pollGrokDeviceCodeMock,
28
61
  } = vi.hoisted(() => {
29
62
  // isolate:false 공유 레지스트리 방어: 같은 fork 에서 앞선 파일이 request-log/token/
30
63
  // run-lifecycle 실모듈을 캐시해 두면 아래 vi.mock 들이 조용히 무시된다(실 DB 호출로
@@ -41,6 +74,7 @@ const {
41
74
  requestLogCreateRunMock: vi.fn(),
42
75
  requestLogFinishRunMock: vi.fn(),
43
76
  appendStepMock: vi.fn(),
77
+ continueToolRunMock: vi.fn(),
44
78
  dispatcherQueryMock: vi.fn(),
45
79
  dispatcherQueryStreamMock: vi.fn(),
46
80
  beforeQueryMock: vi.fn(),
@@ -48,9 +82,37 @@ const {
48
82
  finishRunWithErrorMock: vi.fn(),
49
83
  finishRunAbortedMock: vi.fn(),
50
84
  getRateLimitsByTokenIdMock: vi.fn(),
85
+ replaceByAccountMock: vi.fn(),
86
+ completeGrokOAuthMock: vi.fn(),
87
+ startGrokDeviceCodeMock: vi.fn(),
88
+ pollGrokDeviceCodeMock: vi.fn(),
51
89
  };
52
90
  });
53
91
 
92
+ // OAuth pending state lives in the cache; a Map stub supports the round trip.
93
+ vi.mock("sonamu/cache", () => {
94
+ const store = new Map<string, string>();
95
+ return {
96
+ getCacheManagerRef: () => ({
97
+ set: async ({ key, value }: { key: string; value: string }) => {
98
+ store.set(key, value);
99
+ },
100
+ get: async ({ key }: { key: string }) => store.get(key),
101
+ delete: async ({ key }: { key: string }) => {
102
+ store.delete(key);
103
+ },
104
+ }),
105
+ };
106
+ });
107
+
108
+ // Token exchange and device endpoints hit xAI; startGrokOAuth stays real (fetch is stubbed).
109
+ vi.mock("../../utils/providers/grok", async (importOriginal) => ({
110
+ ...(await importOriginal<typeof import("../../utils/providers/grok")>()),
111
+ completeGrokOAuth: completeGrokOAuthMock,
112
+ startGrokDeviceCode: startGrokDeviceCodeMock,
113
+ pollGrokDeviceCode: pollGrokDeviceCodeMock,
114
+ }));
115
+
54
116
  vi.mock("../request-log/request-log.model", () => ({
55
117
  MICRO_USD: 1_000_000,
56
118
  RequestLogModel: {
@@ -58,6 +120,7 @@ vi.mock("../request-log/request-log.model", () => ({
58
120
  createRun: requestLogCreateRunMock,
59
121
  finishRun: requestLogFinishRunMock,
60
122
  appendStep: appendStepMock,
123
+ continueToolRun: continueToolRunMock,
61
124
  },
62
125
  }));
63
126
 
@@ -69,6 +132,7 @@ vi.mock("../token/token.model", () => ({
69
132
  toggleActive: toggleActiveMock,
70
133
  save: saveMock,
71
134
  updateFields: updateFieldsMock,
135
+ replaceByAccount: replaceByAccountMock,
72
136
  },
73
137
  }));
74
138
 
@@ -89,6 +153,10 @@ vi.mock("./qgrid.dispatcher", async (importOriginal) => {
89
153
  openaiDispatcher: {
90
154
  getRateLimitsByTokenId: getRateLimitsByTokenIdMock,
91
155
  },
156
+ grokDispatcher: null,
157
+ startupState: { openai: "starting", anthropic: "starting" },
158
+ tokens: new Map(),
159
+ subscriber: null,
92
160
  },
93
161
  };
94
162
  });
@@ -1427,3 +1495,945 @@ describe("QgridFrame.usage", () => {
1427
1495
  });
1428
1496
  });
1429
1497
  });
1498
+
1499
+ function grokJwtSegment(value: unknown): string {
1500
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
1501
+ }
1502
+
1503
+ function grokCredentials(overrides: Partial<GrokCredentials> = {}): GrokCredentials {
1504
+ return {
1505
+ authMode: "oauth",
1506
+ accessToken: `${grokJwtSegment({ alg: "ES256", typ: "at+jwt" })}.${grokJwtSegment({
1507
+ iss: "https://auth.x.ai",
1508
+ sub: "11111111-0000-4000-8000-000000000001",
1509
+ exp: 2_000_000_000,
1510
+ })}.c2ln`,
1511
+ refreshToken: "grok-refresh-1",
1512
+ expiresAt: "2030-01-01T00:00:00Z",
1513
+ issuer: "https://auth.x.ai",
1514
+ clientId: GROK_OAUTH_CLIENT_ID,
1515
+ scopes: [...GROK_OAUTH_SCOPES],
1516
+ userId: "11111111-0000-4000-8000-000000000001",
1517
+ sessionId: "22222222-0000-4000-8000-000000000001",
1518
+ agentId: "33333333-0000-4000-8000-000000000001",
1519
+ subscriptionTier: "Super",
1520
+ ...overrides,
1521
+ };
1522
+ }
1523
+
1524
+ describe("QgridFrame Grok registration and routing", () => {
1525
+ beforeEach(() => {
1526
+ // Discovery fails offline, so startGrokOAuth uses the fixed xAI endpoints.
1527
+ vi.stubGlobal(
1528
+ "fetch",
1529
+ vi.fn(async () => {
1530
+ throw new Error("offline");
1531
+ }),
1532
+ );
1533
+ replaceByAccountMock.mockReset().mockResolvedValue([1]);
1534
+ completeGrokOAuthMock.mockReset();
1535
+ startGrokDeviceCodeMock.mockReset();
1536
+ pollGrokDeviceCodeMock.mockReset();
1537
+ findManyMock.mockReset();
1538
+ beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 51, stepIndex: 0 });
1539
+ afterQueryMock.mockReset().mockResolvedValue({});
1540
+ dispatcherQueryMock.mockReset();
1541
+ });
1542
+
1543
+ afterEach(() => {
1544
+ vi.unstubAllGlobals();
1545
+ Object.assign(QgridDispatcher, { grokDispatcher: null });
1546
+ delete QgridDispatcher.startupState.grok;
1547
+ });
1548
+
1549
+ async function startGrokLogin(name: string) {
1550
+ const result = await QgridFrame.oauthStartGrok(name);
1551
+ const url = new URL(result.authUrl);
1552
+ return { result, url, state: url.searchParams.get("state") ?? "" };
1553
+ }
1554
+
1555
+ it("oauthStartGrok returns an xAI authorize URL with the Grok client id in paste mode", async () => {
1556
+ const { result, url, state } = await startGrokLogin("grok-a");
1557
+
1558
+ expect(result.mode).toBe("code");
1559
+ expect(url.origin).toBe("https://auth.x.ai");
1560
+ expect(url.searchParams.get("client_id")).toBe(GROK_OAUTH_CLIENT_ID);
1561
+ expect(url.searchParams.get("code_challenge_method")).toBe("S256");
1562
+ expect(url.searchParams.get("redirect_uri")).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/callback$/);
1563
+ expect(state.length).toBeGreaterThan(20);
1564
+ });
1565
+
1566
+ it("oauthComplete binds a bare code to the pending Grok login and stores Grok credentials", async () => {
1567
+ const { url, state } = await startGrokLogin("grok-a");
1568
+ const credentials = grokCredentials();
1569
+ completeGrokOAuthMock.mockResolvedValueOnce(credentials);
1570
+
1571
+ await expect(QgridFrame.oauthComplete(" bare-code-123 ")).resolves.toEqual({
1572
+ added: true,
1573
+ name: "grok-a",
1574
+ });
1575
+
1576
+ expect(completeGrokOAuthMock).toHaveBeenCalledWith(
1577
+ expect.objectContaining({
1578
+ codeOrUrl: " bare-code-123 ",
1579
+ state,
1580
+ redirectUri: url.searchParams.get("redirect_uri"),
1581
+ verifier: expect.any(String),
1582
+ }),
1583
+ );
1584
+ expect(replaceByAccountMock).toHaveBeenCalledWith("grok", credentials.userId, {
1585
+ provider: "grok",
1586
+ credentials,
1587
+ name: "grok-a",
1588
+ });
1589
+ // The pending login is single-use.
1590
+ await expect(QgridFrame.oauthComplete("bare-code-123")).rejects.toThrow("code#state");
1591
+ });
1592
+
1593
+ it("oauthComplete accepts the pasted callback URL by its state", async () => {
1594
+ const { url, state } = await startGrokLogin("grok-b");
1595
+ completeGrokOAuthMock.mockResolvedValueOnce(grokCredentials());
1596
+ const pasted = `${url.searchParams.get("redirect_uri")}?code=abc&state=${state}`;
1597
+
1598
+ await expect(QgridFrame.oauthComplete(pasted)).resolves.toEqual({ added: true, name: "grok-b" });
1599
+ expect(completeGrokOAuthMock).toHaveBeenCalledWith(
1600
+ expect.objectContaining({ codeOrUrl: pasted, state }),
1601
+ );
1602
+ });
1603
+
1604
+ it("oauthComplete rejects an exchange that does not yield an at+jwt", async () => {
1605
+ await startGrokLogin("grok-a");
1606
+ completeGrokOAuthMock.mockRejectedValueOnce(
1607
+ new GrokOAuthError("invalid_access_token", "access token is not an at+jwt"),
1608
+ );
1609
+
1610
+ await expect(QgridFrame.oauthComplete("bare-code")).rejects.toThrow(
1611
+ SD("qgrid.grokLoginFailed")("access token is not an at+jwt"),
1612
+ );
1613
+ expect(replaceByAccountMock).not.toHaveBeenCalled();
1614
+ });
1615
+
1616
+ it("refuses to store API-key shaped credentials", async () => {
1617
+ await startGrokLogin("grok-a");
1618
+ completeGrokOAuthMock.mockResolvedValueOnce(grokCredentials({ accessToken: "xai-api-key" }));
1619
+
1620
+ await expect(QgridFrame.oauthComplete("bare-code")).rejects.toThrow(
1621
+ SD("qgrid.grokNotOAuthToken"),
1622
+ );
1623
+ expect(replaceByAccountMock).not.toHaveBeenCalled();
1624
+ });
1625
+
1626
+ it("rejects a bare code when no Grok login is pending", async () => {
1627
+ await expect(QgridFrame.oauthComplete("orphan-code")).rejects.toThrow("code#state");
1628
+ expect(completeGrokOAuthMock).not.toHaveBeenCalled();
1629
+ });
1630
+
1631
+ it("device code login polls pending, slow_down, then added", async () => {
1632
+ startGrokDeviceCodeMock.mockResolvedValueOnce({
1633
+ deviceCode: "dev-1",
1634
+ userCode: "ABCD-EFGH",
1635
+ verificationUri: "https://accounts.x.ai/device",
1636
+ expiresIn: 600,
1637
+ interval: 5,
1638
+ });
1639
+ const credentials = grokCredentials();
1640
+ pollGrokDeviceCodeMock
1641
+ .mockResolvedValueOnce({ status: "pending", interval: 5 })
1642
+ .mockResolvedValueOnce({ status: "pending", interval: 10 })
1643
+ .mockResolvedValueOnce({ status: "complete", credentials });
1644
+
1645
+ await expect(QgridFrame.oauthStartGrokDevice("grok-dev")).resolves.toMatchObject({
1646
+ userCode: "ABCD-EFGH",
1647
+ interval: 5,
1648
+ });
1649
+ await expect(QgridFrame.oauthPollGrokDevice("dev-1")).resolves.toEqual({
1650
+ status: "pending",
1651
+ intervalSec: 5,
1652
+ });
1653
+ await expect(QgridFrame.oauthPollGrokDevice("dev-1")).resolves.toEqual({
1654
+ status: "slow_down",
1655
+ intervalSec: 10,
1656
+ });
1657
+ await expect(QgridFrame.oauthPollGrokDevice("dev-1")).resolves.toEqual({
1658
+ status: "added",
1659
+ name: "grok-dev",
1660
+ });
1661
+
1662
+ expect(pollGrokDeviceCodeMock).toHaveBeenLastCalledWith({ deviceCode: "dev-1", interval: 10 });
1663
+ expect(replaceByAccountMock).toHaveBeenCalledWith("grok", credentials.userId, {
1664
+ provider: "grok",
1665
+ credentials,
1666
+ name: "grok-dev",
1667
+ });
1668
+ await expect(QgridFrame.oauthPollGrokDevice("dev-1")).rejects.toThrow("expired");
1669
+ });
1670
+
1671
+ it("device code polling drops the login when the user denies it", async () => {
1672
+ startGrokDeviceCodeMock.mockResolvedValueOnce({
1673
+ deviceCode: "dev-2",
1674
+ userCode: "WXYZ",
1675
+ verificationUri: "https://accounts.x.ai/device",
1676
+ expiresIn: 600,
1677
+ interval: 5,
1678
+ });
1679
+ pollGrokDeviceCodeMock.mockRejectedValueOnce(
1680
+ new GrokOAuthError("access_denied", "authorization denied by the user"),
1681
+ );
1682
+
1683
+ await QgridFrame.oauthStartGrokDevice("grok-dev");
1684
+ await expect(QgridFrame.oauthPollGrokDevice("dev-2")).rejects.toThrow("denied");
1685
+ await expect(QgridFrame.oauthPollGrokDevice("dev-2")).rejects.toThrow("expired");
1686
+ expect(pollGrokDeviceCodeMock).toHaveBeenCalledTimes(1);
1687
+ });
1688
+
1689
+ it("query with grok/<model> reaches the registered Grok dispatcher", async () => {
1690
+ const generate = vi.fn(async () => ({
1691
+ text: "grok says hi",
1692
+ tokenName: "grok-a",
1693
+ usage: {
1694
+ totalTokens: 3,
1695
+ inputTokens: 2,
1696
+ cachedInputTokens: 0,
1697
+ outputTokens: 1,
1698
+ reasoningOutputTokens: 0,
1699
+ },
1700
+ durationMs: 5,
1701
+ model: "grok-4.7",
1702
+ threadCoord: { workerId: 1, threadId: "t", epoch: 0 },
1703
+ }));
1704
+ const routing = new QgridDispatcherClass({
1705
+ grokDispatcher: {
1706
+ generate,
1707
+ generateStream: vi.fn(),
1708
+ start: vi.fn(async () => {}),
1709
+ stop: vi.fn(async () => {}),
1710
+ onTokenAdded: vi.fn(),
1711
+ onTokenUpdated: vi.fn(),
1712
+ onTokenRemoved: vi.fn(),
1713
+ replaceTokens: vi.fn(),
1714
+ },
1715
+ });
1716
+ dispatcherQueryMock.mockImplementationOnce(
1717
+ (input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
1718
+ routing.query(input, signal),
1719
+ );
1720
+
1721
+ const result = await QgridFrame.query({ prompt: "hi", model: "grok/grok-4.7" });
1722
+
1723
+ expect(generate).toHaveBeenCalledWith(expect.objectContaining({ model: "grok-4.7" }));
1724
+ expect(result.text).toBe("grok says hi");
1725
+ });
1726
+
1727
+ it("usage reports cached Grok billing without keepalive", async () => {
1728
+ const credentials = grokCredentials();
1729
+ findManyMock.mockResolvedValueOnce({
1730
+ rows: [{ ...tokenEntry, id: 9, provider: "grok", name: "grok-a", credentials }],
1731
+ });
1732
+ Object.assign(QgridDispatcher, {
1733
+ grokDispatcher: {
1734
+ tokenStates: () => [
1735
+ {
1736
+ id: 9,
1737
+ name: "grok-a",
1738
+ lastBilling: {
1739
+ utilizationPct: 42,
1740
+ periodType: "USAGE_PERIOD_TYPE_WEEKLY",
1741
+ periodEnd: "2030-01-08T00:00:00Z",
1742
+ fetchedAt: 1,
1743
+ },
1744
+ onDemandExcluded: true,
1745
+ zdrVideoExcluded: true,
1746
+ lastTier: "Ultra",
1747
+ },
1748
+ ],
1749
+ },
1750
+ });
1751
+
1752
+ await expect(QgridFrame.usage(9)).resolves.toEqual({
1753
+ provider: "grok",
1754
+ fiveHour: null,
1755
+ sevenDay: { utilization: 42, resetsAt: "2030-01-08T00:00:00Z" },
1756
+ periodType: "USAGE_PERIOD_TYPE_WEEKLY",
1757
+ tier: "Ultra",
1758
+ onDemandExcluded: true,
1759
+ zdrVideoExcluded: true,
1760
+ keepaliveApplicable: false,
1761
+ });
1762
+ });
1763
+
1764
+ it("usage falls back to the stored tier before the first billing fetch", async () => {
1765
+ findManyMock.mockResolvedValueOnce({
1766
+ rows: [
1767
+ { ...tokenEntry, id: 9, provider: "grok", name: "grok-a", credentials: grokCredentials() },
1768
+ ],
1769
+ });
1770
+
1771
+ await expect(QgridFrame.usage(9)).resolves.toMatchObject({
1772
+ provider: "grok",
1773
+ sevenDay: null,
1774
+ tier: "Super",
1775
+ onDemandExcluded: false,
1776
+ zdrVideoExcluded: false,
1777
+ keepaliveApplicable: false,
1778
+ });
1779
+ });
1780
+
1781
+ it("health includes the Grok startup state", async () => {
1782
+ QgridDispatcher.startupState.grok = "ready";
1783
+
1784
+ const health = await QgridFrame.health();
1785
+
1786
+ expect(health.providers.grok).toBe("ready");
1787
+ expect(health.ready).toBe(true);
1788
+ });
1789
+ });
1790
+
1791
+
1792
+ // ─── U11: Grok media/voice endpoints, providerOptions/providerMetadata, tool emulation ───
1793
+
1794
+ type FakeGrok = Record<string, ReturnType<typeof vi.fn>>;
1795
+
1796
+ function grokTurn(overrides: Partial<GenerateResult> = {}): GenerateResult {
1797
+ return {
1798
+ text: "grok says hi",
1799
+ tokenName: "grok/a",
1800
+ usage: {
1801
+ totalTokens: 30,
1802
+ inputTokens: 20,
1803
+ cachedInputTokens: 5,
1804
+ outputTokens: 10,
1805
+ reasoningOutputTokens: 2,
1806
+ contextTokens: 30,
1807
+ },
1808
+ durationMs: 5,
1809
+ ttftMs: 2,
1810
+ model: "grok-4.7",
1811
+ threadCoord: { workerId: 7, threadId: "t", epoch: -1 },
1812
+ costUsd: 0.000_338,
1813
+ costSource: "provider" as const,
1814
+ providerMetadata: { xai: { costInUsdTicks: 3_380_000, contextWindow: 256_000 } },
1815
+ ...overrides,
1816
+ };
1817
+ }
1818
+
1819
+ function directProvider(result: GenerateResult) {
1820
+ return {
1821
+ generate: vi.fn(async (_req: GenerateRequest) => result),
1822
+ generateStream: vi.fn(async (_req: GenerateRequest, cb: GenerateStreamCallbacks) => {
1823
+ cb.onDelta(result.text);
1824
+ cb.onComplete(result);
1825
+ }),
1826
+ start: vi.fn(async () => {}),
1827
+ stop: vi.fn(async () => {}),
1828
+ onTokenAdded: vi.fn(),
1829
+ onTokenUpdated: vi.fn(),
1830
+ onTokenRemoved: vi.fn(),
1831
+ replaceTokens: vi.fn(),
1832
+ };
1833
+ }
1834
+
1835
+ const PNG_B64 = Buffer.from([0x89, 0x50, 0x4e, 0x47, 1, 2, 3, 4]).toString("base64");
1836
+
1837
+ describe("QgridFrame Grok media and voice (U11)", () => {
1838
+ let grok: FakeGrok;
1839
+
1840
+ beforeEach(() => {
1841
+ requestLogCreateRunMock.mockReset().mockResolvedValue(77);
1842
+ requestLogFinishRunMock.mockReset().mockResolvedValue(undefined);
1843
+ appendStepMock.mockReset().mockResolvedValue(1);
1844
+ continueToolRunMock.mockReset().mockResolvedValue(1);
1845
+ findActiveByProviderAndNameMock.mockReset();
1846
+ grok = {
1847
+ generateImages: vi.fn(),
1848
+ editImages: vi.fn(),
1849
+ startVideo: vi.fn(),
1850
+ getVideo: vi.fn(),
1851
+ transcribe: vi.fn(),
1852
+ speak: vi.fn(),
1853
+ };
1854
+ Object.assign(QgridDispatcher, { grokDispatcher: grok });
1855
+ });
1856
+
1857
+ afterEach(() => {
1858
+ Object.assign(QgridDispatcher, { grokDispatcher: null });
1859
+ });
1860
+
1861
+ it("generateImages routes grok/* to the Grok dispatcher and returns b64 plus the reported cost", async () => {
1862
+ grok.editImages!.mockResolvedValueOnce({
1863
+ result: {
1864
+ data: [{ b64_json: PNG_B64, mime_type: "image/png", revised_prompt: "a cat" }],
1865
+ usage: { cost_in_usd_ticks: 500_000_000 },
1866
+ },
1867
+ tokenId: 3,
1868
+ tokenName: "grok/a",
1869
+ });
1870
+ findActiveByProviderAndNameMock.mockResolvedValueOnce({ id: 3 });
1871
+ const ref = "data:image/png;base64," + PNG_B64;
1872
+
1873
+ const out = await QgridFrame.generateImages({
1874
+ model: "grok/grok-imagine-image-quality",
1875
+ prompt: "cat",
1876
+ n: 1,
1877
+ aspectRatio: "1:1",
1878
+ referenceImages: [ref],
1879
+ tokenName: "grok/a",
1880
+ projectName: "proj",
1881
+ });
1882
+
1883
+ expect(grok.generateImages).not.toHaveBeenCalled();
1884
+ expect(grok.editImages).toHaveBeenCalledWith(
1885
+ expect.objectContaining({
1886
+ model: "grok-imagine-image-quality",
1887
+ prompt: "cat",
1888
+ n: 1,
1889
+ aspectRatio: "1:1",
1890
+ images: [ref],
1891
+ preferredTokenId: 3,
1892
+ requirePreferredToken: true,
1893
+ }),
1894
+ );
1895
+ expect(out).toEqual({
1896
+ model: "grok/grok-imagine-image-quality",
1897
+ tokenName: "grok/a",
1898
+ images: [{ b64: PNG_B64, mimeType: "image/png", revisedPrompt: "a cat" }],
1899
+ usage: { imageCount: 1, costInUsdTicks: 500_000_000 },
1900
+ costUsd: 0.05,
1901
+ costSource: "provider",
1902
+ requestLogId: 77,
1903
+ });
1904
+ expect(requestLogCreateRunMock).toHaveBeenCalledWith(
1905
+ expect.objectContaining({
1906
+ requested_model_name: "grok/grok-imagine-image-quality",
1907
+ project_name: "proj",
1908
+ is_image_generation: true,
1909
+ }),
1910
+ );
1911
+ const step = appendStepMock.mock.calls[0]![1] as Record<string, unknown>;
1912
+ expect(step).toMatchObject({ type: "tool_call", tool_name: "image_generation" });
1913
+ expect(JSON.parse(step.tool_args as string)).toMatchObject({
1914
+ kind: "image",
1915
+ imageCount: 1,
1916
+ costInUsdTicks: 500_000_000,
1917
+ costSource: "provider",
1918
+ inputImages: [{ mediaType: "image/png", data: PNG_B64 }],
1919
+ });
1920
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(
1921
+ 77,
1922
+ expect.objectContaining({
1923
+ status: "succeeded",
1924
+ token_name: "grok/a",
1925
+ model_name: "grok/grok-imagine-image-quality",
1926
+ cost_usd: 0,
1927
+ cost_source: "provider",
1928
+ is_image_generation: true,
1929
+ image_cost_usd: 50_000,
1930
+ image_cost_method: "reported:grok/grok-imagine-image-quality:ticks=500000000",
1931
+ }),
1932
+ );
1933
+ });
1934
+
1935
+ it("generateImages falls back to the catalog estimate when xAI reports no ticks", async () => {
1936
+ grok.generateImages!.mockResolvedValueOnce({
1937
+ result: { data: [{ b64_json: PNG_B64 }, { b64_json: PNG_B64 }] },
1938
+ tokenId: 3,
1939
+ tokenName: "grok/a",
1940
+ });
1941
+
1942
+ const out = await QgridFrame.generateImages({ model: "grok/grok-imagine-image-quality", prompt: "x", n: 2, logger: false });
1943
+
1944
+ expect(out.costUsd).toBeCloseTo(0.1, 12);
1945
+ expect(out.costSource).toBe("pricing_table");
1946
+ expect(out.images.map((image) => image.mimeType)).toEqual(["image/png", "image/png"]);
1947
+ expect(out.requestLogId).toBeUndefined();
1948
+ expect(requestLogCreateRunMock).not.toHaveBeenCalled();
1949
+ });
1950
+
1951
+ it.each([
1952
+ [{ model: "openai/gpt-image-2", prompt: "x" }, "model"],
1953
+ [{ model: "grok/grok-imagine-image", prompt: "x", referenceImages: ["http://example.com/a.png"] }, "https"],
1954
+ [{ model: "grok/grok-imagine-image", prompt: "x", referenceImages: ["data:image/gif;base64,R0lG"] }, "image/png"],
1955
+ [
1956
+ {
1957
+ model: "grok/grok-imagine-image",
1958
+ prompt: "x",
1959
+ referenceImages: ["data:image/png;base64," + "A".repeat(28 * 1024 * 1024)],
1960
+ },
1961
+ "7 MiB",
1962
+ ],
1963
+ ])("generateImages rejects invalid input %#", async (input, message) => {
1964
+ await expect(QgridFrame.generateImages(input)).rejects.toThrow(message);
1965
+ expect(grok.generateImages).not.toHaveBeenCalled();
1966
+ expect(grok.editImages).not.toHaveBeenCalled();
1967
+ });
1968
+
1969
+ it("caps inline media so every accepted request fits the JSON body limit", () => {
1970
+ // Largest base64 string whose decoded size is exactly the inline budget.
1971
+ const b64 = (bytes: number) => Buffer.alloc(bytes).toString("base64");
1972
+ const atLimit = "data:image/png;base64," + b64(MEDIA_MAX_INLINE_BYTES);
1973
+ const overLimit = "data:image/png;base64," + b64(MEDIA_MAX_INLINE_BYTES + 1);
1974
+ const half = "data:image/png;base64," + b64(MEDIA_MAX_INLINE_BYTES / 2 + 1);
1975
+
1976
+ const largest = { model: "grok/grok-imagine-image", prompt: "x".repeat(4096), referenceImages: [atLimit] };
1977
+ expect(GenerateImagesInput.safeParse(largest).success).toBe(true);
1978
+ expect(Buffer.byteLength(JSON.stringify(largest))).toBeLessThan(REQUEST_BODY_LIMIT_BYTES);
1979
+ expect(GenerateImagesInput.safeParse({ ...largest, referenceImages: [overLimit] }).success).toBe(false);
1980
+ // The aggregate counts every inline image of the request, not just each one.
1981
+ const split = GenerateImagesInput.safeParse({ ...largest, referenceImages: [half, half] });
1982
+ expect(split.success).toBe(false);
1983
+ expect(split.error?.issues.map((issue) => issue.message)).toContain("inline images exceed 7 MiB in total");
1984
+ expect(
1985
+ VideoStartInput.safeParse({ model: "grok/grok-imagine-video", prompt: "x", image: half, lastFrame: half }).success,
1986
+ ).toBe(false);
1987
+
1988
+ // qgrid has no source-video input, and reference-to-video needs reference images.
1989
+ const video = { model: "grok/grok-imagine-video", prompt: "x" };
1990
+ for (const mode of ["edit-video", "extend-video"]) {
1991
+ expect(VideoStartInput.safeParse({ ...video, mode }).success).toBe(false);
1992
+ }
1993
+ const noReferences = VideoStartInput.safeParse({ ...video, mode: "reference-to-video" });
1994
+ expect(noReferences.error?.issues.map((issue) => issue.path.join("."))).toEqual(["referenceImages"]);
1995
+ expect(
1996
+ VideoStartInput.safeParse({ ...video, mode: "reference-to-video", referenceImages: ["https://x/r.png"] })
1997
+ .success,
1998
+ ).toBe(true);
1999
+
2000
+ const audio = { audio: b64(MEDIA_MAX_INLINE_BYTES), mediaType: "audio/wav" };
2001
+ expect(TranscribeInput.safeParse(audio).success).toBe(true);
2002
+ expect(Buffer.byteLength(JSON.stringify(audio))).toBeLessThan(REQUEST_BODY_LIMIT_BYTES);
2003
+ expect(TranscribeInput.safeParse({ ...audio, audio: b64(MEDIA_MAX_INLINE_BYTES + 1) }).success).toBe(false);
2004
+ });
2005
+
2006
+ it("answers an oversize JSON body with a Sonamu 413 instead of a 500", async () => {
2007
+ const server = Fastify({ bodyLimit: REQUEST_BODY_LIMIT_BYTES });
2008
+ server.setErrorHandler((error, _request, reply) => {
2009
+ handleServerError(mapRequestBodyTooLarge(error), reply);
2010
+ });
2011
+ server.post("/media", async () => ({ ok: true }));
2012
+ try {
2013
+ const payload = JSON.stringify({ audio: "A".repeat(REQUEST_BODY_LIMIT_BYTES), mediaType: "audio/wav" });
2014
+ const tooLarge = await server.inject({
2015
+ method: "POST",
2016
+ url: "/media",
2017
+ headers: { "content-type": "application/json" },
2018
+ payload,
2019
+ });
2020
+ expect(tooLarge.statusCode).toBe(413);
2021
+ expect(tooLarge.json()).toEqual({ name: "Error", message: SD("qgrid.requestBodyTooLarge")(10) });
2022
+
2023
+ const ok = await server.inject({ method: "POST", url: "/media", payload: { ok: true } });
2024
+ expect(ok.statusCode).toBe(200);
2025
+ } finally {
2026
+ await server.close();
2027
+ }
2028
+ });
2029
+
2030
+ it("videoStart answers GrokZdrVideoUnavailableError as 503 and logs the provider message", async () => {
2031
+ const zdr = new GrokZdrVideoUnavailableError(["grok/a"]);
2032
+ grok.startVideo!.mockRejectedValueOnce(zdr);
2033
+
2034
+ const error = await QgridFrame.videoStart({ model: "grok/grok-imagine-video-1.5", prompt: "x" }).catch((e: unknown) => e);
2035
+
2036
+ expect(error).toBeInstanceOf(ServiceUnavailableException);
2037
+ expect(error).toMatchObject({ statusCode: 503, message: SD("qgrid.grokZdrVideoUnavailable")("grok/a") });
2038
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(77, { status: "error", error_message: zdr.message });
2039
+ });
2040
+
2041
+ it("generateImages records a failed provider call as an error run", async () => {
2042
+ grok.generateImages!.mockRejectedValueOnce(new Error("upstream 500"));
2043
+
2044
+ await expect(QgridFrame.generateImages({ model: "grok/grok-imagine-image", prompt: "x" })).rejects.toThrow("upstream 500");
2045
+
2046
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(77, { status: "error", error_message: "upstream 500" });
2047
+ });
2048
+
2049
+ it("videoStart/videoStatus polls the starting token, checks the owner, and passes the URL through", async () => {
2050
+ grok.startVideo!.mockResolvedValueOnce({ result: { request_id: "req-1" }, tokenId: 9, tokenName: "grok/b" });
2051
+ const url = "https://vidgen.x.ai/abc/video.mp4?sig=opaque";
2052
+ grok.getVideo!
2053
+ .mockResolvedValueOnce({ result: { status: "pending" }, tokenId: 9, tokenName: "grok/b" })
2054
+ .mockResolvedValueOnce({
2055
+ result: { status: "done", video: { url, duration: 6 }, usage: { cost_in_usd_ticks: 2_000_000_000 } },
2056
+ tokenId: 9,
2057
+ tokenName: "grok/b",
2058
+ });
2059
+
2060
+ const started = await QgridFrame.videoStart({
2061
+ model: "grok/grok-imagine-video-1.5",
2062
+ prompt: "waves",
2063
+ duration: 6,
2064
+ resolution: "720p",
2065
+ projectName: "proj",
2066
+ });
2067
+ expect(started.jobHandle).toMatch(/^[A-Za-z0-9_-]{22}$/);
2068
+ expect(started).toMatchObject({ model: "grok/grok-imagine-video-1.5", requestLogId: 77 });
2069
+ expect(grok.startVideo).toHaveBeenCalledWith(
2070
+ expect.objectContaining({ model: "grok-imagine-video-1.5", duration: 6, resolution: "720p" }),
2071
+ );
2072
+ expect(appendStepMock).toHaveBeenCalledWith(
2073
+ 77,
2074
+ expect.objectContaining({ type: "tool_call", tool_name: "video_generation", tool_call_id: "video_generation:req-1" }),
2075
+ );
2076
+ expect(appendStepMock.mock.calls[0]![1]).not.toHaveProperty("tool_result");
2077
+
2078
+ // A different owner, or no owner, sees the same 404 as an unknown handle.
2079
+ await expect(QgridFrame.videoStatus({ jobHandle: started.jobHandle, projectName: "other" })).rejects.toThrow(SD("qgrid.videoJobNotFound"));
2080
+ await expect(QgridFrame.videoStatus({ jobHandle: started.jobHandle })).rejects.toThrow(SD("qgrid.videoJobNotFound"));
2081
+ await expect(QgridFrame.videoStatus({ jobHandle: "A".repeat(22), projectName: "proj" })).rejects.toThrow(SD("qgrid.videoJobNotFound"));
2082
+ expect(grok.getVideo).not.toHaveBeenCalled();
2083
+
2084
+ await expect(QgridFrame.videoStatus({ jobHandle: started.jobHandle, projectName: "proj" })).resolves.toEqual({
2085
+ status: "pending",
2086
+ mimeType: "video/mp4",
2087
+ requestLogId: 77,
2088
+ });
2089
+ const done = await QgridFrame.videoStatus({ jobHandle: started.jobHandle, projectName: "proj" });
2090
+ expect(done).toEqual({
2091
+ status: "done",
2092
+ url,
2093
+ mimeType: "video/mp4",
2094
+ durationSec: 6,
2095
+ costInUsdTicks: 2_000_000_000,
2096
+ costUsd: 0.2,
2097
+ requestLogId: 77,
2098
+ });
2099
+ expect(grok.getVideo).toHaveBeenCalledWith(9, "req-1", expect.any(AbortSignal));
2100
+
2101
+ // The terminal result is cached: no further upstream poll, no second log write.
2102
+ await expect(QgridFrame.videoStatus({ jobHandle: started.jobHandle, projectName: "proj" })).resolves.toEqual(done);
2103
+ expect(grok.getVideo).toHaveBeenCalledTimes(2);
2104
+ expect(continueToolRunMock).toHaveBeenCalledTimes(1);
2105
+ expect(continueToolRunMock).toHaveBeenCalledWith(77, [
2106
+ expect.objectContaining({ toolCallId: "video_generation:req-1", isError: false }),
2107
+ ]);
2108
+ expect(requestLogFinishRunMock).toHaveBeenCalledTimes(1);
2109
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(
2110
+ 77,
2111
+ expect.objectContaining({
2112
+ status: "succeeded",
2113
+ response: url,
2114
+ token_name: "grok/b",
2115
+ model_name: "grok/grok-imagine-video-1.5",
2116
+ cost_usd: 200_000,
2117
+ cost_source: "provider",
2118
+ }),
2119
+ );
2120
+ });
2121
+
2122
+ it("videoStatus logs a terminal result once when polls overlap", async () => {
2123
+ grok.startVideo!.mockResolvedValueOnce({ result: { request_id: "req-1" }, tokenId: 9, tokenName: "grok/b" });
2124
+ let release!: () => void;
2125
+ const upstream = new Promise<void>((resolve) => (release = resolve));
2126
+ grok.getVideo!.mockImplementation(async () => {
2127
+ await upstream;
2128
+ return { result: { status: "done", video: { url: "https://x/v.mp4" } }, tokenId: 9, tokenName: "grok/b" };
2129
+ });
2130
+ const { jobHandle } = await QgridFrame.videoStart({ model: "grok/grok-imagine-video-1.5", prompt: "x" });
2131
+
2132
+ const polls = [QgridFrame.videoStatus({ jobHandle }), QgridFrame.videoStatus({ jobHandle })];
2133
+ release();
2134
+ const [first, second] = await Promise.all(polls);
2135
+
2136
+ expect(grok.getVideo).toHaveBeenCalledTimes(2);
2137
+ expect(second).toEqual(first);
2138
+ expect(continueToolRunMock).toHaveBeenCalledTimes(1);
2139
+ expect(requestLogFinishRunMock).toHaveBeenCalledTimes(1);
2140
+ });
2141
+
2142
+ it("videoStatus reports a content-policy block as a failed job", async () => {
2143
+ grok.startVideo!.mockResolvedValueOnce({ result: { request_id: "req-2" }, tokenId: 9, tokenName: "grok/b" });
2144
+ grok.getVideo!.mockResolvedValueOnce({
2145
+ result: { status: "done", video: { url: "https://x/v.mp4", respect_moderation: false } },
2146
+ tokenId: 9,
2147
+ tokenName: "grok/b",
2148
+ });
2149
+ const { jobHandle } = await QgridFrame.videoStart({ model: "grok/grok-imagine-video-1.5", prompt: "x" });
2150
+
2151
+ const status = await QgridFrame.videoStatus({ jobHandle });
2152
+
2153
+ expect(status).toMatchObject({ status: "failed", mimeType: "video/mp4" });
2154
+ expect(status.url).toBeUndefined();
2155
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(77, expect.objectContaining({ status: "error" }));
2156
+ });
2157
+
2158
+ it("transcribe decodes base64 audio and returns the transcript", async () => {
2159
+ grok.transcribe!.mockResolvedValueOnce({
2160
+ result: { text: "hello there", language: "en", duration: 1.5 },
2161
+ tokenId: 3,
2162
+ tokenName: "grok/a",
2163
+ });
2164
+ const audio = Buffer.from("RIFFfakewav").toString("base64");
2165
+
2166
+ const out = await QgridFrame.transcribe({ audio, mediaType: "audio/wav", language: "en" });
2167
+
2168
+ expect(out).toEqual({
2169
+ text: "hello there",
2170
+ language: "en",
2171
+ durationSec: 1.5,
2172
+ model: "grok/grok-voice-transcribe-2.0",
2173
+ tokenName: "grok/a",
2174
+ requestLogId: 77,
2175
+ });
2176
+ const request = grok.transcribe!.mock.calls[0]![0] as { audio: Uint8Array; mediaType: string; model: string };
2177
+ expect(Buffer.from(request.audio).toString()).toBe("RIFFfakewav");
2178
+ expect(request).toMatchObject({ mediaType: "audio/wav", model: "grok-voice-transcribe-2.0" });
2179
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(
2180
+ 77,
2181
+ expect.objectContaining({ status: "succeeded", response: "hello there", cost_usd: null, cost_source: null }),
2182
+ );
2183
+ });
2184
+
2185
+ it("transcribe rejects non-base64 and non-audio input before dispatch", async () => {
2186
+ await expect(QgridFrame.transcribe({ audio: "not base64!", mediaType: "audio/wav" })).rejects.toThrow("base64");
2187
+ await expect(QgridFrame.transcribe({ audio: "AAAA", mediaType: "video/mp4" })).rejects.toThrow("audio/*");
2188
+ expect(grok.transcribe).not.toHaveBeenCalled();
2189
+ });
2190
+
2191
+ it("speak applies the /tts defaults and returns base64 audio", async () => {
2192
+ const bytes = new Uint8Array([0xff, 0xfb, 1, 2]);
2193
+ grok.speak!.mockResolvedValueOnce({ result: { audio: bytes, mediaType: "audio/mpeg" }, tokenId: 3, tokenName: "grok/a" });
2194
+
2195
+ const out = await QgridFrame.speak({ text: "hi" });
2196
+
2197
+ expect(grok.speak).toHaveBeenCalledWith(
2198
+ expect.objectContaining({ text: "hi", voice: "ara", language: "en", format: "mp3" }),
2199
+ );
2200
+ expect(out).toEqual({
2201
+ audio: Buffer.from(bytes).toString("base64"),
2202
+ mimeType: "audio/mpeg",
2203
+ tokenName: "grok/a",
2204
+ requestLogId: 77,
2205
+ });
2206
+ expect(Buffer.from(out.audio, "base64")).toEqual(Buffer.from(bytes));
2207
+ // No ticks reported: the cost is unknown, not a guessed 0.
2208
+ expect(requestLogFinishRunMock).toHaveBeenCalledWith(
2209
+ 77,
2210
+ expect.objectContaining({ status: "succeeded", cost_usd: null, cost_source: null }),
2211
+ );
2212
+ });
2213
+
2214
+ it("media endpoints answer not-ready when no Grok media dispatcher is registered", async () => {
2215
+ Object.assign(QgridDispatcher, { grokDispatcher: null });
2216
+ await expect(QgridFrame.speak({ text: "hi" })).rejects.toThrow(SD("qgrid.dispatcherStarting")("Grok"));
2217
+ expect(requestLogCreateRunMock).not.toHaveBeenCalled();
2218
+ });
2219
+ });
2220
+
2221
+ describe("QgridFrame Grok query contract (U11)", () => {
2222
+ beforeEach(() => {
2223
+ beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 61, stepIndex: 0 });
2224
+ afterQueryMock.mockReset().mockResolvedValue({});
2225
+ dispatcherQueryMock.mockReset();
2226
+ dispatcherQueryStreamMock.mockReset();
2227
+ });
2228
+
2229
+ afterEach(() => {
2230
+ vi.restoreAllMocks();
2231
+ });
2232
+
2233
+ it("forwards providerOptions.xai and surfaces providerMetadata.xai with the provider cost", async () => {
2234
+ const grok = directProvider(grokTurn());
2235
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2236
+ dispatcherQueryMock.mockImplementationOnce((input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
2237
+ routing.query(input, signal),
2238
+ );
2239
+
2240
+ const out = await QgridFrame.query({
2241
+ prompt: "hi",
2242
+ model: "grok/grok-4.7",
2243
+ providerOptions: { xai: { serviceTier: "priority", parallelToolCalls: false } },
2244
+ });
2245
+
2246
+ expect(grok.generate).toHaveBeenCalledWith(
2247
+ expect.objectContaining({
2248
+ model: "grok-4.7",
2249
+ providerOptions: { xai: { serviceTier: "priority", parallelToolCalls: false } },
2250
+ }),
2251
+ );
2252
+ expect(out).toMatchObject({
2253
+ requestLogId: 61,
2254
+ costUsd: 0.000_338,
2255
+ costSource: "provider",
2256
+ providerMetadata: { xai: { costInUsdTicks: 3_380_000, contextWindow: 256_000 } },
2257
+ });
2258
+ });
2259
+
2260
+ it("keeps a reported zero-tick cost authoritative and prices a tickless turn from the catalog", async () => {
2261
+ const zero = directProvider(grokTurn({ costUsd: 0, providerMetadata: { xai: { costInUsdTicks: 0 } } }));
2262
+ const zeroOut = await new QgridDispatcherClass({ grokDispatcher: zero }).query({ prompt: "hi", model: "grok/grok-4.7" });
2263
+ expect(zeroOut).toMatchObject({ costUsd: 0, costSource: "provider" });
2264
+
2265
+ const tickless = directProvider(grokTurn({ costUsd: undefined, costSource: undefined, providerMetadata: undefined }));
2266
+ const ticklessOut = await new QgridDispatcherClass({ grokDispatcher: tickless }).query({ prompt: "hi", model: "grok/grok-4.7" });
2267
+ // 15 uncached input * $2/M + 5 cached * $0.5/M + 10 output * $6/M
2268
+ expect(ticklessOut.costSource).toBe("pricing_table");
2269
+ expect(ticklessOut.costUsd).toBeCloseTo((15 * 2 + 5 * 0.5 + 10 * 6) / 1_000_000, 15);
2270
+ expect(ticklessOut.providerMetadata).toBeUndefined();
2271
+ });
2272
+
2273
+ it("publishes providerMetadata.xai on the stream done event", async () => {
2274
+ const grok = directProvider(grokTurn());
2275
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2276
+ dispatcherQueryStreamMock.mockImplementationOnce(
2277
+ (input: Parameters<typeof routing.queryStream>[0], cb: Parameters<typeof routing.queryStream>[1], signal?: AbortSignal) =>
2278
+ routing.queryStream(input, cb, signal),
2279
+ );
2280
+ const sse = { closed: false, publish: vi.fn(), onClose: vi.fn(), end: vi.fn(async () => {}) };
2281
+ vi.spyOn(Sonamu, "getContext").mockReturnValue({ createSSE: vi.fn(() => sse) } as never);
2282
+
2283
+ const { streamId } = await QgridFrame.prepareStream({
2284
+ prompt: "hi",
2285
+ model: "grok/grok-4.7",
2286
+ providerOptions: { xai: { serviceTier: "default" } },
2287
+ });
2288
+ await QgridFrame.queryStream(streamId);
2289
+
2290
+ expect(grok.generateStream).toHaveBeenCalledWith(
2291
+ expect.objectContaining({ providerOptions: { xai: { serviceTier: "default" } } }),
2292
+ expect.anything(),
2293
+ );
2294
+ expect(sse.publish).toHaveBeenCalledWith(
2295
+ "done",
2296
+ expect.objectContaining({
2297
+ requestLogId: 61,
2298
+ costSource: "provider",
2299
+ providerMetadata: { xai: { costInUsdTicks: 3_380_000, contextWindow: 256_000 } },
2300
+ }),
2301
+ );
2302
+ });
2303
+
2304
+ it.each([true, false])(
2305
+ "answers a Grok caller-validation error as 400, logged run=%s",
2306
+ async (logged) => {
2307
+ const grok = directProvider(grokTurn());
2308
+ const invalid = new GrokInvalidRequestError("Unsupported Grok history item at index 0");
2309
+ grok.generate.mockRejectedValueOnce(invalid);
2310
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2311
+ dispatcherQueryMock.mockImplementationOnce((input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
2312
+ routing.query(input, signal),
2313
+ );
2314
+
2315
+ const error = await QgridFrame.query({
2316
+ prompt: "hi",
2317
+ model: "grok/grok-4.7",
2318
+ history: "[{}]",
2319
+ ...(logged ? {} : { logger: false }),
2320
+ }).catch((e: unknown) => e);
2321
+
2322
+ expect(error).toBeInstanceOf(BadRequestException);
2323
+ expect(error).toMatchObject({
2324
+ statusCode: 400,
2325
+ message: SD("qgrid.grokInvalidRequest")(invalid.message),
2326
+ });
2327
+ if (logged) {
2328
+ // The run keeps the provider's own message.
2329
+ expect(finishRunWithErrorMock).toHaveBeenCalledWith(61, invalid.message, expect.anything());
2330
+ }
2331
+ },
2332
+ );
2333
+
2334
+ it("keeps Grok upstream failures as server errors", async () => {
2335
+ const grok = directProvider(grokTurn());
2336
+ const upstream = new Error("grok upstream 502");
2337
+ grok.generate.mockRejectedValueOnce(upstream);
2338
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2339
+ dispatcherQueryMock.mockImplementationOnce((input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
2340
+ routing.query(input, signal),
2341
+ );
2342
+
2343
+ await expect(QgridFrame.query({ prompt: "hi", model: "grok/grok-4.7" })).rejects.toBe(upstream);
2344
+ });
2345
+
2346
+ it.each([
2347
+ [new GrokOnDemandExcludedError(["grok-a", "grok-b"]), SD("qgrid.grokOnDemandExcluded")("grok-a, grok-b")],
2348
+ [new GrokCapacityError(), SD("qgrid.grokCapacity")],
2349
+ [new GrokNoWorkersError(), SD("qgrid.grokNoWorkers")("NO_GROK_WORKERS")],
2350
+ ])("answers a Grok no-eligible-token error as 503 (%s)", async (excluded, message) => {
2351
+ const grok = directProvider(grokTurn());
2352
+ grok.generate.mockRejectedValueOnce(excluded);
2353
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2354
+ dispatcherQueryMock.mockImplementationOnce((input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
2355
+ routing.query(input, signal),
2356
+ );
2357
+
2358
+ const error = await QgridFrame.query({ prompt: "hi", model: "grok/grok-4.7" }).catch((e: unknown) => e);
2359
+
2360
+ expect(error).toBeInstanceOf(ServiceUnavailableException);
2361
+ expect(error).toMatchObject({ statusCode: 503, message });
2362
+ });
2363
+
2364
+ it("answers GrokRateLimitedError as 429 with the cooldown in Retry-After", async () => {
2365
+ const header = vi.fn();
2366
+ const responseRaw = Object.assign(new EventEmitter(), { headersSent: false, writableEnded: false });
2367
+ vi.spyOn(Sonamu, "getContext").mockReturnValue({
2368
+ transport: "http",
2369
+ request: { raw: Object.assign(new EventEmitter(), { aborted: false }) },
2370
+ reply: { raw: responseRaw, header },
2371
+ } as never);
2372
+ const grok = directProvider(grokTurn());
2373
+ grok.generate.mockRejectedValueOnce(new GrokRateLimitedError(42));
2374
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2375
+ dispatcherQueryMock.mockImplementationOnce((input: Parameters<typeof routing.query>[0], signal?: AbortSignal) =>
2376
+ routing.query(input, signal),
2377
+ );
2378
+
2379
+ const error = await QgridFrame.query({ prompt: "hi", model: "grok/grok-4.7", logger: false }).catch(
2380
+ (e: unknown) => e,
2381
+ );
2382
+
2383
+ expect(error).toMatchObject({ statusCode: 429, message: SD("qgrid.grokRateLimited")(42) });
2384
+ expect(header).toHaveBeenCalledWith("Retry-After", "42");
2385
+
2386
+ // The server error handler keeps a Sonamu 429 and does not pin a 503 Retry-After over it.
2387
+ const reply = { header: vi.fn(), status: vi.fn(), send: vi.fn() };
2388
+ reply.status.mockReturnValue(reply);
2389
+ handleServerError(error as Error, reply as never);
2390
+ expect(reply.status).toHaveBeenCalledWith(429);
2391
+ expect(reply.header).not.toHaveBeenCalled();
2392
+ });
2393
+
2394
+ it("runs grok tools through the same structured-output emulation as openai (D8)", async () => {
2395
+ const tools = [
2396
+ {
2397
+ name: "get_weather",
2398
+ description: "weather",
2399
+ inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
2400
+ },
2401
+ ];
2402
+ const envelope = JSON.stringify({
2403
+ result: { action: "tool_call", answer: null, toolCalls: [{ toolName: "get_weather", args: '{"city":"Seoul"}' }] },
2404
+ });
2405
+ const grok = directProvider(grokTurn({ text: envelope }));
2406
+ const openai = directProvider(grokTurn({ text: envelope, model: "gpt-6-sol", providerMetadata: undefined }));
2407
+ const routing = new QgridDispatcherClass({ grokDispatcher: grok });
2408
+ routing.openaiDispatcher = openai as never;
2409
+
2410
+ const grokOut = await routing.query({ prompt: "weather?", model: "grok/grok-4.7", tools });
2411
+ const openaiOut = await routing.query({ prompt: "weather?", model: "openai/gpt-6-sol", tools });
2412
+
2413
+ const grokReq = grok.generate.mock.calls[0]![0];
2414
+ const openaiReq = openai.generate.mock.calls[0]![0];
2415
+ // No native tool plumbing: the envelope schema is the only tool contract on the wire.
2416
+ expect(grokReq.tools).toBeUndefined();
2417
+ expect(grokReq.outputSchema).toEqual(openaiReq.outputSchema);
2418
+ expect(typeof grokReq.stopAfterOutputMessage).toBe("function");
2419
+ for (const out of [grokOut, openaiOut]) {
2420
+ expect(out.finishReason).toBe("tool-calls");
2421
+ expect(out.content).toEqual([
2422
+ { type: "tool-call", toolCallId: expect.stringMatching(/^call_/), toolName: "get_weather", input: '{"city":"Seoul"}' },
2423
+ ]);
2424
+ }
2425
+
2426
+ // Follow-up with tool results continues the existing way and decodes the answer envelope.
2427
+ const answer = JSON.stringify({ result: { action: "answer", answer: "Sunny", toolCalls: null } });
2428
+ grok.generate.mockResolvedValueOnce(grokTurn({ text: answer }));
2429
+ const followUp = await routing.query({
2430
+ prompt: "weather?",
2431
+ model: "grok/grok-4.7",
2432
+ tools,
2433
+ toolResults: [{ toolCallId: "call_1", toolName: "get_weather", output: "sunny, 21C" }],
2434
+ });
2435
+ expect(followUp).toMatchObject({ finishReason: "stop", text: "Sunny" });
2436
+ const followReq = grok.generate.mock.calls[1]![0];
2437
+ expect(JSON.stringify(followReq.coldInput)).toContain("sunny, 21C");
2438
+ });
2439
+ });