@cartanova/qgrid-cli 2.7.5 → 2.7.7

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 (91) hide show
  1. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +7 -3
  2. package/bundle/dist/application/qgrid/qgrid.frame.js +31 -13
  3. package/bundle/dist/application/qgrid/qgrid.types.js +2 -1
  4. package/bundle/dist/application/qgrid/token-subscriber.js +19 -1
  5. package/bundle/dist/application/qgrid/token-trigger-setup.js +3 -2
  6. package/bundle/dist/application/qgrid/token-window-keepalive.js +159 -0
  7. package/bundle/dist/application/setting/setting.constant.js +16 -2
  8. package/bundle/dist/application/setting/setting.model.js +4 -3
  9. package/bundle/dist/application/setting/setting.store.js +2 -2
  10. package/bundle/dist/application/setting/setting.types.js +4 -2
  11. package/bundle/dist/application/sonamu.generated.js +5 -3
  12. package/bundle/dist/application/sonamu.generated.sso.js +3 -2
  13. package/bundle/dist/application/token/token.model.js +24 -2
  14. package/bundle/dist/application/token/token.types.js +3 -2
  15. package/bundle/dist/i18n/sd.generated.js +2 -1
  16. package/bundle/dist/migrations/20260819102011_alter_tokens_alter1.js +13 -0
  17. package/bundle/dist/migrations/20260820153531_alter_tokens_add1_alter1.js +15 -0
  18. package/bundle/dist/sonamu.config.js +9 -1
  19. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +7 -4
  20. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +11 -3
  21. package/bundle/dist/utils/providers/openai/openai-quota.js +4 -3
  22. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +51 -0
  23. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +14 -4
  24. package/bundle/src/application/qgrid/qgrid.frame.test.ts +67 -0
  25. package/bundle/src/application/qgrid/qgrid.frame.ts +41 -12
  26. package/bundle/src/application/qgrid/qgrid.types.ts +2 -0
  27. package/bundle/src/application/qgrid/token-subscriber.test.ts +112 -0
  28. package/bundle/src/application/qgrid/token-subscriber.ts +35 -0
  29. package/bundle/src/application/qgrid/token-trigger-setup.test.ts +6 -0
  30. package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -1
  31. package/bundle/src/application/qgrid/token-window-keepalive.test.ts +244 -0
  32. package/bundle/src/application/qgrid/token-window-keepalive.ts +237 -0
  33. package/bundle/src/application/setting/setting.constant.test.ts +11 -1
  34. package/bundle/src/application/setting/setting.constant.ts +17 -2
  35. package/bundle/src/application/setting/setting.model.ts +13 -2
  36. package/bundle/src/application/setting/setting.store.ts +2 -2
  37. package/bundle/src/application/setting/setting.types.ts +2 -0
  38. package/bundle/src/application/sonamu.generated.http +4 -1
  39. package/bundle/src/application/sonamu.generated.sso.ts +1 -0
  40. package/bundle/src/application/sonamu.generated.ts +3 -0
  41. package/bundle/src/application/token/token.entity.json +16 -2
  42. package/bundle/src/application/token/token.model.test.ts +66 -0
  43. package/bundle/src/application/token/token.model.ts +30 -1
  44. package/bundle/src/application/token/token.types.test.ts +10 -0
  45. package/bundle/src/application/token/token.types.ts +1 -0
  46. package/bundle/src/i18n/sd.generated.ts +1 -0
  47. package/bundle/src/migrations/20260819102011_alter_tokens_alter1.ts +13 -0
  48. package/bundle/src/migrations/20260820153531_alter_tokens_add1_alter1.ts +15 -0
  49. package/bundle/src/sonamu.config.ts +14 -0
  50. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +46 -0
  51. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +13 -4
  52. package/bundle/src/utils/providers/common/provider-dispatcher.ts +2 -0
  53. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +30 -0
  54. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +17 -1
  55. package/bundle/src/utils/providers/openai/openai-quota.test.ts +31 -0
  56. package/bundle/src/utils/providers/openai/openai-quota.ts +8 -2
  57. package/bundle/web-dist/client/assets/index-BshX4Gd8.css +1 -0
  58. package/bundle/web-dist/client/assets/index-CokxkK2G.js +106 -0
  59. package/bundle/web-dist/client/assets/{logs-BrAu0koY.js → logs-Ckqp98Y3.js} +1 -1
  60. package/bundle/web-dist/client/assets/{monit-D6JEk7n7.js → monit-u0pMHbyf.js} +1 -1
  61. package/bundle/web-dist/client/assets/{routes-Dvu63qre.js → routes-DobxbCAV.js} +2 -2
  62. package/bundle/web-dist/client/assets/{services.generated-CA7oRRYY.js → services.generated-Da1Eq5_R.js} +1 -1
  63. package/bundle/web-dist/client/assets/settings-C8rfyi9z.js +1 -0
  64. package/bundle/web-dist/client/assets/{show-BFGjYsWb.js → show-FbUQYs4L.js} +1 -1
  65. package/bundle/web-dist/client/assets/tokens-DFwEavFD.js +1 -0
  66. package/bundle/web-dist/client/assets/use-oauth-login-flow-D-9h3WUt.js +1 -0
  67. package/bundle/web-dist/client/assets/use-update-token-mutation-CZ57EKA1.js +1 -0
  68. package/bundle/web-dist/client/index.html +3 -3
  69. package/bundle/web-dist/server/assets/{lazyRouteComponent-tS-rTksX.js → lazyRouteComponent-V1S8gqMS.js} +2 -2
  70. package/bundle/web-dist/server/assets/{logs-ttf79emP.js → logs-2Tkqcncq.js} +2 -2
  71. package/bundle/web-dist/server/assets/{monit-CJBUH0l6.js → monit-DrJ15xJL.js} +2 -2
  72. package/bundle/web-dist/server/assets/{plus-BuSuoeon.js → plus-DCai02Na.js} +1 -1
  73. package/bundle/web-dist/server/assets/{react-dom-7_SqhNWR.js → react-dom-BFiG5Daf.js} +1 -1
  74. package/bundle/web-dist/server/assets/{routes-z79f1sOM.js → routes-B9X0lvjQ.js} +5 -4
  75. package/bundle/web-dist/server/assets/{services.generated-B5UkNmgc.js → services.generated-y7xVOTCo.js} +4 -2
  76. package/bundle/web-dist/server/assets/{settings-nCv71sax.js → settings-JznSjwGQ.js} +80 -5
  77. package/bundle/web-dist/server/assets/{show-CLrA4ZQ-.js → show-D8kuAFbc.js} +2 -2
  78. package/bundle/web-dist/server/assets/{tokens-BwlkZELV.js → tokens-CyItE-L-.js} +4 -3
  79. package/bundle/web-dist/server/assets/{use-oauth-login-flow-B2QllVMi.js → use-oauth-login-flow-CItVKngs.js} +2 -7
  80. package/bundle/web-dist/server/assets/use-update-token-mutation-BNaRPVaX.js +7 -0
  81. package/bundle/web-dist/server/entry-server.generated.js +103 -58
  82. package/package.json +1 -1
  83. package/skills/qgrid/references/cli-env-and-server-boot.md +1 -0
  84. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
  85. package/skills/qgrid/references/token-auth-quota-lifecycle.md +25 -1
  86. package/bundle/web-dist/client/assets/index-CwSSXuZv.js +0 -106
  87. package/bundle/web-dist/client/assets/index-D9xE_tO7.css +0 -1
  88. package/bundle/web-dist/client/assets/settings-mwDGFwUi.js +0 -1
  89. package/bundle/web-dist/client/assets/tokens-D-P__jYk.js +0 -1
  90. package/bundle/web-dist/client/assets/use-oauth-login-flow-CJpoJD26.js +0 -1
  91. /package/bundle/web-dist/client/assets/{plus-DqBd6gHk.js → plus-p4vtg6LN.js} +0 -0
