@cartanova/qgrid-cli 2.3.1 → 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.
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +94 -277
- package/bundle/dist/application/qgrid/qgrid.frame.js +15 -5
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +33 -12
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +39 -96
- package/bundle/dist/utils/providers/anthropic/claude-session.js +47 -32
- package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +3 -36
- package/bundle/dist/utils/providers/common/model-cost.js +3 -2
- package/bundle/dist/utils/providers/openai/codex-worker.js +1 -2
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +19 -12
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +262 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +142 -373
- package/bundle/src/application/qgrid/qgrid.frame.ts +38 -23
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +60 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +57 -16
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +92 -340
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +50 -185
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +71 -62
- package/bundle/src/utils/providers/anthropic/claude-session.ts +56 -46
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +19 -30
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +24 -101
- package/bundle/src/utils/providers/common/model-cost.test.ts +19 -1
- package/bundle/src/utils/providers/common/model-cost.ts +2 -1
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +7 -4
- package/bundle/src/utils/providers/openai/codex-worker.ts +2 -8
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +18 -11
- 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
|
-
|
|
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
|
-
//
|
|
61
|
-
|
|
62
|
-
export interface GenerateStreamCallbacks {
|
|
60
|
+
// 스트림 콜백 컨테이너. 계층별로 onComplete payload 만 다르고, 스트림 이벤트 shape 는 동일하다.
|
|
61
|
+
export interface StreamCallbacks<TComplete> {
|
|
63
62
|
onDelta: (text: string) => void;
|
|
64
|
-
onComplete: (result:
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
132
|
+
const label = tokenName ?? `token ${id}`;
|
|
133
|
+
logger.info(`workers deactivated: ${label}`);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
onTokenActivated(id: number): void {
|
|
130
|
-
|
|
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
|
-
|
|
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}]
|
|
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}`);
|