@cartanova/qgrid-cli 2.8.0 → 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.
Files changed (75) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/bundle/dist/application/qgrid/expired-token-reminder.js +9 -8
  4. package/bundle/dist/application/qgrid/qgrid.frame.js +8 -14
  5. package/bundle/dist/application/qgrid/token-death.js +12 -31
  6. package/bundle/dist/application/qgrid/token-trigger-setup.js +2 -1
  7. package/bundle/dist/application/sonamu.generated.js +3 -1
  8. package/bundle/dist/application/sonamu.generated.sso.js +2 -1
  9. package/bundle/dist/application/token/token.model.js +66 -47
  10. package/bundle/dist/application/token/token.types.js +2 -1
  11. package/bundle/dist/i18n/sd.generated.js +2 -1
  12. package/bundle/dist/migrations/20260901163614_alter_tokens_add1_alter1.js +15 -0
  13. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +13 -3
  14. package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +3 -2
  15. package/bundle/dist/utils/providers/common/model-cost.js +9 -11
  16. package/bundle/dist/utils/providers/openai/openai-refresh.js +6 -4
  17. package/bundle/src/application/qgrid/expired-token-reminder.test.ts +8 -6
  18. package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -8
  19. package/bundle/src/application/qgrid/qgrid.frame.test.ts +22 -0
  20. package/bundle/src/application/qgrid/qgrid.frame.ts +8 -15
  21. package/bundle/src/application/qgrid/token-death.test.ts +62 -58
  22. package/bundle/src/application/qgrid/token-death.ts +15 -39
  23. package/bundle/src/application/qgrid/token-trigger-setup.test.ts +6 -0
  24. package/bundle/src/application/qgrid/token-trigger-setup.ts +1 -0
  25. package/bundle/src/application/sonamu.generated.http +0 -27
  26. package/bundle/src/application/sonamu.generated.sso.ts +1 -0
  27. package/bundle/src/application/sonamu.generated.ts +3 -0
  28. package/bundle/src/application/token/token.entity.json +7 -0
  29. package/bundle/src/application/token/token.model.test.ts +112 -37
  30. package/bundle/src/application/token/token.model.ts +76 -48
  31. package/bundle/src/application/token/token.types.ts +1 -0
  32. package/bundle/src/i18n/sd.generated.ts +1 -0
  33. package/bundle/src/migrations/20260901163614_alter_tokens_add1_alter1.ts +15 -0
  34. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +25 -1
  35. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +23 -4
  36. package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +7 -1
  37. package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +3 -1
  38. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +14 -10
  39. package/bundle/src/utils/providers/common/model-cost.test.ts +19 -19
  40. package/bundle/src/utils/providers/common/model-cost.ts +19 -16
  41. package/bundle/src/utils/providers/openai/openai-refresh.test.ts +16 -7
  42. package/bundle/src/utils/providers/openai/openai-refresh.ts +2 -2
  43. package/bundle/web-dist/client/assets/{index-D208L33c.js → index-DDSQ_6GL.js} +4 -4
  44. package/bundle/web-dist/client/assets/{logs-noN5Aka6.js → logs-D2OMhBlx.js} +1 -1
  45. package/bundle/web-dist/client/assets/{monit-Ber13xcA.js → monit-CYLYHrc-.js} +1 -1
  46. package/bundle/web-dist/client/assets/{routes-3_xetpSK.js → routes-DlG0sJHR.js} +2 -2
  47. package/bundle/web-dist/client/assets/{services.generated-Da1Eq5_R.js → services.generated-C6jfEwfK.js} +1 -1
  48. package/bundle/web-dist/client/assets/{settings-C5sHCg8X.js → settings-BeCBndh_.js} +1 -1
  49. package/bundle/web-dist/client/assets/{show-BhTgGnkc.js → show-BdmzN1As.js} +1 -1
  50. package/bundle/web-dist/client/assets/{tokens-R7obwdAV.js → tokens-KCfMOeTH.js} +1 -1
  51. package/bundle/web-dist/client/assets/{use-oauth-login-flow-CRAqRZXX.js → use-oauth-login-flow-2dSjIvBk.js} +1 -1
  52. package/bundle/web-dist/client/assets/{use-update-token-mutation-CZ57EKA1.js → use-update-token-mutation-BIvO70-M.js} +1 -1
  53. package/bundle/web-dist/client/index.html +2 -2
  54. package/bundle/web-dist/server/assets/{lazyRouteComponent-V1S8gqMS.js → lazyRouteComponent-C_k4R5n6.js} +2 -2
  55. package/bundle/web-dist/server/assets/{logs-2Tkqcncq.js → logs-lBOU5eUP.js} +2 -2
  56. package/bundle/web-dist/server/assets/{monit-DrJ15xJL.js → monit-BW2X1D23.js} +2 -2
  57. package/bundle/web-dist/server/assets/{plus-DCai02Na.js → plus-DilmHifT.js} +1 -1
  58. package/bundle/web-dist/server/assets/{react-dom-BFiG5Daf.js → react-dom-CHRRsKwb.js} +1 -1
  59. package/bundle/web-dist/server/assets/{routes-Ct3I9263.js → routes-CxMHXfdH.js} +5 -5
  60. package/bundle/web-dist/server/assets/{services.generated-y7xVOTCo.js → services.generated-buX3cJzR.js} +1 -9
  61. package/bundle/web-dist/server/assets/{settings-JznSjwGQ.js → settings-CFoXQaAg.js} +3 -3
  62. package/bundle/web-dist/server/assets/{show-D8kuAFbc.js → show-CcZJO6WF.js} +2 -2
  63. package/bundle/web-dist/server/assets/{tokens-PR42jF1J.js → tokens-C32lycTe.js} +6 -5
  64. package/bundle/web-dist/server/assets/{use-oauth-login-flow-C-33Z5HA.js → use-oauth-login-flow-DCjxIa80.js} +1 -1
  65. package/bundle/web-dist/server/assets/{use-update-token-mutation-BNaRPVaX.js → use-update-token-mutation-CqK8i8Fa.js} +1 -1
  66. package/bundle/web-dist/server/entry-server.generated.js +46 -23
  67. package/package.json +1 -1
  68. package/skills/qgrid/SKILL.md +1 -1
  69. package/skills/qgrid/references/ai-sdk-provider-contract.md +3 -3
  70. package/skills/qgrid/references/anthropic-claude-code-runtime.md +13 -9
  71. package/skills/qgrid/references/cli-env-and-server-boot.md +1 -1
  72. package/skills/qgrid/references/decision-rationale.md +2 -1
  73. package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
  74. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
  75. package/skills/qgrid/references/token-auth-quota-lifecycle.md +17 -13
