@cartanova/qgrid-ai-sdk 0.1.0 → 2.0.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/package.json +20 -13
- package/e2e/e2e-logger.ts +0 -112
- package/e2e/e2e.ts +0 -217
- package/src/index.test.ts +0 -338
- package/src/index.ts +0 -396
- package/src/index.types.ts +0 -131
- package/src/logger.test.ts +0 -563
- package/src/logger.ts +0 -364
- package/src/utils.ts +0 -305
- package/tsconfig.json +0 -15
- package/tsdown.config.ts +0 -9
package/src/index.test.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { qgrid } from "./index";
|
|
4
|
-
|
|
5
|
-
const usage = {
|
|
6
|
-
input_tokens: 10,
|
|
7
|
-
output_tokens: 5,
|
|
8
|
-
cache_creation_input_tokens: 0,
|
|
9
|
-
cache_read_input_tokens: 3,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
vi.unstubAllGlobals();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
function tool() {
|
|
17
|
-
return {
|
|
18
|
-
type: "function",
|
|
19
|
-
name: "getWeather",
|
|
20
|
-
description: "Get weather",
|
|
21
|
-
inputSchema: {
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: { city: { type: "string" } },
|
|
24
|
-
required: ["city"],
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function toolPrompt(callIds: string[]) {
|
|
30
|
-
return [
|
|
31
|
-
{ role: "user", content: [{ type: "text", text: "weather" }] },
|
|
32
|
-
...callIds.flatMap((callId) => [
|
|
33
|
-
{
|
|
34
|
-
role: "assistant",
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
37
|
-
type: "tool-call",
|
|
38
|
-
toolCallId: callId,
|
|
39
|
-
toolName: "getWeather",
|
|
40
|
-
input: { city: callId },
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
role: "tool",
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: "tool-result",
|
|
49
|
-
toolCallId: callId,
|
|
50
|
-
output: { temperature: callId },
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
]),
|
|
55
|
-
];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sseDone(data: unknown) {
|
|
59
|
-
const encoder = new TextEncoder();
|
|
60
|
-
return new ReadableStream<Uint8Array>({
|
|
61
|
-
start(controller) {
|
|
62
|
-
controller.enqueue(encoder.encode(`event: done\ndata: ${JSON.stringify(data)}\n\n`));
|
|
63
|
-
controller.close();
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
describe("qgrid AI SDK provider", () => {
|
|
69
|
-
it("sends tools and maps tool-call response", async () => {
|
|
70
|
-
let queryBody: unknown;
|
|
71
|
-
vi.stubGlobal(
|
|
72
|
-
"fetch",
|
|
73
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
74
|
-
const body = JSON.parse(String(init?.body));
|
|
75
|
-
if (url.includes("/query")) {
|
|
76
|
-
queryBody = body;
|
|
77
|
-
return new Response(
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
text: "",
|
|
80
|
-
content: [
|
|
81
|
-
{
|
|
82
|
-
type: "tool-call",
|
|
83
|
-
toolCallId: "call_weather",
|
|
84
|
-
toolName: "getWeather",
|
|
85
|
-
input: JSON.stringify({ city: "Seoul" }),
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
finishReason: "tool-calls",
|
|
89
|
-
model: "gpt-5.5",
|
|
90
|
-
usage,
|
|
91
|
-
durationMs: 100,
|
|
92
|
-
costUsd: 0.01,
|
|
93
|
-
runContext: { requestLogId: 1 },
|
|
94
|
-
}),
|
|
95
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return new Response("{}", { status: 200 });
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const result = await qgrid("openai/gpt-5.5").doGenerate({
|
|
103
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "weather" }] }],
|
|
104
|
-
tools: [tool()],
|
|
105
|
-
} as never);
|
|
106
|
-
|
|
107
|
-
expect(queryBody).toMatchObject({
|
|
108
|
-
args: {
|
|
109
|
-
prompt: "weather",
|
|
110
|
-
logMode: "run",
|
|
111
|
-
tools: [{ name: "getWeather", description: "Get weather" }],
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
expect(result.finishReason).toEqual({ unified: "tool-calls", raw: "tool_call" });
|
|
115
|
-
expect(result.content).toEqual([
|
|
116
|
-
{
|
|
117
|
-
type: "tool-call",
|
|
118
|
-
toolCallId: "call_weather",
|
|
119
|
-
toolName: "getWeather",
|
|
120
|
-
input: JSON.stringify({ city: "Seoul" }),
|
|
121
|
-
},
|
|
122
|
-
]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("sends runContext and toolResults on tool-call follow-up", async () => {
|
|
126
|
-
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
|
127
|
-
|
|
128
|
-
vi.stubGlobal(
|
|
129
|
-
"fetch",
|
|
130
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
131
|
-
const body = JSON.parse(String(init?.body));
|
|
132
|
-
calls.push({ url, body });
|
|
133
|
-
|
|
134
|
-
if (url.includes("/query")) {
|
|
135
|
-
const prompt = body.args.prompt as string;
|
|
136
|
-
if (prompt === "weather") {
|
|
137
|
-
return new Response(
|
|
138
|
-
JSON.stringify({
|
|
139
|
-
text: "",
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: "tool-call",
|
|
143
|
-
toolCallId: "call_1",
|
|
144
|
-
toolName: "getWeather",
|
|
145
|
-
input: '{"city":"Seoul"}',
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
finishReason: "tool-calls",
|
|
149
|
-
model: "gpt-5.5",
|
|
150
|
-
usage,
|
|
151
|
-
durationMs: 100,
|
|
152
|
-
costUsd: 0.01,
|
|
153
|
-
runContext: { requestLogId: 42 },
|
|
154
|
-
}),
|
|
155
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
// follow-up (stop)
|
|
159
|
-
return new Response(
|
|
160
|
-
JSON.stringify({
|
|
161
|
-
text: "Seoul is 22°C",
|
|
162
|
-
content: [{ type: "text", text: "Seoul is 22°C" }],
|
|
163
|
-
finishReason: "stop",
|
|
164
|
-
model: "gpt-5.5",
|
|
165
|
-
usage,
|
|
166
|
-
durationMs: 80,
|
|
167
|
-
costUsd: 0.005,
|
|
168
|
-
}),
|
|
169
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
return new Response("{}", { status: 200 });
|
|
173
|
-
}),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const model = qgrid("openai/gpt-5.5");
|
|
177
|
-
|
|
178
|
-
// 턴 1: tool-calls
|
|
179
|
-
await model.doGenerate({
|
|
180
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "weather" }] }],
|
|
181
|
-
tools: [tool()],
|
|
182
|
-
} as never);
|
|
183
|
-
|
|
184
|
-
// 턴 2: follow-up with tool result
|
|
185
|
-
const result = await model.doGenerate({
|
|
186
|
-
prompt: toolPrompt(["call_1"]),
|
|
187
|
-
tools: [tool()],
|
|
188
|
-
} as never);
|
|
189
|
-
|
|
190
|
-
// follow-up 호출에 runContext + toolResults가 포함되어야 함
|
|
191
|
-
const followUpQuery = calls.filter((c) => c.url.includes("/query"))[1];
|
|
192
|
-
expect(followUpQuery?.body.args).toMatchObject({
|
|
193
|
-
logMode: "run",
|
|
194
|
-
runContext: { requestLogId: 42 },
|
|
195
|
-
toolResults: [{ toolCallId: "call_1" }],
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// SDK는 직접 createRun/appendStep/finishRun 호출 안 함
|
|
199
|
-
expect(calls.filter((c) => c.url.includes("/createRun"))).toHaveLength(0);
|
|
200
|
-
expect(calls.filter((c) => c.url.includes("/appendStep"))).toHaveLength(0);
|
|
201
|
-
expect(calls.filter((c) => c.url.includes("/finishRun"))).toHaveLength(0);
|
|
202
|
-
|
|
203
|
-
expect(result.content).toEqual([{ type: "text", text: "Seoul is 22°C" }]);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("does not send logMode for non-tool doGenerate", async () => {
|
|
207
|
-
let queryBody: unknown;
|
|
208
|
-
vi.stubGlobal(
|
|
209
|
-
"fetch",
|
|
210
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
211
|
-
const body = JSON.parse(String(init?.body));
|
|
212
|
-
if (url.includes("/query")) {
|
|
213
|
-
queryBody = body;
|
|
214
|
-
return new Response(
|
|
215
|
-
JSON.stringify({
|
|
216
|
-
text: "hello",
|
|
217
|
-
content: [{ type: "text", text: "hello" }],
|
|
218
|
-
finishReason: "stop",
|
|
219
|
-
model: "gpt-5.5",
|
|
220
|
-
usage,
|
|
221
|
-
durationMs: 50,
|
|
222
|
-
costUsd: 0.001,
|
|
223
|
-
}),
|
|
224
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
return new Response("{}", { status: 200 });
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
await qgrid("openai/gpt-5.5").doGenerate({
|
|
232
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
233
|
-
} as never);
|
|
234
|
-
|
|
235
|
-
// logMode가 없어야 함 (서버 auto 경로)
|
|
236
|
-
expect((queryBody as Record<string, unknown>).args).not.toHaveProperty("logMode");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("sends logMode:'run' for all doStream calls", async () => {
|
|
240
|
-
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
|
241
|
-
|
|
242
|
-
vi.stubGlobal(
|
|
243
|
-
"fetch",
|
|
244
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
245
|
-
const body = init?.body ? JSON.parse(String(init.body)) : {};
|
|
246
|
-
calls.push({ url, body });
|
|
247
|
-
|
|
248
|
-
if (url.includes("/prepareStream")) {
|
|
249
|
-
return new Response(JSON.stringify({ streamId: "s1" }), {
|
|
250
|
-
status: 200,
|
|
251
|
-
headers: { "content-type": "application/json" },
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
if (url.includes("/queryStream")) {
|
|
255
|
-
return new Response(
|
|
256
|
-
sseDone({
|
|
257
|
-
text: "streamed",
|
|
258
|
-
content: [{ type: "text", text: "streamed" }],
|
|
259
|
-
finishReason: "stop",
|
|
260
|
-
model: "gpt-5.5",
|
|
261
|
-
usage,
|
|
262
|
-
durationMs: 100,
|
|
263
|
-
costUsd: 0.01,
|
|
264
|
-
}),
|
|
265
|
-
{ status: 200 },
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
return new Response("{}", { status: 200 });
|
|
269
|
-
}),
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const result = await qgrid("openai/gpt-5.5").doStream({
|
|
273
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }],
|
|
274
|
-
} as never);
|
|
275
|
-
for await (const _part of result.stream) {
|
|
276
|
-
// drain
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const prepareCall = calls.find((c) => c.url.includes("/prepareStream"));
|
|
280
|
-
expect(prepareCall?.body.args).toMatchObject({ logMode: "run" });
|
|
281
|
-
|
|
282
|
-
// SDK는 직접 lifecycle 호출 안 함
|
|
283
|
-
expect(calls.filter((c) => c.url.includes("/createRun"))).toHaveLength(0);
|
|
284
|
-
expect(calls.filter((c) => c.url.includes("/finishRun"))).toHaveLength(0);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("clears client run state when prompt does not match pending tool calls", async () => {
|
|
288
|
-
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
|
289
|
-
|
|
290
|
-
vi.stubGlobal(
|
|
291
|
-
"fetch",
|
|
292
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
293
|
-
const body = JSON.parse(String(init?.body));
|
|
294
|
-
calls.push({ url, body });
|
|
295
|
-
|
|
296
|
-
if (url.includes("/query")) {
|
|
297
|
-
return new Response(
|
|
298
|
-
JSON.stringify({
|
|
299
|
-
text: "",
|
|
300
|
-
content: [
|
|
301
|
-
{
|
|
302
|
-
type: "tool-call",
|
|
303
|
-
toolCallId: "call_1",
|
|
304
|
-
toolName: "getWeather",
|
|
305
|
-
input: '{"city":"Seoul"}',
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
finishReason: "tool-calls",
|
|
309
|
-
model: "gpt-5.5",
|
|
310
|
-
usage,
|
|
311
|
-
durationMs: 100,
|
|
312
|
-
costUsd: 0.01,
|
|
313
|
-
runContext: { requestLogId: 1 },
|
|
314
|
-
}),
|
|
315
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
return new Response("{}", { status: 200 });
|
|
319
|
-
}),
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
const model = qgrid("openai/gpt-5.5");
|
|
323
|
-
await model.doGenerate({
|
|
324
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "weather" }] }],
|
|
325
|
-
tools: [tool()],
|
|
326
|
-
} as never);
|
|
327
|
-
|
|
328
|
-
// 다른 prompt로 호출 (tool result 없음) → overlap, runContext 안 보냄
|
|
329
|
-
await model.doGenerate({
|
|
330
|
-
prompt: [{ role: "user", content: [{ type: "text", text: "different" }] }],
|
|
331
|
-
tools: [tool()],
|
|
332
|
-
} as never);
|
|
333
|
-
|
|
334
|
-
const secondQuery = calls.filter((c) => c.url.includes("/query"))[1];
|
|
335
|
-
expect(secondQuery?.body.args).not.toHaveProperty("runContext");
|
|
336
|
-
expect(secondQuery?.body.args).toMatchObject({ logMode: "run" });
|
|
337
|
-
});
|
|
338
|
-
});
|