@@ -0,0 +1,7 @@
1
+ import { g as useMutation, n as QgridService } from "./services.generated-y7xVOTCo.js";
2
+ //#region src/services/token/use-update-token-mutation.ts
3
+ function useUpdateTokenMutation() {
4
+ return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight, keepaliveEnabled }) => QgridService.updateToken(id, name, quotaThreshold, weight, keepaliveEnabled) });
5
+ }
6
+ //#endregion
7
+ export { useUpdateTokenMutation as t };
@@ -1,6 +1,6 @@
1
- import { A as matchMutation, B as __require, C as infiniteQueryBehavior, D as functionalUpdate, E as notifyManager, F as skipToken, I as focusManager, L as Subscribable, M as noop$1, N as partialMatchKey, O as hashKey, P as resolveStaleTime, R as __commonJSMin, S as Mutation, T as onlineManager, V as __toESM, _ as useQuery, b as require_jsx_runtime, c as SD, d as _enum, f as number$1, j as matchQuery, k as hashQueryKeyByOptions, l as SUPPORTED_LOCALES, m as string$2, n as QgridService, p as object, u as setLocale, v as QueryClientProvider, w as Query, x as require_react, z as __exportAll } from "./assets/services.generated-B5UkNmgc.js";
2
- import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-tS-rTksX.js";
3
- import { n as require_shim, t as require_react_dom } from "./assets/react-dom-7_SqhNWR.js";
1
+ import { A as matchMutation, B as __require, C as infiniteQueryBehavior, D as functionalUpdate, E as notifyManager, F as skipToken, I as focusManager, L as Subscribable, M as noop$1, N as partialMatchKey, O as hashKey, P as resolveStaleTime, R as __commonJSMin, S as Mutation, T as onlineManager, V as __toESM, b as require_jsx_runtime, c as SD, d as _enum, f as number$1, j as matchQuery, k as hashQueryKeyByOptions, l as SUPPORTED_LOCALES, m as string$2, n as QgridService, o as TokenService, p as object, u as setLocale, v as QueryClientProvider, w as Query, x as require_react, z as __exportAll } from "./assets/services.generated-y7xVOTCo.js";
2
+ import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-V1S8gqMS.js";
3
+ import { n as require_shim, t as require_react_dom } from "./assets/react-dom-BFiG5Daf.js";
4
4
  import minpath from "node:path";
