@cartanova/qgrid-cli 2.7.9 → 2.8.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.
Files changed (30) hide show
  1. package/bundle/dist/application/qgrid/oauth.js +5 -2
  2. package/bundle/dist/application/qgrid/qgrid.types.js +1 -1
  3. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +12 -7
  4. package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +7 -5
  5. package/bundle/src/application/qgrid/oauth.test.ts +28 -0
  6. package/bundle/src/application/qgrid/oauth.ts +5 -0
  7. package/bundle/src/application/qgrid/qgrid.types.ts +1 -1
  8. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +36 -0
  9. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +20 -7
  10. package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +22 -0
  11. package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +16 -2
  12. package/bundle/web-dist/client/assets/{index-D59Q_9Z4.js → index-D208L33c.js} +3 -3
  13. package/bundle/web-dist/client/assets/{logs-BHVfI8ut.js → logs-noN5Aka6.js} +1 -1
  14. package/bundle/web-dist/client/assets/{monit-EkLxH1oN.js → monit-Ber13xcA.js} +1 -1
  15. package/bundle/web-dist/client/assets/{routes-Kbvuclip.js → routes-3_xetpSK.js} +1 -1
  16. package/bundle/web-dist/client/assets/{settings-B4uTFP3J.js → settings-C5sHCg8X.js} +1 -1
  17. package/bundle/web-dist/client/assets/{show-BjIdSPTd.js → show-BhTgGnkc.js} +1 -1
  18. package/bundle/web-dist/client/assets/{tokens-BbCSxbFu.js → tokens-R7obwdAV.js} +1 -1
  19. package/bundle/web-dist/client/assets/{use-oauth-login-flow-4OltfVAd.js → use-oauth-login-flow-CRAqRZXX.js} +1 -1
  20. package/bundle/web-dist/client/index.html +1 -1
  21. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  22. package/package.json +1 -1
  23. package/skills/qgrid/SKILL.md +7 -2
  24. package/skills/qgrid/references/ai-sdk-provider-contract.md +5 -0
  25. package/skills/qgrid/references/anthropic-claude-code-runtime.md +5 -1
  26. package/skills/qgrid/references/decision-rationale.md +3 -1
  27. package/skills/qgrid/references/docs-workflow.md +3 -3
  28. package/skills/qgrid/references/sonamu-api-web-flow.md +4 -2
  29. package/skills/qgrid/references/token-auth-quota-lifecycle.md +5 -2
  30. package/skills/qgrid/references/verification-and-debugging.md +19 -16
