@cartanova/qgrid-cli 2.9.2 → 2.9.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.
Files changed (35) hide show
  1. package/bundle/dist/application/qgrid/qgrid.frame.js +4 -3
  2. package/bundle/dist/application/qgrid/token-death.js +6 -22
  3. package/bundle/dist/application/qgrid/token-subscriber.js +2 -1
  4. package/bundle/dist/application/token/token.model.js +22 -17
  5. package/bundle/dist/utils/providers/common/model-cost.js +21 -13
  6. package/bundle/src/application/qgrid/qgrid.frame.test.ts +20 -0
  7. package/bundle/src/application/qgrid/qgrid.frame.ts +3 -2
  8. package/bundle/src/application/qgrid/token-death.test.ts +11 -10
  9. package/bundle/src/application/qgrid/token-death.ts +9 -21
  10. package/bundle/src/application/qgrid/token-subscriber.test.ts +5 -1
  11. package/bundle/src/application/qgrid/token-subscriber.ts +1 -0
  12. package/bundle/src/application/token/token.model.test.ts +24 -5
  13. package/bundle/src/application/token/token.model.ts +30 -23
  14. package/bundle/src/utils/providers/common/effort.test.ts +8 -0
  15. package/bundle/src/utils/providers/common/model-cost.test.ts +25 -12
  16. package/bundle/src/utils/providers/common/model-cost.ts +28 -14
  17. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +1 -0
  18. package/bundle/web-dist/client/assets/{index-DH1n8kgG.js → index-fogJvajr.js} +3 -3
  19. package/bundle/web-dist/client/assets/{logs-D36O-NET.js → logs-CsY2cqvt.js} +1 -1
  20. package/bundle/web-dist/client/assets/{monit-DhZAaz9K.js → monit-ChgaLEcD.js} +1 -1
  21. package/bundle/web-dist/client/assets/{routes-C7q1CsSv.js → routes-BvyiTnOZ.js} +2 -2
  22. package/bundle/web-dist/client/assets/{settings-M_jfoIAe.js → settings-ZR2J9rWb.js} +1 -1
  23. package/bundle/web-dist/client/assets/{show-CORjMGQs.js → show-CjwO4-So.js} +1 -1
  24. package/bundle/web-dist/client/assets/{tokens-C7-j4VXD.js → tokens-Bx-hG1gm.js} +1 -1
  25. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Fi62xbBc.js → use-oauth-login-flow-Bqax_icS.js} +1 -1
  26. package/bundle/web-dist/client/index.html +1 -1
  27. package/bundle/web-dist/server/assets/{routes-CxMHXfdH.js → routes-B408iqh1.js} +29 -14
  28. package/bundle/web-dist/server/entry-server.generated.js +3 -2
  29. package/package.json +1 -1
  30. package/skills/qgrid/SKILL.md +1 -0
  31. package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
  32. package/skills/qgrid/references/openai-codex-runtime.md +71 -0
  33. package/skills/qgrid/references/provider-runtime-differences.md +1 -1
  34. package/skills/qgrid/references/sonamu-api-web-flow.md +1 -1
  35. package/skills/qgrid/references/token-auth-quota-lifecycle.md +3 -2
@@ -267,14 +267,13 @@ class TokenModelClass extends BaseModelClass<
267
267
  }
268
268
 
269
269
  /**
270
- * 인증 사망을 한 번만 기록하고, 다른 활성 토큰이 있으면 라우팅에서도 제외한다.
271
- * 마지막 활성 토큰은 systemic 장애 안전판 때문에 active 상태를 유지하되 재로그인
272
- * 필요 상태는 기록한다. `marked`가 공유 DB 인스턴스들의 알림 발송 게이트다.
270
+ * Record confirmed expiry and remove the token from routing atomically.
271
+ * The last active token is also deactivated; its notification is urgent.
273
272
  */
