@cartanova/qgrid-cli 2.3.2 → 2.3.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 (26) hide show
  1. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +94 -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 +42 -98
  5. package/bundle/dist/utils/providers/anthropic/claude-session.js +54 -32
  6. package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +7 -37
  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 +235 -7
  11. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +152 -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 +61 -188
  17. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +89 -62
  18. package/bundle/src/utils/providers/anthropic/claude-session.ts +71 -46
  19. package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +74 -30
  20. package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +40 -102
  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,27 +1,11 @@
1
1
  /**
2
- * stream-json 어댑터 — Anthropic(Claude) provider 의 입력/출력 변환.
2
+ * Claude CLI stream-json adapter.
3
3
  *
4
- * 모듈은 부활의 유일한 본질적 신규 코드다. 두 방향을 분리된 순수 함수로 둔다:
5
- * - 입력: 서버 GenerateRequest payload(coldInput/coldHistory/reuseInput)
6
- * → `claude -p --input-format stream-json` 의 JSONL 라인.
7
- * - 출력: `claude -p --output-format stream-json --verbose` 의 JSONL 라인
8
- * → qgrid 서버 delta(onDelta) + 최종 result. (U3, 같은 파일 하단)
4
+ * Input: qgrid history/input -> Claude JSONL stdin.
5
+ * Output: Claude JSONL stdout -> qgrid deltas + final result metadata.
9
6
  *
10
- * 중요한 계약:
11
- * - 입력은 AI SDK LanguageModelV3Message[] 아니라 **서버측 GenerateRequest** 다.
12
- * ai-sdk 가 이미 QueryInput.history(JsonValue[]) 로 변환했고, decideConvRouting 이
13
- * coldInput/coldHistory/reuseInput 으로 만든 뒤 dispatcher 에 도달한다.
14
- * - content 는 user/assistant 모두 block 배열 `[{type:"text",text}]` 로 정규화한다.
15
- * 문자열 content 는 일부 user 단발만 통과하고 history replay 에서 깨진다
16
- * (`W is not an Object (evaluating '"tool_use_id" in W')`).
17
- * - stream-json 의 user/assistant 는 비대칭이다: assistant 는 history context 로 push 되고
18
- * **user 는 새 prompt 로 enqueue(=실행)된다.** 따라서 cold 경로에서 과거 대화(user/assistant/
19
- * tool)를 그대로 줄로 늘어놓으면 과거 user/tool-output 이 실행 prompt 로 재실행된다. 이를 피하려
20
- * cold history 는 **단일 assistant context 텍스트로 평탄화**하고, 실행 가능한 `type:"user"` 줄은
21
- * **마지막 input 하나뿐**이 되도록 한다. full-fidelity role replay 가 필요하면 session resume
22
- * (reuseInput) 또는 JSONL backstop 으로 간다.
23
- * - tool 결과는 native Anthropic tool_result 블록을 새로 만들지 않고, 기존 OpenAI 와
24
- * 동일하게 평탄화된 text 로 처리한다(emulation 공통 계약).
7
+ * Previous conversation items are flattened into one assistant context line.
8
+ * The only executable user line is the current input.
25
9
  */
26
10
 
27
11
  import { type JsonValue } from "../../../codex-protocol/serde_json/JsonValue";
@@ -30,11 +14,7 @@ import { type ProviderTokenUsageBreakdown } from "../common/provider-dispatcher"
30
14
 
31
15
  // ── 입력 어댑터 ────────────────────────────────────────────────────
32
16
 
