@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.
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +94 -278
- 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 +42 -98
- package/bundle/dist/utils/providers/anthropic/claude-session.js +54 -32
- package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +7 -37
- 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 +235 -7
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +152 -364
- 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 +61 -188
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +89 -62
- package/bundle/src/utils/providers/anthropic/claude-session.ts +71 -46
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.test.ts +74 -30
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +40 -102
- 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
|
@@ -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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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);
|