274
273
  async markReauthRequired(
275
274
  id: number,
276
275
  expectedCredentials: TokenCredentials,
277
- ): Promise<{ marked: boolean; keptAsLastActive: boolean; staleCredentials: boolean }> {
276
+ ): Promise<{ marked: boolean; wasLastActive: boolean; staleCredentials: boolean }> {
278
277
  const wdb = this.getPuri("w");
279
278
  return wdb.transaction(async (trx) => {
280
279
  const token = await trx
@@ -288,42 +287,50 @@ class TokenModelClass extends BaseModelClass<
288
287
  .forUpdate()
289
288
  .first();
290
289
  if (!token || token.reauth_required) {
291
- return { marked: false, keptAsLastActive: false, staleCredentials: false };
290
+ return { marked: false, wasLastActive: false, staleCredentials: false };
292
291
  }
293
292
  if (!isDeepStrictEqual(token.credentials, expectedCredentials)) {
294
- return { marked: false, keptAsLastActive: false, staleCredentials: true };
293
+ return { marked: false, wasLastActive: false, staleCredentials: true };
295
294
  }
296
295
 
297
296
  await trx.knex.raw("SELECT pg_advisory_xact_lock(?, hashtext(?))", [
298
297
  TOKEN_AUTH_DEATH_LOCK_CLASS_ID,
299
298
  token.provider,
300
299
  ]);
301
- const updated = await trx.knex.raw<{ rows: { active: boolean }[] }>(
302
- `UPDATE tokens
300
+ const updated = await trx.knex.raw<{ rows: { was_last_active: boolean }[] }>(
301
+ `WITH previous AS (
302
+ SELECT active AND NOT EXISTS (
303
+ SELECT 1 FROM tokens peer
304
+ WHERE peer.provider = tokens.provider AND peer.active
305
+ AND NOT peer.reauth_required AND peer.id <> tokens.id
306
+ ) AS was_last_active FROM tokens WHERE id = ?
307
+ )
308
+ UPDATE tokens
303
309
  SET reauth_required = true,
304
- active = CASE
305
- WHEN active AND (SELECT count(*) FROM tokens peer
306
- WHERE peer.provider = tokens.provider AND peer.active) > 1
307
- THEN false
308
- ELSE active
309
- END
310
+ active = false
310
311
  WHERE id = ? AND reauth_required = false
311
- RETURNING active`,
312
- [id],
312
+ RETURNING (SELECT was_last_active FROM previous) AS was_last_active`,
313
+ [id, id],
313
314
  );
314
315
  const marked = updated.rows[0];
315
- if (!marked) return { marked: false, keptAsLastActive: false, staleCredentials: false };
316
+ if (!marked) return { marked: false, wasLastActive: false, staleCredentials: false };
316
317
 
317
- if (marked.active) {
318
- logger.error(
319
- `refusing to auto-deactivate token ${id}: last active provider token — ` +
320
- `systemic refresh failure suspected, keeping the pool non-empty`,
321
- );
318
+ if (marked.was_last_active) {
319
+ logger.error(`deactivated expired token ${id}: no usable provider tokens remain`);
322
320
  }
323
- return { marked: true, keptAsLastActive: marked.active, staleCredentials: false };
321
+ return { marked: true, wasLastActive: marked.was_last_active, staleCredentials: false };
324
322
  });
325
323
  }
326
324
 
325
+ /** Repair rows retained by the former last-active exception before pool reconciliation. */
326
+ async deactivateExpiredTokens(): Promise<void> {
327
+ await this.getPuri("w")
328
+ .table("tokens")
329
+ .where("reauth_required", true)
330
+ .where("active", true)
331
+ .update({ active: false });
332
+ }
333
+
327
334
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
328
335
  async reorder(ids: number[]): Promise<{ done: boolean }> {
329
336
  const wdb = this.getPuri("w");
@@ -8,6 +8,14 @@ import {
8
8
  } from "./effort";
9
9
 
10
10
  describe("resolveOpenAIEffort", () => {
11
+ it("preserves every Astra Codex catalog effort including ultra", () => {
12
+ for (const effort of OPENAI_EFFORTS) {
13
+ expect(resolveOpenAIEffort("gpt-6-astra", effort)).toBe(effort);
14
+ }
15
+ expect(resolveOpenAIEffort("gpt-6-astra", "none")).toBeUndefined();
16
+ expect(resolveOpenAIEffort("gpt-6-astra", "minimal")).toBeUndefined();
17
+ });
18
+
11
19
  it("Codex 카탈로그 어휘는 그대로 통과한다", () => {
12
20
  for (const effort of ["low", "medium", "high", "xhigh"]) {
13
21
  expect(resolveOpenAIEffort("gpt-5.5", effort)).toBe(effort);
@@ -4,9 +4,18 @@ import { calculateCostUsd, getModelCosts } from "./model-cost";
4
4
 
5
5
  describe("calculateCostUsd", () => {
6
6
  it.each([
7
- ["gpt-5.6-sol", 5, 30, 0.5, 6.25],
8
- ["gpt-5.6-terra", 2.5, 15, 0.25, 3.125],
9
- ["gpt-5.6-luna", 1, 6, 0.1, 1.25],
7
+ [272_000, 2.77],
8
+ [272_001, 5.51502],
9
+ ])("Astra applies the long-context rate only above 272K (%i input)", (inputTokens, expected) => {
10
+ expect(
11
+ calculateCostUsd("openai/gpt-6-astra", { inputTokens, outputTokens: 1_000 }),
12
+ ).toBeCloseTo(expected, 10);
13
+ });
14
+ it.each([
15
+ ["gpt-6-astra", 10, 50, 1, 12.5],
16
+ ["gpt-5.6-sol", 4, 20, 0.4, 5],
17
+ ["gpt-5.6-terra", 2, 12, 0.2, 2.5],
18
+ ["gpt-5.6-luna", 0.2, 1.2, 0.02, 0.25],
10
19
  ["gpt-5.5", 5, 30, 0.5, undefined],
11
20
  ["gpt-5.4", 2.5, 15, 0.25, undefined],
12
21
  ["gpt-5.4-mini", 0.75, 4.5, 0.075, undefined],
@@ -77,13 +86,15 @@ describe("calculateCostUsd", () => {
77
86
  expect(getModelCosts("anthropic/claude-sonnet-4-6[1m]")).toBe(
78
87
  getModelCosts("claude-sonnet-4-6"),
79
88
  );
89
+ expect(getModelCosts("openai/gpt-6-astra")).toBe(getModelCosts("gpt-6-astra"));
80
90
  expect(getModelCosts("openai/gpt-5.6-sol")).toBe(getModelCosts("gpt-5.6-sol"));
81
91
  });
82
92
 
83
93
  it.each([
84
- ["gpt-5.6-sol", 5, 30, 0.5, 6.25, 3.30625],
85
- ["gpt-5.6-terra", 2.5, 15, 0.25, 3.125, 1.653125],
86
- ["gpt-5.6-luna", 1, 6, 0.1, 1.25, 0.66125],
94
+ ["gpt-6-astra", 10, 50, 1, 12.5, 5.6125],
95
+ ["gpt-5.6-sol", 4, 20, 0.4, 5, 2.245],
96
+ ["gpt-5.6-terra", 2, 12, 0.2, 2.5, 1.3225],
97
+ ["gpt-5.6-luna", 0.2, 1.2, 0.02, 0.25, 0.13225],
87
98
  ])(
88
99
  "%s applies published input, output, cache-read, and cache-write pricing",
89
100
  (model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens, expectedCost) => {
@@ -106,9 +117,10 @@ describe("calculateCostUsd", () => {
106
117
  );
107
118
 
108
119
  it.each([
109
- ["gpt-5.6-sol", 1.245],
110
- ["gpt-5.6-terra", 0.6225],
111
- ["gpt-5.6-luna", 0.249],
120
+ ["gpt-6-astra", 2.475],
121
+ ["gpt-5.6-sol", 0.99],
122
+ ["gpt-5.6-terra", 0.498],
123
+ ["gpt-5.6-luna", 0.0498],
112
124
  ])("%s applies the published long-context surcharge", (model, expectedCost) => {
113
125
  expect(
114
126
  calculateCostUsd(model, {
@@ -120,9 +132,10 @@ describe("calculateCostUsd", () => {
120
132
  });
121
133
 
122
134
  it.each([
123
- ["gpt-5.6-sol", 1.37],
124
- ["gpt-5.6-terra", 0.685],
125
- ["gpt-5.6-luna", 0.274],
135
+ ["gpt-6-astra", 2.725],
136
+ ["gpt-5.6-sol", 1.09],
137
+ ["gpt-5.6-terra", 0.548],
138
+ ["gpt-5.6-luna", 0.0548],
126
139
  ])("%s applies the long-context input multiplier to cache writes", (model, expectedCost) => {
127
140
  expect(
128
141
  calculateCostUsd(model, {
@@ -34,7 +34,7 @@ export interface ModelCosts {
34
34
 
35
35
  // ── OpenAI ─────────────────────────────────────────────────────────
36
36
  //
37
- // 가격 출처: https://developers.openai.com/api/docs/models (2026-07-18 확인)
37
+ // Standard pricing verified 2026-09-07: https://developers.openai.com/api/docs/pricing
38
38
  // 신모델 출시마다 단가가 바뀌므로(5.2→5.4→5.5) 모델 추가 시 반드시 공식 페이지 재확인해야함
39
39
 
40
40
  // GPT-5.4에서 처음 도입된 long-context 할증 (5.2/5.3-codex는 해당 없음, 5.4-mini/nano는 공식 표에서 long-context 단가 없음)
@@ -48,38 +48,52 @@ const LONG_CONTEXT_272K: NonNullable<ModelCosts["longContext"]> = {
48
48
  };
49
49
 
50
50
  // OpenAI 모델 카탈로그. 단가와 함께 Codex 백엔드가 모델별로 받는 최대 reasoning effort 를 한 곳에 둔다.
51
- // maxEffort 출처: ~/.codex/models_cache.json 의 supported_reasoning_levels (2026-09-02 확인).
51
+ // maxEffort 출처: ~/.codex/models_cache.json 의 supported_reasoning_levels (2026-09-07 확인).
52
52
  // 미기재 모델은 xhigh 까지만 받는다.
53
53
  type OpenAIModelSpec = ModelCosts & { maxEffort?: OpenAIEffort };
54
54
 
55
55
  const OPENAI_COSTS: Record<string, OpenAIModelSpec> = {
56
+ // GPT-6 Astra: Standard API-equivalent pricing, verified 2026-09-07.
57
+ // Codex subscription catalog supports ultra; the public API lists only through max.
58
+ // @see https://developers.openai.com/api/docs/models/gpt-6-astra
59
+ // @see https://developers.openai.com/api/docs/pricing
60
+ "gpt-6-astra": {
61
+ maxEffort: "ultra",
62
+ inputTokens: 10,
63
+ outputTokens: 50,
64
+ cachedInputTokens: 1,
65
+ cacheCreationInputTokens: 12.5,
66
+ longContext: LONG_CONTEXT_272K,
67
+ },
56
68
  // GPT-5.6 Sol, Terra, Luna. cache write 단가는 외부 logger/manual usage 입력을 위해
57
69
  // 유지한다. 현재 OpenAI 응답 usage 에 cache write 토큰 필드가 없으면 비용은 0으로 계산된다.
58
70
  // @see https://developers.openai.com/api/docs/models/gpt-5.6-sol
59
71
  // @see https://developers.openai.com/api/docs/models/gpt-5.6-terra
60
72
  // @see https://developers.openai.com/api/docs/models/gpt-5.6-luna
73
+ // Sol promotional rates are available at least through 2026-11-21.
74
+ // No confirmed end date/replacement rate: recheck official pricing instead of auto-reverting.
61
75
  "gpt-5.6-sol": {
62
76
  maxEffort: "ultra",
63
- inputTokens: 5,
64
- outputTokens: 30,
65
- cachedInputTokens: 0.5,
66
- cacheCreationInputTokens: 6.25,
77
+ inputTokens: 4,
78
+ outputTokens: 20,
79
+ cachedInputTokens: 0.4,
80
+ cacheCreationInputTokens: 5,
67
81
  longContext: LONG_CONTEXT_272K,
68
82
  },
69
83
  "gpt-5.6-terra": {
70
84
  maxEffort: "ultra",
71
- inputTokens: 2.5,
72
- outputTokens: 15,
73
- cachedInputTokens: 0.25,
74
- cacheCreationInputTokens: 3.125,
85
+ inputTokens: 2,
86
+ outputTokens: 12,
87
+ cachedInputTokens: 0.2,
88
+ cacheCreationInputTokens: 2.5,
75
89
  longContext: LONG_CONTEXT_272K,
76
90
  },
77
91
  "gpt-5.6-luna": {
78
92
  maxEffort: "max",
79
- inputTokens: 1,
80
- outputTokens: 6,
81
- cachedInputTokens: 0.1,
82
- cacheCreationInputTokens: 1.25,
93
+ inputTokens: 0.2,
94
+ outputTokens: 1.2,
95
+ cachedInputTokens: 0.02,
96
+ cacheCreationInputTokens: 0.25,
83
97
  longContext: LONG_CONTEXT_272K,
84
98
  },
85
99
  // https://openai.com/index/introducing-gpt-5-5/ (2026-04 출시)
@@ -430,6 +430,7 @@ describe("effort 어휘 해석 (Codex 카탈로그 기준)", () => {
430
430
  }
431
431
 
432
432
  it("모델이 지원하는 값은 그대로 보내고, 상한 초과·공개 API 어휘는 reasoning 에서 빼 백엔드 기본값을 쓴다", async () => {
433
+ await expect(mappedEffortFor("gpt-6-astra", "ultra")).resolves.toBe("ultra");
433
434
  await expect(mappedEffortFor("gpt-5.6-terra", "ultra")).resolves.toBe("ultra");
434
435
  await expect(mappedEffortFor("gpt-5.5", "xhigh")).resolves.toBe("xhigh");
435
436
  await expect(mappedEffortFor("gpt-5.5", "max")).resolves.toBeUndefined();