@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.
- 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/qgrid.frame.js +8 -14
- 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-quota.js +3 -2
- 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/qgrid.frame.test.ts +22 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +8 -15
- 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-quota.test.ts +7 -1
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +3 -1
- 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-D208L33c.js → index-DDSQ_6GL.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-noN5Aka6.js → logs-D2OMhBlx.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-Ber13xcA.js → monit-CYLYHrc-.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-3_xetpSK.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-C5sHCg8X.js → settings-BeCBndh_.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BhTgGnkc.js → show-BdmzN1As.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-R7obwdAV.js → tokens-KCfMOeTH.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-CRAqRZXX.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 +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +3 -3
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +13 -9
- package/skills/qgrid/references/cli-env-and-server-boot.md +1 -1
- package/skills/qgrid/references/decision-rationale.md +2 -1
- package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
- package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
- 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
|
|
61
|
-
expect(getModelCosts("claude-sonnet-5"
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
{
|
|
116
|
-
|
|
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) {
|