5
5
  import minproc from "node:process";
6
6
  import { fileURLToPath as urlToPath } from "node:url";
@@ -46060,6 +46060,24 @@ var lucideX = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
46060
46060
  //#region src/assets/chat-icon.png
46061
46061
  var chat_icon_default = "/assets/chat-icon-JYfue7fB.png";
46062
46062
  //#endregion
46063
+ //#region src/components/qgrid/chat-token-selection.ts
46064
+ function chatTokenOptions(tokens, provider) {
46065
+ return tokens.filter((token) => token.active && token.provider === provider).toSorted((a, b) => a.ord - b.ord || a.name.localeCompare(b.name));
46066
+ }
46067
+ function providerTokenMissing(tokens, provider) {
46068
+ return provider !== void 0 && !tokens.some((token) => token.active && token.provider === provider);
46069
+ }
46070
+ function resolvedTokenName(tokenName, options, tokenListLoaded) {
46071
+ if (!tokenName || !tokenListLoaded) return tokenName;
46072
+ return options.some((token) => token.name === tokenName) ? tokenName : "";
46073
+ }
46074
+ function chatConfigChanged(previous, current) {
46075
+ return previous.model !== current.model || previous.system !== current.system || previous.tokenName !== current.tokenName;
46076
+ }
46077
+ function tokenTargetPayload(tokenName) {
46078
+ return tokenName ? { tokenName } : {};
46079
+ }
46080
+ //#endregion
46063
46081
  //#region src/components/qgrid/ChatWidget.tsx
46064
46082
  var CHAT_PROJECT_NAME = "qgrid_chat";
46065
46083
  var DEFAULT_MODEL = "openai/gpt-5.6-terra";
@@ -46104,9 +46122,9 @@ var PROVIDER_LABELS = {
46104
46122
  openai: "OpenAI",
46105
46123
  anthropic: "Anthropic"
46106
46124
  };
