@cartanova/qgrid-cli 2.1.0 → 2.2.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/bundle/dist/application/qgrid/conv-routing.js +68 -0
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +16 -15
- package/bundle/dist/application/qgrid/qgrid.frame.js +17 -5
- package/bundle/dist/application/qgrid/qgrid.types.js +12 -3
- package/bundle/dist/application/qgrid/tool-emulation.js +11 -6
- package/bundle/dist/utils/providers/openai/codex-worker.js +140 -53
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +75 -21
- package/bundle/src/application/qgrid/conv-routing.ts +97 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -5
- package/bundle/src/application/qgrid/qgrid.frame.ts +20 -5
- package/bundle/src/application/qgrid/qgrid.types.ts +14 -1
- package/bundle/src/application/qgrid/tool-emulation.ts +20 -5
- package/bundle/src/application/sonamu.generated.http +14 -2
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +29 -2
- package/bundle/src/utils/providers/openai/codex-worker.test.ts +163 -0
- package/bundle/src/utils/providers/openai/codex-worker.ts +199 -39
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +59 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +116 -27
- package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +1 -0
- package/bundle/web-dist/client/assets/{index-ZV-tHpAg.js → index-DeThumA3.js} +2 -2
- package/bundle/web-dist/client/assets/logs-CtmOVH58.js +1 -0
- package/bundle/web-dist/client/assets/{routes-BMRwPj6F.js → routes-Cfoe9J0I.js} +1 -1
- package/bundle/web-dist/client/assets/show-C6OofoAL.js +30 -0
- package/bundle/web-dist/client/assets/{tokens-Cg1f0Ga2.js → tokens-BAZq8sTo.js} +1 -1
- package/bundle/web-dist/client/index.html +1 -1
- package/bundle/web-dist/server/assets/cost-DzjfVtUr.js +24 -0
- package/bundle/web-dist/server/assets/{logs-Xt934T5f.js → logs-CHM0fhfO.js} +2 -7
- package/bundle/web-dist/server/assets/{routes-B7QYbNIv.js → routes-C9xFMJ_r.js} +1 -1
- package/bundle/web-dist/server/assets/{show-Dxz6UJnD.js → show-BZWqH09U.js} +2 -4
- package/bundle/web-dist/server/entry-server.generated.js +3 -3
- package/package.json +1 -1
- package/bundle/web-dist/client/assets/cost-DE_8cLD-.js +0 -1
- package/bundle/web-dist/client/assets/logs-HJYkbqom.js +0 -1
- package/bundle/web-dist/client/assets/show-DI6iWDzV.js +0 -30
- package/bundle/web-dist/server/assets/cost-DdHTzQDR.js +0 -14
|
@@ -13,14 +13,10 @@ import { type OpenAICredentials } from "../../../application/token/token.types";
|
|
|
13
13
|
import {
|
|
14
14
|
type GenerateRequest,
|
|
15
15
|
type GenerateResult,
|
|
16
|
+
type GenerateStreamCallbacks,
|
|
16
17
|
type ProviderDispatcher,
|
|
17
18
|
} from "../common/provider-dispatcher";
|
|
18
|
-
import {
|
|
19
|
-
CodexAppServerWorker,
|
|
20
|
-
type StreamCallbacks,
|
|
21
|
-
type TurnRequest,
|
|
22
|
-
type TurnResult,
|
|
23
|
-
} from "./codex-worker";
|
|
19
|
+
import { CodexAppServerWorker, type StreamCallbacks, type TurnRequest } from "./codex-worker";
|
|
24
20
|
import { handleChatgptAuthTokensRefresh } from "./openai-refresh";
|
|
25
21
|
|
|
26
22
|
const logger = getLogger(["qgrid", "openai-dispatcher"]);
|
|
@@ -35,6 +31,17 @@ const QUEUE_TIMEOUT_MS = 60_000;
|
|
|
35
31
|
const MAX_QUEUE_SIZE = 50;
|
|
36
32
|
const SPAWN_INTERVAL_MS = 500;
|
|
37
33
|
|
|
34
|
+
// thread 재사용(prompt cache 고정). 끄면 기존 "매 turn 새 thread + history inject" 동작.
|
|
35
|
+
const THREAD_REUSE_ENABLED = process.env.QGRID_OPENAI_THREAD_REUSE !== "false";
|
|
36
|
+
// 좌표 worker 가 busy 일 때 free 를 기다리는 최대 시간. 초과 시 새 thread 로 폴백.
|
|
37
|
+
const REUSE_WORKER_WAIT_MS = 5_000;
|
|
38
|
+
const REUSE_WORKER_POLL_MS = 50;
|
|
39
|
+
|
|
40
|
+
// workerId 합성: tokenId 와 workerIndex(0..4) 를 하나의 안정 숫자로 인코딩.
|
|
41
|
+
function makeWorkerId(tokenId: number, workerIndex: number): number {
|
|
42
|
+
return tokenId * 10 + workerIndex;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
function sleep(ms: number): Promise<void> {
|
|
39
46
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
47
|
}
|
|
@@ -92,6 +99,18 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
92
99
|
|
|
93
100
|
async onTokenUpdated(id: number, name: string, credentials: OpenAICredentials): Promise<void> {
|
|
94
101
|
const existing = this.workerPool.get(id) ?? [];
|
|
102
|
+
if (existing.length === 0) {
|
|
103
|
+
await this.spawnWorkers(id, name, credentials);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (existing.every((w) => w.canReuseForToken(name, credentials))) {
|
|
108
|
+
existing.forEach((w) => w.updateTokenState(name, credentials));
|
|
109
|
+
logger.info(`workers updated in-place for token ${id} (${name})`);
|
|
110
|
+
this.drainQueue();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
this.workerPool.delete(id);
|
|
96
115
|
await Promise.allSettled(existing.map((w) => w.kill()));
|
|
97
116
|
await this.spawnWorkers(id, name, credentials);
|
|
@@ -118,6 +137,17 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
118
137
|
// ── Generate ────────────────────────────────────────────────────
|
|
119
138
|
|
|
120
139
|
async generate(req: GenerateRequest): Promise<GenerateResult> {
|
|
140
|
+
// thread 재사용 경로: 좌표 worker 를 점유해 기존 thread 에 turn 만 실행.
|
|
141
|
+
const reuseWorker = await this.acquireReuseWorker(req);
|
|
142
|
+
if (reuseWorker) {
|
|
143
|
+
logger.info(
|
|
144
|
+
`↻ ${reuseWorker.tokenName}[${reuseWorker.tokenId}] (reuse thread, ${req.model})`,
|
|
145
|
+
);
|
|
146
|
+
return this.executeAndRelease(reuseWorker, (w) =>
|
|
147
|
+
this.executeTurn(w, req, req.reuse!.threadId),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
const worker = this.acquireIdleWorker();
|
|
122
152
|
if (worker) {
|
|
123
153
|
logger.info(`→ ${worker.tokenName}[${worker.tokenId}] (model: ${req.model})`);
|
|
@@ -129,7 +159,15 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
129
159
|
});
|
|
130
160
|
}
|
|
131
161
|
|
|
132
|
-
async generateStream(req: GenerateRequest, cb:
|
|
162
|
+
async generateStream(req: GenerateRequest, cb: GenerateStreamCallbacks): Promise<void> {
|
|
163
|
+
const reuseWorker = await this.acquireReuseWorker(req);
|
|
164
|
+
if (reuseWorker) {
|
|
165
|
+
logger.info(`↻ ${reuseWorker.tokenName}[${reuseWorker.tokenId}] (reuse thread, [stream])`);
|
|
166
|
+
return this.executeAndRelease(reuseWorker, (w) =>
|
|
167
|
+
this.executeStreamTurn(w, req, cb, req.reuse!.threadId),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
const worker = this.acquireIdleWorker();
|
|
134
172
|
if (worker) {
|
|
135
173
|
logger.info(`→ ${worker.tokenName}[${worker.tokenId}] (model: ${req.model}, [stream])`);
|
|
@@ -141,6 +179,37 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
141
179
|
});
|
|
142
180
|
}
|
|
143
181
|
|
|
182
|
+
// reuse 좌표가 유효하면 그 worker 를 busy 점유해 반환. 없거나 폴백 대상이면 null.
|
|
183
|
+
// 폴백 사유: 기능 off / 좌표 없음 / worker 부재·죽음 / epoch 불일치(restart) / thread 소멸 /
|
|
184
|
+
// busy 대기 타임아웃. null 반환 시 호출부는 새 thread 경로로 진행.
|
|
185
|
+
async acquireReuseWorker(req: GenerateRequest): Promise<CodexAppServerWorker | null> {
|
|
186
|
+
if (!THREAD_REUSE_ENABLED || !req.reuse) return null;
|
|
187
|
+
const { workerId, threadId, epoch } = req.reuse;
|
|
188
|
+
|
|
189
|
+
const worker = this.findWorkerById(workerId);
|
|
190
|
+
if (!worker || !worker.isReady || !worker.active) return null;
|
|
191
|
+
if (worker.epoch !== epoch) return null;
|
|
192
|
+
if (!worker.hasThread(threadId)) return null;
|
|
193
|
+
|
|
194
|
+
const deadline = Date.now() + REUSE_WORKER_WAIT_MS;
|
|
195
|
+
for (;;) {
|
|
196
|
+
// restart(epoch 변경) / thread 소멸이 대기 중에 발생하면 폴백.
|
|
197
|
+
if (worker.epoch !== epoch || !worker.hasThread(threadId)) return null;
|
|
198
|
+
if (worker.tryAcquireTurn()) return worker;
|
|
199
|
+
if (Date.now() >= deadline) return null;
|
|
200
|
+
await sleep(REUSE_WORKER_POLL_MS);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
findWorkerById(workerId: number): CodexAppServerWorker | null {
|
|
205
|
+
for (const workers of this.workerPool.values()) {
|
|
206
|
+
for (const w of workers) {
|
|
207
|
+
if (makeWorkerId(w.tokenId, w.workerIndex) === workerId) return w;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
144
213
|
async interruptWorkerTurn(threadId: string, turnId: string): Promise<void> {
|
|
145
214
|
const allWorkers = [...this.workerPool.values()].flat().filter((w) => w.isReady);
|
|
146
215
|
await Promise.allSettled(allWorkers.map((w) => w.interruptTurn(threadId, turnId)));
|
|
@@ -252,53 +321,73 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
252
321
|
}
|
|
253
322
|
}
|
|
254
323
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
324
|
+
// reuseThreadId 가 있으면 기존 thread 에 delta(reuseInput)만, 없으면 새 thread 에 전체
|
|
325
|
+
// prompt(coldInput) + history inject. reuse 가 dispatcher 단에서 폴백되면 후자로 떨어진다.
|
|
326
|
+
private buildTurnRequest(req: GenerateRequest, reuseThreadId?: string): TurnRequest {
|
|
327
|
+
const reusing = reuseThreadId !== undefined;
|
|
328
|
+
return {
|
|
329
|
+
input: reusing && req.reuseInput ? req.reuseInput : req.coldInput,
|
|
330
|
+
history: reusing ? undefined : req.coldHistory,
|
|
258
331
|
developerInstructions: req.systemPrompt,
|
|
259
332
|
outputSchema: req.outputSchema,
|
|
260
333
|
effort: req.effort ?? DEFAULT_EFFORT,
|
|
261
334
|
model: req.model,
|
|
262
|
-
history: req.history,
|
|
263
335
|
verbosity: req.verbosity,
|
|
264
336
|
reasoningSummary: req.reasoningSummary,
|
|
265
337
|
serviceTier: req.serviceTier,
|
|
266
338
|
};
|
|
267
|
-
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async executeTurn(
|
|
342
|
+
worker: CodexAppServerWorker,
|
|
343
|
+
req: GenerateRequest,
|
|
344
|
+
reuseThreadId?: string,
|
|
345
|
+
): Promise<GenerateResult> {
|
|
346
|
+
const turnReq = this.buildTurnRequest(req, reuseThreadId);
|
|
347
|
+
const result = await worker.executeTurn(turnReq, reuseThreadId);
|
|
268
348
|
return {
|
|
269
349
|
text: result.text,
|
|
270
350
|
tokenName: worker.tokenName,
|
|
271
351
|
usage: result.usage,
|
|
272
352
|
durationMs: result.durationMs,
|
|
273
353
|
model: result.model,
|
|
354
|
+
threadCoord: {
|
|
355
|
+
workerId: makeWorkerId(worker.tokenId, worker.workerIndex),
|
|
356
|
+
threadId: result.threadId,
|
|
357
|
+
epoch: worker.epoch,
|
|
358
|
+
},
|
|
274
359
|
};
|
|
275
360
|
}
|
|
276
361
|
|
|
277
362
|
async executeStreamTurn(
|
|
278
363
|
worker: CodexAppServerWorker,
|
|
279
364
|
req: GenerateRequest,
|
|
280
|
-
cb:
|
|
365
|
+
cb: GenerateStreamCallbacks,
|
|
366
|
+
reuseThreadId?: string,
|
|
281
367
|
): Promise<void> {
|
|
282
|
-
const turnReq
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
effort: req.effort ?? DEFAULT_EFFORT,
|
|
287
|
-
model: req.model,
|
|
288
|
-
history: req.history,
|
|
289
|
-
verbosity: req.verbosity,
|
|
290
|
-
reasoningSummary: req.reasoningSummary,
|
|
291
|
-
serviceTier: req.serviceTier,
|
|
292
|
-
};
|
|
368
|
+
const turnReq = this.buildTurnRequest(req, reuseThreadId);
|
|
369
|
+
// worker 의 TurnResult 에 tokenName/threadCoord 를 붙여 상위(GenerateResult)로 올린다.
|
|
370
|
+
// threadId 는 새 thread 면 onThreadId 로 확정되므로 미리 보관해 두고 onComplete 때 읽는다.
|
|
371
|
+
let resolvedThreadId = reuseThreadId ?? "";
|
|
293
372
|
const wrappedCb: StreamCallbacks = {
|
|
294
373
|
...cb,
|
|
374
|
+
onThreadId: (threadId) => {
|
|
375
|
+
resolvedThreadId = threadId;
|
|
376
|
+
cb.onThreadId?.(threadId);
|
|
377
|
+
},
|
|
295
378
|
onComplete: (result) => {
|
|
296
|
-
cb.onComplete({
|
|
297
|
-
|
|
379
|
+
cb.onComplete({
|
|
380
|
+
...result,
|
|
381
|
+
tokenName: worker.tokenName,
|
|
382
|
+
threadCoord: {
|
|
383
|
+
workerId: makeWorkerId(worker.tokenId, worker.workerIndex),
|
|
384
|
+
threadId: resolvedThreadId,
|
|
385
|
+
epoch: worker.epoch,
|
|
386
|
+
},
|
|
298
387
|
});
|
|
299
388
|
},
|
|
300
389
|
};
|
|
301
|
-
await worker.executeTurnStream(turnReq, wrappedCb);
|
|
390
|
+
await worker.executeTurnStream(turnReq, wrappedCb, reuseThreadId);
|
|
302
391
|
}
|
|
303
392
|
|
|
304
393
|
// ── Spawn ───────────────────────────────────────────────────────
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=1e6,t=4;function n(t){return t/e}function r(e){return`$${e.toFixed(t)}`}function i(e){return r(n(e))}function a(e){return e.input_tokens<=0?`—`:`${Math.round(e.cache_read_tokens/e.input_tokens*100)}%`}export{i as n,r,a as t};
|