33
- // Claude stream-json 입력 한 줄(JSONL). content 항상 block 배열.
34
- //
35
- // SDK envelope 필드(uuid / session_id / parent_tool_use_id)는 **여기서 만들지 않는다**.
36
- // 그 값들은 세션을 발급/소유하는 claude-session 의 책임이다. 이 모듈은 role/content 변환만 하는
37
- // 순수 함수로 두고, claude-session 이 직렬화 직전에 envelope 를 decorate 한다. 책임 분리.
17
+ // Claude stream-json 입력 한 줄(JSONL). Envelope fields are added in claude-session.
38
18
  export interface ClaudeStreamJsonLine {
39
19
  type: "user" | "assistant";
40
20
  message: {
@@ -43,7 +23,6 @@ export interface ClaudeStreamJsonLine {
43
23
  };
44
24
  }
45
25
 
46
- // UserInput[] 에서 text 만 뽑아 하나의 문자열로. (image/skill/mention 등은 현재 범위 밖 — text 위주)
47
26
  function userInputToText(input: Array<UserInput>): string {
48
27
  return input
49
28
  .filter((i): i is Extract<UserInput, { type: "text" }> => i.type === "text")
@@ -51,12 +30,11 @@ function userInputToText(input: Array<UserInput>): string {
51
30
  .join("\n");
52
31
  }
53
32
 
54
- // content 를 항상 block 배열로 정규화(문자열 content 는 history replay 에서 깨짐).
55
33
  function textBlock(text: string): Array<{ type: "text"; text: string }> {
56
34
  return [{ type: "text", text }];
57
35
  }
58
36
 
59
- // codex ResponseItem(coldHistory 의 한 항목) 형태 판별용 최소 shape.
37
+ // codex ResponseItem(coldHistory item) 최소 shape.
60
38
  // extractPromptAndHistory(ai-sdk/utils.ts) 가 만드는 형식:
61
39
  // { type:"message", role:"user", content:[{type:"input_text", text}] }
62
40
  // { type:"message", role:"assistant", content:[{type:"output_text", text}] }
@@ -68,8 +46,6 @@ function asObject(v: JsonValue | undefined): ResponseItem | null {
68
46
  return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as ResponseItem) : null;
69
47
  }
70
48
 
71
- // codex message content([{type:input_text|output_text, text}]) 에서 text 추출.
72
- // 여러 content 블록은 줄바꿈으로 구분(token-adjacent 붙음 방지).
73
49
  function responseItemText(item: ResponseItem): string {
74
50
  const content = item.content;
75
51
  if (!Array.isArray(content)) return "";
@@ -82,11 +58,6 @@ function responseItemText(item: ResponseItem): string {
82
58
  .join("\n");
83
59
  }
84
60
 
85
- /**
86
- * cold history(codex ResponseItem[])를 **단일 평탄 텍스트**로 변환한다.
87
- * user/assistant/tool 모두 라벨 붙은 한 덩어리로 — 실행되지 않는 assistant context 로 들어간다.
88
- * (cold 의 user/tool-output 을 type:"user" 로 내보내면 새 prompt 로 재실행되므로 금지.)
89
- */
90
61
  function flattenColdHistory(coldHistory: Array<JsonValue>): string {
91
62
  const parts: Array<string> = [];
92
63
  for (const raw of coldHistory) {
@@ -111,30 +82,13 @@ function flattenColdHistory(coldHistory: Array<JsonValue>): string {
111
82
  return parts.join("\n");
112
83
  }
113
84
 
114
- /**
115
- * 서버 GenerateRequest payload 를 Claude stream-json JSONL 라인 배열로 변환한다.
116
- *
117
- * @param opts.coldHistory cold 경로의 codex ResponseItem 배열(JsonValue[]). resume 경로면 미사용.
118
- * @param opts.input 실행할 input — cold 면 coldInput, resume 면 reuseInput.
119
- * @param opts.isResume resume 경로 여부. true 면 history 재주입 없이 input(delta)만.
120
- *
121
- * 출력 규칙:
122
- * - 실행 가능한 `type:"user"` 줄은 **항상 정확히 1개** — 마지막 input.
123
- * - resume: user(delta) 1줄만. thread 가 이전 turn 을 누적하므로 history 미포함.
124
- * - cold: coldHistory 전체를 **단일 assistant context 텍스트**로 평탄화(실행 안 됨) + user(input) 1줄.
125
- * user/assistant/tool 모두 평탄화되므로 과거 user/tool-output 이 재실행되지 않는다.
126
- * full-fidelity role replay 가 필요하면 session resume 또는 JSONL backstop(범위 밖).
127
- * - tool(function_call/output): native 블록 없이 평탄 텍스트로.
128
- */
129
85
  export function buildStreamJsonInput(opts: {
130
86
  coldHistory?: Array<JsonValue>;
131
87
  input: Array<UserInput>;
132
- isResume: boolean;
133
88
  }): Array<ClaudeStreamJsonLine> {
134
89
  const lines: Array<ClaudeStreamJsonLine> = [];
135
90
 
136
- // cold 경로: history 단일 assistant context 로 평탄화(실행 안 됨).
137
- if (!opts.isResume && opts.coldHistory && opts.coldHistory.length > 0) {
91
+ if (opts.coldHistory && opts.coldHistory.length > 0) {
138
92
  const flattened = flattenColdHistory(opts.coldHistory);
139
93
  if (flattened) {
140
94
  lines.push({
@@ -147,7 +101,6 @@ export function buildStreamJsonInput(opts: {
147
101
  }
148
102
  }
149
103
 
150
- // 실행할 input — resume/cold 공통, 항상 마지막의 단일 user 줄.
151
104
  lines.push({
152
105
  type: "user",
153
106
  message: { role: "user", content: textBlock(userInputToText(opts.input)) },
@@ -156,26 +109,14 @@ export function buildStreamJsonInput(opts: {
156
109
  return lines;
157
110
  }
158
111
 
159
- // JSONL 문자열(claude stdin 으로 흘릴 형태)로 직렬화.
160
112
  export function serializeStreamJsonInput(lines: Array<ClaudeStreamJsonLine>): string {
161
113
  return lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
162
114
  }
163
115
 
164
116
  // ── 출력 어댑터 ────────────────────────────────────────────────────
165
117
  //
166
- // claude -p --output-format stream-json --verbose stdout(JSONL)을
167
- // qgrid 서버 delta(onDelta) + 최종 result 로 변환한다.
168
- //
169
- // 계약:
170
- // - 이 어댑터는 qgrid **서버** 계층이다. AI SDK stream part(text-delta 등)는
171
- // packages/ai-sdk(client provider)가 만든다 — 여기서 만들지 않는다.
172
- // - text_delta → onDelta(text). structured output 일 때 input_json_delta.partial_json
173
- // 조각도 onDelta(text) 로 흘린다(client provider 가 partialObjectStream 으로 누적).
174
- // - 최종 result → structured_output(우선) 또는 result(폴백, 자연어 가능) + usage + 종료.
175
- // - usage 는 input/cache_creation/cache_read/output 4 카테고리를 받아 TokenUsageBreakdown 으로.
176
- // payload 크기 비교는 3 카테고리 합으로(과거 학습 — input 단독 비교 무의미).
177
-
178
- // claude stream-json 출력에서 우리가 보는 usage shape(부분). Anthropic 네이티브 필드.
118
+ // Claude stdout(JSONL)을 qgrid deltas + final result 로 변환한다.
119
+
179
120
  interface ClaudeUsage {
180
121
  input_tokens?: number;
181
122
  output_tokens?: number;
@@ -183,7 +124,6 @@ interface ClaudeUsage {
183
124
  cache_read_input_tokens?: number;
184
125
  }
185
126
 
186
- // 최종 result 파싱 산출물. dispatcher(U1)가 GenerateResult 조립에 쓴다.
187
127
  export interface ClaudeStreamResult {
188
128
  text: string;
189
129
  usage: ProviderTokenUsageBreakdown;
@@ -193,6 +133,10 @@ export interface ClaudeStreamResult {
193
133
  quotaExhausted: boolean;
194
134
  // result.is_error / terminal model_error 등. dispatcher 가 에러로 변환.
195
135
  isError: boolean;
136
+ // 진단용: CC result 라인의 subtype("success" | "error_max_turns" | ...) 과 terminal_reason.
137
+ // isError 판정 사유를 에러 메시지에 드러내기 위해 보존한다(SON-495).
138
+ subtype?: string;
139
+ terminalReason?: string;
196
140
  }
197
141
 
198
142
  export interface ClaudeStreamJsonState {
@@ -220,8 +164,6 @@ function toUsageBreakdown(u: ClaudeUsage): ProviderTokenUsageBreakdown {
220
164
  };
221
165
  }
222
166
 
223
- // result.result 의 코드펜스(```json ... ```) 제거. structured_output 이 없을 때만 쓴다.
224
- // 앞뒤 공백/개행을 먼저 trim 해 `\n```json\n...\n```\n` 같은 형태도 깨끗이 제거.
225
167
  function stripCodeFence(s: string): string {
226
168
  return s
227
169
  .trim()
@@ -237,26 +179,18 @@ function structuredOutputToolUseText(j: ResponseItem): string | undefined {
237
179
  for (const raw of content) {
238
180
  const block = asObject(raw);
239
181
  if (!block) continue;
240
- if (block.type === "tool_use" && block.name === "StructuredOutput" && block.input !== undefined) {
182
+ if (
183
+ block.type === "tool_use" &&
184
+ block.name === "StructuredOutput" &&
185
+ block.input !== undefined
186
+ ) {
241
187
  return JSON.stringify(block.input);
242
188
  }
243
189
  }
244
190
  return undefined;
245
191
  }
246
192
 
247
- /**
248
- * stream-json 출력 라인 하나를 처리한다. 순수 함수 — side effect 는 onDelta 콜백뿐.
249
- *
250
- * @param opts.structuredOutput structured output(jsonSchema) 모드 여부.
251
- * structured 모드에서 Claude 는 자연어 text_delta 와 input_json_delta.partial_json 을
252
- * **함께** 흘릴 수 있다(PoC 확인). 이때 text_delta 를 그대로 onDelta 로 보내면 client 의
253
- * streamObject 파서가 prose 를 JSON 앞에서 만나 깨진다. 그래서:
254
- * - structured 모드: text_delta 무시, partial_json 만 onDelta.
255
- * - text 모드: text_delta 만 onDelta (partial_json 은 안 옴).
256
- *
257
- * @returns result 이벤트면 ClaudeStreamResult, 그 외(delta/system 등)면 null.
258
- * 호출부는 라인 스트림을 돌며 이 함수를 호출하고, non-null 이 나오면 종료.
259
- */
193
+ // Processes one Claude stdout JSONL line. Returns final result on result event.
260
194
  export function handleStreamJsonLine(
261
195
  line: string,
262
196
  onDelta: (text: string) => void,
@@ -271,23 +205,19 @@ export function handleStreamJsonLine(
271
205
  try {
272
206
  j = JSON.parse(trimmed) as { [key: string]: JsonValue | undefined };
273
207
  } catch {
274
- // 깨진 JSON 라인은 graceful skip.
275
208
  return null;
276
209
  }
277
210
 
278
- // 점진 delta: stream_event > content_block_delta > {text_delta | input_json_delta}.
279
211
  if (j.type === "stream_event") {
280
212
  const event = asObject(j.event);
281
213
  if (event && event.type === "content_block_delta") {
282
214
  const delta = asObject(event.delta);
283
215
  if (delta) {
284
216
  if (structuredOutput) {
285
- // structured: JSON 조각만. 자연어 text_delta 는 버린다(streamObject 파서 보호).
286
217
  if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
287
218
  onDelta(delta.partial_json);
288
219
  }
289
220
  } else {
290
- // text: 자연어 delta 만.
291
221
  if (delta.type === "text_delta" && typeof delta.text === "string") {
292
222
  onDelta(delta.text);
293
223
  }
@@ -297,13 +227,17 @@ export function handleStreamJsonLine(
297
227
  return null;
298
228
  }
299
229
 
300
- if (j.type === "assistant" && structuredOutput && state && state.structuredOutputText === undefined) {
230
+ if (
231
+ j.type === "assistant" &&
232
+ structuredOutput &&
233
+ state &&
234
+ state.structuredOutputText === undefined
235
+ ) {
301
236
  const text = structuredOutputToolUseText(j);
302
237
  if (text !== undefined) state.structuredOutputText = text;
303
238
  return null;
304
239
  }
305
240
 
306
- // 최종 result.
307
241
  if (j.type === "result") {
308
242
  let text: string;
309
243
  const preservedStructuredOutput = structuredOutput ? state?.structuredOutputText : undefined;
@@ -317,18 +251,20 @@ export function handleStreamJsonLine(
317
251
 
318
252
  const usage = toUsageBreakdown((asObject(j.usage) ?? {}) as ClaudeUsage);
319
253
  const quotaExhausted = text.startsWith("You've hit");
320
- // 에러 판정: is_error true, terminal model_error,
321
- // 또는 subtype 이 있고 "success" 가 아니면(error_during_execution / error_max_turns /
322
- // error_max_budget_usd / error_max_structured_output_retries 등) 에러로 본다.
323
254
  const subtype = typeof j.subtype === "string" ? j.subtype : undefined;
324
- const structuredMaxTurnsCompleted =
325
- preservedStructuredOutput !== undefined &&
326
- (subtype === "error_max_turns" || j.terminal_reason === "max_turns");
255
+ const terminalReason = typeof j.terminal_reason === "string" ? j.terminal_reason : undefined;
256
+
257
+ // SON-495: 비정상 종료(error_max_structured_output_retries 등)는 정직하게 에러로 처리한다.
258
+ // CC structured output 은 constrained decoding 이 아니라 "tool input 생성 후 사후 AJV 검증"이라
259
+ // 모델이 가끔 placeholder($PARAMETER_NAME)·거부("just kidding")·필드 누락(advance) 같은 발작/
260
+ // 불완전 출력을 낸다. 이를 "거의 맞았으니 살리자"고 흘리면 클라이언트 검증에서 깨지거나(필드 누락)
261
+ // 쓰레기가 새어 나간다(실측 11674/11675). 알려진 best practice(refusal/degenerate output 은 살리지
262
+ // 말고 graceful error)대로, subtype != success 는 그대로 에러로 둔다. 클라이언트가 그 턴을 실패로
263
+ // 다루게 한다(스트림이라 자동 재시도 없음 — 시간 2배 방지).
327
264
  const isError =
328
- !structuredMaxTurnsCompleted &&
329
- (j.is_error === true ||
330
- j.terminal_reason === "model_error" ||
331
- (subtype !== undefined && subtype !== "success"));
265
+ j.is_error === true ||
266
+ j.terminal_reason === "model_error" ||
267
+ (subtype !== undefined && subtype !== "success");
332
268
 
333
269
  return {
334
270
  text,
@@ -337,6 +273,8 @@ export function handleStreamJsonLine(
337
273
  costUsd: typeof j.total_cost_usd === "number" ? j.total_cost_usd : 0,
338
274
  quotaExhausted,
339
275
  isError,
276
+ subtype,
277
+ terminalReason,
340
278
  };
341
279
  }
342
280
 
@@ -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.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"