@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
@@ -3,10 +3,198 @@ import { describe, expect, it, vi } from "vitest";
3
3
  import {
4
4
  type GenerateRequest,
5
5
  type GenerateResult,
6
+ type GenerateStreamCallbacks,
6
7
  } from "../../utils/providers/common/provider-dispatcher";
7
8
  import { buildStrictOutputSchema, QgridDispatcherClass } from "./qgrid.dispatcher";
8
9
 
10
+ function providerResult(overrides: Partial<GenerateResult> = {}): GenerateResult {
11
+ return {
12
+ text: "ok",
13
+ tokenName: "provider/test",
14
+ usage: {
15
+ totalTokens: 10,
16
+ inputTokens: 5,
17
+ cachedInputTokens: 0,
18
+ outputTokens: 5,
19
+ reasoningOutputTokens: 0,
20
+ },
21
+ durationMs: 12,
22
+ model: "model",
23
+ threadCoord: { workerId: 1, threadId: "sess-1", epoch: 0 },
24
+ ...overrides,
25
+ };
26
+ }
27
+
9
28
  describe("QgridDispatcherClass", () => {
29
+ it("AnthropicDispatcher 미초기화 시 query 는 폴백 없이 실패한다", async () => {
30
+ const dispatcher = new QgridDispatcherClass();
31
+
32
+ await expect(
33
+ dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" }),
34
+ ).rejects.toThrow(/Anthropic dispatcher not initialized/);
35
+ });
36
+
37
+ it("AnthropicDispatcher 미초기화 시 queryStream 은 delta 없는 query 폴백 없이 실패한다", async () => {
38
+ const dispatcher = new QgridDispatcherClass();
39
+
40
+ await expect(
41
+ dispatcher.queryStream(
42
+ { prompt: "hi", model: "anthropic/claude-sonnet-4-6" },
43
+ {
44
+ onDelta: vi.fn(),
45
+ onComplete: vi.fn(),
46
+ onError: vi.fn(),
47
+ },
48
+ ),
49
+ ).rejects.toThrow(/Anthropic dispatcher not initialized/);
50
+ });
51
+
52
+ it("provider prefix 없는 모델은 AnthropicDispatcher 로 암묵 라우팅하지 않는다", async () => {
53
+ const dispatcher = new QgridDispatcherClass();
54
+ const generate = vi.fn();
55
+ dispatcher.anthropicDispatcher = { generate } as never;
56
+
57
+ await expect(dispatcher.query({ prompt: "hi", model: "claude-sonnet-4-6" })).rejects.toThrow(
58
+ /Direct LLM API fallback not implemented/,
59
+ );
60
+ expect(generate).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("Anthropic queryStream 은 abortSignal 을 provider request 로 전달한다", async () => {
64
+ const dispatcher = new QgridDispatcherClass();
65
+ const generateStream = vi.fn(async (_req: GenerateRequest, _cb: GenerateStreamCallbacks) => {});
66
+ dispatcher.anthropicDispatcher = { generateStream } as never;
67
+ const abortSignal = new AbortController().signal;
68
+
69
+ await dispatcher.queryStream(
70
+ { prompt: "hi", model: "anthropic/claude-sonnet-4-6" },
71
+ {
72
+ onDelta: vi.fn(),
73
+ onComplete: vi.fn(),
74
+ onError: vi.fn(),
75
+ },
76
+ abortSignal,
77
+ );
78
+
79
+ expect(generateStream.mock.calls[0]![0].abortSignal).toBe(abortSignal);
80
+ });
81
+
82
+ it("Anthropic queryStream provider error 는 상위 onError 로 전달한다", async () => {
83
+ const dispatcher = new QgridDispatcherClass();
84
+ const serverError = new Error(
85
+ "claude error (anthropic/yds): API Error: 529 Overloaded. This is a server-side issue",
86
+ );
87
+ const generateStream = vi.fn(async (_req, cb) => {
88
+ cb.onError(serverError);
89
+ });
90
+ dispatcher.anthropicDispatcher = { generateStream } as never;
91
+
92
+ const onDelta = vi.fn();
93
+ const onComplete = vi.fn();
94
+ const onError = vi.fn();
95
+ await dispatcher.queryStream(
96
+ { prompt: "hi", model: "anthropic/claude-sonnet-4-6" },
97
+ { onDelta, onComplete, onError },
98
+ );
99
+
100
+ expect(onDelta).not.toHaveBeenCalled();
101
+ expect(onComplete).not.toHaveBeenCalled();
102
+ expect(onError).toHaveBeenCalledWith(serverError);
103
+ });
104
+
105
+ it("OpenAI query 는 reuse/reuseInput 을 provider 로 계속 전달한다", async () => {
106
+ const dispatcher = new QgridDispatcherClass();
107
+ const generate = vi.fn(async (_req: GenerateRequest) => providerResult({ model: "gpt-5.5" }));
108
+ dispatcher.openaiDispatcher = { generate } as never;
109
+
110
+ await dispatcher.query({
111
+ prompt: "next",
112
+ model: "openai/gpt-5.5",
113
+ system: "same-system",
114
+ runContext: {
115
+ threadCoord: {
116
+ workerId: 1,
117
+ threadId: "thread-1",
118
+ epoch: 0,
119
+ systemHash: "800ddd9ba811b821",
120
+ },
121
+ },
122
+ });
123
+
124
+ const req = generate.mock.calls[0]![0];
125
+ expect(req.reuse).toEqual({ workerId: 1, threadId: "thread-1", epoch: 0 });
126
+ expect(req.reuseInput).toEqual([{ type: "text", text: "next", text_elements: [] }]);
127
+ });
128
+
129
+ it("Anthropic query 는 reuse/reuseInput 을 provider 로 전달하지 않는다", async () => {
130
+ const dispatcher = new QgridDispatcherClass();
131
+ const generate = vi.fn(async (_req: GenerateRequest) =>
132
+ providerResult({ model: "claude-sonnet-4-6" }),
133
+ );
134
+ dispatcher.anthropicDispatcher = { generate } as never;
135
+
136
+ await dispatcher.query({
137
+ prompt: "next",
138
+ model: "anthropic/claude-sonnet-4-6",
139
+ system: "same-system",
140
+ runContext: {
141
+ threadCoord: {
142
+ workerId: 1,
143
+ threadId: "thread-1",
144
+ epoch: 0,
145
+ systemHash: "800ddd9ba811b821",
146
+ },
147
+ },
148
+ });
149
+
150
+ const req = generate.mock.calls[0]![0];
151
+ expect(req).not.toHaveProperty("reuse");
152
+ expect(req).not.toHaveProperty("reuseInput");
153
+ expect(req.coldInput).toEqual([{ type: "text", text: "next", text_elements: [] }]);
154
+ });
155
+
156
+ it("Anthropic queryStream 도 reuse/reuseInput 을 provider 로 전달하지 않고 delta/complete 를 보존한다", async () => {
157
+ const dispatcher = new QgridDispatcherClass();
158
+ const generateStream = vi.fn(async (_req, cb) => {
159
+ cb.onDelta("d");
160
+ cb.onComplete(providerResult({ model: "claude-sonnet-4-6" }));
161
+ });
162
+ dispatcher.anthropicDispatcher = { generateStream } as never;
163
+
164
+ const onDelta = vi.fn();
165
+ const onComplete = vi.fn();
166
+ await dispatcher.queryStream(
167
+ {
168
+ prompt: "next",
169
+ model: "anthropic/claude-sonnet-4-6",
170
+ system: "same-system",
171
+ runContext: {
172
+ threadCoord: {
173
+ workerId: 1,
174
+ threadId: "thread-1",
175
+ epoch: 0,
176
+ systemHash: "800ddd9ba811b821",
177
+ },
178
+ },
179
+ },
180
+ { onDelta, onComplete, onError: vi.fn() },
181
+ );
182
+
183
+ const req = generateStream.mock.calls[0]![0];
184
+ expect(req).not.toHaveProperty("reuse");
185
+ expect(req).not.toHaveProperty("reuseInput");
186
+ expect(req.coldInput).toEqual([{ type: "text", text: "next", text_elements: [] }]);
187
+ expect(onDelta).toHaveBeenCalledWith("d");
188
+ expect(onComplete).toHaveBeenCalledWith(
189
+ expect.objectContaining({
190
+ text: "ok",
191
+ runContext: expect.objectContaining({
192
+ threadCoord: expect.objectContaining({ threadId: "sess-1" }),
193
+ }),
194
+ }),
195
+ );
196
+ });
197
+
10
198
  it("jsonSchema 를 required + additionalProperties:false 로 strictify 한다", () => {
11
199
  const schema = buildStrictOutputSchema({
12
200
  jsonSchema: JSON.stringify({
@@ -33,16 +221,17 @@ describe("QgridDispatcherClass", () => {
33
221
 
34
222
  expect(schema.required).toEqual(["contents"]);
35
223
  expect(schema.additionalProperties).toBe(false);
36
- const contentsArray = (
37
- schema.properties?.contents as {
38
- anyOf?: Array<{ items?: { required?: string[]; additionalProperties?: boolean } }>;
39
- }
40
- ).anyOf?.[0];
224
+ const contents = schema.properties?.contents as
225
+ | { anyOf?: Array<{ items?: { required?: string[]; additionalProperties?: boolean } }> }
226
+ | undefined;
227
+ const contentsArray = contents?.anyOf?.[0];
41
228
  expect(contentsArray?.items?.required).toEqual(["text"]);
42
229
  expect(contentsArray?.items?.additionalProperties).toBe(false);
43
230
  });
44
231
 
45
- it("Anthropic provider 경로에 strictified outputSchema전달한다", async () => {
232
+ // SON-495: anthropic route strict(required 유지)쓴다 required 살려야 모델이 필드를
233
+ // 빠짐없이 채운다(실측 확정). optionalize 는 제거됨.
234
+ it("Anthropic route 에 strict(required 유지) outputSchema 를 전달한다", async () => {
46
235
  const dispatcher = new QgridDispatcherClass();
47
236
  const generate = vi.fn(
48
237
  async (_req: GenerateRequest): Promise<GenerateResult> => ({
@@ -72,6 +261,45 @@ describe("QgridDispatcherClass", () => {
72
261
  }),
73
262
  });
74
263
 
264
+ const outputSchema = generate.mock.calls[0]![0].outputSchema as {
265
+ required?: string[];
266
+ additionalProperties?: boolean;
267
+ };
268
+ // anthropic 도 OpenAI 와 동일하게 strictify — required 살아있음
269
+ expect(outputSchema.required).toEqual(["contents"]);
270
+ expect(outputSchema.additionalProperties).toBe(false);
271
+ });
272
+
273
+ it("OpenAI route 도 strictify(required 유지)한다", async () => {
274
+ const dispatcher = new QgridDispatcherClass();
275
+ const generate = vi.fn(
276
+ async (_req: GenerateRequest): Promise<GenerateResult> => ({
277
+ text: '{"contents":[{"text":"ok"}]}',
278
+ tokenName: "openai/test",
279
+ usage: {
280
+ totalTokens: 10,
281
+ inputTokens: 5,
282
+ cachedInputTokens: 0,
283
+ outputTokens: 5,
284
+ reasoningOutputTokens: 0,
285
+ },
286
+ durationMs: 12,
287
+ costUsd: 0.01,
288
+ model: "gpt-5.5",
289
+ threadCoord: { workerId: 1, threadId: "sess-1", epoch: 0 },
290
+ }),
291
+ );
292
+ dispatcher.openaiDispatcher = { generate } as never;
293
+
294
+ await dispatcher.query({
295
+ prompt: "hi",
296
+ model: "openai/gpt-5.5",
297
+ jsonSchema: JSON.stringify({
298
+ type: "object",
299
+ properties: { contents: { type: "array", items: { type: "object" } } },
300
+ }),
301
+ });
302
+
75
303
  const outputSchema = generate.mock.calls[0]![0].outputSchema as {
76
304
  required?: string[];
77
305
  additionalProperties?: boolean;
@@ -102,7 +330,7 @@ describe("QgridDispatcherClass", () => {
102
330
  );
103
331
  dispatcher.anthropicDispatcher = { generate } as never;
104
332
 
105
- const result = await dispatcher.query({ prompt: "hi", model: "claude-sonnet-4-6" });
333
+ const result = await dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" });
106
334
 
107
335
  expect(result.costUsd).toBe(0.123456);
108
336
  expect(result.usage.cache_creation_input_tokens).toBe(600);