@cartanova/qgrid-cli 2.3.2 → 2.3.3

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 (26) hide show
  1. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +91 -278
  2. package/bundle/dist/application/qgrid/qgrid.frame.js +15 -5
  3. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +33 -12
  4. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +39 -96
  5. package/bundle/dist/utils/providers/anthropic/claude-session.js +47 -32
  6. package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +3 -36
  7. package/bundle/dist/utils/providers/common/model-cost.js +3 -2
  8. package/bundle/dist/utils/providers/openai/codex-worker.js +1 -2
  9. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +19 -12
  10. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +193 -6
  11. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +140 -364
  12. package/bundle/src/application/qgrid/qgrid.frame.ts +38 -23
  13. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +60 -0
  14. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +57 -16
  15. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +92 -340
  16. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +50 -185
  17. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +71 -62
  18. package/bundle/src/utils/providers/anthropic/claude-session.ts +56 -46
  19. package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +19 -30
  20. package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +24 -101
  21. package/bundle/src/utils/providers/common/model-cost.test.ts +19 -1
  22. package/bundle/src/utils/providers/common/model-cost.ts +2 -1
  23. package/bundle/src/utils/providers/common/provider-dispatcher.ts +7 -4
  24. package/bundle/src/utils/providers/openai/codex-worker.ts +2 -8
  25. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +18 -11
  26. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { calculateCostUsd } from "./model-cost";
3
+ import { calculateCostUsd, getModelCosts } from "./model-cost";
4
4
 