@@ -26,7 +26,11 @@ import {
26
26
  import { serializeAndValidateDispatchSchema } from "../common/schema-validation";
27
27
  import { SmoothWeightedRoundRobin } from "../common/smooth-weighted-round-robin";
28
28
  import { assertSupportedOneMillionSuffix, canonicalAnthropicModel } from "./anthropic-constants";
29
- import { readAnthropicQuotaUsage, type AnthropicQuotaUsageResult } from "./anthropic-quota";
29
+ import {
30
+ invalidateAnthropicQuotaUsage,
31
+ readAnthropicQuotaUsage,
32
+ type AnthropicQuotaUsageResult,
33
+ } from "./anthropic-quota";
30
34
  import { makeAnthropicWorkerId, runClaudeSession } from "./claude-session";
31
35
 
32
36
  const logger = getLogger(["qgrid", "anthropic-dispatcher"]);
@@ -163,7 +167,7 @@ export class AnthropicDispatcher implements ProviderDispatcher {
163
167
 
164
168
  // 지정 요청은 해당 토큰만 quota 판정하고 weighted 상태를 건드리지 않는다.
165
169
  // 미지정 요청만 quota 통과 후보로 smooth weighted round-robin 을 진행한다.
166
- private async selectToken(preferredTokenId?: number): Promise<PooledToken | null> {
170
+ private async selectToken(model: string, preferredTokenId?: number): Promise<PooledToken | null> {
167
171
  const preferred =
168
172
  preferredTokenId === undefined ? undefined : this.tokenPool.get(preferredTokenId);
169
173
  if (preferredTokenId !== undefined && !preferred) {
@@ -172,7 +176,7 @@ export class AnthropicDispatcher implements ProviderDispatcher {
172
176
 
173
177
  const rows = preferred ? [preferred] : [...this.tokenPool.values()];
174
178
  if (rows.length === 0) return null;
175
- const { eligible, overThresholdTokens } = await this.filterEligibleTokens(rows);
179
+ const { eligible, overThresholdTokens } = await this.filterEligibleTokens(rows, model);
176
180
  if (eligible.length === 0) {
177
181
  logger.warn("quota_threshold gate: all_exceeded", {
178
182
  provider: "anthropic",
@@ -193,7 +197,10 @@ export class AnthropicDispatcher implements ProviderDispatcher {
193
197
  return this.tokenPool.get(selectedId) ?? null;
194
198
  }
195
199
 
196
- private async filterEligibleTokens(rows: PooledToken[]): Promise<{
200
+ private async filterEligibleTokens(
201
+ rows: PooledToken[],
202
+ model: string,
203
+ ): Promise<{
197
204
  eligible: PooledToken[];
198
205
  overThresholdTokens: Array<{ tokenName: string; threshold: number }>;
199
206
  }> {
@@ -207,7 +214,7 @@ export class AnthropicDispatcher implements ProviderDispatcher {
207
214
  const checks = await Promise.allSettled(
208
215
  thresholded.map(async (token) => ({
209
216
  token,
210
- result: await readAnthropicQuotaUsage(token.credentials.accessToken),
217
+ result: await readAnthropicQuotaUsage(token.credentials.accessToken, model),
211
218
  })),
212
219
  );
213
220
 
@@ -314,7 +321,7 @@ export class AnthropicDispatcher implements ProviderDispatcher {
314
321
  const model = canonicalAnthropicModel(req.model);
315
322
  const jsonSchema = serializeAndValidateDispatchSchema(req.outputSchema, "anthropic");
316
323
 
317
- const token = await this.selectToken(req.preferredTokenId);
324
+ const token = await this.selectToken(model, req.preferredTokenId);
318
325
  if (!token) throw new Error("No anthropic tokens available");
319
326
 
320
327
  const exec = async (): Promise<GenerateResult> => {
@@ -359,7 +366,13 @@ export class AnthropicDispatcher implements ProviderDispatcher {
359
366
  onDelta,
360
367
  );
361
368
 
362
- if (session.quotaExhausted) throw new Error(`quota exhausted (${token.name})`);
369
+ if (session.quotaExhausted) {
370
+ invalidateAnthropicQuotaUsage(token.credentials.accessToken);
371
+ if (accessToken !== token.credentials.accessToken) {
372
+ invalidateAnthropicQuotaUsage(accessToken);
373
+ }
374
+ throw new Error(`quota exhausted (${token.name})`);
375
+ }
363
376
  if (session.isError) {
364
377
  // 진단(SON-495): isError 판정 사유(subtype/terminal_reason)를 메시지 앞에 드러낸다.
365
378
  // 그동안 detail 이 session.text(완전한 JSON 본문)뿐이라 "schema 위반인지 / max retries 인지 /
@@ -34,6 +34,28 @@ describe("readAnthropicQuotaUsage", () => {
34
34
  });
35
35
  });
36
36
 
37
+ it("uses the separate Fable 5 limit only for Fable requests", async () => {
38
+ const usage = {
39
+ data: {
40
+ five_hour: { utilization: 10, resets_at: null },
41
+ seven_day_overage_included: { utilization: 100, resets_at: null },
42
+ },
43
+ cachedAt: Date.now(),
44
+ };
45
+ fetchUsageWithMetaMock.mockResolvedValue(usage);
46
+
47
+ await expect(readAnthropicQuotaUsage("access-token", "claude-fable-5")).resolves.toMatchObject({
48
+ kind: "ok",
49
+ utilizationPct: 100,
50
+ });
51
+ await expect(
52
+ readAnthropicQuotaUsage("access-token", "claude-sonnet-4-6"),
53
+ ).resolves.toMatchObject({
54
+ kind: "ok",
55
+ utilizationPct: 10,
56
+ });
57
+ });
58
+
37
59
  it("keeps low percent utilization unchanged", async () => {
38
60
  fetchUsageWithMetaMock.mockResolvedValueOnce({
39
61
  data: { five_hour: { utilization: 1, resets_at: null } },
@@ -1,4 +1,9 @@
1
- import { fetchUsageWithMeta } from "../../../application/qgrid/oauth";
1
+ import {
2
+ fetchUsageWithMeta,
3
+ invalidateAnthropicQuotaUsage,
4
+ } from "../../../application/qgrid/oauth";
5
+
6
+ export { invalidateAnthropicQuotaUsage };
2
7
 
3
8
  export type AnthropicQuotaUsageResult =
4
9
  | { kind: "ok"; utilizationPct: number; cacheAgeMs: number }
@@ -6,12 +11,21 @@ export type AnthropicQuotaUsageResult =
6
11
 
7
12
  export async function readAnthropicQuotaUsage(
8
13
  accessToken: string,
14
+ model?: string,
9
15
  ): Promise<AnthropicQuotaUsageResult> {
10
16
  try {
11
17
  const { data, cachedAt } = await fetchUsageWithMeta(accessToken);
12
18
  if (data.error) return { kind: "lookup_failed", reason: data.error };
13
19
 
14
- const utilization = data.five_hour?.utilization;
20
+ const utilizations = [data.five_hour?.utilization];
21
+ if (model === "claude-fable-5") {
22
+ utilizations.push(data.seven_day_overage_included?.utilization);
23
+ }
24
+ const utilization = Math.max(
25
+ ...utilizations.filter(
26
+ (value): value is number => typeof value === "number" && Number.isFinite(value),
27
+ ),
28
+ );
15
29
  if (typeof utilization !== "number" || !Number.isFinite(utilization)) {
16
30
  return { kind: "lookup_failed", reason: "five_hour utilization unavailable" };
17
31
  }