@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.
Files changed (37) hide show
  1. package/bundle/dist/application/qgrid/conv-routing.js +68 -0
  2. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +2 -2
  3. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +16 -15
  4. package/bundle/dist/application/qgrid/qgrid.frame.js +17 -5
  5. package/bundle/dist/application/qgrid/qgrid.types.js +12 -3
  6. package/bundle/dist/application/qgrid/tool-emulation.js +11 -6
  7. package/bundle/dist/utils/providers/openai/codex-worker.js +140 -53
  8. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +75 -21
  9. package/bundle/src/application/qgrid/conv-routing.ts +97 -0
  10. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
  11. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +16 -5
  12. package/bundle/src/application/qgrid/qgrid.frame.ts +20 -5
  13. package/bundle/src/application/qgrid/qgrid.types.ts +14 -1
  14. package/bundle/src/application/qgrid/tool-emulation.ts +20 -5
  15. package/bundle/src/application/sonamu.generated.http +14 -2
  16. package/bundle/src/utils/providers/common/provider-dispatcher.ts +29 -2
  17. package/bundle/src/utils/providers/openai/codex-worker.test.ts +163 -0
  18. package/bundle/src/utils/providers/openai/codex-worker.ts +199 -39
  19. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +59 -0
  20. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +116 -27
  21. package/bundle/web-dist/client/assets/cost-CpjVoAqp.js +1 -0
  22. package/bundle/web-dist/client/assets/{index-ZV-tHpAg.js → index-DeThumA3.js} +2 -2
  23. package/bundle/web-dist/client/assets/logs-CtmOVH58.js +1 -0
  24. package/bundle/web-dist/client/assets/{routes-BMRwPj6F.js → routes-Cfoe9J0I.js} +1 -1
  25. package/bundle/web-dist/client/assets/show-C6OofoAL.js +30 -0
  26. package/bundle/web-dist/client/assets/{tokens-Cg1f0Ga2.js → tokens-BAZq8sTo.js} +1 -1
  27. package/bundle/web-dist/client/index.html +1 -1
  28. package/bundle/web-dist/server/assets/cost-DzjfVtUr.js +24 -0
  29. package/bundle/web-dist/server/assets/{logs-Xt934T5f.js → logs-CHM0fhfO.js} +2 -7
  30. package/bundle/web-dist/server/assets/{routes-B7QYbNIv.js → routes-C9xFMJ_r.js} +1 -1
  31. package/bundle/web-dist/server/assets/{show-Dxz6UJnD.js → show-BZWqH09U.js} +2 -4
  32. package/bundle/web-dist/server/entry-server.generated.js +3 -3
  33. package/package.json +1 -1
  34. package/bundle/web-dist/client/assets/cost-DE_8cLD-.js +0 -1
  35. package/bundle/web-dist/client/assets/logs-HJYkbqom.js +0 -1
  36. package/bundle/web-dist/client/assets/show-DI6iWDzV.js +0 -30
  37. 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: StreamCallbacks): Promise<void> {
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
- async executeTurn(worker: CodexAppServerWorker, req: GenerateRequest): Promise<GenerateResult> {
256
- const turnReq: TurnRequest = {
257
- input: req.input,
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
- const result: TurnResult = await worker.executeTurn(turnReq);
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: StreamCallbacks,
365
+ cb: GenerateStreamCallbacks,
366
+ reuseThreadId?: string,
281
367
  ): Promise<void> {
282
- const turnReq: TurnRequest = {
283
- input: req.input,
284
- developerInstructions: req.systemPrompt,
285
- outputSchema: req.outputSchema,
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({ ...result, tokenName: worker.tokenName } as TurnResult & {
297
- tokenName: string;
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};