@cartanova/qgrid-cli 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/application/qgrid/qgrid.frame.js +13 -17
- package/bundle/dist/application/qgrid/token-subscriber.js +23 -9
- package/bundle/dist/application/qgrid/token-trigger-setup.js +4 -4
- package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
- package/bundle/dist/application/sonamu.generated.js +5 -3
- package/bundle/dist/application/sonamu.generated.sso.js +3 -2
- package/bundle/dist/application/token/token.model.js +8 -3
- package/bundle/dist/application/token/token.types.js +8 -3
- package/bundle/dist/i18n/sd.generated.js +2 -1
- package/bundle/dist/index.js +3 -6
- package/bundle/dist/migrations/20260521134025_alter_request_log_steps_alter1.js +3 -9
- package/bundle/dist/migrations/20260706120000_alter_request_logs_add_image_cost.js +1 -1
- package/bundle/dist/migrations/20260710090000_alter_tokens_add_weight.js +41 -0
- package/bundle/dist/server-bootstrap.js +17 -0
- package/bundle/dist/sonamu.config.js +1 -10
- package/bundle/dist/startup-migrations.js +21 -0
- package/bundle/dist/testing/global.js +93 -5
- package/bundle/dist/testing/test-database-name.js +22 -0
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +24 -27
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +43 -0
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +53 -40
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +36 -14
- package/bundle/src/application/qgrid/qgrid.frame.ts +22 -25
- package/bundle/src/application/qgrid/token-subscriber.test.ts +102 -0
- package/bundle/src/application/qgrid/token-subscriber.ts +37 -6
- package/bundle/src/application/qgrid/token-trigger-setup.test.ts +9 -0
- package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -2
- package/bundle/src/application/qgrid/token-weight-migration.test.ts +42 -0
- package/bundle/src/application/qgrid/tool-emulation.ts +6 -1
- package/bundle/src/application/sonamu.generated.http +4 -2
- package/bundle/src/application/sonamu.generated.sso.ts +1 -0
- package/bundle/src/application/sonamu.generated.ts +10 -1
- package/bundle/src/application/token/token.entity.json +17 -1
- package/bundle/src/application/token/token.model.test.ts +43 -0
- package/bundle/src/application/token/token.model.ts +18 -2
- package/bundle/src/application/token/token.types.test.ts +11 -0
- package/bundle/src/application/token/token.types.ts +3 -0
- package/bundle/src/i18n/sd.generated.ts +1 -0
- package/bundle/src/index.ts +2 -5
- package/bundle/src/migrations/20260521134025_alter_request_log_steps_alter1.ts +4 -8
- package/bundle/src/migrations/20260706120000_alter_request_logs_add_image_cost.ts +1 -1
- package/bundle/src/migrations/20260710090000_alter_tokens_add_weight.ts +38 -0
- package/bundle/src/server-bootstrap.test.ts +39 -0
- package/bundle/src/server-bootstrap.ts +21 -0
- package/bundle/src/sonamu.config.ts +0 -12
- package/bundle/src/startup-migrations.test.ts +25 -0
- package/bundle/src/startup-migrations.ts +20 -0
- package/bundle/src/testing/global.test.ts +130 -0
- package/bundle/src/testing/global.ts +143 -13
- package/bundle/src/testing/test-database-name.test.ts +38 -0
- package/bundle/src/testing/test-database-name.ts +21 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +75 -45
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +23 -29
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -1
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.test.ts +73 -0
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +48 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +109 -6
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +72 -55
- package/bundle/web-dist/client/assets/{index-DsGDBrrH.js → index-D1ExP77_.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-C6EMtwDn.js → logs-vuI-Zgnq.js} +1 -1
- package/bundle/web-dist/client/assets/routes-0Fki0JEi.js +5 -0
- package/bundle/web-dist/client/assets/{sd.generated-BrUnRt0B.js → sd.generated-WA5SGJYZ.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-AsEvHbJ7.js → services.generated-DzZLnb7S.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BSW3PE7Z.js → show-BreAgaFH.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-4VgHQtXd.js +1 -0
- package/bundle/web-dist/client/assets/use-update-token-mutation-BV6msjuy.js +1 -0
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BswulcOx.js → lazyRouteComponent-BrPAFtBA.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-Bk9C0ELo.js → logs-CKM6W3W6.js} +3 -3
- package/bundle/web-dist/server/assets/{routes-D9OiKoqa.js → routes-DhIpPgXz.js} +130 -7
- package/bundle/web-dist/server/assets/{sd.generated-DRuQSIKo.js → sd.generated-Cwg2f53z.js} +1 -0
- package/bundle/web-dist/server/assets/{services.generated-Dp0uKgpV.js → services.generated-CsotZP44.js} +5 -5
- package/bundle/web-dist/server/assets/{show-3GokIDEJ.js → show-CV2UFjaH.js} +3 -3
- package/bundle/web-dist/server/assets/{tokens-Cjw6cQxZ.js → tokens-DfGj1y2v.js} +5 -5
- package/bundle/web-dist/server/assets/use-update-token-mutation-BL96w-Pg.js +7 -0
- package/bundle/web-dist/server/entry-server.generated.js +7 -7
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +1 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +2 -2
- package/skills/qgrid/references/cli-env-and-server-boot.md +9 -9
- package/skills/qgrid/references/decision-rationale.md +23 -0
- package/skills/qgrid/references/openai-codex-runtime.md +4 -3
- package/skills/qgrid/references/prompt-cache-and-usage.md +2 -0
- package/skills/qgrid/references/provider-runtime-differences.md +2 -1
- package/skills/qgrid/references/repo-map.md +2 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +8 -3
- package/skills/qgrid/references/verification-and-debugging.md +3 -0
- package/bundle/web-dist/client/assets/routes-De9iuL80.js +0 -5
- package/bundle/web-dist/client/assets/tokens-BMsL6vk9.js +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { X as __toESM, _ as require_react, f as require_react_dom, g as require_jsx_runtime, h as useQueryClient } from "./sd.generated-
|
|
2
|
-
import { i as TokenService, t as QgridService } from "./services.generated-
|
|
1
|
+
import { X as __toESM, _ as require_react, f as require_react_dom, g as require_jsx_runtime, h as useQueryClient } from "./sd.generated-Cwg2f53z.js";
|
|
2
|
+
import { i as TokenService, t as QgridService } from "./services.generated-CsotZP44.js";
|
|
3
3
|
import { r as Q } from "../entry-server.generated.js";
|
|
4
4
|
import { r as formatUsd } from "./cost-CSH_t17I.js";
|
|
5
|
+
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BL96w-Pg.js";
|
|
5
6
|
//#region ../../node_modules/.pnpm/@dnd-kit+utilities@3.2.2_react@19.2.4/node_modules/@dnd-kit/utilities/dist/utilities.esm.js
|
|
6
7
|
var import_react_dom = /* @__PURE__ */ __toESM(require_react_dom());
|
|
7
8
|
var import_react = /* @__PURE__ */ __toESM(require_react());
|
|
@@ -2318,6 +2319,22 @@ function validateThreshold(raw) {
|
|
|
2318
2319
|
value: n === 0 ? null : n
|
|
2319
2320
|
};
|
|
2320
2321
|
}
|
|
2322
|
+
function validateWeight(raw) {
|
|
2323
|
+
const trimmed = raw.trim();
|
|
2324
|
+
if (!/^\d+$/.test(trimmed)) return {
|
|
2325
|
+
ok: false,
|
|
2326
|
+
error: "1–100 사이 정수를 입력하세요"
|
|
2327
|
+
};
|
|
2328
|
+
const value = Number(trimmed);
|
|
2329
|
+
if (value < 1 || value > 100) return {
|
|
2330
|
+
ok: false,
|
|
2331
|
+
error: "1–100 사이 정수를 입력하세요"
|
|
2332
|
+
};
|
|
2333
|
+
return {
|
|
2334
|
+
ok: true,
|
|
2335
|
+
value
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2321
2338
|
var PROVIDER_THEMES = {
|
|
2322
2339
|
openai: {
|
|
2323
2340
|
stripe: "bg-openai-400",
|
|
@@ -2439,7 +2456,7 @@ function ThresholdControl({ token }) {
|
|
|
2439
2456
|
const [value, setValue] = (0, import_react.useState)("");
|
|
2440
2457
|
const triggerRef = (0, import_react.useRef)(null);
|
|
2441
2458
|
const queryClient = useQueryClient();
|
|
2442
|
-
const updateMutation =
|
|
2459
|
+
const updateMutation = useUpdateTokenMutation();
|
|
2443
2460
|
const supported = QUOTA_THRESHOLD_PROVIDERS.has(token.provider);
|
|
2444
2461
|
const open = pos !== null;
|
|
2445
2462
|
const validation = validateThreshold(value);
|
|
@@ -2474,7 +2491,6 @@ function ThresholdControl({ token }) {
|
|
|
2474
2491
|
if (!validation.ok) return;
|
|
2475
2492
|
await updateMutation.mutateAsync({
|
|
2476
2493
|
id: token.id,
|
|
2477
|
-
name: token.name ?? "",
|
|
2478
2494
|
quotaThreshold: validation.value
|
|
2479
2495
|
});
|
|
2480
2496
|
await Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
|
|
@@ -2560,6 +2576,113 @@ function ThresholdControl({ token }) {
|
|
|
2560
2576
|
})] })]
|
|
2561
2577
|
});
|
|
2562
2578
|
}
|
|
2579
|
+
function WeightControl({ token }) {
|
|
2580
|
+
const [pos, setPos] = (0, import_react.useState)(null);
|
|
2581
|
+
const [value, setValue] = (0, import_react.useState)("");
|
|
2582
|
+
const triggerRef = (0, import_react.useRef)(null);
|
|
2583
|
+
const queryClient = useQueryClient();
|
|
2584
|
+
const updateMutation = useUpdateTokenMutation();
|
|
2585
|
+
const validation = validateWeight(value);
|
|
2586
|
+
const openPopover = () => {
|
|
2587
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
2588
|
+
if (!rect) return;
|
|
2589
|
+
const width = 240;
|
|
2590
|
+
const height = 150;
|
|
2591
|
+
const margin = 8;
|
|
2592
|
+
const gap = 6;
|
|
2593
|
+
const left = Math.min(Math.max(rect.right - width, margin), Math.max(window.innerWidth - width - margin, margin));
|
|
2594
|
+
const above = rect.top - gap - height;
|
|
2595
|
+
const top = Math.min(Math.max(above >= margin ? above : rect.bottom + gap, margin), Math.max(window.innerHeight - height - margin, margin));
|
|
2596
|
+
setValue(String(token.weight));
|
|
2597
|
+
setPos({
|
|
2598
|
+
left,
|
|
2599
|
+
top
|
|
2600
|
+
});
|
|
2601
|
+
};
|
|
2602
|
+
const close = () => setPos(null);
|
|
2603
|
+
const adjust = (delta) => {
|
|
2604
|
+
const current = /^\d+$/.test(value.trim()) ? Number(value.trim()) : 1;
|
|
2605
|
+
setValue(String(Math.min(Math.max(current + delta, 1), 100)));
|
|
2606
|
+
};
|
|
2607
|
+
const save = async () => {
|
|
2608
|
+
if (!validation.ok) return;
|
|
2609
|
+
await updateMutation.mutateAsync({
|
|
2610
|
+
id: token.id,
|
|
2611
|
+
weight: validation.value
|
|
2612
|
+
});
|
|
2613
|
+
await Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
|
|
2614
|
+
close();
|
|
2615
|
+
};
|
|
2616
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2617
|
+
className: "inline-flex",
|
|
2618
|
+
onPointerDown: stopDragPropagation,
|
|
2619
|
+
onClick: stopDragPropagation,
|
|
2620
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
2621
|
+
ref: triggerRef,
|
|
2622
|
+
type: "button",
|
|
2623
|
+
title: "라우팅 가중치 설정",
|
|
2624
|
+
onClick: pos ? close : openPopover,
|
|
2625
|
+
className: "flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] border border-sand-200/80 text-sand-400 hover:text-sand-600 hover:border-sand-300 transition-colors duration-150",
|
|
2626
|
+
children: ["Weight ", token.weight]
|
|
2627
|
+
}), pos && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
2628
|
+
className: "fixed inset-0 z-40",
|
|
2629
|
+
onPointerDown: close
|
|
2630
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2631
|
+
className: "fixed z-50 w-60 panel shadow-xl p-3",
|
|
2632
|
+
style: pos,
|
|
2633
|
+
children: [
|
|
2634
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
|
|
2635
|
+
htmlFor: `weight-${token.id}`,
|
|
2636
|
+
className: "text-[10px] uppercase tracking-wider text-sand-500 font-medium",
|
|
2637
|
+
children: "라우팅 가중치"
|
|
2638
|
+
}),
|
|
2639
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2640
|
+
className: "mt-1 flex items-center gap-2",
|
|
2641
|
+
children: [
|
|
2642
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2643
|
+
type: "button",
|
|
2644
|
+
onClick: () => adjust(-1),
|
|
2645
|
+
className: "size-9 shrink-0 rounded-md border border-sand-200 text-sand-600",
|
|
2646
|
+
children: "−"
|
|
2647
|
+
}),
|
|
2648
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Q, {
|
|
2649
|
+
id: `weight-${token.id}`,
|
|
2650
|
+
value,
|
|
2651
|
+
onChange: (event) => setValue(event.target.value),
|
|
2652
|
+
inputMode: "numeric",
|
|
2653
|
+
className: `h-9 w-16 border rounded-md text-center text-sm tabular-nums ${validation.ok ? "border-sand-200" : "border-red-300"}`
|
|
2654
|
+
}),
|
|
2655
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2656
|
+
type: "button",
|
|
2657
|
+
onClick: () => adjust(1),
|
|
2658
|
+
className: "size-9 shrink-0 rounded-md border border-sand-200 text-sand-600",
|
|
2659
|
+
children: "+"
|
|
2660
|
+
})
|
|
2661
|
+
]
|
|
2662
|
+
}),
|
|
2663
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
2664
|
+
className: `mt-1.5 text-[11px] ${validation.ok ? "text-sand-500" : "text-red-500"}`,
|
|
2665
|
+
children: validation.ok ? "새 요청의 상대 배정 비율입니다." : validation.error
|
|
2666
|
+
}),
|
|
2667
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2668
|
+
className: "mt-2 flex items-center justify-end gap-1.5",
|
|
2669
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2670
|
+
type: "button",
|
|
2671
|
+
onClick: close,
|
|
2672
|
+
className: "px-2 py-1 text-[11px]",
|
|
2673
|
+
children: "Cancel"
|
|
2674
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
2675
|
+
type: "button",
|
|
2676
|
+
onClick: save,
|
|
2677
|
+
disabled: updateMutation.isPending || !validation.ok,
|
|
2678
|
+
className: "px-2 py-1 text-[11px] rounded-md bg-sienna-400 text-white disabled:opacity-50",
|
|
2679
|
+
children: updateMutation.isPending ? "Saving..." : "Save"
|
|
2680
|
+
})]
|
|
2681
|
+
})
|
|
2682
|
+
]
|
|
2683
|
+
})] })]
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2563
2686
|
function SortableTokenCard({ token }) {
|
|
2564
2687
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
2565
2688
|
id: String(token.id),
|
|
@@ -2606,9 +2729,9 @@ function SortableTokenCard({ token }) {
|
|
|
2606
2729
|
token,
|
|
2607
2730
|
theme
|
|
2608
2731
|
}),
|
|
2609
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
2610
|
-
className: "mt-2 flex items-center justify-end",
|
|
2611
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThresholdControl, { token })
|
|
2732
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2733
|
+
className: "mt-2 flex items-center justify-end gap-1.5",
|
|
2734
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(WeightControl, { token }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThresholdControl, { token })]
|
|
2612
2735
|
}),
|
|
2613
2736
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
2614
2737
|
className: "absolute top-2.5 right-3 flex items-center gap-1.5",
|
|
@@ -22600,6 +22600,7 @@ var entityLabels = {
|
|
|
22600
22600
|
"entity.Token.active": "활성 여부",
|
|
22601
22601
|
"entity.Token.ord": "정렬 순서",
|
|
22602
22602
|
"entity.Token.quota_threshold": "quota 사용률 임계치(%). null이면 미설정",
|
|
22603
|
+
"entity.Token.weight": "가중 라운드로빈 라우팅 가중치 (1..100)",
|
|
22603
22604
|
"enum.TokenOrderBy.id-desc": "ID최신순",
|
|
22604
22605
|
"enum.TokenOrderBy.ord-asc": "순서순",
|
|
22605
22606
|
"enum.TokenSearchField.id": "ID",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as hashKey, B as shallowEqualObjects, D as pendingThenable, E as notifyManager, F as noop, G as focusManager, K as Subscribable, L as replaceData, M as isValidTimeout, O as environmentManager, R as resolveEnabled, S as infiniteQueryBehavior, U as timeUntilStale, V as shouldThrowError, W as timeoutManager, X as __toESM, _ as require_react, a as fetch, b as hasNextPage, d as require_lib, g as require_jsx_runtime, h as useQueryClient, i as dedupeAndFlatten, o as useRefreshable, s as useSSEStream, w as fetchState, x as hasPreviousPage, y as getDefaultState, z as resolveStaleTime } from "./sd.generated-
|
|
1
|
+
import { A as hashKey, B as shallowEqualObjects, D as pendingThenable, E as notifyManager, F as noop, G as focusManager, K as Subscribable, L as replaceData, M as isValidTimeout, O as environmentManager, R as resolveEnabled, S as infiniteQueryBehavior, U as timeUntilStale, V as shouldThrowError, W as timeoutManager, X as __toESM, _ as require_react, a as fetch, b as hasNextPage, d as require_lib, g as require_jsx_runtime, h as useQueryClient, i as dedupeAndFlatten, o as useRefreshable, s as useSSEStream, w as fetchState, x as hasPreviousPage, y as getDefaultState, z as resolveStaleTime } from "./sd.generated-Cwg2f53z.js";
|
|
2
2
|
//#region ../../node_modules/.pnpm/@tanstack+query-core@5.95.2/node_modules/@tanstack/query-core/build/modern/queryObserver.js
|
|
3
3
|
var QueryObserver = class extends Subscribable {
|
|
4
4
|
constructor(client, options) {
|
|
@@ -1036,19 +1036,19 @@ var QgridService;
|
|
|
1036
1036
|
}
|
|
1037
1037
|
_QgridService.addToken = addToken;
|
|
1038
1038
|
_QgridService.useAddTokenMutation = () => useMutation({ mutationFn: (params) => addToken(params.provider, params.credentials, params.name) });
|
|
1039
|
-
async function updateToken(id, name, quotaThreshold) {
|
|
1039
|
+
async function updateToken(id, name, quotaThreshold, weight) {
|
|
1040
1040
|
return fetch({
|
|
1041
1041
|
method: "POST",
|
|
1042
1042
|
url: `/api/qgrid/updateToken`,
|
|
1043
1043
|
data: {
|
|
1044
1044
|
id,
|
|
1045
1045
|
name,
|
|
1046
|
-
quotaThreshold
|
|
1046
|
+
quotaThreshold,
|
|
1047
|
+
weight
|
|
1047
1048
|
}
|
|
1048
1049
|
});
|
|
1049
1050
|
}
|
|
1050
1051
|
_QgridService.updateToken = updateToken;
|
|
1051
|
-
_QgridService.useUpdateTokenMutation = () => useMutation({ mutationFn: (params) => updateToken(params.id, params.name, params.quotaThreshold) });
|
|
1052
1052
|
async function removeToken(id) {
|
|
1053
1053
|
return fetch({
|
|
1054
1054
|
method: "POST",
|
|
@@ -1124,4 +1124,4 @@ RequestLogService.useRequestLogs, RequestLogService.useRequestLogsInfinite;
|
|
|
1124
1124
|
RequestLogStepService.useRequestLogSteps, RequestLogStepService.useRequestLogStepsInfinite;
|
|
1125
1125
|
TokenService.useTokens, TokenService.useTokensInfinite;
|
|
1126
1126
|
//#endregion
|
|
1127
|
-
export { TokenService as i, RequestLogService as n, RequestLogStepService as r, QgridService as t };
|
|
1127
|
+
export { useMutation as a, TokenService as i, RequestLogService as n, RequestLogStepService as r, QgridService as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { J as __exportAll, X as __toESM, _ as require_react, g as require_jsx_runtime, q as __commonJSMin } from "./sd.generated-
|
|
2
|
-
import { n as RequestLogService, r as RequestLogStepService } from "./services.generated-
|
|
3
|
-
import { a as Link } from "./lazyRouteComponent-
|
|
1
|
+
import { J as __exportAll, X as __toESM, _ as require_react, g as require_jsx_runtime, q as __commonJSMin } from "./sd.generated-Cwg2f53z.js";
|
|
2
|
+
import { n as RequestLogService, r as RequestLogStepService } from "./services.generated-CsotZP44.js";
|
|
3
|
+
import { a as Link } from "./lazyRouteComponent-BrPAFtBA.js";
|
|
4
4
|
import { t as Route } from "../entry-server.generated.js";
|
|
5
5
|
import { n as formatMicroUsd, t as cacheHitRate } from "./cost-CSH_t17I.js";
|
|
6
6
|
import minpath from "node:path";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { X as __toESM, _ as require_react, g as require_jsx_runtime, h as useQueryClient } from "./sd.generated-
|
|
2
|
-
import { i as TokenService, t as QgridService } from "./services.generated-
|
|
1
|
+
import { X as __toESM, _ as require_react, g as require_jsx_runtime, h as useQueryClient } from "./sd.generated-Cwg2f53z.js";
|
|
2
|
+
import { i as TokenService, t as QgridService } from "./services.generated-CsotZP44.js";
|
|
3
3
|
import { i as lucideKeyRound, r as Q } from "../entry-server.generated.js";
|
|
4
|
+
import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BL96w-Pg.js";
|
|
4
5
|
//#region ~icons/lucide/plus.jsx
|
|
5
6
|
var import_react = /* @__PURE__ */ __toESM(require_react());
|
|
6
7
|
var import_jsx_runtime = require_jsx_runtime();
|
|
@@ -216,7 +217,7 @@ function TokenTable({ data, isLoading }) {
|
|
|
216
217
|
const [editName, setEditName] = (0, import_react.useState)("");
|
|
217
218
|
const queryClient = useQueryClient();
|
|
218
219
|
const removeMutation = QgridService.useRemoveTokenMutation();
|
|
219
|
-
const updateMutation =
|
|
220
|
+
const updateMutation = useUpdateTokenMutation();
|
|
220
221
|
const toggleMutation = QgridService.useToggleTokenMutation();
|
|
221
222
|
const invalidate = () => Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
|
|
222
223
|
const handleToggle = async (token) => {
|
|
@@ -237,8 +238,7 @@ function TokenTable({ data, isLoading }) {
|
|
|
237
238
|
if (!editTarget) return;
|
|
238
239
|
await updateMutation.mutateAsync({
|
|
239
240
|
id: editTarget.id,
|
|
240
|
-
name: editName.trim()
|
|
241
|
-
quotaThreshold: editTarget.quota_threshold
|
|
241
|
+
name: editName.trim()
|
|
242
242
|
});
|
|
243
243
|
await invalidate();
|
|
244
244
|
setEditTarget(null);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { a as useMutation, t as QgridService } from "./services.generated-CsotZP44.js";
|
|
2
|
+
//#region src/services/token/use-update-token-mutation.ts
|
|
3
|
+
function useUpdateTokenMutation() {
|
|
4
|
+
return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight }) => QgridService.updateToken(id, name, quotaThreshold, weight) });
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { useUpdateTokenMutation as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as hashKey, C as Query, E as notifyManager, F as noop$1, G as focusManager, H as skipToken, I as partialMatchKey, K as Subscribable, N as matchMutation, P as matchQuery, S as infiniteQueryBehavior, T as onlineManager, X as __toESM, Y as __require, _ as require_react, c as number, f as require_react_dom, g as require_jsx_runtime, j as hashQueryKeyByOptions, k as functionalUpdate, l as object, m as QueryClientProvider, n as SUPPORTED_LOCALES, p as require_shim, q as __commonJSMin, r as setLocale, t as SD, u as string, v as Mutation, z as resolveStaleTime } from "./assets/sd.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-
|
|
1
|
+
import { A as hashKey, C as Query, E as notifyManager, F as noop$1, G as focusManager, H as skipToken, I as partialMatchKey, K as Subscribable, N as matchMutation, P as matchQuery, S as infiniteQueryBehavior, T as onlineManager, X as __toESM, Y as __require, _ as require_react, c as number, f as require_react_dom, g as require_jsx_runtime, j as hashQueryKeyByOptions, k as functionalUpdate, l as object, m as QueryClientProvider, n as SUPPORTED_LOCALES, p as require_shim, q as __commonJSMin, r as setLocale, t as SD, u as string, v as Mutation, z as resolveStaleTime } from "./assets/sd.generated-Cwg2f53z.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-BrPAFtBA.js";
|
|
3
3
|
//#region ../../node_modules/.pnpm/@tanstack+query-core@5.95.2/node_modules/@tanstack/query-core/build/modern/hydration.js
|
|
4
4
|
function defaultTransformerFn(data) {
|
|
5
5
|
return data;
|
|
@@ -22679,7 +22679,7 @@ var sections = [{
|
|
|
22679
22679
|
icon: lucideFileText
|
|
22680
22680
|
}]
|
|
22681
22681
|
}];
|
|
22682
|
-
var qgridVersion = "2.4.
|
|
22682
|
+
var qgridVersion = "2.4.4";
|
|
22683
22683
|
function Sidebar({ className }) {
|
|
22684
22684
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
22685
22685
|
const isActive = (path) => {
|
|
@@ -44559,11 +44559,11 @@ function RootComponent() {
|
|
|
44559
44559
|
}
|
|
44560
44560
|
//#endregion
|
|
44561
44561
|
//#region src/routes/tokens.tsx
|
|
44562
|
-
var $$splitComponentImporter$3 = () => import("./assets/tokens-
|
|
44562
|
+
var $$splitComponentImporter$3 = () => import("./assets/tokens-DfGj1y2v.js");
|
|
44563
44563
|
var Route$3 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$3, "component") });
|
|
44564
44564
|
//#endregion
|
|
44565
44565
|
//#region src/routes/logs.tsx
|
|
44566
|
-
var $$splitComponentImporter$2 = () => import("./assets/logs-
|
|
44566
|
+
var $$splitComponentImporter$2 = () => import("./assets/logs-CKM6W3W6.js");
|
|
44567
44567
|
var logsSearchSchema = object({
|
|
44568
44568
|
token: string().optional(),
|
|
44569
44569
|
project: string().optional(),
|
|
@@ -44575,11 +44575,11 @@ var Route$2 = createFileRoute("/logs")({
|
|
|
44575
44575
|
});
|
|
44576
44576
|
//#endregion
|
|
44577
44577
|
//#region src/routes/index.tsx
|
|
44578
|
-
var $$splitComponentImporter$1 = () => import("./assets/routes-
|
|
44578
|
+
var $$splitComponentImporter$1 = () => import("./assets/routes-DhIpPgXz.js");
|
|
44579
44579
|
var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
|
|
44580
44580
|
//#endregion
|
|
44581
44581
|
//#region src/routes/requests/show.tsx
|
|
44582
|
-
var $$splitComponentImporter = () => import("./assets/show-
|
|
44582
|
+
var $$splitComponentImporter = () => import("./assets/show-CV2UFjaH.js");
|
|
44583
44583
|
var showSearchSchema = object({ id: number() });
|
|
44584
44584
|
var Route = createFileRoute("/requests/show")({
|
|
44585
44585
|
validateSearch: showSearchSchema,
|
package/package.json
CHANGED
package/skills/qgrid/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ Before changing code, identify the affected path:
|
|
|
15
15
|
- CLI startup, env vars, server boot, OAuth callback URL: read `references/cli-env-and-server-boot.md`.
|
|
16
16
|
- OpenAI models or Codex worker/thread/cache behavior: read `references/openai-codex-runtime.md`.
|
|
17
17
|
- Anthropic models or Claude Code spawn/stream-json behavior: read `references/anthropic-claude-code-runtime.md`.
|
|
18
|
-
- Token registration, OAuth, token sync, active/inactive behavior, or
|
|
18
|
+
- Token registration, OAuth, token sync, active/inactive behavior, quota thresholds, or weighted token routing: read `references/token-auth-quota-lifecycle.md`.
|
|
19
19
|
- Provider comparisons, routing, or cross-provider bugs: read `references/provider-runtime-differences.md`.
|
|
20
20
|
- Tool calling, AI SDK multi-step loops, or tool-call request logs: read `references/tool-calling-and-multiturn.md`.
|
|
21
21
|
- `sessionKey`, `threadCoord`, prompt cache metrics, usage accounting, or cost: read `references/prompt-cache-and-usage.md`.
|
|
@@ -37,8 +37,8 @@ Anthropic tokens live in an in-memory `Map<tokenId, PooledToken>`.
|
|
|
37
37
|
- Startup loads active Anthropic tokens from DB.
|
|
38
38
|
- Token subscriber events add/update/remove entries.
|
|
39
39
|
- Periodic reconcile replaces pool contents from active DB rows.
|
|
40
|
-
- Request selection is
|
|
41
|
-
-
|
|
40
|
+
- Request selection is smooth weighted round-robin over quota-eligible tokens, driven by per-token `tokens.weight` (shared selector in `providers/common/smooth-weighted-round-robin.ts`).
|
|
41
|
+
- Selection and score mutation run synchronously before any await, so concurrent requests observe committed prior selections and spread across tokens.
|
|
42
42
|
|
|
43
43
|
Quota threshold:
|
|
44
44
|
|
|
@@ -50,15 +50,15 @@ When helping a user set up qgrid locally, check for `QGRID_PROJECT_NAME` or conf
|
|
|
50
50
|
|
|
51
51
|
## Server boot lifecycle
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
1.
|
|
56
|
-
2.
|
|
57
|
-
3.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
`packages/api/src/index.ts` runs `bootstrapServer` (`server-bootstrap.ts`) with a strict init → migrate → listen order:
|
|
54
|
+
|
|
55
|
+
1. `Sonamu.init()` loads `.env` from `packages/api/.env` when running API directly and configures the Sonamu database connection from `QGRID_DB_*`.
|
|
56
|
+
2. `runRequiredMigrations` (`startup-migrations.ts`) applies latest migrations from `packages/api/src/migrations` before the server listens. Migration failure exits the process — qgrid must not boot against a schema missing required columns such as `tokens.weight`. This is a hard-fail; it used to be a soft-fail warn inside `onStart`.
|
|
57
|
+
3. `Sonamu.createServer()` starts the server. Its `onStart` in `packages/api/src/sonamu.config.ts`:
|
|
58
|
+
1. Ensures PostgreSQL `tokens_changed` triggers.
|
|
59
|
+
2. Starts `TokenSubscriber` for LISTEN/NOTIFY plus periodic reconcile.
|
|
60
|
+
3. Starts `OpenAIDispatcher` and `AnthropicDispatcher`.
|
|
61
|
+
4. Logs server URL and provider readiness counts.
|
|
62
62
|
|
|
63
63
|
On shutdown, it stops provider dispatchers and the token subscriber.
|
|
64
64
|
|
|
@@ -11,6 +11,7 @@ Do not copy these notes blindly into implementation. Use them to find the releva
|
|
|
11
11
|
- OpenAI/Codex runtime
|
|
12
12
|
- Anthropic/Claude Code runtime
|
|
13
13
|
- Token sync and quota thresholds
|
|
14
|
+
- Weighted token routing
|
|
14
15
|
- Request logs, metrics, and dashboard
|
|
15
16
|
- CLI packaging and server boot
|
|
16
17
|
- Image generation
|
|
@@ -119,6 +120,28 @@ Key decisions:
|
|
|
119
120
|
- Lookup failure is fail-open with logs/metrics. Hard failure happens only when usage was read successfully and the token is over threshold.
|
|
120
121
|
- OpenAI needs the gate in reuse, idle selection, and queue drain paths. A reusable but over-threshold worker must fall back cold to another eligible token when possible.
|
|
121
122
|
|
|
123
|
+
## Weighted Token Routing
|
|
124
|
+
|
|
125
|
+
Sources:
|
|
126
|
+
|
|
127
|
+
- `docs/brainstorms/2026-07-10-weighted-token-routing-requirements.md`
|
|
128
|
+
- `docs/plans/2026-07-10-weighted-token-routing-plan.md`
|
|
129
|
+
|
|
130
|
+
Key decisions:
|
|
131
|
+
|
|
132
|
+
- `tokens.weight` is a per-token integer from 1 to 100 defaulting to 1. It is a relative routing share for new requests, not an enable/disable switch. Weight 0 is invalid; disabling a token stays on `active`.
|
|
133
|
+
- Both providers share one smooth weighted round-robin selector in the provider common layer. The selector knows only token ids, weights, and current scores. Dispatchers own quota, lifecycle, and worker availability, and pass in only the eligible token id set per selection. Do not teach the selector about credentials, workers, or queues.
|
|
134
|
+
- The quota threshold gate runs before weighted selection. An over-threshold token receives no weighted assignment regardless of weight, and the schedule is computed from the remaining eligible tokens.
|
|
135
|
+
- OpenAI thread reuse stays pinned to its original worker and neither reads nor mutates weighted-selector state. Cache affinity intentionally beats weight distribution; only cold requests are weighted.
|
|
136
|
+
- OpenAI routing is work-conserving. A token whose workers are all busy is omitted from the current selection round instead of making requests wait for the heavy-weight token. Queue drain re-runs weighted selection for the head item; the previous released-worker shortcut was removed because it bypassed weighting.
|
|
137
|
+
- Candidate collection, selector mutation, and worker acquisition must run without an intervening `await`. A weighted score is consumed only when the dispatcher can commit the assignment synchronously; this is what keeps concurrent Anthropic selections spread correctly.
|
|
138
|
+
- Selector scores reset when token topology or configured weights change. Temporary ineligibility from quota or busy workers only excludes the token from that selection's candidate set and does not reset schedule state.
|
|
139
|
+
- Weight-change notification is owned by the versioned migration trigger (`tokens_weight_changed_upd`); the boot-time trigger setup SQL intentionally excludes `weight` from its WHEN clause, and a test pins this so exactly one trigger fires per weight-only change.
|
|
140
|
+
- The token subscriber serializes notification and reconcile handling through an operation chain because weight propagation made dispatcher updates awaited; out-of-order token events could otherwise apply stale weights.
|
|
141
|
+
- Required migrations moved from a soft-fail `onStart` step to a hard-fail `bootstrapServer` order (init → migrate → listen). Dispatchers must never boot against a schema missing `tokens.weight`.
|
|
142
|
+
- `updateToken` became a partial field update (`TokenModel.updateFields`) so the dashboard weight control cannot overwrite name or quota threshold edits happening elsewhere.
|
|
143
|
+
- Explicit scope boundaries: no per-model, per-project, per-request, or time-varying weights, and no automatic weight adjustment from remaining quota, latency, cost, errors, or worker counts.
|
|
144
|
+
|
|
122
145
|
## Request Logs, Metrics, And Dashboard
|
|
123
146
|
|
|
124
147
|
Sources:
|
|
@@ -100,9 +100,10 @@ There is no explicit close RPC for old ephemeral threads; qgrid removes them fro
|
|
|
100
100
|
|
|
101
101
|
OpenAI worker selection:
|
|
102
102
|
|
|
103
|
-
- Prefer reuse worker when a valid reuse coordinate exists.
|
|
104
|
-
- Otherwise
|
|
105
|
-
-
|
|
103
|
+
- Prefer reuse worker when a valid reuse coordinate exists. Successful reuse bypasses weighted selection and does not read or mutate its state.
|
|
104
|
+
- Otherwise cold selection is two-level: group quota-eligible ready active workers by token, keep only tokens with at least one idle worker, choose the token with the shared smooth weighted round-robin selector (`providers/common/smooth-weighted-round-robin.ts`, driven by `tokens.weight`), then rotate a per-token worker cursor inside the chosen token.
|
|
105
|
+
- Selection is work-conserving: a token whose workers are all busy is omitted from that selection round, so an idle lower-weight token receives the request immediately instead of waiting for the heavy token.
|
|
106
|
+
- If no eligible worker is idle, enqueue. Queue drain re-runs the same weighted cold selection; it must not hand the queue head to the worker that happened to finish first.
|
|
106
107
|
- Queue timeout: 60 seconds.
|
|
107
108
|
- Max queue size: 50.
|
|
108
109
|
|
|
@@ -45,6 +45,8 @@ Cache hit requires:
|
|
|
45
45
|
|
|
46
46
|
When reuse succeeds, qgrid sends only delta input to `turn/start`. When reuse fails, qgrid falls back to cold execution with full prompt plus history injection.
|
|
47
47
|
|
|
48
|
+
Reuse is decided before weighted token routing and stays pinned to its original worker: a successful reuse neither consumes nor advances the weighted schedule. Cache affinity intentionally beats weight distribution; only cold requests are weighted.
|
|
49
|
+
|
|
48
50
|
Important cache breakers:
|
|
49
51
|
|
|
50
52
|
- Different system prompt.
|
|
@@ -8,7 +8,8 @@ Use this reference when comparing OpenAI and Anthropic behavior, debugging provi
|
|
|
8
8
|
| Worker pool | Token x `QGRID_WORKERS_PER_TOKEN`, default 3, max 5 | No workers; in-memory token pool only |
|
|
9
9
|
| Worker id | `tokenId * 10 + workerIndex` | `tokenId` |
|
|
10
10
|
| Epoch | Worker spawn counter; changes on restart | Always `0` |
|
|
11
|
-
| Request concurrency | One turn per worker; queue when all eligible workers busy | Fresh process per request
|
|
11
|
+
| Request concurrency | One turn per worker; queue when all eligible workers busy | Fresh process per request |
|
|
12
|
+
| Token selection | Smooth weighted RR picks a quota-eligible token with an idle worker, then a per-token worker cursor; reuse bypasses weights | Smooth weighted RR over quota-eligible tokens per request |
|
|
12
13
|
| Model routing | `openai/*`; qgrid strips provider prefix before provider call | `anthropic/*`; provider canonicalizes model and strips prefix/`[1m]` |
|
|
13
14
|
| Prefix-less models | Fallback not implemented | Fallback not implemented |
|
|
14
15
|
| Thread/session | Ephemeral Codex thread stored in worker memory | Fresh Claude `--session-id` per request |
|
|
@@ -26,6 +26,8 @@ Use this map to orient qgrid work quickly.
|
|
|
26
26
|
- OpenAI Codex runtime: `packages/api/src/utils/providers/openai/*`.
|
|
27
27
|
- Anthropic Claude Code runtime: `packages/api/src/utils/providers/anthropic/*`.
|
|
28
28
|
- Cost/usage accounting: `packages/api/src/utils/providers/common/model-cost.ts`.
|
|
29
|
+
- Weighted token selection: `packages/api/src/utils/providers/common/smooth-weighted-round-robin.ts`.
|
|
30
|
+
- Boot order and startup migrations: `packages/api/src/server-bootstrap.ts`, `packages/api/src/startup-migrations.ts`.
|
|
29
31
|
- Tool-call emulation: `packages/api/src/application/qgrid/tool-emulation.ts`.
|
|
30
32
|
|
|
31
33
|
## Web/Sonamu hot spots
|
|
@@ -34,8 +34,9 @@ Use this reference before changing token storage, OAuth flows, token activation,
|
|
|
34
34
|
- `active`: whether provider dispatchers may use it.
|
|
35
35
|
- `ord`: dashboard ordering.
|
|
36
36
|
- `quota_threshold`: nullable integer percentage, validated as 1..100 when present.
|
|
37
|
+
- `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`.
|
|
37
38
|
|
|
38
|
-
On creation, `TokenModel.save` applies
|
|
39
|
+
On creation, `TokenModel.save` applies defaults `quota_threshold = 80` and `weight = 1` independently for any field not provided (skipped entirely when `id` is present).
|
|
39
40
|
|
|
40
41
|
OpenAI credentials:
|
|
41
42
|
|
|
@@ -72,7 +73,7 @@ Duplicate account replacement is account-identifier based:
|
|
|
72
73
|
`QgridFrame` exposes token APIs:
|
|
73
74
|
|
|
74
75
|
- `addToken(provider, credentials, name)`: saves a token directly.
|
|
75
|
-
- `updateToken(id, name?, quotaThreshold?)`:
|
|
76
|
+
- `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.
|
|
76
77
|
- `removeToken(id)`: deletes token row.
|
|
77
78
|
- `toggleToken(id)`: flips `active`.
|
|
78
79
|
- `stats()`: reports dispatcher cache stats.
|
|
@@ -154,13 +155,16 @@ Triggers notify on:
|
|
|
154
155
|
- INSERT
|
|
155
156
|
- DELETE
|
|
156
157
|
- UPDATE when `active`, `credentials`, `provider`, `name`, or `quota_threshold` changes.
|
|
158
|
+
- 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.
|
|
157
159
|
|
|
158
160
|
`TokenSubscriber`:
|
|
159
161
|
|
|
160
162
|
- connects to PostgreSQL and `LISTEN`s on `tokens_changed`;
|
|
161
163
|
- reconnects with jittered exponential backoff up to 30 seconds;
|
|
162
164
|
- runs periodic reconcile every 10 minutes because LISTEN/NOTIFY can miss changes while disconnected;
|
|
163
|
-
- replaces dispatcher cache from `TokenModel.findActive("A")
|
|
165
|
+
- replaces dispatcher cache from `TokenModel.findActive("A")`;
|
|
166
|
+
- serializes notification handling and reconcile through an internal operation chain, so dispatcher updates (now awaited, including weight propagation) apply in arrival order;
|
|
167
|
+
- passes `weight` through add, update, and reconcile calls for both provider dispatchers.
|
|
164
168
|
|
|
165
169
|
On DELETE or missing row:
|
|
166
170
|
|
|
@@ -188,6 +192,7 @@ OpenAI token changes can create, update, deactivate, or kill persistent Codex wo
|
|
|
188
192
|
|
|
189
193
|
- In-place OpenAI worker update is allowed only when account id and plan type match.
|
|
190
194
|
- Otherwise workers are killed and respawned.
|
|
195
|
+
- Weight-only changes never respawn workers; they update token metadata and reset the weighted selector's scores.
|
|
191
196
|
- If no ready active OpenAI workers remain, queued OpenAI requests are rejected.
|
|
192
197
|
|
|
193
198
|
Anthropic token changes only affect the in-memory token pool because every request fresh-spawns Claude Code.
|
|
@@ -59,6 +59,9 @@ Choose by affected area:
|
|
|
59
59
|
| qgrid frame API behavior | `packages/api/src/application/qgrid/qgrid.frame.test.ts` |
|
|
60
60
|
| token subscriber and LISTEN/NOTIFY handling | `packages/api/src/application/qgrid/token-subscriber.test.ts` |
|
|
61
61
|
| token defaults/validation | `packages/api/src/application/token/token.model.test.ts`, `token.types.test.ts` |
|
|
62
|
+
| weighted routing selector | `packages/api/src/utils/providers/common/smooth-weighted-round-robin.test.ts` |
|
|
63
|
+
| token weight migration/trigger split | `packages/api/src/application/qgrid/token-weight-migration.test.ts`, `token-trigger-setup.test.ts` |
|
|
64
|
+
| boot order and startup migrations | `packages/api/src/server-bootstrap.test.ts`, `startup-migrations.test.ts` |
|
|
62
65
|
| request log queries/legacy normalization | `packages/api/src/application/request-log/request-log.model.test.ts` |
|
|
63
66
|
| OpenAI dispatcher routing/queue/quota/reuse | `packages/api/src/utils/providers/openai/openai-dispatcher.test.ts` |
|
|
64
67
|
| OpenAI Codex worker RPC/turn behavior | `packages/api/src/utils/providers/openai/codex-worker.test.ts` |
|