46107
- function humanizeError(message, provider, stats) {
46108
- const providerTokenMissing = provider !== void 0 && stats !== void 0 && !stats.some((t) => t.provider === provider);
46109
- if (/NO_OPENAI_WORKERS|no ready openai workers|no anthropic tokens available/i.test(message) || providerTokenMissing && /기동 중입니다/.test(message)) return `사용 가능한 ${provider ? PROVIDER_LABELS[provider] ?? provider : ""} 토큰이 없습니다. Tokens 페이지에서 토큰을 추가해주세요.`;
46125
+ function humanizeError(message, provider, tokens) {
46126
+ const missing = tokens !== void 0 && providerTokenMissing(tokens, provider);
46127
+ if (/NO_OPENAI_WORKERS|no ready openai workers|no anthropic tokens available/i.test(message) || missing && /기동 중입니다/.test(message)) return `사용 가능한 ${provider ? PROVIDER_LABELS[provider] ?? provider : ""} 토큰이 없습니다. Tokens 페이지에서 토큰을 추가해주세요.`;
46110
46128
  return message;
46111
46129
  }
46112
46130
  function formatCost(costUsd) {
@@ -46130,6 +46148,7 @@ function ChatWidget() {
46130
46148
  const [messages, setMessages] = (0, import_react.useState)([]);
46131
46149
  const [draft, setDraft] = (0, import_react.useState)("");
46132
46150
  const [model, setModel] = (0, import_react.useState)(DEFAULT_MODEL);
46151
+ const [tokenName, setTokenName] = (0, import_react.useState)("");
46133
46152
  const [effort, setEffort] = (0, import_react.useState)("");
46134
46153
  const [system, setSystem] = (0, import_react.useState)("");
46135
46154
  const [settingsOpen, setSettingsOpen] = (0, import_react.useState)(false);
@@ -46143,19 +46162,20 @@ function ChatWidget() {
46143
46162
  const fileInputRef = (0, import_react.useRef)(null);
46144
46163
  const provider = model.split("/")[0];
46145
46164
  const supportsImages = provider === "openai";
46146
- const { data: tokenStats } = useQuery({
46147
- ...QgridService.statsQueryOptions(),
46148
- enabled: open,
46149
- staleTime: 15e3
46150
- });
46151
- const statsRef = (0, import_react.useRef)(void 0);
46165
+ const { data: tokensData } = TokenService.useTokens("A", { orderBy: "ord-asc" }, { enabled: open });
46166
+ const tokenOptions = (0, import_react.useMemo)(() => chatTokenOptions(tokensData?.rows ?? [], provider), [provider, tokensData?.rows]);
46167
+ const tokenListLoaded = tokensData?.rows !== void 0;
46168
+ const tokensRef = (0, import_react.useRef)(void 0);
46152
46169
  (0, import_react.useEffect)(() => {
46153
- statsRef.current = tokenStats;
46154
- }, [tokenStats]);
46155
- const providerTokenMissing = tokenStats !== void 0 && !tokenStats.some((t) => t.provider === provider);
46170
+ tokensRef.current = tokensData?.rows;
46171
+ }, [tokensData?.rows]);
46172
+ const selectedProviderMissing = tokensData?.rows !== void 0 && providerTokenMissing(tokensData.rows, provider);
46156
46173
  (0, import_react.useEffect)(() => {
46157
46174
  if (!supportsImages) setAttachments([]);
46158
46175
  }, [supportsImages]);
46176
+ (0, import_react.useEffect)(() => {
46177
+ setTokenName((current) => resolvedTokenName(current, tokenOptions, tokenListLoaded));
46178
+ }, [tokenListLoaded, tokenOptions]);
46159
46179
  const addAttachments = async (files) => {
46160
46180
  if (!files) return;
46161
46181
  const slots = MAX_ATTACHMENTS - attachments.length;
@@ -46170,7 +46190,7 @@ function ChatWidget() {
46170
46190
  });
46171
46191
  };
46172
46192
  const failStreaming = (message) => {
46173
- const friendly = humanizeError(message, lastConfigRef.current?.model.split("/")[0], statsRef.current);
46193
+ const friendly = humanizeError(message, lastConfigRef.current?.model.split("/")[0], tokensRef.current);
46174
46194
  patchLastAssistant((m) => m.status === "streaming" ? {
46175
46195
  ...m,
46176
46196
  status: "error",
@@ -46234,11 +46254,14 @@ function ChatWidget() {
46234
46254
  const prompt = draft.trim();
46235
46255
  if (!prompt || busy) return;
46236
46256
  const sys = system.trim();
46237
- if (lastConfigRef.current && (lastConfigRef.current.model !== model || lastConfigRef.current.system !== sys)) threadCoordRef.current = void 0;
46238
- lastConfigRef.current = {
46257
+ const selectedTokenName = resolvedTokenName(tokenName, tokenOptions, tokenListLoaded);
46258
+ const currentConfig = {
46239
46259
  model,
46240
- system: sys
46260
+ system: sys,
46261
+ tokenName: selectedTokenName
46241
46262
  };
46263
+ if (lastConfigRef.current && chatConfigChanged(lastConfigRef.current, currentConfig)) threadCoordRef.current = void 0;
46264
+ lastConfigRef.current = currentConfig;
46242
46265
  const images = supportsImages ? attachments : [];
46243
46266
  const history = buildHistory(messages);
46244
46267
  setMessages((prev) => [
@@ -46262,6 +46285,7 @@ function ChatWidget() {
46262
46285
  prompt,
46263
46286
  model,
46264
46287
  projectName: CHAT_PROJECT_NAME,
46288
+ ...tokenTargetPayload(selectedTokenName),
46265
46289
  ...sys ? { system: sys } : {},
46266
46290
  ...effort ? { effort } : {},
46267
46291
  ...images.length > 0 ? { input: images.map((url) => ({
@@ -46317,7 +46341,11 @@ function ChatWidget() {
46317
46341
  children: "채팅"
46318
46342
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
46319
46343
  className: "text-[11px] text-sand-400 truncate leading-tight",
46320
- children: [model, effort ? ` · ${effort}` : ""]
46344
+ children: [
46345
+ model,
46346
+ effort ? ` · ${effort}` : "",
46347
+ tokenName ? ` · ${tokenName}` : ""
46348
+ ]
46321
46349
  })]
46322
46350
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46323
46351
  className: "ml-auto flex items-center gap-0.5",
@@ -46325,7 +46353,7 @@ function ChatWidget() {
46325
46353
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46326
46354
  type: "button",
46327
46355
  onClick: () => setSettingsOpen((v) => !v),
46328
- title: "모델·effort·system prompt 설정",
46356
+ title: "모델·토큰·effort·system prompt 설정",
46329
46357
  className: `size-8 grid place-items-center rounded-full transition-colors duration-150 ${settingsOpen ? "text-sienna-500 bg-sienna-50" : "text-sand-500 hover:text-sand-700 hover:bg-sand-100"}`,
46330
46358
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideSlidersHorizontal, { className: "size-4" })
46331
46359
  }),
@@ -46349,38 +46377,55 @@ function ChatWidget() {
46349
46377
  }),
46350
46378
  settingsOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46351
46379
  className: "px-4 py-2.5 border-b border-sand-100 bg-sand-50/60 shrink-0 flex flex-col gap-1.5",
46352
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46353
- className: "flex items-center gap-1.5",
46354
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
46355
- value: model,
46356
- onChange: (e) => setModel(e.target.value),
46357
- disabled: busy,
46358
- className: "min-w-0 flex-1 rounded-xl border border-sand-200/80 bg-white px-2 py-1.5 text-[12px] text-sand-800 focus:outline-none focus:border-sienna-300 disabled:opacity-50",
46359
- children: MODEL_PRESETS.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46360
- value: m,
46361
- children: m
46362
- }, m))
46363
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
46364
- value: effort,
46365
- onChange: (e) => setEffort(e.target.value),
46380
+ children: [
46381
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46382
+ className: "flex items-center gap-1.5",
46383
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
46384
+ value: model,
46385
+ onChange: (e) => setModel(e.target.value),
46386
+ disabled: busy,
46387
+ className: "min-w-0 flex-1 rounded-xl border border-sand-200/80 bg-white px-2 py-1.5 text-[12px] text-sand-800 focus:outline-none focus:border-sienna-300 disabled:opacity-50",
46388
+ children: MODEL_PRESETS.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46389
+ value: m,
46390
+ children: m
46391
+ }, m))
46392
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
46393
+ value: effort,
46394
+ onChange: (e) => setEffort(e.target.value),
46395
+ disabled: busy,
46396
+ className: "rounded-xl border border-sand-200/80 bg-white px-2 py-1.5 text-[12px] text-sand-600 focus:outline-none focus:border-sienna-300 disabled:opacity-50",
46397
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46398
+ value: "",
46399
+ children: "effort 기본"
46400
+ }), EFFORT_PRESETS.map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46401
+ value: v,
46402
+ children: v
46403
+ }, v))]
46404
+ })]
46405
+ }),
46406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
46407
+ value: tokenName,
46408
+ onChange: (e) => setTokenName(e.target.value),
46366
46409
  disabled: busy,