@@ -30,6 +30,8 @@ describe("calculateCostUsd", () => {
30
30
  );
31
31
 
32
32
  it.each([
33
+ // Fable 5.1 은 cache read 만 0.025x 특례($0.25), 나머지 배율은 표준과 같다.
34
+ ["claude-fable-5-1", 10, 50, 0.25, 20],
33
35
  ["claude-fable-5", 10, 50, 1, 20],
34
36
  ["claude-haiku-4-5", 1, 5, 0.1, 2],
35
37
  ["claude-sonnet-4", 3, 15, 0.3, 6],
@@ -43,6 +45,7 @@ describe("calculateCostUsd", () => {
43
45
  ["claude-opus-4-7", 5, 25, 0.5, 10],
44
46
  ["claude-opus-4-8", 5, 25, 0.5, 10],
45
47
  ["claude-opus-5", 5, 25, 0.5, 10],
48
+ ["claude-sonnet-5", 2, 10, 0.2, 4],
46
49
  ])(
47
50
  "%s official Anthropic rates for 5m/1h cache writes",
48
51
  (model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens) => {
@@ -57,8 +60,8 @@ describe("calculateCostUsd", () => {
57
60
  },
58
61
  );
59
62
 
60
- it("Claude Sonnet 5 introductory pricing switches to standard pricing on 2026-09-01", () => {
61
- expect(getModelCosts("claude-sonnet-5", Date.UTC(2026, 7, 31))).toEqual({
63
+ it("Claude Sonnet 5 는 introductory $2/$10 이 정식 단가라 날짜 분기 없이 고정된다", () => {
64
+ expect(getModelCosts("claude-sonnet-5")).toEqual({
62
65
  inputTokens: 2,
63
66
  outputTokens: 10,
64
67
  cachedInputTokens: 0.2,
@@ -66,18 +69,11 @@ describe("calculateCostUsd", () => {
66
69
  cacheCreationInputTokens5m: 2.5,
67
70
  cacheCreationInputTokens1h: 4,
68
71
  });
69
- expect(getModelCosts("claude-sonnet-5", Date.UTC(2026, 8, 1))).toEqual({
70
- inputTokens: 3,
71
- outputTokens: 15,
72
- cachedInputTokens: 0.3,
73
- cacheCreationInputTokens: 6,
74
- cacheCreationInputTokens5m: 3.75,
75
- cacheCreationInputTokens1h: 6,
76
- });
77
72
  });
78
73
 
79
74
  it("provider prefix 와 [1m] suffix 를 제거한 canonical model 로 가격을 찾는다", () => {
80
75
  expect(getModelCosts("anthropic/claude-fable-5")).toBe(getModelCosts("claude-fable-5"));
76
+ expect(getModelCosts("anthropic/claude-fable-5-1[1m]")).toBe(getModelCosts("claude-fable-5-1"));
81
77
  expect(getModelCosts("anthropic/claude-sonnet-4-6[1m]")).toBe(
82
78
  getModelCosts("claude-sonnet-4-6"),
83
79
  );
@@ -184,6 +180,17 @@ describe("calculateCostUsd", () => {
184
180
  ).toBeCloseTo(5.8, 10);
185
181
  });
186
182
 
183
+ it("Fable 5.1 은 cache read 만 $0.25 로 계산하고 나머지 단가는 Fable 5 와 같다", () => {
184
+ const usage = {
185
+ inputTokens: 100_000,
186
+ outputTokens: 100_000,
187
+ cachedInputTokens: 50_000,
188
+ cacheCreationInputTokens: 25_000,
189
+ };
190
+ // Fable 5: 5.8. cache read 50K 가 $1 → $0.25 로 바뀌면 0.05 → 0.0125 만큼 줄어든다.
191
+ expect(calculateCostUsd("claude-fable-5-1", usage)).toBeCloseTo(5.7625, 10);
192
+ });
193
+
187
194
  it("Fable 의 5분/1시간 cache write 단가도 구분한다", () => {
188
195
  expect(
189
196
  calculateCostUsd("claude-fable-5", {
@@ -197,21 +204,14 @@ describe("calculateCostUsd", () => {
197
204
  ).toBeCloseTo(5.725, 10);
198
205
  });
199
206
 
200
- it("Sonnet 5 introductory/standard 단가 전환을 cost 계산에도 적용한다", () => {
207
+ it("Sonnet 5 는 $2/$10 단가로 cost 를 계산한다", () => {
201
208
  const usage = {
202
209
  inputTokens: 100_000,
203
210
  outputTokens: 100_000,
204
211
  cachedInputTokens: 50_000,
205
212
  cacheCreationInputTokens: 25_000,
206
213
  };
207
- expect(calculateCostUsd("claude-sonnet-5", usage, Date.UTC(2026, 7, 31))).toBeCloseTo(
208
- 1.16,
209
- 10,
210
- );
211
- expect(calculateCostUsd("claude-sonnet-5", usage, Date.UTC(2026, 8, 1))).toBeCloseTo(
212
- 1.74,
213
- 10,
214
- );
214
+ expect(calculateCostUsd("claude-sonnet-5", usage)).toBeCloseTo(1.16, 10);
215
215
  });
216
216
 
217
217
  it("legacy/원시 Anthropic usage 처럼 cache 가 input 보다 커도 음수 비용을 만들지 않는다", () => {
@@ -79,6 +79,9 @@ const OPENAI_COSTS: Record<string, ModelCosts> = {
79
79
  cachedInputTokens: 0.5,
80
80
  longContext: LONG_CONTEXT_272K,
81
81
  },
82
+ // 아래 gpt-5.4 / gpt-5.4-mini / gpt-5.3-codex / gpt-5.2 는 qgrid 가 쓰는 ChatGPT 구독 Codex 경로에서
83
+ // 더 이상 제공되지 않는다(5.4 계열은 2026-08-31 retired, 대체 gpt-5.6-terra / gpt-5.6-luna).
84
+ // 과거 request log 재산정(cost_source 없는 legacy 행)에 필요하므로 단가 행은 유지한다.
82
85
  // https://openai.com/index/introducing-gpt-5-4/ (2026-03 출시)
83
86
  "gpt-5.4": {
84
87
  inputTokens: 2.5,
@@ -95,11 +98,16 @@ const OPENAI_COSTS: Record<string, ModelCosts> = {
95
98
 
96
99
  // ── Anthropic ───────────────────────────────────────────────────────
97
100
 
98
- function anthropicCosts(inputTokens: number, outputTokens: number): ModelCosts {
101
+ function anthropicCosts(
102
+ inputTokens: number,
103
+ outputTokens: number,
104
+ overrides: Pick<Partial<ModelCosts>, "cachedInputTokens"> = {},
105
+ ): ModelCosts {
99
106
  return {
100
107
  inputTokens,
101
108
  outputTokens,
102
- cachedInputTokens: inputTokens / 10,
109
+ // 표준 cache read 는 base input 의 0.1x. Fable 5.1 처럼 공식 특례 단가가 있으면 override 한다.
110
+ cachedInputTokens: overrides.cachedInputTokens ?? inputTokens / 10,
103
111
  // Claude Code subscription OAuth 는 1h TTL 을 자동 선택한다. 구버전처럼 응답에
104
112
  // TTL breakdown 이 없을 때만 이 1h 단가를 fallback 으로 사용한다.
105
113
  cacheCreationInputTokens: inputTokens * 2,
@@ -108,7 +116,11 @@ function anthropicCosts(inputTokens: number, outputTokens: number): ModelCosts {
108
116
  };
109
117
  }
110
118
 
119
+ // 가격 출처: https://platform.claude.com/docs/en/about-claude/pricing (2026-09-02 확인)
111
120
  const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
121
+ // Fable 5.1 (2026-09-01 출시): input/output 은 Fable 5 와 같고 cache read 만 0.025x($0.25) 특례.
122
+ // @see https://platform.claude.com/docs/en/models/fable-5-1/overview
123
+ "claude-fable-5-1": anthropicCosts(10, 50, { cachedInputTokens: 0.25 }),
112
124
  "claude-fable-5": anthropicCosts(10, 50),
113
125
  sonnet: anthropicCosts(3, 15),
114
126
  "claude-3-5-haiku": anthropicCosts(0.8, 4),
@@ -126,26 +138,18 @@ const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
126
138
  "claude-opus-4-7": anthropicCosts(5, 25),
127
139
  "claude-opus-4-8": anthropicCosts(5, 25),
128
140
  "claude-opus-5": anthropicCosts(5, 25),
141
+ // Sonnet 5 의 introductory $2/$10 이 정식 단가로 확정됐다. 2026-09-01 에 예정됐던 $3/$15 인상은
142
+ // 취소됐으므로 날짜 분기 없이 고정 단가로 계산한다.
143
+ "claude-sonnet-5": anthropicCosts(2, 10),
129
144
  };
130
145
 
131
- // Anthropic 공식 introductory pricing: 2026-08-31까지 $2/$10, 이후 $3/$15.
132
- // qgrid 는 요청 시점에 계산하므로 배포를 다시 하지 않아도 2026-09-01 UTC부터 표준 단가로 전환한다.
133
- const CLAUDE_SONNET_5_INTRO_PRICING_END_MS = Date.UTC(2026, 8, 1);
134
- const CLAUDE_SONNET_5_INTRO_COSTS = anthropicCosts(2, 10);
135
- const CLAUDE_SONNET_5_STANDARD_COSTS = anthropicCosts(3, 15);
136
-
137
146
  // gpt-5.3-codex-spark 는 research preview 로 공식 token 단가가 아직 final 이 아니다.
138
147
  // 지원 타입은 유지하되, 공식 단가가 공개될 때까지 아래 generic estimate 로 계산한다.
139
148
  // @see https://help.openai.com/en/articles/20001106-codex-rate-card
140
149
  const DEFAULT_COSTS: ModelCosts = { inputTokens: 3, outputTokens: 15, cachedInputTokens: 0.3 };
141
150
 
142
- export function getModelCosts(model: string, atMs = Date.now()): ModelCosts {
151
+ export function getModelCosts(model: string): ModelCosts {
143
152
  const normalizedModel = (model.split("/").pop() ?? model).replace(/\[1m\]$/i, "");
144
- if (normalizedModel === "claude-sonnet-5") {
145
- return atMs < CLAUDE_SONNET_5_INTRO_PRICING_END_MS
146
- ? CLAUDE_SONNET_5_INTRO_COSTS
147
- : CLAUDE_SONNET_5_STANDARD_COSTS;
148
- }
149
153
  return OPENAI_COSTS[normalizedModel] ?? ANTHROPIC_COSTS[normalizedModel] ?? DEFAULT_COSTS;
150
154
  }
151
155
 
@@ -159,9 +163,8 @@ export function calculateCostUsd(
159
163
  cacheCreationInputTokens5m?: number;
160
164
  cacheCreationInputTokens1h?: number;
161
165
  },
162
- atMs = Date.now(),
163
166
  ): number {
164
- const costs = getModelCosts(model, atMs);
167
+ const costs = getModelCosts(model);
165
168
  const cachedInput = usage.cachedInputTokens ?? 0;
166
169
  const cacheCreationInput5m = Math.max(usage.cacheCreationInputTokens5m ?? 0, 0);
167
170
  const cacheCreationInput1h = Math.max(usage.cacheCreationInputTokens1h ?? 0, 0);
@@ -1,10 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
- const { findOneMock, saveMock, deactivateMock } = vi.hoisted(() => ({
4
- findOneMock: vi.fn(),
5
- saveMock: vi.fn(),
6
- deactivateMock: vi.fn(),
7
- }));
3
+ const { findOneMock, saveMock, deactivateMock } = vi.hoisted(() => {
4
+ // isolate:false 공유 레지스트리에 실제 TokenModel 이 먼저 올라와도 이 파일의 mock을 보장한다.
5
+ vi.resetModules();
6
+ return {
7
+ findOneMock: vi.fn(),
8
+ saveMock: vi.fn(),
9
+ deactivateMock: vi.fn(),
10
+ };
11
+ });
8
12
 
9
13
  vi.mock("../../../application/token/token.model", () => ({
10
14
  TokenModel: { findOne: findOneMock, save: saveMock },
@@ -80,6 +84,7 @@ describe("OpenAI refresh", () => {
80
84
  expect(saveMock).toHaveBeenCalledWith([
81
85
  expect.objectContaining({
82
86
  id: 10,
87
+ reauth_required: false,
83
88
  credentials: expect.objectContaining({
84
89
  accessToken: "new-access",
85
90
  refreshToken: "new-refresh",
@@ -112,8 +117,12 @@ describe("OpenAI refresh", () => {
112
117
 
113
118
  await expect(handleChatgptAuthTokensRefresh(11)).rejects.toThrow("OpenAI refresh failed: 401");
114
119
  expect(deactivateMock).toHaveBeenCalledWith(
115
- { id: 11, name: "token-11", provider: "openai" },
116
- "old-refresh",
120
+ {
121
+ id: 11,
122
+ name: "token-11",
123
+ provider: "openai",
124
+ credentials: expect.objectContaining({ refreshToken: "old-refresh" }),
125
+ },
117
126
  "openai:refresh_token_reused",
118
127
  );
119
128
  });
@@ -114,8 +114,7 @@ async function doRefresh(tokenId: number): Promise<RefreshResult> {
114
114
  // 상태코드를 바꿀 때 판정이 조용히 멎는다.
115
115
  if (errorCode && PERMANENT_FAILURE_CODES.has(errorCode)) {
116
116
  await deactivateAuthDeadToken(
117
- { id: tokenId, name: token.name, provider: "openai" },
118
- creds.refreshToken,
117
+ { id: tokenId, name: token.name, provider: "openai", credentials: creds },
119
118
  `openai:${errorCode}`,
120
119
  );
121
120
  }
@@ -158,6 +157,7 @@ async function doRefresh(tokenId: number): Promise<RefreshResult> {
158
157
  provider: "openai",
159
158
  credentials: updatedCreds,
160
159
  name: token.name,
160
+ reauth_required: false,
161
161
  },
162
162
  ]);
163
163
  } catch (e) {