5
5
  describe("calculateCostUsd", () => {
6
6
  it("Anthropic cache read/write 를 전체 입력에서 분리해 각각 단가를 적용한다", () => {
@@ -36,4 +36,22 @@ describe("calculateCostUsd", () => {
36
36
  expect(cost).toBeGreaterThan(0);
37
37
  expect(cost).toBeCloseTo(0.0027222, 10);
38
38
  });
39
+
40
+ it("[1m] suffix 는 cost lookup 에서 strip 하고 long-context 할증은 붙이지 않는다", () => {
41
+ const base = getModelCosts("claude-sonnet-4-6");
42
+ const suffixed = getModelCosts("claude-sonnet-4-6[1m]");
43
+ expect(suffixed).toBe(base);
44
+ expect(suffixed.longContext).toBeUndefined();
45
+
46
+ const usage = {
47
+ inputTokens: 250_000,
48
+ outputTokens: 1_000,
49
+ cachedInputTokens: 200_000,
50
+ cacheCreationInputTokens: 0,
51
+ };
52
+ expect(calculateCostUsd("claude-sonnet-4-6[1m]", usage)).toBeCloseTo(
53
+ calculateCostUsd("claude-sonnet-4-6", usage),
54
+ 10,
55
+ );
56
+ });
39
57
  });
@@ -164,7 +164,8 @@ const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
164
164
  const DEFAULT_COSTS: ModelCosts = { inputTokens: 3, outputTokens: 15, cachedInputTokens: 0.3 };
165
165
 
166
166
  export function getModelCosts(model: string): ModelCosts {
167
- return OPENAI_COSTS[model] ?? ANTHROPIC_COSTS[model] ?? DEFAULT_COSTS;
167
+ const normalizedModel = model.replace(/\[1m\]$/i, "");
168
+ return OPENAI_COSTS[normalizedModel] ?? ANTHROPIC_COSTS[normalizedModel] ?? DEFAULT_COSTS;
168
169
  }
169
170
 
170
171
  export function calculateCostUsd(
@@ -57,16 +57,19 @@ export interface GenerateResult {
57
57
  threadCoord: ReuseThreadCoord;
58
58
  }
59
59
 
60
- // 상위(qgrid.dispatcher)로 가는 스트림 콜백. onComplete non-stream GenerateResult
61
- // 동일 shape(tokenName/threadCoord 포함)를 받아, 두 경로가 같은 일급 타입을 공유한다.
62
- export interface GenerateStreamCallbacks {
60
+ // 스트림 콜백 컨테이너. 계층별로 onComplete payload 다르고, 스트림 이벤트 shape 는 동일하다.
61
+ export interface StreamCallbacks<TComplete> {
63
62
  onDelta: (text: string) => void;
64
- onComplete: (result: GenerateResult) => void;
63
+ onComplete: (result: TComplete) => void;
65
64
  onError: (error: Error) => void;
66
65
  onThreadId?: (threadId: string) => void;
67
66
  onTurnId?: (turnId: string) => void;
68
67
  }
69
68
 
69
+ // 상위(qgrid.dispatcher)로 가는 스트림 콜백. onComplete 는 non-stream 의 GenerateResult 와
70
+ // 동일 shape(tokenName/threadCoord 포함)를 받아, 두 경로가 같은 일급 타입을 공유한다.
71
+ export type GenerateStreamCallbacks = StreamCallbacks<GenerateResult>;
72
+
70
73
  export interface ProviderDispatcher {
71
74
  generate(req: GenerateRequest): Promise<GenerateResult>;
72
75
  start(): Promise<void>;
@@ -9,6 +9,7 @@ import { type ThreadStartResponse } from "../../../codex-protocol/v2/ThreadStart
9
9
  import { type TokenUsageBreakdown } from "../../../codex-protocol/v2/TokenUsageBreakdown";
10
10
  import { type TurnStartParams } from "../../../codex-protocol/v2/TurnStartParams";
11
11
  import { type TurnStartResponse } from "../../../codex-protocol/v2/TurnStartResponse";
12
+ import { type StreamCallbacks as CommonStreamCallbacks } from "../common/provider-dispatcher";
12
13
  import { CodexRpcClient } from "./codex-rpc";
13
14
 
14
15
  const logger = getLogger(["qgrid", "codex-worker"]);
@@ -46,13 +47,7 @@ export interface TurnResult {
46
47
  model: string;
47
48
  }
48
49
 
49
- export interface StreamCallbacks {
50
- onDelta: (text: string) => void;
51
- onComplete: (result: TurnResult) => void;
52
- onError: (error: Error) => void;
53
- onThreadId?: (threadId: string) => void;
54
- onTurnId?: (turnId: string) => void;
55
- }
50
+ export type StreamCallbacks = CommonStreamCallbacks<TurnResult>;
56
51
 
57
52
  type RefreshableWorkerCredentials = Pick<WorkerConfig, "accessToken" | "accountId" | "planType">;
58
53
  type ActiveTurnAbort = (error: Error) => void;
@@ -191,7 +186,6 @@ export class CodexAppServerWorker {
191
186
  await this.waitForLoginCompleted();
192
187
  this.ready = true;
193
188
  this.restartAttempts = 0;
194
- logger.info(`worker ${this.config.tokenName} ready`);
195
189
  }
196
190
 
197
191
  async startBrowserLogin(): Promise<string> {
@@ -89,12 +89,16 @@ export class OpenAIDispatcher implements ProviderDispatcher {
89
89
 
90
90
  async onTokenRemoved(id: number): Promise<void> {
91
91
  const workers = this.workerPool.get(id) ?? [];
92
+ const tokenName = workers[0]?.tokenName;
92
93
  this.workerPool.delete(id);
93
94
  await Promise.allSettled(workers.map((w) => w.kill()));
94
95
  if (this.getAllReadyActiveWorkers().length === 0) {
95
96
  this.rejectAllQueued("NO_OPENAI_WORKERS");
96
97
  }
97
- if (workers.length > 0) logger.info(`workers removed for token ${id}: ${workers.length}`);
98
+ if (workers.length > 0) {
99
+ const label = tokenName ?? `token ${id}`;
100
+ logger.info(`workers removed: ${label} (${workers.length})`);
101
+ }
98
102
  }
99
103
 
100
104
  async onTokenUpdated(id: number, name: string, credentials: OpenAICredentials): Promise<void> {
@@ -106,7 +110,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
106
110
 
107
111
  if (existing.every((w) => w.canReuseForToken(name, credentials))) {
108
112
  existing.forEach((w) => w.updateTokenState(name, credentials));
109
- logger.info(`workers updated in-place for token ${id} (${name})`);
113
+ logger.info(`workers updated in-place: ${name}`);
110
114
  this.drainQueue();
111
115
  return;
112
116
  }
@@ -117,21 +121,27 @@ export class OpenAIDispatcher implements ProviderDispatcher {
117
121
  }
118
122
 
119
123
  onTokenDeactivated(id: number): void {
120
- (this.workerPool.get(id) ?? []).forEach((w) => {
124
+ const workers = this.workerPool.get(id) ?? [];
125
+ const tokenName = workers[0]?.tokenName;
126
+ workers.forEach((w) => {
121
127
  w.active = false;
122
128
  });
123
129
  if (this.getAllReadyActiveWorkers().length === 0) {
124
130
  this.rejectAllQueued("NO_ACTIVE_WORKERS");
125
131
  }
126
- logger.info(`workers deactivated: token ${id}`);
132
+ const label = tokenName ?? `token ${id}`;
133
+ logger.info(`workers deactivated: ${label}`);
127
134
  }
128
135
 
129
136
  onTokenActivated(id: number): void {
130
- (this.workerPool.get(id) ?? []).forEach((w) => {
137
+ const workers = this.workerPool.get(id) ?? [];
138
+ const tokenName = workers[0]?.tokenName;
139
+ workers.forEach((w) => {
131
140
  w.active = true;
132
141
  });
133
142
  this.drainQueue();
134
- logger.info(`workers activated: token ${id}`);
143
+ const label = tokenName ?? `token ${id}`;
144
+ logger.info(`workers activated: ${label}`);
135
145
  }
136
146
 
137
147
  // ── Generate ────────────────────────────────────────────────────
@@ -403,10 +413,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
403
413
  const worker = await this.spawnSingleWorker(tokenId, tokenName, credentials, i);
404
414
  if (worker) workers.push(worker);
405
415
  }
406
- if (workers.length > 0) {
407
- this.workerPool.set(tokenId, workers);
408
- logger.info(`${workers.length}/${WORKERS_PER_TOKEN} workers spawned for ${tokenName}`);
409
- }
416
+ if (workers.length > 0) this.workerPool.set(tokenId, workers);
410
417
  }
411
418
 
412
419
  async spawnSingleWorker(
@@ -435,7 +442,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
435
442
 
436
443
  try {
437
444
  await worker.initialize();
438
- logger.info(`worker spawned: ${tokenName}[${workerIndex}] (id=${tokenId + 1})`);
445
+ logger.info(`worker spawned: ${tokenName}[${workerIndex}]`);
439
446
  return worker;
440
447
  } catch (e) {
441
448
  logger.warn(`worker spawn failed: ${tokenName}[${workerIndex}]: ${(e as Error).message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"