46367
- className: "rounded-xl border border-sand-200/80 bg-white px-2 py-1.5 text-[12px] text-sand-600 focus:outline-none focus:border-sienna-300 disabled:opacity-50",
46410
+ "aria-label": "요청 토큰",
46411
+ className: "w-full rounded-xl border border-sand-200/80 bg-white px-2 py-1.5 text-[12px] text-sand-700 focus:outline-none focus:border-sienna-300 disabled:opacity-50",
46368
46412
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46369
46413
  value: "",
46370
- children: "effort 기본"
46371
- }), EFFORT_PRESETS.map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46372
- value: v,
46373
- children: v
46374
- }, v))]
46375
- })]
46376
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
46377
- value: system,
46378
- onChange: (e) => setSystem(e.target.value),
46379
- disabled: busy,
46380
- placeholder: "system prompt (선택)",
46381
- rows: 2,
46382
- className: "w-full resize-none rounded-xl border border-sand-200/80 bg-white px-2.5 py-1.5 text-[12px] text-sand-800 placeholder:text-sand-400 focus:outline-none focus:border-sienna-300 disabled:opacity-50"
46383
- })]
46414
+ children: "토큰 자동 분배"
46415
+ }), tokenOptions.map((token) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
46416
+ value: token.name,
46417
+ children: token.name
46418
+ }, token.name))]
46419
+ }),
46420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
46421
+ value: system,
46422
+ onChange: (e) => setSystem(e.target.value),
46423
+ disabled: busy,
46424
+ placeholder: "system prompt (선택)",
46425
+ rows: 2,
46426
+ className: "w-full resize-none rounded-xl border border-sand-200/80 bg-white px-2.5 py-1.5 text-[12px] text-sand-800 placeholder:text-sand-400 focus:outline-none focus:border-sienna-300 disabled:opacity-50"
46427
+ })
46428
+ ]
46384
46429
  }),
