@cartanova/qgrid-cli 2.7.9 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +9 -8
- package/bundle/dist/application/qgrid/oauth.js +5 -2
- package/bundle/dist/application/qgrid/qgrid.frame.js +8 -14
- package/bundle/dist/application/qgrid/qgrid.types.js +1 -1
- package/bundle/dist/application/qgrid/token-death.js +12 -31
- package/bundle/dist/application/qgrid/token-trigger-setup.js +2 -1
- package/bundle/dist/application/sonamu.generated.js +3 -1
- package/bundle/dist/application/sonamu.generated.sso.js +2 -1
- package/bundle/dist/application/token/token.model.js +66 -47
- package/bundle/dist/application/token/token.types.js +2 -1
- package/bundle/dist/i18n/sd.generated.js +2 -1
- package/bundle/dist/migrations/20260901163614_alter_tokens_add1_alter1.js +15 -0
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +13 -3
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +12 -7
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +8 -5
- package/bundle/dist/utils/providers/common/model-cost.js +9 -11
- package/bundle/dist/utils/providers/openai/openai-refresh.js +6 -4
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +8 -6
- package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -8
- package/bundle/src/application/qgrid/oauth.test.ts +28 -0
- package/bundle/src/application/qgrid/oauth.ts +5 -0
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +22 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +8 -15
- package/bundle/src/application/qgrid/qgrid.types.ts +1 -1
- package/bundle/src/application/qgrid/token-death.test.ts +62 -58
- package/bundle/src/application/qgrid/token-death.ts +15 -39
- package/bundle/src/application/qgrid/token-trigger-setup.test.ts +6 -0
- package/bundle/src/application/qgrid/token-trigger-setup.ts +1 -0
- package/bundle/src/application/sonamu.generated.http +0 -27
- package/bundle/src/application/sonamu.generated.sso.ts +1 -0
- package/bundle/src/application/sonamu.generated.ts +3 -0
- package/bundle/src/application/token/token.entity.json +7 -0
- package/bundle/src/application/token/token.model.test.ts +112 -37
- package/bundle/src/application/token/token.model.ts +76 -48
- package/bundle/src/application/token/token.types.ts +1 -0
- package/bundle/src/i18n/sd.generated.ts +1 -0
- package/bundle/src/migrations/20260901163614_alter_tokens_add1_alter1.ts +15 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +25 -1
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +23 -4
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +36 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +20 -7
- package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +28 -0
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +18 -2
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +14 -10
- package/bundle/src/utils/providers/common/model-cost.test.ts +19 -19
- package/bundle/src/utils/providers/common/model-cost.ts +19 -16
- package/bundle/src/utils/providers/openai/openai-refresh.test.ts +16 -7
- package/bundle/src/utils/providers/openai/openai-refresh.ts +2 -2
- package/bundle/web-dist/client/assets/{index-D59Q_9Z4.js → index-DDSQ_6GL.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-BHVfI8ut.js → logs-D2OMhBlx.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-EkLxH1oN.js → monit-CYLYHrc-.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-Kbvuclip.js → routes-DlG0sJHR.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-Da1Eq5_R.js → services.generated-C6jfEwfK.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-B4uTFP3J.js → settings-BeCBndh_.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BjIdSPTd.js → show-BdmzN1As.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-BbCSxbFu.js → tokens-KCfMOeTH.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-4OltfVAd.js → use-oauth-login-flow-2dSjIvBk.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-CZ57EKA1.js → use-update-token-mutation-BIvO70-M.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{lazyRouteComponent-V1S8gqMS.js → lazyRouteComponent-C_k4R5n6.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-2Tkqcncq.js → logs-lBOU5eUP.js} +2 -2
- package/bundle/web-dist/server/assets/{monit-DrJ15xJL.js → monit-BW2X1D23.js} +2 -2
- package/bundle/web-dist/server/assets/{plus-DCai02Na.js → plus-DilmHifT.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BFiG5Daf.js → react-dom-CHRRsKwb.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-Ct3I9263.js → routes-CxMHXfdH.js} +5 -5
- package/bundle/web-dist/server/assets/{services.generated-y7xVOTCo.js → services.generated-buX3cJzR.js} +1 -9
- package/bundle/web-dist/server/assets/{settings-JznSjwGQ.js → settings-CFoXQaAg.js} +3 -3
- package/bundle/web-dist/server/assets/{show-D8kuAFbc.js → show-CcZJO6WF.js} +2 -2
- package/bundle/web-dist/server/assets/{tokens-PR42jF1J.js → tokens-C32lycTe.js} +6 -5
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-C-33Z5HA.js → use-oauth-login-flow-DCjxIa80.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BNaRPVaX.js → use-update-token-mutation-CqK8i8Fa.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +46 -23
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +8 -3
- package/skills/qgrid/references/ai-sdk-provider-contract.md +8 -3
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +17 -9
- package/skills/qgrid/references/cli-env-and-server-boot.md +1 -1
- package/skills/qgrid/references/decision-rationale.md +5 -2
- package/skills/qgrid/references/docs-workflow.md +3 -3
- package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
- package/skills/qgrid/references/sonamu-api-web-flow.md +6 -2
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +20 -13
- package/skills/qgrid/references/verification-and-debugging.md +19 -16
|
@@ -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, 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-
|
|
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-
|
|
3
|
-
import { n as require_shim, t as require_react_dom } from "./assets/react-dom-
|
|
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-buX3cJzR.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-C_k4R5n6.js";
|
|
3
|
+
import { n as require_shim, t as require_react_dom } from "./assets/react-dom-CHRRsKwb.js";
|
|
4
4
|
import minpath from "node:path";
|
|
5
5
|
import minproc from "node:process";
|
|
6
6
|
import { fileURLToPath as urlToPath } from "node:url";
|
|
@@ -46080,15 +46080,35 @@ function tokenTargetPayload(tokenName) {
|
|
|
46080
46080
|
//#endregion
|
|
46081
46081
|
//#region src/components/qgrid/ChatWidget.tsx
|
|
46082
46082
|
var CHAT_PROJECT_NAME = "qgrid_chat";
|
|
46083
|
-
var DEFAULT_MODEL = "
|
|
46084
|
-
var
|
|
46085
|
-
"
|
|
46086
|
-
|
|
46087
|
-
|
|
46088
|
-
|
|
46089
|
-
|
|
46090
|
-
|
|
46091
|
-
|
|
46083
|
+
var DEFAULT_MODEL = "anthropic/claude-fable-5-1";
|
|
46084
|
+
var MODEL_PRESET_GROUPS = [{
|
|
46085
|
+
label: "OpenAI",
|
|
46086
|
+
models: [
|
|
46087
|
+
"openai/gpt-5.6-terra",
|
|
46088
|
+
"openai/gpt-5.6-sol",
|
|
46089
|
+
"openai/gpt-5.6-luna",
|
|
46090
|
+
"openai/gpt-5.5",
|
|
46091
|
+
"openai/gpt-5.3-codex-spark"
|
|
46092
|
+
]
|
|
46093
|
+
}, {
|
|
46094
|
+
label: "Anthropic",
|
|
46095
|
+
models: [
|
|
46096
|
+
"anthropic/claude-fable-5-1",
|
|
46097
|
+
"anthropic/claude-fable-5",
|
|
46098
|
+
"anthropic/claude-opus-5",
|
|
46099
|
+
"anthropic/claude-sonnet-5",
|
|
46100
|
+
"anthropic/claude-opus-4-8",
|
|
46101
|
+
"anthropic/claude-opus-4-7",
|
|
46102
|
+
"anthropic/claude-opus-4-6",
|
|
46103
|
+
"anthropic/claude-opus-4-5",
|
|
46104
|
+
"anthropic/claude-sonnet-4-6",
|
|
46105
|
+
"anthropic/claude-sonnet-4-5",
|
|
46106
|
+
"anthropic/claude-haiku-4-5",
|
|
46107
|
+
"anthropic/claude-opus-4-1",
|
|
46108
|
+
"anthropic/claude-opus-4",
|
|
46109
|
+
"anthropic/claude-sonnet-4"
|
|
46110
|
+
]
|
|
46111
|
+
}];
|
|
46092
46112
|
var EFFORT_PRESETS = [
|
|
46093
46113
|
"low",
|
|
46094
46114
|
"medium",
|
|
@@ -46385,10 +46405,13 @@ function ChatWidget() {
|
|
|
46385
46405
|
onChange: (e) => setModel(e.target.value),
|
|
46386
46406
|
disabled: busy,
|
|
46387
46407
|
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:
|
|
46389
|
-
|
|
46390
|
-
children: m
|
|
46391
|
-
|
|
46408
|
+
children: MODEL_PRESET_GROUPS.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("optgroup", {
|
|
46409
|
+
label: group.label,
|
|
46410
|
+
children: group.models.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
46411
|
+
value: m,
|
|
46412
|
+
children: m
|
|
46413
|
+
}, m))
|
|
46414
|
+
}, group.label))
|
|
46392
46415
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
46393
46416
|
value: effort,
|
|
46394
46417
|
onChange: (e) => setEffort(e.target.value),
|
|
@@ -46625,7 +46648,7 @@ function ChatWidget() {
|
|
|
46625
46648
|
}
|
|
46626
46649
|
//#endregion
|
|
46627
46650
|
//#region src/components/Sidebar.tsx
|
|
46628
|
-
var qgridVersion = "2.
|
|
46651
|
+
var qgridVersion = "2.9.0";
|
|
46629
46652
|
function Sidebar({ className }) {
|
|
46630
46653
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
46631
46654
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
|
|
@@ -68510,15 +68533,15 @@ function RootComponent() {
|
|
|
68510
68533
|
}
|
|
68511
68534
|
//#endregion
|
|
68512
68535
|
//#region src/routes/tokens.tsx
|
|
68513
|
-
var $$splitComponentImporter$5 = () => import("./assets/tokens-
|
|
68536
|
+
var $$splitComponentImporter$5 = () => import("./assets/tokens-C32lycTe.js");
|
|
68514
68537
|
var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
|
|
68515
68538
|
//#endregion
|
|
68516
68539
|
//#region src/routes/settings.tsx
|
|
68517
|
-
var $$splitComponentImporter$4 = () => import("./assets/settings-
|
|
68540
|
+
var $$splitComponentImporter$4 = () => import("./assets/settings-CFoXQaAg.js");
|
|
68518
68541
|
var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
|
|
68519
68542
|
//#endregion
|
|
68520
68543
|
//#region src/routes/monit.tsx
|
|
68521
|
-
var $$splitComponentImporter$3 = () => import("./assets/monit-
|
|
68544
|
+
var $$splitComponentImporter$3 = () => import("./assets/monit-BW2X1D23.js");
|
|
68522
68545
|
var monitSearchSchema = object({ level: _enum([
|
|
68523
68546
|
"all",
|
|
68524
68547
|
"warn",
|
|
@@ -68530,7 +68553,7 @@ var Route$3 = createFileRoute("/monit")({
|
|
|
68530
68553
|
});
|
|
68531
68554
|
//#endregion
|
|
68532
68555
|
//#region src/routes/logs.tsx
|
|
68533
|
-
var $$splitComponentImporter$2 = () => import("./assets/logs-
|
|
68556
|
+
var $$splitComponentImporter$2 = () => import("./assets/logs-lBOU5eUP.js");
|
|
68534
68557
|
var logsSearchSchema = object({
|
|
68535
68558
|
token: string$2().optional(),
|
|
68536
68559
|
project: string$2().optional(),
|
|
@@ -68544,11 +68567,11 @@ var Route$2 = createFileRoute("/logs")({
|
|
|
68544
68567
|
});
|
|
68545
68568
|
//#endregion
|
|
68546
68569
|
//#region src/routes/index.tsx
|
|
68547
|
-
var $$splitComponentImporter$1 = () => import("./assets/routes-
|
|
68570
|
+
var $$splitComponentImporter$1 = () => import("./assets/routes-CxMHXfdH.js");
|
|
68548
68571
|
var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
|
|
68549
68572
|
//#endregion
|
|
68550
68573
|
//#region src/routes/requests/show.tsx
|
|
68551
|
-
var $$splitComponentImporter = () => import("./assets/show-
|
|
68574
|
+
var $$splitComponentImporter = () => import("./assets/show-CcZJO6WF.js");
|
|
68552
68575
|
var showSearchSchema = object({ id: number$1() });
|
|
68553
68576
|
var Route = createFileRoute("/requests/show")({
|
|
68554
68577
|
validateSearch: showSearchSchema,
|
package/package.json
CHANGED
package/skills/qgrid/SKILL.md
CHANGED
|
@@ -5,6 +5,11 @@ description: Work on the qgrid repository, an LLM subscription-token proxy with
|
|
|
5
5
|
|
|
6
6
|
# Qgrid
|
|
7
7
|
|
|
8
|
+
## Command Scope
|
|
9
|
+
|
|
10
|
+
Run the repository-specific `mise` commands in this skill only from the qgrid source
|
|
11
|
+
repository. In a downstream project, use that project's configured task and tool runner.
|
|
12
|
+
|
|
8
13
|
## Start Here
|
|
9
14
|
|
|
10
15
|
Use this skill for qgrid repository work. Treat qgrid as a runtime-contract-heavy system: model input travels through the active AI SDK provider, Sonamu API, provider dispatcher, provider-specific runtime, response mapping, request logging, and dashboard display.
|
|
@@ -42,10 +47,10 @@ Request logging is enabled by default. Use `providerOptions.qgrid.logger: false`
|
|
|
42
47
|
- The old v1 SDK package (`packages/sdk`, `@cartanova/qgrid-sdk`) has been removed from the repository. It survives only as a deprecated npm artifact (1.9.0); do not resurrect it or write new code against it.
|
|
43
48
|
- Let the server infer single-turn versus tool-run request-log lifecycle. Do not recreate caller-selected logging modes in the SDK or API examples.
|
|
44
49
|
- Route provider models by prefix: `openai/*` goes to the OpenAI Codex runtime, `anthropic/*` goes to the Anthropic Claude Code runtime. Prefix-less model fallback is not implemented.
|
|
45
|
-
- Treat Fable 5 safety fallback as upstream Claude Code behavior, not qgrid routing: a classifier refusal can retry on Opus 4.8. Do not add a second qgrid retry. Preserve requested Fable versus actual serving Opus, fallback history, and provider-reported cost; a fresh Claude Code process means the fallback is not sticky across qgrid requests.
|
|
50
|
+
- Treat Fable (5 and 5.1) safety fallback as upstream Claude Code behavior, not qgrid routing: a classifier refusal can retry on Opus 5 or Opus 4.8 depending on refusal category. Do not add a second qgrid retry. Preserve requested Fable versus actual serving Opus, fallback history, and provider-reported cost; a fresh Claude Code process means the fallback is not sticky across qgrid requests.
|
|
46
51
|
- Treat dashboard work as Sonamu API/model/generated-client/web work, not isolated frontend work.
|
|
47
52
|
- Prefer `getPuri()` for Sonamu model queries. Treat `getDB()` as a legacy escape hatch only when Puri cannot express the required query. Inside `@transactional` methods, all participating queries must use `getPuri()`; `getDB()` does not reuse Sonamu's ambient transaction connection.
|
|
48
|
-
- Never hand-author migration files for Sonamu-managed schema.
|
|
53
|
+
- Never hand-author migration files for Sonamu-managed schema. In the qgrid source repository, change the entity definition, inspect `sonamu migrate status`, and create migration files with `mise exec -- pnpm --dir packages/api sonamu migrate generate`. Inspect the generated files before applying them. If Sonamu cannot express a required schema change, stop and ask rather than silently replacing its workflow with a custom migration.
|
|
49
54
|
- Keep OpenAI Codex built-in tools, apps, plugins, skills, web search, shell, and environment instruction blocks disabled unless the user explicitly asks for agentic Codex behavior.
|
|
50
55
|
- Treat OpenAI image generation as an opt-in Codex `image_generation` tool path. Inspect current code before modifying it; it is not a direct Images API call and its image cost is an estimate.
|
|
51
56
|
|
|
@@ -53,7 +58,7 @@ Request logging is enabled by default. Use `providerOptions.qgrid.logger: false`
|
|
|
53
58
|
|
|
54
59
|
Choose verification by blast radius:
|
|
55
60
|
|
|
56
|
-
-
|
|
61
|
+
- In the qgrid source repository, run type/lint/format with `mise run check`.
|
|
57
62
|
- Package tests: use the relevant package test command or targeted Vitest files.
|
|
58
63
|
- Provider runtime changes: run targeted tests for `packages/api/src/utils/providers/**` and, when practical, the relevant smoke script in `scripts/` or `packages/api/scripts/`.
|
|
59
64
|
- AI SDK changes: run `packages/ai-sdk` tests and inspect generated request payload behavior.
|
|
@@ -57,6 +57,10 @@ Do not write `providerOptions: { ... } satisfies QgridProviderOptions`: the expo
|
|
|
57
57
|
|
|
58
58
|
`providerOptions.qgrid` supports:
|
|
59
59
|
|
|
60
|
+
- `tokenName`: strict active-token targeting for both providers. Include the provider prefix and
|
|
61
|
+
match it to the model (`anthropic/yds` with `anthropic/*`, for example). Missing, inactive, or
|
|
62
|
+
over-threshold targets fail without falling back to another token. An explicitly empty value is
|
|
63
|
+
rejected by the SDK before transport; omission keeps weighted round-robin routing.
|
|
60
64
|
- `logger`: request-log switch. Omitted or `true` is the default and enables qgrid logging; `false` guarantees that generation creates zero qgrid request-log rows.
|
|
61
65
|
- `sessionKey`: source for model-scoped opaque OpenAI prompt-cache affinity. Disabled for Anthropic models.
|
|
62
66
|
- `effort`
|
|
@@ -143,6 +147,7 @@ Payload responsibilities:
|
|
|
143
147
|
- Send `jsonSchema` whenever the response format top-level schema is `object`, including requests that also contain tools.
|
|
144
148
|
- Send `history` as JSON string when prior messages exist.
|
|
145
149
|
- Send `projectName` when configured.
|
|
150
|
+
- Send `tokenName` when configured so generate and stream use the same strict target.
|
|
146
151
|
- Send `logger: false` when the per-call option disables request logging; otherwise rely on the server default.
|
|
147
152
|
- Preserve and resend `runContext` for tool-call follow-ups.
|
|
148
153
|
- Let the server infer single-turn completion versus an open tool-call run. Logging-disabled tool calls still use the SDK's local pending-call correlation and continue without a request-log id.
|
|
@@ -208,10 +213,10 @@ For tools plus structured output, qgrid serializes the final schema-constrained
|
|
|
208
213
|
|
|
209
214
|
When `imageGeneration` was requested and the server returns no image part, the AI SDK provider throws a version-skew/error guard instead of silently accepting text-only output.
|
|
210
215
|
|
|
211
|
-
For a successful Fable 5
|
|
216
|
+
For a successful Fable refusal fallback (Fable 5 or 5.1):
|
|
212
217
|
|
|
213
|
-
- `response.modelId` and `providerMetadata.qgrid.model` are the actual serving model,
|
|
214
|
-
- `providerMetadata.qgrid.requestedModel` remains `claude-fable-5`.
|
|
218
|
+
- `response.modelId` and `providerMetadata.qgrid.model` are the actual serving model, an Opus model such as `claude-opus-5` or `claude-opus-4-8`.
|
|
219
|
+
- `providerMetadata.qgrid.requestedModel` remains the requested Fable model, for example `claude-fable-5-1`.
|
|
215
220
|
- `providerMetadata.qgrid.modelFallbacks` preserves the refusal route and optional category/explanation.
|
|
216
221
|
- `providerMetadata.qgrid.costSource` reports whether cost came from Claude Code or qgrid's pricing table. Prefer the provider-reported combined cost for this path.
|
|
217
222
|
|
|
@@ -44,7 +44,12 @@ Anthropic tokens live in an in-memory `Map<tokenId, PooledToken>`.
|
|
|
44
44
|
|
|
45
45
|
Quota threshold:
|
|
46
46
|
|
|
47
|
-
- `quota_threshold`
|
|
47
|
+
- `quota_threshold` checks `five_hour.utilization` for every model and also checks
|
|
48
|
+
`seven_day_overage_included.utilization` for the Fable family (Fable 5 and 5.1,
|
|
49
|
+
`isFableFamilyModel` in `anthropic-constants.ts`), using the higher utilization.
|
|
50
|
+
- A runtime quota-exhausted result invalidates every 60-second usage-cache key used before and
|
|
51
|
+
after a possible OAuth refresh, so the next untargeted request rechecks usage and can select
|
|
52
|
+
another token.
|
|
48
53
|
- Lookup failure is fail-open.
|
|
49
54
|
- If all eligible tokens exceed threshold, throw `QuotaThresholdExceededError`.
|
|
50
55
|
|
|
@@ -72,7 +77,7 @@ claude -p
|
|
|
72
77
|
--model <canonical-model-or-1m-suffix>
|
|
73
78
|
--system-prompt <text> # small system prompt
|
|
74
79
|
--system-prompt-file <path> # large system prompt
|
|
75
|
-
--thinking disabled # omitted for Fable 5 and Opus 5
|
|
80
|
+
--thinking disabled # omitted for Fable 5/5.1 and Opus 5
|
|
76
81
|
--effort <effort-or-low>
|
|
77
82
|
--disable-slash-commands
|
|
78
83
|
--session-id <uuid>
|
|
@@ -92,7 +97,7 @@ Important details:
|
|
|
92
97
|
- Large system prompts over 64 KiB are written to a temporary file to avoid argv `E2BIG` — this
|
|
93
98
|
same branch absorbs large injected schema contracts, so the old 64 KiB schema argv limit no
|
|
94
99
|
longer applies (only the global 512 KiB caller-schema complexity limit remains).
|
|
95
|
-
- `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off for existing models. Fable 5
|
|
100
|
+
- `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off for existing models. Fable 5 and Fable 5.1 require always-on adaptive thinking (CLI catalog `rejects_disabled_thinking`). Opus 5 defaults to adaptive thinking and rejects disabled thinking at `xhigh`/`max` effort. qgrid omits all three suppressors for these models (`usesAdaptiveThinking`) and uses `effort` to control depth.
|
|
96
101
|
|
|
97
102
|
## Spawn env
|
|
98
103
|
|
|
@@ -105,13 +110,13 @@ Included env:
|
|
|
105
110
|
- `CLAUDE_CODE_OAUTH_TOKEN`
|
|
106
111
|
- `CLAUDE_CONFIG_DIR`
|
|
107
112
|
- `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`
|
|
108
|
-
- `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` except for Fable 5 and Opus 5
|
|
113
|
+
- `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` except for Fable 5/5.1 and Opus 5
|
|
109
114
|
- `CLAUDE_CODE_DISABLE_CLAUDE_MDS=1`
|
|
110
115
|
- `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`
|
|
111
116
|
- `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS=1`
|
|
112
117
|
- `CLAUDE_CODE_DISABLE_WORKFLOWS=1`
|
|
113
118
|
- `CLAUDE_CODE_ATTRIBUTION_HEADER=0`
|
|
114
|
-
- `MAX_THINKING_TOKENS=0` except for Fable 5 and Opus 5
|
|
119
|
+
- `MAX_THINKING_TOKENS=0` except for Fable 5/5.1 and Opus 5
|
|
115
120
|
- `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` when model does not support qgrid's 1M path
|
|
116
121
|
- `MAX_STRUCTURED_OUTPUT_RETRIES` for streaming structured output only (not non-streaming `generate`)
|
|
117
122
|
|
|
@@ -180,11 +185,13 @@ cacheCreationInputTokens1h = cache_creation.ephemeral_1h_input_tokens
|
|
|
180
185
|
|
|
181
186
|
## Fable refusal fallback
|
|
182
187
|
|
|
183
|
-
Fable 5
|
|
188
|
+
Fable 5 and Fable 5.1 run upstream safety classifiers. A refusal is a successful HTTP response with
|
|
184
189
|
`stop_reason: "refusal"`, not an HTTP/provider error, and can happen before output or after
|
|
185
|
-
partial streamed output. Claude Code handles the refusal fallback and can retry on Opus
|
|
186
|
-
|
|
187
|
-
|
|
190
|
+
partial streamed output. Claude Code handles the refusal fallback and can retry on another Opus
|
|
191
|
+
model; the current CLI routes by refusal category (bio → Opus 5, cyber → Opus 4.8). Do not
|
|
192
|
+
hardcode the fallback target in qgrid: read it from the stream evidence below. This is distinct
|
|
193
|
+
from the CLI `--fallback-model` flag, which is for overload on the default model. qgrid does not
|
|
194
|
+
configure either mechanism or add another retry.
|
|
188
195
|
|
|
189
196
|
The stream adapter must preserve all observable routing evidence:
|
|
190
197
|
|
|
@@ -247,6 +254,7 @@ Model normalization strips provider prefix and `[1m]` for canonical model/cost k
|
|
|
247
254
|
|
|
248
255
|
qgrid's exact 1M support set currently includes:
|
|
249
256
|
|
|
257
|
+
- `claude-fable-5-1`
|
|
250
258
|
- `claude-fable-5`
|
|
251
259
|
- `claude-opus-5`
|
|
252
260
|
- `claude-sonnet-5`
|
|
@@ -66,7 +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`
|
|
69
|
+
| `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED` | off | Marks this instance as the Anthropic 5-hour window keepalive runner (2.8.0, `ea7feb3`). Only the literal `true` enables it; set it on exactly one instance per shared DB so developer instances do not fire duplicates. Two more gates apply: the global setting `qgrid.tokenWindowKeepaliveEnabled` (`false` turns everything off; changing it reschedules immediately) and the per-token `tokens.keepalive_enabled` flag (default `false`, dashboard settings-panel toggle). In 2.7.6 this env was a default-on global switch with no per-token opt-in. |
|
|
70
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. |
|
|
71
71
|
|
|
72
72
|
## Server boot lifecycle
|
|
@@ -33,7 +33,7 @@ Key decisions:
|
|
|
33
33
|
- AI SDK's standard `timeout` owns the client-side total budget and reaches custom providers only as an `AbortSignal`. `providerOptions.qgrid.timeoutMs` separately owns the Anthropic server-side Claude process timer. For non-stream calls, the qgrid SDK may derive its own request-scoped Undici header/body budget from that explicit qgrid value (`timeoutMs + 60s`), but must not mutate the global dispatcher or pretend the original AI SDK timeout number is available.
|
|
34
34
|
- `projectName` is provider config/payload camelCase. `project_name` is the Sonamu/database request-log column. Prefer `QGRID_PROJECT_NAME` as the project-wide default so request logs and metrics remain filterable at volume.
|
|
35
35
|
- `fallbackModels` may exist as a typed future option, but it is not a server wire contract until the server implements fallback routing. Do not forward it as behavior just because the type exists.
|
|
36
|
-
- Fable
|
|
36
|
+
- Fable refusal fallback is an upstream Claude Code safety path to another Opus model (currently Opus 5 or Opus 4.8 by refusal category), not qgrid's reserved `fallbackModels` feature or Claude Code's overload-only `--fallback-model` flag. qgrid observes and reports the actual route and provider cost; it must not retry again or hardcode the target.
|
|
37
37
|
- The SDK should stay a thin adapter. Server APIs own request-log lifecycle, provider dispatch, structured-output emulation, and provider runtime details.
|
|
38
38
|
- Request logging defaults to enabled. `providerOptions.qgrid.logger: false` is the single per-generation opt-out for both native qgrid logs and `createQgridLogger` telemetry logs; it must not alter generation or tool behavior.
|
|
39
39
|
|
|
@@ -140,8 +140,11 @@ Key decisions:
|
|
|
140
140
|
- The token subscriber disables statement/idle timeouts for `LISTEN` and uses reconnect backoff.
|
|
141
141
|
- `quota_threshold` is a routing guardrail, not a load balancer. It prevents an individual token from crossing a configured utilization percentage; it does not equalize traffic.
|
|
142
142
|
- Threshold values are nullable integers from 1 to 100. `0` is not valid; `100` means exclude only at full utilization.
|
|
143
|
-
- Provider criteria differ by primary window: Anthropic uses `five_hour.utilization
|
|
143
|
+
- Provider criteria differ by primary window: Anthropic uses `five_hour.utilization` plus the
|
|
144
|
+
Fable-only `seven_day_overage_included.utilization` when applicable; OpenAI uses
|
|
145
|
+
`primary.usedPercent`.
|
|
144
146
|
- Threshold gates are by token id, not token name. Names are display/logging labels and can change.
|
|
147
|
+
- Token window keepalive is opt-in at two levels since 2.8.0 (`ea7feb3`): an instance fires only with env `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED=true`, and only for tokens with `keepalive_enabled = true`. The runner gate exists because developer instances sharing the production DB fired duplicate pings under the 2.7.6 default-on switch; the per-token flag lets shared or credit-metered accounts stay out. The cost is that upgrades start with zero targets until both are set.
|
|
145
148
|
- Lookup failure is fail-open with logs/metrics. Hard failure happens only when usage was read successfully and the token is over threshold.
|
|
146
149
|
- OpenAI applies the gate during token selection. An affinity-preferred token over threshold must fall back to another eligible token when possible.
|
|
147
150
|
|
|
@@ -35,15 +35,15 @@ When a repo doc establishes or changes durable rationale for a qgrid feature, re
|
|
|
35
35
|
|
|
36
36
|
`packages/cli/skills/qgrid` is the canonical qgrid skill. The CLI package ships this directory to consuming projects. The repository-local `.agents/skills/qgrid` directory is a checked mirror for agents working in this repository; do not edit the mirror as a separate source.
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
- In the qgrid source repository, run `mise run qgrid-docs:sync` after changing the canonical skill.
|
|
39
|
+
- In the qgrid source repository, run `mise run qgrid-docs:check` before finishing documentation or runtime-contract work.
|
|
40
40
|
- `docs/qgrid-doc-map.json` maps each durable topic to code, canonical skill references, and the matching Notion page.
|
|
41
41
|
- Code and tests remain the source of truth. Update the skill and Notion together when a code change alters a documented contract.
|
|
42
42
|
- Keep dated operational snapshots, internal server addresses, and other environment-specific facts in Notion. Keep reusable runtime contracts in the skill.
|
|
43
43
|
|
|
44
44
|
## Retract pre-release caveats after shipping
|
|
45
45
|
|
|
46
|
-
Documenting a contract before it ships is fine, but the caveat must be retracted once it does. Wording like "not deployed yet", "based on the working tree", "migration is still untracked", or "package version is still X" becomes a false statement the moment the release lands, and `
|
|
46
|
+
Documenting a contract before it ships is fine, but the caveat must be retracted once it does. Wording like "not deployed yet", "based on the working tree", "migration is still untracked", or "package version is still X" becomes a false statement the moment the release lands, and `mise run qgrid-docs:check` will not catch it — that task validates the skill mirror and `docs/qgrid-doc-map.json` paths only, never Notion body text.
|
|
47
47
|
|
|
48
48
|
- When you add a pre-release caveat, record the retraction in the release page at the same time. Do not rely on memory.
|
|
49
49
|
- After a release, search the wiki for `미배포`, `배포되지 않았`, `working tree`, `untracked`, `아직`, and the previous version number.
|
|
@@ -48,7 +48,7 @@ Tool calls are produced through qgrid structured-output emulation, not native pr
|
|
|
48
48
|
|
|
49
49
|
New request logs store routed model identity in the existing model fields; they do not add a separate provider column:
|
|
50
50
|
|
|
51
|
-
- While the parent is running, `requested_model_name` is the full requested id such as `openai/gpt-5.
|
|
51
|
+
- While the parent is running, `requested_model_name` is the full requested id such as `openai/gpt-5.6-terra` and `model_name` is `NULL`.
|
|
52
52
|
- Completed parent and generate-step model values use full `provider/model` ids.
|
|
53
53
|
- `requested_model_name` preserves the exact requested route, including modifiers such as Anthropic `[1m]`; `model_name` is the actual canonical serving route. They differ after a Fable refusal fallback, for example requested Fable versus serving Opus.
|
|
54
54
|
- A completed multi-step parent uses the final turn's requested and serving models. It never stores the literal model value `mixed`; usage and cost still aggregate across steps, and `cost_source` may be `mixed`.
|
|
@@ -26,10 +26,12 @@ qgrid dashboard work is usually a Sonamu API/model/generated-client/web change,
|
|
|
26
26
|
## Migration workflow
|
|
27
27
|
|
|
28
28
|
Sonamu entity definitions are the source of truth for managed database schema.
|
|
29
|
+
These commands apply only inside the qgrid source repository. Downstream projects must use
|
|
30
|
+
their own configured task and tool runner.
|
|
29
31
|
|
|
30
32
|
1. Change the relevant `*.entity.json` files.
|
|
31
|
-
2. Run `pnpm --dir packages/api sonamu migrate status` and inspect `preparedCodes`.
|
|
32
|
-
3. Run `pnpm --dir packages/api sonamu migrate generate`.
|
|
33
|
+
2. Run `mise exec -- pnpm --dir packages/api sonamu migrate status` and inspect `preparedCodes`.
|
|
34
|
+
3. Run `mise exec -- pnpm --dir packages/api sonamu migrate generate`.
|
|
33
35
|
4. Inspect the generated, table-scoped migration files without rewriting them by hand.
|
|
34
36
|
5. Apply them through Sonamu's migration target flow or qgrid's startup migration runner.
|
|
35
37
|
6. Re-run migration status and require `pending: []` and `preparedCodes: []`.
|
|
@@ -62,6 +64,8 @@ Tool contract visibility: the detail page renders a Tools panel between Response
|
|
|
62
64
|
|
|
63
65
|
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
66
|
|
|
67
|
+
Chat model presets: the model `<select>` lists the served subset of `QgridSupportedModel` grouped by provider (`MODEL_PRESET_GROUPS` in `ChatWidget.tsx`), newest first, and defaults to `anthropic/claude-fable-5-1`. Ids that the ChatGPT-subscription Codex route no longer serves (`gpt-5.4` and `gpt-5.4-mini` retired 2026-08-31 with `gpt-5.6-terra` / `gpt-5.6-luna` as replacements; `gpt-5.2` and `gpt-5.3-codex` removed earlier) and the catalog-less `claude-sonnet-4-7` stay in the SDK type but are excluded here. web does not depend on ai-sdk, so a new model must be added in both places.
|
|
68
|
+
|
|
65
69
|
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.
|
|
66
70
|
|
|
67
71
|
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.
|
|
@@ -34,11 +34,13 @@ Use this reference before changing token storage, OAuth flows, token activation,
|
|
|
34
34
|
- `credentials`: JSONB credentials.
|
|
35
35
|
- `name`: display/logging name.
|
|
36
36
|
- `active`: whether provider dispatchers may use it.
|
|
37
|
+
- `reauth_required`: whether a permanent refresh failure proved that OAuth login must be repeated.
|
|
37
38
|
- `ord`: dashboard ordering.
|
|
38
39
|
- `quota_threshold`: nullable integer percentage, validated as 1..100 when present.
|
|
39
40
|
- `weight`: non-null integer 1..100, database default 1. Relative share for weighted round-robin routing of new requests. Weight does not enable or disable a token; that stays on `active`.
|
|
40
41
|
|
|
41
|
-
On creation, `TokenModel.save` applies
|
|
42
|
+
On creation, the database defaults `reauth_required = false`; `TokenModel.save` also applies `quota_threshold = 80` and `weight = 1` independently for any field not provided (skipped entirely when `id` is present).
|
|
43
|
+
`TokenModel.save` is internal only; callers use the curated qgrid token APIs so they cannot write lifecycle fields directly.
|
|
42
44
|
|
|
43
45
|
OpenAI credentials:
|
|
44
46
|
|
|
@@ -77,7 +79,7 @@ Duplicate account replacement is account-identifier based:
|
|
|
77
79
|
- `addToken(provider, credentials, name)`: saves a token directly.
|
|
78
80
|
- `updateToken(id, name?, quotaThreshold?, weight?)`: partial field update through `TokenModel.updateFields`; omitted fields preserve stored values, so one control cannot overwrite another control's setting. `weight` is validated as an integer 1..100. The web client uses a hand-written `useUpdateTokenMutation` hook instead of the generated tanstack-mutation client for this endpoint.
|
|
79
81
|
- `removeToken(id)`: deletes token row.
|
|
80
|
-
- `toggleToken(id)`: flips `active
|
|
82
|
+
- `toggleToken(id)`: atomically flips `active`; it refuses to reactivate a token with `reauth_required = true`, because only OAuth can restore a dead login.
|
|
81
83
|
- `stats()`: reports dispatcher cache stats.
|
|
82
84
|
- `health()`: reports active token cache count and subscriber status.
|
|
83
85
|
- `usage(tokenId?)`: returns provider usage/rate-limit summary.
|
|
@@ -113,7 +115,8 @@ Usage/quota:
|
|
|
113
115
|
|
|
114
116
|
- Usage API is `https://api.anthropic.com/api/oauth/usage`.
|
|
115
117
|
- qgrid caches Anthropic usage by access-token suffix for 60 seconds.
|
|
116
|
-
- Quota threshold uses `five_hour.utilization
|
|
118
|
+
- Quota threshold uses `five_hour.utilization`; Fable family models (Fable 5 and 5.1) additionally
|
|
119
|
+
use the usage-credit `seven_day_overage_included.utilization`, and either window can exclude the token.
|
|
117
120
|
- Usage lookup failure is fail-open for routing.
|
|
118
121
|
|
|
119
122
|
## OpenAI OAuth And Refresh Flow
|
|
@@ -153,7 +156,7 @@ Triggers notify on:
|
|
|
153
156
|
|
|
154
157
|
- INSERT
|
|
155
158
|
- DELETE
|
|
156
|
-
- UPDATE when `active`, `credentials`, `provider`, `name`, or `quota_threshold` changes.
|
|
159
|
+
- UPDATE when `active`, `reauth_required`, `credentials`, `provider`, `name`, or `quota_threshold` changes.
|
|
157
160
|
- UPDATE when `weight` changes, through a separate `tokens_weight_changed_upd` trigger owned by the versioned migration `20260710090000_alter_tokens_add_weight.ts`. The boot-time setup SQL intentionally leaves `weight` out of its WHEN clause (test-enforced in `token-trigger-setup.test.ts`) so exactly one trigger fires per weight-only change.
|
|
158
161
|
|
|
159
162
|
`TokenSubscriber`:
|
|
@@ -200,14 +203,15 @@ Anthropic token changes only affect the in-memory token pool because every reque
|
|
|
200
203
|
When a refresh fails in a way that only re-login can fix, qgrid removes the token from routing instead of letting every subsequent request 401.
|
|
201
204
|
|
|
202
205
|
- Detection lives at the existing refresh failure points, not a background scanner. OpenAI classifies on the error code (`refresh_token_expired`, `refresh_token_reused`, `refresh_token_invalidated`) regardless of HTTP status; Anthropic treats refresh 400/401 as auth-dead.
|
|
203
|
-
- Before deactivating, qgrid
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
+
- Before deactivating, qgrid locks the token row and confirms the full credentials used by the failed attempt still match the stored credentials. A successful concurrent refresh therefore wins without a stale failure killing the rotated login. `QgridFrame.refreshToken` also dedups concurrent refreshes per token.
|
|
207
|
+
- Auth death is one conditional `reauth_required=false → true` transaction. Provider-scoped advisory locking serializes the last-active decision, and the update also sets `active=false` when another active token exists. The process whose update affects a row is the only one that sends the Slack death notification, so a shared database produces exactly one alert per event. Manual active toggles never set `reauth_required`, and an expired inactive token cannot be reactivated without completing OAuth.
|
|
208
|
+
- A successful refresh clears a concurrent stale `reauth_required` mark but preserves `active`, so it never overrides an operator's manual inactive choice.
|
|
209
|
+
- The provider's last active token is marked `reauth_required=true` but never auto-deactivated. A systemic failure (client_id revocation, OAuth contract change) would otherwise empty the pool; qgrid logs an error, keeps the token, and sends its own urgent Slack alert. The persisted flag gates repeat alerts across every instance.
|
|
206
210
|
- The other notification is registration: every completed login sends one alert, whether the account is new or replacing an existing row through the account dedup. Manually toggling a token back to active is not a registration and sends nothing.
|
|
207
|
-
- Quiet hours suppress non-urgent Slack notifications: weekends, plus 20:00–08:00 on weekdays, in `Asia/Seoul`. All three parts are settings (`slack.quietFromHour`, `slack.quietUntilHour`, `slack.notifyOnWeekends`), defaulting to that window. The hour is computed through `Intl` with an explicit timezone rather than `getHours()` — dev0 runs in UTC, where "20:00" would land at 05:00 Korean time. Expiry alerts repeat, so one dropped in quiet hours returns on the first cycle of the next working window. The last-active-token death alert sets `urgent: true` and ignores quiet hours: it means
|
|
211
|
+
- Quiet hours suppress non-urgent Slack notifications: weekends, plus 20:00–08:00 on weekdays, in `Asia/Seoul`. All three parts are settings (`slack.quietFromHour`, `slack.quietUntilHour`, `slack.notifyOnWeekends`), defaulting to that window. The hour is computed through `Intl` with an explicit timezone rather than `getHours()` — dev0 runs in UTC, where "20:00" would land at 05:00 Korean time. Expiry alerts repeat, so one dropped in quiet hours returns on the first cycle of the next working window. The last-active-token death alert sets `urgent: true` and ignores quiet hours: it means that provider has no usable login, which cannot wait for the next working day.
|
|
208
212
|
- `slack.enabled` exists because holidays cannot be expressed as a recurring rule — substitute holidays, one-off national days, and company closures change every year, so an operator turns notifications off directly instead. It gates the same non-urgent path as quiet hours, which keeps the escape hatch for pool-empty alerts identical in both mechanisms.
|
|
209
213
|
- Notifications are fail-open: unset `SLACK_BOT_TOKEN`/`SLACK_CHANNEL_ID` is a silent no-op, and send failures (including HTTP 200 with `ok:false`) only warn. Payloads carry an internal reason code such as `anthropic:400`, never the raw provider response body.
|
|
210
|
-
- `expired-token-reminder.ts` repeats the session-expiry alert for tokens
|
|
214
|
+
- `expired-token-reminder.ts` repeats the session-expiry alert only for tokens with `reauth_required = true`; manually inactive tokens are not expired sessions. It reuses the same `세션 만료` title rather than inventing a second name — there is one event, so a differently-named alert would read as a separate problem. It runs once at startup and then every `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` (unset or 0 disables), batches every expired token into one message, and sends nothing when none require re-login. The startup run matters because a restart is exactly when someone is watching, and waiting a full interval to learn a token is already dead is backwards; the cost is one extra alert per restart — a periodic "all clear" trains people to ignore the channel.
|
|
211
215
|
- Owners are mentioned through `SLACK_USER_MAP` (`tokenName:SlackUserId`) on every token notification, not just the reminder. The map is explicit rather than resolved through `users.list`: Slack handles diverge from token names often enough (`haze`→`haze.lee`, `byeongjun`→`potados`) that auto-matching drops the mention silently, and an alert nobody is tagged in has no one to act on it. Shared accounts are left out of the map on purpose and appear by name only. Each instance sends its own reminder — unlike deactivation there is no state change to gate on, and a few duplicates a day cost less than a locking scheme.
|
|
212
216
|
|
|
213
217
|
## Quota Threshold Semantics
|
|
@@ -234,18 +238,21 @@ When a refresh fails in a way that only re-login can fix, qgrid removes the toke
|
|
|
234
238
|
- 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.
|
|
235
239
|
- 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.
|
|
236
240
|
- 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.
|
|
237
|
-
- Exposure boundary:
|
|
241
|
+
- Exposure boundary: raw qgrid callers and AI SDK consumers can pass `tokenName`; the AI SDK sends
|
|
242
|
+
it from `providerOptions.qgrid.tokenName` on both generate and stream. Callers that omit it keep
|
|
243
|
+
weighted round-robin routing, while an explicitly empty SDK value fails before transport.
|
|
238
244
|
|
|
239
245
|
## Token Window Keepalive
|
|
240
246
|
|
|
241
247
|
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.
|
|
242
248
|
|
|
243
|
-
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.
|
|
249
|
+
Keepalive breaks that coupling by burning the window clock during idle time. Every keepalive-enabled 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.
|
|
244
250
|
|
|
245
|
-
-
|
|
251
|
+
- Since 2.8.0 (`ea7feb3`) keepalive is opt-in at two levels. The runner instance must have env `QGRID_TOKEN_WINDOW_KEEPALIVE_ENABLED=true` (default off; set it on one instance per shared DB), and a token is a target only when `tokens.keepalive_enabled` is `true` (default `false`, toggled in the dashboard settings panel). The global setting `qgrid.tokenWindowKeepaliveEnabled` is a kill switch (`false` disables everything), not a substitute for either gate. In 2.7.6 every instance fired for every active token.
|
|
252
|
+
- Operational trap: the 2.8.0 migration adds `keepalive_enabled` as default `false` without backfilling existing tokens. After upgrading, both the runner env and the per-token toggles must be set, or keepalive silently schedules zero tokens. This happened on dev0 after the 2026-08-31 self-update; a lingering 2.7.6 instance masked it until 2026-09-01. Diagnose with `GET /api/setting/listSettings` (`keepaliveRunnerEnabled`), `GET /api/token/findMany?subset=A` (`keepalive_enabled`), and the latest request log under project `qgrid-token-window-keepalive`.
|
|
246
253
|
- 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.
|
|
247
254
|
- 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.
|
|
248
255
|
- 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.
|
|
249
|
-
-
|
|
256
|
+
- Only runner instances fire. If more than one instance is marked as runner, 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.
|
|
250
257
|
- Fires are logged under the `qgrid-token-window-keepalive` project name so they can be excluded from traffic statistics.
|
|
251
258
|
- 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.
|
|
@@ -12,36 +12,39 @@ Use this reference to choose tests, smoke scripts, and first debugging targets f
|
|
|
12
12
|
|
|
13
13
|
## Package Scripts
|
|
14
14
|
|
|
15
|
+
The commands in this section apply only inside the qgrid source repository. In a downstream
|
|
16
|
+
project, use that project's configured task and tool runner.
|
|
17
|
+
|
|
15
18
|
Root:
|
|
16
19
|
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
+
- `mise run check`: qgrid docs consistency, oxlint, and oxfmt check for the repo.
|
|
21
|
+
- `mise run build`: recursive package build.
|
|
22
|
+
- `mise run dev`: parallel package dev servers.
|
|
20
23
|
|
|
21
24
|
API package `qgrid-api`:
|
|
22
25
|
|
|
23
|
-
- `pnpm --filter qgrid-api test`: Vitest run.
|
|
24
|
-
- `pnpm --filter qgrid-api test:watch`: standalone watch mode.
|
|
25
|
-
- `pnpm --filter qgrid-api build`: Sonamu build.
|
|
26
|
-
- `pnpm --filter qgrid-api sonamu`: run Sonamu CLI.
|
|
26
|
+
- `mise exec -- pnpm --filter qgrid-api test`: Vitest run.
|
|
27
|
+
- `mise exec -- pnpm --filter qgrid-api test:watch`: standalone watch mode.
|
|
28
|
+
- `mise exec -- pnpm --filter qgrid-api build`: Sonamu build.
|
|
29
|
+
- `mise exec -- pnpm --filter qgrid-api sonamu`: run Sonamu CLI.
|
|
27
30
|
|
|
28
31
|
AI SDK package `@cartanova/qgrid-ai-sdk`:
|
|
29
32
|
|
|
30
|
-
- `pnpm --filter @cartanova/qgrid-ai-sdk test`.
|
|
31
|
-
- `pnpm --filter @cartanova/qgrid-ai-sdk build`.
|
|
32
|
-
- `pnpm --filter @cartanova/qgrid-ai-sdk e2e`.
|
|
33
|
-
- `pnpm --filter @cartanova/qgrid-ai-sdk e2e:tools-output`.
|
|
34
|
-
- `pnpm --filter @cartanova/qgrid-ai-sdk e2e:logger`.
|
|
33
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-ai-sdk test`.
|
|
34
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-ai-sdk build`.
|
|
35
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-ai-sdk e2e`.
|
|
36
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-ai-sdk e2e:tools-output`.
|
|
37
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-ai-sdk e2e:logger`.
|
|
35
38
|
|
|
36
39
|
CLI package `@cartanova/qgrid-cli`:
|
|
37
40
|
|
|
38
|
-
- `pnpm --filter @cartanova/qgrid-cli build`.
|
|
39
|
-
- `pnpm --filter @cartanova/qgrid-cli bundle` builds the API first and copies the server bundle.
|
|
41
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-cli build`.
|
|
42
|
+
- `mise exec -- pnpm --filter @cartanova/qgrid-cli bundle` builds the API first and copies the server bundle.
|
|
40
43
|
|
|
41
44
|
Web package `qgrid-web`:
|
|
42
45
|
|
|
43
|
-
- `pnpm --filter qgrid-web dev`.
|
|
44
|
-
- `pnpm --filter qgrid-web preview`.
|
|
46
|
+
- `mise exec -- pnpm --filter qgrid-web dev`.
|
|
47
|
+
- `mise exec -- pnpm --filter qgrid-web preview`.
|
|
45
48
|
|
|
46
49
|
The old v1 SDK package (`packages/sdk`) has been removed from the repository; all public-SDK verification goes through `packages/ai-sdk`.
|
|
47
50
|
|