46385
46430
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46386
46431
  ref: scrollRef,
@@ -46480,7 +46525,7 @@ function ChatWidget() {
46480
46525
  }, i))
46481
46526
  })
46482
46527
  }),
46483
- providerTokenMissing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46528
+ selectedProviderMissing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46484
46529
  className: "px-4 py-1.5 border-t border-caution-400/30 bg-caution-400/10 text-[11px] text-caution-500 shrink-0",
46485
46530
  children: [
46486
46531
  PROVIDER_LABELS[provider] ?? provider,
@@ -46580,7 +46625,7 @@ function ChatWidget() {
46580
46625
  }
46581
46626
  //#endregion
46582
46627
  //#region src/components/Sidebar.tsx
46583
- var qgridVersion = "2.7.5";
46628
+ var qgridVersion = "2.7.7";
46584
46629
  function Sidebar({ className }) {
46585
46630
  const pathname = useRouterState({ select: (s) => s.location.pathname });
46586
46631
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
@@ -68465,15 +68510,15 @@ function RootComponent() {
68465
68510
  }
68466
68511
  //#endregion
68467
68512
  //#region src/routes/tokens.tsx
68468
- var $$splitComponentImporter$5 = () => import("./assets/tokens-BwlkZELV.js");
68513
+ var $$splitComponentImporter$5 = () => import("./assets/tokens-CyItE-L-.js");
68469
68514
  var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
68470
68515
  //#endregion
68471
68516
  //#region src/routes/settings.tsx
68472
- var $$splitComponentImporter$4 = () => import("./assets/settings-nCv71sax.js");
68517
+ var $$splitComponentImporter$4 = () => import("./assets/settings-JznSjwGQ.js");
68473
68518
  var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
68474
68519
  //#endregion
68475
68520
  //#region src/routes/monit.tsx
68476
- var $$splitComponentImporter$3 = () => import("./assets/monit-CJBUH0l6.js");
68521
+ var $$splitComponentImporter$3 = () => import("./assets/monit-DrJ15xJL.js");
68477
68522
  var monitSearchSchema = object({ level: _enum([
68478
68523
  "all",
68479
68524
  "warn",
@@ -68485,7 +68530,7 @@ var Route$3 = createFileRoute("/monit")({
68485
68530
  });
68486
68531
  //#endregion
68487
68532
  //#region src/routes/logs.tsx
68488
- var $$splitComponentImporter$2 = () => import("./assets/logs-ttf79emP.js");
68533
+ var $$splitComponentImporter$2 = () => import("./assets/logs-2Tkqcncq.js");
68489
68534
  var logsSearchSchema = object({
68490
68535
  token: string$2().optional(),
68491
68536
  project: string$2().optional(),
@@ -68499,11 +68544,11 @@ var Route$2 = createFileRoute("/logs")({
68499
68544
  });
68500
68545
  //#endregion
68501
68546
  //#region src/routes/index.tsx
68502
- var $$splitComponentImporter$1 = () => import("./assets/routes-z79f1sOM.js");
68547
+ var $$splitComponentImporter$1 = () => import("./assets/routes-B9X0lvjQ.js");
68503
68548
  var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
68504
68549
  //#endregion
68505
68550
  //#region src/routes/requests/show.tsx
68506
- var $$splitComponentImporter = () => import("./assets/show-CLrA4ZQ-.js");
68551
+ var $$splitComponentImporter = () => import("./assets/show-D8kuAFbc.js");
68507
68552
  var showSearchSchema = object({ id: number$1() });
68508
68553
  var Route = createFileRoute("/requests/show")({
68509
68554
  validateSearch: showSearchSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"
@@ -66,6 +66,7 @@ When helping a user set up qgrid locally, check for `QGRID_PROJECT_NAME` or conf
66
66
  |---|---:|---|
67
67
  | `QGRID_OPENAI_TRANSPORT` | `websocket` | OpenAI transport selector (`https` or `websocket`). Invalid values fail at dispatcher configuration time. This is the only OpenAI runtime knob; there is no concurrency cap. |
68
68
  | `QGRID_OPENAI_AUTOSCALE` / `QGRID_OPENAI_MIN_WORKERS_PER_TOKEN` / `QGRID_OPENAI_MAX_WORKERS_PER_TOKEN` / `QGRID_OPENAI_PERMITS_PER_TOKEN` | ignored | Removed. OpenAI requests run uncapped like Anthropic (stateless per-request execution); setting these has no effect. |
69
+ | `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED` (setting `qgrid.tokenWindowKeepaliveEnabled`) | on | Anthropic 5-hour window keepalive. Only the literal `false` disables it; anything else leaves it on. Changing the setting reschedules immediately. Disabling is the only switch — there is no per-token opt-in. |
69
70
  | `MAX_STRUCTURED_OUTPUT_RETRIES` | `1` for structured Anthropic streaming calls | Claude Code structured-output retry count for streaming only, clamped to at least 1 by qgrid. Non-streaming `generate` leaves the variable unset and uses Claude Code's default retry budget. |
70
71
 
71
72
  ## Server boot lifecycle
@@ -60,6 +60,8 @@ Structured output visibility: the list has an `Fmt` column (`json` badge from `i
60
60
 
61
61
  Tool contract visibility: the detail page renders a Tools panel between Response Type and Steps for requests whose `tools` is non-empty. It starts collapsed and its header shows the tool count plus a responsive, truncated name list built from the detail subset — collapsed views issue no extra query. Expanding fires `toolsView` once and caches it for that request id, since a stored tool contract never changes. Each card shows the name, the full description, the parameter count, and a compact display-only zod shape; enum values past the sixth collapse to `/* +N */`, with the full shape reachable through hover and keyboard focus. All JSON Schema conversion happens server-side — the web renders completed strings and holds no schema logic.
62
62
 
63
+ Chat token targeting: the dashboard chat panel can pin a conversation to one account. It defaults to automatic distribution and lists only active tokens whose provider matches the selected model, sending the choice as `tokenName` so the request lands on exactly that account with no fallback. Changing either the model or the token resets the existing conversation coordinates, because a thread's prompt cache belongs to the account that built it. A selection that disappears — deactivated or deleted — is cleared automatically, but the selection is preserved while the token list is still loading so a slow fetch does not silently reset the user's choice.
64
+
63
65
  The monit tab's vitals strip shows per-provider in-flight counts, an in-flight sparkline, token chips with quota gauges (fed by the dispatcher's 60-second rate-limits cache; `resetsAt` arrives as ms epoch), and a 1-hour per-provider request/error/cache-hit stats line polled separately every 30 seconds.
64
66
 
65
67
  Reference input images for image generation render in the detail view under the user prompt from the first synthetic `image_generation` step's `tool_args.inputImages`. Keep the prompt itself text-only, redact large base64 in request JSON views, and avoid rendering duplicate input previews for multi-output image turns.
@@ -12,6 +12,8 @@ Use this reference before changing token storage, OAuth flows, token activation,
12
12
  - Token sync and reconcile
13
13
  - Provider runtime consequences
14
14
  - Quota threshold semantics
15
+ - Explicit token targeting
16
+ - Token window keepalive
15
17
 
16
18
  ## Source Files
17
19
 
@@ -222,4 +224,26 @@ When a refresh fails in a way that only re-login can fix, qgrid removes the toke
222
224
  - If all ready/eligible tokens are over threshold, qgrid throws `QuotaThresholdExceededError`.
223
225
  - Log messages use `quota_threshold gate` with reasons such as `over_threshold`, `lookup_fail_open`, and `all_exceeded`.
224
226
 
225
- OpenAI queue items keep an `excludedTokenIds` set so a token found over threshold for a request is not immediately reselected for that same queued request.
227
+ ## Explicit Token Targeting
228
+
229
+ `tokenName` addresses one exact token instead of letting the router choose. It is resolved at the qgrid frame boundary into the dispatcher's existing `preferredTokenId`, so dispatchers stay provider-neutral and no second targeting contract exists.
230
+
231
+ - Resolution requires an active token matching both provider and name. The provider is read from the name prefix (`anthropic/yds`), and `tokens` carries a `(provider, name)` unique index so the address is unambiguous.
232
+ - A targeted request never falls back. Missing, inactive, or over-threshold targets fail rather than silently serving from another account — the whole point of addressing a token is that the answer comes from that account.
233
+ - Targeted selection does not consume weighted round-robin state. Without that guarantee, background targeting such as keepalive would skew the token distribution of ordinary traffic.
234
+ - Targeting is distinct from cache affinity. Affinity sets `preferredTokenId` as a preference and still falls back when the token is ineligible; explicit targeting adds `requirePreferredToken` so the same field becomes strict. The Anthropic path only ever receives explicit targeting — affinity coordinates are not forwarded there, so making it strict cannot break affinity behavior.
235
+ - Exposure boundary: `tokenName` is not part of the AI SDK provider options. It is reachable on the qgrid frame wire and used by the dashboard chat token picker and internal callers; SDK consumers keep round-robin.
236
+
237
+ ## Token Window Keepalive
238
+
239
+ Anthropic's 5-hour usage window anchors on the first request, so an unused token has no window at all — `five_hour.resets_at` is null until something is sent, and windows are per-account and independent. The practical cost is that starting work starts the clock: burn the quota in two hours and the remaining three are dead.
240
+
241
+ Keepalive breaks that coupling by burning the window clock during idle time. Every active Anthropic token gets one cheap request as soon as its window expires, so windows always run and a workday lands mid-window rather than on a boundary. Across an eight-hour day this raises the number of windows the day can touch from a fixed 2 to an expected 2.6.
242
+
243
+ - Scope is every active Anthropic token. There is no per-token opt-in; the only control is the global switch.
244
+ - The next fire is scheduled from the token's real `resetsAt` rather than a fixed five-hour timer, so a provider-side change to the window length is followed automatically.
245
+ - Per-token fire guards prevent repeats: 61 seconds after a successful fire (the usage API caches for a minute, so an immediate re-read would still show the old window) and five hours after a failure. Guards survive rescheduling — only timers are replaced — and the guard is recorded before the request is sent so a reschedule mid-flight cannot fire a second time.
246
+ - Rescheduling happens only when target-set membership changes (active/provider). Credential rotation updates the token row and fires the change notification, but it does not change membership and must not trigger a sweep; otherwise every OAuth refresh would re-poll usage for every token.
247
+ - Multiple instances each fire. A ping into an already-open window does nothing, so the duplicate is harmless and cheaper than a locking scheme — the same reasoning the expiry reminder uses.
248
+ - Fires are logged under the `qgrid-token-window-keepalive` project name so they can be excluded from traffic statistics.
249
+ - Known limits: the first sweep after boot has no concurrency cap, so N empty windows mean N simultaneous fires; and a failed fire costs that token five hours of window misalignment.